Skip to content

Commit 38c2015

Browse files
author
Piotr Oleś
committed
Cluster support
1 parent 42572f2 commit 38c2015

File tree

8 files changed

+183
-31
lines changed

8 files changed

+183
-31
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ TODO
5959
|**colors**| `boolean` | If `false`, disables colors for logger. Default: `true`. |
6060
|**logger**| `LoggerInterface` | Logger instance. It should be object that implements method: `error`, `warn`, `info`. Default: `console`.|
6161
|**silent**| `boolean` | If `true`, logger will not be used. Default: `false`.|
62+
|**cluster**| `number` | You can split type checking to few workers to speed-up on increment build. But remember: if you don't want type checker to affect build time, you should keep 1 core for build and 1 core for system. Also - node doesn't share memory so keep in mind that memory usage will increase linear. Default: `1`.|
6263

6364
## License ##
6465
MIT

lib/FilesRegister.js

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,21 @@
11

2-
function FilesRegister (oldRegister) {
2+
function FilesRegister () {
33
this.register = {};
4-
5-
if (oldRegister) {
6-
oldRegister.forEach(function (fileName, fileEntry) {
7-
if (!fileEntry.modified) {
8-
// don't copy - just set reference to save memory
9-
this.register[fileName] = fileEntry;
10-
}
11-
}.bind(this));
12-
}
134
}
145
module.exports = FilesRegister;
156

16-
FilesRegister.prototype.forEach = function (loop) {
7+
FilesRegister.prototype.forEach = function (callback) {
178
Object.keys(this.register).forEach(function (key) {
18-
loop(key, this.register[key]);
9+
callback(key, this.register[key]);
1910
}.bind(this));
2011
};
2112

13+
FilesRegister.prototype.keys = function() {
14+
return Object.keys(this.register);
15+
};
16+
2217
FilesRegister.prototype.addFile = function (fileName) {
2318
this.register[fileName] = {
24-
modified: false,
2519
mtime: undefined,
2620
source: undefined,
2721
linted: false,
@@ -75,10 +69,12 @@ FilesRegister.prototype.setMtime = function (fileName, mtime) {
7569
this.ensureFile(fileName);
7670

7771
if (this.register[fileName].mtime !== mtime) {
78-
this.register[fileName].modified = true;
7972
this.register[fileName].linted = false;
8073
this.register[fileName].lints = [];
8174
this.register[fileName].mtime = mtime;
75+
76+
// file has been changed - we are not sure about it's current source
77+
this.register[fileName].source = undefined;
8278
}
8379
};
8480

@@ -96,6 +92,7 @@ FilesRegister.prototype.consumeLint = function (lint) {
9692
var fileName = lint.getFileName();
9793

9894
this.ensureFile(fileName);
95+
9996
this.register[fileName].linted = true;
10097
this.register[fileName].lints.push(lint);
10198
};
@@ -105,3 +102,9 @@ FilesRegister.prototype.consumeLints = function (lints) {
105102
this.consumeLint(lint);
106103
}.bind(this));
107104
};
105+
106+
FilesRegister.prototype.setAllLinted = function () {
107+
this.keys().forEach(function (fileName) {
108+
this.register[fileName].linted = true;
109+
}.bind(this))
110+
};

lib/IncrementalChecker.js

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,17 @@ var path = require('path');
44
var endsWith = require('lodash.endswith');
55
var FilesRegister = require('./FilesRegister');
66
var FilesWatcher = require('./FilesWatcher');
7+
var WorkSplitter = require('./WorkSplitter');
78

8-
function IncrementalChecker (programConfigFile, linterConfigFile, watchPaths) {
9+
function IncrementalChecker (programConfigFile, linterConfigFile, watchPaths, workNumber, workDivision) {
910
this.programConfigFile = programConfigFile;
1011
this.linterConfigFile = linterConfigFile;
1112
this.watchPaths = watchPaths;
13+
this.workNumber = workNumber || 0;
14+
this.workDivision = workDivision || 1;
15+
16+
// it's shared between compilations
17+
this.register = new FilesRegister();
1218
}
1319
module.exports = IncrementalChecker;
1420

@@ -35,7 +41,7 @@ IncrementalChecker.createProgram = function (programConfig, register, watcher, o
3541
if (!watcher.isWatchingFile(filePath)) {
3642
var stats = fs.statSync(filePath);
3743

38-
register.setMtime(filePath, stats.mtime);
44+
register.setMtime(filePath, stats.mtime.valueOf());
3945
}
4046

4147
// get source file only if there is no source in files register
@@ -64,13 +70,11 @@ IncrementalChecker.createLinter = function (program) {
6470
};
6571

6672
IncrementalChecker.prototype.nextIteration = function () {
67-
this.register = new FilesRegister(this.register);
68-
6973
if (!this.watcher) {
7074
this.watcher = new FilesWatcher(this.watchPaths, ['.ts', '.tsx']);
7175

7276
// connect watcher with register
73-
this.watcher.onChange(function (filePath, stats) { this.register.setMtime(filePath, stats.mtime); }.bind(this));
77+
this.watcher.onChange(function (filePath, stats) { this.register.setMtime(filePath, stats.mtime.valueOf()); }.bind(this));
7478
this.watcher.onUnlink(function (filePath) { this.register.removeFile(filePath); }.bind(this));
7579

7680
this.watcher.watch();
@@ -89,7 +93,27 @@ IncrementalChecker.prototype.nextIteration = function () {
8993
};
9094

9195
IncrementalChecker.prototype.getDiagnostics = function (cancellationToken) {
92-
var diagnostics = this.program.getSemanticDiagnostics(undefined, cancellationToken);
96+
var diagnostics = [];
97+
var times = [];
98+
var filesToCheck = this.program.getSourceFiles();
99+
var work = new WorkSplitter(filesToCheck, this.workNumber, this.workDivision);
100+
101+
work.forEach(function (sourceFile, i) {
102+
times[i] = process.hrtime();
103+
if (cancellationToken) {
104+
cancellationToken.throwIfCancellationRequested();
105+
}
106+
107+
ts.addRange(diagnostics, this.program.getSemanticDiagnostics(sourceFile, cancellationToken));
108+
times[i] = process.hrtime(times[i]);
109+
}.bind(this));
110+
111+
var elapsed = times.map(function (time) {
112+
return Math.round(Math.round(time[0] * 1E9 + time[1]) / 1E6); // in ms
113+
});
114+
elapsed.sort(function (a, b) { return b - a; });
115+
116+
diagnostics = ts.sortAndDeduplicateDiagnostics(diagnostics);
93117

94118
return diagnostics.map(function (diagnostic) {
95119
var position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
@@ -108,18 +132,20 @@ IncrementalChecker.prototype.getDiagnostics = function (cancellationToken) {
108132
};
109133

110134
IncrementalChecker.prototype.getLints = function (cancellationToken) {
111-
this.register.forEach(function (fileName, fileEntry) {
135+
var filesToLint = this.register.keys().filter(function (fileName) {
136+
return !endsWith(fileName, '.d.ts') && !this.register.getFile(fileName).linted;
137+
}.bind(this));
138+
139+
var work = new WorkSplitter(filesToLint, this.workNumber, this.workDivision);
140+
141+
work.forEach(function (fileName) {
112142
cancellationToken.throwIfCancellationRequested();
113143

114-
if (!endsWith(fileName, '.d.ts') && !fileEntry.linted) {
115-
this.linter.lint(fileName, undefined, this.linterConfig);
116-
fileEntry.linted = true;
117-
}
144+
this.linter.lint(fileName, undefined, this.linterConfig);
118145
}.bind(this));
119146

120-
this.register.consumeLints(
121-
this.linter.getResult().failures
122-
);
147+
this.register.setAllLinted();
148+
this.register.consumeLints(this.linter.getResult().failures);
123149

124150
var lints = this.register.getLints();
125151

lib/WorkResultSet.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
var ts = require('typescript');
2+
3+
function WorkResultSet() {
4+
this.result = {};
5+
}
6+
module.exports = WorkResultSet;
7+
8+
WorkResultSet.prototype.set = function (key, result) {
9+
this.result[key] = result;
10+
};
11+
12+
WorkResultSet.prototype.has = function (key) {
13+
return undefined !== this.result[key];
14+
};
15+
16+
WorkResultSet.prototype.clear = function () {
17+
this.result = {};
18+
};
19+
20+
WorkResultSet.prototype.done = function (keys) {
21+
return keys.every(function (key) {
22+
return this.has(key)
23+
}.bind(this));
24+
};
25+
26+
WorkResultSet.prototype.merge = function () {
27+
var merged = {
28+
diagnostics: [],
29+
lints: []
30+
};
31+
32+
Object.keys(this.result).forEach(function (key) {
33+
merged.diagnostics.push.apply(merged.diagnostics, this.result[key].diagnostics);
34+
merged.lints.push.apply(merged.lints, this.result[key].lints);
35+
}.bind(this));
36+
37+
merged.diagnostics = ts.sortAndDeduplicateDiagnostics(merged.diagnostics);
38+
39+
return merged;
40+
};

lib/WorkSplitter.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
function WorkSplitter (workSet, workNumber, workDivision) {
3+
this.workSet = workSet;
4+
this.workNumber = workNumber;
5+
this.workDivision = workDivision;
6+
this.workSize = Math.floor(this.workSet.length / this.workDivision);
7+
this.workBegin = this.workNumber * this.workSize;
8+
this.workEnd = this.workBegin + this.workSize;
9+
10+
// be sure that we will process all work for odd workSize.
11+
if (this.workNumber === this.workSet.length - 1) {
12+
this.workEnd = this.workSet.length;
13+
}
14+
}
15+
module.exports = WorkSplitter;
16+
17+
WorkSplitter.prototype.forEach = function (callback) {
18+
for (var i = this.workBegin; i < this.workEnd; ++i) {
19+
callback(this.workSet[i], i);
20+
}
21+
};
22+
23+
WorkSplitter.prototype.getWorkSize = function () {
24+
return this.workSize;
25+
};
26+
27+
WorkSplitter.prototype.getWorkBegin = function () {
28+
return this.workBegin;
29+
};
30+
31+
WorkSplitter.prototype.getWorkEnd = function () {
32+
return this.workEnd;
33+
};

lib/cluster.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
var cluster = require('cluster');
2+
var process = require('process');
3+
4+
if (cluster.isMaster) {
5+
var WorkResultSet = require('./WorkResultSet');
6+
var resultSet = new WorkResultSet();
7+
8+
// fork workers...
9+
var division = parseInt(process.env.WORK_DIVISION);
10+
11+
for (var i = 0; i < division; i++) {
12+
cluster.fork({ WORK_NUMBER: i });
13+
}
14+
15+
var workerIds = Object.keys(cluster.workers);
16+
17+
process.on('message', function (message) {
18+
// broadcast message to all workers
19+
workerIds.forEach(function (workerId) {
20+
cluster.workers[workerId].send(message);
21+
});
22+
23+
// clear previous result set
24+
resultSet.clear();
25+
});
26+
27+
// listen to all workers
28+
workerIds.forEach(function (workerId) {
29+
cluster.workers[workerId].on('message', function (message) {
30+
// set result from worker
31+
resultSet.set(workerId, message);
32+
33+
// if we have result from all workers, send merged
34+
if (resultSet.done(workerIds)) {
35+
process.send(resultSet.merge());
36+
}
37+
});
38+
});
39+
} else {
40+
require('./service');
41+
}

lib/index.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ function ForkTsCheckerWebpackPlugin (options) {
2222
this.ignoreLints = options.ignoreLints || [];
2323
this.logger = options.logger || console;
2424
this.silent = !!options.silent;
25+
this.cluster = options.cluster || 1;
2526

2627
this.tsconfigPath = undefined;
2728
this.tslintPath = undefined;
@@ -151,13 +152,14 @@ ForkTsCheckerWebpackPlugin.prototype.pluginDone = function () {
151152

152153
ForkTsCheckerWebpackPlugin.prototype.spawnService = function () {
153154
this.service = childProcess.fork(
154-
path.resolve(__dirname, './service.js'),
155+
path.resolve(__dirname, this.cluster > 1 ? './cluster.js' : './service.js'),
155156
[],
156157
{
157158
env: {
158159
TSCONFIG: this.tsconfigPath,
159160
TSLINT: this.tslintPath,
160-
WATCH: this.watchPaths.join('|')
161+
WATCH: this.watchPaths.join('|'),
162+
WORK_DIVISION: this.cluster > 1 ? this.cluster : 1
161163
}
162164
}
163165
);
@@ -193,6 +195,10 @@ ForkTsCheckerWebpackPlugin.prototype.spawnService = function () {
193195
};
194196

195197
ForkTsCheckerWebpackPlugin.prototype.handleServiceMessage = function (message) {
198+
if (message.type === 'log') {
199+
console.log(message.payload);
200+
return;
201+
}
196202
if (this.cancellationToken) {
197203
this.cancellationToken.cleanupCancellation();
198204
// job is done - nothing to cancel

lib/service.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ var CancellationToken = require('./CancellationToken');
66
var checker = new IncrementalChecker(
77
process.env.TSCONFIG,
88
process.env.TSLINT,
9-
process.env.WATCH === '' ? [] : process.env.WATCH.split('|')
9+
process.env.WATCH === '' ? [] : process.env.WATCH.split('|'),
10+
parseInt(process.env.WORK_NUMBER, 10),
11+
parseInt(process.env.WORK_DIVISION, 10)
1012
);
1113

1214
var diagnostics = [];

0 commit comments

Comments
 (0)