xref: /aosp_15_r20/development/tools/motion/motion_test_watcher_app/src/app/timeline.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright 2024 Google LLC
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
17// golden recordings from motion tests produces frames at exactly 16ms each
18const frameDuration = 0.016;
19
20/** Maps frame time to frame number and vice versa. */
21export class Timeline {
22  /** Duration , represented as a double-precision floating-point value in seconds. */
23  readonly duration: number;
24  /**
25   * The labels of all
26   */
27  readonly frameLabels: ReadonlyArray<string>;
28  readonly frameTimes: ReadonlyArray<number>;
29
30  constructor(frameIds: ReadonlyArray<string | number>) {
31    this.frameLabels = frameIds.map((it) =>
32      typeof it === 'string' ? it : `${it}ms`,
33    );
34
35    let lastFrameTime = 0;
36
37    const frameTimes = [lastFrameTime];
38
39    for (let i = 0; i < frameIds.length - 1; i++) {
40      const thisFrame = frameIds[i];
41      const nextFrame = frameIds[i + 1];
42
43      if (typeof thisFrame === 'number' && typeof nextFrame === 'number') {
44        lastFrameTime += (nextFrame - thisFrame) / 1000;
45      } else {
46        lastFrameTime += frameDuration;
47      }
48      frameTimes.push(lastFrameTime);
49    }
50    this.frameTimes = frameTimes;
51
52    this.duration = frameTimes[frameTimes.length - 1] + frameDuration;
53  }
54
55  /** Number of frames in this timeline. */
56  get frameCount(): number {
57    return this.frameLabels.length;
58  }
59
60  /**
61   * Compute the frame number associated the time in seconds.
62   *
63   * Each frame starts at the frame time, and just before the next frame's start time, or duration
64   * if it's the last frame.
65   */
66  timeToFrame(timeSeconds: number): number {
67    if (timeSeconds < 0) return Number.NEGATIVE_INFINITY;
68    if (timeSeconds >= this.duration) return Number.POSITIVE_INFINITY;
69
70    const frameTimes = this.frameTimes;
71
72    let start = 0;
73    let end = frameTimes.length - 1;
74
75    while (start <= end) {
76      let mid = (start + end) >> 1;
77
78      if (frameTimes[mid] === timeSeconds) {
79        return mid;
80      }
81
82      if (timeSeconds < frameTimes[mid]) {
83        end = mid - 1;
84      } else {
85        start = mid + 1;
86      }
87    }
88
89    return end;
90  }
91
92  /**
93   * Start time of the frame with `frameNumber`.
94   */
95  frameToTime(frameNumber: number): number {
96    frameNumber = Math.floor(frameNumber);
97    if (frameNumber < 0) return Number.NEGATIVE_INFINITY;
98    const frameTimes = this.frameTimes;
99    if (frameNumber >= frameTimes.length) return Number.POSITIVE_INFINITY;
100
101    return frameTimes[frameNumber];
102  }
103
104  clampTimeToFrame(timeSeconds: number): number {
105    return this.frameToTime(this.timeToFrame(timeSeconds));
106  }
107}
108