• 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 {assertExists} from '../base/logging';
19import {Actions} from '../common/actions';
20import {
21  getContainingTrackId,
22  TrackGroupState,
23  TrackState,
24} from '../common/state';
25
26import {globals} from './globals';
27import {drawGridLines} from './gridline_helper';
28import {
29  BLANK_CHECKBOX,
30  CHECKBOX,
31  EXPAND_DOWN,
32  EXPAND_UP,
33  INDETERMINATE_CHECKBOX,
34} from './icons';
35import {Panel, PanelSize} from './panel';
36import {Track} from './track';
37import {TrackContent} from './track_panel';
38import {trackRegistry} from './track_registry';
39import {
40  drawVerticalLineAtTime,
41} from './vertical_line_helper';
42
43interface Attrs {
44  trackGroupId: string;
45  selectable: boolean;
46}
47
48export class TrackGroupPanel extends Panel<Attrs> {
49  private readonly trackGroupId: string;
50  private shellWidth = 0;
51  private backgroundColor = '#ffffff';  // Updated from CSS later.
52  private summaryTrack: Track|undefined;
53
54  constructor({attrs}: m.CVnode<Attrs>) {
55    super();
56    this.trackGroupId = attrs.trackGroupId;
57    const trackCreator = trackRegistry.get(this.summaryTrackState.kind);
58    const engineId = this.summaryTrackState.engineId;
59    const engine = globals.engines.get(engineId);
60    if (engine !== undefined) {
61      this.summaryTrack =
62          trackCreator.create({trackId: this.summaryTrackState.id, engine});
63    }
64  }
65
66  get trackGroupState(): TrackGroupState {
67    return assertExists(globals.state.trackGroups[this.trackGroupId]);
68  }
69
70  get summaryTrackState(): TrackState {
71    return assertExists(globals.state.tracks[this.trackGroupState.tracks[0]]);
72  }
73
74  view({attrs}: m.CVnode<Attrs>) {
75    const collapsed = this.trackGroupState.collapsed;
76    let name = this.trackGroupState.name;
77    if (name[0] === '/') {
78      name = StripPathFromExecutable(name);
79    }
80
81    // The shell should be highlighted if the current search result is inside
82    // this track group.
83    let highlightClass = '';
84    const searchIndex = globals.state.searchIndex;
85    if (searchIndex !== -1) {
86      const trackId = globals.currentSearchResults.trackIds[searchIndex];
87      const parentTrackId = getContainingTrackId(globals.state, trackId);
88      if (parentTrackId === attrs.trackGroupId) {
89        highlightClass = 'flash';
90      }
91    }
92
93    const selection = globals.state.currentSelection;
94
95    const trackGroup = globals.state.trackGroups[attrs.trackGroupId];
96    let checkBox = BLANK_CHECKBOX;
97    if (selection !== null && selection.kind === 'AREA') {
98      const selectedArea = globals.state.areas[selection.areaId];
99      if (selectedArea.tracks.includes(attrs.trackGroupId) &&
100          trackGroup.tracks.every((id) => selectedArea.tracks.includes(id))) {
101        checkBox = CHECKBOX;
102      } else if (
103          selectedArea.tracks.includes(attrs.trackGroupId) ||
104          trackGroup.tracks.some((id) => selectedArea.tracks.includes(id))) {
105        checkBox = INDETERMINATE_CHECKBOX;
106      }
107    }
108
109    let child = null;
110    if (this.summaryTrackState.labels &&
111        this.summaryTrackState.labels.length > 0) {
112      child = this.summaryTrackState.labels.join(', ');
113    }
114
115    return m(
116        `.track-group-panel[collapsed=${collapsed}]`,
117        {id: 'track_' + this.trackGroupId},
118        m(`.shell`,
119          {
120            onclick: (e: MouseEvent) => {
121              globals.dispatch(Actions.toggleTrackGroupCollapsed({
122                trackGroupId: attrs.trackGroupId,
123              })),
124                  e.stopPropagation();
125            },
126            class: `${highlightClass}`,
127          },
128
129          m('.fold-button',
130            m('i.material-icons',
131              this.trackGroupState.collapsed ? EXPAND_DOWN : EXPAND_UP)),
132          m('.title-wrapper',
133            m('h1.track-title',
134              {title: name},
135              name,
136              ('namespace' in this.summaryTrackState.config) &&
137                  m('span.chip', 'metric')),
138            (this.trackGroupState.collapsed && child !== null) ?
139                m('h2.track-subtitle', child) :
140                null),
141          selection && selection.kind === 'AREA' ?
142              m('i.material-icons.track-button',
143                {
144                  onclick: (e: MouseEvent) => {
145                    globals.dispatch(Actions.toggleTrackSelection(
146                        {id: attrs.trackGroupId, isTrackGroup: true}));
147                    e.stopPropagation();
148                  },
149                },
150                checkBox) :
151              ''),
152
153        this.summaryTrack ?
154            m(TrackContent,
155              {track: this.summaryTrack},
156              (!this.trackGroupState.collapsed && child !== null) ?
157                  m('span', child) :
158                  null) :
159            null);
160  }
161
162  oncreate(vnode: m.CVnodeDOM<Attrs>) {
163    this.onupdate(vnode);
164  }
165
166  onupdate({dom}: m.CVnodeDOM<Attrs>) {
167    const shell = assertExists(dom.querySelector('.shell'));
168    this.shellWidth = shell.getBoundingClientRect().width;
169    // TODO(andrewbb): move this to css_constants
170    if (this.trackGroupState.collapsed) {
171      this.backgroundColor =
172          getComputedStyle(dom).getPropertyValue('--collapsed-background');
173    } else {
174      this.backgroundColor =
175          getComputedStyle(dom).getPropertyValue('--expanded-background');
176    }
177    if (this.summaryTrack !== undefined) {
178      this.summaryTrack.onFullRedraw();
179    }
180  }
181
182  onremove() {
183    if (this.summaryTrack !== undefined) {
184      this.summaryTrack.onDestroy();
185      this.summaryTrack = undefined;
186    }
187  }
188
189  highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
190    const {visibleTimeScale} = globals.frontendLocalState;
191    const selection = globals.state.currentSelection;
192    if (!selection || selection.kind !== 'AREA') return;
193    const selectedArea = globals.state.areas[selection.areaId];
194    const selectedAreaDuration = selectedArea.end - selectedArea.start;
195    if (selectedArea.tracks.includes(this.trackGroupId)) {
196      ctx.fillStyle = 'rgba(131, 152, 230, 0.3)';
197      ctx.fillRect(
198          visibleTimeScale.tpTimeToPx(selectedArea.start) + this.shellWidth,
199          0,
200          visibleTimeScale.durationToPx(selectedAreaDuration),
201          size.height);
202    }
203  }
204
205  renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
206    const collapsed = this.trackGroupState.collapsed;
207
208    ctx.fillStyle = this.backgroundColor;
209    ctx.fillRect(0, 0, size.width, size.height);
210
211    if (!collapsed) return;
212
213    this.highlightIfTrackSelected(ctx, size);
214
215    drawGridLines(
216        ctx,
217        size.width,
218        size.height);
219
220    ctx.save();
221    ctx.translate(this.shellWidth, 0);
222    if (this.summaryTrack) {
223      this.summaryTrack.render(ctx);
224    }
225    ctx.restore();
226
227    this.highlightIfTrackSelected(ctx, size);
228
229    const {visibleTimeScale} = globals.frontendLocalState;
230    // Draw vertical line when hovering on the notes panel.
231    if (globals.state.hoveredNoteTimestamp !== -1n) {
232      drawVerticalLineAtTime(
233          ctx,
234          visibleTimeScale,
235          globals.state.hoveredNoteTimestamp,
236          size.height,
237          `#aaa`);
238    }
239    if (globals.state.hoverCursorTimestamp !== -1n) {
240      drawVerticalLineAtTime(
241          ctx,
242          visibleTimeScale,
243          globals.state.hoverCursorTimestamp,
244          size.height,
245          `#344596`);
246    }
247
248    if (globals.state.currentSelection !== null) {
249      if (globals.state.currentSelection.kind === 'SLICE' &&
250          globals.sliceDetails.wakeupTs !== undefined) {
251        drawVerticalLineAtTime(
252            ctx,
253            visibleTimeScale,
254            globals.sliceDetails.wakeupTs,
255            size.height,
256            `black`);
257      }
258    }
259    // All marked areas should have semi-transparent vertical lines
260    // marking the start and end.
261    for (const note of Object.values(globals.state.notes)) {
262      if (note.noteType === 'AREA') {
263        const transparentNoteColor =
264            'rgba(' + hex.rgb(note.color.substr(1)).toString() + ', 0.65)';
265        drawVerticalLineAtTime(
266            ctx,
267            visibleTimeScale,
268            globals.state.areas[note.areaId].start,
269            size.height,
270            transparentNoteColor,
271            1);
272        drawVerticalLineAtTime(
273            ctx,
274            visibleTimeScale,
275            globals.state.areas[note.areaId].end,
276            size.height,
277            transparentNoteColor,
278            1);
279      } else if (note.noteType === 'DEFAULT') {
280        drawVerticalLineAtTime(
281            ctx, visibleTimeScale, note.timestamp, size.height, note.color);
282      }
283    }
284  }
285}
286
287function StripPathFromExecutable(path: string) {
288  return path.split('/').slice(-1)[0];
289}
290