Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce maxParallelFileOps to limit parallel writes #4570

Merged
merged 1 commit into from Jul 15, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 9 additions & 3 deletions docs/999-big-list-of-options.md
Expand Up @@ -333,11 +333,11 @@ For this case, choosing `"ifRelativeSource"` will check if the original import w

Note that when a relative path is directly marked as "external" using the [`external`](guide/en/#external) option, then it will be the same relative path in the output. When it is resolved first via a plugin or Rollup core and then marked as external, the above logic will apply.

#### maxParallelFileReads
#### maxParallelFileOps

Type: `number`<br> CLI: `--maxParallelFileReads <number>`<br> Default: 20
Type: `number`<br> CLI: `--maxParallelFileOps <number>`<br> Default: 20

Limits the number of files rollup will open in parallel when reading modules. Without a limit or with a high enough value, builds can fail with an "EMFILE: too many open files". This dependes on how many open file handles the os allows.
Limits the number of files rollup will open in parallel when reading modules or writing chunks. Without a limit or with a high enough value, builds can fail with an "EMFILE: too many open files". This depends on how many open file handles the operating system allows.

#### onwarn

Expand Down Expand Up @@ -1869,6 +1869,12 @@ _Use the [`output.inlineDynamicImports`](guide/en/#outputinlinedynamicimports) o

_Use the [`output.manualChunks`](guide/en/#outputmanualchunks) output option instead, which has the same signature._

#### maxParallelFileReads

_Use the [`maxParallelFileOps`](guide/en/#maxParallelFileOps) option instead._<br> Type: `number`<br> CLI: `--maxParallelFileReads <number>`<br> Default: 20

Limits the number of files rollup will open in parallel when reading modules. Without a limit or with a high enough value, builds can fail with an "EMFILE: too many open files". This depends on how many open file handles the os allows.

#### output.dynamicImportFunction

_Use the [`renderDynamicImport`](guide/en/#renderdynamicimport) plugin hook instead._<br> Type: `string`<br> CLI: `--dynamicImportFunction <name>`<br> Default: `import`
Expand Down
3 changes: 3 additions & 0 deletions src/Graph.ts
Expand Up @@ -14,6 +14,7 @@ import type {
WatchChangeHook
} from './rollup/types';
import { PluginDriver } from './utils/PluginDriver';
import Queue from './utils/Queue';
import { BuildPhase } from './utils/buildPhase';
import { errImplicitDependantIsNotIncluded, error } from './utils/error';
import { analyseModuleExecution } from './utils/executionOrder';
Expand Down Expand Up @@ -48,6 +49,7 @@ export default class Graph {
readonly cachedModules = new Map<string, ModuleJSON>();
readonly deoptimizationTracker = new PathTracker();
entryModules: Module[] = [];
readonly fileOperationQueue: Queue;
readonly moduleLoader: ModuleLoader;
readonly modulesById = new Map<string, Module | ExternalModule>();
needsTreeshakingPass = false;
Expand Down Expand Up @@ -87,6 +89,7 @@ export default class Graph {
this.pluginDriver = new PluginDriver(this, options, options.plugins, this.pluginCache);
this.acornParser = acorn.Parser.extend(...(options.acornInjectPlugins as any));
this.moduleLoader = new ModuleLoader(this, this.modulesById, this.options, this.pluginDriver);
this.fileOperationQueue = new Queue(options.maxParallelFileOps);
}

async build(): Promise<void> {
Expand Down
6 changes: 1 addition & 5 deletions src/ModuleLoader.ts
Expand Up @@ -16,7 +16,6 @@ import type {
ResolveIdResult
} from './rollup/types';
import type { PluginDriver } from './utils/PluginDriver';
import Queue from './utils/Queue';
import { EMPTY_OBJECT } from './utils/blank';
import {
errBadLoader,
Expand Down Expand Up @@ -72,7 +71,6 @@ export class ModuleLoader {
private readonly modulesWithLoadedDependencies = new Set<Module>();
private nextChunkNamePriority = 0;
private nextEntryModuleIndex = 0;
private readonly readQueue: Queue<LoadResult>;

constructor(
private readonly graph: Graph,
Expand All @@ -83,8 +81,6 @@ export class ModuleLoader {
this.hasModuleSideEffects = options.treeshake
? options.treeshake.moduleSideEffects
: () => true;

this.readQueue = new Queue(options.maxParallelFileReads);
}

async addAdditionalModules(unresolvedModules: readonly string[]): Promise<Module[]> {
Expand Down Expand Up @@ -252,7 +248,7 @@ export class ModuleLoader {
timeStart('load modules', 3);
let source: LoadResult;
try {
source = await this.readQueue.run(
source = await this.graph.fileOperationQueue.run(
async () =>
(await this.pluginDriver.hookFirst('load', [id])) ?? (await fs.readFile(id, 'utf8'))
);
Expand Down
4 changes: 3 additions & 1 deletion src/rollup/rollup.ts
Expand Up @@ -174,7 +174,9 @@ function handleGenerateWrite(
});
}
await Promise.all(
Object.values(generated).map(chunk => writeOutputFile(chunk, outputOptions))
Object.values(generated).map(chunk =>
graph.fileOperationQueue.run(() => writeOutputFile(chunk, outputOptions))
)
);
await outputPluginDriver.hookParallel('writeBundle', [outputOptions, generated]);
}
Expand Down
4 changes: 4 additions & 0 deletions src/rollup/types.d.ts
Expand Up @@ -553,6 +553,8 @@ export interface InputOptions {
makeAbsoluteExternalsRelative?: boolean | 'ifRelativeSource';
/** @deprecated Use the "manualChunks" output option instead. */
manualChunks?: ManualChunksOption;
maxParallelFileOps?: number;
/** @deprecated Use the "maxParallelFileOps" option instead. */
maxParallelFileReads?: number;
moduleContext?: ((id: string) => string | null | void) | { [id: string]: string };
onwarn?: WarningHandlerWithDefault;
Expand Down Expand Up @@ -581,6 +583,8 @@ export interface NormalizedInputOptions {
makeAbsoluteExternalsRelative: boolean | 'ifRelativeSource';
/** @deprecated Use the "manualChunks" output option instead. */
manualChunks: ManualChunksOption | undefined;
maxParallelFileOps: number;
/** @deprecated Use the "maxParallelFileOps" option instead. */
maxParallelFileReads: number;
moduleContext: (id: string) => string;
onwarn: WarningHandler;
Expand Down
16 changes: 8 additions & 8 deletions src/utils/Queue.ts
@@ -1,20 +1,20 @@
interface Task<T> {
(): T | Promise<T>;
(): Promise<T>;
}

interface QueueItem<T> {
interface QueueItem {
reject: (reason?: unknown) => void;
resolve: (value: T) => void;
task: Task<T>;
resolve: (value: any) => void;
task: Task<unknown>;
}

export default class Queue<T> {
private readonly queue: QueueItem<T>[] = [];
export default class Queue {
private readonly queue: QueueItem[] = [];
private workerCount = 0;

constructor(private maxParallel: number) {}

run(task: Task<T>): Promise<T> {
run<T>(task: Task<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push({ reject, resolve, task });
this.work();
Expand All @@ -25,7 +25,7 @@ export default class Queue<T> {
if (this.workerCount >= this.maxParallel) return;
this.workerCount++;

let entry: QueueItem<T> | undefined;
let entry: QueueItem | undefined;
while ((entry = this.queue.shift())) {
const { reject, resolve, task } = entry;

Expand Down
1 change: 1 addition & 0 deletions src/utils/options/mergeOptions.ts
Expand Up @@ -120,6 +120,7 @@ function mergeInputOptions(
input: getOption('input') || [],
makeAbsoluteExternalsRelative: getOption('makeAbsoluteExternalsRelative'),
manualChunks: getOption('manualChunks'),
maxParallelFileOps: getOption('maxParallelFileOps'),
maxParallelFileReads: getOption('maxParallelFileReads'),
moduleContext: getOption('moduleContext'),
onwarn: getOnWarn(config, defaultOnWarnHandler),
Expand Down
25 changes: 19 additions & 6 deletions src/utils/options/normalizeInputOptions.ts
Expand Up @@ -38,6 +38,7 @@ export function normalizeInputOptions(config: InputOptions): {
const context = config.context ?? 'undefined';
const onwarn = getOnwarn(config);
const strictDeprecations = config.strictDeprecations || false;
const maxParallelFileOps = getmaxParallelFileOps(config, onwarn, strictDeprecations);
const options: NormalizedInputOptions & InputOptions = {
acorn: getAcorn(config) as unknown as NormalizedInputOptions['acorn'],
acornInjectPlugins: getAcornInjectPlugins(config),
Expand All @@ -49,7 +50,8 @@ export function normalizeInputOptions(config: InputOptions): {
input: getInput(config),
makeAbsoluteExternalsRelative: config.makeAbsoluteExternalsRelative ?? true,
manualChunks: getManualChunks(config, onwarn, strictDeprecations),
maxParallelFileReads: getMaxParallelFileReads(config),
maxParallelFileOps,
maxParallelFileReads: maxParallelFileOps,
moduleContext: getModuleContext(config, context),
onwarn,
perf: config.perf || false,
Expand Down Expand Up @@ -175,13 +177,24 @@ const getManualChunks = (
return configManualChunks;
};

const getMaxParallelFileReads = (
config: InputOptions
): NormalizedInputOptions['maxParallelFileReads'] => {
const getmaxParallelFileOps = (
config: InputOptions,
warn: WarningHandler,
strictDeprecations: boolean
): NormalizedInputOptions['maxParallelFileOps'] => {
const maxParallelFileReads = config.maxParallelFileReads as unknown;
if (typeof maxParallelFileReads === 'number') {
if (maxParallelFileReads <= 0) return Infinity;
return maxParallelFileReads;
warnDeprecationWithOptions(
'The "maxParallelFileReads" option is deprecated. Use the "maxParallelFileOps" option instead.',
false,
warn,
strictDeprecations
);
}
const maxParallelFileOps = (config.maxParallelFileOps as unknown) ?? maxParallelFileReads;
if (typeof maxParallelFileOps === 'number') {
if (maxParallelFileOps <= 0) return Infinity;
return maxParallelFileOps;
}
return 20;
};
Expand Down
7 changes: 7 additions & 0 deletions test/chunking-form/index.js
Expand Up @@ -11,6 +11,13 @@ runTestSuiteWithSamples('chunking form', resolve(__dirname, 'samples'), (dir, co
() => {
let bundle;

if (config.before) {
before(config.before);
}
if (config.after) {
after(config.after);
}

for (const format of FORMATS) {
it('generates ' + format, async () => {
chdir(dir);
Expand Down
28 changes: 28 additions & 0 deletions test/chunking-form/samples/max-parallel-file-operations/_config.js
@@ -0,0 +1,28 @@
const assert = require('assert');
const { promises: fs } = require('fs');
const { wait } = require('../../../utils');

const fsWriteFile = fs.writeFile;
let currentWrites = 0;
let maxWrites = 0;

module.exports = {
description: 'maxParallelFileOps limits write operations',
options: {
maxParallelFileOps: 3,
output: { preserveModules: true }
},
before() {
fs.writeFile = async (path, content) => {
currentWrites++;
maxWrites = Math.max(maxWrites, currentWrites);
await fsWriteFile(path, content);
await wait(50);
currentWrites--;
};
},
after() {
fs.writeFile = fsWriteFile;
assert.strictEqual(maxWrites, 3, 'Wrong number of parallel file writes: ' + maxWrites);
}
};
@@ -0,0 +1,9 @@
define(['exports'], (function (exports) { 'use strict';

const x1 = 1;

exports.x1 = x1;

Object.defineProperty(exports, '__esModule', { value: true });

}));
@@ -0,0 +1,9 @@
define(['exports'], (function (exports) { 'use strict';

const x2 = 2;

exports.x2 = x2;

Object.defineProperty(exports, '__esModule', { value: true });

}));
@@ -0,0 +1,9 @@
define(['exports'], (function (exports) { 'use strict';

const x3 = 3;

exports.x3 = x3;

Object.defineProperty(exports, '__esModule', { value: true });

}));
@@ -0,0 +1,9 @@
define(['exports'], (function (exports) { 'use strict';

const x4 = 4;

exports.x4 = x4;

Object.defineProperty(exports, '__esModule', { value: true });

}));
@@ -0,0 +1,9 @@
define(['exports'], (function (exports) { 'use strict';

const x5 = 5;

exports.x5 = x5;

Object.defineProperty(exports, '__esModule', { value: true });

}));
@@ -0,0 +1,13 @@
define(['exports', './1', './2', './3', './4', './5'], (function (exports, _1, _2, _3, _4, _5) { 'use strict';



exports.x1 = _1.x1;
exports.x2 = _2.x2;
exports.x3 = _3.x3;
exports.x4 = _4.x4;
exports.x5 = _5.x5;

Object.defineProperty(exports, '__esModule', { value: true });

}));
@@ -0,0 +1,7 @@
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

const x1 = 1;

exports.x1 = x1;
@@ -0,0 +1,7 @@
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

const x2 = 2;

exports.x2 = x2;
@@ -0,0 +1,7 @@
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

const x3 = 3;

exports.x3 = x3;
@@ -0,0 +1,7 @@
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

const x4 = 4;

exports.x4 = x4;
@@ -0,0 +1,7 @@
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

const x5 = 5;

exports.x5 = x5;
@@ -0,0 +1,17 @@
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var _1 = require('./1.js');
var _2 = require('./2.js');
var _3 = require('./3.js');
var _4 = require('./4.js');
var _5 = require('./5.js');



exports.x1 = _1.x1;
exports.x2 = _2.x2;
exports.x3 = _3.x3;
exports.x4 = _4.x4;
exports.x5 = _5.x5;
@@ -0,0 +1,3 @@
const x1 = 1;

export { x1 };
@@ -0,0 +1,3 @@
const x2 = 2;

export { x2 };
@@ -0,0 +1,3 @@
const x3 = 3;

export { x3 };
@@ -0,0 +1,3 @@
const x4 = 4;

export { x4 };
@@ -0,0 +1,3 @@
const x5 = 5;

export { x5 };
@@ -0,0 +1,5 @@
export { x1 } from './1.js';
export { x2 } from './2.js';
export { x3 } from './3.js';
export { x4 } from './4.js';
export { x5 } from './5.js';