2020-04-20 16:00:10 -04:00
|
|
|
/**
|
|
|
|
* @license
|
2020-05-19 15:08:49 -04:00
|
|
|
* Copyright Google LLC All Rights Reserved.
|
2020-04-20 16:00:10 -04:00
|
|
|
*
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
|
|
* found in the LICENSE file at https://angular.io/license
|
|
|
|
*/
|
|
|
|
|
|
|
|
import {Bar} from 'cli-progress';
|
2020-04-24 19:29:53 -04:00
|
|
|
import * as multimatch from 'multimatch';
|
2020-04-20 16:00:10 -04:00
|
|
|
import {cpus} from 'os';
|
|
|
|
import {exec} from 'shelljs';
|
|
|
|
|
2020-05-20 17:19:17 -04:00
|
|
|
import {info} from '../utils/console';
|
|
|
|
|
2020-04-24 19:29:53 -04:00
|
|
|
import {Formatter, FormatterAction, getActiveFormatters} from './formatters';
|
2020-04-20 16:00:10 -04:00
|
|
|
|
2020-04-24 19:29:53 -04:00
|
|
|
const AVAILABLE_THREADS = Math.max(cpus().length - 1, 1);
|
2020-04-20 16:00:10 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Run the provided commands in parallel for each provided file.
|
|
|
|
*
|
2020-04-24 19:29:53 -04:00
|
|
|
* Running the formatter is split across (number of available cpu threads - 1) processess.
|
|
|
|
* The task is done in multiple processess to speed up the overall time of the task, as running
|
|
|
|
* across entire repositories takes a large amount of time.
|
|
|
|
* As a data point for illustration, using 8 process rather than 1 cut the execution
|
|
|
|
* time from 276 seconds to 39 seconds for the same 2700 files.
|
|
|
|
*
|
2020-04-20 16:00:10 -04:00
|
|
|
* A promise is returned, completed when the command has completed running for each file.
|
2020-04-24 19:29:53 -04:00
|
|
|
* The promise resolves with a list of failures, or `false` if no formatters have matched.
|
2020-04-20 16:00:10 -04:00
|
|
|
*/
|
2020-04-24 19:29:53 -04:00
|
|
|
export function runFormatterInParallel(allFiles: string[], action: FormatterAction) {
|
|
|
|
return new Promise<false|string[]>((resolve) => {
|
|
|
|
const formatters = getActiveFormatters();
|
|
|
|
const failures: string[] = [];
|
|
|
|
const pendingCommands: {formatter: Formatter, file: string}[] = [];
|
|
|
|
|
|
|
|
for (const formatter of formatters) {
|
|
|
|
pendingCommands.push(...multimatch(allFiles, formatter.getFileMatcher(), {
|
|
|
|
dot: true
|
|
|
|
}).map(file => ({formatter, file})));
|
|
|
|
}
|
|
|
|
|
|
|
|
// If no commands are generated, resolve the promise as `false` as no files
|
|
|
|
// were run against the any formatters.
|
|
|
|
if (pendingCommands.length === 0) {
|
|
|
|
return resolve(false);
|
2020-04-20 16:00:10 -04:00
|
|
|
}
|
2020-04-24 19:29:53 -04:00
|
|
|
|
|
|
|
switch (action) {
|
|
|
|
case 'format':
|
2020-05-20 17:19:17 -04:00
|
|
|
info(`Formatting ${pendingCommands.length} file(s)`);
|
2020-04-24 19:29:53 -04:00
|
|
|
break;
|
|
|
|
case 'check':
|
2020-05-20 17:19:17 -04:00
|
|
|
info(`Checking format of ${pendingCommands.length} file(s)`);
|
2020-04-24 19:29:53 -04:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw Error(`Invalid format action "${action}": allowed actions are "format" and "check"`);
|
|
|
|
}
|
|
|
|
|
2020-04-20 16:00:10 -04:00
|
|
|
// The progress bar instance to use for progress tracking.
|
|
|
|
const progressBar =
|
|
|
|
new Bar({format: `[{bar}] ETA: {eta}s | {value}/{total} files`, clearOnComplete: true});
|
|
|
|
// A local copy of the files to run the command on.
|
|
|
|
// An array to represent the current usage state of each of the threads for parallelization.
|
|
|
|
const threads = new Array<boolean>(AVAILABLE_THREADS).fill(false);
|
|
|
|
|
|
|
|
// Recursively run the command on the next available file from the list using the provided
|
|
|
|
// thread.
|
|
|
|
function runCommandInThread(thread: number) {
|
2020-04-24 19:29:53 -04:00
|
|
|
const nextCommand = pendingCommands.pop();
|
2020-04-20 16:00:10 -04:00
|
|
|
// If no file was pulled from the array, return as there are no more files to run against.
|
2020-04-24 19:29:53 -04:00
|
|
|
if (nextCommand === undefined) {
|
|
|
|
threads[thread] = false;
|
2020-04-20 16:00:10 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-04-24 19:29:53 -04:00
|
|
|
// Get the file and formatter for the next command.
|
|
|
|
const {file, formatter} = nextCommand;
|
|
|
|
|
2020-04-20 16:00:10 -04:00
|
|
|
exec(
|
2020-04-24 19:29:53 -04:00
|
|
|
`${formatter.commandFor(action)} ${file}`,
|
2020-04-20 16:00:10 -04:00
|
|
|
{async: true, silent: true},
|
|
|
|
(code, stdout, stderr) => {
|
|
|
|
// Run the provided callback function.
|
2020-04-24 19:29:53 -04:00
|
|
|
const failed = formatter.callbackFor(action)(file, code, stdout, stderr);
|
|
|
|
if (failed) {
|
|
|
|
failures.push(file);
|
|
|
|
}
|
2020-04-20 16:00:10 -04:00
|
|
|
// Note in the progress bar another file being completed.
|
|
|
|
progressBar.increment(1);
|
|
|
|
// If more files exist in the list, run again to work on the next file,
|
|
|
|
// using the same slot.
|
2020-04-24 19:29:53 -04:00
|
|
|
if (pendingCommands.length) {
|
2020-04-20 16:00:10 -04:00
|
|
|
return runCommandInThread(thread);
|
|
|
|
}
|
|
|
|
// If not more files are available, mark the thread as unused.
|
|
|
|
threads[thread] = false;
|
|
|
|
// If all of the threads are false, as they are unused, mark the progress bar
|
|
|
|
// completed and resolve the promise.
|
|
|
|
if (threads.every(active => !active)) {
|
|
|
|
progressBar.stop();
|
2020-04-24 19:29:53 -04:00
|
|
|
resolve(failures);
|
2020-04-20 16:00:10 -04:00
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
// Mark the thread as in use as the command execution has been started.
|
|
|
|
threads[thread] = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start the progress bar
|
2020-04-24 19:29:53 -04:00
|
|
|
progressBar.start(pendingCommands.length, 0);
|
2020-04-20 16:00:10 -04:00
|
|
|
// Start running the command on files from the least in each available thread.
|
|
|
|
threads.forEach((_, idx) => runCommandInThread(idx));
|
|
|
|
});
|
|
|
|
}
|