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"
6
7const [command, root, mode, operation, shape = "struct", countString = "500"] = process.argv.slice(2)
8const count = Number(countString)
9const includes = (values: ReadonlyArray<string>, value: string | undefined) =>
10 value !== undefined && values.includes(value)
11if (command !== "heap" && command !== "cold" && command !== "generate") {
12 throw new Error("command must be heap, cold or generate")
13}
14if (root === undefined) throw new Error("root is required")
15if (!includes(["interpreted", "jit", "aot"], mode)) {
16 throw new Error("mode must be interpreted, jit or aot")
17}
18if (!includes(["decode", "invalid", "is", "make"], operation)) {
19 throw new Error("operation must be decode, invalid, is or make")
20}
21if (!includes(["struct", "array", "transform", "default"], shape)) {
22 throw new Error("shape must be struct, array, transform or default")
23}
24if (!Number.isSafeInteger(count) || count <= 0) throw new Error("count must be a positive integer")
25const load = (path: string) => import(pathToFileURL(join(root, "packages/effect/src", path + ".ts")).href)
26const Schema = await load("Schema")
27const Parser = await load("SchemaParser")
28const AST = await load("SchemaAST")
29const Effect = await load("Effect")
30const create = () => {
31 const struct = Schema.Struct({ name: Schema.String, age: Schema.Number, active: Schema.Boolean })
32 return shape === "array" ? Schema.Array(struct)
33 : shape === "transform" ? Schema.Struct({ value: Schema.NumberFromString })
34 : shape === "default" ? Schema.Struct({ value: Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(1))) })
35 : struct
36}
37const valid = shape === "array" ? Array.from({ length: 32 }, () => ({ name: "Ada", age: 37, active: true }))
38 : shape === "transform" ? { value: "1" }
39 : shape === "default" ? { value: 1 }
40 : { name: "Ada", age: 37, active: true }
41const typeValid = shape === "transform" ? { value: 1 } : valid
42const input = operation === "invalid" ? { name: "Ada", age: "bad", active: true }
43 : operation === "make" && shape === "default" ? {}
44 : operation === "make" || operation === "is" ? typeValid
45 : valid
46const expected = shape === "transform" || shape === "default" ? { value: 1 } : valid
47
48if (command === "generate") {
49 const AOT = await load("schema/SchemaAOTCompiler")
50 const schema = create()
51 const targetOperation = operation === "make" ? "make" : operation === "is" ? "is" : "decode"
52 const ast = targetOperation === "decode" ? schema.ast : AST.toType(schema.ast)
53 writeFileSync(process.argv[8], AOT.compile([{ ast, operations: [targetOperation] }]))
54} else {
55 for (let i = 0; i < 5; i++) globalThis.gc?.()
56 const beforeImport = process.memoryUsage().heapUsed
57 let enable: ((ast: unknown) => void) | undefined
58 let install: ((asts: Array<unknown>) => void) | undefined
59 let directory: string | undefined
60 if (mode === "jit") {
61 enable = (await load("schema/SchemaJITCompiler")).enable
62 } else if (mode === "aot") {
63 directory = mkdtempSync(join(root, "packages/effect/.compiler-memory-"))
64 const generated = join(directory, "generated.mjs")
65 execFileSync(process.execPath, [fileURLToPath(import.meta.url), "generate", root, mode, operation, shape, countString, generated])
66 install = (await import(pathToFileURL(generated).href)).install
67 }
68 for (let i = 0; i < 5; i++) globalThis.gc?.()
69 const moduleBytes = process.memoryUsage().heapUsed - beforeImport
70 const prepare = (schema: any) => {
71 const ast = operation === "make" || operation === "is" ? AST.toType(schema.ast) : schema.ast
72 if (enable) enable(ast)
73 if (install) install([ast])
74 return operation === "make" ? Parser.make(schema)
75 : operation === "is" ? Parser.is(schema)
76 : Parser.decodeUnknownSync(schema)
77 }
78 const run = (parse: (input: unknown) => unknown) => {
79 try {
80 return parse(input)
81 } catch (error) {
82 if (operation !== "invalid") throw error
83 return error
84 }
85 }
86 const validate = (value: unknown) => {
87 if (operation === "invalid") {
88 assert.ok(value instanceof Error)
89 assert.equal(value.message, "Schema validation failed")
90 assert.equal("cause" in value, true)
91 }
92 else if (operation === "is") assert.equal(value, true)
93 else assert.deepEqual(value, expected)
94 }
95 try {
96 for (let i = 0; i < 20; i++) validate(run(prepare(create())))
97 if (command === "heap") {
98 assert.equal(typeof globalThis.gc, "function")
99 const schemas = Array.from({ length: count }, create)
100 for (let i = 0; i < 5; i++) globalThis.gc!()
101 const before = process.memoryUsage().heapUsed
102 const parsers = schemas.map((schema) => {
103 const parse = prepare(schema)
104 validate(run(parse))
105 return parse
106 })
107 for (let i = 0; i < 5; i++) globalThis.gc!()
108 const after = process.memoryUsage().heapUsed
109 // Keep schemas and adapters alive across the measurement.
110 assert.equal(parsers.length, count)
111 assert.equal(schemas.length, count)
112 process.stdout.write(JSON.stringify({ mode, operation, shape, count, moduleBytes, bytesPerSchema: (after - before) / count }))
113 } else if (command === "cold") {
114 const samples = []
115 for (let round = 0; round < 7; round++) {
116 let value: unknown
117 const start = process.hrtime.bigint()
118 for (let i = 0; i < count; i++) value = run(prepare(create()))
119 samples.push(Number(process.hrtime.bigint() - start) / count)
120 validate(value)
121 }
122 process.stdout.write(JSON.stringify({ mode, operation, shape, count, nsPerSchema: samples }))
123 }
124 } finally {
125 if (directory) rmSync(directory, { recursive: true, force: true })
126 }
127}