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