blob: bdffdec6c08f3cfc403832f50022416a8bf38c1e [file] [log] [blame]
// Copyright (C) 2018 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 {assertTrue} from '../../base/logging';
import {Actions} from '../../common/actions';
import {TrackState} from '../../common/state';
import {checkerboardExcept} from '../../frontend/checkerboard';
import {globals} from '../../frontend/globals';
import {Track} from '../../frontend/track';
import {trackRegistry} from '../../frontend/track_registry';
import {
Config,
CPU_SLICE_TRACK_KIND,
Data,
SliceData,
SummaryData
} from './common';
const MARGIN_TOP = 5;
const RECT_HEIGHT = 30;
interface Color {
c: string;
h: number;
s: number;
l: number;
}
const MD_PALETTE: Color[] = [
{c: 'red', h: 4, s: 90, l: 58},
{c: 'pink', h: 340, s: 82, l: 52},
{c: 'purple', h: 291, s: 64, l: 42},
{c: 'deep purple', h: 262, s: 52, l: 47},
{c: 'indigo', h: 231, s: 48, l: 48},
{c: 'blue', h: 207, s: 90, l: 54},
{c: 'light blue', h: 199, s: 98, l: 48},
{c: 'cyan', h: 187, s: 100, l: 42},
{c: 'teal', h: 174, s: 100, l: 29},
{c: 'green', h: 122, s: 39, l: 49},
{c: 'light green', h: 88, s: 50, l: 53},
{c: 'lime', h: 66, s: 70, l: 54},
{c: 'yellow', h: 54, s: 100, l: 62},
{c: 'amber', h: 45, s: 100, l: 51},
{c: 'orange', h: 36, s: 100, l: 50},
{c: 'deep organge', h: 14, s: 100, l: 57},
{c: 'brown', h: 16, s: 25, l: 38},
{c: 'grey', h: 0, s: 0, l: 62},
{c: 'blue gray', h: 200, s: 18, l: 46},
];
function hash(s: string, max: number): number {
let hash = 0x811c9dc5 & 0xfffffff;
for (let i = 0; i < s.length; i++) {
hash ^= s.charCodeAt(i);
hash = (hash * 16777619) & 0xffffffff;
}
return Math.abs(hash) % max;
}
function cropText(str: string, charWidth: number, rectWidth: number) {
const maxTextWidth = rectWidth - 4;
let displayText = '';
const nameLength = str.length * charWidth;
if (nameLength < maxTextWidth) {
displayText = str;
} else {
// -3 for the 3 ellipsis.
const displayedChars = Math.floor(maxTextWidth / charWidth) - 3;
if (displayedChars > 3) {
displayText = str.substring(0, displayedChars) + '...';
}
}
return displayText;
}
function getCurResolution() {
// Truncate the resolution to the closest power of 10.
const resolution = globals.frontendLocalState.timeScale.deltaPxToDuration(1);
return Math.pow(10, Math.floor(Math.log10(resolution)));
}
class CpuSliceTrack extends Track<Config, Data> {
static readonly kind = CPU_SLICE_TRACK_KIND;
static create(trackState: TrackState): CpuSliceTrack {
return new CpuSliceTrack(trackState);
}
private mouseXpos?: number;
private reqPending = false;
private hue: number;
private utidHoveredInThisTrack = -1;
constructor(trackState: TrackState) {
super(trackState);
// TODO: this needs to be kept in sync with the hue generation algorithm
// of overview_timeline_panel.ts
this.hue = (128 + (32 * this.config.cpu)) % 256;
}
reqDataDeferred() {
const {visibleWindowTime} = globals.frontendLocalState;
const reqStart = visibleWindowTime.start - visibleWindowTime.duration;
const reqEnd = visibleWindowTime.end + visibleWindowTime.duration;
const reqRes = getCurResolution();
this.reqPending = false;
globals.dispatch(Actions.reqTrackData({
trackId: this.trackState.id,
start: reqStart,
end: reqEnd,
resolution: reqRes
}));
}
renderCanvas(ctx: CanvasRenderingContext2D): void {
// TODO: fonts and colors should come from the CSS and not hardcoded here.
const {timeScale, visibleWindowTime} = globals.frontendLocalState;
const data = this.data();
// If there aren't enough cached slices data in |data| request more to
// the controller.
const inRange = data !== undefined &&
(visibleWindowTime.start >= data.start &&
visibleWindowTime.end <= data.end);
if (!inRange || data === undefined ||
data.resolution !== getCurResolution()) {
if (!this.reqPending) {
this.reqPending = true;
setTimeout(() => this.reqDataDeferred(), 50);
}
}
if (data === undefined) return; // Can't possibly draw anything.
// If the cached trace slices don't fully cover the visible time range,
// show a gray rectangle with a "Loading..." label.
checkerboardExcept(
ctx,
timeScale.timeToPx(visibleWindowTime.start),
timeScale.timeToPx(visibleWindowTime.end),
timeScale.timeToPx(data.start),
timeScale.timeToPx(data.end));
if (data.kind === 'summary') {
this.renderSummary(ctx, data);
} else if (data.kind === 'slice') {
this.renderSlices(ctx, data);
}
}
renderSummary(ctx: CanvasRenderingContext2D, data: SummaryData): void {
const {timeScale, visibleWindowTime} = globals.frontendLocalState;
const startPx = Math.floor(timeScale.timeToPx(visibleWindowTime.start));
const bottomY = MARGIN_TOP + RECT_HEIGHT;
let lastX = startPx;
let lastY = bottomY;
ctx.fillStyle = `hsl(${this.hue}, 50%, 60%)`;
ctx.beginPath();
ctx.moveTo(lastX, lastY);
for (let i = 0; i < data.utilizations.length; i++) {
const utilization = data.utilizations[i];
const startTime = i * data.bucketSizeSeconds + data.start;
lastX = Math.floor(timeScale.timeToPx(startTime));
ctx.lineTo(lastX, lastY);
lastY = MARGIN_TOP + Math.round(RECT_HEIGHT * (1 - utilization));
ctx.lineTo(lastX, lastY);
}
ctx.lineTo(lastX, bottomY);
ctx.closePath();
ctx.fill();
}
renderSlices(ctx: CanvasRenderingContext2D, data: SliceData): void {
const {timeScale, visibleWindowTime} = globals.frontendLocalState;
assertTrue(data.starts.length === data.ends.length);
assertTrue(data.starts.length === data.utids.length);
ctx.textAlign = 'center';
ctx.font = '12px Google Sans';
const charWidth = ctx.measureText('dbpqaouk').width / 8;
for (let i = 0; i < data.starts.length; i++) {
const tStart = data.starts[i];
const tEnd = data.ends[i];
const utid = data.utids[i];
if (tEnd <= visibleWindowTime.start || tStart >= visibleWindowTime.end) {
continue;
}
const rectStart = timeScale.timeToPx(tStart);
const rectEnd = timeScale.timeToPx(tEnd);
const rectWidth = rectEnd - rectStart;
if (rectWidth < 0.1) continue;
// TODO: consider de-duplicating this code with the copied one from
// chrome_slices/frontend.ts.
let title = `[utid:${utid}]`;
let subTitle = '';
const color = Object.assign({}, MD_PALETTE[14]);
const threadInfo = globals.threads.get(utid);
if (threadInfo !== undefined) {
const hasProc = !!threadInfo.pid;
const procName = threadInfo.procName || '';
let hashKey = threadInfo.tid;
if (hasProc) {
title = `${procName} [${threadInfo.pid}]`;
subTitle = `${threadInfo.threadName} [${threadInfo.tid}]`;
hashKey = threadInfo.pid!;
} else {
title = `${threadInfo.threadName} [${threadInfo.tid}]`;
}
const colorIdx = hash(hashKey.toString(), 16);
Object.assign(color, MD_PALETTE[colorIdx]);
}
const hovered = globals.frontendLocalState.highlightedUtid === utid;
color.l =
hovered ? Math.max(color.l - 40, 20) : Math.min(color.l + 10, 80);
color.s -= 20;
ctx.fillStyle = `hsl(${color.h}, ${color.s}%, ${color.l}%)`;
ctx.fillRect(rectStart, MARGIN_TOP, rectEnd - rectStart, RECT_HEIGHT);
// Don't render text when we have less than 5px to play with.
if (rectWidth < 5) continue;
title = cropText(title, charWidth, rectWidth);
subTitle = cropText(subTitle, charWidth, rectWidth);
const rectXCenter = rectStart + rectWidth / 2;
ctx.fillStyle = '#fff';
ctx.font = '12px Google Sans';
ctx.fillText(title, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 - 3);
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
ctx.font = '10px Google Sans';
ctx.fillText(subTitle, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 + 11);
}
const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack);
if (hoveredThread !== undefined) {
let line1 = '';
let line2 = '';
if (hoveredThread.pid) {
line1 = `P: ${hoveredThread.procName} [${hoveredThread.pid}]`;
line2 = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`;
} else {
line1 = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`;
}
ctx.font = '10px Google Sans';
const line1Width = ctx.measureText(line1).width;
const line2Width = ctx.measureText(line2).width;
const width = Math.max(line1Width, line2Width);
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
ctx.fillRect(this.mouseXpos!, MARGIN_TOP, width + 16, RECT_HEIGHT);
ctx.fillStyle = 'hsl(200, 50%, 40%)';
ctx.textAlign = 'left';
ctx.fillText(line1, this.mouseXpos! + 8, 18);
ctx.fillText(line2, this.mouseXpos! + 8, 28);
}
}
onMouseMove({x, y}: {x: number, y: number}) {
const data = this.data();
this.mouseXpos = x;
if (data === undefined || data.kind === 'summary') return;
const {timeScale} = globals.frontendLocalState;
if (y < MARGIN_TOP || y > MARGIN_TOP + RECT_HEIGHT) {
this.utidHoveredInThisTrack = -1;
globals.frontendLocalState.setHighlightedUtid(-1);
return;
}
const t = timeScale.pxToTime(x);
let hoveredUtid = -1;
for (let i = 0; i < data.starts.length; i++) {
const tStart = data.starts[i];
const tEnd = data.ends[i];
const utid = data.utids[i];
if (tStart <= t && t <= tEnd) {
hoveredUtid = utid;
break;
}
}
this.utidHoveredInThisTrack = hoveredUtid;
globals.frontendLocalState.setHighlightedUtid(hoveredUtid);
}
onMouseOut() {
this.utidHoveredInThisTrack = -1;
globals.frontendLocalState.setHighlightedUtid(-1);
this.mouseXpos = 0;
}
}
trackRegistry.register(CpuSliceTrack);