xref: /aosp_15_r20/development/tools/winscope/src/test/unit/utils.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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 {ComponentFixture} from '@angular/core/testing';
18import {assertDefined} from 'common/assert_utils';
19import {Timestamp} from 'common/time';
20import {TimestampConverter} from 'common/timestamp_converter';
21import {UrlUtils} from 'common/url_utils';
22import {ParserFactory as LegacyParserFactory} from 'parsers/legacy/parser_factory';
23import {ParserFactory as PerfettoParserFactory} from 'parsers/perfetto/parser_factory';
24import {TracesParserFactory} from 'parsers/traces/traces_parser_factory';
25import {Parser} from 'trace/parser';
26import {Trace} from 'trace/trace';
27import {Traces} from 'trace/traces';
28import {TraceFile} from 'trace/trace_file';
29import {TraceMetadata} from 'trace/trace_metadata';
30import {TraceEntryTypeMap, TraceType} from 'trace/trace_type';
31import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
32import {QueryResult, Row, RowIterator} from 'trace_processor/query_result';
33import {TraceProcessorFactory} from 'trace_processor/trace_processor_factory';
34import {TimestampConverterUtils} from './timestamp_converter_utils';
35import {TraceBuilder} from './trace_builder';
36
37class UnitTestUtils {
38  static async getFixtureFile(
39    srcFilename: string,
40    dstFilename: string = srcFilename,
41  ): Promise<File> {
42    const url = UrlUtils.getRootUrl() + 'base/src/test/fixtures/' + srcFilename;
43    const response = await fetch(url);
44    expect(response.ok).toBeTrue();
45    const blob = await response.blob();
46    const file = new File([blob], dstFilename);
47    return file;
48  }
49
50  static async getTrace<T extends TraceType>(
51    type: T,
52    filename: string,
53  ): Promise<Trace<T>> {
54    const converter = UnitTestUtils.getTimestampConverter(false);
55    const legacyParsers = await UnitTestUtils.getParsers(filename, converter);
56    expect(legacyParsers.length).toBeLessThanOrEqual(1);
57    if (legacyParsers.length === 1) {
58      expect(legacyParsers[0].getTraceType()).toEqual(type);
59      return new TraceBuilder<T>()
60        .setType(type)
61        .setParser(legacyParsers[0] as unknown as Parser<T>)
62        .build();
63    }
64
65    const perfettoParsers = await UnitTestUtils.getPerfettoParsers(filename);
66    expect(perfettoParsers.length).toEqual(1);
67    expect(perfettoParsers[0].getTraceType()).toEqual(type);
68    return new TraceBuilder<T>()
69      .setType(type)
70      .setParser(perfettoParsers[0] as unknown as Parser<T>)
71      .build();
72  }
73
74  static async getParser(
75    filename: string,
76    converter = UnitTestUtils.getTimestampConverter(),
77    initializeRealToElapsedTimeOffsetNs = true,
78    metadata: TraceMetadata = {},
79  ): Promise<Parser<object>> {
80    const parsers = await UnitTestUtils.getParsers(
81      filename,
82      converter,
83      initializeRealToElapsedTimeOffsetNs,
84      metadata,
85    );
86
87    expect(parsers.length)
88      .withContext(`Should have been able to create a parser for ${filename}`)
89      .toBeGreaterThanOrEqual(1);
90
91    return parsers[0];
92  }
93
94  static async getParsers(
95    filename: string,
96    converter = UnitTestUtils.getTimestampConverter(),
97    initializeRealToElapsedTimeOffsetNs = true,
98    metadata: TraceMetadata = {},
99  ): Promise<Array<Parser<object>>> {
100    const file = new TraceFile(
101      await UnitTestUtils.getFixtureFile(filename),
102      undefined,
103    );
104    const fileAndParsers = await new LegacyParserFactory().createParsers(
105      [file],
106      converter,
107      metadata,
108    );
109
110    if (initializeRealToElapsedTimeOffsetNs) {
111      const monotonicOffset = fileAndParsers
112        .find(
113          (fileAndParser) =>
114            fileAndParser.parser.getRealToMonotonicTimeOffsetNs() !== undefined,
115        )
116        ?.parser.getRealToMonotonicTimeOffsetNs();
117      if (monotonicOffset !== undefined) {
118        converter.setRealToMonotonicTimeOffsetNs(monotonicOffset);
119      }
120      const bootTimeOffset = fileAndParsers
121        .find(
122          (fileAndParser) =>
123            fileAndParser.parser.getRealToBootTimeOffsetNs() !== undefined,
124        )
125        ?.parser.getRealToBootTimeOffsetNs();
126      if (bootTimeOffset !== undefined) {
127        converter.setRealToBootTimeOffsetNs(bootTimeOffset);
128      }
129    }
130
131    return fileAndParsers.map((fileAndParser) => {
132      fileAndParser.parser.createTimestamps();
133      return fileAndParser.parser;
134    });
135  }
136
137  static async getPerfettoParser<T extends TraceType>(
138    traceType: T,
139    fixturePath: string,
140    withUTCOffset = false,
141  ): Promise<Parser<TraceEntryTypeMap[T]>> {
142    const parsers = await UnitTestUtils.getPerfettoParsers(
143      fixturePath,
144      withUTCOffset,
145    );
146    const parser = assertDefined(
147      parsers.find((parser) => parser.getTraceType() === traceType),
148    );
149    return parser as Parser<TraceEntryTypeMap[T]>;
150  }
151
152  static async getPerfettoParsers(
153    fixturePath: string,
154    withUTCOffset = false,
155  ): Promise<Array<Parser<object>>> {
156    const file = await UnitTestUtils.getFixtureFile(fixturePath);
157    const traceFile = new TraceFile(file);
158    const converter = UnitTestUtils.getTimestampConverter(withUTCOffset);
159    const parsers = await new PerfettoParserFactory().createParsers(
160      traceFile,
161      converter,
162      undefined,
163    );
164    parsers.forEach((parser) => {
165      converter.setRealToBootTimeOffsetNs(
166        assertDefined(parser.getRealToBootTimeOffsetNs()),
167      );
168      parser.createTimestamps();
169    });
170    return parsers;
171  }
172
173  static async getTracesParser(
174    filenames: string[],
175    withUTCOffset = false,
176  ): Promise<Parser<object>> {
177    const converter = UnitTestUtils.getTimestampConverter(withUTCOffset);
178    const legacyParsers = (
179      await Promise.all(
180        filenames.map(async (filename) =>
181          UnitTestUtils.getParsers(filename, converter, true),
182        ),
183      )
184    ).reduce((acc, cur) => acc.concat(cur), []);
185
186    const perfettoParsers = (
187      await Promise.all(
188        filenames.map(async (filename) =>
189          UnitTestUtils.getPerfettoParsers(filename),
190        ),
191      )
192    ).reduce((acc, cur) => acc.concat(cur), []);
193
194    const parsersArray = legacyParsers.concat(perfettoParsers);
195
196    const offset = parsersArray
197      .filter((parser) => parser.getRealToBootTimeOffsetNs() !== undefined)
198      .sort((a, b) =>
199        Number(
200          (a.getRealToBootTimeOffsetNs() ?? 0n) -
201            (b.getRealToBootTimeOffsetNs() ?? 0n),
202        ),
203      )
204      .at(-1)
205      ?.getRealToBootTimeOffsetNs();
206
207    if (offset !== undefined) {
208      converter.setRealToBootTimeOffsetNs(offset);
209    }
210
211    const traces = new Traces();
212    parsersArray.forEach((parser) => {
213      const trace = Trace.fromParser(parser);
214      traces.addTrace(trace);
215    });
216
217    const tracesParsers = await new TracesParserFactory().createParsers(
218      traces,
219      converter,
220    );
221    expect(tracesParsers.length)
222      .withContext(
223        `Should have been able to create a traces parser for [${filenames.join()}]`,
224      )
225      .toEqual(1);
226    return tracesParsers[0];
227  }
228
229  static getTimestampConverter(withUTCOffset = false): TimestampConverter {
230    return withUTCOffset
231      ? new TimestampConverter(TimestampConverterUtils.ASIA_TIMEZONE_INFO)
232      : new TimestampConverter(TimestampConverterUtils.UTC_TIMEZONE_INFO);
233  }
234
235  static async getWindowManagerState(index = 0): Promise<HierarchyTreeNode> {
236    return UnitTestUtils.getTraceEntry(
237      'traces/elapsed_and_real_timestamp/WindowManager.pb',
238      index,
239    );
240  }
241
242  static async getLayerTraceEntry(index = 0): Promise<HierarchyTreeNode> {
243    return await UnitTestUtils.getTraceEntry<HierarchyTreeNode>(
244      'traces/elapsed_timestamp/SurfaceFlinger.pb',
245      index,
246    );
247  }
248
249  static async getViewCaptureEntry(): Promise<HierarchyTreeNode> {
250    return await UnitTestUtils.getTraceEntry<HierarchyTreeNode>(
251      'traces/elapsed_and_real_timestamp/com.google.android.apps.nexuslauncher_0.vc',
252    );
253  }
254
255  static async getMultiDisplayLayerTraceEntry(): Promise<HierarchyTreeNode> {
256    return await UnitTestUtils.getTraceEntry<HierarchyTreeNode>(
257      'traces/elapsed_and_real_timestamp/SurfaceFlinger_multidisplay.pb',
258    );
259  }
260
261  static async getImeTraceEntries(): Promise<
262    [Map<TraceType, HierarchyTreeNode>, Map<TraceType, HierarchyTreeNode>]
263  > {
264    let surfaceFlingerEntry: HierarchyTreeNode | undefined;
265    {
266      const parser = (await UnitTestUtils.getParser(
267        'traces/ime/SurfaceFlinger_with_IME.pb',
268      )) as Parser<HierarchyTreeNode>;
269      surfaceFlingerEntry = await parser.getEntry(5);
270    }
271
272    let windowManagerEntry: HierarchyTreeNode | undefined;
273    {
274      const parser = (await UnitTestUtils.getParser(
275        'traces/ime/WindowManager_with_IME.pb',
276      )) as Parser<HierarchyTreeNode>;
277      windowManagerEntry = await parser.getEntry(2);
278    }
279
280    const entries = new Map<TraceType, HierarchyTreeNode>();
281    entries.set(
282      TraceType.INPUT_METHOD_CLIENTS,
283      await UnitTestUtils.getTraceEntry('traces/ime/InputMethodClients.pb'),
284    );
285    entries.set(
286      TraceType.INPUT_METHOD_MANAGER_SERVICE,
287      await UnitTestUtils.getTraceEntry(
288        'traces/ime/InputMethodManagerService.pb',
289      ),
290    );
291    entries.set(
292      TraceType.INPUT_METHOD_SERVICE,
293      await UnitTestUtils.getTraceEntry('traces/ime/InputMethodService.pb'),
294    );
295    entries.set(TraceType.SURFACE_FLINGER, surfaceFlingerEntry);
296    entries.set(TraceType.WINDOW_MANAGER, windowManagerEntry);
297
298    const secondEntries = new Map<TraceType, HierarchyTreeNode>();
299    secondEntries.set(
300      TraceType.INPUT_METHOD_CLIENTS,
301      await UnitTestUtils.getTraceEntry('traces/ime/InputMethodClients.pb', 1),
302    );
303    secondEntries.set(TraceType.SURFACE_FLINGER, surfaceFlingerEntry);
304    secondEntries.set(TraceType.WINDOW_MANAGER, windowManagerEntry);
305
306    return [entries, secondEntries];
307  }
308
309  static async getTraceEntry<T>(filename: string, index = 0) {
310    const parser = (await UnitTestUtils.getParser(filename)) as Parser<T>;
311    return parser.getEntry(index);
312  }
313
314  static timestampEqualityTester(first: any, second: any): boolean | undefined {
315    if (first instanceof Timestamp && second instanceof Timestamp) {
316      return UnitTestUtils.testTimestamps(first, second);
317    }
318    return undefined;
319  }
320
321  static checkSectionCollapseAndExpand<T>(
322    htmlElement: HTMLElement,
323    fixture: ComponentFixture<T>,
324    selector: string,
325    sectionTitle: string,
326  ) {
327    const section = assertDefined(htmlElement.querySelector(selector));
328    const collapseButton = assertDefined(
329      section.querySelector('collapsible-section-title button'),
330    ) as HTMLElement;
331    collapseButton.click();
332    fixture.detectChanges();
333    expect(section.classList).toContain('collapsed');
334    const collapsedSections = assertDefined(
335      htmlElement.querySelector('collapsed-sections'),
336    );
337    const collapsedSection = assertDefined(
338      collapsedSections.querySelector('.collapsed-section'),
339    ) as HTMLElement;
340    expect(collapsedSection.textContent).toContain(sectionTitle);
341    collapsedSection.click();
342    fixture.detectChanges();
343    UnitTestUtils.checkNoCollapsedSectionButtons(htmlElement);
344  }
345
346  static checkNoCollapsedSectionButtons(htmlElement: HTMLElement) {
347    const collapsedSections = assertDefined(
348      htmlElement.querySelector('collapsed-sections'),
349    );
350    expect(
351      collapsedSections.querySelectorAll('.collapsed-section').length,
352    ).toEqual(0);
353  }
354
355  static makeEmptyTrace<T extends TraceType>(
356    traceType: T,
357    descriptors: string[] = [],
358  ): Trace<TraceEntryTypeMap[T]> {
359    return new TraceBuilder<TraceEntryTypeMap[T]>()
360      .setEntries([])
361      .setTimestamps([])
362      .setDescriptors(descriptors)
363      .setType(traceType)
364      .build();
365  }
366
367  static makeSearchTraceSpies(
368    ts?: Timestamp,
369  ): [jasmine.SpyObj<QueryResult>, jasmine.SpyObj<RowIterator<Row>>] {
370    const spyQueryResult = jasmine.createSpyObj<QueryResult>('result', [
371      'numRows',
372      'columns',
373      'iter',
374    ]);
375    spyQueryResult.numRows.and.returnValue(1);
376    spyQueryResult.columns.and.returnValue(
377      ts === undefined ? ['property'] : ['ts', 'property'],
378    );
379
380    const spyIter = jasmine.createSpyObj<RowIterator<Row>>('iter', [
381      'valid',
382      'next',
383      'get',
384    ]);
385    if (ts) {
386      spyIter.get.withArgs('ts').and.returnValue(ts.getValueNs());
387    }
388    spyIter.get.withArgs('property').and.returnValue('test_value');
389    spyIter.valid.and.returnValue(true);
390    spyIter.next.and.callFake(() =>
391      assertDefined(spyIter).valid.and.returnValue(false),
392    );
393    spyQueryResult.iter.and.returnValue(spyIter);
394
395    return [spyQueryResult, spyIter];
396  }
397
398  static async runQueryAndGetResult(query: string): Promise<QueryResult> {
399    const tp = await TraceProcessorFactory.getSingleInstance();
400    return tp.query(query).waitAllRows();
401  }
402
403  private static testTimestamps(
404    timestamp: Timestamp,
405    expectedTimestamp: Timestamp,
406  ): boolean {
407    if (timestamp.format() !== expectedTimestamp.format()) return false;
408    if (timestamp.getValueNs() !== expectedTimestamp.getValueNs()) {
409      return false;
410    }
411    return true;
412  }
413}
414
415export {UnitTestUtils};
416