mirror of
https://github.com/denoland/deno.git
synced 2024-11-21 20:38:55 +00:00
chore(cli): split 40_testing (#22112)
No code changes -- just splitting 40_testing into three files and removing a couple of unused lines of code.
This commit is contained in:
parent
e06be89143
commit
7038074c85
426
cli/js/40_bench.js
Normal file
426
cli/js/40_bench.js
Normal file
@ -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;
|
@ -1,7 +1,8 @@
|
||||
// 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, withPermissions } from "ext:cli/40_test_common.js";
|
||||
|
||||
const ops = core.ops;
|
||||
const {
|
||||
ArrayPrototypeFilter,
|
||||
@ -14,21 +15,76 @@ const {
|
||||
MapPrototypeGet,
|
||||
MapPrototypeHas,
|
||||
MapPrototypeSet,
|
||||
MathCeil,
|
||||
ObjectKeys,
|
||||
Promise,
|
||||
SafeArrayIterator,
|
||||
Set,
|
||||
StringPrototypeReplaceAll,
|
||||
SymbolToStringTag,
|
||||
TypeError,
|
||||
} = primordials;
|
||||
|
||||
import { setExitHandler } from "ext:runtime/30_os.js";
|
||||
import { Console } from "ext:deno_console/01_console.js";
|
||||
import { serializePermissions } from "ext:runtime/10_permissions.js";
|
||||
import { setTimeout } from "ext:deno_web/02_timers.js";
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* id: number,
|
||||
* name: string,
|
||||
* fn: TestFunction
|
||||
* origin: string,
|
||||
* location: TestLocation,
|
||||
* ignore: boolean,
|
||||
* only: boolean.
|
||||
* sanitizeOps: boolean,
|
||||
* sanitizeResources: boolean,
|
||||
* sanitizeExit: boolean,
|
||||
* permissions: PermissionOptions,
|
||||
* }} TestDescription
|
||||
*
|
||||
* @typedef {{
|
||||
* id: number,
|
||||
* name: string,
|
||||
* fn: TestFunction
|
||||
* origin: string,
|
||||
* location: TestLocation,
|
||||
* ignore: boolean,
|
||||
* level: number,
|
||||
* parent: TestDescription | TestStepDescription,
|
||||
* rootId: number,
|
||||
* rootName: String,
|
||||
* sanitizeOps: boolean,
|
||||
* sanitizeResources: boolean,
|
||||
* sanitizeExit: boolean,
|
||||
* }} TestStepDescription
|
||||
*
|
||||
* @typedef {{
|
||||
* context: TestContext,
|
||||
* children: TestStepDescription[],
|
||||
* completed: boolean,
|
||||
* }} TestState
|
||||
*
|
||||
* @typedef {{
|
||||
* context: TestContext,
|
||||
* children: TestStepDescription[],
|
||||
* completed: boolean,
|
||||
* failed: boolean,
|
||||
* }} TestStepState
|
||||
*
|
||||
* @typedef {{
|
||||
* id: number,
|
||||
* name: string,
|
||||
* fn: BenchFunction
|
||||
* origin: string,
|
||||
* ignore: boolean,
|
||||
* only: boolean.
|
||||
* sanitizeExit: boolean,
|
||||
* permissions: PermissionOptions,
|
||||
* }} BenchDescription
|
||||
*/
|
||||
|
||||
/** @type {Map<number, TestState | TestStepState>} */
|
||||
const testStates = new Map();
|
||||
|
||||
const opSanitizerDelayResolveQueue = [];
|
||||
let hasSetOpSanitizerDelayMacrotask = false;
|
||||
|
||||
@ -560,126 +616,6 @@ function wrapInner(fn) {
|
||||
};
|
||||
}
|
||||
|
||||
function pledgePermissions(permissions) {
|
||||
return ops.op_pledge_test_permissions(
|
||||
serializePermissions(permissions),
|
||||
);
|
||||
}
|
||||
|
||||
function restorePermissions(token) {
|
||||
ops.op_restore_test_permissions(token);
|
||||
}
|
||||
|
||||
function withPermissions(fn, permissions) {
|
||||
return async function applyPermissions(...params) {
|
||||
const token = pledgePermissions(permissions);
|
||||
|
||||
try {
|
||||
return await fn(...new SafeArrayIterator(params));
|
||||
} finally {
|
||||
restorePermissions(token);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const ESCAPE_ASCII_CHARS = [
|
||||
["\b", "\\b"],
|
||||
["\f", "\\f"],
|
||||
["\t", "\\t"],
|
||||
["\n", "\\n"],
|
||||
["\r", "\\r"],
|
||||
["\v", "\\v"],
|
||||
];
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {string}
|
||||
*/
|
||||
function escapeName(name) {
|
||||
// Check if we need to escape a character
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
const ch = name.charCodeAt(i);
|
||||
if (ch <= 13 && ch >= 8) {
|
||||
// Slow path: We do need to escape it
|
||||
for (const [escape, replaceWith] of ESCAPE_ASCII_CHARS) {
|
||||
name = StringPrototypeReplaceAll(name, escape, replaceWith);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
// We didn't need to escape anything, return original string
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* id: number,
|
||||
* name: string,
|
||||
* fn: TestFunction
|
||||
* origin: string,
|
||||
* location: TestLocation,
|
||||
* ignore: boolean,
|
||||
* only: boolean.
|
||||
* sanitizeOps: boolean,
|
||||
* sanitizeResources: boolean,
|
||||
* sanitizeExit: boolean,
|
||||
* permissions: PermissionOptions,
|
||||
* }} TestDescription
|
||||
*
|
||||
* @typedef {{
|
||||
* id: number,
|
||||
* name: string,
|
||||
* fn: TestFunction
|
||||
* origin: string,
|
||||
* location: TestLocation,
|
||||
* ignore: boolean,
|
||||
* level: number,
|
||||
* parent: TestDescription | TestStepDescription,
|
||||
* rootId: number,
|
||||
* rootName: String,
|
||||
* sanitizeOps: boolean,
|
||||
* sanitizeResources: boolean,
|
||||
* sanitizeExit: boolean,
|
||||
* }} TestStepDescription
|
||||
*
|
||||
* @typedef {{
|
||||
* context: TestContext,
|
||||
* children: TestStepDescription[],
|
||||
* completed: boolean,
|
||||
* }} TestState
|
||||
*
|
||||
* @typedef {{
|
||||
* context: TestContext,
|
||||
* children: TestStepDescription[],
|
||||
* completed: boolean,
|
||||
* failed: boolean,
|
||||
* }} TestStepState
|
||||
*
|
||||
* @typedef {{
|
||||
* id: number,
|
||||
* name: string,
|
||||
* fn: BenchFunction
|
||||
* origin: string,
|
||||
* ignore: boolean,
|
||||
* only: boolean.
|
||||
* sanitizeExit: boolean,
|
||||
* permissions: PermissionOptions,
|
||||
* }} BenchDescription
|
||||
*/
|
||||
|
||||
/** @type {Map<number, TestState | TestStepState>} */
|
||||
const testStates = new Map();
|
||||
/** @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;
|
||||
|
||||
const registerTestIdRetBuf = new Uint32Array(1);
|
||||
const registerTestIdRetBufU8 = new Uint8Array(registerTestIdRetBuf.buffer);
|
||||
|
||||
@ -689,10 +625,6 @@ function testInner(
|
||||
maybeFn,
|
||||
overrides = {},
|
||||
) {
|
||||
if (typeof ops.op_register_test != "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
let testDesc;
|
||||
const defaults = {
|
||||
ignore: false,
|
||||
@ -822,405 +754,6 @@ test.only = function (
|
||||
return testInner(nameOrFnOrOptions, optionsOrFn, maybeFn, { only: true });
|
||||
};
|
||||
|
||||
let registeredWarmupBench = false;
|
||||
|
||||
// Main bench function provided by Deno.
|
||||
function bench(
|
||||
nameOrFnOrOptions,
|
||||
optionsOrFn,
|
||||
maybeFn,
|
||||
) {
|
||||
if (typeof ops.op_register_bench != "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
function getFullName(desc) {
|
||||
if ("parent" in desc) {
|
||||
return `${getFullName(desc.parent)} ... ${desc.name}`;
|
||||
@ -1388,5 +921,4 @@ function wrapTest(desc) {
|
||||
return wrapOuter(testFn, desc);
|
||||
}
|
||||
|
||||
globalThis.Deno.bench = bench;
|
||||
globalThis.Deno.test = test;
|
60
cli/js/40_test_common.js
Normal file
60
cli/js/40_test_common.js
Normal file
@ -0,0 +1,60 @@
|
||||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
||||
import { core, primordials } from "ext:core/mod.js";
|
||||
import { serializePermissions } from "ext:runtime/10_permissions.js";
|
||||
const ops = core.ops;
|
||||
const {
|
||||
StringPrototypeReplaceAll,
|
||||
SafeArrayIterator,
|
||||
} = primordials;
|
||||
|
||||
const ESCAPE_ASCII_CHARS = [
|
||||
["\b", "\\b"],
|
||||
["\f", "\\f"],
|
||||
["\t", "\\t"],
|
||||
["\n", "\\n"],
|
||||
["\r", "\\r"],
|
||||
["\v", "\\v"],
|
||||
];
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {string}
|
||||
*/
|
||||
export function escapeName(name) {
|
||||
// Check if we need to escape a character
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
const ch = name.charCodeAt(i);
|
||||
if (ch <= 13 && ch >= 8) {
|
||||
// Slow path: We do need to escape it
|
||||
for (const [escape, replaceWith] of ESCAPE_ASCII_CHARS) {
|
||||
name = StringPrototypeReplaceAll(name, escape, replaceWith);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
// We didn't need to escape anything, return original string
|
||||
return name;
|
||||
}
|
||||
|
||||
export function pledgePermissions(permissions) {
|
||||
return ops.op_pledge_test_permissions(
|
||||
serializePermissions(permissions),
|
||||
);
|
||||
}
|
||||
|
||||
export function restorePermissions(token) {
|
||||
ops.op_restore_test_permissions(token);
|
||||
}
|
||||
|
||||
export function withPermissions(fn, permissions) {
|
||||
return async function applyPermissions(...params) {
|
||||
const token = pledgePermissions(permissions);
|
||||
|
||||
try {
|
||||
return await fn(...new SafeArrayIterator(params));
|
||||
} finally {
|
||||
restorePermissions(token);
|
||||
}
|
||||
};
|
||||
}
|
@ -633,14 +633,20 @@ impl CliMainWorkerFactory {
|
||||
);
|
||||
|
||||
if self.shared.subcommand.needs_test() {
|
||||
worker.js_runtime.lazy_load_es_module_from_code(
|
||||
"ext:cli/40_testing.js",
|
||||
deno_core::FastString::StaticAscii(include_str!("js/40_testing.js")),
|
||||
)?;
|
||||
worker.js_runtime.lazy_load_es_module_from_code(
|
||||
"ext:cli/40_jupyter.js",
|
||||
deno_core::FastString::StaticAscii(include_str!("js/40_jupyter.js")),
|
||||
)?;
|
||||
macro_rules! test_file {
|
||||
($($file:literal),*) => {
|
||||
$(worker.js_runtime.lazy_load_es_module_from_code(
|
||||
concat!("ext:cli/", $file),
|
||||
deno_core::FastString::StaticAscii(include_str!(concat!("js/", $file))),
|
||||
)?;)*
|
||||
}
|
||||
}
|
||||
test_file!(
|
||||
"40_test_common.js",
|
||||
"40_test.js",
|
||||
"40_bench.js",
|
||||
"40_jupyter.js"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(CliMainWorker {
|
||||
|
Loading…
Reference in New Issue
Block a user