Skip to content

Commit 9d613fd

Browse files
reuse processes using a process pool
1 parent 96ca197 commit 9d613fd

File tree

13 files changed

+224
-96
lines changed

13 files changed

+224
-96
lines changed

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ insert_final_newline = true
66
indent_style = space
77
indent_size = 2
88

9-
[{*.js,test/index.sh}]
9+
[{*.js}]
1010
indent_size = 4

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ install: npm install
1717
before_script: greenkeeper-lockfile-update
1818
after_script: greenkeeper-lockfile-upload
1919
script: npm run test:ci
20+
cache:
21+
directories:
22+
- node_modules

src/main/mocha.ts

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import { fork } from 'child_process';
1+
import * as assert from 'assert';
22
import * as CircularJSON from 'circular-json';
33
import * as debug from 'debug';
44
import * as Mocha from 'mocha';
55
import { resolve as pathResolve } from 'path';
66

7+
import ProcessPool from './process-pool';
78
import RunnerMain from './runner';
89
import TaskManager from './task-manager';
910
import {
10-
removeDebugArgs,
1111
subprocessParseReviver,
1212
} from './util';
1313

14-
import { DEBUG_SUBPROCESS, SUITE_OWN_OPTIONS } from '../config';
14+
import { SUITE_OWN_OPTIONS } from '../config';
1515
import {
1616
IRetriedTest,
1717
ISubprocessOutputMessage,
@@ -24,6 +24,7 @@ import {
2424
const debugLog = debug('mocha-parallel-tests');
2525

2626
export default class MochaWrapper extends Mocha {
27+
private pool = new ProcessPool();
2728
private isTypescriptRunMode = false;
2829
private maxParallel: number | undefined;
2930
private requires: string[] = [];
@@ -50,6 +51,7 @@ export default class MochaWrapper extends Mocha {
5051

5152
setMaxParallel(maxParallel: number) {
5253
this.maxParallel = maxParallel;
54+
this.pool.setMaxParallel(maxParallel);
5355
}
5456

5557
enableExitMode() {
@@ -98,6 +100,9 @@ export default class MochaWrapper extends Mocha {
98100

99101
taskManager
100102
.on('taskFinished', (testResults: ISubprocessResult) => {
103+
if (!testResults) {
104+
throw new Error('No output from test');
105+
}
101106
const {
102107
code,
103108
execTime,
@@ -136,6 +141,7 @@ export default class MochaWrapper extends Mocha {
136141
};
137142

138143
runner.emitFinishEvents(done);
144+
this.pool.destroyAll();
139145
});
140146

141147
return runner;
@@ -162,89 +168,90 @@ export default class MochaWrapper extends Mocha {
162168
}
163169

164170
private spawnTestProcess(file: string): Promise<ISubprocessResult> {
165-
return new Promise((resolve) => {
166-
const nodeFlags: string[] = [];
167-
const extension = this.isTypescriptRunMode ? 'ts' : 'js';
168-
const runnerPath = pathResolve(__dirname, `../subprocess/runner.${extension}`);
171+
return new Promise<ISubprocessResult>(async (resolve) => {
169172
const resolvedFilePath = pathResolve(file);
170173

171-
const forkArgs: string[] = ['--test', resolvedFilePath];
174+
const testOptions: {[key: string]: any} = { test: resolvedFilePath };
175+
172176
for (const option of SUITE_OWN_OPTIONS) {
173177
const propValue = this.suite[option]();
174178
// bail is undefined by default, we need to somehow pass its value to the subprocess
175-
forkArgs.push(`--${option}`, propValue === undefined ? false : propValue);
179+
testOptions[option] = propValue === undefined ? false : propValue;
176180
}
177181

178182
for (const requirePath of this.requires) {
179-
forkArgs.push('--require', requirePath);
183+
testOptions.require = requirePath;
180184
}
181185

182-
for (const compilerPath of this.compilers) {
183-
forkArgs.push('--compilers', compilerPath);
184-
}
186+
testOptions.compilers = this.compilers || [];
185187

186188
if (this.options.delay) {
187-
forkArgs.push('--delay');
189+
testOptions.delay = true;
188190
}
189191

190192
if (this.options.grep) {
191-
forkArgs.push('--grep', this.options.grep.toString());
193+
testOptions.grep = this.options.grep.toString();
192194
}
193195

194196
if (this.exitImmediately) {
195-
forkArgs.push('--exit');
197+
testOptions.exit = true;
196198
}
197199

198200
if (this.options.fullStackTrace) {
199-
forkArgs.push('--full-trace');
201+
testOptions.fullStackTrace = true;
200202
}
201203

202-
const test = fork(runnerPath, forkArgs, {
203-
// otherwise `--inspect-brk` and other params will be passed to subprocess
204-
execArgv: process.execArgv.filter(removeDebugArgs),
205-
stdio: ['ipc'],
206-
});
207-
208-
if (this.isTypescriptRunMode) {
209-
nodeFlags.push('--require', 'ts-node/register');
204+
let test;
205+
try {
206+
test = await this.pool.getOrCreate(this.isTypescriptRunMode);
207+
} catch (e) {
208+
throw e;
210209
}
211210

212-
debugLog('Process spawned. You can run it manually with this command:');
213-
debugLog(`node ${nodeFlags.join(' ')} ${runnerPath} ${forkArgs.concat([DEBUG_SUBPROCESS.argument]).join(' ')}`);
211+
test.send(JSON.stringify({ type: 'start', testOptions }));
214212

215213
const events: Array<ISubprocessOutputMessage | ISubprocessRunnerMessage> = [];
216214
let syncedSubprocessData: ISubprocessSyncedData | undefined;
217215
const startedAt = Date.now();
218216

219-
test.on('message', function onMessageHandler({ event, data }) {
217+
function onMessageHandler({ event, data }) {
220218
if (event === 'sync') {
221219
syncedSubprocessData = data;
220+
} else if (event === 'end') {
221+
clean();
222+
resolve({
223+
code: data.code || 0,
224+
events,
225+
execTime: Date.now() - startedAt,
226+
file,
227+
syncedSubprocessData,
228+
});
222229
} else {
230+
assert(event);
223231
events.push({
224232
data,
225233
event,
226234
type: 'runner',
227235
});
228236
}
229-
});
237+
}
230238

231-
test.stdout.on('data', function onStdoutData(data: Buffer) {
239+
function onStdoutData(data: Buffer) {
232240
events.push({
233241
data,
234242
event: undefined,
235243
type: 'stdout',
236244
});
237-
});
245+
}
238246

239-
test.stderr.on('data', function onStderrData(data: Buffer) {
247+
function onStderrData(data: Buffer) {
240248
events.push({
241249
data,
242250
event: undefined,
243251
type: 'stderr',
244252
});
245-
});
246-
247-
test.on('close', (code) => {
253+
}
254+
function onClose(code) {
248255
debugLog(`Process for ${file} exited with code ${code}`);
249256

250257
resolve({
@@ -254,7 +261,21 @@ export default class MochaWrapper extends Mocha {
254261
file,
255262
syncedSubprocessData,
256263
});
257-
});
264+
}
265+
266+
test.on('message', onMessageHandler);
267+
test.stdout.on('data', onStdoutData);
268+
test.stderr.on('data', onStderrData);
269+
test.on('close', onClose);
270+
271+
function clean() {
272+
test.removeListener('message', onMessageHandler);
273+
test.stdout.removeListener('data', onStdoutData);
274+
test.stderr.removeListener('data', onStderrData);
275+
test.removeListener('close', onClose);
276+
test.destroy();
277+
return null;
278+
}
258279
});
259280
}
260281
}

src/main/process-pool.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { ChildProcess, fork } from 'child_process';
2+
import * as os from 'os';
3+
import { resolve as pathResolve } from 'path';
4+
import { removeDebugArgs } from './util';
5+
6+
interface IMochaProcess extends ChildProcess {
7+
destroy: () => void;
8+
}
9+
10+
export default class ProcessPool {
11+
private maxParallel: number;
12+
private waitingList: Array<(process: IMochaProcess) => void> = [];
13+
private unusedProcesses: IMochaProcess[] = [];
14+
private processes: IMochaProcess[] = [];
15+
16+
constructor() {
17+
this.maxParallel = os.cpus().length;
18+
}
19+
20+
setMaxParallel(n) {
21+
this.maxParallel = n;
22+
}
23+
24+
async getOrCreate(isTypescriptRunMode): Promise<IMochaProcess> {
25+
const extension = isTypescriptRunMode ? 'ts' : 'js';
26+
const runnerPath = pathResolve(__dirname, `../subprocess/runner.${extension}`);
27+
28+
const lastUnusedProcess = this.unusedProcesses.pop();
29+
if (lastUnusedProcess) {
30+
return lastUnusedProcess;
31+
}
32+
33+
if (this.processes.length >= this.maxParallel) {
34+
const process: IMochaProcess = await new Promise<IMochaProcess>((resolve) => {
35+
this.waitingList.push((proc: IMochaProcess) => {
36+
resolve(proc);
37+
});
38+
});
39+
return process;
40+
}
41+
return this.create(runnerPath, {
42+
// otherwise `--inspect-brk` and other params will be passed to subprocess
43+
execArgv: process.execArgv.filter(removeDebugArgs),
44+
stdio: ['ipc'],
45+
});
46+
}
47+
48+
create(runnerPath, opt) {
49+
const process = fork(runnerPath, [], opt) as IMochaProcess;
50+
51+
this.processes.push(process);
52+
53+
process.destroy = () => {
54+
const nextOnWaitingList = this.waitingList.pop();
55+
if (nextOnWaitingList) {
56+
nextOnWaitingList(process);
57+
} else {
58+
this.unusedProcesses.push(process);
59+
}
60+
};
61+
62+
return process;
63+
}
64+
65+
destroyAll() {
66+
this.processes.forEach((proc) => {
67+
proc.kill();
68+
});
69+
}
70+
}

src/main/runner.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export default class RunnerMain extends Runner {
149149
private emitSubprocessEvents() {
150150
for (const { event, data, type } of this.subprocessTestResults.events) {
151151
if (type === 'runner') {
152+
assert(event);
152153
switch (event) {
153154
case 'start':
154155
case 'end':

0 commit comments

Comments
 (0)