Commit Graph

19 Commits

Author SHA1 Message Date
Christopher Haster 7e307f2160 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.
2026-02-19 14:03:46 -06:00
Christopher Haster e8bdd4d381 Reworked bench.py/bench_runner/how bench measurements are recorded
This is based on how bench.py/bench_runners have actually been used in
practice. The main changes have been to make the output of bench.py more
readibly consumable by plot.py/plotmpl.py without needing a bunch of
hacky intermediary scripts.

Now instead of a single per-bench BENCH_START/BENCH_STOP, benches can
have multiple named BENCH_START/BENCH_STOP invocations to measure
multiple things in one run:

  BENCH_START("fetch", i, STEP);
  lfsr_rbyd_fetch(&lfs, &rbyd_, rbyd.block, CFG->block_size) => 0;
  BENCH_STOP("fetch");

Benches can also now report explicit results, for non-io measurements:

  BENCH_RESULT("usage", i, STEP, rbyd.eoff);

The extra iter/size parameters to BENCH_START/BENCH_RESULT also allow
some extra information to be calculated post-bench. This infomation gets
tagged with an extra bench_agg field to help organize results in
plot.py/plotmpl.py:

  - bench_meas=<meas>+amor, bench_agg=raw - amortized results
  - bench_meas=<meas>+div,  bench_agg=raw - per-byte results
  - bench_meas=<meas>+avg,  bench_agg=avg - average over BENCH_SEED
  - bench_meas=<meas>+min,  bench_agg=min - minimum over BENCH_SEED
  - bench_meas=<meas>+max,  bench_agg=max - maximum over BENCH_SEED

---

Also removed all bench.tomls for now. This may seem counterproductive in
a commit to improve benchmarking, but I'm not sure there's actual value
to keeping bench cases committed in tree.

These were alway quick to fall out of date (at the time of this commit
most of the low-level bench.tomls, rbyd, btree, etc, no longer
compiled), and most benchmarks were one-off collections of scripts/data
with results too large/cumbersome to commit and keep updated in tree.

I think the better way to approach benchmarking is a seperate repo
(multiple repos?) with all related scripts/state/code and results
committed into a hopefully reproducible snapshot. Keeping the
bench.tomls in that repo makes more sense in this model.

There may be some value to having benchmarks in CI in the future, but
for that to make sense they would need to actually fail on performance
regression. How to do that isn't so clear. Anyways we can always address
this in the future rather than now.
2023-11-03 10:27:17 -05:00
Christopher Haster 4efb55e0d7 In tests/benches, renamed cfg -> CFG
This is to better indicate this is a runner generated variable.
2023-08-04 14:05:07 -05:00
Christopher Haster 5be7bae518 Replaced tn/bn prefixes with an actual dependency system in tests/benches
The previous system of relying on test name prefixes for ordering was
simple, but organizing tests by dependencies and topologically sorting
during compilation is 1. more flexible and 2. simplifies test names,
which get typed a lot.

Note these are not "hard" dependencies, each test suite should work fine
in isolation. These "after" dependencies just hint an ordering when all
tests are ran.

As such, it's worth noting the tests should NOT error of a dependency is
missing. This unfortunately makes it a bit hard to catch typos, but
allows faster compilation of a subset of tests.

---

To make this work the way tests are linked has changed from using custom
linker section (fun linker magic!) to a weakly linked array appended to
every source file (also fun linker magic!).

At least with this method test.py has strict control over the test
ordering, and doesn't depend on 1. the order in which the linker merges
sections, and 2. the order tests are passed to test.py. I didn't realize
the previous system was so fragile.
2023-08-04 13:33:00 -05:00
Christopher Haster 2fe2078f50 Renamed tests/benches such that order is logical
It doesn't make sense to test more complex logic, such as t2_btree.toml,
when the logic it is built on, t1_rbyd.toml, does not past testing. The
test runner already guarantees a consistent lexicographic order, so all
we need to do is renamed these from test_* -> tn_*.

Note, if we every have more than 10 tests, we will need to bump up the
number of digits for all tests, so t1_rbyd.toml -> t01_rbyd.toml. This
is the main downside of lexicographic ordering. But we'll cross that
bridge when we get to it.
2023-06-30 16:37:23 -05:00
Christopher Haster 938cee1640 Updated benches based on changes, commented out outdated benchmarks 2023-06-30 03:00:07 -05:00
Christopher Haster 6a96866737 Added an mtree traversal benchmark
Note that because we amortize the traversal cost over the number of
entries, mtree traversal may have some strange looking results when
compared to mtree lookup.

Though it's interesting to note this is a valid result. In mtree lookups
we need to fetch the mdir for each entry, which is expensive. However
mtree traversal can strictly avoid fetching each mdir more than once.
This does make mdir traversal faster when iterating over all mdirs in
order.

This can be represented in big O notation if we treat the number of
entries (n) and block size (b) as variables:

- mtree traversal via lookup    = O(nb+nlog(b)logb(n))
- mtree traversal via traversal = O(nlog(b)logb(n))
2023-05-30 19:21:34 -05:00
Christopher Haster 09b3d24036 Moved btree rbyd validation into mtree traversal
Validating btree nodes during lfsr_btree_lookup was useful as a
proof-of-concept, but it's not really needed if we validate btree nodes
during mtree traversal.

mtree traversal provides the first reads into the filesystem. It's how
we find the real mroot, and (in theory at the moment) it provides the core
operation for error detection in correction. With this in mind,
implementing btree node validation in mtree traversal makes a lot of
sense, with lfsr_btree_lookup leveraging an assumed successful
validation for faster/smaller btree walks.

Note that btree node validation during traversal is still optional. We
really don't want to pay this cost during block allocation for example.

---

It may look concerning that there's no related validation in btree traversal
layer itself.

It turns out that a quirk of btree traversal returning inner btree nodes on
first visit, before actually traversing the btree node, is that it's
safe for us to validte the btree node in only the mtree traversal layer.
As long as we don't continue traversing on finding a corrupted btree,
the btree traversal layer will never traverse an unvalidated btree node.

This keeps all the validation logic in the same place, mtree traversal.
I don't know if this will stay this way if/when more error correction
features are added, but it's convenient in the meantime.
2023-05-30 18:52:02 -05:00
Christopher Haster f7d4497b80 Added some simple mtree benchmarks
It's interesting to note the different performance characteristics of
purely CoW btrees vs our mutable mtree.

The main downside of our mtree is the need to fetch leaf mdirs. This
fetch is expensive, and can be avoided in CoW btrees by storing the
trunk in each branch's parent.

On the other hand, btrees need to propagate all changes upwards to the
root.

An interesting takeaway is that a sort of mdir-trunk cache may be a very
interesting optimization for relatively little RAM cost. This may be
something to explore in the future.
2023-05-30 18:04:48 -05:00
Christopher Haster 41c28952c5 Updated benches/bench_btree.toml with btree changes 2023-05-30 16:36:50 -05:00
Christopher Haster b97192886c Updated benches to match internal API changes 2023-05-30 13:43:40 -05:00
Christopher Haster 5d68d7eccd Updated benchmarks after internal API changes 2023-04-16 12:56:37 -05:00
Christopher Haster 27fc481ec2 Generalized btree benchmarks, amortized benchmarks, plot.py/plotmpl.py tweaks
These benchmarks are now more useful for seeing how these B-trees perform.

In plot.py/plotmpl.py:

- Added --legend as another alias for -l, --legend-right.

- Allowed omitting of datasets from the legend by using empty strings
  in --labels.

- Do not sum multiple data points on the same x coordinate. This was a
  bad idea that risks invalid results going unnoticed.

  As a plus multiple data points on the same x coordinate can be abused for
  a cheap representation of measurement error.
2023-03-19 01:21:31 -05:00
Christopher Haster 2b8594528e Added some simple B-tree benchmarks 2023-03-17 14:20:09 -05:00
Christopher Haster 386ef69570 Updated rbyd benchmarks based on grow/shrink changes 2023-03-17 14:20:09 -05:00
Christopher Haster 8c9f5e9afe Brought bench_rbyd up to date with id/tag changes
Here's a quick snapshot of the measured bytes read during
lfsr_rbyd_lookup over number of tags in the rbyd block, showing the
expected O(log n) growth:

.25KiB ^                                 : .      . .. ..  .....
       |                         ::: :: :::::: ::::::: :::::::::
       |           .  . ...::.::::::::::::::::::::::::::::::::::
       |        ::::::::::::::::::::::::::::::::::::::::::::::::
       |   :.::.::::::::::::::::::::::::::::::::::::::::::::::::
       |  ::::::::::::::::::::::::::::::::::::::::::::::::::::::
       |.::::::::::::::::::::::::::::::::::::::::::::::::::::'::
       :::::::::::::::::'':'::' ::':'':'' '' '':'''' ''    '
       ::::::::::::::::'  ' ''  ': '  '        '
       :::: ::  :
       ::'
       :
       :
       :
    0B '------------------------------------------------------->
       0tags                                              4Ktags
2023-02-14 14:59:23 -06:00
Christopher Haster 9a8e1d93c6 Added some rbyd benchmarks, fixed/tweaked some related scripts
- Added both uattr (limited to 256) and id (limited to 65535) benchmarks
  covering the main rbyd operations

- Fixed issue where --defines gets passed to the test/bench runners when
  querying id-specific information. After changing the test/bench
  runners to prioritize explicit defines, this causes problems for
  recorded benchmark results and debug related things.

- In plot.py/plotmpl.py, made --by/-x/-y in subplots behave somewhat
  reasonably, contributing to a global dataset and the figure's legend,
  colors, etc, but only shown in the specified subplot. This is useful
  mainly for showing different -y values on different subplots.

- In plot.py/plotmpl.py, added --labels to allow explicit configuration
  of legend labels, much like --colors/--formats/--chars/etc. This
  removes one of the main annoying needs for modifying benchmark results.
2023-02-12 17:14:42 -06:00
Christopher Haster b0382fa891 Added BENCH/TEST_PRNG, replacing other ad-hoc sources of randomness
When you add a function to every benchmark suite, you know if should
probably be provided by the benchmark runner itself. That being said,
randomness in tests/benchmarks is a bit tricky because it needs to be
strictly controlled and reproducible.

No global state is used, allowing tests/benches to maintain multiple
randomness stream which can be useful for checking results during a run.

There's an argument for having global prng state in that the prng could
be preserved across power-loss, but I have yet to see a use for this,
and it would add a significant requirement to any future test/bench runner.
2022-12-06 23:09:07 -06:00
Christopher Haster 4fe0738ff4 Added bench.py and bench_runner.c for benchmarking
These are really just different flavors of test.py and test_runner.c
without support for power-loss testing, but with support for measuring
the cumulative number of bytes read, programmed, and erased.

Note that the existing define parameterization should work perfectly
fine for running benchmarks across various dimensions:

./scripts/bench.py \
    runners/bench_runner \
    bench_file_read \
    -gnor \
    -DSIZE='range(0,131072,1024)'

Also added a couple basic benchmarks as a starting point.
2022-11-15 13:33:34 -06:00