code wiki / _hdl_build / nx_segbloom.nx
nx_segbloom.nx source
↩ module page · 225 lines · 10071 B
1// nx_segbloom.nx -- EVIDENCE FIRST: does a per-segment TERM BLOOM actually skip enough segments to
2// be worth wiring into the hot search path? MEASURES ONLY. It writes nothing, changes no live code
3// and touches no existing call site -- because the honest order is prove-the-win, then wire it.
4//
5// THE DEFECT IT TARGETS (measured 2026-08-06): /api/search costs a FIXED ~6.6s even for a query that
6// matches ZERO documents, against a 42ms control on the same daemon. ss_term (nx_seg_store:2441)
7// walks EVERY live segment and calls ss_terms_find on that segment's mmap'd terms block:
8// while s < ns { let tb = h[5+8*s]; ss_terms_find(tb, h[6+8*s], term, outs) ... }
9// The blocks are already mapped, so the cost is ONE PAGE FAULT PER SEGMENT -- ~896 segments x ~8ms.
10// A resident bloom per segment answers "definitely not here" without touching the mapping at all,
11// so the fault never happens. Segments are IMMUTABLE in an append-only store, which is what makes
12// this sound: a per-segment term bloom is computed once and can never go stale.
13//
14// REUSE, NOT REINVENTION: the bloom is nx_sketch_bloom (v2, murmur3 + Kirsch-Mitzenmacher double
15// hashing, already used in production by nx_ingest_runner) and the bits/item + k constants are the
16// pre-computed FPR breakpoints from nx_bloom_capacity. Nothing here is a new filter.
17//
18// nx_segbloom stat <domain> [maxsegs]
19// nx_segbloom probe <domain> <profile 1=10pct|2=1pct> <term> [maxsegs]
20// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
21import "nx_seg_store.nx"
22import "nx_sketch_bloom.nx"
23import "nx_syscalls.nx"
24
25const SB_PATHCAP: i64 = 512
26const SB_SMALL: i64 = 64
27const SB_MAXSEGS: i64 = 8192
28// FPR breakpoints lifted from nx_bloom_capacity (bits-per-item Q10 -> whole bits, and k).
29const SB_BITS_10PCT: i64 = 5
30const SB_K_10PCT: i64 = 3
31const SB_BITS_1PCT: i64 = 10
32const SB_K_1PCT: i64 = 7
33
34func sb_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
35func sb_w(s: *u8, n: i64) -> i64 { sys_write(1, s, n); return 0 }
36func sb_puts(s: *u8) -> i64 { sb_w(s, sb_slen(s)); return 0 }
37func sb_num(v: i64) -> i64 {
38 let t: *u8 = sys_mmap(SB_SMALL)
39 var m: i64 = v
40 if m < 0 { sb_puts("-" as *u8); m = 0 - m }
41 var k: i64 = 0
42 if m == 0 { t[0] = 48 as u8; k = 1 }
43 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
44 let o: *u8 = sys_mmap(SB_SMALL)
45 var i: i64 = 0
46 while i < k { o[i] = t[k - 1 - i]; i = i + 1 }
47 sb_w(o, k)
48 return 0
49}
50func sb_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { d[o+i] = s[i]; i = i + 1 } return o + i }
51// prefix is built here rather than imported from the docportal module so this stays a leaf on
52// nx_seg_store alone -- the same reasoning nx_docportal_search_seg gives for inlining dp_prefix.
53func sb_prefix(domain: *u8, out: *u8) -> i64 {
54 var o: i64 = 0
55 o = sb_cat(out, o, "knowledge/store/dp-" as *u8)
56 o = sb_cat(out, o, domain)
57 o = sb_cat(out, o, "-pub-" as *u8)
58 out[o] = 0 as u8
59 return o
60}
61func sb_next_pow2(n: i64) -> i64 {
62 var p: i64 = 64
63 while p < n { p = p * 2 }
64 return p
65}
66func sb_open(domain: *u8) -> *i64 {
67 let prefix: *u8 = sys_mmap(SB_PATHCAP)
68 sb_prefix(domain, prefix)
69 sb_puts("=== nx_segbloom " as *u8); sb_puts(prefix); sb_puts(" ===\n" as *u8)
70 // usemmap=1: identical open mode to the live docportal serve path, so the measurement is of the
71 // production shape and not of a mode nothing actually runs.
72 return ss_open2(prefix, 1)
73}
74func cmd_stat(domain: *u8, maxsegs: i64) -> i64 {
75 let h: *i64 = sb_open(domain)
76 if (h as i64) == 0 { sb_puts("REFUSED: shard absent or empty\n" as *u8); return 1 }
77 var ns: i64 = h[0]
78 sb_puts(" segments=" as *u8); sb_num(ns); sb_puts("\n" as *u8)
79 if maxsegs > 0 { if ns > maxsegs { ns = maxsegs; sb_puts(" (capped to " as *u8); sb_num(ns); sb_puts(" segments for this run)\n" as *u8) } }
80 var tot: i64 = 0
81 var mn: i64 = 0 - 1
82 var mx: i64 = 0
83 var empty: i64 = 0
84 var s: i64 = 0
85 while s < ns {
86 let c: i64 = ss_term_count(h, s)
87 tot = tot + c
88 if c == 0 { empty = empty + 1 }
89 if mn < 0 { mn = c } else { if c < mn { mn = c } }
90 if c > mx { mx = c }
91 s = s + 1
92 }
93 sb_puts(" terms: total=" as *u8); sb_num(tot)
94 sb_puts(" min=" as *u8); sb_num(mn)
95 sb_puts(" max=" as *u8); sb_num(mx)
96 if ns > 0 { sb_puts(" avg=" as *u8); sb_num(tot / ns) }
97 sb_puts(" empty_segs=" as *u8); sb_num(empty); sb_puts("\n" as *u8)
98 // projected resident cost -- the number that decides whether this is wirable at all
99 sb_puts(" projected bloom bytes: 10pct=" as *u8); sb_num((tot * SB_BITS_10PCT) / 8)
100 sb_puts(" 1pct=" as *u8); sb_num((tot * SB_BITS_1PCT) / 8); sb_puts("\n" as *u8)
101 return 0
102}
103func cmd_probe(domain: *u8, profile: i64, term: *u8, maxsegs: i64) -> i64 {
104 let h: *i64 = sb_open(domain)
105 if (h as i64) == 0 { sb_puts("REFUSED: shard absent or empty\n" as *u8); return 1 }
106 var ns: i64 = h[0]
107 if maxsegs > 0 { if ns > maxsegs { ns = maxsegs } }
108 var bits: i64 = SB_BITS_10PCT
109 var kk: i64 = SB_K_10PCT
110 if profile == 2 { bits = SB_BITS_1PCT; kk = SB_K_1PCT }
111 sb_puts(" segments=" as *u8); sb_num(ns)
112 sb_puts(" profile=" as *u8); if profile == 2 { sb_puts("1pct" as *u8) } else { sb_puts("10pct" as *u8) }
113 sb_puts(" (bits/term=" as *u8); sb_num(bits); sb_puts(" k=" as *u8); sb_num(kk); sb_puts(")\n" as *u8)
114 let blooms: *i64 = sys_mmap(8 * SB_MAXSEGS) as *i64
115 let tp: *i64 = sys_mmap(SB_SMALL) as *i64
116 let tl: *i64 = sys_mmap(SB_SMALL) as *i64
117 let dc: *i64 = sys_mmap(SB_SMALL) as *i64
118 let t0: i64 = sys_now_us()
119 var totbytes: i64 = 0
120 var totterms: i64 = 0
121 var s: i64 = 0
122 while s < ns {
123 let c: i64 = ss_term_count(h, s)
124 var cap: i64 = sb_next_pow2(c * bits)
125 if c == 0 { cap = 64 }
126 let bf: *BloomS = nx_bloom_alloc(cap, kk)
127 blooms[s] = bf as i64
128 totbytes = totbytes + (cap / 8)
129 totterms = totterms + c
130 var i: i64 = 0
131 while i < c {
132 if ss_term_at(h, s, i, tp, tl, dc) == 1 {
133 nx_bloom_insert(bf, tp[0] as *u8, tl[0])
134 }
135 i = i + 1
136 }
137 s = s + 1
138 }
139 let t1: i64 = sys_now_us()
140 sb_puts(" built " as *u8); sb_num(totterms); sb_puts(" terms into " as *u8); sb_num(totbytes)
141 sb_puts(" bloom bytes in " as *u8); sb_num((t1 - t0) / 1000); sb_puts(" ms\n" as *u8)
142 // THE MEASUREMENT: how many segments would the bloom let us SKIP for this term?
143 let tn: i64 = sb_slen(term)
144 var maybe: i64 = 0
145 s = 0
146 while s < ns {
147 let bf: *BloomS = blooms[s] as *BloomS
148 if nx_bloom_contains(bf, term, tn) == 1 { maybe = maybe + 1 }
149 s = s + 1
150 }
151 let skip: i64 = ns - maybe
152 sb_puts(" term '" as *u8); sb_w(term, tn); sb_puts("': maybe=" as *u8); sb_num(maybe)
153 sb_puts(" skip=" as *u8); sb_num(skip)
154 if ns > 0 { sb_puts(" skip_rate=" as *u8); sb_num((skip * 1000) / ns); sb_puts(" permil" as *u8) }
155 sb_puts("\n" as *u8)
156 // GROUND TRUTH, not just the bloom's opinion. A bloom may only ever be a FALSE POSITIVE, never a
157 // false negative -- so if ss_terms_find finds the term in a segment the bloom skipped, the whole
158 // idea is unsound and must be reported as such rather than quietly averaged away.
159 let outs: *i64 = sys_mmap(SB_SMALL) as *i64
160 var truth: i64 = 0
161 var unsound: i64 = 0
162 s = 0
163 while s < ns {
164 let tb: *u8 = h[5 + 8 * s] as *u8
165 var present: i64 = 0
166 if h[6 + 8 * s] >= 8 { if ss_terms_find(tb, h[6 + 8 * s], term, outs) == 1 { present = 1 } }
167 if present == 1 {
168 truth = truth + 1
169 let bf: *BloomS = blooms[s] as *BloomS
170 if nx_bloom_contains(bf, term, tn) == 0 { unsound = unsound + 1 }
171 }
172 s = s + 1
173 }
174 sb_puts(" ground truth: segments actually holding the term=" as *u8); sb_num(truth)
175 sb_puts(" false_negatives=" as *u8); sb_num(unsound); sb_puts("\n" as *u8)
176 if unsound > 0 {
177 sb_puts("verdict=UNSOUND (a bloom reported ABSENT for a segment that HOLDS the term -- do NOT wire)\n" as *u8)
178 return 2
179 }
180 if truth > maybe { sb_puts("verdict=IMPOSSIBLE (truth exceeds maybe)\n" as *u8); return 2 }
181 let fp: i64 = maybe - truth
182 sb_puts(" false positives=" as *u8); sb_num(fp)
183 if ns > 0 { sb_puts(" (" as *u8); sb_num((fp * 1000) / ns); sb_puts(" permil of segments)" as *u8) }
184 sb_puts("\n" as *u8)
185 sb_puts("verdict=SOUND skip_rate=" as *u8)
186 if ns > 0 { sb_num((skip * 1000) / ns) } else { sb_num(0) }
187 sb_puts(" permil -- that fraction of per-segment page faults disappears\n" as *u8)
188 return 0
189}
190func sb_atoi(s: *u8) -> i64 {
191 var v: i64 = 0
192 var i: i64 = 0
193 var seen: i64 = 0
194 while s[i] != (0 as u8) {
195 let c: i64 = s[i] as i64
196 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); seen = 1 } }
197 i = i + 1
198 }
199 if seen == 0 { return 0 }
200 return v
201}
202func main(argc: i64, argv: *i64) -> i64 {
203 if argc < 3 {
204 sb_puts("usage: nx_segbloom stat <domain> [maxsegs] | probe <domain> <profile 1|2> <term> [maxsegs]\n" as *u8)
205 sys_exit(0); return 0
206 }
207 let verb: *u8 = argv[1] as *u8
208 if sb_slen(verb) == 4 {
209 if verb[0] == (115 as u8) { if verb[1] == (116 as u8) { if verb[2] == (97 as u8) { if verb[3] == (116 as u8) {
210 var mx: i64 = 0
211 if argc >= 4 { mx = sb_atoi(argv[3] as *u8) }
212 let rc: i64 = cmd_stat(argv[2] as *u8, mx)
213 sys_exit(rc); return rc
214 } } } }
215 }
216 if argc >= 5 {
217 var mx2: i64 = 0
218 if argc >= 6 { mx2 = sb_atoi(argv[5] as *u8) }
219 let rc: i64 = cmd_probe(argv[2] as *u8, sb_atoi(argv[3] as *u8), argv[4] as *u8, mx2)
220 sys_exit(rc); return rc
221 }
222 sb_puts("usage: nx_segbloom stat <domain> [maxsegs] | probe <domain> <profile 1|2> <term> [maxsegs]\n" as *u8)
223 sys_exit(2)
224 return 2
225}