Introduce concurrency to asynchronous file linting for improved performance.

This commit is contained in:
David Anson 2020-09-12 12:01:20 -07:00
parent 2851a691ba
commit 4286f68152
3 changed files with 101 additions and 54 deletions

View file

@ -80,7 +80,7 @@ module.exports.includesSorted = function includesSorted(array, element) {
let left = 0; let left = 0;
let right = array.length - 1; let right = array.length - 1;
while (left <= right) { while (left <= right) {
/* eslint-disable no-bitwise */ // eslint-disable-next-line no-bitwise
const mid = (left + right) >> 1; const mid = (left + right) >> 1;
if (array[mid] < element) { if (array[mid] < element) {
left = mid + 1; left = mid + 1;

View file

@ -90,7 +90,9 @@ function newResults(ruleList) {
function toString(useAlias) { function toString(useAlias) {
let ruleNameToRule = null; let ruleNameToRule = null;
const results = []; const results = [];
Object.keys(lintResults).forEach(function forFile(file) { const keys = Object.keys(lintResults);
keys.sort();
keys.forEach(function forFile(file) {
const fileResults = lintResults[file]; const fileResults = lintResults[file];
if (Array.isArray(fileResults)) { if (Array.isArray(fileResults)) {
fileResults.forEach(function forResult(result) { fileResults.forEach(function forResult(result) {
@ -727,39 +729,38 @@ function lintInput(options, synchronous, callback) {
md.use(...plugin); md.use(...plugin);
}); });
const results = newResults(ruleList); const results = newResults(ruleList);
// Helper to lint the next string or file let done = false;
/* eslint-disable consistent-return */ // Linting of strings is always synchronous
let syncItem = null;
// eslint-disable-next-line jsdoc/require-jsdoc // eslint-disable-next-line jsdoc/require-jsdoc
function lintNextItem() { function syncCallback(err, result) {
let iterating = true;
let item = null;
// eslint-disable-next-line jsdoc/require-jsdoc
function lintNextItemCallback(err, result) {
if (err) { if (err) {
iterating = false; done = true;
return callback(err); return callback(err);
} }
results[item] = result; results[syncItem] = result;
return iterating || lintNextItem(); return null;
} }
while (iterating) { while (!done && (syncItem = stringsKeys.shift())) {
if ((item = stringsKeys.shift())) {
lintContent( lintContent(
ruleList, ruleList,
item, syncItem,
strings[item] || "", strings[syncItem] || "",
md, md,
config, config,
frontMatter, frontMatter,
handleRuleFailures, handleRuleFailures,
noInlineConfig, noInlineConfig,
resultVersion, resultVersion,
lintNextItemCallback); syncCallback
} else if ((item = files.shift())) { );
iterating = synchronous; }
if (synchronous) {
// Lint files synchronously
while (!done && (syncItem = files.shift())) {
lintFile( lintFile(
ruleList, ruleList,
item, syncItem,
md, md,
config, config,
frontMatter, frontMatter,
@ -767,13 +768,59 @@ function lintInput(options, synchronous, callback) {
noInlineConfig, noInlineConfig,
resultVersion, resultVersion,
synchronous, synchronous,
lintNextItemCallback); syncCallback
} else { );
}
return done || callback(null, results);
}
// Lint files asynchronously
let concurrency = 0;
// eslint-disable-next-line jsdoc/require-jsdoc
function lintConcurrently() {
const asyncItem = files.shift();
if (done) {
// Nothing to do
} else if (asyncItem) {
concurrency++;
lintFile(
ruleList,
asyncItem,
md,
config,
frontMatter,
handleRuleFailures,
noInlineConfig,
resultVersion,
synchronous,
(err, result) => {
concurrency--;
if (err) {
done = true;
return callback(err);
}
results[asyncItem] = result;
lintConcurrently();
return null;
}
);
} else if (concurrency === 0) {
done = true;
return callback(null, results); return callback(null, results);
} }
return null;
} }
} // Testing on a Raspberry Pi 4 Model B with an artificial 5ms file access
return lintNextItem(); // delay suggests that a concurrency factor of 8 can eliminate the impact
// of that delay (i.e., total time is the same as with no delay).
lintConcurrently();
lintConcurrently();
lintConcurrently();
lintConcurrently();
lintConcurrently();
lintConcurrently();
lintConcurrently();
lintConcurrently();
return null;
} }
/** /**

View file

@ -440,9 +440,6 @@ tape("resultFormattingV1", (test) => {
test.deepEqual(actualResult, expectedResult, "Undetected issues."); test.deepEqual(actualResult, expectedResult, "Undetected issues.");
const actualMessage = actualResult.toString(); const actualMessage = actualResult.toString();
const expectedMessage = const expectedMessage =
"truncate: 1: MD021/no-multiple-space-closed-atx" +
" Multiple spaces inside hashes on closed atx style heading" +
" [Context: \"# Multiple spa...tyle heading #\"]\n" +
"./test/atx_heading_spacing.md: 3: MD002/first-heading-h1" + "./test/atx_heading_spacing.md: 3: MD002/first-heading-h1" +
" First heading should be a top level heading" + " First heading should be a top level heading" +
" [Expected: h1; Actual: h2]\n" + " [Expected: h1; Actual: h2]\n" +
@ -457,7 +454,10 @@ tape("resultFormattingV1", (test) => {
" [Context: \"## Heading 3 {MD019}\"]\n" + " [Context: \"## Heading 3 {MD019}\"]\n" +
"./test/first_heading_bad_atx.md: 1: MD002/first-heading-h1" + "./test/first_heading_bad_atx.md: 1: MD002/first-heading-h1" +
" First heading should be a top level heading" + " First heading should be a top level heading" +
" [Expected: h1; Actual: h2]"; " [Expected: h1; Actual: h2]\n" +
"truncate: 1: MD021/no-multiple-space-closed-atx" +
" Multiple spaces inside hashes on closed atx style heading" +
" [Context: \"# Multiple spa...tyle heading #\"]";
test.equal(actualMessage, expectedMessage, "Incorrect message."); test.equal(actualMessage, expectedMessage, "Incorrect message.");
test.end(); test.end();
}); });
@ -535,9 +535,6 @@ tape("resultFormattingV2", (test) => {
test.deepEqual(actualResult, expectedResult, "Undetected issues."); test.deepEqual(actualResult, expectedResult, "Undetected issues.");
const actualMessage = actualResult.toString(); const actualMessage = actualResult.toString();
const expectedMessage = const expectedMessage =
"truncate: 1: MD021/no-multiple-space-closed-atx" +
" Multiple spaces inside hashes on closed atx style heading" +
" [Context: \"# Multiple spa...tyle heading #\"]\n" +
"./test/atx_heading_spacing.md: 3:" + "./test/atx_heading_spacing.md: 3:" +
" MD002/first-heading-h1/first-header-h1" + " MD002/first-heading-h1/first-header-h1" +
" First heading should be a top level heading" + " First heading should be a top level heading" +
@ -554,7 +551,10 @@ tape("resultFormattingV2", (test) => {
"./test/first_heading_bad_atx.md: 1:" + "./test/first_heading_bad_atx.md: 1:" +
" MD002/first-heading-h1/first-header-h1" + " MD002/first-heading-h1/first-header-h1" +
" First heading should be a top level heading" + " First heading should be a top level heading" +
" [Expected: h1; Actual: h2]"; " [Expected: h1; Actual: h2]\n" +
"truncate: 1: MD021/no-multiple-space-closed-atx" +
" Multiple spaces inside hashes on closed atx style heading" +
" [Context: \"# Multiple spa...tyle heading #\"]";
test.equal(actualMessage, expectedMessage, "Incorrect message."); test.equal(actualMessage, expectedMessage, "Incorrect message.");
test.end(); test.end();
}); });