• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2018 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 {assertTrue} from '../base/logging';
16import {duration, Span, Time, time, TimeSpan} from '../base/time';
17import {Actions} from '../common/actions';
18import {
19  HighPrecisionTime,
20  HighPrecisionTimeSpan,
21} from '../common/high_precision_time';
22import {
23  Area,
24  FrontendLocalState as FrontendState,
25  Timestamped,
26  VisibleState,
27} from '../common/state';
28import {raf} from '../core/raf_scheduler';
29
30import {globals} from './globals';
31import {ratelimit} from './rate_limiters';
32import {PxSpan, TimeScale} from './time_scale';
33
34interface Range {
35  start?: number;
36  end?: number;
37}
38
39function chooseLatest<T extends Timestamped>(current: T, next: T): T {
40  if (next !== current && next.lastUpdate > current.lastUpdate) {
41    // |next| is from state. Callers may mutate the return value of
42    // this function so we need to clone |next| to prevent bad mutations
43    // of state:
44    return Object.assign({}, next);
45  }
46  return current;
47}
48
49// Immutable object describing a (high precision) time window, providing methods
50// for common mutation operations (pan, zoom), and accessors for common
51// properties such as spans and durations in several formats.
52// This object relies on the trace time span in globals and ensures start and
53// ends of the time window remain within the confines of the trace time, and
54// also applies a hard-coded minimum zoom level.
55export class TimeWindow {
56  readonly hpTimeSpan = HighPrecisionTimeSpan.ZERO;
57  readonly timeSpan = TimeSpan.ZERO;
58
59  private readonly MIN_DURATION_NS = 10;
60
61  constructor(start = HighPrecisionTime.ZERO, durationNanos = 1e9) {
62    durationNanos = Math.max(this.MIN_DURATION_NS, durationNanos);
63
64    const traceTimeSpan = globals.stateTraceTime();
65    const traceDurationNanos = traceTimeSpan.duration.nanos;
66
67    if (durationNanos > traceDurationNanos) {
68      start = traceTimeSpan.start;
69      durationNanos = traceDurationNanos;
70    }
71
72    if (start.lt(traceTimeSpan.start)) {
73      start = traceTimeSpan.start;
74    }
75
76    const end = start.addNanos(durationNanos);
77    if (end.gt(traceTimeSpan.end)) {
78      start = traceTimeSpan.end.subNanos(durationNanos);
79    }
80
81    this.hpTimeSpan = new HighPrecisionTimeSpan(
82      start,
83      start.addNanos(durationNanos),
84    );
85    this.timeSpan = new TimeSpan(
86      this.hpTimeSpan.start.toTime('floor'),
87      this.hpTimeSpan.end.toTime('ceil'),
88    );
89  }
90
91  static fromHighPrecisionTimeSpan(span: Span<HighPrecisionTime>): TimeWindow {
92    return new TimeWindow(span.start, span.duration.nanos);
93  }
94
95  // Pan the window by certain number of seconds
96  pan(offset: HighPrecisionTime) {
97    return new TimeWindow(
98      this.hpTimeSpan.start.add(offset),
99      this.hpTimeSpan.duration.nanos,
100    );
101  }
102
103  // Zoom in or out a bit centered on a specific offset from the root
104  // Offset represents the center of the zoom as a normalized value between 0
105  // and 1 where 0 is the start of the time window and 1 is the end
106  zoom(ratio: number, offset: number) {
107    const traceDuration = globals.stateTraceTime().duration;
108    const minDuration = Math.min(this.MIN_DURATION_NS, traceDuration.nanos);
109    const currentDurationNanos = this.hpTimeSpan.duration.nanos;
110    const newDurationNanos = Math.max(
111      currentDurationNanos * ratio,
112      minDuration,
113    );
114    // Delta between new and old duration
115    // +ve if new duration is shorter than old duration
116    const durationDeltaNanos = currentDurationNanos - newDurationNanos;
117    // If offset is 0, don't move the start at all
118    // If offset if 1, move the start by the amount the duration has changed
119    // If new duration is shorter - move start to right
120    // If new duration is longer - move start to left
121    const start = this.hpTimeSpan.start.addNanos(durationDeltaNanos * offset);
122    const durationNanos = newDurationNanos;
123    return new TimeWindow(start, durationNanos);
124  }
125
126  createTimeScale(startPx: number, endPx: number): TimeScale {
127    return new TimeScale(
128      this.hpTimeSpan.start,
129      this.hpTimeSpan.duration.nanos,
130      new PxSpan(startPx, endPx),
131    );
132  }
133
134  get earliest(): time {
135    return this.timeSpan.start;
136  }
137
138  get latest(): time {
139    return this.timeSpan.end;
140  }
141}
142
143/**
144 * State that is shared between several frontend components, but not the
145 * controller. This state is updated at 60fps.
146 */
147export class Timeline {
148  private visibleWindow = new TimeWindow();
149  private _timeScale = this.visibleWindow.createTimeScale(0, 0);
150  private _windowSpan = PxSpan.ZERO;
151
152  // This is used to calculate the tracks within a Y range for area selection.
153  areaY: Range = {};
154
155  private _visibleState: VisibleState = {
156    lastUpdate: 0,
157    start: Time.ZERO,
158    end: Time.fromSeconds(10),
159    resolution: 1n,
160  };
161
162  private _selectedArea?: Area;
163
164  // TODO: there is some redundancy in the fact that both |visibleWindowTime|
165  // and a |timeScale| have a notion of time range. That should live in one
166  // place only.
167
168  zoomVisibleWindow(ratio: number, centerPoint: number) {
169    this.visibleWindow = this.visibleWindow.zoom(ratio, centerPoint);
170    this._timeScale = this.visibleWindow.createTimeScale(
171      this._windowSpan.start,
172      this._windowSpan.end,
173    );
174    this.kickUpdateLocalState();
175  }
176
177  panVisibleWindow(delta: HighPrecisionTime) {
178    this.visibleWindow = this.visibleWindow.pan(delta);
179    this._timeScale = this.visibleWindow.createTimeScale(
180      this._windowSpan.start,
181      this._windowSpan.end,
182    );
183    this.kickUpdateLocalState();
184  }
185
186  mergeState(state: FrontendState): void {
187    // This is unfortunately subtle. This class mutates this._visibleState.
188    // Since we may not mutate |state| (in order to make immer's immutable
189    // updates work) this means that we have to make a copy of the visibleState.
190    // when updating it. We don't want to have to do that unnecessarily so
191    // chooseLatest returns a shallow clone of state.visibleState *only* when
192    // that is the newer state. All of these complications should vanish when
193    // we remove this class.
194    const previousVisibleState = this._visibleState;
195    this._visibleState = chooseLatest(this._visibleState, state.visibleState);
196    const visibleStateWasUpdated = previousVisibleState !== this._visibleState;
197    if (visibleStateWasUpdated) {
198      this.updateLocalTime(
199        new HighPrecisionTimeSpan(
200          HighPrecisionTime.fromTime(this._visibleState.start),
201          HighPrecisionTime.fromTime(this._visibleState.end),
202        ),
203      );
204    }
205  }
206
207  // Set the highlight box to draw
208  selectArea(
209    start: time,
210    end: time,
211    tracks = this._selectedArea ? this._selectedArea.tracks : [],
212  ) {
213    assertTrue(
214      end >= start,
215      `Impossible select area: start [${start}] >= end [${end}]`,
216    );
217    this._selectedArea = {start, end, tracks};
218    raf.scheduleFullRedraw();
219  }
220
221  deselectArea() {
222    this._selectedArea = undefined;
223    raf.scheduleRedraw();
224  }
225
226  get selectedArea(): Area | undefined {
227    return this._selectedArea;
228  }
229
230  private ratelimitedUpdateVisible = ratelimit(() => {
231    globals.dispatch(Actions.setVisibleTraceTime(this._visibleState));
232  }, 50);
233
234  private updateLocalTime(ts: Span<HighPrecisionTime>) {
235    const traceBounds = globals.stateTraceTime();
236    const start = ts.start.clamp(traceBounds.start, traceBounds.end);
237    const end = ts.end.clamp(traceBounds.start, traceBounds.end);
238    this.visibleWindow = TimeWindow.fromHighPrecisionTimeSpan(
239      new HighPrecisionTimeSpan(start, end),
240    );
241    this._timeScale = this.visibleWindow.createTimeScale(
242      this._windowSpan.start,
243      this._windowSpan.end,
244    );
245    this.updateResolution();
246  }
247
248  private updateResolution() {
249    this._visibleState.lastUpdate = Date.now() / 1000;
250    this._visibleState.resolution = globals.getCurResolution();
251    this.ratelimitedUpdateVisible();
252  }
253
254  private kickUpdateLocalState() {
255    this._visibleState.lastUpdate = Date.now() / 1000;
256    this._visibleState.start = this.visibleWindowTime.start.toTime();
257    this._visibleState.end = this.visibleWindowTime.end.toTime();
258    this._visibleState.resolution = globals.getCurResolution();
259    this.ratelimitedUpdateVisible();
260  }
261
262  updateVisibleTime(ts: Span<HighPrecisionTime>) {
263    this.updateLocalTime(ts);
264    this.kickUpdateLocalState();
265  }
266
267  // Whenever start/end px of the timeScale is changed, update
268  // the resolution.
269  updateLocalLimits(pxStart: number, pxEnd: number) {
270    // Numbers received here can be negative or equal, but we should fix that
271    // before updating the timescale.
272    pxStart = Math.max(0, pxStart);
273    pxEnd = Math.max(0, pxEnd);
274    if (pxStart === pxEnd) pxEnd = pxStart + 1;
275    this._timeScale = this.visibleWindow.createTimeScale(pxStart, pxEnd);
276    this._windowSpan = new PxSpan(pxStart, pxEnd);
277    this.updateResolution();
278  }
279
280  // Get the time scale for the visible window
281  get visibleTimeScale(): TimeScale {
282    return this._timeScale;
283  }
284
285  // Produces a TimeScale object for this time window provided start and end px
286  getTimeScale(startPx: number, endPx: number): TimeScale {
287    return this.visibleWindow.createTimeScale(startPx, endPx);
288  }
289
290  // Get the bounds of the window in pixels
291  get windowSpan(): PxSpan {
292    return this._windowSpan;
293  }
294
295  // Get the bounds of the visible window as a high-precision time span
296  get visibleWindowTime(): Span<HighPrecisionTime> {
297    return this.visibleWindow.hpTimeSpan;
298  }
299
300  // Get the bounds of the visible window as a time span
301  get visibleTimeSpan(): Span<time, duration> {
302    return this.visibleWindow.timeSpan;
303  }
304}
305