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