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.
This commit is contained in:
Christopher Haster
2026-02-02 11:52:20 -06:00
parent 8a35b9870b
commit 7e307f2160
4 changed files with 497 additions and 0 deletions
+95
View File
@@ -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;
}
+22
View File
@@ -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
+122
View File
@@ -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));
}
}
'''
+258
View File
@@ -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;
'''