• 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 * as m from 'mithril';
16
17import {Actions} from '../common/actions';
18import {LogExists, LogExistsKey} from '../common/logs';
19import {QueryResponse} from '../common/queries';
20import {TimeSpan} from '../common/time';
21
22import {copyToClipboard} from './clipboard';
23import {DragGestureHandler} from './drag_gesture_handler';
24import {globals} from './globals';
25import {LogPanel} from './logs_panel';
26import {NotesEditorPanel, NotesPanel} from './notes_panel';
27import {OverviewTimelinePanel} from './overview_timeline_panel';
28import {createPage} from './pages';
29import {PanAndZoomHandler} from './pan_and_zoom_handler';
30import {Panel} from './panel';
31import {AnyAttrsVnode, PanelContainer} from './panel_container';
32import {SliceDetailsPanel} from './slice_panel';
33import {ThreadStatePanel} from './thread_state_panel';
34import {TimeAxisPanel} from './time_axis_panel';
35import {computeZoom} from './time_scale';
36import {TimeSelectionPanel} from './time_selection_panel';
37import {TRACK_SHELL_WIDTH} from './track_constants';
38import {TrackGroupPanel} from './track_group_panel';
39import {TrackPanel} from './track_panel';
40
41const DRAG_HANDLE_HEIGHT_PX = 28;
42const DEFAULT_DETAILS_HEIGHT_PX = 230 + DRAG_HANDLE_HEIGHT_PX;
43const UP_ICON = 'keyboard_arrow_up';
44const DOWN_ICON = 'keyboard_arrow_down';
45
46function hasLogs(): boolean {
47  const data = globals.trackDataStore.get(LogExistsKey) as LogExists;
48  return data && data.exists;
49}
50
51class QueryTable extends Panel {
52  view() {
53    const resp = globals.queryResults.get('command') as QueryResponse;
54    if (resp === undefined) {
55      return m('');
56    }
57    const cols = [];
58    for (const col of resp.columns) {
59      cols.push(m('td', col));
60    }
61    const header = m('tr', cols);
62
63    const rows = [];
64    for (let i = 0; i < resp.rows.length; i++) {
65      const cells = [];
66      for (const col of resp.columns) {
67        cells.push(m('td', resp.rows[i][col]));
68      }
69      rows.push(m('tr', cells));
70    }
71    return m(
72        'div',
73        m('header.overview',
74          `Query result - ${Math.round(resp.durationMs)} ms`,
75          m('span.code', resp.query),
76          resp.error ? null :
77                       m('button.query-ctrl',
78                         {
79                           onclick: () => {
80                             const lines: string[][] = [];
81                             lines.push(resp.columns);
82                             for (const row of resp.rows) {
83                               const line = [];
84                               for (const col of resp.columns) {
85                                 line.push(row[col].toString());
86                               }
87                               lines.push(line);
88                             }
89                             copyToClipboard(
90                                 lines.map(line => line.join('\t')).join('\n'));
91                           },
92                         },
93                         'Copy as .tsv'),
94          m('button.query-ctrl',
95            {
96              onclick: () => {
97                globals.queryResults.delete('command');
98                globals.rafScheduler.scheduleFullRedraw();
99              }
100            },
101            'Close'), ),
102        resp.error ?
103            m('.query-error', `SQL error: ${resp.error}`) :
104            m('table.query-table', m('thead', header), m('tbody', rows)));
105  }
106
107  renderCanvas() {}
108}
109
110interface DragHandleAttrs {
111  height: number;
112  resize: (height: number) => void;
113}
114
115class DragHandle implements m.ClassComponent<DragHandleAttrs> {
116  private dragStartHeight = 0;
117  private height = 0;
118  private resize: (height: number) => void = () => {};
119  private isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX;
120
121  oncreate({dom, attrs}: m.CVnodeDOM<DragHandleAttrs>) {
122    this.resize = attrs.resize;
123    this.height = attrs.height;
124    const elem = dom as HTMLElement;
125    new DragGestureHandler(
126        elem,
127        this.onDrag.bind(this),
128        this.onDragStart.bind(this),
129        this.onDragEnd.bind(this));
130  }
131
132  onupdate({attrs}: m.CVnodeDOM<DragHandleAttrs>) {
133    this.resize = attrs.resize;
134    this.height = attrs.height;
135  }
136
137  onDrag(_x: number, y: number) {
138    const newHeight = this.dragStartHeight + (DRAG_HANDLE_HEIGHT_PX / 2) - y;
139    this.isClosed = Math.floor(newHeight) <= DRAG_HANDLE_HEIGHT_PX;
140    this.resize(Math.floor(newHeight));
141    globals.rafScheduler.scheduleFullRedraw();
142  }
143
144  onDragStart(_x: number, _y: number) {
145    this.dragStartHeight = this.height;
146  }
147
148  onDragEnd() {}
149
150  view() {
151    const icon = this.isClosed ? UP_ICON : DOWN_ICON;
152    const title = this.isClosed ? 'Show panel' : 'Hide panel';
153    return m(
154        '.handle',
155        m('.handle-title', 'Current Selection'),
156        m('i.material-icons',
157          {
158            onclick: () => {
159              if (this.height === DRAG_HANDLE_HEIGHT_PX) {
160                this.isClosed = false;
161                this.resize(DEFAULT_DETAILS_HEIGHT_PX);
162              } else {
163                this.isClosed = true;
164                this.resize(DRAG_HANDLE_HEIGHT_PX);
165              }
166              globals.rafScheduler.scheduleFullRedraw();
167            },
168            title
169          },
170          icon));
171  }
172}
173
174/**
175 * Top-most level component for the viewer page. Holds tracks, brush timeline,
176 * panels, and everything else that's part of the main trace viewer page.
177 */
178class TraceViewer implements m.ClassComponent {
179  private onResize: () => void = () => {};
180  private zoomContent?: PanAndZoomHandler;
181  private detailsHeight = DRAG_HANDLE_HEIGHT_PX;
182  // Used to set details panel to default height on selection.
183  private showDetailsPanel = true;
184  // Used to prevent global deselection if a pan/drag select occurred.
185  private keepCurrentSelection = false;
186
187  oncreate(vnode: m.CVnodeDOM) {
188    const frontendLocalState = globals.frontendLocalState;
189    const updateDimensions = () => {
190      const rect = vnode.dom.getBoundingClientRect();
191      frontendLocalState.timeScale.setLimitsPx(
192          0, rect.width - TRACK_SHELL_WIDTH);
193    };
194
195    updateDimensions();
196
197    // TODO: Do resize handling better.
198    this.onResize = () => {
199      updateDimensions();
200      globals.rafScheduler.scheduleFullRedraw();
201    };
202
203    // Once ResizeObservers are out, we can stop accessing the window here.
204    window.addEventListener('resize', this.onResize);
205
206    const panZoomEl =
207        vnode.dom.querySelector('.pan-and-zoom-content') as HTMLElement;
208
209    this.zoomContent = new PanAndZoomHandler({
210      element: panZoomEl,
211      contentOffsetX: TRACK_SHELL_WIDTH,
212      onPanned: (pannedPx: number) => {
213        this.keepCurrentSelection = true;
214        const traceTime = globals.state.traceTime;
215        const vizTime = globals.frontendLocalState.visibleWindowTime;
216        const origDelta = vizTime.duration;
217        const tDelta = frontendLocalState.timeScale.deltaPxToDuration(pannedPx);
218        let tStart = vizTime.start + tDelta;
219        let tEnd = vizTime.end + tDelta;
220        if (tStart < traceTime.startSec) {
221          tStart = traceTime.startSec;
222          tEnd = tStart + origDelta;
223        } else if (tEnd > traceTime.endSec) {
224          tEnd = traceTime.endSec;
225          tStart = tEnd - origDelta;
226        }
227        frontendLocalState.updateVisibleTime(new TimeSpan(tStart, tEnd));
228        globals.rafScheduler.scheduleRedraw();
229      },
230      onZoomed: (zoomedPositionPx: number, zoomRatio: number) => {
231        // TODO(hjd): Avoid hardcoding TRACK_SHELL_WIDTH.
232        // TODO(hjd): Improve support for zooming in overview timeline.
233        const span = frontendLocalState.visibleWindowTime;
234        const scale = frontendLocalState.timeScale;
235        const zoomPx = zoomedPositionPx - TRACK_SHELL_WIDTH;
236        const newSpan = computeZoom(scale, span, 1 - zoomRatio, zoomPx);
237        frontendLocalState.updateVisibleTime(newSpan);
238        globals.rafScheduler.scheduleRedraw();
239      },
240      onDragSelect: (selectStartPx: number|null, selectEndPx: number) => {
241        if (!selectStartPx) return;
242        this.keepCurrentSelection = true;
243        globals.frontendLocalState.setShowTimeSelectPreview(false);
244        const traceTime = globals.state.traceTime;
245        const scale = frontendLocalState.timeScale;
246        const startPx = Math.min(selectStartPx, selectEndPx);
247        const endPx = Math.max(selectStartPx, selectEndPx);
248        const startTs = Math.max(traceTime.startSec,
249                               scale.pxToTime(startPx - TRACK_SHELL_WIDTH));
250        const endTs = Math.min(traceTime.endSec,
251                               scale.pxToTime(endPx - TRACK_SHELL_WIDTH));
252        globals.dispatch(Actions.selectTimeSpan({startTs, endTs}));
253        globals.rafScheduler.scheduleRedraw();
254      }
255    });
256  }
257
258  onremove() {
259    window.removeEventListener('resize', this.onResize);
260    if (this.zoomContent) this.zoomContent.shutdown();
261  }
262
263  view() {
264    const scrollingPanels: AnyAttrsVnode[] =
265        globals.state.scrollingTracks.map(id => m(TrackPanel, {key: id, id}));
266
267    for (const group of Object.values(globals.state.trackGroups)) {
268      scrollingPanels.push(m(TrackGroupPanel, {
269        trackGroupId: group.id,
270        key: `trackgroup-${group.id}`,
271      }));
272      if (group.collapsed) continue;
273      for (const trackId of group.tracks) {
274        scrollingPanels.push(m(TrackPanel, {
275          key: `track-${group.id}-${trackId}`,
276          id: trackId,
277        }));
278      }
279    }
280    scrollingPanels.unshift(m(QueryTable));
281
282    const detailsPanels: AnyAttrsVnode[] = [];
283    const curSelection = globals.state.currentSelection;
284    if (curSelection) {
285      switch (curSelection.kind) {
286        case 'NOTE':
287          detailsPanels.push(m(NotesEditorPanel, {
288            key: 'notes',
289            id: curSelection.id,
290          }));
291          break;
292        case 'SLICE':
293          detailsPanels.push(m(SliceDetailsPanel, {
294            key: 'slice',
295            utid: curSelection.utid,
296          }));
297          break;
298        case 'THREAD_STATE':
299          detailsPanels.push(m(ThreadStatePanel, {
300            key: 'thread_state',
301            ts: curSelection.ts,
302            dur: curSelection.dur,
303            utid: curSelection.utid,
304            state: curSelection.state
305          }));
306          break;
307        default:
308          break;
309      }
310    } else if (hasLogs()) {
311      detailsPanels.push(m(LogPanel, {}));
312    }
313
314    this.showDetailsPanel = detailsPanels.length > 0;
315
316    return m(
317        '.page',
318        m('.pan-and-zoom-content',
319          {
320            onclick: () => {
321              // We don't want to deselect when panning/drag selecting.
322              if (this.keepCurrentSelection) {
323                this.keepCurrentSelection = false;
324                return;
325              }
326              globals.dispatch(Actions.deselect({}));
327            }
328          },
329          m('.pinned-panel-container', m(PanelContainer, {
330              doesScroll: false,
331              panels: [
332                m(OverviewTimelinePanel, {key: 'overview'}),
333                m(TimeAxisPanel, {key: 'timeaxis'}),
334                m(TimeSelectionPanel, {key: 'timeselection'}),
335                m(NotesPanel, {key: 'notes'}),
336                ...globals.state.pinnedTracks.map(
337                    id => m(TrackPanel, {key: id, id})),
338              ],
339            })),
340          m('.scrolling-panel-container', m(PanelContainer, {
341              doesScroll: true,
342              panels: scrollingPanels,
343            }))),
344        m('.details-content',
345          {
346            style: {
347              height: `${this.detailsHeight}px`,
348              display: this.showDetailsPanel ? null : 'none'
349            }
350          },
351          m(DragHandle, {
352            resize: (height: number) => {
353              this.detailsHeight = Math.max(height, DRAG_HANDLE_HEIGHT_PX);
354            },
355            height: this.detailsHeight,
356          }),
357          m('.details-panel-container',
358            m(PanelContainer, {doesScroll: true, panels: detailsPanels}))));
359  }
360}
361
362export const ViewerPage = createPage({
363  view() {
364    return m(TraceViewer);
365  }
366});
367