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