mirror of https://github.com/lukechilds/node.git
Browse Source
This commit switches from the eslint command-line tool to a custom tool that uses eslint programmatically in order to perform linting in parallel and to display linting results incrementally instead of buffering them until the end. Fixes: https://github.com/nodejs/node/issues/5596 PR-URL: https://github.com/nodejs/node/pull/5638 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Johan Bergström <bugs@bergstroem.nu>process-exit-stdio-flushing
Brian White
9 years ago
3 changed files with 279 additions and 5 deletions
@ -0,0 +1,262 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
const rulesDirs = ['tools/eslint-rules']; |
||||
|
// This is the maximum number of files to be linted per worker at any given time
|
||||
|
const maxWorkload = 40; |
||||
|
|
||||
|
const cluster = require('cluster'); |
||||
|
const path = require('path'); |
||||
|
const fs = require('fs'); |
||||
|
const totalCPUs = require('os').cpus().length; |
||||
|
|
||||
|
const CLIEngine = require('./eslint').CLIEngine; |
||||
|
const glob = require('./eslint/node_modules/glob'); |
||||
|
|
||||
|
const cwd = process.cwd(); |
||||
|
const cli = new CLIEngine({ |
||||
|
rulePaths: rulesDirs |
||||
|
}); |
||||
|
|
||||
|
if (cluster.isMaster) { |
||||
|
var numCPUs = 1; |
||||
|
const paths = []; |
||||
|
var files = null; |
||||
|
var totalPaths = 0; |
||||
|
var failures = 0; |
||||
|
var successes = 0; |
||||
|
var lastLineLen = 0; |
||||
|
var curPath = 'Starting ...'; |
||||
|
var showProgress = true; |
||||
|
const globOptions = { |
||||
|
nodir: true |
||||
|
}; |
||||
|
const workerConfig = {}; |
||||
|
var startTime; |
||||
|
var formatter; |
||||
|
var outFn; |
||||
|
var fd; |
||||
|
var i; |
||||
|
|
||||
|
// Check if spreading work among all cores/cpus
|
||||
|
if (process.argv.indexOf('-J') !== -1) |
||||
|
numCPUs = totalCPUs; |
||||
|
|
||||
|
// Check if spreading work among an explicit number of cores/cpus
|
||||
|
i = process.argv.indexOf('-j'); |
||||
|
if (i !== -1) { |
||||
|
if (!process.argv[i + 1]) |
||||
|
throw new Error('Missing parallel job count'); |
||||
|
numCPUs = parseInt(process.argv[i + 1], 10); |
||||
|
if (!isFinite(numCPUs) || numCPUs <= 0) |
||||
|
throw new Error('Bad parallel job count'); |
||||
|
} |
||||
|
|
||||
|
// Check for custom JSLint report formatter
|
||||
|
i = process.argv.indexOf('-f'); |
||||
|
if (i !== -1) { |
||||
|
if (!process.argv[i + 1]) |
||||
|
throw new Error('Missing format name'); |
||||
|
const format = process.argv[i + 1]; |
||||
|
formatter = cli.getFormatter(format); |
||||
|
if (!formatter) |
||||
|
throw new Error('Invalid format name'); |
||||
|
// Automatically disable progress display
|
||||
|
showProgress = false; |
||||
|
// Tell worker to send all results, not just linter errors
|
||||
|
workerConfig.sendAll = true; |
||||
|
} else { |
||||
|
// Use default formatter
|
||||
|
formatter = cli.getFormatter(); |
||||
|
} |
||||
|
|
||||
|
// Check if outputting JSLint report to a file instead of stdout
|
||||
|
i = process.argv.indexOf('-o'); |
||||
|
if (i !== -1) { |
||||
|
if (!process.argv[i + 1]) |
||||
|
throw new Error('Missing output filename'); |
||||
|
var outPath = process.argv[i + 1]; |
||||
|
if (!path.isAbsolute(outPath)) |
||||
|
outPath = path.join(cwd, outPath); |
||||
|
fd = fs.openSync(outPath, 'w'); |
||||
|
outFn = function(str) { |
||||
|
fs.writeSync(fd, str, 'utf8'); |
||||
|
}; |
||||
|
process.on('exit', function() { |
||||
|
fs.closeSync(fd); |
||||
|
}); |
||||
|
} else { |
||||
|
outFn = function(str) { |
||||
|
process.stdout.write(str); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
// Process the rest of the arguments as paths to lint, ignoring any unknown
|
||||
|
// flags
|
||||
|
for (i = 2; i < process.argv.length; ++i) { |
||||
|
if (process.argv[i][0] === '-') { |
||||
|
switch (process.argv[i]) { |
||||
|
case '-f': // Skip format name
|
||||
|
case '-o': // Skip filename
|
||||
|
case '-j': // Skip parallel job count number
|
||||
|
++i; |
||||
|
break; |
||||
|
} |
||||
|
continue; |
||||
|
} |
||||
|
paths.push(process.argv[i]); |
||||
|
} |
||||
|
|
||||
|
if (paths.length === 0) |
||||
|
return; |
||||
|
totalPaths = paths.length; |
||||
|
|
||||
|
if (showProgress) { |
||||
|
// Start the progress display update timer when the first worker is ready
|
||||
|
cluster.once('online', function(worker) { |
||||
|
startTime = process.hrtime(); |
||||
|
setInterval(printProgress, 1000).unref(); |
||||
|
printProgress(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
cluster.on('online', function(worker) { |
||||
|
// Configure worker and give it some initial work to do
|
||||
|
worker.send(workerConfig); |
||||
|
sendWork(worker); |
||||
|
}); |
||||
|
|
||||
|
cluster.on('message', function(worker, results) { |
||||
|
if (typeof results !== 'number') { |
||||
|
// The worker sent us results that are not all successes
|
||||
|
if (!workerConfig.sendAll) |
||||
|
failures += results.length; |
||||
|
outFn(formatter(results) + '\r\n'); |
||||
|
printProgress(); |
||||
|
} else { |
||||
|
successes += results; |
||||
|
} |
||||
|
// Try to give the worker more work to do
|
||||
|
sendWork(worker); |
||||
|
}); |
||||
|
|
||||
|
process.on('exit', function() { |
||||
|
if (showProgress) { |
||||
|
curPath = 'Done'; |
||||
|
printProgress(); |
||||
|
outFn('\r\n'); |
||||
|
} |
||||
|
process.exit(failures ? 1 : 0); |
||||
|
}); |
||||
|
|
||||
|
for (i = 0; i < numCPUs; ++i) |
||||
|
cluster.fork(); |
||||
|
|
||||
|
function sendWork(worker) { |
||||
|
if (!files || !files.length) { |
||||
|
// We either just started or we have no more files to lint for the current
|
||||
|
// path. Find the next path that has some files to be linted.
|
||||
|
while (paths.length) { |
||||
|
var dir = paths.shift(); |
||||
|
curPath = dir; |
||||
|
if (dir.indexOf('/') > 0) |
||||
|
dir = path.join(cwd, dir); |
||||
|
const patterns = cli.resolveFileGlobPatterns([dir]); |
||||
|
dir = path.resolve(patterns[0]); |
||||
|
files = glob.sync(dir, globOptions); |
||||
|
if (files.length) |
||||
|
break; |
||||
|
} |
||||
|
if ((!files || !files.length) && !paths.length) { |
||||
|
// We exhausted all input paths and thus have nothing left to do, so end
|
||||
|
// the worker
|
||||
|
return worker.disconnect(); |
||||
|
} |
||||
|
} |
||||
|
// Give the worker an equal portion of the work left for the current path,
|
||||
|
// but not exceeding a maximum file count in order to help keep *all*
|
||||
|
// workers busy most of the time instead of only a minority doing most of
|
||||
|
// the work.
|
||||
|
const sliceLen = Math.min(maxWorkload, Math.ceil(files.length / numCPUs)); |
||||
|
var slice; |
||||
|
if (sliceLen === files.length) { |
||||
|
// Micro-ptimization to avoid splicing to an empty array
|
||||
|
slice = files; |
||||
|
files = null; |
||||
|
} else { |
||||
|
slice = files.splice(0, sliceLen); |
||||
|
} |
||||
|
worker.send(slice); |
||||
|
} |
||||
|
|
||||
|
function printProgress() { |
||||
|
if (!showProgress) |
||||
|
return; |
||||
|
|
||||
|
// Clear line
|
||||
|
outFn('\r' + ' '.repeat(lastLineLen) + '\r'); |
||||
|
|
||||
|
// Calculate and format the data for displaying
|
||||
|
const elapsed = process.hrtime(startTime)[0]; |
||||
|
const mins = padString(Math.floor(elapsed / 60), 2, '0'); |
||||
|
const secs = padString(elapsed % 60, 2, '0'); |
||||
|
const passed = padString(successes, 6, ' '); |
||||
|
const failed = padString(failures, 6, ' '); |
||||
|
var pct = Math.ceil(((totalPaths - paths.length) / totalPaths) * 100); |
||||
|
pct = padString(pct, 3, ' '); |
||||
|
|
||||
|
var line = `[${mins}:${secs}|%${pct}|+${passed}|-${failed}]: ${curPath}`; |
||||
|
|
||||
|
// Truncate line like cpplint does in case it gets too long
|
||||
|
if (line.length > 75) |
||||
|
line = line.slice(0, 75) + '...'; |
||||
|
|
||||
|
// Store the line length so we know how much to erase the next time around
|
||||
|
lastLineLen = line.length; |
||||
|
|
||||
|
outFn(line); |
||||
|
} |
||||
|
|
||||
|
function padString(str, len, chr) { |
||||
|
str = '' + str; |
||||
|
if (str.length >= len) |
||||
|
return str; |
||||
|
return chr.repeat(len - str.length) + str; |
||||
|
} |
||||
|
} else { |
||||
|
// Worker
|
||||
|
|
||||
|
var config = {}; |
||||
|
process.on('message', function(files) { |
||||
|
if (files instanceof Array) { |
||||
|
// Lint some files
|
||||
|
const report = cli.executeOnFiles(files); |
||||
|
if (config.sendAll) { |
||||
|
// Return both success and error results
|
||||
|
|
||||
|
const results = report.results; |
||||
|
// Silence warnings for files with no errors while keeping the "ok"
|
||||
|
// status
|
||||
|
if (report.warningCount > 0) { |
||||
|
for (var i = 0; i < results.length; ++i) { |
||||
|
const result = results[i]; |
||||
|
if (result.errorCount === 0 && result.warningCount > 0) { |
||||
|
result.warningCount = 0; |
||||
|
result.messages = []; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
process.send(results); |
||||
|
} else if (report.errorCount === 0) { |
||||
|
// No errors, return number of successful lint operations
|
||||
|
process.send(files.length); |
||||
|
} else { |
||||
|
// One or more errors, return the error results only
|
||||
|
process.send(CLIEngine.getErrorResults(report.results)); |
||||
|
} |
||||
|
} else if (typeof files === 'object') { |
||||
|
// The master process is actually sending us our configuration and not a
|
||||
|
// list of files to lint
|
||||
|
config = files; |
||||
|
} |
||||
|
}); |
||||
|
} |
Loading…
Reference in new issue