1/* 2 * Copyright (C) 2023 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 {DragDropModule} from '@angular/cdk/drag-drop'; 18import {ChangeDetectionStrategy} from '@angular/core'; 19import {ComponentFixture, TestBed} from '@angular/core/testing'; 20import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 21import {MatButtonModule} from '@angular/material/button'; 22import {MatFormFieldModule} from '@angular/material/form-field'; 23import {MatIconModule} from '@angular/material/icon'; 24import {MatInputModule} from '@angular/material/input'; 25import {MatSelectModule} from '@angular/material/select'; 26import {MatTooltipModule} from '@angular/material/tooltip'; 27import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 28import {assertDefined} from 'common/assert_utils'; 29import {Rect} from 'common/geometry/rect'; 30import {TimestampConverterUtils} from 'common/time/test_utils'; 31import {TimeRange, Timestamp} from 'common/time/time'; 32import {PropertyTreeBuilder} from 'test/unit/property_tree_builder'; 33import {TraceBuilder} from 'test/unit/trace_builder'; 34import {waitToBeCalled} from 'test/utils'; 35import {TraceType} from 'trace/trace_type'; 36import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; 37import {TransitionTimelineComponent} from './transition_timeline_component'; 38 39describe('TransitionTimelineComponent', () => { 40 let fixture: ComponentFixture<TransitionTimelineComponent>; 41 let component: TransitionTimelineComponent; 42 let htmlElement: HTMLElement; 43 44 const time0 = TimestampConverterUtils.makeRealTimestamp(0n); 45 const time5 = TimestampConverterUtils.makeRealTimestamp(5n); 46 const time10 = TimestampConverterUtils.makeRealTimestamp(10n); 47 const time20 = TimestampConverterUtils.makeRealTimestamp(20n); 48 const time30 = TimestampConverterUtils.makeRealTimestamp(30n); 49 const time35 = TimestampConverterUtils.makeRealTimestamp(35n); 50 const time60 = TimestampConverterUtils.makeRealTimestamp(60n); 51 const time85 = TimestampConverterUtils.makeRealTimestamp(85n); 52 const time110 = TimestampConverterUtils.makeRealTimestamp(110n); 53 const time120 = TimestampConverterUtils.makeRealTimestamp(120n); 54 const time160 = TimestampConverterUtils.makeRealTimestamp(160n); 55 56 const range10to110 = new TimeRange(time10, time110); 57 const range0to160 = new TimeRange(time0, time160); 58 59 beforeEach(async () => { 60 await TestBed.configureTestingModule({ 61 imports: [ 62 FormsModule, 63 MatButtonModule, 64 MatFormFieldModule, 65 MatInputModule, 66 MatIconModule, 67 MatSelectModule, 68 MatTooltipModule, 69 ReactiveFormsModule, 70 BrowserAnimationsModule, 71 DragDropModule, 72 ], 73 declarations: [TransitionTimelineComponent], 74 }) 75 .overrideComponent(TransitionTimelineComponent, { 76 set: {changeDetection: ChangeDetectionStrategy.Default}, 77 }) 78 .compileComponents(); 79 fixture = TestBed.createComponent(TransitionTimelineComponent); 80 component = fixture.componentInstance; 81 htmlElement = fixture.nativeElement; 82 component.timestampConverter = TimestampConverterUtils.TIMESTAMP_CONVERTER; 83 component.fullRange = range0to160; 84 }); 85 86 it('can be created', () => { 87 expect(component).toBeTruthy(); 88 }); 89 90 it('can draw non-overlapping transitions', async () => { 91 const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect'); 92 93 const transitions = [ 94 makeTransition(time10, time30), 95 makeTransition(time60, time110), 96 ]; 97 await setTraceAndSelectionRange(transitions, [time10, time60]); 98 99 const padding = 5; 100 const oneRowTotalHeight = 30; 101 const oneRowHeight = oneRowTotalHeight - padding; 102 const width = component.canvasDrawer.getScaledCanvasWidth(); 103 104 expect(drawRectSpy).toHaveBeenCalledTimes(2); 105 expect(drawRectSpy).toHaveBeenCalledWith( 106 new Rect(0, padding, Math.floor(width / 5), oneRowHeight), 107 component.color, 108 1, 109 false, 110 false, 111 ); 112 expect(drawRectSpy).toHaveBeenCalledWith( 113 new Rect( 114 Math.floor(width / 2), 115 padding, 116 Math.floor(width / 2), 117 oneRowHeight, 118 ), 119 component.color, 120 1, 121 false, 122 false, 123 ); 124 }); 125 126 it('can draw transitions zoomed in', async () => { 127 const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect'); 128 129 const transitions = [ 130 makeTransition(time10, time20), // drawn 131 makeTransition(time60, time160), // drawn 132 makeTransition(time120, time160), // not drawn - starts after selection range 133 makeTransition(time0, time5), // not drawn - finishes before selection range 134 makeTransition(time5, undefined), // not drawn - starts before selection range with unknown finish time 135 ]; 136 await setTraceAndSelectionRange(transitions, [ 137 time10, 138 time60, 139 time120, 140 time0, 141 time5, 142 ]); 143 144 const padding = 5; 145 const oneRowTotalHeight = 146 (component.canvasDrawer.getScaledCanvasHeight() - 2 * padding) / 3; 147 const oneRowHeight = oneRowTotalHeight - padding; 148 const width = component.canvasDrawer.getScaledCanvasWidth(); 149 150 expect(drawRectSpy).toHaveBeenCalledTimes(2); // does not draw final transition 151 expect(drawRectSpy).toHaveBeenCalledWith( 152 new Rect(0, padding, Math.floor(width / 10), oneRowHeight), 153 component.color, 154 1, 155 false, 156 false, 157 ); 158 expect(drawRectSpy).toHaveBeenCalledWith( 159 new Rect(Math.floor(width / 2), padding, Math.floor(width), oneRowHeight), 160 component.color, 161 1, 162 false, 163 false, 164 ); 165 }); 166 167 it('can draw selected entry', async () => { 168 const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect'); 169 const drawRectBorderSpy = spyOn(component.canvasDrawer, 'drawRectBorder'); 170 const waitPromises = [ 171 waitToBeCalled(drawRectSpy, 1), 172 waitToBeCalled(drawRectBorderSpy, 1), 173 ]; 174 await setDefaultTraceAndSelectionRange(true); 175 await Promise.all(waitPromises); 176 177 const expectedRect = getExpectedBorderedRect(); 178 expect(drawRectSpy).toHaveBeenCalledOnceWith( 179 expectedRect, 180 component.color, 181 1, 182 false, 183 false, 184 ); 185 expect(drawRectBorderSpy).toHaveBeenCalledTimes(1); 186 expect(drawRectBorderSpy).toHaveBeenCalledWith(expectedRect); 187 }); 188 189 it('can draw hovering entry', async () => { 190 const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect'); 191 await setDefaultTraceAndSelectionRange(); 192 const expectedRect = getExpectedBorderedRect(); 193 194 expect(drawRectSpy).toHaveBeenCalledOnceWith( 195 expectedRect, 196 component.color, 197 1, 198 false, 199 false, 200 ); 201 202 const drawRectBorderSpy = spyOn( 203 component.canvasDrawer, 204 'drawRectBorder', 205 ).and.callThrough(); 206 207 await dispatchMousemoveEvent(); 208 expect(drawRectBorderSpy).toHaveBeenCalledOnceWith(expectedRect); 209 210 drawRectSpy.calls.reset(); 211 drawRectBorderSpy.calls.reset(); 212 213 await dispatchMousemoveEvent(); 214 expect(drawRectSpy).toHaveBeenCalledOnceWith( 215 expectedRect, 216 component.color, 217 1, 218 false, 219 false, 220 ); 221 expect(drawRectBorderSpy).toHaveBeenCalledOnceWith(expectedRect); 222 }); 223 224 it('redraws timeline to clear hover entry after mouse out', async () => { 225 await setDefaultTraceAndSelectionRange(); 226 const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect'); 227 228 const mouseoutEvent = new MouseEvent('mouseout'); 229 component.getCanvas().dispatchEvent(mouseoutEvent); 230 fixture.detectChanges(); 231 await fixture.whenRenderingDone(); 232 expect(drawRectSpy).not.toHaveBeenCalled(); 233 234 await dispatchMousemoveEvent(); 235 component.getCanvas().dispatchEvent(mouseoutEvent); 236 fixture.detectChanges(); 237 await fixture.whenRenderingDone(); 238 239 expect(drawRectSpy).toHaveBeenCalledOnceWith( 240 getExpectedBorderedRect(), 241 component.color, 242 1, 243 false, 244 false, 245 ); 246 }); 247 248 it('can draw overlapping transitions (default)', async () => { 249 const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect'); 250 const transitions = [ 251 makeTransition(time10, time85), 252 makeTransition(time60, time110), 253 ]; 254 await setTraceAndSelectionRange(transitions, [time10, time60]); 255 256 const padding = 5; 257 const rows = 2; 258 const oneRowTotalHeight = 259 (component.canvasDrawer.getScaledCanvasHeight() - 2 * padding) / rows; 260 const oneRowHeight = oneRowTotalHeight - padding; 261 const width = component.canvasDrawer.getScaledCanvasWidth(); 262 263 expect(drawRectSpy).toHaveBeenCalledTimes(2); 264 expect(drawRectSpy).toHaveBeenCalledWith( 265 new Rect(0, padding, Math.floor((width * 3) / 4), oneRowHeight), 266 component.color, 267 1, 268 false, 269 false, 270 ); 271 expect(drawRectSpy).toHaveBeenCalledWith( 272 new Rect( 273 Math.floor(width / 2), 274 padding + oneRowTotalHeight, 275 Math.floor(width / 2), 276 oneRowHeight, 277 ), 278 component.color, 279 1, 280 false, 281 false, 282 ); 283 }); 284 285 it('can draw overlapping transitions (contained)', async () => { 286 const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect'); 287 const transitions = [ 288 makeTransition(time10, time85), 289 makeTransition(time35, time60), 290 ]; 291 await setTraceAndSelectionRange(transitions, [time10, time35]); 292 293 const padding = 5; 294 const rows = 2; 295 const oneRowTotalHeight = 296 (component.canvasDrawer.getScaledCanvasHeight() - 2 * padding) / rows; 297 const oneRowHeight = oneRowTotalHeight - padding; 298 const width = component.canvasDrawer.getScaledCanvasWidth(); 299 300 expect(drawRectSpy).toHaveBeenCalledTimes(2); 301 expect(drawRectSpy).toHaveBeenCalledWith( 302 new Rect(0, padding, Math.floor((width * 3) / 4), oneRowHeight), 303 component.color, 304 1, 305 false, 306 false, 307 ); 308 expect(drawRectSpy).toHaveBeenCalledWith( 309 new Rect( 310 Math.floor(width / 4), 311 padding + oneRowTotalHeight, 312 Math.floor(width / 4), 313 oneRowHeight, 314 ), 315 component.color, 316 1, 317 false, 318 false, 319 ); 320 }); 321 322 it('can draw aborted transitions', async () => { 323 const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect'); 324 const transitions = [makeTransition(time35, undefined, time85)]; 325 await setTraceAndSelectionRange(transitions, [time35]); 326 327 const padding = 5; 328 const oneRowTotalHeight = 30; 329 const oneRowHeight = oneRowTotalHeight - padding; 330 const width = component.canvasDrawer.getScaledCanvasWidth(); 331 332 expect(drawRectSpy).toHaveBeenCalledTimes(1); 333 expect(drawRectSpy).toHaveBeenCalledWith( 334 new Rect( 335 Math.floor((width * 1) / 4), 336 padding, 337 Math.floor(width / 2), 338 oneRowHeight, 339 ), 340 component.color, 341 0.25, 342 false, 343 false, 344 ); 345 }); 346 347 it('can draw transition with unknown start time', async () => { 348 const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect'); 349 const transitions = [makeTransition(undefined, time85)]; 350 await setTraceAndSelectionRange(transitions, [time0]); 351 352 const padding = 5; 353 const oneRowTotalHeight = 30; 354 const oneRowHeight = oneRowTotalHeight - padding; 355 356 expect(drawRectSpy).toHaveBeenCalledTimes(1); 357 expect(drawRectSpy).toHaveBeenCalledWith( 358 new Rect( 359 Math.floor((component.canvasDrawer.getScaledCanvasWidth() * 74) / 100), 360 padding, 361 oneRowHeight, 362 oneRowHeight, 363 ), 364 component.color, 365 1, 366 true, 367 false, 368 ); 369 }); 370 371 it('can draw transition with unknown end time', async () => { 372 const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect'); 373 const transitions = [makeTransition(time35, undefined)]; 374 await setTraceAndSelectionRange(transitions, [time35]); 375 376 const padding = 5; 377 const oneRowTotalHeight = 30; 378 const oneRowHeight = oneRowTotalHeight - padding; 379 380 expect(drawRectSpy).toHaveBeenCalledTimes(1); 381 expect(drawRectSpy).toHaveBeenCalledWith( 382 new Rect( 383 Math.floor((component.canvasDrawer.getScaledCanvasWidth() * 1) / 4), 384 padding, 385 oneRowHeight, 386 oneRowHeight, 387 ), 388 component.color, 389 1, 390 false, 391 true, 392 ); 393 }); 394 395 it('does not render transition with create time but no dispatch time', async () => { 396 const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect'); 397 const transitions = [makeTransition(undefined, time85, undefined, time10)]; 398 await setTraceAndSelectionRange(transitions, [time10]); 399 expect(drawRectSpy).not.toHaveBeenCalled(); 400 }); 401 402 it('handles missing trace entries', async () => { 403 const transition0 = makeTransition(time10, time30); 404 const transition1 = makeTransition(time60, time110); 405 406 component.trace = new TraceBuilder<PropertyTreeNode>() 407 .setType(TraceType.TRANSITION) 408 .setEntries([transition0, transition1]) 409 .setTimestamps([time10, time20]) 410 .build(); 411 component.transitionEntries = [transition0, undefined]; 412 component.selectionRange = range10to110; 413 414 const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect'); 415 416 fixture.detectChanges(); 417 await fixture.whenRenderingDone(); 418 419 expect(drawRectSpy).toHaveBeenCalledTimes(1); 420 }); 421 422 it('emits scroll event', async () => { 423 await setDefaultTraceAndSelectionRange(); 424 425 const spy = spyOn(component.onScrollEvent, 'emit'); 426 htmlElement.dispatchEvent(new WheelEvent('wheel')); 427 fixture.detectChanges(); 428 expect(spy).toHaveBeenCalled(); 429 }); 430 431 it('tracks mouse position', async () => { 432 await setDefaultTraceAndSelectionRange(); 433 434 const spy = spyOn(component.onMouseXRatioUpdate, 'emit'); 435 const canvas = assertDefined(component.canvasRef).nativeElement; 436 437 const mouseMoveEvent = new MouseEvent('mousemove'); 438 Object.defineProperty(mouseMoveEvent, 'target', {value: canvas}); 439 Object.defineProperty(mouseMoveEvent, 'offsetX', {value: 100}); 440 canvas.dispatchEvent(mouseMoveEvent); 441 fixture.detectChanges(); 442 443 expect(spy).toHaveBeenCalledWith(100 / canvas.offsetWidth); 444 445 const mouseLeaveEvent = new MouseEvent('mouseleave'); 446 canvas.dispatchEvent(mouseLeaveEvent); 447 fixture.detectChanges(); 448 expect(spy).toHaveBeenCalledWith(undefined); 449 }); 450 451 async function setDefaultTraceAndSelectionRange(setSelectedEntry = false) { 452 const transitions = [makeTransition(time35, time85)]; 453 component.trace = new TraceBuilder<PropertyTreeNode>() 454 .setType(TraceType.TRANSITION) 455 .setEntries(transitions) 456 .setTimestamps([time35]) 457 .build(); 458 component.transitionEntries = transitions; 459 component.selectionRange = range10to110; 460 if (setSelectedEntry) component.selectedEntry = component.trace.getEntry(0); 461 fixture.detectChanges(); 462 await fixture.whenRenderingDone(); 463 } 464 465 function makeTransition( 466 dispatchTime: Timestamp | undefined, 467 finishTime: Timestamp | undefined, 468 abortTime?: Timestamp, 469 createTime?: Timestamp, 470 ): PropertyTreeNode { 471 const shellDataChildren = []; 472 if (dispatchTime !== undefined) { 473 shellDataChildren.push({name: 'dispatchTimeNs', value: dispatchTime}); 474 } 475 if (dispatchTime !== undefined) { 476 shellDataChildren.push({name: 'abortTimeNs', value: abortTime}); 477 } 478 479 const wmDataChildren = []; 480 if (finishTime !== undefined) { 481 wmDataChildren.push({name: 'finishTimeNs', value: finishTime}); 482 } 483 if (createTime !== undefined) { 484 wmDataChildren.push({name: 'createTimeNs', value: createTime}); 485 } 486 487 return new PropertyTreeBuilder() 488 .setIsRoot(true) 489 .setRootId('TransitionsTraceEntry') 490 .setName('transition') 491 .setChildren([ 492 { 493 name: 'wmData', 494 children: wmDataChildren, 495 }, 496 { 497 name: 'shellData', 498 children: shellDataChildren, 499 }, 500 {name: 'aborted', value: abortTime !== undefined}, 501 ]) 502 .build(); 503 } 504 505 async function setTraceAndSelectionRange( 506 transitions: PropertyTreeNode[], 507 timestamps: Timestamp[], 508 range = range10to110, 509 ) { 510 component.trace = new TraceBuilder<PropertyTreeNode>() 511 .setType(TraceType.TRANSITION) 512 .setEntries(transitions) 513 .setTimestamps(timestamps) 514 .build(); 515 component.transitionEntries = transitions; 516 component.selectionRange = range; 517 fixture.detectChanges(); 518 await fixture.whenRenderingDone(); 519 } 520 521 function getExpectedBorderedRect(): Rect { 522 const padding = 5; 523 const oneRowTotalHeight = 30; 524 const oneRowHeight = oneRowTotalHeight - padding; 525 const width = component.canvasDrawer.getScaledCanvasWidth(); 526 return new Rect( 527 Math.floor((width * 1) / 4), 528 padding, 529 Math.floor(width / 2), 530 oneRowHeight, 531 ); 532 } 533 534 async function dispatchMousemoveEvent() { 535 const mousemoveEvent = new MouseEvent('mousemove'); 536 spyOnProperty(mousemoveEvent, 'offsetX').and.returnValue( 537 Math.floor(component.canvasDrawer.getScaledCanvasWidth() / 2), 538 ); 539 spyOnProperty(mousemoveEvent, 'offsetY').and.returnValue(25 / 2); 540 component.getCanvas().dispatchEvent(mousemoveEvent); 541 fixture.detectChanges(); 542 await fixture.whenRenderingDone(); 543 } 544}); 545