performance slopping but actually very informative
@@ -0,0 +1,186 @@
|
||||
# `/` Performance Investigation Summary
|
||||
|
||||
This is the high-level handoff for the default route at:
|
||||
|
||||
```sh
|
||||
https://rover.otter.land/
|
||||
```
|
||||
|
||||
The detailed issue writeups live in `perf/issues/`.
|
||||
|
||||
## Main Finding
|
||||
|
||||
The `/` page is doing too much main-thread work for low-end devices. On the current powerful
|
||||
machine it can appear smooth, but CPU throttling shows real risk for mobile and weaker
|
||||
computers.
|
||||
|
||||
The biggest measured problem is repeated global input listener churn. The broader underlying
|
||||
theme is that high-frequency live data and broad React context invalidation cause many parts
|
||||
of the page to rerender or recommit DOM attributes.
|
||||
|
||||
## Key Measurements
|
||||
|
||||
### Clean `/` Profile, 8x Mobile CPU Throttle
|
||||
|
||||
Artifact:
|
||||
|
||||
```sh
|
||||
perf/results/2026-06-11T03-15-51-398Z/report.json
|
||||
```
|
||||
|
||||
Results:
|
||||
|
||||
```txt
|
||||
average frame interval: ~140ms
|
||||
p95 frame interval: ~166.8ms
|
||||
p99 frame interval: ~383.4ms
|
||||
long tasks: 67
|
||||
event listener delta: +13,481
|
||||
JS heap delta: +22.76MB
|
||||
ScriptDuration delta: ~35.85s
|
||||
TaskDuration delta: ~52.88s
|
||||
```
|
||||
|
||||
Top notable CPU self-time included:
|
||||
|
||||
```txt
|
||||
removeEventListener ~1.30s
|
||||
addEventListener ~0.94s
|
||||
```
|
||||
|
||||
### Listener Audit
|
||||
|
||||
Artifact:
|
||||
|
||||
```sh
|
||||
perf/results/2026-06-11T03-03-31-625Z-listeners/listener-report.json
|
||||
```
|
||||
|
||||
Worst churn:
|
||||
|
||||
```txt
|
||||
keydown adds=8040 removes=8039
|
||||
keyup adds=8040 removes=8039
|
||||
blur adds=8040 removes=8039
|
||||
gamepadconnected adds=8040 removes=8039
|
||||
gamepaddisconnected adds=8040 removes=8039
|
||||
```
|
||||
|
||||
### `/` Runtime Audit, 6x Mobile CPU Throttle, 30s
|
||||
|
||||
Artifact:
|
||||
|
||||
```sh
|
||||
perf/results/2026-06-11T03-21-51-263Z-root-runtime/root-runtime-report.json
|
||||
```
|
||||
|
||||
Results:
|
||||
|
||||
```txt
|
||||
average frame interval: ~88ms
|
||||
p95 frame interval: 150ms
|
||||
p99 frame interval: 183ms
|
||||
long tasks: 18
|
||||
WebSocket messages: at least 1000 sampled
|
||||
WebSocket bytes: ~798KB sampled
|
||||
DOM mutation records: 41,062
|
||||
DOM attribute records: 40,761
|
||||
```
|
||||
|
||||
Top sampled WebSocket events:
|
||||
|
||||
```txt
|
||||
log:entry 526
|
||||
sensorFrame 306
|
||||
commandAck 145
|
||||
session:sync 5
|
||||
```
|
||||
|
||||
### `/` Runtime Audit With Mutation Targets, 6x Mobile CPU Throttle, 18s
|
||||
|
||||
Artifact:
|
||||
|
||||
```sh
|
||||
perf/results/2026-06-11T03-22-44-714Z-root-runtime/root-runtime-report.json
|
||||
```
|
||||
|
||||
Results:
|
||||
|
||||
```txt
|
||||
average frame interval: ~116.6ms
|
||||
p95 frame interval: ~166.7ms
|
||||
p99 frame interval: ~316.6ms
|
||||
long tasks: 16
|
||||
DOM mutation records: 18,758
|
||||
DOM attribute records: 18,490
|
||||
```
|
||||
|
||||
Top WebSocket events:
|
||||
|
||||
```txt
|
||||
log:entry 850
|
||||
sensorFrame 277
|
||||
commandAck 200
|
||||
session:sync 23
|
||||
```
|
||||
|
||||
Top mutation targets:
|
||||
|
||||
```txt
|
||||
input.accent-emerald-400 name 5300
|
||||
input.accent-emerald-400 type 2650
|
||||
field-input name ~1300 each
|
||||
div.h-full style 1238
|
||||
path d 621
|
||||
path fill 619
|
||||
```
|
||||
|
||||
## Ordered Work Plan
|
||||
|
||||
1. Fix `KeyboardInputManager` and `GamepadInputManager` listener churn.
|
||||
2. Split/stabilize `ControlContext` so action consumers do not rerender on every state change.
|
||||
3. Batch or gate `log:entry` socket updates.
|
||||
4. Throttle visual telemetry rendering.
|
||||
5. Memoize or lazy-mount `ReplaySourcesPanel`.
|
||||
6. Split and memoize chat composer/nickname form.
|
||||
7. Clean up or share timers.
|
||||
|
||||
## Reproduction Commands
|
||||
|
||||
Install Playwright dependency if missing:
|
||||
|
||||
```sh
|
||||
npm install --prefix perf playwright@1.60.0
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
Clean CPU profile:
|
||||
|
||||
```sh
|
||||
CPU_THROTTLE=8 VIEWPORT=390x844 MOBILE=1 node perf/live-cpu-profile.mjs https://rover.otter.land/ perf/results
|
||||
```
|
||||
|
||||
Listener audit:
|
||||
|
||||
```sh
|
||||
node perf/live-listener-audit.mjs https://rover.otter.land/ perf/results
|
||||
```
|
||||
|
||||
Runtime audit:
|
||||
|
||||
```sh
|
||||
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=30000 node perf/live-root-runtime-audit.mjs https://rover.otter.land/ perf/results
|
||||
```
|
||||
|
||||
Runtime audit with shorter mutation target sample:
|
||||
|
||||
```sh
|
||||
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=18000 node perf/live-root-runtime-audit.mjs https://rover.otter.land/ perf/results
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
The `perf/results/2026-06-11T03-11-39-596Z-route-sweep/` route sweep includes `/spectate`,
|
||||
`/mini`, `/display`, and `/scanner`. Those are not the current priority. Use them only for
|
||||
context.
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
# Issue 001: Global Input Listener Churn
|
||||
|
||||
## Summary
|
||||
|
||||
The `/` page repeatedly removes and re-adds global keyboard and gamepad event listeners.
|
||||
This is the clearest and most actionable CPU problem found on the control page.
|
||||
|
||||
On weaker CPUs, this turns into measurable main-thread cost. It also makes the input layer
|
||||
fragile because every broad render/effect invalidation touches global browser listeners.
|
||||
|
||||
## Severity
|
||||
|
||||
High.
|
||||
|
||||
This should be fixed first. It is directly measured and likely amplifies other `/` issues.
|
||||
|
||||
## Affected Files
|
||||
|
||||
- `webui/src/controls/inputs/KeyboardInputManager.jsx`
|
||||
- `webui/src/controls/inputs/GamepadInputManager.jsx`
|
||||
- `webui/src/controls/inputs/gamepadHub.js`
|
||||
- Related multiplier: `webui/src/controls/ControlContext.jsx`
|
||||
|
||||
## Evidence
|
||||
|
||||
### Listener Audit
|
||||
|
||||
Artifact:
|
||||
|
||||
```sh
|
||||
perf/results/2026-06-11T03-03-31-625Z-listeners/listener-report.json
|
||||
```
|
||||
|
||||
At the second snapshot, the audit saw:
|
||||
|
||||
```txt
|
||||
keydown adds=8040 removes=8039
|
||||
keyup adds=8040 removes=8039
|
||||
blur adds=8040 removes=8039
|
||||
gamepadconnected adds=8040 removes=8039
|
||||
gamepaddisconnected adds=8040 removes=8039
|
||||
```
|
||||
|
||||
The stack mapped those to the live bundle locations corresponding to:
|
||||
|
||||
- `KeyboardInputManager.jsx` global `keydown`, `keyup`, `blur` effect
|
||||
- `gamepadHub.js` global `gamepadconnected`, `gamepaddisconnected` listener registration
|
||||
|
||||
### Root Runtime Audit
|
||||
|
||||
Artifact:
|
||||
|
||||
```sh
|
||||
perf/results/2026-06-11T03-21-51-263Z-root-runtime/root-runtime-report.json
|
||||
```
|
||||
|
||||
In a 30s `/` run at mobile viewport with 6x CPU throttle:
|
||||
|
||||
```txt
|
||||
add:window:keydown 770
|
||||
remove:window:keydown 769
|
||||
add:window:keyup 770
|
||||
remove:window:keyup 769
|
||||
add:window:blur 770
|
||||
remove:window:blur 769
|
||||
add:window:gamepadconnected 770
|
||||
remove:window:gamepadconnected 769
|
||||
add:window:gamepaddisconnected 770
|
||||
remove:window:gamepaddisconnected 769
|
||||
```
|
||||
|
||||
### Clean CPU Profile
|
||||
|
||||
Artifact:
|
||||
|
||||
```sh
|
||||
perf/results/2026-06-11T03-15-51-398Z/report.json
|
||||
```
|
||||
|
||||
Clean `/` profile, mobile viewport, 8x CPU throttle:
|
||||
|
||||
```txt
|
||||
average frame interval: ~140ms
|
||||
p95 frame interval: ~166.8ms
|
||||
p99 frame interval: ~383.4ms
|
||||
long tasks: 67
|
||||
event listener delta: +13,481
|
||||
removeEventListener: ~1.30s self time
|
||||
addEventListener: ~0.94s self time
|
||||
```
|
||||
|
||||
## Likely Cause
|
||||
|
||||
`KeyboardInputManager` installs global listeners inside a `useEffect` with a very large
|
||||
dependency list:
|
||||
|
||||
```jsx
|
||||
useEffect(() => {
|
||||
function handleKeyDown(event) { ... }
|
||||
function handleKeyUp(event) { ... }
|
||||
function handleBlur() { ... }
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown, { capture: true });
|
||||
window.addEventListener('keyup', handleKeyUp, { capture: true });
|
||||
window.addEventListener('blur', handleBlur);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown, { capture: true });
|
||||
window.removeEventListener('keyup', handleKeyUp, { capture: true });
|
||||
window.removeEventListener('blur', handleBlur);
|
||||
};
|
||||
}, [
|
||||
actionTokens,
|
||||
blurChat,
|
||||
driveFromKeys,
|
||||
ensureServoLoop,
|
||||
ensureSongLoop,
|
||||
focusChat,
|
||||
isChatFocused,
|
||||
keymap.chatFocus,
|
||||
...
|
||||
dockAssist,
|
||||
]);
|
||||
```
|
||||
|
||||
Many dependencies change when `ControlContext` or settings/session state changes. That
|
||||
causes the effect to tear down and reinstall global listeners over and over.
|
||||
|
||||
`GamepadInputManager` has the same pattern in a subscription effect:
|
||||
|
||||
```jsx
|
||||
useEffect(() => {
|
||||
return subscribeGamepadHub((hubState) => { ... });
|
||||
}, [
|
||||
activeSignature,
|
||||
ensureProfile,
|
||||
gamepadSettings?.defaults?.profile,
|
||||
gamepadSettings?.profiles,
|
||||
handleButtonEdge,
|
||||
handleCameraAxis,
|
||||
registerInputState,
|
||||
runMacro,
|
||||
setAuxMotors,
|
||||
setDriveVector,
|
||||
setMode,
|
||||
toggleNightVision,
|
||||
dockAssist,
|
||||
]);
|
||||
```
|
||||
|
||||
When this effect resubscribes, `subscribeGamepadHub` may add/remove global device listeners
|
||||
in `gamepadHub.js`.
|
||||
|
||||
## Fix Strategy
|
||||
|
||||
### KeyboardInputManager
|
||||
|
||||
Install global listeners once:
|
||||
|
||||
```jsx
|
||||
const latestRef = useRef(null);
|
||||
|
||||
latestRef.current = {
|
||||
keymap,
|
||||
actionTokens,
|
||||
isChatFocused,
|
||||
focusChat,
|
||||
resetAll,
|
||||
driveFromKeys,
|
||||
ensureServoLoop,
|
||||
ensureSongLoop,
|
||||
...
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
function handleKeyDown(event) {
|
||||
const latest = latestRef.current;
|
||||
if (!latest) return;
|
||||
// Existing logic, reading from latest instead of closure variables.
|
||||
}
|
||||
|
||||
function handleKeyUp(event) {
|
||||
const latest = latestRef.current;
|
||||
if (!latest) return;
|
||||
// Existing logic.
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
latestRef.current?.resetAll();
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown, { capture: true });
|
||||
window.addEventListener('keyup', handleKeyUp, { capture: true });
|
||||
window.addEventListener('blur', handleBlur);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown, { capture: true });
|
||||
window.removeEventListener('keyup', handleKeyUp, { capture: true });
|
||||
window.removeEventListener('blur', handleBlur);
|
||||
};
|
||||
}, []);
|
||||
```
|
||||
|
||||
Keep mutable input state in refs as it already mostly does:
|
||||
|
||||
- `activeTokensRef`
|
||||
- `lastVectorRef`
|
||||
- `lastAuxRef`
|
||||
- `servoIntervalRef`
|
||||
- `songIntervalRef`
|
||||
- `hornActiveRef`
|
||||
|
||||
### GamepadInputManager
|
||||
|
||||
Use the same pattern:
|
||||
|
||||
- Keep latest settings/actions/state in a ref.
|
||||
- Subscribe to `subscribeGamepadHub` once.
|
||||
- The subscription callback reads latest values from the ref.
|
||||
|
||||
Do not resubscribe just because profile/settings/action references changed.
|
||||
|
||||
### gamepadHub.js
|
||||
|
||||
After `GamepadInputManager` is stable, confirm whether `gamepadHub.js` still needs changes.
|
||||
It already tries to add global device listeners only when there are subscribers. The churn
|
||||
is likely caused by subscriber churn, not necessarily a bug in the hub itself.
|
||||
|
||||
## Validation
|
||||
|
||||
Before fix:
|
||||
|
||||
```sh
|
||||
node perf/live-listener-audit.mjs https://rover.otter.land/ perf/results
|
||||
```
|
||||
|
||||
Expected current bad result:
|
||||
|
||||
```txt
|
||||
hundreds or thousands of add/remove pairs for:
|
||||
keydown
|
||||
keyup
|
||||
blur
|
||||
gamepadconnected
|
||||
gamepaddisconnected
|
||||
```
|
||||
|
||||
After fix:
|
||||
|
||||
```txt
|
||||
keydown add ~= 1
|
||||
keyup add ~= 1
|
||||
blur add ~= 1
|
||||
gamepadconnected add ~= 1
|
||||
gamepaddisconnected add ~= 1
|
||||
removes only on page teardown
|
||||
```
|
||||
|
||||
Also run:
|
||||
|
||||
```sh
|
||||
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=30000 node perf/live-root-runtime-audit.mjs https://rover.otter.land/ perf/results
|
||||
CPU_THROTTLE=8 VIEWPORT=390x844 MOBILE=1 node perf/live-cpu-profile.mjs https://rover.otter.land/ perf/results
|
||||
```
|
||||
|
||||
Expected improvements:
|
||||
|
||||
- `JSEventListeners` delta should stop climbing dramatically.
|
||||
- `addEventListener` / `removeEventListener` should disappear from top CPU self-time.
|
||||
- Average frame interval and long-task count should improve, though other issues will remain.
|
||||
|
||||
## Risks
|
||||
|
||||
- Keyboard control safety matters. Be careful to preserve `resetAll()` on blur and text-input
|
||||
ignoring behavior.
|
||||
- If using refs, avoid stale data bugs by updating the ref every render before browser events
|
||||
can fire.
|
||||
- Verify horn hold, mic push-to-talk, chat focus, drive macro, dock assist, camera tilt, song
|
||||
controls, and Home Assistant shortcuts.
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
# Issue 002: ControlContext Broad Invalidation
|
||||
|
||||
## Summary
|
||||
|
||||
`ControlContext` exposes one large changing context value containing all control state,
|
||||
pipeline objects, limiter state, and action callbacks. This likely causes many consumers
|
||||
to re-render and effects to re-run when unrelated control state changes.
|
||||
|
||||
This is probably the multiplier behind the global input listener churn and some repeated
|
||||
DOM commits on `/`.
|
||||
|
||||
## Severity
|
||||
|
||||
High, but fix after issue 001 unless doing both together.
|
||||
|
||||
Issue 001 is the symptom with the clearest measurement. Issue 002 is the architectural
|
||||
cause that may also affect other parts of the page.
|
||||
|
||||
## Affected Files
|
||||
|
||||
- `webui/src/controls/ControlContext.jsx`
|
||||
- `webui/src/controls/inputs/KeyboardInputManager.jsx`
|
||||
- `webui/src/controls/inputs/GamepadInputManager.jsx`
|
||||
- Many consumers of `useControlSystem()`
|
||||
|
||||
## Evidence
|
||||
|
||||
The `contextValue` in `ControlContext.jsx` is:
|
||||
|
||||
```jsx
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
state,
|
||||
dispatch,
|
||||
pipeline,
|
||||
overcurrentLimiter,
|
||||
actions: {
|
||||
setMode,
|
||||
setDriveVector,
|
||||
setAuxMotors,
|
||||
...
|
||||
},
|
||||
}),
|
||||
[
|
||||
state,
|
||||
pipeline,
|
||||
overcurrentLimiter,
|
||||
setMode,
|
||||
setDriveVector,
|
||||
...
|
||||
],
|
||||
);
|
||||
```
|
||||
|
||||
Any state change changes the whole context value. Any consumer using:
|
||||
|
||||
```jsx
|
||||
const { state, actions } = useControlSystem();
|
||||
```
|
||||
|
||||
is subscribed to the broad value, even if it only needs one action or one small state field.
|
||||
|
||||
This connects to measured symptoms:
|
||||
|
||||
- Input listener effects rerun because their action/callback dependencies change.
|
||||
- `/` runtime audit saw repeated attribute commits in UI that should not need to change every
|
||||
telemetry/log tick.
|
||||
- CPU profiles show large minified React frames (`C9`, `ad`, `Fl`, etc.) alongside listener
|
||||
churn.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
React context invalidation is coarse. When provider value identity changes, every consumer
|
||||
below it can be scheduled to render. Even memoized children are not enough if they consume
|
||||
the changing context directly.
|
||||
|
||||
This project has several high-frequency update sources:
|
||||
|
||||
- telemetry frames
|
||||
- logs
|
||||
- session syncs
|
||||
- control state changes
|
||||
- settings state
|
||||
- timers
|
||||
|
||||
If `ControlContext` changes broadly, it makes it harder to isolate these updates.
|
||||
|
||||
## Likely Cause Pattern
|
||||
|
||||
Several action callbacks depend on `state` or `pipeline`, which changes their identity:
|
||||
|
||||
```jsx
|
||||
const setDriveVector = useCallback(..., [pipeline, recordControlIntent, state.manualDockAssist?.active]);
|
||||
const setServoAngle = useCallback(..., [pipeline, recordControlIntent, state.manualDockAssist?.active]);
|
||||
const runMacro = useCallback(..., [driveMacroBackoffEnabled, pipeline, ..., state.macros, ...]);
|
||||
const startHorn = useCallback(..., [dispatch, normalizedHornSettings, pipeline, ..., state.horn?.active, ...]);
|
||||
```
|
||||
|
||||
Those callbacks are then included in `contextValue`.
|
||||
|
||||
Consumers that include those callbacks in effects rerun when the callback identity changes.
|
||||
|
||||
## Fix Strategy
|
||||
|
||||
### Option A: Split State and Actions Contexts
|
||||
|
||||
Create separate contexts:
|
||||
|
||||
```jsx
|
||||
const ControlStateContext = createContext(null);
|
||||
const ControlActionsContext = createContext(null);
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```jsx
|
||||
<ControlStateContext.Provider value={stateValue}>
|
||||
<ControlActionsContext.Provider value={actionsValue}>
|
||||
{children}
|
||||
</ControlActionsContext.Provider>
|
||||
</ControlStateContext.Provider>
|
||||
```
|
||||
|
||||
Action-only consumers should not rerender when state changes.
|
||||
|
||||
### Option B: Add Selectors
|
||||
|
||||
Create a small external-store style control store, similar to `SessionContext` or
|
||||
`TelemetryContext`, so consumers can select only the state slice they need:
|
||||
|
||||
```jsx
|
||||
useControlSelector((state) => state.camera.angle)
|
||||
useControlActions()
|
||||
```
|
||||
|
||||
This is more work but scales better.
|
||||
|
||||
### Option C: Stabilize Actions With Refs
|
||||
|
||||
Keep action function identities stable and read latest state/pipeline from refs:
|
||||
|
||||
```jsx
|
||||
const latestRef = useRef(null);
|
||||
latestRef.current = { state, pipeline, overcurrentLimiter, ... };
|
||||
|
||||
const setDriveVector = useCallback((vector, meta = {}) => {
|
||||
const { state, pipeline } = latestRef.current;
|
||||
...
|
||||
}, []);
|
||||
```
|
||||
|
||||
This pairs well with issue 001 because stable actions make stable input effects easier.
|
||||
|
||||
## Recommended Path
|
||||
|
||||
For incremental work:
|
||||
|
||||
1. Fix issue 001 directly using refs in input managers.
|
||||
2. Split `ControlActionsContext` from `ControlStateContext`.
|
||||
3. Convert heavy/control-critical consumers first:
|
||||
- `KeyboardInputManager`
|
||||
- `GamepadInputManager`
|
||||
- mobile controls
|
||||
- right pane controls
|
||||
4. Later add selector hooks for state-heavy consumers.
|
||||
|
||||
## Validation
|
||||
|
||||
After issue 001 + partial issue 002:
|
||||
|
||||
```sh
|
||||
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=30000 node perf/live-root-runtime-audit.mjs https://rover.otter.land/ perf/results
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
- listener add/remove counts near 1
|
||||
- fewer repeated input attribute mutations
|
||||
- fewer long tasks
|
||||
- lower `ScriptDuration` and `TaskDuration`
|
||||
|
||||
For development builds, React DevTools Profiler would be ideal to verify fewer renders. The
|
||||
deployed production page does not currently publish source maps, so local profiling may be
|
||||
more useful after implementing.
|
||||
|
||||
## Risks
|
||||
|
||||
- Ref-based stable actions can hide stale state bugs if refs are not updated reliably.
|
||||
- Splitting context touches many files. Prefer a small staged migration.
|
||||
- Control commands are safety-sensitive; verify stop/horn/mic/docking behavior carefully.
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
# Issue 003: High-Volume Socket Log Stream
|
||||
|
||||
## Summary
|
||||
|
||||
The `/` page receives a very high volume of `log:entry` socket messages. These update the
|
||||
global session store even when logs are not the primary visible task.
|
||||
|
||||
This is not the single biggest CPU stack item, but it is a major source of live update
|
||||
pressure on low-end devices.
|
||||
|
||||
## Severity
|
||||
|
||||
Medium-high.
|
||||
|
||||
Fix after listener/context work, unless logs are known to be unusually noisy in production.
|
||||
|
||||
## Affected Files
|
||||
|
||||
- `webui/src/context/SessionContext.jsx`
|
||||
- `webui/src/components/LogPanel/index.jsx`
|
||||
- Server log emission paths, depending on where `log:entry` is emitted
|
||||
|
||||
## Evidence
|
||||
|
||||
Artifact:
|
||||
|
||||
```sh
|
||||
perf/results/2026-06-11T03-22-44-714Z-root-runtime/root-runtime-report.json
|
||||
```
|
||||
|
||||
In an 18s `/` audit at mobile viewport with 6x CPU throttle:
|
||||
|
||||
```txt
|
||||
log:entry count: 850
|
||||
log:entry bytes: 185,008
|
||||
```
|
||||
|
||||
Artifact:
|
||||
|
||||
```sh
|
||||
perf/results/2026-06-11T03-21-51-263Z-root-runtime/root-runtime-report.json
|
||||
```
|
||||
|
||||
In a 30s audit, among the most recent sampled 1000 WebSocket messages:
|
||||
|
||||
```txt
|
||||
log:entry count: 526
|
||||
log:entry bytes: 111,535
|
||||
```
|
||||
|
||||
In `SessionContext.jsx`, each log entry does:
|
||||
|
||||
```jsx
|
||||
function handleLogEntry(entry) {
|
||||
setState((prev) => ({ ...prev, logs: [...prev.logs.slice(-199), entry] }));
|
||||
}
|
||||
```
|
||||
|
||||
This creates a new session state object and a new logs array for every log message.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Even if `LogPanel` is not visible or not currently selected, `SessionContext` still processes
|
||||
the update. Every session subscriber selector is evaluated on each `setState`.
|
||||
|
||||
At 850 log messages in 18s, this is roughly 47 log entries per second.
|
||||
|
||||
On a powerful desktop this is fine. On a weaker phone or low-end laptop, it becomes steady
|
||||
background pressure.
|
||||
|
||||
## Likely Cause
|
||||
|
||||
The server appears to broadcast operational logs very frequently. The client stores the last
|
||||
200 logs globally.
|
||||
|
||||
The `/` page includes `LogPanel` under settings and may also have other components depending
|
||||
on session state.
|
||||
|
||||
## Fix Strategies
|
||||
|
||||
### Option A: Do Not Subscribe to Logs Unless Log UI Is Visible
|
||||
|
||||
Best conceptual fix:
|
||||
|
||||
- Add an explicit client event like `log:subscribe` / `log:unsubscribe`.
|
||||
- Only subscribe when `LogPanel` is open or user is admin.
|
||||
- For ordinary mobile control users, skip logs entirely.
|
||||
|
||||
### Option B: Batch Log Entries Client-Side
|
||||
|
||||
Keep receiving logs, but batch state updates:
|
||||
|
||||
```jsx
|
||||
const pendingLogsRef = useRef([]);
|
||||
const flushTimerRef = useRef(null);
|
||||
|
||||
function handleLogEntry(entry) {
|
||||
pendingLogsRef.current.push(entry);
|
||||
if (flushTimerRef.current) return;
|
||||
flushTimerRef.current = setTimeout(() => {
|
||||
const batch = pendingLogsRef.current.splice(0);
|
||||
flushTimerRef.current = null;
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
logs: [...prev.logs, ...batch].slice(-200),
|
||||
}));
|
||||
}, 250);
|
||||
}
|
||||
```
|
||||
|
||||
This changes 47 state updates/sec into about 4 updates/sec.
|
||||
|
||||
### Option C: Store Logs in a Separate External Store
|
||||
|
||||
Move logs out of main `SessionContext` so unrelated session consumers are not touched by
|
||||
log spam.
|
||||
|
||||
For example:
|
||||
|
||||
```jsx
|
||||
LogStoreProvider
|
||||
useLogEntries()
|
||||
```
|
||||
|
||||
This isolates log updates from user/roster/session state.
|
||||
|
||||
### Option D: Server-Side Sampling or Severity Filtering
|
||||
|
||||
Only send:
|
||||
|
||||
- warnings/errors by default
|
||||
- full logs to admin/log panel subscribers
|
||||
- sampled info/debug entries
|
||||
|
||||
## Recommended Path
|
||||
|
||||
Start with client batching. It is low-risk and easy to validate.
|
||||
|
||||
Then consider subscription gating if logs are not essential for normal users.
|
||||
|
||||
## Validation
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=30000 node perf/live-root-runtime-audit.mjs https://rover.otter.land/ perf/results
|
||||
```
|
||||
|
||||
The socket `log:entry` count may remain high if only batching client-side, but UI state
|
||||
updates should reduce. To measure batching directly, add temporary counters in
|
||||
`SessionContext` or extend the audit to count React commits after local changes.
|
||||
|
||||
Expected user-visible perf improvements:
|
||||
|
||||
- lower `ScriptDuration`
|
||||
- lower `TaskDuration`
|
||||
- fewer repeated DOM commits if log updates were causing broad subscribers to render
|
||||
|
||||
## Risks
|
||||
|
||||
- Logs may be used for operator visibility during incidents.
|
||||
- Batching can delay log display by 100-500ms. That should be acceptable for UI logs.
|
||||
- Subscription gating requires server/client protocol changes.
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
# Issue 004: Sensor Telemetry Render Frequency
|
||||
|
||||
## Summary
|
||||
|
||||
The `/` page receives frequent `sensorFrame` messages and renders multiple telemetry/HUD
|
||||
consumers from those frames. On throttled mobile CPU, this shows up as repeated SVG path
|
||||
attribute mutations and significant live DOM churn.
|
||||
|
||||
## Severity
|
||||
|
||||
Medium-high.
|
||||
|
||||
This is likely the biggest non-input-manager source of steady rendering work.
|
||||
|
||||
## Affected Files
|
||||
|
||||
- `webui/src/context/TelemetryContext.jsx`
|
||||
- `webui/src/components/TopDownMap/TopDownMapContent.jsx`
|
||||
- `webui/src/components/TopDownMap/visuals.jsx`
|
||||
- `webui/src/components/DriverVideo/index.jsx`
|
||||
- `webui/src/components/HudOverlays/*`
|
||||
- `webui/src/components/TelemetryPanel/index.jsx`
|
||||
- `webui/src/components/PiHostStatsCard/index.jsx`
|
||||
|
||||
## Evidence
|
||||
|
||||
Artifact:
|
||||
|
||||
```sh
|
||||
perf/results/2026-06-11T03-22-44-714Z-root-runtime/root-runtime-report.json
|
||||
```
|
||||
|
||||
In an 18s `/` audit at mobile viewport with 6x CPU throttle:
|
||||
|
||||
```txt
|
||||
sensorFrame count: 277
|
||||
sensorFrame bytes: 520,169
|
||||
```
|
||||
|
||||
That is about 15.4 sensor frames/sec.
|
||||
|
||||
The same audit saw SVG attribute mutations:
|
||||
|
||||
```txt
|
||||
path d mutations: 621
|
||||
path fill mutations: 619
|
||||
```
|
||||
|
||||
These map strongly to `TopDownMapContent.jsx` and `visuals.jsx`, where sensor values are
|
||||
converted into SVG arcs, cones, wheels, brush visuals, and colors.
|
||||
|
||||
Artifact:
|
||||
|
||||
```sh
|
||||
perf/results/2026-06-11T03-21-51-263Z-root-runtime/root-runtime-report.json
|
||||
```
|
||||
|
||||
In a 30s audit:
|
||||
|
||||
```txt
|
||||
sensorFrame count among latest sampled 1000 messages: 306
|
||||
sensorFrame bytes: 574,616
|
||||
```
|
||||
|
||||
## Current Code Path
|
||||
|
||||
`TelemetryContext.jsx` updates every incoming frame:
|
||||
|
||||
```jsx
|
||||
function handleSensorFrame({ roverId, sensors = {}, frame = {} }) {
|
||||
if (!roverId) return;
|
||||
const previous = framesRef.current[roverId] ?? {};
|
||||
framesRef.current = {
|
||||
...framesRef.current,
|
||||
[roverId]: {
|
||||
...previous,
|
||||
roverId,
|
||||
sensors,
|
||||
raw: frame?.data || null,
|
||||
receivedAt: Date.now(),
|
||||
},
|
||||
};
|
||||
notifyRover(roverId);
|
||||
}
|
||||
```
|
||||
|
||||
Consumers subscribe by rover:
|
||||
|
||||
```jsx
|
||||
useTelemetryFrame(roverId)
|
||||
```
|
||||
|
||||
Every `notifyRover(roverId)` wakes all consumers for that rover.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Telemetry itself may need to remain high-frequency for control safety or debugging, but the
|
||||
visual UI usually does not need to render at full sensor frequency.
|
||||
|
||||
The expensive part is not receiving the socket message. It is causing React/SVG/HUD updates
|
||||
and DOM attribute changes at the same cadence.
|
||||
|
||||
## Fix Strategies
|
||||
|
||||
### Option A: Throttle Render Notifications
|
||||
|
||||
Keep the latest frame in `framesRef` immediately, but notify subscribers at a lower rate.
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
const pendingRoversRef = useRef(new Set());
|
||||
const notifyScheduledRef = useRef(false);
|
||||
|
||||
function scheduleNotify(roverId) {
|
||||
pendingRoversRef.current.add(roverId);
|
||||
if (notifyScheduledRef.current) return;
|
||||
notifyScheduledRef.current = true;
|
||||
setTimeout(() => {
|
||||
notifyScheduledRef.current = false;
|
||||
const rovers = [...pendingRoversRef.current];
|
||||
pendingRoversRef.current.clear();
|
||||
rovers.forEach((id) => notifyRover(id));
|
||||
}, 100); // 10Hz visual updates
|
||||
}
|
||||
```
|
||||
|
||||
Then `handleSensorFrame` writes latest data immediately but calls `scheduleNotify(roverId)`.
|
||||
|
||||
### Option B: Separate Raw Telemetry From Visual Telemetry
|
||||
|
||||
Expose:
|
||||
|
||||
```jsx
|
||||
useTelemetryFrameRaw(roverId) // high-frequency, only for critical logic
|
||||
useTelemetryFrameVisual(roverId) // throttled for UI rendering
|
||||
```
|
||||
|
||||
Use throttled visual telemetry in:
|
||||
|
||||
- TopDownMap
|
||||
- HUD overlays
|
||||
- telemetry panels
|
||||
- battery bars
|
||||
- host stats display
|
||||
|
||||
Keep raw telemetry for safety mechanisms if needed.
|
||||
|
||||
### Option C: Memoize/Split Sensor Consumers by Field
|
||||
|
||||
Do not re-render the entire map or all overlays when only one sensor field changed.
|
||||
|
||||
Examples:
|
||||
|
||||
- Battery components subscribe only to battery fields.
|
||||
- Light bump bars subscribe only to light bump fields.
|
||||
- Overcurrent overlay subscribes only to overcurrent fields.
|
||||
|
||||
This is more architectural but can reduce unnecessary work.
|
||||
|
||||
### Option D: Render TopDownMap Less Often
|
||||
|
||||
At the component level:
|
||||
|
||||
```jsx
|
||||
const visualSensors = useThrottledValue(sensors, 100);
|
||||
```
|
||||
|
||||
This is easier than changing `TelemetryContext`, but less central.
|
||||
|
||||
## Recommended Path
|
||||
|
||||
Start with a throttled visual notification path in `TelemetryContext`. It gives the largest
|
||||
blast-radius reduction without changing every consumer immediately.
|
||||
|
||||
A reasonable target:
|
||||
|
||||
- Desktop: 10-15Hz visual updates
|
||||
- Mobile: 5-10Hz visual updates
|
||||
|
||||
## Validation
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=18000 node perf/live-root-runtime-audit.mjs https://rover.otter.land/ perf/results
|
||||
```
|
||||
|
||||
Expected improvements:
|
||||
|
||||
- `path d` and `path fill` mutation counts should drop.
|
||||
- `LayoutCount` and `RecalcStyleCount` may drop.
|
||||
- `ScriptDuration` should drop.
|
||||
- Frame p95 should improve.
|
||||
|
||||
Socket `sensorFrame` count may remain the same if only rendering is throttled.
|
||||
|
||||
## Risks
|
||||
|
||||
- Do not throttle logic that protects the rover or sends control commands unless you are sure
|
||||
it is only display logic.
|
||||
- Operators may expect smooth sensor visuals, but 10Hz is usually enough for dashboards.
|
||||
- Make sure low battery/overcurrent warnings still feel responsive.
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
# Issue 005: ReplaySourcesPanel Repeated DOM Commits
|
||||
|
||||
## Summary
|
||||
|
||||
`ReplaySourcesPanel` appears to re-commit many input/checkbox attributes repeatedly on `/`,
|
||||
especially in mobile portrait where the panel is always mounted near the top of the page.
|
||||
|
||||
This may be a component-local issue, or it may be caused by broader context/session updates.
|
||||
Tackle it after issues 001 and 002 unless profiling still shows it as large.
|
||||
|
||||
## Severity
|
||||
|
||||
Medium.
|
||||
|
||||
It is visible in mutation probes, but likely partly downstream of broader invalidation.
|
||||
|
||||
## Affected Files
|
||||
|
||||
- `webui/src/components/ReplaySourcesPanel/index.jsx`
|
||||
- `webui/src/App.jsx`
|
||||
- `webui/src/context/SessionContext.jsx`
|
||||
- `webui/src/settings/SettingsProvider.jsx`
|
||||
|
||||
## Evidence
|
||||
|
||||
Artifact:
|
||||
|
||||
```sh
|
||||
perf/results/2026-06-11T03-22-44-714Z-root-runtime/root-runtime-report.json
|
||||
```
|
||||
|
||||
In an 18s `/` audit at mobile viewport with 6x CPU throttle, top mutation targets included:
|
||||
|
||||
```txt
|
||||
input.accent-emerald-400 name: 5300
|
||||
input.accent-emerald-400 type: 2650
|
||||
input#replay-sources-mobile-portrait-title name: 666
|
||||
input#replay-sources-mobile-portrait-title type: 333
|
||||
```
|
||||
|
||||
The `accent-emerald-400` class maps to `ReplaySourcesPanel` checkboxes:
|
||||
|
||||
```jsx
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeSidebar}
|
||||
...
|
||||
className="accent-emerald-400"
|
||||
/>
|
||||
```
|
||||
|
||||
and source item checkboxes:
|
||||
|
||||
```jsx
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.includes(item.key)}
|
||||
onChange={() => onToggle(item.key)}
|
||||
className="accent-emerald-400"
|
||||
/>
|
||||
```
|
||||
|
||||
The mobile portrait route mounts it here:
|
||||
|
||||
```jsx
|
||||
<ReplaySourcesPanel panelId="replay-sources-mobile-portrait" />
|
||||
```
|
||||
|
||||
in `App.jsx`.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Repeated DOM attribute commits are a symptom of repeated React commits. Even if the attribute
|
||||
values do not change semantically, React still touches DOM attributes when the subtree
|
||||
rerenders/commits.
|
||||
|
||||
The panel is not central to driving, so it should not be doing meaningful work during normal
|
||||
control-page idle.
|
||||
|
||||
## Likely Causes
|
||||
|
||||
The component subscribes to several session slices:
|
||||
|
||||
```jsx
|
||||
const replaySources = useSessionSelector((state) => state.session?.replaySources ?? []);
|
||||
const mode = useSessionSelector((state) => state.session?.mode || null);
|
||||
const assignmentRoverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
|
||||
const roster = useSessionSelector((state) => state.session?.roster ?? []);
|
||||
const replayState = useSessionSelector((state) => state.session?.replay || null);
|
||||
const latestReplay = useSessionSelector((state) => state.latestReplay);
|
||||
```
|
||||
|
||||
Potential problems:
|
||||
|
||||
- `session:sync` may provide new array/object identities even when contents are unchanged.
|
||||
- `normalizeSources(replaySources || [])` runs every render and produces new objects.
|
||||
- `GroupList` is not memoized.
|
||||
- `selected.includes(item.key)` is recomputed for each item on each render.
|
||||
- `remainingMs` interval updates every 250ms when cooldown is active.
|
||||
- Parent/mobile layout re-renders can also re-render this panel.
|
||||
|
||||
## Fix Strategies
|
||||
|
||||
### Option A: Memoize Normalized Sources
|
||||
|
||||
```jsx
|
||||
const sources = useMemo(
|
||||
() => normalizeSources(replaySources || []),
|
||||
[replaySources],
|
||||
);
|
||||
```
|
||||
|
||||
This is already not memoized in the current code.
|
||||
|
||||
### Option B: Use Shallow Equality Selectors
|
||||
|
||||
For arrays like `replaySources` and `roster`, use a selector/equality function that avoids
|
||||
rerendering when content did not change.
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
const replaySources = useSessionSelector(
|
||||
(state) => state.session?.replaySources ?? [],
|
||||
shallowReplaySourcesEqual,
|
||||
);
|
||||
```
|
||||
|
||||
### Option C: Memoize GroupList
|
||||
|
||||
```jsx
|
||||
const GroupList = React.memo(function GroupList(...) { ... });
|
||||
```
|
||||
|
||||
Also memoize:
|
||||
|
||||
```jsx
|
||||
const selectedSet = useMemo(() => new Set(selected), [selected]);
|
||||
```
|
||||
|
||||
Then use `selectedSet.has(item.key)`.
|
||||
|
||||
### Option D: Do Not Mount Replay Panel By Default on Mobile
|
||||
|
||||
If replay is not a core mobile driving workflow, consider putting it behind a tab/accordion
|
||||
or lazy mounting it only when expanded.
|
||||
|
||||
This is a product decision.
|
||||
|
||||
### Option E: Stop Passing Unstable Callbacks
|
||||
|
||||
Memoize callbacks:
|
||||
|
||||
```jsx
|
||||
const toggleKey = useCallback((key) => { ... }, []);
|
||||
const handleReplay = useCallback(async () => { ... }, [...]);
|
||||
```
|
||||
|
||||
This helps if child components are memoized.
|
||||
|
||||
## Recommended Path
|
||||
|
||||
1. Fix issues 001 and 002 first.
|
||||
2. Rerun root runtime audit.
|
||||
3. If `ReplaySourcesPanel` still dominates mutation targets:
|
||||
- memoize `sources`
|
||||
- memoize `GroupList`
|
||||
- add shallow equality selectors
|
||||
- consider lazy mounting on mobile
|
||||
|
||||
## Validation
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=18000 node perf/live-root-runtime-audit.mjs https://rover.otter.land/ perf/results
|
||||
```
|
||||
|
||||
Expected improvement:
|
||||
|
||||
```txt
|
||||
input.accent-emerald-400 name/type mutations should drop sharply
|
||||
replay title input name/type mutations should drop
|
||||
```
|
||||
|
||||
## Risks
|
||||
|
||||
- Replay source selections and saved panel settings must still update correctly.
|
||||
- If lazy mounting, preserve persisted title/sidebar settings.
|
||||
- Do not break replay cooldown display.
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# Issue 006: Chat and Nickname Composer Churn
|
||||
|
||||
## Summary
|
||||
|
||||
The mobile `/` page defaults to the chat tab, and the chat/nickname composer shows repeated
|
||||
DOM attribute commits in runtime audits. This is not the top CPU issue, but it is part of
|
||||
steady idle churn.
|
||||
|
||||
## Severity
|
||||
|
||||
Medium-low to medium.
|
||||
|
||||
Fix after the listener/context/log/telemetry work unless chat remains high in mutation
|
||||
reports.
|
||||
|
||||
## Affected Files
|
||||
|
||||
- `webui/src/components/ChatPanel/index.jsx`
|
||||
- `webui/src/components/NicknameForm/index.jsx`
|
||||
- `webui/src/context/ChatContext.jsx`
|
||||
- `webui/src/App.jsx`
|
||||
|
||||
## Evidence
|
||||
|
||||
Artifact:
|
||||
|
||||
```sh
|
||||
perf/results/2026-06-11T03-22-44-714Z-root-runtime/root-runtime-report.json
|
||||
```
|
||||
|
||||
In an 18s `/` audit at mobile viewport with 6x CPU throttle:
|
||||
|
||||
```txt
|
||||
input.field-input.chat-composer-input name: 666
|
||||
input.field-input.flex-1 name: 1328
|
||||
input.field-input.w-full name: 1332
|
||||
input.accent-cyan-500 name: 1324
|
||||
input.accent-cyan-500 type: 662
|
||||
```
|
||||
|
||||
These map to:
|
||||
|
||||
- chat composer input
|
||||
- nickname form input
|
||||
- TTS/speak checkbox
|
||||
- related chat form controls
|
||||
|
||||
The chat panel is mounted by default in mobile feature tabs:
|
||||
|
||||
```jsx
|
||||
const [activeTab, setActiveTab] = useState('chat');
|
||||
...
|
||||
<TabPanel id="chat">
|
||||
<ChatPanel nicknameLayout="stacked" />
|
||||
...
|
||||
</TabPanel>
|
||||
```
|
||||
|
||||
## Why This Matters
|
||||
|
||||
The chat UI is useful, but it should not re-commit its composer inputs hundreds of times
|
||||
while the user is simply watching/driving.
|
||||
|
||||
Also, text inputs and focus-related logic interact with keyboard driving. Excess churn here
|
||||
can have indirect effects on input handling.
|
||||
|
||||
## Likely Causes
|
||||
|
||||
`ChatPanel` consumes:
|
||||
|
||||
```jsx
|
||||
const role = useSessionSelector((state) => state.session?.role || null);
|
||||
const currentRoverId = useSessionSelector((state) => state.session?.assignment?.roverId || null);
|
||||
const roster = useSessionSelector((state) => state.session?.roster ?? []);
|
||||
const { messages, typing, sendMessage, ... } = useChat();
|
||||
const { value: ttsSettings, save: saveTtsSettings } = useSettingsNamespace('tts', ...);
|
||||
```
|
||||
|
||||
It also has effects that sync local state from settings:
|
||||
|
||||
```jsx
|
||||
useEffect(() => {
|
||||
const nextEngine = ...
|
||||
if (engine !== nextEngine) setEngine(nextEngine);
|
||||
...
|
||||
}, [engine, googlePitch, googleSpeed, pitch, ttsSettings..., voice]);
|
||||
```
|
||||
|
||||
Potential problems:
|
||||
|
||||
- broad session/chat context updates rerender the whole panel
|
||||
- message list and composer are in the same component
|
||||
- typing events cause panel updates
|
||||
- nickname form may be rerendering with the whole chat panel
|
||||
- TTS settings sync effect has many dependencies
|
||||
|
||||
## Fix Strategies
|
||||
|
||||
### Option A: Split ChatPanel Into Memoized Subcomponents
|
||||
|
||||
Separate:
|
||||
|
||||
- `ChatMessageList`
|
||||
- `ChatComposer`
|
||||
- `TtsControls`
|
||||
- `NicknameForm`
|
||||
|
||||
Only `ChatMessageList` should rerender when messages change.
|
||||
Only `ChatComposer` should rerender when draft/sending/focus changes.
|
||||
|
||||
### Option B: Memoize Composer
|
||||
|
||||
```jsx
|
||||
const ChatComposer = React.memo(function ChatComposer(props) { ... });
|
||||
```
|
||||
|
||||
Pass stable callbacks where possible.
|
||||
|
||||
### Option C: Debounce Typing Updates
|
||||
|
||||
`setTypingActive(Boolean(next.trim()))` fires on every keystroke. That is fine while typing,
|
||||
but should not be involved during idle. If it causes socket chatter, debounce it.
|
||||
|
||||
### Option D: Stabilize NicknameForm
|
||||
|
||||
If `NicknameForm` consumes settings/session state broadly, make it use precise selectors and
|
||||
memoize it.
|
||||
|
||||
### Option E: Avoid Re-Syncing Local TTS State Too Often
|
||||
|
||||
The settings sync effect can be simplified or guarded so it only runs when the settings
|
||||
object actually changes, not on every local state update.
|
||||
|
||||
## Recommended Path
|
||||
|
||||
1. Fix issues 001-004 first.
|
||||
2. Rerun mutation audit.
|
||||
3. If chat/nickname inputs remain high:
|
||||
- split `ChatPanel`
|
||||
- memoize composer
|
||||
- stabilize `NicknameForm`
|
||||
|
||||
## Validation
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=18000 node perf/live-root-runtime-audit.mjs https://rover.otter.land/ perf/results
|
||||
```
|
||||
|
||||
Expected improvements:
|
||||
|
||||
```txt
|
||||
chat-composer-input mutations should drop
|
||||
NicknameForm input mutations should drop
|
||||
accent-cyan-500 checkbox mutations should drop
|
||||
```
|
||||
|
||||
Also manually verify:
|
||||
|
||||
- typing
|
||||
- sending chat
|
||||
- TTS options
|
||||
- nickname edit/save
|
||||
- Enter-to-focus/send chat shortcut
|
||||
|
||||
## Risks
|
||||
|
||||
- Chat focus behavior is tied to keyboard controls.
|
||||
- Splitting components can accidentally break `registerInputRef`.
|
||||
- TTS settings must still persist correctly.
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
# Issue 007: Timers and Polling Cleanup
|
||||
|
||||
## Summary
|
||||
|
||||
The `/` page creates multiple intervals and timeouts. They are not the dominant problem,
|
||||
but they contribute to steady background work and can wake components even when hidden.
|
||||
|
||||
## Severity
|
||||
|
||||
Low to medium.
|
||||
|
||||
Address after the larger structural issues unless a specific timer is found to be hot.
|
||||
|
||||
## Affected Files
|
||||
|
||||
Known or likely timer users:
|
||||
|
||||
- `webui/src/components/AlertFeed/index.jsx`
|
||||
- `webui/src/components/ReplaySourcesPanel/index.jsx`
|
||||
- `webui/src/components/HudOverlays/TurnsOverlay/index.jsx`
|
||||
- `webui/src/components/RoverQueuesPanel/index.jsx`
|
||||
- `webui/src/components/ModeGateOverlay/index.jsx`
|
||||
- `webui/src/components/SocketConnectionPill/index.jsx`
|
||||
- `webui/src/controls/ControlContext.jsx` horn heat interval
|
||||
- `webui/src/controls/overcurrentLimiter.js`
|
||||
|
||||
## Evidence
|
||||
|
||||
Artifact:
|
||||
|
||||
```sh
|
||||
perf/results/2026-06-11T03-22-44-714Z-root-runtime/root-runtime-report.json
|
||||
```
|
||||
|
||||
In an 18s `/` audit at mobile viewport with 6x CPU throttle:
|
||||
|
||||
```txt
|
||||
intervals created: 22
|
||||
timeouts created: 11
|
||||
interval fires: 148
|
||||
timeout fires: 6
|
||||
```
|
||||
|
||||
Most common timer registrations:
|
||||
|
||||
```txt
|
||||
interval:200 7
|
||||
timeout:12000 4
|
||||
interval:60000 4
|
||||
interval:250 4
|
||||
interval:3000 4
|
||||
interval:1000 2
|
||||
```
|
||||
|
||||
In a 30s audit:
|
||||
|
||||
```txt
|
||||
interval fires: 593
|
||||
```
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Timers wake the main thread. On a fast desktop they disappear into the noise; on a throttled
|
||||
mobile CPU, timers can combine with socket traffic and React work to create visible stalls.
|
||||
|
||||
The timer issue is not that any one interval is terrible. It is that many UI elements keep
|
||||
time independently.
|
||||
|
||||
## Likely Sources
|
||||
|
||||
### ReplaySourcesPanel
|
||||
|
||||
Cooldown display:
|
||||
|
||||
```jsx
|
||||
const interval = setInterval(update, 250);
|
||||
```
|
||||
|
||||
Only needed while cooldown is active and panel is visible.
|
||||
|
||||
### AlertFeed
|
||||
|
||||
Likely uses a frequent interval for alert lifetimes. This should only run while alerts exist.
|
||||
|
||||
### TurnsOverlay / RoverQueuesPanel
|
||||
|
||||
Countdown displays often use `setInterval(() => setNow(Date.now()), 250 or 1000)`.
|
||||
|
||||
### ControlContext Horn Heat
|
||||
|
||||
Horn heat interval at 100ms only runs when horn is active/cooling:
|
||||
|
||||
```jsx
|
||||
const tickMs = 100;
|
||||
const interval = setInterval(...)
|
||||
```
|
||||
|
||||
This is probably okay, but verify it only runs when needed.
|
||||
|
||||
## Fix Strategies
|
||||
|
||||
### Option A: Visibility-Gate Timers
|
||||
|
||||
Only run panel-specific timers when the panel is visible/open.
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
if (!isVisible || !needsCountdown) return;
|
||||
```
|
||||
|
||||
### Option B: Coarse Intervals
|
||||
|
||||
Use 1000ms for human-readable countdowns unless sub-second precision matters.
|
||||
|
||||
Replay cooldowns and queue countdowns probably do not need 250ms.
|
||||
|
||||
### Option C: Shared Clock Store
|
||||
|
||||
Instead of each component creating its own interval, create one shared clock:
|
||||
|
||||
```jsx
|
||||
useClock(1000)
|
||||
useClock(250)
|
||||
```
|
||||
|
||||
Then multiple components can share a single interval.
|
||||
|
||||
### Option D: CSS Animations for Pure Visual Expiry
|
||||
|
||||
For alert progress bars, use CSS animation if the UI does not need React state each tick.
|
||||
|
||||
## Recommended Path
|
||||
|
||||
After major fixes, rerun the runtime audit. If intervals still show high:
|
||||
|
||||
1. Increase replay cooldown interval from 250ms to 1000ms.
|
||||
2. Gate timers by tab visibility.
|
||||
3. Create a shared clock hook for countdown UI.
|
||||
4. Convert alert lifetime visuals to CSS where possible.
|
||||
|
||||
## Validation
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=30000 node perf/live-root-runtime-audit.mjs https://rover.otter.land/ perf/results
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- lower `intervals`
|
||||
- lower `intervalFires`
|
||||
- no visible regression in countdowns/alerts
|
||||
|
||||
## Risks
|
||||
|
||||
- Timers used for control safety should not be slowed without careful review.
|
||||
- Alert expiration should still happen reliably.
|
||||
- Queue/turn countdowns can update less often, but should not become misleading.
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# `/` CPU Performance Issue Index
|
||||
|
||||
This folder breaks the live-site `/` CPU investigation into separate, work-ready issues.
|
||||
Each issue can be handled in its own chat or branch without needing to re-run the whole
|
||||
investigation first.
|
||||
|
||||
The testing target was the deployed page:
|
||||
|
||||
```sh
|
||||
https://rover.otter.land/
|
||||
```
|
||||
|
||||
The important local perf tools are:
|
||||
|
||||
```sh
|
||||
node perf/live-cpu-profile.mjs https://rover.otter.land/ perf/results
|
||||
node perf/live-listener-audit.mjs https://rover.otter.land/ perf/results
|
||||
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=30000 node perf/live-root-runtime-audit.mjs https://rover.otter.land/ perf/results
|
||||
```
|
||||
|
||||
The strongest `/` artifacts from this investigation are:
|
||||
|
||||
- `perf/results/2026-06-11T03-15-51-398Z/report.json`
|
||||
Clean `/` profile, mobile viewport, 8x CPU throttle.
|
||||
- `perf/results/2026-06-11T03-03-31-625Z-listeners/listener-report.json`
|
||||
Listener add/remove stack audit.
|
||||
- `perf/results/2026-06-11T03-21-51-263Z-root-runtime/root-runtime-report.json`
|
||||
`/` runtime audit, mobile viewport, 6x CPU throttle, 30s sample.
|
||||
- `perf/results/2026-06-11T03-22-44-714Z-root-runtime/root-runtime-report.json`
|
||||
`/` runtime audit, mobile viewport, 6x CPU throttle, 18s sample with mutation targets.
|
||||
|
||||
## Ranked Issues
|
||||
|
||||
1. [Global input listener churn](./001-global-input-listener-churn.md)
|
||||
2. [ControlContext broad invalidation](./002-control-context-broad-invalidation.md)
|
||||
3. [High-volume socket log stream](./003-high-volume-log-stream.md)
|
||||
4. [Sensor telemetry render frequency](./004-sensor-telemetry-render-frequency.md)
|
||||
5. [ReplaySourcesPanel repeated DOM commits](./005-replay-sources-panel-churn.md)
|
||||
6. [Chat and nickname composer churn](./006-chat-and-nickname-churn.md)
|
||||
7. [Timers and polling cleanup](./007-timers-and-polling.md)
|
||||
|
||||
## Quick Read
|
||||
|
||||
The highest-confidence first fix is issue 001. It is directly measured, large, and
|
||||
structurally fixable: install keyboard/gamepad global listeners once and have them read
|
||||
latest state/actions from refs.
|
||||
|
||||
Issue 002 is the likely multiplier behind issue 001 and several UI churn symptoms. The
|
||||
current `ControlContext` value changes broadly, and many callbacks depend on `state` or
|
||||
`pipeline`; that causes consumers/effects to refresh even when the visible behavior did
|
||||
not materially change.
|
||||
|
||||
Issues 003 and 004 are independent live-data pressure: the `/` page receives a lot of
|
||||
socket traffic, especially `log:entry` and `sensorFrame`, and those streams feed UI work.
|
||||
|
||||
Issue 005 and issue 006 are component-level symptoms visible in DOM mutation probes. They
|
||||
should probably be tackled after the context/listener work, because broad invalidation may
|
||||
be causing some of their repeated commits.
|
||||
|
||||
## Important Scope Note
|
||||
|
||||
There are also `/spectate` results in `perf/results` because the investigation briefly
|
||||
compared routes. The user later clarified that `/spectate` is not the current priority.
|
||||
These issue writeups intentionally focus on `/` only.
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const url = process.argv[2] || 'https://rover.otter.land';
|
||||
const outDir = process.argv[3] || path.resolve('perf/results');
|
||||
const cpuThrottle = Number(process.env.CPU_THROTTLE || 1);
|
||||
const viewport = (process.env.VIEWPORT || '1440x950').split('x').map((part) => Number(part));
|
||||
const isMobile = process.env.MOBILE === '1';
|
||||
const runId = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const runDir = path.join(outDir, runId);
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
function summarizeCpuProfile(profile) {
|
||||
const nodes = new Map(profile.nodes.map((node) => [node.id, node]));
|
||||
const totals = new Map();
|
||||
const samples = profile.samples || [];
|
||||
const deltas = profile.timeDeltas || [];
|
||||
let totalUs = 0;
|
||||
|
||||
for (let i = 0; i < samples.length; i += 1) {
|
||||
const delta = deltas[i] || 0;
|
||||
totalUs += delta;
|
||||
const node = nodes.get(samples[i]);
|
||||
if (!node) continue;
|
||||
const frame = node.callFrame || {};
|
||||
const key = `${frame.url || '(anonymous)'}::${frame.functionName || '(anonymous)'}`;
|
||||
const prev = totals.get(key) || {
|
||||
url: frame.url || '',
|
||||
functionName: frame.functionName || '(anonymous)',
|
||||
lineNumber: frame.lineNumber,
|
||||
columnNumber: frame.columnNumber,
|
||||
selfMs: 0,
|
||||
samples: 0,
|
||||
};
|
||||
prev.selfMs += delta / 1000;
|
||||
prev.samples += 1;
|
||||
totals.set(key, prev);
|
||||
}
|
||||
|
||||
return {
|
||||
totalProfileMs: totalUs / 1000,
|
||||
sampleCount: samples.length,
|
||||
topSelfTime: [...totals.values()]
|
||||
.sort((a, b) => b.selfMs - a.selfMs)
|
||||
.slice(0, 30),
|
||||
};
|
||||
}
|
||||
|
||||
function interestingMetrics(metrics) {
|
||||
const wanted = [
|
||||
'Timestamp',
|
||||
'Documents',
|
||||
'Frames',
|
||||
'JSEventListeners',
|
||||
'Nodes',
|
||||
'LayoutCount',
|
||||
'RecalcStyleCount',
|
||||
'LayoutDuration',
|
||||
'RecalcStyleDuration',
|
||||
'ScriptDuration',
|
||||
'TaskDuration',
|
||||
'JSHeapUsedSize',
|
||||
'JSHeapTotalSize',
|
||||
];
|
||||
return Object.fromEntries(
|
||||
metrics.metrics
|
||||
.filter((metric) => wanted.includes(metric.name))
|
||||
.map((metric) => [metric.name, metric.value]),
|
||||
);
|
||||
}
|
||||
|
||||
async function performInteractions(page) {
|
||||
await page.mouse.wheel(0, 1400);
|
||||
await sleep(800);
|
||||
await page.mouse.wheel(0, -900);
|
||||
await sleep(800);
|
||||
await page.keyboard.press('Tab');
|
||||
await sleep(300);
|
||||
await page.keyboard.press('Tab');
|
||||
await sleep(300);
|
||||
|
||||
const buttons = await page.locator('button:visible').elementHandles();
|
||||
for (const button of buttons.slice(0, 3)) {
|
||||
const box = await button.boundingBox();
|
||||
if (!box || box.width < 8 || box.height < 8) continue;
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await sleep(150);
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
await fs.mkdir(runDir, { recursive: true });
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: [
|
||||
'--autoplay-policy=no-user-gesture-required',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
],
|
||||
});
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: viewport[0] || 1440, height: viewport[1] || 950 },
|
||||
deviceScaleFactor: 1,
|
||||
isMobile,
|
||||
hasTouch: isMobile,
|
||||
ignoreHTTPSErrors: true,
|
||||
});
|
||||
|
||||
await context.addInitScript(() => {
|
||||
window.__perfProbe = {
|
||||
longTasks: [],
|
||||
frames: [],
|
||||
intervalDrifts: [],
|
||||
};
|
||||
|
||||
try {
|
||||
new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
window.__perfProbe.longTasks.push({
|
||||
name: entry.name,
|
||||
startTime: entry.startTime,
|
||||
duration: entry.duration,
|
||||
});
|
||||
}
|
||||
}).observe({ type: 'longtask', buffered: true });
|
||||
} catch {}
|
||||
|
||||
let lastFrame = performance.now();
|
||||
const frameLoop = (now) => {
|
||||
window.__perfProbe.frames.push(now - lastFrame);
|
||||
lastFrame = now;
|
||||
requestAnimationFrame(frameLoop);
|
||||
};
|
||||
requestAnimationFrame(frameLoop);
|
||||
|
||||
let lastTick = performance.now();
|
||||
setInterval(() => {
|
||||
const now = performance.now();
|
||||
window.__perfProbe.intervalDrifts.push(now - lastTick - 250);
|
||||
lastTick = now;
|
||||
}, 250);
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
const consoleMessages = [];
|
||||
const failedRequests = [];
|
||||
const responses = [];
|
||||
|
||||
page.on('console', (message) => {
|
||||
consoleMessages.push({
|
||||
type: message.type(),
|
||||
text: message.text().slice(0, 1000),
|
||||
location: message.location(),
|
||||
});
|
||||
});
|
||||
page.on('requestfailed', (request) => {
|
||||
failedRequests.push({
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
failure: request.failure()?.errorText,
|
||||
});
|
||||
});
|
||||
page.on('response', (response) => {
|
||||
const status = response.status();
|
||||
if (status >= 400) responses.push({ url: response.url(), status });
|
||||
});
|
||||
|
||||
const cdp = await context.newCDPSession(page);
|
||||
await cdp.send('Performance.enable');
|
||||
await cdp.send('Profiler.enable');
|
||||
if (cpuThrottle > 1) {
|
||||
await cdp.send('Emulation.setCPUThrottlingRate', { rate: cpuThrottle });
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
await page.waitForLoadState('load', { timeout: 60000 }).catch(() => {});
|
||||
await sleep(5000);
|
||||
const afterLoadMetrics = interestingMetrics(await cdp.send('Performance.getMetrics'));
|
||||
await page.screenshot({ path: path.join(runDir, 'loaded.png'), fullPage: true });
|
||||
|
||||
await cdp.send('Profiler.start');
|
||||
await sleep(15000);
|
||||
await performInteractions(page);
|
||||
await sleep(15000);
|
||||
const { profile } = await cdp.send('Profiler.stop');
|
||||
const endMetrics = interestingMetrics(await cdp.send('Performance.getMetrics'));
|
||||
|
||||
const perfProbe = await page.evaluate(() => window.__perfProbe);
|
||||
await page.screenshot({ path: path.join(runDir, 'after-profile.png'), fullPage: true });
|
||||
|
||||
const cpuSummary = summarizeCpuProfile(profile);
|
||||
const frameDeltas = perfProbe.frames.slice(5);
|
||||
const longTasks = perfProbe.longTasks.sort((a, b) => b.duration - a.duration);
|
||||
const intervalDrifts = perfProbe.intervalDrifts;
|
||||
const percentile = (values, p) => {
|
||||
if (!values.length) return 0;
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))];
|
||||
};
|
||||
|
||||
const report = {
|
||||
url,
|
||||
runDir,
|
||||
elapsedMs: Date.now() - start,
|
||||
userAgent: await page.evaluate(() => navigator.userAgent),
|
||||
title: await page.title(),
|
||||
finalUrl: page.url(),
|
||||
environment: {
|
||||
cpuThrottle,
|
||||
viewport: { width: viewport[0] || 1440, height: viewport[1] || 950 },
|
||||
isMobile,
|
||||
},
|
||||
metrics: {
|
||||
afterLoad: afterLoadMetrics,
|
||||
end: endMetrics,
|
||||
delta: Object.fromEntries(
|
||||
Object.keys(endMetrics).map((key) => [key, endMetrics[key] - (afterLoadMetrics[key] || 0)]),
|
||||
),
|
||||
},
|
||||
cpuSummary,
|
||||
responsiveness: {
|
||||
longTaskCount: longTasks.length,
|
||||
topLongTasks: longTasks.slice(0, 20),
|
||||
frameCount: frameDeltas.length,
|
||||
averageFrameMs: frameDeltas.reduce((sum, value) => sum + value, 0) / frameDeltas.length,
|
||||
p95FrameMs: percentile(frameDeltas, 0.95),
|
||||
p99FrameMs: percentile(frameDeltas, 0.99),
|
||||
intervalDriftCount: intervalDrifts.length,
|
||||
averageIntervalDriftMs:
|
||||
intervalDrifts.reduce((sum, value) => sum + value, 0) / intervalDrifts.length,
|
||||
p95IntervalDriftMs: percentile(intervalDrifts, 0.95),
|
||||
p99IntervalDriftMs: percentile(intervalDrifts, 0.99),
|
||||
},
|
||||
consoleMessages,
|
||||
failedRequests,
|
||||
errorResponses: responses,
|
||||
};
|
||||
|
||||
await fs.writeFile(path.join(runDir, 'cpu-profile.cpuprofile'), JSON.stringify(profile));
|
||||
await fs.writeFile(path.join(runDir, 'report.json'), JSON.stringify(report, null, 2));
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const url = process.argv[2] || 'https://rover.otter.land';
|
||||
const outDir = process.argv[3] || path.resolve('perf/results');
|
||||
const runId = `${new Date().toISOString().replace(/[:.]/g, '-')}-listeners`;
|
||||
const runDir = path.join(outDir, runId);
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
async function run() {
|
||||
await fs.mkdir(runDir, { recursive: true });
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1440, height: 950 },
|
||||
ignoreHTTPSErrors: true,
|
||||
});
|
||||
|
||||
await context.addInitScript(() => {
|
||||
const originalAdd = EventTarget.prototype.addEventListener;
|
||||
const originalRemove = EventTarget.prototype.removeEventListener;
|
||||
const listenerIds = new WeakMap();
|
||||
let nextListenerId = 1;
|
||||
const rows = new Map();
|
||||
|
||||
function listenerId(listener) {
|
||||
if ((typeof listener !== 'function' && typeof listener !== 'object') || listener === null) {
|
||||
return String(listener);
|
||||
}
|
||||
let id = listenerIds.get(listener);
|
||||
if (!id) {
|
||||
id = nextListenerId++;
|
||||
listenerIds.set(listener, id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
function targetName(target) {
|
||||
if (target === window) return 'window';
|
||||
if (target === document) return 'document';
|
||||
if (target?.nodeType === Node.DOCUMENT_NODE) return 'document';
|
||||
if (target?.nodeType === Node.ELEMENT_NODE) {
|
||||
const tag = target.tagName?.toLowerCase() || 'element';
|
||||
const id = target.id ? `#${target.id}` : '';
|
||||
const className = String(target.className || '').trim().split(/\s+/).slice(0, 3).join('.');
|
||||
return `${tag}${id}${className ? `.${className}` : ''}`;
|
||||
}
|
||||
if (target?.constructor?.name) return target.constructor.name;
|
||||
return String(target);
|
||||
}
|
||||
|
||||
function shortStack() {
|
||||
return String(new Error().stack || '')
|
||||
.split('\n')
|
||||
.slice(3, 9)
|
||||
.map((line) => line.trim().replace(location.origin, ''))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function note(kind, target, type, listener) {
|
||||
const stack = shortStack();
|
||||
const key = `${targetName(target)}::${type}::${stack}`;
|
||||
const row = rows.get(key) || {
|
||||
target: targetName(target),
|
||||
type: String(type),
|
||||
stack,
|
||||
adds: 0,
|
||||
removes: 0,
|
||||
activeApprox: 0,
|
||||
firstSeen: performance.now(),
|
||||
lastSeen: 0,
|
||||
};
|
||||
row[kind] += 1;
|
||||
row.activeApprox += kind === 'adds' ? 1 : -1;
|
||||
row.lastSeen = performance.now();
|
||||
row.exampleListener = String(listener).slice(0, 180);
|
||||
rows.set(key, row);
|
||||
}
|
||||
|
||||
EventTarget.prototype.addEventListener = function patchedAdd(type, listener, options) {
|
||||
note('adds', this, type, listener);
|
||||
return originalAdd.call(this, type, listener, options);
|
||||
};
|
||||
|
||||
EventTarget.prototype.removeEventListener = function patchedRemove(type, listener, options) {
|
||||
note('removes', this, type, listener);
|
||||
return originalRemove.call(this, type, listener, options);
|
||||
};
|
||||
|
||||
window.__listenerAudit = {
|
||||
snapshot() {
|
||||
return [...rows.values()]
|
||||
.sort((a, b) => Math.abs(b.activeApprox) - Math.abs(a.activeApprox) || b.adds - a.adds)
|
||||
.slice(0, 100);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
await page.waitForLoadState('load', { timeout: 60000 }).catch(() => {});
|
||||
await sleep(10000);
|
||||
const first = await page.evaluate(() => window.__listenerAudit.snapshot());
|
||||
await page.mouse.wheel(0, 1500);
|
||||
await sleep(1000);
|
||||
await page.mouse.wheel(0, -900);
|
||||
await sleep(20000);
|
||||
const second = await page.evaluate(() => window.__listenerAudit.snapshot());
|
||||
|
||||
const report = { url, finalUrl: page.url(), first, second };
|
||||
await fs.writeFile(path.join(runDir, 'listener-report.json'), JSON.stringify(report, null, 2));
|
||||
await page.screenshot({ path: path.join(runDir, 'listener-audit.png'), fullPage: true });
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,253 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const url = process.argv[2] || 'https://rover.otter.land/';
|
||||
const outDir = process.argv[3] || path.resolve('perf/results');
|
||||
const cpuThrottle = Number(process.env.CPU_THROTTLE || 6);
|
||||
const viewport = (process.env.VIEWPORT || '390x844').split('x').map((part) => Number(part));
|
||||
const isMobile = process.env.MOBILE !== '0';
|
||||
const sampleMs = Number(process.env.SAMPLE_MS || 45000);
|
||||
const runId = `${new Date().toISOString().replace(/[:.]/g, '-')}-root-runtime`;
|
||||
const runDir = path.join(outDir, runId);
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
function percentile(values, p) {
|
||||
if (!values.length) return 0;
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))];
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await fs.mkdir(runDir, { recursive: true });
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: viewport[0] || 390, height: viewport[1] || 844 },
|
||||
deviceScaleFactor: isMobile ? 3 : 1,
|
||||
isMobile,
|
||||
hasTouch: isMobile,
|
||||
ignoreHTTPSErrors: true,
|
||||
});
|
||||
|
||||
await context.addInitScript(() => {
|
||||
const data = {
|
||||
startedAt: performance.now(),
|
||||
listeners: new Map(),
|
||||
timers: new Map(),
|
||||
timeouts: 0,
|
||||
intervals: 0,
|
||||
intervalFires: 0,
|
||||
timeoutFires: 0,
|
||||
webSocketMessages: [],
|
||||
webSocketCounts: new Map(),
|
||||
webSocketBytesByEvent: new Map(),
|
||||
mutations: [],
|
||||
mutationTargets: new Map(),
|
||||
frames: [],
|
||||
longTasks: [],
|
||||
consoleErrors: [],
|
||||
};
|
||||
|
||||
function localPercentile(values, p) {
|
||||
if (!values.length) return 0;
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))];
|
||||
}
|
||||
|
||||
function targetName(target) {
|
||||
if (target === window) return 'window';
|
||||
if (target === document) return 'document';
|
||||
if (target?.nodeType === Node.ELEMENT_NODE) {
|
||||
const tag = target.tagName?.toLowerCase() || 'element';
|
||||
const id = target.id ? `#${target.id}` : '';
|
||||
const classes = String(target.className || '').split(/\s+/).filter(Boolean).slice(0, 2).join('.');
|
||||
return `${tag}${id}${classes ? `.${classes}` : ''}`;
|
||||
}
|
||||
return target?.constructor?.name || String(target);
|
||||
}
|
||||
|
||||
function listenerKey(target, type, kind) {
|
||||
return `${kind}:${targetName(target)}:${String(type)}`;
|
||||
}
|
||||
|
||||
const originalAdd = EventTarget.prototype.addEventListener;
|
||||
const originalRemove = EventTarget.prototype.removeEventListener;
|
||||
EventTarget.prototype.addEventListener = function add(type, listener, options) {
|
||||
const key = listenerKey(this, type, 'add');
|
||||
data.listeners.set(key, (data.listeners.get(key) || 0) + 1);
|
||||
return originalAdd.call(this, type, listener, options);
|
||||
};
|
||||
EventTarget.prototype.removeEventListener = function remove(type, listener, options) {
|
||||
const key = listenerKey(this, type, 'remove');
|
||||
data.listeners.set(key, (data.listeners.get(key) || 0) + 1);
|
||||
return originalRemove.call(this, type, listener, options);
|
||||
};
|
||||
|
||||
const originalSetInterval = window.setInterval;
|
||||
const originalSetTimeout = window.setTimeout;
|
||||
window.setInterval = function patchedSetInterval(fn, delay, ...args) {
|
||||
data.intervals += 1;
|
||||
const key = `interval:${delay}`;
|
||||
data.timers.set(key, (data.timers.get(key) || 0) + 1);
|
||||
return originalSetInterval.call(this, (...innerArgs) => {
|
||||
data.intervalFires += 1;
|
||||
return typeof fn === 'function' ? fn(...innerArgs) : eval(fn);
|
||||
}, delay, ...args);
|
||||
};
|
||||
window.setTimeout = function patchedSetTimeout(fn, delay, ...args) {
|
||||
data.timeouts += 1;
|
||||
const key = `timeout:${delay}`;
|
||||
data.timers.set(key, (data.timers.get(key) || 0) + 1);
|
||||
return originalSetTimeout.call(this, (...innerArgs) => {
|
||||
data.timeoutFires += 1;
|
||||
return typeof fn === 'function' ? fn(...innerArgs) : eval(fn);
|
||||
}, delay, ...args);
|
||||
};
|
||||
|
||||
const OriginalWebSocket = window.WebSocket;
|
||||
if (OriginalWebSocket) {
|
||||
window.WebSocket = function PatchedWebSocket(...args) {
|
||||
const ws = new OriginalWebSocket(...args);
|
||||
ws.addEventListener('message', (event) => {
|
||||
const raw = event.data;
|
||||
const text = typeof raw === 'string' ? raw : '';
|
||||
const match = text.slice(0, 120).match(/\["([^"\]]+)/);
|
||||
const eventName = match?.[1] || text.slice(0, 12);
|
||||
data.webSocketCounts.set(eventName, (data.webSocketCounts.get(eventName) || 0) + 1);
|
||||
data.webSocketBytesByEvent.set(
|
||||
eventName,
|
||||
(data.webSocketBytesByEvent.get(eventName) || 0) + (typeof raw === 'string' ? raw.length : raw?.byteLength || raw?.size || 0),
|
||||
);
|
||||
data.webSocketMessages.push({
|
||||
at: Math.round(performance.now()),
|
||||
size: typeof raw === 'string' ? raw.length : raw?.byteLength || raw?.size || 0,
|
||||
prefix: text.slice(0, 80),
|
||||
});
|
||||
if (data.webSocketMessages.length > 1000) data.webSocketMessages.shift();
|
||||
});
|
||||
return ws;
|
||||
};
|
||||
window.WebSocket.prototype = OriginalWebSocket.prototype;
|
||||
Object.assign(window.WebSocket, OriginalWebSocket);
|
||||
}
|
||||
|
||||
try {
|
||||
new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
data.longTasks.push({ startTime: entry.startTime, duration: entry.duration, name: entry.name });
|
||||
}
|
||||
}).observe({ type: 'longtask', buffered: true });
|
||||
} catch {}
|
||||
|
||||
let lastFrame = performance.now();
|
||||
requestAnimationFrame(function frame(now) {
|
||||
data.frames.push(now - lastFrame);
|
||||
lastFrame = now;
|
||||
requestAnimationFrame(frame);
|
||||
});
|
||||
|
||||
new MutationObserver((records) => {
|
||||
let added = 0;
|
||||
let removed = 0;
|
||||
let attrs = 0;
|
||||
let text = 0;
|
||||
for (const record of records) {
|
||||
added += record.addedNodes?.length || 0;
|
||||
removed += record.removedNodes?.length || 0;
|
||||
if (record.type === 'attributes') attrs += 1;
|
||||
if (record.type === 'characterData') text += 1;
|
||||
if (record.type === 'attributes' || record.type === 'characterData') {
|
||||
const key = `${targetName(record.target)}:${record.type}:${record.attributeName || ''}`;
|
||||
data.mutationTargets.set(key, (data.mutationTargets.get(key) || 0) + 1);
|
||||
}
|
||||
}
|
||||
data.mutations.push({ at: Math.round(performance.now()), records: records.length, added, removed, attrs, text });
|
||||
if (data.mutations.length > 1000) data.mutations.shift();
|
||||
}).observe(document, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
});
|
||||
|
||||
const originalConsoleError = console.error;
|
||||
console.error = (...args) => {
|
||||
data.consoleErrors.push(args.map((arg) => String(arg)).join(' ').slice(0, 500));
|
||||
return originalConsoleError.apply(console, args);
|
||||
};
|
||||
|
||||
window.__rootAudit = {
|
||||
snapshot() {
|
||||
const frames = data.frames.slice(5);
|
||||
const messages = data.webSocketMessages;
|
||||
const socketBuckets = new Map(
|
||||
[...data.webSocketCounts.entries()].map(([event, count]) => [
|
||||
event,
|
||||
{ event, count, bytes: data.webSocketBytesByEvent.get(event) || 0 },
|
||||
]),
|
||||
);
|
||||
return {
|
||||
elapsedMs: performance.now() - data.startedAt,
|
||||
listeners: [...data.listeners.entries()].map(([key, count]) => ({ key, count })).sort((a, b) => b.count - a.count).slice(0, 40),
|
||||
timers: [...data.timers.entries()].map(([key, count]) => ({ key, count })).sort((a, b) => b.count - a.count).slice(0, 40),
|
||||
timeouts: data.timeouts,
|
||||
intervals: data.intervals,
|
||||
timeoutFires: data.timeoutFires,
|
||||
intervalFires: data.intervalFires,
|
||||
webSocketMessageCount: messages.length,
|
||||
webSocketBytes: messages.reduce((sum, message) => sum + message.size, 0),
|
||||
webSocketEvents: [...socketBuckets.values()].sort((a, b) => b.count - a.count),
|
||||
mutationsTotal: data.mutations.reduce((acc, row) => ({
|
||||
records: acc.records + row.records,
|
||||
added: acc.added + row.added,
|
||||
removed: acc.removed + row.removed,
|
||||
attrs: acc.attrs + row.attrs,
|
||||
text: acc.text + row.text,
|
||||
}), { records: 0, added: 0, removed: 0, attrs: 0, text: 0 }),
|
||||
mutationTargets: [...data.mutationTargets.entries()]
|
||||
.map(([key, count]) => ({ key, count }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 50),
|
||||
mutationBursts: [...data.mutations].sort((a, b) => b.records - a.records).slice(0, 20),
|
||||
frames: {
|
||||
count: frames.length,
|
||||
average: frames.reduce((sum, value) => sum + value, 0) / frames.length,
|
||||
p95: localPercentile(frames, 0.95),
|
||||
p99: localPercentile(frames, 0.99),
|
||||
},
|
||||
longTasks: {
|
||||
count: data.longTasks.length,
|
||||
top: [...data.longTasks].sort((a, b) => b.duration - a.duration).slice(0, 20),
|
||||
},
|
||||
consoleErrors: data.consoleErrors,
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
const cdp = await context.newCDPSession(page);
|
||||
await cdp.send('Performance.enable');
|
||||
if (cpuThrottle > 1) await cdp.send('Emulation.setCPUThrottlingRate', { rate: cpuThrottle });
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 90000 });
|
||||
await page.waitForLoadState('load', { timeout: 90000 }).catch(() => {});
|
||||
await sleep(sampleMs);
|
||||
const metrics = await cdp.send('Performance.getMetrics');
|
||||
const snapshot = await page.evaluate(() => window.__rootAudit.snapshot());
|
||||
const report = {
|
||||
url,
|
||||
runDir,
|
||||
environment: { cpuThrottle, viewport: { width: viewport[0] || 390, height: viewport[1] || 844 }, isMobile, sampleMs },
|
||||
metrics: Object.fromEntries(metrics.metrics.map((metric) => [metric.name, metric.value])),
|
||||
snapshot,
|
||||
};
|
||||
await page.screenshot({ path: path.join(runDir, 'root-runtime.png'), fullPage: true });
|
||||
await fs.writeFile(path.join(runDir, 'root-runtime-report.json'), JSON.stringify(report, null, 2));
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,284 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const baseUrl = process.argv[2] || 'https://rover.otter.land';
|
||||
const outDir = process.argv[3] || path.resolve('perf/results');
|
||||
const cpuThrottle = Number(process.env.CPU_THROTTLE || 6);
|
||||
const viewport = (process.env.VIEWPORT || '390x844').split('x').map((part) => Number(part));
|
||||
const isMobile = process.env.MOBILE !== '0';
|
||||
const sampleMs = Number(process.env.SAMPLE_MS || 22000);
|
||||
const routes = (process.env.ROUTES || '/,/spectate,/mini,/display,/scanner')
|
||||
.split(',')
|
||||
.map((route) => route.trim())
|
||||
.filter(Boolean);
|
||||
const runId = `${new Date().toISOString().replace(/[:.]/g, '-')}-route-sweep`;
|
||||
const runDir = path.join(outDir, runId);
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
function percentile(values, p) {
|
||||
if (!values.length) return 0;
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))];
|
||||
}
|
||||
|
||||
function interestingMetrics(metrics) {
|
||||
const wanted = [
|
||||
'Documents',
|
||||
'JSEventListeners',
|
||||
'Nodes',
|
||||
'LayoutCount',
|
||||
'RecalcStyleCount',
|
||||
'LayoutDuration',
|
||||
'RecalcStyleDuration',
|
||||
'ScriptDuration',
|
||||
'TaskDuration',
|
||||
'JSHeapUsedSize',
|
||||
'JSHeapTotalSize',
|
||||
];
|
||||
return Object.fromEntries(
|
||||
metrics.metrics
|
||||
.filter((metric) => wanted.includes(metric.name))
|
||||
.map((metric) => [metric.name, metric.value]),
|
||||
);
|
||||
}
|
||||
|
||||
function summarizeCpuProfile(profile) {
|
||||
const nodes = new Map(profile.nodes.map((node) => [node.id, node]));
|
||||
const totals = new Map();
|
||||
let totalUs = 0;
|
||||
for (let i = 0; i < (profile.samples || []).length; i += 1) {
|
||||
const delta = profile.timeDeltas?.[i] || 0;
|
||||
totalUs += delta;
|
||||
const frame = nodes.get(profile.samples[i])?.callFrame || {};
|
||||
const key = `${frame.url || '(browser)'}::${frame.functionName || '(anonymous)'}:${frame.lineNumber}:${frame.columnNumber}`;
|
||||
const row = totals.get(key) || {
|
||||
url: frame.url || '',
|
||||
functionName: frame.functionName || '(anonymous)',
|
||||
lineNumber: frame.lineNumber,
|
||||
columnNumber: frame.columnNumber,
|
||||
selfMs: 0,
|
||||
samples: 0,
|
||||
};
|
||||
row.selfMs += delta / 1000;
|
||||
row.samples += 1;
|
||||
totals.set(key, row);
|
||||
}
|
||||
return {
|
||||
totalProfileMs: totalUs / 1000,
|
||||
topSelfTime: [...totals.values()].sort((a, b) => b.selfMs - a.selfMs).slice(0, 15),
|
||||
};
|
||||
}
|
||||
|
||||
async function installProbe(context) {
|
||||
await context.addInitScript(() => {
|
||||
const originalAdd = EventTarget.prototype.addEventListener;
|
||||
const originalRemove = EventTarget.prototype.removeEventListener;
|
||||
const listenerRows = new Map();
|
||||
|
||||
function targetName(target) {
|
||||
if (target === window) return 'window';
|
||||
if (target === document) return 'document';
|
||||
if (target?.nodeType === Node.ELEMENT_NODE) {
|
||||
const tag = target.tagName?.toLowerCase() || 'element';
|
||||
return target.id ? `${tag}#${target.id}` : tag;
|
||||
}
|
||||
return target?.constructor?.name || String(target);
|
||||
}
|
||||
|
||||
function stack() {
|
||||
return String(new Error().stack || '')
|
||||
.split('\n')
|
||||
.slice(3, 7)
|
||||
.map((line) => line.trim().replace(location.origin, ''))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function note(kind, target, type) {
|
||||
const key = `${targetName(target)}::${type}::${stack()}`;
|
||||
const row = listenerRows.get(key) || {
|
||||
target: targetName(target),
|
||||
type: String(type),
|
||||
stack: stack(),
|
||||
adds: 0,
|
||||
removes: 0,
|
||||
};
|
||||
row[kind] += 1;
|
||||
listenerRows.set(key, row);
|
||||
}
|
||||
|
||||
EventTarget.prototype.addEventListener = function patchedAdd(type, listener, options) {
|
||||
note('adds', this, type);
|
||||
return originalAdd.call(this, type, listener, options);
|
||||
};
|
||||
EventTarget.prototype.removeEventListener = function patchedRemove(type, listener, options) {
|
||||
note('removes', this, type);
|
||||
return originalRemove.call(this, type, listener, options);
|
||||
};
|
||||
|
||||
window.__sweepProbe = {
|
||||
longTasks: [],
|
||||
frames: [],
|
||||
intervalDrifts: [],
|
||||
listenerSnapshot() {
|
||||
return [...listenerRows.values()]
|
||||
.map((row) => ({ ...row, churn: row.adds + row.removes }))
|
||||
.sort((a, b) => b.churn - a.churn)
|
||||
.slice(0, 20);
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
window.__sweepProbe.longTasks.push({
|
||||
name: entry.name,
|
||||
startTime: entry.startTime,
|
||||
duration: entry.duration,
|
||||
});
|
||||
}
|
||||
}).observe({ type: 'longtask', buffered: true });
|
||||
} catch {}
|
||||
|
||||
let lastFrame = performance.now();
|
||||
const frameLoop = (now) => {
|
||||
window.__sweepProbe.frames.push(now - lastFrame);
|
||||
lastFrame = now;
|
||||
requestAnimationFrame(frameLoop);
|
||||
};
|
||||
requestAnimationFrame(frameLoop);
|
||||
|
||||
let lastTick = performance.now();
|
||||
setInterval(() => {
|
||||
const now = performance.now();
|
||||
window.__sweepProbe.intervalDrifts.push(now - lastTick - 250);
|
||||
lastTick = now;
|
||||
}, 250);
|
||||
});
|
||||
}
|
||||
|
||||
async function runRoute(browser, route) {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: viewport[0] || 390, height: viewport[1] || 844 },
|
||||
deviceScaleFactor: isMobile ? 3 : 1,
|
||||
isMobile,
|
||||
hasTouch: isMobile,
|
||||
ignoreHTTPSErrors: true,
|
||||
});
|
||||
await installProbe(context);
|
||||
const page = await context.newPage();
|
||||
const cdp = await context.newCDPSession(page);
|
||||
await cdp.send('Performance.enable');
|
||||
await cdp.send('Profiler.enable');
|
||||
if (cpuThrottle > 1) {
|
||||
await cdp.send('Emulation.setCPUThrottlingRate', { rate: cpuThrottle });
|
||||
}
|
||||
|
||||
const consoleErrors = [];
|
||||
const errorResponses = [];
|
||||
page.on('console', (message) => {
|
||||
if (['error', 'warning'].includes(message.type())) {
|
||||
consoleErrors.push({ type: message.type(), text: message.text().slice(0, 500) });
|
||||
}
|
||||
});
|
||||
page.on('response', (response) => {
|
||||
if (response.status() >= 400) {
|
||||
errorResponses.push({ url: response.url(), status: response.status() });
|
||||
}
|
||||
});
|
||||
|
||||
const url = new URL(route, baseUrl).toString();
|
||||
const startWall = Date.now();
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 90000 });
|
||||
await page.waitForLoadState('load', { timeout: 90000 }).catch(() => {});
|
||||
await sleep(4000);
|
||||
const startMetrics = interestingMetrics(await cdp.send('Performance.getMetrics'));
|
||||
await cdp.send('Profiler.start');
|
||||
await sleep(sampleMs / 2);
|
||||
await page.mouse.wheel(0, 900).catch(() => {});
|
||||
await sleep(sampleMs / 2);
|
||||
const { profile } = await cdp.send('Profiler.stop');
|
||||
const endMetrics = interestingMetrics(await cdp.send('Performance.getMetrics'));
|
||||
const probe = await page.evaluate(() => ({
|
||||
longTasks: window.__sweepProbe.longTasks,
|
||||
frames: window.__sweepProbe.frames.slice(5),
|
||||
intervalDrifts: window.__sweepProbe.intervalDrifts,
|
||||
listeners: window.__sweepProbe.listenerSnapshot(),
|
||||
}));
|
||||
const screenshotName = route === '/' ? 'root' : route.replace(/^\//, '').replace(/\W+/g, '-');
|
||||
await page.screenshot({ path: path.join(runDir, `${screenshotName || 'root'}.png`), fullPage: true });
|
||||
await context.close();
|
||||
|
||||
const delta = Object.fromEntries(
|
||||
Object.keys(endMetrics).map((key) => [key, endMetrics[key] - (startMetrics[key] || 0)]),
|
||||
);
|
||||
const frames = probe.frames;
|
||||
const longTasks = probe.longTasks.sort((a, b) => b.duration - a.duration);
|
||||
return {
|
||||
route,
|
||||
finalUrl: page.url(),
|
||||
elapsedMs: Date.now() - startWall,
|
||||
metrics: { start: startMetrics, end: endMetrics, delta },
|
||||
cpu: summarizeCpuProfile(profile),
|
||||
responsiveness: {
|
||||
longTaskCount: longTasks.length,
|
||||
worstLongTaskMs: longTasks[0]?.duration || 0,
|
||||
topLongTasks: longTasks.slice(0, 8),
|
||||
frameCount: frames.length,
|
||||
averageFrameMs: frames.reduce((sum, value) => sum + value, 0) / frames.length,
|
||||
p95FrameMs: percentile(frames, 0.95),
|
||||
p99FrameMs: percentile(frames, 0.99),
|
||||
intervalDriftP95Ms: percentile(probe.intervalDrifts, 0.95),
|
||||
intervalDriftP99Ms: percentile(probe.intervalDrifts, 0.99),
|
||||
},
|
||||
listenerChurn: probe.listeners,
|
||||
consoleErrors,
|
||||
errorResponses,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await fs.mkdir(runDir, { recursive: true });
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: ['--autoplay-policy=no-user-gesture-required'],
|
||||
});
|
||||
const results = [];
|
||||
for (const route of routes) {
|
||||
console.error(`profiling ${route} at ${cpuThrottle}x`);
|
||||
results.push(await runRoute(browser, route));
|
||||
}
|
||||
await browser.close();
|
||||
|
||||
const report = {
|
||||
baseUrl,
|
||||
runDir,
|
||||
environment: {
|
||||
cpuThrottle,
|
||||
viewport: { width: viewport[0] || 390, height: viewport[1] || 844 },
|
||||
isMobile,
|
||||
sampleMs,
|
||||
routes,
|
||||
},
|
||||
results,
|
||||
rankedByAverageFrame: [...results].sort(
|
||||
(a, b) => b.responsiveness.averageFrameMs - a.responsiveness.averageFrameMs,
|
||||
),
|
||||
rankedByTaskDuration: [...results].sort(
|
||||
(a, b) => b.metrics.delta.TaskDuration - a.metrics.delta.TaskDuration,
|
||||
),
|
||||
rankedByListenerChurn: [...results].sort((a, b) => {
|
||||
const churnA = a.listenerChurn.reduce((sum, row) => sum + row.churn, 0);
|
||||
const churnB = b.listenerChurn.reduce((sum, row) => sum + row.churn, 0);
|
||||
return churnB - churnA;
|
||||
}),
|
||||
};
|
||||
await fs.writeFile(path.join(runDir, 'route-sweep-report.json'), JSON.stringify(report, null, 2));
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"playwright": "^1.60.0"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 395 KiB |
|
After Width: | Height: | Size: 390 KiB |
@@ -0,0 +1,334 @@
|
||||
{
|
||||
"url": "https://rover.otter.land",
|
||||
"runDir": "perf/results/2026-06-11T03-02-07-015Z",
|
||||
"elapsedMs": 38584,
|
||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.7778.96 Safari/537.36",
|
||||
"title": "Roomba Rover",
|
||||
"finalUrl": "https://rover.otter.land/",
|
||||
"metrics": {
|
||||
"afterLoad": {
|
||||
"Timestamp": 38634.714754,
|
||||
"Documents": 1,
|
||||
"Frames": 1,
|
||||
"JSEventListeners": 66899,
|
||||
"Nodes": 2977,
|
||||
"LayoutCount": 89,
|
||||
"RecalcStyleCount": 274,
|
||||
"LayoutDuration": 0.106766,
|
||||
"RecalcStyleDuration": 0.065778,
|
||||
"ScriptDuration": 4.06233,
|
||||
"TaskDuration": 4.933287,
|
||||
"JSHeapUsedSize": 35455032,
|
||||
"JSHeapTotalSize": 80216064
|
||||
},
|
||||
"end": {
|
||||
"Timestamp": 38667.823166,
|
||||
"Documents": 1,
|
||||
"Frames": 1,
|
||||
"JSEventListeners": 75714,
|
||||
"Nodes": 2955,
|
||||
"LayoutCount": 654,
|
||||
"RecalcStyleCount": 2517,
|
||||
"LayoutDuration": 0.32419,
|
||||
"RecalcStyleDuration": 0.312982,
|
||||
"ScriptDuration": 32.06036,
|
||||
"TaskDuration": 37.597822,
|
||||
"JSHeapUsedSize": 39685520,
|
||||
"JSHeapTotalSize": 86081536
|
||||
},
|
||||
"delta": {
|
||||
"Timestamp": 33.10841200000141,
|
||||
"Documents": 0,
|
||||
"Frames": 0,
|
||||
"JSEventListeners": 8815,
|
||||
"Nodes": -22,
|
||||
"LayoutCount": 565,
|
||||
"RecalcStyleCount": 2243,
|
||||
"LayoutDuration": 0.21742399999999998,
|
||||
"RecalcStyleDuration": 0.24720399999999998,
|
||||
"ScriptDuration": 27.998030000000004,
|
||||
"TaskDuration": 32.664535,
|
||||
"JSHeapUsedSize": 4230488,
|
||||
"JSHeapTotalSize": 5865472
|
||||
}
|
||||
},
|
||||
"cpuSummary": {
|
||||
"totalProfileMs": 32896.945,
|
||||
"sampleCount": 30557,
|
||||
"topSelfTime": [
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "(program)",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 4657.831000000009,
|
||||
"samples": 4273
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "C9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 94304,
|
||||
"selfMs": 2470.519000000005,
|
||||
"samples": 2297
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "ad",
|
||||
"lineNumber": 9,
|
||||
"columnNumber": 7898,
|
||||
"selfMs": 2383.6760000000036,
|
||||
"samples": 2219
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "removeEventListener",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 1596.3859999999981,
|
||||
"samples": 1476
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "addEventListener",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 1314.546999999998,
|
||||
"samples": 1219
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "T9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 115769,
|
||||
"selfMs": 1280.09,
|
||||
"samples": 1195
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Q",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 39681,
|
||||
"selfMs": 1246.5730000000015,
|
||||
"samples": 1160
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "va",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 10653,
|
||||
"selfMs": 1170.771,
|
||||
"samples": 1088
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Fl",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 101199,
|
||||
"selfMs": 1028.4150000000006,
|
||||
"samples": 954
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "n2",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 42234,
|
||||
"selfMs": 1007.8280000000008,
|
||||
"samples": 938
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "w1",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 40723,
|
||||
"selfMs": 992.6080000000019,
|
||||
"samples": 927
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "zl",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 80827,
|
||||
"selfMs": 975.6390000000019,
|
||||
"samples": 905
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "v9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 71024,
|
||||
"selfMs": 909.7280000000015,
|
||||
"samples": 848
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "(anonymous)",
|
||||
"lineNumber": 17,
|
||||
"columnNumber": 5982,
|
||||
"selfMs": 710.5740000000005,
|
||||
"samples": 662
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Eh",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 95724,
|
||||
"selfMs": 656.2710000000002,
|
||||
"samples": 613
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "w4",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 91309,
|
||||
"selfMs": 544.7980000000002,
|
||||
"samples": 508
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Al",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 97450,
|
||||
"selfMs": 485.71200000000005,
|
||||
"samples": 453
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "kc",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 1388916,
|
||||
"selfMs": 482.39700000000016,
|
||||
"samples": 450
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "B6",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 34145,
|
||||
"selfMs": 475.1790000000002,
|
||||
"samples": 446
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "o1",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 70367,
|
||||
"selfMs": 441.8229999999999,
|
||||
"samples": 412
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "lt",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 7349,
|
||||
"selfMs": 398.3480000000001,
|
||||
"samples": 369
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "r",
|
||||
"lineNumber": 1,
|
||||
"columnNumber": 967,
|
||||
"selfMs": 348.187,
|
||||
"samples": 326
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "(idle)",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 319.7700000000001,
|
||||
"samples": 297
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "X",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 40176,
|
||||
"selfMs": 283.33400000000006,
|
||||
"samples": 264
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "a2",
|
||||
"lineNumber": 9,
|
||||
"columnNumber": 71,
|
||||
"selfMs": 258.0969999999999,
|
||||
"samples": 239
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "n9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 56257,
|
||||
"selfMs": 216.19699999999995,
|
||||
"samples": 202
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "(garbage collector)",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 214.13299999999995,
|
||||
"samples": 195
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "t9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 50605,
|
||||
"selfMs": 207.45599999999985,
|
||||
"samples": 194
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "now",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 202.39299999999992,
|
||||
"samples": 189
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "po",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 214243,
|
||||
"selfMs": 192.35299999999984,
|
||||
"samples": 178
|
||||
}
|
||||
]
|
||||
},
|
||||
"responsiveness": {
|
||||
"longTaskCount": 1,
|
||||
"topLongTasks": [
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 5330.30000000447,
|
||||
"duration": 83
|
||||
}
|
||||
],
|
||||
"frameCount": 2261,
|
||||
"averageFrameMs": 16.83551525873507,
|
||||
"p95FrameMs": 16.799999999999272,
|
||||
"p99FrameMs": 16.80000000000291,
|
||||
"intervalDriftCount": 152,
|
||||
"averageIntervalDriftMs": 0.02039473680289168,
|
||||
"p95IntervalDriftMs": 4.600000001490116,
|
||||
"p99IntervalDriftMs": 23.399999998509884
|
||||
},
|
||||
"consoleMessages": [
|
||||
{
|
||||
"type": "info",
|
||||
"text": "[socket] connecting to https://rover.otter.land",
|
||||
"location": {
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"line": 11,
|
||||
"column": 63768,
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 63768
|
||||
}
|
||||
}
|
||||
],
|
||||
"failedRequests": [],
|
||||
"errorResponses": []
|
||||
}
|
||||
|
After Width: | Height: | Size: 402 KiB |
|
After Width: | Height: | Size: 328 KiB |
|
After Width: | Height: | Size: 360 KiB |
@@ -0,0 +1,423 @@
|
||||
{
|
||||
"url": "https://rover.otter.land",
|
||||
"runDir": "perf/results/2026-06-11T03-04-44-940Z",
|
||||
"elapsedMs": 41919,
|
||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.7778.96 Safari/537.36",
|
||||
"title": "Roomba Rover",
|
||||
"finalUrl": "https://rover.otter.land/",
|
||||
"environment": {
|
||||
"cpuThrottle": 4,
|
||||
"viewport": {
|
||||
"width": 1366,
|
||||
"height": 768
|
||||
},
|
||||
"isMobile": false
|
||||
},
|
||||
"metrics": {
|
||||
"afterLoad": {
|
||||
"Timestamp": 38792.763829,
|
||||
"Documents": 1,
|
||||
"Frames": 1,
|
||||
"JSEventListeners": 6177,
|
||||
"Nodes": 2848,
|
||||
"LayoutCount": 38,
|
||||
"RecalcStyleCount": 43,
|
||||
"LayoutDuration": 0.233029,
|
||||
"RecalcStyleDuration": 0.13828,
|
||||
"ScriptDuration": 3.172814,
|
||||
"TaskDuration": 4.881969,
|
||||
"JSHeapUsedSize": 17107224,
|
||||
"JSHeapTotalSize": 28311552
|
||||
},
|
||||
"end": {
|
||||
"Timestamp": 38828.522855,
|
||||
"Documents": 1,
|
||||
"Frames": 1,
|
||||
"JSEventListeners": 3175,
|
||||
"Nodes": 3046,
|
||||
"LayoutCount": 767,
|
||||
"RecalcStyleCount": 773,
|
||||
"LayoutDuration": 1.403177,
|
||||
"RecalcStyleDuration": 0.88412,
|
||||
"ScriptDuration": 24.747626,
|
||||
"TaskDuration": 39.759494,
|
||||
"JSHeapUsedSize": 41718720,
|
||||
"JSHeapTotalSize": 81625088
|
||||
},
|
||||
"delta": {
|
||||
"Timestamp": 35.75902599999972,
|
||||
"Documents": 0,
|
||||
"Frames": 0,
|
||||
"JSEventListeners": -3002,
|
||||
"Nodes": 198,
|
||||
"LayoutCount": 729,
|
||||
"RecalcStyleCount": 730,
|
||||
"LayoutDuration": 1.170148,
|
||||
"RecalcStyleDuration": 0.7458400000000001,
|
||||
"ScriptDuration": 21.574812,
|
||||
"TaskDuration": 34.877525,
|
||||
"JSHeapUsedSize": 24611496,
|
||||
"JSHeapTotalSize": 53313536
|
||||
}
|
||||
},
|
||||
"cpuSummary": {
|
||||
"totalProfileMs": 34677.553,
|
||||
"sampleCount": 31910,
|
||||
"topSelfTime": [
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "(program)",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 12142.741999999993,
|
||||
"samples": 10968
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "C9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 94304,
|
||||
"selfMs": 1976.3040000000042,
|
||||
"samples": 1842
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "ad",
|
||||
"lineNumber": 9,
|
||||
"columnNumber": 7898,
|
||||
"selfMs": 1545.372000000002,
|
||||
"samples": 1431
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "(anonymous)",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 1532780,
|
||||
"selfMs": 974.0629999999992,
|
||||
"samples": 908
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "v9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 71024,
|
||||
"selfMs": 844.4459999999995,
|
||||
"samples": 805
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Q",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 39681,
|
||||
"selfMs": 841.3410000000006,
|
||||
"samples": 763
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Fl",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 101199,
|
||||
"selfMs": 826.49,
|
||||
"samples": 777
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "w1",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 40723,
|
||||
"selfMs": 811.0100000000001,
|
||||
"samples": 754
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "removeEventListener",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 811.0059999999984,
|
||||
"samples": 723
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "va",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 10653,
|
||||
"selfMs": 752.6890000000008,
|
||||
"samples": 698
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "(idle)",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 664.0250000000005,
|
||||
"samples": 625
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "zl",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 80827,
|
||||
"selfMs": 596.2229999999997,
|
||||
"samples": 556
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "addEventListener",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 573.276,
|
||||
"samples": 544
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Eh",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 95724,
|
||||
"selfMs": 515.44,
|
||||
"samples": 477
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Kl",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 117211,
|
||||
"selfMs": 499.10000000000025,
|
||||
"samples": 470
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Al",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 97450,
|
||||
"selfMs": 497.787,
|
||||
"samples": 453
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "n2",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 42234,
|
||||
"selfMs": 408.08700000000005,
|
||||
"samples": 381
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "o1",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 70367,
|
||||
"selfMs": 381.6320000000001,
|
||||
"samples": 359
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "r",
|
||||
"lineNumber": 1,
|
||||
"columnNumber": 967,
|
||||
"selfMs": 345.4999999999999,
|
||||
"samples": 328
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Oh",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 116246,
|
||||
"selfMs": 334.56799999999987,
|
||||
"samples": 313
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "a2",
|
||||
"lineNumber": 9,
|
||||
"columnNumber": 71,
|
||||
"selfMs": 314.36699999999996,
|
||||
"samples": 293
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "lt",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 7349,
|
||||
"selfMs": 313.014,
|
||||
"samples": 286
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Z4",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 28258,
|
||||
"selfMs": 290.6389999999998,
|
||||
"samples": 261
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "(garbage collector)",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 236.61700000000016,
|
||||
"samples": 210
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "B6",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 34145,
|
||||
"selfMs": 229.56900000000002,
|
||||
"samples": 213
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "t9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 50605,
|
||||
"selfMs": 208.06900000000002,
|
||||
"samples": 191
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "Blob",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 179.771,
|
||||
"samples": 158
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "w4",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 91309,
|
||||
"selfMs": 147.32200000000006,
|
||||
"samples": 132
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "n9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 56257,
|
||||
"selfMs": 135.37199999999999,
|
||||
"samples": 125
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "ma",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 10401,
|
||||
"selfMs": 129.19600000000003,
|
||||
"samples": 118
|
||||
}
|
||||
]
|
||||
},
|
||||
"responsiveness": {
|
||||
"longTaskCount": 14,
|
||||
"topLongTasks": [
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 193.60000000149012,
|
||||
"duration": 184
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 381.20000000298023,
|
||||
"duration": 137
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 23610.70000000298,
|
||||
"duration": 131
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 1045,
|
||||
"duration": 130
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 1972.2999999970198,
|
||||
"duration": 107
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 6042.70000000298,
|
||||
"duration": 105
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 1874.3999999985099,
|
||||
"duration": 97
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 2697.7000000029802,
|
||||
"duration": 96
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 518.2999999970198,
|
||||
"duration": 81
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 2618.5,
|
||||
"duration": 79
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 839.3999999985099,
|
||||
"duration": 75
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 974.1000000014901,
|
||||
"duration": 70
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 770.7999999970198,
|
||||
"duration": 68
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 39623.79999999702,
|
||||
"duration": 54
|
||||
}
|
||||
],
|
||||
"frameCount": 577,
|
||||
"averageFrameMs": 69.98561525129982,
|
||||
"p95FrameMs": 133.29999999999927,
|
||||
"p99FrameMs": 166.70000000000027,
|
||||
"intervalDriftCount": 159,
|
||||
"averageIntervalDriftMs": 6.808805031455913,
|
||||
"p95IntervalDriftMs": 83.19999999552965,
|
||||
"p99IntervalDriftMs": 239.80000000447035
|
||||
},
|
||||
"consoleMessages": [
|
||||
{
|
||||
"type": "info",
|
||||
"text": "[socket] connecting to https://rover.otter.land",
|
||||
"location": {
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"line": 11,
|
||||
"column": 63768,
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 63768
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"text": "Failed to load resource: the server responded with a status of 404 ()",
|
||||
"location": {
|
||||
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
|
||||
"line": 0,
|
||||
"column": 0,
|
||||
"lineNumber": 0,
|
||||
"columnNumber": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"failedRequests": [],
|
||||
"errorResponses": [
|
||||
{
|
||||
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
|
||||
"status": 404
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 226 KiB |
@@ -0,0 +1,403 @@
|
||||
{
|
||||
"url": "https://rover.otter.land",
|
||||
"runDir": "perf/results/2026-06-11T03-05-35-042Z",
|
||||
"elapsedMs": 41711,
|
||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.7778.96 Safari/537.36",
|
||||
"title": "Roomba Rover",
|
||||
"finalUrl": "https://rover.otter.land/",
|
||||
"environment": {
|
||||
"cpuThrottle": 4,
|
||||
"viewport": {
|
||||
"width": 390,
|
||||
"height": 844
|
||||
},
|
||||
"isMobile": true
|
||||
},
|
||||
"metrics": {
|
||||
"afterLoad": {
|
||||
"Timestamp": 38842.912408,
|
||||
"Documents": 3,
|
||||
"Frames": 1,
|
||||
"JSEventListeners": 5426,
|
||||
"Nodes": 2393,
|
||||
"LayoutCount": 100,
|
||||
"RecalcStyleCount": 106,
|
||||
"LayoutDuration": 0.207151,
|
||||
"RecalcStyleDuration": 0.170546,
|
||||
"ScriptDuration": 3.283922,
|
||||
"TaskDuration": 5.024573,
|
||||
"JSHeapUsedSize": 12044476,
|
||||
"JSHeapTotalSize": 27000832
|
||||
},
|
||||
"end": {
|
||||
"Timestamp": 38878.342222,
|
||||
"Documents": 1,
|
||||
"Frames": 1,
|
||||
"JSEventListeners": 40107,
|
||||
"Nodes": 2536,
|
||||
"LayoutCount": 1291,
|
||||
"RecalcStyleCount": 1289,
|
||||
"LayoutDuration": 1.04779,
|
||||
"RecalcStyleDuration": 1.180594,
|
||||
"ScriptDuration": 24.454767,
|
||||
"TaskDuration": 39.786697,
|
||||
"JSHeapUsedSize": 17640640,
|
||||
"JSHeapTotalSize": 82411520
|
||||
},
|
||||
"delta": {
|
||||
"Timestamp": 35.42981400000281,
|
||||
"Documents": -2,
|
||||
"Frames": 0,
|
||||
"JSEventListeners": 34681,
|
||||
"Nodes": 143,
|
||||
"LayoutCount": 1191,
|
||||
"RecalcStyleCount": 1183,
|
||||
"LayoutDuration": 0.840639,
|
||||
"RecalcStyleDuration": 1.0100479999999998,
|
||||
"ScriptDuration": 21.170845,
|
||||
"TaskDuration": 34.762124,
|
||||
"JSHeapUsedSize": 5596164,
|
||||
"JSHeapTotalSize": 55410688
|
||||
}
|
||||
},
|
||||
"cpuSummary": {
|
||||
"totalProfileMs": 34204.39,
|
||||
"sampleCount": 31429,
|
||||
"topSelfTime": [
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "(program)",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 12727.823999999955,
|
||||
"samples": 11515
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "C9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 94304,
|
||||
"selfMs": 1738.6469999999997,
|
||||
"samples": 1612
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "ad",
|
||||
"lineNumber": 9,
|
||||
"columnNumber": 7898,
|
||||
"selfMs": 1547.8840000000005,
|
||||
"samples": 1425
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "removeEventListener",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 1048.3049999999994,
|
||||
"samples": 922
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Fl",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 101199,
|
||||
"selfMs": 1013.1350000000006,
|
||||
"samples": 958
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "w1",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 40723,
|
||||
"selfMs": 846.9869999999997,
|
||||
"samples": 777
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "va",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 10653,
|
||||
"selfMs": 826.692,
|
||||
"samples": 756
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Q",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 39681,
|
||||
"selfMs": 813.2879999999994,
|
||||
"samples": 744
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "v9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 71024,
|
||||
"selfMs": 764.6159999999996,
|
||||
"samples": 716
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "addEventListener",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 711.6550000000001,
|
||||
"samples": 655
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "(anonymous)",
|
||||
"lineNumber": 17,
|
||||
"columnNumber": 9774,
|
||||
"selfMs": 643.1059999999997,
|
||||
"samples": 593
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Eh",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 95724,
|
||||
"selfMs": 609.4899999999997,
|
||||
"samples": 573
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "zl",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 80827,
|
||||
"selfMs": 558.8860000000001,
|
||||
"samples": 525
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Kl",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 117211,
|
||||
"selfMs": 555.4119999999999,
|
||||
"samples": 516
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "(idle)",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 476.40799999999973,
|
||||
"samples": 445
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Al",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 97450,
|
||||
"selfMs": 392.31999999999965,
|
||||
"samples": 400
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "kc",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 1388916,
|
||||
"selfMs": 363.94399999999945,
|
||||
"samples": 336
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "n2",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 42234,
|
||||
"selfMs": 356.98299999999995,
|
||||
"samples": 334
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Z4",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 28258,
|
||||
"selfMs": 322.32900000000024,
|
||||
"samples": 293
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "lt",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 7349,
|
||||
"selfMs": 291.17899999999986,
|
||||
"samples": 268
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "o1",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 70367,
|
||||
"selfMs": 269.2040000000001,
|
||||
"samples": 257
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "B6",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 34145,
|
||||
"selfMs": 248.9579999999999,
|
||||
"samples": 228
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "r",
|
||||
"lineNumber": 1,
|
||||
"columnNumber": 967,
|
||||
"selfMs": 246.97999999999996,
|
||||
"samples": 223
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Oh",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 116246,
|
||||
"selfMs": 239.94800000000004,
|
||||
"samples": 223
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "a2",
|
||||
"lineNumber": 9,
|
||||
"columnNumber": 71,
|
||||
"selfMs": 219.92599999999987,
|
||||
"samples": 209
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "t9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 50605,
|
||||
"selfMs": 170.78300000000004,
|
||||
"samples": 157
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "N7",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 100487,
|
||||
"selfMs": 150.48000000000005,
|
||||
"samples": 144
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "w4",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 91309,
|
||||
"selfMs": 144.64899999999994,
|
||||
"samples": 138
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Td.k1.createElement",
|
||||
"lineNumber": 1,
|
||||
"columnNumber": 6938,
|
||||
"selfMs": 138.19599999999997,
|
||||
"samples": 132
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "n9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 56257,
|
||||
"selfMs": 135.858,
|
||||
"samples": 128
|
||||
}
|
||||
]
|
||||
},
|
||||
"responsiveness": {
|
||||
"longTaskCount": 10,
|
||||
"topLongTasks": [
|
||||
{
|
||||
"name": "unknown",
|
||||
"startTime": 5777.89999999851,
|
||||
"duration": 234
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 185,
|
||||
"duration": 188
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 23461.59999999404,
|
||||
"duration": 179
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 957.7999999970198,
|
||||
"duration": 111
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 376.59999999403954,
|
||||
"duration": 106
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 1511.7999999970198,
|
||||
"duration": 83
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 696.7999999970198,
|
||||
"duration": 73
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 770.7999999970198,
|
||||
"duration": 69
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 482.59999999403954,
|
||||
"duration": 67
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 892.6999999955297,
|
||||
"duration": 64
|
||||
}
|
||||
],
|
||||
"frameCount": 1031,
|
||||
"averageFrameMs": 38.92502424830261,
|
||||
"p95FrameMs": 66.70000000000073,
|
||||
"p99FrameMs": 133.3000000000029,
|
||||
"intervalDriftCount": 157,
|
||||
"averageIntervalDriftMs": 8.188535031856626,
|
||||
"p95IntervalDriftMs": 62.100000001490116,
|
||||
"p99IntervalDriftMs": 431.09999999403954
|
||||
},
|
||||
"consoleMessages": [
|
||||
{
|
||||
"type": "info",
|
||||
"text": "[socket] connecting to https://rover.otter.land",
|
||||
"location": {
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"line": 11,
|
||||
"column": 63768,
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 63768
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"text": "Failed to load resource: the server responded with a status of 404 ()",
|
||||
"location": {
|
||||
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
|
||||
"line": 0,
|
||||
"column": 0,
|
||||
"lineNumber": 0,
|
||||
"columnNumber": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"failedRequests": [],
|
||||
"errorResponses": [
|
||||
{
|
||||
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
|
||||
"status": 404
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 604 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 5.8 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,527 @@
|
||||
{
|
||||
"url": "https://rover.otter.land/spectate",
|
||||
"runDir": "perf/results/2026-06-11T03-14-34-168Z",
|
||||
"elapsedMs": 64190,
|
||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.7778.96 Safari/537.36",
|
||||
"title": "Roomba Rover",
|
||||
"finalUrl": "https://rover.otter.land/spectate",
|
||||
"environment": {
|
||||
"cpuThrottle": 8,
|
||||
"viewport": {
|
||||
"width": 390,
|
||||
"height": 844
|
||||
},
|
||||
"isMobile": true
|
||||
},
|
||||
"metrics": {
|
||||
"afterLoad": {
|
||||
"Timestamp": 39382.268457,
|
||||
"Documents": 3,
|
||||
"Frames": 1,
|
||||
"JSEventListeners": 494,
|
||||
"Nodes": 8431,
|
||||
"LayoutCount": 16,
|
||||
"RecalcStyleCount": 18,
|
||||
"LayoutDuration": 0.410692,
|
||||
"RecalcStyleDuration": 0.442447,
|
||||
"ScriptDuration": 2.378027,
|
||||
"TaskDuration": 5.043961,
|
||||
"JSHeapUsedSize": 14710548,
|
||||
"JSHeapTotalSize": 20447232
|
||||
},
|
||||
"end": {
|
||||
"Timestamp": 39435.115434,
|
||||
"Documents": 1,
|
||||
"Frames": 1,
|
||||
"JSEventListeners": 509,
|
||||
"Nodes": 12246,
|
||||
"LayoutCount": 532,
|
||||
"RecalcStyleCount": 540,
|
||||
"LayoutDuration": 2.892458,
|
||||
"RecalcStyleDuration": 6.325277,
|
||||
"ScriptDuration": 21.960492,
|
||||
"TaskDuration": 54.170961,
|
||||
"JSHeapUsedSize": 14649044,
|
||||
"JSHeapTotalSize": 65634304
|
||||
},
|
||||
"delta": {
|
||||
"Timestamp": 52.84697700000106,
|
||||
"Documents": -2,
|
||||
"Frames": 0,
|
||||
"JSEventListeners": 15,
|
||||
"Nodes": 3815,
|
||||
"LayoutCount": 516,
|
||||
"RecalcStyleCount": 522,
|
||||
"LayoutDuration": 2.481766,
|
||||
"RecalcStyleDuration": 5.88283,
|
||||
"ScriptDuration": 19.582465,
|
||||
"TaskDuration": 49.126999999999995,
|
||||
"JSHeapUsedSize": -61504,
|
||||
"JSHeapTotalSize": 45187072
|
||||
}
|
||||
},
|
||||
"cpuSummary": {
|
||||
"totalProfileMs": 41344.156,
|
||||
"sampleCount": 31576,
|
||||
"topSelfTime": [
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "(program)",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 21236.87599999999,
|
||||
"samples": 16120
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "(idle)",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 2981.150999999998,
|
||||
"samples": 2232
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "(anonymous)",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 64833,
|
||||
"selfMs": 1365.8530000000014,
|
||||
"samples": 1053
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "C9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 94304,
|
||||
"selfMs": 1234.6300000000003,
|
||||
"samples": 936
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "ad",
|
||||
"lineNumber": 9,
|
||||
"columnNumber": 7898,
|
||||
"selfMs": 842.8760000000004,
|
||||
"samples": 664
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Fl",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 101199,
|
||||
"selfMs": 674.3050000000004,
|
||||
"samples": 527
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "w1",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 40723,
|
||||
"selfMs": 621.5390000000002,
|
||||
"samples": 475
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Q",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 39681,
|
||||
"selfMs": 505.4100000000001,
|
||||
"samples": 396
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "Blob",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 479.50199999999967,
|
||||
"samples": 331
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Eh",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 95724,
|
||||
"selfMs": 423.5530000000003,
|
||||
"samples": 343
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "decodeString",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 48609,
|
||||
"selfMs": 423.2989999999997,
|
||||
"samples": 328
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "B6",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 34145,
|
||||
"selfMs": 404.793,
|
||||
"samples": 311
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Kl",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 117211,
|
||||
"selfMs": 398.8710000000001,
|
||||
"samples": 291
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "setAttribute",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 384.0259999999998,
|
||||
"samples": 319
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "zl",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 80827,
|
||||
"selfMs": 372.5609999999999,
|
||||
"samples": 298
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Al",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 97450,
|
||||
"selfMs": 370.4030000000001,
|
||||
"samples": 280
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "n2",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 42234,
|
||||
"selfMs": 356.20099999999996,
|
||||
"samples": 280
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Kt",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 48095,
|
||||
"selfMs": 303.89100000000013,
|
||||
"samples": 222
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "va",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 10653,
|
||||
"selfMs": 289.63100000000003,
|
||||
"samples": 227
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "(garbage collector)",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 257.03600000000006,
|
||||
"samples": 183
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "createObjectURL",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 223.09499999999994,
|
||||
"samples": 181
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "r",
|
||||
"lineNumber": 1,
|
||||
"columnNumber": 967,
|
||||
"selfMs": 212.97800000000012,
|
||||
"samples": 168
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "w4",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 91309,
|
||||
"selfMs": 202.98600000000016,
|
||||
"samples": 154
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "removeChild",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 197.55400000000006,
|
||||
"samples": 144
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "v9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 71024,
|
||||
"selfMs": 175.75600000000009,
|
||||
"samples": 132
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "X",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 40176,
|
||||
"selfMs": 166.36100000000005,
|
||||
"samples": 136
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "o1",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 70367,
|
||||
"selfMs": 162.70300000000003,
|
||||
"samples": 132
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "V",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 38470,
|
||||
"selfMs": 159.72000000000003,
|
||||
"samples": 124
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Oh",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 116246,
|
||||
"selfMs": 151.22299999999993,
|
||||
"samples": 109
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "V8",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 1487297,
|
||||
"selfMs": 147.01899999999992,
|
||||
"samples": 113
|
||||
}
|
||||
]
|
||||
},
|
||||
"responsiveness": {
|
||||
"longTaskCount": 322,
|
||||
"topLongTasks": [
|
||||
{
|
||||
"name": "unknown",
|
||||
"startTime": 12297.80000000447,
|
||||
"duration": 518
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 250.39999999850988,
|
||||
"duration": 401
|
||||
},
|
||||
{
|
||||
"name": "unknown",
|
||||
"startTime": 12929.20000000298,
|
||||
"duration": 308
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 1907.3999999985099,
|
||||
"duration": 274
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 36069.80000000447,
|
||||
"duration": 230
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 2530.89999999851,
|
||||
"duration": 195
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 54969,
|
||||
"duration": 190
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 55840.80000000447,
|
||||
"duration": 189
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 24437.39999999851,
|
||||
"duration": 188
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 47105.5,
|
||||
"duration": 172
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 1736,
|
||||
"duration": 170
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 2367.60000000149,
|
||||
"duration": 163
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 1320.3000000044703,
|
||||
"duration": 161
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 659.6000000014901,
|
||||
"duration": 159
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 30711.60000000149,
|
||||
"duration": 146
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 54631.5,
|
||||
"duration": 146
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 3079,
|
||||
"duration": 139
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 819.2000000029802,
|
||||
"duration": 136
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 42828.70000000298,
|
||||
"duration": 132
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 1482.2000000029802,
|
||||
"duration": 129
|
||||
}
|
||||
],
|
||||
"frameCount": 460,
|
||||
"averageFrameMs": 125.46608695652174,
|
||||
"p95FrameMs": 233.39999999999782,
|
||||
"p99FrameMs": 433.3000000000011,
|
||||
"intervalDriftCount": 78,
|
||||
"averageIntervalDriftMs": 502.9153846153655,
|
||||
"p95IntervalDriftMs": 1530.1999999955297,
|
||||
"p99IntervalDriftMs": 2415.60000000149
|
||||
},
|
||||
"consoleMessages": [
|
||||
{
|
||||
"type": "info",
|
||||
"text": "[socket] connecting to https://rover.otter.land",
|
||||
"location": {
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"line": 11,
|
||||
"column": 63768,
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 63768
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"text": "Failed to load resource: the server responded with a status of 404 ()",
|
||||
"location": {
|
||||
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
|
||||
"line": 0,
|
||||
"column": 0,
|
||||
"lineNumber": 0,
|
||||
"columnNumber": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"text": "Failed to load resource: the server responded with a status of 404 ()",
|
||||
"location": {
|
||||
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
|
||||
"line": 0,
|
||||
"column": 0,
|
||||
"lineNumber": 0,
|
||||
"columnNumber": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"text": "Failed to enter spectator mode Error: Spectator role required\n at lo.<anonymous> (https://rover.otter.land/assets/index-l3JqdlyF.js:12:64487)\n at lo.onack (https://rover.otter.land/assets/index-l3JqdlyF.js:12:55937)\n at lo.onpacket (https://rover.otter.land/assets/index-l3JqdlyF.js:12:55148)\n at V2.emit (https://rover.otter.land/assets/index-l3JqdlyF.js:12:25579)\n at https://rover.otter.land/assets/index-l3JqdlyF.js:12:61163",
|
||||
"location": {
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"line": 125,
|
||||
"column": 14862,
|
||||
"lineNumber": 125,
|
||||
"columnNumber": 14862
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"text": "Failed to enter spectator mode Error: Spectator role required\n at lo.<anonymous> (https://rover.otter.land/assets/index-l3JqdlyF.js:12:64487)\n at lo.onack (https://rover.otter.land/assets/index-l3JqdlyF.js:12:55937)\n at lo.onpacket (https://rover.otter.land/assets/index-l3JqdlyF.js:12:55148)\n at V2.emit (https://rover.otter.land/assets/index-l3JqdlyF.js:12:25579)\n at https://rover.otter.land/assets/index-l3JqdlyF.js:12:61163",
|
||||
"location": {
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"line": 125,
|
||||
"column": 14862,
|
||||
"lineNumber": 125,
|
||||
"columnNumber": 14862
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"text": "Failed to load resource: the server responded with a status of 404 ()",
|
||||
"location": {
|
||||
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
|
||||
"line": 0,
|
||||
"column": 0,
|
||||
"lineNumber": 0,
|
||||
"columnNumber": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"text": "Failed to enter spectator mode Error: Spectator role required\n at lo.<anonymous> (https://rover.otter.land/assets/index-l3JqdlyF.js:12:64487)\n at lo.onack (https://rover.otter.land/assets/index-l3JqdlyF.js:12:55937)\n at lo.onpacket (https://rover.otter.land/assets/index-l3JqdlyF.js:12:55148)\n at V2.emit (https://rover.otter.land/assets/index-l3JqdlyF.js:12:25579)\n at https://rover.otter.land/assets/index-l3JqdlyF.js:12:61163",
|
||||
"location": {
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"line": 125,
|
||||
"column": 14862,
|
||||
"lineNumber": 125,
|
||||
"columnNumber": 14862
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"text": "Failed to enter spectator mode Error: Spectator role required\n at lo.<anonymous> (https://rover.otter.land/assets/index-l3JqdlyF.js:12:64487)\n at lo.onack (https://rover.otter.land/assets/index-l3JqdlyF.js:12:55937)\n at lo.onpacket (https://rover.otter.land/assets/index-l3JqdlyF.js:12:55148)\n at V2.emit (https://rover.otter.land/assets/index-l3JqdlyF.js:12:25579)\n at https://rover.otter.land/assets/index-l3JqdlyF.js:12:61163",
|
||||
"location": {
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"line": 125,
|
||||
"column": 14862,
|
||||
"lineNumber": 125,
|
||||
"columnNumber": 14862
|
||||
}
|
||||
}
|
||||
],
|
||||
"failedRequests": [],
|
||||
"errorResponses": [
|
||||
{
|
||||
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
|
||||
"status": 404
|
||||
},
|
||||
{
|
||||
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
|
||||
"status": 404
|
||||
},
|
||||
{
|
||||
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
|
||||
"status": 404
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 221 KiB |
@@ -0,0 +1,483 @@
|
||||
{
|
||||
"url": "https://rover.otter.land/",
|
||||
"runDir": "perf/results/2026-06-11T03-15-51-398Z",
|
||||
"elapsedMs": 68429,
|
||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.7778.96 Safari/537.36",
|
||||
"title": "Roomba Rover",
|
||||
"finalUrl": "https://rover.otter.land/",
|
||||
"environment": {
|
||||
"cpuThrottle": 8,
|
||||
"viewport": {
|
||||
"width": 390,
|
||||
"height": 844
|
||||
},
|
||||
"isMobile": true
|
||||
},
|
||||
"metrics": {
|
||||
"afterLoad": {
|
||||
"Timestamp": 39459.436441,
|
||||
"Documents": 1,
|
||||
"Frames": 1,
|
||||
"JSEventListeners": 964,
|
||||
"Nodes": 2332,
|
||||
"LayoutCount": 17,
|
||||
"RecalcStyleCount": 19,
|
||||
"LayoutDuration": 0.26351,
|
||||
"RecalcStyleDuration": 0.176918,
|
||||
"ScriptDuration": 3.214383,
|
||||
"TaskDuration": 5.084233,
|
||||
"JSHeapUsedSize": 8964616,
|
||||
"JSHeapTotalSize": 20971520
|
||||
},
|
||||
"end": {
|
||||
"Timestamp": 39514.079576,
|
||||
"Documents": 1,
|
||||
"Frames": 1,
|
||||
"JSEventListeners": 14445,
|
||||
"Nodes": 2446,
|
||||
"LayoutCount": 390,
|
||||
"RecalcStyleCount": 565,
|
||||
"LayoutDuration": 1.338259,
|
||||
"RecalcStyleDuration": 1.340215,
|
||||
"ScriptDuration": 39.06709,
|
||||
"TaskDuration": 57.967635,
|
||||
"JSHeapUsedSize": 32831868,
|
||||
"JSHeapTotalSize": 80576512
|
||||
},
|
||||
"delta": {
|
||||
"Timestamp": 54.64313499999844,
|
||||
"Documents": 0,
|
||||
"Frames": 0,
|
||||
"JSEventListeners": 13481,
|
||||
"Nodes": 114,
|
||||
"LayoutCount": 373,
|
||||
"RecalcStyleCount": 546,
|
||||
"LayoutDuration": 1.0747490000000002,
|
||||
"RecalcStyleDuration": 1.163297,
|
||||
"ScriptDuration": 35.852707,
|
||||
"TaskDuration": 52.883402000000004,
|
||||
"JSHeapUsedSize": 23867252,
|
||||
"JSHeapTotalSize": 59604992
|
||||
}
|
||||
},
|
||||
"cpuSummary": {
|
||||
"totalProfileMs": 42872.299,
|
||||
"sampleCount": 32614,
|
||||
"topSelfTime": [
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "(program)",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 11770.136000000031,
|
||||
"samples": 8654
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "C9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 94304,
|
||||
"selfMs": 2390.596999999998,
|
||||
"samples": 1844
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "ad",
|
||||
"lineNumber": 9,
|
||||
"columnNumber": 7898,
|
||||
"selfMs": 2379.0989999999983,
|
||||
"samples": 1867
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "(anonymous)",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 129074,
|
||||
"selfMs": 1738.0870000000016,
|
||||
"samples": 1347
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Fl",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 101199,
|
||||
"selfMs": 1436.2390000000005,
|
||||
"samples": 1104
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "removeEventListener",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 1298.9879999999996,
|
||||
"samples": 915
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "(idle)",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 1226.9839999999986,
|
||||
"samples": 919
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "v9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 71024,
|
||||
"selfMs": 1131.7309999999986,
|
||||
"samples": 880
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Q",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 39681,
|
||||
"selfMs": 1085.1430000000012,
|
||||
"samples": 833
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "va",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 10653,
|
||||
"selfMs": 1036.2670000000012,
|
||||
"samples": 808
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "w1",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 40723,
|
||||
"selfMs": 1021.851000000001,
|
||||
"samples": 794
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "addEventListener",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 935.4199999999993,
|
||||
"samples": 733
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "zl",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 80827,
|
||||
"selfMs": 898.5989999999993,
|
||||
"samples": 684
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Eh",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 95724,
|
||||
"selfMs": 875.6580000000007,
|
||||
"samples": 685
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Al",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 97450,
|
||||
"selfMs": 826.9259999999988,
|
||||
"samples": 630
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Kl",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 117211,
|
||||
"selfMs": 710.8159999999998,
|
||||
"samples": 538
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "n2",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 42234,
|
||||
"selfMs": 586.3330000000001,
|
||||
"samples": 449
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "kc",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 1388916,
|
||||
"selfMs": 431.253,
|
||||
"samples": 337
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Z4",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 28258,
|
||||
"selfMs": 429.435,
|
||||
"samples": 323
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "o1",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 70367,
|
||||
"selfMs": 426.5330000000003,
|
||||
"samples": 321
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "Oh",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 116246,
|
||||
"selfMs": 376.3850000000001,
|
||||
"samples": 289
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "decodeString",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 48609,
|
||||
"selfMs": 349.87800000000004,
|
||||
"samples": 266
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "B6",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 34145,
|
||||
"selfMs": 310.59499999999986,
|
||||
"samples": 239
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "r",
|
||||
"lineNumber": 1,
|
||||
"columnNumber": 967,
|
||||
"selfMs": 293.97700000000003,
|
||||
"samples": 230
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "(garbage collector)",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 260.9860000000001,
|
||||
"samples": 178
|
||||
},
|
||||
{
|
||||
"url": "",
|
||||
"functionName": "setAttribute",
|
||||
"lineNumber": -1,
|
||||
"columnNumber": -1,
|
||||
"selfMs": 234.46500000000006,
|
||||
"samples": 165
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "n9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 56257,
|
||||
"selfMs": 221.65700000000007,
|
||||
"samples": 163
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "t9",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 50605,
|
||||
"selfMs": 209.57700000000006,
|
||||
"samples": 175
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "ma",
|
||||
"lineNumber": 8,
|
||||
"columnNumber": 10401,
|
||||
"selfMs": 205.84599999999998,
|
||||
"samples": 161
|
||||
},
|
||||
{
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"functionName": "w4",
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 91309,
|
||||
"selfMs": 181.84999999999997,
|
||||
"samples": 140
|
||||
}
|
||||
]
|
||||
},
|
||||
"responsiveness": {
|
||||
"longTaskCount": 67,
|
||||
"topLongTasks": [
|
||||
{
|
||||
"name": "unknown",
|
||||
"startTime": 12851.70000000298,
|
||||
"duration": 419
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 224.70000000298023,
|
||||
"duration": 369
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 1742.9000000059605,
|
||||
"duration": 229
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 601.4000000059605,
|
||||
"duration": 205
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 3460.2000000029802,
|
||||
"duration": 203
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 36108.80000000447,
|
||||
"duration": 202
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 3289.8000000044703,
|
||||
"duration": 170
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 1209.8000000044703,
|
||||
"duration": 143
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 1605.5,
|
||||
"duration": 137
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 1353.3000000044703,
|
||||
"duration": 123
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 4036.60000000149,
|
||||
"duration": 110
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 31583.70000000298,
|
||||
"duration": 107
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 807.1000000014901,
|
||||
"duration": 106
|
||||
},
|
||||
{
|
||||
"name": "unknown",
|
||||
"startTime": 13343,
|
||||
"duration": 100
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 24534.20000000298,
|
||||
"duration": 100
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 30488.30000000447,
|
||||
"duration": 97
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 21806.40000000596,
|
||||
"duration": 89
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 55545.5,
|
||||
"duration": 87
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 36721.70000000298,
|
||||
"duration": 82
|
||||
},
|
||||
{
|
||||
"name": "self",
|
||||
"startTime": 44980.20000000298,
|
||||
"duration": 79
|
||||
}
|
||||
],
|
||||
"frameCount": 427,
|
||||
"averageFrameMs": 140.080093676815,
|
||||
"p95FrameMs": 166.79999999999563,
|
||||
"p99FrameMs": 383.4000000000001,
|
||||
"intervalDriftCount": 58,
|
||||
"averageIntervalDriftMs": 790.1465517241379,
|
||||
"p95IntervalDriftMs": 1347.1999999955297,
|
||||
"p99IntervalDriftMs": 1620.7999999970198
|
||||
},
|
||||
"consoleMessages": [
|
||||
{
|
||||
"type": "info",
|
||||
"text": "[socket] connecting to https://rover.otter.land",
|
||||
"location": {
|
||||
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
|
||||
"line": 11,
|
||||
"column": 63768,
|
||||
"lineNumber": 11,
|
||||
"columnNumber": 63768
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"text": "Failed to load resource: the server responded with a status of 404 ()",
|
||||
"location": {
|
||||
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
|
||||
"line": 0,
|
||||
"column": 0,
|
||||
"lineNumber": 0,
|
||||
"columnNumber": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"text": "Failed to load resource: the server responded with a status of 404 ()",
|
||||
"location": {
|
||||
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
|
||||
"line": 0,
|
||||
"column": 0,
|
||||
"lineNumber": 0,
|
||||
"columnNumber": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"text": "Failed to load resource: the server responded with a status of 404 ()",
|
||||
"location": {
|
||||
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
|
||||
"line": 0,
|
||||
"column": 0,
|
||||
"lineNumber": 0,
|
||||
"columnNumber": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"failedRequests": [],
|
||||
"errorResponses": [
|
||||
{
|
||||
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
|
||||
"status": 404
|
||||
},
|
||||
{
|
||||
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
|
||||
"status": 404
|
||||
},
|
||||
{
|
||||
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
|
||||
"status": 404
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,581 @@
|
||||
{
|
||||
"url": "https://rover.otter.land/",
|
||||
"runDir": "perf/results/2026-06-11T03-21-51-263Z-root-runtime",
|
||||
"environment": {
|
||||
"cpuThrottle": 6,
|
||||
"viewport": {
|
||||
"width": 390,
|
||||
"height": 844
|
||||
},
|
||||
"isMobile": true,
|
||||
"sampleMs": 30000
|
||||
},
|
||||
"metrics": {
|
||||
"Timestamp": 39844.241947,
|
||||
"AudioHandlers": 0,
|
||||
"AudioWorkletProcessors": 0,
|
||||
"Documents": 1,
|
||||
"Frames": 1,
|
||||
"JSEventListeners": 15871,
|
||||
"LayoutObjects": 1938,
|
||||
"MediaKeySessions": 0,
|
||||
"MediaKeys": 0,
|
||||
"Nodes": 2430,
|
||||
"Resources": 13,
|
||||
"ContextLifecycleStateObservers": 12,
|
||||
"V8PerContextDatas": 2,
|
||||
"WorkerGlobalScopes": 0,
|
||||
"UACSSResources": 0,
|
||||
"RTCPeerConnections": 2,
|
||||
"ResourceFetchers": 1,
|
||||
"AdSubframes": 0,
|
||||
"DetachedScriptStates": 2,
|
||||
"ArrayBufferContents": 3,
|
||||
"LayoutCount": 297,
|
||||
"RecalcStyleCount": 320,
|
||||
"LayoutDuration": 0.596739,
|
||||
"RecalcStyleDuration": 0.63084,
|
||||
"DevToolsCommandDuration": 0.000137,
|
||||
"ScriptDuration": 20.110942,
|
||||
"V8CompileDuration": 0,
|
||||
"TaskDuration": 29.506741,
|
||||
"TaskOtherDuration": 8.168083,
|
||||
"ThreadTime": 30.245205,
|
||||
"ProcessTime": 35.2,
|
||||
"JSHeapUsedSize": 18751188,
|
||||
"JSHeapTotalSize": 45088768,
|
||||
"FirstMeaningfulPaint": 0,
|
||||
"DomContentLoaded": 39814.213839,
|
||||
"NavigationStart": 39813.701422
|
||||
},
|
||||
"snapshot": {
|
||||
"elapsedMs": 30586.79999999702,
|
||||
"listeners": [
|
||||
{
|
||||
"key": "add:window:keydown",
|
||||
"count": 770
|
||||
},
|
||||
{
|
||||
"key": "add:window:keyup",
|
||||
"count": 770
|
||||
},
|
||||
{
|
||||
"key": "add:window:blur",
|
||||
"count": 770
|
||||
},
|
||||
{
|
||||
"key": "add:window:gamepadconnected",
|
||||
"count": 770
|
||||
},
|
||||
{
|
||||
"key": "add:window:gamepaddisconnected",
|
||||
"count": 770
|
||||
},
|
||||
{
|
||||
"key": "remove:window:keydown",
|
||||
"count": 769
|
||||
},
|
||||
{
|
||||
"key": "remove:window:keyup",
|
||||
"count": 769
|
||||
},
|
||||
{
|
||||
"key": "remove:window:blur",
|
||||
"count": 769
|
||||
},
|
||||
{
|
||||
"key": "remove:window:gamepadconnected",
|
||||
"count": 769
|
||||
},
|
||||
{
|
||||
"key": "remove:window:gamepaddisconnected",
|
||||
"count": 769
|
||||
},
|
||||
{
|
||||
"key": "add:input:invalid",
|
||||
"count": 18
|
||||
},
|
||||
{
|
||||
"key": "add:window:online",
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"key": "add:document:visibilitychange",
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"key": "remove:window:online",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "remove:document:visibilitychange",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "add:audio:canplay",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "add:audio:ended",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "add:audio:error",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "add:audio:pause",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "add:audio:play",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "add:audio:stalled",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "add:audio:waiting",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:auxclick",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:click",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:contextmenu",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:copy",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:cut",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:drag",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:dragend",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:dragenter",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:dragexit",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:dragleave",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:dragover",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:dragstart",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:drop",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:gotpointercapture",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:input",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:keydown",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:keypress",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:keyup",
|
||||
"count": 2
|
||||
}
|
||||
],
|
||||
"timers": [
|
||||
{
|
||||
"key": "interval:200",
|
||||
"count": 12
|
||||
},
|
||||
{
|
||||
"key": "timeout:12000",
|
||||
"count": 6
|
||||
},
|
||||
{
|
||||
"key": "timeout:0",
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"key": "interval:250",
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"key": "interval:3000",
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"key": "interval:60000",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "timeout:15000",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "interval:1000",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "interval:2000",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "timeout:4000",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "timeout:10000",
|
||||
"count": 1
|
||||
}
|
||||
],
|
||||
"timeouts": 13,
|
||||
"intervals": 25,
|
||||
"timeoutFires": 6,
|
||||
"intervalFires": 593,
|
||||
"webSocketMessageCount": 1000,
|
||||
"webSocketBytes": 797561,
|
||||
"webSocketEvents": [
|
||||
{
|
||||
"event": "log:entry",
|
||||
"count": 526,
|
||||
"bytes": 111535
|
||||
},
|
||||
{
|
||||
"event": "sensorFrame",
|
||||
"count": 306,
|
||||
"bytes": 574616
|
||||
},
|
||||
{
|
||||
"event": "commandAck",
|
||||
"count": 145,
|
||||
"bytes": 13778
|
||||
},
|
||||
{
|
||||
"event": "chat:typing",
|
||||
"count": 7,
|
||||
"bytes": 3073
|
||||
},
|
||||
{
|
||||
"event": "session:sync",
|
||||
"count": 5,
|
||||
"bytes": 87372
|
||||
},
|
||||
{
|
||||
"event": "chat:message",
|
||||
"count": 4,
|
||||
"bytes": 4115
|
||||
},
|
||||
{
|
||||
"event": "roverHostStats",
|
||||
"count": 4,
|
||||
"bytes": 3069
|
||||
},
|
||||
{
|
||||
"event": "2",
|
||||
"count": 3,
|
||||
"bytes": 3
|
||||
}
|
||||
],
|
||||
"mutationsTotal": {
|
||||
"records": 41062,
|
||||
"added": 229,
|
||||
"removed": 35,
|
||||
"attrs": 40761,
|
||||
"text": 37
|
||||
},
|
||||
"mutationBursts": [
|
||||
{
|
||||
"at": 1534,
|
||||
"records": 164,
|
||||
"added": 105,
|
||||
"removed": 1,
|
||||
"attrs": 58,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 3007,
|
||||
"records": 93,
|
||||
"added": 13,
|
||||
"removed": 3,
|
||||
"attrs": 68,
|
||||
"text": 9
|
||||
},
|
||||
{
|
||||
"at": 1212,
|
||||
"records": 73,
|
||||
"added": 22,
|
||||
"removed": 5,
|
||||
"attrs": 42,
|
||||
"text": 4
|
||||
},
|
||||
{
|
||||
"at": 6080,
|
||||
"records": 72,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 72,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 7837,
|
||||
"records": 72,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 72,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 8493,
|
||||
"records": 72,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 72,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 16038,
|
||||
"records": 72,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 72,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 26549,
|
||||
"records": 72,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 72,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 10855,
|
||||
"records": 71,
|
||||
"added": 1,
|
||||
"removed": 0,
|
||||
"attrs": 70,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 4339,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 6195,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 6225,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 9779,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 15605,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 15797,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 15862,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 18303,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 18367,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 20816,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 20907,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
}
|
||||
],
|
||||
"frames": {
|
||||
"count": 338,
|
||||
"average": 88.21153846153847,
|
||||
"p95": 150,
|
||||
"p99": 183.29999999999927
|
||||
},
|
||||
"longTasks": {
|
||||
"count": 18,
|
||||
"top": [
|
||||
{
|
||||
"startTime": 228,
|
||||
"duration": 287,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 520.5,
|
||||
"duration": 226,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 1534.1000000014901,
|
||||
"duration": 140,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 2490,
|
||||
"duration": 136,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 1080,
|
||||
"duration": 132,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 1212.3000000044703,
|
||||
"duration": 124,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 1434.8999999985099,
|
||||
"duration": 99,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 747.1000000014901,
|
||||
"duration": 85,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 2928.60000000149,
|
||||
"duration": 78,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 11610.30000000447,
|
||||
"duration": 72,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 4531.10000000149,
|
||||
"duration": 62,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 19226.5,
|
||||
"duration": 62,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 6920.70000000298,
|
||||
"duration": 54,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 7968.39999999851,
|
||||
"duration": 52,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 3745.60000000149,
|
||||
"duration": 51,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 12822.20000000298,
|
||||
"duration": 51,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 8143.70000000298,
|
||||
"duration": 50,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 10881.5,
|
||||
"duration": 50,
|
||||
"name": "self"
|
||||
}
|
||||
]
|
||||
},
|
||||
"consoleErrors": []
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 923 KiB |
@@ -0,0 +1,843 @@
|
||||
{
|
||||
"url": "https://rover.otter.land/",
|
||||
"runDir": "perf/results/2026-06-11T03-22-44-714Z-root-runtime",
|
||||
"environment": {
|
||||
"cpuThrottle": 6,
|
||||
"viewport": {
|
||||
"width": 390,
|
||||
"height": 844
|
||||
},
|
||||
"isMobile": true,
|
||||
"sampleMs": 18000
|
||||
},
|
||||
"metrics": {
|
||||
"Timestamp": 39885.73698,
|
||||
"AudioHandlers": 0,
|
||||
"AudioWorkletProcessors": 0,
|
||||
"Documents": 1,
|
||||
"Frames": 1,
|
||||
"JSEventListeners": 8530,
|
||||
"LayoutObjects": 1864,
|
||||
"MediaKeySessions": 0,
|
||||
"MediaKeys": 0,
|
||||
"Nodes": 2379,
|
||||
"Resources": 12,
|
||||
"ContextLifecycleStateObservers": 12,
|
||||
"V8PerContextDatas": 2,
|
||||
"WorkerGlobalScopes": 0,
|
||||
"UACSSResources": 0,
|
||||
"RTCPeerConnections": 2,
|
||||
"ResourceFetchers": 1,
|
||||
"AdSubframes": 0,
|
||||
"DetachedScriptStates": 2,
|
||||
"ArrayBufferContents": 8,
|
||||
"LayoutCount": 116,
|
||||
"RecalcStyleCount": 120,
|
||||
"LayoutDuration": 0.379961,
|
||||
"RecalcStyleDuration": 0.361646,
|
||||
"DevToolsCommandDuration": 0.000126,
|
||||
"ScriptDuration": 11.946269,
|
||||
"V8CompileDuration": 0,
|
||||
"TaskDuration": 17.591645,
|
||||
"TaskOtherDuration": 4.903643,
|
||||
"ThreadTime": 18.399532,
|
||||
"ProcessTime": 20.97,
|
||||
"JSHeapUsedSize": 24567940,
|
||||
"JSHeapTotalSize": 44564480,
|
||||
"FirstMeaningfulPaint": 0,
|
||||
"DomContentLoaded": 39867.72907,
|
||||
"NavigationStart": 39867.192605
|
||||
},
|
||||
"snapshot": {
|
||||
"elapsedMs": 18737.20000000298,
|
||||
"listeners": [
|
||||
{
|
||||
"key": "add:window:keydown",
|
||||
"count": 334
|
||||
},
|
||||
{
|
||||
"key": "add:window:keyup",
|
||||
"count": 334
|
||||
},
|
||||
{
|
||||
"key": "add:window:blur",
|
||||
"count": 334
|
||||
},
|
||||
{
|
||||
"key": "add:window:gamepadconnected",
|
||||
"count": 334
|
||||
},
|
||||
{
|
||||
"key": "add:window:gamepaddisconnected",
|
||||
"count": 334
|
||||
},
|
||||
{
|
||||
"key": "remove:window:keydown",
|
||||
"count": 333
|
||||
},
|
||||
{
|
||||
"key": "remove:window:keyup",
|
||||
"count": 333
|
||||
},
|
||||
{
|
||||
"key": "remove:window:blur",
|
||||
"count": 333
|
||||
},
|
||||
{
|
||||
"key": "remove:window:gamepadconnected",
|
||||
"count": 333
|
||||
},
|
||||
{
|
||||
"key": "remove:window:gamepaddisconnected",
|
||||
"count": 333
|
||||
},
|
||||
{
|
||||
"key": "add:input:invalid",
|
||||
"count": 18
|
||||
},
|
||||
{
|
||||
"key": "add:window:online",
|
||||
"count": 5
|
||||
},
|
||||
{
|
||||
"key": "add:document:visibilitychange",
|
||||
"count": 5
|
||||
},
|
||||
{
|
||||
"key": "remove:window:online",
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"key": "remove:document:visibilitychange",
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"key": "add:audio:canplay",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "add:audio:ended",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "add:audio:error",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "add:audio:pause",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "add:audio:play",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "add:audio:stalled",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "add:audio:waiting",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:auxclick",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:click",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:contextmenu",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:copy",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:cut",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:drag",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:dragend",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:dragenter",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:dragexit",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:dragleave",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:dragover",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:dragstart",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:drop",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:gotpointercapture",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:input",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:keydown",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:keypress",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "add:div#root:keyup",
|
||||
"count": 2
|
||||
}
|
||||
],
|
||||
"timers": [
|
||||
{
|
||||
"key": "interval:200",
|
||||
"count": 7
|
||||
},
|
||||
{
|
||||
"key": "timeout:0",
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"key": "timeout:12000",
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"key": "interval:60000",
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"key": "interval:250",
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"key": "interval:3000",
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"key": "interval:1000",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "timeout:15000",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "interval:2000",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "timeout:4000",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "timeout:10000",
|
||||
"count": 1
|
||||
}
|
||||
],
|
||||
"timeouts": 11,
|
||||
"intervals": 22,
|
||||
"timeoutFires": 6,
|
||||
"intervalFires": 148,
|
||||
"webSocketMessageCount": 1000,
|
||||
"webSocketBytes": 762620,
|
||||
"webSocketEvents": [
|
||||
{
|
||||
"event": "log:entry",
|
||||
"count": 850,
|
||||
"bytes": 185008
|
||||
},
|
||||
{
|
||||
"event": "sensorFrame",
|
||||
"count": 277,
|
||||
"bytes": 520169
|
||||
},
|
||||
{
|
||||
"event": "commandAck",
|
||||
"count": 200,
|
||||
"bytes": 19180
|
||||
},
|
||||
{
|
||||
"event": "session:sync",
|
||||
"count": 23,
|
||||
"bytes": 401624
|
||||
},
|
||||
{
|
||||
"event": "",
|
||||
"count": 5,
|
||||
"bytes": 5008402
|
||||
},
|
||||
{
|
||||
"event": "chat:typing",
|
||||
"count": 4,
|
||||
"bytes": 1748
|
||||
},
|
||||
{
|
||||
"event": "roverSnapshot:frame",
|
||||
"count": 3,
|
||||
"bytes": 291
|
||||
},
|
||||
{
|
||||
"event": "2",
|
||||
"count": 3,
|
||||
"bytes": 3
|
||||
},
|
||||
{
|
||||
"event": "roverHostStats",
|
||||
"count": 3,
|
||||
"bytes": 2306
|
||||
},
|
||||
{
|
||||
"event": "alert",
|
||||
"count": 2,
|
||||
"bytes": 256
|
||||
},
|
||||
{
|
||||
"event": "alert:new",
|
||||
"count": 2,
|
||||
"bytes": 329
|
||||
},
|
||||
{
|
||||
"event": "0{\"sid\":\"pSJ",
|
||||
"count": 1,
|
||||
"bytes": 106
|
||||
},
|
||||
{
|
||||
"event": "40{\"sid\":\"3t",
|
||||
"count": 1,
|
||||
"bytes": 32
|
||||
},
|
||||
{
|
||||
"event": "mode",
|
||||
"count": 1,
|
||||
"bytes": 27
|
||||
},
|
||||
{
|
||||
"event": "controlGranted",
|
||||
"count": 1,
|
||||
"bytes": 41
|
||||
},
|
||||
{
|
||||
"event": "auth:role",
|
||||
"count": 1,
|
||||
"bytes": 31
|
||||
},
|
||||
{
|
||||
"event": "rovers",
|
||||
"count": 1,
|
||||
"bytes": 5485
|
||||
},
|
||||
{
|
||||
"event": "chat:init",
|
||||
"count": 1,
|
||||
"bytes": 91696
|
||||
},
|
||||
{
|
||||
"event": "overseer:memory",
|
||||
"count": 1,
|
||||
"bytes": 1505
|
||||
},
|
||||
{
|
||||
"event": "log:init",
|
||||
"count": 1,
|
||||
"bytes": 41011
|
||||
},
|
||||
{
|
||||
"event": "barcode:state",
|
||||
"count": 1,
|
||||
"bytes": 1619
|
||||
},
|
||||
{
|
||||
"event": "kinect:pointCloudFrame",
|
||||
"count": 1,
|
||||
"bytes": 262
|
||||
},
|
||||
{
|
||||
"event": "kinect:colorFrame",
|
||||
"count": 1,
|
||||
"bytes": 169
|
||||
},
|
||||
{
|
||||
"event": "430[{\"succes",
|
||||
"count": 1,
|
||||
"bytes": 42
|
||||
},
|
||||
{
|
||||
"event": "431[{\"url\":\"",
|
||||
"count": 1,
|
||||
"bytes": 133
|
||||
},
|
||||
{
|
||||
"event": "432[{\"url\":\"",
|
||||
"count": 1,
|
||||
"bytes": 145
|
||||
},
|
||||
{
|
||||
"event": "roverSnapshot:status",
|
||||
"count": 1,
|
||||
"bytes": 79
|
||||
},
|
||||
{
|
||||
"event": "Freaky",
|
||||
"count": 1,
|
||||
"bytes": 40
|
||||
},
|
||||
{
|
||||
"event": "434[{\"succes",
|
||||
"count": 1,
|
||||
"bytes": 194
|
||||
},
|
||||
{
|
||||
"event": "435[{\"succes",
|
||||
"count": 1,
|
||||
"bytes": 194
|
||||
}
|
||||
],
|
||||
"mutationsTotal": {
|
||||
"records": 18758,
|
||||
"added": 215,
|
||||
"removed": 17,
|
||||
"attrs": 18490,
|
||||
"text": 36
|
||||
},
|
||||
"mutationTargets": [
|
||||
{
|
||||
"key": "input.accent-emerald-400:attributes:name",
|
||||
"count": 5300
|
||||
},
|
||||
{
|
||||
"key": "input.accent-emerald-400:attributes:type",
|
||||
"count": 2650
|
||||
},
|
||||
{
|
||||
"key": "input.field-input.w-full:attributes:name",
|
||||
"count": 1332
|
||||
},
|
||||
{
|
||||
"key": "input.field-input.flex-1:attributes:name",
|
||||
"count": 1328
|
||||
},
|
||||
{
|
||||
"key": "input.accent-cyan-500:attributes:name",
|
||||
"count": 1324
|
||||
},
|
||||
{
|
||||
"key": "div.h-full:attributes:style",
|
||||
"count": 1238
|
||||
},
|
||||
{
|
||||
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:name",
|
||||
"count": 666
|
||||
},
|
||||
{
|
||||
"key": "input.field-input.chat-composer-input:attributes:name",
|
||||
"count": 666
|
||||
},
|
||||
{
|
||||
"key": "input:attributes:name",
|
||||
"count": 666
|
||||
},
|
||||
{
|
||||
"key": "input.field-input.w-full:attributes:type",
|
||||
"count": 666
|
||||
},
|
||||
{
|
||||
"key": "input.accent-cyan-500:attributes:type",
|
||||
"count": 662
|
||||
},
|
||||
{
|
||||
"key": "path.[object.SVGAnimatedString]:attributes:d",
|
||||
"count": 621
|
||||
},
|
||||
{
|
||||
"key": "path.[object.SVGAnimatedString]:attributes:fill",
|
||||
"count": 619
|
||||
},
|
||||
{
|
||||
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:type",
|
||||
"count": 333
|
||||
},
|
||||
{
|
||||
"key": "input:attributes:type",
|
||||
"count": 333
|
||||
},
|
||||
{
|
||||
"key": "Text:characterData:",
|
||||
"count": 36
|
||||
},
|
||||
{
|
||||
"key": "input.min-w-0.flex-1:attributes:name",
|
||||
"count": 32
|
||||
},
|
||||
{
|
||||
"key": "section.panel-section.border:attributes:style",
|
||||
"count": 10
|
||||
},
|
||||
{
|
||||
"key": "header.flex.items-center:attributes:style",
|
||||
"count": 9
|
||||
},
|
||||
{
|
||||
"key": "path.[object.SVGAnimatedString]:attributes:stroke",
|
||||
"count": 6
|
||||
},
|
||||
{
|
||||
"key": "span.inline-flex.items-center:attributes:class",
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"key": "button.mobile-touch-control.flex:attributes:disabled",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "button.mobile-touch-control.min-h-9:attributes:disabled",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:value",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "input.field-input.flex-1:attributes:value",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"key": "div.flex.flex-col:attributes:class",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:placeholder",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "div.rounded-sm.px-2:attributes:class",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "button.button-dark.min-w-0:attributes:disabled",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "button.mobile-touch-control.flex:attributes:class",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "div.mobile-touch-control.flex:attributes:class",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "input.field-input.w-full:attributes:value",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "button.button-dark.text-sm:attributes:disabled",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "video.h-full.w-full:attributes:playsinline",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "video.h-full.w-full:attributes:autoplay",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "video.h-full.w-full:attributes:disableremoteplayback",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "audio:attributes:autoplay",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "audio:attributes:disableremoteplayback",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "div.fixed.bottom-2:attributes:class",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"key": "span.rounded.px-1:attributes:class",
|
||||
"count": 1
|
||||
}
|
||||
],
|
||||
"mutationBursts": [
|
||||
{
|
||||
"at": 1633,
|
||||
"records": 164,
|
||||
"added": 105,
|
||||
"removed": 1,
|
||||
"attrs": 58,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 3468,
|
||||
"records": 79,
|
||||
"added": 11,
|
||||
"removed": 2,
|
||||
"attrs": 64,
|
||||
"text": 2
|
||||
},
|
||||
{
|
||||
"at": 1295,
|
||||
"records": 73,
|
||||
"added": 22,
|
||||
"removed": 5,
|
||||
"attrs": 42,
|
||||
"text": 4
|
||||
},
|
||||
{
|
||||
"at": 5655,
|
||||
"records": 72,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 72,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 5790,
|
||||
"records": 72,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 72,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 16256,
|
||||
"records": 72,
|
||||
"added": 3,
|
||||
"removed": 0,
|
||||
"attrs": 67,
|
||||
"text": 2
|
||||
},
|
||||
{
|
||||
"at": 6959,
|
||||
"records": 70,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 69,
|
||||
"text": 1
|
||||
},
|
||||
{
|
||||
"at": 4549,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 4590,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 5282,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 5711,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 6485,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 7777,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 8498,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 8545,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 9500,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 9569,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 11021,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 11795,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
},
|
||||
{
|
||||
"at": 17038,
|
||||
"records": 68,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"attrs": 68,
|
||||
"text": 0
|
||||
}
|
||||
],
|
||||
"frames": {
|
||||
"count": 153,
|
||||
"average": 116.5535947712418,
|
||||
"p95": 166.69999999999982,
|
||||
"p99": 316.60000000000014
|
||||
},
|
||||
"longTasks": {
|
||||
"count": 16,
|
||||
"top": [
|
||||
{
|
||||
"startTime": 238.79999999701977,
|
||||
"duration": 299,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 542.8999999985099,
|
||||
"duration": 192,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 3064.10000000149,
|
||||
"duration": 177,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 1632.8999999985099,
|
||||
"duration": 170,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 1154.8999999985099,
|
||||
"duration": 140,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 2930.2999999970198,
|
||||
"duration": 133,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 1505.6999999955297,
|
||||
"duration": 127,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 735.6999999955297,
|
||||
"duration": 117,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 1295.8999999985099,
|
||||
"duration": 108,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 3674.89999999851,
|
||||
"duration": 83,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 3387,
|
||||
"duration": 81,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 3567.89999999851,
|
||||
"duration": 59,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 1909,
|
||||
"duration": 57,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 7985.10000000149,
|
||||
"duration": 55,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 12968.5,
|
||||
"duration": 55,
|
||||
"name": "self"
|
||||
},
|
||||
{
|
||||
"startTime": 931.6999999955297,
|
||||
"duration": 53,
|
||||
"name": "self"
|
||||
}
|
||||
]
|
||||
},
|
||||
"consoleErrors": []
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 875 KiB |