• 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 {hex} from 'color-convert';
16import * as m from 'mithril';
17
18import {Actions} from '../common/actions';
19import {TrackState} from '../common/state';
20
21import {TRACK_SHELL_WIDTH} from './css_constants';
22import {PerfettoMouseEvent} from './events';
23import {globals} from './globals';
24import {drawGridLines} from './gridline_helper';
25import {BLANK_CHECKBOX, CHECKBOX, STAR, STAR_BORDER} from './icons';
26import {Panel, PanelSize} from './panel';
27import {verticalScrollToTrack} from './scroll_helper';
28import {SliceRect, Track} from './track';
29import {trackRegistry} from './track_registry';
30import {
31  drawVerticalLineAtTime,
32} from './vertical_line_helper';
33
34function isPinned(id: string) {
35  return globals.state.pinnedTracks.indexOf(id) !== -1;
36}
37
38function isSelected(id: string) {
39  const selection = globals.state.currentSelection;
40  if (selection === null || selection.kind !== 'AREA') return false;
41  const selectedArea = globals.state.areas[selection.areaId];
42  return selectedArea.tracks.includes(id);
43}
44
45interface TrackShellAttrs {
46  track: Track;
47  trackState: TrackState;
48}
49
50class TrackShell implements m.ClassComponent<TrackShellAttrs> {
51  // Set to true when we click down and drag the
52  private dragging = false;
53  private dropping: 'before'|'after'|undefined = undefined;
54  private attrs?: TrackShellAttrs;
55
56  oninit(vnode: m.Vnode<TrackShellAttrs>) {
57    this.attrs = vnode.attrs;
58  }
59
60  view({attrs}: m.CVnode<TrackShellAttrs>) {
61    // The shell should be highlighted if the current search result is inside
62    // this track.
63    let highlightClass = '';
64    const searchIndex = globals.state.searchIndex;
65    if (searchIndex !== -1) {
66      const trackId = globals.currentSearchResults.trackIds[searchIndex];
67      if (trackId === attrs.trackState.id) {
68        highlightClass = 'flash';
69      }
70    }
71
72    const dragClass = this.dragging ? `drag` : '';
73    const dropClass = this.dropping ? `drop-${this.dropping}` : '';
74    return m(
75        `.track-shell[draggable=true]`,
76        {
77          class: `${highlightClass} ${dragClass} ${dropClass}`,
78          onmousedown: this.onmousedown.bind(this),
79          ondragstart: this.ondragstart.bind(this),
80          ondragend: this.ondragend.bind(this),
81          ondragover: this.ondragover.bind(this),
82          ondragleave: this.ondragleave.bind(this),
83          ondrop: this.ondrop.bind(this),
84        },
85        m(
86            'h1',
87            {
88              title: attrs.trackState.name,
89            },
90            attrs.trackState.name,
91            ('namespace' in attrs.trackState.config) &&
92                m('span.chip', 'metric'),
93            ),
94        m('.track-buttons',
95          attrs.track.getTrackShellButtons(),
96          m(TrackButton, {
97            action: () => {
98              globals.dispatch(
99                  Actions.toggleTrackPinned({trackId: attrs.trackState.id}));
100            },
101            i: isPinned(attrs.trackState.id) ? STAR : STAR_BORDER,
102            tooltip: isPinned(attrs.trackState.id) ? 'Unpin' : 'Pin to top',
103            showButton: isPinned(attrs.trackState.id),
104          }),
105          globals.state.currentSelection !== null &&
106                  globals.state.currentSelection.kind === 'AREA' ?
107              m(TrackButton, {
108                action: (e: PerfettoMouseEvent) => {
109                  globals.dispatch(Actions.toggleTrackSelection(
110                      {id: attrs.trackState.id, isTrackGroup: false}));
111                  e.stopPropagation();
112                },
113                i: isSelected(attrs.trackState.id) ? CHECKBOX : BLANK_CHECKBOX,
114                tooltip: isSelected(attrs.trackState.id) ?
115                    'Remove track' :
116                    'Add track to selection',
117                showButton: true,
118              }) :
119              ''));
120  }
121
122  onmousedown(e: MouseEvent) {
123    // Prevent that the click is intercepted by the PanAndZoomHandler and that
124    // we start panning while dragging.
125    e.stopPropagation();
126  }
127
128  ondragstart(e: DragEvent) {
129    const dataTransfer = e.dataTransfer;
130    if (dataTransfer === null) return;
131    this.dragging = true;
132    globals.rafScheduler.scheduleFullRedraw();
133    dataTransfer.setData('perfetto/track', `${this.attrs!.trackState.id}`);
134    dataTransfer.setDragImage(new Image(), 0, 0);
135    e.stopImmediatePropagation();
136  }
137
138  ondragend() {
139    this.dragging = false;
140    globals.rafScheduler.scheduleFullRedraw();
141  }
142
143  ondragover(e: DragEvent) {
144    if (this.dragging) return;
145    if (!(e.target instanceof HTMLElement)) return;
146    const dataTransfer = e.dataTransfer;
147    if (dataTransfer === null) return;
148    if (!dataTransfer.types.includes('perfetto/track')) return;
149    dataTransfer.dropEffect = 'move';
150    e.preventDefault();
151
152    // Apply some hysteresis to the drop logic so that the lightened border
153    // changes only when we get close enough to the border.
154    if (e.offsetY < e.target.scrollHeight / 3) {
155      this.dropping = 'before';
156    } else if (e.offsetY > e.target.scrollHeight / 3 * 2) {
157      this.dropping = 'after';
158    }
159    globals.rafScheduler.scheduleFullRedraw();
160  }
161
162  ondragleave() {
163    this.dropping = undefined;
164    globals.rafScheduler.scheduleFullRedraw();
165  }
166
167  ondrop(e: DragEvent) {
168    if (this.dropping === undefined) return;
169    const dataTransfer = e.dataTransfer;
170    if (dataTransfer === null) return;
171    globals.rafScheduler.scheduleFullRedraw();
172    const srcId = dataTransfer.getData('perfetto/track');
173    const dstId = this.attrs!.trackState.id;
174    globals.dispatch(Actions.moveTrack({srcId, op: this.dropping, dstId}));
175    this.dropping = undefined;
176  }
177}
178
179export interface TrackContentAttrs { track: Track; }
180export class TrackContent implements m.ClassComponent<TrackContentAttrs> {
181  private mouseDownX?: number;
182  private mouseDownY?: number;
183  private selectionOccurred = false;
184
185  view(node: m.CVnode<TrackContentAttrs>) {
186    const attrs = node.attrs;
187    return m(
188        '.track-content',
189        {
190          onmousemove: (e: PerfettoMouseEvent) => {
191            attrs.track.onMouseMove(
192                {x: e.layerX - TRACK_SHELL_WIDTH, y: e.layerY});
193            globals.rafScheduler.scheduleRedraw();
194          },
195          onmouseout: () => {
196            attrs.track.onMouseOut();
197            globals.rafScheduler.scheduleRedraw();
198          },
199          onmousedown: (e: PerfettoMouseEvent) => {
200            this.mouseDownX = e.layerX;
201            this.mouseDownY = e.layerY;
202          },
203          onmouseup: (e: PerfettoMouseEvent) => {
204            if (this.mouseDownX === undefined ||
205                this.mouseDownY === undefined) {
206              return;
207            }
208            if (Math.abs(e.layerX - this.mouseDownX) > 1 ||
209                Math.abs(e.layerY - this.mouseDownY) > 1) {
210              this.selectionOccurred = true;
211            }
212            this.mouseDownX = undefined;
213            this.mouseDownY = undefined;
214          },
215          onclick: (e: PerfettoMouseEvent) => {
216            // This click event occurs after any selection mouse up/drag events
217            // so we have to look if the mouse moved during this click to know
218            // if a selection occurred.
219            if (this.selectionOccurred) {
220              this.selectionOccurred = false;
221              return;
222            }
223            // Returns true if something was selected, so stop propagation.
224            if (attrs.track.onMouseClick(
225                    {x: e.layerX - TRACK_SHELL_WIDTH, y: e.layerY})) {
226              e.stopPropagation();
227            }
228            globals.rafScheduler.scheduleRedraw();
229          }
230        },
231        node.children);
232  }
233}
234
235interface TrackComponentAttrs {
236  trackState: TrackState;
237  track: Track;
238}
239class TrackComponent implements m.ClassComponent<TrackComponentAttrs> {
240  view({attrs}: m.CVnode<TrackComponentAttrs>) {
241    return m(
242        '.track',
243        {
244          style: {
245            height: `${Math.max(24, attrs.track.getHeight())}px`,
246          },
247          id: 'track_' + attrs.trackState.id,
248        },
249        [
250          m(TrackShell, {track: attrs.track, trackState: attrs.trackState}),
251          m(TrackContent, {track: attrs.track})
252        ]);
253  }
254
255  oncreate({attrs}: m.CVnode<TrackComponentAttrs>) {
256    if (globals.frontendLocalState.scrollToTrackId === attrs.trackState.id) {
257      verticalScrollToTrack(attrs.trackState.id);
258      globals.frontendLocalState.scrollToTrackId = undefined;
259    }
260  }
261}
262
263export interface TrackButtonAttrs {
264  action: (e: PerfettoMouseEvent) => void;
265  i: string;
266  tooltip: string;
267  showButton: boolean;
268}
269export class TrackButton implements m.ClassComponent<TrackButtonAttrs> {
270  view({attrs}: m.CVnode<TrackButtonAttrs>) {
271    return m(
272        'i.material-icons.track-button',
273        {
274          class: `${attrs.showButton ? 'show' : ''}`,
275          onclick: attrs.action,
276          title: attrs.tooltip,
277        },
278        attrs.i);
279  }
280}
281
282interface TrackPanelAttrs {
283  id: string;
284  selectable: boolean;
285}
286
287export class TrackPanel extends Panel<TrackPanelAttrs> {
288  // TODO(hjd): It would be nicer if these could not be undefined here.
289  // We should implement a NullTrack which can be used if the trackState
290  // has disappeared.
291  private track: Track|undefined;
292  private trackState: TrackState|undefined;
293
294  constructor(vnode: m.CVnode<TrackPanelAttrs>) {
295    super();
296    const trackId = vnode.attrs.id;
297    const trackState = globals.state.tracks[trackId];
298    if (trackState === undefined) {
299      return;
300    }
301    const engine = globals.engines.get(trackState.engineId);
302    if (engine === undefined) {
303      return;
304    }
305    const trackCreator = trackRegistry.get(trackState.kind);
306    this.track = trackCreator.create({trackId, engine});
307    this.trackState = trackState;
308  }
309
310  view() {
311    if (this.track === undefined || this.trackState === undefined) {
312      return m('div', 'No such track');
313    }
314    return m(TrackComponent, {trackState: this.trackState, track: this.track});
315  }
316
317  oncreate() {
318    if (this.track !== undefined) {
319      this.track.onFullRedraw();
320    }
321  }
322
323  onupdate() {
324    if (this.track !== undefined) {
325      this.track.onFullRedraw();
326    }
327  }
328
329  onremove() {
330    if (this.track !== undefined) {
331      this.track.onDestroy();
332      this.track = undefined;
333    }
334  }
335
336  highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
337    const localState = globals.frontendLocalState;
338    const selection = globals.state.currentSelection;
339    const trackState = this.trackState;
340    if (!selection || selection.kind !== 'AREA' || trackState === undefined) {
341      return;
342    }
343    const selectedArea = globals.state.areas[selection.areaId];
344    if (selectedArea.tracks.includes(trackState.id)) {
345      const timeScale = localState.timeScale;
346      ctx.fillStyle = 'rgba(131, 152, 230, 0.3)';
347      ctx.fillRect(
348          timeScale.timeToPx(selectedArea.startSec) + TRACK_SHELL_WIDTH,
349          0,
350          timeScale.deltaTimeToPx(selectedArea.endSec - selectedArea.startSec),
351          size.height);
352    }
353  }
354
355  renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
356    ctx.save();
357
358    drawGridLines(
359        ctx,
360        globals.frontendLocalState.timeScale,
361        globals.frontendLocalState.visibleWindowTime,
362        size.width,
363        size.height);
364
365    ctx.translate(TRACK_SHELL_WIDTH, 0);
366    if (this.track !== undefined) {
367      this.track.render(ctx);
368    }
369    ctx.restore();
370
371    this.highlightIfTrackSelected(ctx, size);
372
373    const localState = globals.frontendLocalState;
374    // Draw vertical line when hovering on the notes panel.
375    if (globals.state.hoveredNoteTimestamp !== -1) {
376      drawVerticalLineAtTime(
377          ctx,
378          localState.timeScale,
379          globals.state.hoveredNoteTimestamp,
380          size.height,
381          `#aaa`);
382    }
383    if (globals.state.hoveredLogsTimestamp !== -1) {
384      drawVerticalLineAtTime(
385          ctx,
386          localState.timeScale,
387          globals.state.hoveredLogsTimestamp,
388          size.height,
389          `#344596`);
390    }
391    if (globals.state.currentSelection !== null) {
392      if (globals.state.currentSelection.kind === 'NOTE') {
393        const note = globals.state.notes[globals.state.currentSelection.id];
394        if (note.noteType === 'DEFAULT') {
395          drawVerticalLineAtTime(
396              ctx,
397              localState.timeScale,
398              note.timestamp,
399              size.height,
400              note.color);
401        }
402      }
403
404      if (globals.state.currentSelection.kind === 'SLICE' &&
405          globals.sliceDetails.wakeupTs !== undefined) {
406        drawVerticalLineAtTime(
407            ctx,
408            localState.timeScale,
409            globals.sliceDetails.wakeupTs,
410            size.height,
411            `black`);
412      }
413    }
414    // All marked areas should have semi-transparent vertical lines
415    // marking the start and end.
416    for (const note of Object.values(globals.state.notes)) {
417      if (note.noteType === 'AREA') {
418        const transparentNoteColor =
419            'rgba(' + hex.rgb(note.color.substr(1)).toString() + ', 0.65)';
420        drawVerticalLineAtTime(
421            ctx,
422            localState.timeScale,
423            globals.state.areas[note.areaId].startSec,
424            size.height,
425            transparentNoteColor,
426            1);
427        drawVerticalLineAtTime(
428            ctx,
429            localState.timeScale,
430            globals.state.areas[note.areaId].endSec,
431            size.height,
432            transparentNoteColor,
433            1);
434      }
435    }
436  }
437
438  getSliceRect(tStart: number, tDur: number, depth: number): SliceRect
439      |undefined {
440    if (this.track === undefined) {
441      return undefined;
442    }
443    return this.track.getSliceRect(tStart, tDur, depth);
444  }
445}
446