Repository navigation
[Bug]: bazel_sandbox_plugin is very slow #190
Copy link
Copy link
Open
Labels
bugSomething isn't workingSomething isn't workingneed: investigationInvestigation required to proceed furtherInvestigation required to proceed furtherperformanceImprove performance of existing featuresImprove performance of existing features
Description
Activity
By caching the resolution results for certain paths, I was able to decrease the plugin run time on one project for me from about ~7.5s to ~3.5s. I'm going to try messing with the go plugin first before I create a PR though.
For anyone interested, my changed bazel-sandbox.js:
const path = require('path') const process = require('process') // Regex matching any non-relative import path const pkgImport = /^[^.]/ const bindir = process.env.BAZEL_BINDIR const execroot = process.env.JS_BINARY__EXECROOT function isCachablePath(importPath) { // If it doesn't start with . or '/', we can't reliably // resolve the path from resolveDir because it may be an // alias return importPath.startsWith('.') || importPath.startsWith('/') || importPath.startsWith('\\') } // Under Bazel, esbuild will follow symlinks out of the sandbox when the sandbox is enabled. See https://github.com/aspect-build/rules_esbuild/issues/58. // This plugin using a separate resolver to detect if the the resolution has left the execroot (which is the root of the sandbox // when sandboxing is enabled) and patches the resolution back into the sandbox. function bazelSandboxPlugin() { return { name: 'bazel-sandbox', setup(build) { const moduleCache = new Map() const alreadyResolvedPaths = new Map(); const startTime = Date.now(); let cacheHits = 0; let totalCalls = 0; build.onEnd(() => { console.error('Build Time:', Date.now() - startTime); console.error(`Cache efficiency: ${cacheHits}/${totalCalls}`); }); build.onResolve( { filter: /./ }, async ({ path: importPath, ...otherOptions }) => { // NB: these lines are to prevent infinite recursion when we call `build.resolve`. if (otherOptions.pluginData) { if (otherOptions.pluginData.executedSandboxPlugin) { return } } else { otherOptions.pluginData = {} } otherOptions.pluginData.executedSandboxPlugin = true totalCalls += 1; let resolver; const promisedResolution = new Promise((resolve) => { resolver = resolve; }); if (isCachablePath(importPath)) { const resolvedPath = path.resolve(otherOptions.resolveDir, importPath) const resolution = alreadyResolvedPaths.get(resolvedPath); if (resolution) { cacheHits += 1; if (resolution instanceof Promise) { // There's already a resolution in progress for this path. // We just need to wait for the result return await resolution; } else { return resolution; } } else { alreadyResolvedPaths.set(resolvedPath, promisedResolution); } } const sub = async () => { // Prevent us from loading different forms of a module (CJS vs ESM). if (pkgImport.test(importPath)) { if (!moduleCache.has(importPath)) { moduleCache.set( importPath, resolveInExecroot(build, importPath, otherOptions) ) } return await moduleCache.get(importPath) } return await resolveInExecroot(build, importPath, otherOptions) } const result = await sub(); if (isCachablePath(importPath)) { const resolvedPath = path.resolve(otherOptions.resolveDir, importPath) alreadyResolvedPaths.set(resolvedPath, result); } resolver(result); return result; } ) }, } } async function resolveInExecroot(build, importPath, otherOptions) { const result = await build.resolve(importPath, otherOptions) if (result.errors && result.errors.length) { // There was an error resolving, just return the error as-is. return result } if ( !result.path.startsWith('.') && !result.path.startsWith('/') && !result.path.startsWith('\\') ) { // Not a relative or absolute path. Likely a module resolution that is marked "external" return result } // If esbuild attempts to leave the execroot, map the path back into the execroot. if (!result.path.startsWith(execroot)) { // If it tried to leave bazel-bin, error out completely. if (!result.path.includes(bindir)) { throw new Error( `Error: esbuild resolved a path outside of BAZEL_BINDIR (${bindir}): ${result.path}` ) } // Otherwise remap the bindir-relative path const correctedPath = path.join( execroot, result.path.substring(result.path.indexOf(bindir)) ) if (!!process.env.JS_BINARY__LOG_DEBUG) { console.error( `DEBUG: [bazel-sandbox] correcting esbuild resolution ${result.path} that left the sandbox to ${correctedPath}.` ) } result.path = correctedPath } return result } module.exports = { bazelSandboxPlugin }- addedperformanceImprove performance of existing featuresImprove performance of existing featuresneed: investigationInvestigation required to proceed furtherInvestigation required to proceed further
on Apr 25, 2025
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't workingneed: investigationInvestigation required to proceed furtherInvestigation required to proceed furtherperformanceImprove performance of existing featuresImprove performance of existing features
What happened?
#160 added a JavaScript plugin.
Users report on Slack that this new feature in 0.18.0 increases bundling times by an unacceptable amount.
Maybe it can simply be optimized.
Or it could use the esbuild Go API instead if someone has the time to evaluate that and re-write it.
Setting https://github.com/aspect-build/rules_esbuild/blob/main/docs/esbuild.md#esbuild_bundle-bazel_sandbox_plugin to
Falseis the workaround, though it reverts to earlier behavior of non-hermetic esbuild sometimes not picking up changes.Version
0.18.0
How to reproduce
No response
Any other information?
No response