xref: /aosp_15_r20/external/perfetto/ui/src/frontend/time_selection_panel.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2019 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use size 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 {time, Time} from '../base/time';
17import {timestampFormat} from '../core/timestamp_format';
18import {
19  BACKGROUND_COLOR,
20  FOREGROUND_COLOR,
21  TRACK_SHELL_WIDTH,
22} from './css_constants';
23import {getMaxMajorTicks, generateTicks, TickType} from './gridline_helper';
24import {Size2D} from '../base/geom';
25import {Panel} from './panel_container';
26import {canvasClip} from '../base/canvas_utils';
27import {TimeScale} from '../base/time_scale';
28import {TraceImpl} from '../core/trace_impl';
29import {formatDuration} from '../components/time_utils';
30import {TimestampFormat} from '../public/timeline';
31import {assertUnreachable} from '../base/logging';
32
33export interface BBox {
34  x: number;
35  y: number;
36  width: number;
37  height: number;
38}
39
40// Draws a vertical line with two horizontal tails at the left and right and
41// a label in the middle. It looks a bit like a stretched H:
42// |--- Label ---|
43// The |target| bounding box determines where to draw the H.
44// The |bounds| bounding box gives the visible region, this is used to adjust
45// the positioning of the label to ensure it is on screen.
46function drawHBar(
47  ctx: CanvasRenderingContext2D,
48  target: BBox,
49  bounds: BBox,
50  label: string,
51) {
52  ctx.fillStyle = FOREGROUND_COLOR;
53
54  const xLeft = Math.floor(target.x);
55  const xRight = Math.floor(target.x + target.width);
56  const yMid = Math.floor(target.height / 2 + target.y);
57  const xWidth = xRight - xLeft;
58
59  // Don't draw in the track shell.
60  ctx.beginPath();
61  ctx.rect(bounds.x, bounds.y, bounds.width, bounds.height);
62  ctx.clip();
63
64  // Draw horizontal bar of the H.
65  ctx.fillRect(xLeft, yMid, xWidth, 1);
66  // Draw left vertical bar of the H.
67  ctx.fillRect(xLeft, target.y, 1, target.height);
68  // Draw right vertical bar of the H.
69  ctx.fillRect(xRight, target.y, 1, target.height);
70
71  const labelWidth = ctx.measureText(label).width;
72
73  // Find a good position for the label:
74  // By default put the label in the middle of the H:
75  let labelXLeft = Math.floor(xWidth / 2 - labelWidth / 2 + xLeft);
76
77  if (
78    labelWidth > target.width ||
79    labelXLeft < bounds.x ||
80    labelXLeft + labelWidth > bounds.x + bounds.width
81  ) {
82    // It won't fit in the middle or would be at least partly out of bounds
83    // so put it either to the left or right:
84    if (xRight > bounds.x + bounds.width) {
85      // If the H extends off the right side of the screen the label
86      // goes on the left of the H.
87      labelXLeft = xLeft - labelWidth - 3;
88    } else {
89      // Otherwise the label goes on the right of the H.
90      labelXLeft = xRight + 3;
91    }
92  }
93
94  ctx.fillStyle = BACKGROUND_COLOR;
95  ctx.fillRect(labelXLeft - 1, 0, labelWidth + 1, target.height);
96
97  ctx.textBaseline = 'middle';
98  ctx.fillStyle = FOREGROUND_COLOR;
99  ctx.font = '10px Roboto Condensed';
100  ctx.fillText(label, labelXLeft, yMid);
101}
102
103function drawIBar(
104  ctx: CanvasRenderingContext2D,
105  xPos: number,
106  bounds: BBox,
107  label: string,
108) {
109  if (xPos < bounds.x) return;
110
111  ctx.fillStyle = FOREGROUND_COLOR;
112  ctx.fillRect(xPos, 0, 1, bounds.width);
113
114  const yMid = Math.floor(bounds.height / 2 + bounds.y);
115  const labelWidth = ctx.measureText(label).width;
116  const padding = 3;
117
118  let xPosLabel;
119  if (xPos + padding + labelWidth > bounds.width) {
120    xPosLabel = xPos - padding;
121    ctx.textAlign = 'right';
122  } else {
123    xPosLabel = xPos + padding;
124    ctx.textAlign = 'left';
125  }
126
127  ctx.fillStyle = BACKGROUND_COLOR;
128  ctx.fillRect(xPosLabel - 1, 0, labelWidth + 2, bounds.height);
129
130  ctx.textBaseline = 'middle';
131  ctx.fillStyle = FOREGROUND_COLOR;
132  ctx.font = '10px Roboto Condensed';
133  ctx.fillText(label, xPosLabel, yMid);
134}
135
136export class TimeSelectionPanel implements Panel {
137  readonly kind = 'panel';
138  readonly selectable = false;
139
140  constructor(private readonly trace: TraceImpl) {}
141
142  render(): m.Children {
143    return m('.time-selection-panel');
144  }
145
146  renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
147    ctx.fillStyle = '#999';
148    ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
149
150    const trackSize = {...size, width: size.width - TRACK_SHELL_WIDTH};
151
152    ctx.save();
153    ctx.translate(TRACK_SHELL_WIDTH, 0);
154    canvasClip(ctx, 0, 0, trackSize.width, trackSize.height);
155    this.renderPanel(ctx, trackSize);
156    ctx.restore();
157  }
158
159  private renderPanel(ctx: CanvasRenderingContext2D, size: Size2D): void {
160    const visibleWindow = this.trace.timeline.visibleWindow;
161    const timescale = new TimeScale(visibleWindow, {
162      left: 0,
163      right: size.width,
164    });
165    const timespan = visibleWindow.toTimeSpan();
166
167    if (size.width > 0 && timespan.duration > 0n) {
168      const maxMajorTicks = getMaxMajorTicks(size.width);
169      const offset = this.trace.timeline.timestampOffset();
170      const tickGen = generateTicks(timespan, maxMajorTicks, offset);
171      for (const {type, time} of tickGen) {
172        const px = Math.floor(timescale.timeToPx(time));
173        if (type === TickType.MAJOR) {
174          ctx.fillRect(px, 0, 1, size.height);
175        }
176      }
177    }
178
179    const localArea = this.trace.timeline.selectedArea;
180    const selection = this.trace.selection.selection;
181    if (localArea !== undefined) {
182      const start = Time.min(localArea.start, localArea.end);
183      const end = Time.max(localArea.start, localArea.end);
184      this.renderSpan(ctx, timescale, size, start, end);
185    } else {
186      if (selection.kind === 'area') {
187        const start = Time.min(selection.start, selection.end);
188        const end = Time.max(selection.start, selection.end);
189        this.renderSpan(ctx, timescale, size, start, end);
190      } else if (selection.kind === 'track_event') {
191        const start = selection.ts;
192        const end = Time.add(selection.ts, selection.dur);
193        if (end > start) {
194          this.renderSpan(ctx, timescale, size, start, end);
195        }
196      }
197    }
198
199    if (this.trace.timeline.hoverCursorTimestamp !== undefined) {
200      this.renderHover(
201        ctx,
202        timescale,
203        size,
204        this.trace.timeline.hoverCursorTimestamp,
205      );
206    }
207
208    for (const note of this.trace.notes.notes.values()) {
209      const noteIsSelected =
210        selection.kind === 'note' && selection.id === note.id;
211      if (note.noteType === 'SPAN' && noteIsSelected) {
212        this.renderSpan(ctx, timescale, size, note.start, note.end);
213      }
214    }
215
216    ctx.restore();
217  }
218
219  renderHover(
220    ctx: CanvasRenderingContext2D,
221    timescale: TimeScale,
222    size: Size2D,
223    ts: time,
224  ) {
225    const xPos = Math.floor(timescale.timeToPx(ts));
226    const domainTime = this.trace.timeline.toDomainTime(ts);
227    const label = stringifyTimestamp(domainTime);
228    drawIBar(ctx, xPos, this.getBBoxFromSize(size), label);
229  }
230
231  renderSpan(
232    ctx: CanvasRenderingContext2D,
233    timescale: TimeScale,
234    trackSize: Size2D,
235    start: time,
236    end: time,
237  ) {
238    const xLeft = timescale.timeToPx(start);
239    const xRight = timescale.timeToPx(end);
240    const label = formatDuration(this.trace, end - start);
241    drawHBar(
242      ctx,
243      {
244        x: xLeft,
245        y: 0,
246        width: xRight - xLeft,
247        height: trackSize.height,
248      },
249      this.getBBoxFromSize(trackSize),
250      label,
251    );
252  }
253
254  private getBBoxFromSize(size: Size2D): BBox {
255    return {
256      x: 0,
257      y: 0,
258      width: size.width,
259      height: size.height,
260    };
261  }
262}
263
264function stringifyTimestamp(time: time): string {
265  const fmt = timestampFormat();
266  switch (fmt) {
267    case TimestampFormat.UTC:
268    case TimestampFormat.TraceTz:
269    case TimestampFormat.Timecode:
270      const THIN_SPACE = '\u2009';
271      return Time.toTimecode(time).toString(THIN_SPACE);
272    case TimestampFormat.TraceNs:
273      return time.toString();
274    case TimestampFormat.TraceNsLocale:
275      return time.toLocaleString();
276    case TimestampFormat.Seconds:
277      return Time.formatSeconds(time);
278    case TimestampFormat.Milliseconds:
279      return Time.formatMilliseconds(time);
280    case TimestampFormat.Microseconds:
281      return Time.formatMicroseconds(time);
282    default:
283      assertUnreachable(fmt);
284  }
285}
286