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