// 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(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): 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) { 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) { 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 { return this.visibleWindow.hpTimeSpan; } // Get the bounds of the visible window as a time span get visibleTimeSpan(): Span { return this.visibleWindow.timeSpan; } }