catalog table_key — zero-copy (one alloc, byte-identical)
keys::table_key(catalog, ident) builds the redb lookup key "{catalog}\x1f{ns_joined_by_dot}\x1f{table}". It runs on every catalog op and once per commit — a hot path the warehouse-write bench (skade.funnel_append-style) flagged.
The change
OLD (format!) |
NEW (allocation-lean) | |
| ns path | ns.clone().inner().join(".") → clone the Vec<String> + a join String |
iterate &[String] by reference (NamespaceIdent derefs to [String]) |
| assemble | format!("{cat}{SEP}{ns}{SEP}{name}") → another String |
one String::with_capacity(exact) + push_str/push |
| allocations | 2 (join + format) + a Vec clone | 1 (pre-sized) |
ns_path also dropped its .clone().inner() (now ns.join(".")) — benefits every key builder.
Extended to the whole builder family (follow-up wave)
The same 2-alloc→1 pattern now covers every namespace/prefix builder in keys.rs, factored through two zero-alloc helpers — ns_path_len (exact byte-capacity, no allocation) and push_ns_path (append parts by reference):
| Builder | OLD | NEW | On the hot path of |
namespace_key |
ns_path join + format! (2) |
1 pre-sized String |
every namespace lookup/create/update |
namespace_prop_key |
2 | 1 | property get/set |
namespace_prop_prefix |
2 | 1 | property range-scan bound |
table_prefix |
2 | 1 | list_tables scan bound |
catalog_prefix |
format! machinery |
with_capacity+push |
catalog-wide scan bound |
Byte-identity pinned by ns_keys_are_byte_identical_to_legacy_format (single/multi-part + non-ASCII parts, length via bytes). Micro-bench bench_catalog_key now times all three shapes; light run on this box (dwarf, release, N=200k): namespace_key ~337→32 ns/op, table_key ~313→34, table_prefix ~252→33 (~8–10×). These are structural (fewer allocs), so kept regardless of micro-noise; heavy corpus sweep → the quiet bench box (Loki/Odin), never here.
Proof
- Byte-identical (mandatory — it's a lookup key):
keys::tests::table_key_is_byte_identical_to_legacy_formatpins the new builder against the exact oldformat!across single/multi-part namespaces + non-ASCII leaves. Always-on. - Faster: in-crate
#[ignore]micro-benchbench_catalog_key(run-- --ignored --nocapture): legacy 513 ns/op → zero-copy 226 ns/op (~2.3×) on this box (light, N=200k). Heavy corpus runs → the quiet bench box (Loki/Odin), never here.
Harness bencher (skade_catalog_key_build) — the bench-feature hook (DONE)
table_key & family are pub(crate) — the catalog's internal on-disk encoding, deliberately off the public API. The nornir-Bencher harness (its own skade-katalog-bench crate) still needs to time them so the warehouse no-regression gate has a real throughput number. That reach is now provided by a bench-feature pub hook: skade_katalog::bench_keys::* re-exports the builders as thin #[inline] pass-throughs only under --features bench. A normal build (and cargo publish, which never enables it) neither widens the API nor pays a byte — it only re-gates visibility of already-compiled fns, pulling no extra deps.
- Hook:
src/lib.rs#[cfg(feature = "bench")] pub mod bench_keys. - Feature: root
Cargo.tomlbench = []; the bench crate depends onskade-katalog = { path = "..", features = ["bench"] }. - Bencher:
bench/examples/nornir-bench.rsstruct CatalogKeyBuild(id = "skade_catalog_key_build"), batched timing (a ~30 ns op — per-iterInstant::now()would swamp it, so it times the whole loop and divides,black_box-pinning the builder + a livesink). Emitsops_sec(keyed offtable_key, one per commit) plus per-builder*_ns_per_op.LIGHT, pure-in-proc (no containers/S3); the heavy N sweep goes to Loki/Odin viaBENCH_CATALOG_KEY_ITERS. - The always-on
#[ignore]d in-cratebench_catalog_keymicro-test stays as the zero-dependency local sanity check; the harness bencher is the CI/gate number.