• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2019 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 {Actions} from '../common/actions';
16import {Area} from '../common/state';
17import {TPTime} from '../common/time';
18
19import {Flow, globals} from './globals';
20import {toggleHelp} from './help_modal';
21import {
22  focusHorizontalRange,
23  verticalScrollToTrack,
24} from './scroll_helper';
25import {executeSearch} from './search_handler';
26
27const INSTANT_FOCUS_DURATION = 1n;
28const INCOMPLETE_SLICE_DURATION = 30_000n;
29type Direction = 'Forward'|'Backward';
30
31// Handles all key events than are not handled by the
32// pan and zoom handler. Returns true if the event was handled.
33export function handleKey(e: KeyboardEvent, down: boolean): boolean {
34  const key = e.key.toLowerCase();
35  const selection = globals.state.currentSelection;
36  const noModifiers = !(e.ctrlKey || e.metaKey || e.altKey || e.shiftKey);
37  const ctrlOrMeta = (e.ctrlKey || e.metaKey) && !(e.altKey || e.shiftKey);
38  // No other modifiers other than possibly Shift.
39  const maybeShift = !(e.ctrlKey || e.metaKey || e.altKey);
40
41  if (down && 'm' === key && maybeShift) {
42    if (selection && selection.kind === 'AREA') {
43      globals.dispatch(Actions.toggleMarkCurrentArea({persistent: e.shiftKey}));
44    } else if (selection) {
45      lockSliceSpan(e.shiftKey);
46    }
47    return true;
48  }
49  if (down && 'f' === key && noModifiers) {
50    findCurrentSelection();
51    return true;
52  }
53  if (down && 'a' === key && ctrlOrMeta) {
54    let tracksToSelect: string[] = [];
55
56    const selection = globals.state.currentSelection;
57    if (selection !== null && selection.kind === 'AREA') {
58      const area = globals.state.areas[selection.areaId];
59      const coversEntireTimeRange =
60          globals.state.traceTime.start === area.start &&
61          globals.state.traceTime.end === area.end;
62      if (!coversEntireTimeRange) {
63        // If the current selection is an area which does not cover the entire
64        // time range, preserve the list of selected tracks and expand the time
65        // range.
66        tracksToSelect = area.tracks;
67      } else {
68        // If the entire time range is already covered, update the selection to
69        // cover all tracks.
70        tracksToSelect = Object.keys(globals.state.tracks);
71      }
72    } else {
73      // If the current selection is not an area, select all.
74      tracksToSelect = Object.keys(globals.state.tracks);
75    }
76    const {start, end} = globals.state.traceTime;
77    globals.dispatch(Actions.selectArea({
78      area: {
79        start,
80        end,
81        tracks: tracksToSelect,
82      },
83    }));
84    e.preventDefault();
85    return true;
86  }
87  if (down && 'b' === key && ctrlOrMeta) {
88    globals.dispatch(Actions.toggleSidebar({}));
89    return true;
90  }
91  if (down && '?' === key && maybeShift) {
92    toggleHelp();
93    return true;
94  }
95  if (down && 'enter' === key && maybeShift) {
96    e.preventDefault();
97    executeSearch(e.shiftKey);
98    return true;
99  }
100  if (down && 'escape' === key) {
101    globals.frontendLocalState.deselectArea();
102    globals.makeSelection(Actions.deselect({}));
103    globals.dispatch(Actions.removeNote({id: '0'}));
104    return true;
105  }
106  if (down && ']' === key && ctrlOrMeta) {
107    focusOtherFlow('Forward');
108    return true;
109  }
110  if (down && ']' === key && noModifiers) {
111    moveByFocusedFlow('Forward');
112    return true;
113  }
114  if (down && '[' === key && ctrlOrMeta) {
115    focusOtherFlow('Backward');
116    return true;
117  }
118  if (down && '[' === key && noModifiers) {
119    moveByFocusedFlow('Backward');
120    return true;
121  }
122  return false;
123}
124
125// Search |boundFlows| for |flowId| and return the id following it.
126// Returns the first flow id if nothing was found or |flowId| was the last flow
127// in |boundFlows|, and -1 if |boundFlows| is empty
128function findAnotherFlowExcept(boundFlows: Flow[], flowId: number): number {
129  let selectedFlowFound = false;
130
131  if (boundFlows.length === 0) {
132    return -1;
133  }
134
135  for (const flow of boundFlows) {
136    if (selectedFlowFound) {
137      return flow.id;
138    }
139
140    if (flow.id === flowId) {
141      selectedFlowFound = true;
142    }
143  }
144  return boundFlows[0].id;
145}
146
147// Change focus to the next flow event (matching the direction)
148function focusOtherFlow(direction: Direction) {
149  if (!globals.state.currentSelection ||
150      globals.state.currentSelection.kind !== 'CHROME_SLICE') {
151    return;
152  }
153  const sliceId = globals.state.currentSelection.id;
154  if (sliceId === -1) {
155    return;
156  }
157
158  const boundFlows = globals.connectedFlows.filter(
159      (flow) => flow.begin.sliceId === sliceId && direction === 'Forward' ||
160          flow.end.sliceId === sliceId && direction === 'Backward');
161
162  if (direction === 'Backward') {
163    const nextFlowId =
164        findAnotherFlowExcept(boundFlows, globals.state.focusedFlowIdLeft);
165    globals.dispatch(Actions.setHighlightedFlowLeftId({flowId: nextFlowId}));
166  } else {
167    const nextFlowId =
168        findAnotherFlowExcept(boundFlows, globals.state.focusedFlowIdRight);
169    globals.dispatch(Actions.setHighlightedFlowRightId({flowId: nextFlowId}));
170  }
171}
172
173// Select the slice connected to the flow in focus
174function moveByFocusedFlow(direction: Direction): void {
175  if (!globals.state.currentSelection ||
176      globals.state.currentSelection.kind !== 'CHROME_SLICE') {
177    return;
178  }
179
180  const sliceId = globals.state.currentSelection.id;
181  const flowId =
182      (direction === 'Backward' ? globals.state.focusedFlowIdLeft :
183                                  globals.state.focusedFlowIdRight);
184
185  if (sliceId === -1 || flowId === -1) {
186    return;
187  }
188
189  // Find flow that is in focus and select corresponding slice
190  for (const flow of globals.connectedFlows) {
191    if (flow.id === flowId) {
192      const flowPoint = (direction === 'Backward' ? flow.begin : flow.end);
193      const uiTrackId =
194          globals.state.uiTrackIdByTraceTrackId[flowPoint.trackId];
195      if (uiTrackId) {
196        globals.makeSelection(Actions.selectChromeSlice({
197          id: flowPoint.sliceId,
198          trackId: uiTrackId,
199          table: 'slice',
200          scroll: true,
201        }));
202      }
203    }
204  }
205}
206
207function findTimeRangeOfSelection(): {startTs: TPTime, endTs: TPTime} {
208  const selection = globals.state.currentSelection;
209  let startTs = -1n;
210  let endTs = -1n;
211  if (selection === null) {
212    return {startTs, endTs};
213  } else if (selection.kind === 'SLICE' || selection.kind === 'CHROME_SLICE') {
214    const slice = globals.sliceDetails;
215    if (slice.ts && slice.dur !== undefined && slice.dur > 0) {
216      startTs = slice.ts;
217      endTs = startTs + slice.dur;
218    } else if (slice.ts) {
219      startTs = slice.ts;
220      // This will handle either:
221      // a)slice.dur === -1 -> unfinished slice
222      // b)slice.dur === 0  -> instant event
223      endTs = slice.dur === -1n ? startTs + INCOMPLETE_SLICE_DURATION :
224                                  startTs + INSTANT_FOCUS_DURATION;
225    }
226  } else if (selection.kind === 'THREAD_STATE') {
227    const threadState = globals.threadStateDetails;
228    if (threadState.ts && threadState.dur) {
229      startTs = threadState.ts;
230      endTs = startTs + threadState.dur;
231    }
232  } else if (selection.kind === 'COUNTER') {
233    startTs = selection.leftTs;
234    endTs = selection.rightTs;
235  } else if (selection.kind === 'AREA') {
236    const selectedArea = globals.state.areas[selection.areaId];
237    if (selectedArea) {
238      startTs = selectedArea.start;
239      endTs = selectedArea.end;
240    }
241  } else if (selection.kind === 'NOTE') {
242    const selectedNote = globals.state.notes[selection.id];
243    // Notes can either be default or area notes. Area notes are handled
244    // above in the AREA case.
245    if (selectedNote && selectedNote.noteType === 'DEFAULT') {
246      startTs = selectedNote.timestamp;
247      endTs = selectedNote.timestamp + INSTANT_FOCUS_DURATION;
248    }
249  } else if (selection.kind === 'LOG') {
250    // TODO(hjd): Make focus selection work for logs.
251  } else if (
252      selection.kind === 'DEBUG_SLICE' ||
253      selection.kind === 'TOP_LEVEL_SCROLL') {
254    startTs = selection.start;
255    if (selection.duration > 0) {
256      endTs = startTs + selection.duration;
257    } else {
258      endTs = startTs + INSTANT_FOCUS_DURATION;
259    }
260  }
261
262  return {startTs, endTs};
263}
264
265
266function lockSliceSpan(persistent = false) {
267  const range = findTimeRangeOfSelection();
268  if (range.startTs !== -1n && range.endTs !== -1n &&
269      globals.state.currentSelection !== null) {
270    const tracks = globals.state.currentSelection.trackId ?
271        [globals.state.currentSelection.trackId] :
272        [];
273    const area: Area = {start: range.startTs, end: range.endTs, tracks};
274    globals.dispatch(Actions.markArea({area, persistent}));
275  }
276}
277
278export function findCurrentSelection() {
279  const selection = globals.state.currentSelection;
280  if (selection === null) return;
281
282  const range = findTimeRangeOfSelection();
283  if (range.startTs !== -1n && range.endTs !== -1n) {
284    focusHorizontalRange(range.startTs, range.endTs);
285  }
286
287  if (selection.trackId) {
288    verticalScrollToTrack(selection.trackId, true);
289  }
290}
291