diff --git a/packages/compiler-cli/ngcc/src/execution/task_selection/parallel_task_queue.ts b/packages/compiler-cli/ngcc/src/execution/task_selection/parallel_task_queue.ts new file mode 100644 index 0000000000..d2a063fcf0 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/execution/task_selection/parallel_task_queue.ts @@ -0,0 +1,176 @@ +/** + * @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} from '../api'; +import {stringifyTask} from '../utils'; + +import {BaseTaskQueue} from './base_task_queue'; + + +/** + * A `TaskQueue` implementation that assumes tasks are processed in parallel, thus has to ensure a + * task's dependencies have been processed before processing the task. + */ +export class ParallelTaskQueue extends BaseTaskQueue { + /** + * A mapping from each task to the list of tasks that are blocking it (if any). + * + * A task can block another task, if the latter's entry-point depends on the former's entry-point + * _and_ the former is also generating typings (i.e. has `processDts: true`). + * + * 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. + */ + private blockedTasks: Map>; + + constructor(tasks: PartiallyOrderedTasks, graph: DepGraph) { + const blockedTasks = computeBlockedTasks(tasks, graph); + const sortedTasks = sortTasksByPriority(tasks, blockedTasks); + + super(sortedTasks); + this.blockedTasks = blockedTasks; + } + + getNextTask(): Task|null { + // Look for the first available (i.e. not blocked) task. + // (NOTE: Since tasks are sorted by priority, the first available one is the best choice.) + const nextTaskIdx = this.tasks.findIndex(task => !this.blockedTasks.has(task)); + if (nextTaskIdx === -1) return null; + + // Remove the task from the list of available tasks and add it to the list of in-progress tasks. + const nextTask = this.tasks[nextTaskIdx]; + this.tasks.splice(nextTaskIdx, 1); + this.inProgressTasks.add(nextTask); + + return nextTask; + } + + markTaskCompleted(task: Task): void { + super.markTaskCompleted(task); + + const unblockedTasks: Task[] = []; + + // Remove the completed task from the lists of tasks blocking other tasks. + for (const [otherTask, blockingTasks] of Array.from(this.blockedTasks)) { + if (blockingTasks.has(task)) { + blockingTasks.delete(task); + + // If the other task is not blocked any more, mark it for unblocking. + if (blockingTasks.size === 0) { + unblockedTasks.push(otherTask); + } + } + } + + // Unblock tasks that are no longer blocked. + unblockedTasks.forEach(task => this.blockedTasks.delete(task)); + } + + toString(): string { + return `${super.toString()}\n` + + ` Blocked tasks (${this.blockedTasks.size}): ${this.stringifyBlockedTasks(' ')}`; + } + + private stringifyBlockedTasks(indentation: string): string { + return Array.from(this.blockedTasks) + .map( + ([task, blockingTasks]) => + `\n${indentation}- ${stringifyTask(task)} (${blockingTasks.size}): ` + + this.stringifyTasks(Array.from(blockingTasks), `${indentation} `)) + .join(''); + } +} + +// Helpers + +/** + * Compute a mapping of blocked tasks to the tasks that are blocking them. + * + * As a performance optimization, we take into account the fact that `tasks` are sorted in such a + * way that a task can only be blocked by earlier tasks (i.e. dependencies always come before + * dependants in the list of tasks). + * + * @param tasks A (partially ordered) list of tasks. + * @param graph The dependency graph between entry-points. + * @return The map of blocked tasks to the tasks that are blocking them. + */ +function computeBlockedTasks( + tasks: PartiallyOrderedTasks, graph: DepGraph): Map> { + const blockedTasksMap = new Map>(); + const candidateBlockers = new Map(); + + tasks.forEach(task => { + // Find the earlier tasks (`candidateBlockers`) that are blocking this task. + const deps = graph.dependenciesOf(task.entryPoint.path); + const blockingTasks = + deps.filter(dep => candidateBlockers.has(dep)).map(dep => candidateBlockers.get(dep) !); + + // If this task is blocked, add it to the map of blocked tasks. + if (blockingTasks.length > 0) { + blockedTasksMap.set(task, new Set(blockingTasks)); + } + + // If this task can be potentially blocking (i.e. it generates typings), add it to the list + // of candidate blockers for subsequent tasks. + if (task.processDts) { + const entryPointPath = task.entryPoint.path; + + // There should only be one task per entry-point that generates typings (and thus can block + // other tasks), so the following should theoretically never happen, but check just in case. + if (candidateBlockers.has(entryPointPath)) { + const otherTask = candidateBlockers.get(entryPointPath) !; + + throw new Error( + 'Invariant violated: Multiple tasks are assigned generating typings for ' + + `'${entryPointPath}':\n - ${stringifyTask(otherTask)}\n - ${stringifyTask(task)}`); + } + + candidateBlockers.set(entryPointPath, task); + } + }); + + return blockedTasksMap; +} + +/** + * 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 blockedTasks A mapping from a task to the list of tasks that are blocking it (if any). + * @return The list of tasks sorted by priority. + */ +function sortTasksByPriority( + tasks: PartiallyOrderedTasks, blockedTasks: Map>): PartiallyOrderedTasks { + const priorityPerTask = new Map(); + const allBlockingTaskSets = Array.from(blockedTasks.values()); + const computePriority = (task: Task, idx: number): [number, number] => + [allBlockingTaskSets.reduce( + (count, blockingTasks) => count + (blockingTasks.has(task) ? 1 : 0), 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); + }); +} diff --git a/packages/compiler-cli/ngcc/test/BUILD.bazel b/packages/compiler-cli/ngcc/test/BUILD.bazel index 54d2264902..4a72d20160 100644 --- a/packages/compiler-cli/ngcc/test/BUILD.bazel +++ b/packages/compiler-cli/ngcc/test/BUILD.bazel @@ -24,6 +24,7 @@ ts_library( "//packages/compiler-cli/test/helpers", "@npm//@types/convert-source-map", "@npm//convert-source-map", + "@npm//dependency-graph", "@npm//magic-string", "@npm//typescript", ], diff --git a/packages/compiler-cli/ngcc/test/execution/task_selection/parallel_task_queue_spec.ts b/packages/compiler-cli/ngcc/test/execution/task_selection/parallel_task_queue_spec.ts new file mode 100644 index 0000000000..83a6920d91 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/execution/task_selection/parallel_task_queue_spec.ts @@ -0,0 +1,556 @@ +/** + * @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 {PartiallyOrderedTasks, Task, TaskQueue} from '../../../src/execution/api'; +import {ParallelTaskQueue} from '../../../src/execution/task_selection/parallel_task_queue'; +import {EntryPoint} from '../../../src/packages/entry_point'; + + +describe('ParallelTaskQueue', () => { + // Helpers + /** + * Create a `TaskQueue` by generating mock tasks (optionally with interdependencies). + * + * NOTE 1: The first task for each entry-point generates typings (which is similar to what happens + * in the actual code). + * NOTE 2: The `ParallelTaskQueue` implementation relies on the fact that tasks are sorted in such + * a way that a task can only be blocked by earlier tasks (i.e. dependencies always come + * before dependants in the list of tasks). + * To preserve this attribute, you need to ensure that entry-points will only depend on + * entry-points with a lower index. Take this into account when defining `entryPointDeps`. + * (Failing to do so, will result in an error.) + * + * @param entryPointCount The number of different entry-points to mock. + * @param tasksPerEntryPointCount The number of tasks to generate per entry-point (i.e. simulating + * processing multiple format properties). + * @param entryPointDeps An object mapping an entry-point to its dependencies. Keys are + * entry-point indices and values are arrays of entry-point indices that the + * entry-point corresponding to the key depends on. + * For example, if entry-point #2 depends on entry-points #0 and #1, + * `entryPointDeps` would be `{2: [0, 1]}`. + * @return An object with the following properties: + * - `graph`: The dependency graph for the generated mock entry-point. + * - `tasks`: The (partially ordered) list of generated mock tasks. + * - `queue`: The created `TaskQueue`. + */ + const createQueue = (entryPointCount: number, tasksPerEntryPointCount = 1, entryPointDeps: { + [entryPointIndex: string]: number[] + } = {}): {tasks: PartiallyOrderedTasks, graph: DepGraph, queue: TaskQueue} => { + const entryPoints: EntryPoint[] = []; + const tasks: PartiallyOrderedTasks = [] as any; + const graph = new DepGraph(); + + // Create the entry-points and the associated tasks. + for (let epIdx = 0; epIdx < entryPointCount; epIdx++) { + const entryPoint = { + name: `entry-point-${epIdx}`, + path: `/path/to/entry/point/${epIdx}`, + } as EntryPoint; + + entryPoints.push(entryPoint); + graph.addNode(entryPoint.path); + + for (let tIdx = 0; tIdx < tasksPerEntryPointCount; tIdx++) { + tasks.push({ entryPoint, formatProperty: `prop-${tIdx}`, processDts: tIdx === 0, } as Task); + } + } + + // Define entry-point interdependencies. + for (const epIdx of Object.keys(entryPointDeps).map(strIdx => +strIdx)) { + const fromPath = entryPoints[epIdx].path; + for (const depIdx of entryPointDeps[epIdx]) { + // Ensure that each entry-point only depends on entry-points at a lower index. + if (depIdx >= epIdx) { + throw Error( + 'Invalid `entryPointDeps`: Entry-points can only depend on entry-points at a lower ' + + `index, but entry-point #${epIdx} depends on #${depIdx} in: ` + + JSON.stringify(entryPointDeps, null, 2)); + } + + const toPath = entryPoints[depIdx].path; + graph.addDependency(fromPath, toPath); + } + } + + return {tasks, graph, queue: new ParallelTaskQueue(tasks.slice(), graph)}; + }; + + /** + * Simulate processing the next task: + * - Request the next task from the specified queue. + * - If a task was returned, mark it as completed. + * - Return the task (this allows making assertions against the picked tasks in tests). + * + * @param queue The `TaskQueue` to get the next task from. + * @return The "processed" task (if any). + */ + const processNextTask = (queue: TaskQueue): ReturnType => { + const task = queue.getNextTask(); + if (task !== null) queue.markTaskCompleted(task); + return task; + }; + + describe('allTaskCompleted', () => { + it('should be `false`, when there are unprocessed tasks', () => { + const {queue} = createQueue(2); + expect(queue.allTasksCompleted).toBe(false); + + processNextTask(queue); + expect(queue.allTasksCompleted).toBe(false); + }); + + it('should be `false`, when there are tasks in progress', () => { + const {queue} = createQueue(2); + + queue.getNextTask(); + expect(queue.allTasksCompleted).toBe(false); + + processNextTask(queue); + expect(queue.allTasksCompleted).toBe(false); // The first task is still in progress. + }); + + it('should be `true`, when there are no unprocess or in-progress tasks', () => { + const {queue} = createQueue(3); + + const task1 = queue.getNextTask() !; + const task2 = queue.getNextTask() !; + const task3 = queue.getNextTask() !; + expect(queue.allTasksCompleted).toBe(false); + + queue.markTaskCompleted(task1); + queue.markTaskCompleted(task3); + expect(queue.allTasksCompleted).toBe(false); // The second task is still in progress. + + queue.markTaskCompleted(task2); + expect(queue.allTasksCompleted).toBe(true); + }); + + it('should be `true`, if the queue was empty from the beginning', () => { + const {queue} = createQueue(0); + expect(queue.allTasksCompleted).toBe(true); + }); + + it('should remain `true` once the queue has been emptied', () => { + const {queue} = createQueue(1); + expect(queue.allTasksCompleted).toBe(false); + + processNextTask(queue); + expect(queue.allTasksCompleted).toBe(true); + + queue.getNextTask(); + expect(queue.allTasksCompleted).toBe(true); + }); + }); + + describe('constructor()', () => { + it('should throw, if there are multiple tasks that generate typings for a single entry-point', + () => { + const {tasks, graph} = createQueue(2, 2, { + 0: [], // Entry-point #0 does not depend on anything. + 1: [0], // Entry-point #1 depends on #0. + }); + tasks[1].processDts = true; // Tweak task #1 to also generate typings. + + expect(() => new ParallelTaskQueue(tasks, graph)) + .toThrowError( + 'Invariant violated: Multiple tasks are assigned generating typings for ' + + '\'/path/to/entry/point/0\':\n' + + ' - {entryPoint: entry-point-0, formatProperty: prop-0, processDts: true}\n' + + ' - {entryPoint: entry-point-0, formatProperty: prop-1, processDts: true}'); + }); + }); + + describe('getNextTask()', () => { + it('should return the tasks in order (when they are not blocked by other tasks)', () => { + const {tasks, queue} = createQueue(3, 2, {}); // 2 tasks per entry-point; no dependencies. + + expect(queue.getNextTask()).toBe(tasks[0]); + expect(queue.getNextTask()).toBe(tasks[1]); + expect(queue.getNextTask()).toBe(tasks[2]); + expect(queue.getNextTask()).toBe(tasks[3]); + expect(queue.getNextTask()).toBe(tasks[4]); + expect(queue.getNextTask()).toBe(tasks[5]); + }); + + it('should return `null`, when there are no more tasks', () => { + const {tasks, queue} = createQueue(3); + tasks.forEach(() => expect(queue.getNextTask()).not.toBe(null)); + + expect(queue.getNextTask()).toBe(null); + expect(queue.getNextTask()).toBe(null); + + const {tasks: tasks2, queue: queue2} = createQueue(0); + + expect(tasks2).toEqual([]); + expect(queue.getNextTask()).toBe(null); + expect(queue.getNextTask()).toBe(null); + }); + + it('should return `null`, if all unprocessed tasks are blocked', () => { + const {tasks, queue} = createQueue(2, 2, { + 0: [], // Entry-point #0 does not depend on anything. + 1: [0], // Entry-point #1 depends on #0. + }); + + // Verify that the first two tasks are for the first entry-point. + expect(tasks[0].entryPoint.name).toBe('entry-point-0'); + expect(tasks[1].entryPoint.name).toBe('entry-point-0'); + + // Verify that the last two tasks are for the second entry-point. + expect(tasks[2].entryPoint.name).toBe('entry-point-1'); + expect(tasks[3].entryPoint.name).toBe('entry-point-1'); + + // Return the first two tasks first, since they are not blocked. + expect(queue.getNextTask()).toBe(tasks[0]); + expect(queue.getNextTask()).toBe(tasks[1]); + + // No task available, until task #0 (which geenrates typings for entry-point #0) is completed. + expect(tasks[0].processDts).toBe(true); + expect(tasks[1].processDts).toBe(false); + + queue.markTaskCompleted(tasks[1]); + expect(queue.getNextTask()).toBe(null); + + // Finally, unblock tasks for entry-point #1, once task #0 is completed. + queue.markTaskCompleted(tasks[0]); + expect(queue.getNextTask()).toBe(tasks[2]); + expect(queue.getNextTask()).toBe(tasks[3]); + }); + + it('should prefer tasks that are blocking many tasks', () => { + // Tasks by priority: #1, #0, #2, #3 + // - Entry-point #0 transitively blocks 1 entry-point(s): 2 + // - Entry-point #1 transitively blocks 2 entry-point(s): 2, 3 + // - Entry-point #2 transitively blocks 0 entry-point(s): - + // - Entry-point #3 transitively blocks 0 entry-point(s): - + const {tasks, queue} = createQueue(5, 1, { + 0: [], + 1: [], + 2: [0, 1], + 3: [1], + }); + + // First return task #1, since it blocks the most other tasks. + expect(processNextTask(queue)).toBe(tasks[1]); + + // Then return task #0, since it blocks the most other tasks after #1. + expect(processNextTask(queue)).toBe(tasks[0]); + + // Then return task #2, since it comes before #3. + expect(processNextTask(queue)).toBe(tasks[2]); + + // Finally return task #3. + expect(processNextTask(queue)).toBe(tasks[3]); + }); + + it('should return a lower priority task, if higher priority tasks are blocked', () => { + // Tasks by priority: #0, #1, #3, #2, #4 + // - Entry-point #0 transitively blocks 3 entry-point(s): 1, 2, 4 + // - Entry-point #1 transitively blocks 2 entry-point(s): 2, 4 + // - Entry-point #2 transitively blocks 0 entry-point(s): - + // - Entry-point #3 transitively blocks 1 entry-point(s): 4 + // - Entry-point #4 transitively blocks 0 entry-point(s): - + const deps = { + 0: [], + 1: [0], + 2: [0, 1], + 3: [], + 4: [0, 1, 3], + }; + + const {tasks, queue} = createQueue(5, 1, deps); + + // First return task #0, since that blocks the most other tasks. + expect(queue.getNextTask()).toBe(tasks[0]); + + // While task #0 is still in progress, return task #3. + // Despite task #3's having a lower priority than #1 (blocking 1 vs 2 other tasks), task #1 is + // currently blocked on #0 (while #3 is not blocked by anything). + expect(queue.getNextTask()).toBe(tasks[3]); + + // Use the same dependencies as above, but complete task #0 before asking for another task to + // verify that task #1 would be returned in this case. + const {tasks: tasks2, queue: queue2} = createQueue(5, 1, deps); + + expect(processNextTask(queue2)).toBe(tasks2[0]); + expect(processNextTask(queue2)).toBe(tasks2[1]); + }); + + it('should keep they initial relative order of tasks for tasks with the same priority', () => { + // Tasks by priority: #1, #3, #0, #2, #4 + // - Entry-point #0 transitively blocks 0 entry-point(s): - + // - Entry-point #1 transitively blocks 1 entry-point(s): 2 + // - Entry-point #2 transitively blocks 0 entry-point(s): - + // - Entry-point #3 transitively blocks 1 entry-point(s): 4 + // - Entry-point #4 transitively blocks 0 entry-point(s): - + const {tasks, queue} = createQueue(5, 1, { + 0: [], + 1: [], + 2: [1], + 3: [], + 4: [3], + }); + + // First return task #1 (even if it has the same priority as #3), because it comes before #3 + // in the initial task list. + // Note that task #0 is not returned (even if it comes first in the initial task list), + // because it has a lower priority (i.e. blocks fewer other tasks). + expect(processNextTask(queue)).toBe(tasks[1]); + + // Then return task #3 (skipping over both #0 and #2), becasue it blocks the most other tasks. + expect(processNextTask(queue)).toBe(tasks[3]); + + // The rest of the tasks (#0, #2, #4) block no tasks, so their initial relative order is + // preserved. + expect(queue.getNextTask()).toBe(tasks[0]); + expect(queue.getNextTask()).toBe(tasks[2]); + expect(queue.getNextTask()).toBe(tasks[4]); + }); + }); + + describe('markTaskCompleted()', () => { + it('should mark a task as completed', () => { + const {queue} = createQueue(2); + + const task1 = queue.getNextTask() !; + const task2 = queue.getNextTask() !; + expect(queue.allTasksCompleted).toBe(false); + + queue.markTaskCompleted(task1); + queue.markTaskCompleted(task2); + expect(queue.allTasksCompleted).toBe(true); + }); + + it('should throw, if the specified task is not in progress', () => { + const {tasks, queue} = createQueue(3); + queue.getNextTask(); + + expect(() => queue.markTaskCompleted(tasks[2])) + .toThrowError( + `Trying to mark task that was not in progress as completed: ` + + `{entryPoint: entry-point-2, formatProperty: prop-0, processDts: true}`); + }); + + it('should remove the completed task from the lists of blocking tasks (so other tasks can be unblocked)', + () => { + const {tasks, queue} = createQueue(3, 1, { + 0: [], // Entry-point #0 does not depend on anything. + 1: [0], // Entry-point #1 depends on #0. + 2: [0, 1], // Entry-point #2 depends on #0 and #1. + }); + + // Pick task #0 first, since it is the only one that is not blocked by other tasks. + expect(queue.getNextTask()).toBe(tasks[0]); + + // No task available, until task #0 is completed. + expect(queue.getNextTask()).toBe(null); + + // Once task #0 is completed, task #1 is unblocked. + queue.markTaskCompleted(tasks[0]); + expect(queue.getNextTask()).toBe(tasks[1]); + + // Task #2 is still blocked on #1. + expect(queue.getNextTask()).toBe(null); + + // Once task #1 is completed, task #2 is unblocked. + queue.markTaskCompleted(tasks[1]); + expect(queue.getNextTask()).toBe(tasks[2]); + }); + }); + + describe('toString()', () => { + it('should include the `TaskQueue` constructor\'s name', () => { + const {queue} = createQueue(0); + expect(queue.toString()).toMatch(/^ParallelTaskQueue\n/); + }); + + it('should include the value of `allTasksCompleted`', () => { + const {queue: queue1} = createQueue(0); + expect(queue1.toString()).toContain(' All tasks completed: true\n'); + + const {queue: queue2} = createQueue(3); + expect(queue2.toString()).toContain(' All tasks completed: false\n'); + + processNextTask(queue2); + processNextTask(queue2); + const task = queue2.getNextTask() !; + + expect(queue2.toString()).toContain(' All tasks completed: false\n'); + + queue2.markTaskCompleted(task); + expect(queue2.toString()).toContain(' All tasks completed: true\n'); + }); + + it('should include the unprocessed tasks', () => { + const {queue} = createQueue(3); + expect(queue.toString()) + .toContain( + ' Unprocessed tasks (3): \n' + + ' - {entryPoint: entry-point-0, formatProperty: prop-0, processDts: true}\n' + + ' - {entryPoint: entry-point-1, formatProperty: prop-0, processDts: true}\n' + + ' - {entryPoint: entry-point-2, formatProperty: prop-0, processDts: true}\n'); + + const task1 = queue.getNextTask() !; + expect(queue.toString()) + .toContain( + ' Unprocessed tasks (2): \n' + + ' - {entryPoint: entry-point-1, formatProperty: prop-0, processDts: true}\n' + + ' - {entryPoint: entry-point-2, formatProperty: prop-0, processDts: true}\n'); + + queue.markTaskCompleted(task1); + const task2 = queue.getNextTask() !; + expect(queue.toString()) + .toContain( + ' Unprocessed tasks (1): \n' + + ' - {entryPoint: entry-point-2, formatProperty: prop-0, processDts: true}\n'); + + queue.markTaskCompleted(task2); + processNextTask(queue); + expect(queue.toString()).toContain(' Unprocessed tasks (0): \n'); + }); + + it('should include the in-progress tasks', () => { + const {queue} = createQueue(3); + expect(queue.toString()).toContain(' In-progress tasks (0): \n'); + + const task1 = queue.getNextTask() !; + expect(queue.toString()) + .toContain( + ' In-progress tasks (1): \n' + + ' - {entryPoint: entry-point-0, formatProperty: prop-0, processDts: true}\n'); + + queue.markTaskCompleted(task1); + const task2 = queue.getNextTask() !; + expect(queue.toString()) + .toContain( + ' In-progress tasks (1): \n' + + ' - {entryPoint: entry-point-1, formatProperty: prop-0, processDts: true}\n'); + + queue.markTaskCompleted(task2); + processNextTask(queue); + expect(queue.toString()).toContain(' In-progress tasks (0): \n'); + }); + + it('should include the blocked/blocking tasks', () => { + // Entry-point #0 transitively blocks 2 entry-point(s): 1, 3 + // Entry-point #1 transitively blocks 1 entry-point(s): 3 + // Entry-point #2 transitively blocks 1 entry-point(s): 3 + // Entry-point #3 transitively blocks 0 entry-point(s): - + const {tasks, queue} = createQueue(4, 2, { + 1: [0], + 3: [1, 2], + }); + + // Since there 4 entry-points and two tasks per entry-point (8 tasks in total), in comments + // below, tasks are denoted as `#X.Y` (where `X` is the entry-point index and Y is the task + // index). + // For example, the second task for the third entry-point would be `#2.1`. + + expect(queue.toString()) + .toContain( + ' Blocked tasks (4): \n' + + // Tasks #1.0 and #1.1 are blocked by #0.0. + ' - {entryPoint: entry-point-1, formatProperty: prop-0, processDts: true} (1): \n' + + ' - {entryPoint: entry-point-0, formatProperty: prop-0, processDts: true}\n' + + ' - {entryPoint: entry-point-1, formatProperty: prop-1, processDts: false} (1): \n' + + ' - {entryPoint: entry-point-0, formatProperty: prop-0, processDts: true}\n' + + // Tasks #3.0 and #3.1 are blocked by #0.0 (transitively), #1.0 and #2.0. + ' - {entryPoint: entry-point-3, formatProperty: prop-0, processDts: true} (3): \n' + + ' - {entryPoint: entry-point-0, formatProperty: prop-0, processDts: true}\n' + + ' - {entryPoint: entry-point-1, formatProperty: prop-0, processDts: true}\n' + + ' - {entryPoint: entry-point-2, formatProperty: prop-0, processDts: true}\n' + + ' - {entryPoint: entry-point-3, formatProperty: prop-1, processDts: false} (3): \n' + + ' - {entryPoint: entry-point-0, formatProperty: prop-0, processDts: true}\n' + + ' - {entryPoint: entry-point-1, formatProperty: prop-0, processDts: true}\n' + + ' - {entryPoint: entry-point-2, formatProperty: prop-0, processDts: true}'); + + expect(processNextTask(queue)).toBe(tasks[0]); // Process #0.0. + expect(processNextTask(queue)).toBe(tasks[2]); // Process #1.0. + expect(queue.toString()) + .toContain( + ' Blocked tasks (2): \n' + + // Tasks #3.0 and #3.1 are blocked by #2.0. + ' - {entryPoint: entry-point-3, formatProperty: prop-0, processDts: true} (1): \n' + + ' - {entryPoint: entry-point-2, formatProperty: prop-0, processDts: true}\n' + + ' - {entryPoint: entry-point-3, formatProperty: prop-1, processDts: false} (1): \n' + + ' - {entryPoint: entry-point-2, formatProperty: prop-0, processDts: true}'); + + expect(processNextTask(queue)).toBe(tasks[4]); // Process #2.0. + expect(queue.toString()).toContain(' Blocked tasks (0): '); + }); + + it('should display all info together', () => { + // An initially empty queue. + const {queue: queue1} = createQueue(0); + expect(queue1.toString()) + .toBe( + 'ParallelTaskQueue\n' + + ' All tasks completed: true\n' + + ' Unprocessed tasks (0): \n' + + ' In-progress tasks (0): \n' + + ' Blocked tasks (0): '); + + // A queue with three tasks (and one interdependency). + const {tasks: tasks2, queue: queue2} = createQueue(3, 1, {2: [1]}); + expect(queue2.toString()) + .toBe( + 'ParallelTaskQueue\n' + + ' All tasks completed: false\n' + + ' Unprocessed tasks (3): \n' + + ' - {entryPoint: entry-point-1, formatProperty: prop-0, processDts: true}\n' + + ' - {entryPoint: entry-point-0, formatProperty: prop-0, processDts: true}\n' + + ' - {entryPoint: entry-point-2, formatProperty: prop-0, processDts: true}\n' + + ' In-progress tasks (0): \n' + + ' Blocked tasks (1): \n' + + ' - {entryPoint: entry-point-2, formatProperty: prop-0, processDts: true} (1): \n' + + ' - {entryPoint: entry-point-1, formatProperty: prop-0, processDts: true}'); + + // Start processing tasks #1 and #0 (#2 is still blocked on #1). + expect(queue2.getNextTask()).toBe(tasks2[1]); + expect(queue2.getNextTask()).toBe(tasks2[0]); + expect(queue2.toString()) + .toBe( + 'ParallelTaskQueue\n' + + ' All tasks completed: false\n' + + ' Unprocessed tasks (1): \n' + + ' - {entryPoint: entry-point-2, formatProperty: prop-0, processDts: true}\n' + + ' In-progress tasks (2): \n' + + ' - {entryPoint: entry-point-1, formatProperty: prop-0, processDts: true}\n' + + ' - {entryPoint: entry-point-0, formatProperty: prop-0, processDts: true}\n' + + ' Blocked tasks (1): \n' + + ' - {entryPoint: entry-point-2, formatProperty: prop-0, processDts: true} (1): \n' + + ' - {entryPoint: entry-point-1, formatProperty: prop-0, processDts: true}'); + + // Complete task #1 nd start processing #2 (which is not unblocked). + queue2.markTaskCompleted(tasks2[1]); + expect(queue2.getNextTask()).toBe(tasks2[2]); + expect(queue2.toString()) + .toBe( + 'ParallelTaskQueue\n' + + ' All tasks completed: false\n' + + ' Unprocessed tasks (0): \n' + + ' In-progress tasks (2): \n' + + ' - {entryPoint: entry-point-0, formatProperty: prop-0, processDts: true}\n' + + ' - {entryPoint: entry-point-2, formatProperty: prop-0, processDts: true}\n' + + ' Blocked tasks (0): '); + + // Complete tasks #2 and #0. All tasks are now completed. + queue2.markTaskCompleted(tasks2[2]); + queue2.markTaskCompleted(tasks2[0]); + expect(queue2.toString()) + .toBe( + 'ParallelTaskQueue\n' + + ' All tasks completed: true\n' + + ' Unprocessed tasks (0): \n' + + ' In-progress tasks (0): \n' + + ' Blocked tasks (0): '); + }); + }); +});