1import assert from "node:assert/strict"
2import { execFileSync } from "node:child_process"
3import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
4import { join } from "node:path"
5import { fileURLToPath, pathToFileURL } from "node:url"
6import * as v8 from "node:v8"
7
8const implementations = [
9 "effect-interpreted",
10 "effect-jit",
11 "effect-aot",
12 "valibot",
13 "zod-jitless",
14 "zod-compiled"
15] as const
16type Implementation = typeof implementations[number]
17
18const cases = [
19 "struct-decode",
20 "struct-invalid",
21 "struct-is",
22 "array-decode",
23 "union-decode",
24 "transform-decode",
25 "default-make"
26] as const
27type CaseName = typeof cases[number]
28
29type BuiltCase = {
30 readonly schema: unknown
31 input: unknown
32 expected: unknown
33 readonly invalid?: boolean
34 readonly ast?: unknown
35}
36
37type Parser = (input: unknown) => unknown
38
39const [command, root, implementationArgument, caseArgument, countArgument, output] = process.argv.slice(2)
40const includes = <A extends string>(values: ReadonlyArray<A>, value: string | undefined): value is A =>
41 value !== undefined && values.includes(value as A)
42
43if (command !== "measure" && command !== "generate") {
44 throw new Error("command must be measure or generate")
45}
46if (root === undefined) throw new Error("root is required")
47if (!includes(implementations, implementationArgument)) {
48 throw new Error(`implementation must be one of: ${implementations.join(", ")}`)
49}
50if (!includes(cases, caseArgument)) {
51 throw new Error(`case must be one of: ${cases.join(", ")}`)
52}
53const implementation: Implementation = implementationArgument
54const caseName: CaseName = caseArgument
55const count = Number(countArgument)
56if (!Number.isSafeInteger(count) || count <= 0) throw new Error("count must be a positive integer")
57
58const forceGc = () => {
59 assert.equal(typeof globalThis.gc, "function", "run this probe with --expose-gc")
60 for (let i = 0; i < 5; i++) globalThis.gc!()
61}
62
63type CpuSample = {
64 readonly cpuMicros: number
65 readonly systemMicros: number
66 readonly userMicros: number
67 readonly wallNanos: number
68}
69
70const measureCpu = async <A,>(f: () => A | Promise<A>): Promise<readonly [A, CpuSample]> => {
71 const cpuStart = process.cpuUsage()
72 const wallStart = process.hrtime.bigint()
73 const value = await f()
74 const wallNanos = Number(process.hrtime.bigint() - wallStart)
75 const cpu = process.cpuUsage(cpuStart)
76 return [value, {
77 cpuMicros: cpu.user + cpu.system,
78 systemMicros: cpu.system,
79 userMicros: cpu.user,
80 wallNanos
81 }]
82}
83
84type MemorySample = {
85 readonly bytecodeBytes: number
86 readonly codeBytes: number
87 readonly externalSourceBytes: number
88 readonly heapBytes: number
89 readonly maxRssBytes: number
90 readonly rssBytes: number
91}
92
93const memory = (): MemorySample => {
94 const usage = process.memoryUsage()
95 const code = v8.getHeapCodeStatistics()
96 return {
97 bytecodeBytes: code.bytecode_and_metadata_size,
98 codeBytes: code.code_and_metadata_size,
99 externalSourceBytes: code.external_script_source_size,
100 heapBytes: usage.heapUsed,
101 maxRssBytes: process.resourceUsage().maxRSS * 1024,
102 rssBytes: usage.rss
103 }
104}
105
106const delta = (after: MemorySample, before: MemorySample) => ({
107 bytecodeBytes: after.bytecodeBytes - before.bytecodeBytes,
108 codeBytes: after.codeBytes - before.codeBytes,
109 externalSourceBytes: after.externalSourceBytes - before.externalSourceBytes,
110 heapBytes: after.heapBytes - before.heapBytes,
111 maxRssBytes: after.maxRssBytes - before.maxRssBytes,
112 rssBytes: after.rssBytes - before.rssBytes
113})
114
115const perSchema = (sample: ReturnType<typeof delta>) => ({
116 bytecodeBytes: sample.bytecodeBytes / count,
117 codeBytes: sample.codeBytes / count,
118 externalSourceBytes: sample.externalSourceBytes / count,
119 heapBytes: sample.heapBytes / count,
120 maxRssBytes: sample.maxRssBytes / count,
121 rssBytes: sample.rssBytes / count
122})
123
124const loadEffectModule = (path: string) =>
125 import(pathToFileURL(join(root, "packages/effect/src", `${path}.ts`)).href)
126
127const loadEffectSchemaModules = async () => ({
128 Effect: await loadEffectModule("Effect"),
129 Schema: await loadEffectModule("Schema"),
130 SchemaAST: await loadEffectModule("SchemaAST")
131})
132
133type EffectSchemaModules = Awaited<ReturnType<typeof loadEffectSchemaModules>>
134
135const buildEffectCase = ({ Effect, Schema, SchemaAST }: EffectSchemaModules, index: number): BuiltCase => {
136 const suffix = String(index)
137 const name = `name${suffix}`
138 const age = `age${suffix}`
139 const active = `active${suffix}`
140 const person = () => Schema.Struct({ [name]: Schema.String, [age]: Schema.Number, [active]: Schema.Boolean })
141 const value = { [name]: "Ada", [age]: 37, [active]: true }
142 switch (caseName) {
143 case "struct-decode": {
144 const schema = person()
145 return { schema, ast: schema.ast, input: value, expected: value }
146 }
147 case "struct-invalid": {
148 const schema = person()
149 return { schema, ast: schema.ast, input: { ...value, [age]: "bad" }, expected: true, invalid: true }
150 }
151 case "struct-is": {
152 const schema = person()
153 return { schema, ast: SchemaAST.toType(schema.ast), input: value, expected: true }
154 }
155 case "array-decode": {
156 const schema = Schema.Array(person())
157 const input = Array.from({ length: 32 }, () => ({ ...value }))
158 return { schema, ast: schema.ast, input, expected: input }
159 }
160 case "union-decode": {
161 const tag = `tag${suffix}`
162 const schema = Schema.Union(
163 Array.from({ length: 8 }, (_, member) =>
164 Schema.Struct({ [tag]: Schema.Literal(member), [`value${suffix}`]: Schema.Number }))
165 )
166 const input = { [tag]: 7, [`value${suffix}`]: 1 }
167 return { schema, ast: schema.ast, input, expected: input }
168 }
169 case "transform-decode": {
170 const schema = Schema.Struct(
171 Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, Schema.NumberFromString]))
172 )
173 const input = Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, String(field)]))
174 const expected = Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, field]))
175 return { schema, ast: schema.ast, input, expected }
176 }
177 case "default-make": {
178 const schema = Schema.Struct(
179 Object.fromEntries(
180 Array.from(
181 { length: 32 },
182 (_, field) => [
183 `v${suffix}_${field}`,
184 Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(field)))
185 ]
186 )
187 )
188 )
189 const expected = Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, field]))
190 return { schema, ast: SchemaAST.toType(schema.ast), input: {}, expected }
191 }
192 }
193}
194
195const loadEffect = async (jit: boolean) => {
196 const modules = await loadEffectSchemaModules()
197 const SchemaParser = await loadEffectModule("SchemaParser")
198 const enable = jit ? (await loadEffectModule("schema/SchemaJITCompiler")).enable : undefined
199
200 const prepare = (built: ReadonlyArray<BuiltCase>): ReadonlyArray<Parser> => {
201 return built.map(({ ast, schema }) => {
202 if (enable !== undefined) enable(ast)
203 if (caseName === "struct-is") return SchemaParser.is(schema)
204 if (caseName === "default-make") return SchemaParser.make(schema)
205 return SchemaParser.decodeUnknownSync(schema)
206 })
207 }
208
209 return { build: (index: number) => buildEffectCase(modules, index), prepare }
210}
211
212const loadZod = async (compiled: boolean) => {
213 const z = await import("zod/v4")
214
215 const build = (index: number): BuiltCase => {
216 const suffix = String(index)
217 const name = `name${suffix}`
218 const age = `age${suffix}`
219 const active = `active${suffix}`
220 const person = () => z.object({ [name]: z.string(), [age]: z.number(), [active]: z.boolean() })
221 const value = { [name]: "Ada", [age]: 37, [active]: true }
222 switch (caseName) {
223 case "struct-decode":
224 return { schema: person(), input: value, expected: value }
225 case "struct-invalid":
226 return { schema: person(), input: { ...value, [age]: "bad" }, expected: true, invalid: true }
227 case "struct-is":
228 return { schema: person(), input: value, expected: true }
229 case "array-decode": {
230 const input = Array.from({ length: 32 }, () => ({ ...value }))
231 return { schema: z.array(person()), input, expected: input }
232 }
233 case "union-decode": {
234 const tag = `tag${suffix}`
235 const schema = z.union(
236 Array.from({ length: 8 }, (_, member) =>
237 z.object({ [tag]: z.literal(member), [`value${suffix}`]: z.number() })) as [
238 ReturnType<typeof person>,
239 ReturnType<typeof person>,
240 ...Array<ReturnType<typeof person>>
241 ]
242 )
243 const input = { [tag]: 7, [`value${suffix}`]: 1 }
244 return { schema, input, expected: input }
245 }
246 case "transform-decode": {
247 const schema = z.object(
248 Object.fromEntries(
249 Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, z.string().transform(Number)])
250 )
251 )
252 const input = Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, String(field)]))
253 const expected = Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, field]))
254 return { schema, input, expected }
255 }
256 case "default-make": {
257 const schema = z.object(
258 Object.fromEntries(
259 Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, z.number().default(field)])
260 )
261 )
262 const expected = Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, field]))
263 return { schema, input: {}, expected }
264 }
265 }
266 }
267
268 const prepare = (built: ReadonlyArray<BuiltCase>): ReadonlyArray<Parser> => {
269 return built.map(({ schema }) => {
270 const target = compiled ? z.compile(schema, { strict: true }) : schema
271 if (caseName === "struct-is") {
272 return compiled
273 ? (input: unknown) => z.validate(target, input)
274 : (input: unknown) => z.validate(target, input, { jitless: true })
275 }
276 return compiled
277 ? (input: unknown) => target.parse(input)
278 : (input: unknown) => target.parse(input, { jitless: true })
279 })
280 }
281
282 return { build, prepare }
283}
284
285const loadValibot = async () => {
286 const v = await import("valibot")
287
288 const build = (index: number): BuiltCase => {
289 const suffix = String(index)
290 const name = `name${suffix}`
291 const age = `age${suffix}`
292 const active = `active${suffix}`
293 const person = () => v.object({ [name]: v.string(), [age]: v.number(), [active]: v.boolean() })
294 const value = { [name]: "Ada", [age]: 37, [active]: true }
295 switch (caseName) {
296 case "struct-decode":
297 return { schema: person(), input: value, expected: value }
298 case "struct-invalid":
299 return { schema: person(), input: { ...value, [age]: "bad" }, expected: true, invalid: true }
300 case "struct-is":
301 return { schema: person(), input: value, expected: true }
302 case "array-decode": {
303 const input = Array.from({ length: 32 }, () => ({ ...value }))
304 return { schema: v.array(person()), input, expected: input }
305 }
306 case "union-decode": {
307 const tag = `tag${suffix}`
308 const schema = v.variant(
309 tag,
310 Array.from({ length: 8 }, (_, member) =>
311 v.object({ [tag]: v.literal(member), [`value${suffix}`]: v.number() }))
312 )
313 const input = { [tag]: 7, [`value${suffix}`]: 1 }
314 return { schema, input, expected: input }
315 }
316 case "transform-decode": {
317 const schema = v.object(
318 Object.fromEntries(
319 Array.from({ length: 32 }, (_, field) => [
320 `v${suffix}_${field}`,
321 v.pipe(v.string(), v.transform(Number))
322 ])
323 )
324 )
325 const input = Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, String(field)]))
326 const expected = Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, field]))
327 return { schema, input, expected }
328 }
329 case "default-make": {
330 const schema = v.object(
331 Object.fromEntries(
332 Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, v.optional(v.number(), field)])
333 )
334 )
335 const expected = Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, field]))
336 return { schema, input: {}, expected }
337 }
338 }
339 }
340
341 const prepare = (built: ReadonlyArray<BuiltCase>): ReadonlyArray<Parser> => {
342 return built.map(({ schema }) =>
343 caseName === "struct-is"
344 ? (input: unknown) => v.is(schema, input)
345 : (input: unknown) => v.parse(schema, input))
346 }
347
348 return { build, prepare }
349}
350
351const runParser = (parser: Parser, built: BuiltCase): unknown => {
352 if (!built.invalid) return parser(built.input)
353 try {
354 parser(built.input)
355 return false
356 } catch {
357 return true
358 }
359}
360
361const validate = (actual: unknown, built: BuiltCase) => assert.deepEqual(actual, built.expected)
362
363if (command === "generate") {
364 if (output === undefined) throw new Error("output is required for generate")
365 const [modules, moduleCpu] = await measureCpu(async () => {
366 return {
367 ...await loadEffectSchemaModules(),
368 AOT: await loadEffectModule("schema/SchemaAOTCompiler")
369 }
370 })
371 const [built, schemaCpu] = await measureCpu(() =>
372 Array.from({ length: count }, (_, index) => buildEffectCase(modules, index).ast)
373 )
374 const operation = caseName === "struct-is" ? "is" : caseName === "default-make" ? "make" : "decode"
375 const [source, generateCpu] = await measureCpu(() =>
376 modules.AOT.compile(built.map((ast) => ({ ast, operations: [operation] }))))
377 writeFileSync(output, source)
378 process.stdout.write(JSON.stringify({
379 generateCpu,
380 heapBytes: process.memoryUsage().heapUsed,
381 maxRssBytes: process.resourceUsage().maxRSS * 1024,
382 moduleCpu,
383 schemaCpu,
384 sourceBytes: Buffer.byteLength(source)
385 }))
386} else {
387 forceGc()
388 const beforeModule = memory()
389 const [library, moduleCpu] = await measureCpu(() => {
390 switch (implementation) {
391 case "effect-interpreted":
392 return loadEffect(false)
393 case "effect-jit":
394 case "effect-aot":
395 return loadEffect(implementation === "effect-jit")
396 case "zod-jitless":
397 case "zod-compiled":
398 return loadZod(implementation === "zod-compiled")
399 case "valibot":
400 return loadValibot()
401 }
402 })
403 forceGc()
404 const afterModule = memory()
405
406 const [built, schemaCpu] = await measureCpu(() => Array.from({ length: count }, (_, index) => library.build(index)))
407 forceGc()
408 const afterSchemas = memory()
409
410 let aotBuild: unknown
411 let aotModuleCpu: CpuSample | undefined
412 let directory: string | undefined
413 let parsers: ReadonlyArray<Parser>
414 let prepareCpu: CpuSample
415 try {
416 if (implementation === "effect-aot") {
417 directory = mkdtempSync(join(root, "packages/effect/.compiler-resources-"))
418 const generated = join(directory, "generated.mjs")
419 aotBuild = JSON.parse(execFileSync(
420 process.execPath,
421 ["--expose-gc", fileURLToPath(import.meta.url), "generate", root, implementation, caseName, String(count), generated],
422 { encoding: "utf8" }
423 ))
424 const [installed, measuredModuleCpu] = await measureCpu(async () => {
425 const generatedModule = await import(pathToFileURL(generated).href)
426 generatedModule.install(built.map((value) => value.ast))
427 })
428 void installed
429 aotModuleCpu = measuredModuleCpu
430 }
431 const prepared = await measureCpu(() => library.prepare(built))
432 parsers = prepared[0]
433 prepareCpu = prepared[1]
434 forceGc()
435 const afterPrepare = memory()
436
437 const [results, firstCallCpu] = await measureCpu(() => parsers.map((parser, index) => runParser(parser, built[index])))
438 results.forEach((actual, index) => validate(actual, built[index]))
439 results.length = 0
440 forceGc()
441 const afterFirstCall = memory()
442
443 for (const value of built) {
444 value.input = undefined
445 value.expected = undefined
446 }
447 forceGc()
448 const afterRelease = memory()
449
450 assert.equal(built.length, count)
451 assert.equal(parsers.length, count)
452 process.stdout.write(JSON.stringify({
453 aotBuild,
454 case: caseName,
455 count,
456 cpu: {
457 aotModule: aotModuleCpu,
458 firstCall: firstCallCpu,
459 module: moduleCpu,
460 prepare: prepareCpu,
461 schema: schemaCpu
462 },
463 implementation,
464 memory: {
465 compilerPerSchema: perSchema(delta(afterFirstCall, afterSchemas)),
466 firstCallPerSchema: perSchema(delta(afterFirstCall, afterPrepare)),
467 module: delta(afterModule, beforeModule),
468 preparePerSchema: perSchema(delta(afterPrepare, afterSchemas)),
469 retainedLibraryPerSchema: perSchema(delta(afterRelease, afterModule)),
470 schemaPerSchema: perSchema(delta(afterSchemas, afterModule)),
471 totalWithFixturePerSchema: perSchema(delta(afterFirstCall, afterModule))
472 }
473 }))
474 } finally {
475 if (directory !== undefined) {
476 rmSync(directory, { recursive: true, force: true })
477 }
478 }
479}