Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@
"jasmine": "^3.3.1",
"jasmine-core": "~3.7.0",
"jasmine-spec-reporter": "~7.0.0",
"jest-worker": "27.0.2",
"jquery": "^3.3.1",
"jsonc-parser": "3.0.0",
"karma": "~6.3.0",
Expand Down
1 change: 0 additions & 1 deletion packages/angular_devkit/build_angular/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ ts_library(
"@npm//glob",
"@npm//https-proxy-agent",
"@npm//inquirer",
"@npm//jest-worker",
"@npm//karma",
"@npm//karma-source-map-support",
"@npm//less",
Expand Down
1 change: 0 additions & 1 deletion packages/angular_devkit/build_angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
"glob": "7.1.7",
"https-proxy-agent": "5.0.0",
"inquirer": "8.1.1",
"jest-worker": "27.0.2",
"karma-source-map-support": "1.4.0",
"less": "4.1.1",
"less-loader": "10.0.0",
Expand Down
74 changes: 16 additions & 58 deletions packages/angular_devkit/build_angular/src/utils/action-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,73 +6,40 @@
* found in the LICENSE file at https://angular.io/license
*/

import { Worker as JestWorker } from 'jest-worker';
import * as os from 'os';
import * as path from 'path';
import { serialize } from 'v8';
import Piscina from 'piscina';
import { BundleActionCache } from './action-cache';
import { maxWorkers } from './environment-options';
import { I18nOptions } from './i18n-options';
import { InlineOptions, ProcessBundleOptions, ProcessBundleResult } from './process-bundle';

let workerFile = require.resolve('./process-bundle');
workerFile =
path.extname(workerFile) === '.ts' ? require.resolve('./process-bundle-bootstrap') : workerFile;
const workerFile = require.resolve('./process-bundle');

export class BundleActionExecutor {
private largeWorker?: JestWorker;
private smallWorker?: JestWorker;
private workerPool?: Piscina;
private cache?: BundleActionCache;

constructor(
private workerOptions: { cachePath?: string; i18n: I18nOptions },
integrityAlgorithm?: string,
private readonly sizeThreshold = 32 * 1024,
) {
if (workerOptions.cachePath) {
this.cache = new BundleActionCache(workerOptions.cachePath, integrityAlgorithm);
}
}

private static executeMethod<O>(worker: JestWorker, method: string, input: unknown): Promise<O> {
return (worker as unknown as Record<string, (i: unknown) => Promise<O>>)[method](input);
}

private ensureLarge(): JestWorker {
if (this.largeWorker) {
return this.largeWorker;
}

// larger files are processed in a separate process to limit memory usage in the main process
return (this.largeWorker = new JestWorker(workerFile, {
exposedMethods: ['process', 'inlineLocales'],
setupArgs: [[...serialize(this.workerOptions)]],
numWorkers: maxWorkers,
}));
}

private ensureSmall(): JestWorker {
if (this.smallWorker) {
return this.smallWorker;
private ensureWorkerPool(): Piscina {
if (this.workerPool) {
return this.workerPool;
}

// small files are processed in a limited number of threads to improve speed
// The limited number also prevents a large increase in memory usage for an otherwise short operation
return (this.smallWorker = new JestWorker(workerFile, {
exposedMethods: ['process', 'inlineLocales'],
setupArgs: [this.workerOptions],
numWorkers: os.cpus().length < 2 ? 1 : 2,
enableWorkerThreads: true,
}));
}
this.workerPool = new Piscina({
filename: workerFile,
name: 'process',
workerData: this.workerOptions,
maxThreads: maxWorkers,
});

private executeAction<O>(method: string, action: { code: string }): Promise<O> {
// code.length is not an exact byte count but close enough for this
if (action.code.length > this.sizeThreshold) {
return BundleActionExecutor.executeMethod<O>(this.ensureLarge(), method, action);
} else {
return BundleActionExecutor.executeMethod<O>(this.ensureSmall(), method, action);
}
return this.workerPool;
}

async process(action: ProcessBundleOptions): Promise<ProcessBundleResult> {
Expand All @@ -89,7 +56,7 @@ export class BundleActionExecutor {
} catch {}
}

return this.executeAction<ProcessBundleResult>('process', action);
return this.ensureWorkerPool().run(action, { name: 'process' });
}

processAll(actions: Iterable<ProcessBundleOptions>): AsyncIterable<ProcessBundleResult> {
Expand All @@ -99,7 +66,7 @@ export class BundleActionExecutor {
async inline(
action: InlineOptions,
): Promise<{ file: string; diagnostics: { type: string; message: string }[]; count: number }> {
return this.executeAction('inlineLocales', action);
return this.ensureWorkerPool().run(action, { name: 'inlineLocales' });
}

inlineAll(actions: Iterable<InlineOptions>) {
Expand Down Expand Up @@ -129,15 +96,6 @@ export class BundleActionExecutor {
}

stop(): void {
// Floating promises are intentional here
// https://github.com/facebook/jest/tree/56079a5aceacf32333089cea50c64385885fee26/packages/jest-worker#end
if (this.largeWorker) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.largeWorker.end();
}
if (this.smallWorker) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.smallWorker.end();
}
void this.workerPool?.destroy();
}
}
39 changes: 21 additions & 18 deletions packages/angular_devkit/build_angular/src/utils/process-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,16 @@ import * as fs from 'fs';
import * as path from 'path';
import { RawSourceMap, SourceMapConsumer, SourceMapGenerator } from 'source-map';
import { minify } from 'terser';
import * as v8 from 'v8';
import { sources } from 'webpack';
import { workerData } from 'worker_threads';
import { allowMangle, allowMinify, shouldBeautify } from './environment-options';
import { I18nOptions } from './i18n-options';

const { ConcatSource, OriginalSource, ReplaceSource, SourceMapSource } = sources;

type LocalizeUtilities = typeof import('@angular/localize/src/tools/src/source_file_utils');

// Lazy loaded webpack-sources object
// Webpack is only imported if needed during the processing
let webpackSources: typeof import('webpack').sources | undefined;

// If code size is larger than 500KB, consider lower fidelity but faster sourcemap merge
const FAST_SOURCEMAP_THRESHOLD = 500 * 1024;

Expand Down Expand Up @@ -78,16 +79,7 @@ export const enum CacheKey {
DownlevelMap = 3,
}

let cachePath: string | undefined;
let i18n: I18nOptions | undefined;

export function setup(data: number[] | { cachePath: string; i18n: I18nOptions }): void {
const options = Array.isArray(data)
? (v8.deserialize(Buffer.from(data)) as { cachePath: string; i18n: I18nOptions })
: data;
cachePath = options.cachePath;
i18n = options.i18n;
}
const { cachePath, i18n } = (workerData || {}) as { cachePath?: string; i18n?: I18nOptions };

async function cachePut(
content: string,
Expand Down Expand Up @@ -224,9 +216,14 @@ async function mergeSourceMaps(
return mergeSourceMapsFast(inputSourceMap, resultSourceMap);
}

// Load Webpack only when needed
if (webpackSources === undefined) {
webpackSources = (await import('webpack')).sources;
}

// SourceMapSource produces high-quality sourcemaps
// Final sourcemap will always be available when providing the input sourcemaps
const finalSourceMap = new SourceMapSource(
const finalSourceMap = new webpackSources.SourceMapSource(
resultCode,
filename,
resultSourceMap,
Expand Down Expand Up @@ -413,7 +410,7 @@ async function terserMangle(
code,
options.map,
outputCode,
(minifyOutput.map as unknown) as RawSourceMap,
minifyOutput.map as unknown as RawSourceMap,
options.filename || '0',
code.length > FAST_SOURCEMAP_THRESHOLD,
);
Expand Down Expand Up @@ -735,6 +732,12 @@ async function inlineLocalesDirect(ast: ParseResult, options: InlineOptions) {
delete inputMap.sourceRoot;
}

// Load Webpack only when needed
if (webpackSources === undefined) {
webpackSources = (await import('webpack')).sources;
}
const { ConcatSource, OriginalSource, ReplaceSource, SourceMapSource } = webpackSources;

for (const locale of i18n.inlineLocales) {
const content = new ReplaceSource(
inputMap
Expand All @@ -761,12 +764,12 @@ async function inlineLocalesDirect(ast: ParseResult, options: InlineOptions) {
content.replace(position.start, position.end - 1, code);
}

let outputSource: sources.Source = content;
let outputSource: import('webpack').sources.Source = content;
if (options.setLocale) {
const setLocaleText = `var $localize=Object.assign(void 0===$localize?{}:$localize,{locale:"${locale}"});\n`;

// If locale data is provided, load it and prepend to file
let localeDataSource: sources.Source | null = null;
let localeDataSource;
const localeDataPath = i18n.locales[locale] && i18n.locales[locale].dataPath;
if (localeDataPath) {
const localeDataContent = await loadLocaleData(localeDataPath, true, options.es5);
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6695,7 +6695,7 @@ jasminewd2@^2.1.0:
resolved "https://registry.yarnpkg.com/jasminewd2/-/jasminewd2-2.2.0.tgz#e37cf0b17f199cce23bea71b2039395246b4ec4e"
integrity sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=

jest-worker@27.0.2, jest-worker@^27.0.2:
jest-worker@^27.0.2:
version "27.0.2"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.0.2.tgz#4ebeb56cef48b3e7514552f80d0d80c0129f0b05"
integrity sha512-EoBdilOTTyOgmHXtw/cPc+ZrCA0KJMrkXzkrPGNwLmnvvlN1nj7MPrxpT7m+otSv2e1TLaVffzDnE/LB14zJMg==
Expand Down