| 1 | import assert from "node:assert/strict" |
| 2 | import { execFileSync } from "node:child_process" |
| 3 | import { mkdtempSync, rmSync, writeFileSync } from "node:fs" |
| 4 | import { join } from "node:path" |
| 5 | import { fileURLToPath, pathToFileURL } from "node:url" |
| 6 | |
| 7 | const [command, root, mode, operation, shape = "struct", countString = "500"] = process.argv.slice(2) |
| 8 | const count = Number(countString) |
| 9 | const includes = (values: ReadonlyArray<string>, value: string | undefined) => |
| 10 | value !== undefined && values.includes(value) |
| 11 | if (command !== "heap" && command !== "cold" && command !== "generate") { |
| 12 | throw new Error("command must be heap, cold or generate") |
| 13 | } |
| 14 | if (root === undefined) throw new Error("root is required") |
| 15 | if (!includes(["interpreted", "jit", "aot"], mode)) { |
| 16 | throw new Error("mode must be interpreted, jit or aot") |
| 17 | } |
| 18 | if (!includes(["decode", "invalid", "is", "make"], operation)) { |
| 19 | throw new Error("operation must be decode, invalid, is or make") |
| 20 | } |
| 21 | if (!includes(["struct", "array", "transform", "default"], shape)) { |
| 22 | throw new Error("shape must be struct, array, transform or default") |
| 23 | } |
| 24 | if (!Number.isSafeInteger(count) || count <= 0) throw new Error("count must be a positive integer") |
| 25 | const load = (path: string) => import(pathToFileURL(join(root, "packages/effect/src", path + ".ts")).href) |
| 26 | const Schema = await load("Schema") |
| 27 | const Parser = await load("SchemaParser") |
| 28 | const AST = await load("SchemaAST") |
| 29 | const Effect = await load("Effect") |
| 30 | const 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 | } |
| 37 | const 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 } |
| 41 | const typeValid = shape === "transform" ? { value: 1 } : valid |
| 42 | const input = operation === "invalid" ? { name: "Ada", age: "bad", active: true } |
| 43 | : operation === "make" && shape === "default" ? {} |
| 44 | : operation === "make" || operation === "is" ? typeValid |
| 45 | : valid |
| 46 | const expected = shape === "transform" || shape === "default" ? { value: 1 } : valid |
| 47 | |
| 48 | if (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 | } |