feat(db2-keys): gate the residency measurement, close out task 7
- new `residency` leg in db-bench.py driving docs/examples/residency-bench: two tables identical except the annotation, control cap + binding cap - ITS OWN PROGRAM, not a db-bench mode: declaring a resident: keys table is a WHOLE-PROGRAM constraint, so the no-WO_DATA refusal fires for every mode in the module. Putting those classes in db-bench's shared types made growth/ceiling/randread — which run without WO_DATA — refuse to start. Caught by running the leg, not by reading it - gates the RATIOS, waives the absolutes: ops/sec under a cap is swap and disk I/O and belongs to the box. Same split randread makes - rss_ratio 2.55 floor 2.0 tol 10% (structural, like bytes_per_row); overcap_vs_swap_x 1.53 floor 1.0; in_ram_cost_x 4.23 ceiling 8.0; all_collapse_x 105.4 floor 2.0 - all_collapse_x exists because the leg's FIRST run silently measured nothing: at QUICK's 40k rows a 48 MiB cap binds neither mode, so the "over-cap" half was not over cap. The cap now scales with N and the leg asserts it binds - verified the gate bites: rss_ratio 1.4, overcap_vs_swap_x 0.6 and in_ram_cost_x 12.0 are all rejected - task 7 closed: both criteria moved to Met with how each was verified Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit a310496664982c372f51b43113465eb8ad9e9fb5)
This commit is contained in:
parent
6fc5b4b7d4
commit
841f6c8f3f
8 changed files with 416 additions and 271 deletions
|
|
@ -863,5 +863,29 @@
|
||||||
"floor": 100,
|
"floor": 100,
|
||||||
"tolerance_pct": 100,
|
"tolerance_pct": 100,
|
||||||
"value": 3
|
"value": 3
|
||||||
|
},
|
||||||
|
"residency.all_collapse_x": {
|
||||||
|
"dir": "higher",
|
||||||
|
"floor": 2.0,
|
||||||
|
"tolerance_pct": 100,
|
||||||
|
"value": 105.4
|
||||||
|
},
|
||||||
|
"residency.in_ram_cost_x": {
|
||||||
|
"dir": "lower",
|
||||||
|
"floor": 8.0,
|
||||||
|
"tolerance_pct": 50,
|
||||||
|
"value": 4.23
|
||||||
|
},
|
||||||
|
"residency.overcap_vs_swap_x": {
|
||||||
|
"dir": "higher",
|
||||||
|
"floor": 1.0,
|
||||||
|
"tolerance_pct": 100,
|
||||||
|
"value": 1.53
|
||||||
|
},
|
||||||
|
"residency.rss_ratio": {
|
||||||
|
"dir": "higher",
|
||||||
|
"floor": 2.0,
|
||||||
|
"tolerance_pct": 10,
|
||||||
|
"value": 2.55
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -651,190 +651,6 @@ fn replayseed_mode(n: Int, m: Int) -> Int {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
-- databasev2 2 task 7: the residency A/B, one mode per table so the two runs
|
|
||||||
-- differ ONLY in the annotation. Same fill, same Weyl key order, same read
|
|
||||||
-- count as randread_mode above — the comparison is against that leg's own
|
|
||||||
-- 273x swap figure, measured on the same box under the same cap.
|
|
||||||
-- The WIDE half of the residency A/B. Same structure as kread_all/kread_keys
|
|
||||||
-- but with Text columns, which is the only shape where dropping a payload
|
|
||||||
-- frees anything: an Int's value is its inline slot word, a Text's is a
|
|
||||||
-- separate allocation.
|
|
||||||
-- databasev2 2 task 7, the GB-scale leg. Same A/B as wread_*, but each row
|
|
||||||
-- carries ~2 KB of Text so a realistic data volume is reachable in a few
|
|
||||||
-- hundred thousand inserts rather than millions — the insert path is
|
|
||||||
-- fsync-bound at roughly 2 000 rows/s, so row COUNT is the expensive axis and
|
|
||||||
-- row SIZE is nearly free.
|
|
||||||
--
|
|
||||||
-- This is the shape the mode actually claims: data far larger than the cap,
|
|
||||||
-- with only the id map, the indexes and the (never-released) slabs resident.
|
|
||||||
fn heavy_pad() -> Text {
|
|
||||||
let p = "0123456789abcdef0123456789abcdef";
|
|
||||||
let out = "";
|
|
||||||
let i = 0;
|
|
||||||
while i < 20 { out = out .. p; i = i + 1; }
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hread_all(n: Int, r: Int) -> Int {
|
|
||||||
let pad = heavy_pad();
|
|
||||||
let i = 1;
|
|
||||||
while i <= n {
|
|
||||||
insert WideA { k: i, a: "a${i}${pad}", b: "b${i}${pad}", note: "n${i}${pad}" };
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
print("hreadfilled ${n} ${self_rss_kb()}");
|
|
||||||
let h: map<Int, Int> = {};
|
|
||||||
let hits = 0;
|
|
||||||
let t0 = time.ticks();
|
|
||||||
let j = 0;
|
|
||||||
while j < r {
|
|
||||||
let key = 1 + (j * 2654435761) % n;
|
|
||||||
let o0 = time.ticks();
|
|
||||||
for row in from x in WideA where x.k == key take 1 select x {
|
|
||||||
if len(row.a) > 0 { hits = hits + 1; }
|
|
||||||
}
|
|
||||||
hist_add(h, time.ticks() - o0);
|
|
||||||
j = j + 1;
|
|
||||||
}
|
|
||||||
let el = time.ticks() - t0;
|
|
||||||
report("hreadall", r, el, h);
|
|
||||||
print("hreadallrss ${self_rss_kb()} ${hits}");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hread_keys(n: Int, r: Int) -> Int {
|
|
||||||
let pad = heavy_pad();
|
|
||||||
let i = 1;
|
|
||||||
while i <= n {
|
|
||||||
insert WideK { k: i, a: "a${i}${pad}", b: "b${i}${pad}", note: "n${i}${pad}" };
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
print("hreadfilled ${n} ${self_rss_kb()}");
|
|
||||||
let h: map<Int, Int> = {};
|
|
||||||
let hits = 0;
|
|
||||||
let t0 = time.ticks();
|
|
||||||
let j = 0;
|
|
||||||
while j < r {
|
|
||||||
let key = 1 + (j * 2654435761) % n;
|
|
||||||
let o0 = time.ticks();
|
|
||||||
for row in from x in WideK where x.k == key take 1 select x {
|
|
||||||
if len(row.a) > 0 { hits = hits + 1; }
|
|
||||||
}
|
|
||||||
hist_add(h, time.ticks() - o0);
|
|
||||||
j = j + 1;
|
|
||||||
}
|
|
||||||
let el = time.ticks() - t0;
|
|
||||||
report("hreadkeys", r, el, h);
|
|
||||||
print("hreadkeysrss ${self_rss_kb()} ${hits}");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wread_all(n: Int, r: Int) -> Int {
|
|
||||||
let pad = "0123456789abcdef0123456789abcdef";
|
|
||||||
let i = 1;
|
|
||||||
while i <= n {
|
|
||||||
insert WideA { k: i, a: "a${i}${pad}", b: "b${i}${pad}", note: "n${i}${pad}${pad}" };
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
print("wreadfilled ${n} ${self_rss_kb()}");
|
|
||||||
let h: map<Int, Int> = {};
|
|
||||||
let hits = 0;
|
|
||||||
let t0 = time.ticks();
|
|
||||||
let j = 0;
|
|
||||||
while j < r {
|
|
||||||
let key = 1 + (j * 2654435761) % n;
|
|
||||||
let o0 = time.ticks();
|
|
||||||
for row in from x in WideA where x.k == key take 1 select x {
|
|
||||||
if len(row.a) > 0 { hits = hits + 1; }
|
|
||||||
}
|
|
||||||
hist_add(h, time.ticks() - o0);
|
|
||||||
j = j + 1;
|
|
||||||
}
|
|
||||||
let el = time.ticks() - t0;
|
|
||||||
report("wreadall", r, el, h);
|
|
||||||
print("wreadallrss ${self_rss_kb()} ${hits}");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wread_keys(n: Int, r: Int) -> Int {
|
|
||||||
let pad = "0123456789abcdef0123456789abcdef";
|
|
||||||
let i = 1;
|
|
||||||
while i <= n {
|
|
||||||
insert WideK { k: i, a: "a${i}${pad}", b: "b${i}${pad}", note: "n${i}${pad}${pad}" };
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
print("wreadfilled ${n} ${self_rss_kb()}");
|
|
||||||
let h: map<Int, Int> = {};
|
|
||||||
let hits = 0;
|
|
||||||
let t0 = time.ticks();
|
|
||||||
let j = 0;
|
|
||||||
while j < r {
|
|
||||||
let key = 1 + (j * 2654435761) % n;
|
|
||||||
let o0 = time.ticks();
|
|
||||||
for row in from x in WideK where x.k == key take 1 select x {
|
|
||||||
if len(row.a) > 0 { hits = hits + 1; }
|
|
||||||
}
|
|
||||||
hist_add(h, time.ticks() - o0);
|
|
||||||
j = j + 1;
|
|
||||||
}
|
|
||||||
let el = time.ticks() - t0;
|
|
||||||
report("wreadkeys", r, el, h);
|
|
||||||
print("wreadkeysrss ${self_rss_kb()} ${hits}");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn kread_all(n: Int, r: Int) -> Int {
|
|
||||||
let i = 1;
|
|
||||||
while i <= n {
|
|
||||||
insert RowA { k: i, v: item_v(i) };
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
print("kreadfilled ${n} ${self_rss_kb()}");
|
|
||||||
let h: map<Int, Int> = {};
|
|
||||||
let hits = 0;
|
|
||||||
let t0 = time.ticks();
|
|
||||||
let j = 0;
|
|
||||||
while j < r {
|
|
||||||
let key = 1 + (j * 2654435761) % n;
|
|
||||||
let o0 = time.ticks();
|
|
||||||
for row in from x in RowA where x.k == key take 1 select x {
|
|
||||||
if row.v == item_v(key) { hits = hits + 1; }
|
|
||||||
}
|
|
||||||
hist_add(h, time.ticks() - o0);
|
|
||||||
j = j + 1;
|
|
||||||
}
|
|
||||||
let el = time.ticks() - t0;
|
|
||||||
report("kreadall", r, el, h);
|
|
||||||
print("kreadallrss ${self_rss_kb()} ${hits}");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn kread_keys(n: Int, r: Int) -> Int {
|
|
||||||
let i = 1;
|
|
||||||
while i <= n {
|
|
||||||
insert RowK { k: i, v: item_v(i) };
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
print("kreadfilled ${n} ${self_rss_kb()}");
|
|
||||||
let h: map<Int, Int> = {};
|
|
||||||
let hits = 0;
|
|
||||||
let t0 = time.ticks();
|
|
||||||
let j = 0;
|
|
||||||
while j < r {
|
|
||||||
let key = 1 + (j * 2654435761) % n;
|
|
||||||
let o0 = time.ticks();
|
|
||||||
for row in from x in RowK where x.k == key take 1 select x {
|
|
||||||
if row.v == item_v(key) { hits = hits + 1; }
|
|
||||||
}
|
|
||||||
hist_add(h, time.ticks() - o0);
|
|
||||||
j = j + 1;
|
|
||||||
}
|
|
||||||
let el = time.ticks() - t0;
|
|
||||||
report("kreadkeys", r, el, h);
|
|
||||||
print("kreadkeysrss ${self_rss_kb()} ${hits}");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn randread_mode(n: Int, r: Int) -> Int {
|
fn randread_mode(n: Int, r: Int) -> Int {
|
||||||
let bref = insert Bucket { tag: "randread" };
|
let bref = insert Bucket { tag: "randread" };
|
||||||
let i = 1;
|
let i = 1;
|
||||||
|
|
@ -1008,33 +824,6 @@ fn main(args: multi Text) -> Int {
|
||||||
}
|
}
|
||||||
return replayseed_mode(n, mm);
|
return replayseed_mode(n, mm);
|
||||||
}
|
}
|
||||||
if args[0] == "hreadall" or args[0] == "hreadkeys" {
|
|
||||||
if len(args) < 3 { print_err("usage: hreadall|hreadkeys N R"); return 2; }
|
|
||||||
let hn = parse_int(args[1]);
|
|
||||||
let hr = parse_int(args[2]);
|
|
||||||
if hn == nil or hr == nil { print_err("db-bench: N and R must be positive"); return 2; }
|
|
||||||
if args[0] == "hreadall" { return hread_all(hn, hr); }
|
|
||||||
return hread_keys(hn, hr);
|
|
||||||
}
|
|
||||||
if args[0] == "wreadall" or args[0] == "wreadkeys" {
|
|
||||||
if len(args) < 3 { print_err("usage: wreadall|wreadkeys N R"); return 2; }
|
|
||||||
let wn = parse_int(args[1]);
|
|
||||||
let wr = parse_int(args[2]);
|
|
||||||
if wn == nil or wr == nil { print_err("db-bench: N and R must be positive"); return 2; }
|
|
||||||
if args[0] == "wreadall" { return wread_all(wn, wr); }
|
|
||||||
return wread_keys(wn, wr);
|
|
||||||
}
|
|
||||||
if args[0] == "kreadall" or args[0] == "kreadkeys" {
|
|
||||||
if len(args) < 3 { print_err("usage: kreadall|kreadkeys N R"); return 2; }
|
|
||||||
let n = parse_int(args[1]);
|
|
||||||
let rr = parse_int(args[2]);
|
|
||||||
if n == nil or rr == nil {
|
|
||||||
print_err("db-bench: N and R must be positive numbers");
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
if args[0] == "kreadall" { return kread_all(n, rr); }
|
|
||||||
return kread_keys(n, rr);
|
|
||||||
}
|
|
||||||
if args[0] == "randread" {
|
if args[0] == "randread" {
|
||||||
if len(args) < 3 {
|
if len(args) < 3 {
|
||||||
return usage();
|
return usage();
|
||||||
|
|
|
||||||
|
|
@ -57,47 +57,3 @@ class MixJob {
|
||||||
class Flood {
|
class Flood {
|
||||||
n: Int
|
n: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
-- databasev2 2 task 7: the residency A/B. These two are IDENTICAL except for
|
|
||||||
-- the `resident` annotation, so a difference between them is the storage
|
|
||||||
-- mode's doing and nothing else's. Item above carries a `ref` and a second
|
|
||||||
-- index, which would confound the comparison.
|
|
||||||
--
|
|
||||||
-- The question they exist to answer: iteration 1 measured a 273x collapse for
|
|
||||||
-- random reads over a table larger than RAM, on demand-paged anonymous memory.
|
|
||||||
-- `resident: keys` reads rows back with pread through the page cache instead.
|
|
||||||
-- If that is not materially better than 273x, the mode buys nothing the kernel
|
|
||||||
-- was not already doing.
|
|
||||||
@table(name: "rowsall", index: [k], durable: true, resident: all)
|
|
||||||
class RowA {
|
|
||||||
k: Int
|
|
||||||
v: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
@table(name: "rowskeys", index: [k], durable: true, resident: keys)
|
|
||||||
class RowK {
|
|
||||||
k: Int
|
|
||||||
v: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
-- databasev2 2 task 7, second A/B: the WIDE shape. The Int-only pair above
|
|
||||||
-- cannot show what keys-residency is for — dropping a payload frees each
|
|
||||||
-- field's VALUE, and an Int's value IS its inline slot word, so nothing is
|
|
||||||
-- freed and the slab stays allocated either way. A row with Text columns
|
|
||||||
-- drags separate db_text allocations that dropping genuinely releases, which
|
|
||||||
-- is the only shape where the mode can pay for itself.
|
|
||||||
@table(name: "wideall", index: [k], durable: true, resident: all)
|
|
||||||
class WideA {
|
|
||||||
k: Int
|
|
||||||
a: Text
|
|
||||||
b: Text
|
|
||||||
note: Text
|
|
||||||
}
|
|
||||||
|
|
||||||
@table(name: "widekeys", index: [k], durable: true, resident: keys)
|
|
||||||
class WideK {
|
|
||||||
k: Int
|
|
||||||
a: Text
|
|
||||||
b: Text
|
|
||||||
note: Text
|
|
||||||
}
|
|
||||||
|
|
|
||||||
152
docs/examples/residency-bench/main.wo
Normal file
152
docs/examples/residency-bench/main.wo
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
-- residency-bench — databasev2 2 task 7.
|
||||||
|
--
|
||||||
|
-- Two tables identical except the `resident` annotation (see types.wo), the
|
||||||
|
-- same fill, the same Weyl key order, the same read count. Run under a cgroup
|
||||||
|
-- memory cap by scripts/db-bench.py's `residency` leg:
|
||||||
|
--
|
||||||
|
-- control : a cap that binds neither mode -> what the mode COSTS
|
||||||
|
-- over-cap: a cap between the two resident sets -> what the mode BUYS
|
||||||
|
--
|
||||||
|
-- Prints the same shape db-bench's own legs do, so the harness parses it with
|
||||||
|
-- the same helpers: `<op> <n> <ops/sec> <p50> <p99>` plus an rss/hits line.
|
||||||
|
use fs
|
||||||
|
use time
|
||||||
|
|
||||||
|
fn hist_add(mut h: map<Int, Int>, us: Int) {
|
||||||
|
let b = us;
|
||||||
|
if b < 0 {
|
||||||
|
b = 0;
|
||||||
|
}
|
||||||
|
if b > 20000 {
|
||||||
|
b = 20000;
|
||||||
|
}
|
||||||
|
if has(h, b) {
|
||||||
|
set(h, b, get(h, b) + 1);
|
||||||
|
} else {
|
||||||
|
set(h, b, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hist_pct(h: map<Int, Int>, total: Int, pct: Int) -> Int {
|
||||||
|
let target = total * pct / 100;
|
||||||
|
if target < 1 {
|
||||||
|
target = 1;
|
||||||
|
}
|
||||||
|
let seen = 0;
|
||||||
|
let b = 0;
|
||||||
|
while b <= 20000 {
|
||||||
|
if has(h, b) {
|
||||||
|
seen = seen + get(h, b);
|
||||||
|
if seen >= target {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b = b + 1;
|
||||||
|
}
|
||||||
|
return 20000;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn report(op: Text, n: Int, total_us: Int, h: map<Int, Int>) {
|
||||||
|
let us = total_us;
|
||||||
|
if us < 1 {
|
||||||
|
us = 1;
|
||||||
|
}
|
||||||
|
let rate = n * 1000000 / us;
|
||||||
|
print("${op} ${n} ${rate} ${hist_pct(h, n, 50)} ${hist_pct(h, n, 99)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn self_rss_kb() -> Int {
|
||||||
|
let st = try fs.read_all("/proc/self/status", 16384) catch (e) "";
|
||||||
|
let i = index_of(st, "VmRSS:");
|
||||||
|
if i < 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let rest = substr(st, i + 6, 24);
|
||||||
|
let n = 0;
|
||||||
|
let j = 0;
|
||||||
|
while j < len(rest) {
|
||||||
|
let c = byte_at(rest, j);
|
||||||
|
if c >= 48 and c <= 57 {
|
||||||
|
n = n * 10 + (c - 48);
|
||||||
|
} else {
|
||||||
|
if n > 0 {
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
j = j + 1;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wide_pad() -> Text {
|
||||||
|
return "0123456789abcdef0123456789abcdef";
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wread_all(n: Int, r: Int) -> Int {
|
||||||
|
let pad = wide_pad();
|
||||||
|
let i = 1;
|
||||||
|
while i <= n {
|
||||||
|
insert WideA { k: i, a: "a${i}${pad}", b: "b${i}${pad}", note: "n${i}${pad}${pad}" };
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
print("wreadfilled ${n} ${self_rss_kb()}");
|
||||||
|
let h: map<Int, Int> = {};
|
||||||
|
let hits = 0;
|
||||||
|
let t0 = time.ticks();
|
||||||
|
let j = 0;
|
||||||
|
while j < r {
|
||||||
|
let key = 1 + (j * 2654435761) % n;
|
||||||
|
let o0 = time.ticks();
|
||||||
|
for row in from x in WideA where x.k == key take 1 select x {
|
||||||
|
if len(row.a) > 0 { hits = hits + 1; }
|
||||||
|
}
|
||||||
|
hist_add(h, time.ticks() - o0);
|
||||||
|
j = j + 1;
|
||||||
|
}
|
||||||
|
report("wreadall", r, time.ticks() - t0, h);
|
||||||
|
print("wreadallrss ${self_rss_kb()} ${hits}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wread_keys(n: Int, r: Int) -> Int {
|
||||||
|
let pad = wide_pad();
|
||||||
|
let i = 1;
|
||||||
|
while i <= n {
|
||||||
|
insert WideK { k: i, a: "a${i}${pad}", b: "b${i}${pad}", note: "n${i}${pad}${pad}" };
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
print("wreadfilled ${n} ${self_rss_kb()}");
|
||||||
|
let h: map<Int, Int> = {};
|
||||||
|
let hits = 0;
|
||||||
|
let t0 = time.ticks();
|
||||||
|
let j = 0;
|
||||||
|
while j < r {
|
||||||
|
let key = 1 + (j * 2654435761) % n;
|
||||||
|
let o0 = time.ticks();
|
||||||
|
for row in from x in WideK where x.k == key take 1 select x {
|
||||||
|
if len(row.a) > 0 { hits = hits + 1; }
|
||||||
|
}
|
||||||
|
hist_add(h, time.ticks() - o0);
|
||||||
|
j = j + 1;
|
||||||
|
}
|
||||||
|
report("wreadkeys", r, time.ticks() - t0, h);
|
||||||
|
print("wreadkeysrss ${self_rss_kb()} ${hits}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main(args: multi Text) -> Int {
|
||||||
|
if len(args) < 3 {
|
||||||
|
print_err("usage: residency-bench wreadall|wreadkeys N R");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
let n = parse_int(args[1]);
|
||||||
|
let r = parse_int(args[2]);
|
||||||
|
if n == nil or r == nil {
|
||||||
|
print_err("residency-bench: N and R must be positive numbers");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if args[0] == "wreadall" { return wread_all(n, r); }
|
||||||
|
if args[0] == "wreadkeys" { return wread_keys(n, r); }
|
||||||
|
print_err("residency-bench: unknown mode ${args[0]}");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
34
docs/examples/residency-bench/types.wo
Normal file
34
docs/examples/residency-bench/types.wo
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
-- databasev2 2 task 7: the residency A/B.
|
||||||
|
--
|
||||||
|
-- These two tables are IDENTICAL except for the `resident` annotation, so any
|
||||||
|
-- difference between them is the storage mode's doing and nothing else's.
|
||||||
|
--
|
||||||
|
-- WHY THIS IS ITS OWN PROGRAM rather than a mode inside db-bench: declaring a
|
||||||
|
-- `resident: keys` table is a WHOLE-PROGRAM constraint. Its rows live only in
|
||||||
|
-- the write-ahead log, so the runtime refuses to start without WO_DATA — and
|
||||||
|
-- that refusal fires for every mode in the module, including ones that never
|
||||||
|
-- touch the table. Putting these classes in db-bench's shared module made its
|
||||||
|
-- `growth`, `ceiling` and `randread` legs, which deliberately run WITHOUT
|
||||||
|
-- WO_DATA, refuse to start.
|
||||||
|
--
|
||||||
|
-- The shape is WIDE on purpose. An Int-only pair shows the two modes as
|
||||||
|
-- indistinguishable, and that is structural: dropping a payload frees each
|
||||||
|
-- field's VALUE, and an Int's value IS its inline slot word, so nothing is
|
||||||
|
-- freed and the slab stays allocated either way. Only rows with Text columns —
|
||||||
|
-- separate allocations that dropping genuinely releases — can show what the
|
||||||
|
-- mode is for.
|
||||||
|
@table(name: "wideall", index: [k], durable: true, resident: all)
|
||||||
|
class WideA {
|
||||||
|
k: Int
|
||||||
|
a: Text
|
||||||
|
b: Text
|
||||||
|
note: Text
|
||||||
|
}
|
||||||
|
|
||||||
|
@table(name: "widekeys", index: [k], durable: true, resident: keys)
|
||||||
|
class WideK {
|
||||||
|
k: Int
|
||||||
|
a: Text
|
||||||
|
b: Text
|
||||||
|
note: Text
|
||||||
|
}
|
||||||
6
docs/examples/residency-bench/wo.toml
Normal file
6
docs/examples/residency-bench/wo.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
name = "residency-bench"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "databasev2 2 task 7: resident: all vs resident: keys, identical tables, one annotation apart"
|
||||||
|
|
||||||
|
[runtime]
|
||||||
|
wo = ">= 0.1"
|
||||||
|
|
@ -66,7 +66,7 @@ declared per-table policy. Durability is untouched and unconditional.
|
||||||
| 5c | shared borrow/release accessor, then id→offset storage | ✅ `2e347de` (accessor, pure refactor, `db-bench --quick` 85/0), `18ce4d5` (offset storage), `f9c36ef` (insert + boot wiring) |
|
| 5c | shared borrow/release accessor, then id→offset storage | ✅ `2e347de` (accessor, pure refactor, `db-bench --quick` 85/0), `18ce4d5` (offset storage), `f9c36ef` (insert + boot wiring) |
|
||||||
| 5d | rewire the readers: remaining `wo_row_ptr` sites, slab scans, FK restrict, `@unique` across the boundary | ✅ `11a92df` (db.c), + this commit (table.c, wal.c, compaction). Updates **refused**, not rewired — see below |
|
| 5d | rewire the readers: remaining `wo_row_ptr` sites, slab scans, FK restrict, `@unique` across the boundary | ✅ `11a92df` (db.c), + this commit (table.c, wal.c, compaction). Updates **refused**, not rewired — see below |
|
||||||
| 6 | the two runtime refusals (no-`WO_DATA`, the byte budget) | ⬜ |
|
| 6 | the two runtime refusals (no-`WO_DATA`, the byte budget) | ⬜ |
|
||||||
| 7 | measure, gate, document, close out | 🔄 measured 2026-08-30 (below); gate + closeout outstanding |
|
| 7 | measure, gate, document, close out | ✅ measured, gated and documented 2026-08-30 |
|
||||||
|
|
||||||
**The `durable` half is complete and usable.** A volatile table is a full table
|
**The `durable` half is complete and usable.** A volatile table is a full table
|
||||||
in-process — same indexes, same `@unique`, same FK restrict, same query surface
|
in-process — same indexes, same `@unique`, same FK restrict, same query surface
|
||||||
|
|
@ -274,9 +274,49 @@ over-capacity table fast — under a hard memory cap it is within 1.5× of simpl
|
||||||
letting the kernel swap. The honest guidance is "use it to fit more, not to go
|
letting the kernel swap. The honest guidance is "use it to fit more, not to go
|
||||||
faster", and the docs should say so.
|
faster", and the docs should say so.
|
||||||
|
|
||||||
**Still outstanding for task 7:** wire these legs into `scripts/db-bench.py`
|
**Gated 2026-08-30.** `scripts/db-bench.py` grew a `residency` leg driving
|
||||||
with tolerances and a baseline entry, and re-measure the `resident: all` read
|
`docs/examples/residency-bench` — its own program, because declaring a
|
||||||
baseline to confirm no cost for a feature not used.
|
`resident: keys` table is a WHOLE-PROGRAM constraint: the runtime refuses to
|
||||||
|
start without `WO_DATA`, for every mode in the module. Putting those classes in
|
||||||
|
db-bench's shared types made `growth`, `ceiling` and `randread` — which
|
||||||
|
deliberately run without `WO_DATA` — refuse to start. That regression was caught
|
||||||
|
by running the leg, not by reading it.
|
||||||
|
|
||||||
|
What is gated, and what deliberately is not, follows `randread`'s existing
|
||||||
|
split: the absolute ops/sec under a cap is swap and disk I/O and belongs to the
|
||||||
|
box, so it is recorded and waived; the RATIOS are the engine's property.
|
||||||
|
|
||||||
|
| metric | baseline | floor | tolerance |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `residency.rss_ratio` | 2.55 | 2.0 | 10% |
|
||||||
|
| `residency.overcap_vs_swap_x` | 1.53 | 1.0 | 100% |
|
||||||
|
| `residency.in_ram_cost_x` | 4.23 | 8.0 (ceiling) | 50% |
|
||||||
|
| `residency.all_collapse_x` | 105.4 | 2.0 | 100% |
|
||||||
|
|
||||||
|
`rss_ratio` carries the tight tolerance because footprint is structural — the
|
||||||
|
same class of number as `bytes_per_row`. The two throughput ratios are guarded
|
||||||
|
by their FLOORS rather than their bands, which is this harness's established
|
||||||
|
answer to a metric whose absolute value belongs to the disk. `all_collapse_x`
|
||||||
|
exists only to assert the cap actually binds; a leg whose "over-cap" half is not
|
||||||
|
over cap silently measures nothing, which is exactly what the first run of this
|
||||||
|
leg did.
|
||||||
|
|
||||||
|
Verified by feeding the gate a breaching run: `rss_ratio` 1.4,
|
||||||
|
`overcap_vs_swap_x` 0.6 and `in_ram_cost_x` 12.0 are all rejected.
|
||||||
|
- **Given** the `resident: all` read baseline, **when** re-measured, **then**
|
||||||
|
inside tolerance — no cost for a feature not used. ✅ Verified as a
|
||||||
|
by-product of the residency leg: `resident: all` is unchanged at 1 354 554
|
||||||
|
reads/sec uncapped, and every other db-bench leg still runs, which the
|
||||||
|
keys-resident classes had briefly broken by forcing `WO_DATA` module-wide.
|
||||||
|
- **Given** a `resident: keys` table larger than RAM, **when** read randomly,
|
||||||
|
**then** its read cost is measured against the resident baseline on its own
|
||||||
|
read path. ✅ Measured 2026-08-30 and the answer is qualified: **1.53×**
|
||||||
|
faster than letting the kernel swap under a cap that binds one and not the
|
||||||
|
other — real, but nowhere near iteration 1's 273× swap figure would suggest,
|
||||||
|
because cgroup limits charge the page cache, so moving rows to a file does
|
||||||
|
not escape a container memory limit. The unambiguous win is footprint:
|
||||||
|
**2.55×** smaller resident set. Full method, numbers and the failed first
|
||||||
|
attempt above; gated by `residency.*`.
|
||||||
|
|
||||||
Outstanding:
|
Outstanding:
|
||||||
|
|
||||||
|
|
@ -285,17 +325,6 @@ Outstanding:
|
||||||
every write)*
|
every write)*
|
||||||
- **Given** the resident footprint crossing the budget, **when** it does,
|
- **Given** the resident footprint crossing the budget, **when** it does,
|
||||||
**then** a refusal naming the table and the annotation. *(task 6)*
|
**then** a refusal naming the table and the annotation. *(task 6)*
|
||||||
- **Given** the `resident: all` read baseline, **when** re-measured, **then**
|
|
||||||
inside tolerance — no cost for a feature not used. *(task 7)*
|
|
||||||
- **Given** a `resident: keys` table larger than RAM, **when** read randomly,
|
|
||||||
**then** its read cost is **measured against the resident baseline on its own
|
|
||||||
read path**, not inherited from databasev2 1's swap figure. *(task 7)* — that
|
|
||||||
figure is **273×** for demand-paged anonymous memory
|
|
||||||
([1](01-ram-ceiling-measurement.md)); `pread` through the page cache should do
|
|
||||||
better, and the whole value of `resident: keys` rests on how much better. If it
|
|
||||||
is not materially better than swapping, the design buys nothing that the
|
|
||||||
kernel was not already doing.
|
|
||||||
|
|
||||||
## Out Of Scope
|
## Out Of Scope
|
||||||
|
|
||||||
- **Checkpoint and compaction** — [3](03-wal-checkpoint.md). Boot rebuilds the
|
- **Checkpoint and compaction** — [3](03-wal-checkpoint.md). Boot rebuilds the
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,16 @@ Exit 0 = campaign green; 1 = gate breach or a durability leg failed.
|
||||||
Plan deviation, disclosed: one python driver instead of bash+python —
|
Plan deviation, disclosed: one python driver instead of bash+python —
|
||||||
the live stdout sampling and JSON assembly are the whole job.
|
the live stdout sampling and JSON assembly are the whole job.
|
||||||
"""
|
"""
|
||||||
import json, os, re, subprocess, sys, time, random, shutil
|
import json, os, re, subprocess, sys, tempfile, time, random, shutil
|
||||||
|
|
||||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
BIN = os.path.join(ROOT, "docs/examples/db-bench/target/db-bench")
|
BIN = os.path.join(ROOT, "docs/examples/db-bench/target/db-bench")
|
||||||
|
# databasev2 2 task 7. Its own program, not a mode in db-bench: declaring a
|
||||||
|
# `resident: keys` table is a WHOLE-PROGRAM constraint — the runtime refuses to
|
||||||
|
# start without WO_DATA, for every mode in the module. Putting those classes in
|
||||||
|
# db-bench's shared types made growth/ceiling/randread, which deliberately run
|
||||||
|
# WITHOUT WO_DATA, refuse to start.
|
||||||
|
RESID_BIN = os.path.join(ROOT, "docs/examples/residency-bench/target/residency-bench")
|
||||||
WOC = os.path.join(ROOT, "compiler/_build/default/bin/woc")
|
WOC = os.path.join(ROOT, "compiler/_build/default/bin/woc")
|
||||||
WOVM = os.path.join(ROOT, "runtime/wovm")
|
WOVM = os.path.join(ROOT, "runtime/wovm")
|
||||||
BASELINE = os.path.join(ROOT, "bench/baseline.json")
|
BASELINE = os.path.join(ROOT, "bench/baseline.json")
|
||||||
|
|
@ -60,6 +66,10 @@ def build():
|
||||||
"-o", BIN, "--runtime", WOVM], capture_output=True, text=True)
|
"-o", BIN, "--runtime", WOVM], capture_output=True, text=True)
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
bad("build", r.stderr.strip()[:200]); sys.exit(1)
|
bad("build", r.stderr.strip()[:200]); sys.exit(1)
|
||||||
|
r = subprocess.run([WOC, "build", os.path.join(ROOT, "docs/examples/residency-bench"),
|
||||||
|
"-o", RESID_BIN, "--runtime", WOVM], capture_output=True, text=True)
|
||||||
|
if r.returncode != 0:
|
||||||
|
bad("build residency-bench", r.stderr.strip()[:200]); sys.exit(1)
|
||||||
ok("builds")
|
ok("builds")
|
||||||
|
|
||||||
def run(args, env_extra, timeout, sample_after=None):
|
def run(args, env_extra, timeout, sample_after=None):
|
||||||
|
|
@ -303,6 +313,26 @@ def tolerance_for(key):
|
||||||
if key.startswith("growth."): return 100
|
if key.startswith("growth."): return 100
|
||||||
if key.startswith("ceiling."): return 100
|
if key.startswith("ceiling."): return 100
|
||||||
if key.startswith("randread."): return 100
|
if key.startswith("randread."): return 100
|
||||||
|
# databasev2 2 task 7. Same split randread makes, for the same reason: the
|
||||||
|
# absolute ops/sec under a cap is swap and disk I/O and belongs to the box,
|
||||||
|
# so it is recorded and waived. The RATIOS are the engine's property.
|
||||||
|
#
|
||||||
|
# rss_ratio is the headline claim — keys must hold the same rows in a
|
||||||
|
# materially smaller resident set or the mode has no purpose — and it is
|
||||||
|
# STRUCTURAL: 87.5 MiB against 34.4 MiB, reproducible, the same class of
|
||||||
|
# number as bytes_per_row. It gets the same tight tolerance, and residency()
|
||||||
|
# additionally hard-fails below 2.0x regardless of drift.
|
||||||
|
if key == "residency.rss_ratio": return 10
|
||||||
|
# in_ram_cost is a throughput ratio between two cached runs: stable in
|
||||||
|
# shape (a pread and a fold against a pointer dereference) but it moves
|
||||||
|
# with page-cache weather, so it is gated loosely rather than waived.
|
||||||
|
if key == "residency.in_ram_cost_x": return 50
|
||||||
|
# overcap_vs_swap compares two I/O-bound runs, so BOTH halves are the box's.
|
||||||
|
# The ratio is still worth recording — it is the answer to the question the
|
||||||
|
# iteration was written to ask — but residency() guards the direction of it
|
||||||
|
# (keys must not be slower than swapping) rather than its magnitude.
|
||||||
|
if key == "residency.overcap_vs_swap_x": return 100
|
||||||
|
if key.startswith("residency."): return 100
|
||||||
if key.startswith("replay."): return 100
|
if key.startswith("replay."): return 100
|
||||||
# databasev2 4: batch SHAPE follows arrival timing, so gating it tightly
|
# databasev2 4: batch SHAPE follows arrival timing, so gating it tightly
|
||||||
# would gate the scheduler — what must hold is that the mean exceeds one
|
# would gate the scheduler — what must hold is that the mean exceeds one
|
||||||
|
|
@ -558,6 +588,130 @@ def ceiling(metrics):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# databasev2 2 task 7: does `resident: keys` beat letting the kernel swap?
|
||||||
|
RESID_N = 40000 if QUICK else 200000
|
||||||
|
RESID_R = 10000 if QUICK else 40000
|
||||||
|
RESID_FIT_MB = 256 # control: neither mode is under pressure
|
||||||
|
# Between the two resident sets, so `all` pages and `keys` does not. This MUST
|
||||||
|
# scale with N: at QUICK's 40k rows `all` needs only ~17 MiB, so a 48 MiB cap
|
||||||
|
# binds neither mode and the comparison silently becomes "keys is slower when
|
||||||
|
# nothing is under pressure" — which is true, and not what this leg asks.
|
||||||
|
RESID_CAP_MB = 10 if QUICK else 48
|
||||||
|
|
||||||
|
|
||||||
|
def parse_resid(lines, op):
|
||||||
|
"""`<op> <n> <ops/sec> <p50> <p99>`, plus the mode's own rss/hits line."""
|
||||||
|
ops = p50 = p99 = rss = hits = filled = None
|
||||||
|
for l in lines:
|
||||||
|
f = l.split()
|
||||||
|
if not f:
|
||||||
|
continue
|
||||||
|
if f[0] == op and len(f) == 5:
|
||||||
|
ops, p50, p99 = int(f[2]), int(f[3]), int(f[4])
|
||||||
|
elif f[0] == op + "rss" and len(f) == 3:
|
||||||
|
rss, hits = int(f[1]), int(f[2])
|
||||||
|
elif f[0] == "wreadfilled" and len(f) == 3:
|
||||||
|
filled = int(f[2])
|
||||||
|
return ops, p50, p99, rss, hits, filled
|
||||||
|
|
||||||
|
|
||||||
|
def residency(metrics):
|
||||||
|
"""`resident: keys` against `resident: all`, on tables IDENTICAL except the
|
||||||
|
annotation, so a difference is the storage mode's doing and nothing else's.
|
||||||
|
|
||||||
|
Measured 2026-08-30 on the WIDE shape deliberately. An Int-only pair shows
|
||||||
|
the two modes as indistinguishable, and that is structural rather than
|
||||||
|
surprising: dropping a payload frees each field's VALUE, and an Int's value
|
||||||
|
IS its inline slot word, so nothing is freed and the slab stays allocated
|
||||||
|
either way. A benchmark built on that shape would condemn the feature for a
|
||||||
|
reason that has nothing to do with the feature.
|
||||||
|
|
||||||
|
WHAT IS GATED, and what deliberately is not. The RATIOS are the engine's
|
||||||
|
property and get real tolerances; the absolute ops/sec under a cap is swap
|
||||||
|
and disk I/O, so it belongs to the box and is recorded but waived. This is
|
||||||
|
the same split randread() already makes for the same reason.
|
||||||
|
|
||||||
|
The headline claim is rss_ratio: keys must hold the same rows in a
|
||||||
|
materially smaller resident set, or the mode has no purpose. The measured
|
||||||
|
figure was 2.55x (34.4 MiB against 87.5 MiB)."""
|
||||||
|
if cap_wrapper(RESID_FIT_MB, 0) is None:
|
||||||
|
ok("residency: SKIPPED -- no rootless cgroup v2 memory cap on this host")
|
||||||
|
return
|
||||||
|
res = {}
|
||||||
|
for mode, op in (("all", "wreadall"), ("keys", "wreadkeys")):
|
||||||
|
for legname, cap_mb, swap_mb in (("fit", RESID_FIT_MB, 0),
|
||||||
|
("cap", RESID_CAP_MB, 512)):
|
||||||
|
w = cap_wrapper(cap_mb, swap_mb)
|
||||||
|
data = tempfile.mkdtemp(prefix="resid-")
|
||||||
|
env = dict(os.environ)
|
||||||
|
env["WO_DATA"] = data # a keys-resident table cannot run without one
|
||||||
|
env["WO_SHARDS"] = "1"
|
||||||
|
pr = subprocess.run(w + [RESID_BIN, op, str(RESID_N), str(RESID_R)],
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||||
|
text=True, env=env, timeout=1800)
|
||||||
|
shutil.rmtree(data, ignore_errors=True)
|
||||||
|
lines = pr.stdout.splitlines()
|
||||||
|
ops, p50, p99, rss, hits, filled = parse_resid(lines, op)
|
||||||
|
key = f"residency.{mode}.{legname}"
|
||||||
|
if pr.returncode != 0 or ops is None:
|
||||||
|
bad(f"{key}: run failed",
|
||||||
|
f"rc={pr.returncode} {(lines[-1:] or ['no output'])[0][:120]}")
|
||||||
|
return
|
||||||
|
if hits != RESID_R:
|
||||||
|
# a comparison over reads that did not resolve measures nothing
|
||||||
|
bad(f"{key}: only {hits}/{RESID_R} reads resolved", "keys must all exist")
|
||||||
|
return
|
||||||
|
metrics[f"{key}.ops_sec"] = ops
|
||||||
|
metrics[f"{key}.read_p50us"] = p50
|
||||||
|
metrics[f"{key}.read_p99us"] = p99
|
||||||
|
metrics[f"{key}.filled_rss_kb"] = filled
|
||||||
|
res[f"{mode}.{legname}"] = (ops, filled)
|
||||||
|
ok(f"{key}: {ops} reads/sec, p50 {p50}us p99 {p99}us, {filled} KiB after fill")
|
||||||
|
|
||||||
|
all_rss = res["all.fit"][1]
|
||||||
|
keys_rss = res["keys.fit"][1]
|
||||||
|
rss_ratio = round(all_rss / max(keys_rss, 1), 2)
|
||||||
|
metrics["residency.rss_ratio"] = rss_ratio
|
||||||
|
|
||||||
|
# what the mode costs when memory is NOT tight: a pread and a fold per row
|
||||||
|
# against a pointer dereference
|
||||||
|
in_ram_cost = round(res["all.fit"][0] / max(res["keys.fit"][0], 1), 2)
|
||||||
|
metrics["residency.in_ram_cost_x"] = in_ram_cost
|
||||||
|
|
||||||
|
# the question the iteration was written to answer: under a cap that binds
|
||||||
|
# `all` and not `keys`, is keys actually better than swapping?
|
||||||
|
vs_swap = round(res["keys.cap"][0] / max(res["all.cap"][0], 1), 2)
|
||||||
|
metrics["residency.overcap_vs_swap_x"] = vs_swap
|
||||||
|
|
||||||
|
if rss_ratio < 2.0:
|
||||||
|
bad("residency: keys saves less than 2x RSS",
|
||||||
|
f"{all_rss} KiB vs {keys_rss} KiB = {rss_ratio}x -- the mode's whole purpose")
|
||||||
|
else:
|
||||||
|
ok(f"residency: keys holds the same rows in {rss_ratio}x less RSS "
|
||||||
|
f"({all_rss} -> {keys_rss} KiB)")
|
||||||
|
|
||||||
|
# The cap must actually BIND the resident half, or the comparison is
|
||||||
|
# meaningless. randread learned this the same way; assert it rather than
|
||||||
|
# trusting the constants to stay right as N changes.
|
||||||
|
all_collapse = res["all.fit"][0] / max(res["all.cap"][0], 1)
|
||||||
|
metrics["residency.all_collapse_x"] = round(all_collapse, 2)
|
||||||
|
if all_collapse < 2.0:
|
||||||
|
bad("residency: the cap did not bind `resident: all`",
|
||||||
|
f"{res['all.fit'][0]} -> {res['all.cap'][0]} reads/sec is only "
|
||||||
|
f"{all_collapse:.2f}x; {RESID_N} rows fit under {RESID_CAP_MB} MiB, resize the leg")
|
||||||
|
return
|
||||||
|
|
||||||
|
if vs_swap < 1.0:
|
||||||
|
bad("residency: keys is SLOWER than letting the kernel swap",
|
||||||
|
f"{res['keys.cap'][0]} vs {res['all.cap'][0]} reads/sec under a "
|
||||||
|
f"{RESID_CAP_MB} MiB cap -- the mode buys nothing here")
|
||||||
|
else:
|
||||||
|
ok(f"residency: under a {RESID_CAP_MB} MiB cap keys is {vs_swap}x swapping "
|
||||||
|
f"({res['keys.cap'][0]} vs {res['all.cap'][0]} reads/sec)")
|
||||||
|
|
||||||
|
ok(f"residency: costs {in_ram_cost}x read throughput when memory is not tight")
|
||||||
|
|
||||||
|
|
||||||
def parse_randread(lines):
|
def parse_randread(lines):
|
||||||
"""ops/sec, p50, p99, resolved-read count and post-fill RSS from the
|
"""ops/sec, p50, p99, resolved-read count and post-fill RSS from the
|
||||||
sample's own randread lines. `randreadfilled` also starts with "randread",
|
sample's own randread lines. `randreadfilled` also starts with "randread",
|
||||||
|
|
@ -847,6 +1001,7 @@ def main():
|
||||||
growth(metrics)
|
growth(metrics)
|
||||||
ceiling(metrics)
|
ceiling(metrics)
|
||||||
randread(metrics)
|
randread(metrics)
|
||||||
|
residency(metrics)
|
||||||
replay(metrics)
|
replay(metrics)
|
||||||
checkpoint_leg(metrics)
|
checkpoint_leg(metrics)
|
||||||
os.makedirs(RESULTS_DIR, exist_ok=True)
|
os.makedirs(RESULTS_DIR, exist_ok=True)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue