blob: 54b55f8fbdd06b216b6020c78192cddecd378d4d [file] [log] [blame]
// 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 {Actions} from '../common/actions';
import {HttpRpcState} from '../common/http_rpc_engine';
import {
FrontendLocalState as FrontendState,
OmniboxState,
Timestamped,
TimestampedAreaSelection,
VisibleState,
} from '../common/state';
import {TimeSpan} from '../common/time';
import {randomColor} from './colorizer';
import {globals} from './globals';
import {debounce, ratelimit} from './rate_limiters';
import {TimeScale} from './time_scale';
interface Range {
start?: number;
end?: number;
}
function chooseLatest<T extends Timestamped<{}>>(current: T, next: T): T {
if (next !== current && next.lastUpdate > current.lastUpdate) {
return 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]);
perfDebug = false;
hoveredUtid = -1;
hoveredPid = -1;
hoveredLogsTimestamp = -1;
hoveredNoteTimestamp = -1;
vidTimestamp = -1;
localOnlyMode = false;
sidebarVisible = true;
showPanningHint = false;
visibleTracks = new Set<string>();
prevVisibleTracks = new Set<string>();
searchIndex = -1;
currentTab?: string;
scrollToTrackId?: string|number;
httpRpcState: HttpRpcState = {connected: false};
newVersionAvailable = false;
// This is used to calculate the tracks within a Y range for area selection.
areaY: Range = {};
// True if the user is in the process of doing an area selection.
selectingArea = false;
private scrollBarWidth?: number;
private _omniboxState: OmniboxState = {
lastUpdate: 0,
omnibox: '',
mode: 'SEARCH',
};
private _visibleState: VisibleState = {
lastUpdate: 0,
startSec: 0,
endSec: 10,
resolution: 1,
};
private _selectedArea: TimestampedAreaSelection = {
lastUpdate: 0,
};
// 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;
}
togglePerfDebug() {
this.perfDebug = !this.perfDebug;
globals.rafScheduler.scheduleFullRedraw();
}
setHoveredUtidAndPid(utid: number, pid: number) {
this.hoveredUtid = utid;
this.hoveredPid = pid;
globals.rafScheduler.scheduleRedraw();
}
// Sets the timestamp at which a vertical line will be drawn.
setHoveredLogsTimestamp(ts: number) {
if (this.hoveredLogsTimestamp === ts) return;
this.hoveredLogsTimestamp = ts;
globals.rafScheduler.scheduleRedraw();
}
setHoveredNoteTimestamp(ts: number) {
if (this.hoveredNoteTimestamp === ts) return;
this.hoveredNoteTimestamp = ts;
globals.rafScheduler.scheduleRedraw();
}
setVidTimestamp(ts: number) {
if (this.vidTimestamp === ts) return;
this.vidTimestamp = ts;
globals.rafScheduler.scheduleRedraw();
}
addVisibleTrack(trackId: string) {
this.visibleTracks.add(trackId);
}
setSearchIndex(index: number) {
this.searchIndex = index;
globals.rafScheduler.scheduleRedraw();
}
toggleSidebar() {
this.sidebarVisible = !this.sidebarVisible;
globals.rafScheduler.scheduleFullRedraw();
}
setHttpRpcState(httpRpcState: HttpRpcState) {
this.httpRpcState = httpRpcState;
globals.rafScheduler.scheduleFullRedraw();
}
// Called when beginning a canvas redraw.
clearVisibleTracks() {
this.prevVisibleTracks = new Set(this.visibleTracks);
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)}));
}
}
mergeState(state: FrontendState): void {
this._omniboxState = chooseLatest(this._omniboxState, state.omniboxState);
this._visibleState = chooseLatest(this._visibleState, state.visibleState);
this._selectedArea = chooseLatest(this._selectedArea, state.selectedArea);
if (this._visibleState === state.visibleState) {
this.updateLocalTime(
new TimeSpan(this._visibleState.startSec, this._visibleState.endSec));
}
}
private selectAreaDebounced = debounce(() => {
globals.dispatch(Actions.selectArea(this._selectedArea));
}, 20);
selectArea(
startSec: number, endSec: number,
tracks = this._selectedArea.area ? this._selectedArea.area.tracks : []) {
if (this.currentNoteSelectionEqualToCurrentAreaSelection()) {
globals.dispatch(Actions.deselect({}));
}
this.showPanningHint = true;
this._selectedArea = {
area: {startSec, endSec, tracks},
lastUpdate: Date.now() / 1000
};
this.selectAreaDebounced();
globals.rafScheduler.scheduleFullRedraw();
}
toggleTrackSelection(id: string, isTrackGroup = false) {
const area = this._selectedArea.area;
if (!area) return;
const index = area.tracks.indexOf(id);
if (index > -1) {
area.tracks.splice(index, 1);
if (isTrackGroup) { // Also remove all child tracks.
for (const childTrack of globals.state.trackGroups[id].tracks) {
const childIndex = area.tracks.indexOf(childTrack);
if (childIndex > -1) {
area.tracks.splice(childIndex, 1);
}
}
}
} else {
area.tracks.push(id);
if (isTrackGroup) { // Also add all child tracks.
for (const childTrack of globals.state.trackGroups[id].tracks) {
if (!area.tracks.includes(childTrack)) {
area.tracks.push(childTrack);
}
}
}
}
this._selectedArea.lastUpdate = Date.now() / 1000;
this.selectAreaDebounced();
globals.rafScheduler.scheduleFullRedraw();
}
toggleLockArea() {
if (!this._selectedArea.area) return;
if (this.currentNoteSelectionEqualToCurrentAreaSelection()) {
if (globals.state.currentSelection != null &&
globals.state.currentSelection.kind === 'NOTE') {
globals.dispatch(
Actions.removeNote({id: globals.state.currentSelection.id}));
}
} else {
const color = randomColor();
globals.dispatch(Actions.addAreaNote({
timestamp: this._selectedArea.area.startSec,
area: this._selectedArea.area,
color
}));
}
globals.rafScheduler.scheduleFullRedraw();
}
deselectArea() {
// When an area is deselected (and it is marked) also deselect the current
// marked selection if it is for the same area.
if (this.currentNoteSelectionEqualToCurrentAreaSelection()) {
globals.dispatch(Actions.deselect({}));
}
this._selectedArea = {lastUpdate: Date.now() / 1000};
this.selectAreaDebounced();
globals.frontendLocalState.currentTab = undefined;
globals.rafScheduler.scheduleFullRedraw();
}
get selectedArea(): TimestampedAreaSelection {
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(this.timeScale.startPx, this.timeScale.endPx);
}
// We lock an area selection by adding an area note. When we select the note
// it will also select the area but then the user can select other things,
// like a slice or different note and the area note will be deselected even
// though the area selection remains. So it is useful to know if we currently
// have the same area note selected and area selection.
private currentNoteSelectionEqualToCurrentAreaSelection() {
if (!this._selectedArea.area) return false;
if (globals.state.currentSelection != null &&
globals.state.currentSelection.kind === 'NOTE') {
const curNote = globals.state.notes[globals.state.currentSelection.id];
// TODO(taylori): Do the tracks need to be the same too?
if (curNote.noteType === 'AREA' &&
curNote.area.startSec === this._selectedArea.area.startSec &&
curNote.area.endSec === this._selectedArea.area.endSec) {
return true;
}
}
return false;
}
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();
}
updateResolution(pxStart: number, pxEnd: number) {
this.timeScale.setLimitsPx(pxStart, pxEnd);
this._visibleState.lastUpdate = Date.now() / 1000;
this._visibleState.resolution = globals.getCurResolution();
this.ratelimitedUpdateVisible();
}
}