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 {Actions} from '../common/actions'; 17import {HttpRpcState} from '../common/http_rpc_engine'; 18import { 19 Area, 20 FrontendLocalState as FrontendState, 21 OmniboxState, 22 Timestamped, 23 VisibleState, 24} from '../common/state'; 25import {TimeSpan} from '../common/time'; 26 27import {globals} from './globals'; 28import {debounce, ratelimit} from './rate_limiters'; 29import {TimeScale} from './time_scale'; 30 31interface Range { 32 start?: number; 33 end?: number; 34} 35 36function chooseLatest<T extends Timestamped<{}>>(current: T, next: T): T { 37 if (next !== current && next.lastUpdate > current.lastUpdate) { 38 // |next| is from state. Callers may mutate the return value of 39 // this function so we need to clone |next| to prevent bad mutations 40 // of state: 41 return Object.assign({}, next); 42 } 43 return current; 44} 45 46function capBetween(t: number, start: number, end: number) { 47 return Math.min(Math.max(t, start), end); 48} 49 50// Calculate the space a scrollbar takes up so that we can subtract it from 51// the canvas width. 52function calculateScrollbarWidth() { 53 const outer = document.createElement('div'); 54 outer.style.overflowY = 'scroll'; 55 const inner = document.createElement('div'); 56 outer.appendChild(inner); 57 document.body.appendChild(outer); 58 const width = 59 outer.getBoundingClientRect().width - inner.getBoundingClientRect().width; 60 document.body.removeChild(outer); 61 return width; 62} 63 64/** 65 * State that is shared between several frontend components, but not the 66 * controller. This state is updated at 60fps. 67 */ 68export class FrontendLocalState { 69 visibleWindowTime = new TimeSpan(0, 10); 70 timeScale = new TimeScale(this.visibleWindowTime, [0, 0]); 71 showPanningHint = false; 72 showCookieConsent = false; 73 visibleTracks = new Set<string>(); 74 prevVisibleTracks = new Set<string>(); 75 scrollToTrackId?: string|number; 76 httpRpcState: HttpRpcState = {connected: false}; 77 newVersionAvailable = false; 78 showPivotTable = false; 79 80 // This is used to calculate the tracks within a Y range for area selection. 81 areaY: Range = {}; 82 83 private scrollBarWidth?: number; 84 85 private _omniboxState: OmniboxState = { 86 lastUpdate: 0, 87 omnibox: '', 88 mode: 'SEARCH', 89 }; 90 91 private _visibleState: VisibleState = { 92 lastUpdate: 0, 93 startSec: 0, 94 endSec: 10, 95 resolution: 1, 96 }; 97 98 private _selectedArea?: Area; 99 100 // TODO: there is some redundancy in the fact that both |visibleWindowTime| 101 // and a |timeScale| have a notion of time range. That should live in one 102 // place only. 103 104 getScrollbarWidth() { 105 if (this.scrollBarWidth === undefined) { 106 this.scrollBarWidth = calculateScrollbarWidth(); 107 } 108 return this.scrollBarWidth; 109 } 110 111 setHttpRpcState(httpRpcState: HttpRpcState) { 112 this.httpRpcState = httpRpcState; 113 globals.rafScheduler.scheduleFullRedraw(); 114 } 115 116 addVisibleTrack(trackId: string) { 117 this.visibleTracks.add(trackId); 118 } 119 120 // Called when beginning a canvas redraw. 121 clearVisibleTracks() { 122 this.visibleTracks.clear(); 123 } 124 125 // Called when the canvas redraw is complete. 126 sendVisibleTracks() { 127 if (this.prevVisibleTracks.size !== this.visibleTracks.size || 128 ![...this.prevVisibleTracks].every( 129 value => this.visibleTracks.has(value))) { 130 globals.dispatch( 131 Actions.setVisibleTracks({tracks: Array.from(this.visibleTracks)})); 132 this.prevVisibleTracks = new Set(this.visibleTracks); 133 } 134 } 135 136 togglePivotTable() { 137 this.showPivotTable = !this.showPivotTable; 138 globals.rafScheduler.scheduleFullRedraw(); 139 } 140 141 mergeState(state: FrontendState): void { 142 // This is unfortunately subtle. This class mutates this._visibleState. 143 // Since we may not mutate |state| (in order to make immer's immutable 144 // updates work) this means that we have to make a copy of the visibleState. 145 // when updating it. We don't want to have to do that unnecessarily so 146 // chooseLatest returns a shallow clone of state.visibleState *only* when 147 // that is the newer state. All of these complications should vanish when 148 // we remove this class. 149 const previousVisibleState = this._visibleState; 150 this._omniboxState = chooseLatest(this._omniboxState, state.omniboxState); 151 this._visibleState = chooseLatest(this._visibleState, state.visibleState); 152 const visibleStateWasUpdated = previousVisibleState !== this._visibleState; 153 if (visibleStateWasUpdated) { 154 this.updateLocalTime( 155 new TimeSpan(this._visibleState.startSec, this._visibleState.endSec)); 156 } 157 } 158 159 selectArea( 160 startSec: number, endSec: number, 161 tracks = this._selectedArea ? this._selectedArea.tracks : []) { 162 assertTrue(endSec >= startSec); 163 this.showPanningHint = true; 164 this._selectedArea = {startSec, endSec, tracks}, 165 globals.rafScheduler.scheduleFullRedraw(); 166 } 167 168 deselectArea() { 169 this._selectedArea = undefined; 170 globals.rafScheduler.scheduleRedraw(); 171 } 172 173 get selectedArea(): Area|undefined { 174 return this._selectedArea; 175 } 176 177 private setOmniboxDebounced = debounce(() => { 178 globals.dispatch(Actions.setOmnibox({...this._omniboxState})); 179 }, 20); 180 181 setOmnibox(value: string, mode: 'SEARCH'|'COMMAND') { 182 this._omniboxState.omnibox = value; 183 this._omniboxState.mode = mode; 184 this._omniboxState.lastUpdate = Date.now() / 1000; 185 this.setOmniboxDebounced(); 186 } 187 188 get omnibox(): string { 189 return this._omniboxState.omnibox; 190 } 191 192 private ratelimitedUpdateVisible = ratelimit(() => { 193 globals.dispatch(Actions.setVisibleTraceTime(this._visibleState)); 194 }, 50); 195 196 private updateLocalTime(ts: TimeSpan) { 197 const traceTime = globals.state.traceTime; 198 const startSec = capBetween(ts.start, traceTime.startSec, traceTime.endSec); 199 const endSec = capBetween(ts.end, traceTime.startSec, traceTime.endSec); 200 this.visibleWindowTime = new TimeSpan(startSec, endSec); 201 this.timeScale.setTimeBounds(this.visibleWindowTime); 202 this.updateResolution(); 203 } 204 205 private updateResolution() { 206 this._visibleState.lastUpdate = Date.now() / 1000; 207 this._visibleState.resolution = globals.getCurResolution(); 208 this.ratelimitedUpdateVisible(); 209 } 210 211 updateVisibleTime(ts: TimeSpan) { 212 this.updateLocalTime(ts); 213 this._visibleState.lastUpdate = Date.now() / 1000; 214 this._visibleState.startSec = this.visibleWindowTime.start; 215 this._visibleState.endSec = this.visibleWindowTime.end; 216 this._visibleState.resolution = globals.getCurResolution(); 217 this.ratelimitedUpdateVisible(); 218 } 219 220 getVisibleStateBounds(): [number, number] { 221 return [this.visibleWindowTime.start, this.visibleWindowTime.end]; 222 } 223 224 // Whenever start/end px of the timeScale is changed, update 225 // the resolution. 226 updateLocalLimits(pxStart: number, pxEnd: number) { 227 // Numbers received here can be negative or equal, but we should fix that 228 // before updating the timescale. 229 pxStart = Math.max(0, pxStart); 230 pxEnd = Math.max(0, pxEnd); 231 if (pxStart === pxEnd) pxEnd = pxStart + 1; 232 this.timeScale.setLimitsPx(pxStart, pxEnd); 233 this.updateResolution(); 234 } 235} 236