1/* 2 * Copyright (C) 2023 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 {assertDefined} from 'common/assert_utils'; 18import {FileUtils} from 'common/file_utils'; 19import {TimeRange} from 'common/time'; 20import {UserWarning} from 'messaging/user_warning'; 21import {TraceHasOldData, TraceOverridden} from 'messaging/user_warnings'; 22import {FileAndParser} from 'parsers/file_and_parser'; 23import {FileAndParsers} from 'parsers/file_and_parsers'; 24import {ParserBuilder} from 'test/unit/parser_builder'; 25import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils'; 26import {UserNotifierChecker} from 'test/unit/user_notifier_checker'; 27import {Parser} from 'trace/parser'; 28import {TraceFile} from 'trace/trace_file'; 29import {TraceType} from 'trace/trace_type'; 30import {LoadedParsers} from './loaded_parsers'; 31 32describe('LoadedParsers', () => { 33 const realZeroTimestamp = TimestampConverterUtils.makeRealTimestamp(0n); 34 const elapsedZeroTimestamp = TimestampConverterUtils.makeElapsedTimestamp(0n); 35 const oldTimestamps = [ 36 realZeroTimestamp, 37 TimestampConverterUtils.makeRealTimestamp(1n), 38 TimestampConverterUtils.makeRealTimestamp(2n), 39 TimestampConverterUtils.makeRealTimestamp(3n), 40 TimestampConverterUtils.makeRealTimestamp(4n), 41 ]; 42 43 const elapsedTimestamps = [ 44 elapsedZeroTimestamp, 45 TimestampConverterUtils.makeElapsedTimestamp(1n), 46 TimestampConverterUtils.makeElapsedTimestamp(2n), 47 TimestampConverterUtils.makeElapsedTimestamp(3n), 48 TimestampConverterUtils.makeElapsedTimestamp(4n), 49 ]; 50 51 const timestamps = [ 52 TimestampConverterUtils.makeRealTimestamp(5n * 60n * 1000000000n + 10n), // 5m10ns 53 TimestampConverterUtils.makeRealTimestamp(5n * 60n * 1000000000n + 11n), // 5m11ns 54 TimestampConverterUtils.makeRealTimestamp(5n * 60n * 1000000000n + 12n), // 5m12ns 55 ]; 56 57 const filename = 'filename'; 58 59 const parserSf0 = new ParserBuilder<object>() 60 .setType(TraceType.SURFACE_FLINGER) 61 .setTimestamps(timestamps) 62 .setDescriptors([filename]) 63 .build(); 64 const parserSf1 = new ParserBuilder<object>() 65 .setType(TraceType.SURFACE_FLINGER) 66 .setTimestamps(timestamps) 67 .setDescriptors([filename]) 68 .build(); 69 const parserSf_longButOldData = new ParserBuilder<object>() 70 .setType(TraceType.SURFACE_FLINGER) 71 .setTimestamps(oldTimestamps) 72 .setDescriptors([filename]) 73 .build(); 74 const parserSf_empty = new ParserBuilder<object>() 75 .setType(TraceType.SURFACE_FLINGER) 76 .setTimestamps([]) 77 .setDescriptors([filename]) 78 .build(); 79 const parserSf_elapsed = new ParserBuilder<object>() 80 .setType(TraceType.SURFACE_FLINGER) 81 .setTimestamps(elapsedTimestamps) 82 .setDescriptors([filename]) 83 .setNoOffsets(true) 84 .build(); 85 const parserWm0 = new ParserBuilder<object>() 86 .setType(TraceType.WINDOW_MANAGER) 87 .setTimestamps(timestamps) 88 .setDescriptors([filename]) 89 .build(); 90 const parserWm1 = new ParserBuilder<object>() 91 .setType(TraceType.WINDOW_MANAGER) 92 .setTimestamps(timestamps) 93 .setDescriptors([filename]) 94 .build(); 95 const parserWm_dump = new ParserBuilder<object>() 96 .setType(TraceType.WINDOW_MANAGER) 97 .setTimestamps([realZeroTimestamp]) 98 .setDescriptors([filename]) 99 .build(); 100 const parserWm_elapsed = new ParserBuilder<object>() 101 .setType(TraceType.WINDOW_MANAGER) 102 .setTimestamps(elapsedTimestamps) 103 .setDescriptors([filename]) 104 .setNoOffsets(true) 105 .build(); 106 const parserWmTransitions = new ParserBuilder<object>() 107 .setType(TraceType.WM_TRANSITION) 108 .setTimestamps([ 109 elapsedZeroTimestamp, 110 elapsedZeroTimestamp, 111 elapsedZeroTimestamp, 112 ]) 113 .setDescriptors([filename]) 114 .build(); 115 const parserEventlog = new ParserBuilder<object>() 116 .setType(TraceType.EVENT_LOG) 117 .setTimestamps(timestamps) 118 .setDescriptors([filename]) 119 .setNoOffsets(true) 120 .build(); 121 const parserScreenRecording = new ParserBuilder<object>() 122 .setType(TraceType.SCREEN_RECORDING) 123 .setTimestamps(timestamps) 124 .setDescriptors([filename]) 125 .build(); 126 const parserViewCapture0 = new ParserBuilder<object>() 127 .setType(TraceType.VIEW_CAPTURE) 128 .setEntries([]) 129 .setDescriptors([filename]) 130 .build(); 131 const parserViewCapture1 = new ParserBuilder<object>() 132 .setType(TraceType.VIEW_CAPTURE) 133 .setEntries([]) 134 .setDescriptors([filename]) 135 .build(); 136 137 let loadedParsers: LoadedParsers; 138 let userNotifierChecker: UserNotifierChecker; 139 140 beforeAll(() => { 141 userNotifierChecker = new UserNotifierChecker(); 142 }); 143 144 beforeEach(() => { 145 loadedParsers = new LoadedParsers(); 146 expect(loadedParsers.getParsers().length).toEqual(0); 147 userNotifierChecker.reset(); 148 }); 149 150 it('can load a single legacy parser', () => { 151 loadParsers([parserSf0], []); 152 expectLoadResult([parserSf0], []); 153 }); 154 155 it('can load a single perfetto parser', () => { 156 loadParsers([], [parserSf0]); 157 expectLoadResult([parserSf0], []); 158 }); 159 160 it('loads multiple perfetto parsers with same trace type', async () => { 161 loadParsers([], [parserSf0, parserSf1]); 162 expectLoadResult([parserSf0, parserSf1], []); 163 }); 164 165 it('loads legacy parser without dropping already-loaded legacy parser (different trace type)', async () => { 166 loadParsers([parserSf0], []); 167 expectLoadResult([parserSf0], []); 168 169 loadParsers([parserWm0], []); 170 expectLoadResult([parserSf0, parserWm0], []); 171 }); 172 173 it('loads legacy parser without dropping already-loaded legacy parser (same trace type)', async () => { 174 loadParsers([parserSf0], []); 175 expectLoadResult([parserSf0], []); 176 177 loadParsers([parserSf1], []); 178 expectLoadResult([parserSf0, parserSf1], []); 179 }); 180 181 it('drops elapsed-only parsers if parsers with real timestamps present', () => { 182 loadParsers([parserSf_elapsed, parserSf0], []); 183 expectLoadResult([parserSf0], [new TraceHasOldData(filename)]); 184 }); 185 186 it('doesnt drop elapsed-only parsers if no parsers with real timestamps present', () => { 187 loadParsers([parserSf_elapsed, parserWm_elapsed], []); 188 expectLoadResult([parserSf_elapsed, parserWm_elapsed], []); 189 }); 190 191 it('keeps real-time parsers without offset', () => { 192 loadParsers([parserSf0, parserEventlog], []); 193 expectLoadResult([parserSf0, parserEventlog], []); 194 }); 195 196 describe('drops legacy parser with old data (dangling old trace file)', () => { 197 const timeGapFrom = assertDefined( 198 parserSf_longButOldData.getTimestamps()?.at(-1), 199 ); 200 const timeGapTo = assertDefined(parserWm0.getTimestamps()?.at(0)); 201 const timeGap = new TimeRange(timeGapFrom, timeGapTo); 202 203 it('taking into account other legacy parsers', () => { 204 loadParsers([parserSf_longButOldData, parserWm0], []); 205 expectLoadResult([parserWm0], [new TraceHasOldData(filename, timeGap)]); 206 }); 207 208 it('taking into account perfetto parsers', () => { 209 loadParsers([parserSf_longButOldData], [parserWm0]); 210 expectLoadResult([parserWm0], [new TraceHasOldData(filename, timeGap)]); 211 }); 212 213 it('taking into account already-loaded parsers', () => { 214 loadParsers([parserWm0], []); 215 216 // Drop parser with old data, even if it provides 217 // a longer trace than the already-loaded parser 218 loadParsers([parserSf_longButOldData], []); 219 expectLoadResult([parserWm0], [new TraceHasOldData(filename, timeGap)]); 220 }); 221 222 it('doesnt drop legacy parser with dump (zero timestamp)', () => { 223 loadParsers([parserWm_dump, parserSf0], []); 224 expectLoadResult([parserWm_dump, parserSf0], []); 225 }); 226 227 it('doesnt drop legacy parser with wm transitions', () => { 228 // Only Shell Transition data used to set timestamps of merged Transition trace, 229 // so WM Transition data should not be considered by "old data" policy 230 loadParsers([parserWmTransitions, parserSf0], []); 231 expectLoadResult([parserWmTransitions, parserSf0], []); 232 }); 233 234 it('is robust to traces with time range overlap', () => { 235 const parser = parserSf0; 236 const timestamps = assertDefined(parserSf0.getTimestamps()); 237 238 const timestampsOverlappingFront = [ 239 timestamps[0].add(-1n), 240 timestamps[0].add(1n), 241 ]; 242 const parserOverlappingFront = new ParserBuilder<object>() 243 .setType(TraceType.TRANSACTIONS) 244 .setTimestamps(timestampsOverlappingFront) 245 .setDescriptors([filename]) 246 .build(); 247 248 const timestampsOverlappingBack = [ 249 timestamps[timestamps.length - 1].add(-1n), 250 timestamps[timestamps.length - 1].add(1n), 251 ]; 252 const parserOverlappingBack = new ParserBuilder<object>() 253 .setType(TraceType.TRANSITION) 254 .setTimestamps(timestampsOverlappingBack) 255 .setDescriptors([filename]) 256 .build(); 257 258 const timestampsOverlappingEntirely = [ 259 timestamps[0].add(-1n), 260 timestamps[timestamps.length - 1].add(1n), 261 ]; 262 const parserOverlappingEntirely = new ParserBuilder<object>() 263 .setType(TraceType.VIEW_CAPTURE) 264 .setTimestamps(timestampsOverlappingEntirely) 265 .setDescriptors([filename]) 266 .build(); 267 268 const timestampsOverlappingExactly = [ 269 timestamps[0], 270 timestamps[timestamps.length - 1], 271 ]; 272 const parserOverlappingExactly = new ParserBuilder<object>() 273 .setType(TraceType.WINDOW_MANAGER) 274 .setTimestamps(timestampsOverlappingExactly) 275 .setDescriptors([filename]) 276 .build(); 277 278 loadParsers( 279 [ 280 parser, 281 parserOverlappingFront, 282 parserOverlappingBack, 283 parserOverlappingEntirely, 284 parserOverlappingExactly, 285 ], 286 [], 287 ); 288 expectLoadResult( 289 [ 290 parser, 291 parserOverlappingFront, 292 parserOverlappingBack, 293 parserOverlappingEntirely, 294 parserOverlappingExactly, 295 ], 296 [], 297 ); 298 }); 299 }); 300 301 it('loads perfetto parser dropping all already-loaded perfetto parsers', () => { 302 loadParsers([], [parserSf0, parserWm0]); 303 expectLoadResult([parserSf0, parserWm0], []); 304 305 // We currently run only one Perfetto TP WebWorker at a time, 306 // so Perfetto parsers previously loaded are now invalid 307 // and must be removed (previous WebWorker is not running anymore). 308 loadParsers([], [parserSf1, parserWm1]); 309 expectLoadResult([parserSf1, parserWm1], []); 310 }); 311 312 describe('prioritizes perfetto parsers over legacy parsers', () => { 313 // While transitioning to the Perfetto format, devices might still have old legacy trace files 314 // dangling in the disk that get automatically included into bugreports. Hence, Perfetto parsers 315 // must always override legacy ones so that dangling legacy files are ignored. 316 317 it('when a perfetto parser is already loaded', () => { 318 loadParsers([parserSf0], [parserSf1]); 319 expectLoadResult([parserSf1], [new TraceOverridden(filename)]); 320 userNotifierChecker.reset(); 321 322 loadParsers([parserSf0], []); 323 expectLoadResult([parserSf1], [new TraceOverridden(filename)]); 324 }); 325 326 it('when a perfetto parser is loaded afterwards', () => { 327 loadParsers([parserSf0], []); 328 expectLoadResult([parserSf0], []); 329 330 loadParsers([], [parserSf1]); 331 expectLoadResult([parserSf1], [new TraceOverridden(filename)]); 332 }); 333 }); 334 335 describe('is robust to multiple parsers of same type loaded at once', () => { 336 it('legacy parsers', () => { 337 loadParsers([parserSf0, parserSf1], []); 338 expectLoadResult([parserSf0, parserSf1], []); 339 }); 340 341 it('legacy + perfetto parsers', () => { 342 loadParsers([parserSf0, parserSf0], [parserSf1]); 343 expectLoadResult( 344 [parserSf1], 345 [new TraceOverridden(filename), new TraceOverridden(filename)], 346 ); 347 }); 348 }); 349 350 describe('is robust to parser with no entries', () => { 351 it('legacy parser', () => { 352 loadParsers([parserSf_empty], []); 353 expectLoadResult([parserSf_empty], []); 354 }); 355 356 it('perfetto parser', () => { 357 loadParsers([], [parserSf_empty]); 358 expectLoadResult([parserSf_empty], []); 359 }); 360 }); 361 362 describe('handles screen recordings and screenshots', () => { 363 const parserScreenRecording0 = new ParserBuilder<object>() 364 .setType(TraceType.SCREEN_RECORDING) 365 .setTimestamps(timestamps) 366 .setDescriptors(['screen_recording.mp4']) 367 .build(); 368 const parserScreenRecording1 = new ParserBuilder<object>() 369 .setType(TraceType.SCREEN_RECORDING) 370 .setTimestamps(timestamps) 371 .setDescriptors(['screen_recording.mp4']) 372 .build(); 373 const parserScreenshot0 = new ParserBuilder<object>() 374 .setType(TraceType.SCREENSHOT) 375 .setTimestamps(timestamps) 376 .setDescriptors(['screenshot.png']) 377 .build(); 378 const parserScreenshot1 = new ParserBuilder<object>() 379 .setType(TraceType.SCREENSHOT) 380 .setTimestamps(timestamps) 381 .setDescriptors(['screenshot.png']) 382 .build(); 383 const overrideError = new TraceOverridden( 384 'screenshot.png', 385 TraceType.SCREEN_RECORDING, 386 ); 387 388 it('loads screenshot parser', () => { 389 loadParsers([parserScreenshot0], []); 390 expectLoadResult([parserScreenshot0], []); 391 }); 392 393 it('loads screen recording parser', () => { 394 loadParsers([parserScreenRecording0], []); 395 expectLoadResult([parserScreenRecording0], []); 396 }); 397 398 it('does not load screenshot parser after loading screen recording parser in same call', () => { 399 loadParsers([parserScreenshot0, parserScreenRecording0], []); 400 expectLoadResult([parserScreenRecording0], [overrideError]); 401 }); 402 403 it('does not load screenshot parser after loading screen recording parser in previous call', () => { 404 loadParsers([parserScreenRecording0], []); 405 expectLoadResult([parserScreenRecording0], []); 406 407 loadParsers([parserScreenshot0], []); 408 expectLoadResult([parserScreenRecording0], [overrideError]); 409 }); 410 411 it('overrides previously loaded screenshot parser with screen recording parser', () => { 412 loadParsers([parserScreenshot0], []); 413 expectLoadResult([parserScreenshot0], []); 414 415 loadParsers([parserScreenRecording0], []); 416 expectLoadResult([parserScreenRecording0], [overrideError]); 417 }); 418 419 it('loads multiple screen recordings', () => { 420 loadParsers([parserScreenRecording0], []); 421 expectLoadResult([parserScreenRecording0], []); 422 423 loadParsers([parserScreenRecording1], []); 424 expectLoadResult([parserScreenRecording0, parserScreenRecording1], []); 425 }); 426 }); 427 428 it('can remove parsers', () => { 429 loadParsers([parserSf0], [parserWm0]); 430 expectLoadResult([parserSf0, parserWm0], []); 431 432 loadedParsers.remove(parserWm0); 433 expectLoadResult([parserSf0], []); 434 435 loadedParsers.remove(parserSf0); 436 expectLoadResult([], []); 437 }); 438 439 it('can remove parsers but keep for download', async () => { 440 loadParsers([parserSf0, parserWm0], []); 441 expectLoadResult([parserSf0, parserWm0], []); 442 443 loadedParsers.remove(parserWm0, true); 444 expectLoadResult([parserSf0], []); 445 446 await expectDownloadResult([ 447 'sf/filename.winscope', 448 'wm/filename.winscope', 449 ]); 450 }); 451 452 it('can be cleared', () => { 453 loadedParsers.clear(); 454 loadParsers([parserSf0], [parserWm0]); 455 expectLoadResult([parserSf0, parserWm0], []); 456 457 loadedParsers.clear(); 458 expectLoadResult([], []); 459 460 loadParsers([parserSf0], [parserWm0]); 461 expectLoadResult([parserSf0, parserWm0], []); 462 }); 463 464 it('can make zip archive of traces with appropriate directories and extensions', async () => { 465 const fileDuplicated = new File([], filename); 466 467 const legacyFiles = [ 468 // ScreenRecording 469 new File([], filename), 470 471 // ViewCapture 472 // Multiple parsers point to the same viewcapture file, 473 // but we expect to see only one in the output archive (deduplicated) 474 fileDuplicated, 475 fileDuplicated, 476 477 // WM 478 new File([], filename + '.pb'), 479 480 // WM 481 // Same filename as above. 482 // Expect this file to be automatically renamed to avoid clashes/overwrites 483 new File([], filename + '.pb'), 484 ]; 485 486 loadParsers( 487 [ 488 parserScreenRecording, 489 parserViewCapture0, 490 parserViewCapture1, 491 parserWm0, 492 parserWm1, 493 ], 494 [parserSf0, parserWmTransitions], 495 legacyFiles, 496 ); 497 expectLoadResult( 498 [ 499 parserScreenRecording, 500 parserViewCapture0, 501 parserViewCapture1, 502 parserWm0, 503 parserWm1, 504 parserSf0, 505 parserWmTransitions, 506 ], 507 [], 508 ); 509 510 await expectDownloadResult([ 511 'filename.mp4', 512 'filename.perfetto-trace', 513 'vc/filename.winscope', 514 'wm/filename (1).pb', 515 'wm/filename.pb', 516 ]); 517 }); 518 519 it('makes zip archive with progress listener', async () => { 520 loadParsers([parserSf0], [parserWm0]); 521 expectLoadResult([parserSf0, parserWm0], []); 522 523 const progressSpy = jasmine.createSpy(); 524 await loadedParsers.makeZipArchive(progressSpy); 525 526 expect(progressSpy).toHaveBeenCalledTimes(5); 527 expect(progressSpy).toHaveBeenCalledWith(0); 528 expect(progressSpy).toHaveBeenCalledWith(0.25); 529 expect(progressSpy).toHaveBeenCalledWith(0.5); 530 expect(progressSpy).toHaveBeenCalledWith(0.75); 531 expect(progressSpy).toHaveBeenCalledWith(1); 532 }); 533 534 function loadParsers( 535 legacy: Array<Parser<object>>, 536 perfetto: Array<Parser<object>>, 537 legacyFiles?: File[], 538 ) { 539 const legacyFileAndParsers = legacy.map((parser, i) => { 540 const legacyFile = legacyFiles ? legacyFiles[i] : new File([], filename); 541 return new FileAndParser(new TraceFile(legacyFile), parser); 542 }); 543 544 const perfettoTraceFile = new TraceFile(new File([], filename)); 545 const perfettoFileAndParsers = 546 perfetto.length > 0 547 ? new FileAndParsers(perfettoTraceFile, perfetto) 548 : undefined; 549 550 loadedParsers.addParsers(legacyFileAndParsers, perfettoFileAndParsers); 551 } 552 553 function expectLoadResult( 554 expectedParsers: Array<Parser<object>>, 555 expectedWarnings: UserWarning[], 556 ) { 557 const actualParsers = loadedParsers.getParsers(); 558 expect(actualParsers.length).toEqual(expectedParsers.length); 559 expect(new Set([...actualParsers])).toEqual(new Set([...expectedParsers])); 560 561 userNotifierChecker.expectAdded(expectedWarnings); 562 } 563 564 async function expectDownloadResult(expectedArchiveContents: string[]) { 565 const zipArchive = await loadedParsers.makeZipArchive(); 566 const actualArchiveContents = (await FileUtils.unzipFile(zipArchive)) 567 .map((file) => file.name) 568 .sort(); 569 expect(actualArchiveContents).toEqual(expectedArchiveContents); 570 } 571}); 572