Skip to content

Commit

Permalink
Highlight vsyncs in columns across all tracks (#52)
Browse files Browse the repository at this point in the history
For traces that have the VSYNC-app counter track in the SurfaceFlinger,
support a 'v' key toggle to show the alignment of the vsyncs with other
content in the tracks.

Signed-off-by: Christian W. Damus <[email protected]>
  • Loading branch information
cdamus authored Oct 16, 2023
1 parent bdd0153 commit 7610239
Show file tree
Hide file tree
Showing 11 changed files with 272 additions and 0 deletions.
1 change: 1 addition & 0 deletions ui/src/assets/common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
--alt-background-color: #d3d3d3;
--overview-background-color: #ffffff;
--track-highlight-background: #ffe263;
--track-vsync-background: #e7e7e7;
}

@mixin transition($time: 0.1s) {
Expand Down
5 changes: 5 additions & 0 deletions ui/src/controller/trace_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ import {
import {TrackControllerArgs, trackControllerRegistry} from './track_controller';
import {decideTracks} from './track_decider';
import {VisualisedArgController} from './visualised_args_controller';
import {VsyncController} from './vsync_controller';

type States = 'init' | 'loading_trace' | 'ready';

Expand Down Expand Up @@ -328,6 +329,10 @@ export class TraceController extends Controller<States> {
Child('traceError', TraceErrorController, {engine}));
childControllers.push(Child('metrics', MetricsController, {engine}));

// Vsync data from SurfaceFlinger's VSYNC-app counter, if available
childControllers.push(
Child('vsync', VsyncController, {engine, app: globals}));

return childControllers;

default:
Expand Down
124 changes: 124 additions & 0 deletions ui/src/controller/vsync_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright (C) 2022 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 {assertExists} from '../base/logging';
import {Engine} from '../common/engine';
import {LONG, NUM} from '../common/query_result';
import {Span} from '../common/time';
import {globals} from '../frontend/globals';
import {publishVsyncData} from '../frontend/publish';
import {Controller} from './controller';

export interface VsyncControllerArgs {
engine: Engine;
}

export class VsyncController extends Controller<'init'|'ready'> {
private engine: Engine;
private trackId?: number;
private timeSpan?: Span<bigint>;

constructor(args: VsyncControllerArgs) {
super('init');

this.engine = args.engine;
}

onDestroy() {
publishVsyncData({});
}

run() {
switch (this.state) {
case 'init':
this.getVsyncTrack().then((trackId) => this.trackId = trackId);
this.setState('ready');
break;
case 'ready':
if (this.trackId === undefined) {
// Still waiting for the track ID
return;
}
const visibleTimeSpan = globals.stateVisibleTime();
if (this.timeSpan === undefined ||
!this.timeSpan.equals(visibleTimeSpan)) {
this.timeSpan = visibleTimeSpan;
this.loadVsyncData(this.trackId, this.timeSpan);
}
break;
default:
throw new Error(`Unexpected state ${this.state}`);
}
}

async getVsyncTrack(): Promise<number|undefined> {
// Determine the track ID of the SurfaceFlinger VSYNC-app counter, if
// it exists.
const result = await this.engine.query(`
select process_counter_track.id as trackId
from process
join process_counter_track using (upid)
where process.name like '%/surfaceflinger' and
process_counter_track.name='VSYNC-app'
limit 1;
`);
if (result.numRows() < 1) {
return undefined;
}

return result.firstRow({'trackId': NUM}).trackId;
}

async loadVsyncData(trackId: number, timeSpan: Span<bigint>) {
// Try to get at least two changes of the counter, even if that means
// reaching beyond the currently visible timespan of the trace.
// But in any case get all changes of the counter that are in that
// visible span. Depending on the trace, and when zoomed in tight
// towards the end of the trace, there may not even be as many as
// two counter events to retrieve, so in that case we'll just
// get what we can.
const result = await this.engine.query(`
select ts, value from (
select row_number() over (order by ts) as rn, ts, value
from counter
where track_id = ${trackId} and ts >= ${timeSpan.start}
order by ts)
where rn <= 2 or ts <= ${timeSpan.end};
`);

const toggleTs: bigint[] = [];
let initiallyOn: boolean|undefined;
let lastWasOn = false;

const row = result.iter({ts: LONG, value: NUM});
for (; row.valid(); row.next()) {
const on = (row.value === 1);
if (initiallyOn === undefined) {
initiallyOn = !on;
lastWasOn = initiallyOn;
}
if (on !== lastWasOn) {
lastWasOn = on;
toggleTs.push(row.ts);
} // Otherwise, it didn't toggle
}

if (toggleTs.length === 0) {
publishVsyncData({});
} else {
initiallyOn = assertExists(initiallyOn);
publishVsyncData({initiallyOn, toggleTs});
}
}
}
12 changes: 12 additions & 0 deletions ui/src/frontend/frontend_local_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ export class FrontendLocalState {
};

private _selectedArea?: Area;
private _vsyncHighlight = false;

// TODO: there is some redundancy in the fact that both |visibleWindowTime|
// and a |timeScale| have a notion of time range. That should live in one
Expand Down Expand Up @@ -265,6 +266,17 @@ export class FrontendLocalState {
return this._selectedArea;
}

// Toggle whether to show the vsync highlight across the tracks (if available)
toggleVsyncHighlight() {
this._vsyncHighlight = !this._vsyncHighlight;
globals.rafScheduler.scheduleRedraw();
}

// Whether to show the vsync highlight across the tracks (if available)
get vsyncHighlight(): boolean {
return this._vsyncHighlight;
}

private ratelimitedUpdateVisible = ratelimit(() => {
globals.dispatch(Actions.setVisibleTraceTime(this._visibleState));
}, 50);
Expand Down
23 changes: 23 additions & 0 deletions ui/src/frontend/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,16 @@ export interface FtraceStat {
count: number;
}

// Vsync counter alternation extracted from the VSYNC-app counter
// of the SurfaceFlinger process, if such process and counter exist.
// The span of the data is what is currently visible in the UI.
export interface VsyncData {
// Was the counter on at the beginning of the visible timespan?
initiallyOn: boolean;
// The timestamps at which the counter toggles on or off.
toggleTs: bigint[];
}

function getRoot() {
// Works out the root directory where the content should be served from
// e.g. `http://origin/v1.2.3/`.
Expand Down Expand Up @@ -256,6 +266,9 @@ class Globals {
private _ftraceCounters?: FtraceStat[] = undefined;
private _ftracePanelData?: FtracePanelData = undefined;

// SurfaceFlinger app Vsync counter data.
private _vsyncData?: VsyncData = undefined;

// TODO(hjd): Remove once we no longer need to update UUID on redraw.
private _publishRedraw?: () => void = undefined;

Expand Down Expand Up @@ -319,6 +332,7 @@ class Globals {
this._threadStateDetails = {};
this._flamegraphDetails = {};
this._cpuProfileDetails = {};
this._vsyncData = {initiallyOn: false, toggleTs: []};
this.engines.clear();
}

Expand Down Expand Up @@ -698,6 +712,14 @@ class Globals {
this._trackFilteringEnabled = trackFilteringEnabled;
}

get vsyncData(): VsyncData {
return assertExists(this._vsyncData);
}

set vsyncData(data: VsyncData) {
this._vsyncData = assertExists(data);
}

private fireEngineReady(engine: EngineConfig) {
for (const observer of this._engineReadyObservers) {
observer(engine);
Expand Down Expand Up @@ -744,6 +766,7 @@ class Globals {
this._ignoreUnknownPostMessage = undefined;
this._disableMainRendering = undefined;
this._disableHashBasedRouting = undefined;
this._vsyncData = undefined;
}

// This variable is set by the is_internal_user.js script if the user is a
Expand Down
3 changes: 3 additions & 0 deletions ui/src/frontend/help_modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ class KeyMappingsHelp implements m.ClassComponent {
m('tr',
m('td', keycap(ctrlOrCmd), ' + ', keycap('a')),
m('td', 'Select all')),
m('tr',
m('td', keycap('v')),
m('td', 'Highlight VSyncs')),
...sidebarInstructions,
m('tr', m('td', keycap('?')), m('td', 'Show help')),
));
Expand Down
4 changes: 4 additions & 0 deletions ui/src/frontend/keyboard_event_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ export function handleKey(e: KeyboardEvent, down: boolean): boolean {
moveByFocusedFlow('Backward');
return true;
}
if (down && 'v' === key && noModifiers) {
globals.frontendLocalState.toggleVsyncHighlight();
return true;
}
return false;
}

Expand Down
9 changes: 9 additions & 0 deletions ui/src/frontend/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
SliceDetails,
ThreadDesc,
ThreadStateDetails,
VsyncData,
} from './globals';
import {findCurrentSelection} from './keyboard_event_handler';

Expand Down Expand Up @@ -208,3 +209,11 @@ export function publishFtracePanelData(data: FtracePanelData) {
globals.ftracePanelData = data;
globals.publishRedraw();
}

export function publishVsyncData(data: Partial<VsyncData>) {
globals.vsyncData = {
initiallyOn: !!data.initiallyOn,
toggleTs: data.toggleTs ?? [],
};
globals.publishRedraw();
}
10 changes: 10 additions & 0 deletions ui/src/frontend/track_group_panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {trackRegistry} from './track_registry';
import {
drawVerticalLineAtTime,
} from './vertical_line_helper';
import {getActiveVsyncData, renderVsyncColumns} from './vsync_helper';

interface Attrs {
trackGroupId: string;
Expand Down Expand Up @@ -301,6 +302,15 @@ export class TrackGroupPanel extends Panel<Attrs> {

if (!collapsed) return;

// If we have vsync data, render columns under the track group
const vsync = getActiveVsyncData();
if (vsync) {
ctx.save();
ctx.translate(this.shellWidth, 0);
renderVsyncColumns(ctx, size.height, vsync);
ctx.restore();
}

this.highlightIfTrackSelected(ctx, size);

drawGridLines(
Expand Down
11 changes: 11 additions & 0 deletions ui/src/frontend/track_panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {trackRegistry} from './track_registry';
import {
drawVerticalLineAtTime,
} from './vertical_line_helper';
import {getActiveVsyncData, renderVsyncColumns} from './vsync_helper';
import {SCROLLING_TRACK_GROUP, getContainingTrackIds} from '../common/state';

function getTitleSize(title: string): string|undefined {
Expand Down Expand Up @@ -416,6 +417,16 @@ export class TrackPanel extends Panel<TrackPanelAttrs> {
renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
ctx.save();

// If we have vsync data, render columns under the track and
// under the grid lines painted next
const vsync = getActiveVsyncData();
if (vsync) {
ctx.save();
ctx.translate(TRACK_SHELL_WIDTH, 0);
renderVsyncColumns(ctx, size.height, vsync);
ctx.restore();
}

drawGridLines(
ctx,
size.width,
Expand Down
70 changes: 70 additions & 0 deletions ui/src/frontend/vsync_helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (C) 2023 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 {getCssStr} from './css_constants';
import {globals, VsyncData} from './globals';

// Get the vsync data that the user has opted to highlight under the tracks.
export function getActiveVsyncData(): VsyncData|undefined {
return globals.frontendLocalState.vsyncHighlight ?
globals.vsyncData :
undefined;
}

// Render columns where the SurfaceFlinger VSYNC-app counter is on
// in the background of a track or track group of the given |height|,
// as indicated by the |vsync| data.
//
// Preconditions:
// - the rendering |ctx| is saved, if necessary, because
// this function reconfigures it
// - the rendering |ctx| is translated to put the origin at the beginning
// edge of the track or track group
export function renderVsyncColumns(ctx: CanvasRenderingContext2D,
height: number, vsync: VsyncData) {
const vsyncBackground = getCssStr('--track-vsync-background') ?? '#e7e7e7';
const {visibleWindowTime, visibleTimeScale} = globals.frontendLocalState;
const startPx =
Math.floor(visibleTimeScale.hpTimeToPx(visibleWindowTime.start));
const endPx =
Math.floor(visibleTimeScale.hpTimeToPx(visibleWindowTime.end));
const startTs = visibleWindowTime.start.toTPTime();
const endTs = visibleWindowTime.end.toTPTime();

ctx.fillStyle = vsyncBackground;

let fill = vsync.initiallyOn;
let lastX = startPx;
for (const ts of vsync.toggleTs) {
if (ts < startTs) {
fill = !fill;
continue;
}
const x = visibleTimeScale.tpTimeToPx(ts);
if (fill) {
ctx.fillRect(lastX, 0, x - lastX, height);
}
if (ts > endTs) {
break;
}
lastX = x;
fill = !fill;
}

// Do we need to fill out to the end?
if (fill && lastX > startPx && lastX < endPx) {
ctx.fillRect(lastX, 0, endPx - lastX, height);
}
}

0 comments on commit 7610239

Please sign in to comment.