xref: /aosp_15_r20/external/perfetto/ui/src/base/high_precision_time_span.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2024 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {TimeSpan, time} from './time';
16import {HighPrecisionTime} from './high_precision_time';
17
18/**
19 * Represents a time span using a high precision time value to represent the
20 * start of the span, and a number to represent the duration of the span.
21 */
22export class HighPrecisionTimeSpan {
23  static readonly ZERO = new HighPrecisionTimeSpan(HighPrecisionTime.ZERO, 0);
24
25  readonly start: HighPrecisionTime;
26  readonly duration: number;
27
28  constructor(start: HighPrecisionTime, duration: number) {
29    this.start = start;
30    this.duration = duration;
31  }
32
33  /**
34   * Create a new span from integral start and end points.
35   *
36   * @param start The start of the span.
37   * @param end The end of the span.
38   */
39  static fromTime(start: time, end: time): HighPrecisionTimeSpan {
40    return new HighPrecisionTimeSpan(
41      new HighPrecisionTime(start),
42      Number(end - start),
43    );
44  }
45
46  /**
47   * The center point of the span.
48   */
49  get midpoint(): HighPrecisionTime {
50    return this.start.addNumber(this.duration / 2);
51  }
52
53  /**
54   * The end of the span.
55   */
56  get end(): HighPrecisionTime {
57    return this.start.addNumber(this.duration);
58  }
59
60  /**
61   * Checks if this span exactly equals another.
62   */
63  equals(other: HighPrecisionTimeSpan): boolean {
64    return this.start.equals(other.start) && this.duration === other.duration;
65  }
66
67  /**
68   * Create a new span with the same duration but the start point moved through
69   * time by some amount of time.
70   */
71  translate(time: number): HighPrecisionTimeSpan {
72    return new HighPrecisionTimeSpan(this.start.addNumber(time), this.duration);
73  }
74
75  /**
76   * Create a new span with the the start of the span moved backward and the end
77   * of the span moved forward by a certain amount of time.
78   */
79  pad(time: number): HighPrecisionTimeSpan {
80    return new HighPrecisionTimeSpan(
81      this.start.subNumber(time),
82      this.duration + 2 * time,
83    );
84  }
85
86  /**
87   * Create a new span which is zoomed in or out centered on a specific point.
88   *
89   * @param ratio The scaling ratio, the new duration will be the current
90   * duration * ratio.
91   * @param center The center point as a normalized value between 0 and 1 where
92   * 0 is the start of the time window and 1 is the end.
93   * @param minDur Don't allow the time span to become shorter than this.
94   */
95  scale(ratio: number, center: number, minDur: number): HighPrecisionTimeSpan {
96    const currentDuration = this.duration;
97    const newDuration = Math.max(currentDuration * ratio, minDur);
98    // Delta between new and old duration
99    // +ve if new duration is shorter than old duration
100    const durationDeltaNanos = currentDuration - newDuration;
101    // If offset is 0, don't move the start at all
102    // If offset if 1, move the start by the amount the duration has changed
103    // If new duration is shorter - move start to right
104    // If new duration is longer - move start to left
105    const start = this.start.addNumber(durationDeltaNanos * center);
106    return new HighPrecisionTimeSpan(start, newDuration);
107  }
108
109  /**
110   * Create a new span that represents the intersection of this span with
111   * another.
112   *
113   * If the two spans do not overlap at all, the empty span is returned.
114   *
115   * @param start THe start of the other span.
116   * @param end The end of the other span.
117   */
118  intersect(start: time, end: time): HighPrecisionTimeSpan {
119    if (!this.overlaps(start, end)) {
120      return HighPrecisionTimeSpan.ZERO;
121    }
122    const newStart = this.start.clamp(start, end);
123    const newEnd = this.end.clamp(start, end);
124    const newDuration = newEnd.sub(newStart).toNumber();
125    return new HighPrecisionTimeSpan(newStart, newDuration);
126  }
127
128  /**
129   * Create a new timespan which fits within the specified bounds, preserving
130   * its duration if possible.
131   *
132   * This function moves the timespan forwards or backwards in time while
133   * keeping its duration unchanged, so that it fits entirely within the range
134   * defined by `start` and `end`.
135   *
136   * If the specified bounds are smaller than the current timespan's duration, a
137   * new timespan matching the bounds is returned.
138   *
139   * @param start The start of the bounds within which the timespan should fit.
140   * @param end The end of the bounds within which the timespan should fit.
141   *
142   * @example
143   * // assume `timespan` is defined as: [5, 8)
144   * timespan.fitWithin(10n, 20n); // -> [10, 13)
145   * timespan.fitWithin(-10n, -5n); // -> [-8, -5)
146   * timespan.fitWithin(1n, 2n); // -> [1, 2)
147   */
148  fitWithin(start: time, end: time): HighPrecisionTimeSpan {
149    if (this.duration > Number(end - start)) {
150      // Current span is greater than the limits
151      return HighPrecisionTimeSpan.fromTime(start, end);
152    }
153    if (this.start.integral < start) {
154      // Current span starts before limits
155      return new HighPrecisionTimeSpan(
156        new HighPrecisionTime(start),
157        this.duration,
158      );
159    }
160    if (this.end.gt(end)) {
161      // Current span ends after limits
162      return new HighPrecisionTimeSpan(
163        new HighPrecisionTime(end).subNumber(this.duration),
164        this.duration,
165      );
166    }
167    return this;
168  }
169
170  /**
171   * Clamp duration to some minimum value. The start remains the same, just the
172   * duration is changed.
173   */
174  clampDuration(minDuration: number): HighPrecisionTimeSpan {
175    if (this.duration < minDuration) {
176      return new HighPrecisionTimeSpan(this.start, minDuration);
177    } else {
178      return this;
179    }
180  }
181
182  /**
183   * Checks whether this span completely contains a time instant.
184   */
185  contains(t: time): boolean {
186    return this.start.lte(t) && this.end.gt(t);
187  }
188
189  /**
190   * Checks whether this span entirely contains another span.
191   *
192   * @param start The start of the span to check.
193   * @param end The end of the span to check.
194   */
195  containsSpan(start: time, end: time): boolean {
196    return this.start.lte(start) && this.end.gte(end);
197  }
198
199  /**
200   * Checks if this span overlaps at all with another.
201   *
202   * @param start The start of the span to check.
203   * @param end The end of the span to check.
204   */
205  overlaps(start: time, end: time): boolean {
206    return !(this.start.gte(end) || this.end.lte(start));
207  }
208
209  /**
210   * Get the span of integer intervals values that overlap this span.
211   */
212  toTimeSpan(): TimeSpan {
213    return new TimeSpan(this.start.toTime('floor'), this.end.toTime('ceil'));
214  }
215}
216