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 {TimezoneInfo} from 'common/time'; 20import {UserNotifier} from 'common/user_notifier'; 21import {TraceOverridden} from 'messaging/user_warnings'; 22import {TraceFile} from 'trace/trace_file'; 23import {TraceMetadata} from 'trace/trace_metadata'; 24 25export interface FilterResult { 26 legacy: TraceFile[]; 27 metadata: TraceMetadata; 28 perfetto?: TraceFile; 29 timezoneInfo?: TimezoneInfo; 30} 31 32export class TraceFileFilter { 33 private static readonly BUGREPORT_SYSTRACE_PATH = 34 'FS/data/misc/perfetto-traces/bugreport/systrace.pftrace'; 35 private static readonly BUGREPORT_LEGACY_FILES_ALLOWLIST = [ 36 'FS/data/misc/wmtrace/', 37 'FS/data/misc/perfetto-traces/', 38 'proto/window_CRITICAL.proto', 39 'proto/input_method_CRITICAL.proto', 40 'proto/SurfaceFlinger_CRITICAL.proto', 41 ]; 42 private static readonly PERFETTO_EXTENSIONS = [ 43 '.pftrace', 44 '.perfetto-trace', 45 '.perfetto', 46 ]; 47 48 async filter(files: TraceFile[]): Promise<FilterResult> { 49 const bugreportMainEntry = files.find((file) => 50 file.file.name.endsWith('main_entry.txt'), 51 ); 52 53 const perfettoFiles = files.filter((file) => this.isPerfettoFile(file)); 54 const {mFiles, metadata} = await this.extractAndAnalyzeMetadata(files); 55 const legacyFiles = files.filter( 56 (file) => !this.isPerfettoFile(file) && !mFiles.includes(file), 57 ); 58 59 if (!(await this.isBugreport(bugreportMainEntry, files))) { 60 const perfettoFile = this.pickLargestFile(perfettoFiles); 61 return { 62 perfetto: perfettoFile, 63 legacy: legacyFiles, 64 metadata, 65 }; 66 } 67 68 const timezoneInfo = await this.processRawBugReport( 69 assertDefined(bugreportMainEntry), 70 files, 71 ); 72 73 return await this.filterBugreport( 74 assertDefined(bugreportMainEntry), 75 perfettoFiles, 76 legacyFiles, 77 metadata, 78 timezoneInfo, 79 ); 80 } 81 82 private async processRawBugReport( 83 bugreportMainEntry: TraceFile, 84 files: TraceFile[], 85 ): Promise<TimezoneInfo | undefined> { 86 const bugreportName = (await bugreportMainEntry.file.text()).trim(); 87 const rawBugReport = files.find((file) => file.file.name === bugreportName); 88 if (!rawBugReport) { 89 return undefined; 90 } 91 92 const traceBuffer = new Uint8Array(await rawBugReport.file.arrayBuffer()); 93 const fileData = new TextDecoder().decode(traceBuffer); 94 95 const timezoneStartIndex = fileData.indexOf('[persist.sys.timezone]'); 96 if (timezoneStartIndex === -1) { 97 return undefined; 98 } 99 const timezone = this.extractValueFromRawBugReport( 100 fileData, 101 timezoneStartIndex, 102 ); 103 104 return {timezone, locale: 'en-US'}; 105 } 106 107 private extractValueFromRawBugReport( 108 fileData: string, 109 startIndex: number, 110 ): string { 111 return fileData 112 .slice(startIndex) 113 .split(']', 2) 114 .map((substr) => { 115 const start = substr.lastIndexOf('['); 116 return substr.slice(start + 1); 117 })[1]; 118 } 119 120 private async isBugreport( 121 bugreportMainEntry: TraceFile | undefined, 122 files: TraceFile[], 123 ): Promise<boolean> { 124 if (!bugreportMainEntry) { 125 return false; 126 } 127 const bugreportName = (await bugreportMainEntry.file.text()).trim(); 128 return ( 129 files.find((file) => { 130 return ( 131 file.parentArchive === bugreportMainEntry.parentArchive && 132 file.file.name === bugreportName 133 ); 134 }) !== undefined 135 ); 136 } 137 138 private async filterBugreport( 139 bugreportMainEntry: TraceFile, 140 perfettoFiles: TraceFile[], 141 legacyFiles: TraceFile[], 142 metadata: TraceMetadata, 143 timezoneInfo?: TimezoneInfo, 144 ): Promise<FilterResult> { 145 const isFileAllowlisted = (file: TraceFile) => { 146 for (const traceDir of TraceFileFilter.BUGREPORT_LEGACY_FILES_ALLOWLIST) { 147 if (file.file.name.startsWith(traceDir)) { 148 return true; 149 } 150 } 151 return false; 152 }; 153 154 const fileBelongsToBugreport = (file: TraceFile) => 155 file.parentArchive === bugreportMainEntry.parentArchive; 156 157 legacyFiles = legacyFiles.filter((file) => { 158 return isFileAllowlisted(file) || !fileBelongsToBugreport(file); 159 }); 160 161 const unzippedLegacyFiles: TraceFile[] = []; 162 163 for (const file of legacyFiles) { 164 if (await FileUtils.isZipFile(file.file)) { 165 try { 166 const subFiles = await FileUtils.unzipFile(file.file); 167 const subTraceFiles = subFiles.map((subFile) => { 168 return new TraceFile(subFile, file.file); 169 }); 170 unzippedLegacyFiles.push(...subTraceFiles); 171 } catch { 172 unzippedLegacyFiles.push(file); 173 } 174 } else { 175 unzippedLegacyFiles.push(file); 176 } 177 } 178 const perfettoFile = perfettoFiles.find( 179 (file) => file.file.name === TraceFileFilter.BUGREPORT_SYSTRACE_PATH, 180 ); 181 return { 182 perfetto: perfettoFile, 183 legacy: unzippedLegacyFiles, 184 metadata, 185 timezoneInfo, 186 }; 187 } 188 189 private isPerfettoFile(file: TraceFile): boolean { 190 return TraceFileFilter.PERFETTO_EXTENSIONS.some((perfettoExt) => { 191 return ( 192 file.file.name.endsWith(perfettoExt) || 193 file.file.name.endsWith(`${perfettoExt}.gz`) 194 ); 195 }); 196 } 197 198 private async extractAndAnalyzeMetadata( 199 files: TraceFile[], 200 ): Promise<{mFiles: TraceFile[]; metadata: TraceMetadata}> { 201 const mFiles = []; 202 const metadata: TraceMetadata = {}; 203 for (const file of files) { 204 const buffer = new Uint8Array(await file.file.arrayBuffer()); 205 const text = new TextDecoder().decode(buffer); 206 try { 207 const data = JSON.parse(text); 208 if ( 209 data.realToElapsedTimeOffsetNanos !== undefined && 210 data.elapsedRealTimeNanos !== undefined 211 ) { 212 metadata.screenRecordingOffsets = { 213 realToElapsedTimeOffsetNanos: BigInt( 214 data.realToElapsedTimeOffsetNanos, 215 ), 216 elapsedRealTimeNanos: BigInt(data.elapsedRealTimeNanos), 217 }; 218 mFiles.push(file); 219 break; 220 } 221 } catch (e) { 222 // swallow - looking for metadata json 223 } 224 } 225 return {metadata, mFiles}; 226 } 227 228 private pickLargestFile(files: TraceFile[]): TraceFile | undefined { 229 if (files.length === 0) { 230 return undefined; 231 } 232 return files.reduce((largestSoFar, file) => { 233 const [largest, overridden] = 234 largestSoFar.file.size > file.file.size 235 ? [largestSoFar, file] 236 : [file, largestSoFar]; 237 UserNotifier.add(new TraceOverridden(overridden.getDescriptor())); 238 return largest; 239 }); 240 } 241} 242