• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {FrameMapBuilder} from './frame_map_builder';
19import {FramesRange, TraceEntry} from './trace';
20import {Traces} from './traces';
21import {TraceType} from './trace_type';
22
23export class FrameMapper {
24  // Value used to narrow time-based searches of corresponding trace entries
25  private static readonly MAX_UI_PIPELINE_LATENCY_NS = 2000000000n; // 2 seconds
26
27  constructor(private traces: Traces) {}
28
29  async computeMapping() {
30    this.pickMostReliableTraceAndSetInitialFrameInfo();
31    await this.propagateFrameInfoToOtherTraces();
32  }
33
34  private pickMostReliableTraceAndSetInitialFrameInfo() {
35    const TRACES_IN_PREFERENCE_ORDER = [
36      TraceType.SCREEN_RECORDING,
37      TraceType.SURFACE_FLINGER,
38      TraceType.WINDOW_MANAGER,
39    ];
40
41    const type = TRACES_IN_PREFERENCE_ORDER.find(
42      (type) => this.traces.getTrace(type) !== undefined
43    );
44    if (type === undefined) {
45      return;
46    }
47
48    const trace = assertDefined(this.traces.getTrace(type));
49    const frameMapBuilder = new FrameMapBuilder(trace.lengthEntries, trace.lengthEntries);
50
51    for (let i = 0; i < trace.lengthEntries; ++i) {
52      frameMapBuilder.setFrames(i, {start: i, end: i + 1});
53    }
54
55    const frameMap = frameMapBuilder.build();
56    trace.setFrameInfo(frameMap, frameMap.getFullTraceFramesRange());
57  }
58
59  private async propagateFrameInfoToOtherTraces() {
60    this.tryPropagateFromScreenRecordingToSurfaceFlinger();
61    await this.tryPropagateFromSurfaceFlingerToTransactions();
62    this.tryPropagateFromTransactionsToWindowManager();
63    this.tryPropagateFromWindowManagerToProtoLog();
64    this.tryPropagateFromWindowManagerToIme();
65  }
66
67  private tryPropagateFromScreenRecordingToSurfaceFlinger() {
68    const frameMapBuilder = this.tryStartFrameMapping(
69      TraceType.SCREEN_RECORDING,
70      TraceType.SURFACE_FLINGER
71    );
72    if (!frameMapBuilder) {
73      return;
74    }
75
76    const screenRecording = assertDefined(this.traces.getTrace(TraceType.SCREEN_RECORDING));
77    const surfaceFlinger = assertDefined(this.traces.getTrace(TraceType.SURFACE_FLINGER));
78
79    screenRecording.forEachEntry((srcEntry) => {
80      const startSearchTime = srcEntry.getTimestamp().add(-FrameMapper.MAX_UI_PIPELINE_LATENCY_NS);
81      const endSearchTime = srcEntry.getTimestamp();
82      const matches = surfaceFlinger.sliceTime(startSearchTime, endSearchTime);
83      if (matches.lengthEntries > 0) {
84        const dstEntry = matches.getEntry(matches.lengthEntries - 1);
85        frameMapBuilder.setFrames(dstEntry.getIndex(), srcEntry.getFramesRange());
86      }
87    });
88
89    const frameMap = frameMapBuilder.build();
90    surfaceFlinger.setFrameInfo(frameMap, frameMap.getFullTraceFramesRange());
91  }
92
93  private async tryPropagateFromSurfaceFlingerToTransactions() {
94    const frameMapBuilder = this.tryStartFrameMapping(
95      TraceType.SURFACE_FLINGER,
96      TraceType.TRANSACTIONS
97    );
98    if (!frameMapBuilder) {
99      return;
100    }
101
102    const transactions = assertDefined(this.traces.getTrace(TraceType.TRANSACTIONS));
103    const surfaceFlinger = assertDefined(this.traces.getTrace(TraceType.SURFACE_FLINGER));
104
105    const vsyncIdToFrames = new Map<bigint, FramesRange>();
106
107    for (let srcEntryIndex = 0; srcEntryIndex < surfaceFlinger.lengthEntries; ++srcEntryIndex) {
108      const srcEntry = surfaceFlinger.getEntry(srcEntryIndex);
109      const vsyncId = await this.getVsyncIdProperty(srcEntry, 'vSyncId');
110      if (vsyncId === undefined) {
111        continue;
112      }
113      const srcFrames = srcEntry.getFramesRange();
114      if (!srcFrames) {
115        continue;
116      }
117      let frames = vsyncIdToFrames.get(vsyncId);
118      if (!frames) {
119        frames = {start: Number.MAX_VALUE, end: Number.MIN_VALUE};
120      }
121      frames.start = Math.min(frames.start, srcFrames.start);
122      frames.end = Math.max(frames.end, srcFrames.end);
123      vsyncIdToFrames.set(vsyncId, frames);
124    }
125
126    for (let dstEntryIndex = 0; dstEntryIndex < transactions.lengthEntries; ++dstEntryIndex) {
127      const dstEntry = transactions.getEntry(dstEntryIndex);
128      const vsyncId = await this.getVsyncIdProperty(dstEntry, 'vsyncId');
129      if (vsyncId === undefined) {
130        continue;
131      }
132      const frames = vsyncIdToFrames.get(vsyncId);
133      if (frames === undefined) {
134        continue;
135      }
136      frameMapBuilder.setFrames(dstEntry.getIndex(), frames);
137    }
138
139    const frameMap = frameMapBuilder.build();
140    transactions.setFrameInfo(frameMap, frameMap.getFullTraceFramesRange());
141  }
142
143  private tryPropagateFromTransactionsToWindowManager() {
144    const frameMapBuilder = this.tryStartFrameMapping(
145      TraceType.TRANSACTIONS,
146      TraceType.WINDOW_MANAGER
147    );
148    if (!frameMapBuilder) {
149      return;
150    }
151
152    const windowManager = assertDefined(this.traces.getTrace(TraceType.WINDOW_MANAGER));
153    const transactions = assertDefined(this.traces.getTrace(TraceType.TRANSACTIONS));
154
155    let prevWindowManagerEntry: TraceEntry<object> | undefined;
156    windowManager.forEachEntry((windowManagerEntry) => {
157      if (prevWindowManagerEntry) {
158        const matches = transactions.sliceTime(
159          prevWindowManagerEntry.getTimestamp(),
160          windowManagerEntry.getTimestamp()
161        );
162        frameMapBuilder.setFrames(prevWindowManagerEntry.getIndex(), matches.getFramesRange());
163      }
164      prevWindowManagerEntry = windowManagerEntry;
165    });
166
167    if (windowManager.lengthEntries > 0) {
168      const lastWindowManagerEntry = windowManager.getEntry(-1);
169      const startSearchTime = lastWindowManagerEntry.getTimestamp();
170      const endSearchTime = startSearchTime.add(FrameMapper.MAX_UI_PIPELINE_LATENCY_NS);
171      const matches = transactions.sliceTime(startSearchTime, endSearchTime);
172      frameMapBuilder.setFrames(lastWindowManagerEntry.getIndex(), matches.getFramesRange());
173    }
174
175    const frameMap = frameMapBuilder.build();
176    windowManager.setFrameInfo(frameMap, frameMap.getFullTraceFramesRange());
177  }
178
179  private tryPropagateFromWindowManagerToProtoLog() {
180    const frameMapBuilder = this.tryStartFrameMapping(
181      TraceType.WINDOW_MANAGER,
182      TraceType.PROTO_LOG
183    );
184    if (!frameMapBuilder) {
185      return;
186    }
187
188    const protoLog = assertDefined(this.traces.getTrace(TraceType.PROTO_LOG));
189    const windowManager = assertDefined(this.traces.getTrace(TraceType.WINDOW_MANAGER));
190
191    windowManager.forEachEntry((prevSrcEntry) => {
192      const srcEntryIndex = prevSrcEntry.getIndex() + 1;
193      const srcEntry =
194        srcEntryIndex < windowManager.lengthEntries
195          ? windowManager.getEntry(srcEntryIndex)
196          : undefined;
197      if (srcEntry === undefined) {
198        return;
199      }
200      const startSearchTime = prevSrcEntry.getTimestamp().add(1n);
201      const endSearchTime = srcEntry.getTimestamp().add(1n);
202      const matches = protoLog.sliceTime(startSearchTime, endSearchTime);
203      matches.forEachEntry((dstEntry) => {
204        frameMapBuilder.setFrames(dstEntry.getIndex(), srcEntry.getFramesRange());
205      });
206    });
207
208    if (windowManager.lengthEntries > 0) {
209      const firstEntry = windowManager.getEntry(0);
210      const startSearchTime = firstEntry
211        .getTimestamp()
212        .add(-FrameMapper.MAX_UI_PIPELINE_LATENCY_NS);
213      const endSearchTime = firstEntry.getTimestamp().add(1n);
214      const matches = protoLog.sliceTime(startSearchTime, endSearchTime);
215      matches.forEachEntry((dstEntry) => {
216        frameMapBuilder.setFrames(dstEntry.getIndex(), firstEntry.getFramesRange());
217      });
218    }
219
220    const frameMap = frameMapBuilder.build();
221    protoLog.setFrameInfo(frameMap, frameMap.getFullTraceFramesRange());
222  }
223
224  private tryPropagateFromWindowManagerToIme() {
225    const imeTypes = [
226      TraceType.INPUT_METHOD_CLIENTS,
227      TraceType.INPUT_METHOD_MANAGER_SERVICE,
228      TraceType.INPUT_METHOD_SERVICE,
229    ];
230    for (const imeType of imeTypes) {
231      const frameMapBuilder = this.tryStartFrameMapping(TraceType.WINDOW_MANAGER, imeType);
232      if (frameMapBuilder) {
233        this.propagateFromWindowManagerToIme(imeType, frameMapBuilder);
234      }
235    }
236  }
237
238  private propagateFromWindowManagerToIme(
239    imeTraceType: TraceType,
240    frameMapBuilder: FrameMapBuilder
241  ) {
242    // Value used to narrow time-based searches of corresponding WindowManager entries
243    const MAX_TIME_DIFFERENCE_NS = 200000000n; // 200 ms
244
245    const ime = assertDefined(this.traces.getTrace(imeTraceType));
246    const windowManager = assertDefined(this.traces.getTrace(TraceType.WINDOW_MANAGER));
247    const abs = (n: bigint): bigint => (n < 0n ? -n : n);
248
249    ime.forEachEntry((dstEntry) => {
250      const srcEntry = windowManager.findClosestEntry(dstEntry.getTimestamp());
251      if (!srcEntry) {
252        return;
253      }
254      const timeDifferenceNs = abs(
255        srcEntry.getTimestamp().getValueNs() - dstEntry.getTimestamp().getValueNs()
256      );
257      if (timeDifferenceNs > MAX_TIME_DIFFERENCE_NS) {
258        return;
259      }
260      frameMapBuilder.setFrames(dstEntry.getIndex(), srcEntry.getFramesRange());
261    });
262
263    const frameMap = frameMapBuilder.build();
264    ime.setFrameInfo(frameMap, frameMap.getFullTraceFramesRange());
265  }
266
267  private tryStartFrameMapping(
268    srcTraceType: TraceType,
269    dstTraceType: TraceType
270  ): FrameMapBuilder | undefined {
271    const srcTrace = this.traces.getTrace(srcTraceType);
272    const dstTrace = this.traces.getTrace(dstTraceType);
273    if (!srcTrace || !dstTrace || !srcTrace.hasFrameInfo()) {
274      return undefined;
275    }
276
277    const framesRange = srcTrace.getFramesRange();
278    const lengthFrames = framesRange ? framesRange.end : 0;
279    return new FrameMapBuilder(dstTrace.lengthEntries, lengthFrames);
280  }
281
282  private async getVsyncIdProperty(
283    entry: TraceEntry<object>,
284    propertyKey: string
285  ): Promise<bigint | undefined> {
286    const entryValue = await entry.getValue();
287    const vsyncId = (entryValue as any)[propertyKey];
288    if (vsyncId === undefined) {
289      console.error(`Failed to get trace entry's '${propertyKey}' property:`, entryValue);
290      return undefined;
291    }
292    try {
293      return BigInt(vsyncId.toString());
294    } catch (e) {
295      console.error(`Failed to convert trace entry's vsyncId to bigint:`, entryValue);
296      return undefined;
297    }
298  }
299}
300