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