| // SPDX-License-Identifier: GPL-2.0 |
| /* Copyright (c) 2025 Cloudflare */ |
| |
| /* |
| * All of these benchmarks operate on tries with keys in the range |
| * [0, args.nr_entries), i.e. there are no gaps or partially filled |
| * branches of the trie for any key < args.nr_entries. |
| * |
| * This gives an idea of worst-case behaviour. |
| */ |
| |
| #include <argp.h> |
| #include <linux/time64.h> |
| #include <linux/if_ether.h> |
| #include "lpm_trie_bench.skel.h" |
| #include "lpm_trie_map.skel.h" |
| #include "bench.h" |
| #include "testing_helpers.h" |
| #include "progs/lpm_trie.h" |
| |
| static struct ctx { |
| struct lpm_trie_bench *bench; |
| } ctx; |
| |
| static struct { |
| __u32 nr_entries; |
| __u32 prefixlen; |
| bool random; |
| } args = { |
| .nr_entries = 0, |
| .prefixlen = 32, |
| .random = false, |
| }; |
| |
| enum { |
| ARG_NR_ENTRIES = 9000, |
| ARG_PREFIX_LEN, |
| ARG_RANDOM, |
| }; |
| |
| static const struct argp_option opts[] = { |
| { "nr_entries", ARG_NR_ENTRIES, "NR_ENTRIES", 0, |
| "Number of unique entries in the LPM trie" }, |
| { "prefix_len", ARG_PREFIX_LEN, "PREFIX_LEN", 0, |
| "Number of prefix bits to use in the LPM trie" }, |
| { "random", ARG_RANDOM, NULL, 0, "Access random keys during op" }, |
| {}, |
| }; |
| |
| static error_t lpm_parse_arg(int key, char *arg, struct argp_state *state) |
| { |
| long ret; |
| |
| switch (key) { |
| case ARG_NR_ENTRIES: |
| ret = strtol(arg, NULL, 10); |
| if (ret < 1 || ret > UINT_MAX) { |
| fprintf(stderr, "Invalid nr_entries count."); |
| argp_usage(state); |
| } |
| args.nr_entries = ret; |
| break; |
| case ARG_PREFIX_LEN: |
| ret = strtol(arg, NULL, 10); |
| if (ret < 1 || ret > UINT_MAX) { |
| fprintf(stderr, "Invalid prefix_len value."); |
| argp_usage(state); |
| } |
| args.prefixlen = ret; |
| break; |
| case ARG_RANDOM: |
| args.random = true; |
| break; |
| default: |
| return ARGP_ERR_UNKNOWN; |
| } |
| return 0; |
| } |
| |
| const struct argp bench_lpm_trie_map_argp = { |
| .options = opts, |
| .parser = lpm_parse_arg, |
| }; |
| |
| static void validate_common(void) |
| { |
| if (env.consumer_cnt != 0) { |
| fprintf(stderr, "benchmark doesn't support consumer\n"); |
| exit(1); |
| } |
| |
| if (args.nr_entries == 0) { |
| fprintf(stderr, "Missing --nr_entries parameter\n"); |
| exit(1); |
| } |
| |
| if ((1UL << args.prefixlen) < args.nr_entries) { |
| fprintf(stderr, "prefix_len value too small for nr_entries\n"); |
| exit(1); |
| } |
| } |
| |
| static void lpm_insert_validate(void) |
| { |
| validate_common(); |
| |
| if (env.producer_cnt != 1) { |
| fprintf(stderr, "lpm-trie-insert requires a single producer\n"); |
| exit(1); |
| } |
| |
| if (args.random) { |
| fprintf(stderr, "lpm-trie-insert does not support --random\n"); |
| exit(1); |
| } |
| } |
| |
| static void lpm_delete_validate(void) |
| { |
| validate_common(); |
| |
| if (env.producer_cnt != 1) { |
| fprintf(stderr, "lpm-trie-delete requires a single producer\n"); |
| exit(1); |
| } |
| |
| if (args.random) { |
| fprintf(stderr, "lpm-trie-delete does not support --random\n"); |
| exit(1); |
| } |
| } |
| |
| static void lpm_free_validate(void) |
| { |
| validate_common(); |
| |
| if (env.producer_cnt != 1) { |
| fprintf(stderr, "lpm-trie-free requires a single producer\n"); |
| exit(1); |
| } |
| |
| if (args.random) { |
| fprintf(stderr, "lpm-trie-free does not support --random\n"); |
| exit(1); |
| } |
| } |
| |
| static struct trie_key *keys; |
| static __u32 *vals; |
| |
| static void fill_map(int map_fd) |
| { |
| int err; |
| |
| DECLARE_LIBBPF_OPTS(bpf_map_batch_opts, opts, |
| .elem_flags = 0, |
| .flags = 0, |
| ); |
| |
| err = bpf_map_update_batch(map_fd, keys, vals, &args.nr_entries, &opts); |
| if (err) { |
| fprintf(stderr, "failed to batch update keys to map: %d\n", |
| -err); |
| exit(1); |
| } |
| } |
| |
| static void empty_map(int map_fd) |
| { |
| int err; |
| |
| DECLARE_LIBBPF_OPTS(bpf_map_batch_opts, opts, |
| .elem_flags = 0, |
| .flags = 0, |
| ); |
| |
| err = bpf_map_delete_batch(map_fd, keys, &args.nr_entries, &opts); |
| if (err) { |
| fprintf(stderr, "failed to batch delete keys for map: %d\n", |
| -err); |
| exit(1); |
| } |
| } |
| |
| static void attach_prog(void) |
| { |
| int i; |
| |
| ctx.bench = lpm_trie_bench__open_and_load(); |
| if (!ctx.bench) { |
| fprintf(stderr, "failed to open skeleton\n"); |
| exit(1); |
| } |
| |
| ctx.bench->bss->nr_entries = args.nr_entries; |
| ctx.bench->bss->prefixlen = args.prefixlen; |
| ctx.bench->bss->random = args.random; |
| |
| if (lpm_trie_bench__attach(ctx.bench)) { |
| fprintf(stderr, "failed to attach skeleton\n"); |
| exit(1); |
| } |
| |
| keys = calloc(args.nr_entries, sizeof(*keys)); |
| vals = calloc(args.nr_entries, sizeof(*vals)); |
| |
| for (i = 0; i < args.nr_entries; i++) { |
| struct trie_key *k = &keys[i]; |
| __u32 *v = &vals[i]; |
| |
| k->prefixlen = args.prefixlen; |
| k->data = i; |
| *v = 1; |
| } |
| } |
| |
| static void attach_prog_and_fill_map(void) |
| { |
| int fd; |
| |
| attach_prog(); |
| |
| fd = bpf_map__fd(ctx.bench->maps.trie_map); |
| fill_map(fd); |
| } |
| |
| static void lpm_noop_setup(void) |
| { |
| attach_prog(); |
| ctx.bench->bss->op = LPM_OP_NOOP; |
| } |
| |
| static void lpm_baseline_setup(void) |
| { |
| attach_prog(); |
| ctx.bench->bss->op = LPM_OP_BASELINE; |
| } |
| |
| static void lpm_lookup_setup(void) |
| { |
| attach_prog_and_fill_map(); |
| ctx.bench->bss->op = LPM_OP_LOOKUP; |
| } |
| |
| static void lpm_insert_setup(void) |
| { |
| attach_prog(); |
| ctx.bench->bss->op = LPM_OP_INSERT; |
| } |
| |
| static void lpm_update_setup(void) |
| { |
| attach_prog_and_fill_map(); |
| ctx.bench->bss->op = LPM_OP_UPDATE; |
| } |
| |
| static void lpm_delete_setup(void) |
| { |
| attach_prog_and_fill_map(); |
| ctx.bench->bss->op = LPM_OP_DELETE; |
| } |
| |
| static void lpm_free_setup(void) |
| { |
| attach_prog(); |
| ctx.bench->bss->op = LPM_OP_FREE; |
| } |
| |
| static void lpm_measure(struct bench_res *res) |
| { |
| res->hits = atomic_swap(&ctx.bench->bss->hits, 0); |
| res->duration_ns = atomic_swap(&ctx.bench->bss->duration_ns, 0); |
| } |
| |
| static void bench_reinit_map(void) |
| { |
| int fd = bpf_map__fd(ctx.bench->maps.trie_map); |
| |
| switch (ctx.bench->bss->op) { |
| case LPM_OP_INSERT: |
| /* trie_map needs to be emptied */ |
| empty_map(fd); |
| break; |
| case LPM_OP_DELETE: |
| /* trie_map needs to be refilled */ |
| fill_map(fd); |
| break; |
| default: |
| fprintf(stderr, "Unexpected REINIT return code for op %d\n", |
| ctx.bench->bss->op); |
| exit(1); |
| } |
| } |
| |
| /* For NOOP, BASELINE, LOOKUP, INSERT, UPDATE, and DELETE */ |
| static void *lpm_producer(void *unused __always_unused) |
| { |
| int err; |
| char in[ETH_HLEN]; /* unused */ |
| |
| LIBBPF_OPTS(bpf_test_run_opts, opts, .data_in = in, |
| .data_size_in = sizeof(in), .repeat = 1, ); |
| |
| while (true) { |
| int fd = bpf_program__fd(ctx.bench->progs.run_bench); |
| err = bpf_prog_test_run_opts(fd, &opts); |
| if (err) { |
| fprintf(stderr, "failed to run BPF prog: %d\n", err); |
| exit(1); |
| } |
| |
| /* Check for kernel error code */ |
| if ((int)opts.retval < 0) { |
| fprintf(stderr, "BPF prog returned error: %d\n", |
| opts.retval); |
| exit(1); |
| } |
| |
| switch (opts.retval) { |
| case LPM_BENCH_SUCCESS: |
| break; |
| case LPM_BENCH_REINIT_MAP: |
| bench_reinit_map(); |
| break; |
| default: |
| fprintf(stderr, "Unexpected BPF prog return code %d for op %d\n", |
| opts.retval, ctx.bench->bss->op); |
| exit(1); |
| } |
| } |
| |
| return NULL; |
| } |
| |
| static void *lpm_free_producer(void *unused __always_unused) |
| { |
| while (true) { |
| struct lpm_trie_map *skel; |
| |
| skel = lpm_trie_map__open_and_load(); |
| if (!skel) { |
| fprintf(stderr, "failed to open skeleton\n"); |
| exit(1); |
| } |
| |
| fill_map(bpf_map__fd(skel->maps.trie_free_map)); |
| lpm_trie_map__destroy(skel); |
| } |
| |
| return NULL; |
| } |
| |
| /* |
| * The standard bench op_report_*() functions assume measurements are |
| * taken over a 1-second interval but operations that modify the map |
| * (INSERT, DELETE, and FREE) cannot run indefinitely without |
| * "resetting" the map to the initial state. Depending on the size of |
| * the map, this likely needs to happen before the 1-second timer fires. |
| * |
| * Calculate the fraction of a second over which the op measurement was |
| * taken (to ignore any time spent doing the reset) and report the |
| * throughput results per second. |
| */ |
| static void frac_second_report_progress(int iter, struct bench_res *res, |
| long delta_ns, double rate_divisor, |
| char rate) |
| { |
| double hits_per_sec, hits_per_prod; |
| |
| hits_per_sec = res->hits / rate_divisor / |
| (res->duration_ns / (double)NSEC_PER_SEC); |
| hits_per_prod = hits_per_sec / env.producer_cnt; |
| |
| printf("Iter %3d (%7.3lfus): ", iter, |
| (delta_ns - NSEC_PER_SEC) / 1000.0); |
| printf("hits %8.3lf%c/s (%7.3lf%c/prod)\n", hits_per_sec, rate, |
| hits_per_prod, rate); |
| } |
| |
| static void frac_second_report_final(struct bench_res res[], int res_cnt, |
| double lat_divisor, double rate_divisor, |
| char rate, const char *unit) |
| { |
| double hits_mean = 0.0, hits_stddev = 0.0; |
| double latency = 0.0; |
| int i; |
| |
| for (i = 0; i < res_cnt; i++) { |
| double val = res[i].hits / rate_divisor / |
| (res[i].duration_ns / (double)NSEC_PER_SEC); |
| hits_mean += val / (0.0 + res_cnt); |
| latency += res[i].duration_ns / res[i].hits / (0.0 + res_cnt); |
| } |
| |
| if (res_cnt > 1) { |
| for (i = 0; i < res_cnt; i++) { |
| double val = |
| res[i].hits / rate_divisor / |
| (res[i].duration_ns / (double)NSEC_PER_SEC); |
| hits_stddev += (hits_mean - val) * (hits_mean - val) / |
| (res_cnt - 1.0); |
| } |
| |
| hits_stddev = sqrt(hits_stddev); |
| } |
| printf("Summary: throughput %8.3lf \u00B1 %5.3lf %c ops/s (%7.3lf%c ops/prod), ", |
| hits_mean, hits_stddev, rate, hits_mean / env.producer_cnt, |
| rate); |
| printf("latency %8.3lf %s/op\n", |
| latency / lat_divisor / env.producer_cnt, unit); |
| } |
| |
| static void insert_ops_report_progress(int iter, struct bench_res *res, |
| long delta_ns) |
| { |
| double rate_divisor = 1000000.0; |
| char rate = 'M'; |
| |
| frac_second_report_progress(iter, res, delta_ns, rate_divisor, rate); |
| } |
| |
| static void delete_ops_report_progress(int iter, struct bench_res *res, |
| long delta_ns) |
| { |
| double rate_divisor = 1000000.0; |
| char rate = 'M'; |
| |
| frac_second_report_progress(iter, res, delta_ns, rate_divisor, rate); |
| } |
| |
| static void free_ops_report_progress(int iter, struct bench_res *res, |
| long delta_ns) |
| { |
| double rate_divisor = 1000.0; |
| char rate = 'K'; |
| |
| frac_second_report_progress(iter, res, delta_ns, rate_divisor, rate); |
| } |
| |
| static void insert_ops_report_final(struct bench_res res[], int res_cnt) |
| { |
| double lat_divisor = 1.0; |
| double rate_divisor = 1000000.0; |
| const char *unit = "ns"; |
| char rate = 'M'; |
| |
| frac_second_report_final(res, res_cnt, lat_divisor, rate_divisor, rate, |
| unit); |
| } |
| |
| static void delete_ops_report_final(struct bench_res res[], int res_cnt) |
| { |
| double lat_divisor = 1.0; |
| double rate_divisor = 1000000.0; |
| const char *unit = "ns"; |
| char rate = 'M'; |
| |
| frac_second_report_final(res, res_cnt, lat_divisor, rate_divisor, rate, |
| unit); |
| } |
| |
| static void free_ops_report_final(struct bench_res res[], int res_cnt) |
| { |
| double lat_divisor = 1000000.0; |
| double rate_divisor = 1000.0; |
| const char *unit = "ms"; |
| char rate = 'K'; |
| |
| frac_second_report_final(res, res_cnt, lat_divisor, rate_divisor, rate, |
| unit); |
| } |
| |
| /* noop bench measures harness-overhead */ |
| const struct bench bench_lpm_trie_noop = { |
| .name = "lpm-trie-noop", |
| .argp = &bench_lpm_trie_map_argp, |
| .validate = validate_common, |
| .setup = lpm_noop_setup, |
| .producer_thread = lpm_producer, |
| .measure = lpm_measure, |
| .report_progress = ops_report_progress, |
| .report_final = ops_report_final, |
| }; |
| |
| /* baseline overhead for lookup and update */ |
| const struct bench bench_lpm_trie_baseline = { |
| .name = "lpm-trie-baseline", |
| .argp = &bench_lpm_trie_map_argp, |
| .validate = validate_common, |
| .setup = lpm_baseline_setup, |
| .producer_thread = lpm_producer, |
| .measure = lpm_measure, |
| .report_progress = ops_report_progress, |
| .report_final = ops_report_final, |
| }; |
| |
| /* measure cost of doing a lookup on existing entries in a full trie */ |
| const struct bench bench_lpm_trie_lookup = { |
| .name = "lpm-trie-lookup", |
| .argp = &bench_lpm_trie_map_argp, |
| .validate = validate_common, |
| .setup = lpm_lookup_setup, |
| .producer_thread = lpm_producer, |
| .measure = lpm_measure, |
| .report_progress = ops_report_progress, |
| .report_final = ops_report_final, |
| }; |
| |
| /* measure cost of inserting new entries into an empty trie */ |
| const struct bench bench_lpm_trie_insert = { |
| .name = "lpm-trie-insert", |
| .argp = &bench_lpm_trie_map_argp, |
| .validate = lpm_insert_validate, |
| .setup = lpm_insert_setup, |
| .producer_thread = lpm_producer, |
| .measure = lpm_measure, |
| .report_progress = insert_ops_report_progress, |
| .report_final = insert_ops_report_final, |
| }; |
| |
| /* measure cost of updating existing entries in a full trie */ |
| const struct bench bench_lpm_trie_update = { |
| .name = "lpm-trie-update", |
| .argp = &bench_lpm_trie_map_argp, |
| .validate = validate_common, |
| .setup = lpm_update_setup, |
| .producer_thread = lpm_producer, |
| .measure = lpm_measure, |
| .report_progress = ops_report_progress, |
| .report_final = ops_report_final, |
| }; |
| |
| /* measure cost of deleting existing entries from a full trie */ |
| const struct bench bench_lpm_trie_delete = { |
| .name = "lpm-trie-delete", |
| .argp = &bench_lpm_trie_map_argp, |
| .validate = lpm_delete_validate, |
| .setup = lpm_delete_setup, |
| .producer_thread = lpm_producer, |
| .measure = lpm_measure, |
| .report_progress = delete_ops_report_progress, |
| .report_final = delete_ops_report_final, |
| }; |
| |
| /* measure cost of freeing a full trie */ |
| const struct bench bench_lpm_trie_free = { |
| .name = "lpm-trie-free", |
| .argp = &bench_lpm_trie_map_argp, |
| .validate = lpm_free_validate, |
| .setup = lpm_free_setup, |
| .producer_thread = lpm_free_producer, |
| .measure = lpm_measure, |
| .report_progress = free_ops_report_progress, |
| .report_final = free_ops_report_final, |
| }; |