| 
									
										
										
										
											2020-04-20 13:00:10 -07:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @license | 
					
						
							| 
									
										
										
										
											2020-05-19 12:08:49 -07:00
										 |  |  |  * Copyright Google LLC All Rights Reserved. | 
					
						
							| 
									
										
										
										
											2020-04-20 13:00:10 -07: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 16:29:53 -07:00
										 |  |  | import * as multimatch from 'multimatch'; | 
					
						
							| 
									
										
										
										
											2020-04-20 13:00:10 -07:00
										 |  |  | import {cpus} from 'os'; | 
					
						
							|  |  |  | import {exec} from 'shelljs'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-20 14:19:17 -07:00
										 |  |  | import {info} from '../utils/console'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-24 16:29:53 -07:00
										 |  |  | import {Formatter, FormatterAction, getActiveFormatters} from './formatters'; | 
					
						
							| 
									
										
										
										
											2020-04-20 13:00:10 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-24 16:29:53 -07:00
										 |  |  | const AVAILABLE_THREADS = Math.max(cpus().length - 1, 1); | 
					
						
							| 
									
										
										
										
											2020-04-20 13:00:10 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Run the provided commands in parallel for each provided file. | 
					
						
							|  |  |  |  * | 
					
						
							| 
									
										
										
										
											2020-04-24 16:29:53 -07: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 13:00:10 -07:00
										 |  |  |  * A promise is returned, completed when the command has completed running for each file. | 
					
						
							| 
									
										
										
										
											2020-04-24 16:29:53 -07:00
										 |  |  |  * The promise resolves with a list of failures, or `false` if no formatters have matched. | 
					
						
							| 
									
										
										
										
											2020-04-20 13:00:10 -07:00
										 |  |  |  */ | 
					
						
							| 
									
										
										
										
											2020-04-24 16:29:53 -07: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 13:00:10 -07:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2020-04-24 16:29:53 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     switch (action) { | 
					
						
							|  |  |  |       case 'format': | 
					
						
							| 
									
										
										
										
											2020-05-20 14:19:17 -07:00
										 |  |  |         info(`Formatting ${pendingCommands.length} file(s)`); | 
					
						
							| 
									
										
										
										
											2020-04-24 16:29:53 -07:00
										 |  |  |         break; | 
					
						
							|  |  |  |       case 'check': | 
					
						
							| 
									
										
										
										
											2020-05-20 14:19:17 -07:00
										 |  |  |         info(`Checking format of ${pendingCommands.length} file(s)`); | 
					
						
							| 
									
										
										
										
											2020-04-24 16:29:53 -07:00
										 |  |  |         break; | 
					
						
							|  |  |  |       default: | 
					
						
							|  |  |  |         throw Error(`Invalid format action "${action}": allowed actions are "format" and "check"`); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-20 13:00:10 -07: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 16:29:53 -07:00
										 |  |  |       const nextCommand = pendingCommands.pop(); | 
					
						
							| 
									
										
										
										
											2020-04-20 13:00:10 -07:00
										 |  |  |       // If no file was pulled from the array, return as there are no more files to run against.
 | 
					
						
							| 
									
										
										
										
											2020-04-24 16:29:53 -07:00
										 |  |  |       if (nextCommand === undefined) { | 
					
						
							|  |  |  |         threads[thread] = false; | 
					
						
							| 
									
										
										
										
											2020-04-20 13:00:10 -07:00
										 |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-24 16:29:53 -07:00
										 |  |  |       // Get the file and formatter for the next command.
 | 
					
						
							|  |  |  |       const {file, formatter} = nextCommand; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-20 13:00:10 -07:00
										 |  |  |       exec( | 
					
						
							| 
									
										
										
										
											2020-04-24 16:29:53 -07:00
										 |  |  |           `${formatter.commandFor(action)} ${file}`, | 
					
						
							| 
									
										
										
										
											2020-04-20 13:00:10 -07:00
										 |  |  |           {async: true, silent: true}, | 
					
						
							|  |  |  |           (code, stdout, stderr) => { | 
					
						
							|  |  |  |             // Run the provided callback function.
 | 
					
						
							| 
									
										
										
										
											2020-04-24 16:29:53 -07:00
										 |  |  |             const failed = formatter.callbackFor(action)(file, code, stdout, stderr); | 
					
						
							|  |  |  |             if (failed) { | 
					
						
							|  |  |  |               failures.push(file); | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2020-04-20 13:00:10 -07: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 16:29:53 -07:00
										 |  |  |             if (pendingCommands.length) { | 
					
						
							| 
									
										
										
										
											2020-04-20 13:00:10 -07: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 16:29:53 -07:00
										 |  |  |               resolve(failures); | 
					
						
							| 
									
										
										
										
											2020-04-20 13:00:10 -07:00
										 |  |  |             } | 
					
						
							|  |  |  |           }, | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |       // Mark the thread as in use as the command execution has been started.
 | 
					
						
							|  |  |  |       threads[thread] = true; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Start the progress bar
 | 
					
						
							| 
									
										
										
										
											2020-04-24 16:29:53 -07:00
										 |  |  |     progressBar.start(pendingCommands.length, 0); | 
					
						
							| 
									
										
										
										
											2020-04-20 13:00:10 -07:00
										 |  |  |     // Start running the command on files from the least in each available thread.
 | 
					
						
							|  |  |  |     threads.forEach((_, idx) => runCommandInThread(idx)); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | } |