From ad50c0df34c524d4b59ebe2dcb46241b3b2ee509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Fri, 24 Jan 2025 13:06:36 +0100 Subject: [PATCH] refactor(tsc): remove TS program creation during snapshotting (#27797) This commit refactors how a snapshot is created for the TypeScript compiler. Instead of having 4 ops, only a single op ("op_load") is left. This is achieved by not creating a "ts.Program" during snapshotting, that during benchmarking doesn't provide much benefit. This greatly simplifies build script for the TS snapshot and opens up way to simplify it even further in follow up PRs. --- cli/build.rs | 99 ++++++++++++++----------------------- cli/tsc/99_main_compiler.js | 67 +++++++------------------ 2 files changed, 56 insertions(+), 110 deletions(-) diff --git a/cli/build.rs b/cli/build.rs index 742f227ec9..c8e156a265 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -13,48 +13,13 @@ mod ts { use std::path::PathBuf; use deno_core::op2; + use deno_core::v8; use deno_core::OpState; use deno_error::JsErrorBox; use serde::Serialize; use super::*; - #[derive(Debug, Serialize)] - #[serde(rename_all = "camelCase")] - struct BuildInfoResponse { - build_specifier: String, - libs: Vec, - } - - #[op2] - #[serde] - fn op_build_info(state: &mut OpState) -> BuildInfoResponse { - let build_specifier = "asset:///bootstrap.ts".to_string(); - let build_libs = state - .borrow::>() - .iter() - .map(|s| s.to_string()) - .collect(); - BuildInfoResponse { - build_specifier, - libs: build_libs, - } - } - - #[op2(fast)] - fn op_is_node_file() -> bool { - false - } - - #[op2] - #[string] - fn op_script_version( - _state: &mut OpState, - #[string] _arg: &str, - ) -> Result, JsErrorBox> { - Ok(Some("1".to_string())) - } - #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] struct LoadResponse { @@ -74,19 +39,10 @@ mod ts { let op_crate_libs = state.borrow::>(); let path_dts = state.borrow::(); let re_asset = lazy_regex::regex!(r"asset:/{3}lib\.(\S+)\.d\.ts"); - let build_specifier = "asset:///bootstrap.ts"; - // we need a basic file to send to tsc to warm it up. - if load_specifier == build_specifier { - Ok(LoadResponse { - data: r#"Deno.writeTextFile("hello.txt", "hello deno!");"#.to_string(), - version: "1".to_string(), - // this corresponds to `ts.ScriptKind.TypeScript` - script_kind: 3, - }) - // specifiers come across as `asset:///lib.{lib_name}.d.ts` and we need to - // parse out just the name so we can lookup the asset. - } else if let Some(caps) = re_asset.captures(load_specifier) { + // specifiers come across as `asset:///lib.{lib_name}.d.ts` and we need to + // parse out just the name so we can lookup the asset. + if let Some(caps) = re_asset.captures(load_specifier) { if let Some(lib) = caps.get(1).map(|m| m.as_str()) { // if it comes from an op crate, we were supplied with the path to the // file. @@ -100,28 +56,25 @@ mod ts { }; let data = std::fs::read_to_string(path).map_err(JsErrorBox::from_err)?; - Ok(LoadResponse { + return Ok(LoadResponse { data, version: "1".to_string(), // this corresponds to `ts.ScriptKind.TypeScript` script_kind: 3, - }) - } else { - Err(JsErrorBox::new( - "InvalidSpecifier", - format!("An invalid specifier was requested: {}", load_specifier), - )) + }); } - } else { - Err(JsErrorBox::new( - "InvalidSpecifier", - format!("An invalid specifier was requested: {}", load_specifier), - )) } + + Err(JsErrorBox::new( + "InvalidSpecifier", + format!("An invalid specifier was requested: {}", load_specifier), + )) } deno_core::extension!(deno_tsc, - ops = [op_build_info, op_is_node_file, op_load, op_script_version], + ops = [ + op_load, + ], esm_entry_point = "ext:deno_tsc/99_main_compiler.js", esm = [ dir "tsc", @@ -277,6 +230,28 @@ mod ts { ) .unwrap(); + // Leak to satisfy type-checker. It's okay since it's only run once for a build script. + let build_libs_ = Box::leak(Box::new(build_libs.clone())); + let runtime_cb = Box::new(|rt: &mut deno_core::JsRuntimeForSnapshot| { + let scope = &mut rt.handle_scope(); + + let context = scope.get_current_context(); + let global = context.global(scope); + + let name = v8::String::new(scope, "snapshot").unwrap(); + let snapshot_fn_val = global.get(scope, name.into()).unwrap(); + let snapshot_fn: v8::Local = + snapshot_fn_val.try_into().unwrap(); + let undefined = v8::undefined(scope); + let build_libs = build_libs_.clone(); + let build_libs_v8 = + deno_core::serde_v8::to_v8(scope, build_libs).unwrap(); + + snapshot_fn + .call(scope, undefined.into(), &[build_libs_v8]) + .unwrap(); + }); + let output = create_snapshot( CreateSnapshotOptions { cargo_manifest_dir: env!("CARGO_MANIFEST_DIR"), @@ -287,7 +262,7 @@ mod ts { path_dts, )], extension_transpiler: None, - with_runtime_cb: None, + with_runtime_cb: Some(runtime_cb), skip_op_registration: false, }, None, diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index c99cfce9a2..872b9c5479 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -22,7 +22,6 @@ import { getAssets, host, setLogDebug, - SOURCE_FILE_CACHE, } from "./97_ts_host.js"; import { serverMainLoop } from "./98_lsp.js"; @@ -39,16 +38,6 @@ const ops = core.ops; /** @type {Map} */ const normalizedToOriginalMap = new Map(); -const SNAPSHOT_COMPILE_OPTIONS = { - esModuleInterop: true, - jsx: ts.JsxEmit.React, - module: ts.ModuleKind.ESNext, - noEmit: true, - strict: true, - target: ts.ScriptTarget.ESNext, - lib: ["lib.deno.window.d.ts"], -}; - /** @type {Array<[string, number]>} */ const stats = []; let statsStart = 0; @@ -225,44 +214,26 @@ function exec({ config, debug: debugFlag, rootNames, localOnly }) { debug("<<< exec stop"); } -// A build time only op that provides some setup information that is used to -// ensure the snapshot is setup properly. -/** @type {{ buildSpecifier: string; libs: string[]; nodeBuiltInModuleNames: string[] }} */ -const { buildSpecifier, libs } = ops.op_build_info(); - -for (const lib of libs) { - const specifier = `lib.${lib}.d.ts`; - // we are using internal APIs here to "inject" our custom libraries into - // tsc, so things like `"lib": [ "deno.ns" ]` are supported. - if (!ts.libs.includes(lib)) { - ts.libs.push(lib); - ts.libMap.set(lib, `lib.${lib}.d.ts`); +globalThis.snapshot = function (libs) { + for (const lib of libs) { + const specifier = `lib.${lib}.d.ts`; + // we are using internal APIs here to "inject" our custom libraries into + // tsc, so things like `"lib": [ "deno.ns" ]` are supported. + if (!ts.libs.includes(lib)) { + ts.libs.push(lib); + ts.libMap.set(lib, `lib.${lib}.d.ts`); + } + // we are caching in memory common type libraries that will be re-used by + // tsc on when the snapshot is restored + assert( + !!host.getSourceFile( + `${ASSETS_URL_PREFIX}${specifier}`, + ts.ScriptTarget.ESNext, + ), + `failed to load '${ASSETS_URL_PREFIX}${specifier}'`, + ); } - // we are caching in memory common type libraries that will be re-used by - // tsc on when the snapshot is restored - assert( - !!host.getSourceFile( - `${ASSETS_URL_PREFIX}${specifier}`, - ts.ScriptTarget.ESNext, - ), - `failed to load '${ASSETS_URL_PREFIX}${specifier}'`, - ); -} -// this helps ensure as much as possible is in memory that is re-usable -// before the snapshotting is done, which helps unsure fast "startup" for -// subsequent uses of tsc in Deno. -const TS_SNAPSHOT_PROGRAM = ts.createProgram({ - rootNames: [buildSpecifier], - options: SNAPSHOT_COMPILE_OPTIONS, - host, -}); -assert( - ts.getPreEmitDiagnostics(TS_SNAPSHOT_PROGRAM).length === 0, - "lib.d.ts files have errors", -); - -// remove this now that we don't need it anymore for warming up tsc -SOURCE_FILE_CACHE.delete(buildSpecifier); +}; // exposes the functions that are called by `tsc::exec()` when type // checking TypeScript.