mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(ui): toggle rendered frame rate with backtick
This commit is contained in:
1 parent
53dc685c8d
commit
bf6c401e27
10 files changed
+253
-2
No files matched your search
@@ -53,6 +53,11 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
|
|||||||
toggle. Camera departure cancels pending work, arrival checks the final view,
|
toggle. Camera departure cancels pending work, arrival checks the final view,
|
||||||
and superseded requests cannot keep a newer view loading.
|
and superseded requests cannot keep a newer view loading.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Press backtick (`) to toggle a rendered-frame-rate readout beneath the logo.
|
||||||
|
Typing fields retain the key; monitoring stops when hidden.
|
||||||
|
|
||||||
|
|
||||||
- Extract vessel feed, store, rendering, selection, trail and card components with explicit source and scene services.
|
- Extract vessel feed, store, rendering, selection, trail and card components with explicit source and scene services.
|
||||||
- Bound contact retention for incomplete vessel observations, preserve source freshness and refresh history references in place.
|
- Bound contact retention for incomplete vessel observations, preserve source freshness and refresh history references in place.
|
||||||
|
|||||||
@@ -3269,3 +3269,11 @@ while the camera is stationary, backing off from 1.5 to 30 seconds. Failed road
|
|||||||
requests report an unavailable source. Leaving the traffic altitude range or
|
requests report an unavailable source. Leaving the traffic altitude range or
|
||||||
disabling the layer cancels pending work; superseded road and flow requests cannot
|
disabling the layer cancels pending work; superseded road and flow requests cannot
|
||||||
release the current request or keep its loading indicator active.
|
release the current request or keep its loading indicator active.
|
||||||
|
|
||||||
|
### Optional frame-rate readout
|
||||||
|
|
||||||
|
Backtick (`) toggles an FPS readout beneath the title logo. It counts actual
|
||||||
|
Cesium post-render events over one-second windows and does not request extra
|
||||||
|
frames. Typing fields, modified keys and key repeats do not toggle it. The
|
||||||
|
readout starts hidden each session and releases its timer and frame listener
|
||||||
|
when hidden or when the application is disposed.
|
||||||
@@ -428,5 +428,7 @@
|
|||||||
"src/layers/bikeshare/source.test.mjs",
|
"src/layers/bikeshare/source.test.mjs",
|
||||||
"scripts/traffic-fixtures.mjs",
|
"scripts/traffic-fixtures.mjs",
|
||||||
"src/layers/traffic/navigation.test.mjs",
|
"src/layers/traffic/navigation.test.mjs",
|
||||||
"scripts/qa-traffic-navigation.mjs"
|
"scripts/qa-traffic-navigation.mjs",
|
||||||
|
"src/ui/frameRateMonitor.js",
|
||||||
|
"src/ui/frameRateMonitor.test.mjs"
|
||||||
]
|
]
|
||||||
@@ -520,7 +520,8 @@
|
|||||||
"src/ui/visualPresets.js",
|
"src/ui/visualPresets.js",
|
||||||
"src/worldFocus.js",
|
"src/worldFocus.js",
|
||||||
"src/ui/uiLifetime.js",
|
"src/ui/uiLifetime.js",
|
||||||
"src/app/stateChannel.js"
|
"src/app/stateChannel.js",
|
||||||
|
"src/ui/frameRateMonitor.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,6 +32,16 @@ try {
|
|||||||
const result = await page.evaluate(async () => {
|
const result = await page.evaluate(async () => {
|
||||||
const ui = window.__godsEyeView.styleManager;
|
const ui = window.__godsEyeView.styleManager;
|
||||||
const counts = {};
|
const counts = {};
|
||||||
|
document.dispatchEvent(
|
||||||
|
new KeyboardEvent('keydown', {
|
||||||
|
key: '`',
|
||||||
|
code: 'Backquote',
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const fpsReadout = document.querySelector('.frame-rate-readout');
|
||||||
|
const fpsWasVisible = fpsReadout && !fpsReadout.hidden;
|
||||||
const watchObserver = (name) => {
|
const watchObserver = (name) => {
|
||||||
const owner =
|
const owner =
|
||||||
name === '_draggableResizeObserver'
|
name === '_draggableResizeObserver'
|
||||||
@@ -210,6 +220,8 @@ try {
|
|||||||
ui._lifetime.frames.size === 0 &&
|
ui._lifetime.frames.size === 0 &&
|
||||||
ui._lifetime.timers.size === 0 &&
|
ui._lifetime.timers.size === 0 &&
|
||||||
ui._lifetime.removers.size === 0,
|
ui._lifetime.removers.size === 0,
|
||||||
|
frameRateReleased:
|
||||||
|
fpsWasVisible && !fpsReadout.isConnected && fpsReadout.hidden,
|
||||||
stateStopped: stateNotifications === 0,
|
stateStopped: stateNotifications === 0,
|
||||||
idempotent: once === JSON.stringify(counts),
|
idempotent: once === JSON.stringify(counts),
|
||||||
};
|
};
|
||||||
@@ -218,6 +230,10 @@ try {
|
|||||||
window.removeEventListener = removeEventListener;
|
window.removeEventListener = removeEventListener;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
check(
|
||||||
|
'visible frame-rate monitor is removed on UI disposal',
|
||||||
|
result.frameRateReleased,
|
||||||
|
);
|
||||||
check(
|
check(
|
||||||
'real UI has the expected live resources before disposal',
|
'real UI has the expected live resources before disposal',
|
||||||
result.connected,
|
result.connected,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { createFrameRateMonitor } from './frameRateMonitor.js';
|
||||||
import { createStateChannel } from '../app/stateChannel.js';
|
import { createStateChannel } from '../app/stateChannel.js';
|
||||||
import { setSplitFlapText } from '../splitFlap.js';
|
import { setSplitFlapText } from '../splitFlap.js';
|
||||||
import { UiLifetime } from './uiLifetime.js';
|
import { UiLifetime } from './uiLifetime.js';
|
||||||
@@ -1389,6 +1390,11 @@ export class StyleManager {
|
|||||||
setScopeMaskFeather,
|
setScopeMaskFeather,
|
||||||
} = this.services;
|
} = this.services;
|
||||||
this._applicationShortcuts?.destroy();
|
this._applicationShortcuts?.destroy();
|
||||||
|
this._frameRateMonitor?.destroy();
|
||||||
|
this._frameRateMonitor = createFrameRateMonitor({
|
||||||
|
viewer: this.viewer,
|
||||||
|
documentRef: document,
|
||||||
|
});
|
||||||
this._applicationShortcuts = bindApplicationShortcuts({
|
this._applicationShortcuts = bindApplicationShortcuts({
|
||||||
documentRef: document,
|
documentRef: document,
|
||||||
searchInput: this._locationSearch,
|
searchInput: this._locationSearch,
|
||||||
@@ -5293,6 +5299,7 @@ export class StyleManager {
|
|||||||
this._panelLayout.destroy();
|
this._panelLayout.destroy();
|
||||||
this._applicationShortcuts?.destroy();
|
this._applicationShortcuts?.destroy();
|
||||||
this._displayControls?.destroy();
|
this._displayControls?.destroy();
|
||||||
|
this._frameRateMonitor?.destroy();
|
||||||
this._mapSourceControls?.destroy();
|
this._mapSourceControls?.destroy();
|
||||||
this._clearLayersControl?.destroy();
|
this._clearLayersControl?.destroy();
|
||||||
this._locationControls?.destroy();
|
this._locationControls?.destroy();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
Vendored
+9
@@ -591,6 +591,15 @@ body.cockpit-mode #cockpit-cloud-effects.active {
|
|||||||
text-transform: uppercase;
|
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 ──────────────────────────── */
|
||||||
#style-indicator {
|
#style-indicator {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -41,6 +41,19 @@
|
|||||||
width: 36px;
|
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 {
|
#location-bar {
|
||||||
left: 16px;
|
left: 16px;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
|
|||||||
Reference in new issue
Block a user