From 12480ae5e6062ae8ef31b49c413d12ddd691246a Mon Sep 17 00:00:00 2001 From: robertsLando Date: Thu, 16 Apr 2026 13:58:34 +0200 Subject: [PATCH] perf: speed up the dependency walker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace Babel with acorn for AST parsing in the detector (~3-5x faster parsing), add a module resolution cache to skip redundant follow() calls, and batch independent fs.stat() calls with Promise.all(). The acorn parser tries module mode first, falling back to script mode for legacy packages using strict-mode-incompatible syntax (with statements, octal escapes). The resolution cache deep-clones markers on cache hit to prevent shared mutation from stepActivate. Benchmarked on zwave-js-ui: - SEA no-bundle (walker-heavy): -29.5% (38.5s → 27.2s) - SEA + bundle: -11.6% (10.1s → 9.0s) - Standard PKG + bundle: -11.6% (16.7s → 14.8s) Binary sizes unchanged. All 94 test-50-* tests pass. Closes #239 Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/detector.ts | 183 ++++++++++++++++--------------- lib/walker.ts | 284 +++++++++++++++++++++++++++++++++++------------- package.json | 1 + yarn.lock | 5 + 4 files changed, 309 insertions(+), 164 deletions(-) diff --git a/lib/detector.ts b/lib/detector.ts index 35fbf11c..e6efaae4 100644 --- a/lib/detector.ts +++ b/lib/detector.ts @@ -1,49 +1,43 @@ -import * as babelTypes from '@babel/types'; -import * as babel from '@babel/parser'; -import generate from '@babel/generator'; +import * as acorn from 'acorn'; import { log } from './log'; import { ALIAS_AS_RELATIVE, ALIAS_AS_RESOLVABLE } from './common'; -function isLiteral(node: babelTypes.Node): node is babelTypes.Literal { +// Minimal ESTree node types used by the detector +interface AcornNode { + type: string; + start: number; + end: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +} + +function isLiteral(node: AcornNode): boolean { if (node == null) { return false; } - if (!node.type.endsWith('Literal')) { - return false; + if (node.type === 'Literal') { + // Exclude null, regex + return node.value !== null && !(node.value instanceof RegExp); } - if (node.type === 'TemplateLiteral' && node.expressions.length !== 0) { - return false; + if (node.type === 'TemplateLiteral') { + return node.expressions.length === 0; } - return true; + return false; } -function getLiteralValue(node: babelTypes.Literal) { +function getLiteralValue(node: AcornNode) { if (node.type === 'TemplateLiteral') { return node.quasis[0].value.raw; } - if (node.type === 'NullLiteral') { - throw new Error('Unexpected null in require expression'); - } - - if (node.type === 'RegExpLiteral') { - throw new Error('Unexpected regexp in require expression'); - } - return node.value; } -function reconstructSpecifiers( - specs: ( - | babelTypes.ImportDefaultSpecifier - | babelTypes.ImportNamespaceSpecifier - | babelTypes.ImportSpecifier - )[], -) { +function reconstructSpecifiers(specs: AcornNode[]) { if (!specs || !specs.length) { return ''; } @@ -51,7 +45,7 @@ function reconstructSpecifiers( const defaults = []; for (const spec of specs) { - if (babelTypes.isImportDefaultSpecifier(spec)) { + if (spec.type === 'ImportDefaultSpecifier') { defaults.push(spec.local.name); } } @@ -59,10 +53,8 @@ function reconstructSpecifiers( const nonDefaults = []; for (const spec of specs) { - if (babelTypes.isImportSpecifier(spec)) { - const importedName = babelTypes.isIdentifier(spec.imported) - ? spec.imported.name - : spec.imported.value; + if (spec.type === 'ImportSpecifier') { + const importedName = spec.imported.name; if (spec.local.name === importedName) { nonDefaults.push(spec.local.name); @@ -79,8 +71,8 @@ function reconstructSpecifiers( return defaults.join(', '); } -function reconstruct(node: babelTypes.Node) { - let v = generate(node, { comments: false }).code.replace(/\n/g, ''); +function reconstruct(node: AcornNode, source: string) { + let v = source.slice(node.start, node.end).replace(/\n/g, ''); let v2; while (true) { @@ -121,12 +113,12 @@ function valid2(v2?: Was['v2']) { ); } -function visitorRequireResolve(n: babelTypes.Node) { - if (!babelTypes.isCallExpression(n)) { +function visitorRequireResolve(n: AcornNode) { + if (n.type !== 'CallExpression') { return null; } - if (!babelTypes.isMemberExpression(n.callee)) { + if (n.callee.type !== 'MemberExpression') { return null; } @@ -150,12 +142,12 @@ function visitorRequireResolve(n: babelTypes.Node) { }; } -function visitorRequire(n: babelTypes.Node) { - if (!babelTypes.isCallExpression(n)) { +function visitorRequire(n: AcornNode) { + if (n.type !== 'CallExpression') { return null; } - if (!babelTypes.isIdentifier(n.callee)) { + if (n.callee.type !== 'Identifier') { return null; } @@ -173,20 +165,20 @@ function visitorRequire(n: babelTypes.Node) { }; } -function visitorImport(n: babelTypes.Node) { - if (!babelTypes.isImportDeclaration(n)) { +function visitorImport(n: AcornNode) { + if (n.type !== 'ImportDeclaration') { return null; } return { v1: n.source.value, v3: reconstructSpecifiers(n.specifiers) }; } -function visitorPathJoin(n: babelTypes.Node) { - if (!babelTypes.isCallExpression(n)) { +function visitorPathJoin(n: AcornNode) { + if (n.type !== 'CallExpression') { return null; } - if (!babelTypes.isMemberExpression(n.callee)) { + if (n.callee.type !== 'MemberExpression') { return null; } @@ -218,10 +210,10 @@ function visitorPathJoin(n: babelTypes.Node) { return null; } - return { v1: getLiteralValue(n.arguments[1] as babelTypes.StringLiteral) }; + return { v1: getLiteralValue(n.arguments[1]) }; } -export function visitorSuccessful(node: babelTypes.Node, test = false) { +export function visitorSuccessful(node: AcornNode, test = false) { let was: Was | null = visitorRequireResolve(node); if (was) { @@ -283,12 +275,12 @@ export function visitorSuccessful(node: babelTypes.Node, test = false) { return null; } -function nonLiteralRequireResolve(n: babelTypes.Node) { - if (!babelTypes.isCallExpression(n)) { +function nonLiteralRequireResolve(n: AcornNode, source: string) { + if (n.type !== 'CallExpression') { return null; } - if (!babelTypes.isMemberExpression(n.callee)) { + if (n.callee.type !== 'MemberExpression') { return null; } @@ -309,7 +301,7 @@ function nonLiteralRequireResolve(n: babelTypes.Node) { const m = n.arguments[1]; if (!m) { - return { v1: reconstruct(n.arguments[0]) }; + return { v1: reconstruct(n.arguments[0], source) }; } if (!isLiteral(n.arguments[1])) { @@ -317,17 +309,17 @@ function nonLiteralRequireResolve(n: babelTypes.Node) { } return { - v1: reconstruct(n.arguments[0]), + v1: reconstruct(n.arguments[0], source), v2: getLiteralValue(n.arguments[1]), }; } -function nonLiteralRequire(n: babelTypes.Node) { - if (!babelTypes.isCallExpression(n)) { +function nonLiteralRequire(n: AcornNode, source: string) { + if (n.type !== 'CallExpression') { return null; } - if (!babelTypes.isIdentifier(n.callee)) { + if (n.callee.type !== 'Identifier') { return null; } @@ -342,7 +334,7 @@ function nonLiteralRequire(n: babelTypes.Node) { const m = n.arguments[1]; if (!m) { - return { v1: reconstruct(n.arguments[0]) }; + return { v1: reconstruct(n.arguments[0], source) }; } if (!isLiteral(n.arguments[1])) { @@ -350,13 +342,14 @@ function nonLiteralRequire(n: babelTypes.Node) { } return { - v1: reconstruct(n.arguments[0]), + v1: reconstruct(n.arguments[0], source), v2: getLiteralValue(n.arguments[1]), }; } -export function visitorNonLiteral(n: babelTypes.Node) { - const was = nonLiteralRequireResolve(n) || nonLiteralRequire(n); +export function visitorNonLiteral(n: AcornNode, source: string) { + const was = + nonLiteralRequireResolve(n, source) || nonLiteralRequire(n, source); if (was) { if (!valid2(was.v2)) { @@ -373,12 +366,12 @@ export function visitorNonLiteral(n: babelTypes.Node) { return null; } -function isRequire(n: babelTypes.Node) { - if (!babelTypes.isCallExpression(n)) { +function isRequire(n: AcornNode, source: string) { + if (n.type !== 'CallExpression') { return null; } - if (!babelTypes.isIdentifier(n.callee)) { + if (n.callee.type !== 'Identifier') { return null; } @@ -392,15 +385,15 @@ function isRequire(n: babelTypes.Node) { return null; } - return { v1: reconstruct(n.arguments[0]) }; + return { v1: reconstruct(n.arguments[0], source) }; } -function isRequireResolve(n: babelTypes.Node) { - if (!babelTypes.isCallExpression(n)) { +function isRequireResolve(n: AcornNode, source: string) { + if (n.type !== 'CallExpression') { return null; } - if (!babelTypes.isMemberExpression(n.callee)) { + if (n.callee.type !== 'MemberExpression') { return null; } @@ -420,11 +413,11 @@ function isRequireResolve(n: babelTypes.Node) { return null; } - return { v1: reconstruct(n.arguments[0]) }; + return { v1: reconstruct(n.arguments[0], source) }; } -export function visitorMalformed(n: babelTypes.Node) { - const was = isRequireResolve(n) || isRequire(n); +export function visitorMalformed(n: AcornNode, source: string) { + const was = isRequireResolve(n, source) || isRequire(n, source); if (was) { return { alias: was.v1 }; @@ -433,12 +426,12 @@ export function visitorMalformed(n: babelTypes.Node) { return null; } -export function visitorUseSCWD(n: babelTypes.Node) { - if (!babelTypes.isCallExpression(n)) { +export function visitorUseSCWD(n: AcornNode, source: string) { + if (n.type !== 'CallExpression') { return null; } - if (!babelTypes.isMemberExpression(n.callee)) { + if (n.callee.type !== 'MemberExpression') { return null; } @@ -452,7 +445,9 @@ export function visitorUseSCWD(n: babelTypes.Node) { return null; } - const was = { v1: n.arguments.map(reconstruct).join(', ') }; + const was = { + v1: n.arguments.map((a: AcornNode) => reconstruct(a, source)).join(', '), + }; if (was) { return { alias: was.v1 }; @@ -461,25 +456,24 @@ export function visitorUseSCWD(n: babelTypes.Node) { return null; } -type VisitorFunction = (node: babelTypes.Node, trying?: boolean) => boolean; +type VisitorFunction = (node: AcornNode, trying?: boolean) => boolean; -function traverse(ast: babelTypes.File, visitor: VisitorFunction) { +function traverse(ast: AcornNode, visitor: VisitorFunction) { // modified esprima-walk to support // visitor return value and "trying" flag - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const stack: Array<[any, boolean]> = [[ast, false]]; + const stack: Array<[AcornNode, boolean]> = [[ast, false]]; for (let i = 0; i < stack.length; i += 1) { const item = stack[i]; const [node] = item; if (node) { - const trying = item[1] || babelTypes.isTryStatement(node); + const trying = item[1] || node.type === 'TryStatement'; if (visitor(node, trying)) { for (const key in node) { - if (node[key as keyof babelTypes.File]) { - const child = node[key as keyof babelTypes.File]; + if (node[key]) { + const child = node[key]; if (child instanceof Array) { for (let j = 0; j < child.length; j += 1) { @@ -496,25 +490,40 @@ function traverse(ast: babelTypes.File, visitor: VisitorFunction) { } export function parse(body: string) { - return babel.parse(body, { - allowImportExportEverywhere: true, - allowReturnOutsideFunction: true, - }); + // Try module mode first (handles import/export), fall back to script mode + // for legacy code that uses strict-mode-incompatible syntax (e.g. `with` + // statements, octal escapes). This matches Babel's permissive behavior. + try { + return acorn.parse(body, { + ecmaVersion: 'latest', + sourceType: 'module', + allowReturnOutsideFunction: true, + allowImportExportEverywhere: true, + allowHashBang: true, + }) as unknown as AcornNode; + } catch (_) { + return acorn.parse(body, { + ecmaVersion: 'latest', + sourceType: 'script', + allowReturnOutsideFunction: true, + allowHashBang: true, + }) as unknown as AcornNode; + } } export function detect(body: string, visitor: VisitorFunction, file?: string) { - let json; + let ast: AcornNode | undefined; try { - json = parse(body); + ast = parse(body); } catch (error) { const fileInfo = file ? ` in ${file}` : ''; - log.warn(`Babel parse has failed: ${(error as Error).message}${fileInfo}`); + log.warn(`Acorn parse has failed: ${(error as Error).message}${fileInfo}`); } - if (!json) { + if (!ast) { return; } - traverse(json, visitor); + traverse(ast, visitor); } diff --git a/lib/walker.ts b/lib/walker.ts index 4f68def1..92acd403 100644 --- a/lib/walker.ts +++ b/lib/walker.ts @@ -261,9 +261,11 @@ function stepDetect( body = body.toString(); } + const source = body as string; + try { detector.detect( - body, + source, (node, trying) => { const { toplevel } = marker; let d = detector.visitorSuccessful(node) as unknown as Derivative; @@ -279,7 +281,7 @@ function stepDetect( return false; } - d = detector.visitorNonLiteral(node) as unknown as Derivative; + d = detector.visitorNonLiteral(node, source) as unknown as Derivative; if (d) { if (typeof d === 'object' && d.mustExclude) { @@ -298,7 +300,7 @@ function stepDetect( return false; } - d = detector.visitorMalformed(node) as unknown as Derivative; + d = detector.visitorMalformed(node, source) as unknown as Derivative; if (d) { // there is no 'mustExclude' @@ -308,7 +310,7 @@ function stepDetect( return false; } - d = detector.visitorUseSCWD(node) as unknown as Derivative; + d = detector.visitorUseSCWD(node, source) as unknown as Derivative; if (d) { // there is no 'mustExclude' @@ -378,6 +380,37 @@ class Walker { private dictionary: ConfigDictionary; + // Module resolution cache: avoids redundant follow() calls for the same + // specifier resolved from the same directory. + private resolveCache = new Map< + string, + { + newFile: string; + newPackages: { packageJson: string; marker?: Marker }[]; + newPackageForNewRecords?: { packageJson: string; marker?: Marker }; + } + >(); + + // Performance timing accumulators (milliseconds) + private timings = { + read: 0, + strip: 0, + detect: 0, + patch: 0, + derivatives: 0, + esmTransform: 0, + stat: 0, + total: 0, + }; + + private fileCounts = { + jsParsed: 0, + json: 0, + nativeAddon: 0, + other: 0, + total: 0, + }; + constructor() { this.tasks = []; this.records = {}; @@ -541,9 +574,11 @@ class Walker { if (scripts) { scripts = expandFiles(scripts, base); - for (const script of scripts) { - const stat = await fs.stat(script); + const scriptStats = await Promise.all( + scripts.map((s) => fs.stat(s).then((stat) => ({ path: s, stat }))), + ); + for (const { path: script, stat } of scriptStats) { if (stat.isFile()) { if (!isDotJS(script) && !isDotJSON(script) && !isDotNODE(script)) { log.warn("Non-javascript file is specified in 'scripts'.", [ @@ -567,9 +602,12 @@ class Walker { if (assets) { assets = expandFiles(assets, base); - for (const asset of assets) { + const assetStats = await Promise.all( + assets.map((a) => fs.stat(a).then((stat) => ({ path: a, stat }))), + ); + + for (const { path: asset, stat } of assetStats) { log.debug(' Adding asset : .... ', asset); - const stat = await fs.stat(asset); if (stat.isFile()) { await this.appendBlobOrContent({ @@ -587,10 +625,14 @@ class Walker { if (files) { files = expandFiles(files, base); - for (let file of files) { - file = normalizePath(file); - const stat = await fs.stat(file); + const normalizedFiles = files.map((f) => normalizePath(f)); + const fileStats = await Promise.all( + normalizedFiles.map((f) => + fs.stat(f).then((stat) => ({ path: f, stat })), + ), + ); + for (const { path: file, stat } of fileStats) { if (stat.isFile()) { // 1) remove sources of top-level(!) package 'files' i.e. ship as BLOB // 2) non-source (non-js) files of top-level package are shipped as CONTENT @@ -812,87 +854,130 @@ class Walker { marker: Marker, derivative: Derivative, ) { - const newPackages: { packageJson: string; marker?: Marker }[] = []; + const basedir = path.dirname(record.file); + const cacheKey = `${derivative.alias}\0${basedir}`; + const cached = this.resolveCache.get(cacheKey); + + let newFile: string; + let newPackages: { packageJson: string; marker?: Marker }[]; + let newPackageForNewRecords: + | { packageJson: string; marker?: Marker } + | undefined; + + if (cached) { + // Cache hit — reuse prior resolution. Deep-clone marker objects to + // prevent shared mutation (stepActivate modifies marker.config). + // The appendBlobOrContent calls below are no-ops for already-seen + // files (deduped by appendRecord). + newFile = cached.newFile; + newPackages = cached.newPackages.map((p) => ({ + packageJson: p.packageJson, + marker: p.marker + ? { + config: p.marker.config + ? JSON.parse(JSON.stringify(p.marker.config)) + : undefined, + configPath: p.marker.configPath, + base: p.marker.base, + } + : undefined, + })); + newPackageForNewRecords = cached.newPackageForNewRecords + ? newPackages.find( + (p) => + p.packageJson === cached.newPackageForNewRecords!.packageJson, + ) + : undefined; + } else { + // Cache miss — perform full resolution + newPackages = []; - const catchReadFile = (file: string) => { - assert(isPackageJson(file), `walker: ${file} must be package.json`); - newPackages.push({ packageJson: file }); - }; + const catchReadFile = (file: string) => { + assert(isPackageJson(file), `walker: ${file} must be package.json`); + newPackages.push({ packageJson: file }); + }; - const catchPackageFilter = (config: PackageJson, base: string) => { - const newPackage = newPackages[newPackages.length - 1]; - newPackage.marker = { - config, - configPath: newPackage.packageJson, - base, + const catchPackageFilter = (config: PackageJson, base: string) => { + const newPackage = newPackages[newPackages.length - 1]; + newPackage.marker = { + config, + configPath: newPackage.packageJson, + base, + }; }; - }; - let newFile = ''; - let failure: Error | undefined; + newFile = ''; + let failure: Error | undefined; - const basedir = path.dirname(record.file); - try { - newFile = await follow(derivative.alias, { - basedir, - // default is extensions: ['.js'], but - // it is not enough because 'typos.json' - // is not taken in require('./typos') - // in 'normalize-package-data/lib/fixer.js' - // Also include .mjs to support ESM files that get transformed to .js - extensions: MODULE_RESOLVE_EXTENSIONS, - catchReadFile, - catchPackageFilter, - }); - } catch (error) { - failure = error as Error; - } + try { + newFile = await follow(derivative.alias, { + basedir, + // default is extensions: ['.js'], but + // it is not enough because 'typos.json' + // is not taken in require('./typos') + // in 'normalize-package-data/lib/fixer.js' + // Also include .mjs to support ESM files that get transformed to .js + extensions: MODULE_RESOLVE_EXTENSIONS, + catchReadFile, + catchPackageFilter, + }); + } catch (error) { + failure = error as Error; + } - if (failure) { - const { toplevel } = marker; - const mainNotFound = - newPackages.length > 0 && !newPackages[0].marker?.config?.main; - const debug = - !toplevel || - derivative.mayExclude || - (mainNotFound && derivative.fromDependencies); - const level = debug ? 'debug' : 'warn'; + if (failure) { + const { toplevel } = marker; + const mainNotFound = + newPackages.length > 0 && !newPackages[0].marker?.config?.main; + const debug = + !toplevel || + derivative.mayExclude || + (mainNotFound && derivative.fromDependencies); + const level = debug ? 'debug' : 'warn'; + + if (mainNotFound) { + const message = "Entry 'main' not found in %1"; + log[level](message, [ + `%1: ${newPackages[0].packageJson}`, + `%2: ${record.file}`, + ]); + } else { + log[level](`${pc.yellow(failure.message)} in ${record.file}`); + } - if (mainNotFound) { - const message = "Entry 'main' not found in %1"; - log[level](message, [ - `%1: ${newPackages[0].packageJson}`, - `%2: ${record.file}`, - ]); - } else { - log[level](`${pc.yellow(failure.message)} in ${record.file}`); + return; } - return; - } + newPackageForNewRecords = undefined; - let newPackageForNewRecords; + for (const newPackage of newPackages) { + let newFile2; - for (const newPackage of newPackages) { - let newFile2; + try { + newFile2 = await follow(derivative.alias, { + basedir, + extensions: MODULE_RESOLVE_EXTENSIONS, + ignoreFile: newPackage.packageJson, + }); + if (strictVerify) { + assert(newFile2 === normalizePath(newFile2)); + } + } catch (_) { + // not setting is enough + } - try { - newFile2 = await follow(derivative.alias, { - basedir: path.dirname(record.file), - extensions: MODULE_RESOLVE_EXTENSIONS, - ignoreFile: newPackage.packageJson, - }); - if (strictVerify) { - assert(newFile2 === normalizePath(newFile2)); + if (newFile2 !== newFile) { + newPackageForNewRecords = newPackage; + break; } - } catch (_) { - // not setting is enough } - if (newFile2 !== newFile) { - newPackageForNewRecords = newPackage; - break; - } + // Store in cache for future lookups from the same directory + this.resolveCache.set(cacheKey, { + newFile, + newPackages, + newPackageForNewRecords, + }); } // Add all discovered package.json files, not just the one determined by the double-resolution logic @@ -975,6 +1060,17 @@ class Walker { if (record[store] !== undefined) return; record[store] = false; // default is discard + this.fileCounts.total += 1; + if (isDotJSON(record.file)) { + this.fileCounts.json += 1; + } else if (isDotNODE(record.file)) { + this.fileCounts.nativeAddon += 1; + } else if (isDotJS(record.file) || record.file.endsWith('.mjs')) { + this.fileCounts.jsParsed += 1; + } else { + this.fileCounts.other += 1; + } + this.appendStat({ file: record.file, store: STORE_STAT, @@ -1012,11 +1108,18 @@ class Walker { this.hasPatch(record) ) { if (!record.body) { + let t0 = performance.now(); await stepRead(record); + this.timings.read += performance.now() - t0; + + t0 = performance.now(); this.stepPatch(record); + this.timings.patch += performance.now() - t0; if (store === STORE_BLOB || needsSeaRead) { + t0 = performance.now(); stepStrip(record); + this.timings.strip += performance.now() - t0; } } @@ -1108,6 +1211,7 @@ class Walker { (isDotJS(record.file) || record.file.endsWith('.mjs')) ) { if (isESMFile(record.file)) { + const t0 = performance.now(); try { const result = transformESMtoCJS( record.body.toString('utf8'), @@ -1127,13 +1231,19 @@ class Walker { `Failed to transform ESM module to CJS for file "${record.file}": ${message}`, ); } + this.timings.esmTransform += performance.now() - t0; } } if (store === STORE_BLOB || needsSeaRead) { const derivatives2: Derivative[] = []; + let t0 = performance.now(); stepDetect(record, marker, derivatives2); + this.timings.detect += performance.now() - t0; + + t0 = performance.now(); await this.stepDerivatives(record, marker, derivatives2); + this.timings.derivatives += performance.now() - t0; // After dependencies are resolved, rewrite .mjs require paths to .js // since the packer renames .mjs files to .js in the snapshot. @@ -1185,6 +1295,7 @@ class Walker { }); } + const t0 = performance.now(); try { const valueStat = await fs.stat(record.file); @@ -1202,6 +1313,7 @@ class Walker { log.error(`Cannot stat, ${exception.code}`, record.file); throw wasReported(exception.message); } + this.timings.stat += performance.now() - t0; if (path.dirname(record.file) !== record.file) { // root directory @@ -1298,12 +1410,30 @@ class Walker { const { tasks } = this; + const walkerStart = performance.now(); + for (let i = 0; i < tasks.length; i += 1) { // NO MULTIPLE WORKERS! THIS WILL LEAD TO NON-DETERMINISTIC // ORDER. one-by-one fifo is the only way to iterate tasks await this.step(tasks[i]); } + this.timings.total = performance.now() - walkerStart; + + const t = this.timings; + const c = this.fileCounts; + log.debug('Walker performance summary:'); + log.debug( + ` Files: ${c.total} total (${c.jsParsed} JS, ${c.json} JSON, ${c.nativeAddon} native, ${c.other} other)`, + ); + log.debug( + ` Timings: read=${t.read.toFixed(0)}ms detect=${t.detect.toFixed(0)}ms derivatives=${t.derivatives.toFixed(0)}ms`, + ); + log.debug( + ` esmTransform=${t.esmTransform.toFixed(0)}ms patch=${t.patch.toFixed(0)}ms strip=${t.strip.toFixed(0)}ms stat=${t.stat.toFixed(0)}ms`, + ); + log.debug(` Total walker time: ${t.total.toFixed(0)}ms`); + return { symLinks: this.symLinks, records: this.records, diff --git a/package.json b/package.json index 8ea997ac..c12685ca 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@babel/types": "^7.23.0", "@roberts_lando/vfs": "^0.3.2", "@yao-pkg/pkg-fetch": "3.5.33", + "acorn": "^8.16.0", "esbuild": "^0.27.3", "into-stream": "^9.1.0", "minimist": "^1.2.6", diff --git a/yarn.lock b/yarn.lock index 77e8a894..1eca3386 100644 --- a/yarn.lock +++ b/yarn.lock @@ -874,6 +874,11 @@ acorn@^8.15.0: resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== +acorn@^8.16.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" + integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== + agent-base@6: version "6.0.2" resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz"