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