-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Switch to easier to use benchmark setup stolen from Gadget with profi…
…ling built in
- Loading branch information
Showing
13 changed files
with
327 additions
and
117 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,53 +1,42 @@ | ||
import { Bench } from "tinybench"; | ||
import { withCodSpeed } from "@codspeed/tinybench-plugin"; | ||
import findRoot from "find-root"; | ||
import fs from "fs"; | ||
import { FruitAisle } from "../spec/fixtures/FruitAisle"; | ||
import { LargeRoot } from "../spec/fixtures/LargeRoot"; | ||
import { TestClassModel } from "../spec/fixtures/TestClassModel"; | ||
import { BigTestModelSnapshot, TestModelSnapshot } from "../spec/fixtures/TestModel"; | ||
import { registerPropertyAccess } from "./property-access-model-class"; | ||
|
||
const root = findRoot(__dirname); | ||
const largeRoot = JSON.parse(fs.readFileSync(root + "/spec/fixtures/large-root-snapshot.json", "utf8")); | ||
const fruitAisle = JSON.parse(fs.readFileSync(root + "/spec/fixtures/fruit-aisle-snapshot.json", "utf8")); | ||
|
||
void (async () => { | ||
let suite = new Bench(); | ||
if (process.env.CI) { | ||
suite = withCodSpeed(suite); | ||
import globby from "globby"; | ||
import { hideBin } from "yargs/helpers"; | ||
import yargs from "yargs/yargs"; | ||
import { benchTable, createSuite } from "./benchmark"; | ||
|
||
/** Script for running */ | ||
const argv = yargs(hideBin(process.argv)) | ||
.option("benchmarks", { | ||
alias: ["b", "t"], | ||
type: "string", | ||
describe: "Benchmark file pattern to match", | ||
}) | ||
.usage("Usage: run.ts [options]") | ||
.help().argv; | ||
|
||
export const runAll = async () => { | ||
let benchmarkFiles = await globby(__dirname + "/**/*.benchmark.ts"); | ||
let suite = createSuite(); | ||
|
||
if (argv.benchmarks) { | ||
benchmarkFiles = benchmarkFiles.filter((file) => file.includes(argv.benchmarks!)); | ||
} | ||
console.info("running benchmarks", { benchmarkFiles }); | ||
|
||
suite | ||
.add("instantiating a small root", function () { | ||
TestClassModel.createReadOnly(TestModelSnapshot); | ||
}) | ||
.add("instantiating a large root", function () { | ||
LargeRoot.createReadOnly(largeRoot); | ||
}) | ||
.add("instantiating a large union", function () { | ||
FruitAisle.createReadOnly(fruitAisle); | ||
}) | ||
.add("instantiating a diverse root", function () { | ||
TestClassModel.createReadOnly(BigTestModelSnapshot); | ||
}) | ||
.add("instantiating a small root (mobx-state-tree)", function () { | ||
TestClassModel.create(TestModelSnapshot); | ||
}) | ||
.add("instantiating a large root (mobx-state-tree)", function () { | ||
LargeRoot.create(largeRoot); | ||
}) | ||
.add("instantiating a large union (mobx-state-tree)", function () { | ||
FruitAisle.create(fruitAisle); | ||
}) | ||
.add("instantiating a diverse root (mobx-state-tree)", function () { | ||
TestClassModel.create(BigTestModelSnapshot); | ||
}); | ||
|
||
suite = registerPropertyAccess(suite); | ||
for (const file of benchmarkFiles) { | ||
let benchmark = await import(file); | ||
if (benchmark.default) { | ||
benchmark = benchmark.default; | ||
} | ||
suite = await benchmark.fn(suite); | ||
} | ||
|
||
await suite.warmup(); | ||
await suite.run(); | ||
|
||
console.table(suite.table()); | ||
})(); | ||
console.table(benchTable(suite)); | ||
}; | ||
|
||
if (require.main === module) { | ||
void runAll(); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import { writeFile } from "fs-extra"; | ||
import { compact } from "lodash"; | ||
import { Bench, type Options } from "tinybench"; | ||
import yargs from "yargs"; | ||
import { hideBin } from "yargs/helpers"; | ||
import type { Profiler } from "inspector"; | ||
import { Session } from "inspector"; | ||
|
||
export const newInspectorSession = () => { | ||
const session = new Session(); | ||
const post = (method: string, params?: Record<string, unknown>): any => | ||
new Promise((resolve, reject) => { | ||
session.post(method, params, (err: Error | null, result: any) => { | ||
if (err) { | ||
reject(err); | ||
} else { | ||
resolve(result); | ||
} | ||
}); | ||
}); | ||
|
||
session.connect(); | ||
return { session, post }; | ||
}; | ||
|
||
export type BenchmarkGenerator = ((suite: Bench) => Bench | Promise<Bench>) & { options?: Options }; | ||
|
||
/** | ||
* Set up a new benchmark in our library of benchmarks | ||
* If this file is executed directly, it will run the benchmark | ||
* Otherwise, it will export the benchmark for use in other files | ||
* | ||
* @example | ||
* export default benchmarker((suite) => { | ||
* return suite.add("My Benchmark", async () => { | ||
* // something expensive | ||
* }); | ||
* }); | ||
**/ | ||
export const benchmarker = (fn: BenchmarkGenerator, options?: Options) => { | ||
fn.options = options; | ||
|
||
const err = new NiceStackError(); | ||
const callerFile = (err.stack as unknown as NodeJS.CallSite[])[2].getFileName(); | ||
|
||
if (require.main?.filename === callerFile) { | ||
void runBenchmark(fn); | ||
} else { | ||
return { fn }; | ||
} | ||
}; | ||
|
||
/** Wrap a plain old async function in the weird deferred management code benchmark.js requires */ | ||
export const asyncBench = (fn: () => Promise<void>) => { | ||
return { | ||
defer: true, | ||
fn: async (deferred: any) => { | ||
await fn(); | ||
deferred.resolve(); | ||
}, | ||
}; | ||
}; | ||
|
||
/** Boot up a benchmark suite for registering new cases on */ | ||
export const createSuite = (options: Options = { iterations: 100 }) => { | ||
const suite = new Bench(options); | ||
|
||
suite.addEventListener("error", (event: any) => { | ||
console.error(event); | ||
}); | ||
|
||
return suite; | ||
}; | ||
|
||
/** Run one benchmark function in isolation */ | ||
const runBenchmark = async (fn: BenchmarkGenerator) => { | ||
const args = yargs(hideBin(process.argv)) | ||
.option("p", { | ||
alias: "profile", | ||
default: false, | ||
describe: "profile each benchmarked case as it runs, writing a CPU profile to disk for each", | ||
type: "boolean", | ||
}) | ||
.option("b", { | ||
alias: "blocking", | ||
default: false, | ||
describe: "track event loop blocking time during each iteration, which changes the stats", | ||
type: "boolean", | ||
}).argv; | ||
|
||
let suite = createSuite(fn.options); | ||
|
||
if (args.profile) { | ||
const key = formatDateForFile(); | ||
|
||
const { post } = newInspectorSession(); | ||
await post("Profiler.enable"); | ||
await post("Profiler.setSamplingInterval", { interval: 20 }); | ||
|
||
suite.addEventListener("add", (event) => { | ||
const oldBeforeAll = event.task.opts.beforeAll; | ||
const oldAfterAll = event.task.opts.beforeAll; | ||
|
||
event.task.opts.beforeAll = async function () { | ||
await post("Profiler.start"); | ||
await oldBeforeAll?.call(this); | ||
}; | ||
event.task.opts.afterAll = async function () { | ||
await oldAfterAll?.call(this); | ||
const { profile } = (await post("Profiler.stop")) as Profiler.StopReturnType; | ||
await writeFile(`./bench-${event.task.name}-${key}.cpuprofile`, JSON.stringify(profile)); | ||
}; | ||
}); | ||
} | ||
|
||
suite = await fn(suite); | ||
|
||
console.log("running benchmark"); | ||
|
||
await suite.warmup(); | ||
await suite.run(); | ||
|
||
console.table(benchTable(suite)); | ||
}; | ||
|
||
class NiceStackError extends Error { | ||
constructor() { | ||
super(); | ||
const oldStackTrace = Error.prepareStackTrace; | ||
try { | ||
Error.prepareStackTrace = (err, structuredStackTrace) => structuredStackTrace; | ||
|
||
Error.captureStackTrace(this); | ||
|
||
this.stack; // Invoke the getter for `stack`. | ||
} finally { | ||
Error.prepareStackTrace = oldStackTrace; | ||
} | ||
} | ||
} | ||
|
||
const formatDateForFile = () => { | ||
const now = new Date(); | ||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}_${String( | ||
now.getHours() | ||
).padStart(2, "0")}-${String(now.getMinutes()).padStart(2, "0")}-${String(now.getSeconds()).padStart(2, "0")}`; | ||
}; | ||
|
||
export const benchTable = (bench: Bench) => { | ||
return compact( | ||
bench.tasks.map(({ name: t, result: e }) => { | ||
if (!e) return null; | ||
return { | ||
"Task Name": t, | ||
"ops/sec": e.error ? "NaN" : parseInt(e.hz.toString(), 10).toLocaleString(), | ||
"Average Time (ms)": e.error ? "NaN" : e.mean, | ||
"p99 Time (ms)": e.error ? "NaN" : e.p99, | ||
Margin: e.error ? "NaN" : `\xB1${e.rme.toFixed(2)}%`, | ||
Samples: e.error ? "NaN" : e.samples.length, | ||
}; | ||
}) | ||
); | ||
}; |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,15 @@ | ||
import { Bench } from "tinybench"; | ||
import findRoot from "find-root"; | ||
import fs from "fs"; | ||
import { FruitAisle } from "../spec/fixtures/FruitAisle"; | ||
import { benchmarker } from "./benchmark"; | ||
|
||
const root = findRoot(__dirname); | ||
const fruitBasket = JSON.parse(fs.readFileSync(root + "/spec/fixtures/fruit-aisle-snapshot.json", "utf8")); | ||
|
||
void (async () => { | ||
const suite = new Bench(); | ||
|
||
export default benchmarker(async (suite) => { | ||
suite.add("instantiating a large union", function () { | ||
FruitAisle.createReadOnly(fruitBasket); | ||
}); | ||
|
||
await suite.warmup(); | ||
await suite.run(); | ||
console.table(suite.table()); | ||
})(); | ||
return suite | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.