1/* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import {Component, Input} from '@angular/core'; 18import {TimelineUtils} from 'app/components/timeline/timeline_utils'; 19import {assertDefined, assertTrue} from 'common/assert_utils'; 20import {Point} from 'common/geometry/point'; 21import {Rect} from 'common/geometry/rect'; 22import {TimeRange, Timestamp} from 'common/time'; 23import {AbsoluteEntryIndex, Trace, TraceEntry} from 'trace/trace'; 24import {TraceType} from 'trace/trace_type'; 25import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; 26import {AbstractTimelineRowComponent} from './abstract_timeline_row_component'; 27 28@Component({ 29 selector: 'transition-timeline', 30 template: ` 31 <div 32 class="transition-timeline" 33 matTooltip="Some or all transitions will not be rendered in timeline due to unknown dispatch and finish or abort time" 34 [matTooltipDisabled]="shouldNotRenderEntries.length === 0" 35 [style.background-color]="getBackgroundColor()" 36 (click)="onTimelineClick($event)" 37 #wrapper> 38 <canvas 39 id="canvas" 40 (mousemove)="trackMousePos($event)" 41 (mouseleave)="onMouseLeave($event)" #canvas></canvas> 42 </div> 43 `, 44 styles: [ 45 ` 46 .transition-timeline { 47 height: 4rem; 48 } 49 .transition-timeline:hover { 50 background-color: var(--hover-element-color); 51 cursor: pointer; 52 } 53 `, 54 ], 55}) 56export class TransitionTimelineComponent extends AbstractTimelineRowComponent<PropertyTreeNode> { 57 @Input() selectedEntry: TraceEntry<PropertyTreeNode> | undefined; 58 @Input() trace: Trace<PropertyTreeNode> | undefined; 59 @Input() transitionEntries: Array<PropertyTreeNode | undefined> | undefined; 60 @Input() fullRange: TimeRange | undefined; 61 62 hoveringEntry?: TraceEntry<PropertyTreeNode>; 63 rowsToUse = new Map<number, number>(); 64 maxRowsRequires = 0; 65 shouldNotRenderEntries: number[] = []; 66 67 ngOnInit() { 68 assertDefined(this.trace); 69 assertTrue(this.trace?.type === TraceType.TRANSITION); 70 assertDefined(this.selectionRange); 71 assertDefined(this.transitionEntries); 72 assertDefined(this.fullRange); 73 this.computeRowsToUse(); 74 } 75 76 getAvailableWidth() { 77 return this.canvasDrawer.getScaledCanvasWidth(); 78 } 79 80 override onHover(mousePoint: Point) { 81 this.drawSegmentHover(mousePoint); 82 } 83 84 override handleMouseOut(e: MouseEvent) { 85 if (this.hoveringEntry) { 86 // If undefined there is no current hover effect so no need to clear 87 this.redraw(); 88 } 89 this.hoveringEntry = undefined; 90 } 91 92 override drawTimeline() { 93 let selectedRect: Rect | undefined; 94 assertDefined(this.trace).forEachEntry((entry) => { 95 const index = entry.getIndex(); 96 const rect = this.getRectFromIndex(entry.getIndex()); 97 if (!rect) { 98 return; 99 } 100 const transition = assertDefined(this.transitionEntries?.at(index)); 101 this.drawSegment(rect, transition); 102 if (index === this.selectedEntry?.getIndex()) { 103 selectedRect = rect; 104 } 105 }); 106 if (selectedRect) { 107 this.canvasDrawer.drawRectBorder(selectedRect); 108 } 109 } 110 111 private getRectFromIndex(entryIndex: AbsoluteEntryIndex): Rect | undefined { 112 if (this.shouldNotRenderEntries.includes(entryIndex)) { 113 return undefined; 114 } 115 const transition = this.transitionEntries?.at(entryIndex); 116 if (!transition) { 117 return undefined; 118 } 119 const timeRange = TimelineUtils.getTimeRangeForTransition( 120 transition, 121 assertDefined(this.selectionRange), 122 assertDefined(this.timestampConverter), 123 ); 124 if (!timeRange) { 125 return undefined; 126 } 127 const row = this.getRowToUseFor(entryIndex); 128 return this.getSegmentRect(timeRange.from, timeRange.to, row); 129 } 130 131 protected override getEntryAt( 132 mousePoint: Point, 133 ): TraceEntry<PropertyTreeNode> | undefined { 134 const transitionEntries = assertDefined(this.trace).mapEntry( 135 (entry) => entry, 136 ); 137 138 for (const entry of transitionEntries) { 139 const rect = this.getRectFromIndex(entry.getIndex()); 140 if (rect?.containsPoint(mousePoint)) { 141 return entry; 142 } 143 } 144 return undefined; 145 } 146 147 private drawSegmentHover(mousePoint: Point) { 148 const currentHoverEntry = this.getEntryAt(mousePoint); 149 150 if (this.hoveringEntry) { 151 this.redraw(); 152 } 153 154 this.hoveringEntry = currentHoverEntry; 155 156 if (!this.hoveringEntry) { 157 return; 158 } 159 160 const rect = this.getRectFromIndex(this.hoveringEntry.getIndex()); 161 if (rect) { 162 this.canvasDrawer.drawRectBorder(rect); 163 } 164 } 165 166 private getXPosOf(entry: Timestamp): number { 167 const start = assertDefined(this.selectionRange).from.getValueNs(); 168 const end = assertDefined(this.selectionRange).to.getValueNs(); 169 170 return Number( 171 (BigInt(this.getAvailableWidth()) * (entry.getValueNs() - start)) / 172 (end - start), 173 ); 174 } 175 176 private getSegmentRect( 177 start: Timestamp, 178 end: Timestamp, 179 rowToUse: number, 180 ): Rect { 181 const xPosStart = this.getXPosOf(start); 182 const selectionStart = assertDefined(this.selectionRange).from.getValueNs(); 183 const selectionEnd = assertDefined(this.selectionRange).to.getValueNs(); 184 185 const borderPadding = 5; 186 let totalRowHeight = 187 (this.canvasDrawer.getScaledCanvasHeight() - 2 * borderPadding) / 188 this.maxRowsRequires; 189 if (totalRowHeight < 10) { 190 totalRowHeight = 10; 191 } 192 if (this.maxRowsRequires === 1) { 193 totalRowHeight = 30; 194 } 195 196 const padding = 5; 197 const rowHeight = totalRowHeight - padding; 198 199 const width = Math.max( 200 Number( 201 (BigInt(this.getAvailableWidth()) * 202 (end.getValueNs() - start.getValueNs())) / 203 (selectionEnd - selectionStart), 204 ), 205 rowHeight, 206 ); 207 208 return new Rect( 209 xPosStart, 210 borderPadding + rowToUse * totalRowHeight, 211 width, 212 rowHeight, 213 ); 214 } 215 216 private drawSegment(rect: Rect, transition: PropertyTreeNode) { 217 const aborted = assertDefined( 218 transition.getChildByName('aborted'), 219 ).getValue(); 220 const alpha = aborted ? 0.25 : 1.0; 221 222 const hasUnknownStart = 223 TimelineUtils.isTransitionWithUnknownStart(transition); 224 const hasUnknownEnd = TimelineUtils.isTransitionWithUnknownEnd(transition); 225 this.canvasDrawer.drawRect( 226 rect, 227 this.color, 228 alpha, 229 hasUnknownStart, 230 hasUnknownEnd, 231 ); 232 } 233 234 private getRowToUseFor(entryIndex: AbsoluteEntryIndex): number { 235 const rowToUse = this.rowsToUse.get(entryIndex); 236 if (rowToUse === undefined) { 237 throw new Error(`Could not find entry ${entryIndex} in rowsToUse`); 238 } 239 return rowToUse; 240 } 241 242 private computeRowsToUse(): void { 243 const rowAvailableFrom: Array<bigint | undefined> = []; 244 assertDefined(this.trace).forEachEntry((entry) => { 245 const index = entry.getIndex(); 246 const transition = this.transitionEntries?.at(entry.getIndex()); 247 if (!transition) { 248 return; 249 } 250 251 const timeRange = TimelineUtils.getTimeRangeForTransition( 252 transition, 253 assertDefined(this.fullRange), 254 assertDefined(this.timestampConverter), 255 ); 256 257 if (timeRange === undefined) { 258 this.shouldNotRenderEntries.push(index); 259 return; 260 } 261 262 let rowToUse = 0; 263 while ((rowAvailableFrom[rowToUse] ?? 0n) > timeRange.from.getValueNs()) { 264 rowToUse++; 265 } 266 267 rowAvailableFrom[rowToUse] = timeRange.to.getValueNs(); 268 269 if (rowToUse + 1 > this.maxRowsRequires) { 270 this.maxRowsRequires = rowToUse + 1; 271 } 272 this.rowsToUse.set(index, rowToUse); 273 }); 274 } 275} 276