Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e9826f1
feat(bundle-source): add opt-in chrome trace profiling
turadg Feb 19, 2026
5e66892
feat(bundle-source): add trace merge and summary tool
turadg Feb 19, 2026
3b3a129
feat(bundle-source): add agoric source-spec registry bundling tool
turadg Feb 19, 2026
db1cc48
feat(bundle-source): add profile-agoric-bundling tool
turadg Feb 19, 2026
d765c58
chore(bundle-source): render profile summary with console.table
turadg Feb 19, 2026
f9dcde5
feat(profiling): add compartment-mapper phase spans
turadg Feb 19, 2026
79b91d3
feat(profiling): add compression and babel stage spans
turadg Feb 19, 2026
897cfb9
fix(profiling): await node-modules parent span timing
turadg Feb 19, 2026
49bc42a
feat(profiling): add critical-path metrics and fix module-source span…
turadg Feb 19, 2026
4b7d4f6
perf(bundle-source): speed up multi-entry bundling and add profiling …
turadg Feb 19, 2026
b32c6c3
fixup! feat(bundle-source): add opt-in chrome trace profiling
turadg Jun 9, 2026
351080d
fixup! feat(bundle-source): add trace merge and summary tool
turadg Jun 9, 2026
24d6454
fixup! feat(bundle-source): add profile-agoric-bundling tool
turadg Jun 9, 2026
c3f71b9
fixup! feat(profiling): add compartment-mapper phase spans
turadg Jun 9, 2026
ac76142
fixup! feat(profiling): add compression and babel stage spans
turadg Jun 9, 2026
0947c78
fixup! perf(bundle-source): speed up multi-entry bundling and add pro…
turadg Jun 9, 2026
087a8c4
fixup! feat(bundle-source): add opt-in chrome trace profiling
turadg Jun 9, 2026
a4a1f09
fixup! perf(bundle-source): speed up multi-entry bundling and add pro…
turadg Jun 9, 2026
722a684
fixup! feat(bundle-source): add opt-in chrome trace profiling
turadg Jun 9, 2026
3a448fc
fixup! perf(bundle-source): speed up multi-entry bundling and add pro…
turadg Jun 9, 2026
f2f1e57
fixup! perf(bundle-source): speed up multi-entry bundling and add pro…
turadg Jun 9, 2026
a5ef3f0
fixup! perf(bundle-source): speed up multi-entry bundling and add pro…
turadg Jun 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions packages/bundle-source/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,66 @@ map for every physical module.
It is not yet quite clever enough to collect source maps for sources that do
not exist.

## Profiling
Comment thread
turadg marked this conversation as resolved.

`bundle-source` can emit Chrome trace files for performance analysis.
This works for programmatic usage and CLI usage, including builds in larger
repos like `agoric-sdk`.

Enable with environment variables:

```console
ENDO_BUNDLE_SOURCE_PROFILE=1 \
ENDO_BUNDLE_SOURCE_PROFILE_DIR=/tmp/bs-profiles \
yarn bundle-source app.js > /tmp/app-bundle.json
```

Each bundle call writes one `*.trace.json` file. Open these in Chrome tracing
tools or convert for Speedscope.

You can also control profiling in code:

```js
await bundleSource('program.js', {
profile: {
enabled: true,
traceDir: '/tmp/bs-profiles',
// or traceFile: '/tmp/specific.trace.json'
},
});
```

Environment variables:
- `ENDO_BUNDLE_SOURCE_PROFILE`: enable profiling when truthy (`1`, `true`, `yes`, `on`)
- `ENDO_BUNDLE_SOURCE_PROFILE_DIR`: output directory for generated trace files
- `ENDO_BUNDLE_SOURCE_PROFILE_FILE`: explicit output file for a single run
- `ENDO_BUNDLE_SOURCE_PROFILE_STDERR`: if truthy, prints each generated trace path to stderr

Merge and summarize Chrome trace files:

```console
yarn workspace @endo/bundle-source trace:merge -- /tmp/bs-profiles
Comment thread
turadg marked this conversation as resolved.
```

This utility is not specific to `bundle-source`; it accepts trace files and
directories containing `*.trace.json` files from any compatible producer.
This generates:
- `merged.trace.json` for trace viewers.
- `summary.json` with aggregate span statistics.
- `summary.md` with a top spans table by total duration.

For Agoric SDK bundle profiling, profile every `source-spec-registry.js` entry
from an `agoric-sdk` checkout using the current checkout's `bundle-source`:

```console
yarn workspace @endo/bundle-source profile:agoric-bundling -- \
Comment thread
turadg marked this conversation as resolved.
--agoric-sdk-root /opt/agoric/agoric-sdk \
--out-dir /tmp/profile-agoric-bundling
```

This Agoric-specific helper writes bundles, raw traces, merged trace, and
summary files to `--out-dir`, and prints a top-spans summary table at the end.

## `moduleFormat` explanations

<a id="getexport-moduleformat"></a>
Expand Down
2 changes: 2 additions & 0 deletions packages/bundle-source/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"scripts": {
"build": "exit 0",
"test": "ava",
"profile:agoric-bundling": "node --experimental-strip-types tools/profile-agoric-bundling.mts",
"trace:merge": "node tools/trace-merge.js",
"test:xs": "exit 0",
"lint-fix": "eslint --fix '**/*.js'",
"lint": "yarn lint:types && yarn lint:eslint",
Expand Down
221 changes: 136 additions & 85 deletions packages/bundle-source/src/endo.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@
*/
export const makeBundlingKit = (
io,
{ cacheSourceMaps, elideComments, noTransforms, commonDependencies },
{
cacheSourceMaps,
elideComments,
noTransforms,
commonDependencies,
profiler,
},
) => {
const { pathResolve, userInfo, computeSha512, platform, env } = io;
if (noTransforms && elideComments) {
Expand All @@ -33,8 +39,8 @@

/** @type {Set<Promise<void>>} */
const sourceMapJobs = new Set();
/** @type {(sourceMap: string, sourceDescriptor: SourceMapDescriptor) => Promise<void>} */
let writeSourceMap = async () => {};
/** @type {((sourceMap: string, sourceDescriptor: SourceMapDescriptor) => Promise<void>) | undefined} */
let writeSourceMap;
if (cacheSourceMaps) {
if (!computeSha512) {
throw new Error('computeSha512 is required when cacheSourceMaps is true');
Expand All @@ -50,63 +56,71 @@
sourceMap,
{ sha512, compartment: packageLocation, module: moduleSpecifier },
) => {
const location = new URL(moduleSpecifier, packageLocation).href;
const locationSha512 = computeSha512(location);
const locationSha512Head = locationSha512.slice(0, 2);
const locationSha512Tail = locationSha512.slice(2);
const sha512Head = sha512.slice(0, 2);
const sha512Tail = sha512.slice(2);
const sourceMapTrackerDirectory = pathResolve(
sourceMapsTrackerDirectory,
locationSha512Head,
);
const sourceMapTrackerPath = pathResolve(
sourceMapTrackerDirectory,
locationSha512Tail,
);
const sourceMapDirectory = pathResolve(
sourceMapsCacheDirectory,
sha512Head,
);
const sourceMapPath = pathResolve(
sourceMapDirectory,
`${sha512Tail}.map.json`,
const endWriteSourceMap = profiler?.startSpan(
'bundleSource.writeSourceMap',
{ moduleSpecifier, packageLocation },
);
try {
const location = new URL(moduleSpecifier, packageLocation).href;
const locationSha512 = computeSha512(location);
const locationSha512Head = locationSha512.slice(0, 2);
const locationSha512Tail = locationSha512.slice(2);
const sha512Head = sha512.slice(0, 2);
const sha512Tail = sha512.slice(2);
const sourceMapTrackerDirectory = pathResolve(
sourceMapsTrackerDirectory,
locationSha512Head,
);
const sourceMapTrackerPath = pathResolve(
sourceMapTrackerDirectory,
locationSha512Tail,
);
const sourceMapDirectory = pathResolve(
sourceMapsCacheDirectory,
sha512Head,
);
const sourceMapPath = pathResolve(
sourceMapDirectory,
`${sha512Tail}.map.json`,
);

await fs.promises
.readFile(sourceMapTrackerPath, 'utf-8')
.then(async oldSha512 => {
oldSha512 = oldSha512.trim();
if (oldSha512 === sha512) {
return;
}
const oldSha512Head = oldSha512.slice(0, 2);
const oldSha512Tail = oldSha512.slice(2);
const oldSourceMapDirectory = pathResolve(
sourceMapsCacheDirectory,
oldSha512Head,
);
const oldSourceMapPath = pathResolve(
oldSourceMapDirectory,
`${oldSha512Tail}.map.json`,
);
await fs.promises.unlink(oldSourceMapPath);
const entries = await fs.promises.readdir(oldSourceMapDirectory);
if (entries.length === 0) {
await fs.promises.rmdir(oldSourceMapDirectory);
}
})
.catch(error => {
if (error.code !== 'ENOENT') {
throw error;
}
});
await fs.promises

Check warning on line 87 in packages/bundle-source/src/endo.js

View workflow job for this annotation

GitHub Actions / lint

The first `await` appearing in an async function must not be nested

Check warning on line 87 in packages/bundle-source/src/endo.js

View workflow job for this annotation

GitHub Actions / lint

The first `await` appearing in an async function must not be nested
.readFile(sourceMapTrackerPath, 'utf-8')
.then(async oldSha512 => {
oldSha512 = oldSha512.trim();
if (oldSha512 === sha512) {
return;
}
const oldSha512Head = oldSha512.slice(0, 2);
const oldSha512Tail = oldSha512.slice(2);
const oldSourceMapDirectory = pathResolve(
sourceMapsCacheDirectory,
oldSha512Head,
);
const oldSourceMapPath = pathResolve(
oldSourceMapDirectory,
`${oldSha512Tail}.map.json`,
);
await fs.promises.unlink(oldSourceMapPath);
const entries = await fs.promises.readdir(oldSourceMapDirectory);
if (entries.length === 0) {
await fs.promises.rmdir(oldSourceMapDirectory);
}
})
.catch(error => {
if (error.code !== 'ENOENT') {
throw error;
}
});

await fs.promises.mkdir(sourceMapDirectory, { recursive: true });
await fs.promises.writeFile(sourceMapPath, sourceMap);
await fs.promises.mkdir(sourceMapDirectory, { recursive: true });
await fs.promises.writeFile(sourceMapPath, sourceMap);

await fs.promises.mkdir(sourceMapTrackerDirectory, { recursive: true });
await fs.promises.writeFile(sourceMapTrackerPath, sha512);
await fs.promises.mkdir(sourceMapTrackerDirectory, { recursive: true });
await fs.promises.writeFile(sourceMapTrackerPath, sha512);
} finally {
endWriteSourceMap?.();
}
};
}

Expand All @@ -124,25 +138,44 @@
location,
sourceMap,
) => {
const endTransformModule = profiler?.startSpan(
'bundleSource.transformModule',
{
parser,
specifier,
location,
},
);
if (!['mjs', 'cjs'].includes(parser)) {
throw Error(`Parser ${parser} not supported in evadeEvalCensor`);
}
const babelSourceType = parser === 'mjs' ? 'module' : 'script';
const source = textDecoder.decode(sourceBytes);
const priorSourceMap =
typeof sourceMap === 'string' ? sourceMap : undefined;
const { code: object, map } = await evadeCensor(source, {
sourceType: babelSourceType,
sourceMap: priorSourceMap,
sourceUrl: new URL(specifier, location).href,
elideComments,
});
const objectBytes = textEncoder.encode(object);
return {
bytes: objectBytes,
parser,
sourceMap: map ? JSON.stringify(map) : undefined,
};
/** @type {number | undefined} */
let outputBytes;
try {
const { code: object, map } = await evadeCensor(source, {

Check warning on line 159 in packages/bundle-source/src/endo.js

View workflow job for this annotation

GitHub Actions / lint

The first `await` appearing in an async function must not be nested

Check warning on line 159 in packages/bundle-source/src/endo.js

View workflow job for this annotation

GitHub Actions / lint

The first `await` appearing in an async function must not be nested
sourceType: babelSourceType,
sourceMap: priorSourceMap,
sourceUrl: new URL(specifier, location).href,
elideComments,
profileStartSpan: profiler?.startSpan,
});
const objectBytes = textEncoder.encode(object);
outputBytes = objectBytes.length;
return {
bytes: objectBytes,
parser,
sourceMap: map ? JSON.stringify(map) : undefined,
};
} finally {
endTransformModule?.({
inputBytes: sourceBytes.length,
outputBytes,
});
}
};

/** @type {ParserForLanguageLike} */
Expand Down Expand Up @@ -199,18 +232,26 @@
packageLocation,
options = undefined,
) {
const endTypeErasure = profiler?.startSpan('bundleSource.typeErase', {
parser: 'mts',
specifier,
});
const sourceText = textDecoder.decode(sourceBytes);
const { code: objectText } = transformSync(sourceText, {
mode: 'strip-only',
});
const objectBytes = textEncoder.encode(objectText);
return parserForLanguage.mjs.parse(
objectBytes,
specifier,
moduleLocation,
packageLocation,
options,
);
try {
return parserForLanguage.mjs.parse(
objectBytes,
specifier,
moduleLocation,
packageLocation,
options,
);
} finally {
endTypeErasure?.();
}
},
heuristicImports: false,
synchronous: false,
Expand All @@ -225,18 +266,26 @@
packageLocation,
options = undefined,
) {
const endTypeErasure = profiler?.startSpan('bundleSource.typeErase', {
parser: 'cts',
specifier,
});
const sourceText = textDecoder.decode(sourceBytes);
const { code: objectText } = transformSync(sourceText, {
mode: 'strip-only',
});
const objectBytes = textEncoder.encode(objectText);
return parserForLanguage.cjs.parse(
objectBytes,
specifier,
moduleLocation,
packageLocation,
options,
);
try {
return parserForLanguage.cjs.parse(
objectBytes,
specifier,
moduleLocation,
packageLocation,
options,
);
} finally {
endTypeErasure?.();
}
},
heuristicImports: true,
synchronous: false,
Expand All @@ -245,9 +294,11 @@
parserForLanguage = { ...parserForLanguage, mts: mtsParser, cts: ctsParser };

/** @type {BundlingKit['sourceMapHook']} */
const sourceMapHook = (sourceMap, sourceDescriptor) => {
sourceMapJobs.add(writeSourceMap(sourceMap, sourceDescriptor));
};
const sourceMapHook =
writeSourceMap &&
((sourceMap, sourceDescriptor) => {
sourceMapJobs.add(writeSourceMap(sourceMap, sourceDescriptor));
});

const workspaceLanguageForExtension = { mts: 'mts', cts: 'cts' };
const workspaceModuleLanguageForExtension = { ts: 'mts' };
Expand Down
1 change: 1 addition & 0 deletions packages/bundle-source/src/exports.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type {
BundleProfilingOptions,
BundleOptions,
BundleSource,
BundleSourceResult,
Expand Down
Loading
Loading