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 right = array.length - 1;
while (left <= right) {
/* eslint-disable no-bitwise */
// eslint-disable-next-line no-bitwise
const mid = (left + right) >> 1;
if (array[mid] < element) {
left = mid + 1;

View file

@ -90,7 +90,9 @@ function newResults(ruleList) {
function toString(useAlias) {
let ruleNameToRule = null;
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];
if (Array.isArray(fileResults)) {
fileResults.forEach(function forResult(result) {
@ -727,39 +729,38 @@ function lintInput(options, synchronous, callback) {
md.use(...plugin);
});
const results = newResults(ruleList);
// Helper to lint the next string or file
/* eslint-disable consistent-return */
let done = false;
// Linting of strings is always synchronous
let syncItem = null;
// eslint-disable-next-line jsdoc/require-jsdoc
function lintNextItem() {
let iterating = true;
let item = null;
// eslint-disable-next-line jsdoc/require-jsdoc
function lintNextItemCallback(err, result) {
function syncCallback(err, result) {
if (err) {
iterating = false;
done = true;
return callback(err);
}
results[item] = result;
return iterating || lintNextItem();
results[syncItem] = result;
return null;
}
while (iterating) {
if ((item = stringsKeys.shift())) {
while (!done && (syncItem = stringsKeys.shift())) {
lintContent(
ruleList,
item,
strings[item] || "",
syncItem,
strings[syncItem] || "",
md,
config,
frontMatter,
handleRuleFailures,
noInlineConfig,
resultVersion,
lintNextItemCallback);
} else if ((item = files.shift())) {
iterating = synchronous;
syncCallback
);
}
if (synchronous) {
// Lint files synchronously
while (!done && (syncItem = files.shift())) {
lintFile(
ruleList,
item,
syncItem,
md,
config,
frontMatter,
@ -767,13 +768,59 @@ function lintInput(options, synchronous, callback) {
noInlineConfig,
resultVersion,
synchronous,
lintNextItemCallback);
} else {
syncCallback
);
}
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 null;
}
}
return lintNextItem();
// Testing on a Raspberry Pi 4 Model B with an artificial 5ms file access
// 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.");
const actualMessage = actualResult.toString();
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" +
" First heading should be a top level heading" +
" [Expected: h1; Actual: h2]\n" +
@ -457,7 +454,10 @@ tape("resultFormattingV1", (test) => {
" [Context: \"## Heading 3 {MD019}\"]\n" +
"./test/first_heading_bad_atx.md: 1: MD002/first-heading-h1" +
" 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.end();
});
@ -535,9 +535,6 @@ tape("resultFormattingV2", (test) => {
test.deepEqual(actualResult, expectedResult, "Undetected issues.");
const actualMessage = actualResult.toString();
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/first-header-h1" +
" First heading should be a top level heading" +
@ -554,7 +551,10 @@ tape("resultFormattingV2", (test) => {
"./test/first_heading_bad_atx.md: 1:" +
" MD002/first-heading-h1/first-header-h1" +
" 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.end();
});