diff --git a/src/native/corehost/browserhost/loader/index.ts b/src/native/corehost/browserhost/loader/index.ts index db7a8fb61f787c..c1988d51328b87 100644 --- a/src/native/corehost/browserhost/loader/index.ts +++ b/src/native/corehost/browserhost/loader/index.ts @@ -16,7 +16,7 @@ import GitHash from "consts:gitHash"; import { loaderConfig, getLoaderConfig } from "./config"; import { exit, isExited, isRuntimeRunning, addOnExitListener, registerExit, quitNow } from "./exit"; import { invokeLibraryInitializers } from "./lib-initializers"; -import { check, error, info, warn, debug } from "./logging"; +import { check, error, info, warn, debug, fastCheck } from "./logging"; import { dotnetAssert, dotnetLoaderExports, dotnetLogger, dotnetUpdateInternals, dotnetUpdateInternalsSubscriber } from "./cross-module"; import { rejectRunMainPromise, resolveRunMainPromise, getRunMainPromise, abortStartup } from "./run"; @@ -77,6 +77,7 @@ export function dotnetInitializeModule(): RuntimeAPI { Object.assign(dotnetLogger, logger); const assert: AssertType = { check, + fastCheck, }; Object.assign(dotnetAssert, assert); @@ -99,6 +100,7 @@ export function dotnetInitializeModule(): RuntimeAPI { logger.warn, logger.error, assert.check, + assert.fastCheck, dotnetLoaderExports.resolveRunMainPromise, dotnetLoaderExports.rejectRunMainPromise, dotnetLoaderExports.getRunMainPromise, diff --git a/src/native/corehost/browserhost/loader/logging.ts b/src/native/corehost/browserhost/loader/logging.ts index ce6ab84cf94f6e..ee20d8e6008f06 100644 --- a/src/native/corehost/browserhost/loader/logging.ts +++ b/src/native/corehost/browserhost/loader/logging.ts @@ -1,11 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// WASM-TODO: inline the code +export function check(condition: unknown, message: string): asserts condition { + if (!condition) { + throw new Error(`dotnetAssert failed: ${message}`); + } +} -export function check(condition: unknown, messageFactory: string | (() => string)): asserts condition { +// calls to fastCheck will be inlined by rollup +// so that the string formatting or allocation of a closure would only happen in failure cases +// this is important for performance sensitive code paths +export function fastCheck(condition: unknown, messageFactory: (() => string)): asserts condition { if (!condition) { - const message = typeof messageFactory === "string" ? messageFactory : messageFactory(); + const message = messageFactory(); throw new Error(`dotnetAssert failed: ${message}`); } } diff --git a/src/native/libs/Common/JavaScript/cross-module/index.ts b/src/native/libs/Common/JavaScript/cross-module/index.ts index f89b3a73f12793..1118680637d06e 100644 --- a/src/native/libs/Common/JavaScript/cross-module/index.ts +++ b/src/native/libs/Common/JavaScript/cross-module/index.ts @@ -110,19 +110,20 @@ export function dotnetUpdateInternalsSubscriber() { }; const assertLocal: AssertType = { check: table[4], + fastCheck: table[5], }; const loaderExportsLocal: LoaderExports = { - resolveRunMainPromise: table[5], - rejectRunMainPromise: table[6], - getRunMainPromise: table[7], - createPromiseCompletionSource: table[8], - isControllablePromise: table[9], - getPromiseCompletionSource: table[10], - isExited: table[11], - isRuntimeRunning: table[12], - addOnExitListener: table[13], - abortStartup: table[14], - quitNow: table[15], + resolveRunMainPromise: table[6], + rejectRunMainPromise: table[7], + getRunMainPromise: table[8], + createPromiseCompletionSource: table[9], + isControllablePromise: table[10], + getPromiseCompletionSource: table[11], + isExited: table[12], + isRuntimeRunning: table[13], + addOnExitListener: table[14], + abortStartup: table[15], + quitNow: table[16], }; Object.assign(dotnetLoaderExports, loaderExportsLocal); Object.assign(logger, loggerLocal); diff --git a/src/native/libs/Common/JavaScript/types/exchange.ts b/src/native/libs/Common/JavaScript/types/exchange.ts index b65e8264a94c72..f1043e438d7e15 100644 --- a/src/native/libs/Common/JavaScript/types/exchange.ts +++ b/src/native/libs/Common/JavaScript/types/exchange.ts @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { check, error, info, warn, debug } from "../../../../corehost/browserhost/loader/logging"; +import type { check, error, info, warn, debug, fastCheck } from "../../../../corehost/browserhost/loader/logging"; import type { resolveRunMainPromise, rejectRunMainPromise, getRunMainPromise, abortStartup } from "../../../../corehost/browserhost/loader/run"; import type { addOnExitListener, isExited, isRuntimeRunning, quitNow } from "../../../../corehost/browserhost/loader/exit"; @@ -29,6 +29,7 @@ export type LoggerType = { export type AssertType = { check: typeof check, + fastCheck: typeof fastCheck, } export type LoaderExports = { @@ -51,6 +52,7 @@ export type LoaderExportsTable = [ typeof warn, typeof error, typeof check, + typeof fastCheck, typeof resolveRunMainPromise, typeof rejectRunMainPromise, typeof getRunMainPromise, diff --git a/src/native/libs/System.Native.Browser/utils/memory.ts b/src/native/libs/System.Native.Browser/utils/memory.ts index 9b9fb60dd0c617..550801544ec23e 100644 --- a/src/native/libs/System.Native.Browser/utils/memory.ts +++ b/src/native/libs/System.Native.Browser/utils/memory.ts @@ -9,8 +9,8 @@ const min_int64_big = BigInt("-9223372036854775808"); const sharedArrayBufferDefined = typeof SharedArrayBuffer !== "undefined"; export function assertIntInRange(value: Number, min: Number, max: Number) { - dotnetAssert.check(Number.isSafeInteger(value), () => `Value is not an integer: ${value} (${typeof (value)})`); - dotnetAssert.check(value >= min && value <= max, () => `Overflow: value ${value} is out of ${min} ${max} range`); + dotnetAssert.fastCheck(Number.isSafeInteger(value), () => `Value is not an integer: ${value} (${typeof (value)})`); + dotnetAssert.fastCheck(value >= min && value <= max, () => `Overflow: value ${value} is out of ${min} ${max} range`); } /** note: boolean is 8 bits not 32 bits when inside a structure or array */ @@ -82,7 +82,7 @@ export function setHeapI32(offset: MemOffset, value: number): void { * Throws for values which are not 52 bit integer. See Number.isSafeInteger() */ export function setHeapI52(offset: MemOffset, value: number): void { - dotnetAssert.check(Number.isSafeInteger(value), () => `Value is not a safe integer: ${value} (${typeof (value)})`); + dotnetAssert.fastCheck(Number.isSafeInteger(value), () => `Value is not a safe integer: ${value} (${typeof (value)})`); throw new Error("WASM-TODO"); } @@ -90,25 +90,25 @@ export function setHeapI52(offset: MemOffset, value: number): void { * Throws for values which are not 52 bit integer or are negative. See Number.isSafeInteger(). */ export function setHeapU52(offset: MemOffset, value: number): void { - dotnetAssert.check(Number.isSafeInteger(value), () => `Value is not a safe integer: ${value} (${typeof (value)})`); - dotnetAssert.check(value >= 0, "Can't convert negative Number into UInt64"); + dotnetAssert.fastCheck(Number.isSafeInteger(value), () => `Value is not a safe integer: ${value} (${typeof (value)})`); + dotnetAssert.fastCheck(value >= 0, () => "Can't convert negative Number into UInt64"); throw new Error("WASM-TODO"); } export function setHeapI64Big(offset: MemOffset, value: bigint): void { - dotnetAssert.check(typeof value === "bigint", () => `Value is not an bigint: ${value} (${typeof (value)})`); - dotnetAssert.check(value >= min_int64_big && value <= max_int64_big, () => `Overflow: value ${value} is out of ${min_int64_big} ${max_int64_big} range`); + dotnetAssert.fastCheck(typeof value === "bigint", () => `Value is not an bigint: ${value} (${typeof (value)})`); + dotnetAssert.fastCheck(value >= min_int64_big && value <= max_int64_big, () => `Overflow: value ${value} is out of ${min_int64_big} ${max_int64_big} range`); Module.HEAP64[offset >>> 3] = value; } export function setHeapF32(offset: MemOffset, value: number): void { - dotnetAssert.check(typeof value === "number", () => `Value is not a Number: ${value} (${typeof (value)})`); + dotnetAssert.fastCheck(typeof value === "number", () => `Value is not a Number: ${value} (${typeof (value)})`); Module.HEAPF32[offset >>> 2] = value; } export function setHeapF64(offset: MemOffset, value: number): void { - dotnetAssert.check(typeof value === "number", () => `Value is not a Number: ${value} (${typeof (value)})`); + dotnetAssert.fastCheck(typeof value === "number", () => `Value is not a Number: ${value} (${typeof (value)})`); Module.HEAPF64[offset >>> 3] = value; } diff --git a/src/native/rollup.config.defines.js b/src/native/rollup.config.defines.js index aa0d31c92e4892..b4bc4f859b4348 100644 --- a/src/native/rollup.config.defines.js +++ b/src/native/rollup.config.defines.js @@ -52,3 +52,15 @@ export const envConstants = { gitHash, isContinuousIntegrationBuild, }; + +/* eslint-disable quotes */ +export const inlinefastCheck = [ + { + pattern: 'dotnetAssert\\.fastCheck\\(([^,]*), \\(\\) => *`([^`]*)`\\);', + replacement: (match) => `if (!(${match[1]})) throw new Error(\`Assert failed: ${match[2]}\`); // inlined fastCheck` + }, + { + pattern: 'dotnetAssert\\.fastCheck\\(([^,]*), \\(\\) => *"([^"]*)"\\);', + replacement: (match) => `if (!(${match[1]})) throw new Error(\`Assert failed: ${match[2]}\`); // inlined fastCheck` + }, +]; diff --git a/src/native/rollup.config.js b/src/native/rollup.config.js index 24ec8ce9f30414..241403ff6546bd 100644 --- a/src/native/rollup.config.js +++ b/src/native/rollup.config.js @@ -8,9 +8,10 @@ import dts from "rollup-plugin-dts"; import { externalDependencies, envConstants, banner, banner_dts, isDebug, staticLibDestination, - keep_classnames, keep_fnames, reserved + keep_classnames, keep_fnames, reserved, + inlinefastCheck, } from "./rollup.config.defines.js"; -import { terserPlugin, writeOnChangePlugin, consts, onwarn, alwaysLF, iife2fe, emsAmbient, sourcemapPathTransform } from "./rollup.config.plugins.js"; +import { terserPlugin, writeOnChangePlugin, consts, onwarn, alwaysLF, iife2fe, emsAmbient, regexReplace, sourcemapPathTransform } from "./rollup.config.plugins.js"; import { promises as fs } from "fs"; const dotnetDTS = { @@ -189,6 +190,7 @@ function configure({ input, output, terser, external }) { external: external ? [...external, ...externalDependencies] : externalDependencies, plugins: [ nodeResolve(), + regexReplace([...inlinefastCheck]), consts(envConstants), typescript({ tsconfig: "./tsconfig.json", diff --git a/src/native/rollup.config.plugins.js b/src/native/rollup.config.plugins.js index 7eac65e1df23a8..faeabfe01ed734 100644 --- a/src/native/rollup.config.plugins.js +++ b/src/native/rollup.config.plugins.js @@ -7,6 +7,7 @@ import virtual from "@rollup/plugin-virtual"; import * as fs from "fs"; import * as path from "path"; import terser from "@rollup/plugin-terser"; +import MagicString from "magic-string"; import { isContinuousIntegrationBuild, gitHash } from "./rollup.config.defines.js"; @@ -36,6 +37,50 @@ export const writeOnChangePlugin = () => ({ generateBundle: writeWhenChanged }); +export function regexReplace(replacements = []) { + return { + name: "regexReplace", + + renderChunk(code, chunk) { + const id = chunk.fileName; + return executeReplacement(this, code, id); + }, + + transform(code, id) { + return executeReplacement(this, code, id); + } + }; + + function executeReplacement(_, code, id) { + const magicString = new MagicString(code); + if (!codeHasReplacements(code, id, magicString)) { + return null; + } + + const result = { code: magicString.toString() }; + result.map = magicString.generateMap({ hires: true }); + return result; + } + + function codeHasReplacements(code, id, magicString) { + let result = false; + let match; + for (const rep of replacements) { + const { pattern, replacement } = rep; + const rx = new RegExp(pattern, "gm"); + while ((match = rx.exec(code))) { + result = true; + const updated = replacement(match); + const start = match.index; + const end = start + match[0].length; + magicString.overwrite(start, end, updated); + } + } + + return result; + } +} + // Drop invocation from IIFE export function iife2fe() { return { @@ -45,10 +90,10 @@ export function iife2fe() { if (name.endsWith(".map")) return; const asset = bundle[name]; const code = asset.code; - //throw new Error("iife2fe " + code); asset.code = code - .replace(/}\({}\);/, "};") // }({}); ->}; - .replace(/}\)\({}\);/, "});"); // })({}); ->}); + // spaces are there to preserve source mappings + .replace(/}\({}\);/, "} ;") // }({}); -> } ; + .replace(/}\)\({}\);/, "}) ;"); // })({}); -> }) ; } }; }