code wiki / (root) / nx_commitbench.nx

nx_commitbench.nx source

↩ module page · 184 lines · 7892 B

1// nx_commitbench.nx -- IS ss_commit_deferred WORTH ADOPTING? MEASURE BEFORE MIGRATING. 2// 3// WHY. ss_commit fsyncs the segment AND the directory on every call, so a logical transaction that 4// appends N rows pays N durability barriers. ss_commit_deferred exists to make that ONE barrier -- 5// it has existed since 2026-08-01, and as of 2026-08-07 NOTHING CALLS IT (debt 1786111613). The 6// obvious move is to go adopt it everywhere. The disciplined move is to find out what it buys first: 7// an unmeasured migration across a shared store is a lot of risk for a number nobody has. 8// 9// ★CORRECTNESS IS MEASURED ALONGSIDE SPEED, NOT AFTER IT. Both arms are counted for surviving keys, 10// because a faster arm that loses rows is not faster, it is broken -- and ss_commit_deferred's own 11// header states the honest bound it trades: after it returns the data IS VISIBLE (segment written, 12// manifest renamed), only DURABILITY ACROSS POWER LOSS is deferred to the caller's ss_sync_now. So 13// the row counts MUST match; if they ever do not, the deferred path is not what it claims. 14// 15// ⚠WHAT THIS CANNOT MEASURE, STATED SO THE NUMBER IS NOT OVERSOLD: it cannot measure the crash 16// window. Arm B is exposed between its last deferred commit and its ss_sync_now, and no benchmark on 17// a running host can price that. The speed number is real; the risk it buys is a judgement call that 18// belongs to whoever adopts it, per-caller. 19// usage: nx_commitbench [n] default 40 commits per arm 20// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 21import "nx_seg_store.nx" 22import "nx_syscalls.nx" 23 24const CB_DEFAULT_N: i64 = 40 25const CB_CAP: i64 = 4096 26const CB_KEYCAP: i64 = 64 27const CB_KIND_LIVE: i64 = 1 28const CB_ZERO: i64 = 48 29const CB_NINE: i64 = 57 30const CB_B10: i64 = 10 31const CB_ROUNDS: i64 = 5 // interleaved A/B rounds; the reported figure is their MEDIAN ratio 32 33func cb_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 34func cb_n(v: i64) -> i64 { 35 if v == 0 { cb_w("0" as *u8); return 0 } 36 var x: i64 = v 37 if x < 0 { cb_w("-" as *u8); x = 0 - x } 38 let b: *u8 = sys_mmap(32) 39 var i: i64 = 0 40 while x > 0 { b[i] = ((x % CB_B10) + CB_ZERO) as u8; x = x / CB_B10; i = i + 1 } 41 while i > 0 { i = i - 1; sys_write(1, ((b as i64) + i) as *u8, 1) } 42 return 0 43} 44func cb_atoi(s: *u8) -> i64 { 45 var v: i64 = 0 46 var i: i64 = 0 47 var go: i64 = 1 48 while go == 1 { 49 let c: i64 = s[i] as i64 50 if c >= CB_ZERO { if c <= CB_NINE { v = v * CB_B10 + (c - CB_ZERO); i = i + 1 } else { go = 0 } } else { go = 0 } 51 } 52 return v 53} 54 55func cb_key(out: *u8, i: i64) -> i64 { 56 var o: i64 = ss_cat(out, 0, "k" as *u8) 57 o = ss_catn(out, o, i) 58 out[o] = 0 as u8 59 return o 60} 61 62// truncate the live manifest -> the plane starts empty; segment files stay on disk (rule 13) 63func cb_reset(prefix: *u8) -> i64 { 64 let mf: *u8 = sys_mmap(512) 65 var o: i64 = ss_cat(mf, 0, prefix) 66 o = ss_cat(mf, o, "manifest.txt" as *u8) 67 mf[o] = 0 as u8 68 let e: *u8 = sys_mmap(16) 69 ss_writefile(mf, e, 0) 70 return 0 71} 72 73// deferred == 1 -> ss_commit_deferred x N then ONE ss_sync_now; else ss_commit x N. 74// Returns elapsed ms. 75func cb_arm(prefix: *u8, n: i64, deferred: i64) -> i64 { 76 cb_reset(prefix) 77 let key: *u8 = sys_mmap(CB_KEYCAP) 78 let t0: i64 = sys_now_ms() 79 var i: i64 = 0 80 while i < n { 81 cb_key(key, i) 82 let w: *i64 = ss_begin_cap(CB_CAP) 83 ss_add(w, CB_KIND_LIVE, key, "v" as *u8, 1) 84 let segid: i64 = ss_next_segid(prefix) 85 if deferred == 1 { ss_commit_deferred(prefix, w, segid) } 86 if deferred == 0 { ss_commit(prefix, w, segid) } 87 i = i + 1 88 } 89 // THE BARRIER THE BATCH OWES -- one, at the end, instead of N 90 if deferred == 1 { ss_sync_now(prefix) } 91 return sys_now_ms() - t0 92} 93 94func cb_survivors(prefix: *u8, n: i64) -> i64 { 95 let pp: *i64 = sys_mmap(16) as *i64 96 let lp: *i64 = sys_mmap(16) as *i64 97 let key: *u8 = sys_mmap(CB_KEYCAP) 98 var found: i64 = 0 99 var i: i64 = 0 100 while i < n { 101 cb_key(key, i) 102 if ss_get(prefix, key, pp, lp) == 1 { found = found + 1 } 103 i = i + 1 104 } 105 return found 106} 107 108func main(argc: i64, argv: *i64) -> i64 { 109 var n: i64 = CB_DEFAULT_N 110 if argc >= 2 { n = cb_atoi(argv[1] as *u8) } 111 if n <= 0 { n = CB_DEFAULT_N } 112 113 cb_w("nx_commitbench -- what does ss_commit_deferred actually buy?\n" as *u8) 114 cb_w(" commits per arm: " as *u8); cb_n(n); cb_w("\n\n" as *u8) 115 116 let pa: *u8 = "knowledge/store/commitbench-durable-" as *u8 117 let pb: *u8 = "knowledge/store/commitbench-deferred-" as *u8 118 119 // >>INTERLEAVED ROUNDS, ALTERNATING ORDER.<< The first version ran arm A to completion then arm B, 120 // and produced x104, x101 and x391 on three consecutive runs of the SAME binary -- because run 3 121 // caught an I/O stall INSIDE arm A (159318ms vs ~64000ms elsewhere) and credited the whole stall 122 // to the durable path. >>A SEQUENTIAL A/B FALSELY ATTRIBUTES A TIME WINDOW TO WHICHEVER ARM WAS 123 // RUNNING<< -- the same law this estate already banked for sequential URL sweeps. 124 // Rounds interleave A and B, and the ORDER FLIPS each round so neither arm always runs first (a 125 // cold-cache or warm-cache advantage would otherwise land on the same arm every time). The 126 // reported figure is the MEDIAN round ratio, which a single stalled round cannot move. 127 let ras: *i64 = sys_mmap(8 * 64) as *i64 128 var r: i64 = 0 129 var sum_a: i64 = 0 130 var sum_b: i64 = 0 131 while r < CB_ROUNDS { 132 var ma: i64 = 0 133 var mb: i64 = 0 134 if (r % 2) == 0 { 135 ma = cb_arm(pa, n, 0) 136 mb = cb_arm(pb, n, 1) 137 } 138 if (r % 2) == 1 { 139 mb = cb_arm(pb, n, 1) 140 ma = cb_arm(pa, n, 0) 141 } 142 sum_a = sum_a + ma 143 sum_b = sum_b + mb 144 var ratio: i64 = 100 145 if mb > 0 { ratio = (ma * 100) / mb } 146 ras[r] = ratio 147 cb_w(" round " as *u8); cb_n(r + 1) 148 cb_w(": durable " as *u8); cb_n(ma) 149 cb_w("ms deferred " as *u8); cb_n(mb) 150 cb_w("ms ratio x100 = " as *u8); cb_n(ratio); cb_w("\n" as *u8) 151 r = r + 1 152 } 153 let keep_a: i64 = cb_survivors(pa, n) 154 let keep_b: i64 = cb_survivors(pb, n) 155 cb_w("\n rows kept: durable " as *u8); cb_n(keep_a) 156 cb_w("/" as *u8); cb_n(n) 157 cb_w(" deferred " as *u8); cb_n(keep_b) 158 cb_w("/" as *u8); cb_n(n); cb_w("\n" as *u8) 159 if keep_a != n { cb_w(" >>ARM A LOST ROWS -- the durable path is not keeping everything; speed is moot.<<\n" as *u8) } 160 if keep_b != keep_a { cb_w(" >>ARM B DISAGREES WITH ARM A -- deferred is not visibility-equivalent. Do not adopt.<<\n" as *u8) } 161 if keep_b == keep_a { if keep_a == n { cb_w(" EQUIVALENT: both arms kept every row, so the timings compare like with like.\n" as *u8) } } 162 // median by insertion sort (CB_ROUNDS is tiny) 163 var si: i64 = 1 164 while si < CB_ROUNDS { 165 let key: i64 = ras[si] 166 var sj: i64 = si - 1 167 var go2: i64 = 1 168 while go2 == 1 { 169 if sj < 0 { go2 = 0 } else { 170 if ras[sj] > key { ras[sj + 1] = ras[sj]; sj = sj - 1 } else { go2 = 0 } 171 } 172 } 173 ras[sj + 1] = key 174 si = si + 1 175 } 176 cb_w("\n MEDIAN ratio x100 = " as *u8); cb_n(ras[CB_ROUNDS / 2]) 177 cb_w(" (100 = no gain; 250 = 2.5x faster)\n" as *u8) 178 cb_w(" spread: min " as *u8); cb_n(ras[0]) 179 cb_w(" max " as *u8); cb_n(ras[CB_ROUNDS - 1]) 180 cb_w(" >>A WIDE SPREAD MEANS THE HOST, NOT THE METHOD, IS BEING MEASURED.<<\n" as *u8) 181 cb_w(" >>The deferred arm is EXPOSED between its last commit and ss_sync_now. This bench cannot\n" as *u8) 182 cb_w(" price that window; adopt per-caller, never for a row that must survive a crash alone.<<\n" as *u8) sys_exit(0) 183 return 0 184}