1import assert from "node:assert/strict"
2import { execFileSync } from "node:child_process"
3import { createHash } from "node:crypto"
4import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
5import os from "node:os"
6import { basename, join, resolve } from "node:path"
7import { fileURLToPath } from "node:url"
8import { aggregate, analyzePairs, median } from "../../stats.mts"
9import { caseNames } from "./fixtures.mts"
10import { analyzeProfile, categories, shareInterval } from "./profile.mts"
11
12const [outputArgument = "tmp/schema-construction", roundsArgument = "24", countArgument = "1000", namesArgument] =
13 process.argv.slice(2)
14const rounds = Number(roundsArgument)
15const count = Number(countArgument)
16assert.ok(Number.isSafeInteger(rounds) && rounds > 0)
17assert.ok(Number.isSafeInteger(count) && count > 1)
18const names = namesArgument ? namesArgument.split(",") : caseNames
19assert.ok(names.length > 0 && names.every((name) => caseNames.includes(name)) && new Set(names).size === names.length)
20const root = process.cwd()
21const output = resolve(outputArgument)
22const worker = fileURLToPath(new URL("./worker.mts", import.meta.url))
23const git = (...args: Array<string>) => execFileSync("git", args, { encoding: "utf8" }).trim()
24const revision = git("rev-parse", "HEAD")
25const sourceTree = git("rev-parse", "HEAD:packages/effect/src")
26assert.equal(
27 git("status", "--porcelain", "--untracked-files=all", "--", "packages/effect/src"),
28 "",
29 "Source must be clean"
30)
31const sha256 = (path: string) => createHash("sha256").update(readFileSync(path)).digest("hex")
32const harnessFiles = ["fixtures.mts", "worker.mts", "profile.mts", "run.mts"].map((name) =>
33 fileURLToPath(new URL(name, import.meta.url))
34)
35const hashes = Object.fromEntries(
36 [...harnessFiles, "packages/effect/runtimeperf/stats.mts", "pnpm-lock.yaml"].map((path) => [path, sha256(path)])
37)
38const intervals = [0, 100, 500]
39// All six permutations balance both absolute order and pair order.
40const orders = [[0, 100, 500], [500, 100, 0], [100, 0, 500], [500, 0, 100], [0, 500, 100], [100, 500, 0]]
41const runWorker = (args: Array<string>) =>
42 JSON.parse(execFileSync(process.execPath, [worker, ...args], {
43 encoding: "utf8",
44 timeout: 120_000,
45 maxBuffer: 16 * 1024 * 1024
46 }))
47mkdirSync(output, { recursive: false })
48mkdirSync(join(output, "profiles"))
49mkdirSync(join(output, "harness"))
50for (const file of [...harnessFiles, "packages/effect/runtimeperf/stats.mts"]) {
51 copyFileSync(file, join(output, "harness", basename(file)))
52}
53const report = {
54 complete: false,
55 environment: {
56 date: new Date().toISOString(),
57 node: process.version,
58 v8: process.versions.v8,
59 arch: process.arch,
60 platform: process.platform,
61 release: os.release(),
62 cpu: os.cpus()[0]?.model,
63 execArgv: process.execArgv,
64 nodeOptions: process.env.NODE_OPTIONS ?? ""
65 },
66 source: { revision, sourceTree, hashes, status: git("status", "--short") },
67 measurement: { rounds, count, names, intervals, orders, warmup: false, bootstrapIterations: 10_000 },
68 validations: [] as Array<unknown>,
69 results: [] as Array<any>,
70 summaries: [] as Array<any>
71}
72const save = () => writeFileSync(join(output, "results.json"), JSON.stringify(report, null, 2))
73try {
74 for (const name of names) report.validations.push(runWorker(["validate", name]))
75 save()
76 for (let round = 0; round < rounds; round++) {
77 const rotation = round % names.length
78 for (const name of [...names.slice(rotation), ...names.slice(0, rotation)]) {
79 for (const interval of orders[round % orders.length]) {
80 const profilePath = interval > 0 ? join(output, "profiles", `${name}-${round}-${interval}.cpuprofile`) : ""
81 const sample = runWorker(["measure", name, String(count), String(interval), profilePath])
82 assert.equal(sample.name, name)
83 assert.equal(sample.count, count)
84 assert.equal(sample.interval, interval)
85 assert.ok(Number.isFinite(sample.elapsedMs) && sample.elapsedMs > 0)
86 report.results.push({
87 round,
88 ...sample,
89 ...(profilePath ? { profile: analyzeProfile(JSON.parse(readFileSync(profilePath, "utf8"))) } : {})
90 })
91 }
92 }
93 save()
94 process.stderr.write(`construction round ${round + 1}/${rounds} complete\n`)
95 }
96 assert.equal(git("rev-parse", "HEAD"), revision, "HEAD changed during measurement")
97 assert.equal(git("status", "--porcelain", "--untracked-files=all", "--", "packages/effect/src"), "", "Source changed")
98 for (const [path, hash] of Object.entries(hashes)) {
99 assert.equal(sha256(path), hash, `${path} changed during measurement`)
100 }
101 assert.equal(report.results.length, rounds * names.length * intervals.length)
102 for (const name of names) {
103 const baseline = report.results.filter((sample) => sample.name === name && sample.interval === 0)
104 report.summaries.push({
105 name,
106 baselineMs: aggregate(baseline.map((sample) => sample.elapsedMs)),
107 profiles: intervals.filter((interval) => interval > 0).map((interval) => {
108 const samples = report.results.filter((sample) => sample.name === name && sample.interval === interval)
109 const observed = samples.filter((sample) => sample.profile.totalSamples > 0)
110 assert.deepEqual(samples.map((sample) => sample.round), baseline.map((sample) => sample.round))
111 const sites = new Map<string, any>()
112 for (const sample of samples) {
113 for (const site of sample.profile.sites) {
114 const frame = site.frame
115 const key = `${site.category}:${frame.url}:${frame.lineNumber}:${frame.columnNumber}:${frame.functionName}`
116 const existing = sites.get(key) ?? { ...site, samples: 0 }
117 existing.samples += site.samples
118 sites.set(key, existing)
119 }
120 }
121 return {
122 interval,
123 elapsedMs: aggregate(samples.map((sample) => sample.elapsedMs)),
124 overhead: analyzePairs(baseline.map((sample) => sample.elapsedMs), samples.map((sample) => sample.elapsedMs)),
125 sampleCount: samples.reduce((sum, sample) => sum + sample.profile.totalSamples, 0),
126 observedProcesses: observed.length,
127 sparseProcesses: samples.filter((sample) => sample.profile.totalSamples < 10).length,
128 medianSpanCoverage: median(samples.map((sample) => sample.profile.spanUs / 1000 / sample.elapsedMs)),
129 shares: Object.fromEntries(categories.map((category) => [
130 category,
131 observed.length > 0
132 ? shareInterval(observed.map((sample) => sample.profile.counts[category] / sample.profile.totalSamples))
133 : null
134 ])),
135 topSites: [...sites.values()].sort((a, b) => b.samples - a.samples).slice(0, 20)
136 }
137 })
138 })
139 }
140 report.complete = true
141} finally {
142 save()
143}
144process.stdout.write(`${join(output, "results.json")}\n`)