• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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