-
Notifications
You must be signed in to change notification settings - Fork 12k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
perf(@angular-devkit/build-angular): use worker pool for JavaScript t…
…ransforms in esbuild builder When using the experimental esbuild-based browser application builder, the JavaScript transformation steps of the build process will now be performed within a worker pool to allow for the steps to be executed in parallel when possible. This also moves the steps off of the main thread which provides more time for the build orchestration and esbuild integration code to execute.
- Loading branch information
Showing
3 changed files
with
200 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
97 changes: 97 additions & 0 deletions
97
...ngular_devkit/build_angular/src/builders/browser-esbuild/javascript-transformer-worker.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import { transformAsync } from '@babel/core'; | ||
import { readFile } from 'node:fs/promises'; | ||
import angularApplicationPreset from '../../babel/presets/application'; | ||
import { requiresLinking } from '../../babel/webpack-loader'; | ||
import { loadEsmModule } from '../../utils/load-esm'; | ||
|
||
interface JavaScriptTransformRequest { | ||
filename: string; | ||
data: string; | ||
sourcemap: boolean; | ||
thirdPartySourcemaps: boolean; | ||
advancedOptimizations: boolean; | ||
forceAsyncTransformation?: boolean; | ||
skipLinker: boolean; | ||
} | ||
|
||
export default async function transformJavaScript( | ||
request: JavaScriptTransformRequest, | ||
): Promise<Uint8Array> { | ||
request.data ??= await readFile(request.filename, 'utf-8'); | ||
const transformedData = await transformWithBabel(request); | ||
|
||
return Buffer.from(transformedData, 'utf-8'); | ||
} | ||
|
||
let linkerPluginCreator: | ||
| typeof import('@angular/compiler-cli/linker/babel').createEs2015LinkerPlugin | ||
| undefined; | ||
|
||
async function transformWithBabel({ | ||
filename, | ||
data, | ||
...options | ||
}: JavaScriptTransformRequest): Promise<string> { | ||
const forceAsyncTransformation = | ||
options.forceAsyncTransformation ?? | ||
(!/[\\/][_f]?esm2015[\\/]/.test(filename) && /async\s+function\s*\*/.test(data)); | ||
const shouldLink = !options.skipLinker && (await requiresLinking(filename, data)); | ||
const useInputSourcemap = | ||
options.sourcemap && | ||
(!!options.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename)); | ||
|
||
// If no additional transformations are needed, return the data directly | ||
if (!forceAsyncTransformation && !options.advancedOptimizations && !shouldLink) { | ||
// Strip sourcemaps if they should not be used | ||
return useInputSourcemap ? data : data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''); | ||
} | ||
|
||
const angularPackage = /[\\/]node_modules[\\/]@angular[\\/]/.test(filename); | ||
|
||
// Lazy load the linker plugin only when linking is required | ||
if (shouldLink) { | ||
linkerPluginCreator ??= ( | ||
await loadEsmModule<typeof import('@angular/compiler-cli/linker/babel')>( | ||
'@angular/compiler-cli/linker/babel', | ||
) | ||
).createEs2015LinkerPlugin; | ||
} | ||
|
||
const result = await transformAsync(data, { | ||
filename, | ||
inputSourceMap: (useInputSourcemap ? undefined : false) as undefined, | ||
sourceMaps: options.sourcemap ? 'inline' : false, | ||
compact: false, | ||
configFile: false, | ||
babelrc: false, | ||
browserslistConfigFile: false, | ||
plugins: [], | ||
presets: [ | ||
[ | ||
angularApplicationPreset, | ||
{ | ||
angularLinker: linkerPluginCreator && { | ||
shouldLink, | ||
jitMode: false, | ||
linkerPluginCreator, | ||
}, | ||
forceAsyncTransformation, | ||
optimize: options.advancedOptimizations && { | ||
looseEnums: angularPackage, | ||
pureTopLevel: angularPackage, | ||
}, | ||
}, | ||
], | ||
], | ||
}); | ||
|
||
return result?.code ?? data; | ||
} |
91 changes: 91 additions & 0 deletions
91
packages/angular_devkit/build_angular/src/builders/browser-esbuild/javascript-transformer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import Piscina from 'piscina'; | ||
|
||
/** | ||
* Transformation options that should apply to all transformed files and data. | ||
*/ | ||
export interface JavaScriptTransformerOptions { | ||
sourcemap: boolean; | ||
thirdPartySourcemaps?: boolean; | ||
advancedOptimizations?: boolean; | ||
} | ||
|
||
/** | ||
* A class that performs transformation of JavaScript files and raw data. | ||
* A worker pool is used to distribute the transformation actions and allow | ||
* parallel processing. Transformation behavior is based on the filename and | ||
* data. Transformations may include: async downleveling, Angular linking, | ||
* and advanced optimizations. | ||
*/ | ||
export class JavaScriptTransformer { | ||
#workerPool: Piscina; | ||
|
||
constructor(private options: JavaScriptTransformerOptions, maxThreads?: number) { | ||
this.#workerPool = new Piscina({ | ||
filename: require.resolve('./javascript-transformer-worker'), | ||
maxThreads, | ||
}); | ||
} | ||
|
||
/** | ||
* Performs JavaScript transformations on a file from the filesystem. | ||
* If no transformations are required, the data for the original file will be returned. | ||
* @param filename The full path to the file. | ||
* @returns A promise that resolves to a UTF-8 encoded Uint8Array containing the result. | ||
*/ | ||
transformFile(filename: string): Promise<Uint8Array> { | ||
// Always send the request to a worker. Files are almost always from node modules which measn | ||
// they may need linking. The data is also not yet available to perform most transformation checks. | ||
return this.#workerPool.run({ | ||
filename, | ||
...this.options, | ||
}); | ||
} | ||
|
||
/** | ||
* Performs JavaScript transformations on the provided data of a file. The file does not need | ||
* to exist on the filesystem. | ||
* @param filename The full path of the file represented by the data. | ||
* @param data The data of the file that should be transformed. | ||
* @param skipLinker If true, bypass all Angular linker processing; if false, attempt linking. | ||
* @returns A promise that resolves to a UTF-8 encoded Uint8Array containing the result. | ||
*/ | ||
async transformData(filename: string, data: string, skipLinker: boolean): Promise<Uint8Array> { | ||
// Perform a quick test to determine if the data needs any transformations. | ||
// This allows directly returning the data without the worker communication overhead. | ||
let forceAsyncTransformation; | ||
if (skipLinker && !this.options.advancedOptimizations) { | ||
// If the linker is being skipped and no optimizations are needed, only async transformation is left. | ||
// This checks for async generator functions. All other async transformation is handled by esbuild. | ||
forceAsyncTransformation = data.includes('async') && /async\s+function\s*\*/.test(data); | ||
|
||
if (!forceAsyncTransformation) { | ||
return Buffer.from(data, 'utf-8'); | ||
} | ||
} | ||
|
||
return this.#workerPool.run({ | ||
filename, | ||
data, | ||
// Send the async check result if present to avoid rechecking in the worker | ||
forceAsyncTransformation, | ||
skipLinker, | ||
...this.options, | ||
}); | ||
} | ||
|
||
/** | ||
* Stops all active transformation tasks and shuts down all workers. | ||
* @returns A void promise that resolves when closing is complete. | ||
*/ | ||
close(): Promise<void> { | ||
return this.#workerPool.destroy(); | ||
} | ||
} |