| // Copyright (C) 2018 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import {assertTrue} from '../base/logging'; |
| import {duration, Span, Time, time, TimeSpan} from '../base/time'; |
| import {Actions} from '../common/actions'; |
| import { |
| HighPrecisionTime, |
| HighPrecisionTimeSpan, |
| } from '../common/high_precision_time'; |
| import { |
| Area, |
| FrontendLocalState as FrontendState, |
| Timestamped, |
| VisibleState, |
| } from '../common/state'; |
| import {raf} from '../core/raf_scheduler'; |
| |
| import {globals} from './globals'; |
| import {ratelimit} from './rate_limiters'; |
| import {PxSpan, TimeScale} from './time_scale'; |
| |
| interface Range { |
| start?: number; |
| end?: number; |
| } |
| |
| function chooseLatest<T extends Timestamped>(current: T, next: T): T { |
| if (next !== current && next.lastUpdate > current.lastUpdate) { |
| // |next| is from state. Callers may mutate the return value of |
| // this function so we need to clone |next| to prevent bad mutations |
| // of state: |
| return Object.assign({}, next); |
| } |
| return current; |
| } |
| |
| // Immutable object describing a (high precision) time window, providing methods |
| // for common mutation operations (pan, zoom), and accessors for common |
| // properties such as spans and durations in several formats. |
| // This object relies on the trace time span in globals and ensures start and |
| // ends of the time window remain within the confines of the trace time, and |
| // also applies a hard-coded minimum zoom level. |
| export class TimeWindow { |
| readonly hpTimeSpan = HighPrecisionTimeSpan.ZERO; |
| readonly timeSpan = TimeSpan.ZERO; |
| |
| private readonly MIN_DURATION_NS = 10; |
| |
| constructor(start = HighPrecisionTime.ZERO, durationNanos = 1e9) { |
| durationNanos = Math.max(this.MIN_DURATION_NS, durationNanos); |
| |
| const traceTimeSpan = globals.stateTraceTime(); |
| const traceDurationNanos = traceTimeSpan.duration.nanos; |
| |
| if (durationNanos > traceDurationNanos) { |
| start = traceTimeSpan.start; |
| durationNanos = traceDurationNanos; |
| } |
| |
| if (start.lt(traceTimeSpan.start)) { |
| start = traceTimeSpan.start; |
| } |
| |
| const end = start.addNanos(durationNanos); |
| if (end.gt(traceTimeSpan.end)) { |
| start = traceTimeSpan.end.subNanos(durationNanos); |
| } |
| |
| this.hpTimeSpan = new HighPrecisionTimeSpan( |
| start, |
| start.addNanos(durationNanos), |
| ); |
| this.timeSpan = new TimeSpan( |
| this.hpTimeSpan.start.toTime('floor'), |
| this.hpTimeSpan.end.toTime('ceil'), |
| ); |
| } |
| |
| static fromHighPrecisionTimeSpan(span: Span<HighPrecisionTime>): TimeWindow { |
| return new TimeWindow(span.start, span.duration.nanos); |
| } |
| |
| // Pan the window by certain number of seconds |
| pan(offset: HighPrecisionTime) { |
| return new TimeWindow( |
| this.hpTimeSpan.start.add(offset), |
| this.hpTimeSpan.duration.nanos, |
| ); |
| } |
| |
| // Zoom in or out a bit centered on a specific offset from the root |
| // Offset represents the center of the zoom as a normalized value between 0 |
| // and 1 where 0 is the start of the time window and 1 is the end |
| zoom(ratio: number, offset: number) { |
| const traceDuration = globals.stateTraceTime().duration; |
| const minDuration = Math.min(this.MIN_DURATION_NS, traceDuration.nanos); |
| const currentDurationNanos = this.hpTimeSpan.duration.nanos; |
| const newDurationNanos = Math.max( |
| currentDurationNanos * ratio, |
| minDuration, |
| ); |
| // Delta between new and old duration |
| // +ve if new duration is shorter than old duration |
| const durationDeltaNanos = currentDurationNanos - newDurationNanos; |
| // If offset is 0, don't move the start at all |
| // If offset if 1, move the start by the amount the duration has changed |
| // If new duration is shorter - move start to right |
| // If new duration is longer - move start to left |
| const start = this.hpTimeSpan.start.addNanos(durationDeltaNanos * offset); |
| const durationNanos = newDurationNanos; |
| return new TimeWindow(start, durationNanos); |
| } |
| |
| createTimeScale(startPx: number, endPx: number): TimeScale { |
| return new TimeScale( |
| this.hpTimeSpan.start, |
| this.hpTimeSpan.duration.nanos, |
| new PxSpan(startPx, endPx), |
| ); |
| } |
| |
| get earliest(): time { |
| return this.timeSpan.start; |
| } |
| |
| get latest(): time { |
| return this.timeSpan.end; |
| } |
| } |
| |
| /** |
| * State that is shared between several frontend components, but not the |
| * controller. This state is updated at 60fps. |
| */ |
| export class Timeline { |
| private visibleWindow = new TimeWindow(); |
| private _timeScale = this.visibleWindow.createTimeScale(0, 0); |
| private _windowSpan = PxSpan.ZERO; |
| |
| // This is used to calculate the tracks within a Y range for area selection. |
| areaY: Range = {}; |
| |
| private _visibleState: VisibleState = { |
| lastUpdate: 0, |
| start: Time.ZERO, |
| end: Time.fromSeconds(10), |
| resolution: 1n, |
| }; |
| |
| private _selectedArea?: Area; |
| |
| // TODO: there is some redundancy in the fact that both |visibleWindowTime| |
| // and a |timeScale| have a notion of time range. That should live in one |
| // place only. |
| |
| zoomVisibleWindow(ratio: number, centerPoint: number) { |
| this.visibleWindow = this.visibleWindow.zoom(ratio, centerPoint); |
| this._timeScale = this.visibleWindow.createTimeScale( |
| this._windowSpan.start, |
| this._windowSpan.end, |
| ); |
| this.kickUpdateLocalState(); |
| } |
| |
| panVisibleWindow(delta: HighPrecisionTime) { |
| this.visibleWindow = this.visibleWindow.pan(delta); |
| this._timeScale = this.visibleWindow.createTimeScale( |
| this._windowSpan.start, |
| this._windowSpan.end, |
| ); |
| this.kickUpdateLocalState(); |
| } |
| |
| mergeState(state: FrontendState): void { |
| // This is unfortunately subtle. This class mutates this._visibleState. |
| // Since we may not mutate |state| (in order to make immer's immutable |
| // updates work) this means that we have to make a copy of the visibleState. |
| // when updating it. We don't want to have to do that unnecessarily so |
| // chooseLatest returns a shallow clone of state.visibleState *only* when |
| // that is the newer state. All of these complications should vanish when |
| // we remove this class. |
| const previousVisibleState = this._visibleState; |
| this._visibleState = chooseLatest(this._visibleState, state.visibleState); |
| const visibleStateWasUpdated = previousVisibleState !== this._visibleState; |
| if (visibleStateWasUpdated) { |
| this.updateLocalTime( |
| new HighPrecisionTimeSpan( |
| HighPrecisionTime.fromTime(this._visibleState.start), |
| HighPrecisionTime.fromTime(this._visibleState.end), |
| ), |
| ); |
| } |
| } |
| |
| // Set the highlight box to draw |
| selectArea( |
| start: time, |
| end: time, |
| tracks = this._selectedArea ? this._selectedArea.tracks : [], |
| ) { |
| assertTrue( |
| end >= start, |
| `Impossible select area: start [${start}] >= end [${end}]`, |
| ); |
| this._selectedArea = {start, end, tracks}; |
| raf.scheduleFullRedraw(); |
| } |
| |
| deselectArea() { |
| this._selectedArea = undefined; |
| raf.scheduleRedraw(); |
| } |
| |
| get selectedArea(): Area | undefined { |
| return this._selectedArea; |
| } |
| |
| private ratelimitedUpdateVisible = ratelimit(() => { |
| globals.dispatch(Actions.setVisibleTraceTime(this._visibleState)); |
| }, 50); |
| |
| private updateLocalTime(ts: Span<HighPrecisionTime>) { |
| const traceBounds = globals.stateTraceTime(); |
| const start = ts.start.clamp(traceBounds.start, traceBounds.end); |
| const end = ts.end.clamp(traceBounds.start, traceBounds.end); |
| this.visibleWindow = TimeWindow.fromHighPrecisionTimeSpan( |
| new HighPrecisionTimeSpan(start, end), |
| ); |
| this._timeScale = this.visibleWindow.createTimeScale( |
| this._windowSpan.start, |
| this._windowSpan.end, |
| ); |
| this.updateResolution(); |
| } |
| |
| private updateResolution() { |
| this._visibleState.lastUpdate = Date.now() / 1000; |
| this._visibleState.resolution = globals.getCurResolution(); |
| this.ratelimitedUpdateVisible(); |
| } |
| |
| private kickUpdateLocalState() { |
| this._visibleState.lastUpdate = Date.now() / 1000; |
| this._visibleState.start = this.visibleWindowTime.start.toTime(); |
| this._visibleState.end = this.visibleWindowTime.end.toTime(); |
| this._visibleState.resolution = globals.getCurResolution(); |
| this.ratelimitedUpdateVisible(); |
| } |
| |
| updateVisibleTime(ts: Span<HighPrecisionTime>) { |
| this.updateLocalTime(ts); |
| this.kickUpdateLocalState(); |
| } |
| |
| // Whenever start/end px of the timeScale is changed, update |
| // the resolution. |
| updateLocalLimits(pxStart: number, pxEnd: number) { |
| // Numbers received here can be negative or equal, but we should fix that |
| // before updating the timescale. |
| pxStart = Math.max(0, pxStart); |
| pxEnd = Math.max(0, pxEnd); |
| if (pxStart === pxEnd) pxEnd = pxStart + 1; |
| this._timeScale = this.visibleWindow.createTimeScale(pxStart, pxEnd); |
| this._windowSpan = new PxSpan(pxStart, pxEnd); |
| this.updateResolution(); |
| } |
| |
| // Get the time scale for the visible window |
| get visibleTimeScale(): TimeScale { |
| return this._timeScale; |
| } |
| |
| // Produces a TimeScale object for this time window provided start and end px |
| getTimeScale(startPx: number, endPx: number): TimeScale { |
| return this.visibleWindow.createTimeScale(startPx, endPx); |
| } |
| |
| // Get the bounds of the window in pixels |
| get windowSpan(): PxSpan { |
| return this._windowSpan; |
| } |
| |
| // Get the bounds of the visible window as a high-precision time span |
| get visibleWindowTime(): Span<HighPrecisionTime> { |
| return this.visibleWindow.hpTimeSpan; |
| } |
| |
| // Get the bounds of the visible window as a time span |
| get visibleTimeSpan(): Span<time, duration> { |
| return this.visibleWindow.timeSpan; |
| } |
| } |