
Later when we implement the ability to continue processing when tasks have failed to compile, we will also need to avoid processing tasks that depend upon the failed task. This refactoring exposes this list of dependent tasks in a way that can be used to skip processing of tasks that depend upon a failed task. It also changes the blocking model of the parallel mode of operation so that non-typings tasks are now blocked on their corresponding typings task. Previously the non-typings tasks could be triggered to run in parallel to the typings task, since they do not have a hard dependency on each other, but this made it difficult to skip task correctly if the typings task failed, since it was possible that a non-typings task was already in flight when the typings task failed. The result of this is a small potential degradation of performance in async parallel processing mode, in the rare cases that there were not enough unblocked tasks to make use of all the available workers. PR Close #36083
139 lines
5.7 KiB
TypeScript
139 lines
5.7 KiB
TypeScript
/**
|
|
* @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 {DepGraph} from 'dependency-graph';
|
|
import {EntryPoint} from '../../packages/entry_point';
|
|
import {PartiallyOrderedTasks, Task, TaskDependencies} from './api';
|
|
|
|
/** Stringify a task for debugging purposes. */
|
|
export const stringifyTask = (task: Task): string =>
|
|
`{entryPoint: ${task.entryPoint.name}, formatProperty: ${task.formatProperty}, processDts: ${task.processDts}}`;
|
|
|
|
/**
|
|
* Compute a mapping of tasks to the tasks that are dependent on them (if any).
|
|
*
|
|
* Task A can depend upon task B, if either:
|
|
*
|
|
* * A and B have the same entry-point _and_ B is generating the typings for that entry-point
|
|
* (i.e. has `processDts: true`).
|
|
* * A's entry-point depends on B's entry-point _and_ B is also generating typings.
|
|
*
|
|
* NOTE: If a task is not generating typings, then it cannot affect anything which depends on its
|
|
* entry-point, regardless of the dependency graph. To put this another way, only the task
|
|
* which produces the typings for a dependency needs to have been completed.
|
|
*
|
|
* As a performance optimization, we take into account the fact that `tasks` are sorted in such a
|
|
* way that a task can only depend on earlier tasks (i.e. dependencies always come before
|
|
* dependents in the list of tasks).
|
|
*
|
|
* @param tasks A (partially ordered) list of tasks.
|
|
* @param graph The dependency graph between entry-points.
|
|
* @return A map from each task to those tasks directly dependent upon it.
|
|
*/
|
|
export function computeTaskDependencies(
|
|
tasks: PartiallyOrderedTasks, graph: DepGraph<EntryPoint>): TaskDependencies {
|
|
const dependencies = new TaskDependencies();
|
|
const candidateDependencies = new Map<string, Task>();
|
|
|
|
tasks.forEach(task => {
|
|
const entryPointPath = task.entryPoint.path;
|
|
|
|
// Find the earlier tasks (`candidateDependencies`) that this task depends upon.
|
|
const deps = graph.dependenciesOf(entryPointPath);
|
|
const taskDependencies = deps.filter(dep => candidateDependencies.has(dep))
|
|
.map(dep => candidateDependencies.get(dep) !);
|
|
|
|
// If this task has dependencies, add it to the dependencies and dependents maps.
|
|
if (taskDependencies.length > 0) {
|
|
for (const dependency of taskDependencies) {
|
|
const taskDependents = getDependentsSet(dependencies, dependency);
|
|
taskDependents.add(task);
|
|
}
|
|
}
|
|
|
|
if (task.processDts) {
|
|
// SANITY CHECK:
|
|
// There should only be one task per entry-point that generates typings (and thus can be a
|
|
// dependency of other tasks), so the following should theoretically never happen, but check
|
|
// just in case.
|
|
if (candidateDependencies.has(entryPointPath)) {
|
|
const otherTask = candidateDependencies.get(entryPointPath) !;
|
|
throw new Error(
|
|
'Invariant violated: Multiple tasks are assigned generating typings for ' +
|
|
`'${entryPointPath}':\n - ${stringifyTask(otherTask)}\n - ${stringifyTask(task)}`);
|
|
}
|
|
// This task can potentially be a dependency (i.e. it generates typings), so add it to the
|
|
// list of candidate dependencies for subsequent tasks.
|
|
candidateDependencies.set(entryPointPath, task);
|
|
} else {
|
|
// This task is not generating typings so we need to add it to the dependents of the task that
|
|
// does generate typings, if that exists
|
|
if (candidateDependencies.has(entryPointPath)) {
|
|
const typingsTask = candidateDependencies.get(entryPointPath) !;
|
|
const typingsTaskDependents = getDependentsSet(dependencies, typingsTask);
|
|
typingsTaskDependents.add(task);
|
|
}
|
|
}
|
|
});
|
|
|
|
return dependencies;
|
|
}
|
|
|
|
export function getDependentsSet(map: TaskDependencies, task: Task): Set<Task> {
|
|
if (!map.has(task)) {
|
|
map.set(task, new Set());
|
|
}
|
|
return map.get(task) !;
|
|
}
|
|
|
|
/**
|
|
* Invert the given mapping of Task dependencies.
|
|
*
|
|
* @param dependencies The mapping of tasks to the tasks that depend upon them.
|
|
* @returns A mapping of tasks to the tasks that they depend upon.
|
|
*/
|
|
export function getBlockedTasks(dependencies: TaskDependencies): Map<Task, Set<Task>> {
|
|
const blockedTasks = new Map<Task, Set<Task>>();
|
|
for (const [dependency, dependents] of dependencies) {
|
|
for (const dependent of dependents) {
|
|
const dependentSet = getDependentsSet(blockedTasks, dependent);
|
|
dependentSet.add(dependency);
|
|
}
|
|
}
|
|
return blockedTasks;
|
|
}
|
|
|
|
/**
|
|
* Sort a list of tasks by priority.
|
|
*
|
|
* Priority is determined by the number of other tasks that a task is (transitively) blocking:
|
|
* The more tasks a task is blocking the higher its priority is, because processing it will
|
|
* potentially unblock more tasks.
|
|
*
|
|
* To keep the behavior predictable, if two tasks block the same number of other tasks, their
|
|
* relative order in the original `tasks` lists is preserved.
|
|
*
|
|
* @param tasks A (partially ordered) list of tasks.
|
|
* @param dependencies The mapping of tasks to the tasks that depend upon them.
|
|
* @return The list of tasks sorted by priority.
|
|
*/
|
|
export function sortTasksByPriority(
|
|
tasks: PartiallyOrderedTasks, dependencies: TaskDependencies): PartiallyOrderedTasks {
|
|
const priorityPerTask = new Map<Task, [number, number]>();
|
|
const computePriority = (task: Task, idx: number):
|
|
[number, number] => [dependencies.has(task) ? dependencies.get(task) !.size : 0, idx];
|
|
|
|
tasks.forEach((task, i) => priorityPerTask.set(task, computePriority(task, i)));
|
|
|
|
return tasks.slice().sort((task1, task2) => {
|
|
const [p1, idx1] = priorityPerTask.get(task1) !;
|
|
const [p2, idx2] = priorityPerTask.get(task2) !;
|
|
|
|
return (p2 - p1) || (idx1 - idx2);
|
|
});
|
|
}
|