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} from 'common/assert_utils'; 20import {Point} from 'common/geometry_types'; 21import {Rect} from 'common/rect'; 22import {Timestamp} from 'common/time'; 23import {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 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() traceEntries: PropertyTreeNode[] | undefined; 60 61 hoveringEntry?: TraceEntry<PropertyTreeNode>; 62 rowsToUse = new Map<number, number>(); 63 maxRowsRequires = 0; 64 shouldNotRenderEntries: number[] = []; 65 66 ngOnInit() { 67 assertDefined(this.trace); 68 assertDefined(this.selectionRange); 69 assertDefined(this.traceEntries); 70 this.processTraceEntries(); 71 } 72 73 getAvailableWidth() { 74 return this.canvasDrawer.getScaledCanvasWidth(); 75 } 76 77 override onHover(mousePoint: Point) { 78 this.drawSegmentHover(mousePoint); 79 } 80 81 override handleMouseOut(e: MouseEvent) { 82 if (this.hoveringEntry) { 83 // If undefined there is no current hover effect so no need to clear 84 this.redraw(); 85 } 86 this.hoveringEntry = undefined; 87 } 88 89 override drawTimeline() { 90 (this.trace as Trace<PropertyTreeNode>).mapEntry((entry) => { 91 const transition = this.traceEntries?.at(entry.getIndex()); 92 if (!transition) { 93 return; 94 } 95 const timeRange = TimelineUtils.getTimeRangeForTransition( 96 transition, 97 assertDefined(this.selectionRange), 98 assertDefined(this.timestampConverter), 99 ); 100 if (!timeRange) { 101 return; 102 } 103 const rowToUse = this.getRowToUseFor(entry); 104 const aborted = assertDefined( 105 transition.getChildByName('aborted'), 106 ).getValue(); 107 this.drawSegment(timeRange.from, timeRange.to, rowToUse, aborted); 108 }); 109 this.drawSelectedTransitionEntry(); 110 } 111 112 protected override getEntryAt( 113 mousePoint: Point, 114 ): TraceEntry<PropertyTreeNode> | undefined { 115 if (assertDefined(this.trace).type !== TraceType.TRANSITION) { 116 return undefined; 117 } 118 119 const transitions = assertDefined(this.trace).mapEntry((entry) => { 120 const transition = this.traceEntries?.at(entry.getIndex()); 121 if (!transition) { 122 return; 123 } 124 const timeRange = TimelineUtils.getTimeRangeForTransition( 125 transition, 126 assertDefined(this.selectionRange), 127 assertDefined(this.timestampConverter), 128 ); 129 130 if (!timeRange) { 131 return undefined; 132 } 133 const rowToUse = this.getRowToUseFor(entry); 134 const rect = this.getSegmentRect(timeRange.from, timeRange.to, rowToUse); 135 if (rect.containsPoint(mousePoint)) { 136 return entry; 137 } 138 return undefined; 139 }); 140 141 return transitions.find((entry) => entry !== undefined); 142 } 143 144 private drawSegmentHover(mousePoint: Point) { 145 const currentHoverEntry = this.getEntryAt(mousePoint); 146 147 if (this.hoveringEntry) { 148 this.redraw(); 149 } 150 151 this.hoveringEntry = currentHoverEntry; 152 153 if (!this.hoveringEntry) { 154 return; 155 } 156 157 const transition = this.traceEntries?.at(this.hoveringEntry.getIndex()); 158 if (!transition) { 159 return; 160 } 161 const timeRange = TimelineUtils.getTimeRangeForTransition( 162 transition, 163 assertDefined(this.selectionRange), 164 assertDefined(this.timestampConverter), 165 ); 166 167 if (!timeRange) { 168 return; 169 } 170 171 const rowToUse = this.getRowToUseFor(this.hoveringEntry); 172 const rect = this.getSegmentRect(timeRange.from, timeRange.to, rowToUse); 173 this.canvasDrawer.drawRectBorder(rect); 174 } 175 176 private getXPosOf(entry: Timestamp): number { 177 const start = assertDefined(this.selectionRange).from.getValueNs(); 178 const end = assertDefined(this.selectionRange).to.getValueNs(); 179 180 return Number( 181 (BigInt(this.getAvailableWidth()) * (entry.getValueNs() - start)) / 182 (end - start), 183 ); 184 } 185 186 private getSegmentRect( 187 start: Timestamp, 188 end: Timestamp, 189 rowToUse: number, 190 ): Rect { 191 const xPosStart = this.getXPosOf(start); 192 const selectionStart = assertDefined(this.selectionRange).from.getValueNs(); 193 const selectionEnd = assertDefined(this.selectionRange).to.getValueNs(); 194 195 const width = Number( 196 (BigInt(this.getAvailableWidth()) * 197 (end.getValueNs() - start.getValueNs())) / 198 (selectionEnd - selectionStart), 199 ); 200 201 const borderPadding = 5; 202 let totalRowHeight = 203 (this.canvasDrawer.getScaledCanvasHeight() - 2 * borderPadding) / 204 this.maxRowsRequires; 205 if (totalRowHeight < 10) { 206 totalRowHeight = 10; 207 } 208 if (this.maxRowsRequires === 1) { 209 totalRowHeight = 30; 210 } 211 212 const padding = 5; 213 const rowHeight = totalRowHeight - padding; 214 215 return new Rect( 216 xPosStart, 217 borderPadding + rowToUse * totalRowHeight, 218 width, 219 rowHeight, 220 ); 221 } 222 223 private drawSegment( 224 start: Timestamp, 225 end: Timestamp, 226 rowToUse: number, 227 aborted: boolean, 228 ) { 229 const rect = this.getSegmentRect(start, end, rowToUse); 230 const alpha = aborted ? 0.25 : 1.0; 231 this.canvasDrawer.drawRect(rect, this.color, alpha); 232 } 233 234 private drawSelectedTransitionEntry() { 235 if (this.selectedEntry === undefined) { 236 return; 237 } 238 239 const transition = this.traceEntries?.at(this.selectedEntry.getIndex()); 240 if (!transition) { 241 return; 242 } 243 const timeRange = TimelineUtils.getTimeRangeForTransition( 244 transition, 245 assertDefined(this.selectionRange), 246 assertDefined(this.timestampConverter), 247 ); 248 if (!timeRange) { 249 return; 250 } 251 252 const rowIndex = this.getRowToUseFor(this.selectedEntry); 253 const rect = this.getSegmentRect(timeRange.from, timeRange.to, rowIndex); 254 const alpha = transition.getChildByName('aborted') ? 0.25 : 1.0; 255 this.canvasDrawer.drawRect(rect, this.color, alpha); 256 this.canvasDrawer.drawRectBorder(rect); 257 } 258 259 private getRowToUseFor(entry: TraceEntry<PropertyTreeNode>): number { 260 const rowToUse = this.rowsToUse.get(entry.getIndex()); 261 if (rowToUse === undefined) { 262 console.error('Failed to find', entry, 'in', this.rowsToUse); 263 throw new Error('Could not find entry in rowsToUse'); 264 } 265 return rowToUse; 266 } 267 268 private processTraceEntries(): void { 269 const rowAvailableFrom: Array<bigint | undefined> = []; 270 assertDefined(this.trace).mapEntry((entry) => { 271 const index = entry.getIndex(); 272 const transition = this.traceEntries?.at(entry.getIndex()); 273 if (!transition) { 274 return; 275 } 276 277 const timeRange = TimelineUtils.getTimeRangeForTransition( 278 transition, 279 assertDefined(this.selectionRange), 280 assertDefined(this.timestampConverter), 281 ); 282 283 if (!timeRange) { 284 this.shouldNotRenderEntries.push(index); 285 } 286 287 let rowToUse = 0; 288 while ( 289 (rowAvailableFrom[rowToUse] ?? 0n) > 290 (timeRange?.from.getValueNs() ?? 291 assertDefined(this.selectionRange).from.getValueNs()) 292 ) { 293 rowToUse++; 294 } 295 296 rowAvailableFrom[rowToUse] = 297 timeRange?.to.getValueNs() ?? 298 assertDefined(this.selectionRange).to.getValueNs(); 299 300 if (rowToUse + 1 > this.maxRowsRequires) { 301 this.maxRowsRequires = rowToUse + 1; 302 } 303 this.rowsToUse.set(index, rowToUse); 304 }); 305 } 306} 307