blob: 8fef8a05da57b351af5be290deeaab69fe4665b3 [file] [log] [blame]
// Copyright (c) 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:cocoon_service/protos.dart' show CommitStatus, Stage, Task;
/// Simplify [List<CommitStatus>] for easy retrieval of tasks and ordering of columns.
///
/// Construction performs the following steps:
/// 1. Scan through every [Task] in [List<CommitStatus>] to find all unique
/// columns in the matrix. Uniqueness is defined by the key "[Task.stageName]:[Task.name]".
/// 2. Construct an empty [List<Column>] based on the size given from (1).
/// 3. Scan through [List<CommitStatus>] again, but insert every [Task] into
/// their associated [Column]. Insert the task based on the row it appears in
/// [List<CommitStatus>], which is just the index in the list it appears in.
/// For example, a task from the 3rd row will be inserted into the 3rd row of [Column.tasks].
/// A. If a [Column.sampleTask] is null, set it to the first [Task] found
/// for that [Column].
///
/// The columns of the matrix can be sorted by passing a custom comparator function.
/// Note that the runtime of this sort is mostly the runtime of the comparator function
/// performed O(nlogn) times.
///
/// On addition of a new [CommitStatus], or modification of an existing [CommitStatus],
/// this needs to be rebuilt.
///
/// An internal column map is maintained to allow easy reordering of [List<Column>].
/// Additional operations after construction only modify this column map.
class TaskMatrix {
TaskMatrix({@required this.statuses}) {
_columnKeyIndex = createColumnKeyIndex();
// There is no fancy mapping at the start, so each index just points to itself.
_columnMap = List<int>.generate(_columnKeyIndex.length, (int i) => i);
_matrix = createTaskMatrix(statuses);
}
final List<CommitStatus> statuses;
List<Column> _matrix;
/// A map for taking in the corresponding column in [StatusGrid] and
/// where it maps to in this matrix.
List<int> _columnMap;
/// A key, value table to find what grid column a [Task] is in.
///
/// This is necessary to ensure every possible task has a column in the grid.
Map<String, int> _columnKeyIndex;
int get columns => _matrix.length;
int get rows => statuses.length;
/// Return [Task] for a cell in [StatusGrid].
Task task(int gridRow, int gridCol) {
final int mapCol = _columnMap[gridCol];
return _matrix[mapCol].tasks[gridRow];
}
/// Return a sample task from a column.
///
/// This is used for the [TaskIcon] widget row.
Task sampleTask(int gridCol) {
final int mapCol = _columnMap[gridCol];
return _matrix[mapCol].sampleTask;
}
/// Sort the columns of this matrix based on [compare].
///
/// This sort function does not change any of the underlying matrix.
/// Instead, it remaps the column map to point to the correct order.
///
/// This cannot sort the rows of the matrix.
void sort(int compare(Column a, Column b)) {
_columnMap.sort((int indexA, int indexB) {
return compare(_matrix[indexA], _matrix[indexB]);
});
}
/// A unique index for grouping [Task] from separate [CommitStatus].
///
/// [task.stageName] and [task.name] are not unique. However, they
/// are unique when combined together.
@visibleForTesting
String taskColumnKey(Task task) {
return '${task.stageName}:${task.name}';
}
/// A map of all [taskColumnKey] to a unique index from [List<CommitStatus>].
///
/// Scans through all [Task] in [List<CommitStatus>] to find all unique [taskColumnKey].
/// This ensures a column is allocated in [createTaskMatrix].
///
/// When a new [taskColumnKey] is found, it is inserted and the index is incremeted.
@visibleForTesting
Map<String, int> createColumnKeyIndex({List<CommitStatus> statuses}) {
statuses ??= this.statuses;
final Map<String, int> taskColumnKeyIndex = <String, int>{};
int currentIndex = 0;
/// O(Tasks * CommitStatuses).
/// In production, this is usually O(85 * 100) ~ 8500 operations.
for (CommitStatus status in statuses) {
for (Stage stage in status.stages) {
for (Task task in stage.tasks) {
final String key = taskColumnKey(task);
if (taskColumnKeyIndex.containsKey(key)) {
continue;
}
taskColumnKeyIndex[key] = currentIndex++;
}
}
}
return taskColumnKeyIndex;
}
/// Create a matrix of [Task] by grouping related tasks into a [Column].
///
/// If no [Task] can be placed in a cell of the matrix, it will be left null.
@visibleForTesting
List<Column> createTaskMatrix(List<CommitStatus> statuses,
{Map<String, int> columnKeyIndex}) {
columnKeyIndex ??= _columnKeyIndex;
final List<Column> matrix = List<Column>.generate(
columnKeyIndex.keys.length,
(int i) => Column(statuses.length, columnKeyIndex.keys.elementAt(i)));
for (int row = 0; row < statuses.length; row++) {
final CommitStatus status = statuses[row];
/// Organize [Task] in [CommitStatus] to the [Column] they map to.
for (Stage stage in status.stages) {
for (Task task in stage.tasks) {
final String columnKey = taskColumnKey(task);
final int columnIndex = columnKeyIndex[columnKey];
final Column column = matrix[columnIndex];
final List<Task> tasks = column.tasks;
tasks[row] = task;
column.sampleTask ??= task;
}
}
}
return matrix;
}
}
/// Helper class to group [Task] that have the same [taskColumnKey] as those tasks are the same
/// except for what [Commit] they were run on.
class Column {
Column(int size, this.key) : tasks = List<Task>(size);
/// The [taskColumnKey] that all [Task] in [List<Task>] have.
final String key;
/// The rows of [Task].
///
/// There is guranteed to be one non-null entry.
List<Task> tasks;
/// The most recent [Task] that is not null.
///
/// Useful for show information about a [Column] that is true in
/// the tasks of [List<Task>]. For example, when showing the
/// [TaskIcon] row in [StatusGrid].
Task sampleTask;
}