performance slopping but actually very informative

This commit is contained in:
legop3
2026-06-11 14:07:28 -04:00
parent 4a213add69
commit 6bec6aee1a
46 changed files with 16164 additions and 0 deletions
+186
View File
@@ -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.
+164
View File
@@ -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.
+172
View File
@@ -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.
+161
View File
@@ -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.
+65
View File
@@ -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.
+251
View File
@@ -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;
});
+120
View File
@@ -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;
});
+253
View File
@@ -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;
});
+284
View File
@@ -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;
});
+5
View File
@@ -0,0 +1,5 @@
{
"dependencies": {
"playwright": "^1.60.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

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": []
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

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
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

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
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because one or more lines are too long
Binary file not shown.

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
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

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": []
}
}
Binary file not shown.

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": []
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 875 KiB