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