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