runners: emubd/kiwibd: Adopted emulated simtime API

This is based on some work in external benchmarks. What's worked well
there is emulating a global simtime based on per-byte estimates.

This moves the emulated simtime into emubd/kiwibd, and extends the idea
with both per-byte and per-op timing estimates for hopefully more
realistic results.

---

The problem is how NAND flash reads work.

Per-byte timing estimates are surprisingly accurate for NOR flash. There
is some overhead for sending the address, but it's mostly dominated by
bus cost (~20ns/B [1]).

NAND flash, on the otherhand, technically does support byte-level reads,
but first needs to read into 2KiB buffer. Surprisingly, these are pretty
close in cost (~19ns/B bus [2] vs ~12ns/B buffer [2]).

This close-ness makes modeling NAND flash difficult. If we set
read_size=1, we risk hiding the cost of small reads, which littlefs3 is
full of (rbyd lookups). If we set read_size=2048, we unfairly penalize
littlefs3 for the same reason.

---

The solution here is to expose both per-byte and per-op timing
estimates. This lets you model NAND reads using two data points:

  ^
  |                                realtime --> ...............o
  |                                             :    .....'''' :
  |                              ...............:''''  ^       :
  |                              :....'''''            |       :
  |               ..........::::::                  simtime    :
  |          .....:''''                                        :
  |o....:::::.....:                                            :
  |:                                                           :
  |:                                                           :
  +:-----------------------------------------------------------:>
   min read                                              max read

Where:

  bus_timing = 19ns
  buffer_timing = 25us
  buffer_size = 2KiB
  erase_size = 128KiB

  min_read = buffer_timing
  max_read = (erase_size/buffer_size)*buffer_timing - buffer_timing
  read_timing = min_read
  readed_timing = ((max_read - min_read)/erase_size) + bus_timing

  simtime = reads*read_timing + readed*readed_timing
            (per-op)            (per-byte)

This should correctly penalize small reads without complicating
emubd/kiwibd too much.

That's the idea anyways! It will take some use to understand if this is
a reasonable approach.

As a plus, this is a superset of the per-byte model, so both can be used
for realistic vs idealistic simulations (and to test the bus+buffer
model itself).

1: https://www.winbond.com/resource-files/W25Q256JV%20SPI%20RevQ%2002072025%20Plus.pdf
2: https://www.winbond.com/resource-files/W25N01GV%20Rev%20R%20070323.pdf
This commit is contained in:
Christopher Haster
2026-01-18 23:57:23 -06:00
parent 1b70c1f199
commit d3dd927de3
10 changed files with 548 additions and 278 deletions
+42
View File
@@ -25,6 +25,42 @@
BENCH_DEFINE(CRYSTAL_THRESH, BLOCK_SIZE/8 )
BENCH_DEFINE(LOOKGBMAP_THRESH, BLOCK_COUNT/4 )
BENCH_DEFINE(ERASE_VALUE, 0xff )
// the default timings here are based on NOR flash, specifically
// w25q64jv:
//
// https://www.winbond.com/resource-files/W25Q256JV%20SPI%20RevQ%2002072025%20Plus.pdf
//
// note one thing unique to NOR flash is the extreme erase cost
//
// FR=104 MHz, quad prog (9.6 ns * 8/4)
// => +~19 ns for bus (not read!)
//
// readed=40ns/B fR=50 MHz, quad read (20 ns * 8/4)
// progged=1582ns/B tPP=0.4 ms, page=256 (0.4 ms / 256 + bus)
// erased=10986ns/B tSE=45 ms, sector=4096 (45 ms / 4096)
//
// reads=0ns (no transaction cost)
// progs=400000ns tPP=0.4 ms, page=256
// erases=45000000ns tSE=45 ms, sector=4096
// readed=40ns/B fR=50 MHz, quad read (20 ns * 8/4)
// progged=1484ns/B tPP=0.4 ms (((4096/256)*0.4 ms - 0.4 ms)/4096 + bus)
// erased=0ns/B (no per-byte cost)
//
#ifdef BENCH_SIMPLE
BENCH_DEFINE(READS_TIMING, 0 )
BENCH_DEFINE(PROGS_TIMING, 0 )
BENCH_DEFINE(ERASES_TIMING, 0 )
BENCH_DEFINE(READED_TIMING, 40 )
BENCH_DEFINE(PROGGED_TIMING, 1582 )
BENCH_DEFINE(ERASED_TIMING, 10986 )
#else
BENCH_DEFINE(READS_TIMING, 0 )
BENCH_DEFINE(PROGS_TIMING, 400000 )
BENCH_DEFINE(ERASES_TIMING, 45000000 )
BENCH_DEFINE(READED_TIMING, 40 )
BENCH_DEFINE(PROGGED_TIMING, 1484 )
BENCH_DEFINE(ERASED_TIMING, 0 )
#endif
BENCH_DEFINE(ERASE_CYCLES, 0 )
BENCH_DEFINE(BADBLOCK_BEHAVIOR, LFS3_EMUBD_BADBLOCK_PROGERROR )
BENCH_DEFINE(POWERLOSS_BEHAVIOR, LFS3_EMUBD_POWERLOSS_ATOMIC )
@@ -65,6 +101,12 @@
// struct lfs3_*bd_cfg fields
#ifdef BENCH_BDCFG
BENCH_BDCFG(erase_value, ERASE_VALUE )
BENCH_BDCFG(reads_timing, READS_TIMING )
BENCH_BDCFG(progs_timing, PROGS_TIMING )
BENCH_BDCFG(erases_timing, ERASES_TIMING )
BENCH_BDCFG(readed_timing, READED_TIMING )
BENCH_BDCFG(progged_timing, PROGGED_TIMING )
BENCH_BDCFG(erased_timing, ERASED_TIMING )
BENCH_BDCFG(erase_cycles, ERASE_CYCLES )
BENCH_BDCFG(badblock_behavior, BADBLOCK_BEHAVIOR )
BENCH_BDCFG(powerloss_behavior, POWERLOSS_BEHAVIOR )
+60 -15
View File
@@ -436,9 +436,9 @@ FILE *bench_trace_file = NULL;
uint32_t bench_trace_cycles = 0;
uint64_t bench_trace_time = 0;
uint64_t bench_trace_open_time = 0;
lfs3_emubd_sleep_t bench_read_sleep = 0.0;
lfs3_emubd_sleep_t bench_prog_sleep = 0.0;
lfs3_emubd_sleep_t bench_erase_sleep = 0.0;
lfs3_emubd_ns_t bench_read_sleep = 0.0;
lfs3_emubd_ns_t bench_prog_sleep = 0.0;
lfs3_emubd_ns_t bench_erase_sleep = 0.0;
// this determines both the backtrace buffer and the trace printf buffer, if
// trace ends up interleaved or truncated this may need to be increased
@@ -604,9 +604,13 @@ void bench_permutation(size_t i, uint32_t *buffer, size_t size) {
typedef struct bench_record {
const char *m;
uintmax_t n;
lfs3_emubd_io_t last_reads;
lfs3_emubd_io_t last_progs;
lfs3_emubd_io_t last_erases;
lfs3_emubd_io_t last_readed;
lfs3_emubd_io_t last_proged;
lfs3_emubd_io_t last_progged;
lfs3_emubd_io_t last_erased;
lfs3_emubd_ns_t last_simtime;
} bench_record_t;
static struct lfs3_cfg *bench_cfg = NULL;
@@ -622,12 +626,20 @@ void bench_reset(struct lfs3_cfg *cfg) {
void bench_start(const char *m, uintmax_t n) {
// measure current read/prog/erase
assert(bench_cfg);
lfs3_emubd_sio_t reads = lfs3_emubd_reads(bench_cfg);
assert(reads >= 0);
lfs3_emubd_sio_t progs = lfs3_emubd_progs(bench_cfg);
assert(progs >= 0);
lfs3_emubd_sio_t erases = lfs3_emubd_erases(bench_cfg);
assert(erases >= 0);
lfs3_emubd_sio_t readed = lfs3_emubd_readed(bench_cfg);
assert(readed >= 0);
lfs3_emubd_sio_t proged = lfs3_emubd_proged(bench_cfg);
assert(proged >= 0);
lfs3_emubd_sio_t progged = lfs3_emubd_progged(bench_cfg);
assert(progged >= 0);
lfs3_emubd_sio_t erased = lfs3_emubd_erased(bench_cfg);
assert(erased >= 0);
// note this can error if no timings provided
lfs3_emubd_sns_t simtime = lfs3_emubd_simtime(bench_cfg);
// allocate a new record
bench_record_t *record = mappend(
@@ -637,31 +649,64 @@ void bench_start(const char *m, uintmax_t n) {
&bench_record_capacity);
record->m = m;
record->n = n;
record->last_reads = reads;
record->last_progs = progs;
record->last_erases = erases;
record->last_readed = readed;
record->last_proged = proged;
record->last_progged = progged;
record->last_erased = erased;
record->last_simtime = simtime;
}
void bench_stop(const char *m) {
// measure current read/prog/erase
assert(bench_cfg);
lfs3_emubd_sio_t reads = lfs3_emubd_reads(bench_cfg);
assert(reads >= 0);
lfs3_emubd_sio_t progs = lfs3_emubd_progs(bench_cfg);
assert(progs >= 0);
lfs3_emubd_sio_t erases = lfs3_emubd_erases(bench_cfg);
assert(erases >= 0);
lfs3_emubd_sio_t readed = lfs3_emubd_readed(bench_cfg);
assert(readed >= 0);
lfs3_emubd_sio_t proged = lfs3_emubd_proged(bench_cfg);
assert(proged >= 0);
lfs3_emubd_sio_t progged = lfs3_emubd_progged(bench_cfg);
assert(progged >= 0);
lfs3_emubd_sio_t erased = lfs3_emubd_erased(bench_cfg);
assert(erased >= 0);
// note this can error if no timings provided
lfs3_emubd_sns_t simtime = lfs3_emubd_simtime(bench_cfg);
// find our record
for (size_t i = 0; i < bench_record_count; i++) {
if (strcmp(bench_records[i].m, m) == 0) {
// print results
printf("benched %s %jd %"PRIu64" %"PRIu64" %"PRIu64"\n",
bench_records[i].m,
bench_records[i].n,
readed - bench_records[i].last_readed,
proged - bench_records[i].last_proged,
erased - bench_records[i].last_erased);
if (simtime >= 0) {
printf("benched %s %jd "
"%"PRIu64" %"PRIu64" %"PRIu64" "
"%"PRIu64" %"PRIu64" %"PRIu64" "
"%"PRIu64"\n",
bench_records[i].m,
bench_records[i].n,
reads - bench_records[i].last_reads,
progs - bench_records[i].last_progs,
erases - bench_records[i].last_erases,
readed - bench_records[i].last_readed,
progged - bench_records[i].last_progged,
erased - bench_records[i].last_erased,
simtime - bench_records[i].last_simtime);
} else {
printf("benched %s %jd "
"%"PRIu64" %"PRIu64" %"PRIu64" "
"%"PRIu64" %"PRIu64" %"PRIu64"\n",
bench_records[i].m,
bench_records[i].n,
reads - bench_records[i].last_reads,
progs - bench_records[i].last_progs,
erases - bench_records[i].last_erases,
readed - bench_records[i].last_readed,
progged - bench_records[i].last_progged,
erased - bench_records[i].last_erased);
}
// remove our record
memmove(&bench_records[i],
+3 -3
View File
@@ -447,9 +447,9 @@ FILE *test_trace_file = NULL;
uint32_t test_trace_cycles = 0;
uint64_t test_trace_time = 0;
uint64_t test_trace_open_time = 0;
lfs3_emubd_sleep_t test_read_sleep = 0.0;
lfs3_emubd_sleep_t test_prog_sleep = 0.0;
lfs3_emubd_sleep_t test_erase_sleep = 0.0;
lfs3_emubd_ns_t test_read_sleep = 0.0;
lfs3_emubd_ns_t test_prog_sleep = 0.0;
lfs3_emubd_ns_t test_erase_sleep = 0.0;
volatile size_t TEST_PLS = 0;