perf(@angular/build): use native sass-embedded compiler daemon - #33935
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces support for the persistent Dart Sass embedded compiler (sass-embedded) via a new SassAsyncCompilerImplementation. It refactors the existing worker-based Sass compilation into SassWorkerImplementation and introduces a conditional check (useSassWorker) to choose between the native asynchronous compiler and the worker-based fallback. Feedback on the changes highlights potential issues with error handling during asynchronous initialization and shutdown, specifically recommending try-finally blocks to ensure failed initialization promises are cleared so subsequent attempts can retry, and a try-catch block in close() to prevent initialization failures from disrupting the shutdown sequence. Additionally, it is suggested to validate importers upfront in SassAsyncCompilerImplementation to match the fail-fast behavior of the worker implementation.
e682431 to
cb652c2
Compare
Replaces the Piscina-based worker thread pool executing pure-JS Dart Sass (`sass` compiled via `dart2js`) with a native Dart Sass asynchronous compiler daemon (`sass-embedded`). The build system previously relied on multi-threaded worker pools with synchronous compilation to bypass performance overheads inherent to pure-JS asynchronous Dart Sass. However, this approach incurred significant V8 heap and isolate duplication across worker threads, as well as thread contention and blocking due to `Atomics.wait()` synchronization loops during custom URL and module rebasing. With `sass-embedded`, Sass compilation is delegated to a long-lived native Dart AOT binary communicating asynchronously via Protocol Buffers over stdio pipes. This eliminates duplicate V8 isolates, replaces synchronous `Atomics.wait()` with event-driven asynchronous message handling, and dramatically reduces memory pressure on the Node.js process. The compiler infrastructure is structured around a shared `SassServiceImplementation` interface with two isolated implementations dynamically loaded on demand: - `SassAsyncCompilerImplementation`: Manages the native `sass-embedded` daemon lifecycle and asynchronous URL rebasing importers (`AsyncModuleUrlRebasingImporter`). - `SassWorkerImplementation`: Retained as an isolated worker pool fallback. Environment Variable & Fallbacks: - `NG_BUILD_SASS_WORKER=1` (or `true`): Forces the build system to fall back to the pure-JS `SassWorkerImplementation` using worker threads. - In restricted environments where native platform binaries cannot be spawned (such as WebContainers detected via `process.versions.webcontainer`), the build system automatically falls back to `SassWorkerImplementation` without requiring manual configuration. ### Benchmarks: Sass Service in Isolation #### 10 Small Files (Angular Components) | Metric | Worker Pool (`main`) | Native Async (`this change`) | Delta | Speedup / Improvement | | :--- | :---: | :---: | :---: | :---: | | Cold Start | 369.04 ms | 229.89 ms | -139.15 ms | 1.61x faster (-37.7%) | | Warm Median | 22.46 ms | 5.27 ms | -17.19 ms | 4.26x faster (-76.5%) | | Warm Mean | 25.82 ms | 7.25 ms | -18.57 ms | 3.56x faster (-71.9%) | | Warm P95 | 37.30 ms | 14.55 ms | -22.75 ms | 2.56x faster (-61.0%) | | Warm Throughput | 445.2 files/s | 1,899.3 files/s | +1,454.1 files/s | 4.27x throughput (+326.6%) | | Incremental Median | 6.54 ms | 2.08 ms | -4.46 ms | 3.14x faster (-68.2%) | | Incremental Throughput | 153.0 files/s | 480.5 files/s | +327.5 files/s | 3.14x throughput (+214.0%) | | Peak Memory RSS | 281.16 MB | 78.73 MB | -202.43 MB | -72.0% memory saved | #### 10 Large Files (Design System Tokens & Themes) | Metric | Worker Pool (`main`) | Native Async (`this change`) | Delta | Speedup / Improvement | | :--- | :---: | :---: | :---: | :---: | | Cold Start | 823.95 ms | 386.25 ms | -437.70 ms | 2.13x faster (-53.1%) | | Warm Median | 231.85 ms | 175.64 ms | -56.21 ms | 1.32x faster (-24.2%) | | Warm Mean | 241.50 ms | 172.22 ms | -69.28 ms | 1.40x faster (-28.7%) | | Warm P95 | 293.09 ms | 215.48 ms | -77.61 ms | 1.36x faster (-26.5%) | | Warm Throughput | 43.1 files/s | 56.9 files/s | +13.8 files/s | 1.32x throughput (+32.0%) | | Incremental Median | 72.63 ms | 35.34 ms | -37.29 ms | 2.06x faster (-51.3%) | | Incremental Throughput | 13.8 files/s | 28.3 files/s | +14.5 files/s | 2.05x throughput (+105.4%) | | Peak Memory RSS | 986.47 MB | 112.34 MB | -874.13 MB | -88.6% memory saved | ### Benchmarks: Real Angular CLI Application Scenarios (ng build) #### Small App (5 small files) | Phase / Metric | Main (`NG_BUILD_SASS_WORKER=1`) | This Change (Native Async) | Delta | Improvement | | :--- | :---: | :---: | :---: | :---: | | Cold Wall Time | 3,201.3 ms | 2,950.2 ms | -251.1 ms | -7.8% | | Cold CLI Time | 2,456.0 ms | 2,302.0 ms | -154.0 ms | -6.3% | | Cold Peak RSS | 1,227.3 MB | 1,058.9 MB | -168.4 MB | -13.7% | | Warm (Avg) Wall Time | 2,762.0 ms | 2,475.3 ms | -286.7 ms | -10.4% | | Warm (Avg) CLI Time | 2,071.3 ms | 1,820.3 ms | -251.0 ms | -12.1% | | Warm (Avg) Peak RSS | 1,102.3 MB | 894.8 MB | -207.5 MB | -18.8% | #### Small App (5 large files) | Phase / Metric | Main (`NG_BUILD_SASS_WORKER=1`) | This Change (Native Async) | Delta | Improvement | | :--- | :---: | :---: | :---: | :---: | | Cold Wall Time | 3,605.9 ms | 3,175.8 ms | -430.1 ms | -11.9% | | Cold CLI Time | 2,874.0 ms | 2,482.0 ms | -392.0 ms | -13.6% | | Cold Peak RSS | 1,420.2 MB | 1,072.3 MB | -347.9 MB | -24.5% | | Warm (Avg) Wall Time | 3,123.7 ms | 2,682.5 ms | -441.2 ms | -14.1% | | Warm (Avg) CLI Time | 2,435.7 ms | 2,028.7 ms | -407.0 ms | -16.7% | | Warm (Avg) Peak RSS | 1,278.2 MB | 911.3 MB | -366.9 MB | -28.7% | #### Large App (500 files: 250 large + 250 small) | Phase / Metric | Main (`NG_BUILD_SASS_WORKER=1`) | This Change (Native Async) | Delta | Improvement | | :--- | :---: | :---: | :---: | :---: | | Cold Wall Time | 15,259.7 ms | 15,348.6 ms | +88.9 ms | +0.6% (Parity) | | Cold CLI Time | 14,425.0 ms | 14,373.0 ms | -52.0 ms | -0.4% (Parity) | | Cold Peak RSS | 2,305.1 MB | 1,582.7 MB | -722.4 MB | -31.3% memory saved | | Warm Run 1 Wall Time | 14,673.1 ms | 14,277.1 ms | -396.0 ms | -2.7% | | Warm Run 1 CLI Time | 13,812.0 ms | 13,375.0 ms | -437.0 ms | -3.2% | | Warm Run 1 Peak RSS | 2,180.5 MB | 1,485.6 MB | -694.9 MB | -31.9% memory saved | | Warm Run 2 Wall Time | 14,771.2 ms | 14,502.7 ms | -268.5 ms | -1.8% | | Warm Run 2 CLI Time | 13,943.0 ms | 13,527.0 ms | -416.0 ms | -3.0% | | Warm Run 2 Peak RSS | 2,220.8 MB | 1,493.3 MB | -727.5 MB | -32.8% memory saved | | Warm (Avg) Peak RSS | 2,200.7 MB | 1,489.5 MB | -711.2 MB | -32.3% memory saved | Closes angular#29319
cb652c2 to
8686fc5
Compare
|
This PR was merged into the repository. The changes were merged into the following branches:
|
Replaces the Piscina-based worker thread pool executing pure-JS Dart Sass
(
sasscompiled viadart2js) with a native Dart Sass asynchronous compilerdaemon (
sass-embedded).The build system previously relied on multi-threaded worker pools with synchronous
compilation to bypass performance overheads inherent to pure-JS asynchronous Dart
Sass. However, this approach incurred significant V8 heap and isolate duplication
across worker threads, as well as thread contention and blocking due to
Atomics.wait()synchronization loops during custom URL and module rebasing.With
sass-embedded, Sass compilation is delegated to a long-lived native DartAOT binary communicating asynchronously via Protocol Buffers over stdio pipes.
This eliminates duplicate V8 isolates, replaces synchronous
Atomics.wait()with event-driven asynchronous message handling, and dramatically reduces memory
pressure on the Node.js process.
The compiler infrastructure is structured around a shared
SassServiceImplementationinterface with two isolated implementations dynamically loaded on demand:
SassAsyncCompilerImplementation: Manages the nativesass-embeddeddaemon lifecycleand asynchronous URL rebasing importers (
AsyncModuleUrlRebasingImporter).SassWorkerImplementation: Retained as an isolated worker pool fallback.Environment Variable & Fallbacks
NG_BUILD_SASS_WORKER=1(ortrue): Forces the build system to fall back tothe pure-JS
SassWorkerImplementationusing worker threads.(such as WebContainers detected via
process.versions.webcontainer), the buildsystem automatically falls back to
SassWorkerImplementationwithout requiringmanual configuration.
Benchmarks: Sass Service in Isolation
10 Small Files (Angular Components)
main)this change)10 Large Files (Design System Tokens & Themes)
main)this change)Benchmarks: Real Angular CLI Application Scenarios (ng build)
Small App (5 small files)
NG_BUILD_SASS_WORKER=1)Small App (5 large files)
NG_BUILD_SASS_WORKER=1)Large App (500 files: 250 large + 250 small)
NG_BUILD_SASS_WORKER=1)Closes #29319