xref: /aosp_15_r20/development/tools/winscope/src/app/trace_file_filter.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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