slopfixing / issue 002

This commit is contained in:
legop3
2026-06-12 02:19:08 -04:00
parent be8c3d7b29
commit c0050498b2
36 changed files with 5841 additions and 299 deletions
@@ -1,3 +1,4 @@
# FIXED!
# Issue 001: Global Input Listener Churn
## Summary
@@ -0,0 +1,849 @@
{
"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.

After

Width:  |  Height:  |  Size: 680 KiB

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

After

Width:  |  Height:  |  Size: 703 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

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

After

Width:  |  Height:  |  Size: 169 KiB

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

After

Width:  |  Height:  |  Size: 461 KiB

File diff suppressed because it is too large Load Diff
@@ -0,0 +1,896 @@
{
"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": []
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -12,7 +12,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
<title>Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-DhJId5qi.js"></script>
<script type="module" crossorigin src="/assets/index-D0FgC_A-.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BVKa8Zph.css">
</head>
<body>
+2 -3
View File
@@ -11,7 +11,7 @@ import {
ControlSystemProvider,
KeyboardInputManager,
GamepadInputManager,
useControlSystem,
useControlSelector,
} from './controls/index.js';
import RoomCameraPanel from './components/RoomCameraPanel/index.jsx';
import KinectPanel from './components/KinectPanel/index.jsx';
@@ -105,10 +105,9 @@ function MobileFeatureTabs({
const roverId = String(state.session?.assignment?.roverId || '').trim();
return roverId ? state.session?.audioForward?.[roverId] || null : null;
});
const { state: controlState } = useControlSystem();
const pttActive = useControlSelector((control) => Boolean(control.state.mic?.pttActive));
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' });
const vipDotClass = isVerified ? 'bg-emerald-400' : 'bg-amber-400';
const pttActive = Boolean(controlState?.mic?.pttActive);
const openMicEnabled = Boolean(vipAudio?.openMicEnabled);
const pttMode = vipAudio?.pttMode === 'clip' ? 'clip' : 'live';
const vipMicActive = Boolean(
@@ -2,7 +2,7 @@
// Purpose: Defines the Drive Dock Action module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useMemo, useState } from 'react';
import { useControlSystem } from '../../controls/index.js';
import { useControlActions, useControlSelector } from '../../controls/index.js';
import { useTelemetryFrame } from '../../context/TelemetryContext.jsx';
import { formatKeyLabel } from '../../controls/keymapUtils.js';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
@@ -103,10 +103,9 @@ export default function DriveDockAction({
compactHeightClass = '',
}) {
const isMobile = layout === 'mobile';
const {
state: { roverId, keymap },
actions,
} = useControlSystem();
const roverId = useControlSelector((control) => control.state.roverId);
const keymap = useControlSelector((control) => control.state.keymap);
const actions = useControlActions();
const dockAssist = useManualDockAssist();
const frame = useTelemetryFrame(roverId);
const state = driveDockState ?? deriveDriveDockState(frame);
+2 -4
View File
@@ -1,6 +1,6 @@
import { useSessionSelector } from '../../context/SessionContext.jsx';
import RoverMediaPlayer from '../RoverMediaPlayer/index.jsx';
import { useControlSystem } from '../../controls/index.js';
import { useControlSelector } from '../../controls/index.js';
import { useDriverVideoModePolicy } from '../../hooks/useDriverVideoModePolicy.js';
import TurnsOverlay from '../HudOverlays/TurnsOverlay/index.jsx';
import HudOverlay from '../HudOverlays/HudOverlay/index.jsx';
@@ -15,9 +15,7 @@ import CardFrame from '../CardFrame/index.jsx';
export default function DriverVideo({ layoutFormat = 'desktop' }) {
const roverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
const videoMode = useDriverVideoModePolicy(roverId);
const {
state: { lastControlIntentAt },
} = useControlSystem();
const lastControlIntentAt = useControlSelector((control) => control.state.lastControlIntentAt);
if (!roverId) {
return (
+4 -4
View File
@@ -1,13 +1,13 @@
// Help Overlay
// Purpose: Defines the Help Overlay module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useControlSystem } from '../../controls/index.js';
import { useControlSelector } from '../../controls/index.js';
import HelpContentView from '../HelpContentView/index.jsx';
export default function HelpOverlay({ visible, layout, onClose, showOnLoad, onToggleShowOnLoad }) {
if (!visible) return null;
const keymap = useControlSelector((control) => control.state.keymap);
const { state } = useControlSystem();
if (!visible) return null;
const handleCheckbox = (event) => {
const keepShowing = !event.target.checked;
@@ -39,7 +39,7 @@ export default function HelpOverlay({ visible, layout, onClose, showOnLoad, onTo
</div>
</div>
<div className="max-h-[85vh] overflow-y-auto p-0.5">
<HelpContentView layout={layout} keymap={state?.keymap || {}} />
<HelpContentView layout={layout} keymap={keymap || {}} />
</div>
</div>
</div>
+3 -3
View File
@@ -1,12 +1,12 @@
// Help Panel
// Purpose: Defines the Help Panel module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useControlSystem } from '../../controls/index.js';
import { useControlSelector } from '../../controls/index.js';
import CardFrame from '../CardFrame/index.jsx';
import HelpContentView from '../HelpContentView/index.jsx';
export default function HelpPanel({ layout, onOpenOverlay }) {
const { state } = useControlSystem();
const keymap = useControlSelector((control) => control.state.keymap);
const actions = (
<button
@@ -20,7 +20,7 @@ export default function HelpPanel({ layout, onOpenOverlay }) {
return (
<CardFrame title="Help" actions={actions} bodyClassName="space-y-0.5 text-sm">
<HelpContentView layout={layout} keymap={state?.keymap || {}} />
<HelpContentView layout={layout} keymap={keymap || {}} />
</CardFrame>
);
}
@@ -3,7 +3,7 @@
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useMemo } from 'react';
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
import { useControlSystem } from '../../controls/index.js';
import { useControlSelector } from '../../controls/index.js';
import { formatKeyLabel } from '../../controls/keymapUtils.js';
import CardFrame from '../CardFrame/index.jsx';
@@ -169,9 +169,7 @@ function LampTile({ entity, connected, controlsLocked, onToggle, onSetColor, onS
}
export default function HomeAssistantControls() {
const {
state: { keymap },
} = useControlSystem();
const keymap = useControlSelector((control) => control.state.keymap);
const ha = useSessionSelector((state) => state.session?.homeAssistant || null);
const { homeAssistantToggle, homeAssistantSetLightColor, homeAssistantSetLightWhite } =
useSessionActions();
@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useSessionSelector } from '../../../context/SessionContext.jsx';
import { useControlSystem } from '../../../controls/index.js';
import { useControlSelector } from '../../../controls/index.js';
import SocialButton from '../../SocialButton/index.jsx';
function TurnsOverlay({
@@ -15,9 +15,7 @@ function TurnsOverlay({
const turnQueues = useSessionSelector((state) => state.session?.turnQueues ?? {});
const socketId = useSessionSelector((state) => state.session?.socketId || null);
const activeDrivers = useSessionSelector((state) => state.session?.activeDrivers ?? {});
const {
state: { lastControlIntentAt },
} = useControlSystem();
const lastControlIntentAt = useControlSelector((control) => control.state.lastControlIntentAt);
const [now, setNow] = useState(() => Date.now());
const [showTurnCue, setShowTurnCue] = useState(false);
const [turnCueStartAt, setTurnCueStartAt] = useState(null);
@@ -2,7 +2,7 @@
// Purpose: Defines the Keymap Settings module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useMemo, useState, useEffect, useCallback } from 'react';
import { useControlSystem } from '../../controls/index.js';
import { useControlActions, useControlSelector } from '../../controls/index.js';
import { DEFAULT_KEYMAP } from '../../controls/constants.js';
import { canonicalizeKeyInput, formatKeyLabel } from '../../controls/keymapUtils.js';
import { setKeyboardCaptureLocked } from '../../controls/inputs/keyboardCaptureLock.js';
@@ -191,10 +191,8 @@ function BindingRow({ action, value, isActive, onCaptureStart, onCancel, onCaptu
}
export default function KeymapSettings() {
const {
state: { keymap },
actions: { updateKeyBinding, resetKeyBindings },
} = useControlSystem();
const keymap = useControlSelector((control) => control.state.keymap);
const { updateKeyBinding, resetKeyBindings } = useControlActions();
const { value: inputSettings, save: saveInputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
const keyboardSettings = {
...INPUT_SETTINGS_DEFAULTS.keyboard,
@@ -2,7 +2,7 @@
// Purpose: Assembles the mobile auxiliary controls column, which is the left column by default.
// Scope: Owns mobile aux/camera/night vision/horn wiring while reusing desktop variation components where intended.
import { useCallback, useRef } from 'react';
import { useControlSystem } from '../../controls/index.js';
import { useControlActions, useControlSelector } from '../../controls/index.js';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
import HornControl from '../HornControl/index.jsx';
import NightVisionControl from '../NightVisionControl/index.jsx';
@@ -11,19 +11,20 @@ import VacuumControls from './VacuumControls.jsx';
import VerticalCameraTilt from './VerticalCameraTilt.jsx';
function AuxColumnContent() {
const {
state: { roverId, camera, horn },
pipeline,
actions: { setServoAngle, setNightVision, setAuxMotors, startHorn, stopHorn },
} = useControlSystem();
const roverId = useControlSelector((control) => control.state.roverId);
const camera = useControlSelector((control) => control.state.camera);
const horn = useControlSelector((control) => control.state.horn);
const nightVision = useControlSelector((control) => control.pipeline?.nightVision);
const nightVisionState = useControlSelector((control) => control.pipeline?.nightVisionState);
const pipelineHorn = useControlSelector((control) => control.pipeline?.horn);
const { setServoAngle, setNightVision, setAuxMotors, startHorn, stopHorn } = useControlActions();
const dockAssist = useManualDockAssist();
const disabled = !roverId;
const activeAuxButtonRef = useRef(null);
const cameraConfig = camera?.config;
const cameraEnabled = Boolean(roverId && camera?.enabled && cameraConfig);
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
const nightVisionState = pipeline?.nightVisionState;
const hornAvailable = Boolean(roverId && pipeline?.horn);
const nightVisionAvailable = Boolean(roverId && nightVision);
const hornAvailable = Boolean(roverId && pipelineHorn);
const hornBlocked = horn?.overheated;
const cameraMin = typeof cameraConfig?.minAngle === 'number' ? cameraConfig.minAngle : -45;
const cameraMax = typeof cameraConfig?.maxAngle === 'number' ? cameraConfig.maxAngle : 45;
@@ -2,7 +2,7 @@
// Purpose: Provides the mobile movement control pad and speed mode selector.
// Scope: Converts touch pad cells into keyboard-style drive vectors; movement column owns drive/dock placement.
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useControlSystem } from '../../controls/index.js';
import { useControlActions, useControlSelector } from '../../controls/index.js';
import { normalizeKeymapEntries } from '../../controls/keymapUtils.js';
import {
computeKeyboardDriveVector,
@@ -29,10 +29,8 @@ function getSpeedModeConfig(speedMode) {
}
export default function ControlPadPanel({ disabled = false }) {
const {
state: { keymap: rawKeymap },
actions: { setDriveVector, registerInputState },
} = useControlSystem();
const rawKeymap = useControlSelector((control) => control.state.keymap);
const { setDriveVector, registerInputState } = useControlActions();
const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
const [speedMode, setSpeedMode] = useState('normal');
const [activeInputLabel, setActiveInputLabel] = useState('stop');
@@ -1,14 +1,12 @@
// Movement Column
// Purpose: Assembles the mobile movement column, which is the right column by default.
// Scope: Integrates drive/dock actions with the mobile control pad without hiding that dependency inside the pad.
import { useControlSystem } from '../../controls/index.js';
import { useControlSelector } from '../../controls/index.js';
import DriveDockAction, { useDriveDockState } from '../DriveDockAction/index.jsx';
import ControlPadPanel from './ControlPadPanel.jsx';
function MovementColumnContent({ layout }) {
const {
state: { roverId },
} = useControlSystem();
const roverId = useControlSelector((control) => control.state.roverId);
const driveDockState = useDriveDockState(roverId);
const dockedNotDriving = driveDockState.docked && !driveDockState.driving;
const expandAction = dockedNotDriving || driveDockState.dockingInProgress;
@@ -2,7 +2,7 @@
// Purpose: Defines the Overcurrent Limiter Panel module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useMemo } from 'react';
import { useControlSystem } from '../../controls/index.js';
import { useControlSelector } from '../../controls/index.js';
import { OVERCURRENT_GROUPS } from '../../controls/overcurrentLimiter.js';
import CardFrame from '../CardFrame/index.jsx';
@@ -26,10 +26,8 @@ function ProgressBar({ value, color = 'bg-emerald-500' }) {
}
export default function OvercurrentLimiterPanel() {
const {
state: { roverId },
overcurrentLimiter,
} = useControlSystem();
const roverId = useControlSelector((control) => control.state.roverId);
const overcurrentLimiter = useControlSelector((control) => control.overcurrentLimiter);
const groups = useMemo(() => OVERCURRENT_GROUPS.map((group) => group.key), []);
return (
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { useControlSystem } from '../../controls/index.js';
import { useControlSelector } from '../../controls/index.js';
import { formatKeyLabel } from '../../controls/keymapUtils.js';
import NicknameForm from '../NicknameForm/index.jsx';
import SocialButton from '../SocialButton/index.jsx';
@@ -53,9 +53,9 @@ export default function QuickstartOverlay({
onOpenHelp,
onClose,
}) {
const { state } = useControlSystem();
const rawKeymap = useControlSelector((control) => control.state.keymap);
const isDesktop = layout === 'desktop';
const keymap = useMemo(() => state?.keymap || {}, [state?.keymap]);
const keymap = useMemo(() => rawKeymap || {}, [rawKeymap]);
if (!visible) return null;
+13 -14
View File
@@ -13,7 +13,7 @@ import Tabs, { Tab, TabList, TabPanel, TabPanels } from '../Tabs/index.jsx';
import TopDownMap from '../TopDownMap/index.jsx';
import DriveDockAction, { useDriveDockState } from '../DriveDockAction/index.jsx';
import { useTelemetryFrame } from '../../context/TelemetryContext.jsx';
import { useControlSystem } from '../../controls/index.js';
import { useControlActions, useControlSelector } from '../../controls/index.js';
import RoverQueuesPanel from '../RoverQueuesPanel/index.jsx';
import RawUserPilePanel from '../RawUserPilePanel/index.jsx';
import { formatKeyLabel } from '../../controls/keymapUtils.js';
@@ -36,9 +36,7 @@ const CHAT_DOCK_MAX_HEIGHT = 300;
const CHAT_DOCK_BOTTOM_INSET = 8;
function TopDownMapPanel() {
const {
state: { roverId },
} = useControlSystem();
const roverId = useControlSelector((control) => control.state.roverId);
const frame = useTelemetryFrame(roverId);
const sensors = frame?.sensors || {};
@@ -54,20 +52,22 @@ function TopDownMapPanel() {
}
function DriveDockPanel() {
const {
state: { roverId, keymap, camera, horn },
pipeline,
actions: { setServoAngle, setNightVision, startHorn, stopHorn },
} = useControlSystem();
const roverId = useControlSelector((control) => control.state.roverId);
const keymap = useControlSelector((control) => control.state.keymap);
const camera = useControlSelector((control) => control.state.camera);
const horn = useControlSelector((control) => control.state.horn);
const nightVision = useControlSelector((control) => control.pipeline?.nightVision);
const nightVisionState = useControlSelector((control) => control.pipeline?.nightVisionState);
const pipelineHorn = useControlSelector((control) => control.pipeline?.horn);
const { setServoAngle, setNightVision, startHorn, stopHorn } = useControlActions();
const dockAssist = useManualDockAssist();
const driveDockState = useDriveDockState(roverId);
const hideInlineControls = driveDockState.docked && !driveDockState.driving;
const config = camera?.config;
const cameraEnabled = Boolean(roverId && camera?.enabled && config);
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
const nightVisionState = pipeline?.nightVisionState;
const hornAvailable = Boolean(roverId && pipeline?.horn);
const nightVisionAvailable = Boolean(roverId && nightVision);
const hornAvailable = Boolean(roverId && pipelineHorn);
const hornBlocked = horn?.overheated;
const min = typeof config?.minAngle === 'number' ? config.minAngle : -30;
const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30;
@@ -146,10 +146,9 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
const roverId = String(state.session?.assignment?.roverId || '').trim();
return roverId ? state.session?.audioForward?.[roverId] || null : null;
});
const { state: controlState } = useControlSystem();
const pttActive = useControlSelector((control) => Boolean(control.state.mic?.pttActive));
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' });
const vipDotClass = isVerified ? 'bg-emerald-400' : 'bg-red-600';
const pttActive = Boolean(controlState?.mic?.pttActive);
const openMicEnabled = Boolean(vipAudio?.openMicEnabled);
const pttMode = vipAudio?.pttMode === 'clip' ? 'clip' : 'live';
const vipMicActive = Boolean(
+4 -5
View File
@@ -2,7 +2,7 @@
// Purpose: Defines the Settings Panel module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useMemo } from 'react';
import { useControlSystem } from '../../controls/index.js';
import { useControlActions, useControlSelector } from '../../controls/index.js';
import AuthPanel from '../AuthPanel/index.jsx';
import AdminPanel from '../AdminPanel/index.jsx';
import KeymapSettings from '../KeymapSettings/index.jsx';
@@ -91,10 +91,9 @@ function RangeSetting({ label, value, disabled = false, onChange }) {
}
export default function SettingsPanel() {
const {
state: { keymap, roverId },
actions: { sendOiCommand, setSensorStream },
} = useControlSystem();
const keymap = useControlSelector((control) => control.state.keymap);
const roverId = useControlSelector((control) => control.state.roverId);
const { sendOiCommand, setSensorStream } = useControlActions();
const canControl = Boolean(roverId);
const [hudMapDesktop, setHudMapDesktop] = useHudMapSetting();
const socket = useSocket();
@@ -3,7 +3,7 @@
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { fieldClass } from '../constants.js';
import { useControlSystem } from '../../../controls/index.js';
import { useControlSelector } from '../../../controls/index.js';
import { formatKeyLabel } from '../../../controls/keymapUtils.js';
import { useSettingsNamespace } from '../../../settings/index.js';
import { MAX_UPLOAD_BYTES, TARGET_SAMPLE_RATE, RTC_CONFIG } from './constants.js';
@@ -28,7 +28,8 @@ export default function VipAudioUploadCard({
readyMicWhip,
stopMicWhip,
}) {
const { state: controlState } = useControlSystem();
const keymap = useControlSelector((control) => control.state.keymap);
const pttActive = useControlSelector((control) => Boolean(control.state.mic?.pttActive));
const { value: vipAudio, save: saveVipAudio } = useSettingsNamespace('vipAudio', {
openMicEnabled: false,
pttMode: 'live',
@@ -60,7 +61,6 @@ export default function VipAudioUploadCard({
const clipRecordingRef = useRef(false);
const clipSampleRateRef = useRef(TARGET_SAMPLE_RATE);
const pttActive = Boolean(controlState?.mic?.pttActive);
const selectedForwardState = useMemo(
() => (roverId ? audioForwardByRover?.[roverId] || null : null),
@@ -81,7 +81,7 @@ export default function VipAudioUploadCard({
const whipLinkActive = !clipMode && (micState === 'live' || micState === 'starting');
const clipRecording = clipMode && clipState === 'recording';
const clipSending = clipMode && clipState === 'sending';
const pttKeyLabel = formatKeyLabel(controlState?.keymap?.micPtt?.[0]) || 'M';
const pttKeyLabel = formatKeyLabel(keymap?.micPtt?.[0]) || 'M';
const setPttMode = useCallback(
(nextMode) => {
+216 -45
View File
@@ -1,6 +1,17 @@
// Control Context Provider
// Purpose: Exposes control-system state/actions to control-capable components. Scope: Owns reducer wiring, pipeline integration, and top-level provider hooks.
import { createContext, useCallback, useContext, useEffect, useMemo, useReducer, useRef } from 'react';
/* eslint-disable react-refresh/only-export-components */
import {
createContext,
useCallback,
useContext,
useEffect,
useLayoutEffect,
useMemo,
useReducer,
useRef,
useState,
} from 'react';
import { controlReducer, initialControlState } from './controlReducer.js';
import { computeDifferentialSpeeds, clamp } from './controlMath.js';
import { useCommandPipeline } from './commandPipeline.js';
@@ -27,6 +38,35 @@ import {
const ControlSystemContext = createContext(null);
function normalizeSelector(selector) {
return typeof selector === 'function' ? selector : (snapshot) => snapshot;
}
const CONTROL_ACTION_NAMES = [
'setMode',
'setDriveVector',
'setAuxMotors',
'setServoAngle',
'nudgeServo',
'goServoHome',
'runMacro',
'stopAllMotion',
'sendOiCommand',
'setSensorStream',
'setNightVision',
'toggleNightVision',
'updateKeyBinding',
'resetKeyBindings',
'registerInputState',
'setManualDockAssistActive',
'toggleManualDockAssist',
'setSongNote',
'sendSong',
'startHorn',
'stopHorn',
'setMicPttActive',
];
function cloneKeymap(map) {
return Object.fromEntries(
Object.entries(map || {}).map(([key, values]) => [key, Array.isArray(values) ? [...values] : []]),
@@ -59,6 +99,53 @@ function removeDriveSequenceBackoff(steps = []) {
export function ControlSystemProvider({ children }) {
const [state, dispatch] = useReducer(controlReducer, initialControlState);
const snapshotRef = useRef(null);
const subscribersRef = useRef(new Set());
const actionImplementationsRef = useRef({});
const [stableActions] = useState(() => {
/*
Public action functions intentionally keep stable identities. Each wrapper
reads the current implementation at call time, so consumers can depend on
actions without re-rendering just because the real implementation now
closes over a new state slice, pipeline object, or settings value.
*/
return Object.fromEntries(
CONTROL_ACTION_NAMES.map((name) => [
name,
(...args) => actionImplementationsRef.current[name]?.(...args),
]),
);
}, []);
const getControlSnapshot = useCallback(() => snapshotRef.current, []);
const subscribeControlSnapshot = useCallback((selector, listener, equalityFn = Object.is) => {
const normalizedSelector = normalizeSelector(selector);
const currentSnapshot = snapshotRef.current;
const subscriber = {
selector: normalizedSelector,
equalityFn,
listener,
current: normalizedSelector(currentSnapshot),
};
subscribersRef.current.add(subscriber);
return () => {
subscribersRef.current.delete(subscriber);
};
}, []);
const notifyControlSubscribers = useCallback((nextSnapshot) => {
/*
The provider context value below is stable, so React will not fan out an
update to every consumer automatically. This loop is the replacement: it
asks each subscribed component whether the exact value it selected changed
and only wakes that component when its selected value is different.
*/
subscribersRef.current.forEach((subscriber) => {
const nextSelected = subscriber.selector(nextSnapshot);
if (!subscriber.equalityFn(subscriber.current, nextSelected)) {
subscriber.current = nextSelected;
subscriber.listener(nextSelected);
}
});
}, []);
const prevModeRef = useRef(null);
const pendingLightsRef = useRef(false);
const servoAngleRef = useRef(initialControlState.camera.angle);
@@ -166,7 +253,7 @@ export function ControlSystemProvider({ children }) {
if (pipeline.roverId) {
pipeline.enableSensorStream();
}
}, [pipeline.roverId, pipeline.enableSensorStream]);
}, [pipeline]);
const setMode = useCallback(
(mode) => {
@@ -430,6 +517,8 @@ export function ControlSystemProvider({ children }) {
return { waveform, freqs: normalized };
}, [hornSettings]);
const stopHornRef = useRef(null);
const startHorn = useCallback(() => {
if (!pipeline.horn) return;
if (state.horn?.overheated) return false;
@@ -445,7 +534,7 @@ export function ControlSystemProvider({ children }) {
hornAutoStopRef.current = null;
}
hornAutoStopRef.current = setTimeout(() => {
stopHorn();
stopHornRef.current?.();
}, HORN_MAX_MS);
recordControlIntent();
return true;
@@ -474,15 +563,13 @@ export function ControlSystemProvider({ children }) {
}
dispatch({ type: 'control/set-horn-heat', payload: { heat: nextHeat, overheated: nextOverheated } });
}
}, [dispatch, pipeline, state.horn?.heat, state.horn?.overheated, HORN_HEAT_UP_PER_SEC]);
}, [dispatch, pipeline, state.horn?.heat, state.horn?.overheated]);
const hornStateRef = useRef({
active: Boolean(state.horn?.active),
heat: state.horn?.heat ?? 0,
overheated: Boolean(state.horn?.overheated),
});
const stopHornRef = useRef(stopHorn);
useEffect(() => {
hornStateRef.current = {
active: Boolean(state.horn?.active),
@@ -525,7 +612,7 @@ export function ControlSystemProvider({ children }) {
}
}, tickMs);
return () => clearInterval(interval);
}, [dispatch, hornNeedsTick, HORN_HEAT_COOL_PER_SEC, HORN_HEAT_RESUME_THRESHOLD, HORN_HEAT_UP_PER_SEC]);
}, [dispatch, hornNeedsTick]);
const setManualDockAssistActive = useCallback(
(active) => {
@@ -561,41 +648,32 @@ export function ControlSystemProvider({ children }) {
dispatch({ type: 'control/set-mic-ptt', payload: Boolean(active) });
}, []);
const contextValue = useMemo(
const actionImplementations = useMemo(
() => ({
state,
dispatch,
pipeline,
overcurrentLimiter,
actions: {
setMode,
setDriveVector,
setAuxMotors,
setServoAngle,
nudgeServo,
goServoHome,
runMacro,
stopAllMotion,
sendOiCommand,
setSensorStream,
setNightVision,
toggleNightVision,
updateKeyBinding,
resetKeyBindings,
registerInputState,
setManualDockAssistActive,
toggleManualDockAssist,
setSongNote,
sendSong,
startHorn,
stopHorn,
setMicPttActive,
},
setMode,
setDriveVector,
setAuxMotors,
setServoAngle,
nudgeServo,
goServoHome,
runMacro,
stopAllMotion,
sendOiCommand,
setSensorStream,
setNightVision,
toggleNightVision,
updateKeyBinding,
resetKeyBindings,
registerInputState,
setManualDockAssistActive,
toggleManualDockAssist,
setSongNote,
sendSong,
startHorn,
stopHorn,
setMicPttActive,
}),
[
state,
pipeline,
overcurrentLimiter,
setMode,
setDriveVector,
setAuxMotors,
@@ -621,13 +699,106 @@ export function ControlSystemProvider({ children }) {
],
);
return <ControlSystemContext.Provider value={contextValue}>{children}</ControlSystemContext.Provider>;
const snapshot = useMemo(
() => ({
state,
dispatch,
pipeline,
overcurrentLimiter,
actions: stableActions,
}),
[state, pipeline, overcurrentLimiter, stableActions],
);
if (snapshotRef.current == null) {
/*
The first render has to make a snapshot available synchronously because
descendants can call selector hooks during that same render pass. Later
renders publish updates from the layout effect below, after React has
committed the provider's new state.
*/
snapshotRef.current = snapshot;
}
useLayoutEffect(() => {
actionImplementationsRef.current = actionImplementations;
}, [actionImplementations]);
useLayoutEffect(() => {
if (snapshotRef.current === snapshot) return;
snapshotRef.current = snapshot;
notifyControlSubscribers(snapshot);
}, [notifyControlSubscribers, snapshot]);
const store = useMemo(
() => ({
getSnapshot: getControlSnapshot,
subscribe: subscribeControlSnapshot,
actions: stableActions,
}),
[getControlSnapshot, stableActions, subscribeControlSnapshot],
);
return <ControlSystemContext.Provider value={store}>{children}</ControlSystemContext.Provider>;
}
export function useControlSelector(selector, equalityFn = Object.is) {
const store = useContext(ControlSystemContext);
if (!store) {
throw new Error('useControlSelector must be used within ControlSystemProvider');
}
const selectorRef = useRef(selector);
const equalityRef = useRef(equalityFn);
const [selected, setSelected] = useState(() => selector(store.getSnapshot()));
useEffect(() => {
/*
Selector functions are often declared inline at the call site. Refreshing
these refs from an effect keeps the subscription callback pointed at the
newest selector/equality pair without making render mutate refs.
*/
selectorRef.current = selector;
equalityRef.current = equalityFn;
}, [equalityFn, selector]);
useEffect(() => {
/*
Recheck once after subscribing because the provider may have published a
newer snapshot between this component's render and its effect. The equality
guard keeps that synchronization from causing a redundant render.
*/
setSelected((prev) => {
const next = selectorRef.current(store.getSnapshot());
return equalityRef.current(prev, next) ? prev : next;
});
return store.subscribe(
(snapshot) => selectorRef.current(snapshot),
(nextSelected) => {
setSelected((prev) => (equalityRef.current(prev, nextSelected) ? prev : nextSelected));
},
(a, b) => equalityRef.current(a, b),
);
}, [store]);
return selected;
}
export function useControlActions() {
const store = useContext(ControlSystemContext);
if (!store) {
throw new Error('useControlActions must be used within ControlSystemProvider');
}
return store.actions;
}
export function useControlSystem() {
const context = useContext(ControlSystemContext);
if (!context) {
throw new Error('useControlSystem must be used within ControlSystemProvider');
}
return context;
/*
This compatibility hook intentionally selects the whole snapshot, so it has
the same broad update behavior as the old context API. New and migrated
consumers should prefer useControlSelector/useControlActions so they only
re-render for data they actually read.
*/
return useControlSelector((snapshot) => snapshot);
}
+6 -1
View File
@@ -1,6 +1,11 @@
// Control Module Exports
// Purpose: Re-exports control context/providers and input managers from one entrypoint. Scope: Keeps control imports stable and concise for app/module consumers.
export { ControlSystemProvider, useControlSystem } from './ControlContext.jsx';
export {
ControlSystemProvider,
useControlActions,
useControlSelector,
useControlSystem,
} from './ControlContext.jsx';
export { default as KeyboardInputManager } from './inputs/KeyboardInputManager.jsx';
export { default as GamepadInputManager } from './inputs/GamepadInputManager.jsx';
export { useOvercurrentLimiter } from './overcurrentLimiter.js';
@@ -1,7 +1,7 @@
// Gamepad Input Manager
// Purpose: Converts polled gamepad state into normalized control actions/commands. Scope: Integrates bindings, deadzone math, and dispatch callbacks for driving.
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
import { useControlSystem } from '../ControlContext.jsx';
import { useControlActions, useControlSelector } from '../ControlContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js';
import { GAMEPAD_SETTINGS_DEFAULTS, GAMEPAD_PROFILE_DEFAULT } from '../../settings/namespaces.js';
import {
@@ -48,17 +48,16 @@ function pickActivePad(pads, activeSignature) {
export default function GamepadInputManager() {
const {
state,
actions: {
setMode,
setDriveVector,
setAuxMotors,
setServoAngle,
runMacro,
toggleNightVision,
registerInputState,
},
} = useControlSystem();
setMode,
setDriveVector,
setAuxMotors,
setServoAngle,
runMacro,
toggleNightVision,
registerInputState,
} = useControlActions();
const cameraAngle = useControlSelector((control) => control.state.camera?.angle);
const cameraConfig = useControlSelector((control) => control.state.camera?.config);
const dockAssist = useManualDockAssist();
const { value: gamepadSettings, save: saveGamepadSettings } = useSettingsNamespace(
'gamepad',
@@ -162,8 +161,8 @@ export default function GamepadInputManager() {
// with the newest settings and control actions without resubscribing to the hub.
latestRef.current = {
activeSignature,
cameraAngle: state.camera?.angle,
cameraConfig: state.camera?.config,
cameraAngle,
cameraConfig,
dockAssist,
gamepadSettings,
registerInputState,
@@ -1,7 +1,7 @@
// Keyboard Input Manager
// Purpose: Captures and translates keyboard events into normalized control intents. Scope: Owns keydown/keyup listeners and dispatch coordination for drive controls.
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
import { useControlSystem } from '../ControlContext.jsx';
import { useControlActions, useControlSelector } from '../ControlContext.jsx';
import { useChat } from '../../context/ChatContext.jsx';
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
import { normalizeKeymapEntries, tokensForEvent } from '../keymapUtils.js';
@@ -83,30 +83,32 @@ function shouldIgnoreEvent(event) {
export default function KeyboardInputManager() {
const {
state,
actions: {
setMode,
setDriveVector,
setAuxMotors,
nudgeServo,
runMacro,
stopAllMotion,
registerInputState,
toggleNightVision,
startHorn,
stopHorn,
setMicPttActive,
setSongNote,
sendSong,
},
} = useControlSystem();
setMode,
setDriveVector,
setAuxMotors,
nudgeServo,
runMacro,
stopAllMotion,
registerInputState,
toggleNightVision,
startHorn,
stopHorn,
setMicPttActive,
setSongNote,
sendSong,
} = useControlActions();
const rawKeymap = useControlSelector((control) => control.state.keymap);
const cameraNudgeDegrees = useControlSelector((control) => control.state.camera?.config?.nudgeDegrees);
const songNote = useControlSelector((control) => control.state.song?.note);
const roverId = useControlSelector((control) => control.state.roverId);
const hornActive = useControlSelector((control) => Boolean(control.state.horn?.active));
const homeAssistant = useSessionSelector((state) => state.session?.homeAssistant || null);
const dockAssist = useManualDockAssist();
const { homeAssistantSetState } = useSessionActions();
const { focusChat, isChatFocused } = useChat();
const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
const { save: saveVideoSettings } = useSettingsNamespace('video', VIDEO_SETTINGS_DEFAULTS);
const keymap = useMemo(() => normalizeKeymapEntries(state.keymap), [state.keymap]);
const keymap = useMemo(() => normalizeKeymapEntries(rawKeymap), [rawKeymap]);
const actionTokens = useMemo(() => {
const tokens = new Set();
Object.values(keymap).forEach((bindingSet) => {
@@ -132,8 +134,8 @@ export default function KeyboardInputManager() {
return mapTiltSpeedToInterval(tiltSpeed);
}, [inputSettings?.keyboard]);
const servoStep = useMemo(
() => Math.abs(state.camera?.config?.nudgeDegrees || 1),
[state.camera?.config?.nudgeDegrees],
() => Math.abs(cameraNudgeDegrees || 1),
[cameraNudgeDegrees],
);
const activeTokensRef = useRef(new Set());
@@ -352,7 +354,7 @@ export default function KeyboardInputManager() {
setMicPttActive,
setMode,
setSongNote,
songNote: state.song?.note,
songNote,
startHorn,
stopAllMotion,
stopHorn,
@@ -455,11 +457,11 @@ export default function KeyboardInputManager() {
useEffect(() => {
latestResetAllRef.current();
}, [state.roverId]);
}, [roverId]);
useEffect(() => {
hornActiveRef.current = Boolean(state.horn?.active);
}, [state.horn?.active]);
hornActiveRef.current = hornActive;
}, [hornActive]);
useEffect(
() => () => {
@@ -1,19 +1,17 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useControlSystem } from '../../controls/index.js';
import { useControlActions, useControlSelector } from '../../controls/index.js';
import { useTelemetryFrame } from '../../context/TelemetryContext.jsx';
export function useManualDockAssist(options = {}) {
const { manageLifecycle = false } = options;
const {
state: { roverId, manualDockAssist },
actions,
} = useControlSystem();
const roverId = useControlSelector((control) => control.state.roverId);
const active = useControlSelector((control) => Boolean(control.state.manualDockAssist?.active));
const actions = useControlActions();
const frame = useTelemetryFrame(roverId);
const sensors = frame?.sensors || {};
const chargingLabel = sensors?.chargingState?.label || '';
const docked = Boolean(sensors?.chargingSources?.homeBase);
const charging = docked && chargingLabel.toLowerCase() !== 'not charging' && chargingLabel !== '';
const active = Boolean(manualDockAssist?.active);
const wasDockedRef = useRef(false);
const enterAssist = useCallback(() => {