From 7e307f2160abff7299be992f64979610b5ed8108 Mon Sep 17 00:00:00 2001 From: Christopher Haster Date: Mon, 2 Feb 2026 11:52:20 -0600 Subject: [PATCH] benches: Added bench_rbyd, bench_wt, and bench_helpers These were copied from external benchmarks, and tweaked/simplified a bit based on gained experience. I mostly just wanted something to test the bench runner/scripts, with bench_rbyd showcasing a low-level litmus benchmark, and bench_wt showcasing a high-level throughput benchmark. Though bench_wt has proven to be a _very_ versatile benchmark, and will likely be the first stop for getting an understanding of high-level performance implications. --- Also added bench_helpers.h/c, which includes a couple helper functions: - bench_helpers_warmup - Warm up the filesystem by writing a 1 block file 2*block_count times. This is meant to exhaust any preerased state, post-format lookahead buffers, etc. - bench_helpers_usage - Find a tight bound on disk usage. This allocates a bitmap to find the tight bound, unlike lfs3_fs_usage, which is best-effort. However the bitmap is hidden behind BENCH_HEAP_PAUSE to prevent messing with parallel heap measurements. --- benches/bench_helpers.c | 95 +++++++++++++++ benches/bench_helpers.h | 22 ++++ benches/bench_rbyd.toml | 122 +++++++++++++++++++ benches/bench_wt.toml | 258 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 497 insertions(+) create mode 100644 benches/bench_helpers.c create mode 100644 benches/bench_helpers.h create mode 100644 benches/bench_rbyd.toml create mode 100644 benches/bench_wt.toml diff --git a/benches/bench_helpers.c b/benches/bench_helpers.c new file mode 100644 index 00000000..0253605a --- /dev/null +++ b/benches/bench_helpers.c @@ -0,0 +1,95 @@ +/* + * Some extra bench helpers + * + */ +#include "benches/bench_helpers.h" + + +// warm up the filesystem +// +// this writes a 1 block file 2*block_count times to get it into a good +// state for benchmarking +int bench_helpers_warmup(lfs3_t *lfs3) { + #ifdef BENCH_YES_STACK + BENCH_STACK_PAUSE(); + #endif + #ifdef BENCH_YES_HEAP + BENCH_HEAP_PAUSE(); + #endif + + uint8_t *wbuf = malloc(BLOCK_SIZE); + memset(wbuf, '1', BLOCK_SIZE); + + lfs3_file_t file; + lfs3_file_open(lfs3, &file, "warmup", + LFS3_O_WRONLY | LFS3_O_CREAT | LFS3_O_EXCL) => 0; + for (lfs3_block_t i = 0; i < 2*BLOCK_COUNT; i++) { + lfs3_file_rewind(lfs3, &file) => 0; + lfs3_file_write(lfs3, &file, wbuf, BLOCK_SIZE) => BLOCK_SIZE; + lfs3_file_sync(lfs3, &file) => 0; + } + lfs3_file_close(lfs3, &file) => 0; + + lfs3_remove(lfs3, "warmup") => 0; + + free(wbuf); + + #ifdef BENCH_YES_HEAP + BENCH_HEAP_RESUME(); + #endif + #ifdef BENCH_YES_STACK + BENCH_STACK_RESUME(); + #endif + return 0; +} + + +// find tight disk usage +uintmax_t bench_helpers_usage(lfs3_t *lfs3) { + #ifdef BENCH_YES_STACK + BENCH_STACK_PAUSE(); + #endif + #ifdef BENCH_YES_HEAP + BENCH_HEAP_PAUSE(); + #endif + + // measure disk usage + // + // littlefs can be a dag, so build a bitmap to find the exact + // disk usage + uint8_t *usage_bmap = malloc((BLOCK_COUNT+8-1)/8); + memset(usage_bmap, 0, (BLOCK_COUNT+8-1)/8); + + lfs3_trv_t trv; + lfs3_trv_open(lfs3, &trv, 0) => 0; + while (true) { + struct lfs3_tinfo tinfo; + int err = lfs3_trv_read(lfs3, &trv, &tinfo); + assert(!err || err == LFS3_ERR_NOENT); + if (err == LFS3_ERR_NOENT) { + break; + } + + usage_bmap[tinfo.block/8] |= 1 << (tinfo.block % 8); + } + lfs3_trv_close(lfs3, &trv) => 0; + + lfs3_size_t usage = 0; + for (lfs3_size_t j = 0; j < BLOCK_COUNT; j++) { + if (usage_bmap[j / 8] & (1 << (j % 8))) { + usage += 1; + } + } + + free(usage_bmap); + + #ifdef BENCH_YES_HEAP + BENCH_HEAP_RESUME(); + #endif + #ifdef BENCH_YES_STACK + BENCH_STACK_RESUME(); + #endif + return (uintmax_t)usage * (uintmax_t)BLOCK_SIZE; +} + + diff --git a/benches/bench_helpers.h b/benches/bench_helpers.h new file mode 100644 index 00000000..57c55701 --- /dev/null +++ b/benches/bench_helpers.h @@ -0,0 +1,22 @@ +/* + * Some extra bench helpers + * + */ +#ifndef BENCH_HELPERS_H +#define BENCH_HELPERS_H + +#include "runners/bench_runner.h" + + +// warm up the filesystem +// +// this writes a 1 block file 2*block_count times to get it into a good +// state for benchmarking +int bench_helpers_warmup(lfs3_t *lfs3); + + +// find tight disk usage +uintmax_t bench_helpers_usage(lfs3_t *lfs3); + + +#endif diff --git a/benches/bench_rbyd.toml b/benches/bench_rbyd.toml new file mode 100644 index 00000000..b3a80eb6 --- /dev/null +++ b/benches/bench_rbyd.toml @@ -0,0 +1,122 @@ +# Low-level rbyd benchmarks + +# set block_size to the full size of disk so we can test arbitrarily +# large rbyd trees, we don't really care about block sizes at this +# abstraction level +# +defines.BLOCK_SIZE = 'DISK_SIZE' +defines.BLOCK_COUNT = 1 + +[cases.bench_rbyd] +# 0 = in-order +# 1 = reversed-order +# 2 = random-order +defines.ORDER = 2 +defines.N = 1024 +defines.STEP = 1 +defines.SEED = 42 +defines.SIZE = 4 +# set of probes to measure +# 0x01 => create +# 0x02 => delete +# 0x04 => fetch +# 0x08 => lookup +# 0x10 => usage +defines.MASK = 0x1f +in = 'lfs3.c' +code = ''' + lfs3_t lfs3; + lfs3_init(&lfs3, LFS3_M_RDWR, CFG) => 0; + + for (lfs3_size_t n = 0; n < N; n += STEP) { + lfs3_rbyd_t rbyd = { + .blocks[0] = 0, + .eoff = 0, + .cksum = 0, + .trunk = 0, + .weight = 0, + }; + lfs3_bd_erase(&lfs3, rbyd.blocks[0]) => 0; + + // create N attrs + // + // note we need to take care to generate indexes within a valid + // range as the rbyd grows + uint32_t prng = SEED; + for (lfs3_size_t i = 0; i < n; i++) { + // create an attr + lfs3_off_t i_ + = (ORDER == 0) ? rbyd.weight + : (ORDER == 1) ? 0 + : BENCH_PRNG(&prng) % (rbyd.weight+1); + uint8_t wbuf[SIZE]; + memset(wbuf, 'a'+(BENCH_PRNG(&prng) % 26), SIZE); + lfs3_rbyd_commit(&lfs3, &rbyd, i_, LFS3_RATTRS( + LFS3_RATTR(3, LFS3_TAG_DATA, +1, LFS3_FROM_DATA), + LFS3_RATTR_ARG(SIZE), + LFS3_RATTR_ARG(wbuf), + LFS3_RATTR_NULL)) => 0; + } + + // create an attr + if (MASK & 0x01) { + BENCH_START("create"); + lfs3_off_t i_ + = (ORDER == 0) ? rbyd.weight + : (ORDER == 1) ? 0 + : BENCH_PRNG(&prng) % (rbyd.weight+1); + lfs3_off_t n_ = rbyd.weight; + uint8_t wbuf[SIZE]; + memset(wbuf, 'a'+(BENCH_PRNG(&prng) % 26), SIZE); + lfs3_rbyd_commit(&lfs3, &rbyd, i_, LFS3_RATTRS( + LFS3_RATTR(3, LFS3_TAG_DATA, +1, LFS3_FROM_DATA), + LFS3_RATTR_ARG(SIZE), + LFS3_RATTR_ARG(wbuf), + LFS3_RATTR_NULL)) => 0; + assert(rbyd.weight == n_+1); + BENCH_STOP("create", n+STEP); + } + + // delete an attr + if (MASK & 0x02) { + BENCH_START("delete"); + if (n > 0) { + lfs3_off_t i_ = BENCH_PRNG(&prng) % rbyd.weight; + lfs3_off_t n_ = rbyd.weight; + lfs3_rbyd_commit(&lfs3, &rbyd, i_, LFS3_RATTRS( + LFS3_RATTR(1, LFS3_tag_RM, -1), + LFS3_RATTR_NULL)) => 0; + assert(rbyd.weight == n_-1); + } + BENCH_STOP("delete", n+STEP); + } + + // fetch the rbyd + if (MASK & 0x04) { + BENCH_START("fetch"); + lfs3_rbyd_t rbyd_; + lfs3_rbyd_fetch(&lfs3, &rbyd_, rbyd.blocks[0], 0) => 0; + assert(rbyd_.weight == rbyd.weight); + BENCH_STOP("fetch", n+STEP); + } + + // lookup an attr + if (MASK & 0x08) { + BENCH_START("lookup"); + lfs3_off_t i_ = BENCH_PRNG(&prng) % rbyd.weight; + lfs3_data_t data_; + lfs3_stag_t tag_ = lfs3_rbyd_lookup(&lfs3, &rbyd, + i_, LFS3_TAG_DATA, + &data_); + assert(tag_ == LFS3_TAG_DATA); + assert(lfs3_data_size(&data_) == SIZE); + BENCH_STOP("lookup", n+STEP); + } + + // measure the disk usage + if (MASK & 0x10) { + BENCH_RESULT("usage", n+STEP, lfs3_rbyd_eoff(&rbyd)); + } + } +''' + diff --git a/benches/bench_wt.toml b/benches/bench_wt.toml new file mode 100644 index 00000000..904c93db --- /dev/null +++ b/benches/bench_wt.toml @@ -0,0 +1,258 @@ +# High-level write-throughput benchmarks + +# these are common and can be overridden suite-wide +# +# note for bench_*_many, file size defaults to CHUNK, and SIZE = sum of +# all files +# +defines.SIZE = 32768 +defines.CHUNK = 64 +defines.SEED = 42 + +# note FILE_SIZE is bench_*_many specific +defines.FILE_SIZE = 'CHUNK' +defines.FILE_COUNT = '(SIZE+(FILE_SIZE-1)) / lfs3_max(FILE_SIZE, 1)' + +# simulated time, in nanoseconds, to run the bench +defines.SIM_TIME = 60000000000 # 1 minute +# simulation size in bytes +defines.SIM_SIZE = 0 + +# set this to true to skip bench warmup +defines.SKIP_WARMUP = false + +# don't fruncate, this is logging specific +defines.NO_FRUNCATE = 0 + + +# include common bench helpers +code = ''' +#include "benches/bench_helpers.h" +''' + +# sequential write throughput +[cases.bench_wt_seq] +code = ''' + lfs3_t lfs3; + lfs3_format(&lfs3, LFS3_F_RDWR, CFG) => 0; + lfs3_mount(&lfs3, LFS3_M_RDWR, CFG) => 0; + if (!SKIP_WARMUP) { + int err = bench_helpers_warmup(&lfs3); + if (err) { + LFS3_ERROR("Bench warmup failed: %d", err); + return; + } + } + uint32_t prng = SEED; + + // reset our timer + lfs3_kiwibd_simreset(CFG); + + // open a file + BENCH_START("write"); + lfs3_file_t file; + lfs3_file_open(&lfs3, &file, "bench_linear", + LFS3_O_WRONLY | LFS3_O_CREAT | LFS3_O_EXCL) => 0; + lfs3_off_t size = 0; + uint64_t written = 0; + // ok, one of these needs to be non-zero + LFS3_ASSERT(SIM_TIME > 0 || SIM_SIZE > 0); + while (!(SIM_SIZE && written >= (uint64_t)SIM_SIZE) + && !(SIM_TIME && lfs3_kiwibd_simtime(CFG) >= SIM_TIME)) { + // arguably we should just rewind and continue writing to the + // front of the file when we hit the end, but this overly + // penalizes littlefs2, so instead we truncate + if (size >= SIZE) { + lfs3_file_rewind(&lfs3, &file) => 0; + lfs3_file_truncate(&lfs3, &file, 0) => 0; + size = 0; + } + + // write to the file + uint8_t wbuf[CHUNK]; + for (lfs3_size_t j = 0; j < CHUNK; j++) { + wbuf[j] = 'a' + (BENCH_PRNG(&prng) % 26); + } + lfs3_file_write(&lfs3, &file, wbuf, CHUNK) => CHUNK; + + size += CHUNK; + written += CHUNK; + } + lfs3_file_close(&lfs3, &file) => 0; + // report the amount we managed to write + BENCH_STOP("write", written); + + lfs3_unmount(&lfs3) => 0; +''' + +# random write throughput +[cases.bench_wt_random] +code = ''' + lfs3_t lfs3; + lfs3_format(&lfs3, LFS3_F_RDWR, CFG) => 0; + lfs3_mount(&lfs3, LFS3_M_RDWR, CFG) => 0; + if (!SKIP_WARMUP) { + int err = bench_helpers_warmup(&lfs3); + if (err) { + return; + } + } + uint32_t prng = SEED; + + // reset our timer + lfs3_kiwibd_simreset(CFG); + + // open a file + BENCH_START("write"); + lfs3_file_t file; + lfs3_file_open(&lfs3, &file, "bench_random", + LFS3_O_WRONLY | LFS3_O_CREAT | LFS3_O_EXCL) => 0; + lfs3_off_t size = 0; + uint64_t written = 0; + while (!(SIM_SIZE && written >= (uint64_t)SIM_SIZE) + && !(SIM_TIME && lfs3_kiwibd_simtime(CFG) >= SIM_TIME)) { + // seek to a random location + lfs3_off_t pos = BENCH_PRNG(&prng) % SIZE; + lfs3_file_seek(&lfs3, &file, pos, LFS3_SEEK_SET) => pos; + + // write to the file + uint8_t wbuf[CHUNK]; + for (lfs3_size_t j = 0; j < CHUNK; j++) { + wbuf[j] = 'a' + (BENCH_PRNG(&prng) % 26); + } + lfs3_file_write(&lfs3, &file, wbuf, CHUNK) => CHUNK; + + size = lfs3_max(size, pos + CHUNK); + written += CHUNK; + } + lfs3_file_close(&lfs3, &file) => 0; + // report the amount we managed to write + BENCH_STOP("write", written); + + lfs3_unmount(&lfs3) => 0; +''' + +# logging write throughput +# +# two big differences from seq: +# 1. fruncate/rotations instead of rewind+truncate +# 2. sync called on every write +[cases.bench_wt_logging] +code = ''' + lfs3_t lfs3; + lfs3_format(&lfs3, LFS3_F_RDWR, CFG) => 0; + lfs3_mount(&lfs3, LFS3_M_RDWR, CFG) => 0; + if (!SKIP_WARMUP) { + int err = bench_helpers_warmup(&lfs3); + if (err) { + return; + } + } + uint32_t prng = SEED; + + // reset our timer + lfs3_kiwibd_simreset(CFG); + + // open a file + BENCH_START("write"); + lfs3_file_t file; + lfs3_file_open(&lfs3, &file, "bench_log", + LFS3_O_WRONLY | LFS3_O_CREAT | LFS3_O_EXCL | LFS3_O_APPEND) => 0; + uint64_t written = 0; + // ok, one of these needs to be non-zero + LFS3_ASSERT(SIM_TIME > 0 || SIM_SIZE > 0); + while (!(SIM_SIZE && written >= (uint64_t)SIM_SIZE) + && !(SIM_TIME && lfs3_kiwibd_simtime(CFG) >= SIM_TIME)) { + // append to log + uint8_t wbuf[CHUNK]; + for (lfs3_size_t j = 0; j < CHUNK; j++) { + wbuf[j] = 'a' + (BENCH_PRNG(&prng) % 26); + } + lfs3_file_write(&lfs3, &file, wbuf, CHUNK) => CHUNK; + + // sync + lfs3_file_sync(&lfs3, &file) => 0; + + // fruncate or rotate if full + lfs3_soff_t size = lfs3_file_size(&lfs3, &file); + assert(size >= 0); + if (size > SIZE) { + if (!NO_FRUNCATE) { + lfs3_file_fruncate(&lfs3, &file, SIZE) => 0; + } else { + lfs3_file_close(&lfs3, &file) => 0; + lfs3_rename(&lfs3, "bench_log", "bench_log.1") => 0; + lfs3_file_open(&lfs3, &file, "bench_log", + LFS3_O_WRONLY + | LFS3_O_CREAT + | LFS3_O_EXCL + | LFS3_O_APPEND) => 0; + } + } + + written += CHUNK; + } + lfs3_file_close(&lfs3, &file) => 0; + // report the amount we managed to write + BENCH_STOP("write", written); + + lfs3_unmount(&lfs3) => 0; +''' + +# many small file write throughput +[cases.bench_wt_many] +code = ''' + lfs3_t lfs3; + lfs3_format(&lfs3, LFS3_F_RDWR, CFG) => 0; + lfs3_mount(&lfs3, LFS3_M_RDWR, CFG) => 0; + if (!SKIP_WARMUP) { + int err = bench_helpers_warmup(&lfs3); + if (err) { + return; + } + } + uint32_t prng = SEED; + + // reset our timer + lfs3_kiwibd_simreset(CFG); + + // open a file + BENCH_START("write"); + uint64_t written = 0; + // ok, one of these needs to be non-zero + LFS3_ASSERT(SIM_TIME > 0 || SIM_SIZE > 0); + while (!(SIM_SIZE && written >= (uint64_t)SIM_SIZE) + && !(SIM_TIME && lfs3_kiwibd_simtime(CFG) >= SIM_TIME)) { + // choose a random filename + lfs3_off_t pos = BENCH_PRNG(&prng) % FILE_COUNT; + char name[256]; + sprintf(name, "bench_%08x", pos); + uint8_t wbuf[CHUNK]; + + // create the file + // + // note file == CHUNK here, the sum of all files should add up + // roughly to the benchmark SIZE + lfs3_file_t file; + lfs3_file_open(&lfs3, &file, name, + LFS3_O_WRONLY | LFS3_O_CREAT | LFS3_O_TRUNC) => 0; + for (lfs3_size_t i = 0; i < (FILE_SIZE+(CHUNK-1))/CHUNK; i++) { + lfs3_ssize_t d = lfs3_min(CHUNK, FILE_SIZE); + memset(wbuf, 'a'+(BENCH_PRNG(&prng) % 26), d); + lfs3_file_write(&lfs3, &file, wbuf, d) => d; + + written += d; + + // taking too long? + if (SIM_TIME && lfs3_kiwibd_simtime(CFG) >= SIM_TIME) { + break; + } + } + lfs3_file_close(&lfs3, &file) => 0; + } + // report the amount we managed to write + BENCH_STOP("write", written); + + lfs3_unmount(&lfs3) => 0; +''' +