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 {OnProgressUpdateType} from 'common/function_utils'; 20import {INVALID_TIME_NS, TimeRange, Timestamp} from 'common/time'; 21import {TIME_UNIT_TO_NANO} from 'common/time_units'; 22import {UserNotifier} from 'common/user_notifier'; 23import {TraceHasOldData, TraceOverridden} from 'messaging/user_warnings'; 24import {FileAndParser} from 'parsers/file_and_parser'; 25import {FileAndParsers} from 'parsers/file_and_parsers'; 26import {Parser} from 'trace/parser'; 27import {TraceFile} from 'trace/trace_file'; 28import {TRACE_INFO} from 'trace/trace_info'; 29import {TraceEntryTypeMap, TraceType} from 'trace/trace_type'; 30 31export class LoadedParsers { 32 static readonly MAX_ALLOWED_TIME_GAP_BETWEEN_TRACES_NS = BigInt( 33 5 * TIME_UNIT_TO_NANO.m, 34 ); // 5m 35 static readonly MAX_ALLOWED_TIME_GAP_BETWEEN_RTE_OFFSET = BigInt( 36 5 * TIME_UNIT_TO_NANO.s, 37 ); // 5s 38 static readonly REAL_TIME_TRACES_WITHOUT_RTE_OFFSET = [ 39 TraceType.CUJS, 40 TraceType.EVENT_LOG, 41 ]; 42 43 private legacyParsers = new Array<FileAndParser>(); 44 private perfettoParsers = new Array<FileAndParser>(); 45 private legacyParsersKeptForDownload = new Array<FileAndParser>(); 46 private perfettoParsersKeptForDownload = new Array<FileAndParser>(); 47 48 addParsers( 49 legacyParsers: FileAndParser[], 50 perfettoParsers: FileAndParsers | undefined, 51 ) { 52 if (perfettoParsers) { 53 this.addPerfettoParsers(perfettoParsers); 54 } 55 // Traces were simultaneously upgraded to contain real-to-boottime or real-to-monotonic offsets. 56 // If we have a mix of parsers with and without offsets, the ones without must be dangling 57 // trace files with old data, and should be filtered out. 58 legacyParsers = this.filterOutParsersWithoutOffsetsIfRequired( 59 legacyParsers, 60 perfettoParsers, 61 ); 62 legacyParsers = this.filterOutLegacyParsersWithOldData(legacyParsers); 63 legacyParsers = this.filterScreenshotParsersIfRequired(legacyParsers); 64 65 this.addLegacyParsers(legacyParsers); 66 } 67 68 getParsers(): Array<Parser<object>> { 69 const fileAndParsers = [ 70 ...this.legacyParsers.values(), 71 ...this.perfettoParsers.values(), 72 ]; 73 return fileAndParsers.map((fileAndParser) => fileAndParser.parser); 74 } 75 76 remove<T extends TraceType>( 77 parser: Parser<TraceEntryTypeMap[T]>, 78 keepForDownload = false, 79 ) { 80 const predicate = ( 81 fileAndParser: FileAndParser, 82 parsersToKeep: FileAndParser[], 83 ) => { 84 const shouldRemove = fileAndParser.parser === parser; 85 if (shouldRemove && keepForDownload) { 86 parsersToKeep.push(fileAndParser); 87 } 88 return !shouldRemove; 89 }; 90 this.legacyParsers = this.legacyParsers.filter( 91 (fileAndParser: FileAndParser) => 92 predicate(fileAndParser, this.legacyParsersKeptForDownload), 93 ); 94 this.perfettoParsers = this.perfettoParsers.filter( 95 (fileAndParser: FileAndParser) => 96 predicate(fileAndParser, this.perfettoParsersKeptForDownload), 97 ); 98 } 99 100 clear() { 101 this.legacyParsers = []; 102 this.perfettoParsers = []; 103 } 104 105 async makeZipArchive(onProgressUpdate?: OnProgressUpdateType): Promise<Blob> { 106 const outputFilesSoFar = new Set<File>(); 107 const outputFilenameToFiles = new Map<string, File[]>(); 108 109 if (onProgressUpdate) onProgressUpdate(0); 110 const totalParsers = 111 this.perfettoParsers.length + 112 this.perfettoParsersKeptForDownload.length + 113 this.legacyParsers.length + 114 this.legacyParsersKeptForDownload.length; 115 let progress = 0; 116 117 const tryPushOutputFile = (file: File, filename: string) => { 118 // Remove duplicates because some parsers (e.g. view capture) could share the same file 119 if (outputFilesSoFar.has(file)) { 120 return; 121 } 122 outputFilesSoFar.add(file); 123 124 if (outputFilenameToFiles.get(filename) === undefined) { 125 outputFilenameToFiles.set(filename, []); 126 } 127 assertDefined(outputFilenameToFiles.get(filename)).push(file); 128 }; 129 130 const makeArchiveFile = ( 131 filename: string, 132 file: File, 133 clashCount: number, 134 ): File => { 135 if (clashCount === 0) { 136 return new File([file], filename); 137 } 138 139 const filenameWithoutExt = 140 FileUtils.removeExtensionFromFilename(filename); 141 const extension = FileUtils.getFileExtension(filename); 142 143 if (extension === undefined) { 144 return new File([file], `${filename} (${clashCount})`); 145 } 146 147 return new File( 148 [file], 149 `${filenameWithoutExt} (${clashCount}).${extension}`, 150 ); 151 }; 152 153 const tryPushOutPerfettoFile = (parsers: FileAndParser[]) => { 154 const file: TraceFile = parsers.values().next().value.file; 155 let outputFilename = FileUtils.removeDirFromFileName(file.file.name); 156 if (FileUtils.getFileExtension(file.file.name) === undefined) { 157 outputFilename += '.perfetto-trace'; 158 } 159 tryPushOutputFile(file.file, outputFilename); 160 }; 161 162 if (this.perfettoParsers.length > 0) { 163 tryPushOutPerfettoFile(this.perfettoParsers); 164 } else if (this.perfettoParsersKeptForDownload.length > 0) { 165 tryPushOutPerfettoFile(this.perfettoParsersKeptForDownload); 166 } 167 if (onProgressUpdate) { 168 progress = 169 this.perfettoParsers.length + 170 this.perfettoParsersKeptForDownload.length; 171 onProgressUpdate((0.5 * progress) / totalParsers); 172 } 173 174 const tryPushOutputLegacyFile = (fileAndParser: FileAndParser) => { 175 const {file, parser} = fileAndParser; 176 const traceType = parser.getTraceType(); 177 const archiveDir = 178 TRACE_INFO[traceType].downloadArchiveDir.length > 0 179 ? TRACE_INFO[traceType].downloadArchiveDir + '/' 180 : ''; 181 let outputFilename = 182 archiveDir + FileUtils.removeDirFromFileName(file.file.name); 183 if (FileUtils.getFileExtension(file.file.name) === undefined) { 184 outputFilename += TRACE_INFO[traceType].legacyExt; 185 } 186 tryPushOutputFile(file.file, outputFilename); 187 if (onProgressUpdate) { 188 progress++; 189 onProgressUpdate((0.5 * progress) / totalParsers); 190 } 191 }; 192 193 this.legacyParsers.forEach(tryPushOutputLegacyFile); 194 this.legacyParsersKeptForDownload.forEach(tryPushOutputLegacyFile); 195 196 const archiveFiles = [...outputFilenameToFiles.entries()] 197 .map(([filename, files]) => { 198 return files.map((file, clashCount) => 199 makeArchiveFile(filename, file, clashCount), 200 ); 201 }) 202 .flat(); 203 204 return await FileUtils.createZipArchive( 205 archiveFiles, 206 onProgressUpdate 207 ? (perc: number) => onProgressUpdate(0.5 * (1 + perc)) 208 : undefined, 209 ); 210 } 211 212 getLatestRealToMonotonicOffset( 213 parsers: Array<Parser<object>>, 214 ): bigint | undefined { 215 const p = parsers 216 .filter((offset) => offset.getRealToMonotonicTimeOffsetNs() !== undefined) 217 .sort((a, b) => { 218 return Number( 219 (a.getRealToMonotonicTimeOffsetNs() ?? 0n) - 220 (b.getRealToMonotonicTimeOffsetNs() ?? 0n), 221 ); 222 }) 223 .at(-1); 224 return p?.getRealToMonotonicTimeOffsetNs(); 225 } 226 227 getLatestRealToBootTimeOffset( 228 parsers: Array<Parser<object>>, 229 ): bigint | undefined { 230 const p = parsers 231 .filter((offset) => offset.getRealToBootTimeOffsetNs() !== undefined) 232 .sort((a, b) => { 233 return Number( 234 (a.getRealToBootTimeOffsetNs() ?? 0n) - 235 (b.getRealToBootTimeOffsetNs() ?? 0n), 236 ); 237 }) 238 .at(-1); 239 return p?.getRealToBootTimeOffsetNs(); 240 } 241 242 private addLegacyParsers(parsers: FileAndParser[]) { 243 const legacyParsersBeingLoaded = new Map<TraceType, Parser<object>>(); 244 245 parsers.forEach((fileAndParser) => { 246 const {parser} = fileAndParser; 247 if (this.shouldUseLegacyParser(parser)) { 248 legacyParsersBeingLoaded.set(parser.getTraceType(), parser); 249 this.legacyParsers.push(fileAndParser); 250 } 251 }); 252 } 253 254 private addPerfettoParsers({file, parsers}: FileAndParsers) { 255 // We currently run only one Perfetto TP WebWorker at a time, so Perfetto parsers previously 256 // loaded are now invalid and must be removed (previous WebWorker is not running anymore). 257 this.perfettoParsers = []; 258 259 parsers.forEach((parser) => { 260 this.perfettoParsers.push(new FileAndParser(file, parser)); 261 262 // While transitioning to the Perfetto format, devices might still have old legacy trace files 263 // dangling in the disk that get automatically included into bugreports. Hence, Perfetto 264 // parsers must always override legacy ones so that dangling legacy files are ignored. 265 this.legacyParsers = this.legacyParsers.filter((fileAndParser) => { 266 const isOverriddenByPerfettoParser = 267 fileAndParser.parser.getTraceType() === parser.getTraceType(); 268 if (isOverriddenByPerfettoParser) { 269 UserNotifier.add( 270 new TraceOverridden(fileAndParser.parser.getDescriptors().join()), 271 ); 272 } 273 return !isOverriddenByPerfettoParser; 274 }); 275 }); 276 } 277 278 private shouldUseLegacyParser(newParser: Parser<object>): boolean { 279 // While transitioning to the Perfetto format, devices might still have old legacy trace files 280 // dangling in the disk that get automatically included into bugreports. Hence, Perfetto parsers 281 // must always override legacy ones so that dangling legacy files are ignored. 282 const isOverriddenByPerfettoParser = this.perfettoParsers.some( 283 (fileAndParser) => 284 fileAndParser.parser.getTraceType() === newParser.getTraceType(), 285 ); 286 if (isOverriddenByPerfettoParser) { 287 UserNotifier.add(new TraceOverridden(newParser.getDescriptors().join())); 288 return false; 289 } 290 291 return true; 292 } 293 294 private filterOutLegacyParsersWithOldData( 295 newLegacyParsers: FileAndParser[], 296 ): FileAndParser[] { 297 let allParsers = [ 298 ...newLegacyParsers, 299 ...this.legacyParsers.values(), 300 ...this.perfettoParsers.values(), 301 ]; 302 303 const latestMonotonicOffset = this.getLatestRealToMonotonicOffset( 304 allParsers.map(({parser, file}) => parser), 305 ); 306 const latestBootTimeOffset = this.getLatestRealToBootTimeOffset( 307 allParsers.map(({parser, file}) => parser), 308 ); 309 310 newLegacyParsers = newLegacyParsers.filter(({parser, file}) => { 311 const monotonicOffset = parser.getRealToMonotonicTimeOffsetNs(); 312 if (monotonicOffset && latestMonotonicOffset) { 313 const isOldData = 314 Math.abs(Number(monotonicOffset - latestMonotonicOffset)) > 315 LoadedParsers.MAX_ALLOWED_TIME_GAP_BETWEEN_RTE_OFFSET; 316 if (isOldData) { 317 UserNotifier.add(new TraceHasOldData(file.getDescriptor())); 318 return false; 319 } 320 } 321 322 const bootTimeOffset = parser.getRealToBootTimeOffsetNs(); 323 if (bootTimeOffset && latestBootTimeOffset) { 324 const isOldData = 325 Math.abs(Number(bootTimeOffset - latestBootTimeOffset)) > 326 LoadedParsers.MAX_ALLOWED_TIME_GAP_BETWEEN_RTE_OFFSET; 327 if (isOldData) { 328 UserNotifier.add(new TraceHasOldData(file.getDescriptor())); 329 return false; 330 } 331 } 332 333 return true; 334 }); 335 336 allParsers = [ 337 ...newLegacyParsers, 338 ...this.legacyParsers.values(), 339 ...this.perfettoParsers.values(), 340 ]; 341 342 const timeRanges = allParsers 343 .map(({parser}) => { 344 const timestamps = parser.getTimestamps(); 345 if (!timestamps || timestamps.length === 0) { 346 return undefined; 347 } 348 return new TimeRange(timestamps[0], timestamps[timestamps.length - 1]); 349 }) 350 .filter((range) => range !== undefined) as TimeRange[]; 351 352 const timeGap = this.findLastTimeGapAboveThreshold(timeRanges); 353 if (!timeGap) { 354 return newLegacyParsers; 355 } 356 357 return newLegacyParsers.filter(({parser, file}) => { 358 // Only Shell Transition data used to set timestamps of merged Transition trace, 359 // so WM Transition data should not be considered by "old data" policy 360 if (parser.getTraceType() === TraceType.WM_TRANSITION) { 361 return true; 362 } 363 364 let timestamps = parser.getTimestamps(); 365 if (!this.hasValidTimestamps(timestamps)) { 366 return true; 367 } 368 timestamps = assertDefined(timestamps); 369 370 const endTimestamp = timestamps[timestamps.length - 1]; 371 const isOldData = endTimestamp.getValueNs() <= timeGap.from.getValueNs(); 372 if (isOldData) { 373 UserNotifier.add(new TraceHasOldData(file.getDescriptor(), timeGap)); 374 return false; 375 } 376 377 return true; 378 }); 379 } 380 381 private filterScreenshotParsersIfRequired( 382 newLegacyParsers: FileAndParser[], 383 ): FileAndParser[] { 384 const hasOldScreenRecordingParsers = this.legacyParsers.some( 385 (entry) => entry.parser.getTraceType() === TraceType.SCREEN_RECORDING, 386 ); 387 const hasNewScreenRecordingParsers = newLegacyParsers.some( 388 (entry) => entry.parser.getTraceType() === TraceType.SCREEN_RECORDING, 389 ); 390 const hasScreenRecordingParsers = 391 hasOldScreenRecordingParsers || hasNewScreenRecordingParsers; 392 393 if (!hasScreenRecordingParsers) { 394 return newLegacyParsers; 395 } 396 397 const oldScreenshotParsers = this.legacyParsers.filter( 398 (fileAndParser) => 399 fileAndParser.parser.getTraceType() === TraceType.SCREENSHOT, 400 ); 401 const newScreenshotParsers = newLegacyParsers.filter( 402 (fileAndParser) => 403 fileAndParser.parser.getTraceType() === TraceType.SCREENSHOT, 404 ); 405 406 oldScreenshotParsers.forEach((fileAndParser) => { 407 UserNotifier.add( 408 new TraceOverridden( 409 fileAndParser.parser.getDescriptors().join(), 410 TraceType.SCREEN_RECORDING, 411 ), 412 ); 413 this.remove(fileAndParser.parser); 414 }); 415 416 newScreenshotParsers.forEach((newScreenshotParser) => { 417 UserNotifier.add( 418 new TraceOverridden( 419 newScreenshotParser.parser.getDescriptors().join(), 420 TraceType.SCREEN_RECORDING, 421 ), 422 ); 423 }); 424 425 return newLegacyParsers.filter( 426 (fileAndParser) => 427 fileAndParser.parser.getTraceType() !== TraceType.SCREENSHOT, 428 ); 429 } 430 431 private filterOutParsersWithoutOffsetsIfRequired( 432 newLegacyParsers: FileAndParser[], 433 perfettoParsers: FileAndParsers | undefined, 434 ): FileAndParser[] { 435 const hasParserWithOffset = 436 perfettoParsers || 437 newLegacyParsers.find(({parser, file}) => { 438 return ( 439 parser.getRealToBootTimeOffsetNs() !== undefined || 440 parser.getRealToMonotonicTimeOffsetNs() !== undefined 441 ); 442 }); 443 const hasParserWithoutOffset = newLegacyParsers.find(({parser, file}) => { 444 const timestamps = parser.getTimestamps(); 445 return ( 446 this.hasValidTimestamps(timestamps) && 447 parser.getRealToBootTimeOffsetNs() === undefined && 448 parser.getRealToMonotonicTimeOffsetNs() === undefined 449 ); 450 }); 451 452 if (hasParserWithOffset && hasParserWithoutOffset) { 453 return newLegacyParsers.filter(({parser, file}) => { 454 if ( 455 LoadedParsers.REAL_TIME_TRACES_WITHOUT_RTE_OFFSET.some( 456 (traceType) => parser.getTraceType() === traceType, 457 ) 458 ) { 459 return true; 460 } 461 const hasOffset = 462 parser.getRealToMonotonicTimeOffsetNs() !== undefined || 463 parser.getRealToBootTimeOffsetNs() !== undefined; 464 if (!hasOffset) { 465 UserNotifier.add(new TraceHasOldData(parser.getDescriptors().join())); 466 } 467 return hasOffset; 468 }); 469 } 470 471 return newLegacyParsers; 472 } 473 474 private findLastTimeGapAboveThreshold( 475 ranges: readonly TimeRange[], 476 ): TimeRange | undefined { 477 const rangesSortedByEnd = ranges 478 .slice() 479 .sort((a, b) => (a.to.getValueNs() < b.to.getValueNs() ? -1 : +1)); 480 481 for (let i = rangesSortedByEnd.length - 2; i >= 0; --i) { 482 const curr = rangesSortedByEnd[i]; 483 const next = rangesSortedByEnd[i + 1]; 484 const gap = next.from.getValueNs() - curr.to.getValueNs(); 485 if (gap > LoadedParsers.MAX_ALLOWED_TIME_GAP_BETWEEN_TRACES_NS) { 486 return new TimeRange(curr.to, next.from); 487 } 488 } 489 490 return undefined; 491 } 492 493 private hasValidTimestamps(timestamps: Timestamp[] | undefined): boolean { 494 if (!timestamps || timestamps.length === 0) { 495 return false; 496 } 497 498 const isDump = 499 timestamps.length === 1 && timestamps[0].getValueNs() === INVALID_TIME_NS; 500 if (isDump) { 501 return false; 502 } 503 return true; 504 } 505} 506