• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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