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 {assertDefined} from 'common/assert_utils'; 18import {FileUtils} from 'common/file_utils'; 19import { 20 TimestampConverterUtils, 21 timestampEqualityTester, 22} from 'common/time/test_utils'; 23import {ProgressListenerStub} from 'messaging/progress_listener_stub'; 24import {UserWarning} from 'messaging/user_warning'; 25import { 26 CorruptedArchive, 27 InvalidPerfettoTrace, 28 NoValidFiles, 29 PerfettoPacketLoss, 30 TraceOverridden, 31 UnsupportedFileFormat, 32} from 'messaging/user_warnings'; 33import {getFixtureFile} from 'test/unit/fixture_utils'; 34import {TracesUtils} from 'test/unit/traces_utils'; 35import {UserNotifierChecker} from 'test/unit/user_notifier_checker'; 36import {TraceType} from 'trace/trace_type'; 37import {QueryResult} from 'trace_processor/query_result'; 38import {TraceProcessor} from 'trace_processor/trace_processor'; 39import {FilesSource} from './files_source'; 40import {TracePipeline} from './trace_pipeline'; 41 42describe('TracePipeline', () => { 43 let validSfFile: File; 44 let validWmFile: File; 45 let shellTransitionFile: File; 46 let wmTransitionFile: File; 47 let screenshotFile: File; 48 let screenRecordingFile: File; 49 let brMainEntryFile: File; 50 let brCodenameFile: File; 51 let brSfFile: File; 52 let jpgFile: File; 53 54 let progressListener: ProgressListenerStub; 55 let tracePipeline: TracePipeline; 56 let userNotifierChecker: UserNotifierChecker; 57 58 beforeAll(async () => { 59 userNotifierChecker = new UserNotifierChecker(); 60 wmTransitionFile = await getFixtureFile( 61 'traces/elapsed_and_real_timestamp/wm_transition_trace.pb', 62 ); 63 shellTransitionFile = await getFixtureFile( 64 'traces/elapsed_and_real_timestamp/shell_transition_trace.pb', 65 ); 66 validSfFile = await getFixtureFile( 67 'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb', 68 ); 69 validWmFile = await getFixtureFile( 70 'traces/elapsed_and_real_timestamp/WindowManager.pb', 71 ); 72 screenshotFile = await getFixtureFile('traces/screenshot.png'); 73 screenRecordingFile = await getFixtureFile( 74 'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4', 75 ); 76 brMainEntryFile = await getFixtureFile( 77 'bugreports/main_entry.txt', 78 'main_entry.txt', 79 ); 80 brCodenameFile = await getFixtureFile( 81 'bugreports/bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt', 82 'bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt', 83 ); 84 brSfFile = await getFixtureFile( 85 'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb', 86 'FS/data/misc/wmtrace/surface_flinger.bp', 87 ); 88 jpgFile = await getFixtureFile('winscope_homepage.jpg'); 89 }); 90 91 beforeEach(async () => { 92 jasmine.addCustomEqualityTester(timestampEqualityTester); 93 94 progressListener = new ProgressListenerStub(); 95 spyOn(progressListener, 'onProgressUpdate'); 96 spyOn(progressListener, 'onOperationFinished'); 97 userNotifierChecker.reset(); 98 99 tracePipeline = new TracePipeline(); 100 }); 101 102 it('can load valid trace files', async () => { 103 expect(tracePipeline.getTraces().getSize()).toEqual(0); 104 105 await loadFiles([validSfFile, validWmFile], FilesSource.TEST); 106 await expectLoadResult(2, []); 107 108 expect(tracePipeline.getDownloadArchiveFilename()).toMatch( 109 new RegExp(`${FilesSource.TEST}_`), 110 ); 111 expect(tracePipeline.getTraces().getSize()).toEqual(2); 112 113 const traceEntries = await TracesUtils.extractEntries( 114 tracePipeline.getTraces(), 115 ); 116 expect(traceEntries.get(TraceType.WINDOW_MANAGER)?.length).toBeGreaterThan( 117 0, 118 ); 119 expect(traceEntries.get(TraceType.SURFACE_FLINGER)?.length).toBeGreaterThan( 120 0, 121 ); 122 }); 123 124 it('can load valid gzipped file and archive', async () => { 125 expect(tracePipeline.getTraces().getSize()).toEqual(0); 126 127 const gzippedFile = await getFixtureFile('traces/WindowManager.pb.gz'); 128 const gzippedArchive = await getFixtureFile('traces/WindowManager.zip.gz'); 129 130 await loadFiles([gzippedFile, gzippedArchive], FilesSource.TEST); 131 await expectLoadResult(2, []); 132 133 const traces = tracePipeline.getTraces(); 134 expect(traces.getSize()).toEqual(2); 135 expect(traces.getTraces(TraceType.WINDOW_MANAGER).length).toEqual(2); 136 137 const traceEntries = await TracesUtils.extractEntries(traces); 138 expect(traceEntries.get(TraceType.WINDOW_MANAGER)?.length).toBeGreaterThan( 139 0, 140 ); 141 }); 142 143 it('can set download archive filename based on files source', async () => { 144 await loadFiles([validSfFile]); 145 await expectLoadResult(1, []); 146 expect(tracePipeline.getDownloadArchiveFilename()).toMatch( 147 new RegExp('SurfaceFlinger_'), 148 ); 149 150 tracePipeline.clear(); 151 152 await loadFiles([validSfFile, validWmFile], FilesSource.COLLECTED); 153 await expectLoadResult(2, []); 154 expect(tracePipeline.getDownloadArchiveFilename()).toMatch( 155 new RegExp(`${FilesSource.COLLECTED}_`), 156 ); 157 }); 158 159 it('can convert illegal uploaded archive filename to legal name for download archive', async () => { 160 const fileWithIllegalName = await getFixtureFile( 161 'traces/SFtrace(with_illegal_characters).pb', 162 ); 163 await loadFiles([fileWithIllegalName]); 164 await expectLoadResult(1, []); 165 const downloadFilename = tracePipeline.getDownloadArchiveFilename(); 166 expect(FileUtils.DOWNLOAD_FILENAME_REGEX.test(downloadFilename)).toBeTrue(); 167 }); 168 169 it('detects bugreports and filters out files based on their directory', async () => { 170 expect(tracePipeline.getTraces().getSize()).toEqual(0); 171 172 const bugreportFiles = [ 173 brMainEntryFile, 174 brCodenameFile, 175 brSfFile, 176 await getFixtureFile( 177 'traces/elapsed_and_real_timestamp/wm_transition_trace.pb', 178 'FS/data/misc/ignored-dir/window_manager.bp', 179 ), 180 ]; 181 182 const bugreportArchive = new File( 183 [await FileUtils.createZipArchive(bugreportFiles)], 184 'bugreport.zip', 185 ); 186 187 // Corner case: 188 // Another file is loaded along the bugreport -> the file must not be ignored 189 // 190 // Note: 191 // The even weirder corner case where two bugreports are loaded at the same time is 192 // currently not properly handled. 193 const otherFile = await getFixtureFile( 194 'traces/elapsed_and_real_timestamp/InputMethodClients.pb', 195 'would-be-ignored-if-was-in-bugreport-archive/input_method_clients.pb', 196 ); 197 198 await loadFiles([bugreportArchive, otherFile]); 199 await expectLoadResult(2, []); 200 201 const traces = tracePipeline.getTraces(); 202 expect(traces.getTrace(TraceType.SURFACE_FLINGER)).toBeDefined(); 203 expect(traces.getTrace(TraceType.WINDOW_MANAGER)).toBeUndefined(); // ignored 204 expect(traces.getTrace(TraceType.INPUT_METHOD_CLIENTS)).toBeDefined(); 205 }); 206 207 it('detects bugreports and extracts timezone info, then calculates utc offset', async () => { 208 const bugreportFiles = [brMainEntryFile, brCodenameFile, brSfFile]; 209 const bugreportArchive = new File( 210 [await FileUtils.createZipArchive(bugreportFiles)], 211 'bugreport.zip', 212 ); 213 214 await loadFiles([bugreportArchive]); 215 await expectLoadResult(1, []); 216 217 const timestampConverter = tracePipeline.getTimestampConverter(); 218 expect(timestampConverter); 219 expect(timestampConverter.getUTCOffset()).toEqual('UTC+05:30'); 220 221 const expectedTimestamp = 222 TimestampConverterUtils.makeRealTimestampWithUTCOffset( 223 1659107089102062832n, 224 ); 225 expect( 226 timestampConverter.makeTimestampFromMonotonicNs(14500282843n), 227 ).toEqual(expectedTimestamp); 228 }); 229 230 it('is robust to corrupted archive', async () => { 231 const corruptedArchive = await getFixtureFile('corrupted_archive.zip'); 232 233 await loadFiles([corruptedArchive]); 234 235 await expectLoadResult(0, [ 236 new CorruptedArchive(corruptedArchive), 237 new NoValidFiles(), 238 ]); 239 }); 240 241 it('is robust to invalid trace files', async () => { 242 const invalidFiles = [jpgFile]; 243 await loadFiles(invalidFiles); 244 245 await expectLoadResult(0, [ 246 new UnsupportedFileFormat('winscope_homepage.jpg'), 247 ]); 248 }); 249 250 it('is robust to invalid perfetto trace files', async () => { 251 const invalidFiles = [ 252 await getFixtureFile('traces/perfetto/invalid_protolog.perfetto-trace'), 253 ]; 254 255 await loadFiles(invalidFiles); 256 257 await expectLoadResult(0, [ 258 new InvalidPerfettoTrace('invalid_protolog.perfetto-trace', [ 259 'Perfetto trace has no Winscope trace entries', 260 ]), 261 ]); 262 }); 263 264 it('shows warning for packet loss', async () => { 265 const file = [ 266 await getFixtureFile('traces/perfetto/layers_trace.perfetto-trace'), 267 ]; 268 const queryResultObj = jasmine.createSpyObj<QueryResult>('result', [ 269 'numRows', 270 'firstRow', 271 'waitAllRows', 272 ]); 273 queryResultObj.numRows.and.returnValue(1); 274 queryResultObj.firstRow.and.returnValue({value: 2n}); 275 queryResultObj.waitAllRows.and.returnValue(Promise.resolve(queryResultObj)); 276 277 const spy = spyOn( 278 TraceProcessor.prototype, 279 'queryAllRows', 280 ).and.callThrough(); 281 spy 282 .withArgs( 283 "select name, value from stats where name = 'traced_buf_trace_writer_packet_loss'", 284 ) 285 .and.returnValue(Promise.resolve(queryResultObj)); 286 287 await loadFiles(file); 288 await expectLoadResult(1, [ 289 new PerfettoPacketLoss('layers_trace.perfetto-trace', 2), 290 ]); 291 }); 292 293 it('is robust to mixed valid and invalid trace files', async () => { 294 expect(tracePipeline.getTraces().getSize()).toEqual(0); 295 const files = [ 296 jpgFile, 297 await getFixtureFile('traces/dump_WindowManager.pb'), 298 ]; 299 300 await loadFiles(files); 301 302 await expectLoadResult(1, [ 303 new UnsupportedFileFormat('winscope_homepage.jpg'), 304 ]); 305 }); 306 307 it('can remove traces', async () => { 308 await loadFiles([validSfFile, validWmFile]); 309 await expectLoadResult(2, []); 310 311 const sfTrace = assertDefined( 312 tracePipeline.getTraces().getTrace(TraceType.SURFACE_FLINGER), 313 ); 314 const wmTrace = assertDefined( 315 tracePipeline.getTraces().getTrace(TraceType.WINDOW_MANAGER), 316 ); 317 318 tracePipeline.removeTrace(sfTrace); 319 await expectLoadResult(1, []); 320 321 tracePipeline.removeTrace(wmTrace); 322 await expectLoadResult(0, []); 323 }); 324 325 it('removes constituent traces of transitions trace but keeps for download', async () => { 326 const files = [wmTransitionFile, wmTransitionFile, shellTransitionFile]; 327 await loadFiles(files); 328 await expectLoadResult(1, []); 329 330 const transitionTrace = assertDefined( 331 tracePipeline.getTraces().getTrace(TraceType.TRANSITION), 332 ); 333 334 tracePipeline.removeTrace(transitionTrace); 335 await expectLoadResult(0, []); 336 337 await loadFiles([wmTransitionFile]); 338 await expectLoadResult(1, []); 339 expect( 340 tracePipeline.getTraces().getTrace(TraceType.WM_TRANSITION), 341 ).toBeDefined(); 342 await expectDownloadResult([ 343 'transition/shell_transition_trace.pb', 344 'transition/wm_transition_trace.pb', 345 ]); 346 }); 347 348 it('removes constituent traces of CUJs trace but keeps for download', async () => { 349 const files = [await getFixtureFile('traces/eventlog.winscope')]; 350 await loadFiles(files); 351 await expectLoadResult(1, []); 352 353 const cujTrace = assertDefined( 354 tracePipeline.getTraces().getTrace(TraceType.CUJS), 355 ); 356 357 tracePipeline.removeTrace(cujTrace); 358 await expectLoadResult(0, []); 359 await expectDownloadResult(['eventlog/eventlog.winscope']); 360 }); 361 362 it('removes constituent traces of input trace but keeps for download', async () => { 363 const files = [ 364 await getFixtureFile('traces/perfetto/input-events.perfetto-trace'), 365 ]; 366 await loadFiles(files); 367 await expectLoadResult(1, []); 368 369 const inputTrace = assertDefined( 370 tracePipeline.getTraces().getTrace(TraceType.INPUT_EVENT_MERGED), 371 ); 372 373 tracePipeline.removeTrace(inputTrace); 374 await expectLoadResult(0, []); 375 await expectDownloadResult(['input-events.perfetto-trace']); 376 }); 377 378 it('gets loaded traces', async () => { 379 await loadFiles([validSfFile, validWmFile]); 380 await expectLoadResult(2, []); 381 382 const traces = tracePipeline.getTraces(); 383 384 const actualTraceTypes = new Set(traces.mapTrace((trace) => trace.type)); 385 const expectedTraceTypes = new Set([ 386 TraceType.SURFACE_FLINGER, 387 TraceType.WINDOW_MANAGER, 388 ]); 389 expect(actualTraceTypes).toEqual(expectedTraceTypes); 390 391 const sfTrace = assertDefined(traces.getTrace(TraceType.SURFACE_FLINGER)); 392 expect(sfTrace.getDescriptors().length).toBeGreaterThan(0); 393 }); 394 395 it('gets screenrecording data', async () => { 396 const files = [screenRecordingFile]; 397 await loadFiles(files); 398 await expectLoadResult(1, []); 399 400 const video = await tracePipeline.getScreenRecordingVideo(); 401 expect(video).toBeDefined(); 402 expect(video?.size).toBeGreaterThan(0); 403 }); 404 405 it('gets screenshot data', async () => { 406 const files = [screenshotFile]; 407 await loadFiles(files); 408 await expectLoadResult(1, []); 409 410 const video = await tracePipeline.getScreenRecordingVideo(); 411 expect(video).toBeDefined(); 412 expect(video?.size).toBeGreaterThan(0); 413 }); 414 415 it('prioritizes screenrecording over screenshot data', async () => { 416 const files = [screenshotFile, screenRecordingFile]; 417 await loadFiles(files); 418 await expectLoadResult(1, [ 419 new TraceOverridden('screenshot.png', TraceType.SCREEN_RECORDING), 420 ]); 421 422 const video = await tracePipeline.getScreenRecordingVideo(); 423 expect(video).toBeDefined(); 424 expect(video?.size).toBeGreaterThan(0); 425 }); 426 427 it('creates traces with correct type', async () => { 428 await loadFiles([validSfFile, validWmFile]); 429 await expectLoadResult(2, []); 430 431 const traces = tracePipeline.getTraces(); 432 traces.forEachTrace((trace, type) => { 433 expect(trace.type).toEqual(type); 434 }); 435 }); 436 437 it('creates zip archive with loaded trace files', async () => { 438 const files = [ 439 screenRecordingFile, 440 await getFixtureFile('traces/perfetto/transactions_trace.perfetto-trace'), 441 ]; 442 await loadFiles(files); 443 await expectLoadResult(2, []); 444 445 await expectDownloadResult([ 446 'screen_recording_metadata_v2.mp4', 447 'transactions_trace.perfetto-trace', 448 ]); 449 }); 450 451 it('can be cleared', async () => { 452 await loadFiles([validSfFile, validWmFile]); 453 await expectLoadResult(2, []); 454 455 tracePipeline.clear(); 456 expect(tracePipeline.getTraces().getSize()).toEqual(0); 457 }); 458 459 it('can filter traces without visualization', async () => { 460 await loadFiles([shellTransitionFile]); 461 await expectLoadResult(1, []); 462 463 tracePipeline.filterTracesWithoutVisualization(); 464 expect(tracePipeline.getTraces().getSize()).toEqual(0); 465 expect( 466 tracePipeline.getTraces().getTrace(TraceType.SHELL_TRANSITION), 467 ).toBeUndefined(); 468 }); 469 470 it('tries to create search trace', async () => { 471 const perfettoFile = await getFixtureFile( 472 'traces/perfetto/layers_trace.perfetto-trace', 473 ); 474 await loadFiles([perfettoFile]); 475 const validQuery = 'select ts from surfaceflinger_layers_snapshot'; 476 expect(await tracePipeline.tryCreateSearchTrace(validQuery)).toBeDefined(); 477 expect(await tracePipeline.tryCreateSearchTrace('fail')).toBeUndefined(); 478 }); 479 480 it('creates screen recording using metadata', async () => { 481 const screenRecording = await getFixtureFile( 482 'traces/elapsed_and_real_timestamp/screen_recording_no_metadata.mp4', 483 ); 484 const metadata = await getFixtureFile( 485 'traces/elapsed_and_real_timestamp/screen_recording_metadata.json', 486 ); 487 await loadFiles([screenRecording, metadata]); 488 await expectLoadResult(1, []); 489 }); 490 491 async function loadFiles( 492 files: File[], 493 source: FilesSource = FilesSource.TEST, 494 ) { 495 await tracePipeline.loadFiles(files, source, progressListener); 496 expect(progressListener.onOperationFinished).toHaveBeenCalled(); 497 await tracePipeline.buildTraces(); 498 } 499 500 async function expectLoadResult( 501 numberOfTraces: number, 502 expectedWarnings: UserWarning[], 503 ) { 504 userNotifierChecker.expectAdded(expectedWarnings); 505 expect(tracePipeline.getTraces().getSize()).toEqual(numberOfTraces); 506 } 507 508 async function expectDownloadResult(expectedArchiveContents: string[]) { 509 const zipArchive = await tracePipeline.makeZipArchiveWithLoadedTraceFiles(); 510 const actualArchiveContents = (await FileUtils.unzipFile(zipArchive)) 511 .map((file) => file.name) 512 .sort(); 513 expect(actualArchiveContents).toEqual(expectedArchiveContents); 514 } 515}); 516