• 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';
16
17import {findRef, toHTMLElement} from '../base/dom_utils';
18import {clamp} from '../base/math_utils';
19import {Time} from '../base/time';
20import {Actions} from '../common/actions';
21import {TrackCacheEntry} from '../common/track_cache';
22import {featureFlags} from '../core/feature_flags';
23import {raf} from '../core/raf_scheduler';
24import {TrackTags} from '../public';
25
26import {TRACK_SHELL_WIDTH} from './css_constants';
27import {globals} from './globals';
28import {NotesPanel} from './notes_panel';
29import {OverviewTimelinePanel} from './overview_timeline_panel';
30import {createPage} from './pages';
31import {PanAndZoomHandler} from './pan_and_zoom_handler';
32import {Panel, PanelContainer, PanelOrGroup} from './panel_container';
33import {publishShowPanningHint} from './publish';
34import {TabPanel} from './tab_panel';
35import {TickmarkPanel} from './tickmark_panel';
36import {TimeAxisPanel} from './time_axis_panel';
37import {TimeSelectionPanel} from './time_selection_panel';
38import {DISMISSED_PANNING_HINT_KEY} from './topbar';
39import {TrackGroupPanel} from './track_group_panel';
40import {TrackPanel} from './track_panel';
41import {assertExists} from '../base/logging';
42
43const OVERVIEW_PANEL_FLAG = featureFlags.register({
44  id: 'overviewVisible',
45  name: 'Overview Panel',
46  description: 'Show the panel providing an overview of the trace',
47  defaultValue: true,
48});
49
50// Checks if the mousePos is within 3px of the start or end of the
51// current selected time range.
52function onTimeRangeBoundary(mousePos: number): 'START' | 'END' | null {
53  const selection = globals.state.selection;
54  if (selection.kind === 'area') {
55    // If frontend selectedArea exists then we are in the process of editing the
56    // time range and need to use that value instead.
57    const area = globals.timeline.selectedArea
58      ? globals.timeline.selectedArea
59      : selection;
60    const {visibleTimeScale} = globals.timeline;
61    const start = visibleTimeScale.timeToPx(area.start);
62    const end = visibleTimeScale.timeToPx(area.end);
63    const startDrag = mousePos - TRACK_SHELL_WIDTH;
64    const startDistance = Math.abs(start - startDrag);
65    const endDistance = Math.abs(end - startDrag);
66    const range = 3 * window.devicePixelRatio;
67    // We might be within 3px of both boundaries but we should choose
68    // the closest one.
69    if (startDistance < range && startDistance <= endDistance) return 'START';
70    if (endDistance < range && endDistance <= startDistance) return 'END';
71  }
72  return null;
73}
74
75/**
76 * Top-most level component for the viewer page. Holds tracks, brush timeline,
77 * panels, and everything else that's part of the main trace viewer page.
78 */
79class TraceViewer implements m.ClassComponent {
80  private zoomContent?: PanAndZoomHandler;
81  // Used to prevent global deselection if a pan/drag select occurred.
82  private keepCurrentSelection = false;
83
84  private overviewTimelinePanel = new OverviewTimelinePanel();
85  private timeAxisPanel = new TimeAxisPanel();
86  private timeSelectionPanel = new TimeSelectionPanel();
87  private notesPanel = new NotesPanel();
88  private tickmarkPanel = new TickmarkPanel();
89
90  private readonly PAN_ZOOM_CONTENT_REF = 'pan-and-zoom-content';
91
92  oncreate(vnode: m.CVnodeDOM) {
93    const timeline = globals.timeline;
94    const panZoomElRaw = findRef(vnode.dom, this.PAN_ZOOM_CONTENT_REF);
95    const panZoomEl = toHTMLElement(assertExists(panZoomElRaw));
96
97    this.zoomContent = new PanAndZoomHandler({
98      element: panZoomEl,
99      onPanned: (pannedPx: number) => {
100        const {visibleTimeScale} = globals.timeline;
101
102        this.keepCurrentSelection = true;
103        const tDelta = visibleTimeScale.pxDeltaToDuration(pannedPx);
104        timeline.panVisibleWindow(tDelta);
105
106        // If the user has panned they no longer need the hint.
107        localStorage.setItem(DISMISSED_PANNING_HINT_KEY, 'true');
108        raf.scheduleRedraw();
109      },
110      onZoomed: (zoomedPositionPx: number, zoomRatio: number) => {
111        // TODO(hjd): Avoid hardcoding TRACK_SHELL_WIDTH.
112        // TODO(hjd): Improve support for zooming in overview timeline.
113        const zoomPx = zoomedPositionPx - TRACK_SHELL_WIDTH;
114        const rect = vnode.dom.getBoundingClientRect();
115        const centerPoint = zoomPx / (rect.width - TRACK_SHELL_WIDTH);
116        timeline.zoomVisibleWindow(1 - zoomRatio, centerPoint);
117        raf.scheduleRedraw();
118      },
119      editSelection: (currentPx: number) => {
120        return onTimeRangeBoundary(currentPx) !== null;
121      },
122      onSelection: (
123        dragStartX: number,
124        dragStartY: number,
125        prevX: number,
126        currentX: number,
127        currentY: number,
128        editing: boolean,
129      ) => {
130        const traceTime = globals.traceContext;
131        const {visibleTimeScale} = timeline;
132        this.keepCurrentSelection = true;
133        if (editing) {
134          const selection = globals.state.selection;
135          if (selection.kind === 'area') {
136            const area = globals.timeline.selectedArea
137              ? globals.timeline.selectedArea
138              : selection;
139            let newTime = visibleTimeScale
140              .pxToHpTime(currentX - TRACK_SHELL_WIDTH)
141              .toTime();
142            // Have to check again for when one boundary crosses over the other.
143            const curBoundary = onTimeRangeBoundary(prevX);
144            if (curBoundary == null) return;
145            const keepTime = curBoundary === 'START' ? area.end : area.start;
146            // Don't drag selection outside of current screen.
147            if (newTime < keepTime) {
148              newTime = Time.max(
149                newTime,
150                visibleTimeScale.timeSpan.start.toTime(),
151              );
152            } else {
153              newTime = Time.min(
154                newTime,
155                visibleTimeScale.timeSpan.end.toTime(),
156              );
157            }
158            // When editing the time range we always use the saved tracks,
159            // since these will not change.
160            timeline.selectArea(
161              Time.max(Time.min(keepTime, newTime), traceTime.start),
162              Time.min(Time.max(keepTime, newTime), traceTime.end),
163              selection.tracks,
164            );
165          }
166        } else {
167          let startPx = Math.min(dragStartX, currentX) - TRACK_SHELL_WIDTH;
168          let endPx = Math.max(dragStartX, currentX) - TRACK_SHELL_WIDTH;
169          if (startPx < 0 && endPx < 0) return;
170          const {pxSpan} = visibleTimeScale;
171          startPx = clamp(startPx, pxSpan.start, pxSpan.end);
172          endPx = clamp(endPx, pxSpan.start, pxSpan.end);
173          timeline.selectArea(
174            visibleTimeScale.pxToHpTime(startPx).toTime('floor'),
175            visibleTimeScale.pxToHpTime(endPx).toTime('ceil'),
176          );
177          timeline.areaY.start = dragStartY;
178          timeline.areaY.end = currentY;
179          publishShowPanningHint();
180        }
181        raf.scheduleRedraw();
182      },
183      endSelection: (edit: boolean) => {
184        globals.timeline.areaY.start = undefined;
185        globals.timeline.areaY.end = undefined;
186        const area = globals.timeline.selectedArea;
187        // If we are editing we need to pass the current id through to ensure
188        // the marked area with that id is also updated.
189        if (edit) {
190          const selection = globals.state.selection;
191          if (selection.kind === 'area' && area) {
192            globals.dispatch(Actions.selectArea({...area}));
193          }
194        } else if (area) {
195          globals.makeSelection(Actions.selectArea({...area}));
196        }
197        // Now the selection has ended we stored the final selected area in the
198        // global state and can remove the in progress selection from the
199        // timeline.
200        globals.timeline.deselectArea();
201        // Full redraw to color track shell.
202        raf.scheduleFullRedraw();
203      },
204    });
205  }
206
207  onremove() {
208    if (this.zoomContent) this.zoomContent.dispose();
209  }
210
211  view() {
212    const scrollingPanels: PanelOrGroup[] = globals.state.scrollingTracks.map(
213      (key) => {
214        const trackBundle = this.resolveTrack(key);
215        return new TrackPanel({
216          trackKey: key,
217          title: trackBundle.title,
218          tags: trackBundle.tags,
219          trackFSM: trackBundle.trackFSM,
220          closeable: trackBundle.closeable,
221        });
222      },
223    );
224
225    for (const group of Object.values(globals.state.trackGroups)) {
226      const key = group.summaryTrack;
227      let headerPanel;
228      if (key) {
229        const trackBundle = this.resolveTrack(key);
230        headerPanel = new TrackGroupPanel({
231          groupKey: group.key,
232          trackFSM: trackBundle.trackFSM,
233          labels: trackBundle.labels,
234          tags: trackBundle.tags,
235          collapsed: group.collapsed,
236          title: group.name,
237        });
238      } else {
239        headerPanel = new TrackGroupPanel({
240          groupKey: group.key,
241          collapsed: group.collapsed,
242          title: group.name,
243        });
244      }
245
246      const childTracks: Panel[] = [];
247      if (!group.collapsed) {
248        for (const key of group.tracks) {
249          const trackBundle = this.resolveTrack(key);
250          const panel = new TrackPanel({
251            trackKey: key,
252            title: trackBundle.title,
253            tags: trackBundle.tags,
254            trackFSM: trackBundle.trackFSM,
255            closeable: trackBundle.closeable,
256          });
257          childTracks.push(panel);
258        }
259      }
260
261      scrollingPanels.push({
262        kind: 'group',
263        collapsed: group.collapsed,
264        childPanels: childTracks,
265        header: headerPanel,
266      });
267    }
268
269    const overviewPanel = [];
270    if (OVERVIEW_PANEL_FLAG.get()) {
271      overviewPanel.push(this.overviewTimelinePanel);
272    }
273
274    const result = m(
275      '.page.viewer-page',
276      m(
277        '.pan-and-zoom-content',
278        {
279          ref: this.PAN_ZOOM_CONTENT_REF,
280          onclick: () => {
281            // We don't want to deselect when panning/drag selecting.
282            if (this.keepCurrentSelection) {
283              this.keepCurrentSelection = false;
284              return;
285            }
286            globals.clearSelection();
287          },
288        },
289        m(
290          '.pf-timeline-header',
291          m(PanelContainer, {
292            className: 'header-panel-container',
293            panels: [
294              ...overviewPanel,
295              this.timeAxisPanel,
296              this.timeSelectionPanel,
297              this.notesPanel,
298              this.tickmarkPanel,
299            ],
300          }),
301          m('.scrollbar-spacer-vertical'),
302        ),
303        m(PanelContainer, {
304          className: 'pinned-panel-container',
305          panels: globals.state.pinnedTracks.map((key) => {
306            const trackBundle = this.resolveTrack(key);
307            return new TrackPanel({
308              trackKey: key,
309              title: trackBundle.title,
310              tags: trackBundle.tags,
311              trackFSM: trackBundle.trackFSM,
312              revealOnCreate: true,
313              closeable: trackBundle.closeable,
314            });
315          }),
316        }),
317        m(PanelContainer, {
318          className: 'scrolling-panel-container',
319          panels: scrollingPanels,
320          onPanelStackResize: (width) => {
321            const timelineWidth = width - TRACK_SHELL_WIDTH;
322            globals.timeline.updateLocalLimits(0, timelineWidth);
323          },
324        }),
325      ),
326      this.renderTabPanel(),
327    );
328
329    globals.trackManager.flushOldTracks();
330    return result;
331  }
332
333  // Resolve a track and its metadata through the track cache
334  private resolveTrack(key: string): TrackBundle {
335    const trackState = globals.state.tracks[key];
336    const {uri, name, labels, closeable} = trackState;
337    const trackDesc = globals.trackManager.resolveTrackInfo(uri);
338    const trackCacheEntry =
339      trackDesc && globals.trackManager.resolveTrack(key, trackDesc);
340    const trackFSM = trackCacheEntry;
341    const tags = trackCacheEntry?.desc.tags;
342    const trackIds = trackCacheEntry?.desc.trackIds;
343    return {
344      title: name,
345      tags,
346      trackFSM,
347      labels,
348      trackIds,
349      closeable: closeable ?? false,
350    };
351  }
352
353  private renderTabPanel() {
354    return m(TabPanel);
355  }
356}
357
358interface TrackBundle {
359  title: string;
360  closeable: boolean;
361  trackFSM?: TrackCacheEntry;
362  tags?: TrackTags;
363  labels?: string[];
364  trackIds?: number[];
365}
366
367export const ViewerPage = createPage({
368  view() {
369    return m(TraceViewer);
370  },
371});
372