blob: d941357e12767796cc77c65bc245b486ac41a840 [file]
// Copyright (C) 2025 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {canvasSave, drawDoubleHeadedArrow} from '../../base/canvas_utils';
import type {Size2D} from '../../base/geom';
import {QuerySlot} from '../../base/query_slot';
import {Duration, type time} from '../../base/time';
import type {TimeScale} from '../../base/time_scale';
import {drawVerticalLineAtTime} from '../../base/vertical_line_helper';
import {asSchedSqlId} from '../../components/sql_utils/core_types';
import {
getSched,
getSchedWakeupInfo,
type SchedWakeupInfo,
} from '../../components/sql_utils/sched';
import type {CanvasColors} from '../../public/canvas_colors';
import type {Selection, TrackEventSelection} from '../../public/selection';
import type {Trace} from '../../public/trace';
import type {Overlay, TrackBounds} from '../../public/track';
import {CPU_SLICE_URI_PREFIX, uriForSchedTrack} from './common';
const MARGIN = 3;
const DIAMOND_SIZE = 8;
const ARROW_HEIGHT = 12;
export class WakerOverlay implements Overlay {
private readonly trace: Trace;
private readonly wakeupSlot = new QuerySlot<SchedWakeupInfo | undefined>();
constructor(trace: Trace) {
this.trace = trace;
}
render(
canvasCtx: CanvasRenderingContext2D,
timescale: TimeScale,
size: Size2D,
renderedTracks: ReadonlyArray<TrackBounds>,
colors: CanvasColors,
): void {
const selection = this.trace.selection.selection;
// Get out if selection is not a CPU slice.
if (!this.cpuSliceTrackSelected(selection)) {
return;
}
// Declaratively fetch wakeup info - QuerySlot handles caching and scheduling
const result = this.wakeupSlot.use({
key: {eventId: selection.eventId},
queryFn: () => this.loadWakeupInfo(selection),
});
const wakeup = result.data;
if (!wakeup?.wakeupTs) {
return;
}
// Draw the vertical line at the wakeup timestamp
drawVerticalLineAtTime(
canvasCtx,
timescale,
wakeup.wakeupTs,
size.height,
colors.COLOR_TIMELINE_OVERLAY,
);
// Draw the marker on the waker CPU track
if (wakeup.wakerCpu !== undefined) {
this.drawWakerMarker(
canvasCtx,
timescale,
renderedTracks,
wakeup.wakeupTs,
wakeup.wakerCpu,
colors.COLOR_TIMELINE_OVERLAY,
);
}
this.drawLatencyArrow(
canvasCtx,
timescale,
renderedTracks,
wakeup.wakeupTs,
selection.trackUri,
selection.ts,
colors.COLOR_TIMELINE_OVERLAY,
colors.COLOR_BACKGROUND,
colors.COLOR_TEXT,
);
}
private cpuSliceTrackSelected(
selection: Selection,
): selection is TrackEventSelection {
return (
selection.kind === 'track_event' &&
selection.trackUri.startsWith(CPU_SLICE_URI_PREFIX)
);
}
private async loadWakeupInfo(
selection: TrackEventSelection,
): Promise<SchedWakeupInfo | undefined> {
const sched = await getSched(
this.trace.engine,
asSchedSqlId(selection.eventId),
);
if (!sched) return undefined;
const cache = await getSchedWakeupInfo(this.trace.engine, sched);
return cache;
}
private drawWakerMarker(
canvasCtx: CanvasRenderingContext2D,
timescale: TimeScale,
renderedTracks: ReadonlyArray<TrackBounds>,
wakeupTs: time,
wakerCpu: number,
color: string,
): void {
const wakerCpuTrackUri = uriForSchedTrack(wakerCpu);
const wakerTrack = renderedTracks.find(
(track) => wakerCpuTrackUri === track.node.uri,
);
if (!wakerTrack) return;
const bounds = wakerTrack.verticalBounds;
const trackHeight = bounds.bottom - bounds.top;
const rectHeight = trackHeight - 2 * MARGIN;
const wakeupPosPx = Math.floor(timescale.timeToPx(wakeupTs));
using _ = canvasSave(canvasCtx);
canvasCtx.translate(0, bounds.top);
canvasCtx.beginPath();
const yCenter = MARGIN + rectHeight / 2;
canvasCtx.moveTo(wakeupPosPx, yCenter + DIAMOND_SIZE);
canvasCtx.fillStyle = color;
canvasCtx.lineTo(wakeupPosPx + DIAMOND_SIZE * 0.75, yCenter);
canvasCtx.lineTo(wakeupPosPx, yCenter - DIAMOND_SIZE);
canvasCtx.lineTo(wakeupPosPx - DIAMOND_SIZE * 0.75, yCenter);
canvasCtx.fill();
canvasCtx.closePath();
}
private drawLatencyArrow(
canvasCtx: CanvasRenderingContext2D,
timescale: TimeScale,
renderedTracks: ReadonlyArray<TrackBounds>,
wakeupTs: time,
wakedTrackUri: string,
wakedSliceTs: time,
arrowColor: string,
backgroundColor: string,
textColor: string,
): void {
const wakedTrack = renderedTracks.find(
(track) => wakedTrackUri === track.node.uri,
);
if (!wakedTrack) return;
const bounds = wakedTrack.verticalBounds;
const trackHeight = bounds.bottom - bounds.top;
const rectHeight = trackHeight - 2 * MARGIN;
const wakeupPosPx = timescale.timeToPx(wakeupTs);
const wakedSliceStartPx = timescale.timeToPx(wakedSliceTs);
const latencyWidthPx = wakedSliceStartPx - wakeupPosPx;
using _ = canvasSave(canvasCtx);
canvasCtx.translate(0, bounds.top);
// Draw the double-headed arrow
drawDoubleHeadedArrow(
canvasCtx,
wakeupPosPx,
MARGIN + rectHeight,
latencyWidthPx,
latencyWidthPx >= 20, // Only draw arrow heads if width is sufficient
arrowColor,
);
// Draw latency text if space permits
const latency = wakedSliceTs - wakeupTs;
const displayText = Duration.humanise(latency);
const measured = canvasCtx.measureText(displayText);
if (latencyWidthPx >= measured.width + 2) {
const textX = wakeupPosPx + latencyWidthPx / 2;
const textY = MARGIN + rectHeight - 1;
const textBgY = MARGIN + rectHeight - ARROW_HEIGHT;
// Semi-transparent background for text
canvasCtx.globalAlpha = 0.7;
canvasCtx.fillStyle = backgroundColor;
canvasCtx.fillRect(
textX - measured.width / 2 - 1,
textBgY,
measured.width + 2,
ARROW_HEIGHT - 1, // Height adjusted to fit within arrow bounds
);
canvasCtx.globalAlpha = 1.0;
// Latency text
canvasCtx.textBaseline = 'bottom';
canvasCtx.fillStyle = textColor;
canvasCtx.textAlign = 'center';
canvasCtx.fillText(displayText, textX, textY);
}
}
}