Skip to contentSkip to conversation

bpftool: Batch hash map dumps with bounded buffers

PATCH Superseded Tianyi Chen 2 participants 4 messages Nov 3 – Nov 4

Subsystems (4)

v7 of 10 Superseded Latest is v10 →

lore
Share & tools
lore
#0 lore raw

v7 cover bpftool: Batch hash map dumps with bounded buffers

Rebased on current bpf-next, retaining Quentin's Acked-by.

Changes in v7:
- Use the upstream error formatter for batch failures.
- Refresh the matched-base timings and syscall counts in patch 1.

The 12 batch tests and both upstream non-iterable-map tests pass on x86-64 KVM with Linux 7.2.13 and 7.3.0. All 33 focused subtests also pass with the map-info/recursive series; per-CPU fallback output is unchanged.

v6: https://lore.kernel.org/r/[email protected]

Tianyi Chen (2):
bpftool: Use batch lookups for hash map dumps selftests/bpf: Check bpftool batch map dump contents

 tools/bpf/bpftool/map.c                       | 123 ++++++++++-
 .../bpf/prog_tests/bpftool_map_batch.c        | 191 ++++++++++++++++++
 2 files changed, 306 insertions(+), 8 deletions(-)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/bpftool_map_batch.c

base-commit: cd5bc55288d4fd999595c8f2af6a5408cbe92a52

Signature · 1 line
2.56.0
#1 lore raw

PATCH v7 1/2 bpftool: Use batch lookups for hash map dumps

Use BPF_MAP_LOOKUP_BATCH for ordinary hash maps to reduce BPF syscalls while preserving plain, JSON and BTF output.

Bound the combined key/value buffers to 4 MiB. Start with up to 256 entries and grow on ENOSPC using the same input cursor. At the buffer limit, continue individual lookups from the last emitted key without restarting output. Also fall back if the initial batch is unsupported.

Process the final partial batch on ENOENT, but do not read output buffers on other errors. Keep fatal diagnostics on stderr so JSON arrays contain only map entries. Traversal remains non-atomic.

For a 100,000-entry map with 4-byte keys and values, median wall times with stdout redirected to /dev/null were:

               baseline       batch
  plain        87.460 ms      55.677 ms
  JSON         78.495 ms      47.157 ms

Both builds used GCC 16.2.1 in an x86-64 KVM guest running 7.3.0-4.cachyos1.fc44.x86_64, pinned to one guest CPU. After two warm-ups, 15 untraced samples per binary and mode alternated baseline and patched runs. Separate strace runs counted 200,005 versus 396 BPF syscalls for a plain dump. These results are specific to this fixture and guest.

---
 tools/bpf/bpftool/map.c | 123 +++++++++++++++++++++++++++++++++++++---
 1 file changed, 115 insertions(+), 8 deletions(-)
tools/bpf/bpftool/map.c +115 -8
index 4b65edf74859b..fdd32cd40709b 100644
@@ -741,15 +741,10 @@ static int do_show(int argc, char **argv)
 	return errno == ENOENT ? 0 : -1;
 }
 
-static int dump_map_elem(int fd, void *key, void *value,
-			 struct bpf_map_info *map_info, struct btf *btf,
-			 json_writer_t *btf_wtr, const int *cpu_ids, int cpu_cnt)
+static void print_map_elem(void *key, void *value,
+			   struct bpf_map_info *map_info, struct btf *btf,
+			   json_writer_t *btf_wtr, const int *cpu_ids, int cpu_cnt)
 {
-	if (bpf_map_lookup_elem(fd, key, value)) {
-		print_entry_error(map_info, key, errno);
-		return -1;
-	}
-
 	if (json_output) {
 		print_entry_json(map_info, key, value, btf, cpu_ids, cpu_cnt);
 	} else if (btf) {
@@ -763,10 +758,113 @@ static int dump_map_elem(int fd, void *key, void *value,
 	} else {
 		print_entry_plain(map_info, key, value, cpu_ids, cpu_cnt);
 	}
+}
+
+static int dump_map_elem(int fd, void *key, void *value,
+			 struct bpf_map_info *map_info, struct btf *btf,
+			 json_writer_t *btf_wtr, const int *cpu_ids, int cpu_cnt)
+{
+	if (bpf_map_lookup_elem(fd, key, value)) {
+		print_entry_error(map_info, key, errno);
+		return -1;
+	}
 
+	print_map_elem(key, value, map_info, btf, btf_wtr, cpu_ids, cpu_cnt);
 	return 0;
 }
 
+#define MAP_DUMP_BATCH_FALLBACK 1
+#define MAP_DUMP_BATCH_SIZE 256U
+#define MAP_DUMP_BATCH_MAX_BYTES (4 * 1024 * 1024)
+
+static bool map_dump_can_batch(const struct bpf_map_info *info)
+{
+	return info->type == BPF_MAP_TYPE_HASH && info->max_entries &&
+	       (__u64)info->key_size + info->value_size <= MAP_DUMP_BATCH_MAX_BYTES;
+}
+
+/* Return MAP_DUMP_BATCH_FALLBACK to continue with individual lookups. */
+static int dump_map_batch(int fd, void *key, void *value,
+			  struct bpf_map_info *info, struct btf *btf,
+			  json_writer_t *wtr, unsigned int *num_elems)
+{
+	/* HASH batch cursors are u32 bucket indices, independent of key size. */
+	__u32 batch = 0, next_batch = 0;
+	__u32 capacity, max_capacity, count, i;
+	void *keys = NULL, *values = NULL, *buf;
+	bool first = true, can_restart = true;
+	int err;
+
+	max_capacity = MAP_DUMP_BATCH_MAX_BYTES / (info->key_size + info->value_size);
+	max_capacity = min(max_capacity, info->max_entries);
+	capacity = min(max_capacity, MAP_DUMP_BATCH_SIZE);
+resize:
+	buf = realloc(keys, (size_t)capacity * info->key_size);
+	if (!buf) {
+		err = ENOMEM;
+		goto error;
+	}
+	keys = buf;
+	buf = realloc(values, (size_t)capacity * info->value_size);
+	if (!buf) {
+		err = ENOMEM;
+		goto error;
+	}
+	values = buf;
+
+	while (true) {
+		count = capacity;
+		err = bpf_map_lookup_batch(fd, first ? NULL : &batch,
+					   &next_batch, keys, values, &count, NULL);
+		err = err ? errno : 0;
+		/*
+		 * Older kernels reject the command before updating count. Do not
+		 * inspect the buffers on these errors, or restart after progress.
+		 */
+		if (can_restart && (err == EINVAL || err == EOPNOTSUPP ||
+				    err == 524 /* ENOTSUPP */)) {
+			err = MAP_DUMP_BATCH_FALLBACK;
+			goto out;
+		}
+		can_restart = false;
+		if (err == ENOSPC) {
+			if (capacity == max_capacity) {
+				err = MAP_DUMP_BATCH_FALLBACK;
+				goto out;
+			}
+			capacity += min(capacity, max_capacity - capacity);
+			/* Preserve the input cursor: the oversized bucket was not read. */
+			goto resize;
+		}
+		/* In particular, EFAULT can leave count and the buffers invalid. */
+		if (err && err != ENOENT)
+			goto error;
+		for (i = 0; i < count; i++) {
+			/*
+			 * Keep the alignment provided by individual lookups, including
+			 * for BTF types whose map key/value size is not aligned.
+			 */
+			memcpy(key, keys + (size_t)i * info->key_size, info->key_size);
+			memcpy(value, values + (size_t)i * info->value_size, info->value_size);
+			print_map_elem(key, value, info, btf, wtr, NULL, 0);
+			(*num_elems)++;
+		}
+		if (err == ENOENT) {
+			err = 0;
+			goto out;
+		}
+		first = false;
+		batch = next_batch;
+	}
+error:
+	fprintf(stderr, "Error: can't lookup map batch: %s\n", bpf_strerror(err));
+	err = -1;
+out:
+	free(keys);
+	free(values);
+	return err;
+}
+
 static int maps_have_btf(int *fds, int nb_fds)
 {
 	struct bpf_map_info info = {};
@@ -895,6 +993,14 @@ map_dump(int fd, struct bpf_map_info *info, json_writer_t *wtr,
 		p_info("Warning: cannot read values from %s map with value_size != 8",
 		       map_type_str);
 	}
+	if (map_dump_can_batch(info)) {
+		err = dump_map_batch(fd, key, value, info, btf, wtr, &num_elems);
+		if (err != MAP_DUMP_BATCH_FALLBACK)
+			goto end_dump;
+		/* Continue after the last emitted key rather than restarting. */
+		if (num_elems)
+			prev_key = key;
+	}
 	while (true) {
 		err = bpf_map_get_next_key(fd, prev_key, key);
 		if (err) {
@@ -911,6 +1017,7 @@ map_dump(int fd, struct bpf_map_info *info, json_writer_t *wtr,
 		prev_key = key;
 	}
 
+end_dump:
 	if (wtr) {
 		jsonw_end_array(wtr);	/* elements */
 		if (show_header)
Signature · 1 line
2.56.0
#2 lore raw

Re: [PATCH bpf-next v7 1/2] bpftool: Use batch lookups for hash map dumps

Quoted 4 lines · Tianyi Chen

Bound the combined key/value buffers to 4 MiB. Start with up to 256 entries and grow on ENOSPC using the same input cursor. At the buffer limit, continue individual lookups from the last emitted key without restarting output. Also fall back if the initial batch is unsupported.

This is overkill for 32 ms per 100k elements.
v4 measured 0.774 s vs 0.734 s without the redirect to /dev/null.

n_buckets >= max_entries in htab. A bucket with more than 256 elements is not a real case. The growth is dead code. No subtest in patch 2 reaches it.

bpf_map_get_next_key() walks the map in the same order as the batch. So continue from the last printed key after any error, not only after ENOSPC. Then no need for the growth, can_restart, the list of errnos, three return values, fprintf(stderr) and the end_dump label.

Review

Quoted 3 lines
(quoted diff) +3 -0
+static bool map_dump_can_batch(const struct bpf_map_info *info)
+{
+	return info->type == BPF_MAP_TYPE_HASH && info->max_entries &&

No need for the type check either. The kernel has three batch lookups:

  kernel function                        maps                      cursor
  -------------------------------------  ------------------------  ------
  __htab_map_lookup_and_delete_batch()   hash, lru_hash,           u32

percpu_hash, lru_percpu_hash, hash_of_maps
generic_map_lookup_batch() array, percpu_array, key
array_of_maps, lpm_trie
__rhtab_map_lookup_and_delete_batch() rhash key

All three walk the map in the order of bpf_map_get_next_key(). A map without map_lookup_batch returns ENOTSUPP and takes the old path.

Review

Quoted 2 lines
(quoted diff) +2 -0
+		if (err && err != ENOENT)
+			goto error;

The batch takes the bucket lock. htab_lock_bucket() can fail, and the batch returns that error. bpf_map_lookup_elem() doesn't take the lock. So a dump that works today fails here.

Something like this (not even compile tested):

tools/bpf/bpftool/map.c +79 -14
index 4b65edf..a522d80 100644
@@ -59,13 +59,18 @@ static int map_type_from_str(const char *type)
 	return -1;
 }
 
-static void *alloc_value(struct bpf_map_info *info)
+static size_t map_value_size(struct bpf_map_info *info)
 {
 	if (map_is_per_cpu(info->type))
-		return malloc(round_up(info->value_size, 8) *
-			      get_possible_cpus());
+		return (size_t)round_up(info->value_size, 8) *
+		       get_possible_cpus();
 	else
-		return malloc(info->value_size);
+		return info->value_size;
+}
+
+static void *alloc_value(struct bpf_map_info *info)
+{
+	return malloc(map_value_size(info));
 }
 
 static int do_dump_btf(const struct btf_dumper *d,
@@ -741,15 +746,10 @@ static int do_show(int argc, char **argv)
 	return errno == ENOENT ? 0 : -1;
 }
 
-static int dump_map_elem(int fd, void *key, void *value,
-			 struct bpf_map_info *map_info, struct btf *btf,
-			 json_writer_t *btf_wtr, const int *cpu_ids, int cpu_cnt)
+static void print_map_elem(void *key, void *value,
+			   struct bpf_map_info *map_info, struct btf *btf,
+			   json_writer_t *btf_wtr, const int *cpu_ids, int cpu_cnt)
 {
-	if (bpf_map_lookup_elem(fd, key, value)) {
-		print_entry_error(map_info, key, errno);
-		return -1;
-	}
-
 	if (json_output) {
 		print_entry_json(map_info, key, value, btf, cpu_ids, cpu_cnt);
 	} else if (btf) {
@@ -763,10 +763,70 @@ static int dump_map_elem(int fd, void *key, void *value,
 	} else {
 		print_entry_plain(map_info, key, value, cpu_ids, cpu_cnt);
 	}
+}
 
+static int dump_map_elem(int fd, void *key, void *value,
+			 struct bpf_map_info *map_info, struct btf *btf,
+			 json_writer_t *btf_wtr, const int *cpu_ids, int cpu_cnt)
+{
+	if (bpf_map_lookup_elem(fd, key, value)) {
+		print_entry_error(map_info, key, errno);
+		return -1;
+	}
+
+	print_map_elem(key, value, map_info, btf, btf_wtr, cpu_ids, cpu_cnt);
 	return 0;
 }
 
+#define MAP_DUMP_BATCH_BYTES (1U << 20)
+
+/*
+ * Print the elements that batched lookups return. 'key' is the last of them.
+ * Return true at the end of the map. After any error the caller continues
+ * with bpf_map_get_next_key(). It walks the map in the same order.
+ */
+static bool dump_map_batch(int fd, void *key, struct bpf_map_info *info,
+			   struct btf *btf, json_writer_t *btf_wtr,
+			   const int *cpu_ids, int cpu_cnt,
+			   unsigned int *num_elems)
+{
+	void *keys, *values, *batch, *in_batch = NULL;
+	size_t value_size = map_value_size(info);
+	__u32 max_cnt, cnt, i;
+	bool done = false;
+	int err;
+
+	max_cnt = min(MAP_DUMP_BATCH_BYTES / (info->key_size + value_size),
+		      (size_t)info->max_entries);
+	/* The cursor is a u32 in htab maps and a key in all other maps */
+	batch = malloc(max(info->key_size, 4U));
+	keys = malloc(max_cnt * info->key_size);
+	values = malloc(max_cnt * value_size);
+	if (!max_cnt || !batch || !keys || !values)
+		goto out;
+
+	do {
+		cnt = max_cnt;
+		err = bpf_map_lookup_batch(fd, in_batch, batch, keys, values,
+					   &cnt, NULL);
+		if (err && err != -ENOENT)
+			goto out;
+		for (i = 0; i < cnt; i++) {
+			memcpy(key, keys + i * info->key_size, info->key_size);
+			print_map_elem(key, values + i * value_size, info, btf,
+				       btf_wtr, cpu_ids, cpu_cnt);
+		}
+		*num_elems += cnt;
+		in_batch = batch;
+	} while (!err);
+	done = true;
+out:
+	free(batch);
+	free(keys);
+	free(values);
+	return done;
+}
+
 static int maps_have_btf(int *fds, int nb_fds)
 {
 	struct bpf_map_info info = {};
@@ -836,7 +896,8 @@ map_dump(int fd, struct bpf_map_info *info, json_writer_t *wtr,
 	struct btf *btf = NULL;
 	int *cpu_ids = NULL;
 	int cpu_cnt = 0;
-	int err;
+	int err = 0;
+	bool done;
 
 	key = malloc(info->key_size);
 	value = alloc_value(info);
@@ -895,7 +956,11 @@ map_dump(int fd, struct bpf_map_info *info, json_writer_t *wtr,
 		p_info("Warning: cannot read values from %s map with value_size != 8",
 		       map_type_str);
 	}
-	while (true) {
+	done = dump_map_batch(fd, key, info, btf, wtr, cpu_ids, cpu_cnt,
+			      &num_elems);
+	if (num_elems)
+		prev_key = key;
+	while (!done) {
 		err = bpf_map_get_next_key(fd, prev_key, key);
 		if (err) {
 			if (errno == ENOENT) {

pw-bot: cr

#3 lore raw

PATCH v7 2/2 selftests/bpf: Check bpftool batch map dump contents

In reply to Tianyi Chen

Exercise hash map dumps around the initial batch size and across multiple batches, including empty and single-entry maps. Compare each complete unordered key/value set with the input data in plain, JSON and pretty JSON output.

Cover one-byte keys, three-byte values and BTF-formatted maps to catch cursor sizing, buffer alignment and formatting regressions.

Also cover a sparse hash map whose maximum capacity exceeds the batch buffer budget.

---
 .../bpf/prog_tests/bpftool_map_batch.c        | 191 ++++++++++++++++++
 1 file changed, 191 insertions(+)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/bpftool_map_batch.c
tools/testing/selftests/bpf/prog_tests/bpftool_map_batch.c new file +191 -0
new file mode 100644
index 0000000000000..5387de2ba79fd
@@ -0,0 +1,191 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <test_progs.h>
+#include <bpftool_helpers.h>
+#include <bpf/btf.h>
+#include <ctype.h>
+
+#define MAX_ENTRIES 1025
+#define RECORD_SIZE 256
+#define OUTPUT_SIZE (MAX_ENTRIES * RECORD_SIZE + 1024)
+
+struct dump_case {
+	const char *name;
+	unsigned int count;
+	unsigned int key_size;
+	unsigned int value_size;
+	bool btf;
+	unsigned int max_entries;
+};
+
+static void hex_bytes(char *out, const void *data, unsigned int size, bool json)
+{
+	const unsigned char *bytes = data;
+	unsigned int i;
+
+	if (json)
+		*out++ = '[';
+	for (i = 0; i < size; i++) {
+		if (json && i)
+			*out++ = ',';
+		out += sprintf(out, json ? "\"0x%02x\"" : "%02x", bytes[i]);
+	}
+	if (json)
+		*out++ = ']';
+	*out = '\0';
+}
+
+static void expected_record(char *record, const struct dump_case *test,
+			    unsigned int index, bool json)
+{
+	__u32 key = index, value = index * 37 + 11;
+	unsigned char short_key = index;
+	char key_hex[64], value_hex[64], formatted[96];
+
+	hex_bytes(key_hex, test->key_size == 1 ? (void *)&short_key : &key,
+		  test->key_size, json);
+	hex_bytes(value_hex, &value, test->value_size, json);
+	snprintf(formatted, sizeof(formatted), "{\"key\":%u,\"value\":%u}",
+		 key, value);
+	if (json && test->btf)
+		snprintf(record, RECORD_SIZE,
+			 "{\"key\":%s,\"value\":%s,\"formatted\":%s}",
+			 key_hex, value_hex, formatted);
+	else if (json)
+		snprintf(record, RECORD_SIZE, "{\"key\":%s,\"value\":%s}",
+			 key_hex, value_hex);
+	else if (test->btf)
+		snprintf(record, RECORD_SIZE, "%s", formatted);
+	else
+		snprintf(record, RECORD_SIZE, "key:%svalue:%s", key_hex, value_hex);
+}
+
+static void check_dump(const struct dump_case *test, __u32 id, bool json, bool pretty)
+{
+	bool array = json || test->btf;
+	char command[MAX_BPFTOOL_CMD_LEN], expected[RECORD_SIZE], footer[64];
+	bool seen[MAX_ENTRIES] = {};
+	char *output, *src, *dst, *cursor;
+	unsigned int i, n;
+	int err;
+
+	output = calloc(1, OUTPUT_SIZE);
+	if (!ASSERT_OK_PTR(output, "alloc_output"))
+		return;
+	snprintf(command, sizeof(command), "%smap dump id %u",
+		 pretty ? "-p " : json ? "-j " : "", id);
+	err = get_bpftool_command_output(command, output, OUTPUT_SIZE);
+	if (!ASSERT_OK(err, "map_dump"))
+		goto out;
+	/*
+	 * Ignore presentation whitespace, but compare complete records and all
+	 * punctuation. Expected contents come only from the input data, never
+	 * from another map walk or bpftool invocation.
+	 */
+	for (src = output, dst = output; *src; src++)
+		if (!isspace((unsigned char)*src))
+			*dst++ = *src;
+	*dst = '\0';
+	cursor = output;
+	if (array) {
+		if (!ASSERT_EQ(*cursor, '[', "array_start"))
+			goto out;
+		cursor++;
+	}
+	for (n = 0; n < test->count; n++) {
+		if (array && n) {
+			if (!ASSERT_EQ(*cursor, ',', "record_separator"))
+				goto out;
+			cursor++;
+		}
+		for (i = 0; i < test->count; i++) {
+			if (seen[i])
+				continue;
+			expected_record(expected, test, i, json);
+			if (!strncmp(cursor, expected, strlen(expected)))
+				break;
+		}
+		if (!ASSERT_LT(i, test->count, "unique_expected_record"))
+			goto out;
+		seen[i] = true;
+		cursor += strlen(expected);
+	}
+	if (array) {
+		ASSERT_STREQ(cursor, "]", "array_end_and_count");
+	} else {
+		snprintf(footer, sizeof(footer), "Found%uelement%s", test->count,
+			 test->count == 1 ? "" : "s");
+		ASSERT_STREQ(cursor, footer, "plain_count");
+	}
+out:
+	free(output);
+}
+
+static void run_dump_case(const struct dump_case *test)
+{
+	LIBBPF_OPTS(bpf_map_create_opts, opts);
+	struct bpf_map_info info = {};
+	__u32 info_len = sizeof(info);
+	struct btf *btf = NULL;
+	unsigned int i;
+	int fd = -1;
+
+	if (test->btf) {
+		btf = btf__new_empty();
+		if (!ASSERT_OK_PTR(btf, "btf_new"))
+			return;
+		if (!ASSERT_EQ(btf__add_int(btf, "unsigned int", 4, 0), 1,
+			       "btf_int") ||
+		    !ASSERT_OK(btf__load_into_kernel(btf), "btf_load"))
+			goto out;
+		opts.btf_fd = btf__fd(btf);
+		opts.btf_key_type_id = 1;
+		opts.btf_value_type_id = 1;
+	}
+	if (test->max_entries)
+		opts.map_flags = BPF_F_NO_PREALLOC;
+	fd = bpf_map_create(BPF_MAP_TYPE_HASH, "dump_batch", test->key_size,
+			    test->value_size, test->max_entries ?: (test->count ?: 1), &opts);
+	if (!ASSERT_OK_FD(fd, "map_create"))
+		goto out;
+	for (i = 0; i < test->count; i++) {
+		__u32 key = i, value = i * 37 + 11;
+		unsigned char short_key = i;
+		void *key_ptr = test->key_size == 1 ? (void *)&short_key : &key;
+
+		if (!ASSERT_OK(bpf_map_update_elem(fd, key_ptr, &value, BPF_ANY),
+			       "map_update"))
+			goto out;
+	}
+	if (!ASSERT_OK(bpf_map_get_info_by_fd(fd, &info, &info_len), "map_info"))
+		goto out;
+	check_dump(test, info.id, false, false);
+	check_dump(test, info.id, true, false);
+	check_dump(test, info.id, true, true);
+out:
+	if (fd >= 0)
+		close(fd);
+	btf__free(btf);
+}
+
+void test_bpftool_map_batch(void)
+{
+	static const struct dump_case cases[] = {
+		{ "empty", 0, 4, 4 },
+		{ "single", 1, 4, 4 },
+		{ "below_batch", 255, 4, 4 },
+		{ "exact_batch", 256, 4, 4 },
+		{ "above_batch", 257, 4, 4 },
+		{ "multiple_batches", 1025, 4, 4 },
+		{ "large_sparse", 1025, 4, 4, false, 1U << 20 },
+		{ "one_byte_key", 256, 1, 4 },
+		{ "odd_value_size", 257, 4, 3 },
+		{ "btf_empty", 0, 4, 4, true },
+		{ "btf_single", 1, 4, 4, true },
+		{ "btf_multiple_batches", 1025, 4, 4, true },
+	};
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(cases); i++)
+		if (test__start_subtest(cases[i].name))
+			run_dump_case(&cases[i]);
+}
Signature · 1 line
2.56.0