blob: dd8756de56cf0784c82e8caa388c163afa3cbccd [file] [log] [blame]
Steve Golton832f1c22023-10-17 14:21:09 +01001// Copyright (C) 2023 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 {duration, Span, Time, time} from '../base/time';
16import {Actions} from '../common/actions';
Steve Golton832f1c22023-10-17 14:21:09 +010017import {cropText, drawIncompleteSlice} from '../common/canvas_utils';
Steve Goltonbc1d1142023-11-27 11:57:58 +000018import {getColorForSlice} from '../common/colorizer';
Steve Golton832f1c22023-10-17 14:21:09 +010019import {HighPrecisionTime} from '../common/high_precision_time';
20import {TrackData} from '../common/track_data';
Steve Golton4c924692023-12-04 10:16:00 +000021import {TrackHelperLEGACY} from '../common/track_helper';
Steve Goltonad72b582023-11-23 15:48:55 +000022import {SliceRect} from '../public';
Steve Golton832f1c22023-10-17 14:21:09 +010023
Steve Golton4c924692023-12-04 10:16:00 +000024import {CROP_INCOMPLETE_SLICE_FLAG} from './base_slice_track';
Steve Golton832f1c22023-10-17 14:21:09 +010025import {checkerboardExcept} from './checkerboard';
26import {globals} from './globals';
Steve Golton832f1c22023-10-17 14:21:09 +010027import {PxSpan, TimeScale} from './time_scale';
Steve Golton832f1c22023-10-17 14:21:09 +010028
29export const SLICE_TRACK_KIND = 'ChromeSliceTrack';
30const SLICE_HEIGHT = 18;
31const TRACK_PADDING = 2;
32const CHEVRON_WIDTH_PX = 10;
33const HALF_CHEVRON_WIDTH_PX = CHEVRON_WIDTH_PX / 2;
Liangliang Sui0d289e62023-11-28 20:54:40 +080034const INCOMPLETE_SLICE_WIDTH_PX = 20;
Steve Golton832f1c22023-10-17 14:21:09 +010035
36export interface SliceData extends TrackData {
37 // Slices are stored in a columnar fashion.
38 strings: string[];
39 sliceIds: Float64Array;
40 starts: BigInt64Array;
41 ends: BigInt64Array;
42 depths: Uint16Array;
43 titles: Uint16Array; // Index into strings.
44 colors?: Uint16Array; // Index into strings.
45 isInstant: Uint16Array;
46 isIncomplete: Uint16Array;
47 cpuTimeRatio?: Float64Array;
48}
49
50// Track base class which handles rendering slices in a generic way.
51// This is the old way of rendering slices - i.e. "track v1" format - and
52// exists as a patch to allow old tracks to be converted to controller-less
53// tracks before they are ported to v2.
54// Slice tracks should extend this class and implement the abstract methods,
55// notably onBoundsChange().
Steve Golton4c924692023-12-04 10:16:00 +000056// Note: This class is deprecated and should not be used for new tracks. Use
57// |BaseSliceTrack| instead.
58export abstract class SliceTrackLEGACY extends TrackHelperLEGACY<SliceData> {
Steve Golton832f1c22023-10-17 14:21:09 +010059 constructor(
Steve Goltoncfe3e3d2023-10-26 16:24:31 +010060 private maxDepth: number, protected trackKey: string,
Steve Golton832f1c22023-10-17 14:21:09 +010061 private tableName: string, private namespace?: string) {
62 super();
63 }
64
65 protected namespaceTable(tableName: string = this.tableName): string {
66 if (this.namespace) {
67 return this.namespace + '_' + tableName;
68 } else {
69 return tableName;
70 }
71 }
72
73 private hoveredTitleId = -1;
74
75 // Font used to render the slice name on the current track.
76 protected getFont() {
77 return '12px Roboto Condensed';
78 }
79
80 renderCanvas(ctx: CanvasRenderingContext2D): void {
81 // TODO: fonts and colors should come from the CSS and not hardcoded here.
82 const data = this.data;
83 if (data === undefined) return; // Can't possibly draw anything.
84
85 const {visibleTimeSpan, visibleWindowTime, visibleTimeScale, windowSpan} =
86 globals.frontendLocalState;
87
88 // If the cached trace slices don't fully cover the visible time range,
89 // show a gray rectangle with a "Loading..." label.
90 checkerboardExcept(
91 ctx,
92 this.getHeight(),
93 visibleTimeScale.hpTimeToPx(visibleWindowTime.start),
94 visibleTimeScale.hpTimeToPx(visibleWindowTime.end),
95 visibleTimeScale.timeToPx(data.start),
96 visibleTimeScale.timeToPx(data.end),
97 );
98
99 ctx.textAlign = 'center';
100
101 // measuretext is expensive so we only use it once.
102 const charWidth = ctx.measureText('ACBDLqsdfg').width / 10;
103
104 // The draw of the rect on the selected slice must happen after the other
105 // drawings, otherwise it would result under another rect.
106 let drawRectOnSelected = () => {};
107
108
109 for (let i = 0; i < data.starts.length; i++) {
110 const tStart = Time.fromRaw(data.starts[i]);
111 let tEnd = Time.fromRaw(data.ends[i]);
112 const depth = data.depths[i];
113 const titleId = data.titles[i];
114 const sliceId = data.sliceIds[i];
115 const isInstant = data.isInstant[i];
116 const isIncomplete = data.isIncomplete[i];
117 const title = data.strings[titleId];
118 const colorOverride = data.colors && data.strings[data.colors[i]];
119 if (isIncomplete) { // incomplete slice
120 // TODO(stevegolton): This isn't exactly equivalent, ideally we should
121 // choose tEnd once we've converted to screen space coords.
Liangliang Sui0d289e62023-11-28 20:54:40 +0800122 tEnd = this.getEndTimeIfInComplete(tStart);
Steve Golton832f1c22023-10-17 14:21:09 +0100123 }
124
125 if (!visibleTimeSpan.intersects(tStart, tEnd)) {
126 continue;
127 }
128
129 const rect = this.getSliceRect(
130 visibleTimeScale, visibleTimeSpan, windowSpan, tStart, tEnd, depth);
131 if (!rect || !rect.visible) {
132 continue;
133 }
134
135 const currentSelection = globals.state.currentSelection;
136 const isSelected = currentSelection &&
137 currentSelection.kind === 'CHROME_SLICE' &&
138 currentSelection.id !== undefined && currentSelection.id === sliceId;
139
140 const highlighted = titleId === this.hoveredTitleId ||
141 globals.state.highlightedSliceId === sliceId;
142
143 const hasFocus = highlighted || isSelected;
Steve Goltonbc1d1142023-11-27 11:57:58 +0000144 const colorScheme = getColorForSlice(title);
145 const colorObj = hasFocus ? colorScheme.variant : colorScheme.base;
146 const textColor =
147 hasFocus ? colorScheme.textVariant : colorScheme.textBase;
Steve Golton832f1c22023-10-17 14:21:09 +0100148
149 let color: string;
150 if (colorOverride === undefined) {
Steve Goltonbc1d1142023-11-27 11:57:58 +0000151 color = colorObj.cssString;
Steve Golton832f1c22023-10-17 14:21:09 +0100152 } else {
153 color = colorOverride;
154 }
155 ctx.fillStyle = color;
156
157 // We draw instant events as upward facing chevrons starting at A:
158 // A
159 // ###
160 // ##C##
161 // ## ##
162 // D B
163 // Then B, C, D and back to A:
164 if (isInstant) {
165 if (isSelected) {
166 drawRectOnSelected = () => {
167 ctx.save();
168 ctx.translate(rect.left, rect.top);
169
170 // Draw a rectangle around the selected slice
Steve Goltonbc1d1142023-11-27 11:57:58 +0000171 ctx.strokeStyle = colorObj.setHSL({s: 100, l: 10}).cssString;
Steve Golton832f1c22023-10-17 14:21:09 +0100172 ctx.beginPath();
173 ctx.lineWidth = 3;
174 ctx.strokeRect(
175 -HALF_CHEVRON_WIDTH_PX, 0, CHEVRON_WIDTH_PX, SLICE_HEIGHT);
176 ctx.closePath();
177
178 // Draw inner chevron as interior
179 ctx.fillStyle = color;
180 this.drawChevron(ctx);
181
182 ctx.restore();
183 };
184 } else {
185 ctx.save();
186 ctx.translate(rect.left, rect.top);
187 this.drawChevron(ctx);
188 ctx.restore();
189 }
190 continue;
191 }
192
193 if (isIncomplete && rect.width > SLICE_HEIGHT / 4) {
Steve Golton4c924692023-12-04 10:16:00 +0000194 drawIncompleteSlice(
195 ctx,
196 rect.left,
197 rect.top,
198 rect.width,
199 SLICE_HEIGHT,
200 !CROP_INCOMPLETE_SLICE_FLAG.get());
Steve Golton832f1c22023-10-17 14:21:09 +0100201 } else if (
202 data.cpuTimeRatio !== undefined && data.cpuTimeRatio[i] < 1 - 1e-9) {
203 // We draw two rectangles, representing the ratio between wall time and
204 // time spent on cpu.
205 const cpuTimeRatio = data.cpuTimeRatio![i];
206 const firstPartWidth = rect.width * cpuTimeRatio;
207 const secondPartWidth = rect.width * (1 - cpuTimeRatio);
Steve Goltonbc1d1142023-11-27 11:57:58 +0000208 ctx.fillRect(rect.left, rect.top, rect.width, SLICE_HEIGHT);
209 ctx.fillStyle = '#FFFFFF50';
Steve Golton832f1c22023-10-17 14:21:09 +0100210 ctx.fillRect(
211 rect.left + firstPartWidth,
212 rect.top,
213 secondPartWidth,
214 SLICE_HEIGHT);
215 } else {
216 ctx.fillRect(rect.left, rect.top, rect.width, SLICE_HEIGHT);
217 }
218
219 // Selected case
220 if (isSelected) {
221 drawRectOnSelected = () => {
Steve Goltonbc1d1142023-11-27 11:57:58 +0000222 ctx.strokeStyle = colorObj.setHSL({s: 100, l: 10}).cssString;
Steve Golton832f1c22023-10-17 14:21:09 +0100223 ctx.beginPath();
224 ctx.lineWidth = 3;
225 ctx.strokeRect(
226 rect.left, rect.top - 1.5, rect.width, SLICE_HEIGHT + 3);
227 ctx.closePath();
228 };
229 }
230
231 // Don't render text when we have less than 5px to play with.
232 if (rect.width >= 5) {
Steve Goltonbc1d1142023-11-27 11:57:58 +0000233 ctx.fillStyle = textColor.cssString;
Steve Golton832f1c22023-10-17 14:21:09 +0100234 const displayText = cropText(title, charWidth, rect.width);
235 const rectXCenter = rect.left + rect.width / 2;
236 ctx.textBaseline = 'middle';
237 ctx.font = this.getFont();
238 ctx.fillText(displayText, rectXCenter, rect.top + SLICE_HEIGHT / 2);
239 }
240 }
241 drawRectOnSelected();
242 }
243
244 drawChevron(ctx: CanvasRenderingContext2D) {
245 // Draw a chevron at a fixed location and size. Should be used with
246 // ctx.translate and ctx.scale to alter location and size.
247 ctx.beginPath();
248 ctx.moveTo(0, 0);
249 ctx.lineTo(HALF_CHEVRON_WIDTH_PX, SLICE_HEIGHT);
250 ctx.lineTo(0, SLICE_HEIGHT - HALF_CHEVRON_WIDTH_PX);
251 ctx.lineTo(-HALF_CHEVRON_WIDTH_PX, SLICE_HEIGHT);
252 ctx.lineTo(0, 0);
253 ctx.fill();
254 }
255
256 getSliceIndex({x, y}: {x: number, y: number}): number|void {
257 const data = this.data;
258 if (data === undefined) return;
259 const {
260 visibleTimeScale: timeScale,
Steve Golton832f1c22023-10-17 14:21:09 +0100261 } = globals.frontendLocalState;
262 if (y < TRACK_PADDING) return;
263 const instantWidthTime = timeScale.pxDeltaToDuration(HALF_CHEVRON_WIDTH_PX);
264 const t = timeScale.pxToHpTime(x);
265 const depth = Math.floor((y - TRACK_PADDING) / SLICE_HEIGHT);
266
267 for (let i = 0; i < data.starts.length; i++) {
268 if (depth !== data.depths[i]) {
269 continue;
270 }
271 const start = Time.fromRaw(data.starts[i]);
272 const tStart = HighPrecisionTime.fromTime(start);
273 if (data.isInstant[i]) {
274 if (tStart.sub(t).abs().lt(instantWidthTime)) {
275 return i;
276 }
277 } else {
278 const end = Time.fromRaw(data.ends[i]);
279 let tEnd = HighPrecisionTime.fromTime(end);
280 if (data.isIncomplete[i]) {
Liangliang Sui0d289e62023-11-28 20:54:40 +0800281 const endTime = this.getEndTimeIfInComplete(start);
282 tEnd = HighPrecisionTime.fromTime(endTime);
Steve Golton832f1c22023-10-17 14:21:09 +0100283 }
284 if (tStart.lte(t) && t.lte(tEnd)) {
285 return i;
286 }
287 }
288 }
289 }
290
Steve Golton4c924692023-12-04 10:16:00 +0000291 getEndTimeIfInComplete(start: time): time {
Liangliang Sui0d289e62023-11-28 20:54:40 +0800292 const {visibleTimeScale, visibleWindowTime} = globals.frontendLocalState;
293
294 let end = visibleWindowTime.end.toTime('ceil');
295 if (CROP_INCOMPLETE_SLICE_FLAG.get()) {
Steve Golton4c924692023-12-04 10:16:00 +0000296 const widthTime =
297 visibleTimeScale.pxDeltaToDuration(INCOMPLETE_SLICE_WIDTH_PX)
298 .toTime();
Liangliang Sui0d289e62023-11-28 20:54:40 +0800299 end = Time.add(start, widthTime);
300 }
301
302 return end;
303 }
304
Steve Golton832f1c22023-10-17 14:21:09 +0100305 onMouseMove({x, y}: {x: number, y: number}) {
306 this.hoveredTitleId = -1;
307 globals.dispatch(Actions.setHighlightedSliceId({sliceId: -1}));
308 const sliceIndex = this.getSliceIndex({x, y});
309 if (sliceIndex === undefined) return;
310 const data = this.data;
311 if (data === undefined) return;
312 this.hoveredTitleId = data.titles[sliceIndex];
313 const sliceId = data.sliceIds[sliceIndex];
314 globals.dispatch(Actions.setHighlightedSliceId({sliceId}));
315 }
316
317 onMouseOut() {
318 this.hoveredTitleId = -1;
319 globals.dispatch(Actions.setHighlightedSliceId({sliceId: -1}));
320 }
321
322 onMouseClick({x, y}: {x: number, y: number}): boolean {
323 const sliceIndex = this.getSliceIndex({x, y});
324 if (sliceIndex === undefined) return false;
325 const data = this.data;
326 if (data === undefined) return false;
327 const sliceId = data.sliceIds[sliceIndex];
328 if (sliceId !== undefined && sliceId !== -1) {
329 globals.makeSelection(Actions.selectChromeSlice({
330 id: sliceId,
Steve Goltoncfe3e3d2023-10-26 16:24:31 +0100331 trackKey: this.trackKey,
Steve Golton832f1c22023-10-17 14:21:09 +0100332 table: this.namespace,
333 }));
334 return true;
335 }
336 return false;
337 }
338
339 getHeight() {
340 return SLICE_HEIGHT * (this.maxDepth + 1) + 2 * TRACK_PADDING;
341 }
342
343 getSliceRect(
344 visibleTimeScale: TimeScale, visibleWindow: Span<time, duration>,
345 windowSpan: PxSpan, tStart: time, tEnd: time, depth: number): SliceRect
346 |undefined {
347 const pxEnd = windowSpan.end;
348 const left = Math.max(visibleTimeScale.timeToPx(tStart), 0);
349 const right = Math.min(visibleTimeScale.timeToPx(tEnd), pxEnd);
350
351 const visible = visibleWindow.intersects(tStart, tEnd);
352
353 return {
354 left,
355 width: Math.max(right - left, 1),
356 top: TRACK_PADDING + depth * SLICE_HEIGHT,
357 height: SLICE_HEIGHT,
358 visible,
359 };
360 }
361}