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