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 {TracesParserFactory} from 'parsers/legacy/traces_parser_factory'; 24import {ParserFactory as PerfettoParserFactory} from 'parsers/perfetto/parser_factory'; 25import {Parser} from 'trace/parser'; 26import {Trace} from 'trace/trace'; 27import {Traces} from 'trace/traces'; 28import {TraceFile} from 'trace/trace_file'; 29import {TraceEntryTypeMap, TraceType} from 'trace/trace_type'; 30import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node'; 31import {TimestampConverterUtils} from './timestamp_converter_utils'; 32import {TraceBuilder} from './trace_builder'; 33 34class UnitTestUtils { 35 static async getFixtureFile( 36 srcFilename: string, 37 dstFilename: string = srcFilename, 38 ): Promise<File> { 39 const url = UrlUtils.getRootUrl() + 'base/src/test/fixtures/' + srcFilename; 40 const response = await fetch(url); 41 expect(response.ok).toBeTrue(); 42 const blob = await response.blob(); 43 const file = new File([blob], dstFilename); 44 return file; 45 } 46 47 static async getTrace<T extends TraceType>( 48 type: T, 49 filename: string, 50 ): Promise<Trace<T>> { 51 const converter = UnitTestUtils.getTimestampConverter(false); 52 const legacyParsers = await UnitTestUtils.getParsers(filename, converter); 53 expect(legacyParsers.length).toBeLessThanOrEqual(1); 54 if (legacyParsers.length === 1) { 55 expect(legacyParsers[0].getTraceType()).toEqual(type); 56 return new TraceBuilder<T>() 57 .setType(type) 58 .setParser(legacyParsers[0] as unknown as Parser<T>) 59 .build(); 60 } 61 62 const perfettoParsers = await UnitTestUtils.getPerfettoParsers(filename); 63 expect(perfettoParsers.length).toEqual(1); 64 expect(perfettoParsers[0].getTraceType()).toEqual(type); 65 return new TraceBuilder<T>() 66 .setType(type) 67 .setParser(perfettoParsers[0] as unknown as Parser<T>) 68 .build(); 69 } 70 71 static async getParser( 72 filename: string, 73 converter = UnitTestUtils.getTimestampConverter(), 74 initializeRealToElapsedTimeOffsetNs = true, 75 ): Promise<Parser<object>> { 76 const parsers = await UnitTestUtils.getParsers( 77 filename, 78 converter, 79 initializeRealToElapsedTimeOffsetNs, 80 ); 81 return parsers[0]; 82 } 83 84 static async getParsers( 85 filename: string, 86 converter = UnitTestUtils.getTimestampConverter(), 87 initializeRealToElapsedTimeOffsetNs = true, 88 ): Promise<Array<Parser<object>>> { 89 const file = new TraceFile( 90 await UnitTestUtils.getFixtureFile(filename), 91 undefined, 92 ); 93 const fileAndParsers = await new LegacyParserFactory().createParsers( 94 [file], 95 converter, 96 undefined, 97 undefined, 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 const parsers = fileAndParsers.map((fileAndParser) => { 122 fileAndParser.parser.createTimestamps(); 123 return fileAndParser.parser; 124 }); 125 126 expect(parsers.length) 127 .withContext(`Should have been able to create a parser for ${filename}`) 128 .toBeGreaterThanOrEqual(1); 129 130 return parsers; 131 } 132 133 static async getPerfettoParser<T extends TraceType>( 134 traceType: T, 135 fixturePath: string, 136 withUTCOffset = false, 137 ): Promise<Parser<TraceEntryTypeMap[T]>> { 138 const parsers = await UnitTestUtils.getPerfettoParsers( 139 fixturePath, 140 withUTCOffset, 141 ); 142 const parser = assertDefined( 143 parsers.find((parser) => parser.getTraceType() === traceType), 144 ); 145 return parser as Parser<TraceEntryTypeMap[T]>; 146 } 147 148 static async getPerfettoParsers( 149 fixturePath: string, 150 withUTCOffset = false, 151 ): Promise<Array<Parser<object>>> { 152 const file = await UnitTestUtils.getFixtureFile(fixturePath); 153 const traceFile = new TraceFile(file); 154 const converter = UnitTestUtils.getTimestampConverter(withUTCOffset); 155 const parsers = await new PerfettoParserFactory().createParsers( 156 traceFile, 157 converter, 158 undefined, 159 ); 160 parsers.forEach((parser) => { 161 converter.setRealToBootTimeOffsetNs( 162 assertDefined(parser.getRealToBootTimeOffsetNs()), 163 ); 164 parser.createTimestamps(); 165 }); 166 return parsers; 167 } 168 169 static async getTracesParser( 170 filenames: string[], 171 withUTCOffset = false, 172 ): Promise<Parser<object>> { 173 const converter = UnitTestUtils.getTimestampConverter(withUTCOffset); 174 const parsersArray = await Promise.all( 175 filenames.map(async (filename) => 176 UnitTestUtils.getParser(filename, converter, true), 177 ), 178 ); 179 const offset = parsersArray 180 .filter((parser) => parser.getRealToBootTimeOffsetNs() !== undefined) 181 .sort((a, b) => 182 Number( 183 (a.getRealToBootTimeOffsetNs() ?? 0n) - 184 (b.getRealToBootTimeOffsetNs() ?? 0n), 185 ), 186 ) 187 .at(-1) 188 ?.getRealToBootTimeOffsetNs(); 189 190 if (offset !== undefined) { 191 converter.setRealToBootTimeOffsetNs(offset); 192 } 193 194 const traces = new Traces(); 195 parsersArray.forEach((parser) => { 196 const trace = Trace.fromParser(parser); 197 traces.addTrace(trace); 198 }); 199 200 const tracesParsers = await new TracesParserFactory().createParsers( 201 traces, 202 converter, 203 ); 204 expect(tracesParsers.length) 205 .withContext( 206 `Should have been able to create a traces parser for [${filenames.join()}]`, 207 ) 208 .toEqual(1); 209 return tracesParsers[0]; 210 } 211 212 static getTimestampConverter(withUTCOffset = false): TimestampConverter { 213 return withUTCOffset 214 ? new TimestampConverter(TimestampConverterUtils.ASIA_TIMEZONE_INFO) 215 : new TimestampConverter(TimestampConverterUtils.UTC_TIMEZONE_INFO); 216 } 217 218 static async getWindowManagerState(index = 0): Promise<HierarchyTreeNode> { 219 return UnitTestUtils.getTraceEntry( 220 'traces/elapsed_and_real_timestamp/WindowManager.pb', 221 index, 222 ); 223 } 224 225 static async getLayerTraceEntry(index = 0): Promise<HierarchyTreeNode> { 226 return await UnitTestUtils.getTraceEntry<HierarchyTreeNode>( 227 'traces/elapsed_timestamp/SurfaceFlinger.pb', 228 index, 229 ); 230 } 231 232 static async getViewCaptureEntry(): Promise<HierarchyTreeNode> { 233 return await UnitTestUtils.getTraceEntry<HierarchyTreeNode>( 234 'traces/elapsed_and_real_timestamp/com.google.android.apps.nexuslauncher_0.vc', 235 ); 236 } 237 238 static async getMultiDisplayLayerTraceEntry(): Promise<HierarchyTreeNode> { 239 return await UnitTestUtils.getTraceEntry<HierarchyTreeNode>( 240 'traces/elapsed_and_real_timestamp/SurfaceFlinger_multidisplay.pb', 241 ); 242 } 243 244 static async getImeTraceEntries(): Promise< 245 [Map<TraceType, HierarchyTreeNode>, Map<TraceType, HierarchyTreeNode>] 246 > { 247 let surfaceFlingerEntry: HierarchyTreeNode | undefined; 248 { 249 const parser = (await UnitTestUtils.getParser( 250 'traces/ime/SurfaceFlinger_with_IME.pb', 251 )) as Parser<HierarchyTreeNode>; 252 surfaceFlingerEntry = await parser.getEntry(5); 253 } 254 255 let windowManagerEntry: HierarchyTreeNode | undefined; 256 { 257 const parser = (await UnitTestUtils.getParser( 258 'traces/ime/WindowManager_with_IME.pb', 259 )) as Parser<HierarchyTreeNode>; 260 windowManagerEntry = await parser.getEntry(2); 261 } 262 263 const entries = new Map<TraceType, HierarchyTreeNode>(); 264 entries.set( 265 TraceType.INPUT_METHOD_CLIENTS, 266 await UnitTestUtils.getTraceEntry('traces/ime/InputMethodClients.pb'), 267 ); 268 entries.set( 269 TraceType.INPUT_METHOD_MANAGER_SERVICE, 270 await UnitTestUtils.getTraceEntry( 271 'traces/ime/InputMethodManagerService.pb', 272 ), 273 ); 274 entries.set( 275 TraceType.INPUT_METHOD_SERVICE, 276 await UnitTestUtils.getTraceEntry('traces/ime/InputMethodService.pb'), 277 ); 278 entries.set(TraceType.SURFACE_FLINGER, surfaceFlingerEntry); 279 entries.set(TraceType.WINDOW_MANAGER, windowManagerEntry); 280 281 const secondEntries = new Map<TraceType, HierarchyTreeNode>(); 282 secondEntries.set( 283 TraceType.INPUT_METHOD_CLIENTS, 284 await UnitTestUtils.getTraceEntry('traces/ime/InputMethodClients.pb', 1), 285 ); 286 secondEntries.set(TraceType.SURFACE_FLINGER, surfaceFlingerEntry); 287 secondEntries.set(TraceType.WINDOW_MANAGER, windowManagerEntry); 288 289 return [entries, secondEntries]; 290 } 291 292 static timestampEqualityTester(first: any, second: any): boolean | undefined { 293 if (first instanceof Timestamp && second instanceof Timestamp) { 294 return UnitTestUtils.testTimestamps(first, second); 295 } 296 return undefined; 297 } 298 299 static checkSectionCollapseAndExpand<T>( 300 htmlElement: HTMLElement, 301 fixture: ComponentFixture<T>, 302 selector: string, 303 sectionTitle: string, 304 ) { 305 const section = assertDefined(htmlElement.querySelector(selector)); 306 const collapseButton = assertDefined( 307 section.querySelector('collapsible-section-title button'), 308 ) as HTMLElement; 309 collapseButton.click(); 310 fixture.detectChanges(); 311 expect(section.classList).toContain('collapsed'); 312 const collapsedSections = assertDefined( 313 htmlElement.querySelector('collapsed-sections'), 314 ); 315 const collapsedSection = assertDefined( 316 collapsedSections.querySelector('.collapsed-section'), 317 ) as HTMLElement; 318 expect(collapsedSection.textContent).toContain(sectionTitle); 319 collapsedSection.click(); 320 fixture.detectChanges(); 321 UnitTestUtils.checkNoCollapsedSectionButtons(htmlElement); 322 } 323 324 static checkNoCollapsedSectionButtons(htmlElement: HTMLElement) { 325 const collapsedSections = assertDefined( 326 htmlElement.querySelector('collapsed-sections'), 327 ); 328 expect( 329 collapsedSections.querySelectorAll('.collapsed-section').length, 330 ).toEqual(0); 331 } 332 333 private static testTimestamps( 334 timestamp: Timestamp, 335 expectedTimestamp: Timestamp, 336 ): boolean { 337 if (timestamp.format() !== expectedTimestamp.format()) return false; 338 if (timestamp.getValueNs() !== expectedTimestamp.getValueNs()) { 339 return false; 340 } 341 return true; 342 } 343 344 private static async getTraceEntry<T>(filename: string, index = 0) { 345 const parser = (await UnitTestUtils.getParser(filename)) as Parser<T>; 346 return parser.getEntry(index); 347 } 348} 349 350export {UnitTestUtils}; 351