Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from '../../tools/esbuild/bundler-files';
import { useRolldownChunks } from '../../utils/environment-options';
import { assertIsError } from '../../utils/error';
import { loadRolldown } from '../../utils/load-rolldown';
import { toPosixPath } from '../../utils/path';

/**
Expand Down Expand Up @@ -116,7 +117,7 @@ function bundleOutputToEsbuildMetafile(
// Read once per module: in Rolldown, `renderedLength` is an uncached getter that
// copies the entire module code across the NAPI bridge on each access.
const { renderedLength } = renderedModule;

for (const [originalInputPath, originalInputInfo] of Object.entries(
originalOutputEntry.inputs,
)) {
Expand Down Expand Up @@ -296,7 +297,7 @@ export async function optimizeChunks(
];

if (useRolldownChunks) {
const { rolldown } = await import('rolldown');
const { rolldown } = await loadRolldown();
bundle = await rolldown({
input: mainFile,
plugins,
Expand Down
16 changes: 8 additions & 8 deletions packages/angular/build/src/builders/library/pipeline/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,8 @@

import assert from 'node:assert';
import path from 'node:path';
import {
type OutputChunk,
type Plugin,
type RolldownOutput,
type RolldownPluginOption,
rolldown,
} from 'rolldown';
import { dts } from 'rolldown-plugin-dts';
import type { OutputChunk, Plugin, RolldownOutput, RolldownPluginOption } from 'rolldown';
import { loadRolldown } from '../../../utils/load-rolldown';
import { toPosixPath } from '../../../utils/path';
import type { BundleResult, NormalizedEntryPoint, NormalizedLibraryOptions } from '../types';
import {
Expand Down Expand Up @@ -317,6 +311,7 @@ async function executeMultiBundle(
sourcemap = true;
}

const { rolldown } = await loadRolldown();
const bundle = await rolldown({
context: 'this',
input,
Expand Down Expand Up @@ -389,6 +384,11 @@ async function bundleAllDts(
return { filesToEmit: [], moduleIdsByBundle: new Map() };
}

// Ensure Rolldown's native binding is initialized via `loadRolldown()` before evaluating
// `rolldown-plugin-dts`, which has top-level imports of `rolldown/experimental`.
await loadRolldown();
const { dts } = await import('rolldown-plugin-dts');

const dtsSourcemap = options.declarationMap;
// Filter out `rolldown-plugin-dts:resolver` because all `.d.ts` files are already emitted
// in-memory by the Angular/TypeScript compilation and resolved via `createMemoryFileLoaderPlugin`.
Expand Down
47 changes: 47 additions & 0 deletions packages/angular/build/src/utils/load-rolldown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @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.dev/license
*/

const MIMALLOC_PURGE_DELAY = 'MIMALLOC_PURGE_DELAY';

let rolldownPromise: Promise<typeof import('rolldown')> | undefined;

/**
* Dynamically loads the `rolldown` module while configuring its embedded
* mimalloc v3 allocator to immediately purge freed arena memory.
*
* Rolldown's native binding statically links mimalloc v3, which defaults to a
* 4-second arena purge delay (`MIMALLOC_PURGE_DELAY=1000` * `MIMALLOC_ARENA_PURGE_MULT=4`)
* and only triggers purges cooperatively during subsequent allocations. In single-run
* CLI builds where Rolldown is not invoked again after bundling completes, non-zero
* purge delays prevent freed arena pages from being decommitted back to the OS,
* leading to high peak RSS during post-bundle stages.
*
* Because mimalloc reads and caches environment options synchronously when the native
* `.node` addon is first loaded (`mi_process_init` -> `_mi_options_init`), temporarily
* setting `MIMALLOC_PURGE_DELAY=0` during the initial `import('rolldown')` call
* configures Rolldown's allocator without leaving the environment variable set for
* subsequent code or child processes.
*
* @returns A promise that resolves to the `rolldown` module namespace.
*/
export function loadRolldown(): Promise<typeof import('rolldown')> {
if (!rolldownPromise) {
const hasCustomPurgeDelay = Object.hasOwn(process.env, MIMALLOC_PURGE_DELAY);
if (!hasCustomPurgeDelay) {
process.env[MIMALLOC_PURGE_DELAY] = '0';
}

rolldownPromise = import('rolldown').finally(() => {
if (!hasCustomPurgeDelay) {
delete process.env[MIMALLOC_PURGE_DELAY];
}
});
Comment thread
clydin marked this conversation as resolved.
}

return rolldownPromise;
}
55 changes: 55 additions & 0 deletions packages/angular/build/src/utils/load-rolldown_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @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.dev/license
*/

const MIMALLOC_PURGE_DELAY = 'MIMALLOC_PURGE_DELAY';

describe('loadRolldown', () => {
const originalEnvValue = process.env[MIMALLOC_PURGE_DELAY];

function getFreshLoadRolldown(): typeof import('./load-rolldown') {
delete require.cache[require.resolve('./load-rolldown')];

return require('./load-rolldown');
}

afterEach(() => {
if (originalEnvValue !== undefined) {
process.env[MIMALLOC_PURGE_DELAY] = originalEnvValue;
} else {
delete process.env[MIMALLOC_PURGE_DELAY];
}
delete require.cache[require.resolve('./load-rolldown')];
});

it('loads rolldown and cleans up temporary MIMALLOC_PURGE_DELAY when unset', async () => {
delete process.env[MIMALLOC_PURGE_DELAY];
const { loadRolldown } = getFreshLoadRolldown();

const promise1 = loadRolldown();
expect(process.env[MIMALLOC_PURGE_DELAY]).toBe('0');

const promise2 = loadRolldown();
expect(promise1).toBe(promise2);

const rolldownModule = await promise1;
expect(typeof rolldownModule.rolldown).toBe('function');
expect(Object.hasOwn(process.env, MIMALLOC_PURGE_DELAY)).toBeFalse();
});

it('preserves user-configured MIMALLOC_PURGE_DELAY when already set', async () => {
process.env[MIMALLOC_PURGE_DELAY] = '1000';
const { loadRolldown } = getFreshLoadRolldown();

const promise = loadRolldown();
expect(process.env[MIMALLOC_PURGE_DELAY]).toBe('1000');

const rolldownModule = await promise;
expect(typeof rolldownModule.rolldown).toBe('function');
expect(process.env[MIMALLOC_PURGE_DELAY]).toBe('1000');
});
});
Loading