feat(ui): toggle rendered frame rate with backtick

This commit is contained in:
Sameh Khamis committed 2026-09-13 14:51:24 -07:00
1 parent 53dc685c8d
commit bf6c401e27
10 files changed
+253 -2

No files matched your search

+7
View File
@@ -1,3 +1,4 @@
import { createFrameRateMonitor } from './frameRateMonitor.js';
import { createStateChannel } from '../app/stateChannel.js';
import { setSplitFlapText } from '../splitFlap.js';
import { UiLifetime } from './uiLifetime.js';
@@ -1389,6 +1390,11 @@ export class StyleManager {
setScopeMaskFeather,
} = this.services;
this._applicationShortcuts?.destroy();
this._frameRateMonitor?.destroy();
this._frameRateMonitor = createFrameRateMonitor({
viewer: this.viewer,
documentRef: document,
});
this._applicationShortcuts = bindApplicationShortcuts({
documentRef: document,
searchInput: this._locationSearch,
@@ -5293,6 +5299,7 @@ export class StyleManager {
this._panelLayout.destroy();
this._applicationShortcuts?.destroy();
this._displayControls?.destroy();
this._frameRateMonitor?.destroy();
this._mapSourceControls?.destroy();
this._clearLayersControl?.destroy();
this._locationControls?.destroy();
+81
View File
@@ -0,0 +1,81 @@
/** Count rendered globe frames while the optional readout is visible. */
export function createFrameRateMonitor({ viewer, documentRef = document }) {
const host = documentRef.getElementById('title-bar');
const frameEvent = viewer?.scene?.postRender;
if (!host || !frameEvent) return { destroy() {} };
const readout = documentRef.createElement('div');
readout.className = 'frame-rate-readout';
readout.hidden = true;
readout.textContent = 'FPS —';
readout.title = 'Rendered globe frames per second · toggle with `';
host.appendChild(readout);
let removeFrameListener = null;
let timer = null;
let frames = 0;
let startedAt = 0;
let destroyed = false;
function hide() {
readout.hidden = true;
removeFrameListener?.();
removeFrameListener = null;
clearInterval(timer);
timer = null;
}
function show() {
frames = 0;
startedAt = performance.now();
readout.textContent = 'FPS —';
readout.hidden = false;
removeFrameListener = frameEvent.addEventListener(() => {
frames++;
});
timer = setInterval(() => {
const now = performance.now();
const elapsed = now - startedAt;
readout.textContent =
documentRef.hidden || elapsed <= 0
? 'FPS —'
: `FPS ${Math.round((frames * 1000) / elapsed)}`;
frames = 0;
startedAt = now;
}, 1000);
}
function onKeyDown(event) {
if (event.key !== '`' && event.code !== 'Backquote') return;
if (
event.defaultPrevented ||
event.repeat ||
event.isComposing ||
event.ctrlKey ||
event.altKey ||
event.metaKey ||
event.shiftKey
)
return;
if (
event.target?.isContentEditable ||
event.target?.closest?.(
'input, textarea, select, [contenteditable]:not([contenteditable="false"])',
)
)
return;
event.preventDefault();
if (readout.hidden) show();
else hide();
}
documentRef.addEventListener('keydown', onKeyDown);
return {
destroy() {
if (destroyed) return;
destroyed = true;
hide();
documentRef.removeEventListener('keydown', onKeyDown);
readout.remove();
},
};
}
+109
View File
@@ -0,0 +1,109 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createFrameRateMonitor } from './frameRateMonitor.js';
function fixture(t) {
const keys = new Set();
const frames = new Set();
let readout;
const documentRef = {
hidden: false,
getElementById: () => ({
appendChild: (node) => {
readout = node;
},
}),
createElement: () => ({
remove() {
this.removed = true;
},
}),
addEventListener: (name, handler) => keys.add(handler),
removeEventListener: (name, handler) => keys.delete(handler),
};
const viewer = {
scene: {
postRender: {
addEventListener(handler) {
frames.add(handler);
return () => frames.delete(handler);
},
},
},
};
const monitor = createFrameRateMonitor({ viewer, documentRef });
t.after(() => monitor.destroy());
return {
monitor,
documentRef,
frames,
keys,
readout: () => readout,
press(options = {}) {
const event = {
key: '`',
preventDefault() {
this.defaultPrevented = true;
},
...options,
};
for (const handler of keys) handler(event);
return event;
},
};
}
test('FPS measures rendered frames only while shown and releases its resources', (t) => {
t.mock.timers.enable({ apis: ['setInterval'] });
let now = 0;
t.mock.method(performance, 'now', () => now);
const f = fixture(t);
assert.equal(f.readout().hidden, true);
assert.equal(f.frames.size, 0);
assert.equal(f.press().defaultPrevented, true);
assert.equal(f.frames.size, 1);
for (let frame = 0; frame < 30; frame++)
for (const render of f.frames) render();
now = 1000;
t.mock.timers.tick(1000);
assert.equal(f.readout().textContent, 'FPS 30');
now = 2000;
t.mock.timers.tick(1000);
assert.equal(
f.readout().textContent,
'FPS 0',
'idle rendering is not reported as display refresh rate',
);
f.press();
assert.equal(f.readout().hidden, true);
assert.equal(f.frames.size, 0);
now = 3000;
t.mock.timers.tick(1000);
f.press();
assert.equal(f.readout().textContent, 'FPS —');
f.monitor.destroy();
f.monitor.destroy();
assert.equal(f.frames.size, 0);
assert.equal(f.keys.size, 0);
assert.equal(f.readout().removed, true);
});
test('typing, modified keys, composition and repeated presses do not toggle FPS', (t) => {
const f = fixture(t);
for (const options of [
{ target: { closest: () => ({}) } },
{ target: { isContentEditable: true } },
{ ctrlKey: true },
{ altKey: true },
{ metaKey: true },
{ shiftKey: true },
{ repeat: true },
{ isComposing: true },
{ defaultPrevented: true },
{ key: 'a' },
]) {
f.press(options);
assert.equal(f.readout().hidden, true);
assert.equal(f.frames.size, 0);
}
});
+9
View File
@@ -591,6 +591,15 @@ body.cockpit-mode #cockpit-cloud-effects.active {
text-transform: uppercase;
}
#title-bar .frame-rate-readout {
margin-top: 6px;
font-family: var(--font-mono);
font-size: 11px;
font-variant-numeric: tabular-nums;
letter-spacing: 1px;
color: var(--accent);
}
/* ── Style Indicator ──────────────────────────── */
#style-indicator {
position: fixed;
+13
View File
@@ -41,6 +41,19 @@
width: 36px;
}
#title-bar .frame-rate-readout {
position: fixed;
top: 52px;
left: 20px;
margin: 0;
font-size: 10px;
line-height: 12px;
}
#title-bar:has(.frame-rate-readout:not([hidden])) .subtitle {
visibility: hidden;
}
#location-bar {
left: 16px;
right: 16px;