| 1 | import assert from "node:assert/strict" |
| 2 | import { execFileSync } from "node:child_process" |
| 3 | import { createHash } from "node:crypto" |
| 4 | import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs" |
| 5 | import os from "node:os" |
| 6 | import { basename, join, resolve } from "node:path" |
| 7 | import { fileURLToPath } from "node:url" |
| 8 | import { aggregate, analyzePairs, median } from "../../stats.mts" |
| 9 | import { caseNames } from "./fixtures.mts" |
| 10 | import { analyzeProfile, categories, shareInterval } from "./profile.mts" |
| 11 | |
| 12 | const [outputArgument = "tmp/schema-construction", roundsArgument = "24", countArgument = "1000", namesArgument] = |
| 13 | process.argv.slice(2) |
| 14 | const rounds = Number(roundsArgument) |
| 15 | const count = Number(countArgument) |
| 16 | assert.ok(Number.isSafeInteger(rounds) && rounds > 0) |
| 17 | assert.ok(Number.isSafeInteger(count) && count > 1) |
| 18 | const names = namesArgument ? namesArgument.split(",") : caseNames |
| 19 | assert.ok(names.length > 0 && names.every((name) => caseNames.includes(name)) && new Set(names).size === names.length) |
| 20 | const root = process.cwd() |
| 21 | const output = resolve(outputArgument) |
| 22 | const worker = fileURLToPath(new URL("./worker.mts", import.meta.url)) |
| 23 | const git = (...args: Array<string>) => execFileSync("git", args, { encoding: "utf8" }).trim() |
| 24 | const revision = git("rev-parse", "HEAD") |
| 25 | const sourceTree = git("rev-parse", "HEAD:packages/effect/src") |
| 26 | assert.equal( |
| 27 | git("status", "--porcelain", "--untracked-files=all", "--", "packages/effect/src"), |
| 28 | "", |
| 29 | "Source must be clean" |
| 30 | ) |
| 31 | const sha256 = (path: string) => createHash("sha256").update(readFileSync(path)).digest("hex") |
| 32 | const harnessFiles = ["fixtures.mts", "worker.mts", "profile.mts", "run.mts"].map((name) => |
| 33 | fileURLToPath(new URL(name, import.meta.url)) |
| 34 | ) |
| 35 | const hashes = Object.fromEntries( |
| 36 | [...harnessFiles, "packages/effect/runtimeperf/stats.mts", "pnpm-lock.yaml"].map((path) => [path, sha256(path)]) |
| 37 | ) |
| 38 | const intervals = [0, 100, 500] |
| 39 | // All six permutations balance both absolute order and pair order. |
| 40 | const orders = [[0, 100, 500], [500, 100, 0], [100, 0, 500], [500, 0, 100], [0, 500, 100], [100, 500, 0]] |
| 41 | const 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 | })) |
| 47 | mkdirSync(output, { recursive: false }) |
| 48 | mkdirSync(join(output, "profiles")) |
| 49 | mkdirSync(join(output, "harness")) |
| 50 | for (const file of [...harnessFiles, "packages/effect/runtimeperf/stats.mts"]) { |
| 51 | copyFileSync(file, join(output, "harness", basename(file))) |
| 52 | } |
| 53 | const 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 | } |
| 72 | const save = () => writeFileSync(join(output, "results.json"), JSON.stringify(report, null, 2)) |
| 73 | try { |
| 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 | } |
| 144 | process.stdout.write(`${join(output, "results.json")}\n`) |