refactor(ngcc): abstract task selection behind an interface (#32427)

This change does not alter the current behavior, but makes it easier to
introduce `TaskQueue`s implementing different task selection algorithms,
for example to support executing multiple tasks in parallel (while
respecting interdependencies between them).

Inspired by/Based on @alxhub's prototype: alxhub/angular@cb631bdb1

PR Close #32427
This commit is contained in:
George Kalpakas
2019-08-27 17:36:25 +03:00
committed by Matias Niemelä
parent 0cf94e3ed5
commit 2844dd2972
9 changed files with 468 additions and 13 deletions

View File

@ -7,6 +7,7 @@
*/
import {EntryPoint, EntryPointJsonProperty} from '../packages/entry_point';
import {PartiallyOrderedList} from '../utils';
/**
@ -15,7 +16,7 @@ import {EntryPoint, EntryPointJsonProperty} from '../packages/entry_point';
* @return A list of tasks that need to be executed in order to process the necessary format
* properties for all entry-points.
*/
export type AnalyzeEntryPointsFn = () => Task[];
export type AnalyzeEntryPointsFn = () => TaskQueue;
/** The type of the function that can process/compile a task. */
export type CompileFn = (task: Task) => void;
@ -32,6 +33,21 @@ export interface Executor {
void|Promise<void>;
}
/**
* Represents a partially ordered list of tasks.
*
* The ordering/precedence of tasks is determined by the inter-dependencies between their associated
* entry-points. Specifically, the tasks' order/precedence is such that tasks associated to
* dependent entry-points always come after tasks associated with their dependencies.
*
* As result of this ordering, it is guaranteed that - by processing tasks in the order in which
* they appear in the list - a task's dependencies will always have been processed before processing
* the task itself.
*
* See `DependencyResolver#sortEntryPointsByDependency()`.
*/
export type PartiallyOrderedTasks = PartiallyOrderedList<Task>;
/** Represents a unit of work: processing a specific format property of an entry-point. */
export interface Task {
/** The `EntryPoint` which needs to be processed as part of the task. */
@ -65,3 +81,43 @@ export const enum TaskProcessingOutcome {
/** Successfully processed the target format property. */
Processed,
}
/**
* A wrapper around a list of tasks and providing utility methods for getting the next task of
* interest and determining when all tasks have been completed.
*
* (This allows different implementations to impose different constraints on when a task's
* processing can start.)
*/
export interface TaskQueue {
/** Whether all tasks have been completed. */
allTasksCompleted: boolean;
/**
* Get the next task whose processing can start (if any).
*
* This implicitly marks the task as in-progress.
* (This information is used to determine whether all tasks have been completed.)
*
* @return The next task available for processing or `null`, if no task can be processed at the
* moment (including if there are no more unprocessed tasks).
*/
getNextTask(): Task|null;
/**
* Mark a task as completed.
*
* This removes the task from the internal list of in-progress tasks.
* (This information is used to determine whether all tasks have been completed.)
*
* @param task The task to mark as completed.
*/
markTaskCompleted(task: Task): void;
/**
* Return a string representation of the task queue (for debugging purposes).
*
* @return A string representation of the task queue.
*/
toString(): string;
}

View File

@ -22,13 +22,15 @@ export class SingleProcessExecutor implements Executor {
execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn): void {
this.logger.debug(`Running ngcc on ${this.constructor.name}.`);
const tasks = analyzeEntryPoints();
const taskQueue = analyzeEntryPoints();
const compile =
createCompileFn((task, outcome) => onTaskCompleted(this.pkgJsonUpdater, task, outcome));
// Process all tasks.
for (const task of tasks) {
while (!taskQueue.allTasksCompleted) {
const task = taskQueue.getNextTask() !;
compile(task);
taskQueue.markTaskCompleted(task);
}
}
}

View File

@ -0,0 +1,47 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {PartiallyOrderedTasks, Task, TaskQueue} from '../api';
import {stringifyTask} from '../utils';
/**
* A base `TaskQueue` implementation to be used as base for concrete implementations.
*/
export abstract class BaseTaskQueue implements TaskQueue {
get allTasksCompleted(): boolean {
return (this.tasks.length === 0) && (this.inProgressTasks.size === 0);
}
protected inProgressTasks = new Set<Task>();
constructor(protected tasks: PartiallyOrderedTasks) {}
abstract getNextTask(): Task|null;
markTaskCompleted(task: Task): void {
if (!this.inProgressTasks.has(task)) {
throw new Error(
`Trying to mark task that was not in progress as completed: ${stringifyTask(task)}`);
}
this.inProgressTasks.delete(task);
}
toString(): string {
const inProgTasks = Array.from(this.inProgressTasks);
return `${this.constructor.name}\n` +
` All tasks completed: ${this.allTasksCompleted}\n` +
` Unprocessed tasks (${this.tasks.length}): ${this.stringifyTasks(this.tasks, ' ')}\n` +
` In-progress tasks (${inProgTasks.length}): ${this.stringifyTasks(inProgTasks, ' ')}`;
}
protected stringifyTasks(tasks: Task[], indentation: string): string {
return tasks.map(task => `\n${indentation}- ${stringifyTask(task)}`).join('');
}
}

View File

@ -0,0 +1,37 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {Task} from '../api';
import {stringifyTask} from '../utils';
import {BaseTaskQueue} from './base_task_queue';
/**
* A `TaskQueue` implementation that assumes tasks are processed serially and each one is completed
* before requesting the next one.
*/
export class SerialTaskQueue extends BaseTaskQueue {
getNextTask(): Task|null {
const nextTask = this.tasks.shift() || null;
if (nextTask) {
if (this.inProgressTasks.size > 0) {
// `SerialTaskQueue` can have max one in-progress task.
const inProgressTask = this.inProgressTasks.values().next().value;
throw new Error(
'Trying to get next task, while there is already a task in progress: ' +
stringifyTask(inProgressTask));
}
this.inProgressTasks.add(nextTask);
}
return nextTask;
}
}

View File

@ -32,3 +32,7 @@ export const onTaskCompleted =
pkgJsonUpdater, entryPoint.packageJson, packageJsonPath, propsToMarkAsProcessed);
}
};
/** Stringify a task for debugging purposes. */
export const stringifyTask = (task: Task): string =>
`{entryPoint: ${task.entryPoint.name}, formatProperty: ${task.formatProperty}, processDts: ${task.processDts}}`;