0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-02-07 23:06:50 -05:00

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.
This commit is contained in:
Bartek Iwańczuk 2025-01-24 13:06:36 +01:00 committed by GitHub
parent 886dbe6be1
commit ad50c0df34
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 56 additions and 110 deletions

View file

@ -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<String>,
}
#[op2]
#[serde]
fn op_build_info(state: &mut OpState) -> BuildInfoResponse {
let build_specifier = "asset:///bootstrap.ts".to_string();
let build_libs = state
.borrow::<Vec<&str>>()
.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<Option<String>, 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::<HashMap<&str, PathBuf>>();
let path_dts = state.borrow::<PathBuf>();
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<v8::Function> =
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,

View file

@ -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<string, string>} */
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.