/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import {Component, Input} from '@angular/core'; import {TimelineUtils} from 'app/components/timeline/timeline_utils'; import {assertDefined} from 'common/assert_utils'; import {Point} from 'common/geometry_types'; import {Rect} from 'common/rect'; import {Timestamp} from 'common/time'; import {Trace, TraceEntry} from 'trace/trace'; import {TraceType} from 'trace/trace_type'; import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; import {AbstractTimelineRowComponent} from './abstract_timeline_row_component'; @Component({ selector: 'transition-timeline', template: `
`, styles: [ ` .transition-timeline { height: 4rem; } .transition-timeline:hover { background-color: var(--hover-element-color); cursor: pointer; } `, ], }) export class TransitionTimelineComponent extends AbstractTimelineRowComponent { @Input() selectedEntry: TraceEntry | undefined; @Input() trace: Trace | undefined; @Input() traceEntries: PropertyTreeNode[] | undefined; hoveringEntry?: TraceEntry; rowsToUse = new Map(); maxRowsRequires = 0; shouldNotRenderEntries: number[] = []; ngOnInit() { assertDefined(this.trace); assertDefined(this.selectionRange); assertDefined(this.traceEntries); this.processTraceEntries(); } getAvailableWidth() { return this.canvasDrawer.getScaledCanvasWidth(); } override onHover(mousePoint: Point) { this.drawSegmentHover(mousePoint); } override handleMouseOut(e: MouseEvent) { if (this.hoveringEntry) { // If undefined there is no current hover effect so no need to clear this.redraw(); } this.hoveringEntry = undefined; } override drawTimeline() { (this.trace as Trace).mapEntry((entry) => { const transition = this.traceEntries?.at(entry.getIndex()); if (!transition) { return; } const timeRange = TimelineUtils.getTimeRangeForTransition( transition, assertDefined(this.selectionRange), assertDefined(this.timestampConverter), ); if (!timeRange) { return; } const rowToUse = this.getRowToUseFor(entry); const aborted = assertDefined( transition.getChildByName('aborted'), ).getValue(); this.drawSegment(timeRange.from, timeRange.to, rowToUse, aborted); }); this.drawSelectedTransitionEntry(); } protected override getEntryAt( mousePoint: Point, ): TraceEntry | undefined { if (assertDefined(this.trace).type !== TraceType.TRANSITION) { return undefined; } const transitions = assertDefined(this.trace).mapEntry((entry) => { const transition = this.traceEntries?.at(entry.getIndex()); if (!transition) { return; } const timeRange = TimelineUtils.getTimeRangeForTransition( transition, assertDefined(this.selectionRange), assertDefined(this.timestampConverter), ); if (!timeRange) { return undefined; } const rowToUse = this.getRowToUseFor(entry); const rect = this.getSegmentRect(timeRange.from, timeRange.to, rowToUse); if (rect.containsPoint(mousePoint)) { return entry; } return undefined; }); return transitions.find((entry) => entry !== undefined); } private drawSegmentHover(mousePoint: Point) { const currentHoverEntry = this.getEntryAt(mousePoint); if (this.hoveringEntry) { this.redraw(); } this.hoveringEntry = currentHoverEntry; if (!this.hoveringEntry) { return; } const transition = this.traceEntries?.at(this.hoveringEntry.getIndex()); if (!transition) { return; } const timeRange = TimelineUtils.getTimeRangeForTransition( transition, assertDefined(this.selectionRange), assertDefined(this.timestampConverter), ); if (!timeRange) { return; } const rowToUse = this.getRowToUseFor(this.hoveringEntry); const rect = this.getSegmentRect(timeRange.from, timeRange.to, rowToUse); this.canvasDrawer.drawRectBorder(rect); } private getXPosOf(entry: Timestamp): number { const start = assertDefined(this.selectionRange).from.getValueNs(); const end = assertDefined(this.selectionRange).to.getValueNs(); return Number( (BigInt(this.getAvailableWidth()) * (entry.getValueNs() - start)) / (end - start), ); } private getSegmentRect( start: Timestamp, end: Timestamp, rowToUse: number, ): Rect { const xPosStart = this.getXPosOf(start); const selectionStart = assertDefined(this.selectionRange).from.getValueNs(); const selectionEnd = assertDefined(this.selectionRange).to.getValueNs(); const width = Number( (BigInt(this.getAvailableWidth()) * (end.getValueNs() - start.getValueNs())) / (selectionEnd - selectionStart), ); const borderPadding = 5; let totalRowHeight = (this.canvasDrawer.getScaledCanvasHeight() - 2 * borderPadding) / this.maxRowsRequires; if (totalRowHeight < 10) { totalRowHeight = 10; } if (this.maxRowsRequires === 1) { totalRowHeight = 30; } const padding = 5; const rowHeight = totalRowHeight - padding; return new Rect( xPosStart, borderPadding + rowToUse * totalRowHeight, width, rowHeight, ); } private drawSegment( start: Timestamp, end: Timestamp, rowToUse: number, aborted: boolean, ) { const rect = this.getSegmentRect(start, end, rowToUse); const alpha = aborted ? 0.25 : 1.0; this.canvasDrawer.drawRect(rect, this.color, alpha); } private drawSelectedTransitionEntry() { if (this.selectedEntry === undefined) { return; } const transition = this.traceEntries?.at(this.selectedEntry.getIndex()); if (!transition) { return; } const timeRange = TimelineUtils.getTimeRangeForTransition( transition, assertDefined(this.selectionRange), assertDefined(this.timestampConverter), ); if (!timeRange) { return; } const rowIndex = this.getRowToUseFor(this.selectedEntry); const rect = this.getSegmentRect(timeRange.from, timeRange.to, rowIndex); const alpha = transition.getChildByName('aborted') ? 0.25 : 1.0; this.canvasDrawer.drawRect(rect, this.color, alpha); this.canvasDrawer.drawRectBorder(rect); } private getRowToUseFor(entry: TraceEntry): number { const rowToUse = this.rowsToUse.get(entry.getIndex()); if (rowToUse === undefined) { console.error('Failed to find', entry, 'in', this.rowsToUse); throw new Error('Could not find entry in rowsToUse'); } return rowToUse; } private processTraceEntries(): void { const rowAvailableFrom: Array = []; assertDefined(this.trace).mapEntry((entry) => { const index = entry.getIndex(); const transition = this.traceEntries?.at(entry.getIndex()); if (!transition) { return; } const timeRange = TimelineUtils.getTimeRangeForTransition( transition, assertDefined(this.selectionRange), assertDefined(this.timestampConverter), ); if (!timeRange) { this.shouldNotRenderEntries.push(index); } let rowToUse = 0; while ( (rowAvailableFrom[rowToUse] ?? 0n) > (timeRange?.from.getValueNs() ?? assertDefined(this.selectionRange).from.getValueNs()) ) { rowToUse++; } rowAvailableFrom[rowToUse] = timeRange?.to.getValueNs() ?? assertDefined(this.selectionRange).to.getValueNs(); if (rowToUse + 1 > this.maxRowsRequires) { this.maxRowsRequires = rowToUse + 1; } this.rowsToUse.set(index, rowToUse); }); } }