| /** |
| * @module modules/job-timeline-sk |
| * @description <h2><code>job-timeline-sk</code></h2> |
| * |
| * Displays a timeline of the tasks which comprise a given job, to easily |
| * determine where time is being spent. |
| */ |
| import { define } from '../../../elements-sk/modules/define'; |
| import { Job, Task } from '../rpc'; |
| import { draw, Block, Lane, Data } from '../gantt-chart-sk'; |
| import { |
| GetTaskSchedulerService, |
| GetJobResponse, |
| TaskSummaries, |
| TaskSummary, |
| GetTaskResponse, |
| } from '../rpc'; |
| |
| function ts(tsStr: string) { |
| // If the timestamp is zero-ish, return the current datetime. |
| if (Date.parse(tsStr) <= 0) { |
| return new Date(Date.now()); // Use Date.now so that it can be mocked. |
| } |
| return new Date(tsStr); |
| } |
| |
| export class JobTimelineSk extends HTMLElement { |
| connectedCallback() { |
| if (this.hasAttribute('job-id')) { |
| const rpc = GetTaskSchedulerService(this); |
| rpc |
| .getJob({ |
| id: this.getAttribute('job-id')!, |
| }) |
| .then((jobResp: GetJobResponse) => { |
| const taskIds = jobResp |
| .job!.tasks!.map((tasks: TaskSummaries) => tasks.tasks!) |
| .reduce((acc: TaskSummary[], subArray: TaskSummary[]) => |
| acc.concat(subArray) |
| ) |
| .map((task: TaskSummary) => task.id); |
| Promise.all( |
| taskIds.map((id: string) => |
| rpc.getTask({ |
| id: id, |
| includeStats: true, |
| }) |
| ) |
| ).then((taskResps: GetTaskResponse[]) => { |
| this.draw( |
| jobResp.job!, |
| taskResps.map((resp: GetTaskResponse) => resp.task!), |
| [] |
| ); |
| }); |
| }); |
| } |
| } |
| |
| draw(job: Job, tasks: Task[], epochs: string[]) { |
| const lanes: Map<string, Lane> = new Map(); |
| for (const t of tasks) { |
| // Create the lane if it doesn't already exist. |
| let lane = lanes.get(t.taskKey!.name); |
| if (!lane) { |
| lane = { |
| label: t.taskKey!.name, |
| blocks: [], |
| }; |
| lanes.set(t.taskKey!.name, lane); |
| } |
| |
| // Creation timestamp may be after start and finish timestamps in the |
| // case of deduplicated tasks. Since we care more about the |
| // contribution to the job than the task itself, set the start and |
| // finish timestamps equal to the creation timestamp in this case. |
| const createTs = ts(t.createdAt!); |
| if (t.startedAt && ts(t.startedAt!).getTime() < createTs.getTime()) { |
| t.startedAt = t.createdAt; |
| } |
| if (t.finishedAt && ts(t.finishedAt!).getTime() < createTs.getTime()) { |
| t.finishedAt = t.createdAt; |
| } |
| |
| // Create blocks. If we have performance data from Swarming, each task |
| // becomes multiple blocks, indicating the download and upload overhead |
| // as well as the run time itself. |
| let lastBlockEnd = createTs; |
| const makeBlock = function (label: string, end: string, color: string) { |
| const block: Block = { |
| label: label, |
| start: lastBlockEnd, |
| end: ts(end), |
| color: color, |
| }; |
| lastBlockEnd = block.end; |
| lane!.blocks.push(block); |
| }; |
| if (t.startedAt) { |
| makeBlock('pending', t.startedAt, '#e69f00'); |
| } |
| if (t.stats) { |
| const startTs = ts(t.startedAt!).getTime(); |
| const finishTs = ts(t.finishedAt!).getTime(); |
| makeBlock( |
| 'overhead', |
| new Date( |
| startTs + 1000 * parseFloat(t.stats.downloadOverheadS) |
| ).toString(), |
| '#d55e00' |
| ); |
| makeBlock( |
| 'running', |
| new Date( |
| finishTs - 1000 * parseFloat(t.stats.uploadOverheadS) |
| ).toString(), |
| '#0072b2' |
| ); |
| makeBlock('overhead', t.finishedAt!, '#d55e00'); |
| } else { |
| makeBlock('running', t.finishedAt || '', '#0072b2'); |
| } |
| } |
| |
| // Draw the chart. |
| const data: Data = { |
| lanes: Array.from(lanes.values()), |
| epochs: epochs.map((epoch) => ts(epoch)), |
| }; |
| if (job.requestedAt) { |
| data.start = ts(job.requestedAt); |
| } |
| if (job.finishedAt) { |
| data.end = ts(job.finishedAt); |
| } |
| draw(this, data); |
| } |
| } |
| |
| define('job-timeline-sk', JobTimelineSk); |