// 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 {Actions} from '../common/actions'; import {HttpRpcState} from '../common/http_rpc_engine'; import { Area, FrontendLocalState as FrontendState, OmniboxState, Timestamped, VisibleState, } from '../common/state'; import {TimeSpan} from '../common/time'; import {globals} from './globals'; import {debounce, ratelimit} from './rate_limiters'; import {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; } function capBetween(t: number, start: number, end: number) { return Math.min(Math.max(t, start), end); } // Calculate the space a scrollbar takes up so that we can subtract it from // the canvas width. function calculateScrollbarWidth() { const outer = document.createElement('div'); outer.style.overflowY = 'scroll'; const inner = document.createElement('div'); outer.appendChild(inner); document.body.appendChild(outer); const width = outer.getBoundingClientRect().width - inner.getBoundingClientRect().width; document.body.removeChild(outer); return width; } /** * State that is shared between several frontend components, but not the * controller. This state is updated at 60fps. */ export class FrontendLocalState { visibleWindowTime = new TimeSpan(0, 10); timeScale = new TimeScale(this.visibleWindowTime, [0, 0]); showPanningHint = false; showCookieConsent = false; visibleTracks = new Set(); prevVisibleTracks = new Set(); scrollToTrackId?: string|number; httpRpcState: HttpRpcState = {connected: false}; newVersionAvailable = false; showPivotTable = false; // This is used to calculate the tracks within a Y range for area selection. areaY: Range = {}; private scrollBarWidth?: number; private _omniboxState: OmniboxState = { lastUpdate: 0, omnibox: '', mode: 'SEARCH', }; private _visibleState: VisibleState = { lastUpdate: 0, startSec: 0, endSec: 10, resolution: 1, }; 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. getScrollbarWidth() { if (this.scrollBarWidth === undefined) { this.scrollBarWidth = calculateScrollbarWidth(); } return this.scrollBarWidth; } setHttpRpcState(httpRpcState: HttpRpcState) { this.httpRpcState = httpRpcState; globals.rafScheduler.scheduleFullRedraw(); } addVisibleTrack(trackId: string) { this.visibleTracks.add(trackId); } // Called when beginning a canvas redraw. clearVisibleTracks() { this.visibleTracks.clear(); } // Called when the canvas redraw is complete. sendVisibleTracks() { if (this.prevVisibleTracks.size !== this.visibleTracks.size || ![...this.prevVisibleTracks].every( value => this.visibleTracks.has(value))) { globals.dispatch( Actions.setVisibleTracks({tracks: Array.from(this.visibleTracks)})); this.prevVisibleTracks = new Set(this.visibleTracks); } } togglePivotTable() { this.showPivotTable = !this.showPivotTable; globals.rafScheduler.scheduleFullRedraw(); } 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._omniboxState = chooseLatest(this._omniboxState, state.omniboxState); this._visibleState = chooseLatest(this._visibleState, state.visibleState); const visibleStateWasUpdated = previousVisibleState !== this._visibleState; if (visibleStateWasUpdated) { this.updateLocalTime( new TimeSpan(this._visibleState.startSec, this._visibleState.endSec)); } } selectArea( startSec: number, endSec: number, tracks = this._selectedArea ? this._selectedArea.tracks : []) { assertTrue(endSec >= startSec); this.showPanningHint = true; this._selectedArea = {startSec, endSec, tracks}, globals.rafScheduler.scheduleFullRedraw(); } deselectArea() { this._selectedArea = undefined; globals.rafScheduler.scheduleRedraw(); } get selectedArea(): Area|undefined { return this._selectedArea; } private setOmniboxDebounced = debounce(() => { globals.dispatch(Actions.setOmnibox({...this._omniboxState})); }, 20); setOmnibox(value: string, mode: 'SEARCH'|'COMMAND') { this._omniboxState.omnibox = value; this._omniboxState.mode = mode; this._omniboxState.lastUpdate = Date.now() / 1000; this.setOmniboxDebounced(); } get omnibox(): string { return this._omniboxState.omnibox; } private ratelimitedUpdateVisible = ratelimit(() => { globals.dispatch(Actions.setVisibleTraceTime(this._visibleState)); }, 50); private updateLocalTime(ts: TimeSpan) { const traceTime = globals.state.traceTime; const startSec = capBetween(ts.start, traceTime.startSec, traceTime.endSec); const endSec = capBetween(ts.end, traceTime.startSec, traceTime.endSec); this.visibleWindowTime = new TimeSpan(startSec, endSec); this.timeScale.setTimeBounds(this.visibleWindowTime); this.updateResolution(); } private updateResolution() { this._visibleState.lastUpdate = Date.now() / 1000; this._visibleState.resolution = globals.getCurResolution(); this.ratelimitedUpdateVisible(); } updateVisibleTime(ts: TimeSpan) { this.updateLocalTime(ts); this._visibleState.lastUpdate = Date.now() / 1000; this._visibleState.startSec = this.visibleWindowTime.start; this._visibleState.endSec = this.visibleWindowTime.end; this._visibleState.resolution = globals.getCurResolution(); this.ratelimitedUpdateVisible(); } getVisibleStateBounds(): [number, number] { return [this.visibleWindowTime.start, this.visibleWindowTime.end]; } // 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.setLimitsPx(pxStart, pxEnd); this.updateResolution(); } }