summaryrefslogtreecommitdiff
path: root/runtime/js
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/js')
-rw-r--r--runtime/js/40_testing.js280
-rw-r--r--runtime/js/90_deno_ns.js1
2 files changed, 278 insertions, 3 deletions
diff --git a/runtime/js/40_testing.js b/runtime/js/40_testing.js
index 0a40e19f1..b79939f4b 100644
--- a/runtime/js/40_testing.js
+++ b/runtime/js/40_testing.js
@@ -409,12 +409,14 @@
// Wrap test function in additional assertion that makes sure
// that the test case does not accidentally exit prematurely.
- function assertExit(fn) {
+ function assertExit(fn, isTest) {
return async function exitSanitizer(...params) {
setExitHandler((exitCode) => {
assert(
false,
- `Test case attempted to exit with exit code: ${exitCode}`,
+ `${
+ isTest ? "Test case" : "Bench"
+ } attempted to exit with exit code: ${exitCode}`,
);
});
@@ -528,6 +530,7 @@
}
const tests = [];
+ const benches = [];
// Main test function provided by Deno.
function test(
@@ -627,6 +630,107 @@
ArrayPrototypePush(tests, testDef);
}
+ // Main bench function provided by Deno.
+ function bench(
+ nameOrFnOrOptions,
+ optionsOrFn,
+ maybeFn,
+ ) {
+ let benchDef;
+ const defaults = {
+ ignore: false,
+ only: false,
+ sanitizeOps: true,
+ sanitizeResources: true,
+ sanitizeExit: true,
+ permissions: null,
+ };
+
+ if (typeof nameOrFnOrOptions === "string") {
+ if (!nameOrFnOrOptions) {
+ throw new TypeError("The bench name can't be empty");
+ }
+ if (typeof optionsOrFn === "function") {
+ benchDef = { 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.",
+ );
+ }
+ benchDef = {
+ ...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()");
+ }
+ benchDef = {
+ ...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");
+ }
+ benchDef = { ...defaults, ...nameOrFnOrOptions, fn, name };
+ }
+
+ benchDef.fn = wrapBenchFnWithSanitizers(
+ reportBenchIteration(benchDef.fn),
+ benchDef,
+ );
+
+ if (benchDef.permissions) {
+ benchDef.fn = withPermissions(
+ benchDef.fn,
+ benchDef.permissions,
+ );
+ }
+
+ ArrayPrototypePush(benches, benchDef);
+ }
+
function formatError(error) {
if (ObjectPrototypeIsPrototypeOf(AggregateErrorPrototype, error)) {
const message = error
@@ -699,10 +803,48 @@
}
}
+ async function runBench(bench) {
+ if (bench.ignore) {
+ return "ignored";
+ }
+
+ const step = new BenchStep({
+ name: bench.name,
+ sanitizeExit: bench.sanitizeExit,
+ warmup: false,
+ });
+
+ try {
+ const warmupIterations = bench.warmupIterations;
+ step.warmup = true;
+
+ for (let i = 0; i < warmupIterations; i++) {
+ await bench.fn(step);
+ }
+
+ const iterations = bench.n;
+ step.warmup = false;
+
+ for (let i = 0; i < iterations; i++) {
+ await bench.fn(step);
+ }
+
+ return "ok";
+ } catch (error) {
+ return {
+ "failed": formatError(error),
+ };
+ }
+ }
+
function getTestOrigin() {
return core.opSync("op_get_test_origin");
}
+ function getBenchOrigin() {
+ return core.opSync("op_get_bench_origin");
+ }
+
function reportTestPlan(plan) {
core.opSync("op_dispatch_test_event", {
plan,
@@ -739,6 +881,53 @@
});
}
+ function reportBenchPlan(plan) {
+ core.opSync("op_dispatch_bench_event", {
+ plan,
+ });
+ }
+
+ function reportBenchConsoleOutput(console) {
+ core.opSync("op_dispatch_bench_event", {
+ output: { console },
+ });
+ }
+
+ function reportBenchWait(description) {
+ core.opSync("op_dispatch_bench_event", {
+ wait: description,
+ });
+ }
+
+ function reportBenchResult(description, result, elapsed) {
+ core.opSync("op_dispatch_bench_event", {
+ result: [description, result, elapsed],
+ });
+ }
+
+ function reportBenchIteration(fn) {
+ return async function benchIteration(step) {
+ let now;
+ if (!step.warmup) {
+ now = benchNow();
+ }
+ await fn(step);
+ if (!step.warmup) {
+ reportIterationTime(benchNow() - now);
+ }
+ };
+ }
+
+ function benchNow() {
+ return core.opSync("op_bench_now");
+ }
+
+ function reportIterationTime(time) {
+ core.opSync("op_dispatch_bench_event", {
+ iterationTime: time,
+ });
+ }
+
async function runTests({
filter = null,
shuffle = null,
@@ -799,6 +988,53 @@
globalThis.console = originalConsole;
}
+ async function runBenchmarks({
+ filter = null,
+ } = {}) {
+ core.setMacrotaskCallback(handleOpSanitizerDelayMacrotask);
+
+ const origin = getBenchOrigin();
+ const originalConsole = globalThis.console;
+
+ globalThis.console = new Console(reportBenchConsoleOutput);
+
+ const only = ArrayPrototypeFilter(benches, (bench) => bench.only);
+ const filtered = ArrayPrototypeFilter(
+ only.length > 0 ? only : benches,
+ createTestFilter(filter),
+ );
+
+ reportBenchPlan({
+ origin,
+ total: filtered.length,
+ filteredOut: benches.length - filtered.length,
+ usedOnly: only.length > 0,
+ });
+
+ for (const bench of filtered) {
+ // TODO(bartlomieju): probably needs some validation?
+ const iterations = bench.n ?? 1000;
+ const warmupIterations = bench.warmup ?? 1000;
+ const description = {
+ origin,
+ name: bench.name,
+ iterations,
+ };
+ bench.n = iterations;
+ bench.warmupIterations = warmupIterations;
+ const earlier = DateNow();
+
+ reportBenchWait(description);
+
+ const result = await runBench(bench);
+ const elapsed = DateNow() - earlier;
+
+ reportBenchResult(description, result, elapsed);
+ }
+
+ globalThis.console = originalConsole;
+ }
+
/**
* @typedef {{
* fn: (t: TestContext) => void | Promise<void>,
@@ -989,6 +1225,27 @@
}
}
+ /**
+ * @typedef {{
+ * name: string;
+ * sanitizeExit: boolean,
+ * warmup: boolean,
+ * }} BenchStepParams
+ */
+ class BenchStep {
+ /** @type {BenchStepParams} */
+ #params;
+
+ /** @param params {BenchStepParams} */
+ constructor(params) {
+ this.#params = params;
+ }
+
+ get name() {
+ return this.#params.name;
+ }
+ }
+
/** @param parentStep {TestStep} */
function createTestContext(parentStep) {
return {
@@ -1121,12 +1378,27 @@
testFn = assertResources(testFn);
}
if (opts.sanitizeExit) {
- testFn = assertExit(testFn);
+ testFn = assertExit(testFn, true);
}
return testFn;
}
/**
+ * @template T {Function}
+ * @param fn {T}
+ * @param opts {{
+ * sanitizeExit: boolean,
+ * }}
+ * @returns {T}
+ */
+ function wrapBenchFnWithSanitizers(fn, opts) {
+ if (opts.sanitizeExit) {
+ fn = assertExit(fn, false);
+ }
+ return fn;
+ }
+
+ /**
* @template T
* @param value {T | undefined}
* @param defaultValue {T}
@@ -1139,9 +1411,11 @@
window.__bootstrap.internals = {
...window.__bootstrap.internals ?? {},
runTests,
+ runBenchmarks,
};
window.__bootstrap.testing = {
test,
+ bench,
};
})(this);
diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js
index d52f267c0..5298d0a69 100644
--- a/runtime/js/90_deno_ns.js
+++ b/runtime/js/90_deno_ns.js
@@ -7,6 +7,7 @@
__bootstrap.denoNs = {
metrics: core.metrics,
test: __bootstrap.testing.test,
+ bench: __bootstrap.testing.bench,
Process: __bootstrap.process.Process,
run: __bootstrap.process.run,
isatty: __bootstrap.tty.isatty,