• 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 m from 'mithril';
16import {BigintMath} from '../base/bigint_math';
17
18import {Actions} from '../common/actions';
19import {featureFlags} from '../common/feature_flags';
20
21import {TRACK_SHELL_WIDTH} from './css_constants';
22import {DetailsPanel} from './details_panel';
23import {globals} from './globals';
24import {NotesPanel} from './notes_panel';
25import {OverviewTimelinePanel} from './overview_timeline_panel';
26import {createPage} from './pages';
27import {PanAndZoomHandler} from './pan_and_zoom_handler';
28import {AnyAttrsVnode, PanelContainer} from './panel_container';
29import {TickmarkPanel} from './tickmark_panel';
30import {TimeAxisPanel} from './time_axis_panel';
31import {TimeSelectionPanel} from './time_selection_panel';
32import {DISMISSED_PANNING_HINT_KEY} from './topbar';
33import {TrackGroupPanel} from './track_group_panel';
34import {TrackPanel} from './track_panel';
35
36const SIDEBAR_WIDTH = 256;
37
38const OVERVIEW_PANEL_FLAG = featureFlags.register({
39  id: 'overviewVisible',
40  name: 'Overview Panel',
41  description: 'Show the panel providing an overview of the trace',
42  defaultValue: true,
43});
44
45// Checks if the mousePos is within 3px of the start or end of the
46// current selected time range.
47function onTimeRangeBoundary(mousePos: number): 'START'|'END'|null {
48  const selection = globals.state.currentSelection;
49  if (selection !== null && selection.kind === 'AREA') {
50    // If frontend selectedArea exists then we are in the process of editing the
51    // time range and need to use that value instead.
52    const area = globals.frontendLocalState.selectedArea ?
53        globals.frontendLocalState.selectedArea :
54        globals.state.areas[selection.areaId];
55    const {visibleTimeScale} = globals.frontendLocalState;
56    const start = visibleTimeScale.tpTimeToPx(area.start);
57    const end = visibleTimeScale.tpTimeToPx(area.end);
58    const startDrag = mousePos - TRACK_SHELL_WIDTH;
59    const startDistance = Math.abs(start - startDrag);
60    const endDistance = Math.abs(end - startDrag);
61    const range = 3 * window.devicePixelRatio;
62    // We might be within 3px of both boundaries but we should choose
63    // the closest one.
64    if (startDistance < range && startDistance <= endDistance) return 'START';
65    if (endDistance < range && endDistance <= startDistance) return 'END';
66  }
67  return null;
68}
69
70export interface TrackGroupAttrs {
71  header: AnyAttrsVnode;
72  collapsed: boolean;
73  childTracks: AnyAttrsVnode[];
74}
75
76export class TrackGroup implements m.ClassComponent<TrackGroupAttrs> {
77  view() {
78    // TrackGroup component acts as a holder for a bunch of tracks rendered
79    // together: the actual rendering happens in PanelContainer. In order to
80    // avoid confusion, this method remains empty.
81  }
82}
83
84/**
85 * Top-most level component for the viewer page. Holds tracks, brush timeline,
86 * panels, and everything else that's part of the main trace viewer page.
87 */
88class TraceViewer implements m.ClassComponent {
89  private onResize: () => void = () => {};
90  private zoomContent?: PanAndZoomHandler;
91  // Used to prevent global deselection if a pan/drag select occurred.
92  private keepCurrentSelection = false;
93
94  oncreate(vnode: m.CVnodeDOM) {
95    const frontendLocalState = globals.frontendLocalState;
96    const updateDimensions = () => {
97      const rect = vnode.dom.getBoundingClientRect();
98      frontendLocalState.updateLocalLimits(
99          0,
100          rect.width - TRACK_SHELL_WIDTH -
101              frontendLocalState.getScrollbarWidth());
102    };
103
104    updateDimensions();
105
106    // TODO: Do resize handling better.
107    this.onResize = () => {
108      updateDimensions();
109      globals.rafScheduler.scheduleFullRedraw();
110    };
111
112    // Once ResizeObservers are out, we can stop accessing the window here.
113    window.addEventListener('resize', this.onResize);
114
115    const panZoomEl =
116        vnode.dom.querySelector('.pan-and-zoom-content') as HTMLElement;
117
118    this.zoomContent = new PanAndZoomHandler({
119      element: panZoomEl,
120      contentOffsetX: SIDEBAR_WIDTH,
121      onPanned: (pannedPx: number) => {
122        const {
123          visibleTimeScale,
124        } = globals.frontendLocalState;
125
126        this.keepCurrentSelection = true;
127        const tDelta = visibleTimeScale.pxDeltaToDuration(pannedPx);
128        frontendLocalState.panVisibleWindow(tDelta);
129
130        // If the user has panned they no longer need the hint.
131        localStorage.setItem(DISMISSED_PANNING_HINT_KEY, 'true');
132        globals.rafScheduler.scheduleRedraw();
133      },
134      onZoomed: (zoomedPositionPx: number, zoomRatio: number) => {
135        // TODO(hjd): Avoid hardcoding TRACK_SHELL_WIDTH.
136        // TODO(hjd): Improve support for zooming in overview timeline.
137        const zoomPx = zoomedPositionPx - TRACK_SHELL_WIDTH;
138        const rect = vnode.dom.getBoundingClientRect();
139        const centerPoint = zoomPx / (rect.width - TRACK_SHELL_WIDTH);
140        frontendLocalState.zoomVisibleWindow(1 - zoomRatio, centerPoint);
141        globals.rafScheduler.scheduleRedraw();
142      },
143      editSelection: (currentPx: number) => {
144        return onTimeRangeBoundary(currentPx) !== null;
145      },
146      onSelection: (
147          dragStartX: number,
148          dragStartY: number,
149          prevX: number,
150          currentX: number,
151          currentY: number,
152          editing: boolean) => {
153        const traceTime = globals.state.traceTime;
154        const {visibleTimeScale} = frontendLocalState;
155        this.keepCurrentSelection = true;
156        if (editing) {
157          const selection = globals.state.currentSelection;
158          if (selection !== null && selection.kind === 'AREA') {
159            const area = globals.frontendLocalState.selectedArea ?
160                globals.frontendLocalState.selectedArea :
161                globals.state.areas[selection.areaId];
162            let newTime =
163                visibleTimeScale.pxToHpTime(currentX - TRACK_SHELL_WIDTH)
164                    .toTPTime();
165            // Have to check again for when one boundary crosses over the other.
166            const curBoundary = onTimeRangeBoundary(prevX);
167            if (curBoundary == null) return;
168            const keepTime = curBoundary === 'START' ? area.end : area.start;
169            // Don't drag selection outside of current screen.
170            if (newTime < keepTime) {
171              newTime = BigintMath.max(
172                  newTime, visibleTimeScale.timeSpan.start.toTPTime());
173            } else {
174              newTime = BigintMath.max(
175                  newTime, visibleTimeScale.timeSpan.end.toTPTime());
176            }
177            // When editing the time range we always use the saved tracks,
178            // since these will not change.
179            frontendLocalState.selectArea(
180                BigintMath.max(
181                    BigintMath.min(keepTime, newTime), traceTime.start),
182                BigintMath.min(
183                    BigintMath.max(keepTime, newTime), traceTime.end),
184                globals.state.areas[selection.areaId].tracks);
185          }
186        } else {
187          let startPx = Math.min(dragStartX, currentX) - TRACK_SHELL_WIDTH;
188          let endPx = Math.max(dragStartX, currentX) - TRACK_SHELL_WIDTH;
189          if (startPx < 0 && endPx < 0) return;
190          startPx = Math.max(startPx, visibleTimeScale.pxSpan.start);
191          endPx = Math.min(endPx, visibleTimeScale.pxSpan.end);
192          frontendLocalState.selectArea(
193              visibleTimeScale.pxToHpTime(startPx).toTPTime('floor'),
194              visibleTimeScale.pxToHpTime(endPx).toTPTime('ceil'),
195          );
196          frontendLocalState.areaY.start = dragStartY;
197          frontendLocalState.areaY.end = currentY;
198        }
199        globals.rafScheduler.scheduleRedraw();
200      },
201      endSelection: (edit: boolean) => {
202        globals.frontendLocalState.areaY.start = undefined;
203        globals.frontendLocalState.areaY.end = undefined;
204        const area = globals.frontendLocalState.selectedArea;
205        // If we are editing we need to pass the current id through to ensure
206        // the marked area with that id is also updated.
207        if (edit) {
208          const selection = globals.state.currentSelection;
209          if (selection !== null && selection.kind === 'AREA' && area) {
210            globals.dispatch(
211                Actions.editArea({area, areaId: selection.areaId}));
212          }
213        } else if (area) {
214          globals.makeSelection(Actions.selectArea({area}));
215        }
216        // Now the selection has ended we stored the final selected area in the
217        // global state and can remove the in progress selection from the
218        // frontendLocalState.
219        globals.frontendLocalState.deselectArea();
220        // Full redraw to color track shell.
221        globals.rafScheduler.scheduleFullRedraw();
222      },
223    });
224  }
225
226  onremove() {
227    window.removeEventListener('resize', this.onResize);
228    if (this.zoomContent) this.zoomContent.shutdown();
229  }
230
231  view() {
232    const scrollingPanels: AnyAttrsVnode[] = globals.state.scrollingTracks.map(
233        (id) => m(TrackPanel, {key: id, id, selectable: true}));
234
235    for (const group of Object.values(globals.state.trackGroups)) {
236      const headerPanel = m(TrackGroupPanel, {
237        trackGroupId: group.id,
238        key: `trackgroup-${group.id}`,
239        selectable: true,
240      });
241
242      const childTracks: AnyAttrsVnode[] = [];
243      // The first track is the summary track, and is displayed as part of the
244      // group panel, we don't want to display it twice so we start from 1.
245      if (!group.collapsed) {
246        for (let i = 1; i < group.tracks.length; ++i) {
247          const id = group.tracks[i];
248          childTracks.push(m(TrackPanel, {
249            key: `track-${group.id}-${id}`,
250            id,
251            selectable: true,
252          }));
253        }
254      }
255      scrollingPanels.push(m(TrackGroup, {
256        header: headerPanel,
257        collapsed: group.collapsed,
258        childTracks,
259      } as TrackGroupAttrs));
260    }
261
262    const overviewPanel = [];
263    if (OVERVIEW_PANEL_FLAG.get()) {
264      overviewPanel.push(m(OverviewTimelinePanel, {key: 'overview'}));
265    }
266
267    return m(
268        '.page',
269        m('.split-panel',
270          m('.pan-and-zoom-content',
271            {
272              onclick: () => {
273                // TODO(stevegolton): Make it possible to click buttons and
274                // things on this element without deselecting the selected
275                // element!
276                // We don't want to deselect when panning/drag selecting.
277                if (this.keepCurrentSelection) {
278                  this.keepCurrentSelection = false;
279                  return;
280                }
281                globals.makeSelection(Actions.deselect({}));
282              },
283            },
284            m('.pinned-panel-container', m(PanelContainer, {
285                doesScroll: false,
286                panels: [
287                  ...overviewPanel,
288                  m(TimeAxisPanel, {key: 'timeaxis'}),
289                  m(TimeSelectionPanel, {key: 'timeselection'}),
290                  m(NotesPanel, {key: 'notes'}),
291                  m(TickmarkPanel, {key: 'searchTickmarks'}),
292                  ...globals.state.pinnedTracks.map(
293                      (id) => m(TrackPanel, {key: id, id, selectable: true})),
294                ],
295                kind: 'OVERVIEW',
296              })),
297            m('.scrolling-panel-container', m(PanelContainer, {
298                doesScroll: true,
299                panels: scrollingPanels,
300                kind: 'TRACKS',
301              })))),
302        m(DetailsPanel));
303  }
304}
305
306export const ViewerPage = createPage({
307  view() {
308    return m(TraceViewer);
309  },
310});
311