Skip to content

Commit

Permalink
perf(@angular-devkit/build-angular): render Sass using a pool of workers
Browse files Browse the repository at this point in the history
A pool of Workers is now used to process Sass render requests. This change allows multiple synchronous render operations to occur at the same time. Sass synchronous render operations can be up to two times faster than the asynchronous variant. The benefit will be most pronounced in applications with large amounts of Sass stylesheets.

(cherry picked from commit 438c6d9)
  • Loading branch information
clydin authored and alan-agius4 committed May 17, 2021
1 parent c0694ca commit 7718efe
Showing 1 changed file with 29 additions and 6 deletions.
35 changes: 29 additions & 6 deletions packages/angular_devkit/build_angular/src/sass/sass-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

import { Importer, ImporterReturnType, Options, Result, SassException } from 'sass';
import { MessageChannel, Worker } from 'worker_threads';
import { maxWorkers } from '../utils/environment-options';

/**
* The maximum number of Workers that will be created to execute render requests.
*/
const MAX_RENDER_WORKERS = maxWorkers;

/**
* The callback type for the `dart-sass` asynchronous render function.
Expand All @@ -19,6 +25,7 @@ type RenderCallback = (error?: SassException, result?: Result) => void;
*/
interface RenderRequest {
id: number;
workerIndex: number;
callback: RenderCallback;
importers?: Importer[];
}
Expand All @@ -39,9 +46,11 @@ interface RenderResponseMessage {
* the worker which can be up to two times faster than the asynchronous variant.
*/
export class SassWorkerImplementation {
private worker?: Worker;
private readonly workers: Worker[] = [];
private readonly availableWorkers: number[] = [];
private readonly requests = new Map<number, RenderRequest>();
private idCounter = 1;
private nextWorkerIndex = 0;

/**
* Provides information about the Sass implementation.
Expand Down Expand Up @@ -74,14 +83,23 @@ export class SassWorkerImplementation {
throw new Error('Sass custom functions are not supported.');
}

if (!this.worker) {
this.worker = this.createWorker();
let workerIndex = this.availableWorkers.pop();
if (workerIndex === undefined) {
if (this.workers.length < MAX_RENDER_WORKERS) {
workerIndex = this.workers.length;
this.workers.push(this.createWorker());
} else {
workerIndex = this.nextWorkerIndex++;
if (this.nextWorkerIndex >= this.workers.length) {
this.nextWorkerIndex = 0;
}
}
}

const request = this.createRequest(callback, importer);
const request = this.createRequest(workerIndex, callback, importer);
this.requests.set(request.id, request);

this.worker.postMessage({
this.workers[workerIndex].postMessage({
id: request.id,
hasImporter: !!importer,
options: serializableOptions,
Expand All @@ -96,7 +114,9 @@ export class SassWorkerImplementation {
* is only needed if early cleanup is needed.
*/
close(): void {
this.worker?.terminate();
for (const worker of this.workers) {
void worker.terminate();
}
this.requests.clear();
}

Expand All @@ -117,6 +137,7 @@ export class SassWorkerImplementation {
}

this.requests.delete(response.id);
this.availableWorkers.push(request.workerIndex);

if (response.result) {
// The results are expected to be Node.js `Buffer` objects but will each be transferred as
Expand Down Expand Up @@ -193,11 +214,13 @@ export class SassWorkerImplementation {
}

private createRequest(
workerIndex: number,
callback: RenderCallback,
importer: Importer | Importer[] | undefined,
): RenderRequest {
return {
id: this.idCounter++,
workerIndex,
callback,
importers: !importer || Array.isArray(importer) ? importer : [importer],
};
Expand Down

0 comments on commit 7718efe

Please sign in to comment.