perf: move "cli/js/40_testing.js" out of main snapshot (#21212)

Closes https://github.com/denoland/deno/issues/21136

---------

Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
This commit is contained in:
Divy Srivastava 2023-11-24 19:46:16 -08:00 committed by GitHub
parent 7d8f0ae038
commit 89424f8e4a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 1269 additions and 1233 deletions

View file

@ -320,8 +320,16 @@ impl DenoSubcommand {
matches!(self, Self::Run(_))
}
pub fn is_test_or_jupyter(&self) -> bool {
matches!(self, Self::Test(_) | Self::Jupyter(_))
// Returns `true` if the subcommand depends on testing infrastructure.
pub fn needs_test(&self) -> bool {
matches!(
self,
Self::Test(_)
| Self::Jupyter(_)
| Self::Repl(_)
| Self::Bench(_)
| Self::Lsp
)
}
}

View file

@ -331,7 +331,6 @@ deno_core::extension!(
esm_entry_point = "ext:cli/99_main.js",
esm = [
dir "js",
"40_testing.js",
"99_main.js"
],
customizer = |ext: &mut deno_core::Extension| {

View file

@ -36,7 +36,7 @@
* }, { raw: true });
* ```
*/
{
(() => {
const internals = Deno[Deno.internal];
const core = internals.core;
@ -428,4 +428,4 @@
}
internals.enableJupyter = enableJupyter;
}
})();

View file

@ -3,36 +3,36 @@
// Do not use primordials because we do not want to depend on the __bootstrap
// namespace.
//
// deno-lint-ignore-file prefer-primordials
// deno-lint-ignore-file
(() => {
const internals = Deno[Deno.internal];
const core = internals.core;
const ops = core.ops;
const core = globalThis.Deno.core;
const ops = core.ops;
const internals = globalThis.__bootstrap.internals;
const {
const {
setExitHandler,
Console,
serializePermissions,
} = internals;
} = internals;
const opSanitizerDelayResolveQueue = [];
let hasSetOpSanitizerDelayMacrotask = false;
const opSanitizerDelayResolveQueue = [];
let hasSetOpSanitizerDelayMacrotask = false;
// Even if every resource is closed by the end of a test, there can be a delay
// until the pending ops have all finished. This function returns a promise
// that resolves when it's (probably) fine to run the op sanitizer.
//
// This is implemented by adding a macrotask callback that runs after the
// all ready async ops resolve, and the timer macrotask. Using just a macrotask
// callback without delaying is sufficient, because when the macrotask callback
// runs after async op dispatch, we know that all async ops that can currently
// return `Poll::Ready` have done so, and have been dispatched to JS.
//
// Worker ops are an exception to this, because there is no way for the user to
// await shutdown of the worker from the thread calling `worker.terminate()`.
// Because of this, we give extra leeway for worker ops to complete, by waiting
// for a whole millisecond if there are pending worker ops.
function opSanitizerDelay(hasPendingWorkerOps) {
// Even if every resource is closed by the end of a test, there can be a delay
// until the pending ops have all finished. This function returns a promise
// that resolves when it's (probably) fine to run the op sanitizer.
//
// This is implemented by adding a macrotask callback that runs after the
// all ready async ops resolve, and the timer macrotask. Using just a macrotask
// callback without delaying is sufficient, because when the macrotask callback
// runs after async op dispatch, we know that all async ops that can currently
// return `Poll::Ready` have done so, and have been dispatched to JS.
//
// Worker ops are an exception to this, because there is no way for the user to
// await shutdown of the worker from the thread calling `worker.terminate()`.
// Because of this, we give extra leeway for worker ops to complete, by waiting
// for a whole millisecond if there are pending worker ops.
function opSanitizerDelay(hasPendingWorkerOps) {
if (!hasSetOpSanitizerDelayMacrotask) {
core.setMacrotaskCallback(handleOpSanitizerDelayMacrotask);
hasSetOpSanitizerDelayMacrotask = true;
@ -47,21 +47,21 @@ function opSanitizerDelay(hasPendingWorkerOps) {
}, hasPendingWorkerOps ? 1 : 0);
});
return p;
}
}
function handleOpSanitizerDelayMacrotask() {
function handleOpSanitizerDelayMacrotask() {
const resolve = opSanitizerDelayResolveQueue.shift();
if (resolve) {
resolve();
return opSanitizerDelayResolveQueue.length === 0;
}
return undefined; // we performed no work, so can skip microtasks checkpoint
}
}
// An async operation to $0 was started in this test, but never completed. This is often caused by not $1.
// An async operation to $0 was started in this test, but never completed. Async operations should not complete in a test if they were not started in that test.
// deno-fmt-ignore
const OP_DETAILS = {
// An async operation to $0 was started in this test, but never completed. This is often caused by not $1.
// An async operation to $0 was started in this test, but never completed. Async operations should not complete in a test if they were not started in that test.
// deno-fmt-ignore
const OP_DETAILS = {
"op_blob_read_part": ["read from a Blob or File", "awaiting the result of a Blob or File read"],
"op_broadcast_recv": ["receive a message from a BroadcastChannel", "closing the BroadcastChannel"],
"op_broadcast_send": ["send a message to a BroadcastChannel", "closing the BroadcastChannel"],
@ -133,24 +133,24 @@ const OP_DETAILS = {
"op_ws_send_binary_ab": ["send a message on a WebSocket", "closing a `WebSocket` or `WebSocketStream`"],
"op_ws_send_ping": ["send a message on a WebSocket", "closing a `WebSocket` or `WebSocketStream`"],
"op_ws_send_pong": ["send a message on a WebSocket", "closing a `WebSocket` or `WebSocketStream`"],
};
};
let opIdHostRecvMessage = -1;
let opIdHostRecvCtrl = -1;
let opNames = null;
let opIdHostRecvMessage = -1;
let opIdHostRecvCtrl = -1;
let opNames = null;
function populateOpNames() {
function populateOpNames() {
opNames = core.ops.op_op_names();
opIdHostRecvMessage = opNames.indexOf("op_host_recv_message");
opIdHostRecvCtrl = opNames.indexOf("op_host_recv_ctrl");
}
}
// Wrap test function in additional assertion that makes sure
// the test case does not leak async "ops" - ie. number of async
// completed ops after the test is the same as number of dispatched
// ops. Note that "unref" ops are ignored since in nature that are
// optional.
function assertOps(fn) {
// Wrap test function in additional assertion that makes sure
// the test case does not leak async "ops" - ie. number of async
// completed ops after the test is the same as number of dispatched
// ops. Note that "unref" ops are ignored since in nature that are
// optional.
function assertOps(fn) {
/** @param desc {TestDescription | TestStepDescription} */
return async function asyncOpSanitizer(desc) {
if (opNames === null) populateOpNames();
@ -262,11 +262,13 @@ function assertOps(fn) {
}
}
return { failed: { leakedOps: [details, core.isOpCallTracingEnabled()] } };
return {
failed: { leakedOps: [details, core.isOpCallTracingEnabled()] },
};
}
};
}
function prettyResourceNames(name) {
function prettyResourceNames(name) {
switch (name) {
case "fsFile":
return ["A file", "opened", "closed"];
@ -331,9 +333,9 @@ function prettyResourceNames(name) {
default:
return [`A "${name}" resource`, "created", "cleaned up"];
}
}
}
function resourceCloseHint(name) {
function resourceCloseHint(name) {
switch (name) {
case "fsFile":
return "Close the file handle by calling `file.close()`.";
@ -398,12 +400,12 @@ function resourceCloseHint(name) {
default:
return "Close the resource before the end of the test.";
}
}
}
// Wrap test function in additional assertion that makes sure
// the test case does not "leak" resources - ie. resource table after
// the test has exactly the same contents as before the test.
function assertResources(fn) {
// Wrap test function in additional assertion that makes sure
// the test case does not "leak" resources - ie. resource table after
// the test has exactly the same contents as before the test.
function assertResources(fn) {
/** @param desc {TestDescription | TestStepDescription} */
return async function resourceSanitizer(desc) {
const pre = core.resources();
@ -440,11 +442,11 @@ function assertResources(fn) {
}
return { failed: { leakedResources: details } };
};
}
}
// Wrap test function in additional assertion that makes sure
// that the test case does not accidentally exit prematurely.
function assertExit(fn, isTest) {
// Wrap test function in additional assertion that makes sure
// that the test case does not accidentally exit prematurely.
function assertExit(fn, isTest) {
return async function exitSanitizer(...params) {
setExitHandler((exitCode) => {
throw new Error(
@ -461,9 +463,9 @@ function assertExit(fn, isTest) {
setExitHandler(null);
}
};
}
}
function wrapOuter(fn, desc) {
function wrapOuter(fn, desc) {
return async function outerWrapped() {
try {
if (desc.ignore) {
@ -480,9 +482,9 @@ function wrapOuter(fn, desc) {
state.completed = true;
}
};
}
}
function wrapInner(fn) {
function wrapInner(fn) {
/** @param desc {TestDescription | TestStepDescription} */
return async function innerWrapped(desc) {
function getRunningStepDescs() {
@ -520,7 +522,9 @@ function wrapInner(fn) {
if (usesSanitizer(desc) && runningStepDescs.length > 0) {
return {
failed: { hasSanitizersAndOverlaps: runningStepDescs.map(getFullName) },
failed: {
hasSanitizersAndOverlaps: runningStepDescs.map(getFullName),
},
};
}
await fn(testStates.get(desc.id).context);
@ -536,19 +540,19 @@ function wrapInner(fn) {
}
return failedSteps == 0 ? null : { failed: { failedSteps } };
};
}
}
function pledgePermissions(permissions) {
function pledgePermissions(permissions) {
return ops.op_pledge_test_permissions(
serializePermissions(permissions),
);
}
}
function restorePermissions(token) {
function restorePermissions(token) {
ops.op_restore_test_permissions(token);
}
}
function withPermissions(fn, permissions) {
function withPermissions(fn, permissions) {
return async function applyPermissions(...params) {
const token = pledgePermissions(permissions);
@ -558,22 +562,22 @@ function withPermissions(fn, permissions) {
restorePermissions(token);
}
};
}
}
const ESCAPE_ASCII_CHARS = [
const ESCAPE_ASCII_CHARS = [
["\b", "\\b"],
["\f", "\\f"],
["\t", "\\t"],
["\n", "\\n"],
["\r", "\\r"],
["\v", "\\v"],
];
];
/**
/**
* @param {string} name
* @returns {string}
*/
function escapeName(name) {
function escapeName(name) {
// Check if we need to escape a character
for (let i = 0; i < name.length; i++) {
const ch = name.charCodeAt(i);
@ -588,9 +592,9 @@ function escapeName(name) {
// We didn't need to escape anything, return original string
return name;
}
}
/**
/**
* @typedef {{
* id: number,
* name: string,
@ -646,27 +650,27 @@ function escapeName(name) {
* }} 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;
/** @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);
const registerTestIdRetBuf = new Uint32Array(1);
const registerTestIdRetBufU8 = new Uint8Array(registerTestIdRetBuf.buffer);
function testInner(
function testInner(
nameOrFnOrOptions,
optionsOrFn,
maybeFn,
overrides = {},
) {
) {
if (typeof ops.op_register_test != "function") {
return;
}
@ -777,37 +781,37 @@ function testInner(
children: [],
completed: false,
});
}
}
// Main test function provided by Deno.
function test(
// Main test function provided by Deno.
function test(
nameOrFnOrOptions,
optionsOrFn,
maybeFn,
) {
) {
return testInner(nameOrFnOrOptions, optionsOrFn, maybeFn);
}
}
test.ignore = function (nameOrFnOrOptions, optionsOrFn, maybeFn) {
test.ignore = function (nameOrFnOrOptions, optionsOrFn, maybeFn) {
return testInner(nameOrFnOrOptions, optionsOrFn, maybeFn, { ignore: true });
};
};
test.only = function (
test.only = function (
nameOrFnOrOptions,
optionsOrFn,
maybeFn,
) {
) {
return testInner(nameOrFnOrOptions, optionsOrFn, maybeFn, { only: true });
};
};
let registeredWarmupBench = false;
let registeredWarmupBench = false;
// Main bench function provided by Deno.
function bench(
// Main bench function provided by Deno.
function bench(
nameOrFnOrOptions,
optionsOrFn,
maybeFn,
) {
) {
if (typeof ops.op_register_bench != "function") {
return;
}
@ -919,16 +923,24 @@ function bench(
const { id, origin } = ops.op_register_bench(benchDesc);
benchDesc.id = id;
benchDesc.origin = origin;
}
}
function compareMeasurements(a, b) {
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) {
function benchStats(
n,
highPrecision,
usedExplicitTimers,
avg,
min,
max,
all,
) {
return {
n,
min,
@ -941,9 +953,9 @@ function benchStats(n, highPrecision, usedExplicitTimers, avg, min, max, all) {
highPrecision,
usedExplicitTimers,
};
}
}
async function benchMeasure(timeBudget, fn, async, context) {
async function benchMeasure(timeBudget, fn, async, context) {
let n = 0;
let avg = 0;
let wavg = 0;
@ -1103,10 +1115,10 @@ async function benchMeasure(timeBudget, fn, async, context) {
max,
all,
);
}
}
/** @param desc {BenchDescription} */
function createBenchContext(desc) {
/** @param desc {BenchDescription} */
function createBenchContext(desc) {
return {
[Symbol.toStringTag]: "BenchContext",
name: desc.name,
@ -1118,7 +1130,9 @@ function createBenchContext(desc) {
);
}
if (currentBenchUserExplicitStart != null) {
throw new TypeError("BenchContext::start() has already been invoked.");
throw new TypeError(
"BenchContext::start() has already been invoked.",
);
}
currentBenchUserExplicitStart = benchNow();
},
@ -1135,10 +1149,10 @@ function createBenchContext(desc) {
currentBenchUserExplicitEnd = end;
},
};
}
}
/** Wrap a user benchmark function in one which returns a structured result. */
function wrapBenchmark(desc) {
/** 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;
@ -1164,7 +1178,12 @@ function wrapBenchmark(desc) {
const benchTimeInMs = 500;
const context = createBenchContext(desc);
const stats = await benchMeasure(benchTimeInMs, fn, desc.async, context);
const stats = await benchMeasure(
benchTimeInMs,
fn,
desc.async,
context,
);
return { ok: stats };
} catch (error) {
@ -1178,24 +1197,24 @@ function wrapBenchmark(desc) {
if (token !== null) restorePermissions(token);
}
};
}
}
function benchNow() {
function benchNow() {
return ops.op_bench_now();
}
}
function getFullName(desc) {
function getFullName(desc) {
if ("parent" in desc) {
return `${getFullName(desc.parent)} ... ${desc.name}`;
}
return desc.name;
}
}
function usesSanitizer(desc) {
function usesSanitizer(desc) {
return desc.sanitizeResources || desc.sanitizeOps || desc.sanitizeExit;
}
}
function stepReportResult(desc, result, elapsed) {
function stepReportResult(desc, result, elapsed) {
const state = testStates.get(desc.id);
for (const childDesc of state.children) {
stepReportResult(childDesc, { failed: "incomplete" }, 0);
@ -1207,10 +1226,10 @@ function stepReportResult(desc, result, elapsed) {
} else {
ops.op_test_event_step_result_failed(desc.id, result.failed, elapsed);
}
}
}
/** @param desc {TestDescription | TestStepDescription} */
function createTestContext(desc) {
/** @param desc {TestDescription | TestStepDescription} */
function createTestContext(desc) {
let parent;
let level;
let rootId;
@ -1254,7 +1273,9 @@ function createTestContext(desc) {
let stepDesc;
if (typeof nameOrFnOrOptions === "string") {
if (!Object.prototype.isPrototypeOf.call(Function.prototype, maybeFn)) {
if (
!Object.prototype.isPrototypeOf.call(Function.prototype, maybeFn)
) {
throw new TypeError("Expected function for second argument.");
}
stepDesc = {
@ -1324,16 +1345,16 @@ function createTestContext(desc) {
return result == "ok";
},
};
}
}
/**
/**
* Wrap a user test function in one which returns a structured result.
* @template T {Function}
* @param testFn {T}
* @param desc {TestDescription | TestStepDescription}
* @returns {T}
*/
function wrapTest(desc) {
function wrapTest(desc) {
let testFn = wrapInner(desc.fn);
if (desc.sanitizeOps) {
testFn = assertOps(testFn);
@ -1348,8 +1369,8 @@ function wrapTest(desc) {
testFn = withPermissions(testFn, desc.permissions);
}
return wrapOuter(testFn, desc);
}
}
import { denoNs } from "ext:runtime/90_deno_ns.js";
denoNs.bench = bench;
denoNs.test = test;
globalThis.Deno.bench = bench;
globalThis.Deno.test = test;
})();

View file

@ -1,3 +1,2 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
import "ext:cli/40_testing.js";
import "ext:cli/runtime/js/99_main.js";

View file

@ -639,9 +639,13 @@ impl CliMainWorkerFactory {
options,
);
if self.shared.subcommand.is_test_or_jupyter() {
if self.shared.subcommand.needs_test() {
worker.js_runtime.execute_script_static(
"40_jupyter.js",
"ext:cli/40_testing.js",
include_str!("js/40_testing.js"),
)?;
worker.js_runtime.execute_script_static(
"ext:cli/40_jupyter.js",
include_str!("js/40_jupyter.js"),
)?;
}

View file

@ -448,6 +448,11 @@ const finalDenoNs = {
resources: core.resources,
close: core.close,
...denoNs,
// Deno.test and Deno.bench are noops here, but kept for compatibility; so
// that they don't cause errors when used outside of `deno test`/`deno bench`
// contexts.
test: () => {},
bench: () => {},
};
const {