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}