| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| import {ComponentFixture} from '@angular/core/testing'; |
| import {assertDefined, assertTrue} from 'common/assert_utils'; |
| import {TimestampConverterUtils} from 'common/time/test_utils'; |
| import {TimestampConverter} from 'common/time/timestamp_converter'; |
| import {ParserFactory as LegacyParserFactory} from 'parsers/legacy/parser_factory'; |
| import {ParserFactory as PerfettoParserFactory} from 'parsers/perfetto/parser_factory'; |
| import {TracesParserFactory} from 'parsers/traces/traces_parser_factory'; |
| import {Parser} from 'trace/parser'; |
| import {Trace} from 'trace/trace'; |
| import {Traces} from 'trace/traces'; |
| import {TraceFile} from 'trace/trace_file'; |
| import {TraceMetadata} from 'trace/trace_metadata'; |
| import {TraceEntryTypeMap, TraceType} from 'trace/trace_type'; |
| import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node'; |
| import {getFixtureFile} from './fixture_utils'; |
| import {TraceBuilder} from './trace_builder'; |
| |
| class UnitTestUtils { |
| static async getTrace<T extends TraceType>( |
| type: T, |
| filename: string, |
| ): Promise<Trace<T>> { |
| const converter = UnitTestUtils.getTimestampConverter(false); |
| const legacyParsers = await UnitTestUtils.getParsers(filename, converter); |
| expect(legacyParsers.length).toBeLessThanOrEqual(1); |
| if (legacyParsers.length === 1) { |
| expect(legacyParsers[0].getTraceType()).toEqual(type); |
| return new TraceBuilder<T>() |
| .setType(type) |
| .setParser(legacyParsers[0] as unknown as Parser<T>) |
| .build(); |
| } |
| |
| const perfettoParsers = await UnitTestUtils.getPerfettoParsers(filename); |
| expect(perfettoParsers.length).toEqual(1); |
| expect(perfettoParsers[0].getTraceType()).toEqual(type); |
| return new TraceBuilder<T>() |
| .setType(type) |
| .setParser(perfettoParsers[0] as unknown as Parser<T>) |
| .build(); |
| } |
| |
| static async getParser( |
| filename: string, |
| converter = UnitTestUtils.getTimestampConverter(), |
| initializeRealToElapsedTimeOffsetNs = true, |
| metadata: TraceMetadata = {}, |
| ): Promise<Parser<object>> { |
| const parsers = await UnitTestUtils.getParsers( |
| filename, |
| converter, |
| initializeRealToElapsedTimeOffsetNs, |
| metadata, |
| ); |
| |
| expect(parsers.length) |
| .withContext(`Should have been able to create a parser for ${filename}`) |
| .toBeGreaterThanOrEqual(1); |
| |
| return parsers[0]; |
| } |
| |
| static async getParsers( |
| filename: string, |
| converter = UnitTestUtils.getTimestampConverter(), |
| initializeRealToElapsedTimeOffsetNs = true, |
| metadata: TraceMetadata = {}, |
| ): Promise<Array<Parser<object>>> { |
| const file = new TraceFile(await getFixtureFile(filename), undefined); |
| const processedFiles = await new LegacyParserFactory().processFiles( |
| [file], |
| converter, |
| metadata, |
| ); |
| const fileAndParsers = processedFiles.parsers; |
| |
| if (initializeRealToElapsedTimeOffsetNs) { |
| const monotonicOffset = fileAndParsers |
| .find( |
| (fileAndParser) => |
| fileAndParser.parser.getRealToMonotonicTimeOffsetNs() !== undefined, |
| ) |
| ?.parser.getRealToMonotonicTimeOffsetNs(); |
| if (monotonicOffset !== undefined) { |
| converter.setRealToMonotonicTimeOffsetNs(monotonicOffset); |
| } |
| const bootTimeOffset = fileAndParsers |
| .find( |
| (fileAndParser) => |
| fileAndParser.parser.getRealToBootTimeOffsetNs() !== undefined, |
| ) |
| ?.parser.getRealToBootTimeOffsetNs(); |
| if (bootTimeOffset !== undefined) { |
| converter.setRealToBootTimeOffsetNs(bootTimeOffset); |
| } |
| } |
| |
| return fileAndParsers.map((fileAndParser) => { |
| fileAndParser.parser.createTimestamps(); |
| return fileAndParser.parser; |
| }); |
| } |
| |
| static async getPerfettoParser<T extends TraceType>( |
| traceType: T, |
| fixturePath: string, |
| withUTCOffset = false, |
| ): Promise<Parser<TraceEntryTypeMap[T]>> { |
| const parsers = await UnitTestUtils.getPerfettoParsers( |
| fixturePath, |
| withUTCOffset, |
| ); |
| const parser = assertDefined( |
| parsers.find((parser) => parser.getTraceType() === traceType), |
| ); |
| return parser as Parser<TraceEntryTypeMap[T]>; |
| } |
| |
| static async getPerfettoParsers( |
| fixturePath: string, |
| withUTCOffset = false, |
| isPerfetto?: boolean, |
| ): Promise<Array<Parser<object>>> { |
| const file = await getFixtureFile(fixturePath); |
| const traceFile = new TraceFile(file); |
| const converter = UnitTestUtils.getTimestampConverter(withUTCOffset); |
| const {parsers, isPerfettoTrace} = |
| await new PerfettoParserFactory().processFiles( |
| traceFile, |
| converter, |
| undefined, |
| ); |
| if (isPerfetto !== undefined) { |
| expect(isPerfettoTrace).toEqual(isPerfetto); |
| } |
| parsers.forEach((parser) => { |
| converter.setRealToBootTimeOffsetNs( |
| assertDefined(parser.getRealToBootTimeOffsetNs()), |
| ); |
| parser.createTimestamps(); |
| }); |
| return parsers; |
| } |
| |
| static async getTracesParser( |
| filenames: string[], |
| withUTCOffset = false, |
| ): Promise<Parser<object>> { |
| const converter = UnitTestUtils.getTimestampConverter(withUTCOffset); |
| const legacyParsers = ( |
| await Promise.all( |
| filenames.map(async (filename) => |
| UnitTestUtils.getParsers(filename, converter, true), |
| ), |
| ) |
| ).reduce((acc, cur) => acc.concat(cur), []); |
| |
| const perfettoParsers = ( |
| await Promise.all( |
| filenames.map(async (filename) => |
| UnitTestUtils.getPerfettoParsers(filename), |
| ), |
| ) |
| ).reduce((acc, cur) => acc.concat(cur), []); |
| |
| const parsersArray = legacyParsers.concat(perfettoParsers); |
| |
| const offset = parsersArray |
| .filter((parser) => parser.getRealToBootTimeOffsetNs() !== undefined) |
| .sort((a, b) => |
| Number( |
| (a.getRealToBootTimeOffsetNs() ?? 0n) - |
| (b.getRealToBootTimeOffsetNs() ?? 0n), |
| ), |
| ) |
| .at(-1) |
| ?.getRealToBootTimeOffsetNs(); |
| |
| if (offset !== undefined) { |
| converter.setRealToBootTimeOffsetNs(offset); |
| } |
| |
| const traces = new Traces(); |
| parsersArray.forEach((parser) => { |
| const trace = Trace.fromParser(parser); |
| traces.addTrace(trace); |
| }); |
| |
| const tracesParsers = await new TracesParserFactory().createParsers( |
| traces, |
| converter, |
| ); |
| assertTrue( |
| tracesParsers.length === 1, |
| () => |
| `Should have been able to create a traces parser for [${filenames.join()}]`, |
| ); |
| return tracesParsers[0]; |
| } |
| |
| static getTimestampConverter(withUTCOffset = false): TimestampConverter { |
| return withUTCOffset |
| ? new TimestampConverter(TimestampConverterUtils.ASIA_TIMEZONE_INFO) |
| : new TimestampConverter(TimestampConverterUtils.UTC_TIMEZONE_INFO); |
| } |
| |
| static async getWindowManagerState(index = 0): Promise<HierarchyTreeNode> { |
| return UnitTestUtils.getTraceEntry( |
| 'traces/elapsed_and_real_timestamp/WindowManager.pb', |
| index, |
| ); |
| } |
| |
| static async getLayerTraceEntry(index = 0): Promise<HierarchyTreeNode> { |
| return await UnitTestUtils.getTraceEntry<HierarchyTreeNode>( |
| 'traces/elapsed_timestamp/SurfaceFlinger.pb', |
| index, |
| ); |
| } |
| |
| static async getViewCaptureEntry(): Promise<HierarchyTreeNode> { |
| return await UnitTestUtils.getTraceEntry<HierarchyTreeNode>( |
| 'traces/elapsed_and_real_timestamp/com.google.android.apps.nexuslauncher_0.vc', |
| ); |
| } |
| |
| static async getMultiDisplayLayerTraceEntry(): Promise<HierarchyTreeNode> { |
| return await UnitTestUtils.getTraceEntry<HierarchyTreeNode>( |
| 'traces/elapsed_and_real_timestamp/SurfaceFlinger_multidisplay.pb', |
| ); |
| } |
| |
| static async getImeTraceEntries(): Promise< |
| [Map<TraceType, HierarchyTreeNode>, Map<TraceType, HierarchyTreeNode>] |
| > { |
| let surfaceFlingerEntry: HierarchyTreeNode | undefined; |
| { |
| const parser = (await UnitTestUtils.getParser( |
| 'traces/ime/SurfaceFlinger_with_IME.pb', |
| )) as Parser<HierarchyTreeNode>; |
| surfaceFlingerEntry = await parser.getEntry(5); |
| } |
| |
| let windowManagerEntry: HierarchyTreeNode | undefined; |
| { |
| const parser = (await UnitTestUtils.getParser( |
| 'traces/ime/WindowManager_with_IME.pb', |
| )) as Parser<HierarchyTreeNode>; |
| windowManagerEntry = await parser.getEntry(2); |
| } |
| |
| const entries = new Map<TraceType, HierarchyTreeNode>(); |
| entries.set( |
| TraceType.INPUT_METHOD_CLIENTS, |
| await UnitTestUtils.getTraceEntry('traces/ime/InputMethodClients.pb'), |
| ); |
| entries.set( |
| TraceType.INPUT_METHOD_MANAGER_SERVICE, |
| await UnitTestUtils.getTraceEntry( |
| 'traces/ime/InputMethodManagerService.pb', |
| ), |
| ); |
| entries.set( |
| TraceType.INPUT_METHOD_SERVICE, |
| await UnitTestUtils.getTraceEntry('traces/ime/InputMethodService.pb'), |
| ); |
| entries.set(TraceType.SURFACE_FLINGER, surfaceFlingerEntry); |
| entries.set(TraceType.WINDOW_MANAGER, windowManagerEntry); |
| |
| const secondEntries = new Map<TraceType, HierarchyTreeNode>(); |
| secondEntries.set( |
| TraceType.INPUT_METHOD_CLIENTS, |
| await UnitTestUtils.getTraceEntry('traces/ime/InputMethodClients.pb', 1), |
| ); |
| secondEntries.set(TraceType.SURFACE_FLINGER, surfaceFlingerEntry); |
| secondEntries.set(TraceType.WINDOW_MANAGER, windowManagerEntry); |
| |
| return [entries, secondEntries]; |
| } |
| |
| static async getTraceEntry<T>(filename: string, index = 0) { |
| const parser = (await UnitTestUtils.getParser(filename)) as Parser<T>; |
| return parser.getEntry(index); |
| } |
| |
| static makeEmptyTrace<T extends TraceType>( |
| traceType: T, |
| descriptors: string[] = [], |
| ): Trace<TraceEntryTypeMap[T]> { |
| return new TraceBuilder<TraceEntryTypeMap[T]>() |
| .setEntries([]) |
| .setTimestamps([]) |
| .setDescriptors(descriptors) |
| .setType(traceType) |
| .build(); |
| } |
| |
| static async checkTooltips<T>( |
| elements: Element[], |
| expTooltips: Array<string | undefined>, |
| fixture: ComponentFixture<T>, |
| ) { |
| for (const [index, el] of elements.entries()) { |
| el.dispatchEvent(new Event('mouseenter')); |
| fixture.detectChanges(); |
| const panel = document.querySelector<HTMLElement>('.mat-tooltip-panel'); |
| if (expTooltips[index] !== undefined) { |
| expect(panel?.textContent).toEqual(expTooltips[index]); |
| } else { |
| expect(panel).toBeNull(); |
| } |
| el.dispatchEvent(new Event('mouseleave')); |
| fixture.detectChanges(); |
| await fixture.whenStable(); |
| } |
| } |
| |
| static makeFakeWebSocket(): jasmine.SpyObj<WebSocket> { |
| const socket = jasmine.createSpyObj<WebSocket>( |
| 'WebSocket', |
| ['onmessage', 'onclose', 'send', 'close', 'onerror'], |
| {'readyState': WebSocket.OPEN, binaryType: 'arraybuffer'}, |
| ); |
| socket.close.and.callFake(() => { |
| socket.onclose!(new CloseEvent('')); |
| }); |
| return socket; |
| } |
| |
| static makeFakeWebSocketMessage( |
| data: Blob | ArrayBuffer | number | string, |
| ): MessageEvent { |
| return jasmine.createSpyObj<MessageEvent>([], {'data': data}); |
| } |
| } |
| |
| export {UnitTestUtils}; |