Compare commits

..
Author SHA1 Message Date
legop3 6d001b5263 might ditch the esp32, this is the best ive gotten with it. 2025-11-09 02:51:44 -05:00
671 changed files with 2794 additions and 105733 deletions
+2 -24
View File
@@ -7,27 +7,5 @@ logs
node_modules/ node_modules/
.pio .pio
.vscode/ .vscode/
config.h include/config.h
robots.json server/robots.json
roverd-dummy
server/config.yaml
server/package-lock.json
package-lock.json
server/data/discord-guilds.json
server/data/global-objective.json
server/data/admin-reason.json
server/data/buttonbox-state.json
server/data/barcode-tts-cache/
server/data/rover-odometers.json
webui/package-lock.json
!server/data/
!server/data/barcode-registry.json
webui/src/config/analytics.jsx
webui/src/config/driverAnalytics.json
webui/src/config/analytics.html
plans/barcodegames.txt
.gitignore
server/data/identity.sqlite
server/data/barcode-games.json
server/data/identity.sqlite-shm
server/data/identity.sqlite-wal
+52 -60
View File
@@ -1,72 +1,64 @@
# Multi Roomba Rover # Multi Roomba Rover
A system for controlling create 2 compatible roombas through a webpage.
You can explore my basement through this project here: a remake of my RoombaRover project with a decentralized and embedded approach
https://rover.otter.land
## This guide is a work in progress, it will cover: ## Hardware stack
- Building rovers
- Installing roverd on a rover's raspberry pi
- Setting up the server
- Configuration
## Building a rover On each roomba:
You WILL need some common hobby electronics knowledge to build one of these, I will not be making a super detailed wiring or assembly guide, some of building your rover will be up to you, things such as a protective cage, camera and electronics mounting will not be covered in any great detail. - an esp32
- a level shifter
- DONT FORGET THE BRC PIN PULSE
- a power supply
- an openIPC camera
- USB wifi card
- microphone
- speaker
- MAYBE a master relay which can be turned off programatically to save the roomba from discharging. based on battery voltage plus urgent battery #?
### Parts per rover ## Current software layout
| Part | Notes | ```
| --- | --- | .
| Create 2 compatible Roomba with the mini DIN port | Tested only with 600 series Roombas, your mileage may vary with newer models | ├── include/
| 4S li-ion -> 5v Voltage regulator | I use cheapo 12-24v USB car chargers, tear them apart, and solder wires onto them | │ ├── config.example.h // copy to config.h with your Wi-Fi + server settings
| 7 pin mini DIN cable | Adafruit sells these labeled as a cable specifically for the iRobot Create 2 | │ └── protocol.h // shared packet layout (control + telemetry)
| Logic level shifter | I use ones from adafruit, the Roomba is 5v logic and the pi is 3.3v logic | ├── src/main.cpp // ESP32 firmware entrypoint (PlatformIO)
| Google voice AIY hat v1 | This serves three purposes: speaker, microphone, and a high current LED driver. None of these things are required to make a rover with a camera that can drive around, but it makes it much more fun. The design of this hat also makes it very easy to connect all of the various wires to all the IO on the pi that they need to go to. | └── server/
| Raspberry pi zero 2 W | This will run roverd to control the rover and manage some Roomba safety things locally. You can probably use any pi you want, but these are usually the cheapest. | ├── package.json // Node.js server + Socket.IO web UI
| A good wifi adapter | Has to work with Linux 6.12 (I use ones with the MT7921AU chipset and they work great), ideally wifi 6 with MU-MIMO if you are running multiple rovers on one AP. The built in wifi on the pi zero 2 W is NOT good enough for this. | ├── robots.example.json // copy/edit to robots.json for your fleet
| A USB hub hat (if using a pi zero) | If you are using a raspberry pi zero, you will want a USB hub hat for your wifi adapter, I would not recommend using an OTG adapter as it may not provide enough power. | ├── src/ // UDP relay + telemetry decoder
| ----CAMERA STUFF---- | ----Below here is all camera related things that youll need---- | └── public/ // barebones HTML/JS UI
| A pi camera v2 (ov5647) | The video publisher in this project is only designed for the ov5647, though it would be totally possible to make your own publisher for other sensors. | ```
| Cheap 5v servo | Unless you have a VERY wide camera lens, you will want to have a servo plugged into the pi to be able to tilt the camera up and down. |
| LED for toggling the headlight | I use white LEDs attached to one of the "driver" pins on the AIY hat to trick one of those generic pi camera LED floodlights into turning on / off on demand.|
| Optional laser pointer | A laser pointer can be wired to another high-current-capable GPIO driver, with GPIO 27 used by default in the rover config. |
| Pi zero camera cable | The pi zero has a smaller version of the camera ribbon connector, you will need the right cable for your camera |
I do not have any exact numbers, but with cheap used Roombas it seems like on average one of my rovers takes about $100 USD to build from scratch. ### Firmware quickstart
### Physical assembly of the rover 1. `cp include/config.example.h include/config.h` and fill in:
This is going to be the very loose section of this guide, the way yout build your rover is up to you. I have mine built out with durable protective metal cages as they are open to the internet and people like breaking things. The electronics on top of the roomba can be as simple as a pi, level shifter, and a camera if you just want to run it around yourself, things like a speaker, microphone, servo, headlight, laser pointer, are all totally optional and can be disabled in the configuration on the raspberry pi. - `WIFI_SSID` / `WIFI_PASSWORD`
- `CONTROL_SERVER_IP` (Node server host)
- `ROOMBA_ID` (unique per robot; must match the server entry)
- tweak ports only if you have a reason.
2. Flash with PlatformIO: `pio run -t upload` (env `esp32s3`).
3. The firmware spawns three FreeRTOS tasks:
- control loop (5ms cadence) consumes UDP control packets and drives the Create 2 via UART pins 16/17. Wheel commands decay to zero if no packets arrive for 250ms.
- telemetry loop (500ms cadence) polls sensor group 100, appends Wi-Fi/LRU stats, and streams UDP telemetry to the server.
- BRC maintenance pulses GPIO5 low for 1s every minute to keep the robot awake.
These are the basics of how evertything is connected on my rovers, this is hard to organize and illustrate through text but hopefully it will give you a good idea: ### Server + web UI quickstart
1. 7 pin mini DIN cable
1. Voltage regulator on the two pairs of VBAT wires
1. 5v output of voltage regulator connected to the power input on the AIY hat
2. level shifter's high voltage side attached to 5v from regulator, ground, TX, RX, and BRC from the DIN cable
2. Level shifter low voltage side connected to 3.3v from the pi, with serial TX and RX, and the BRC pin on their respective low side level shifter lines. BRC pin goes to GPIO 4 on the pi.
3. Google voice AIY hat:
1. speaker
2. microphone
3. servo motor on GPIO pin 26
4. Camera stuff:
1. ov5647 camera
2. full size to pi zero style camera ribbon cable, plug camera into pi
3. an LED connected to a high current driver output on the AIY hat, GPIO 22 by default. This is meant to be poked into the LDR sensor on a pi camera IR floodlight to make it toggleable by the driver.
4. optionally, a laser pointer connected to another high current driver output, GPIO 27 by default.
### Software on your rover 1. `cd server`
Raspberry pi OS installation: 2. `cp robots.example.json robots.json` and add one entry per robot. Only the `id` is required (must match `ROOMBA_ID` in the firmware); override `controlPort`/`maxWheelSpeed` if you deviate from defaults.
3. Install deps: `npm install`
4. Run in dev mode: `npm run dev`
- HTTP + Socket.IO on `http://localhost:8080`
- UDP control bind port `62000`, telemetry bind port `62001` (override with env vars).
5. Open the web UI:
- select a robot
- drive with WASD (left/right wheel mm/s shown in telemetry summary)
- buttons issue Safe/Full/Enable-OI/Dock commands
- sensor list renders the decoded Create 2 group-100 payload plus ESP stats
Everything that runs on the pi is designed for debian Bookworm, because I could not get the AIY voice hat working in Trixie. Each ESP32 announces itself as soon as it streams telemetry, so the server automatically learns the robots current IP address (no static DHCP entries required). If you do know a static IP, you can still set `deviceHost` in `robots.json` and the server will use it immediately.
To install roverd on your pi: UDP streams stay simple:
1. Install `Raspberry Pi OS (Legacy) Lite` (exactly what its called in the imager) on your SD card - server -> ESP32: fixed 12-byte control packet blasted at 50Hz per robot
2. Boot the pi up, plug in your good wifi adapter, use `nmtui` to set it up - ESP32 -> server: framed telemetry header + raw sensor group 100 + trailer (CRC-8)
3. Install `git`, clone this repo into your home folder
4. Move into the repo folder, run the roverd installer using `sudo pi/install_roverd.sh`
5. Reboot your raspberry pi
6. Roverd, along with the audio and video publishers should now be running. Roverd should have created it's default config file at `/etc/roverd.yaml`
Edit the `/etc/roverd.yaml` config file to change settings like your rover's name, color, and set the server IP.
## Setting up the server
-6
View File
@@ -1,6 +0,0 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
config.h
-37
View File
@@ -1,37 +0,0 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the convention is to give header files names that end with `.h'.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
-46
View File
@@ -1,46 +0,0 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into the executable file.
The source code of each library should be placed in a separate directory
("lib/your_library_name/[Code]").
For example, see the structure of the following example libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
Example contents of `src/main.c` using Foo and Bar:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
The PlatformIO Library Dependency Finder will find automatically dependent
libraries by scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html
-15
View File
@@ -1,15 +0,0 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:seeed_xiao_esp32s3]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200
-3
View File
@@ -1,3 +0,0 @@
#define WIFI_SSID "wifissid"
#define WIFI_PASSWORD "wifipassword"
#define SERVER_URL "http://192.168.0.86:8080/buttonbox/press"
-110
View File
@@ -1,110 +0,0 @@
#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <config.h>
// button pin defs
const int buttonPins[] = {15, 16, 17, 18};
const int buttonCount = sizeof(buttonPins) / sizeof(buttonPins[0]);
const int beeperPin = 13;
// Using INPUT_PULLUP means idle = HIGH, pressed = LOW (button to GND).
const int buttonPressedState = LOW;
const unsigned long debounceMs = 40;
const int toneDurationMs = 90;
// One tone per button (1-4).
const int buttonTonesHz[buttonCount] = {262, 330, 392, 523};
int lastStableState[buttonCount];
int lastReading[buttonCount];
unsigned long lastDebounceTime[buttonCount];
void PlayButtonTone(int buttonNumber) {
if (buttonNumber < 1 || buttonNumber > buttonCount) {
return;
}
int frequencyHz = buttonTonesHz[buttonNumber - 1];
tone(beeperPin, frequencyHz, toneDurationMs);
}
// button request function
bool SendButtonPressRequest(int buttonNumber) {
Serial.println("attempting to send button press");
bool success = false;
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
http.begin(SERVER_URL);
http.addHeader("Content-Type", "text/plain");
int httpResponseCode = http.POST(String(buttonNumber));
if (httpResponseCode > 0) {
Serial.println("http response code: ");
Serial.print(httpResponseCode);
Serial.println(http.getString());
success = true;
} else {
Serial.println("HTTP ERROR!!! ");
Serial.print(httpResponseCode);
success = false;
}
http.end();
} else {
Serial.println("Not ocnnectec to wifi! cant send request!");
}
return success;
}
void setup() {
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(100);
Serial.print("wifi connecting... ");
}
Serial.println("WIFI CONNENCTED !!!! :3");
for (int i = 0; i < buttonCount; i++) {
pinMode(buttonPins[i], INPUT_PULLUP);
int initial = digitalRead(buttonPins[i]);
lastStableState[i] = initial;
lastReading[i] = initial;
lastDebounceTime[i] = 0;
}
pinMode(beeperPin, OUTPUT);
noTone(beeperPin);
}
void loop() {
unsigned long now = millis();
for (int i = 0; i < buttonCount; i++) {
int reading = digitalRead(buttonPins[i]);
if (reading != lastReading[i]) {
lastDebounceTime[i] = now;
}
if ((now - lastDebounceTime[i]) > debounceMs) {
if (reading != lastStableState[i]) {
lastStableState[i] = reading;
// Trigger once on press edge.
if (reading == buttonPressedState) {
if (SendButtonPressRequest(i + 1)) {
PlayButtonTone(i + 1);
}
}
}
}
lastReading[i] = reading;
}
delay(5);
}
-11
View File
@@ -1,11 +0,0 @@
This directory is intended for PlatformIO Test Runner and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
-24
View File
@@ -1,24 +0,0 @@
#configuration for roverd
name: dummy1
serverUrl: ws://127.0.0.1:8080/rover
serial:
device: /dev/ttyS0
baud: 115200
brc:
gpioPin: 25
pulseEvery: 1m
pulseWidth: 1s
battery:
full: 2068
warn: 1700
urgent: 1650
maxWheelSpeed: 350
media:
manage: false
service: mediamtx.service
healthUrl: http://127.0.0.1:9997/v3/paths/list
healthInterval: 30s
headlight:
enabled: false
laser:
enabled: false
-24
View File
@@ -1,24 +0,0 @@
#configuration for roverd
name: dummy2
serverUrl: ws://127.0.0.1:8080/rover
serial:
device: /dev/ttyS0
baud: 115200
brc:
gpioPin: 25
pulseEvery: 1m
pulseWidth: 1s
battery:
full: 2068
warn: 1700
urgent: 1650
maxWheelSpeed: 350
media:
manage: false
service: mediamtx.service
healthUrl: http://127.0.0.1:9997/v3/paths/list
healthInterval: 30s
headlight:
enabled: false
laser:
enabled: false
-24
View File
@@ -1,24 +0,0 @@
#configuration for roverd
name: dummy3
serverUrl: ws://127.0.0.1:8080/rover
serial:
device: /dev/ttyS0
baud: 115200
brc:
gpioPin: 25
pulseEvery: 1m
pulseWidth: 1s
battery:
full: 2068
warn: 1700
urgent: 1650
maxWheelSpeed: 350
media:
manage: false
service: mediamtx.service
healthUrl: http://127.0.0.1:9997/v3/paths/list
healthInterval: 30s
headlight:
enabled: false
laser:
enabled: false
BIN
View File
Binary file not shown.
Vendored
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+18
View File
@@ -0,0 +1,18 @@
#pragma once
// Copy this file to include/config.h and fill in your network + server settings.
#define WIFI_SSID "YourNetworkName"
#define WIFI_PASSWORD "YourNetworkPassword"
// UDP server that issues control packets and receives telemetry.
#define CONTROL_SERVER_IP "192.168.1.50"
#define CONTROL_SERVER_PORT 62000
#define TELEMETRY_SERVER_PORT 62001
// Local ports on the ESP32. Keeping them distinct simplifies sniffing.
#define ESP32_CONTROL_PORT 50010
#define ESP32_TELEMETRY_PORT 50011
// Friendly name to embed in telemetry.
#define ROOMBA_ID "roomba-alpha"
+85
View File
@@ -0,0 +1,85 @@
#pragma once
#include <stdint.h>
#include <type_traits>
#include <Arduino.h>
namespace mrr {
constexpr uint8_t kControlMagic = 0xAA;
constexpr uint8_t kTelemetryMagic = 0x55;
constexpr uint8_t kProtocolVersion = 1;
constexpr size_t kSensorGroup100Length = 80;
constexpr size_t kMaxRobotIdLength = 16;
enum class OiModeRequest : uint8_t {
kNoChange = 0,
kPassive = 1,
kSafe = 2,
kFull = 3,
};
enum ActionBits : uint8_t {
kActionSeekDock = 0x01,
kActionPlaySong = 0x02,
kActionLoadSong = 0x04,
kActionEnableOi = 0x08,
};
struct __attribute__((packed)) ControlPacket {
uint8_t magic{kControlMagic};
uint8_t version{kProtocolVersion};
uint16_t seq{};
int16_t left_mmps{};
int16_t right_mmps{};
uint8_t oi_mode{};
uint8_t actions{};
uint8_t song_slot{};
uint8_t checksum{};
};
static_assert(sizeof(ControlPacket) == 12, "ControlPacket must remain packed");
struct __attribute__((packed)) TelemetryPacketHeader {
uint8_t magic{kTelemetryMagic};
uint8_t version{kProtocolVersion};
uint16_t seq{};
uint32_t uptime_ms{};
uint32_t last_control_age_ms{};
int8_t wifi_rssi_dbm{};
uint8_t status_bits{};
uint8_t sensor_bytes{};
uint8_t robot_id_length{};
char robot_id[kMaxRobotIdLength]{};
};
struct __attribute__((packed)) TelemetryPacketTrailer {
int16_t applied_left_mmps{};
int16_t applied_right_mmps{};
uint16_t last_control_seq{};
uint16_t dropped_control_packets{};
uint8_t checksum{};
};
inline uint8_t checksum8(const uint8_t* data, size_t len) {
uint32_t sum = 0;
for (size_t i = 0; i < len; ++i) {
sum += data[i];
}
return static_cast<uint8_t>(sum & 0xFF);
}
template <typename T>
inline uint8_t checksumPayload(const T& pod) {
static_assert(std::is_trivially_copyable<T>::value, "checksum payload must be POD");
return checksum8(reinterpret_cast<const uint8_t*>(&pod), sizeof(T));
}
template <typename T>
inline uint8_t checksumExcludingLastByte(const T& pod) {
static_assert(std::is_trivially_copyable<T>::value, "checksum payload must be POD");
return checksum8(reinterpret_cast<const uint8_t*>(&pod), sizeof(T) - 1);
}
} // namespace mrr
+67
View File
@@ -0,0 +1,67 @@
## esp32 firmware
- hooked up to the roomba's UART on pins 16 and 17
- pin 5 is connected to the roomba's BRC pin
- pulse the BRC pin low for 1 second every minute to keep the roomba awake
- connect to wifi
- connect to the server
- get a full frame of sensor group 100 from the roomba every 500ms
- send it to the server over the sensor UDP stream
- listen to the server's control UDP stream (per roomba) and do the following accordingly:
- set wheel speeds
- seek dock
- enable OI
- safe mode
- full mode
- play song
- load song
## esp32 -> server communication
- one UDP stream to the esp32 for controlling the roomba
- might look like this:
- left wheel speed
- right wheel speed
- OI mode
- seek dock?
- blasts out at a constant rate from the server for each roomba
- the esp32 will listen, and follow the latest command that it sees
- one UDP stream from the esp32 to the server for sending sensor data frames and other telemetry
- one full frame of sensor data per datagram
- send raw sensor data, the server will decode it
- add other telemetry from the esp32, like signal strength, etc.
- maybe use this stream as a sign that the esp32 is still running healthily?
## nodejs server
- KISS
- decode the sensor data from each roomba
- can support multiple roombas connected from the ground up
- keep it simple, worry about getting the esp32 firmware right.
- but the server DOES have to exist for testing
- IS the web server, hosts an entire static folder for the web UI
## server -> web UI communication
- socket.io
- don't do anything fancy with the socket.io setup
- it works fine out of the box, we will optimize it later
## the web UI
- KISS
- plain old html. no styling even. just bare minimum for testing
- what it needs to do:
- allow user to select the roomba from a list
- make the selected roomba drive with WASD
- have buttons to set the OI mode, and tell the roomba to dock
- show a plain list of the sensor data from the selected roomba
### general javascript programming guidelines (applies to the web UI too)
- everything ES6
- one entrypoint file in the web UI
- everything modular
- everything easy to read, understand, and work on
- comment where you think is best to describe whats going on
## closing notes
- keep the user input path (web UI -> server -> roomba) as light and responsive as possible. responsiveness is key for this.
- responsiveness is the name of the game. The future of this program is teleoperation over the internet, with a camera on each roomba. keyboard inputs from the user must be near instant.
- on the esp32 firmware side of things, sensor data is second priority to having a responsive control system
- but sensor data DOES have to exist.
- the future of this project will involve assigning one roomba to a user, make the server able to do that from the ground up.
-186
View File
@@ -1,186 +0,0 @@
# `/` 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.
-147
View File
@@ -1,147 +0,0 @@
# `/spectate` CPU Performance Summary
This is the `/spectate` companion to the `/` performance writeups. The testing target
was the deployed page:
```sh
https://rover.otter.land/spectate
```
The most useful artifact for this pass is:
```sh
perf/results/2026-06-12T04-55-16-888Z-root-runtime/root-runtime-report.json
```
That run used:
```sh
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=30000 \
node perf/live-root-runtime-audit.mjs https://rover.otter.land/spectate perf/results
```
## High-Level Result
`/spectate` is CPU-heavy for a different reason than `/`.
The `/` page's biggest confirmed issue is global input listener churn. `/spectate` does
not show that same dominant pattern. The fresh `/spectate` audit had 379 event listeners,
mostly normal media/image listeners, and no giant add/remove loop.
The `/spectate` hot path is instead a live-display fan-out problem:
- several rover media players are mounted at once
- each rover card renders live HUD telemetry and a top-down SVG map
- camera/snapshot frames create new blobs and `img.src` values
- the route subscribes to broad session/log/chat state while also rendering the whole
spectator layout
- the page maintains several WebRTC players at the same time
## Key Measurements
Fresh `/spectate` runtime audit, 30s, mobile viewport, 6x CPU throttle:
```txt
TaskDuration: 28.48s over a 30.9s sample
ScriptDuration: 12.11s
Long tasks: 81
Worst long task: 375ms
Average frame gap: 113.9ms
p95 frame gap: 233.3ms
p99 frame gap: 300.0ms
Nodes: 7,059
Layout objects: 6,150
RTCPeerConnections: 6
LayoutCount: 243
RecalcStyleCount: 248
```
The 8x CPU-throttled profile from the earlier route comparison was worse:
```txt
Task delta: 49.13s over a 52.85s sample
Script delta: 19.58s
Layout delta: 516
Recalc delta: 522
Node delta: +3,815
```
That profile also put `Blob`, `decodeString`, and `setAttribute` in the CPU stack, which
matches the runtime evidence for camera blobs, socket payload parsing, and SVG/image
attribute churn.
## Socket And DOM Pressure
The fresh audit captured these live events during the 30s window:
```txt
sensorFrame: 1,354 events, 2,531,090 bytes
log:entry: 634 events, 150,179 bytes
unlabeled binary: 126 events, 11,048,477 bytes
roomCamera:frame: 117 events
session:sync: 54 events, 914,000 bytes
commandAck: 68 events
roverHostStats: 17 events
roverSnapshot:frame: 7 events
```
The most important DOM mutations were:
```txt
path d: 2,688 mutations
path fill: 2,682 mutations
img.h-full.w-full src: 117 mutations
span.h-2.w-2 class: 117 mutations
```
The `path` mutations point strongly at the per-rover `TopDownMap` SVG overlays. The
`img.src` and blink class mutations point at room camera snapshot rendering.
## Ordered Issue List
1. [Telemetry fan-out and always-on HUD maps](./spectate-issues/001-telemetry-fanout-hud-maps.md)
2. [Multi-rover WebRTC media fan-out](./spectate-issues/002-multi-rover-webrtc-media-fanout.md)
3. [Room camera and snapshot object URL churn](./spectate-issues/003-room-camera-snapshot-object-url-churn.md)
4. [Broad spectator session rerenders](./spectate-issues/004-broad-spectator-session-rerenders.md)
5. [Shared log/session stream pressure](./spectate-issues/005-shared-log-session-stream-pressure.md)
6. [Spectator role handshake retries](./spectate-issues/006-spectator-role-handshake-retries.md)
## Upstream Dependency Map
Fixing the `/` backlog first is a good plan.
Likely improved by `/` fixes:
- telemetry visual throttling
- log batching/gating
- chat/nickname churn
- session selector/store improvements
- generic timer cleanup
Not likely to be fixed by `/` alone:
- several simultaneous WHEP video/audio players
- per-rover spectator HUD maps
- room camera snapshot blob/object URL churn
- spectator role/subscribe handshake behavior
- mobile policy for how many live feeds should be active at once
## Suggested Re-Test
After each `/spectate` fix, run:
```sh
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=30000 \
node perf/live-root-runtime-audit.mjs https://rover.otter.land/spectate perf/results
```
The most useful numbers to compare:
- `TaskDuration`
- `ScriptDuration`
- long task count and worst duration
- frame average/p95/p99
- `RTCPeerConnections`
- `mutationTargets` for `path d`, `path fill`, and `img src`
- `webSocketEvents` for `sensorFrame`, `log:entry`, `session:sync`, and binary frame volume
@@ -1,278 +0,0 @@
# FIXED!
# 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,
toggleHeadlight,
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.
@@ -1,192 +0,0 @@
# FIXED!
# 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.
-165
View File
@@ -1,165 +0,0 @@
# FIXED!
# 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.
@@ -1,205 +0,0 @@
# FIXED!
# 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.
@@ -1,191 +0,0 @@
# 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
@@ -1,172 +0,0 @@
# 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.
-162
View File
@@ -1,162 +0,0 @@
# FIXED!
# 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
@@ -1,65 +0,0 @@
# `/` 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
@@ -1,251 +0,0 @@
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
@@ -1,120 +0,0 @@
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
@@ -1,253 +0,0 @@
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
@@ -1,284 +0,0 @@
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
@@ -1,5 +0,0 @@
{
"dependencies": {
"playwright": "^1.60.0"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 KiB

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

Before

Width:  |  Height:  |  Size: 390 KiB

@@ -1,334 +0,0 @@
{
"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.

Before

Width:  |  Height:  |  Size: 402 KiB

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

Before

Width:  |  Height:  |  Size: 328 KiB

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

Before

Width:  |  Height:  |  Size: 360 KiB

@@ -1,423 +0,0 @@
{
"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.

Before

Width:  |  Height:  |  Size: 216 KiB

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

Before

Width:  |  Height:  |  Size: 226 KiB

@@ -1,403 +0,0 @@
{
"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.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 604 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

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

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

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

Before

Width:  |  Height:  |  Size: 1.2 MiB

@@ -1,527 +0,0 @@
{
"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.

Before

Width:  |  Height:  |  Size: 216 KiB

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

Before

Width:  |  Height:  |  Size: 221 KiB

@@ -1,483 +0,0 @@
{
"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
}
]
}
@@ -1,581 +0,0 @@
{
"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.

Before

Width:  |  Height:  |  Size: 923 KiB

@@ -1,843 +0,0 @@
{
"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.

Before

Width:  |  Height:  |  Size: 875 KiB

@@ -1,858 +0,0 @@
{
"url": "https://rover.otter.land/spectate",
"runDir": "perf/results/2026-06-12T04-55-16-888Z-root-runtime",
"environment": {
"cpuThrottle": 6,
"viewport": {
"width": 390,
"height": 844
},
"isMobile": true,
"sampleMs": 30000
},
"metrics": {
"Timestamp": 29302.703615,
"AudioHandlers": 0,
"AudioWorkletProcessors": 0,
"Documents": 1,
"Frames": 1,
"JSEventListeners": 379,
"LayoutObjects": 6150,
"MediaKeySessions": 0,
"MediaKeys": 0,
"Nodes": 7059,
"Resources": 40,
"ContextLifecycleStateObservers": 21,
"V8PerContextDatas": 2,
"WorkerGlobalScopes": 0,
"UACSSResources": 0,
"RTCPeerConnections": 6,
"ResourceFetchers": 1,
"AdSubframes": 0,
"DetachedScriptStates": 0,
"ArrayBufferContents": 28,
"LayoutCount": 243,
"RecalcStyleCount": 248,
"LayoutDuration": 0.840302,
"RecalcStyleDuration": 1.699961,
"DevToolsCommandDuration": 0.000155,
"ScriptDuration": 12.114687,
"V8CompileDuration": 0,
"TaskDuration": 28.48339,
"TaskOtherDuration": 13.828285,
"ThreadTime": 30.190528,
"ProcessTime": 39.47,
"JSHeapUsedSize": 22447504,
"JSHeapTotalSize": 42467328,
"FirstMeaningfulPaint": 0,
"DomContentLoaded": 29272.696156,
"NavigationStart": 29272.010802
},
"snapshot": {
"elapsedMs": 30914.699999999255,
"listeners": [
{
"key": "add:audio:canplay",
"count": 9
},
{
"key": "add:audio:ended",
"count": 9
},
{
"key": "add:audio:error",
"count": 9
},
{
"key": "add:audio:pause",
"count": 9
},
{
"key": "add:audio:play",
"count": 9
},
{
"key": "add:audio:stalled",
"count": 9
},
{
"key": "add:audio:waiting",
"count": 9
},
{
"key": "add:window:online",
"count": 4
},
{
"key": "add:document:visibilitychange",
"count": 4
},
{
"key": "add:img:error",
"count": 4
},
{
"key": "add:img:load",
"count": 4
},
{
"key": "add:input:invalid",
"count": 3
},
{
"key": "remove:window:online",
"count": 3
},
{
"key": "remove:document:visibilitychange",
"count": 3
},
{
"key": "add:audio:abort",
"count": 3
},
{
"key": "add:audio:canplaythrough",
"count": 3
},
{
"key": "add:audio:durationchange",
"count": 3
},
{
"key": "add:audio:emptied",
"count": 3
},
{
"key": "add:audio:encrypted",
"count": 3
},
{
"key": "add:audio:loadeddata",
"count": 3
},
{
"key": "add:audio:loadedmetadata",
"count": 3
},
{
"key": "add:audio:loadstart",
"count": 3
},
{
"key": "add:audio:playing",
"count": 3
},
{
"key": "add:audio:progress",
"count": 3
},
{
"key": "add:audio:ratechange",
"count": 3
},
{
"key": "add:audio:resize",
"count": 3
},
{
"key": "add:audio:seeked",
"count": 3
},
{
"key": "add:audio:seeking",
"count": 3
},
{
"key": "add:audio:suspend",
"count": 3
},
{
"key": "add:audio:timeupdate",
"count": 3
},
{
"key": "add:audio:volumechange",
"count": 3
},
{
"key": "add:video:abort",
"count": 3
},
{
"key": "add:video:canplay",
"count": 3
},
{
"key": "add:video:canplaythrough",
"count": 3
},
{
"key": "add:video:durationchange",
"count": 3
},
{
"key": "add:video:emptied",
"count": 3
},
{
"key": "add:video:encrypted",
"count": 3
},
{
"key": "add:video:ended",
"count": 3
},
{
"key": "add:video:error",
"count": 3
},
{
"key": "add:video:loadeddata",
"count": 3
}
],
"timers": [
{
"key": "interval:3000",
"count": 12
},
{
"key": "timeout:12000",
"count": 6
},
{
"key": "timeout:1200",
"count": 4
},
{
"key": "interval:60000",
"count": 3
},
{
"key": "timeout:0",
"count": 3
},
{
"key": "interval:200",
"count": 2
},
{
"key": "timeout:15000",
"count": 1
},
{
"key": "interval:2000",
"count": 1
}
],
"timeouts": 14,
"intervals": 18,
"timeoutFires": 7,
"intervalFires": 112,
"webSocketMessageCount": 1000,
"webSocketBytes": 4207474,
"webSocketEvents": [
{
"event": "sensorFrame",
"count": 1354,
"bytes": 2531090
},
{
"event": "log:entry",
"count": 634,
"bytes": 150179
},
{
"event": "",
"count": 126,
"bytes": 11048477
},
{
"event": "roomCamera:frame",
"count": 117,
"bytes": 10413
},
{
"event": "commandAck",
"count": 68,
"bytes": 6412
},
{
"event": "session:sync",
"count": 54,
"bytes": 914000
},
{
"event": "roverHostStats",
"count": 17,
"bytes": 13058
},
{
"event": "roverSnapshot:frame",
"count": 7,
"bytes": 682
},
{
"event": "2",
"count": 5,
"bytes": 5
},
{
"event": "auth:role",
"count": 4,
"bytes": 139
},
{
"event": "roomCamera:status",
"count": 4,
"bytes": 284
},
{
"event": "roverSnapshot:status",
"count": 3,
"bytes": 238
},
{
"event": "alert",
"count": 2,
"bytes": 256
},
{
"event": "alert:new",
"count": 2,
"bytes": 330
},
{
"event": "0{\"sid\":\"3a5",
"count": 1,
"bytes": 106
},
{
"event": "40{\"sid\":\"lE",
"count": 1,
"bytes": 32
},
{
"event": "mode",
"count": 1,
"bytes": 27
},
{
"event": "controlGranted",
"count": 1,
"bytes": 41
},
{
"event": "rovers",
"count": 1,
"bytes": 5485
},
{
"event": "chat:init",
"count": 1,
"bytes": 77415
},
{
"event": "overseer:memory",
"count": 1,
"bytes": 520
},
{
"event": "log:init",
"count": 1,
"bytes": 44530
},
{
"event": "barcode:state",
"count": 1,
"bytes": 1627
},
{
"event": "kinect:pointCloudFrame",
"count": 1,
"bytes": 262
},
{
"event": "kinect:colorFrame",
"count": 1,
"bytes": 169
},
{
"event": "430[{\"succes",
"count": 1,
"bytes": 40
},
{
"event": "431[{\"succes",
"count": 1,
"bytes": 41
},
{
"event": "432[{\"succes",
"count": 1,
"bytes": 194
},
{
"event": "433[{\"succes",
"count": 1,
"bytes": 40
},
{
"event": "434[{\"url\":\"",
"count": 1,
"bytes": 133
},
{
"event": "435[{\"url\":\"",
"count": 1,
"bytes": 145
},
{
"event": "WALL-E",
"count": 1,
"bytes": 40
},
{
"event": "437[{\"url\":\"",
"count": 1,
"bytes": 133
},
{
"event": "438[{\"url\":\"",
"count": 1,
"bytes": 145
},
{
"event": "Freaky",
"count": 1,
"bytes": 40
},
{
"event": "4310[{\"url\":",
"count": 1,
"bytes": 136
},
{
"event": "4311[{\"url\":",
"count": 1,
"bytes": 148
},
{
"event": "Bweeble",
"count": 1,
"bytes": 42
},
{
"event": "carpet",
"count": 1,
"bytes": 68
},
{
"event": "4314[{\"succe",
"count": 1,
"bytes": 41
},
{
"event": "4315[{\"succe",
"count": 1,
"bytes": 22
},
{
"event": "4316[{\"succe",
"count": 1,
"bytes": 22
},
{
"event": "4317[{\"succe",
"count": 1,
"bytes": 195
},
{
"event": "4318[{\"succe",
"count": 1,
"bytes": 22
},
{
"event": "4319[{\"succe",
"count": 1,
"bytes": 22
}
],
"mutationsTotal": {
"records": 5868,
"added": 440,
"removed": 440,
"attrs": 4921,
"text": 67
},
"mutationTargets": [
{
"key": "path.[object.SVGAnimatedString]:attributes:d",
"count": 2688
},
{
"key": "path.[object.SVGAnimatedString]:attributes:fill",
"count": 2682
},
{
"key": "Text:characterData:",
"count": 164
},
{
"key": "img.h-full.w-full:attributes:src",
"count": 117
},
{
"key": "span.h-2.w-2:attributes:class",
"count": 117
},
{
"key": "input.field-input.flex-1:attributes:name",
"count": 102
},
{
"key": "input.field-input.chat-composer-input:attributes:name",
"count": 102
},
{
"key": "path.[object.SVGAnimatedString]:attributes:stroke",
"count": 18
},
{
"key": "section.panel-section.border:attributes:style",
"count": 11
},
{
"key": "span.rounded.px-1.5:attributes:class",
"count": 9
},
{
"key": "header.flex.items-center:attributes:style",
"count": 8
},
{
"key": "article.surface.px-0.5:attributes:class",
"count": 8
},
{
"key": "li.surface.flex:attributes:class",
"count": 3
},
{
"key": "video.h-full.w-full:attributes:playsinline",
"count": 3
},
{
"key": "video.h-full.w-full:attributes:autoplay",
"count": 3
},
{
"key": "video.h-full.w-full:attributes:disableremoteplayback",
"count": 3
},
{
"key": "audio:attributes:autoplay",
"count": 3
},
{
"key": "audio:attributes:disableremoteplayback",
"count": 3
},
{
"key": "input.accent-cyan-500:attributes:name",
"count": 2
},
{
"key": "div.panel-section.flex:attributes:style",
"count": 2
},
{
"key": "span.rounded.px-1:attributes:class",
"count": 2
},
{
"key": "input.accent-cyan-500:attributes:type",
"count": 1
},
{
"key": "input.field-input.flex-1:attributes:value",
"count": 1
}
],
"mutationBursts": [
{
"at": 13575,
"records": 119,
"added": 57,
"removed": 58,
"attrs": 4,
"text": 0
},
{
"at": 18089,
"records": 117,
"added": 52,
"removed": 53,
"attrs": 12,
"text": 0
},
{
"at": 27878,
"records": 113,
"added": 52,
"removed": 52,
"attrs": 8,
"text": 1
},
{
"at": 22882,
"records": 102,
"added": 49,
"removed": 49,
"attrs": 4,
"text": 0
},
{
"at": 9538,
"records": 100,
"added": 48,
"removed": 48,
"attrs": 4,
"text": 0
},
{
"at": 14782,
"records": 84,
"added": 37,
"removed": 35,
"attrs": 12,
"text": 0
},
{
"at": 15222,
"records": 83,
"added": 39,
"removed": 39,
"attrs": 5,
"text": 0
},
{
"at": 9811,
"records": 68,
"added": 28,
"removed": 28,
"attrs": 12,
"text": 0
},
{
"at": 13733,
"records": 36,
"added": 13,
"removed": 13,
"attrs": 10,
"text": 0
},
{
"at": 19439,
"records": 26,
"added": 10,
"removed": 10,
"attrs": 6,
"text": 0
},
{
"at": 23034,
"records": 18,
"added": 3,
"removed": 3,
"attrs": 12,
"text": 0
},
{
"at": 23113,
"records": 18,
"added": 4,
"removed": 4,
"attrs": 10,
"text": 0
},
{
"at": 28037,
"records": 18,
"added": 4,
"removed": 4,
"attrs": 10,
"text": 0
},
{
"at": 10169,
"records": 16,
"added": 4,
"removed": 4,
"attrs": 8,
"text": 0
},
{
"at": 18258,
"records": 16,
"added": 4,
"removed": 4,
"attrs": 8,
"text": 0
},
{
"at": 13903,
"records": 15,
"added": 4,
"removed": 4,
"attrs": 7,
"text": 0
},
{
"at": 14108,
"records": 15,
"added": 2,
"removed": 2,
"attrs": 10,
"text": 1
},
{
"at": 16376,
"records": 15,
"added": 2,
"removed": 2,
"attrs": 11,
"text": 0
},
{
"at": 11700,
"records": 14,
"added": 0,
"removed": 0,
"attrs": 14,
"text": 0
},
{
"at": 13012,
"records": 14,
"added": 0,
"removed": 0,
"attrs": 13,
"text": 1
}
],
"frames": {
"count": 264,
"average": 113.8840909090909,
"p95": 233.29999999999927,
"p99": 300
},
"longTasks": {
"count": 81,
"top": [
{
"startTime": 311.5999999977648,
"duration": 375,
"name": "self"
},
{
"startTime": 1978.699999999255,
"duration": 299,
"name": "self"
},
{
"startTime": 1254.5999999977648,
"duration": 229,
"name": "self"
},
{
"startTime": 1772,
"duration": 206,
"name": "self"
},
{
"startTime": 1485.3999999985099,
"duration": 170,
"name": "self"
},
{
"startTime": 2646.800000000745,
"duration": 165,
"name": "self"
},
{
"startTime": 2507.5,
"duration": 139,
"name": "self"
},
{
"startTime": 693.8000000007451,
"duration": 125,
"name": "self"
},
{
"startTime": 3026.800000000745,
"duration": 119,
"name": "self"
},
{
"startTime": 4148.39999999851,
"duration": 113,
"name": "self"
},
{
"startTime": 3461.099999997765,
"duration": 109,
"name": "self"
},
{
"startTime": 9812.89999999851,
"duration": 106,
"name": "self"
},
{
"startTime": 14821.39999999851,
"duration": 100,
"name": "self"
},
{
"startTime": 3145.800000000745,
"duration": 97,
"name": "self"
},
{
"startTime": 4447.39999999851,
"duration": 96,
"name": "self"
},
{
"startTime": 9551,
"duration": 96,
"name": "self"
},
{
"startTime": 9939.300000000745,
"duration": 95,
"name": "self"
},
{
"startTime": 18100.89999999851,
"duration": 92,
"name": "self"
},
{
"startTime": 23114.89999999851,
"duration": 92,
"name": "self"
},
{
"startTime": 18302.800000000745,
"duration": 88,
"name": "self"
}
]
},
"consoleErrors": []
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 MiB

@@ -1,723 +0,0 @@
{
"url": "http://127.0.0.1:5173/",
"runDir": "perf/results/2026-06-12T05-27-45-384Z-root-runtime",
"environment": {
"cpuThrottle": 6,
"viewport": {
"width": 390,
"height": 844
},
"isMobile": true,
"sampleMs": 30000
},
"metrics": {
"Timestamp": 20904.330789,
"AudioHandlers": 0,
"AudioWorkletProcessors": 0,
"Documents": 1,
"Frames": 1,
"JSEventListeners": 365,
"LayoutObjects": 1886,
"MediaKeySessions": 0,
"MediaKeys": 0,
"Nodes": 2224,
"Resources": 24,
"ContextLifecycleStateObservers": 14,
"V8PerContextDatas": 2,
"WorkerGlobalScopes": 0,
"UACSSResources": 0,
"RTCPeerConnections": 0,
"ResourceFetchers": 1,
"AdSubframes": 0,
"DetachedScriptStates": 2,
"ArrayBufferContents": 1,
"LayoutCount": 11,
"RecalcStyleCount": 12,
"LayoutDuration": 0.81696,
"RecalcStyleDuration": 0.7972,
"DevToolsCommandDuration": 0.000356,
"ScriptDuration": 26.612771,
"V8CompileDuration": 0,
"TaskDuration": 34.234417,
"TaskOtherDuration": 6.00713,
"ThreadTime": 35.26645,
"ProcessTime": 42.01,
"JSHeapUsedSize": 20964432,
"JSHeapTotalSize": 35799040,
"FirstMeaningfulPaint": 0,
"DomContentLoaded": 20874.278609,
"NavigationStart": 20868.27481
},
"snapshot": {
"elapsedMs": 37773.099999997765,
"listeners": [
{
"key": "add:input:invalid",
"count": 18
},
{
"key": "add:window:online",
"count": 6
},
{
"key": "add:document:visibilitychange",
"count": 6
},
{
"key": "remove:window:online",
"count": 5
},
{
"key": "remove:document:visibilitychange",
"count": 5
},
{
"key": "add:WebSocket:message",
"count": 4
},
{
"key": "add:document:fullscreenchange",
"count": 3
},
{
"key": "add:document:webkitfullscreenchange",
"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
},
{
"key": "add:div#root:lostpointercapture",
"count": 2
},
{
"key": "add:div#root:mousedown",
"count": 2
},
{
"key": "add:div#root:mousemove",
"count": 2
},
{
"key": "add:div#root:mouseout",
"count": 2
},
{
"key": "add:div#root:mouseover",
"count": 2
},
{
"key": "add:div#root:mouseup",
"count": 2
},
{
"key": "add:div#root:paste",
"count": 2
}
],
"timers": [
{
"key": "timeout:0",
"count": 6
},
{
"key": "interval:250",
"count": 5
},
{
"key": "interval:200",
"count": 3
},
{
"key": "timeout:15000",
"count": 2
},
{
"key": "interval:1000",
"count": 2
},
{
"key": "interval:2000",
"count": 2
},
{
"key": "timeout:12000",
"count": 2
},
{
"key": "timeout:4000",
"count": 2
},
{
"key": "interval:60000",
"count": 2
},
{
"key": "interval:30000",
"count": 1
},
{
"key": "timeout:10000",
"count": 1
},
{
"key": "timeout:592",
"count": 1
}
],
"timeouts": 14,
"intervals": 15,
"timeoutFires": 9,
"intervalFires": 46,
"webSocketMessageCount": 103,
"webSocketBytes": 512324,
"webSocketEvents": [
{
"event": "log:entry",
"count": 62,
"bytes": 14692
},
{
"event": "session:sync",
"count": 14,
"bytes": 237697
},
{
"event": "mode",
"count": 2,
"bytes": 54
},
{
"event": "controlGranted",
"count": 2,
"bytes": 82
},
{
"event": "alert",
"count": 2,
"bytes": 256
},
{
"event": "alert:new",
"count": 2,
"bytes": 330
},
{
"event": "auth:role",
"count": 2,
"bytes": 62
},
{
"event": "rovers",
"count": 2,
"bytes": 10972
},
{
"event": "chat:init",
"count": 2,
"bytes": 155226
},
{
"event": "overseer:memory",
"count": 2,
"bytes": 1040
},
{
"event": "log:init",
"count": 2,
"bytes": 87839
},
{
"event": "barcode:state",
"count": 2,
"bytes": 3254
},
{
"event": "kinect:pointCloudFrame",
"count": 2,
"bytes": 524
},
{
"event": "{\"type\":\"con",
"count": 1,
"bytes": 20
},
{
"event": "0{\"sid\":\"SuW",
"count": 1,
"bytes": 106
},
{
"event": "40{\"sid\":\"Wx",
"count": 1,
"bytes": 32
},
{
"event": "0{\"sid\":\"mxD",
"count": 1,
"bytes": 106
},
{
"event": "40{\"sid\":\"mW",
"count": 1,
"bytes": 32
}
],
"mutationsTotal": {
"records": 536,
"added": 170,
"removed": 11,
"attrs": 346,
"text": 9
},
"mutationTargets": [
{
"key": "input.accent-emerald-400:attributes:name",
"count": 102
},
{
"key": "input.accent-emerald-400:attributes:type",
"count": 51
},
{
"key": "input.field-input.w-full:attributes:name",
"count": 28
},
{
"key": "input.field-input.flex-1:attributes:name",
"count": 24
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:name",
"count": 18
},
{
"key": "input.accent-cyan-500:attributes:name",
"count": 18
},
{
"key": "input.field-input.chat-composer-input:attributes:name",
"count": 14
},
{
"key": "input:attributes:name",
"count": 14
},
{
"key": "input.field-input.w-full:attributes:type",
"count": 14
},
{
"key": "section.panel-section.border:attributes:style",
"count": 10
},
{
"key": "Text:characterData:",
"count": 9
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:type",
"count": 9
},
{
"key": "header.flex.items-center:attributes:style",
"count": 9
},
{
"key": "input.accent-cyan-500:attributes:type",
"count": 9
},
{
"key": "input:attributes:type",
"count": 7
},
{
"key": "div.rounded-sm.px-2:attributes:class",
"count": 3
},
{
"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": "div.fixed.bottom-2:attributes:class",
"count": 2
},
{
"key": "input.min-w-0.flex-1:attributes:name",
"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": "span.inline-flex.items-center:attributes:class",
"count": 1
},
{
"key": "button.button-dark.min-w-0:attributes:disabled",
"count": 1
}
],
"mutationBursts": [
{
"at": 17875,
"records": 171,
"added": 117,
"removed": 6,
"attrs": 46,
"text": 2
},
{
"at": 24975,
"records": 62,
"added": 5,
"removed": 0,
"attrs": 57,
"text": 0
},
{
"at": 37525,
"records": 56,
"added": 4,
"removed": 2,
"attrs": 50,
"text": 0
},
{
"at": 29221,
"records": 54,
"added": 1,
"removed": 1,
"attrs": 50,
"text": 2
},
{
"at": 33394,
"records": 51,
"added": 0,
"removed": 1,
"attrs": 49,
"text": 1
},
{
"at": 29801,
"records": 28,
"added": 0,
"removed": 0,
"attrs": 27,
"text": 1
},
{
"at": 30093,
"records": 28,
"added": 0,
"removed": 1,
"attrs": 27,
"text": 0
},
{
"at": 245,
"records": 27,
"added": 27,
"removed": 0,
"attrs": 0,
"text": 0
},
{
"at": 11721,
"records": 23,
"added": 2,
"removed": 0,
"attrs": 20,
"text": 1
},
{
"at": 10636,
"records": 20,
"added": 0,
"removed": 0,
"attrs": 20,
"text": 0
},
{
"at": 218,
"records": 6,
"added": 6,
"removed": 0,
"attrs": 0,
"text": 0
},
{
"at": 272,
"records": 3,
"added": 3,
"removed": 0,
"attrs": 0,
"text": 0
},
{
"at": 227,
"records": 2,
"added": 2,
"removed": 0,
"attrs": 0,
"text": 0
},
{
"at": 352,
"records": 2,
"added": 0,
"removed": 0,
"attrs": 0,
"text": 2
},
{
"at": 282,
"records": 1,
"added": 1,
"removed": 0,
"attrs": 0,
"text": 0
},
{
"at": 5981,
"records": 1,
"added": 1,
"removed": 0,
"attrs": 0,
"text": 0
},
{
"at": 8409,
"records": 1,
"added": 1,
"removed": 0,
"attrs": 0,
"text": 0
}
],
"frames": {
"count": 65,
"average": 571.7707692307692,
"p95": 3433.0999999999985,
"p99": 6416.4
},
"longTasks": {
"count": 34,
"top": [
{
"startTime": 12843.900000002235,
"duration": 5032,
"name": "self"
},
{
"startTime": 25604.5,
"duration": 3618,
"name": "self"
},
{
"startTime": 21451,
"duration": 3524,
"name": "self"
},
{
"startTime": 34259.10000000149,
"duration": 3267,
"name": "self"
},
{
"startTime": 30382.300000000745,
"duration": 3012,
"name": "self"
},
{
"startTime": 3396.2000000029802,
"duration": 2633,
"name": "self"
},
{
"startTime": 6155.400000002235,
"duration": 2255,
"name": "self"
},
{
"startTime": 19590.10000000149,
"duration": 1285,
"name": "self"
},
{
"startTime": 17883.60000000149,
"duration": 1281,
"name": "self"
},
{
"startTime": 8914.300000000745,
"duration": 653,
"name": "self"
},
{
"startTime": 9985.5,
"duration": 651,
"name": "self"
},
{
"startTime": 11118.60000000149,
"duration": 603,
"name": "self"
},
{
"startTime": 8423.300000000745,
"duration": 370,
"name": "self"
},
{
"startTime": 24981.70000000298,
"duration": 323,
"name": "self"
},
{
"startTime": 29579.20000000298,
"duration": 222,
"name": "self"
},
{
"startTime": 30099.400000002235,
"duration": 200,
"name": "self"
},
{
"startTime": 29914.10000000149,
"duration": 179,
"name": "self"
},
{
"startTime": 25344,
"duration": 177,
"name": "self"
},
{
"startTime": 21260.20000000298,
"duration": 140,
"name": "self"
},
{
"startTime": 29228.300000000745,
"duration": 124,
"name": "self"
}
]
},
"consoleErrors": []
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

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

Before

Width:  |  Height:  |  Size: 165 KiB

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

Before

Width:  |  Height:  |  Size: 160 KiB

@@ -1,486 +0,0 @@
{
"url": "http://127.0.0.1:5173/",
"runDir": "perf/results/2026-06-12T05-30-10-213Z",
"elapsedMs": 184508,
"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": "http://127.0.0.1:5173/",
"environment": {
"cpuThrottle": 8,
"viewport": {
"width": 390,
"height": 844
},
"isMobile": true
},
"metrics": {
"afterLoad": {
"Timestamp": 21026.536855,
"Documents": 3,
"Frames": 1,
"JSEventListeners": 213,
"Nodes": 310,
"LayoutCount": 4,
"RecalcStyleCount": 6,
"LayoutDuration": 0.351349,
"RecalcStyleDuration": 0.164,
"ScriptDuration": 8.025346,
"TaskDuration": 12.3993,
"JSHeapUsedSize": 8055732,
"JSHeapTotalSize": 9846784
},
"end": {
"Timestamp": 21172.00552,
"Documents": 1,
"Frames": 1,
"JSEventListeners": 776,
"Nodes": 2366,
"LayoutCount": 28,
"RecalcStyleCount": 51,
"LayoutDuration": 2.921363,
"RecalcStyleDuration": 2.465639,
"ScriptDuration": 125.009213,
"TaskDuration": 154.711298,
"JSHeapUsedSize": 49187212,
"JSHeapTotalSize": 81772544
},
"delta": {
"Timestamp": 145.46866500000033,
"Documents": -2,
"Frames": 0,
"JSEventListeners": 563,
"Nodes": 2056,
"LayoutCount": 24,
"RecalcStyleCount": 45,
"LayoutDuration": 2.570014,
"RecalcStyleDuration": 2.3016389999999998,
"ScriptDuration": 116.983867,
"TaskDuration": 142.311998,
"JSHeapUsedSize": 41131480,
"JSHeapTotalSize": 71925760
}
},
"cpuSummary": {
"totalProfileMs": 109222.492,
"sampleCount": 82521,
"topSelfTime": [
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=7eee8a21",
"functionName": "exports.jsxDEV",
"lineNumber": 238,
"columnNumber": 31,
"selfMs": 50684.10400000017,
"samples": 40987
},
{
"url": "",
"functionName": "(program)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 11988.050000000003,
"samples": 3951
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/chunk-YX2XZWS3.js?v=7eee8a21",
"functionName": "exports.createElement",
"lineNumber": 760,
"columnNumber": 38,
"selfMs": 6174.760000000029,
"samples": 5020
},
{
"url": "",
"functionName": "createTask",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 4341.4680000000035,
"samples": 3581
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=7eee8a21",
"functionName": "ReactElement",
"lineNumber": 126,
"columnNumber": 27,
"selfMs": 4147.347000000001,
"samples": 3387
},
{
"url": "",
"functionName": "run",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 2896.0120000000024,
"samples": 2303
},
{
"url": "",
"functionName": "(idle)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 1528.1939999999993,
"samples": 1205
},
{
"url": "",
"functionName": "(garbage collector)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 1454.5849999999996,
"samples": 1076
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=7eee8a21",
"functionName": "getTaskName",
"lineNumber": 80,
"columnNumber": 26,
"selfMs": 1229.8500000000001,
"samples": 1028
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=7eee8a21",
"functionName": "runWithFiberInDEV",
"lineNumber": 992,
"columnNumber": 32,
"selfMs": 982.0620000000005,
"samples": 808
},
{
"url": "",
"functionName": "now",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 786.4740000000004,
"samples": 634
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=7eee8a21",
"functionName": "validateProperty",
"lineNumber": 2344,
"columnNumber": 31,
"selfMs": 754.6519999999999,
"samples": 611
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=7eee8a21",
"functionName": "updatedAncestorInfoDev",
"lineNumber": 1990,
"columnNumber": 37,
"selfMs": 746.4530000000003,
"samples": 607
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=7eee8a21",
"functionName": "jsxDEVImpl",
"lineNumber": 167,
"columnNumber": 25,
"selfMs": 731.4129999999996,
"samples": 598
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=7eee8a21",
"functionName": "react_stack_bottom_frame",
"lineNumber": 18504,
"columnNumber": 42,
"selfMs": 652.937,
"samples": 533
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/chunk-YX2XZWS3.js?v=7eee8a21",
"functionName": "ReactElement",
"lineNumber": 165,
"columnNumber": 27,
"selfMs": 580.7530000000002,
"samples": 462
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=7eee8a21",
"functionName": "updateProperties",
"lineNumber": 14785,
"columnNumber": 31,
"selfMs": 517.7620000000002,
"samples": 424
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=7eee8a21",
"functionName": "createWorkInProgress",
"lineNumber": 3630,
"columnNumber": 35,
"selfMs": 508.02600000000007,
"samples": 409
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=7eee8a21",
"functionName": "ReactStrictModeWarnings.recordLegacyContextWarning",
"lineNumber": 18468,
"columnNumber": 67,
"selfMs": 495.80399999999986,
"samples": 409
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=7eee8a21",
"functionName": "commitUpdate",
"lineNumber": 15886,
"columnNumber": 27,
"selfMs": 477.684,
"samples": 370
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=7eee8a21",
"functionName": "beginWork",
"lineNumber": 8415,
"columnNumber": 24,
"selfMs": 446.36999999999983,
"samples": 356
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=7eee8a21",
"functionName": "reconcileChildrenArray",
"lineNumber": 4935,
"columnNumber": 39,
"selfMs": 436.57999999999987,
"samples": 350
},
{
"url": "http://127.0.0.1:5173/src/components/ChatMessageRow/index.jsx",
"functionName": "ChatMessageRow",
"lineNumber": 195,
"columnNumber": 38,
"selfMs": 413.4399999999997,
"samples": 344
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=7eee8a21",
"functionName": "getComponentNameFromType",
"lineNumber": 12,
"columnNumber": 39,
"selfMs": 408.42100000000005,
"samples": 327
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=7eee8a21",
"functionName": "addObjectDiffToProperties",
"lineNumber": 3080,
"columnNumber": 40,
"selfMs": 388.74499999999983,
"samples": 298
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=7eee8a21",
"functionName": "setValueForStyles",
"lineNumber": 2231,
"columnNumber": 32,
"selfMs": 388.087,
"samples": 311
},
{
"url": "",
"functionName": "measure",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 382.2370000000001,
"samples": 317
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=7eee8a21",
"functionName": "(anonymous)",
"lineNumber": 5231,
"columnNumber": 23,
"selfMs": 372.06799999999976,
"samples": 277
},
{
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=7eee8a21",
"functionName": "logComponentRender",
"lineNumber": 3166,
"columnNumber": 33,
"selfMs": 342.3039999999999,
"samples": 277
},
{
"url": "http://127.0.0.1:5173/src/context/SessionContext.jsx",
"functionName": "(anonymous)",
"lineNumber": 331,
"columnNumber": 20,
"selfMs": 337.47,
"samples": 275
}
]
},
"responsiveness": {
"longTaskCount": 83,
"topLongTasks": [
{
"name": "self",
"startTime": 81802.5,
"duration": 17746
},
{
"name": "self",
"startTime": 54463,
"duration": 6694
},
{
"name": "self",
"startTime": 99677.39999999851,
"duration": 6024
},
{
"name": "self",
"startTime": 21612.599999997765,
"duration": 5858
},
{
"name": "self",
"startTime": 110758.5,
"duration": 5803
},
{
"name": "self",
"startTime": 62108,
"duration": 5772
},
{
"name": "self",
"startTime": 76151.89999999851,
"duration": 5650
},
{
"name": "self",
"startTime": 125421.19999999925,
"duration": 5648
},
{
"name": "self",
"startTime": 145249.3999999985,
"duration": 5587
},
{
"name": "self",
"startTime": 132005.09999999776,
"duration": 5514
},
{
"name": "self",
"startTime": 68337.5,
"duration": 5496
},
{
"name": "self",
"startTime": 118182.59999999776,
"duration": 5266
},
{
"name": "self",
"startTime": 40730,
"duration": 5224
},
{
"name": "self",
"startTime": 138897,
"duration": 5148
},
{
"name": "self",
"startTime": 151428.19999999925,
"duration": 5134
},
{
"name": "self",
"startTime": 106092.80000000075,
"duration": 4665
},
{
"name": "self",
"startTime": 32223.39999999851,
"duration": 4328
},
{
"name": "self",
"startTime": 4601.89999999851,
"duration": 3794
},
{
"name": "self",
"startTime": 8585.39999999851,
"duration": 3021
},
{
"name": "self",
"startTime": 16030.39999999851,
"duration": 2773
}
],
"frameCount": 152,
"averageFrameMs": 1041.5151315789474,
"p95FrameMs": 5816.499999999985,
"p99FrameMs": 10849.5,
"intervalDriftCount": 37,
"averageIntervalDriftMs": 3979.5216216216418,
"p95IntervalDriftMs": 13818.400000002235,
"p99IntervalDriftMs": 29643.89999999851
},
"consoleMessages": [
{
"type": "debug",
"text": "[vite] connecting...",
"location": {
"url": "http://127.0.0.1:5173/@vite/client",
"line": 732,
"column": 8,
"lineNumber": 732,
"columnNumber": 8
}
},
{
"type": "debug",
"text": "[vite] connected.",
"location": {
"url": "http://127.0.0.1:5173/@vite/client",
"line": 826,
"column": 11,
"lineNumber": 826,
"columnNumber": 11
}
},
{
"type": "info",
"text": "%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold",
"location": {
"url": "http://127.0.0.1:5173/node_modules/.vite/deps/react-dom_client.js?v=7eee8a21",
"line": 20102,
"column": 53,
"lineNumber": 20102,
"columnNumber": 53
}
},
{
"type": "info",
"text": "[socket] connecting to https://rover.otter.land",
"location": {
"url": "http://127.0.0.1:5173/src/lib/socket.js",
"line": 7,
"column": 8,
"lineNumber": 7,
"columnNumber": 8
}
},
{
"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
}
]
}
@@ -1,532 +0,0 @@
{
"url": "http://127.0.0.1:4173/",
"runDir": "perf/results/2026-06-12T05-34-37-713Z-root-runtime",
"environment": {
"cpuThrottle": 6,
"viewport": {
"width": 390,
"height": 844
},
"isMobile": true,
"sampleMs": 30000
},
"metrics": {
"Timestamp": 21313.290308,
"AudioHandlers": 0,
"AudioWorkletProcessors": 0,
"Documents": 1,
"Frames": 1,
"JSEventListeners": 1405,
"LayoutObjects": 243,
"MediaKeySessions": 0,
"MediaKeys": 0,
"Nodes": 353,
"Resources": 0,
"ContextLifecycleStateObservers": 7,
"V8PerContextDatas": 2,
"WorkerGlobalScopes": 0,
"UACSSResources": 0,
"RTCPeerConnections": 0,
"ResourceFetchers": 1,
"AdSubframes": 0,
"DetachedScriptStates": 2,
"ArrayBufferContents": 1,
"LayoutCount": 5,
"RecalcStyleCount": 6,
"LayoutDuration": 0.274398,
"RecalcStyleDuration": 0.205702,
"DevToolsCommandDuration": 0.000402,
"ScriptDuration": 22.219943,
"V8CompileDuration": 0,
"TaskDuration": 31.430573,
"TaskOtherDuration": 8.730128,
"ThreadTime": 31.88879,
"ProcessTime": 36.72,
"JSHeapUsedSize": 5442836,
"JSHeapTotalSize": 9699328,
"FirstMeaningfulPaint": 0,
"DomContentLoaded": 21283.254568,
"NavigationStart": 21280.58028
},
"snapshot": {
"elapsedMs": 32642,
"listeners": [
{
"key": "add:input:invalid",
"count": 9
},
{
"key": "add:WebSocket:message",
"count": 7
},
{
"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
},
{
"key": "add:div#root:lostpointercapture",
"count": 2
},
{
"key": "add:div#root:mousedown",
"count": 2
},
{
"key": "add:div#root:mousemove",
"count": 2
},
{
"key": "add:div#root:mouseout",
"count": 2
},
{
"key": "add:div#root:mouseover",
"count": 2
},
{
"key": "add:div#root:mouseup",
"count": 2
},
{
"key": "add:div#root:paste",
"count": 2
},
{
"key": "add:div#root:pointercancel",
"count": 2
},
{
"key": "add:div#root:pointerdown",
"count": 2
},
{
"key": "add:div#root:pointermove",
"count": 2
},
{
"key": "add:div#root:pointerout",
"count": 2
},
{
"key": "add:div#root:pointerover",
"count": 2
},
{
"key": "add:div#root:pointerup",
"count": 2
},
{
"key": "add:div#root:reset",
"count": 2
},
{
"key": "add:div#root:submit",
"count": 2
},
{
"key": "add:div#root:touchcancel",
"count": 2
},
{
"key": "add:div#root:touchend",
"count": 2
},
{
"key": "add:div#root:touchstart",
"count": 2
},
{
"key": "add:div#root:touchmove",
"count": 2
},
{
"key": "add:div#root:wheel",
"count": 2
}
],
"timers": [
{
"key": "timeout:15000",
"count": 7
},
{
"key": "timeout:5000",
"count": 5
},
{
"key": "timeout:0",
"count": 3
},
{
"key": "interval:1000",
"count": 1
},
{
"key": "interval:2000",
"count": 1
},
{
"key": "timeout:1299",
"count": 1
},
{
"key": "timeout:1507",
"count": 1
}
],
"timeouts": 17,
"intervals": 2,
"timeoutFires": 9,
"intervalFires": 42,
"webSocketMessageCount": 0,
"webSocketBytes": 0,
"webSocketEvents": [],
"mutationsTotal": {
"records": 6000,
"added": 0,
"removed": 0,
"attrs": 6000,
"text": 0
},
"mutationTargets": [
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:name",
"count": 8054
},
{
"key": "input.accent-emerald-400:attributes:name",
"count": 8054
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:type",
"count": 4027
},
{
"key": "input.accent-emerald-400:attributes:type",
"count": 4027
},
{
"key": "input.field-input.w-full:attributes:name",
"count": 8
},
{
"key": "input.field-input.flex-1:attributes:name",
"count": 4
},
{
"key": "input.field-input.chat-composer-input:attributes:name",
"count": 4
},
{
"key": "input:attributes:name",
"count": 4
},
{
"key": "input.field-input.w-full:attributes:type",
"count": 4
},
{
"key": "Text:characterData:",
"count": 2
},
{
"key": "input:attributes:type",
"count": 2
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:value",
"count": 1
}
],
"mutationBursts": [
{
"at": 26663,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26667,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26670,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26676,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26679,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26682,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26684,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26689,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26696,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26700,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26704,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26711,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26715,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26719,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26726,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26730,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26732,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26737,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26744,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
},
{
"at": 26748,
"records": 6,
"added": 0,
"removed": 0,
"attrs": 6,
"text": 0
}
],
"frames": {
"count": 1678,
"average": 19.307985697258644,
"p95": 16.700000000000728,
"p99": 16.80000000000291
},
"longTasks": {
"count": 8,
"top": [
{
"startTime": 625.4000000022352,
"duration": 2057,
"name": "self"
},
{
"startTime": 2705.400000002235,
"duration": 1120,
"name": "self"
},
{
"startTime": 3825.900000002235,
"duration": 400,
"name": "self"
},
{
"startTime": 4491.699999999255,
"duration": 196,
"name": "self"
},
{
"startTime": 4237.300000000745,
"duration": 158,
"name": "self"
},
{
"startTime": 4931.5,
"duration": 115,
"name": "self"
},
{
"startTime": 4828.699999999255,
"duration": 101,
"name": "self"
},
{
"startTime": 25127.300000000745,
"duration": 67,
"name": "self"
}
]
},
"consoleErrors": [
"connect_error undefined websocket error undefined",
"connect_error undefined websocket error undefined",
"connect_error undefined websocket error undefined",
"connect_error undefined websocket error undefined",
"connect_error undefined websocket error undefined",
"connect_error undefined websocket error undefined",
"connect_error undefined websocket error undefined"
]
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

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

Before

Width:  |  Height:  |  Size: 86 KiB

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

Before

Width:  |  Height:  |  Size: 86 KiB

@@ -1,652 +0,0 @@
{
"url": "http://127.0.0.1:4173/",
"runDir": "perf/results/2026-06-12T05-37-11-679Z",
"elapsedMs": 48144,
"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": "http://127.0.0.1:4173/",
"environment": {
"cpuThrottle": 8,
"viewport": {
"width": 390,
"height": 844
},
"isMobile": true
},
"metrics": {
"afterLoad": {
"Timestamp": 21442.422344,
"Documents": 3,
"Frames": 1,
"JSEventListeners": 333,
"Nodes": 364,
"LayoutCount": 5,
"RecalcStyleCount": 7,
"LayoutDuration": 0.567739,
"RecalcStyleDuration": 0.239019,
"ScriptDuration": 5.57054,
"TaskDuration": 7.681989,
"JSHeapUsedSize": 3910380,
"JSHeapTotalSize": 6815744
},
"end": {
"Timestamp": 21481.571566,
"Documents": 3,
"Frames": 1,
"JSEventListeners": 3020,
"Nodes": 424,
"LayoutCount": 10,
"RecalcStyleCount": 20,
"LayoutDuration": 0.71593,
"RecalcStyleDuration": 0.592888,
"ScriptDuration": 25.51692,
"TaskDuration": 45.394233,
"JSHeapUsedSize": 7257516,
"JSHeapTotalSize": 8486912
},
"delta": {
"Timestamp": 39.14922200000001,
"Documents": 0,
"Frames": 0,
"JSEventListeners": 2687,
"Nodes": 60,
"LayoutCount": 5,
"RecalcStyleCount": 13,
"LayoutDuration": 0.14819099999999996,
"RecalcStyleDuration": 0.353869,
"ScriptDuration": 19.946379999999998,
"TaskDuration": 37.712244,
"JSHeapUsedSize": 3347136,
"JSHeapTotalSize": 1671168
}
},
"cpuSummary": {
"totalProfileMs": 36856.206,
"sampleCount": 27449,
"topSelfTime": [
{
"url": "",
"functionName": "(program)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 16008.739999999993,
"samples": 10998
},
{
"url": "",
"functionName": "postMessage",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 1400.7229999999997,
"samples": 1076
},
{
"url": "",
"functionName": "(idle)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 1159.0759999999996,
"samples": 921
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "lt",
"lineNumber": 8,
"columnNumber": 7349,
"selfMs": 939.4450000000005,
"samples": 747
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "ad",
"lineNumber": 9,
"columnNumber": 7898,
"selfMs": 678.3770000000004,
"samples": 534
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "Nc",
"lineNumber": 11,
"columnNumber": 94591,
"selfMs": 648.5520000000006,
"samples": 533
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "(anonymous)",
"lineNumber": 17,
"columnNumber": 58982,
"selfMs": 609.3419999999993,
"samples": 479
},
{
"url": "",
"functionName": "requestAnimationFrame",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 569.4970000000001,
"samples": 461
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "C9",
"lineNumber": 8,
"columnNumber": 94304,
"selfMs": 480.7910000000001,
"samples": 384
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "Z4",
"lineNumber": 8,
"columnNumber": 28258,
"selfMs": 403.8930000000001,
"samples": 305
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "qa",
"lineNumber": 8,
"columnNumber": 23434,
"selfMs": 401.37100000000004,
"samples": 328
},
{
"url": "",
"functionName": "queueMicrotask",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 378.9960000000001,
"samples": 289
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "B6",
"lineNumber": 8,
"columnNumber": 34145,
"selfMs": 375.95699999999994,
"samples": 307
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "a2",
"lineNumber": 9,
"columnNumber": 71,
"selfMs": 349.62499999999983,
"samples": 295
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "c5",
"lineNumber": 8,
"columnNumber": 34654,
"selfMs": 345.6060000000002,
"samples": 274
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "K4",
"lineNumber": 8,
"columnNumber": 79484,
"selfMs": 314.27200000000005,
"samples": 240
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "we",
"lineNumber": 11,
"columnNumber": 73874,
"selfMs": 306.5540000000002,
"samples": 251
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "C1",
"lineNumber": 8,
"columnNumber": 40723,
"selfMs": 284.553,
"samples": 221
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "Eh",
"lineNumber": 8,
"columnNumber": 95724,
"selfMs": 280.467,
"samples": 229
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "Kl",
"lineNumber": 8,
"columnNumber": 117211,
"selfMs": 277.51900000000006,
"samples": 215
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "U8",
"lineNumber": 8,
"columnNumber": 7096,
"selfMs": 276.48700000000014,
"samples": 222
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "z2",
"lineNumber": 8,
"columnNumber": 49302,
"selfMs": 276.39199999999994,
"samples": 206
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "Kt",
"lineNumber": 8,
"columnNumber": 48095,
"selfMs": 273.0230000000001,
"samples": 220
},
{
"url": "",
"functionName": "(garbage collector)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 268.44300000000004,
"samples": 207
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "d1",
"lineNumber": 11,
"columnNumber": 70367,
"selfMs": 268.08499999999987,
"samples": 219
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "zl",
"lineNumber": 8,
"columnNumber": 80827,
"selfMs": 263.652,
"samples": 209
},
{
"url": "",
"functionName": "now",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 259.23800000000006,
"samples": 210
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "Fl",
"lineNumber": 8,
"columnNumber": 101199,
"selfMs": 229.16799999999992,
"samples": 178
},
{
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"functionName": "$",
"lineNumber": 1,
"columnNumber": 10516,
"selfMs": 228.36400000000006,
"samples": 178
},
{
"url": "",
"functionName": "removeAttribute",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 224.73000000000005,
"samples": 182
}
]
},
"responsiveness": {
"longTaskCount": 19,
"topLongTasks": [
{
"name": "self",
"startTime": 577.8000000007451,
"duration": 2326
},
{
"name": "self",
"startTime": 2934,
"duration": 1390
},
{
"name": "self",
"startTime": 4326.39999999851,
"duration": 671
},
{
"name": "unknown",
"startTime": 8756.5,
"duration": 354
},
{
"name": "self",
"startTime": 5309,
"duration": 284
},
{
"name": "self",
"startTime": 5012.89999999851,
"duration": 204
},
{
"name": "self",
"startTime": 5968.5,
"duration": 199
},
{
"name": "self",
"startTime": 28664.800000000745,
"duration": 144
},
{
"name": "self",
"startTime": 5824.800000000745,
"duration": 142
},
{
"name": "self",
"startTime": 29231.199999999255,
"duration": 118
},
{
"name": "unknown",
"startTime": 9170.599999997765,
"duration": 107
},
{
"name": "self",
"startTime": 29349.599999997765,
"duration": 95
},
{
"name": "self",
"startTime": 28837.89999999851,
"duration": 89
},
{
"name": "self",
"startTime": 27044.800000000745,
"duration": 80
},
{
"name": "self",
"startTime": 9278.300000000745,
"duration": 73
},
{
"name": "self",
"startTime": 30803.39999999851,
"duration": 70
},
{
"name": "self",
"startTime": 5632.39999999851,
"duration": 60
},
{
"name": "self",
"startTime": 9110.800000000745,
"duration": 59
},
{
"name": "self",
"startTime": 9434.800000000745,
"duration": 53
}
],
"frameCount": 2103,
"averageFrameMs": 22.205515929624347,
"p95FrameMs": 16.799999999999272,
"p99FrameMs": 100,
"intervalDriftCount": 155,
"averageIntervalDriftMs": 52.86322580645642,
"p95IntervalDriftMs": 113.59999999776483,
"p99IntervalDriftMs": 2354.099999997765
},
"consoleMessages": [
{
"type": "info",
"text": "[socket] connecting to http://127.0.0.1:4173",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 63768,
"lineNumber": 11,
"columnNumber": 63768
}
},
{
"type": "error",
"text": "WebSocket connection to 'ws://127.0.0.1:4173/socket.io/?EIO=4&transport=websocket' failed: Error during WebSocket handshake: Unexpected response code: 200",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 0,
"lineNumber": 11,
"columnNumber": 0
}
},
{
"type": "error",
"text": "connect_error undefined websocket error undefined",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 63988,
"lineNumber": 11,
"columnNumber": 63988
}
},
{
"type": "error",
"text": "WebSocket connection to 'ws://127.0.0.1:4173/socket.io/?EIO=4&transport=websocket' failed: Error during WebSocket handshake: Unexpected response code: 200",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 0,
"lineNumber": 11,
"columnNumber": 0
}
},
{
"type": "error",
"text": "connect_error undefined websocket error undefined",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 63988,
"lineNumber": 11,
"columnNumber": 63988
}
},
{
"type": "error",
"text": "WebSocket connection to 'ws://127.0.0.1:4173/socket.io/?EIO=4&transport=websocket' failed: Error during WebSocket handshake: Unexpected response code: 200",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 0,
"lineNumber": 11,
"columnNumber": 0
}
},
{
"type": "error",
"text": "connect_error undefined websocket error undefined",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 63988,
"lineNumber": 11,
"columnNumber": 63988
}
},
{
"type": "error",
"text": "WebSocket connection to 'ws://127.0.0.1:4173/socket.io/?EIO=4&transport=websocket' failed: Error during WebSocket handshake: Unexpected response code: 200",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 0,
"lineNumber": 11,
"columnNumber": 0
}
},
{
"type": "error",
"text": "connect_error undefined websocket error undefined",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 63988,
"lineNumber": 11,
"columnNumber": 63988
}
},
{
"type": "error",
"text": "WebSocket connection to 'ws://127.0.0.1:4173/socket.io/?EIO=4&transport=websocket' failed: Error during WebSocket handshake: Unexpected response code: 200",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 0,
"lineNumber": 11,
"columnNumber": 0
}
},
{
"type": "error",
"text": "connect_error undefined websocket error undefined",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 63988,
"lineNumber": 11,
"columnNumber": 63988
}
},
{
"type": "error",
"text": "WebSocket connection to 'ws://127.0.0.1:4173/socket.io/?EIO=4&transport=websocket' failed: Error during WebSocket handshake: Unexpected response code: 200",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 0,
"lineNumber": 11,
"columnNumber": 0
}
},
{
"type": "error",
"text": "connect_error undefined websocket error undefined",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 63988,
"lineNumber": 11,
"columnNumber": 63988
}
},
{
"type": "error",
"text": "WebSocket connection to 'ws://127.0.0.1:4173/socket.io/?EIO=4&transport=websocket' failed: Error during WebSocket handshake: Unexpected response code: 200",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 0,
"lineNumber": 11,
"columnNumber": 0
}
},
{
"type": "error",
"text": "connect_error undefined websocket error undefined",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 63988,
"lineNumber": 11,
"columnNumber": 63988
}
},
{
"type": "error",
"text": "WebSocket connection to 'ws://127.0.0.1:4173/socket.io/?EIO=4&transport=websocket' failed: Error during WebSocket handshake: Unexpected response code: 200",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 0,
"lineNumber": 11,
"columnNumber": 0
}
},
{
"type": "error",
"text": "connect_error undefined websocket error undefined",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 63988,
"lineNumber": 11,
"columnNumber": 63988
}
},
{
"type": "error",
"text": "WebSocket connection to 'ws://127.0.0.1:4173/socket.io/?EIO=4&transport=websocket' failed: Error during WebSocket handshake: Unexpected response code: 200",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 0,
"lineNumber": 11,
"columnNumber": 0
}
},
{
"type": "error",
"text": "connect_error undefined websocket error undefined",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 63988,
"lineNumber": 11,
"columnNumber": 63988
}
},
{
"type": "error",
"text": "WebSocket connection to 'ws://127.0.0.1:4173/socket.io/?EIO=4&transport=websocket' failed: Error during WebSocket handshake: Unexpected response code: 200",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 0,
"lineNumber": 11,
"columnNumber": 0
}
},
{
"type": "error",
"text": "connect_error undefined websocket error undefined",
"location": {
"url": "http://127.0.0.1:4173/assets/index-DhJId5qi.js",
"line": 11,
"column": 63988,
"lineNumber": 11,
"columnNumber": 63988
}
}
],
"failedRequests": [],
"errorResponses": []
}
@@ -1,849 +0,0 @@
{
"url": "http://127.0.0.1:4173/",
"runDir": "perf/results/2026-06-12T06-07-57-124Z-root-runtime",
"environment": {
"cpuThrottle": 6,
"viewport": {
"width": 390,
"height": 844
},
"isMobile": true,
"sampleMs": 10000
},
"metrics": {
"Timestamp": 23292.158609,
"AudioHandlers": 0,
"AudioWorkletProcessors": 0,
"Documents": 3,
"Frames": 1,
"JSEventListeners": 398,
"LayoutObjects": 1945,
"MediaKeySessions": 0,
"MediaKeys": 0,
"Nodes": 2266,
"Resources": 18,
"ContextLifecycleStateObservers": 13,
"V8PerContextDatas": 4,
"WorkerGlobalScopes": 0,
"UACSSResources": 0,
"RTCPeerConnections": 0,
"ResourceFetchers": 3,
"AdSubframes": 0,
"DetachedScriptStates": 2,
"ArrayBufferContents": 3,
"LayoutCount": 12,
"RecalcStyleCount": 30,
"LayoutDuration": 0.928481,
"RecalcStyleDuration": 0.991796,
"DevToolsCommandDuration": 0.000323,
"ScriptDuration": 6.326745,
"V8CompileDuration": 0,
"TaskDuration": 11.079249,
"TaskOtherDuration": 2.831904,
"ThreadTime": 11.911973,
"ProcessTime": 14.42,
"JSHeapUsedSize": 8961292,
"JSHeapTotalSize": 12058624,
"FirstMeaningfulPaint": 0,
"DomContentLoaded": 23282.130574,
"NavigationStart": 23279.964199
},
"snapshot": {
"elapsedMs": 20101.5,
"listeners": [
{
"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
},
{
"key": "add:div#root:lostpointercapture",
"count": 2
},
{
"key": "add:div#root:mousedown",
"count": 2
},
{
"key": "add:div#root:mousemove",
"count": 2
},
{
"key": "add:div#root:mouseout",
"count": 2
},
{
"key": "add:div#root:mouseover",
"count": 2
},
{
"key": "add:div#root:mouseup",
"count": 2
},
{
"key": "add:div#root:paste",
"count": 2
},
{
"key": "add:div#root:pointercancel",
"count": 2
},
{
"key": "add:div#root:pointerdown",
"count": 2
},
{
"key": "add:div#root:pointermove",
"count": 2
}
],
"timers": [
{
"key": "timeout:0",
"count": 5
},
{
"key": "interval:250",
"count": 4
},
{
"key": "interval:60000",
"count": 3
},
{
"key": "timeout:12000",
"count": 2
},
{
"key": "interval:200",
"count": 2
},
{
"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
},
{
"key": "interval:3000",
"count": 1
}
],
"timeouts": 10,
"intervals": 12,
"timeoutFires": 4,
"intervalFires": 45,
"webSocketMessageCount": 285,
"webSocketBytes": 5610397,
"webSocketEvents": [
{
"event": "log:entry",
"count": 150,
"bytes": 35714
},
{
"event": "sensorFrame",
"count": 76,
"bytes": 142196
},
{
"event": "session:sync",
"count": 18,
"bytes": 306294
},
{
"event": "commandAck",
"count": 12,
"bytes": 1128
},
{
"event": "",
"count": 4,
"bytes": 4992972
},
{
"event": "roverSnapshot:frame",
"count": 3,
"bytes": 291
},
{
"event": "0{\"sid\":\"CQG",
"count": 1,
"bytes": 106
},
{
"event": "40{\"sid\":\"-s",
"count": 1,
"bytes": 32
},
{
"event": "mode",
"count": 1,
"bytes": 27
},
{
"event": "controlGranted",
"count": 1,
"bytes": 41
},
{
"event": "alert",
"count": 1,
"bytes": 128
},
{
"event": "alert:new",
"count": 1,
"bytes": 165
},
{
"event": "auth:role",
"count": 1,
"bytes": 31
},
{
"event": "rovers",
"count": 1,
"bytes": 5453
},
{
"event": "chat:init",
"count": 1,
"bytes": 77613
},
{
"event": "overseer:memory",
"count": 1,
"bytes": 520
},
{
"event": "log:init",
"count": 1,
"bytes": 44230
},
{
"event": "barcode:state",
"count": 1,
"bytes": 1627
},
{
"event": "kinect:pointCloudFrame",
"count": 1,
"bytes": 262
},
{
"event": "kinect:colorFrame",
"count": 1,
"bytes": 169
},
{
"event": "430[{\"succes",
"count": 1,
"bytes": 40
},
{
"event": "431[{\"succes",
"count": 1,
"bytes": 194
},
{
"event": "432[{\"url\":\"",
"count": 1,
"bytes": 133
},
{
"event": "roverHostStats",
"count": 1,
"bytes": 766
},
{
"event": "2",
"count": 1,
"bytes": 1
},
{
"event": "433[{\"url\":\"",
"count": 1,
"bytes": 145
},
{
"event": "roverSnapshot:status",
"count": 1,
"bytes": 79
},
{
"event": "WALL-E",
"count": 1,
"bytes": 40
}
],
"mutationsTotal": {
"records": 4729,
"added": 175,
"removed": 11,
"attrs": 4524,
"text": 19
},
"mutationTargets": [
{
"key": "input.accent-emerald-400:attributes:name",
"count": 1866
},
{
"key": "input.accent-emerald-400:attributes:type",
"count": 933
},
{
"key": "div.h-full:attributes:style",
"count": 580
},
{
"key": "path.[object.SVGAnimatedString]:attributes:d",
"count": 292
},
{
"key": "path.[object.SVGAnimatedString]:attributes:fill",
"count": 290
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:name",
"count": 242
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:type",
"count": 121
},
{
"key": "input.field-input.flex-1:attributes:name",
"count": 28
},
{
"key": "input.field-input.w-full:attributes:name",
"count": 28
},
{
"key": "Text:characterData:",
"count": 19
},
{
"key": "input.field-input.chat-composer-input:attributes:name",
"count": 18
},
{
"key": "input.accent-cyan-500:attributes:name",
"count": 18
},
{
"key": "input:attributes:name",
"count": 14
},
{
"key": "input.field-input.w-full:attributes:type",
"count": 14
},
{
"key": "section.panel-section.border:attributes:style",
"count": 10
},
{
"key": "input.min-w-0.flex-1:attributes:name",
"count": 10
},
{
"key": "input.accent-cyan-500:attributes:type",
"count": 9
},
{
"key": "header.flex.items-center:attributes:style",
"count": 9
},
{
"key": "input:attributes:type",
"count": 7
},
{
"key": "path.[object.SVGAnimatedString]:attributes:stroke",
"count": 6
},
{
"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": "span.inline-flex.items-center:attributes:class",
"count": 2
},
{
"key": "input.field-input.flex-1:attributes:value",
"count": 2
},
{
"key": "video.h-full.w-full:attributes:playsinline",
"count": 2
},
{
"key": "video.h-full.w-full:attributes:autoplay",
"count": 2
},
{
"key": "video.h-full.w-full:attributes:disableremoteplayback",
"count": 2
},
{
"key": "div.rounded-sm.px-2:attributes:class",
"count": 1
},
{
"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": "button.button-dark.min-w-0:attributes:disabled",
"count": 1
},
{
"key": "div.fixed.bottom-2:attributes:class",
"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": "audio:attributes:autoplay",
"count": 1
},
{
"key": "audio:attributes:disableremoteplayback",
"count": 1
}
],
"mutationBursts": [
{
"at": 7974,
"records": 241,
"added": 123,
"removed": 6,
"attrs": 110,
"text": 2
},
{
"at": 11575,
"records": 147,
"added": 11,
"removed": 2,
"attrs": 132,
"text": 2
},
{
"at": 12385,
"records": 131,
"added": 0,
"removed": 0,
"attrs": 127,
"text": 4
},
{
"at": 12981,
"records": 119,
"added": 0,
"removed": 0,
"attrs": 119,
"text": 0
},
{
"at": 18332,
"records": 117,
"added": 0,
"removed": 0,
"attrs": 116,
"text": 1
},
{
"at": 17140,
"records": 111,
"added": 0,
"removed": 0,
"attrs": 111,
"text": 0
},
{
"at": 13371,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 13636,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 13804,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 14271,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 15643,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 15909,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 18503,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 15238,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 15283,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 15415,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 15495,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 15947,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 16169,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 16317,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
}
],
"frames": {
"count": 86,
"average": 206.1918604651163,
"p95": 566.5999999999995,
"p99": 3399.8999999999996
},
"longTasks": {
"count": 61,
"top": [
{
"startTime": 5290.5,
"duration": 2689,
"name": "self"
},
{
"startTime": 365.4000000022352,
"duration": 1807,
"name": "self"
},
{
"startTime": 2182.900000002235,
"duration": 1038,
"name": "self"
},
{
"startTime": 10371.10000000149,
"duration": 709,
"name": "self"
},
{
"startTime": 9852,
"duration": 519,
"name": "self"
},
{
"startTime": 8202.60000000149,
"duration": 394,
"name": "self"
},
{
"startTime": 3221.699999999255,
"duration": 385,
"name": "self"
},
{
"startTime": 3882.5,
"duration": 356,
"name": "self"
},
{
"startTime": 11221.5,
"duration": 353,
"name": "self"
},
{
"startTime": 18042.900000002235,
"duration": 292,
"name": "self"
},
{
"startTime": 12110.900000002235,
"duration": 276,
"name": "self"
},
{
"startTime": 12738.800000000745,
"duration": 243,
"name": "self"
},
{
"startTime": 16946.5,
"duration": 195,
"name": "self"
},
{
"startTime": 11575.5,
"duration": 186,
"name": "self"
},
{
"startTime": 3615.60000000149,
"duration": 185,
"name": "self"
},
{
"startTime": 14671.699999999255,
"duration": 160,
"name": "self"
},
{
"startTime": 4239.199999999255,
"duration": 130,
"name": "self"
},
{
"startTime": 14837.5,
"duration": 110,
"name": "self"
},
{
"startTime": 15717.60000000149,
"duration": 108,
"name": "self"
},
{
"startTime": 17352.400000002235,
"duration": 107,
"name": "self"
}
]
},
"consoleErrors": []
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 680 KiB

@@ -1,900 +0,0 @@
{
"url": "http://127.0.0.1:4173/",
"runDir": "perf/results/2026-06-12T06-09-03-973Z-root-runtime",
"environment": {
"cpuThrottle": 8,
"viewport": {
"width": 390,
"height": 844
},
"isMobile": true,
"sampleMs": 30000
},
"metrics": {
"Timestamp": 23379.874681,
"AudioHandlers": 0,
"AudioWorkletProcessors": 0,
"Documents": 3,
"Frames": 1,
"JSEventListeners": 683,
"LayoutObjects": 1975,
"MediaKeySessions": 0,
"MediaKeys": 0,
"Nodes": 2710,
"Resources": 20,
"ContextLifecycleStateObservers": 20,
"V8PerContextDatas": 4,
"WorkerGlobalScopes": 0,
"UACSSResources": 0,
"RTCPeerConnections": 4,
"ResourceFetchers": 3,
"AdSubframes": 0,
"DetachedScriptStates": 2,
"ArrayBufferContents": 12,
"LayoutCount": 49,
"RecalcStyleCount": 77,
"LayoutDuration": 1.49198,
"RecalcStyleDuration": 1.886372,
"DevToolsCommandDuration": 0.000349,
"ScriptDuration": 14.419283,
"V8CompileDuration": 0,
"TaskDuration": 29.584723,
"TaskOtherDuration": 11.786739,
"ThreadTime": 32.430498,
"ProcessTime": 38.63,
"JSHeapUsedSize": 7496328,
"JSHeapTotalSize": 13107200,
"FirstMeaningfulPaint": 0,
"DomContentLoaded": 23349.78059,
"NavigationStart": 23346.788167
},
"snapshot": {
"elapsedMs": 38566.699999999255,
"listeners": [
{
"key": "add:input:invalid",
"count": 18
},
{
"key": "add:window:online",
"count": 6
},
{
"key": "add:document:visibilitychange",
"count": 6
},
{
"key": "add:audio:canplay",
"count": 6
},
{
"key": "add:audio:ended",
"count": 6
},
{
"key": "add:audio:error",
"count": 6
},
{
"key": "add:audio:pause",
"count": 6
},
{
"key": "add:audio:play",
"count": 6
},
{
"key": "add:audio:stalled",
"count": 6
},
{
"key": "add:audio:waiting",
"count": 6
},
{
"key": "remove:window:online",
"count": 5
},
{
"key": "remove:document:visibilitychange",
"count": 5
},
{
"key": "remove:audio:play",
"count": 4
},
{
"key": "remove:audio:pause",
"count": 4
},
{
"key": "remove:audio:ended",
"count": 4
},
{
"key": "remove:audio:error",
"count": 4
},
{
"key": "remove:audio:waiting",
"count": 4
},
{
"key": "remove:audio:canplay",
"count": 4
},
{
"key": "remove:audio:stalled",
"count": 4
},
{
"key": "add:WebSocket:message",
"count": 2
},
{
"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
},
{
"key": "add:div#root:lostpointercapture",
"count": 2
},
{
"key": "add:div#root:mousedown",
"count": 2
}
],
"timers": [
{
"key": "timeout:0",
"count": 5
},
{
"key": "interval:250",
"count": 4
},
{
"key": "interval:200",
"count": 4
},
{
"key": "timeout:2000",
"count": 4
},
{
"key": "timeout:12000",
"count": 3
},
{
"key": "interval:60000",
"count": 3
},
{
"key": "timeout:15000",
"count": 2
},
{
"key": "timeout:4000",
"count": 2
},
{
"key": "interval:3000",
"count": 2
},
{
"key": "interval:1000",
"count": 1
},
{
"key": "interval:2000",
"count": 1
},
{
"key": "timeout:10000",
"count": 1
},
{
"key": "timeout:1282",
"count": 1
},
{
"key": "timeout:1257",
"count": 1
}
],
"timeouts": 19,
"intervals": 15,
"timeoutFires": 10,
"intervalFires": 121,
"webSocketMessageCount": 533,
"webSocketBytes": 11199188,
"webSocketEvents": [
{
"event": "log:entry",
"count": 308,
"bytes": 71889
},
{
"event": "sensorFrame",
"count": 90,
"bytes": 168271
},
{
"event": "commandAck",
"count": 36,
"bytes": 3396
},
{
"event": "session:sync",
"count": 33,
"bytes": 560916
},
{
"event": "",
"count": 14,
"bytes": 10130394
},
{
"event": "roverSnapshot:frame",
"count": 10,
"bytes": 967
},
{
"event": "mode",
"count": 2,
"bytes": 54
},
{
"event": "controlGranted",
"count": 2,
"bytes": 82
},
{
"event": "alert",
"count": 2,
"bytes": 256
},
{
"event": "alert:new",
"count": 2,
"bytes": 330
},
{
"event": "auth:role",
"count": 2,
"bytes": 62
},
{
"event": "rovers",
"count": 2,
"bytes": 10906
},
{
"event": "chat:init",
"count": 2,
"bytes": 155226
},
{
"event": "overseer:memory",
"count": 2,
"bytes": 1040
},
{
"event": "log:init",
"count": 2,
"bytes": 89130
},
{
"event": "barcode:state",
"count": 2,
"bytes": 3254
},
{
"event": "kinect:pointCloudFrame",
"count": 2,
"bytes": 524
},
{
"event": "kinect:colorFrame",
"count": 2,
"bytes": 338
},
{
"event": "roverSnapshot:status",
"count": 2,
"bytes": 156
},
{
"event": "0{\"sid\":\"a3i",
"count": 1,
"bytes": 106
},
{
"event": "40{\"sid\":\"Bp",
"count": 1,
"bytes": 32
},
{
"event": "430[{\"succes",
"count": 1,
"bytes": 40
},
{
"event": "431[{\"succes",
"count": 1,
"bytes": 194
},
{
"event": "2",
"count": 1,
"bytes": 1
},
{
"event": "432[{\"url\":\"",
"count": 1,
"bytes": 133
},
{
"event": "433[{\"url\":\"",
"count": 1,
"bytes": 145
},
{
"event": "WALL-E",
"count": 1,
"bytes": 40
},
{
"event": "0{\"sid\":\"3kq",
"count": 1,
"bytes": 106
},
{
"event": "40{\"sid\":\"BX",
"count": 1,
"bytes": 32
},
{
"event": "436[{\"error\"",
"count": 1,
"bytes": 41
},
{
"event": "437[{\"error\"",
"count": 1,
"bytes": 41
},
{
"event": "438[{\"url\":\"",
"count": 1,
"bytes": 133
},
{
"event": "439[{\"url\":\"",
"count": 1,
"bytes": 145
},
{
"event": "Freaky",
"count": 1,
"bytes": 41
},
{
"event": "roverHostStats",
"count": 1,
"bytes": 767
}
],
"mutationsTotal": {
"records": 6037,
"added": 199,
"removed": 35,
"attrs": 5767,
"text": 36
},
"mutationTargets": [
{
"key": "input.accent-emerald-400:attributes:name",
"count": 2648
},
{
"key": "input.accent-emerald-400:attributes:type",
"count": 1324
},
{
"key": "div.h-full:attributes:style",
"count": 512
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:name",
"count": 338
},
{
"key": "path.[object.SVGAnimatedString]:attributes:d",
"count": 262
},
{
"key": "path.[object.SVGAnimatedString]:attributes:fill",
"count": 256
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:type",
"count": 169
},
{
"key": "Text:characterData:",
"count": 36
},
{
"key": "input.field-input.flex-1:attributes:name",
"count": 32
},
{
"key": "input.field-input.w-full:attributes:name",
"count": 28
},
{
"key": "input.accent-cyan-500:attributes:name",
"count": 22
},
{
"key": "section.panel-section.border:attributes:style",
"count": 20
},
{
"key": "input.field-input.chat-composer-input:attributes:name",
"count": 18
},
{
"key": "header.flex.items-center:attributes:style",
"count": 18
},
{
"key": "path.[object.SVGAnimatedString]:attributes:stroke",
"count": 18
},
{
"key": "input:attributes:name",
"count": 14
},
{
"key": "input.field-input.w-full:attributes:type",
"count": 14
},
{
"key": "input.accent-cyan-500:attributes:type",
"count": 11
},
{
"key": "input.min-w-0.flex-1:attributes:name",
"count": 10
},
{
"key": "input:attributes:type",
"count": 7
},
{
"key": "div.rounded-sm.px-2:attributes:class",
"count": 4
},
{
"key": "div.fixed.bottom-2:attributes:class",
"count": 4
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:value",
"count": 3
},
{
"key": "span.inline-flex.items-center:attributes:class",
"count": 3
},
{
"key": "button.mobile-touch-control.flex:attributes:disabled",
"count": 3
},
{
"key": "button.mobile-touch-control.min-h-9:attributes:disabled",
"count": 3
},
{
"key": "button.mobile-touch-control.flex:attributes:class",
"count": 3
},
{
"key": "div.mobile-touch-control.flex:attributes:class",
"count": 3
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:placeholder",
"count": 2
},
{
"key": "video.h-full.w-full:attributes:playsinline",
"count": 2
},
{
"key": "video.h-full.w-full:attributes:autoplay",
"count": 2
},
{
"key": "video.h-full.w-full:attributes:disableremoteplayback",
"count": 2
},
{
"key": "audio:attributes:autoplay",
"count": 2
},
{
"key": "audio:attributes:disableremoteplayback",
"count": 2
},
{
"key": "input.field-input.flex-1:attributes:value",
"count": 2
},
{
"key": "span.inline-block.rounded:attributes:style",
"count": 2
},
{
"key": "div.flex.flex-col:attributes:class",
"count": 1
},
{
"key": "button.button-dark.min-w-0:attributes:disabled",
"count": 1
},
{
"key": "input.field-input.w-full:attributes:value",
"count": 1
},
{
"key": "button.button-dark.text-sm:attributes:disabled",
"count": 1
}
],
"mutationBursts": [
{
"at": 10936,
"records": 241,
"added": 123,
"removed": 6,
"attrs": 110,
"text": 2
},
{
"at": 27789,
"records": 177,
"added": 9,
"removed": 17,
"attrs": 145,
"text": 6
},
{
"at": 17412,
"records": 133,
"added": 2,
"removed": 2,
"attrs": 118,
"text": 11
},
{
"at": 31614,
"records": 111,
"added": 2,
"removed": 1,
"attrs": 104,
"text": 4
},
{
"at": 37740,
"records": 100,
"added": 0,
"removed": 1,
"attrs": 99,
"text": 0
},
{
"at": 38360,
"records": 99,
"added": 0,
"removed": 0,
"attrs": 99,
"text": 0
},
{
"at": 17915,
"records": 59,
"added": 11,
"removed": 2,
"attrs": 45,
"text": 1
},
{
"at": 19317,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 20163,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 20363,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 23753,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 24543,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 31831,
"records": 51,
"added": 11,
"removed": 2,
"attrs": 37,
"text": 1
},
{
"at": 18579,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 18763,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 19129,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 19374,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 21234,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 22011,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 22767,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
}
],
"frames": {
"count": 181,
"average": 185.53591160220995,
"p95": 349.90000000000146,
"p99": 1849.9000000000015
},
"longTasks": {
"count": 165,
"top": [
{
"startTime": 7151.60000000149,
"duration": 3795,
"name": "self"
},
{
"startTime": 459.6000000014901,
"duration": 2544,
"name": "self"
},
{
"startTime": 3019.199999999255,
"duration": 1426,
"name": "self"
},
{
"startTime": 29673.900000002235,
"duration": 1066,
"name": "self"
},
{
"startTime": 14211,
"duration": 972,
"name": "self"
},
{
"startTime": 28901.800000000745,
"duration": 772,
"name": "self"
},
{
"startTime": 13490.699999999255,
"duration": 720,
"name": "self"
},
{
"startTime": 16782.400000002235,
"duration": 639,
"name": "self"
},
{
"startTime": 11240.199999999255,
"duration": 552,
"name": "self"
},
{
"startTime": 31092.5,
"duration": 524,
"name": "self"
},
{
"startTime": 4445.60000000149,
"duration": 495,
"name": "self"
},
{
"startTime": 5394.800000000745,
"duration": 459,
"name": "self"
},
{
"startTime": 27348.199999999255,
"duration": 445,
"name": "self"
},
{
"startTime": 24835.10000000149,
"duration": 363,
"name": "self"
},
{
"startTime": 4963.800000000745,
"duration": 272,
"name": "self"
},
{
"startTime": 37513.60000000149,
"duration": 228,
"name": "self"
},
{
"startTime": 17919.5,
"duration": 215,
"name": "self"
},
{
"startTime": 27801.60000000149,
"duration": 193,
"name": "self"
},
{
"startTime": 17425.10000000149,
"duration": 179,
"name": "self"
},
{
"startTime": 31885.900000002235,
"duration": 176,
"name": "self"
}
]
},
"consoleErrors": []
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 703 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

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

Before

Width:  |  Height:  |  Size: 169 KiB

@@ -1,529 +0,0 @@
{
"url": "http://127.0.0.1:4173/",
"runDir": "perf/results/2026-06-12T06-11-56-994Z",
"elapsedMs": 83940,
"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": "http://127.0.0.1:4173/",
"environment": {
"cpuThrottle": 8,
"viewport": {
"width": 390,
"height": 844
},
"isMobile": true
},
"metrics": {
"afterLoad": {
"Timestamp": 23527.948042,
"Documents": 3,
"Frames": 1,
"JSEventListeners": 267,
"Nodes": 651,
"LayoutCount": 5,
"RecalcStyleCount": 7,
"LayoutDuration": 0.500736,
"RecalcStyleDuration": 0.283234,
"ScriptDuration": 5.693685,
"TaskDuration": 7.662781,
"JSHeapUsedSize": 4764568,
"JSHeapTotalSize": 7077888
},
"end": {
"Timestamp": 23593.417486,
"Documents": 1,
"Frames": 1,
"JSEventListeners": 1032,
"Nodes": 2696,
"LayoutCount": 137,
"RecalcStyleCount": 280,
"LayoutDuration": 3.566888,
"RecalcStyleDuration": 3.703827,
"ScriptDuration": 27.965461,
"TaskDuration": 67.992237,
"JSHeapUsedSize": 12832824,
"JSHeapTotalSize": 19759104
},
"delta": {
"Timestamp": 65.46944399999848,
"Documents": -2,
"Frames": 0,
"JSEventListeners": 765,
"Nodes": 2045,
"LayoutCount": 132,
"RecalcStyleCount": 273,
"LayoutDuration": 3.066152,
"RecalcStyleDuration": 3.420593,
"ScriptDuration": 22.271776000000003,
"TaskDuration": 60.329456,
"JSHeapUsedSize": 8068256,
"JSHeapTotalSize": 12681216
}
},
"cpuSummary": {
"totalProfileMs": 55106.451,
"sampleCount": 40511,
"topSelfTime": [
{
"url": "",
"functionName": "(program)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 26825.782999999923,
"samples": 18512
},
{
"url": "",
"functionName": "(idle)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 4815.981000000005,
"samples": 3701
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "(anonymous)",
"lineNumber": 11,
"columnNumber": 40267,
"selfMs": 2656.4819999999972,
"samples": 2026
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "ad",
"lineNumber": 9,
"columnNumber": 7898,
"selfMs": 1032.9479999999999,
"samples": 818
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "Kt",
"lineNumber": 8,
"columnNumber": 48104,
"selfMs": 951.7199999999993,
"samples": 747
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "Ce",
"lineNumber": 8,
"columnNumber": 94317,
"selfMs": 905.2510000000005,
"samples": 727
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "tryParse",
"lineNumber": 11,
"columnNumber": 49539,
"selfMs": 592.3200000000003,
"samples": 480
},
{
"url": "",
"functionName": "(garbage collector)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 578.5280000000001,
"samples": 430
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "start",
"lineNumber": 11,
"columnNumber": 1485400,
"selfMs": 445.5410000000001,
"samples": 322
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "G4",
"lineNumber": 8,
"columnNumber": 28256,
"selfMs": 432.7939999999998,
"samples": 325
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "lt",
"lineNumber": 8,
"columnNumber": 7348,
"selfMs": 360.4799999999999,
"samples": 279
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "ga",
"lineNumber": 8,
"columnNumber": 10651,
"selfMs": 330.2019999999997,
"samples": 260
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "r",
"lineNumber": 1,
"columnNumber": 967,
"selfMs": 323.32300000000004,
"samples": 239
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "Rl",
"lineNumber": 8,
"columnNumber": 101212,
"selfMs": 308.80500000000006,
"samples": 225
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "S1",
"lineNumber": 8,
"columnNumber": 40721,
"selfMs": 305.08099999999996,
"samples": 243
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "C2",
"lineNumber": 8,
"columnNumber": 49311,
"selfMs": 304.76900000000006,
"samples": 250
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "ws.onmessage",
"lineNumber": 11,
"columnNumber": 33453,
"selfMs": 284.5850000000001,
"samples": 204
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "N2.emit",
"lineNumber": 11,
"columnNumber": 25371,
"selfMs": 275.96699999999987,
"samples": 219
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "Xl",
"lineNumber": 8,
"columnNumber": 117224,
"selfMs": 260.1920000000001,
"samples": 208
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "pa",
"lineNumber": 8,
"columnNumber": 10399,
"selfMs": 249.32600000000016,
"samples": 180
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "S4",
"lineNumber": 11,
"columnNumber": 91365,
"selfMs": 237.0250000000001,
"samples": 179
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "N6",
"lineNumber": 8,
"columnNumber": 34144,
"selfMs": 234.33099999999988,
"samples": 188
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "bl",
"lineNumber": 8,
"columnNumber": 80838,
"selfMs": 228.66600000000017,
"samples": 171
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "o2",
"lineNumber": 8,
"columnNumber": 42232,
"selfMs": 219.2690000000001,
"samples": 182
},
{
"url": "",
"functionName": "setAttribute",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 213.44599999999997,
"samples": 178
},
{
"url": "",
"functionName": "(anonymous)",
"lineNumber": 29,
"columnNumber": 16,
"selfMs": 194.01300000000003,
"samples": 148
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "s2",
"lineNumber": 9,
"columnNumber": 71,
"selfMs": 189.78200000000004,
"samples": 160
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "na",
"lineNumber": 8,
"columnNumber": 3730,
"selfMs": 184.6409999999999,
"samples": 146
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "Eh",
"lineNumber": 8,
"columnNumber": 95737,
"selfMs": 179.85799999999998,
"samples": 138
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "Oh",
"lineNumber": 8,
"columnNumber": 116259,
"selfMs": 179.09700000000007,
"samples": 134
}
]
},
"responsiveness": {
"longTaskCount": 333,
"topLongTasks": [
{
"name": "self",
"startTime": 7254.699999999255,
"duration": 3731
},
{
"name": "self",
"startTime": 409.19999999925494,
"duration": 2518
},
{
"name": "unknown",
"startTime": 13437.400000002235,
"duration": 1801
},
{
"name": "self",
"startTime": 2960.199999999255,
"duration": 1382
},
{
"name": "self",
"startTime": 24715.300000000745,
"duration": 1038
},
{
"name": "self",
"startTime": 54470.699999999255,
"duration": 1002
},
{
"name": "self",
"startTime": 53685.199999999255,
"duration": 785
},
{
"name": "self",
"startTime": 23959.699999999255,
"duration": 755
},
{
"name": "self",
"startTime": 50997,
"duration": 747
},
{
"name": "self",
"startTime": 4343.10000000149,
"duration": 621
},
{
"name": "self",
"startTime": 27087.5,
"duration": 597
},
{
"name": "self",
"startTime": 22357.300000000745,
"duration": 554
},
{
"name": "unknown",
"startTime": 15496,
"duration": 478
},
{
"name": "self",
"startTime": 5324.5,
"duration": 474
},
{
"name": "self",
"startTime": 11302.300000000745,
"duration": 448
},
{
"name": "self",
"startTime": 40393.300000000745,
"duration": 435
},
{
"name": "self",
"startTime": 55742.5,
"duration": 405
},
{
"name": "self",
"startTime": 25906.300000000745,
"duration": 323
},
{
"name": "self",
"startTime": 38613.5,
"duration": 303
},
{
"name": "self",
"startTime": 46738.5,
"duration": 286
}
],
"frameCount": 358,
"averageFrameMs": 207.5804469273743,
"p95FrameMs": 483.3000000000029,
"p99FrameMs": 2016.6,
"intervalDriftCount": 43,
"averageIntervalDriftMs": 1537.434883720913,
"p95IntervalDriftMs": 7938.5,
"p99IntervalDriftMs": 8055.300000000745
},
"consoleMessages": [
{
"type": "info",
"text": "[socket] connecting to https://rover.otter.land",
"location": {
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"line": 11,
"column": 63824,
"lineNumber": 11,
"columnNumber": 63824
}
},
{
"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 400 ()",
"location": {
"url": "https://rover.otter.land/video/Bweeble/whep",
"line": 0,
"column": 0,
"lineNumber": 0,
"columnNumber": 0
}
},
{
"type": "error",
"text": "Failed to load resource: the server responded with a status of 400 ()",
"location": {
"url": "https://rover.otter.land/video/Bweeble-audio/whep",
"line": 0,
"column": 0,
"lineNumber": 0,
"columnNumber": 0
}
},
{
"type": "error",
"text": "Failed to load resource: the server responded with a status of 400 ()",
"location": {
"url": "https://rover.otter.land/video/Bweeble/whep",
"line": 0,
"column": 0,
"lineNumber": 0,
"columnNumber": 0
}
},
{
"type": "error",
"text": "Failed to load resource: the server responded with a status of 400 ()",
"location": {
"url": "https://rover.otter.land/video/Bweeble/whep",
"line": 0,
"column": 0,
"lineNumber": 0,
"columnNumber": 0
}
}
],
"failedRequests": [
{
"url": "https://rover.otter.land/video/Bweeble/whep",
"method": "POST",
"failure": "net::ERR_ABORTED"
},
{
"url": "https://rover.otter.land/video/Bweeble-audio/whep",
"method": "POST",
"failure": "net::ERR_ABORTED"
},
{
"url": "https://rover.otter.land/video/Bweeble/whep",
"method": "POST",
"failure": "net::ERR_ABORTED"
}
],
"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://rover.otter.land/video/Bweeble/whep",
"status": 400
},
{
"url": "https://rover.otter.land/video/Bweeble-audio/whep",
"status": 400
},
{
"url": "https://rover.otter.land/video/Bweeble/whep",
"status": 400
},
{
"url": "https://rover.otter.land/video/Bweeble/whep",
"status": 400
}
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 461 KiB

File diff suppressed because it is too large Load Diff
@@ -1,896 +0,0 @@
{
"url": "https://rover.otter.land/",
"runDir": "perf/results/2026-06-12T06-15-19-037Z-root-runtime",
"environment": {
"cpuThrottle": 8,
"viewport": {
"width": 390,
"height": 844
},
"isMobile": true,
"sampleMs": 30000
},
"metrics": {
"Timestamp": 23755.664379,
"AudioHandlers": 0,
"AudioWorkletProcessors": 0,
"Documents": 3,
"Frames": 1,
"JSEventListeners": 1280,
"LayoutObjects": 1996,
"MediaKeySessions": 0,
"MediaKeys": 0,
"Nodes": 2628,
"Resources": 20,
"ContextLifecycleStateObservers": 20,
"V8PerContextDatas": 4,
"WorkerGlobalScopes": 0,
"UACSSResources": 0,
"RTCPeerConnections": 4,
"ResourceFetchers": 3,
"AdSubframes": 0,
"DetachedScriptStates": 2,
"ArrayBufferContents": 14,
"LayoutCount": 43,
"RecalcStyleCount": 80,
"LayoutDuration": 1.465119,
"RecalcStyleDuration": 1.54029,
"DevToolsCommandDuration": 0.00038,
"ScriptDuration": 15.671196,
"V8CompileDuration": 0,
"TaskDuration": 30.021546,
"TaskOtherDuration": 11.344561,
"ThreadTime": 32.951976,
"ProcessTime": 39.43,
"JSHeapUsedSize": 7054604,
"JSHeapTotalSize": 13893632,
"FirstMeaningfulPaint": 0,
"DomContentLoaded": 23725.629234,
"NavigationStart": 23721.84567
},
"snapshot": {
"elapsedMs": 53572.199999999255,
"listeners": [
{
"key": "add:input:invalid",
"count": 18
},
{
"key": "add:window:online",
"count": 6
},
{
"key": "add:document:visibilitychange",
"count": 6
},
{
"key": "add:audio:canplay",
"count": 6
},
{
"key": "add:audio:ended",
"count": 6
},
{
"key": "add:audio:error",
"count": 6
},
{
"key": "add:audio:pause",
"count": 6
},
{
"key": "add:audio:play",
"count": 6
},
{
"key": "add:audio:stalled",
"count": 6
},
{
"key": "add:audio:waiting",
"count": 6
},
{
"key": "remove:window:online",
"count": 5
},
{
"key": "remove:document:visibilitychange",
"count": 5
},
{
"key": "remove:audio:play",
"count": 4
},
{
"key": "remove:audio:pause",
"count": 4
},
{
"key": "remove:audio:ended",
"count": 4
},
{
"key": "remove:audio:error",
"count": 4
},
{
"key": "remove:audio:waiting",
"count": 4
},
{
"key": "remove:audio:canplay",
"count": 4
},
{
"key": "remove:audio:stalled",
"count": 4
},
{
"key": "add:WebSocket:message",
"count": 2
},
{
"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
},
{
"key": "add:div#root:lostpointercapture",
"count": 2
},
{
"key": "add:div#root:mousedown",
"count": 2
}
],
"timers": [
{
"key": "timeout:0",
"count": 5
},
{
"key": "timeout:12000",
"count": 4
},
{
"key": "interval:60000",
"count": 4
},
{
"key": "interval:250",
"count": 4
},
{
"key": "interval:200",
"count": 4
},
{
"key": "timeout:2000",
"count": 4
},
{
"key": "timeout:15000",
"count": 2
},
{
"key": "timeout:4000",
"count": 2
},
{
"key": "interval:3000",
"count": 2
},
{
"key": "interval:1000",
"count": 1
},
{
"key": "interval:2000",
"count": 1
},
{
"key": "timeout:10000",
"count": 1
},
{
"key": "timeout:1491",
"count": 1
}
],
"timeouts": 19,
"intervals": 16,
"timeoutFires": 11,
"intervalFires": 136,
"webSocketMessageCount": 498,
"webSocketBytes": 11209457,
"webSocketEvents": [
{
"event": "log:entry",
"count": 269,
"bytes": 62887
},
{
"event": "sensorFrame",
"count": 99,
"bytes": 185012
},
{
"event": "session:sync",
"count": 31,
"bytes": 526178
},
{
"event": "commandAck",
"count": 30,
"bytes": 2833
},
{
"event": "",
"count": 15,
"bytes": 10168579
},
{
"event": "roverSnapshot:frame",
"count": 11,
"bytes": 1067
},
{
"event": "mode",
"count": 2,
"bytes": 54
},
{
"event": "controlGranted",
"count": 2,
"bytes": 83
},
{
"event": "alert",
"count": 2,
"bytes": 257
},
{
"event": "alert:new",
"count": 2,
"bytes": 330
},
{
"event": "auth:role",
"count": 2,
"bytes": 62
},
{
"event": "rovers",
"count": 2,
"bytes": 10906
},
{
"event": "chat:init",
"count": 2,
"bytes": 155226
},
{
"event": "overseer:memory",
"count": 2,
"bytes": 1040
},
{
"event": "log:init",
"count": 2,
"bytes": 88662
},
{
"event": "barcode:state",
"count": 2,
"bytes": 3254
},
{
"event": "kinect:pointCloudFrame",
"count": 2,
"bytes": 524
},
{
"event": "kinect:colorFrame",
"count": 2,
"bytes": 338
},
{
"event": "2",
"count": 2,
"bytes": 2
},
{
"event": "roverSnapshot:status",
"count": 2,
"bytes": 159
},
{
"event": "0{\"sid\":\"v0f",
"count": 1,
"bytes": 106
},
{
"event": "40{\"sid\":\"5t",
"count": 1,
"bytes": 32
},
{
"event": "430[{\"succes",
"count": 1,
"bytes": 39
},
{
"event": "431[{\"url\":\"",
"count": 1,
"bytes": 133
},
{
"event": "432[{\"url\":\"",
"count": 1,
"bytes": 145
},
{
"event": "Freaky",
"count": 1,
"bytes": 40
},
{
"event": "434[{\"succes",
"count": 1,
"bytes": 194
},
{
"event": "0{\"sid\":\"t1y",
"count": 1,
"bytes": 106
},
{
"event": "40{\"sid\":\"Bu",
"count": 1,
"bytes": 32
},
{
"event": "436[{\"error\"",
"count": 1,
"bytes": 41
},
{
"event": "437[{\"error\"",
"count": 1,
"bytes": 41
},
{
"event": "438[{\"url\":\"",
"count": 1,
"bytes": 135
},
{
"event": "439[{\"url\":\"",
"count": 1,
"bytes": 147
},
{
"event": "Bweeble",
"count": 1,
"bytes": 42
},
{
"event": "roverHostStats",
"count": 1,
"bytes": 771
}
],
"mutationsTotal": {
"records": 7648,
"added": 227,
"removed": 34,
"attrs": 7351,
"text": 36
},
"mutationTargets": [
{
"key": "input.accent-emerald-400:attributes:name",
"count": 2612
},
{
"key": "input.accent-emerald-400:attributes:type",
"count": 1306
},
{
"key": "input.field-input.flex-1:attributes:name",
"count": 432
},
{
"key": "input.field-input.w-full:attributes:name",
"count": 432
},
{
"key": "input.accent-cyan-500:attributes:name",
"count": 428
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:name",
"count": 330
},
{
"key": "div.h-full:attributes:style",
"count": 278
},
{
"key": "input.field-input.chat-composer-input:attributes:name",
"count": 216
},
{
"key": "input:attributes:name",
"count": 216
},
{
"key": "input.field-input.w-full:attributes:type",
"count": 216
},
{
"key": "input.accent-cyan-500:attributes:type",
"count": 214
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:type",
"count": 165
},
{
"key": "path.[object.SVGAnimatedString]:attributes:d",
"count": 145
},
{
"key": "path.[object.SVGAnimatedString]:attributes:fill",
"count": 139
},
{
"key": "input:attributes:type",
"count": 108
},
{
"key": "Text:characterData:",
"count": 36
},
{
"key": "section.panel-section.border:attributes:style",
"count": 20
},
{
"key": "header.flex.items-center:attributes:style",
"count": 18
},
{
"key": "path.[object.SVGAnimatedString]:attributes:stroke",
"count": 18
},
{
"key": "input.min-w-0.flex-1:attributes:name",
"count": 14
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:value",
"count": 3
},
{
"key": "span.inline-flex.items-center:attributes:class",
"count": 3
},
{
"key": "div.rounded-sm.px-2:attributes:class",
"count": 3
},
{
"key": "button.mobile-touch-control.flex:attributes:disabled",
"count": 3
},
{
"key": "button.mobile-touch-control.min-h-9:attributes:disabled",
"count": 3
},
{
"key": "div.fixed.bottom-2:attributes:class",
"count": 3
},
{
"key": "button.mobile-touch-control.flex:attributes:class",
"count": 3
},
{
"key": "div.mobile-touch-control.flex:attributes:class",
"count": 3
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:placeholder",
"count": 2
},
{
"key": "video.h-full.w-full:attributes:playsinline",
"count": 2
},
{
"key": "video.h-full.w-full:attributes:autoplay",
"count": 2
},
{
"key": "video.h-full.w-full:attributes:disableremoteplayback",
"count": 2
},
{
"key": "audio:attributes:autoplay",
"count": 2
},
{
"key": "audio:attributes:disableremoteplayback",
"count": 2
},
{
"key": "input.field-input.flex-1:attributes:value",
"count": 2
},
{
"key": "span.inline-block.rounded:attributes:style",
"count": 2
},
{
"key": "div.flex.flex-col:attributes:class",
"count": 1
},
{
"key": "button.button-dark.min-w-0:attributes:disabled",
"count": 1
},
{
"key": "input.field-input.w-full:attributes:value",
"count": 1
},
{
"key": "button.button-dark.text-sm:attributes:disabled",
"count": 1
}
],
"mutationBursts": [
{
"at": 10155,
"records": 164,
"added": 105,
"removed": 1,
"attrs": 58,
"text": 0
},
{
"at": 28081,
"records": 103,
"added": 7,
"removed": 12,
"attrs": 79,
"text": 5
},
{
"at": 31972,
"records": 83,
"added": 13,
"removed": 3,
"attrs": 65,
"text": 2
},
{
"at": 18426,
"records": 82,
"added": 11,
"removed": 2,
"attrs": 66,
"text": 3
},
{
"at": 8027,
"records": 69,
"added": 19,
"removed": 5,
"attrs": 42,
"text": 3
},
{
"at": 16714,
"records": 66,
"added": 2,
"removed": 2,
"attrs": 53,
"text": 9
},
{
"at": 40645,
"records": 64,
"added": 0,
"removed": 0,
"attrs": 64,
"text": 0
},
{
"at": 40951,
"records": 64,
"added": 0,
"removed": 0,
"attrs": 64,
"text": 0
},
{
"at": 42708,
"records": 64,
"added": 0,
"removed": 0,
"attrs": 64,
"text": 0
},
{
"at": 36974,
"records": 61,
"added": 0,
"removed": 0,
"attrs": 61,
"text": 0
},
{
"at": 24109,
"records": 60,
"added": 0,
"removed": 0,
"attrs": 60,
"text": 0
},
{
"at": 28572,
"records": 60,
"added": 2,
"removed": 4,
"attrs": 53,
"text": 1
},
{
"at": 36595,
"records": 60,
"added": 0,
"removed": 0,
"attrs": 60,
"text": 0
},
{
"at": 42135,
"records": 60,
"added": 0,
"removed": 0,
"attrs": 60,
"text": 0
},
{
"at": 43257,
"records": 60,
"added": 0,
"removed": 0,
"attrs": 60,
"text": 0
},
{
"at": 48207,
"records": 60,
"added": 0,
"removed": 0,
"attrs": 60,
"text": 0
},
{
"at": 48500,
"records": 60,
"added": 0,
"removed": 0,
"attrs": 60,
"text": 0
},
{
"at": 50368,
"records": 60,
"added": 0,
"removed": 0,
"attrs": 60,
"text": 0
},
{
"at": 51568,
"records": 60,
"added": 0,
"removed": 0,
"attrs": 60,
"text": 0
},
{
"at": 52146,
"records": 60,
"added": 0,
"removed": 0,
"attrs": 60,
"text": 0
}
],
"frames": {
"count": 241,
"average": 202.06639004149378,
"p95": 399.8999999999978,
"p99": 1633.199999999999
},
"longTasks": {
"count": 133,
"top": [
{
"startTime": 1510.7999999970198,
"duration": 2281,
"name": "self"
},
{
"startTime": 10156.89999999851,
"duration": 1493,
"name": "self"
},
{
"startTime": 3806,
"duration": 1311,
"name": "self"
},
{
"startTime": 30277.099999997765,
"duration": 932,
"name": "self"
},
{
"startTime": 14996,
"duration": 931,
"name": "self"
},
{
"startTime": 9269.89999999851,
"duration": 886,
"name": "self"
},
{
"startTime": 7147,
"duration": 880,
"name": "self"
},
{
"startTime": 14298,
"duration": 698,
"name": "self"
},
{
"startTime": 29581.099999997765,
"duration": 696,
"name": "self"
},
{
"startTime": 8029.599999997765,
"duration": 689,
"name": "self"
},
{
"startTime": 5117.39999999851,
"duration": 534,
"name": "self"
},
{
"startTime": 31587,
"duration": 385,
"name": "self"
},
{
"startTime": 18074,
"duration": 356,
"name": "self"
},
{
"startTime": 25434.39999999851,
"duration": 344,
"name": "self"
},
{
"startTime": 16382.29999999702,
"duration": 333,
"name": "self"
},
{
"startTime": 12463.199999999255,
"duration": 300,
"name": "self"
},
{
"startTime": 6124.79999999702,
"duration": 285,
"name": "self"
},
{
"startTime": 5657.199999999255,
"duration": 283,
"name": "self"
},
{
"startTime": 18655.79999999702,
"duration": 242,
"name": "self"
},
{
"startTime": 19023.5,
"duration": 236,
"name": "self"
}
]
},
"consoleErrors": []
}
}
@@ -1,219 +0,0 @@
# Issue 001: Telemetry Fan-Out And Always-On HUD Maps
## Summary
`/spectate` renders multiple rover cards at once, and every rover card includes a live HUD
with an always-on top-down SVG map. During the 30s throttled audit, the page received
1,354 `sensorFrame` events and produced more than 5,300 SVG `path` attribute mutations.
This is the strongest measured `/spectate` CPU problem.
## Severity
High.
This is high-frequency, always-on work. It scales with the number of visible rovers and
hits exactly the devices we care about: mobile browsers and weaker computers.
## Upstream Likelihood
Medium.
The `/` telemetry issue will likely help if it introduces a central throttled visual
telemetry path. However, `/spectate` has a unique multiplier: it renders several rover
HUDs and several `TopDownMap` instances at the same time. Even after upstream telemetry
throttling, `/spectate` probably still needs a policy for how many maps update and how
often.
Related upstream issue:
```txt
perf/issues/004-sensor-telemetry-render-frequency.md
```
## Affected Files
- `webui/src/components/SpectateVideo/index.jsx`
- `webui/src/components/HudOverlays/HudOverlay/index.jsx`
- `webui/src/components/HudOverlays/HudOverlay/HudMapOverlay.jsx`
- `webui/src/components/TopDownMap/TopDownMapContent.jsx`
- `webui/src/components/TopDownMap/visuals.jsx`
- `webui/src/context/TelemetryContext.jsx`
- `webui/src/components/RoverMediaPlayer/index.jsx`
- `webui/src/components/HudOverlays/OvercurrentOverlay/index.jsx`
- `webui/src/components/HudOverlays/LowBatteryOverlay/index.jsx`
- `webui/src/components/HudOverlays/VerticalBatteryOverlay/index.jsx`
## Evidence
Artifact:
```sh
perf/results/2026-06-12T04-55-16-888Z-root-runtime/root-runtime-report.json
```
30s sample, mobile viewport, 6x CPU throttle:
```txt
sensorFrame events: 1,354
sensorFrame bytes: 2,531,090
path d mutations: 2,688
path fill mutations: 2,682
Long tasks: 81
Worst long task: 375ms
Average frame gap: 113.9ms
p95 frame gap: 233.3ms
```
The path mutation pattern is the important clue. Normal text/status updates do not mutate
thousands of SVG path `d` and `fill` attributes. `TopDownMap` does.
## Current Code Path
`SpectateVideo` mounts a full media and overlay stack for each rover:
```txt
webui/src/components/SpectateVideo/index.jsx:17
RoverMediaPlayer
webui/src/components/SpectateVideo/index.jsx:26
HudOverlay variant="spectator"
webui/src/components/SpectateVideo/index.jsx:33
OvercurrentOverlay
webui/src/components/SpectateVideo/index.jsx:35
VerticalBatteryOverlay
```
`HudOverlay` subscribes to telemetry:
```txt
webui/src/components/HudOverlays/HudOverlay/index.jsx:27
const frame = useTelemetryFrame(effectiveRoverId);
```
For spectator mode, it forces the top-down map on:
```txt
webui/src/components/HudOverlays/HudOverlay/index.jsx:50
variant === 'spectator' ? true : ...
```
Then `HudMapOverlay` renders:
```txt
webui/src/components/HudOverlays/HudOverlay/HudMapOverlay.jsx:35
<TopDownMap sensors={sensors} size={240} overlay />
```
There is also duplicated telemetry subscription pressure. `RoverMediaPlayer` subscribes to
telemetry for audio ducking and media state, while `HudOverlay`, battery overlays, and
warning overlays can subscribe for the same rover.
## Why This Matters
The raw socket message rate is not automatically a problem. The expensive part is waking
multiple React subscribers, recomputing visual sensor geometry, and mutating SVG
attributes for every visual update.
On `/`, there is generally one primary driver view. On `/spectate`, the same sensor stream
can be multiplied across several rover cards.
## Fix Strategies
### Option A: Add A Throttled Visual Telemetry Channel
This is the most reusable upstream-friendly fix.
Keep `TelemetryContext` storing the latest frame immediately, but notify visual consumers
at a lower cadence:
```txt
useTelemetryFrameRaw(roverId) high-frequency, only where truly needed
useTelemetryFrameVisual(roverId) throttled display data
```
Use visual telemetry for:
- `TopDownMap`
- `HudOverlay`
- `SpectatorTelemetryOverlay`
- battery overlays
- warning overlays where 100-200ms delay is acceptable
Suggested rates:
```txt
desktop visual telemetry: 10-15Hz
mobile visual telemetry: 5-10Hz
```
### Option B: Pass One Telemetry Frame Through `SpectateVideo`
Right now, each overlay can subscribe independently. Instead, let `SpectateVideo` subscribe
once per rover and pass `sensors` down:
```jsx
const frame = useTelemetryFrameVisual(roverId);
const sensors = frame?.sensors ?? null;
<RoverMediaPlayer roverId={roverId} sensors={sensors} />
<HudOverlay roverId={roverId} sensors={sensors} />
<OvercurrentOverlay roverId={roverId} sensors={sensors} />
<LowBatteryOverlay roverId={roverId} sensors={sensors} />
<VerticalBatteryOverlay roverId={roverId} sensors={sensors} />
```
This may require small prop additions to the warning/battery overlays.
### Option C: Make Spectator HUD Maps Adaptive
Spectator maps are currently always on. For low-end/mobile:
- hide maps by default on portrait mobile
- update maps at a lower cadence than text warnings
- only animate the focused/first rover's map
- show static/minimal maps for secondary rovers
- make map visibility a spectator setting
### Option D: Memoize TopDownMap Geometry
This is useful after throttling. Avoid recalculating path data when the sensor fields that
drive a particular path did not change.
## Recommended Path
Do this after the `/` telemetry issue if that work touches `TelemetryContext`.
Best first implementation:
1. Add `useTelemetryFrameVisual(roverId)` with a 100-150ms visual notification cadence.
2. Switch `HudOverlay` and `TopDownMap` consumers to visual telemetry.
3. In `SpectateVideo`, subscribe once and pass sensors down where practical.
4. Add a spectator/mobile map policy so only the most important maps update live.
## Validation
Run:
```sh
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=30000 \
node perf/live-root-runtime-audit.mjs https://rover.otter.land/spectate perf/results
```
Expected improvements:
- `path d` and `path fill` mutations should drop sharply.
- `ScriptDuration` should drop.
- Long task count should drop.
- Frame p95/p99 should improve.
- `sensorFrame` socket count may stay the same if only rendering is throttled.
## Risks
- Do not throttle control/safety logic that needs immediate data.
- Keep battery/overcurrent warnings responsive enough to be trusted.
- If map updates become too slow, operators may perceive the HUD as stale. A 5-10Hz map is
usually enough for spectators, but verify by watching the live page.
@@ -1,190 +0,0 @@
# Issue 002: Multi-Rover WebRTC Media Fan-Out
## Summary
`/spectate` mounts several `RoverMediaPlayer` instances at once. In the fresh runtime
audit, Chromium reported 6 active `RTCPeerConnections`. That is a large baseline for
low-end clients, especially while the page is also handling telemetry, snapshots, logs,
chat, and SVG HUD maps.
## Severity
High.
This may not show up as React mutations, but it consumes CPU in browser media pipelines,
WebRTC negotiation, decoding, audio handling, and event dispatch. It also scales with the
number of rovers.
## Upstream Likelihood
Low to medium.
General `RoverMediaPlayer` improvements can help both `/` and `/spectate`, but the core
problem is spectate-specific: `/spectate` intentionally displays multiple rover media
players at once. The fix needs a spectator policy for active feeds.
## Affected Files
- `webui/src/components/SpectateVideo/index.jsx`
- `webui/src/components/RoverMediaPlayer/index.jsx`
- `webui/src/hooks/useVideoRequests.js`
- `webui/src/lib/whepPlayer.js`
- `webui/src/spectate/SpectatorApp/components/RoverRow.jsx`
- server-side WHEP/video request handling, if feed quality/rate selection is added
## Evidence
Artifact:
```sh
perf/results/2026-06-12T04-55-16-888Z-root-runtime/root-runtime-report.json
```
30s sample:
```txt
RTCPeerConnections: 6
JSEventListeners: 379
```
The listener list is dominated by normal media listeners:
```txt
audio canplay/ended/error/pause/play/stalled/waiting: 9 each
video media events: 3 each
```
That pattern fits several rover media players, likely with video and dedicated audio
connections.
The same audit also saw:
```txt
TaskDuration: 28.48s
ScriptDuration: 12.11s
Long tasks: 81
```
The earlier 8x throttled `/spectate` profile saw:
```txt
Task delta: 49.13s
Script delta: 19.58s
```
## Current Code Path
Each rover card mounts:
```txt
webui/src/components/SpectateVideo/index.jsx:17
<RoverMediaPlayer roverId={roverId} />
```
`RoverMediaPlayer` automatically creates video and audio request entries:
```txt
webui/src/components/RoverMediaPlayer/index.jsx
autoEntries = [
video rover entry,
optional dedicated audio entry,
]
```
It then requests sources through `useVideoRequests`, starts WHEP players, maintains
restart timers, maintains unmute/audio retry timers, and optionally falls back to
snapshots when no WHEP URL exists.
## Why This Matters
Six peer connections on a powerful desktop may feel fine. On weak laptops and mobile
devices, simultaneous WebRTC decode plus React UI churn can easily saturate the main
thread or media threads. The visible symptom is not necessarily one obvious function in
the CPU profile; it is degraded frame cadence, long tasks, and high browser-level media
work.
## Fix Strategies
### Option A: Cap Active Live Feeds On Mobile
For mobile/low-end layouts:
- one primary live WHEP feed
- secondary rover cards use snapshots or lower-rate previews
- promote a secondary rover to live only when selected/visible/focused
This is the highest-impact spectator-specific design change.
### Option B: Pause Offscreen Feeds
Use `IntersectionObserver` around each `SpectateVideo` or `RoverSpectatorCard`:
- start WHEP when the card is visible
- stop WHEP when the card is far offscreen
- keep a small grace period to avoid flapping while scrolling
This matters especially because portrait `/spectate` can scroll.
### Option C: Disable Dedicated Audio For Non-Focused Rovers
Dedicated audio is useful, but not necessarily for every rover at the same time.
Possible policy:
- audio enabled only for the selected/primary rover
- audio disabled for muted secondary rovers
- audio WHEP starts only after user interaction
This should reduce peer connections and audio retry timers.
### Option D: Request Lower Quality For Spectator Secondary Feeds
If the server/media stack supports it, add spectator quality tiers:
```txt
primary: normal FPS/resolution
secondary: low FPS/resolution or snapshot-only
mobile: lower default bitrate/resolution
```
### Option E: Stop Retry Loops When Hidden
When document visibility is hidden or a card is offscreen:
- stop restart timers
- stop audio retry intervals
- avoid requesting fresh WHEP URLs
## Recommended Path
After the HUD/telemetry issue, add a spectator feed activity policy:
1. Define "primary" versus "secondary" rover cards.
2. Keep only primary feeds fully live on mobile.
3. Use `IntersectionObserver` to avoid live playback for offscreen cards.
4. Disable secondary audio by default.
## Validation
Run:
```sh
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=30000 \
node perf/live-root-runtime-audit.mjs https://rover.otter.land/spectate perf/results
```
Expected improvements:
- `RTCPeerConnections` should drop below 6 on mobile.
- Media listener counts should drop.
- `TaskDuration` should drop.
- Frame p95/p99 should improve.
- User-visible video should still start reliably for the primary rover.
## Risks
- Spectators may expect all feeds live all the time on desktop.
- Switching feeds can introduce startup delay if WHEP is torn down too aggressively.
- Audio policy needs to avoid surprising users. Make the focused rover's audio behavior
clear and consistent.
@@ -1,202 +0,0 @@
# Issue 003: Room Camera And Snapshot Object URL Churn
## Summary
Room camera and rover snapshot frames are delivered as binary payloads. The client creates
a new `Blob`, creates a new object URL, revokes the previous URL, and updates React state
for every frame. In the 30s `/spectate` audit, this caused 117 `img.src` mutations and
11MB of binary socket payloads.
## Severity
Medium-high.
This is not the biggest measured CPU source, but it is steady work and very visible on
low-end devices because JPEG decoding, Blob allocation, object URL churn, image source
updates, and React state updates all happen together.
## Upstream Likelihood
Low.
This is mostly `/spectate` and camera-panel specific. It will not be fixed by the `/`
input, control context, or log work. It may benefit indirectly if broad rerenders are
reduced, but the frame handling itself needs targeted changes.
## Affected Files
- `webui/src/hooks/useRoomCameraSnapshots.js`
- `webui/src/hooks/useRoverSnapshots.js`
- `webui/src/components/RoomCameraPanel/index.jsx`
- `webui/src/components/RoomCameraFeed/index.jsx`
- `server/src/services/roomCameraService/socketGateway.js`
- `server/src/services/roverSnapshotService/socketGateway.js`
## Evidence
Artifact:
```sh
perf/results/2026-06-12T04-55-16-888Z-root-runtime/root-runtime-report.json
```
30s sample:
```txt
unlabeled binary socket events: 126
unlabeled binary bytes: 11,048,477
roomCamera:frame events: 117
roverSnapshot:frame events: 7
img src mutations: 117
blink class mutations: 117
```
The 8x throttled profile also had these CPU entries:
```txt
Blob
decodeString
setAttribute
```
Those line up with binary socket parsing, Blob/object URL creation, and `img.src` updates.
## Current Code Path
`useRoomCameraSnapshots` handles each frame:
```txt
webui/src/hooks/useRoomCameraSnapshots.js:78
const blob = new Blob([buffer], { type: 'image/jpeg' });
webui/src/hooks/useRoomCameraSnapshots.js:79
const url = URL.createObjectURL(blob);
webui/src/hooks/useRoomCameraSnapshots.js:82
URL.revokeObjectURL(prevUrl);
webui/src/hooks/useRoomCameraSnapshots.js:85
setFeeds(...)
```
`useRoverSnapshots` has the same pattern.
`RoomCameraFeed` writes that object URL into an image:
```txt
webui/src/components/RoomCameraFeed/index.jsx:24
<img src={feed.objectUrl} ... />
```
It also toggles blink state for each frame:
```txt
webui/src/components/RoomCameraFeed/index.jsx:12
setBlink((prev) => !prev);
```
Server-side frame rates:
```txt
server/src/services/roomCameraService/socketGateway.js:11
STREAM_INTERVAL_MS = 1000
server/src/services/roverSnapshotService/socketGateway.js:11
STREAM_INTERVAL_MS = 333
```
## Why This Matters
The page is doing memory allocation and DOM/image decode work on every camera frame. At
one room camera per second this can be acceptable, but `/spectate` is already busy. When
combined with live WebRTC and telemetry maps, this becomes another steady CPU drain.
The blink dot is also doing one React state update and class mutation per frame. It is
small, but it is pure extra work.
## Fix Strategies
### Option A: Drop Frames When The Client Is Behind
Keep only the latest pending frame per camera. If the previous object URL has not been
painted or decoded yet, replace the pending buffer instead of forcing every frame through
React.
### Option B: Throttle Display Updates On Mobile
Render room camera frames at a lower client-side display rate:
```txt
desktop: 1fps as today, or configured
mobile: 0.2-0.5fps for secondary room cameras
```
The server can still emit at its existing rate for other clients, but this client can
choose not to render every frame.
### Option C: Lazy Mount Or Collapse Room Cameras On Mobile
On portrait mobile, `SecondaryRow` can defer `RoomCameraPanel` until:
- the user scrolls near it
- the user opens the panel
- the page is idle after initial rover feeds have settled
### Option D: Remove Per-Frame React Blink State
Replace the blink toggle with CSS animation or a timestamp updated less often.
For example:
- keep status text
- use a CSS pulse class while status is `playing`
- avoid toggling React state on every frame
### Option E: Stabilize Room Camera Source Lists
`RoomCameraPanel` currently passes a newly created array of objects into the snapshot hook:
```txt
webui/src/components/RoomCameraPanel/index.jsx:37
useRoomCameraSnapshots(cameras.map((camera) => ({ id: camera.id })))
```
The hook ultimately keys by IDs, so this is not the main issue, but it still creates extra
memo/effect churn on rerender. Prefer a memoized array of IDs:
```jsx
const cameraIds = useMemo(() => cameras.map((camera) => camera.id), [cameras]);
const feedMap = useRoomCameraSnapshots(cameraIds);
```
## Recommended Path
Start with the cheap fixes:
1. Remove the per-frame blink state.
2. Memoize camera IDs in `RoomCameraPanel`.
3. Add client-side render throttling/drop-latest behavior in `useRoomCameraSnapshots`.
4. Consider lazy mounting/collapsing room cameras in mobile spectator layout.
## Validation
Run:
```sh
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=30000 \
node perf/live-root-runtime-audit.mjs https://rover.otter.land/spectate perf/results
```
Expected improvements:
- `img.h-full.w-full:attributes:src` mutations should drop if display is throttled.
- `span.h-2.w-2:attributes:class` mutations should disappear if blink state is removed.
- `ScriptDuration` and long tasks should improve modestly.
- Binary socket bytes may stay the same unless server subscription/rate is changed.
## Risks
- Reducing room camera update rate can make the room view feel stale.
- Object URL lifecycle must stay correct. Do not leak old URLs.
- If lazy mounting unsubscribes too aggressively, first frame display may feel delayed.
@@ -1,202 +0,0 @@
# Issue 004: Broad Spectator Session Rerenders
## Summary
`SpectatorContent` reads the whole session object with `useSession()`. That means broad
session changes can rerender the top-level spectator layout, including the sidebar, rover
row, secondary row, chat, logs, room cameras, and overlays.
The audit saw 54 `session:sync` events in 30s, carrying about 914KB, while the DOM also
showed repeated added/removed-node bursts.
## Severity
Medium-high.
This is likely a multiplier. It may not be the single hottest path, but it can cause
otherwise independent components to rerender together.
## Upstream Likelihood
Medium to high.
If the `/` backlog changes `SessionContext` to provide better selectors, better structural
sharing, batched log/session updates, or more stable action references, this issue may be
partly fixed upstream. However, `SpectatorContent` itself still needs to stop reading the
whole session object.
Related upstream issues:
```txt
perf/issues/003-high-volume-log-stream.md
perf/issues/006-chat-and-nickname-churn.md
perf/issues/007-timers-and-polling.md
```
## Affected Files
- `webui/src/spectate/SpectatorApp/SpectatorContent.jsx`
- `webui/src/hooks/useSpectatorMode.js`
- `webui/src/context/SessionContext.jsx`
- `webui/src/spectate/SpectatorApp/components/RoverRow.jsx`
- `webui/src/spectate/SpectatorApp/components/SecondaryRow.jsx`
- sidebar components mounted by `SpectatorContent`
## Evidence
Artifact:
```sh
perf/results/2026-06-12T04-55-16-888Z-root-runtime/root-runtime-report.json
```
30s sample:
```txt
session:sync events: 54
session:sync bytes: 914,000
DOM mutation records: 5,868
DOM nodes added: 440
DOM nodes removed: 440
```
Largest mutation bursts:
```txt
119 records, 57 added, 58 removed
117 records, 52 added, 53 removed
113 records, 52 added, 52 removed
102 records, 49 added, 49 removed
100 records, 48 added, 48 removed
```
Those bursts suggest periodic component/list updates, not just single text changes.
## Current Code Path
`SpectatorContent` subscribes to the whole session:
```txt
webui/src/spectate/SpectatorApp/SpectatorContent.jsx:22
const { session } = useSession();
```
It then derives:
```txt
inLockdown = session?.mode === 'lockdown'
roster = session?.roster ?? []
```
But because it reads the whole session object, changes to logs, users, active drivers,
room cameras, replay state, chat-related session fields, or any other session branch can
potentially re-render the whole spectator layout.
`useSpectatorMode` also reads the whole session:
```txt
webui/src/hooks/useSpectatorMode.js:7
const { session, setRole, subscribeAll, connected } = useSession();
```
It only needs `mode`, `role`, `connected`, and actions.
## Why This Matters
`/spectate` is already rendering expensive children. A broad rerender at the top makes
other optimizations less effective because unrelated live data can still wake the route.
For example:
- a log entry should not rerender the rover media grid
- a chat composer state change should not rerender room camera feeds
- a session sync should not recreate layout props unless the selected fields changed
## Fix Strategies
### Option A: Replace Whole Session Reads With Selectors
In `SpectatorContent`, use precise selectors:
```jsx
const mode = useSessionSelector((state) => state.session?.mode ?? null);
const roster = useSessionSelector((state) => state.session?.roster ?? EMPTY_ROSTER);
```
Then:
```jsx
const inLockdown = mode === 'lockdown';
```
Make sure selector outputs are stable. If `roster` is rebuilt on every `session:sync`,
this still rerenders. The store may need structural sharing upstream.
### Option B: Split Spectator Layout Into Memoized Regions
Split the page into:
- spectator sidebar
- rover grid
- secondary/camera row
- global overlays
Then each region subscribes only to what it needs.
### Option C: Narrow `useSpectatorMode`
Replace:
```jsx
const { session, setRole, subscribeAll, connected } = useSession();
```
with selectors/actions:
```jsx
const mode = useSessionSelector((state) => state.session?.mode ?? null);
const role = useSessionSelector((state) => state.session?.role ?? null);
const connected = useSessionSelector((state) => state.connected);
const { setRole, subscribeAll } = useSessionActions();
```
### Option D: Batch Or Diff `session:sync`
This may belong to the `/` backlog. If incoming sync payloads replace large object
branches each time, selector users will still rerender. Preserve references for unchanged
branches.
## Recommended Path
Do this after the `/` session/log work if that work changes `SessionContext`.
Local `/spectate` changes:
1. Remove whole-session `useSession()` from `SpectatorContent`.
2. Remove whole-session `useSession()` from `useSpectatorMode`.
3. Split the layout so rover media rows do not depend on sidebar data.
4. Re-test mutation bursts and React commit behavior.
## Validation
Run:
```sh
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=30000 \
node perf/live-root-runtime-audit.mjs https://rover.otter.land/spectate perf/results
```
Expected improvements:
- Fewer added/removed DOM mutation bursts.
- Lower `ScriptDuration`.
- Possibly lower `RecalcStyleCount` and `LayoutCount`.
- `session:sync` socket counts may stay the same unless server/store work changes them.
## Risks
- Selector equality matters. Returning new arrays/objects from selectors can erase the win.
- Be careful with lockdown mode; the full-page lockdown branch must still update promptly.
- If `roster` reference stability is poor upstream, this issue may need `SessionContext`
structural sharing before it fully improves.
@@ -1,177 +0,0 @@
# Issue 005: Shared Log And Session Stream Pressure
## Summary
`/spectate` receives a large volume of shared live socket traffic, especially `log:entry`,
`session:sync`, `sensorFrame`, and `commandAck`. Some of this will likely be fixed by the
`/` backlog, but it remains important for `/spectate` because the route keeps logs/chat
visible in the sidebar while also rendering multiple live media feeds.
## Severity
Medium.
This is not as spectate-specific as the HUD map or WebRTC fan-out issues, but it adds
steady parsing, state, and render pressure.
## Upstream Likelihood
High for logs and shared session pressure.
If the `/` work implements log batching, log virtualization, selector-based session
updates, and chat/nickname churn reduction, a lot of this issue should improve without
special `/spectate` changes.
Related upstream issues:
```txt
perf/issues/003-high-volume-log-stream.md
perf/issues/006-chat-and-nickname-churn.md
perf/issues/007-timers-and-polling.md
perf/issues/004-sensor-telemetry-render-frequency.md
```
## Affected Files
- `webui/src/context/SessionContext.jsx`
- `webui/src/spectate/SpectatorApp/SpectatorContent.jsx`
- `webui/src/spectate/SpectatorApp/components/LogsRow.jsx`
- `webui/src/components/ChatPanel/index.jsx`
- `webui/src/components/RawUserPilePanel/index.jsx`
- `webui/src/components/RoverQueuesPanel/index.jsx`
- server log/session emitters
## Evidence
Artifact:
```sh
perf/results/2026-06-12T04-55-16-888Z-root-runtime/root-runtime-report.json
```
30s sample:
```txt
log:entry events: 634
log:entry bytes: 150,179
session:sync events: 54
session:sync bytes: 914,000
commandAck events: 68
chat:init bytes: 77,415
log:init bytes: 44,530
```
Text and input mutations also appeared:
```txt
Text characterData mutations: 164
input.field-input.flex-1 name mutations: 102
input.chat-composer-input name mutations: 102
```
The input-name churn matches the same type of sidebar/chat behavior observed on `/`.
## Current Code Path
`SpectatorContent` keeps these mounted:
```txt
webui/src/spectate/SpectatorApp/SpectatorContent.jsx:88
<ChatPanel allowSpectatorInput ... />
webui/src/spectate/SpectatorApp/SpectatorContent.jsx:91
<LogsRow ... />
webui/src/spectate/SpectatorApp/SpectatorContent.jsx:79
<RoverQueuesPanel title="Rovers" />
webui/src/spectate/SpectatorApp/SpectatorContent.jsx:68
<RawUserPilePanel ... />
```
That is useful spectator UI, but it means `/spectate` pays for general page state while
also paying for media and telemetry.
## Why This Matters
Logs and session syncs are deceptively expensive:
- socket payloads must be parsed
- state must be updated
- arrays/lists may be copied
- list components may rerender
- text/input attributes may mutate
This becomes worse on `/spectate` because the route is not just a dashboard. It is also a
multi-feed media surface.
## Fix Strategies
### Option A: Reuse The `/` Log Fix
The `/` log issue should probably introduce:
- batching log updates
- capping retained logs
- virtualizing or windowing visible rows
- pausing log rendering when collapsed/offscreen
- separating log ingestion from log rendering
Apply the same path to `LogsRow`.
### Option B: Make Spectator Sidebar Panels Independently Subscribed
The rover media grid should not rerender because logs/chat/users changed. This overlaps
with issue 004.
### Option C: Lower Or Gate Command/Log Visibility For Mobile
On mobile spectator layout, logs are less important than video health. Options:
- collapse logs by default
- batch logs while collapsed
- show only warning/error logs by default
- update visible logs at 1-2Hz
### Option D: Reduce `session:sync` Payload Churn
If the server sends full session snapshots frequently, consider:
- event-specific deltas for high-frequency branches
- preserving client-side branch references for unchanged data
- moving logs/chat out of the broad session path if they are currently coupled
## Recommended Path
Do not start here if you are working the `/` list first. Fix the `/` log/session/chat
items, then rerun `/spectate` and reassess.
If still hot on `/spectate`:
1. Make `LogsRow` batch/virtualize/collapse.
2. Keep the media grid isolated from sidebar state.
3. Add a mobile spectator mode that reduces log rendering frequency.
## Validation
Run:
```sh
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=30000 \
node perf/live-root-runtime-audit.mjs https://rover.otter.land/spectate perf/results
```
Expected improvements:
- Lower `Text:characterData` mutations.
- Fewer added/removed DOM mutation bursts.
- Lower `ScriptDuration`.
- `log:entry` socket count may remain high if only rendering is batched.
## Risks
- Logs are operationally useful; do not hide critical warnings.
- If batching is too aggressive, the UI can feel stale.
- Be careful not to mix log ingestion throttling with log display throttling. Usually,
display throttling is safer.

Some files were not shown because too many files have changed in this diff Show More