summaryrefslogtreecommitdiff
path: root/cli/js/40_bench.js
diff options
context:
space:
mode:
Diffstat (limited to 'cli/js/40_bench.js')
-rw-r--r--cli/js/40_bench.js426
1 files changed, 426 insertions, 0 deletions
diff --git a/cli/js/40_bench.js b/cli/js/40_bench.js
new file mode 100644
index 000000000..b7a93038c
--- /dev/null
+++ b/cli/js/40_bench.js
@@ -0,0 +1,426 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+// deno-lint-ignore-file
+
+import { core, primordials } from "ext:core/mod.js";
+import {
+ escapeName,
+ pledgePermissions,
+ restorePermissions,
+} from "ext:cli/40_test_common.js";
+import { Console } from "ext:deno_console/01_console.js";
+import { setExitHandler } from "ext:runtime/30_os.js";
+const ops = core.ops;
+const {
+ ArrayPrototypePush,
+ Error,
+ MathCeil,
+ SymbolToStringTag,
+ TypeError,
+} = primordials;
+
+/** @type {number | null} */
+let currentBenchId = null;
+// These local variables are used to track time measurements at
+// `BenchContext::{start,end}` calls. They are global instead of using a state
+// map to minimise the overhead of assigning them.
+/** @type {number | null} */
+let currentBenchUserExplicitStart = null;
+/** @type {number | null} */
+let currentBenchUserExplicitEnd = null;
+
+let registeredWarmupBench = false;
+
+// Main bench function provided by Deno.
+function bench(
+ nameOrFnOrOptions,
+ optionsOrFn,
+ maybeFn,
+) {
+ if (!registeredWarmupBench) {
+ registeredWarmupBench = true;
+ const warmupBenchDesc = {
+ name: "<warmup>",
+ fn: function warmup() {},
+ async: false,
+ ignore: false,
+ baseline: false,
+ only: false,
+ sanitizeExit: true,
+ permissions: null,
+ warmup: true,
+ };
+ warmupBenchDesc.fn = wrapBenchmark(warmupBenchDesc);
+ const { id, origin } = ops.op_register_bench(warmupBenchDesc);
+ warmupBenchDesc.id = id;
+ warmupBenchDesc.origin = origin;
+ }
+
+ let benchDesc;
+ const defaults = {
+ ignore: false,
+ baseline: false,
+ only: false,
+ sanitizeExit: true,
+ permissions: null,
+ };
+
+ if (typeof nameOrFnOrOptions === "string") {
+ if (!nameOrFnOrOptions) {
+ throw new TypeError("The bench name can't be empty");
+ }
+ if (typeof optionsOrFn === "function") {
+ benchDesc = { fn: optionsOrFn, name: nameOrFnOrOptions, ...defaults };
+ } else {
+ if (!maybeFn || typeof maybeFn !== "function") {
+ throw new TypeError("Missing bench function");
+ }
+ if (optionsOrFn.fn != undefined) {
+ throw new TypeError(
+ "Unexpected 'fn' field in options, bench function is already provided as the third argument.",
+ );
+ }
+ if (optionsOrFn.name != undefined) {
+ throw new TypeError(
+ "Unexpected 'name' field in options, bench name is already provided as the first argument.",
+ );
+ }
+ benchDesc = {
+ ...defaults,
+ ...optionsOrFn,
+ fn: maybeFn,
+ name: nameOrFnOrOptions,
+ };
+ }
+ } else if (typeof nameOrFnOrOptions === "function") {
+ if (!nameOrFnOrOptions.name) {
+ throw new TypeError("The bench function must have a name");
+ }
+ if (optionsOrFn != undefined) {
+ throw new TypeError("Unexpected second argument to Deno.bench()");
+ }
+ if (maybeFn != undefined) {
+ throw new TypeError("Unexpected third argument to Deno.bench()");
+ }
+ benchDesc = {
+ ...defaults,
+ fn: nameOrFnOrOptions,
+ name: nameOrFnOrOptions.name,
+ };
+ } else {
+ let fn;
+ let name;
+ if (typeof optionsOrFn === "function") {
+ fn = optionsOrFn;
+ if (nameOrFnOrOptions.fn != undefined) {
+ throw new TypeError(
+ "Unexpected 'fn' field in options, bench function is already provided as the second argument.",
+ );
+ }
+ name = nameOrFnOrOptions.name ?? fn.name;
+ } else {
+ if (
+ !nameOrFnOrOptions.fn || typeof nameOrFnOrOptions.fn !== "function"
+ ) {
+ throw new TypeError(
+ "Expected 'fn' field in the first argument to be a bench function.",
+ );
+ }
+ fn = nameOrFnOrOptions.fn;
+ name = nameOrFnOrOptions.name ?? fn.name;
+ }
+ if (!name) {
+ throw new TypeError("The bench name can't be empty");
+ }
+ benchDesc = { ...defaults, ...nameOrFnOrOptions, fn, name };
+ }
+
+ const AsyncFunction = (async () => {}).constructor;
+ benchDesc.async = AsyncFunction === benchDesc.fn.constructor;
+ benchDesc.fn = wrapBenchmark(benchDesc);
+ benchDesc.warmup = false;
+ benchDesc.name = escapeName(benchDesc.name);
+
+ const { id, origin } = ops.op_register_bench(benchDesc);
+ benchDesc.id = id;
+ benchDesc.origin = origin;
+}
+
+function compareMeasurements(a, b) {
+ if (a > b) return 1;
+ if (a < b) return -1;
+
+ return 0;
+}
+
+function benchStats(
+ n,
+ highPrecision,
+ usedExplicitTimers,
+ avg,
+ min,
+ max,
+ all,
+) {
+ return {
+ n,
+ min,
+ max,
+ p75: all[MathCeil(n * (75 / 100)) - 1],
+ p99: all[MathCeil(n * (99 / 100)) - 1],
+ p995: all[MathCeil(n * (99.5 / 100)) - 1],
+ p999: all[MathCeil(n * (99.9 / 100)) - 1],
+ avg: !highPrecision ? (avg / n) : MathCeil(avg / n),
+ highPrecision,
+ usedExplicitTimers,
+ };
+}
+
+async function benchMeasure(timeBudget, fn, async, context) {
+ let n = 0;
+ let avg = 0;
+ let wavg = 0;
+ let usedExplicitTimers = false;
+ const all = [];
+ let min = Infinity;
+ let max = -Infinity;
+ const lowPrecisionThresholdInNs = 1e4;
+
+ // warmup step
+ let c = 0;
+ let iterations = 20;
+ let budget = 10 * 1e6;
+
+ if (!async) {
+ while (budget > 0 || iterations-- > 0) {
+ const t1 = benchNow();
+ fn(context);
+ const t2 = benchNow();
+ const totalTime = t2 - t1;
+ if (currentBenchUserExplicitStart !== null) {
+ currentBenchUserExplicitStart = null;
+ usedExplicitTimers = true;
+ }
+ if (currentBenchUserExplicitEnd !== null) {
+ currentBenchUserExplicitEnd = null;
+ usedExplicitTimers = true;
+ }
+
+ c++;
+ wavg += totalTime;
+ budget -= totalTime;
+ }
+ } else {
+ while (budget > 0 || iterations-- > 0) {
+ const t1 = benchNow();
+ await fn(context);
+ const t2 = benchNow();
+ const totalTime = t2 - t1;
+ if (currentBenchUserExplicitStart !== null) {
+ currentBenchUserExplicitStart = null;
+ usedExplicitTimers = true;
+ }
+ if (currentBenchUserExplicitEnd !== null) {
+ currentBenchUserExplicitEnd = null;
+ usedExplicitTimers = true;
+ }
+
+ c++;
+ wavg += totalTime;
+ budget -= totalTime;
+ }
+ }
+
+ wavg /= c;
+
+ // measure step
+ if (wavg > lowPrecisionThresholdInNs) {
+ let iterations = 10;
+ let budget = timeBudget * 1e6;
+
+ if (!async) {
+ while (budget > 0 || iterations-- > 0) {
+ const t1 = benchNow();
+ fn(context);
+ const t2 = benchNow();
+ const totalTime = t2 - t1;
+ let measuredTime = totalTime;
+ if (currentBenchUserExplicitStart !== null) {
+ measuredTime -= currentBenchUserExplicitStart - t1;
+ currentBenchUserExplicitStart = null;
+ }
+ if (currentBenchUserExplicitEnd !== null) {
+ measuredTime -= t2 - currentBenchUserExplicitEnd;
+ currentBenchUserExplicitEnd = null;
+ }
+
+ n++;
+ avg += measuredTime;
+ budget -= totalTime;
+ ArrayPrototypePush(all, measuredTime);
+ if (measuredTime < min) min = measuredTime;
+ if (measuredTime > max) max = measuredTime;
+ }
+ } else {
+ while (budget > 0 || iterations-- > 0) {
+ const t1 = benchNow();
+ await fn(context);
+ const t2 = benchNow();
+ const totalTime = t2 - t1;
+ let measuredTime = totalTime;
+ if (currentBenchUserExplicitStart !== null) {
+ measuredTime -= currentBenchUserExplicitStart - t1;
+ currentBenchUserExplicitStart = null;
+ }
+ if (currentBenchUserExplicitEnd !== null) {
+ measuredTime -= t2 - currentBenchUserExplicitEnd;
+ currentBenchUserExplicitEnd = null;
+ }
+
+ n++;
+ avg += measuredTime;
+ budget -= totalTime;
+ ArrayPrototypePush(all, measuredTime);
+ if (measuredTime < min) min = measuredTime;
+ if (measuredTime > max) max = measuredTime;
+ }
+ }
+ } else {
+ context.start = function start() {};
+ context.end = function end() {};
+ let iterations = 10;
+ let budget = timeBudget * 1e6;
+
+ if (!async) {
+ while (budget > 0 || iterations-- > 0) {
+ const t1 = benchNow();
+ for (let c = 0; c < lowPrecisionThresholdInNs; c++) {
+ fn(context);
+ }
+ const iterationTime = (benchNow() - t1) / lowPrecisionThresholdInNs;
+
+ n++;
+ avg += iterationTime;
+ ArrayPrototypePush(all, iterationTime);
+ if (iterationTime < min) min = iterationTime;
+ if (iterationTime > max) max = iterationTime;
+ budget -= iterationTime * lowPrecisionThresholdInNs;
+ }
+ } else {
+ while (budget > 0 || iterations-- > 0) {
+ const t1 = benchNow();
+ for (let c = 0; c < lowPrecisionThresholdInNs; c++) {
+ await fn(context);
+ currentBenchUserExplicitStart = null;
+ currentBenchUserExplicitEnd = null;
+ }
+ const iterationTime = (benchNow() - t1) / lowPrecisionThresholdInNs;
+
+ n++;
+ avg += iterationTime;
+ ArrayPrototypePush(all, iterationTime);
+ if (iterationTime < min) min = iterationTime;
+ if (iterationTime > max) max = iterationTime;
+ budget -= iterationTime * lowPrecisionThresholdInNs;
+ }
+ }
+ }
+
+ all.sort(compareMeasurements);
+ return benchStats(
+ n,
+ wavg > lowPrecisionThresholdInNs,
+ usedExplicitTimers,
+ avg,
+ min,
+ max,
+ all,
+ );
+}
+
+/** @param desc {BenchDescription} */
+function createBenchContext(desc) {
+ return {
+ [SymbolToStringTag]: "BenchContext",
+ name: desc.name,
+ origin: desc.origin,
+ start() {
+ if (currentBenchId !== desc.id) {
+ throw new TypeError(
+ "The benchmark which this context belongs to is not being executed.",
+ );
+ }
+ if (currentBenchUserExplicitStart != null) {
+ throw new TypeError(
+ "BenchContext::start() has already been invoked.",
+ );
+ }
+ currentBenchUserExplicitStart = benchNow();
+ },
+ end() {
+ const end = benchNow();
+ if (currentBenchId !== desc.id) {
+ throw new TypeError(
+ "The benchmark which this context belongs to is not being executed.",
+ );
+ }
+ if (currentBenchUserExplicitEnd != null) {
+ throw new TypeError("BenchContext::end() has already been invoked.");
+ }
+ currentBenchUserExplicitEnd = end;
+ },
+ };
+}
+
+/** Wrap a user benchmark function in one which returns a structured result. */
+function wrapBenchmark(desc) {
+ const fn = desc.fn;
+ return async function outerWrapped() {
+ let token = null;
+ const originalConsole = globalThis.console;
+ currentBenchId = desc.id;
+
+ try {
+ globalThis.console = new Console((s) => {
+ ops.op_dispatch_bench_event({ output: s });
+ });
+
+ if (desc.permissions) {
+ token = pledgePermissions(desc.permissions);
+ }
+
+ if (desc.sanitizeExit) {
+ setExitHandler((exitCode) => {
+ throw new Error(
+ `Bench attempted to exit with exit code: ${exitCode}`,
+ );
+ });
+ }
+
+ const benchTimeInMs = 500;
+ const context = createBenchContext(desc);
+ const stats = await benchMeasure(
+ benchTimeInMs,
+ fn,
+ desc.async,
+ context,
+ );
+
+ return { ok: stats };
+ } catch (error) {
+ return { failed: core.destructureError(error) };
+ } finally {
+ globalThis.console = originalConsole;
+ currentBenchId = null;
+ currentBenchUserExplicitStart = null;
+ currentBenchUserExplicitEnd = null;
+ if (bench.sanitizeExit) setExitHandler(null);
+ if (token !== null) restorePermissions(token);
+ }
+ };
+}
+
+function benchNow() {
+ return ops.op_bench_now();
+}
+
+globalThis.Deno.bench = bench;