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 return next; 39 } 40 return current; 41} 42 43function capBetween(t: number, start: number, end: number) { 44 return Math.min(Math.max(t, start), end); 45} 46 47// Calculate the space a scrollbar takes up so that we can subtract it from 48// the canvas width. 49function calculateScrollbarWidth() { 50 const outer = document.createElement('div'); 51 outer.style.overflowY = 'scroll'; 52 const inner = document.createElement('div'); 53 outer.appendChild(inner); 54 document.body.appendChild(outer); 55 const width = 56 outer.getBoundingClientRect().width - inner.getBoundingClientRect().width; 57 document.body.removeChild(outer); 58 return width; 59} 60 61/** 62 * State that is shared between several frontend components, but not the 63 * controller. This state is updated at 60fps. 64 */ 65export class FrontendLocalState { 66 visibleWindowTime = new TimeSpan(0, 10); 67 timeScale = new TimeScale(this.visibleWindowTime, [0, 0]); 68 perfDebug = false; 69 hoveredUtid = -1; 70 hoveredPid = -1; 71 hoveredLogsTimestamp = -1; 72 hoveredNoteTimestamp = -1; 73 highlightedSliceId = -1; 74 focusedFlowIdLeft = -1; 75 focusedFlowIdRight = -1; 76 vidTimestamp = -1; 77 localOnlyMode = false; 78 sidebarVisible = true; 79 showPanningHint = false; 80 showCookieConsent = false; 81 visibleTracks = new Set<string>(); 82 prevVisibleTracks = new Set<string>(); 83 searchIndex = -1; 84 currentTab?: string; 85 scrollToTrackId?: string|number; 86 httpRpcState: HttpRpcState = {connected: false}; 87 newVersionAvailable = false; 88 89 // This is used to calculate the tracks within a Y range for area selection. 90 areaY: Range = {}; 91 92 private scrollBarWidth?: number; 93 94 private _omniboxState: OmniboxState = { 95 lastUpdate: 0, 96 omnibox: '', 97 mode: 'SEARCH', 98 }; 99 100 private _visibleState: VisibleState = { 101 lastUpdate: 0, 102 startSec: 0, 103 endSec: 10, 104 resolution: 1, 105 }; 106 107 private _selectedArea?: Area; 108 109 // TODO: there is some redundancy in the fact that both |visibleWindowTime| 110 // and a |timeScale| have a notion of time range. That should live in one 111 // place only. 112 113 getScrollbarWidth() { 114 if (this.scrollBarWidth === undefined) { 115 this.scrollBarWidth = calculateScrollbarWidth(); 116 } 117 return this.scrollBarWidth; 118 } 119 120 togglePerfDebug() { 121 this.perfDebug = !this.perfDebug; 122 globals.rafScheduler.scheduleFullRedraw(); 123 } 124 125 setHoveredUtidAndPid(utid: number, pid: number) { 126 this.hoveredUtid = utid; 127 this.hoveredPid = pid; 128 globals.rafScheduler.scheduleRedraw(); 129 } 130 131 setHighlightedSliceId(sliceId: number) { 132 this.highlightedSliceId = sliceId; 133 globals.rafScheduler.scheduleRedraw(); 134 } 135 136 setHighlightedFlowLeftId(flowId: number) { 137 this.focusedFlowIdLeft = flowId; 138 globals.rafScheduler.scheduleFullRedraw(); 139 } 140 141 setHighlightedFlowRightId(flowId: number) { 142 this.focusedFlowIdRight = flowId; 143 globals.rafScheduler.scheduleFullRedraw(); 144 } 145 146 // Sets the timestamp at which a vertical line will be drawn. 147 setHoveredLogsTimestamp(ts: number) { 148 if (this.hoveredLogsTimestamp === ts) return; 149 this.hoveredLogsTimestamp = ts; 150 globals.rafScheduler.scheduleRedraw(); 151 } 152 153 setHoveredNoteTimestamp(ts: number) { 154 if (this.hoveredNoteTimestamp === ts) return; 155 this.hoveredNoteTimestamp = ts; 156 globals.rafScheduler.scheduleRedraw(); 157 } 158 159 setVidTimestamp(ts: number) { 160 if (this.vidTimestamp === ts) return; 161 this.vidTimestamp = ts; 162 globals.rafScheduler.scheduleRedraw(); 163 } 164 165 addVisibleTrack(trackId: string) { 166 this.visibleTracks.add(trackId); 167 } 168 169 setSearchIndex(index: number) { 170 this.searchIndex = index; 171 globals.rafScheduler.scheduleRedraw(); 172 } 173 174 toggleSidebar() { 175 this.sidebarVisible = !this.sidebarVisible; 176 globals.rafScheduler.scheduleFullRedraw(); 177 } 178 179 setHttpRpcState(httpRpcState: HttpRpcState) { 180 this.httpRpcState = httpRpcState; 181 globals.rafScheduler.scheduleFullRedraw(); 182 } 183 184 // Called when beginning a canvas redraw. 185 clearVisibleTracks() { 186 this.visibleTracks.clear(); 187 } 188 189 // Called when the canvas redraw is complete. 190 sendVisibleTracks() { 191 if (this.prevVisibleTracks.size !== this.visibleTracks.size || 192 ![...this.prevVisibleTracks].every( 193 value => this.visibleTracks.has(value))) { 194 globals.dispatch( 195 Actions.setVisibleTracks({tracks: Array.from(this.visibleTracks)})); 196 this.prevVisibleTracks = new Set(this.visibleTracks); 197 } 198 } 199 200 mergeState(state: FrontendState): void { 201 this._omniboxState = chooseLatest(this._omniboxState, state.omniboxState); 202 this._visibleState = chooseLatest(this._visibleState, state.visibleState); 203 if (this._visibleState === state.visibleState) { 204 this.updateLocalTime( 205 new TimeSpan(this._visibleState.startSec, this._visibleState.endSec)); 206 } 207 } 208 209 selectArea( 210 startSec: number, endSec: number, 211 tracks = this._selectedArea ? this._selectedArea.tracks : []) { 212 assertTrue(endSec >= startSec); 213 this.showPanningHint = true; 214 this._selectedArea = {startSec, endSec, tracks}, 215 globals.rafScheduler.scheduleFullRedraw(); 216 } 217 218 deselectArea() { 219 this._selectedArea = undefined; 220 globals.rafScheduler.scheduleRedraw(); 221 } 222 223 get selectedArea(): Area|undefined { 224 return this._selectedArea; 225 } 226 227 private setOmniboxDebounced = debounce(() => { 228 globals.dispatch(Actions.setOmnibox(this._omniboxState)); 229 }, 20); 230 231 setOmnibox(value: string, mode: 'SEARCH'|'COMMAND') { 232 this._omniboxState.omnibox = value; 233 this._omniboxState.mode = mode; 234 this._omniboxState.lastUpdate = Date.now() / 1000; 235 this.setOmniboxDebounced(); 236 } 237 238 get omnibox(): string { 239 return this._omniboxState.omnibox; 240 } 241 242 private ratelimitedUpdateVisible = ratelimit(() => { 243 globals.dispatch(Actions.setVisibleTraceTime(this._visibleState)); 244 }, 50); 245 246 private updateLocalTime(ts: TimeSpan) { 247 const traceTime = globals.state.traceTime; 248 const startSec = capBetween(ts.start, traceTime.startSec, traceTime.endSec); 249 const endSec = capBetween(ts.end, traceTime.startSec, traceTime.endSec); 250 this.visibleWindowTime = new TimeSpan(startSec, endSec); 251 this.timeScale.setTimeBounds(this.visibleWindowTime); 252 this.updateResolution(); 253 } 254 255 private updateResolution() { 256 this._visibleState.lastUpdate = Date.now() / 1000; 257 this._visibleState.resolution = globals.getCurResolution(); 258 this.ratelimitedUpdateVisible(); 259 } 260 261 updateVisibleTime(ts: TimeSpan) { 262 this.updateLocalTime(ts); 263 this._visibleState.lastUpdate = Date.now() / 1000; 264 this._visibleState.startSec = this.visibleWindowTime.start; 265 this._visibleState.endSec = this.visibleWindowTime.end; 266 this._visibleState.resolution = globals.getCurResolution(); 267 this.ratelimitedUpdateVisible(); 268 } 269 270 getVisibleStateBounds(): [number, number] { 271 return [this.visibleWindowTime.start, this.visibleWindowTime.end]; 272 } 273 274 // Whenever start/end px of the timeScale is changed, update 275 // the resolution. 276 updateLocalLimits(pxStart: number, pxEnd: number) { 277 // Numbers received here can be negative or equal, but we should fix that 278 // before updating the timescale. 279 pxStart = Math.max(0, pxStart); 280 pxEnd = Math.max(0, pxEnd); 281 if (pxStart === pxEnd) pxEnd = pxStart + 1; 282 this.timeScale.setLimitsPx(pxStart, pxEnd); 283 this.updateResolution(); 284 } 285} 286