Skip to content

Commit 9efbbc5

Browse files
ushironokoclaude
andcommitted
Add opt-in Tier-0 no-eval compiled validation fast path
safeParseCompiled compiles a schema into a closure tree at construction time (no eval / no new Function / no codegen), so it runs where JIT tiers cannot: strict-CSP pages and edge runtimes (Cloudflare Workers, Deno Deploy, Vercel Edge). It removes the interpreter's megamorphic per-node dispatch and per-value dataset allocation, winning ~1.1-2.4x on container schemas across both V8 and JSC, while staying byte-identical to safeParse on success and error paths. The interpreter remains the SSoT; this is an opt-in parallel path. Piped, async, and non-specialized nodes fall back to ~run (defense-in-depth against an accept-invalid bypass), an invariant locked by a test. A bare top-level primitive should use safeParse directly (documented entry overhead). - packages/tskm/src/compile.ts: closure compiler, getCompiledValidate (WeakMap keyed off the schema value), safeParseCompiled - packages/tskm/src/index.ts: opt-in exports - packages/tskm/test/compile.test.ts: byte-identical conformance + piped/refined child bypass guards + fast-path-only-for-bare-leaf invariant (Stryker oracle) - bench/validator: full-materializing per-fixture sink, no-regression gate, 30-schema interleaved shared-IC guard (zero V8 deopts) - test/workerd/smoke.ts: no-eval property proven by execution under node --disallow-code-generation-from-strings Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 02f5520 commit 9efbbc5

8 files changed

Lines changed: 2206 additions & 0 deletions

File tree

bench/validator/ab.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { safeParseCompiled } from "../../packages/tskm/src/compile.ts"
2+
import { safeParse } from "../../packages/tskm/src/index.ts"
3+
import { type BenchResult, bench } from "../lib/measure.ts"
4+
import { createBenchmarkFixtures, runConformance } from "./conformance.ts"
5+
import { gateRegressions, makeConsume } from "./sink.ts"
6+
7+
interface ABResult {
8+
readonly name: string
9+
readonly interpreted: BenchResult
10+
readonly compiled: BenchResult
11+
readonly ratio: number
12+
}
13+
14+
function run(): void {
15+
const conformance = runConformance()
16+
console.log(`conformance: PASS casesChecked=${conformance.casesChecked}`)
17+
18+
const results: ABResult[] = []
19+
for (const fixture of createBenchmarkFixtures()) {
20+
// One consume per fixture so its reads stay monomorphic to this fixture's output shape;
21+
// a single shared consume across all fixtures goes megamorphic on JSC and distorts the
22+
// ratio (see makeConsume). Both A/B halves of a fixture share the SAME instance, so the
23+
// comparison is fair.
24+
const consume = makeConsume()
25+
// This is a shared, loaded dev machine (±10-20% per-run noise). A single A/B can swing a
26+
// marginal case across 1.0x purely from a load spike landing on one half. So repeat the
27+
// A/B and take the MEDIAN ratio, which is stable run-to-run; the displayed ns/op is the
28+
// rep whose ratio is the median.
29+
const REPS = 5
30+
const reps: ABResult[] = []
31+
for (let r = 0; r < REPS; r++) {
32+
const opts = { warmupMs: 100, sampleCount: 20 }
33+
const interpreted = bench(
34+
`${fixture.name}:interpreter`,
35+
() => consume(safeParse(fixture.schema, fixture.input, fixture.config)),
36+
opts,
37+
)
38+
const compiled = bench(
39+
`${fixture.name}:compiled`,
40+
() => consume(safeParseCompiled(fixture.schema, fixture.input, fixture.config)),
41+
opts,
42+
)
43+
reps.push({
44+
name: fixture.name,
45+
interpreted,
46+
compiled,
47+
ratio: interpreted.nsPerOp / compiled.nsPerOp,
48+
})
49+
}
50+
reps.sort((a, b) => a.ratio - b.ratio)
51+
results.push(reps[Math.floor(REPS / 2)] as ABResult)
52+
}
53+
54+
console.log("\n# validator A/B (Bun/JSC)\n")
55+
console.log(renderABTable(results))
56+
57+
const gate = gateRegressions(
58+
results.map((result) => ({ name: result.name, ratio: result.ratio })),
59+
)
60+
console.log("\n# no-regression gate (full-materializing sink)\n")
61+
console.log(gate.lines.join("\n"))
62+
console.log(`\nGATE: ${gate.failed ? "FAIL" : "PASS"}`)
63+
if (gate.failed) {
64+
process.exitCode = 1
65+
}
66+
}
67+
68+
function renderABTable(results: readonly ABResult[]): string {
69+
const rows = [
70+
[
71+
"fixture",
72+
"interp ns/op",
73+
"compiled ns/op",
74+
"ratio",
75+
"interp min/p50/p99",
76+
"compiled min/p50/p99",
77+
],
78+
...results.map((result) => [
79+
result.name,
80+
formatNs(result.interpreted.nsPerOp),
81+
formatNs(result.compiled.nsPerOp),
82+
`${result.ratio.toFixed(2)}x`,
83+
formatTriplet(result.interpreted),
84+
formatTriplet(result.compiled),
85+
]),
86+
]
87+
const header = rows[0] as string[]
88+
const widths = header.map((_, col) => Math.max(...rows.map((row) => (row[col] as string).length)))
89+
return rows
90+
.map((row, index) => {
91+
const line = row
92+
.map((cell, col) => (cell as string).padEnd(widths[col] as number))
93+
.join(" | ")
94+
if (index === 0) {
95+
return `${line}\n${widths.map((width) => "-".repeat(width)).join("-|-")}`
96+
}
97+
return line
98+
})
99+
.join("\n")
100+
}
101+
102+
function formatTriplet(result: BenchResult): string {
103+
return `${formatNs(result.nsPerOpMin)}/${formatNs(result.nsPerOp)}/${formatNs(result.nsPerOpP99)}`
104+
}
105+
106+
function formatNs(value: number): string {
107+
return value.toFixed(value >= 100 ? 1 : 2)
108+
}
109+
110+
run()

0 commit comments

Comments
 (0)