Compare commits

..
Author SHA1 Message Date
legop3 9d8e22ad1e fix some wording in response to community complaints 2026-07-16 16:58:27 -04:00
legop3 385e7c25fa fixins 2026-07-16 16:45:51 -04:00
legop3 3a8a2ebb13 the big 2026-07-15 00:35:39 -04:00
legop3 96d06091ee going on a command sidequest 2026-07-14 23:47:42 -04:00
legop3 15e03e62ed page title from interinstance config name!! 2026-07-14 23:11:14 -04:00
legop3 3aa97baa4f vip only spectator option 2026-07-14 21:38:55 -04:00
legop3 017b3c69d5 spectator page selectors 2026-07-14 21:13:09 -04:00
legop3 ad7de34d6d appoint spectator access when you login from external, actually... 2026-07-14 20:42:37 -04:00
legop3 51fbee400c spectator login for exties 2026-07-14 20:32:25 -04:00
legop3 7fe5730953 idle service and lght lock improvements 2026-07-14 17:38:17 -04:00
legop3 0d6b4d68de bandwidth savings configs 2026-07-14 17:04:08 -04:00
legop3 1c401ff90a label adjustments 2026-07-14 14:47:46 -04:00
legop3 f6b9fa798e ptz operator in /display 2026-07-14 14:32:25 -04:00
legop3 f667bbce53 tryina make mini not reload stuff.. 2026-07-14 13:34:12 -04:00
legop3 f480e01bf7 dont replace mmtx config 2026-07-14 01:23:11 -04:00
legop3 e2e94da656 ptz in mini and better mini 2026-07-13 18:52:37 -04:00
legop3 8ee680ce9f let everyone make ptz presets.. .. .. .. .. . . . . 2026-07-13 17:39:13 -04:00
legop3 be37a39291 oitercurrent 2026-07-13 16:21:05 -04:00
legop3 af484f5099 oitercurrenting 2026-07-13 16:17:36 -04:00
legop3 5d8cb48fd0 update all rovers buttone 2026-07-13 16:03:51 -04:00
legop3 c742fa1c81 run self update as systemd run 2026-07-13 15:49:09 -04:00
legop3 6dc067580d reboot on self update finally 2026-07-13 15:41:17 -04:00
legop3 d9e6317220 slower iframes 2026-07-13 15:34:44 -04:00
legop3 f969e50772 Merge pull request #14 from legop3/ptz
Ptz
2026-07-13 14:55:09 -04:00
legop3 7d3f702e32 lower neolink volume? 2026-07-12 19:50:05 -04:00
legop3 5f3206a065 server gtts installer stuff 2026-07-12 19:47:00 -04:00
legop3 411313b21b noodles 2026-07-12 18:07:26 -04:00
legop3 fa92726e9c installer fix 34343434 2026-07-12 18:01:32 -04:00
legop3 30b8867b3a ptztts? 2026-07-12 17:51:54 -04:00
legop3 e254eea9e4 bwaha 2026-07-12 15:36:48 -04:00
legop3 60eacf982c aeaeawa 2026-07-12 14:55:21 -04:00
legop3 23108241f1 ratelimit slop 2026-07-12 14:46:12 -04:00
legop3 cc525afe20 hopefully just bad option.. . .. .. .. .. .. .. . 2026-07-12 14:34:42 -04:00
legop3 a5884d9eac ugh.. logigng... 2026-07-12 14:30:10 -04:00
legop3 0fc4973cb7 hopefully fix ptz stability isues 2026-07-12 14:19:01 -04:00
legop3 7dfdf62c94 status betterify 2026-07-12 13:35:42 -04:00
legop3 7286c5b36c presetses 2026-07-12 13:19:36 -04:00
legop3 4360e9ca03 gooeygooey 2026-07-12 12:58:17 -04:00
legop3 dd0ba60d49 peteze 2026-07-12 12:50:38 -04:00
legop3 b5cb9bc8e4 boble 2026-07-12 02:25:01 -04:00
legop3 fd1b3e103d bobile 2026-07-12 02:21:13 -04:00
legop3 9732f6c080 slightly larger vido 2026-07-12 02:14:19 -04:00
legop3 2064c4196c videosize 2026-07-12 02:09:01 -04:00
legop3 2cef95e6e3 bwah 2026-07-12 01:58:46 -04:00
legop3 76318e6b36 rover over 2026-07-12 01:36:30 -04:00
legop3 09ce66e7a4 new ptz ui 2026-07-12 01:19:36 -04:00
legop3 fd1caf2df9 what is goung on 2026-07-12 00:01:06 -04:00
legop3 58410cd66b more overcirrent acjustments 2026-07-11 23:53:19 -04:00
legop3 6d4000bed7 impromptu overcurrent adjustment 2026-07-11 23:26:16 -04:00
legop3 6e63f0e19c Acknowledge use of language models in project
Added acknowledgment for assistance from language models.
2026-07-11 22:04:54 -04:00
legop3 ed24d9ea89 request message fix 2026-07-11 19:56:08 -04:00
legop3 b3141e9870 chatinpanel 2026-07-11 19:49:13 -04:00
legop3 64d6d5a601 stoatus 2026-07-11 19:13:30 -04:00
legop3 db44d23947 update idleservice so that its based on users not drivers 2026-07-11 19:06:20 -04:00
legop3 924a3c3d55 whip whep 2026-07-11 18:40:22 -04:00
legop3 4d66defae0 sapshots 2026-07-11 18:35:58 -04:00
legop3 0bb3f89472 better video ptz stuf 2026-07-11 18:25:30 -04:00
legop3 e4ada54cf4 ptsoectate 2026-07-11 18:06:12 -04:00
legop3 b393c2b2b4 move info panel to bottom cause it moves the whole column lol 2026-07-11 16:28:37 -04:00
legop3 18649deeae ffmpreg 2026-07-11 14:07:20 -04:00
legop3 6747658106 noframe 2026-07-11 14:00:48 -04:00
legop3 033a2bae43 awae 2026-07-11 13:56:26 -04:00
legop3 27dbb068be awaw 2026-07-11 13:51:55 -04:00
legop3 71706fb1b9 mobile bobile 2026-07-11 13:46:02 -04:00
legop3 65b54f01d2 gawawa 2026-07-11 13:39:08 -04:00
legop3 026e9de476 awawea 2026-07-11 13:32:15 -04:00
legop3 91bbeb6d8a awawaa 2026-07-11 13:27:03 -04:00
legop3 43e4527ad5 awaw 2026-07-11 13:17:29 -04:00
legop3 a07d532043 pull up 2026-07-11 12:38:33 -04:00
legop3 2bd6215be2 transcoding adjustments 2026-07-11 12:26:13 -04:00
legop3 e02b7a2eb7 replay temp files instead of building from the rolling buffer 2026-07-11 12:16:47 -04:00
legop3 10a586e5d0 ptzreplay 2026-07-11 12:01:21 -04:00
legop3 775dd7b830 low quality ptz snapshots 2026-07-11 04:11:28 -04:00
legop3 f2d3567978 IR controls 2026-07-11 03:59:34 -04:00
legop3 10121650de i give up 2026-07-11 03:53:39 -04:00
legop3 9b875aedcb why?? 2026-07-11 03:52:08 -04:00
legop3 6d685c26ba what. 2026-07-11 03:51:11 -04:00
legop3 dd8e87fda7 oops... numbers.. 2026-07-11 03:49:34 -04:00
legop3 bf3ce9a28a spotlite 2026-07-11 03:48:12 -04:00
legop3 0c0b55fe88 unmute me 2026-07-11 03:33:19 -04:00
legop3 c06ac6bc20 oddiopus 2026-07-11 03:27:47 -04:00
legop3 8ec9ecc8d4 fasterrr 2026-07-11 03:06:42 -04:00
legop3 02599a44e4 ugh. re-encode h264.. 2026-07-11 02:54:17 -04:00
legop3 4b8aa67c32 zoomfixe 2026-07-11 02:29:41 -04:00
legop3 0e5f76fb2f awa 2026-07-11 02:22:13 -04:00
legop3 3af74870a5 fixe 2026-07-11 01:09:26 -04:00
legop3 aee1a9d563 pete 2026-07-11 00:38:05 -04:00
legop3 b3001cf0a3 esm cjs blah blah blah 2026-07-10 23:23:38 -04:00
legop3 d777e3a48e ptzwawa 2026-07-10 23:20:09 -04:00
legop3 ee393adc8e slopping 2026-07-10 23:03:31 -04:00
legop3 c406ace339 planing 2026-07-10 00:57:10 -04:00
legop3 6a31d8bc35 configurable discord prefix! yay 2026-07-10 00:50:19 -04:00
legop3 a6f6ccf079 optional but always on transfer 2026-07-08 12:53:39 -04:00
legop3 5d60544904 slop glorping alsa device options for laptop only 2026-07-07 21:41:00 -04:00
legop3 3a40a65d46 change interinstance mode wording 2026-07-07 12:41:01 -04:00
legop3 f15ace85f6 protect overcurrent faster because of faster rover wheel rover over rover 2026-07-07 12:31:21 -04:00
legop3 656bf90e7f wheel speed sensors and wheel layer rework 2026-07-07 12:21:08 -04:00
legop3 2d75935e65 dont send room cam frames on subscription 2026-07-07 11:36:27 -04:00
legop3 42c248e298 Merge pull request #13 from legop3/fix-laptoprover-audio
Fix laptoprover tts and horn including gtts
2026-07-07 00:34:03 -04:00
legop3 55e8235b02 Keep laptop Google TTS changes out of Pi profile 2026-07-07 00:23:50 -04:00
legop3 77de628c0b Restore Pi Google TTS asset behavior 2026-07-07 00:23:29 -04:00
legop3 f31b21559c Add laptop-only Chrome TTS daemon 2026-07-07 00:23:06 -04:00
legop3 f7514e71cc Restore Pi Chrome TTS daemon unchanged 2026-07-07 00:22:47 -04:00
legop3 f4683cd47d Install GCC runtime for laptop Chrome TTS 2026-07-07 00:14:45 -04:00
legop3 2beb1498fa Preload compiler runtimes for Chrome TTS 2026-07-07 00:14:33 -04:00
legop3 d349df2432 Share Google TTS asset installer with laptop profile 2026-07-07 00:01:51 -04:00
legop3 11340bf3f6 Make laptop ALSA routing match Pi rover 2026-07-07 00:01:26 -04:00
legop3 e6c4931210 Make Debian laptop audio setup appliance-like 2026-07-07 00:01:13 -04:00
legop3 8f0ac358d6 ui adjustments 2026-07-06 21:52:20 -04:00
legop3 4204a66549 ui adjustments 2026-07-06 20:13:45 -04:00
legop3 a8bff428c2 interinstance styling changes yay 2026-07-06 19:10:26 -04:00
legop3 69b49ae1d6 inter-instance UI redo 2026-07-06 15:58:40 -04:00
legop3 07ad43f42f private rover security! and styling updaes 2026-07-06 15:26:43 -04:00
legop3 f9f87c00d3 inter-instance! 2026-07-06 15:17:24 -04:00
legop3 6f6325f477 plannings 2026-07-05 23:37:34 -04:00
legop3 0b3c7869af inter-instance plannings 2026-07-05 23:27:26 -04:00
legop3 b526beb712 driver removal information finaly! 2026-07-05 22:45:38 -04:00
legop3 df22ac6d81 plannings 2026-07-05 22:12:28 -04:00
legop3 6c06275c6d Merge branch 'main' of https://github.com/legop3/MultiRoombaRover 2026-07-05 21:51:35 -04:00
legop3 3aea6d4766 private rover virtual wall changes 2026-07-05 21:51:33 -04:00
legop3 3e632ac607 Remove virtual wall support task for private rovers
Removed the task for adding virtual wall support for private rovers.
2026-07-05 18:58:53 -04:00
legop3 d77ec54bc9 virtual wall private rover safety 2026-07-05 14:57:26 -04:00
legop3 40e8adf15a big overhaul for server and webui feature matching, things default to disabled and disappear from UI when disabled. 2026-07-05 14:42:43 -04:00
legop3 29bf4cc5d2 plannings 2026-07-05 13:18:39 -04:00
legop3 b083938338 light lock rs command 2026-07-04 21:07:13 -04:00
legop3 5cade7a941 Merge pull request #12 from legop3/laptoprover
Laptoprover
2026-07-04 20:24:17 -04:00
legop3 8aaffacc2a plannings 2026-07-04 20:20:15 -04:00
legop3 00277667d6 Update wikiUrl for Green Ball Container 2026-07-04 15:36:47 -04:00
legop3 83a6910c25 Add new entity 'o009' to barcode registry 2026-07-01 13:19:13 -04:00
legop3 ac8be3c4b5 installer splitup and laptop alsa stuff 2026-07-01 12:49:23 -04:00
legop3 295d01f7cc full speed turbo i guess.... 2026-07-01 01:48:39 -04:00
legop3 6ad7701b8f video publisher more options 2026-07-01 00:03:55 -04:00
legop3 282cb3e135 debian-laptop 2026-06-30 23:34:05 -04:00
legop3 d8e63bdf2e new default speeds because faster rovers 2026-06-30 22:48:46 -04:00
legop3 fa4852b93c Merge pull request #11 from legop3/laptoprover
Laptoprover
2026-06-30 22:08:00 -04:00
legop3 37b8f71944 didnt build roverd 2026-06-30 20:22:31 -04:00
legop3 31eacea7bc config reshape 2026-06-30 20:17:43 -04:00
legop3 2eb6c00f9c fix private rover requests hopefully! 2026-06-30 19:34:35 -04:00
legop3 d10a321759 plannings 2026-06-30 19:21:17 -04:00
legop3 0cc40b7608 precision camera stuff 2026-06-29 21:06:21 -04:00
legop3 79be3e1583 improvements and bug fixes 2026-06-29 20:19:24 -04:00
legop3 02df826eb4 queue fixes and admin force improvements 2026-06-29 12:00:29 -04:00
legop3 368ccf363d add brick 2026-06-29 11:31:28 -04:00
legop3 58a10371b0 gignore 2026-06-29 02:20:57 -04:00
legop3 77f2b03741 gah 2026-06-29 02:20:41 -04:00
legop3 f118f6456a better buttonboxing 2026-06-29 02:18:53 -04:00
legop3 8843bd6acf todoing 2026-06-29 01:57:17 -04:00
legop3 c5c3d5b8f5 adjusting colors 2026-06-28 22:58:33 -04:00
legop3 8f2a4dbb8e topdown reworkie 2026-06-28 21:42:18 -04:00
legop3 78fc891cb9 database admin yay 2026-06-28 20:17:58 -04:00
legop3 5fca14bef5 todoing 2026-06-28 19:58:34 -04:00
legop3 622724224a source mapping 2026-06-28 15:45:10 -04:00
legop3 4ee4f7d06e laser locking and idle 2026-06-27 18:06:24 -04:00
legop3 0cfc16528b fixed srt stream url stuff 2026-06-27 16:18:55 -04:00
legop3 8e322ed57b headlight rework and laser addition 2026-06-27 15:57:47 -04:00
legop3 3c1b1dab6e here goes nothin' 2026-06-27 04:23:58 -04:00
legop3 0172ea5150 audio stufs 2026-06-27 02:39:31 -04:00
legop3 d2007976b0 tuning audio publisher for latency, turning gain down 2026-06-26 15:06:23 -04:00
legop3 b50e8bba5f forgot to build... 2026-06-26 13:51:00 -04:00
legop3 274fe279d8 trying stereo audio! 2026-06-26 13:47:23 -04:00
legop3 c2224d00e3 midier slope 2026-06-26 01:19:08 -04:00
legop3 e664402528 midi awawa slopcoding 2026-06-26 00:55:29 -04:00
legop3 5634004ebd move neato and lift into activities 2026-06-23 12:36:21 -04:00
legop3 0a351b9a46 fixing idle skips no longer happening! 2026-06-22 16:05:14 -04:00
legop3 988c387196 video filter alerts 2026-06-21 23:05:50 -04:00
legop3 9c5468ed09 better alertfeed! 2026-06-21 22:39:58 -04:00
legop3 a769974626 fix chat typing stuff and alert feeder 2026-06-21 17:00:12 -04:00
legop3 49bd8acf7b google tts defaults! 2026-06-21 16:44:32 -04:00
legop3 9cf71e8df3 duplicate tab protection 2026-06-21 16:21:25 -04:00
legop3 e95eeeb526 fixe? 2026-06-20 00:51:35 -04:00
legop3 e6c16c3af9 better commanding and bridging slopping 2026-06-20 00:21:55 -04:00
legop3 9cf44b316d show original chat message 2026-06-19 23:26:26 -04:00
legop3 f0a7d72f94 better discord commands, rs commands should work in chat 2026-06-19 23:15:14 -04:00
legop3 bd857e6387 button 2026-06-19 22:09:04 -04:00
legop3 51b53cc2d0 button 2026-06-19 22:01:19 -04:00
legop3 8353d253ce button 2026-06-19 21:58:58 -04:00
legop3 da1d4bf0d5 wiki link button 2026-06-19 21:55:44 -04:00
legop3 2ead99fb70 wiki links for barcodes 2026-06-19 21:46:55 -04:00
legop3 36bf5826b9 better umm.yeah 2026-06-19 20:18:36 -04:00
legop3 eec009c485 scanquest updates 2026-06-19 17:31:22 -04:00
legop3 a5e54bfbf2 removeremove 2026-06-19 17:11:42 -04:00
legop3 36ff14824e remove comparisons, move panel 2026-06-19 17:06:57 -04:00
legop3 3bad454e0d roverdometer 2026-06-19 16:37:47 -04:00
legop3 39bb53104e descripting 2026-06-19 03:37:45 -04:00
legop3 eb633c0659 badbadbad 2026-06-19 03:26:52 -04:00
legop3 302079eadf updating game colors 2026-06-19 03:23:03 -04:00
legop3 2f5f18fa6d style is my name 2026-06-19 03:14:12 -04:00
legop3 56227d6500 restylinh 2026-06-19 03:10:25 -04:00
legop3 db8dc04b8c restyling andalytics 2026-06-19 03:00:42 -04:00
legop3 c7f5cb68f4 MOREE!!!! 2026-06-19 01:22:42 -04:00
legop3 ea8e027be9 more (modular local offline not selling data) analytics! 2026-06-19 01:03:47 -04:00
legop3 c7e811a957 barcode game colors! 2026-06-19 00:03:19 -04:00
legop3 61cb92d72b ovseser fuxzy search 2026-06-18 23:42:13 -04:00
legop3 8a556a3634 overseer disableable 2026-06-18 23:26:16 -04:00
legop3 80f1f82d6e new game! and better points balancing too 2026-06-18 22:46:59 -04:00
legop3 e3d66cc5f6 longer scanquest timers 2026-06-18 22:29:15 -04:00
legop3 c598e25019 barcode gamings 2026-06-18 22:01:10 -04:00
legop3 d3765b7d8f barcode games bot 2026-06-18 21:33:26 -04:00
legop3 df16c84755 update barcode registry 2026-06-18 21:08:55 -04:00
legop3 b7ff447fd4 better barcode games 2026-06-18 20:47:02 -04:00
legop3 3bb31522c3 even more more barcode game stuff 2026-06-18 20:27:41 -04:00
legop3 7e30a856a8 more barcode game stuff 2026-06-18 20:13:07 -04:00
legop3 0af9d560e4 barcode games! 2026-06-18 19:40:09 -04:00
legop3 0c16e5cd11 barcoding 2026-06-18 19:27:37 -04:00
legop3 82eff49d83 promp 2026-06-18 16:47:00 -04:00
legop3 678f968b7c promp 2026-06-18 16:04:39 -04:00
legop3 5d178e745c oops! old analytics crap fix! 2026-06-18 15:07:33 -04:00
legop3 7248af47d2 build with new analytics stuffs 2026-06-18 02:35:58 -04:00
legop3 e7a2abeb9b repo cleanup! 2026-06-18 02:33:06 -04:00
legop3 5d2eda10c7 simplifying analytics 2026-06-18 02:31:21 -04:00
legop3 73928c60d6 Load analytics tags from config during Vite build 2026-06-18 02:29:23 -04:00
legop3 a047e88646 Inject analytics tags from config 2026-06-18 02:29:13 -04:00
legop3 95e9ebff0c Move analytics tags to webui config 2026-06-18 02:28:42 -04:00
legop3 8e0439392f realized i can set it to rover otter land only 2026-06-18 02:20:36 -04:00
legop3 594d081137 rebuildeded 2026-06-18 02:16:05 -04:00
legop3 279b52724d promp 2026-06-18 01:33:45 -04:00
legop3 19049cdd1c promp 2026-06-18 00:28:34 -04:00
legop3 e73cdbe955 promp 2026-06-18 00:06:41 -04:00
legop3 fd890f4c4f promp 2026-06-17 23:41:16 -04:00
legop3 b8e9307858 promp 2026-06-17 22:56:32 -04:00
legop3 9ae9352eca promp 2026-06-17 22:30:58 -04:00
legop3 bb7d2a4e03 Restore overseer context builder with tool results 2026-06-17 21:37:03 -04:00
legop3 d8f617493a Add overseer tool result context 2026-06-17 21:36:24 -04:00
legop3 f130bba8a8 Add overseer tool result context 2026-06-17 21:35:24 -04:00
legop3 57c128ab4d even better analytics config stuffs 2026-06-17 21:14:22 -04:00
legop3 6118033abc debugging 2026-06-17 21:01:49 -04:00
legop3 404e648d3a forgot to buildio! 2026-06-17 21:00:25 -04:00
legop3 7666c3c41b split analytics into separate file for better configurations 2026-06-17 20:59:46 -04:00
legop3 beb43164a4 move otterlytics to / only (inside main app) 2026-06-17 20:36:28 -04:00
legop3 5fa0051bd3 umami recording link script yeah wooo 2026-06-17 19:57:12 -04:00
legop3 ae28bac0ac add otterlytics! 2026-06-17 19:49:36 -04:00
legop3 7799eafb4c promp 2026-06-17 19:19:37 -04:00
legop3 ae72e359bd promp 2026-06-17 19:19:21 -04:00
legop3 3caf3fd362 promp 2026-06-17 19:16:38 -04:00
legop3 e8fa7b39eb promp 2026-06-17 14:47:52 -04:00
legop3 985e46524d promp 2026-06-17 13:07:06 -04:00
legop3 2c2d3d7756 promp 2026-06-17 02:06:11 -04:00
legop3 824e1b1441 promp 2026-06-17 01:33:47 -04:00
legop3 0e2784859e promp 2026-06-16 01:19:23 -04:00
legop3 a77b221c11 promp 2026-06-16 01:18:35 -04:00
legop3 027f7cfc93 promp 2026-06-16 01:07:57 -04:00
legop3 4a6fa9b701 promp 2026-06-16 00:58:57 -04:00
legop3 29455c8913 promp 2026-06-15 23:47:04 -04:00
legop3 344e9cdf47 promp 2026-06-15 23:36:14 -04:00
legop3 65d0b6bbd9 promp 2026-06-15 22:24:04 -04:00
legop3 62cbcddd58 promp 2026-06-15 22:14:37 -04:00
legop3 5a3004add1 promp 2026-06-15 21:36:22 -04:00
legop3 55787f2629 promp replacement 2026-06-15 21:17:56 -04:00
legop3 959541a2e4 promp 2026-06-15 21:15:52 -04:00
legop3 948397a8e1 promp 2026-06-15 20:48:06 -04:00
legop3 883678f942 prromp 2026-06-15 19:06:35 -04:00
legop3 c8e63bcae1 promp 2026-06-15 18:35:35 -04:00
legop3 f11fc5dc60 Merge branch 'main' of https://github.com/legop3/MultiRoombaRover 2026-06-15 17:59:52 -04:00
legop3 84378f915e use default temp for ollama model 2026-06-15 17:59:50 -04:00
legop3 37701e3bba Update Discord ping reward goal to 9000 2026-06-15 16:11:09 -04:00
legop3 630234fab4 Change goal for discordPingEveryone reward to 20000
Updated the goal for the discordPingEveryone reward.
2026-06-15 16:10:16 -04:00
legop3 74810289c2 put prompt back to old one for new model testing 2026-06-15 12:50:56 -04:00
legop3 34692dc974 gayverseer 2026-06-15 03:53:05 -04:00
legop3 b9b3328105 make stalker ping more expensive 2026-06-14 01:23:59 -04:00
legop3 619d334758 add activities tab, make replay popup smaller and muted 2026-06-13 21:53:12 -04:00
legop3 61ace6e7c4 server generated scanner voices! 2026-06-13 17:53:52 -04:00
legop3 bddff1e67f adding barcodesses. piper slop tts 2026-06-13 17:37:22 -04:00
legop3 d28d37575c wasm slop tts 2026-06-13 17:24:34 -04:00
legop3 880ebfefd2 adding more barcode objects 2026-06-13 16:54:29 -04:00
legop3 3fec97a638 adding more barcode objects 2026-06-13 16:48:11 -04:00
legop3 764befa57d giving up on making the notch my problem! 2026-06-12 14:45:40 -04:00
legop3 9775ee06f4 i hate apple i hate apple 2026-06-12 14:44:40 -04:00
legop3 87dc70d11d iphone safe area fixes yess 2026-06-12 14:40:42 -04:00
legop3 033bafeead mobile controls fixes 2026-06-12 14:35:45 -04:00
legop3 3efb31caae slopfixing / issue 007 2026-06-12 14:02:27 -04:00
legop3 01a0b112a1 slopfixing / issue 006 2026-06-12 13:50:28 -04:00
legop3 0a6228cb83 slopfixing / issue 005 2026-06-12 13:28:41 -04:00
legop3 bcd54c28e1 fixeses 2026-06-12 12:40:00 -04:00
legop3 cc06709674 slopfixing / issue 004 2026-06-12 12:33:00 -04:00
legop3 dc193d09db make logs opt-in and remove them from / 2026-06-12 11:53:41 -04:00
legop3 c0050498b2 slopfixing / issue 002 2026-06-12 02:19:08 -04:00
legop3 be8c3d7b29 slopfixing / issue 001 2026-06-12 01:43:39 -04:00
legop3 ac1ffa1cd5 Merge branch 'main' of https://github.com/legop3/MultiRoombaRover 2026-06-12 01:02:25 -04:00
legop3 d4d27cd9d3 more performance sloptesting 2026-06-12 01:01:53 -04:00
legop3 6d813179a4 make ping everyone be 10k for next time! 2026-06-11 15:50:40 -04:00
legop3 3fa66c4537 Merge branch 'main' of https://github.com/legop3/MultiRoombaRover 2026-06-11 15:19:57 -04:00
legop3 6656e6271c docs WIP 2026-06-11 15:19:06 -04:00
legop3 6bec6aee1a performance slopping but actually very informative 2026-06-11 14:07:28 -04:00
legop3 4a213add69 docs WIP 2026-06-10 19:52:34 -04:00
legop3 ce133b2aa1 docs WIP 2026-06-10 19:34:40 -04:00
legop3 cb5b58666c mobile reorganizations 2026-06-10 15:17:28 -04:00
legop3 4fd44d6ad6 more tweaks 2026-06-10 14:44:23 -04:00
legop3 426fd3e7c4 mobile controls fixes hopefully 2026-06-10 14:30:41 -04:00
legop3 bcbd5d03c7 replacing the joystick for mobile! 2026-06-10 13:53:27 -04:00
legop3 8d90a29345 server restart stuck fixes 2026-06-10 13:16:57 -04:00
legop3 5770dc8849 more objects yay 2026-06-10 12:32:53 -04:00
legop3 717a71487c better mobile joystick? 2026-06-10 03:53:16 -04:00
legop3 9826d4245f buncha stuff, charging complete sounds, room display button feedback, etc 2026-06-10 01:46:03 -04:00
legop3 2ce2f23b0a voice setup slopcoding 2026-06-09 15:18:23 -04:00
legop3 2d0e883c97 voice updates and databasing 2026-06-09 15:01:11 -04:00
legop3 de34f1b0a2 female tts 2026-06-09 14:52:47 -04:00
legop3 a702db3fab page flash 2026-06-09 14:45:28 -04:00
legop3 5b6c47b917 scanner page simplifications 2026-06-09 14:42:54 -04:00
legop3 4c9219ddd6 rover label augh lol oh noooo 2026-06-09 14:36:42 -04:00
legop3 0932ea5db7 track barcode registry for easier updating 2026-06-09 14:31:31 -04:00
legop3 5f00d8c715 barcode scanner beginnings 2026-06-09 14:27:48 -04:00
legop3 5dca976f97 larger chat scalee!!! and chat row fixes too :3 2026-06-08 21:18:53 -04:00
legop3 9d36efb847 larger chat, chat icon fixes too finally 2026-06-08 21:07:39 -04:00
legop3 8c121c0925 scaling update 2026-06-08 21:03:59 -04:00
legop3 d2565eaf40 crash fix 2026-06-08 20:59:58 -04:00
legop3 58029fd3a1 forgot to add route rerouter 2026-06-08 20:56:08 -04:00
legop3 7d41e0ae11 /display page woooo 2026-06-08 20:51:51 -04:00
legop3 031e23103f chat bridge command fix lol AND chat box resizing!!! 2026-06-07 22:27:34 -04:00
legop3 45f7efa907 smaller social icons 2026-06-07 20:32:03 -04:00
legop3 48119dffdf change timerses 2026-06-07 18:53:56 -04:00
legop3 d659cbb522 autoclose 10s after video over 2026-06-07 18:51:25 -04:00
legop3 503394f1f0 move replay info line to above 2026-06-07 18:37:11 -04:00
legop3 a994759eb5 replay popups on spectator page 2026-06-07 17:39:31 -04:00
legop3 87473fc424 more layout tweaking 2026-06-07 17:30:43 -04:00
legop3 dde4ac45c7 change layout of replay panel 2026-06-07 17:26:24 -04:00
legop3 5fe7197c0b fix 2026-06-07 17:21:38 -04:00
legop3 e4a818b490 popupaboveinsteadofcovering heheeee 2026-06-07 17:15:36 -04:00
legop3 b2bd3a5e41 better popuping vs not 2026-06-07 17:02:54 -04:00
legop3 3cff250ba0 better bettter replayses 2026-06-07 16:39:50 -04:00
legop3 a15f0697d5 identity system in /mini aswell 2026-06-07 16:19:18 -04:00
legop3 124cdc0c64 spectator nicknames, overseer common issue filtering 2026-06-07 15:21:42 -04:00
legop3 097d23dc66 oops messed up order of 2026-06-07 12:36:29 -04:00
legop3 a7246120cb 2 rows on mobile 2026-06-07 12:31:44 -04:00
legop3 661a4e0f7c 2 rows was bad 2026-06-07 12:17:23 -04:00
legop3 5ac7bd4370 chat panel fixup finally 2026-06-07 11:59:00 -04:00
legop3 8356d37364 yaya 2026-06-07 04:34:27 -04:00
legop3 adedc945f0 MOAR 2026-06-07 04:32:03 -04:00
legop3 0e133e08a6 insane averaging becayse 2026-06-07 04:28:23 -04:00
legop3 7b693e259f increase the averagings 2026-06-07 04:24:26 -04:00
legop3 06f4531bbf averaging and sampling and stuff a 2026-06-07 04:22:16 -04:00
legop3 4efc2be646 mesh um 2026-06-07 04:09:35 -04:00
legop3 c636b41cbc dont make it always get the actual thingy perfect 2026-06-06 17:46:27 -04:00
legop3 b21acc7f19 overseer light controls 2026-06-06 17:23:57 -04:00
legop3 889f660f3e remove neato plsy sound and update user pings 2026-06-06 16:08:45 -04:00
legop3 4bca6273b4 kinect stuff v1 2026-06-06 13:10:18 -04:00
legop3 46c9b8aa43 overser memory viewer! 2026-06-06 03:04:43 -04:00
legop3 353481040b promp 2026-06-06 02:37:21 -04:00
legop3 908776a365 todoing 2026-06-06 01:14:34 -04:00
legop3 229fe5411a better chat pfp imageses 2026-06-05 21:11:28 -04:00
legop3 c097d94ba8 rover self updating! 2026-06-05 15:09:10 -04:00
legop3 870de4f135 change tts limit to 30 seconds 2026-06-05 14:29:24 -04:00
legop3 41b59058bc better RGB light stuff, simpler on the frontend. 2026-06-05 14:02:05 -04:00
legop3 0fe060cd22 swap favicon for the gay one 2026-06-05 13:40:32 -04:00
legop3 094220303f rename google tts to clarify that its local 2026-06-05 03:15:57 -04:00
legop3 bc4088bb2c promp and todoing 2026-06-05 02:48:09 -04:00
legop3 47586591af overseer fixes 2026-06-05 02:08:03 -04:00
legop3 9a8604ea82 remove message length limit 2026-06-05 00:57:59 -04:00
legop3 e9f41e6d62 promp 2026-06-04 13:12:24 -04:00
legop3 ef9c52b2bf promp 2026-06-04 13:01:29 -04:00
legop3 8e9fbb3ecb promp 2026-06-04 12:48:45 -04:00
legop3 d545a4abcf simplicity... 2026-06-03 14:42:43 -04:00
legop3 9f3cd5b8d0 new home assistant light buttons yay 2026-06-03 14:38:09 -04:00
legop3 d6a2c3e0fc completely making it worse loll 2026-06-03 13:23:00 -04:00
legop3 ffa6679cfe no raw making it crunchy?? 2026-06-03 13:18:08 -04:00
legop3 19680b2ae4 whaya 2026-06-03 13:14:01 -04:00
legop3 d87030b46e publisher tweakings 2026-06-03 13:10:27 -04:00
legop3 22382c3f14 video publisher tweakings 2026-06-03 13:06:41 -04:00
legop3 51863c4d11 unslot placeholder data 2026-06-03 12:22:53 -04:00
legop3 dd7e685934 pi stats from roverd to webui slop 2026-06-03 12:14:47 -04:00
legop3 290985bf47 settings UI reworkings 2026-06-02 13:35:46 -04:00
legop3 d1244d587f video color filters 2026-06-02 12:57:55 -04:00
legop3 98156676ee rover labels and theme tweakings 2026-06-02 02:44:47 -04:00
legop3 e1f8000cce cardframe coloring adjustments and such 2026-06-02 00:19:39 -04:00
legop3 33cba41abb oops. didnt build hehe 2026-06-01 23:22:31 -04:00
legop3 56607f41ec LOTS OF STYLING STUFF FINALLY THAT I HAD TO DO FINALYL AAUUFH 2026-06-01 23:21:44 -04:00
legop3 2e83a142ec crashing lol 2026-06-01 21:53:59 -04:00
legop3 b96ae5f553 user alerts channel redo 2026-06-01 21:50:19 -04:00
legop3 5e3a6a2851 time expiration private requests 2026-06-01 20:52:58 -04:00
legop3 af5ddfb11d private rover request fixes finally 2026-06-01 19:53:17 -04:00
legop3 d205d3a05d pride gay theme yay 2026-06-01 16:50:44 -04:00
legop3 12cd90a3b0 private rover replays for those who are allowed the luxury of using them when closed 2026-06-01 14:04:34 -04:00
legop3 292325d85f no more nickname form 2026-06-01 13:15:51 -04:00
legop3 ac93d3227b chrome tts 2026-06-01 00:56:01 -04:00
legop3 e108eaa4eb clean up connection pillio 2026-05-29 18:34:44 -04:00
legop3 9daab1203f exempt spectators oops... 2026-05-28 22:35:09 -04:00
legop3 a3d709236c dead socket kicking and connection info pill 2026-05-28 22:05:18 -04:00
legop3 94ef97f5bb switch to global cooldown, tracking per user is useless i realized lol 2026-05-28 17:34:10 -04:00
legop3 955b613527 reboot your own rover 2026-05-28 16:12:42 -04:00
legop3 6dfc30d1f6 overseer configurables 2026-05-28 03:03:10 -04:00
legop3 c84724c1b6 water my otter 2026-05-28 01:59:17 -04:00
legop3 c1a2a3465f Merge pull request #10 from legop3/cardrewoek
Cardrewoek
2026-05-27 22:48:42 -04:00
legop3 48b87b1127 change chat card title 2026-05-27 22:44:56 -04:00
legop3 796613876b some more social button changes 2026-05-27 00:28:17 -04:00
legop3 742232b882 overseer improvements and new social buttones 2026-05-26 23:58:23 -04:00
legop3 f56d865c8e move nickname entry to inside chat panel 2026-05-21 22:33:47 -04:00
legop3 7e7b07083a rover based colors! 2026-05-21 20:49:01 -04:00
legop3 d72bc0db03 removed title counters finally!!! 2026-05-21 03:06:33 -04:00
legop3 e9989da71a colors!!! 2026-05-21 03:02:29 -04:00
legop3 331302da01 pre cardcolor 2026-05-21 02:42:44 -04:00
legop3 bfd1a94030 beginning of my ideases 2026-05-21 01:57:59 -04:00
legop3 8727555788 overseer off when no one online 2026-05-21 00:56:54 -04:00
legop3 b62af19418 slight word change 2026-05-21 00:04:56 -04:00
legop3 4af93fe05a overseer vote panel improvements 2026-05-21 00:02:16 -04:00
legop3 f9f2e1e2ed FIXATYPOLOL 2026-05-20 23:29:10 -04:00
legop3 78c7e44d63 promp 2026-05-20 01:32:22 -04:00
legop3 ddb03bc809 no tools only messages !!! 2026-05-20 01:31:35 -04:00
legop3 2da58920c4 promp and clear command 2026-05-19 23:19:49 -04:00
legop3 1e5f282a6a promp 2026-05-19 22:32:37 -04:00
legop3 064aaee76c fullscreen prompt cookie saving, and more... 2026-05-19 21:52:03 -04:00
legop3 3a2c1bee35 spectator fix 2026-05-19 21:31:32 -04:00
legop3 e11361b5d5 awawawa 2026-05-19 21:29:51 -04:00
legop3 03929822e6 awawa 2026-05-19 21:27:53 -04:00
legop3 3ce9ea2cdc awaa 2026-05-19 21:22:07 -04:00
legop3 daa8942a29 overseer panel tweaking 2026-05-19 21:12:57 -04:00
legop3 8c2162d918 overseer voting stuff yay! 2026-05-19 21:08:06 -04:00
legop3 ab32b0416c always show tools on spectator. rename neato locate to sound 2026-05-19 20:51:10 -04:00
legop3 3ce2bdc541 fix messed up typing row now that message row changed 2026-05-19 19:22:20 -04:00
legop3 bac957dfd6 promp 2026-05-19 19:18:24 -04:00
legop3 2ad6f5e6d4 promp 2026-05-19 18:48:33 -04:00
legop3 18c70b01c5 okay maybe 2026-05-19 18:45:49 -04:00
legop3 51d8c72ae8 ugh. 2026-05-19 18:41:55 -04:00
legop3 084dee4df9 raaahhhhhflkhdj 2026-05-19 18:28:48 -04:00
legop3 fb65926760 raaaaahhh 2026-05-19 18:26:44 -04:00
legop3 6c3ac6d22d chat line fix lole 2026-05-19 18:23:30 -04:00
legop3 4d179b4372 okay render tools above messages.. yay 2026-05-19 17:59:45 -04:00
legop3 b8d00e063f gagrarh 2026-05-19 17:57:14 -04:00
legop3 a109c49a5b tools dropdown in message row header 2026-05-19 17:52:51 -04:00
legop3 faac11e3fc promp 2026-05-19 17:49:03 -04:00
legop3 133ea3f6d2 args and no more emojis 2026-05-19 17:29:08 -04:00
legop3 f9e472c832 wording and overseer tool embeds 2026-05-19 16:51:02 -04:00
legop3 e432e3de79 replay panel tweaking.. 2026-05-19 16:13:10 -04:00
legop3 8a7be00bf6 acrashing 2026-05-18 00:03:26 -04:00
legop3 372c5dee70 scrole 2026-05-18 00:00:56 -04:00
legop3 135ee78a5d scroll fixing 2026-05-17 23:57:15 -04:00
legop3 84e5cbc787 horn scroll and lower limits!! 2026-05-17 23:51:43 -04:00
legop3 f9c4249086 Merge pull request #9 from legop3/manualdock
Manualdock
2026-05-17 23:35:50 -04:00
legop3 fa4f1f6889 boutta merge 2026-05-17 23:32:25 -04:00
legop3 c42a16362c remove directions from action button 2026-05-17 22:25:12 -04:00
legop3 bf7d40be65 new color for dock assist mode 2026-05-17 21:57:38 -04:00
legop3 3428fbd1cf restore background color changing 2026-05-17 21:51:56 -04:00
legop3 290e0efeca duh, tailwind.. 2026-05-17 21:49:52 -04:00
legop3 24f428622a forgort build 2026-05-17 21:48:45 -04:00
legop3 5593c1f129 background pls?? 2026-05-17 21:48:17 -04:00
legop3 e05f1fb4a7 backgrounding 2026-05-17 21:47:03 -04:00
legop3 03bf356de2 styling changes and disable beep boop 2026-05-17 21:45:13 -04:00
legop3 f6f985d740 fullscreen popup 2026-05-17 21:42:39 -04:00
legop3 27f31531ac refactorio 2026-05-17 21:35:33 -04:00
legop3 31f98a8e32 camera controls better takeover 2026-05-17 21:26:09 -04:00
legop3 287d6291dd realized that wont work lol cause roomba 2026-05-17 19:12:00 -04:00
legop3 3b02a97643 tune when successfully docked 2026-05-17 19:07:38 -04:00
legop3 7dc5ca1835 promp 2026-05-17 18:07:33 -04:00
legop3 553990b375 swap prompt back 2026-05-17 17:22:58 -04:00
legop3 ceb96a74bf autocharge 1 second 2026-05-17 14:49:17 -04:00
legop3 f7178eb4b6 relabel 2026-05-17 14:42:19 -04:00
legop3 13723fe8c6 wording 2026-05-17 14:25:42 -04:00
legop3 394c45000d adjusting wording 2026-05-17 14:19:09 -04:00
legop3 17f516ed0b reduce autocharge timer and change wording 2026-05-17 14:11:09 -04:00
legop3 ba5865c397 hud adjustments 2026-05-17 13:55:11 -04:00
legop3 d67c4dd815 tweaking + hud 2026-05-17 13:47:48 -04:00
legop3 74e5187bad manual docking mode real life testing 2026-05-17 13:32:21 -04:00
legop3 d326ab755a plannings 2026-05-17 03:15:55 -04:00
legop3 0b56de0c81 promp 2026-05-15 01:03:10 -04:00
legop3 384f97cdcf @everyone button box reward 2026-05-15 00:53:40 -04:00
legop3 95c2b89875 promp 2026-05-15 00:29:17 -04:00
legop3 71494da1a8 fix [BOT] [SPECTATOR] 2026-05-14 22:30:53 -04:00
legop3 7e3bc8a4f4 promp 2026-05-14 22:27:37 -04:00
legop3 5123e41ff5 50/50 splitted 2026-05-14 22:10:09 -04:00
legop3 fe91d825d6 removing neato lidar in hopes of future actual robot map access/... 2026-05-14 22:05:03 -04:00
legop3 e1a48a985a promp and replay tweaks 2026-05-14 21:41:37 -04:00
legop3 788d34801a move image to always be after name in chat row 2026-05-14 21:21:37 -04:00
legop3 6a01378fd8 promp 2026-05-14 20:58:02 -04:00
legop3 df2adcc89a promp 2026-05-14 20:25:15 -04:00
legop3 c445671016 chat row fixings ups 2026-05-14 20:16:26 -04:00
legop3 61d88c0871 vippfp 2026-05-14 20:03:08 -04:00
legop3 c487eb134d promp 2026-05-14 19:40:44 -04:00
legop3 0f27370650 promp 2026-05-14 19:21:35 -04:00
legop3 19b9f70e91 promp 2026-05-14 18:57:14 -04:00
legop3 83bd63a1cc promp 2026-05-14 18:19:52 -04:00
legop3 766e36ca6a promp 2026-05-14 17:59:34 -04:00
legop3 3c8f706046 actually allow bots through sockets oopsie ;3 2026-05-14 17:43:16 -04:00
legop3 8f96a93545 remove shy 2026-05-14 17:27:04 -04:00
legop3 bc8a13dd65 bot tag and profile images yay urls only though... 2026-05-14 16:54:13 -04:00
legop3 fa506b0970 no kitty word 2026-05-14 16:01:04 -04:00
legop3 f92cedccb9 promp 2026-05-14 15:54:25 -04:00
legop3 75cece6a59 Merge pull request #8 from legop3/hudrework
Hudrework
2026-05-14 15:45:34 -04:00
legop3 1d8972aad8 Merge branch 'main' into hudrework 2026-05-14 15:44:57 -04:00
legop3 c7aab0c21b todoing 2026-05-14 03:07:25 -04:00
legop3 f1edd0b1a4 todoing 2026-05-14 00:38:39 -04:00
legop3 579fab8334 todoing 2026-05-13 23:11:24 -04:00
legop3 52d00d18e8 snapshot fix 2026-05-13 22:47:47 -04:00
legop3 805b41905f descriptgone 2026-05-13 20:49:36 -04:00
legop3 8a05787a1d connecting to video.... 2026-05-13 20:45:25 -04:00
legop3 9436cc79f1 rarare 2026-05-13 20:39:13 -04:00
legop3 54a035417a labelchangel 2026-05-13 20:15:50 -04:00
legop3 187871edc5 bring the boys back home 2026-05-13 20:14:22 -04:00
legop3 3c9400f488 grover gaming 2026-05-13 20:10:47 -04:00
legop3 36a6ee012c awa 2026-05-13 19:55:55 -04:00
legop3 b38a81d002 fix 2026-05-13 12:35:44 -04:00
legop3 26185435b6 add word 2026-05-13 12:33:24 -04:00
legop3 e3aa907486 description popup goes away when you move now 2026-05-13 12:30:29 -04:00
legop3 794dbc9a96 tweaking of wording 2026-05-13 02:48:28 -04:00
legop3 387534e6bb longer readtime. also chat fix. also descriptions in roster 2026-05-13 02:40:01 -04:00
legop3 1b296d5010 rover descriptions 2026-05-13 02:06:47 -04:00
legop3 fb996469d8 Merge pull request #6 from thecreare/fix-typo
fix the egrereregious typo!1!!11!!111111!!!!!2
2026-05-13 00:55:06 -04:00
legop3 8ef9f7a1dc Merge pull request #7 from thecreare/fix-chat-word-breaks
Fix Fix long long words words overflowing overflowing chat chat
2026-05-13 00:54:16 -04:00
thecreare 1d19d3474f Fix long words overflowing chat 2026-05-12 23:31:20 -04:00
legop3 29e74d37b1 adjustmetns 2026-05-12 16:53:53 -04:00
legop3 48d8d573c3 gahw 2026-05-12 15:36:58 -04:00
legop3 95c715c9eb corner..., 2026-05-12 15:26:36 -04:00
legop3 a1a75a1cda ploo spacing and bigger title 2026-05-12 15:12:26 -04:00
legop3 326208ed1d tweakings 2026-05-12 15:07:37 -04:00
legop3 13c5ce3157 tweakings 2026-05-12 15:03:56 -04:00
legop3 c4f470b329 turns stuff testings 2026-05-12 14:45:03 -04:00
legop3 53ca7aca0a promp 2026-05-12 14:08:48 -04:00
legop3 dfe85723e1 promp 2026-05-12 13:18:09 -04:00
legop3 1779f47148 update default tilt speed to something more reasonable.... 2026-05-12 00:41:34 -04:00
legop3 03608f9b23 promp 2026-05-11 21:45:05 -04:00
legop3 b35a403d70 vptweak 2026-05-11 20:44:34 -04:00
legop3 58ff1e68b4 promp 2026-05-11 20:23:07 -04:00
legop3 2fb01324ed promp 2026-05-11 17:47:21 -04:00
legop3 51b153aa2b promp 2026-05-11 17:29:05 -04:00
legop3 e1eac7544a promp 2026-05-11 15:02:12 -04:00
legop3 603762a8aa neato error fix for llm 2026-05-11 14:24:48 -04:00
legop3 1f5426f110 overseer personality swap 2026-05-11 13:09:36 -04:00
legop3 3de42412dd title chaneeee 2026-05-10 19:43:28 -04:00
legop3 7148e41828 ui changes tweaks and flings. new quickstart overlay to replace help overlay. 2026-05-10 19:41:24 -04:00
legop3 e58e5503e8 lockdown audit done 2026-05-10 01:55:32 -04:00
legop3 a497c8a630 plannings 2026-05-10 01:47:40 -04:00
legop3 0141af2ed6 plannings 2026-05-10 01:34:36 -04:00
legop3 05cfe42860 scrollbaring 2026-05-10 01:22:29 -04:00
legop3 4971edbb59 teeny scrollbars 2026-05-10 01:19:25 -04:00
legop3 b2cae9bfa4 working on mobile scrollbar garbaoeage 2026-05-10 01:16:14 -04:00
legop3 def38ad5a9 brain dumping 2026-05-09 02:09:18 -04:00
legop3 e8c4ffc543 pudate webui package.json thanks crer 2026-05-08 13:26:23 -04:00
thecreare babecb0847 fix the typo 2026-05-07 18:56:33 -04:00
legop3 33f1e66b62 neato lidar antispam 2026-05-05 22:45:08 -04:00
legop3 13885fff4c promp 2026-05-04 23:17:56 -04:00
legop3 21215260ce promp 2026-05-04 23:17:39 -04:00
legop3 a99102dc2a glados lol 2026-05-04 23:06:45 -04:00
legop3 257e4d037d give neato errors and status 2026-05-04 22:59:40 -04:00
legop3 85e42851f3 promp and temp 2026-05-04 22:48:18 -04:00
legop3 49987c445f promp 2026-05-04 13:21:23 -04:00
legop3 f4eab8776f remove tools from chat meta 2026-05-04 12:36:03 -04:00
legop3 eb73ff225c promp 2026-05-04 12:17:40 -04:00
legop3 42770fbfe6 promp 2026-05-04 03:54:16 -04:00
legop3 3a9015753c promp 2026-05-04 03:33:01 -04:00
legop3 0ea96613b0 more better panel stuff and promp 2026-05-04 02:50:32 -04:00
legop3 a66a68d7eb prompsmall 2026-05-04 02:38:43 -04:00
legop3 c8685e9e0e combine metadata/promp 2026-05-04 02:32:02 -04:00
legop3 8e49a106fb promp 2026-05-04 02:22:24 -04:00
legop3 1175ea7deb promp 2026-05-04 02:20:54 -04:00
legop3 37ab5ed278 promp 2026-05-04 02:13:45 -04:00
legop3 8a059a4642 promp 2026-05-04 02:07:36 -04:00
legop3 41aff63ae6 promp 2026-05-04 02:00:13 -04:00
legop3 52addabeed promp 2026-05-04 01:50:35 -04:00
legop3 a979e8c905 promp 2026-05-04 01:39:09 -04:00
legop3 26d59b6650 promp and memry 2026-05-04 01:33:09 -04:00
legop3 d887997a5a promp 2026-05-04 01:25:58 -04:00
legop3 cf7bb5c337 promp 2026-05-04 01:03:18 -04:00
legop3 9ebf86cdfa promp 2026-05-04 00:53:57 -04:00
legop3 9504306af6 promp 2026-05-04 00:37:56 -04:00
legop3 3487fd200c promp 2026-05-04 00:29:36 -04:00
legop3 15fedc7c77 overseer v2 6 (tool actions, text chat) 2026-05-04 00:14:32 -04:00
legop3 5e2c836cf2 overseer v2 5 (real tools) 2026-05-03 23:57:36 -04:00
legop3 7eb33c9af2 overseer v2 4 2026-05-03 23:41:08 -04:00
legop3 5b48b10971 overseer v2 3 2026-05-03 23:20:09 -04:00
legop3 e97b623b43 fix old service that apparently never stops 2026-05-03 23:04:27 -04:00
legop3 906368d45e overseer v2 2 2026-05-03 23:01:01 -04:00
legop3 850f89c8ed overseer v2 1 2026-05-03 22:51:50 -04:00
legop3 32e4afdbe8 Merge pull request #5 from legop3/webuiopt
Webuiopt
2026-05-03 22:07:43 -04:00
legop3 4003379dfb update title 2026-05-03 22:03:33 -04:00
legop3 837701eee8 deeper pass 2026-05-02 17:12:38 -04:00
legop3 2f570d92d7 hud stuff 2026-05-02 17:00:06 -04:00
legop3 ba4b6cfc78 pass 1 (broken) 2026-05-02 15:39:38 -04:00
legop3 53cf39e7fb webui optimization 2026-05-02 15:25:01 -04:00
legop3 e8f6fb9654 pverseer rambling at 430am 2026-05-02 04:35:20 -04:00
legop3 0f97382f42 prompand 2026-05-01 23:40:47 -04:00
legop3 03cd8c00dd lidar adjustmints 2026-05-01 23:19:22 -04:00
legop3 df7b933f41 joecraft 2026-05-01 19:52:53 -04:00
legop3 9c287f068a requet fix and ui change 2026-05-01 19:48:04 -04:00
legop3 6280724f6e nvl 2026-05-01 19:38:53 -04:00
legop3 4890b4e5c6 kjf 2026-05-01 19:37:38 -04:00
legop3 6de19c69df gah 2026-05-01 19:35:18 -04:00
legop3 76fc1c02d5 elidarea 2026-05-01 19:32:15 -04:00
legop3 b76405ed46 gh 2026-05-01 18:56:11 -04:00
legop3 2833175fca gah 2026-05-01 18:47:23 -04:00
legop3 fa981ef298 be webel 2026-05-01 18:41:10 -04:00
legop3 7180d3352d guh 2026-05-01 18:37:18 -04:00
legop3 458199ad11 lidar reasons 2026-05-01 18:26:16 -04:00
legop3 3bb16c140e too ocmplicatd 2026-05-01 18:17:56 -04:00
legop3 8550c85106 gah 2026-05-01 18:12:22 -04:00
legop3 244ff572c4 bolobob 2026-05-01 18:05:42 -04:00
legop3 aa530ef67d neatofix 2026-05-01 17:56:27 -04:00
legop3 e92d670f80 brainslug logfile 2026-05-01 17:45:41 -04:00
legop3 c37a7965ff scan after scan after scan 2026-05-01 17:34:48 -04:00
legop3 c01456409d uifix 2026-05-01 17:32:56 -04:00
legop3 b8bcd49473 stupid machine... 2026-05-01 17:29:49 -04:00
legop3 a8dd71193a lidar adjustmentses 2026-05-01 17:25:45 -04:00
legop3 53723e2f1e lidar fixar 2026-05-01 17:17:15 -04:00
legop3 e3e563feee neato lidara 2026-05-01 17:10:00 -04:00
legop3 6523ea7417 betterneato 2026-05-01 02:35:58 -04:00
legop3 02d5d6ca13 trying to 2026-05-01 01:50:35 -04:00
legop3 7e7eb97ff6 whue 2026-05-01 01:32:02 -04:00
legop3 b491d91ec5 community goal? never jnew her 2026-05-01 01:18:21 -04:00
legop3 eefe434607 disable chat_recent 2026-05-01 00:03:21 -04:00
legop3 b2a2296d98 fixes, changes, no more periodic sync. since theres enough activity at this point... 2026-04-30 13:18:19 -04:00
legop3 962d2ae61e oops forgot build 2026-04-30 04:54:55 -04:00
legop3 ff877f04cc making noise 2026-04-30 04:53:42 -04:00
legop3 963499fa8e risk of fling 2 2026-04-30 02:53:22 -04:00
legop3 0ae60ef4f9 discord unregressions 2026-04-30 01:53:11 -04:00
legop3 a3067a06d9 block lift ui during movement 2026-04-30 00:57:20 -04:00
legop3 09c7a0c813 idle and docked make button show 2026-04-29 23:48:17 -04:00
legop3 d7057289ab better improve 2026-04-29 22:47:55 -04:00
legop3 5654702e51 uhufgj 2026-04-29 19:59:59 -04:00
legop3 90e3c9873b gahhf 2026-04-29 19:53:05 -04:00
legop3 da56086b21 jfhdskljh 2026-04-29 19:48:22 -04:00
legop3 40e625793e loggings 2026-04-29 19:44:52 -04:00
legop3 97196ef202 loging 2026-04-29 19:38:33 -04:00
legop3 c31fffec20 glogar 2026-04-29 19:31:51 -04:00
legop3 9e24ed2ddf thelift 2026-04-29 19:25:31 -04:00
legop3 91a3272780 ohyeah 2026-04-29 19:22:21 -04:00
legop3 943fa11f7e the lift.... 2026-04-29 19:13:12 -04:00
legop3 9bf449ca19 awaa 2026-04-29 18:45:41 -04:00
legop3 01a8d044e7 regressing rn 2026-04-29 18:31:02 -04:00
legop3 fed0cab0b8 glorp 2026-04-29 15:54:55 -04:00
legop3 ef5c6fab89 i hate weather 2026-04-29 15:15:15 -04:00
legop3 fcfcdc907a var liberal 2026-04-29 14:55:42 -04:00
legop3 810c465040 stupid 2026-04-29 14:50:18 -04:00
legop3 9be91c22bd snaofhdskljfs 2026-04-29 14:47:17 -04:00
legop3 88acba5e1a ieuyryehj 2026-04-29 14:38:13 -04:00
legop3 d8a01f9b60 khdslfjyufe 2026-04-29 14:30:24 -04:00
legop3 f26735b9e5 flhj 2026-04-29 14:25:28 -04:00
legop3 2e140d1bd5 etkjrh 2026-04-29 14:22:44 -04:00
legop3 b716e40f53 logging 2026-04-29 14:16:38 -04:00
legop3 9b80b0d248 rover sapshot 2026-04-29 14:09:19 -04:00
legop3 5ebf5fa580 frame 2026-04-29 14:03:56 -04:00
legop3 0003338ecd room camera and rover snapshots 2026-04-29 13:58:04 -04:00
legop3 40e55240e4 chatservice 2026-04-29 13:45:19 -04:00
legop3 d7f34af66f replay beroke 2026-04-29 13:35:24 -04:00
legop3 9b9fa13eaf ugh 2026-04-29 13:23:23 -04:00
legop3 bc6770f407 replay 2026-04-29 13:21:44 -04:00
legop3 e2bd29e2c2 splite 2026-04-29 13:12:04 -04:00
legop3 2eafcce95b rah 2026-04-29 13:07:42 -04:00
legop3 c20c22eafc discord bot 2026-04-29 13:01:21 -04:00
legop3 ea3c030a21 homessis 2026-04-28 21:46:00 -04:00
legop3 a4400655c8 praccess 2026-04-28 21:40:05 -04:00
legop3 5036bbfa07 policy 2026-04-28 21:35:31 -04:00
legop3 85db918085 buttonbox 2026-04-28 21:12:16 -04:00
legop3 dba714cc42 af 2026-04-28 21:08:04 -04:00
legop3 ccd502c0f9 audio forward 2026-04-28 21:03:08 -04:00
legop3 d47face038 llm 2026-04-28 20:55:31 -04:00
legop3 b0f62e1cad commentary 2026-04-28 20:49:07 -04:00
legop3 053dfc6396 fixe 2026-04-28 20:39:45 -04:00
legop3 4a3ace007e rmanager 2026-04-28 20:32:15 -04:00
legop3 019147a6ce stupid 2026-04-28 20:12:39 -04:00
legop3 fc229fcf62 data fix 2026-04-28 20:05:56 -04:00
legop3 50f6569b22 mover ranager 2026-04-28 20:03:00 -04:00
legop3 8deee70d4a fixe 2026-04-28 19:58:12 -04:00
legop3 49f801330f gahew 2026-04-28 19:53:10 -04:00
legop3 f13b85109e comments and descriptions 2026-04-28 19:39:13 -04:00
legop3 07ad9ab253 goop converter 2026-04-28 19:31:02 -04:00
legop3 86eb7e628e mappings 2026-04-28 19:17:23 -04:00
legop3 c31408611e mobeil controls 2026-04-28 14:55:12 -04:00
legop3 bff79b5f8c gamepag mapping 2026-04-28 14:49:22 -04:00
legop3 3d809fd91c drivedock 2026-04-28 14:43:34 -04:00
legop3 7b0f7cbc56 admin panel 2026-04-28 14:38:10 -04:00
legop3 7e13d44458 vip audio card 2026-04-28 14:33:39 -04:00
legop3 4dc930f64b videotile 2026-04-28 14:24:58 -04:00
legop3 3a2b8b3557 videotile 2026-04-28 14:22:13 -04:00
legop3 1d6bc24067 spectatro 2026-04-28 14:17:36 -04:00
legop3 13e9ebc909 it begins 2026-04-28 14:02:14 -04:00
legop3 9b59718a37 refactor planning 2026-04-28 13:48:26 -04:00
legop3 1607968cee oips 2026-04-28 13:19:03 -04:00
legop3 b0cd29c075 optional sidebar 2026-04-28 13:13:42 -04:00
legop3 928c1112a1 woah 2026-04-28 04:52:05 -04:00
legop3 83d9cd51de fix wrap 2026-04-28 04:48:20 -04:00
legop3 aaeb173cea titlet 2026-04-28 04:45:07 -04:00
legop3 850b271154 7 2026-04-28 04:42:05 -04:00
legop3 b054b93c96 chatboxe 2026-04-28 04:36:22 -04:00
legop3 630e112733 prevchat 2026-04-28 04:31:21 -04:00
legop3 2439d5df19 gwah 2026-04-28 04:22:44 -04:00
legop3 4bd3094351 chart 2026-04-28 04:15:49 -04:00
legop3 0eb43738cb sidebar 2026-04-28 04:10:39 -04:00
legop3 a0ef334c8c ereplay 2026-04-28 03:56:18 -04:00
legop3 bef54d0f2e ereplay 2026-04-28 03:34:08 -04:00
legop3 c4a80202fc awa 2026-04-28 02:48:49 -04:00
legop3 16ae430090 ugh 2026-04-28 02:43:41 -04:00
legop3 ae24793057 slkfj 2026-04-28 02:38:01 -04:00
legop3 32faa5866d fkjhds 2026-04-28 02:31:52 -04:00
legop3 0e91f38f1e locationong 2026-04-28 02:22:48 -04:00
legop3 1ea5929f79 repalay 2026-04-28 02:17:25 -04:00
legop3 76e966abec wawaw 2026-04-28 02:08:32 -04:00
legop3 07b1e8bd86 gomer 2026-04-28 02:00:14 -04:00
legop3 bdbf3c0138 reapaltle 2026-04-28 01:52:25 -04:00
legop3 286012809f overseer changes 2026-04-27 20:39:53 -04:00
legop3 f743f767a9 awa 2026-04-26 23:45:08 -04:00
legop3 92fde05847 vip tab restyling 2026-04-26 23:23:52 -04:00
legop3 b92034ecab volkswagen 2026-04-26 23:01:36 -04:00
legop3 aa1a115203 neao 2026-04-26 22:57:05 -04:00
legop3 a5705ede2b button arrangement 2026-04-26 22:53:05 -04:00
legop3 ae6d2df0a2 new fefault tilt speed 2026-04-26 21:22:05 -04:00
legop3 7847f5e79c remove status line stuff 2026-04-26 21:17:59 -04:00
legop3 f565d886a8 oopsie 2026-04-26 21:11:25 -04:00
legop3 304c984423 neatp 2026-04-26 21:09:11 -04:00
legop3 c28cb4bb20 guhw 2026-04-26 21:02:30 -04:00
legop3 374d1448ac month 2026-04-26 20:58:05 -04:00
legop3 de3e32e0f0 neat 2026-04-26 20:43:46 -04:00
legop3 47dc4e06af neatoing 2026-04-26 20:40:02 -04:00
legop3 286cc1c62a neeato 2026-04-26 20:32:29 -04:00
legop3 e63eddc314 overcurrent fix? 2026-04-26 13:11:13 -04:00
legop3 ed22c5c393 swap promp 2026-04-26 12:51:14 -04:00
legop3 863c7daafb guh 2026-04-25 20:55:33 -04:00
legop3 3a5c14412d hydraulic brakes 2026-04-25 20:45:54 -04:00
legop3 41bbd7bd98 night vision fix 2026-04-25 20:35:53 -04:00
legop3 1359c701bb smol changess 2026-04-25 20:21:38 -04:00
legop3 e54b8e34ab deter 2026-04-24 21:44:26 -04:00
legop3 f23a0e15c3 npm run build 2026-04-24 20:37:40 -04:00
legop3 0c93aacdf6 button reward updates 2026-04-24 13:19:56 -04:00
legop3 d3f263c216 promp 2026-04-22 14:22:37 -04:00
legop3 9b732429a4 promp 2026-04-22 14:21:18 -04:00
legop3 ff3c5321df promp 2026-04-19 23:04:52 -04:00
legop3 8fd435b3b4 home assistant white light buttons 2026-04-19 21:34:55 -04:00
legop3 73524990af promp 2026-04-19 18:51:59 -04:00
legop3 b46108a750 promp 2026-04-19 16:36:18 -04:00
legop3 adb13fc69c event tweaking 2026-04-19 16:16:20 -04:00
legop3 aee4f61976 promp 2026-04-19 11:50:00 -04:00
legop3 5438873846 event popup!! 2026-04-19 02:56:13 -04:00
legop3 52352bd692 update some reward numbrs 2026-04-19 01:56:13 -04:00
legop3 e974f4f90f update darkness reward 2026-04-19 01:34:04 -04:00
legop3 1bdae74ee5 prmop 2026-04-19 00:43:17 -04:00
legop3 3baf928402 promp 2026-04-19 00:38:40 -04:00
legop3 ee08408ccd promp 2026-04-18 23:54:01 -04:00
legop3 3534c8ef14 fix 2026-04-18 23:30:11 -04:00
legop3 f4182073ee waui 2026-04-18 22:55:16 -04:00
legop3 cd70258a21 goals were toooo smalls 2026-04-18 19:42:12 -04:00
legop3 a4e4921931 no duplicate reward slots 2026-04-18 19:28:40 -04:00
legop3 b76c5010b4 upped the spams 2026-04-18 19:23:27 -04:00
legop3 e5e8e9ed0f ui tweaks 2026-04-18 19:16:46 -04:00
legop3 3513a11068 buttonboxrewardses 2026-04-18 19:11:26 -04:00
legop3 fe8d3df421 prebubtotn 2026-04-18 18:22:42 -04:00
legop3 388efd1f47 promp and buttonbox beginnings 2026-04-18 02:31:30 -04:00
legop3 71e2e18c13 new new new 2026-04-09 19:30:42 -04:00
legop3 6e764f70bc add mode buttons 2026-04-09 18:14:25 -04:00
legop3 214eb47d96 simplifications 2026-04-09 17:47:06 -04:00
legop3 49a50a97ed loggings 2026-04-09 16:57:42 -04:00
legop3 33900e79f6 switch to button 2026-04-09 16:43:46 -04:00
legop3 7aa648240c guh 2026-04-09 16:36:07 -04:00
legop3 353efeeffd add button support 2026-04-09 16:25:48 -04:00
legop3 8ed617305d switch human alert to button 2026-04-09 14:02:54 -04:00
legop3 5e428b278a gordon 2026-04-06 01:17:25 -04:00
legop3 985e63bab2 guhb 2026-04-06 01:01:49 -04:00
legop3 9a83433f08 installer fix 2026-04-06 00:16:30 -04:00
legop3 1e9a6b2e19 opencv 2026-04-06 00:12:00 -04:00
legop3 f72a1628aa gruh 2026-03-27 16:16:56 -04:00
legop3 85fcaedc6a roerd 2026-03-27 16:15:08 -04:00
legop3 c220eb4022 roverquesting 2026-03-27 16:08:04 -04:00
legop3 998933f4c2 ui block fix 2026-03-25 21:56:23 -04:00
legop3 904f28eeef option to disable bumpoff 2026-03-25 21:43:28 -04:00
legop3 d74879caa9 plam 2026-03-25 21:40:24 -04:00
legop3 b0a11541bd gahw 2026-03-25 20:46:51 -04:00
legop3 24d0242364 video invert optional 2026-03-25 16:23:28 -04:00
legop3 af7782a233 Merge pull request #4 from legop3/privaterover
Merge private rover stuff into main
2026-03-25 15:12:18 -04:00
legop3 b32cc7ef8f promp 2026-03-25 12:39:24 -04:00
legop3 766f7130e0 promp 2026-03-25 12:29:37 -04:00
legop3 3127e02f8c promp 2026-03-25 12:27:19 -04:00
legop3 895960867c promp 2026-03-25 01:08:58 -04:00
legop3 3af21846c8 the 2026-03-24 02:48:16 -04:00
legop3 e0167939d7 plam 2026-03-23 23:27:31 -04:00
legop3 ebb400adc4 warae 2026-03-23 03:55:13 -04:00
legop3 463bd89dd8 oips 2026-03-23 03:53:59 -04:00
legop3 e311e67fc9 rewreaer 2026-03-23 03:50:23 -04:00
legop3 20227cd0f8 colorcoding 2026-03-23 03:39:36 -04:00
legop3 ad1b6b48f8 always there fullscreen button 2026-03-23 02:31:59 -04:00
legop3 626b48f777 mobile swappy controls 2026-03-23 02:04:21 -04:00
legop3 f112f74db0 todoing 2026-03-21 01:56:30 -04:00
legop3 cc1e887705 wawea 2026-03-21 01:10:15 -04:00
legop3 81b0638d6f clipfix 2026-03-21 00:56:20 -04:00
legop3 aeb4b29b88 weaw 2026-03-21 00:52:20 -04:00
legop3 54f8684169 yippee!!! 2026-03-21 00:11:27 -04:00
legop3 36a6c07f95 wooo!!!! 2026-03-21 00:06:22 -04:00
legop3 d6da071fde yayy!!! 2026-03-20 23:56:47 -04:00
legop3 9b1251c16d yay! 2026-03-20 23:41:50 -04:00
legop3 e3b428f9ab whipwhep 2026-03-20 23:35:34 -04:00
legop3 cb97e73987 whipwhep 2026-03-20 23:12:09 -04:00
legop3 1b7885e74b whipwhep 2026-03-20 22:52:39 -04:00
legop3 4542c9e6ae whipwhep 2026-03-20 22:52:35 -04:00
legop3 a58685fa91 the removal... 2026-03-20 22:42:59 -04:00
legop3 140ebb5996 todoing 2026-03-20 22:32:23 -04:00
legop3 76c17abdb6 whipwhep 2026-03-20 22:29:54 -04:00
legop3 f364e6ac60 whipwhep 2026-03-20 22:09:59 -04:00
legop3 57532f53d0 whipwhep 2026-03-20 02:25:43 -04:00
legop3 38b76559f4 whipwhep 2026-03-20 02:07:38 -04:00
legop3 1eb1a33e88 whipwhep 2026-03-20 02:06:03 -04:00
legop3 ab9d388392 whipwhep 2026-03-20 02:03:25 -04:00
legop3 cc6e1fde56 whipwhep 2026-03-20 02:00:17 -04:00
legop3 e2d9a1759b whipwhep 2026-03-20 01:56:15 -04:00
legop3 dbfab6aa26 whipwhep 2026-03-20 01:52:45 -04:00
legop3 c729631104 whipwhep 2026-03-20 01:50:29 -04:00
legop3 7ad3dc7015 whipwhep 2026-03-20 01:43:06 -04:00
legop3 5b8704ccb3 gwah 2026-03-17 19:54:53 -04:00
legop3 73ef6261ed ound 2026-03-17 19:46:53 -04:00
legop3 93204ec1f1 wewa 2026-03-17 19:35:36 -04:00
legop3 618fd3e4de remove duration limits 2026-03-17 19:14:13 -04:00
legop3 f5d7d2d82c ui 2026-03-17 19:07:59 -04:00
legop3 7934e1faf5 fix socket size limit 2026-03-17 18:59:39 -04:00
legop3 f63db3fdd7 oddeo 2026-03-17 18:54:31 -04:00
legop3 0e4954fadf audioo 2026-03-17 18:33:27 -04:00
legop3 c44632ed4d error handler 2026-03-17 16:35:53 -04:00
legop3 9f1efc6b91 fix 2026-03-17 03:31:53 -04:00
legop3 ad664f2d32 fix 2026-03-17 03:22:47 -04:00
legop3 fec9782e3b fix 2026-03-17 03:18:02 -04:00
legop3 84eaab3c9c guh 2026-03-17 03:11:04 -04:00
legop3 aaa93847e1 audio 2026-03-16 21:30:56 -04:00
legop3 4aa53e8f80 promp 2026-03-16 01:07:31 -04:00
legop3 74a758ab7c promp 2026-03-15 23:59:23 -04:00
legop3 182a49903f ploo 2026-03-15 23:14:43 -04:00
legop3 941b9889c9 pifix 2026-03-15 23:11:00 -04:00
legop3 6d40ec070e guh 2026-03-15 23:08:05 -04:00
legop3 169ccdbcf8 guh 2026-03-15 23:05:30 -04:00
legop3 a2098f4dfc fix thing 2026-03-15 23:02:17 -04:00
legop3 3070153052 fix thing 2026-03-15 22:57:15 -04:00
legop3 167582b9b0 fix 2026-03-15 22:46:27 -04:00
legop3 95972af558 things 2026-03-15 22:43:05 -04:00
legop3 2ea222355f audioplan 2026-03-13 17:58:28 -04:00
legop3 529ae62ad0 fix ldadmins not getting vip 2026-03-05 03:14:45 -05:00
legop3 3c6de7c040 verifiction 2026-03-05 03:10:26 -05:00
legop3 3007bab54d todoing 2026-03-04 02:55:37 -05:00
legop3 38e23b0adf todoing 2026-03-04 01:21:55 -05:00
legop3 181fe436ff fix keybindingses 2026-03-04 01:21:37 -05:00
legop3 b7f5967c9f todoing 2026-03-04 00:59:49 -05:00
legop3 e53fb5cb92 newtodo 2026-03-03 17:44:49 -05:00
legop3 325fdc05ea Merge branch 'main' of https://github.com/legop3/MultiRoombaRover 2026-03-03 14:28:49 -05:00
legop3 c4532f5825 plannings 2026-03-03 14:26:47 -05:00
legop3 d952b7ad90 Revise tone and output rules in commentary system
Updated tone description and removed redundant rules.
2026-03-02 01:19:39 -05:00
legop3 13da673495 promp 2026-02-28 12:17:27 -05:00
legop3 30d0f0c686 skpi count 2026-02-28 11:56:25 -05:00
legop3 7a21a53ea7 promt 2026-02-28 11:45:19 -05:00
legop3 4fd7f15292 promt 2026-02-28 11:43:00 -05:00
legop3 1350d7c395 promt 2026-02-28 02:13:21 -05:00
legop3 632d474e61 remove goal 2026-02-28 01:16:08 -05:00
legop3 84a0b02c18 promp 2026-02-28 00:56:30 -05:00
legop3 a44d4aed03 SKIP 2026-02-28 00:48:50 -05:00
legop3 fafe167571 promp 2026-02-28 00:44:52 -05:00
legop3 e73b094c21 promp 2026-02-28 00:35:29 -05:00
legop3 23825d51d2 promp 2026-02-27 23:54:53 -05:00
legop3 3cf3228b6f titanfall 2026-02-27 23:39:23 -05:00
legop3 99b10f7310 promp 2026-02-27 23:22:32 -05:00
legop3 ae52dc8335 promp 2026-02-27 22:11:35 -05:00
legop3 7942e61a40 tempchange 2026-02-27 20:05:43 -05:00
legop3 82fe62b835 no blah 2026-02-27 19:14:38 -05:00
legop3 12ce7c1344 add community goal 2026-02-27 19:05:52 -05:00
legop3 469c52cf7f temp reduction 2026-02-27 09:56:52 -05:00
legop3 1ac6c17a8e promp 2026-02-27 02:53:00 -05:00
legop3 231245a9fd promp 2026-02-27 02:41:54 -05:00
legop3 e30f7272cd promp 2026-02-27 02:21:53 -05:00
legop3 08fc278494 promp 2026-02-27 01:33:56 -05:00
legop3 32f5cc29d8 prompt 2026-02-27 01:30:26 -05:00
legop3 985a2f966a cliff improvements 2026-02-27 00:43:45 -05:00
legop3 e9e75b65ef melodrama 2026-02-26 21:06:08 -05:00
legop3 bbd088f9e1 new stuf 2026-02-26 18:57:50 -05:00
legop3 4284014578 soup 2026-02-26 18:06:50 -05:00
legop3 d34c107b1a gwuh 2026-02-26 03:32:31 -05:00
legop3 5a575aeab4 newstufff 2026-02-26 02:58:45 -05:00
legop3 4d953f3cad promt 2026-02-26 02:38:23 -05:00
legop3 09c7da715e loosenmaybe 2026-02-26 02:05:46 -05:00
legop3 9d58983f59 failmodeling 2026-02-26 01:21:42 -05:00
legop3 7afc79add0 promp 2026-02-26 01:08:58 -05:00
legop3 5d3652468f prommpt 2026-02-26 00:46:15 -05:00
legop3 04bedc7e5c remove length limit 2026-02-26 00:25:35 -05:00
legop3 a687d191c1 promptong 2026-02-25 23:04:02 -05:00
legop3 0d3b3b0995 prompt 2026-02-25 17:59:40 -05:00
legop3 65094ea5dc prompt 2026-02-25 17:49:55 -05:00
legop3 643557afc6 promp 2026-02-25 17:43:38 -05:00
legop3 5a7ce8ae69 stat pills for llm status 2026-02-25 17:28:18 -05:00
legop3 7e233e48b2 promptings 2026-02-25 15:50:20 -05:00
legop3 0a65c43eb0 new indicators in web ui for llm stuff 2026-02-25 15:21:54 -05:00
legop3 30671332af newstuf 2026-02-25 13:33:03 -05:00
legop3 20e89839fe new chat style minotiring 2026-02-25 12:57:06 -05:00
legop3 18c0d0fc8d fix log spam 2026-02-25 12:49:33 -05:00
legop3 b22ba216db new system for chat and stuff 2026-02-25 12:47:17 -05:00
legop3 ad31749813 set frequency to 0 (instant skipped or not) 2026-02-25 05:19:42 -05:00
legop3 9e3bce6ec3 tweakes 2026-02-25 05:11:21 -05:00
legop3 6c3ae17aeb prompt 2026-02-25 04:46:51 -05:00
legop3 965a27f3f2 prompt 2026-02-25 04:35:51 -05:00
legop3 2198c64e00 prompt 2026-02-25 04:28:16 -05:00
legop3 1b871a9ad1 adjustment 2026-02-25 04:25:26 -05:00
legop3 27e36206c2 primpr 2026-02-25 04:02:57 -05:00
legop3 6312f19e3c prompt and wheel drops 2026-02-25 03:55:08 -05:00
legop3 112391cbd1 tell it to watch chat 2026-02-25 03:40:18 -05:00
legop3 1f05a7b79c fix formatting 2026-02-25 03:30:36 -05:00
legop3 c5907d8487 prompt 2026-02-25 03:29:54 -05:00
legop3 47f3e7e654 change rover bot to the overseer 2026-02-25 03:23:58 -05:00
legop3 679e901ec6 remove battery stuffs 2026-02-25 02:23:10 -05:00
legop3 4af0deb1dd prompt 2026-02-25 02:13:06 -05:00
legop3 d38503d19c remove prior bot messages from the info 2026-02-25 02:01:34 -05:00
legop3 f7170c6b49 prompt 2026-02-25 00:57:44 -05:00
legop3 62994d07cd prompt 2026-02-25 00:41:13 -05:00
legop3 0af84084c9 tell it to look at chat... 2026-02-25 00:12:30 -05:00
legop3 8c24c84e59 better temps and prompting 2026-02-25 00:08:58 -05:00
legop3 9647a71d49 wara 2026-02-24 23:59:12 -05:00
legop3 315ce61ef7 tweak 2026-02-24 23:44:54 -05:00
legop3 0f31a662ee tiny prompt 2026-02-24 23:42:32 -05:00
legop3 e44d0f7b93 modufy prompt 2026-02-24 23:33:02 -05:00
legop3 49fc19f044 some tweaks 2026-02-24 23:15:31 -05:00
legop3 bb55e90464 remove mentions of skip 2026-02-24 20:16:46 -05:00
legop3 93971f37fe add llm status panel 2026-02-24 20:13:19 -05:00
legop3 46de9b71f3 switch to ollama library 2026-02-24 19:37:38 -05:00
legop3 1d97756aef fixes nameses 2026-02-24 19:31:00 -05:00
legop3 42c885af28 local ollama commentary 2026-02-24 19:03:20 -05:00
legop3 da79a62db6 change some settings defaults 2026-02-21 19:05:13 -05:00
legop3 d3b35506d9 duck fade 2026-02-21 18:59:00 -05:00
legop3 12a787b7c6 guhw 2026-02-21 18:49:31 -05:00
legop3 bf7d54c4f9 brush ducking only ig 2026-02-21 18:45:59 -05:00
legop3 abd337f723 kjfdalshf 2026-02-21 18:20:02 -05:00
legop3 d3a17608af gologle 2026-02-21 18:15:37 -05:00
legop3 ed93bbf4d6 guh 2026-02-21 18:05:19 -05:00
legop3 d2904f067c ploo 2026-02-21 17:52:52 -05:00
legop3 a76062b873 gug 2026-02-21 17:32:16 -05:00
legop3 32ec8e1aa3 ploo 2026-02-21 17:22:38 -05:00
legop3 657189e979 wuh 2026-02-21 13:40:02 -05:00
legop3 cc92174d79 wa 2026-02-21 13:35:28 -05:00
legop3 eefe6f01bb gawa 2026-02-21 13:30:46 -05:00
legop3 4ccc7dfae6 audio debug 2026-02-21 13:21:44 -05:00
legop3 8313f5da66 chrome fix maybe? 2026-02-21 13:14:27 -05:00
legop3 d5978c5e3e agressive compressive 2026-02-18 22:21:41 -05:00
legop3 f1c6ab44ce maybe finally fix double audio 2026-02-18 22:02:32 -05:00
legop3 d165f69b6f fixes 2026-02-18 21:54:16 -05:00
legop3 438fff0902 volume stuff 2026-02-18 21:39:34 -05:00
legop3 7e772ab607 server reboot too 2026-02-18 15:13:38 -05:00
legop3 40e21d4ac5 add reboot command 2026-02-18 15:09:36 -05:00
legop3 eb52c8c6ac oopeies 2026-02-11 17:16:36 -05:00
legop3 1e1a6b6b01 new battery bars!!! 2026-02-11 17:05:30 -05:00
legop3 dcf7208c08 docs stub 2026-02-11 02:32:38 -05:00
legop3 0c3b3aa711 update colors of social buttons 2026-02-10 20:26:17 -05:00
legop3 42c99410f4 new socials buttons and stuffses 2026-02-10 20:02:55 -05:00
legop3 1ac46f4a03 switch back to css anim 2026-02-10 13:42:23 -05:00
legop3 8ea0f7be70 better side brush animation 2026-02-10 13:36:47 -05:00
legop3 0c992e2457 auto sidebrush 2026-02-10 13:18:29 -05:00
legop3 1842177a6d longer bumpening 2026-02-08 17:02:55 -05:00
legop3 c9a19d46f7 drive macro dock bumpoff 2026-02-08 16:59:25 -05:00
legop3 cf0c12cff7 map hud adjustment for mobile 2026-02-02 11:20:53 -05:00
legop3 b4c122d914 dock drive action and joystick tweaks 2026-01-31 22:00:09 -05:00
legop3 3cfb2683ef a few tweaks to dock drive action 2026-01-31 21:44:01 -05:00
legop3 cb5e5ae755 helpp 2026-01-31 21:26:09 -05:00
legop3 d3852dd86b longer horn allowance 2026-01-31 21:20:19 -05:00
legop3 7c1b290df3 swap stuff around because of how stupid safari is i hate apple i hate apple i hate apple 2026-01-31 21:01:23 -05:00
legop3 133b433255 more organized backend and better mobile control columns 2026-01-31 20:49:14 -05:00
legop3 d64e99b674 more adjustment 2026-01-31 16:30:50 -05:00
legop3 79da247dbe short tap fix 2026-01-31 16:29:36 -05:00
legop3 01cad4515f adjustmet 2026-01-31 16:25:59 -05:00
legop3 dc92785f19 gahwah 2026-01-31 16:22:45 -05:00
legop3 00d1edd11b horn cooldown adjustments 2026-01-31 16:16:26 -05:00
legop3 c24ae71e63 horn limits and such 2026-01-31 16:11:24 -05:00
legop3 92c9ae4834 sine and saw volumes 2026-01-31 15:45:36 -05:00
legop3 5916520b1a horn button light up 2026-01-31 15:37:46 -05:00
legop3 7eeb9d13e4 ui 2026-01-31 15:34:38 -05:00
legop3 3fdb1c05e5 ui improvements 2026-01-31 15:32:08 -05:00
legop3 090d9d6982 horn updates 2026-01-31 15:18:25 -05:00
legop3 b7d636c70a horn pass 2026-01-31 13:44:35 -05:00
legop3 92ab763b27 sawtooth wave 2026-01-31 12:30:21 -05:00
legop3 43dc3df901 hornn testingg 2026-01-31 12:23:53 -05:00
legop3 3997127e5e resize batt in sbs 2026-01-30 23:30:47 -05:00
legop3 c60cae75a4 poosh 2026-01-30 23:01:04 -05:00
legop3 4f6530badf just show em all, all the time 2026-01-30 22:58:10 -05:00
legop3 fd06f23de4 feex 2026-01-30 22:37:39 -05:00
legop3 ca3d95319d mini video prewarm :3 2026-01-30 22:31:40 -05:00
legop3 bdb4f41e93 local spectators get real video maybe if it works 2026-01-30 21:53:02 -05:00
legop3 a5f33c0a92 updated /mini again.. 2026-01-30 21:32:17 -05:00
legop3 a4d6f55576 remove updatedAt from UI 2026-01-29 13:25:58 -05:00
legop3 7216d9ba51 admin reason!! 2026-01-29 13:24:07 -05:00
legop3 be4c6260ae /mini simplifications (was too big) 2026-01-29 12:58:58 -05:00
legop3 5e94fc7283 /mini new (again) 2026-01-28 21:52:41 -05:00
legop3 bf05d07092 gamepad rate lmiit 2026-01-28 17:00:19 -05:00
legop3 c5ae78dcab finally... button.. 2026-01-28 14:36:47 -05:00
legop3 33d329e2de moving things 2026-01-28 14:32:09 -05:00
legop3 690f38972f even more betterer button 2026-01-28 14:28:52 -05:00
legop3 77b5484381 betterer button 2026-01-28 14:25:33 -05:00
legop3 ccf0eeb1aa better button 2026-01-28 14:22:29 -05:00
legop3 d87ec55802 gwuh 2026-01-28 14:16:06 -05:00
legop3 2cc0ce962f spam fix 2026-01-28 14:11:44 -05:00
legop3 d2722e40cc ui fixes 2026-01-28 14:09:09 -05:00
legop3 ae81f8914f ugh 2026-01-28 13:59:29 -05:00
legop3 7e850dab22 stateful night vision rework 2026-01-28 13:45:01 -05:00
legop3 f3cc73f178 light panel adjustments 2026-01-27 20:08:04 -05:00
legop3 eea1774549 color light support in HA panel 2026-01-27 19:29:06 -05:00
legop3 7dbb657dcd 5s autocharge to match ui... 2026-01-27 17:19:07 -05:00
legop3 48e4ddd090 ug 2026-01-26 19:53:19 -05:00
legop3 f7677a443a carphone 2026-01-26 19:49:45 -05:00
legop3 482e7c1e6b camera servo speed limit 2026-01-26 19:45:14 -05:00
legop3 b275d5a08c chat on spectator page 2026-01-26 14:10:13 -05:00
legop3 2062b4888c wawa 2026-01-26 01:03:09 -05:00
legop3 2671247347 tts passthrough for all in queue 2026-01-25 23:45:18 -05:00
legop3 3f34196418 better dockdrive info labels 2026-01-25 20:44:50 -05:00
legop3 689d914b4e gamepad ripout 2026-01-25 17:15:57 -05:00
legop3 b6b1bee1dd more tweakings 2026-01-25 03:18:50 -05:00
legop3 522ec335bf bar fit screen 2026-01-25 03:03:16 -05:00
legop3 b730c98704 taller bar, move user list 2026-01-25 03:01:16 -05:00
legop3 d7833dbba9 spectator tweakings 2026-01-25 02:59:16 -05:00
legop3 536e4fe32e new /mini stuff 2026-01-24 23:54:52 -05:00
legop3 2494f6e276 lockdown fixes 2026-01-24 23:40:22 -05:00
legop3 2dda137855 centered hud for your brain 2026-01-24 03:06:29 -05:00
legop3 8341328b51 round. . . ? 2026-01-24 02:58:36 -05:00
legop3 b718de1e88 better DRD (dead rover detection) 2026-01-23 16:46:09 -05:00
legop3 ec4e969340 disconnected reboot timer for pi 2026-01-23 16:42:13 -05:00
legop3 dd55514cb4 ws keepalive (brownout mitigation) 2026-01-23 16:32:20 -05:00
736 changed files with 99448 additions and 14334 deletions
+19 -2
View File
@@ -5,6 +5,8 @@ create_2_Open_Interface_Spec.txt
logs
node_modules/
__pycache__/
*.py[cod]
.pio
.vscode/
config.h
@@ -14,5 +16,20 @@ server/config.yaml
server/package-lock.json
package-lock.json
server/data/discord-guilds.json
server/data/community-goal.json
server/data
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
+58 -73
View File
@@ -1,89 +1,74 @@
# Multi Roomba Rover
A system for controlling create 2 compatible roombas through a webpage.
a remake of my RoombaRover project with a decentralized and embedded approach
supports multiple roombas
You can explore my basement through this project here:
https://rover.otter.land
on each roomba:
- a raspberry pi zero 2 w
- a raspberry pi camera
- roomba's serial port hooked up to the built in serial port on the raspberry pi
*some of this code was created with help from large language models, and some of it was written by me. This project would not have been possible for me to create without it.*
pi provisioning:
- enable serial port
- disable wifi powersave
- disable bluetooth
## This guide is a work in progress, it will cover:
- Building rovers
- Installing roverd on a rover's raspberry pi
- Setting up the server
- Configuration
## Repo layout
## Building a rover
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.
- `pi/roverd`: tiny Go daemon that bridges the Create 2 serial port, BRC pin, and the control server via WebSockets.
- `server`: Node.js process that terminates rover sockets, relays commands to/from the Socket.IO UI, and serves `public/`.
- `pi/bin` / `pi/systemd`: helper scripts + systemd units for the rover (roverd itself plus the SRT video publisher that streams camera feeds to the server).
- `docs/pi-deployment.md`: per-rover build + install instructions (cross-compiling on Fedora 43, deploying roverd + mediaMTX).
### Parts per rover
## Quick start
| Part | Notes |
| --- | --- |
| Create 2 compatible Roomba with the mini DIN port | Tested only with 600 series Roombas, your mileage may vary with newer models |
| 4S li-ion -> 5v Voltage regulator | I use cheapo 12-24v USB car chargers, tear them apart, and solder wires onto them |
| 7 pin mini DIN cable | Adafruit sells these labeled as a cable specifically for the iRobot Create 2 |
| Logic level shifter | I use ones from adafruit, the Roomba is 5v logic and the pi is 3.3v logic |
| 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. |
| 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. |
| 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. |
| 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. |
| ----CAMERA STUFF---- | ----Below here is all camera related things that youll need---- |
| 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 |
```bash
# build the Pi agent (armv7)
cd pi/roverd
mkdir -p ../../dist
make pi-build
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.
# start the server + UI
cd ../../server
npm install
npm run start
```
### Physical assembly of the rover
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.
Need a fake rover or multi-rover testing without hardware? Use the dummy build:
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:
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.
```bash
cd pi/roverd
mkdir -p ../../dist
make dummy
./../../dist/roverd-dummy -config ./roverd.sample.yaml
```
### Software on your rover
Raspberry pi OS installation:
The dummy binary connects to the Node server, emits simulated sensor frames, and logs every command it receives, so you can spin up as many virtual rovers as youd like on your dev machine.
Everything that runs on the pi is designed for debian Bookworm, because I could not get the AIY voice hat working in Trixie.
## Admin config & authentication
To install roverd on your pi:
1. Install `Raspberry Pi OS (Legacy) Lite` (exactly what its called in the imager) on your SD card
2. Boot the pi up, plug in your good wifi adapter, use `nmtui` to set it up
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`
Before running the Node server, copy `server/config.example.yaml` to `server/config.yaml` and customize the admin records (password hashes, Discord IDs, lockdown permission). Those credentials are used by the driver UIs login panel—only admins can toggle locks/modes, and lockdown admins retain access when the system enters lockdown mode. The spectator page (future) can set `role:set` to `spectator`, and the server enforces all permissions server-side so client tweaks cant grant extra control.
Edit the `/etc/roverd.yaml` config file to change settings like your rover's name, color, and set the server IP.
Deploy a rover by copying the repo + `dist/roverd` to the Pi and running the helper (it installs roverd plus the SRT video publisher service):
```bash
cd ~/MultiRoombaRover
sudo ./pi/install_roverd.sh
```
Then point each rover's `/etc/roverd.yaml` at `ws://<server>:8080/rover`, set `name` to the rovers ID, and (optionally) override `media.publishUrl` if your control server isnt `192.168.0.86`. The video publisher service (`video-publisher.service`) captures the Pi camera with `rpicam-vid`/`libcamera-vid`, pipes the raw H264 into the stock FFmpeg binary, and publishes via SRT to the servers mediaMTX instance at `srt://<server>:9000?streamid=#!::r=<name>,m=publish`. The installer now just pulls `libcamera-apps` + `ffmpeg` from apt (no custom build). Use the “Restart Camera” button if you enable media management so roverd can bounce the publisher service remotely.
Heads-up: the BRC pulser now uses libgpiod; make sure the `roverd` service account is in the `gpio` group (or otherwise allowed to access `/dev/gpiochip*`) and set `brc.gpioChip` if your hardware exposes a different chip name.
## Fedora server deployment
Run the installer from inside the `server/` directory after cloning the repo onto your Fedora 43 Server box:
```bash
cd ~/MultiRoombaRover/server
sudo ./install_server.sh
```
The script must be executed via `sudo` from the user that owns the repo. It will:
- install Node.js/npm plus curl/tar
- run `npm install --production`
- copy `config.example.yaml` to `config.yaml` if needed (edit the file afterwards for admins + `media.whepBaseUrl`)
- download mediaMTX v1.15.3 and drop it into `/usr/local/bin`
- write `/etc/mediamtx/mediamtx.yml` from `server/mediamtx/mediamtx.yml` (SRT ingest on :9000, open ingest, viewer auth webhook at `/mediamtx/auth`)
- create + enable `mediamtx.service` and `multirover.service`, both running as your repo user and pointing at the clone directly
Publishing rovers lives on a trusted network, so the shipped config (tracked at `server/mediamtx/mediamtx.yml`) skips HTTP auth for SRT ingest and whitelists any path that matches `rover-*`. The installer overwrites `/etc/mediamtx/mediamtx.yml` every time you run it—if you need to tweak ports or add TURN servers, edit the template in the repo and rerun `install_server.sh` so every box stays in sync automatically.
Once finished, update `server/config.yaml` with your admin passwords and `media.whepBaseUrl` (set it to the URL you expose publicly, e.g. `https://rover.otter.land/video`). If your proxy cant rewrite paths, create the `/video` location there and add a custom nginx snippet to rewrite `/video/<rover>/whep` to `/<>/whep` before forwarding to mediaMTX. Restart `multirover.service` whenever you edit the config. To pull updates later, just `git pull`, re-run `npm install --production` inside `server/`, and restart the service—no need to rerun the installer.
Room cameras now use JPEG snapshots (4 fps) instead of WHEP. Each entry in `roomCameras` must include a `url` pointing at the snapshot endpoint; the server polls and relays frames over socket.io with the same access rules as before.
### Video handshake + diagnostics
- Every `video:request` returns `{ url, token }`. The browser posts the SDP offer to `url` and includes `Authorization: Basic base64(token:token)`. mediaMTX forwards the username (`token`) to `/mediamtx/auth`, which checks the sockets permissions (driver assignment, admin/spectator role, lockdown state) and either returns 200 or 401—no query parameters are involved anymore.
- To see what mediaMTX is ingesting from the Pis, run `npm run check:media` (or `node scripts/checkMedia.js`). It hits `/v3/paths/list` and prints each rovers `ready` state and byte counters so you can instantly spot publish issues.
## Setting up the server
+6
View File
@@ -0,0 +1,6 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
config.h
+37
View File
@@ -0,0 +1,37 @@
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
@@ -0,0 +1,46 @@
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
@@ -0,0 +1,15 @@
; 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
@@ -0,0 +1,3 @@
#define WIFI_SSID "wifissid"
#define WIFI_PASSWORD "wifipassword"
#define SERVER_URL "http://192.168.0.86:8080/buttonbox/press"
+110
View File
@@ -0,0 +1,110 @@
#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
@@ -0,0 +1,11 @@
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
+3 -1
View File
@@ -18,5 +18,7 @@ media:
service: mediamtx.service
healthUrl: http://127.0.0.1:9997/v3/paths/list
healthInterval: 30s
nightVision:
headlight:
enabled: false
laser:
enabled: false
+3 -1
View File
@@ -18,5 +18,7 @@ media:
service: mediamtx.service
healthUrl: http://127.0.0.1:9997/v3/paths/list
healthInterval: 30s
nightVision:
headlight:
enabled: false
laser:
enabled: false
+3 -1
View File
@@ -18,5 +18,7 @@ media:
service: mediamtx.service
healthUrl: http://127.0.0.1:9997/v3/paths/list
healthInterval: 30s
nightVision:
headlight:
enabled: false
laser:
enabled: false
Vendored Executable
BIN
View File
Binary file not shown.
Vendored
BIN
View File
Binary file not shown.
Vendored Executable
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
-112
View File
@@ -1,112 +0,0 @@
# Pi Deployment Guide
## Tooling
Fedora 43:
```bash
sudo dnf install golang libgpiod
```
Cross-compiling roverd for Pi Zero 2 W (ARMv7):
```bash
cd pi/roverd
mkdir -p ../../dist
make pi-build
```
The binary is placed in `dist/roverd` (relative to the repo root).
### Dummy rover build (for local testing)
If you need extra “virtual” rovers on your laptop/CI box, build the dummy binary:
```bash
cd pi/roverd
mkdir -p ../../dist
make dummy
```
Run the resulting `dist/roverd-dummy` on any machine; it will connect to the server, stream fake Group 100 sensor data, and log drive commands so you can test multi-rover features without additional hardware.
## Automated installation (recommended)
Install the Raspberry Pi camera helpers (Bookworm ships them):
```bash
sudo apt update
sudo apt install libcamera-apps
```
Once the binary (and repo) are on the Pi, run the helper script from the repo root:
```bash
cd ~/MultiRoombaRover
sudo ./pi/install_roverd.sh
```
What the script does:
- creates the `roverd` service account (dialout/gpio/video groups) if missing and installs `/usr/local/bin/roverd`
- copies `pi/roverd/roverd.sample.yaml` to `/etc/roverd.yaml` if the file is absent (existing configs are left untouched)
- installs/enables the `roverd.service` systemd unit, restarting it automatically when a config already exists
- installs `/usr/local/bin/video-publisher`, drops `video-publisher.service`, and enables it so video is published automatically on boot
Flags:
| Flag | Purpose |
|------|---------|
| `-b PATH` | use a different roverd binary (defaults to `dist/roverd`) |
| `-c PATH` | seed `/etc/roverd.yaml` from another template |
If the script installs the sample config, it will remind you to edit `/etc/roverd.yaml` before manually restarting the service: set `name`, `serverUrl`, serial device, BRC pin, battery thresholds, and optionally override `media.publishUrl`. When left blank, roverd automatically publishes to `srt://<server-host>:9000?streamid=#!::r=<name>,m=publish…` (the host comes from `serverUrl`). If your reverse proxy adds prefixes (like `/video/<name>`), use its rewrite options so the rover keeps publishing to plain `<name>`.
Re-run `pi/install_roverd.sh` any time you pull updates—the script overwrites the roverd + video-publisher binaries and drops the latest systemd units so the only configuration you ever touch manually is `/etc/roverd.yaml`. `roverd` rewrites `/var/lib/roverd/video.env` on startup, so no other files need editing.
## Manual installation
1. Copy the binary and config:
```bash
sudo useradd -r -s /usr/sbin/nologin roverd || true
sudo install -o roverd -g roverd -m 0755 dist/roverd /usr/local/bin/roverd
sudo install -o roverd -g roverd -m 0640 pi/roverd/roverd.sample.yaml /etc/roverd.yaml
```
Adjust `/etc/roverd.yaml` for each rover: `name`, `serverUrl` (e.g. `ws://control-server:8080/rover`), serial port path, battery thresholds, GPIO pin for BRC, and (if needed) the media `publishUrl` override. Otherwise, roverd derives `srt://<server-host>:9000?streamid=#!::r=<name>,m=publish&latency=20&mode=caller&transtype=live&pkt_size=1316` based on the `serverUrl`; keep proxy-only path prefixes out of this URL so every rover keeps the same simple stream name.
2. Install the systemd unit:
```bash
sudo install -m 0644 pi/systemd/roverd.service /etc/systemd/system/roverd.service
sudo systemctl daemon-reload
sudo systemctl enable --now roverd.service
```
`roverd` requires access to `/dev/ttyAMA0` and `/dev/gpiochip*`; keeping it under its own user ensures the rest of the system stays isolated—just make sure the account belongs to the `dialout` and `gpio` groups so it can reach the UART and libgpiod.
The video publisher needs read access to the camera devices (`/dev/media*`, `/dev/video*`), so the install script adds the `roverd` service account to the `video` group; if you created the user manually, make sure it belongs to `video`.
**BRC note:** configure `brc.gpioPin` (and `brc.gpioChip` if youre not using `gpiochip0`) and ensure the `roverd` user has permission to toggle that line—no root privileges are required anymore.
If you set `media.manage: true` in `/etc/roverd.yaml`, make sure the `roverd` service account can invoke `systemctl <action> <media.service>` (the installer wires `video-publisher.service` to run as `roverd`, so no sudo tweaks are required unless you rename it).
## Video publisher details
The `video-publisher.service` unit runs `/usr/local/bin/video-publisher`, piping `rpicam-vid`/`libcamera-vid` straight into the stock FFmpeg package:
```bash
rpicam-vid (or libcamera-vid) --inline --timeout 0 --width=WIDTH --height=HEIGHT \
--framerate=FPS --bitrate=BITRATE --codec h264 --profile baseline --output - \
| ffmpeg -hide_banner -loglevel warning -fflags nobuffer \
-f h264 -i pipe:0 -c:v copy -an -flush_packets 1 -f mpegts $PUBLISH_URL
```
`/var/lib/roverd/video.env` carries all tunables (`PUBLISH_URL`, resolution, FPS, bitrate). `roverd` rewrites the file whenever you restart it or hit the “Restart Camera” button so the publisher always inherits the correct rover ID + bitrate knobs. Because we now use the distro FFmpeg build with SRT enabled, the installer is much faster—no custom compile steps.
Use `sudo systemctl status video-publisher` to watch logs; the unit auto-restarts whenever the connection drops or FFmpeg exits with an error.
## Server + UI
From the repo root:
```bash
cd server
npm install
npm run start
```
This launches the HTTP server (serving the barebones UI) and the rover WebSocket endpoint at `ws://<server>:8080/rover`. The UI expects `roverd` instances to send `hello` frames so it can populate the rover list. Use the mode buttons to emit Start/Safe/Full/Passive/Dock commands (they send the raw OI opcode bytes), tap the sensor toggle to request Group 100 streaming, and use WASD for drive testing; the UI emits Drive Direct commands ~8 times per second, so the rover sees them immediately.
+186
View File
@@ -0,0 +1,186 @@
# `/` Performance Investigation Summary
This is the high-level handoff for the default route at:
```sh
https://rover.otter.land/
```
The detailed issue writeups live in `perf/issues/`.
## Main Finding
The `/` page is doing too much main-thread work for low-end devices. On the current powerful
machine it can appear smooth, but CPU throttling shows real risk for mobile and weaker
computers.
The biggest measured problem is repeated global input listener churn. The broader underlying
theme is that high-frequency live data and broad React context invalidation cause many parts
of the page to rerender or recommit DOM attributes.
## Key Measurements
### Clean `/` Profile, 8x Mobile CPU Throttle
Artifact:
```sh
perf/results/2026-06-11T03-15-51-398Z/report.json
```
Results:
```txt
average frame interval: ~140ms
p95 frame interval: ~166.8ms
p99 frame interval: ~383.4ms
long tasks: 67
event listener delta: +13,481
JS heap delta: +22.76MB
ScriptDuration delta: ~35.85s
TaskDuration delta: ~52.88s
```
Top notable CPU self-time included:
```txt
removeEventListener ~1.30s
addEventListener ~0.94s
```
### Listener Audit
Artifact:
```sh
perf/results/2026-06-11T03-03-31-625Z-listeners/listener-report.json
```
Worst churn:
```txt
keydown adds=8040 removes=8039
keyup adds=8040 removes=8039
blur adds=8040 removes=8039
gamepadconnected adds=8040 removes=8039
gamepaddisconnected adds=8040 removes=8039
```
### `/` Runtime Audit, 6x Mobile CPU Throttle, 30s
Artifact:
```sh
perf/results/2026-06-11T03-21-51-263Z-root-runtime/root-runtime-report.json
```
Results:
```txt
average frame interval: ~88ms
p95 frame interval: 150ms
p99 frame interval: 183ms
long tasks: 18
WebSocket messages: at least 1000 sampled
WebSocket bytes: ~798KB sampled
DOM mutation records: 41,062
DOM attribute records: 40,761
```
Top sampled WebSocket events:
```txt
log:entry 526
sensorFrame 306
commandAck 145
session:sync 5
```
### `/` Runtime Audit With Mutation Targets, 6x Mobile CPU Throttle, 18s
Artifact:
```sh
perf/results/2026-06-11T03-22-44-714Z-root-runtime/root-runtime-report.json
```
Results:
```txt
average frame interval: ~116.6ms
p95 frame interval: ~166.7ms
p99 frame interval: ~316.6ms
long tasks: 16
DOM mutation records: 18,758
DOM attribute records: 18,490
```
Top WebSocket events:
```txt
log:entry 850
sensorFrame 277
commandAck 200
session:sync 23
```
Top mutation targets:
```txt
input.accent-emerald-400 name 5300
input.accent-emerald-400 type 2650
field-input name ~1300 each
div.h-full style 1238
path d 621
path fill 619
```
## Ordered Work Plan
1. Fix `KeyboardInputManager` and `GamepadInputManager` listener churn.
2. Split/stabilize `ControlContext` so action consumers do not rerender on every state change.
3. Batch or gate `log:entry` socket updates.
4. Throttle visual telemetry rendering.
5. Memoize or lazy-mount `ReplaySourcesPanel`.
6. Split and memoize chat composer/nickname form.
7. Clean up or share timers.
## Reproduction Commands
Install Playwright dependency if missing:
```sh
npm install --prefix perf playwright@1.60.0
npx playwright install chromium
```
Clean CPU profile:
```sh
CPU_THROTTLE=8 VIEWPORT=390x844 MOBILE=1 node perf/live-cpu-profile.mjs https://rover.otter.land/ perf/results
```
Listener audit:
```sh
node perf/live-listener-audit.mjs https://rover.otter.land/ perf/results
```
Runtime audit:
```sh
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=30000 node perf/live-root-runtime-audit.mjs https://rover.otter.land/ perf/results
```
Runtime audit with shorter mutation target sample:
```sh
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=18000 node perf/live-root-runtime-audit.mjs https://rover.otter.land/ perf/results
```
## Notes
The `perf/results/2026-06-11T03-11-39-596Z-route-sweep/` route sweep includes `/spectate`,
`/mini`, `/display`, and `/scanner`. Those are not the current priority. Use them only for
context.
+147
View File
@@ -0,0 +1,147 @@
# `/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
@@ -0,0 +1,278 @@
# 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.
@@ -0,0 +1,192 @@
# 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
@@ -0,0 +1,165 @@
# 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.
@@ -0,0 +1,205 @@
# 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.
@@ -0,0 +1,191 @@
# Issue 005: ReplaySourcesPanel Repeated DOM Commits
## Summary
`ReplaySourcesPanel` appears to re-commit many input/checkbox attributes repeatedly on `/`,
especially in mobile portrait where the panel is always mounted near the top of the page.
This may be a component-local issue, or it may be caused by broader context/session updates.
Tackle it after issues 001 and 002 unless profiling still shows it as large.
## Severity
Medium.
It is visible in mutation probes, but likely partly downstream of broader invalidation.
## Affected Files
- `webui/src/components/ReplaySourcesPanel/index.jsx`
- `webui/src/App.jsx`
- `webui/src/context/SessionContext.jsx`
- `webui/src/settings/SettingsProvider.jsx`
## Evidence
Artifact:
```sh
perf/results/2026-06-11T03-22-44-714Z-root-runtime/root-runtime-report.json
```
In an 18s `/` audit at mobile viewport with 6x CPU throttle, top mutation targets included:
```txt
input.accent-emerald-400 name: 5300
input.accent-emerald-400 type: 2650
input#replay-sources-mobile-portrait-title name: 666
input#replay-sources-mobile-portrait-title type: 333
```
The `accent-emerald-400` class maps to `ReplaySourcesPanel` checkboxes:
```jsx
<input
type="checkbox"
checked={includeSidebar}
...
className="accent-emerald-400"
/>
```
and source item checkboxes:
```jsx
<input
type="checkbox"
checked={selected.includes(item.key)}
onChange={() => onToggle(item.key)}
className="accent-emerald-400"
/>
```
The mobile portrait route mounts it here:
```jsx
<ReplaySourcesPanel panelId="replay-sources-mobile-portrait" />
```
in `App.jsx`.
## Why This Matters
Repeated DOM attribute commits are a symptom of repeated React commits. Even if the attribute
values do not change semantically, React still touches DOM attributes when the subtree
rerenders/commits.
The panel is not central to driving, so it should not be doing meaningful work during normal
control-page idle.
## Likely Causes
The component subscribes to several session slices:
```jsx
const replaySources = useSessionSelector((state) => state.session?.replaySources ?? []);
const mode = useSessionSelector((state) => state.session?.mode || null);
const assignmentRoverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
const roster = useSessionSelector((state) => state.session?.roster ?? []);
const replayState = useSessionSelector((state) => state.session?.replay || null);
const latestReplay = useSessionSelector((state) => state.latestReplay);
```
Potential problems:
- `session:sync` may provide new array/object identities even when contents are unchanged.
- `normalizeSources(replaySources || [])` runs every render and produces new objects.
- `GroupList` is not memoized.
- `selected.includes(item.key)` is recomputed for each item on each render.
- `remainingMs` interval updates every 250ms when cooldown is active.
- Parent/mobile layout re-renders can also re-render this panel.
## Fix Strategies
### Option A: Memoize Normalized Sources
```jsx
const sources = useMemo(
() => normalizeSources(replaySources || []),
[replaySources],
);
```
This is already not memoized in the current code.
### Option B: Use Shallow Equality Selectors
For arrays like `replaySources` and `roster`, use a selector/equality function that avoids
rerendering when content did not change.
Example:
```jsx
const replaySources = useSessionSelector(
(state) => state.session?.replaySources ?? [],
shallowReplaySourcesEqual,
);
```
### Option C: Memoize GroupList
```jsx
const GroupList = React.memo(function GroupList(...) { ... });
```
Also memoize:
```jsx
const selectedSet = useMemo(() => new Set(selected), [selected]);
```
Then use `selectedSet.has(item.key)`.
### Option D: Do Not Mount Replay Panel By Default on Mobile
If replay is not a core mobile driving workflow, consider putting it behind a tab/accordion
or lazy mounting it only when expanded.
This is a product decision.
### Option E: Stop Passing Unstable Callbacks
Memoize callbacks:
```jsx
const toggleKey = useCallback((key) => { ... }, []);
const handleReplay = useCallback(async () => { ... }, [...]);
```
This helps if child components are memoized.
## Recommended Path
1. Fix issues 001 and 002 first.
2. Rerun root runtime audit.
3. If `ReplaySourcesPanel` still dominates mutation targets:
- memoize `sources`
- memoize `GroupList`
- add shallow equality selectors
- consider lazy mounting on mobile
## Validation
Run:
```sh
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=18000 node perf/live-root-runtime-audit.mjs https://rover.otter.land/ perf/results
```
Expected improvement:
```txt
input.accent-emerald-400 name/type mutations should drop sharply
replay title input name/type mutations should drop
```
## Risks
- Replay source selections and saved panel settings must still update correctly.
- If lazy mounting, preserve persisted title/sidebar settings.
- Do not break replay cooldown display.
+172
View File
@@ -0,0 +1,172 @@
# Issue 006: Chat and Nickname Composer Churn
## Summary
The mobile `/` page defaults to the chat tab, and the chat/nickname composer shows repeated
DOM attribute commits in runtime audits. This is not the top CPU issue, but it is part of
steady idle churn.
## Severity
Medium-low to medium.
Fix after the listener/context/log/telemetry work unless chat remains high in mutation
reports.
## Affected Files
- `webui/src/components/ChatPanel/index.jsx`
- `webui/src/components/NicknameForm/index.jsx`
- `webui/src/context/ChatContext.jsx`
- `webui/src/App.jsx`
## Evidence
Artifact:
```sh
perf/results/2026-06-11T03-22-44-714Z-root-runtime/root-runtime-report.json
```
In an 18s `/` audit at mobile viewport with 6x CPU throttle:
```txt
input.field-input.chat-composer-input name: 666
input.field-input.flex-1 name: 1328
input.field-input.w-full name: 1332
input.accent-cyan-500 name: 1324
input.accent-cyan-500 type: 662
```
These map to:
- chat composer input
- nickname form input
- TTS/speak checkbox
- related chat form controls
The chat panel is mounted by default in mobile feature tabs:
```jsx
const [activeTab, setActiveTab] = useState('chat');
...
<TabPanel id="chat">
<ChatPanel nicknameLayout="stacked" />
...
</TabPanel>
```
## Why This Matters
The chat UI is useful, but it should not re-commit its composer inputs hundreds of times
while the user is simply watching/driving.
Also, text inputs and focus-related logic interact with keyboard driving. Excess churn here
can have indirect effects on input handling.
## Likely Causes
`ChatPanel` consumes:
```jsx
const role = useSessionSelector((state) => state.session?.role || null);
const currentRoverId = useSessionSelector((state) => state.session?.assignment?.roverId || null);
const roster = useSessionSelector((state) => state.session?.roster ?? []);
const { messages, typing, sendMessage, ... } = useChat();
const { value: ttsSettings, save: saveTtsSettings } = useSettingsNamespace('tts', ...);
```
It also has effects that sync local state from settings:
```jsx
useEffect(() => {
const nextEngine = ...
if (engine !== nextEngine) setEngine(nextEngine);
...
}, [engine, googlePitch, googleSpeed, pitch, ttsSettings..., voice]);
```
Potential problems:
- broad session/chat context updates rerender the whole panel
- message list and composer are in the same component
- typing events cause panel updates
- nickname form may be rerendering with the whole chat panel
- TTS settings sync effect has many dependencies
## Fix Strategies
### Option A: Split ChatPanel Into Memoized Subcomponents
Separate:
- `ChatMessageList`
- `ChatComposer`
- `TtsControls`
- `NicknameForm`
Only `ChatMessageList` should rerender when messages change.
Only `ChatComposer` should rerender when draft/sending/focus changes.
### Option B: Memoize Composer
```jsx
const ChatComposer = React.memo(function ChatComposer(props) { ... });
```
Pass stable callbacks where possible.
### Option C: Debounce Typing Updates
`setTypingActive(Boolean(next.trim()))` fires on every keystroke. That is fine while typing,
but should not be involved during idle. If it causes socket chatter, debounce it.
### Option D: Stabilize NicknameForm
If `NicknameForm` consumes settings/session state broadly, make it use precise selectors and
memoize it.
### Option E: Avoid Re-Syncing Local TTS State Too Often
The settings sync effect can be simplified or guarded so it only runs when the settings
object actually changes, not on every local state update.
## Recommended Path
1. Fix issues 001-004 first.
2. Rerun mutation audit.
3. If chat/nickname inputs remain high:
- split `ChatPanel`
- memoize composer
- stabilize `NicknameForm`
## Validation
Run:
```sh
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=18000 node perf/live-root-runtime-audit.mjs https://rover.otter.land/ perf/results
```
Expected improvements:
```txt
chat-composer-input mutations should drop
NicknameForm input mutations should drop
accent-cyan-500 checkbox mutations should drop
```
Also manually verify:
- typing
- sending chat
- TTS options
- nickname edit/save
- Enter-to-focus/send chat shortcut
## Risks
- Chat focus behavior is tied to keyboard controls.
- Splitting components can accidentally break `registerInputRef`.
- TTS settings must still persist correctly.
+162
View File
@@ -0,0 +1,162 @@
# 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
@@ -0,0 +1,65 @@
# `/` CPU Performance Issue Index
This folder breaks the live-site `/` CPU investigation into separate, work-ready issues.
Each issue can be handled in its own chat or branch without needing to re-run the whole
investigation first.
The testing target was the deployed page:
```sh
https://rover.otter.land/
```
The important local perf tools are:
```sh
node perf/live-cpu-profile.mjs https://rover.otter.land/ perf/results
node perf/live-listener-audit.mjs https://rover.otter.land/ perf/results
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=30000 node perf/live-root-runtime-audit.mjs https://rover.otter.land/ perf/results
```
The strongest `/` artifacts from this investigation are:
- `perf/results/2026-06-11T03-15-51-398Z/report.json`
Clean `/` profile, mobile viewport, 8x CPU throttle.
- `perf/results/2026-06-11T03-03-31-625Z-listeners/listener-report.json`
Listener add/remove stack audit.
- `perf/results/2026-06-11T03-21-51-263Z-root-runtime/root-runtime-report.json`
`/` runtime audit, mobile viewport, 6x CPU throttle, 30s sample.
- `perf/results/2026-06-11T03-22-44-714Z-root-runtime/root-runtime-report.json`
`/` runtime audit, mobile viewport, 6x CPU throttle, 18s sample with mutation targets.
## Ranked Issues
1. [Global input listener churn](./001-global-input-listener-churn.md)
2. [ControlContext broad invalidation](./002-control-context-broad-invalidation.md)
3. [High-volume socket log stream](./003-high-volume-log-stream.md)
4. [Sensor telemetry render frequency](./004-sensor-telemetry-render-frequency.md)
5. [ReplaySourcesPanel repeated DOM commits](./005-replay-sources-panel-churn.md)
6. [Chat and nickname composer churn](./006-chat-and-nickname-churn.md)
7. [Timers and polling cleanup](./007-timers-and-polling.md)
## Quick Read
The highest-confidence first fix is issue 001. It is directly measured, large, and
structurally fixable: install keyboard/gamepad global listeners once and have them read
latest state/actions from refs.
Issue 002 is the likely multiplier behind issue 001 and several UI churn symptoms. The
current `ControlContext` value changes broadly, and many callbacks depend on `state` or
`pipeline`; that causes consumers/effects to refresh even when the visible behavior did
not materially change.
Issues 003 and 004 are independent live-data pressure: the `/` page receives a lot of
socket traffic, especially `log:entry` and `sensorFrame`, and those streams feed UI work.
Issue 005 and issue 006 are component-level symptoms visible in DOM mutation probes. They
should probably be tackled after the context/listener work, because broad invalidation may
be causing some of their repeated commits.
## Important Scope Note
There are also `/spectate` results in `perf/results` because the investigation briefly
compared routes. The user later clarified that `/spectate` is not the current priority.
These issue writeups intentionally focus on `/` only.
+251
View File
@@ -0,0 +1,251 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { chromium } from 'playwright';
const url = process.argv[2] || 'https://rover.otter.land';
const outDir = process.argv[3] || path.resolve('perf/results');
const cpuThrottle = Number(process.env.CPU_THROTTLE || 1);
const viewport = (process.env.VIEWPORT || '1440x950').split('x').map((part) => Number(part));
const isMobile = process.env.MOBILE === '1';
const runId = new Date().toISOString().replace(/[:.]/g, '-');
const runDir = path.join(outDir, runId);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function summarizeCpuProfile(profile) {
const nodes = new Map(profile.nodes.map((node) => [node.id, node]));
const totals = new Map();
const samples = profile.samples || [];
const deltas = profile.timeDeltas || [];
let totalUs = 0;
for (let i = 0; i < samples.length; i += 1) {
const delta = deltas[i] || 0;
totalUs += delta;
const node = nodes.get(samples[i]);
if (!node) continue;
const frame = node.callFrame || {};
const key = `${frame.url || '(anonymous)'}::${frame.functionName || '(anonymous)'}`;
const prev = totals.get(key) || {
url: frame.url || '',
functionName: frame.functionName || '(anonymous)',
lineNumber: frame.lineNumber,
columnNumber: frame.columnNumber,
selfMs: 0,
samples: 0,
};
prev.selfMs += delta / 1000;
prev.samples += 1;
totals.set(key, prev);
}
return {
totalProfileMs: totalUs / 1000,
sampleCount: samples.length,
topSelfTime: [...totals.values()]
.sort((a, b) => b.selfMs - a.selfMs)
.slice(0, 30),
};
}
function interestingMetrics(metrics) {
const wanted = [
'Timestamp',
'Documents',
'Frames',
'JSEventListeners',
'Nodes',
'LayoutCount',
'RecalcStyleCount',
'LayoutDuration',
'RecalcStyleDuration',
'ScriptDuration',
'TaskDuration',
'JSHeapUsedSize',
'JSHeapTotalSize',
];
return Object.fromEntries(
metrics.metrics
.filter((metric) => wanted.includes(metric.name))
.map((metric) => [metric.name, metric.value]),
);
}
async function performInteractions(page) {
await page.mouse.wheel(0, 1400);
await sleep(800);
await page.mouse.wheel(0, -900);
await sleep(800);
await page.keyboard.press('Tab');
await sleep(300);
await page.keyboard.press('Tab');
await sleep(300);
const buttons = await page.locator('button:visible').elementHandles();
for (const button of buttons.slice(0, 3)) {
const box = await button.boundingBox();
if (!box || box.width < 8 || box.height < 8) continue;
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await sleep(150);
}
}
async function run() {
await fs.mkdir(runDir, { recursive: true });
const browser = await chromium.launch({
headless: true,
args: [
'--autoplay-policy=no-user-gesture-required',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
],
});
const context = await browser.newContext({
viewport: { width: viewport[0] || 1440, height: viewport[1] || 950 },
deviceScaleFactor: 1,
isMobile,
hasTouch: isMobile,
ignoreHTTPSErrors: true,
});
await context.addInitScript(() => {
window.__perfProbe = {
longTasks: [],
frames: [],
intervalDrifts: [],
};
try {
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
window.__perfProbe.longTasks.push({
name: entry.name,
startTime: entry.startTime,
duration: entry.duration,
});
}
}).observe({ type: 'longtask', buffered: true });
} catch {}
let lastFrame = performance.now();
const frameLoop = (now) => {
window.__perfProbe.frames.push(now - lastFrame);
lastFrame = now;
requestAnimationFrame(frameLoop);
};
requestAnimationFrame(frameLoop);
let lastTick = performance.now();
setInterval(() => {
const now = performance.now();
window.__perfProbe.intervalDrifts.push(now - lastTick - 250);
lastTick = now;
}, 250);
});
const page = await context.newPage();
const consoleMessages = [];
const failedRequests = [];
const responses = [];
page.on('console', (message) => {
consoleMessages.push({
type: message.type(),
text: message.text().slice(0, 1000),
location: message.location(),
});
});
page.on('requestfailed', (request) => {
failedRequests.push({
url: request.url(),
method: request.method(),
failure: request.failure()?.errorText,
});
});
page.on('response', (response) => {
const status = response.status();
if (status >= 400) responses.push({ url: response.url(), status });
});
const cdp = await context.newCDPSession(page);
await cdp.send('Performance.enable');
await cdp.send('Profiler.enable');
if (cpuThrottle > 1) {
await cdp.send('Emulation.setCPUThrottlingRate', { rate: cpuThrottle });
}
const start = Date.now();
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForLoadState('load', { timeout: 60000 }).catch(() => {});
await sleep(5000);
const afterLoadMetrics = interestingMetrics(await cdp.send('Performance.getMetrics'));
await page.screenshot({ path: path.join(runDir, 'loaded.png'), fullPage: true });
await cdp.send('Profiler.start');
await sleep(15000);
await performInteractions(page);
await sleep(15000);
const { profile } = await cdp.send('Profiler.stop');
const endMetrics = interestingMetrics(await cdp.send('Performance.getMetrics'));
const perfProbe = await page.evaluate(() => window.__perfProbe);
await page.screenshot({ path: path.join(runDir, 'after-profile.png'), fullPage: true });
const cpuSummary = summarizeCpuProfile(profile);
const frameDeltas = perfProbe.frames.slice(5);
const longTasks = perfProbe.longTasks.sort((a, b) => b.duration - a.duration);
const intervalDrifts = perfProbe.intervalDrifts;
const percentile = (values, p) => {
if (!values.length) return 0;
const sorted = [...values].sort((a, b) => a - b);
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))];
};
const report = {
url,
runDir,
elapsedMs: Date.now() - start,
userAgent: await page.evaluate(() => navigator.userAgent),
title: await page.title(),
finalUrl: page.url(),
environment: {
cpuThrottle,
viewport: { width: viewport[0] || 1440, height: viewport[1] || 950 },
isMobile,
},
metrics: {
afterLoad: afterLoadMetrics,
end: endMetrics,
delta: Object.fromEntries(
Object.keys(endMetrics).map((key) => [key, endMetrics[key] - (afterLoadMetrics[key] || 0)]),
),
},
cpuSummary,
responsiveness: {
longTaskCount: longTasks.length,
topLongTasks: longTasks.slice(0, 20),
frameCount: frameDeltas.length,
averageFrameMs: frameDeltas.reduce((sum, value) => sum + value, 0) / frameDeltas.length,
p95FrameMs: percentile(frameDeltas, 0.95),
p99FrameMs: percentile(frameDeltas, 0.99),
intervalDriftCount: intervalDrifts.length,
averageIntervalDriftMs:
intervalDrifts.reduce((sum, value) => sum + value, 0) / intervalDrifts.length,
p95IntervalDriftMs: percentile(intervalDrifts, 0.95),
p99IntervalDriftMs: percentile(intervalDrifts, 0.99),
},
consoleMessages,
failedRequests,
errorResponses: responses,
};
await fs.writeFile(path.join(runDir, 'cpu-profile.cpuprofile'), JSON.stringify(profile));
await fs.writeFile(path.join(runDir, 'report.json'), JSON.stringify(report, null, 2));
console.log(JSON.stringify(report, null, 2));
await browser.close();
}
run().catch((error) => {
console.error(error);
process.exitCode = 1;
});
+120
View File
@@ -0,0 +1,120 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { chromium } from 'playwright';
const url = process.argv[2] || 'https://rover.otter.land';
const outDir = process.argv[3] || path.resolve('perf/results');
const runId = `${new Date().toISOString().replace(/[:.]/g, '-')}-listeners`;
const runDir = path.join(outDir, runId);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function run() {
await fs.mkdir(runDir, { recursive: true });
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1440, height: 950 },
ignoreHTTPSErrors: true,
});
await context.addInitScript(() => {
const originalAdd = EventTarget.prototype.addEventListener;
const originalRemove = EventTarget.prototype.removeEventListener;
const listenerIds = new WeakMap();
let nextListenerId = 1;
const rows = new Map();
function listenerId(listener) {
if ((typeof listener !== 'function' && typeof listener !== 'object') || listener === null) {
return String(listener);
}
let id = listenerIds.get(listener);
if (!id) {
id = nextListenerId++;
listenerIds.set(listener, id);
}
return id;
}
function targetName(target) {
if (target === window) return 'window';
if (target === document) return 'document';
if (target?.nodeType === Node.DOCUMENT_NODE) return 'document';
if (target?.nodeType === Node.ELEMENT_NODE) {
const tag = target.tagName?.toLowerCase() || 'element';
const id = target.id ? `#${target.id}` : '';
const className = String(target.className || '').trim().split(/\s+/).slice(0, 3).join('.');
return `${tag}${id}${className ? `.${className}` : ''}`;
}
if (target?.constructor?.name) return target.constructor.name;
return String(target);
}
function shortStack() {
return String(new Error().stack || '')
.split('\n')
.slice(3, 9)
.map((line) => line.trim().replace(location.origin, ''))
.join('\n');
}
function note(kind, target, type, listener) {
const stack = shortStack();
const key = `${targetName(target)}::${type}::${stack}`;
const row = rows.get(key) || {
target: targetName(target),
type: String(type),
stack,
adds: 0,
removes: 0,
activeApprox: 0,
firstSeen: performance.now(),
lastSeen: 0,
};
row[kind] += 1;
row.activeApprox += kind === 'adds' ? 1 : -1;
row.lastSeen = performance.now();
row.exampleListener = String(listener).slice(0, 180);
rows.set(key, row);
}
EventTarget.prototype.addEventListener = function patchedAdd(type, listener, options) {
note('adds', this, type, listener);
return originalAdd.call(this, type, listener, options);
};
EventTarget.prototype.removeEventListener = function patchedRemove(type, listener, options) {
note('removes', this, type, listener);
return originalRemove.call(this, type, listener, options);
};
window.__listenerAudit = {
snapshot() {
return [...rows.values()]
.sort((a, b) => Math.abs(b.activeApprox) - Math.abs(a.activeApprox) || b.adds - a.adds)
.slice(0, 100);
},
};
});
const page = await context.newPage();
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForLoadState('load', { timeout: 60000 }).catch(() => {});
await sleep(10000);
const first = await page.evaluate(() => window.__listenerAudit.snapshot());
await page.mouse.wheel(0, 1500);
await sleep(1000);
await page.mouse.wheel(0, -900);
await sleep(20000);
const second = await page.evaluate(() => window.__listenerAudit.snapshot());
const report = { url, finalUrl: page.url(), first, second };
await fs.writeFile(path.join(runDir, 'listener-report.json'), JSON.stringify(report, null, 2));
await page.screenshot({ path: path.join(runDir, 'listener-audit.png'), fullPage: true });
console.log(JSON.stringify(report, null, 2));
await browser.close();
}
run().catch((error) => {
console.error(error);
process.exitCode = 1;
});
+253
View File
@@ -0,0 +1,253 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { chromium } from 'playwright';
const url = process.argv[2] || 'https://rover.otter.land/';
const outDir = process.argv[3] || path.resolve('perf/results');
const cpuThrottle = Number(process.env.CPU_THROTTLE || 6);
const viewport = (process.env.VIEWPORT || '390x844').split('x').map((part) => Number(part));
const isMobile = process.env.MOBILE !== '0';
const sampleMs = Number(process.env.SAMPLE_MS || 45000);
const runId = `${new Date().toISOString().replace(/[:.]/g, '-')}-root-runtime`;
const runDir = path.join(outDir, runId);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function percentile(values, p) {
if (!values.length) return 0;
const sorted = [...values].sort((a, b) => a - b);
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))];
}
async function main() {
await fs.mkdir(runDir, { recursive: true });
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: viewport[0] || 390, height: viewport[1] || 844 },
deviceScaleFactor: isMobile ? 3 : 1,
isMobile,
hasTouch: isMobile,
ignoreHTTPSErrors: true,
});
await context.addInitScript(() => {
const data = {
startedAt: performance.now(),
listeners: new Map(),
timers: new Map(),
timeouts: 0,
intervals: 0,
intervalFires: 0,
timeoutFires: 0,
webSocketMessages: [],
webSocketCounts: new Map(),
webSocketBytesByEvent: new Map(),
mutations: [],
mutationTargets: new Map(),
frames: [],
longTasks: [],
consoleErrors: [],
};
function localPercentile(values, p) {
if (!values.length) return 0;
const sorted = [...values].sort((a, b) => a - b);
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))];
}
function targetName(target) {
if (target === window) return 'window';
if (target === document) return 'document';
if (target?.nodeType === Node.ELEMENT_NODE) {
const tag = target.tagName?.toLowerCase() || 'element';
const id = target.id ? `#${target.id}` : '';
const classes = String(target.className || '').split(/\s+/).filter(Boolean).slice(0, 2).join('.');
return `${tag}${id}${classes ? `.${classes}` : ''}`;
}
return target?.constructor?.name || String(target);
}
function listenerKey(target, type, kind) {
return `${kind}:${targetName(target)}:${String(type)}`;
}
const originalAdd = EventTarget.prototype.addEventListener;
const originalRemove = EventTarget.prototype.removeEventListener;
EventTarget.prototype.addEventListener = function add(type, listener, options) {
const key = listenerKey(this, type, 'add');
data.listeners.set(key, (data.listeners.get(key) || 0) + 1);
return originalAdd.call(this, type, listener, options);
};
EventTarget.prototype.removeEventListener = function remove(type, listener, options) {
const key = listenerKey(this, type, 'remove');
data.listeners.set(key, (data.listeners.get(key) || 0) + 1);
return originalRemove.call(this, type, listener, options);
};
const originalSetInterval = window.setInterval;
const originalSetTimeout = window.setTimeout;
window.setInterval = function patchedSetInterval(fn, delay, ...args) {
data.intervals += 1;
const key = `interval:${delay}`;
data.timers.set(key, (data.timers.get(key) || 0) + 1);
return originalSetInterval.call(this, (...innerArgs) => {
data.intervalFires += 1;
return typeof fn === 'function' ? fn(...innerArgs) : eval(fn);
}, delay, ...args);
};
window.setTimeout = function patchedSetTimeout(fn, delay, ...args) {
data.timeouts += 1;
const key = `timeout:${delay}`;
data.timers.set(key, (data.timers.get(key) || 0) + 1);
return originalSetTimeout.call(this, (...innerArgs) => {
data.timeoutFires += 1;
return typeof fn === 'function' ? fn(...innerArgs) : eval(fn);
}, delay, ...args);
};
const OriginalWebSocket = window.WebSocket;
if (OriginalWebSocket) {
window.WebSocket = function PatchedWebSocket(...args) {
const ws = new OriginalWebSocket(...args);
ws.addEventListener('message', (event) => {
const raw = event.data;
const text = typeof raw === 'string' ? raw : '';
const match = text.slice(0, 120).match(/\["([^"\]]+)/);
const eventName = match?.[1] || text.slice(0, 12);
data.webSocketCounts.set(eventName, (data.webSocketCounts.get(eventName) || 0) + 1);
data.webSocketBytesByEvent.set(
eventName,
(data.webSocketBytesByEvent.get(eventName) || 0) + (typeof raw === 'string' ? raw.length : raw?.byteLength || raw?.size || 0),
);
data.webSocketMessages.push({
at: Math.round(performance.now()),
size: typeof raw === 'string' ? raw.length : raw?.byteLength || raw?.size || 0,
prefix: text.slice(0, 80),
});
if (data.webSocketMessages.length > 1000) data.webSocketMessages.shift();
});
return ws;
};
window.WebSocket.prototype = OriginalWebSocket.prototype;
Object.assign(window.WebSocket, OriginalWebSocket);
}
try {
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
data.longTasks.push({ startTime: entry.startTime, duration: entry.duration, name: entry.name });
}
}).observe({ type: 'longtask', buffered: true });
} catch {}
let lastFrame = performance.now();
requestAnimationFrame(function frame(now) {
data.frames.push(now - lastFrame);
lastFrame = now;
requestAnimationFrame(frame);
});
new MutationObserver((records) => {
let added = 0;
let removed = 0;
let attrs = 0;
let text = 0;
for (const record of records) {
added += record.addedNodes?.length || 0;
removed += record.removedNodes?.length || 0;
if (record.type === 'attributes') attrs += 1;
if (record.type === 'characterData') text += 1;
if (record.type === 'attributes' || record.type === 'characterData') {
const key = `${targetName(record.target)}:${record.type}:${record.attributeName || ''}`;
data.mutationTargets.set(key, (data.mutationTargets.get(key) || 0) + 1);
}
}
data.mutations.push({ at: Math.round(performance.now()), records: records.length, added, removed, attrs, text });
if (data.mutations.length > 1000) data.mutations.shift();
}).observe(document, {
subtree: true,
childList: true,
attributes: true,
characterData: true,
});
const originalConsoleError = console.error;
console.error = (...args) => {
data.consoleErrors.push(args.map((arg) => String(arg)).join(' ').slice(0, 500));
return originalConsoleError.apply(console, args);
};
window.__rootAudit = {
snapshot() {
const frames = data.frames.slice(5);
const messages = data.webSocketMessages;
const socketBuckets = new Map(
[...data.webSocketCounts.entries()].map(([event, count]) => [
event,
{ event, count, bytes: data.webSocketBytesByEvent.get(event) || 0 },
]),
);
return {
elapsedMs: performance.now() - data.startedAt,
listeners: [...data.listeners.entries()].map(([key, count]) => ({ key, count })).sort((a, b) => b.count - a.count).slice(0, 40),
timers: [...data.timers.entries()].map(([key, count]) => ({ key, count })).sort((a, b) => b.count - a.count).slice(0, 40),
timeouts: data.timeouts,
intervals: data.intervals,
timeoutFires: data.timeoutFires,
intervalFires: data.intervalFires,
webSocketMessageCount: messages.length,
webSocketBytes: messages.reduce((sum, message) => sum + message.size, 0),
webSocketEvents: [...socketBuckets.values()].sort((a, b) => b.count - a.count),
mutationsTotal: data.mutations.reduce((acc, row) => ({
records: acc.records + row.records,
added: acc.added + row.added,
removed: acc.removed + row.removed,
attrs: acc.attrs + row.attrs,
text: acc.text + row.text,
}), { records: 0, added: 0, removed: 0, attrs: 0, text: 0 }),
mutationTargets: [...data.mutationTargets.entries()]
.map(([key, count]) => ({ key, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 50),
mutationBursts: [...data.mutations].sort((a, b) => b.records - a.records).slice(0, 20),
frames: {
count: frames.length,
average: frames.reduce((sum, value) => sum + value, 0) / frames.length,
p95: localPercentile(frames, 0.95),
p99: localPercentile(frames, 0.99),
},
longTasks: {
count: data.longTasks.length,
top: [...data.longTasks].sort((a, b) => b.duration - a.duration).slice(0, 20),
},
consoleErrors: data.consoleErrors,
};
},
};
});
const page = await context.newPage();
const cdp = await context.newCDPSession(page);
await cdp.send('Performance.enable');
if (cpuThrottle > 1) await cdp.send('Emulation.setCPUThrottlingRate', { rate: cpuThrottle });
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 90000 });
await page.waitForLoadState('load', { timeout: 90000 }).catch(() => {});
await sleep(sampleMs);
const metrics = await cdp.send('Performance.getMetrics');
const snapshot = await page.evaluate(() => window.__rootAudit.snapshot());
const report = {
url,
runDir,
environment: { cpuThrottle, viewport: { width: viewport[0] || 390, height: viewport[1] || 844 }, isMobile, sampleMs },
metrics: Object.fromEntries(metrics.metrics.map((metric) => [metric.name, metric.value])),
snapshot,
};
await page.screenshot({ path: path.join(runDir, 'root-runtime.png'), fullPage: true });
await fs.writeFile(path.join(runDir, 'root-runtime-report.json'), JSON.stringify(report, null, 2));
console.log(JSON.stringify(report, null, 2));
await browser.close();
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
+284
View File
@@ -0,0 +1,284 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { chromium } from 'playwright';
const baseUrl = process.argv[2] || 'https://rover.otter.land';
const outDir = process.argv[3] || path.resolve('perf/results');
const cpuThrottle = Number(process.env.CPU_THROTTLE || 6);
const viewport = (process.env.VIEWPORT || '390x844').split('x').map((part) => Number(part));
const isMobile = process.env.MOBILE !== '0';
const sampleMs = Number(process.env.SAMPLE_MS || 22000);
const routes = (process.env.ROUTES || '/,/spectate,/mini,/display,/scanner')
.split(',')
.map((route) => route.trim())
.filter(Boolean);
const runId = `${new Date().toISOString().replace(/[:.]/g, '-')}-route-sweep`;
const runDir = path.join(outDir, runId);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function percentile(values, p) {
if (!values.length) return 0;
const sorted = [...values].sort((a, b) => a - b);
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))];
}
function interestingMetrics(metrics) {
const wanted = [
'Documents',
'JSEventListeners',
'Nodes',
'LayoutCount',
'RecalcStyleCount',
'LayoutDuration',
'RecalcStyleDuration',
'ScriptDuration',
'TaskDuration',
'JSHeapUsedSize',
'JSHeapTotalSize',
];
return Object.fromEntries(
metrics.metrics
.filter((metric) => wanted.includes(metric.name))
.map((metric) => [metric.name, metric.value]),
);
}
function summarizeCpuProfile(profile) {
const nodes = new Map(profile.nodes.map((node) => [node.id, node]));
const totals = new Map();
let totalUs = 0;
for (let i = 0; i < (profile.samples || []).length; i += 1) {
const delta = profile.timeDeltas?.[i] || 0;
totalUs += delta;
const frame = nodes.get(profile.samples[i])?.callFrame || {};
const key = `${frame.url || '(browser)'}::${frame.functionName || '(anonymous)'}:${frame.lineNumber}:${frame.columnNumber}`;
const row = totals.get(key) || {
url: frame.url || '',
functionName: frame.functionName || '(anonymous)',
lineNumber: frame.lineNumber,
columnNumber: frame.columnNumber,
selfMs: 0,
samples: 0,
};
row.selfMs += delta / 1000;
row.samples += 1;
totals.set(key, row);
}
return {
totalProfileMs: totalUs / 1000,
topSelfTime: [...totals.values()].sort((a, b) => b.selfMs - a.selfMs).slice(0, 15),
};
}
async function installProbe(context) {
await context.addInitScript(() => {
const originalAdd = EventTarget.prototype.addEventListener;
const originalRemove = EventTarget.prototype.removeEventListener;
const listenerRows = new Map();
function targetName(target) {
if (target === window) return 'window';
if (target === document) return 'document';
if (target?.nodeType === Node.ELEMENT_NODE) {
const tag = target.tagName?.toLowerCase() || 'element';
return target.id ? `${tag}#${target.id}` : tag;
}
return target?.constructor?.name || String(target);
}
function stack() {
return String(new Error().stack || '')
.split('\n')
.slice(3, 7)
.map((line) => line.trim().replace(location.origin, ''))
.join('\n');
}
function note(kind, target, type) {
const key = `${targetName(target)}::${type}::${stack()}`;
const row = listenerRows.get(key) || {
target: targetName(target),
type: String(type),
stack: stack(),
adds: 0,
removes: 0,
};
row[kind] += 1;
listenerRows.set(key, row);
}
EventTarget.prototype.addEventListener = function patchedAdd(type, listener, options) {
note('adds', this, type);
return originalAdd.call(this, type, listener, options);
};
EventTarget.prototype.removeEventListener = function patchedRemove(type, listener, options) {
note('removes', this, type);
return originalRemove.call(this, type, listener, options);
};
window.__sweepProbe = {
longTasks: [],
frames: [],
intervalDrifts: [],
listenerSnapshot() {
return [...listenerRows.values()]
.map((row) => ({ ...row, churn: row.adds + row.removes }))
.sort((a, b) => b.churn - a.churn)
.slice(0, 20);
},
};
try {
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
window.__sweepProbe.longTasks.push({
name: entry.name,
startTime: entry.startTime,
duration: entry.duration,
});
}
}).observe({ type: 'longtask', buffered: true });
} catch {}
let lastFrame = performance.now();
const frameLoop = (now) => {
window.__sweepProbe.frames.push(now - lastFrame);
lastFrame = now;
requestAnimationFrame(frameLoop);
};
requestAnimationFrame(frameLoop);
let lastTick = performance.now();
setInterval(() => {
const now = performance.now();
window.__sweepProbe.intervalDrifts.push(now - lastTick - 250);
lastTick = now;
}, 250);
});
}
async function runRoute(browser, route) {
const context = await browser.newContext({
viewport: { width: viewport[0] || 390, height: viewport[1] || 844 },
deviceScaleFactor: isMobile ? 3 : 1,
isMobile,
hasTouch: isMobile,
ignoreHTTPSErrors: true,
});
await installProbe(context);
const page = await context.newPage();
const cdp = await context.newCDPSession(page);
await cdp.send('Performance.enable');
await cdp.send('Profiler.enable');
if (cpuThrottle > 1) {
await cdp.send('Emulation.setCPUThrottlingRate', { rate: cpuThrottle });
}
const consoleErrors = [];
const errorResponses = [];
page.on('console', (message) => {
if (['error', 'warning'].includes(message.type())) {
consoleErrors.push({ type: message.type(), text: message.text().slice(0, 500) });
}
});
page.on('response', (response) => {
if (response.status() >= 400) {
errorResponses.push({ url: response.url(), status: response.status() });
}
});
const url = new URL(route, baseUrl).toString();
const startWall = Date.now();
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 90000 });
await page.waitForLoadState('load', { timeout: 90000 }).catch(() => {});
await sleep(4000);
const startMetrics = interestingMetrics(await cdp.send('Performance.getMetrics'));
await cdp.send('Profiler.start');
await sleep(sampleMs / 2);
await page.mouse.wheel(0, 900).catch(() => {});
await sleep(sampleMs / 2);
const { profile } = await cdp.send('Profiler.stop');
const endMetrics = interestingMetrics(await cdp.send('Performance.getMetrics'));
const probe = await page.evaluate(() => ({
longTasks: window.__sweepProbe.longTasks,
frames: window.__sweepProbe.frames.slice(5),
intervalDrifts: window.__sweepProbe.intervalDrifts,
listeners: window.__sweepProbe.listenerSnapshot(),
}));
const screenshotName = route === '/' ? 'root' : route.replace(/^\//, '').replace(/\W+/g, '-');
await page.screenshot({ path: path.join(runDir, `${screenshotName || 'root'}.png`), fullPage: true });
await context.close();
const delta = Object.fromEntries(
Object.keys(endMetrics).map((key) => [key, endMetrics[key] - (startMetrics[key] || 0)]),
);
const frames = probe.frames;
const longTasks = probe.longTasks.sort((a, b) => b.duration - a.duration);
return {
route,
finalUrl: page.url(),
elapsedMs: Date.now() - startWall,
metrics: { start: startMetrics, end: endMetrics, delta },
cpu: summarizeCpuProfile(profile),
responsiveness: {
longTaskCount: longTasks.length,
worstLongTaskMs: longTasks[0]?.duration || 0,
topLongTasks: longTasks.slice(0, 8),
frameCount: frames.length,
averageFrameMs: frames.reduce((sum, value) => sum + value, 0) / frames.length,
p95FrameMs: percentile(frames, 0.95),
p99FrameMs: percentile(frames, 0.99),
intervalDriftP95Ms: percentile(probe.intervalDrifts, 0.95),
intervalDriftP99Ms: percentile(probe.intervalDrifts, 0.99),
},
listenerChurn: probe.listeners,
consoleErrors,
errorResponses,
};
}
async function main() {
await fs.mkdir(runDir, { recursive: true });
const browser = await chromium.launch({
headless: true,
args: ['--autoplay-policy=no-user-gesture-required'],
});
const results = [];
for (const route of routes) {
console.error(`profiling ${route} at ${cpuThrottle}x`);
results.push(await runRoute(browser, route));
}
await browser.close();
const report = {
baseUrl,
runDir,
environment: {
cpuThrottle,
viewport: { width: viewport[0] || 390, height: viewport[1] || 844 },
isMobile,
sampleMs,
routes,
},
results,
rankedByAverageFrame: [...results].sort(
(a, b) => b.responsiveness.averageFrameMs - a.responsiveness.averageFrameMs,
),
rankedByTaskDuration: [...results].sort(
(a, b) => b.metrics.delta.TaskDuration - a.metrics.delta.TaskDuration,
),
rankedByListenerChurn: [...results].sort((a, b) => {
const churnA = a.listenerChurn.reduce((sum, row) => sum + row.churn, 0);
const churnB = b.listenerChurn.reduce((sum, row) => sum + row.churn, 0);
return churnB - churnA;
}),
};
await fs.writeFile(path.join(runDir, 'route-sweep-report.json'), JSON.stringify(report, null, 2));
console.log(JSON.stringify(report, null, 2));
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
+5
View File
@@ -0,0 +1,5 @@
{
"dependencies": {
"playwright": "^1.60.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

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

After

Width:  |  Height:  |  Size: 390 KiB

@@ -0,0 +1,334 @@
{
"url": "https://rover.otter.land",
"runDir": "perf/results/2026-06-11T03-02-07-015Z",
"elapsedMs": 38584,
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.7778.96 Safari/537.36",
"title": "Roomba Rover",
"finalUrl": "https://rover.otter.land/",
"metrics": {
"afterLoad": {
"Timestamp": 38634.714754,
"Documents": 1,
"Frames": 1,
"JSEventListeners": 66899,
"Nodes": 2977,
"LayoutCount": 89,
"RecalcStyleCount": 274,
"LayoutDuration": 0.106766,
"RecalcStyleDuration": 0.065778,
"ScriptDuration": 4.06233,
"TaskDuration": 4.933287,
"JSHeapUsedSize": 35455032,
"JSHeapTotalSize": 80216064
},
"end": {
"Timestamp": 38667.823166,
"Documents": 1,
"Frames": 1,
"JSEventListeners": 75714,
"Nodes": 2955,
"LayoutCount": 654,
"RecalcStyleCount": 2517,
"LayoutDuration": 0.32419,
"RecalcStyleDuration": 0.312982,
"ScriptDuration": 32.06036,
"TaskDuration": 37.597822,
"JSHeapUsedSize": 39685520,
"JSHeapTotalSize": 86081536
},
"delta": {
"Timestamp": 33.10841200000141,
"Documents": 0,
"Frames": 0,
"JSEventListeners": 8815,
"Nodes": -22,
"LayoutCount": 565,
"RecalcStyleCount": 2243,
"LayoutDuration": 0.21742399999999998,
"RecalcStyleDuration": 0.24720399999999998,
"ScriptDuration": 27.998030000000004,
"TaskDuration": 32.664535,
"JSHeapUsedSize": 4230488,
"JSHeapTotalSize": 5865472
}
},
"cpuSummary": {
"totalProfileMs": 32896.945,
"sampleCount": 30557,
"topSelfTime": [
{
"url": "",
"functionName": "(program)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 4657.831000000009,
"samples": 4273
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "C9",
"lineNumber": 8,
"columnNumber": 94304,
"selfMs": 2470.519000000005,
"samples": 2297
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "ad",
"lineNumber": 9,
"columnNumber": 7898,
"selfMs": 2383.6760000000036,
"samples": 2219
},
{
"url": "",
"functionName": "removeEventListener",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 1596.3859999999981,
"samples": 1476
},
{
"url": "",
"functionName": "addEventListener",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 1314.546999999998,
"samples": 1219
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "T9",
"lineNumber": 8,
"columnNumber": 115769,
"selfMs": 1280.09,
"samples": 1195
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Q",
"lineNumber": 8,
"columnNumber": 39681,
"selfMs": 1246.5730000000015,
"samples": 1160
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "va",
"lineNumber": 8,
"columnNumber": 10653,
"selfMs": 1170.771,
"samples": 1088
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Fl",
"lineNumber": 8,
"columnNumber": 101199,
"selfMs": 1028.4150000000006,
"samples": 954
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "n2",
"lineNumber": 8,
"columnNumber": 42234,
"selfMs": 1007.8280000000008,
"samples": 938
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "w1",
"lineNumber": 8,
"columnNumber": 40723,
"selfMs": 992.6080000000019,
"samples": 927
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "zl",
"lineNumber": 8,
"columnNumber": 80827,
"selfMs": 975.6390000000019,
"samples": 905
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "v9",
"lineNumber": 8,
"columnNumber": 71024,
"selfMs": 909.7280000000015,
"samples": 848
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "(anonymous)",
"lineNumber": 17,
"columnNumber": 5982,
"selfMs": 710.5740000000005,
"samples": 662
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Eh",
"lineNumber": 8,
"columnNumber": 95724,
"selfMs": 656.2710000000002,
"samples": 613
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "w4",
"lineNumber": 11,
"columnNumber": 91309,
"selfMs": 544.7980000000002,
"samples": 508
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Al",
"lineNumber": 8,
"columnNumber": 97450,
"selfMs": 485.71200000000005,
"samples": 453
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "kc",
"lineNumber": 11,
"columnNumber": 1388916,
"selfMs": 482.39700000000016,
"samples": 450
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "B6",
"lineNumber": 8,
"columnNumber": 34145,
"selfMs": 475.1790000000002,
"samples": 446
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "o1",
"lineNumber": 11,
"columnNumber": 70367,
"selfMs": 441.8229999999999,
"samples": 412
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "lt",
"lineNumber": 8,
"columnNumber": 7349,
"selfMs": 398.3480000000001,
"samples": 369
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "r",
"lineNumber": 1,
"columnNumber": 967,
"selfMs": 348.187,
"samples": 326
},
{
"url": "",
"functionName": "(idle)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 319.7700000000001,
"samples": 297
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "X",
"lineNumber": 8,
"columnNumber": 40176,
"selfMs": 283.33400000000006,
"samples": 264
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "a2",
"lineNumber": 9,
"columnNumber": 71,
"selfMs": 258.0969999999999,
"samples": 239
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "n9",
"lineNumber": 8,
"columnNumber": 56257,
"selfMs": 216.19699999999995,
"samples": 202
},
{
"url": "",
"functionName": "(garbage collector)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 214.13299999999995,
"samples": 195
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "t9",
"lineNumber": 8,
"columnNumber": 50605,
"selfMs": 207.45599999999985,
"samples": 194
},
{
"url": "",
"functionName": "now",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 202.39299999999992,
"samples": 189
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "po",
"lineNumber": 11,
"columnNumber": 214243,
"selfMs": 192.35299999999984,
"samples": 178
}
]
},
"responsiveness": {
"longTaskCount": 1,
"topLongTasks": [
{
"name": "self",
"startTime": 5330.30000000447,
"duration": 83
}
],
"frameCount": 2261,
"averageFrameMs": 16.83551525873507,
"p95FrameMs": 16.799999999999272,
"p99FrameMs": 16.80000000000291,
"intervalDriftCount": 152,
"averageIntervalDriftMs": 0.02039473680289168,
"p95IntervalDriftMs": 4.600000001490116,
"p99IntervalDriftMs": 23.399999998509884
},
"consoleMessages": [
{
"type": "info",
"text": "[socket] connecting to https://rover.otter.land",
"location": {
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"line": 11,
"column": 63768,
"lineNumber": 11,
"columnNumber": 63768
}
}
],
"failedRequests": [],
"errorResponses": []
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

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

After

Width:  |  Height:  |  Size: 328 KiB

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

After

Width:  |  Height:  |  Size: 360 KiB

@@ -0,0 +1,423 @@
{
"url": "https://rover.otter.land",
"runDir": "perf/results/2026-06-11T03-04-44-940Z",
"elapsedMs": 41919,
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.7778.96 Safari/537.36",
"title": "Roomba Rover",
"finalUrl": "https://rover.otter.land/",
"environment": {
"cpuThrottle": 4,
"viewport": {
"width": 1366,
"height": 768
},
"isMobile": false
},
"metrics": {
"afterLoad": {
"Timestamp": 38792.763829,
"Documents": 1,
"Frames": 1,
"JSEventListeners": 6177,
"Nodes": 2848,
"LayoutCount": 38,
"RecalcStyleCount": 43,
"LayoutDuration": 0.233029,
"RecalcStyleDuration": 0.13828,
"ScriptDuration": 3.172814,
"TaskDuration": 4.881969,
"JSHeapUsedSize": 17107224,
"JSHeapTotalSize": 28311552
},
"end": {
"Timestamp": 38828.522855,
"Documents": 1,
"Frames": 1,
"JSEventListeners": 3175,
"Nodes": 3046,
"LayoutCount": 767,
"RecalcStyleCount": 773,
"LayoutDuration": 1.403177,
"RecalcStyleDuration": 0.88412,
"ScriptDuration": 24.747626,
"TaskDuration": 39.759494,
"JSHeapUsedSize": 41718720,
"JSHeapTotalSize": 81625088
},
"delta": {
"Timestamp": 35.75902599999972,
"Documents": 0,
"Frames": 0,
"JSEventListeners": -3002,
"Nodes": 198,
"LayoutCount": 729,
"RecalcStyleCount": 730,
"LayoutDuration": 1.170148,
"RecalcStyleDuration": 0.7458400000000001,
"ScriptDuration": 21.574812,
"TaskDuration": 34.877525,
"JSHeapUsedSize": 24611496,
"JSHeapTotalSize": 53313536
}
},
"cpuSummary": {
"totalProfileMs": 34677.553,
"sampleCount": 31910,
"topSelfTime": [
{
"url": "",
"functionName": "(program)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 12142.741999999993,
"samples": 10968
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "C9",
"lineNumber": 8,
"columnNumber": 94304,
"selfMs": 1976.3040000000042,
"samples": 1842
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "ad",
"lineNumber": 9,
"columnNumber": 7898,
"selfMs": 1545.372000000002,
"samples": 1431
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "(anonymous)",
"lineNumber": 11,
"columnNumber": 1532780,
"selfMs": 974.0629999999992,
"samples": 908
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "v9",
"lineNumber": 8,
"columnNumber": 71024,
"selfMs": 844.4459999999995,
"samples": 805
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Q",
"lineNumber": 8,
"columnNumber": 39681,
"selfMs": 841.3410000000006,
"samples": 763
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Fl",
"lineNumber": 8,
"columnNumber": 101199,
"selfMs": 826.49,
"samples": 777
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "w1",
"lineNumber": 8,
"columnNumber": 40723,
"selfMs": 811.0100000000001,
"samples": 754
},
{
"url": "",
"functionName": "removeEventListener",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 811.0059999999984,
"samples": 723
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "va",
"lineNumber": 8,
"columnNumber": 10653,
"selfMs": 752.6890000000008,
"samples": 698
},
{
"url": "",
"functionName": "(idle)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 664.0250000000005,
"samples": 625
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "zl",
"lineNumber": 8,
"columnNumber": 80827,
"selfMs": 596.2229999999997,
"samples": 556
},
{
"url": "",
"functionName": "addEventListener",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 573.276,
"samples": 544
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Eh",
"lineNumber": 8,
"columnNumber": 95724,
"selfMs": 515.44,
"samples": 477
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Kl",
"lineNumber": 8,
"columnNumber": 117211,
"selfMs": 499.10000000000025,
"samples": 470
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Al",
"lineNumber": 8,
"columnNumber": 97450,
"selfMs": 497.787,
"samples": 453
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "n2",
"lineNumber": 8,
"columnNumber": 42234,
"selfMs": 408.08700000000005,
"samples": 381
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "o1",
"lineNumber": 11,
"columnNumber": 70367,
"selfMs": 381.6320000000001,
"samples": 359
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "r",
"lineNumber": 1,
"columnNumber": 967,
"selfMs": 345.4999999999999,
"samples": 328
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Oh",
"lineNumber": 8,
"columnNumber": 116246,
"selfMs": 334.56799999999987,
"samples": 313
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "a2",
"lineNumber": 9,
"columnNumber": 71,
"selfMs": 314.36699999999996,
"samples": 293
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "lt",
"lineNumber": 8,
"columnNumber": 7349,
"selfMs": 313.014,
"samples": 286
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Z4",
"lineNumber": 8,
"columnNumber": 28258,
"selfMs": 290.6389999999998,
"samples": 261
},
{
"url": "",
"functionName": "(garbage collector)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 236.61700000000016,
"samples": 210
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "B6",
"lineNumber": 8,
"columnNumber": 34145,
"selfMs": 229.56900000000002,
"samples": 213
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "t9",
"lineNumber": 8,
"columnNumber": 50605,
"selfMs": 208.06900000000002,
"samples": 191
},
{
"url": "",
"functionName": "Blob",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 179.771,
"samples": 158
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "w4",
"lineNumber": 11,
"columnNumber": 91309,
"selfMs": 147.32200000000006,
"samples": 132
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "n9",
"lineNumber": 8,
"columnNumber": 56257,
"selfMs": 135.37199999999999,
"samples": 125
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "ma",
"lineNumber": 8,
"columnNumber": 10401,
"selfMs": 129.19600000000003,
"samples": 118
}
]
},
"responsiveness": {
"longTaskCount": 14,
"topLongTasks": [
{
"name": "self",
"startTime": 193.60000000149012,
"duration": 184
},
{
"name": "self",
"startTime": 381.20000000298023,
"duration": 137
},
{
"name": "self",
"startTime": 23610.70000000298,
"duration": 131
},
{
"name": "self",
"startTime": 1045,
"duration": 130
},
{
"name": "self",
"startTime": 1972.2999999970198,
"duration": 107
},
{
"name": "self",
"startTime": 6042.70000000298,
"duration": 105
},
{
"name": "self",
"startTime": 1874.3999999985099,
"duration": 97
},
{
"name": "self",
"startTime": 2697.7000000029802,
"duration": 96
},
{
"name": "self",
"startTime": 518.2999999970198,
"duration": 81
},
{
"name": "self",
"startTime": 2618.5,
"duration": 79
},
{
"name": "self",
"startTime": 839.3999999985099,
"duration": 75
},
{
"name": "self",
"startTime": 974.1000000014901,
"duration": 70
},
{
"name": "self",
"startTime": 770.7999999970198,
"duration": 68
},
{
"name": "self",
"startTime": 39623.79999999702,
"duration": 54
}
],
"frameCount": 577,
"averageFrameMs": 69.98561525129982,
"p95FrameMs": 133.29999999999927,
"p99FrameMs": 166.70000000000027,
"intervalDriftCount": 159,
"averageIntervalDriftMs": 6.808805031455913,
"p95IntervalDriftMs": 83.19999999552965,
"p99IntervalDriftMs": 239.80000000447035
},
"consoleMessages": [
{
"type": "info",
"text": "[socket] connecting to https://rover.otter.land",
"location": {
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"line": 11,
"column": 63768,
"lineNumber": 11,
"columnNumber": 63768
}
},
{
"type": "error",
"text": "Failed to load resource: the server responded with a status of 404 ()",
"location": {
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
"line": 0,
"column": 0,
"lineNumber": 0,
"columnNumber": 0
}
}
],
"failedRequests": [],
"errorResponses": [
{
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
"status": 404
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

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

After

Width:  |  Height:  |  Size: 226 KiB

@@ -0,0 +1,403 @@
{
"url": "https://rover.otter.land",
"runDir": "perf/results/2026-06-11T03-05-35-042Z",
"elapsedMs": 41711,
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.7778.96 Safari/537.36",
"title": "Roomba Rover",
"finalUrl": "https://rover.otter.land/",
"environment": {
"cpuThrottle": 4,
"viewport": {
"width": 390,
"height": 844
},
"isMobile": true
},
"metrics": {
"afterLoad": {
"Timestamp": 38842.912408,
"Documents": 3,
"Frames": 1,
"JSEventListeners": 5426,
"Nodes": 2393,
"LayoutCount": 100,
"RecalcStyleCount": 106,
"LayoutDuration": 0.207151,
"RecalcStyleDuration": 0.170546,
"ScriptDuration": 3.283922,
"TaskDuration": 5.024573,
"JSHeapUsedSize": 12044476,
"JSHeapTotalSize": 27000832
},
"end": {
"Timestamp": 38878.342222,
"Documents": 1,
"Frames": 1,
"JSEventListeners": 40107,
"Nodes": 2536,
"LayoutCount": 1291,
"RecalcStyleCount": 1289,
"LayoutDuration": 1.04779,
"RecalcStyleDuration": 1.180594,
"ScriptDuration": 24.454767,
"TaskDuration": 39.786697,
"JSHeapUsedSize": 17640640,
"JSHeapTotalSize": 82411520
},
"delta": {
"Timestamp": 35.42981400000281,
"Documents": -2,
"Frames": 0,
"JSEventListeners": 34681,
"Nodes": 143,
"LayoutCount": 1191,
"RecalcStyleCount": 1183,
"LayoutDuration": 0.840639,
"RecalcStyleDuration": 1.0100479999999998,
"ScriptDuration": 21.170845,
"TaskDuration": 34.762124,
"JSHeapUsedSize": 5596164,
"JSHeapTotalSize": 55410688
}
},
"cpuSummary": {
"totalProfileMs": 34204.39,
"sampleCount": 31429,
"topSelfTime": [
{
"url": "",
"functionName": "(program)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 12727.823999999955,
"samples": 11515
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "C9",
"lineNumber": 8,
"columnNumber": 94304,
"selfMs": 1738.6469999999997,
"samples": 1612
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "ad",
"lineNumber": 9,
"columnNumber": 7898,
"selfMs": 1547.8840000000005,
"samples": 1425
},
{
"url": "",
"functionName": "removeEventListener",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 1048.3049999999994,
"samples": 922
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Fl",
"lineNumber": 8,
"columnNumber": 101199,
"selfMs": 1013.1350000000006,
"samples": 958
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "w1",
"lineNumber": 8,
"columnNumber": 40723,
"selfMs": 846.9869999999997,
"samples": 777
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "va",
"lineNumber": 8,
"columnNumber": 10653,
"selfMs": 826.692,
"samples": 756
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Q",
"lineNumber": 8,
"columnNumber": 39681,
"selfMs": 813.2879999999994,
"samples": 744
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "v9",
"lineNumber": 8,
"columnNumber": 71024,
"selfMs": 764.6159999999996,
"samples": 716
},
{
"url": "",
"functionName": "addEventListener",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 711.6550000000001,
"samples": 655
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "(anonymous)",
"lineNumber": 17,
"columnNumber": 9774,
"selfMs": 643.1059999999997,
"samples": 593
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Eh",
"lineNumber": 8,
"columnNumber": 95724,
"selfMs": 609.4899999999997,
"samples": 573
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "zl",
"lineNumber": 8,
"columnNumber": 80827,
"selfMs": 558.8860000000001,
"samples": 525
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Kl",
"lineNumber": 8,
"columnNumber": 117211,
"selfMs": 555.4119999999999,
"samples": 516
},
{
"url": "",
"functionName": "(idle)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 476.40799999999973,
"samples": 445
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Al",
"lineNumber": 8,
"columnNumber": 97450,
"selfMs": 392.31999999999965,
"samples": 400
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "kc",
"lineNumber": 11,
"columnNumber": 1388916,
"selfMs": 363.94399999999945,
"samples": 336
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "n2",
"lineNumber": 8,
"columnNumber": 42234,
"selfMs": 356.98299999999995,
"samples": 334
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Z4",
"lineNumber": 8,
"columnNumber": 28258,
"selfMs": 322.32900000000024,
"samples": 293
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "lt",
"lineNumber": 8,
"columnNumber": 7349,
"selfMs": 291.17899999999986,
"samples": 268
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "o1",
"lineNumber": 11,
"columnNumber": 70367,
"selfMs": 269.2040000000001,
"samples": 257
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "B6",
"lineNumber": 8,
"columnNumber": 34145,
"selfMs": 248.9579999999999,
"samples": 228
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "r",
"lineNumber": 1,
"columnNumber": 967,
"selfMs": 246.97999999999996,
"samples": 223
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Oh",
"lineNumber": 8,
"columnNumber": 116246,
"selfMs": 239.94800000000004,
"samples": 223
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "a2",
"lineNumber": 9,
"columnNumber": 71,
"selfMs": 219.92599999999987,
"samples": 209
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "t9",
"lineNumber": 8,
"columnNumber": 50605,
"selfMs": 170.78300000000004,
"samples": 157
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "N7",
"lineNumber": 11,
"columnNumber": 100487,
"selfMs": 150.48000000000005,
"samples": 144
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "w4",
"lineNumber": 11,
"columnNumber": 91309,
"selfMs": 144.64899999999994,
"samples": 138
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Td.k1.createElement",
"lineNumber": 1,
"columnNumber": 6938,
"selfMs": 138.19599999999997,
"samples": 132
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "n9",
"lineNumber": 8,
"columnNumber": 56257,
"selfMs": 135.858,
"samples": 128
}
]
},
"responsiveness": {
"longTaskCount": 10,
"topLongTasks": [
{
"name": "unknown",
"startTime": 5777.89999999851,
"duration": 234
},
{
"name": "self",
"startTime": 185,
"duration": 188
},
{
"name": "self",
"startTime": 23461.59999999404,
"duration": 179
},
{
"name": "self",
"startTime": 957.7999999970198,
"duration": 111
},
{
"name": "self",
"startTime": 376.59999999403954,
"duration": 106
},
{
"name": "self",
"startTime": 1511.7999999970198,
"duration": 83
},
{
"name": "self",
"startTime": 696.7999999970198,
"duration": 73
},
{
"name": "self",
"startTime": 770.7999999970198,
"duration": 69
},
{
"name": "self",
"startTime": 482.59999999403954,
"duration": 67
},
{
"name": "self",
"startTime": 892.6999999955297,
"duration": 64
}
],
"frameCount": 1031,
"averageFrameMs": 38.92502424830261,
"p95FrameMs": 66.70000000000073,
"p99FrameMs": 133.3000000000029,
"intervalDriftCount": 157,
"averageIntervalDriftMs": 8.188535031856626,
"p95IntervalDriftMs": 62.100000001490116,
"p99IntervalDriftMs": 431.09999999403954
},
"consoleMessages": [
{
"type": "info",
"text": "[socket] connecting to https://rover.otter.land",
"location": {
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"line": 11,
"column": 63768,
"lineNumber": 11,
"columnNumber": 63768
}
},
{
"type": "error",
"text": "Failed to load resource: the server responded with a status of 404 ()",
"location": {
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
"line": 0,
"column": 0,
"lineNumber": 0,
"columnNumber": 0
}
}
],
"failedRequests": [],
"errorResponses": [
{
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
"status": 404
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

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

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

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

After

Width:  |  Height:  |  Size: 1.2 MiB

@@ -0,0 +1,527 @@
{
"url": "https://rover.otter.land/spectate",
"runDir": "perf/results/2026-06-11T03-14-34-168Z",
"elapsedMs": 64190,
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.7778.96 Safari/537.36",
"title": "Roomba Rover",
"finalUrl": "https://rover.otter.land/spectate",
"environment": {
"cpuThrottle": 8,
"viewport": {
"width": 390,
"height": 844
},
"isMobile": true
},
"metrics": {
"afterLoad": {
"Timestamp": 39382.268457,
"Documents": 3,
"Frames": 1,
"JSEventListeners": 494,
"Nodes": 8431,
"LayoutCount": 16,
"RecalcStyleCount": 18,
"LayoutDuration": 0.410692,
"RecalcStyleDuration": 0.442447,
"ScriptDuration": 2.378027,
"TaskDuration": 5.043961,
"JSHeapUsedSize": 14710548,
"JSHeapTotalSize": 20447232
},
"end": {
"Timestamp": 39435.115434,
"Documents": 1,
"Frames": 1,
"JSEventListeners": 509,
"Nodes": 12246,
"LayoutCount": 532,
"RecalcStyleCount": 540,
"LayoutDuration": 2.892458,
"RecalcStyleDuration": 6.325277,
"ScriptDuration": 21.960492,
"TaskDuration": 54.170961,
"JSHeapUsedSize": 14649044,
"JSHeapTotalSize": 65634304
},
"delta": {
"Timestamp": 52.84697700000106,
"Documents": -2,
"Frames": 0,
"JSEventListeners": 15,
"Nodes": 3815,
"LayoutCount": 516,
"RecalcStyleCount": 522,
"LayoutDuration": 2.481766,
"RecalcStyleDuration": 5.88283,
"ScriptDuration": 19.582465,
"TaskDuration": 49.126999999999995,
"JSHeapUsedSize": -61504,
"JSHeapTotalSize": 45187072
}
},
"cpuSummary": {
"totalProfileMs": 41344.156,
"sampleCount": 31576,
"topSelfTime": [
{
"url": "",
"functionName": "(program)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 21236.87599999999,
"samples": 16120
},
{
"url": "",
"functionName": "(idle)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 2981.150999999998,
"samples": 2232
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "(anonymous)",
"lineNumber": 11,
"columnNumber": 64833,
"selfMs": 1365.8530000000014,
"samples": 1053
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "C9",
"lineNumber": 8,
"columnNumber": 94304,
"selfMs": 1234.6300000000003,
"samples": 936
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "ad",
"lineNumber": 9,
"columnNumber": 7898,
"selfMs": 842.8760000000004,
"samples": 664
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Fl",
"lineNumber": 8,
"columnNumber": 101199,
"selfMs": 674.3050000000004,
"samples": 527
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "w1",
"lineNumber": 8,
"columnNumber": 40723,
"selfMs": 621.5390000000002,
"samples": 475
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Q",
"lineNumber": 8,
"columnNumber": 39681,
"selfMs": 505.4100000000001,
"samples": 396
},
{
"url": "",
"functionName": "Blob",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 479.50199999999967,
"samples": 331
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Eh",
"lineNumber": 8,
"columnNumber": 95724,
"selfMs": 423.5530000000003,
"samples": 343
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "decodeString",
"lineNumber": 11,
"columnNumber": 48609,
"selfMs": 423.2989999999997,
"samples": 328
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "B6",
"lineNumber": 8,
"columnNumber": 34145,
"selfMs": 404.793,
"samples": 311
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Kl",
"lineNumber": 8,
"columnNumber": 117211,
"selfMs": 398.8710000000001,
"samples": 291
},
{
"url": "",
"functionName": "setAttribute",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 384.0259999999998,
"samples": 319
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "zl",
"lineNumber": 8,
"columnNumber": 80827,
"selfMs": 372.5609999999999,
"samples": 298
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Al",
"lineNumber": 8,
"columnNumber": 97450,
"selfMs": 370.4030000000001,
"samples": 280
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "n2",
"lineNumber": 8,
"columnNumber": 42234,
"selfMs": 356.20099999999996,
"samples": 280
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Kt",
"lineNumber": 8,
"columnNumber": 48095,
"selfMs": 303.89100000000013,
"samples": 222
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "va",
"lineNumber": 8,
"columnNumber": 10653,
"selfMs": 289.63100000000003,
"samples": 227
},
{
"url": "",
"functionName": "(garbage collector)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 257.03600000000006,
"samples": 183
},
{
"url": "",
"functionName": "createObjectURL",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 223.09499999999994,
"samples": 181
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "r",
"lineNumber": 1,
"columnNumber": 967,
"selfMs": 212.97800000000012,
"samples": 168
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "w4",
"lineNumber": 11,
"columnNumber": 91309,
"selfMs": 202.98600000000016,
"samples": 154
},
{
"url": "",
"functionName": "removeChild",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 197.55400000000006,
"samples": 144
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "v9",
"lineNumber": 8,
"columnNumber": 71024,
"selfMs": 175.75600000000009,
"samples": 132
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "X",
"lineNumber": 8,
"columnNumber": 40176,
"selfMs": 166.36100000000005,
"samples": 136
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "o1",
"lineNumber": 11,
"columnNumber": 70367,
"selfMs": 162.70300000000003,
"samples": 132
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "V",
"lineNumber": 8,
"columnNumber": 38470,
"selfMs": 159.72000000000003,
"samples": 124
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Oh",
"lineNumber": 8,
"columnNumber": 116246,
"selfMs": 151.22299999999993,
"samples": 109
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "V8",
"lineNumber": 11,
"columnNumber": 1487297,
"selfMs": 147.01899999999992,
"samples": 113
}
]
},
"responsiveness": {
"longTaskCount": 322,
"topLongTasks": [
{
"name": "unknown",
"startTime": 12297.80000000447,
"duration": 518
},
{
"name": "self",
"startTime": 250.39999999850988,
"duration": 401
},
{
"name": "unknown",
"startTime": 12929.20000000298,
"duration": 308
},
{
"name": "self",
"startTime": 1907.3999999985099,
"duration": 274
},
{
"name": "self",
"startTime": 36069.80000000447,
"duration": 230
},
{
"name": "self",
"startTime": 2530.89999999851,
"duration": 195
},
{
"name": "self",
"startTime": 54969,
"duration": 190
},
{
"name": "self",
"startTime": 55840.80000000447,
"duration": 189
},
{
"name": "self",
"startTime": 24437.39999999851,
"duration": 188
},
{
"name": "self",
"startTime": 47105.5,
"duration": 172
},
{
"name": "self",
"startTime": 1736,
"duration": 170
},
{
"name": "self",
"startTime": 2367.60000000149,
"duration": 163
},
{
"name": "self",
"startTime": 1320.3000000044703,
"duration": 161
},
{
"name": "self",
"startTime": 659.6000000014901,
"duration": 159
},
{
"name": "self",
"startTime": 30711.60000000149,
"duration": 146
},
{
"name": "self",
"startTime": 54631.5,
"duration": 146
},
{
"name": "self",
"startTime": 3079,
"duration": 139
},
{
"name": "self",
"startTime": 819.2000000029802,
"duration": 136
},
{
"name": "self",
"startTime": 42828.70000000298,
"duration": 132
},
{
"name": "self",
"startTime": 1482.2000000029802,
"duration": 129
}
],
"frameCount": 460,
"averageFrameMs": 125.46608695652174,
"p95FrameMs": 233.39999999999782,
"p99FrameMs": 433.3000000000011,
"intervalDriftCount": 78,
"averageIntervalDriftMs": 502.9153846153655,
"p95IntervalDriftMs": 1530.1999999955297,
"p99IntervalDriftMs": 2415.60000000149
},
"consoleMessages": [
{
"type": "info",
"text": "[socket] connecting to https://rover.otter.land",
"location": {
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"line": 11,
"column": 63768,
"lineNumber": 11,
"columnNumber": 63768
}
},
{
"type": "error",
"text": "Failed to load resource: the server responded with a status of 404 ()",
"location": {
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
"line": 0,
"column": 0,
"lineNumber": 0,
"columnNumber": 0
}
},
{
"type": "error",
"text": "Failed to load resource: the server responded with a status of 404 ()",
"location": {
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
"line": 0,
"column": 0,
"lineNumber": 0,
"columnNumber": 0
}
},
{
"type": "error",
"text": "Failed to enter spectator mode Error: Spectator role required\n at lo.<anonymous> (https://rover.otter.land/assets/index-l3JqdlyF.js:12:64487)\n at lo.onack (https://rover.otter.land/assets/index-l3JqdlyF.js:12:55937)\n at lo.onpacket (https://rover.otter.land/assets/index-l3JqdlyF.js:12:55148)\n at V2.emit (https://rover.otter.land/assets/index-l3JqdlyF.js:12:25579)\n at https://rover.otter.land/assets/index-l3JqdlyF.js:12:61163",
"location": {
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"line": 125,
"column": 14862,
"lineNumber": 125,
"columnNumber": 14862
}
},
{
"type": "error",
"text": "Failed to enter spectator mode Error: Spectator role required\n at lo.<anonymous> (https://rover.otter.land/assets/index-l3JqdlyF.js:12:64487)\n at lo.onack (https://rover.otter.land/assets/index-l3JqdlyF.js:12:55937)\n at lo.onpacket (https://rover.otter.land/assets/index-l3JqdlyF.js:12:55148)\n at V2.emit (https://rover.otter.land/assets/index-l3JqdlyF.js:12:25579)\n at https://rover.otter.land/assets/index-l3JqdlyF.js:12:61163",
"location": {
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"line": 125,
"column": 14862,
"lineNumber": 125,
"columnNumber": 14862
}
},
{
"type": "error",
"text": "Failed to load resource: the server responded with a status of 404 ()",
"location": {
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
"line": 0,
"column": 0,
"lineNumber": 0,
"columnNumber": 0
}
},
{
"type": "error",
"text": "Failed to enter spectator mode Error: Spectator role required\n at lo.<anonymous> (https://rover.otter.land/assets/index-l3JqdlyF.js:12:64487)\n at lo.onack (https://rover.otter.land/assets/index-l3JqdlyF.js:12:55937)\n at lo.onpacket (https://rover.otter.land/assets/index-l3JqdlyF.js:12:55148)\n at V2.emit (https://rover.otter.land/assets/index-l3JqdlyF.js:12:25579)\n at https://rover.otter.land/assets/index-l3JqdlyF.js:12:61163",
"location": {
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"line": 125,
"column": 14862,
"lineNumber": 125,
"columnNumber": 14862
}
},
{
"type": "error",
"text": "Failed to enter spectator mode Error: Spectator role required\n at lo.<anonymous> (https://rover.otter.land/assets/index-l3JqdlyF.js:12:64487)\n at lo.onack (https://rover.otter.land/assets/index-l3JqdlyF.js:12:55937)\n at lo.onpacket (https://rover.otter.land/assets/index-l3JqdlyF.js:12:55148)\n at V2.emit (https://rover.otter.land/assets/index-l3JqdlyF.js:12:25579)\n at https://rover.otter.land/assets/index-l3JqdlyF.js:12:61163",
"location": {
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"line": 125,
"column": 14862,
"lineNumber": 125,
"columnNumber": 14862
}
}
],
"failedRequests": [],
"errorResponses": [
{
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
"status": 404
},
{
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
"status": 404
},
{
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
"status": 404
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

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

After

Width:  |  Height:  |  Size: 221 KiB

@@ -0,0 +1,483 @@
{
"url": "https://rover.otter.land/",
"runDir": "perf/results/2026-06-11T03-15-51-398Z",
"elapsedMs": 68429,
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.7778.96 Safari/537.36",
"title": "Roomba Rover",
"finalUrl": "https://rover.otter.land/",
"environment": {
"cpuThrottle": 8,
"viewport": {
"width": 390,
"height": 844
},
"isMobile": true
},
"metrics": {
"afterLoad": {
"Timestamp": 39459.436441,
"Documents": 1,
"Frames": 1,
"JSEventListeners": 964,
"Nodes": 2332,
"LayoutCount": 17,
"RecalcStyleCount": 19,
"LayoutDuration": 0.26351,
"RecalcStyleDuration": 0.176918,
"ScriptDuration": 3.214383,
"TaskDuration": 5.084233,
"JSHeapUsedSize": 8964616,
"JSHeapTotalSize": 20971520
},
"end": {
"Timestamp": 39514.079576,
"Documents": 1,
"Frames": 1,
"JSEventListeners": 14445,
"Nodes": 2446,
"LayoutCount": 390,
"RecalcStyleCount": 565,
"LayoutDuration": 1.338259,
"RecalcStyleDuration": 1.340215,
"ScriptDuration": 39.06709,
"TaskDuration": 57.967635,
"JSHeapUsedSize": 32831868,
"JSHeapTotalSize": 80576512
},
"delta": {
"Timestamp": 54.64313499999844,
"Documents": 0,
"Frames": 0,
"JSEventListeners": 13481,
"Nodes": 114,
"LayoutCount": 373,
"RecalcStyleCount": 546,
"LayoutDuration": 1.0747490000000002,
"RecalcStyleDuration": 1.163297,
"ScriptDuration": 35.852707,
"TaskDuration": 52.883402000000004,
"JSHeapUsedSize": 23867252,
"JSHeapTotalSize": 59604992
}
},
"cpuSummary": {
"totalProfileMs": 42872.299,
"sampleCount": 32614,
"topSelfTime": [
{
"url": "",
"functionName": "(program)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 11770.136000000031,
"samples": 8654
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "C9",
"lineNumber": 8,
"columnNumber": 94304,
"selfMs": 2390.596999999998,
"samples": 1844
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "ad",
"lineNumber": 9,
"columnNumber": 7898,
"selfMs": 2379.0989999999983,
"samples": 1867
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "(anonymous)",
"lineNumber": 8,
"columnNumber": 129074,
"selfMs": 1738.0870000000016,
"samples": 1347
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Fl",
"lineNumber": 8,
"columnNumber": 101199,
"selfMs": 1436.2390000000005,
"samples": 1104
},
{
"url": "",
"functionName": "removeEventListener",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 1298.9879999999996,
"samples": 915
},
{
"url": "",
"functionName": "(idle)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 1226.9839999999986,
"samples": 919
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "v9",
"lineNumber": 8,
"columnNumber": 71024,
"selfMs": 1131.7309999999986,
"samples": 880
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Q",
"lineNumber": 8,
"columnNumber": 39681,
"selfMs": 1085.1430000000012,
"samples": 833
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "va",
"lineNumber": 8,
"columnNumber": 10653,
"selfMs": 1036.2670000000012,
"samples": 808
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "w1",
"lineNumber": 8,
"columnNumber": 40723,
"selfMs": 1021.851000000001,
"samples": 794
},
{
"url": "",
"functionName": "addEventListener",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 935.4199999999993,
"samples": 733
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "zl",
"lineNumber": 8,
"columnNumber": 80827,
"selfMs": 898.5989999999993,
"samples": 684
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Eh",
"lineNumber": 8,
"columnNumber": 95724,
"selfMs": 875.6580000000007,
"samples": 685
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Al",
"lineNumber": 8,
"columnNumber": 97450,
"selfMs": 826.9259999999988,
"samples": 630
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Kl",
"lineNumber": 8,
"columnNumber": 117211,
"selfMs": 710.8159999999998,
"samples": 538
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "n2",
"lineNumber": 8,
"columnNumber": 42234,
"selfMs": 586.3330000000001,
"samples": 449
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "kc",
"lineNumber": 11,
"columnNumber": 1388916,
"selfMs": 431.253,
"samples": 337
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Z4",
"lineNumber": 8,
"columnNumber": 28258,
"selfMs": 429.435,
"samples": 323
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "o1",
"lineNumber": 11,
"columnNumber": 70367,
"selfMs": 426.5330000000003,
"samples": 321
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "Oh",
"lineNumber": 8,
"columnNumber": 116246,
"selfMs": 376.3850000000001,
"samples": 289
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "decodeString",
"lineNumber": 11,
"columnNumber": 48609,
"selfMs": 349.87800000000004,
"samples": 266
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "B6",
"lineNumber": 8,
"columnNumber": 34145,
"selfMs": 310.59499999999986,
"samples": 239
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "r",
"lineNumber": 1,
"columnNumber": 967,
"selfMs": 293.97700000000003,
"samples": 230
},
{
"url": "",
"functionName": "(garbage collector)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 260.9860000000001,
"samples": 178
},
{
"url": "",
"functionName": "setAttribute",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 234.46500000000006,
"samples": 165
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "n9",
"lineNumber": 8,
"columnNumber": 56257,
"selfMs": 221.65700000000007,
"samples": 163
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "t9",
"lineNumber": 8,
"columnNumber": 50605,
"selfMs": 209.57700000000006,
"samples": 175
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "ma",
"lineNumber": 8,
"columnNumber": 10401,
"selfMs": 205.84599999999998,
"samples": 161
},
{
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"functionName": "w4",
"lineNumber": 11,
"columnNumber": 91309,
"selfMs": 181.84999999999997,
"samples": 140
}
]
},
"responsiveness": {
"longTaskCount": 67,
"topLongTasks": [
{
"name": "unknown",
"startTime": 12851.70000000298,
"duration": 419
},
{
"name": "self",
"startTime": 224.70000000298023,
"duration": 369
},
{
"name": "self",
"startTime": 1742.9000000059605,
"duration": 229
},
{
"name": "self",
"startTime": 601.4000000059605,
"duration": 205
},
{
"name": "self",
"startTime": 3460.2000000029802,
"duration": 203
},
{
"name": "self",
"startTime": 36108.80000000447,
"duration": 202
},
{
"name": "self",
"startTime": 3289.8000000044703,
"duration": 170
},
{
"name": "self",
"startTime": 1209.8000000044703,
"duration": 143
},
{
"name": "self",
"startTime": 1605.5,
"duration": 137
},
{
"name": "self",
"startTime": 1353.3000000044703,
"duration": 123
},
{
"name": "self",
"startTime": 4036.60000000149,
"duration": 110
},
{
"name": "self",
"startTime": 31583.70000000298,
"duration": 107
},
{
"name": "self",
"startTime": 807.1000000014901,
"duration": 106
},
{
"name": "unknown",
"startTime": 13343,
"duration": 100
},
{
"name": "self",
"startTime": 24534.20000000298,
"duration": 100
},
{
"name": "self",
"startTime": 30488.30000000447,
"duration": 97
},
{
"name": "self",
"startTime": 21806.40000000596,
"duration": 89
},
{
"name": "self",
"startTime": 55545.5,
"duration": 87
},
{
"name": "self",
"startTime": 36721.70000000298,
"duration": 82
},
{
"name": "self",
"startTime": 44980.20000000298,
"duration": 79
}
],
"frameCount": 427,
"averageFrameMs": 140.080093676815,
"p95FrameMs": 166.79999999999563,
"p99FrameMs": 383.4000000000001,
"intervalDriftCount": 58,
"averageIntervalDriftMs": 790.1465517241379,
"p95IntervalDriftMs": 1347.1999999955297,
"p99IntervalDriftMs": 1620.7999999970198
},
"consoleMessages": [
{
"type": "info",
"text": "[socket] connecting to https://rover.otter.land",
"location": {
"url": "https://rover.otter.land/assets/index-l3JqdlyF.js",
"line": 11,
"column": 63768,
"lineNumber": 11,
"columnNumber": 63768
}
},
{
"type": "error",
"text": "Failed to load resource: the server responded with a status of 404 ()",
"location": {
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
"line": 0,
"column": 0,
"lineNumber": 0,
"columnNumber": 0
}
},
{
"type": "error",
"text": "Failed to load resource: the server responded with a status of 404 ()",
"location": {
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
"line": 0,
"column": 0,
"lineNumber": 0,
"columnNumber": 0
}
},
{
"type": "error",
"text": "Failed to load resource: the server responded with a status of 404 ()",
"location": {
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
"line": 0,
"column": 0,
"lineNumber": 0,
"columnNumber": 0
}
}
],
"failedRequests": [],
"errorResponses": [
{
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
"status": 404
},
{
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
"status": 404
},
{
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
"status": 404
}
]
}
@@ -0,0 +1,581 @@
{
"url": "https://rover.otter.land/",
"runDir": "perf/results/2026-06-11T03-21-51-263Z-root-runtime",
"environment": {
"cpuThrottle": 6,
"viewport": {
"width": 390,
"height": 844
},
"isMobile": true,
"sampleMs": 30000
},
"metrics": {
"Timestamp": 39844.241947,
"AudioHandlers": 0,
"AudioWorkletProcessors": 0,
"Documents": 1,
"Frames": 1,
"JSEventListeners": 15871,
"LayoutObjects": 1938,
"MediaKeySessions": 0,
"MediaKeys": 0,
"Nodes": 2430,
"Resources": 13,
"ContextLifecycleStateObservers": 12,
"V8PerContextDatas": 2,
"WorkerGlobalScopes": 0,
"UACSSResources": 0,
"RTCPeerConnections": 2,
"ResourceFetchers": 1,
"AdSubframes": 0,
"DetachedScriptStates": 2,
"ArrayBufferContents": 3,
"LayoutCount": 297,
"RecalcStyleCount": 320,
"LayoutDuration": 0.596739,
"RecalcStyleDuration": 0.63084,
"DevToolsCommandDuration": 0.000137,
"ScriptDuration": 20.110942,
"V8CompileDuration": 0,
"TaskDuration": 29.506741,
"TaskOtherDuration": 8.168083,
"ThreadTime": 30.245205,
"ProcessTime": 35.2,
"JSHeapUsedSize": 18751188,
"JSHeapTotalSize": 45088768,
"FirstMeaningfulPaint": 0,
"DomContentLoaded": 39814.213839,
"NavigationStart": 39813.701422
},
"snapshot": {
"elapsedMs": 30586.79999999702,
"listeners": [
{
"key": "add:window:keydown",
"count": 770
},
{
"key": "add:window:keyup",
"count": 770
},
{
"key": "add:window:blur",
"count": 770
},
{
"key": "add:window:gamepadconnected",
"count": 770
},
{
"key": "add:window:gamepaddisconnected",
"count": 770
},
{
"key": "remove:window:keydown",
"count": 769
},
{
"key": "remove:window:keyup",
"count": 769
},
{
"key": "remove:window:blur",
"count": 769
},
{
"key": "remove:window:gamepadconnected",
"count": 769
},
{
"key": "remove:window:gamepaddisconnected",
"count": 769
},
{
"key": "add:input:invalid",
"count": 18
},
{
"key": "add:window:online",
"count": 4
},
{
"key": "add:document:visibilitychange",
"count": 4
},
{
"key": "remove:window:online",
"count": 3
},
{
"key": "remove:document:visibilitychange",
"count": 3
},
{
"key": "add:audio:canplay",
"count": 3
},
{
"key": "add:audio:ended",
"count": 3
},
{
"key": "add:audio:error",
"count": 3
},
{
"key": "add:audio:pause",
"count": 3
},
{
"key": "add:audio:play",
"count": 3
},
{
"key": "add:audio:stalled",
"count": 3
},
{
"key": "add:audio:waiting",
"count": 3
},
{
"key": "add:div#root:auxclick",
"count": 2
},
{
"key": "add:div#root:click",
"count": 2
},
{
"key": "add:div#root:contextmenu",
"count": 2
},
{
"key": "add:div#root:copy",
"count": 2
},
{
"key": "add:div#root:cut",
"count": 2
},
{
"key": "add:div#root:drag",
"count": 2
},
{
"key": "add:div#root:dragend",
"count": 2
},
{
"key": "add:div#root:dragenter",
"count": 2
},
{
"key": "add:div#root:dragexit",
"count": 2
},
{
"key": "add:div#root:dragleave",
"count": 2
},
{
"key": "add:div#root:dragover",
"count": 2
},
{
"key": "add:div#root:dragstart",
"count": 2
},
{
"key": "add:div#root:drop",
"count": 2
},
{
"key": "add:div#root:gotpointercapture",
"count": 2
},
{
"key": "add:div#root:input",
"count": 2
},
{
"key": "add:div#root:keydown",
"count": 2
},
{
"key": "add:div#root:keypress",
"count": 2
},
{
"key": "add:div#root:keyup",
"count": 2
}
],
"timers": [
{
"key": "interval:200",
"count": 12
},
{
"key": "timeout:12000",
"count": 6
},
{
"key": "timeout:0",
"count": 4
},
{
"key": "interval:250",
"count": 4
},
{
"key": "interval:3000",
"count": 4
},
{
"key": "interval:60000",
"count": 3
},
{
"key": "timeout:15000",
"count": 1
},
{
"key": "interval:1000",
"count": 1
},
{
"key": "interval:2000",
"count": 1
},
{
"key": "timeout:4000",
"count": 1
},
{
"key": "timeout:10000",
"count": 1
}
],
"timeouts": 13,
"intervals": 25,
"timeoutFires": 6,
"intervalFires": 593,
"webSocketMessageCount": 1000,
"webSocketBytes": 797561,
"webSocketEvents": [
{
"event": "log:entry",
"count": 526,
"bytes": 111535
},
{
"event": "sensorFrame",
"count": 306,
"bytes": 574616
},
{
"event": "commandAck",
"count": 145,
"bytes": 13778
},
{
"event": "chat:typing",
"count": 7,
"bytes": 3073
},
{
"event": "session:sync",
"count": 5,
"bytes": 87372
},
{
"event": "chat:message",
"count": 4,
"bytes": 4115
},
{
"event": "roverHostStats",
"count": 4,
"bytes": 3069
},
{
"event": "2",
"count": 3,
"bytes": 3
}
],
"mutationsTotal": {
"records": 41062,
"added": 229,
"removed": 35,
"attrs": 40761,
"text": 37
},
"mutationBursts": [
{
"at": 1534,
"records": 164,
"added": 105,
"removed": 1,
"attrs": 58,
"text": 0
},
{
"at": 3007,
"records": 93,
"added": 13,
"removed": 3,
"attrs": 68,
"text": 9
},
{
"at": 1212,
"records": 73,
"added": 22,
"removed": 5,
"attrs": 42,
"text": 4
},
{
"at": 6080,
"records": 72,
"added": 0,
"removed": 0,
"attrs": 72,
"text": 0
},
{
"at": 7837,
"records": 72,
"added": 0,
"removed": 0,
"attrs": 72,
"text": 0
},
{
"at": 8493,
"records": 72,
"added": 0,
"removed": 0,
"attrs": 72,
"text": 0
},
{
"at": 16038,
"records": 72,
"added": 0,
"removed": 0,
"attrs": 72,
"text": 0
},
{
"at": 26549,
"records": 72,
"added": 0,
"removed": 0,
"attrs": 72,
"text": 0
},
{
"at": 10855,
"records": 71,
"added": 1,
"removed": 0,
"attrs": 70,
"text": 0
},
{
"at": 4339,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 6195,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 6225,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 9779,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 15605,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 15797,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 15862,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 18303,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 18367,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 20816,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 20907,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
}
],
"frames": {
"count": 338,
"average": 88.21153846153847,
"p95": 150,
"p99": 183.29999999999927
},
"longTasks": {
"count": 18,
"top": [
{
"startTime": 228,
"duration": 287,
"name": "self"
},
{
"startTime": 520.5,
"duration": 226,
"name": "self"
},
{
"startTime": 1534.1000000014901,
"duration": 140,
"name": "self"
},
{
"startTime": 2490,
"duration": 136,
"name": "self"
},
{
"startTime": 1080,
"duration": 132,
"name": "self"
},
{
"startTime": 1212.3000000044703,
"duration": 124,
"name": "self"
},
{
"startTime": 1434.8999999985099,
"duration": 99,
"name": "self"
},
{
"startTime": 747.1000000014901,
"duration": 85,
"name": "self"
},
{
"startTime": 2928.60000000149,
"duration": 78,
"name": "self"
},
{
"startTime": 11610.30000000447,
"duration": 72,
"name": "self"
},
{
"startTime": 4531.10000000149,
"duration": 62,
"name": "self"
},
{
"startTime": 19226.5,
"duration": 62,
"name": "self"
},
{
"startTime": 6920.70000000298,
"duration": 54,
"name": "self"
},
{
"startTime": 7968.39999999851,
"duration": 52,
"name": "self"
},
{
"startTime": 3745.60000000149,
"duration": 51,
"name": "self"
},
{
"startTime": 12822.20000000298,
"duration": 51,
"name": "self"
},
{
"startTime": 8143.70000000298,
"duration": 50,
"name": "self"
},
{
"startTime": 10881.5,
"duration": 50,
"name": "self"
}
]
},
"consoleErrors": []
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 923 KiB

@@ -0,0 +1,843 @@
{
"url": "https://rover.otter.land/",
"runDir": "perf/results/2026-06-11T03-22-44-714Z-root-runtime",
"environment": {
"cpuThrottle": 6,
"viewport": {
"width": 390,
"height": 844
},
"isMobile": true,
"sampleMs": 18000
},
"metrics": {
"Timestamp": 39885.73698,
"AudioHandlers": 0,
"AudioWorkletProcessors": 0,
"Documents": 1,
"Frames": 1,
"JSEventListeners": 8530,
"LayoutObjects": 1864,
"MediaKeySessions": 0,
"MediaKeys": 0,
"Nodes": 2379,
"Resources": 12,
"ContextLifecycleStateObservers": 12,
"V8PerContextDatas": 2,
"WorkerGlobalScopes": 0,
"UACSSResources": 0,
"RTCPeerConnections": 2,
"ResourceFetchers": 1,
"AdSubframes": 0,
"DetachedScriptStates": 2,
"ArrayBufferContents": 8,
"LayoutCount": 116,
"RecalcStyleCount": 120,
"LayoutDuration": 0.379961,
"RecalcStyleDuration": 0.361646,
"DevToolsCommandDuration": 0.000126,
"ScriptDuration": 11.946269,
"V8CompileDuration": 0,
"TaskDuration": 17.591645,
"TaskOtherDuration": 4.903643,
"ThreadTime": 18.399532,
"ProcessTime": 20.97,
"JSHeapUsedSize": 24567940,
"JSHeapTotalSize": 44564480,
"FirstMeaningfulPaint": 0,
"DomContentLoaded": 39867.72907,
"NavigationStart": 39867.192605
},
"snapshot": {
"elapsedMs": 18737.20000000298,
"listeners": [
{
"key": "add:window:keydown",
"count": 334
},
{
"key": "add:window:keyup",
"count": 334
},
{
"key": "add:window:blur",
"count": 334
},
{
"key": "add:window:gamepadconnected",
"count": 334
},
{
"key": "add:window:gamepaddisconnected",
"count": 334
},
{
"key": "remove:window:keydown",
"count": 333
},
{
"key": "remove:window:keyup",
"count": 333
},
{
"key": "remove:window:blur",
"count": 333
},
{
"key": "remove:window:gamepadconnected",
"count": 333
},
{
"key": "remove:window:gamepaddisconnected",
"count": 333
},
{
"key": "add:input:invalid",
"count": 18
},
{
"key": "add:window:online",
"count": 5
},
{
"key": "add:document:visibilitychange",
"count": 5
},
{
"key": "remove:window:online",
"count": 4
},
{
"key": "remove:document:visibilitychange",
"count": 4
},
{
"key": "add:audio:canplay",
"count": 3
},
{
"key": "add:audio:ended",
"count": 3
},
{
"key": "add:audio:error",
"count": 3
},
{
"key": "add:audio:pause",
"count": 3
},
{
"key": "add:audio:play",
"count": 3
},
{
"key": "add:audio:stalled",
"count": 3
},
{
"key": "add:audio:waiting",
"count": 3
},
{
"key": "add:div#root:auxclick",
"count": 2
},
{
"key": "add:div#root:click",
"count": 2
},
{
"key": "add:div#root:contextmenu",
"count": 2
},
{
"key": "add:div#root:copy",
"count": 2
},
{
"key": "add:div#root:cut",
"count": 2
},
{
"key": "add:div#root:drag",
"count": 2
},
{
"key": "add:div#root:dragend",
"count": 2
},
{
"key": "add:div#root:dragenter",
"count": 2
},
{
"key": "add:div#root:dragexit",
"count": 2
},
{
"key": "add:div#root:dragleave",
"count": 2
},
{
"key": "add:div#root:dragover",
"count": 2
},
{
"key": "add:div#root:dragstart",
"count": 2
},
{
"key": "add:div#root:drop",
"count": 2
},
{
"key": "add:div#root:gotpointercapture",
"count": 2
},
{
"key": "add:div#root:input",
"count": 2
},
{
"key": "add:div#root:keydown",
"count": 2
},
{
"key": "add:div#root:keypress",
"count": 2
},
{
"key": "add:div#root:keyup",
"count": 2
}
],
"timers": [
{
"key": "interval:200",
"count": 7
},
{
"key": "timeout:0",
"count": 4
},
{
"key": "timeout:12000",
"count": 4
},
{
"key": "interval:60000",
"count": 4
},
{
"key": "interval:250",
"count": 4
},
{
"key": "interval:3000",
"count": 4
},
{
"key": "interval:1000",
"count": 2
},
{
"key": "timeout:15000",
"count": 1
},
{
"key": "interval:2000",
"count": 1
},
{
"key": "timeout:4000",
"count": 1
},
{
"key": "timeout:10000",
"count": 1
}
],
"timeouts": 11,
"intervals": 22,
"timeoutFires": 6,
"intervalFires": 148,
"webSocketMessageCount": 1000,
"webSocketBytes": 762620,
"webSocketEvents": [
{
"event": "log:entry",
"count": 850,
"bytes": 185008
},
{
"event": "sensorFrame",
"count": 277,
"bytes": 520169
},
{
"event": "commandAck",
"count": 200,
"bytes": 19180
},
{
"event": "session:sync",
"count": 23,
"bytes": 401624
},
{
"event": "",
"count": 5,
"bytes": 5008402
},
{
"event": "chat:typing",
"count": 4,
"bytes": 1748
},
{
"event": "roverSnapshot:frame",
"count": 3,
"bytes": 291
},
{
"event": "2",
"count": 3,
"bytes": 3
},
{
"event": "roverHostStats",
"count": 3,
"bytes": 2306
},
{
"event": "alert",
"count": 2,
"bytes": 256
},
{
"event": "alert:new",
"count": 2,
"bytes": 329
},
{
"event": "0{\"sid\":\"pSJ",
"count": 1,
"bytes": 106
},
{
"event": "40{\"sid\":\"3t",
"count": 1,
"bytes": 32
},
{
"event": "mode",
"count": 1,
"bytes": 27
},
{
"event": "controlGranted",
"count": 1,
"bytes": 41
},
{
"event": "auth:role",
"count": 1,
"bytes": 31
},
{
"event": "rovers",
"count": 1,
"bytes": 5485
},
{
"event": "chat:init",
"count": 1,
"bytes": 91696
},
{
"event": "overseer:memory",
"count": 1,
"bytes": 1505
},
{
"event": "log:init",
"count": 1,
"bytes": 41011
},
{
"event": "barcode:state",
"count": 1,
"bytes": 1619
},
{
"event": "kinect:pointCloudFrame",
"count": 1,
"bytes": 262
},
{
"event": "kinect:colorFrame",
"count": 1,
"bytes": 169
},
{
"event": "430[{\"succes",
"count": 1,
"bytes": 42
},
{
"event": "431[{\"url\":\"",
"count": 1,
"bytes": 133
},
{
"event": "432[{\"url\":\"",
"count": 1,
"bytes": 145
},
{
"event": "roverSnapshot:status",
"count": 1,
"bytes": 79
},
{
"event": "Freaky",
"count": 1,
"bytes": 40
},
{
"event": "434[{\"succes",
"count": 1,
"bytes": 194
},
{
"event": "435[{\"succes",
"count": 1,
"bytes": 194
}
],
"mutationsTotal": {
"records": 18758,
"added": 215,
"removed": 17,
"attrs": 18490,
"text": 36
},
"mutationTargets": [
{
"key": "input.accent-emerald-400:attributes:name",
"count": 5300
},
{
"key": "input.accent-emerald-400:attributes:type",
"count": 2650
},
{
"key": "input.field-input.w-full:attributes:name",
"count": 1332
},
{
"key": "input.field-input.flex-1:attributes:name",
"count": 1328
},
{
"key": "input.accent-cyan-500:attributes:name",
"count": 1324
},
{
"key": "div.h-full:attributes:style",
"count": 1238
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:name",
"count": 666
},
{
"key": "input.field-input.chat-composer-input:attributes:name",
"count": 666
},
{
"key": "input:attributes:name",
"count": 666
},
{
"key": "input.field-input.w-full:attributes:type",
"count": 666
},
{
"key": "input.accent-cyan-500:attributes:type",
"count": 662
},
{
"key": "path.[object.SVGAnimatedString]:attributes:d",
"count": 621
},
{
"key": "path.[object.SVGAnimatedString]:attributes:fill",
"count": 619
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:type",
"count": 333
},
{
"key": "input:attributes:type",
"count": 333
},
{
"key": "Text:characterData:",
"count": 36
},
{
"key": "input.min-w-0.flex-1:attributes:name",
"count": 32
},
{
"key": "section.panel-section.border:attributes:style",
"count": 10
},
{
"key": "header.flex.items-center:attributes:style",
"count": 9
},
{
"key": "path.[object.SVGAnimatedString]:attributes:stroke",
"count": 6
},
{
"key": "span.inline-flex.items-center:attributes:class",
"count": 4
},
{
"key": "button.mobile-touch-control.flex:attributes:disabled",
"count": 3
},
{
"key": "button.mobile-touch-control.min-h-9:attributes:disabled",
"count": 3
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:value",
"count": 2
},
{
"key": "input.field-input.flex-1:attributes:value",
"count": 2
},
{
"key": "div.flex.flex-col:attributes:class",
"count": 1
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:placeholder",
"count": 1
},
{
"key": "div.rounded-sm.px-2:attributes:class",
"count": 1
},
{
"key": "button.button-dark.min-w-0:attributes:disabled",
"count": 1
},
{
"key": "button.mobile-touch-control.flex:attributes:class",
"count": 1
},
{
"key": "div.mobile-touch-control.flex:attributes:class",
"count": 1
},
{
"key": "input.field-input.w-full:attributes:value",
"count": 1
},
{
"key": "button.button-dark.text-sm:attributes:disabled",
"count": 1
},
{
"key": "video.h-full.w-full:attributes:playsinline",
"count": 1
},
{
"key": "video.h-full.w-full:attributes:autoplay",
"count": 1
},
{
"key": "video.h-full.w-full:attributes:disableremoteplayback",
"count": 1
},
{
"key": "audio:attributes:autoplay",
"count": 1
},
{
"key": "audio:attributes:disableremoteplayback",
"count": 1
},
{
"key": "div.fixed.bottom-2:attributes:class",
"count": 1
},
{
"key": "span.rounded.px-1:attributes:class",
"count": 1
}
],
"mutationBursts": [
{
"at": 1633,
"records": 164,
"added": 105,
"removed": 1,
"attrs": 58,
"text": 0
},
{
"at": 3468,
"records": 79,
"added": 11,
"removed": 2,
"attrs": 64,
"text": 2
},
{
"at": 1295,
"records": 73,
"added": 22,
"removed": 5,
"attrs": 42,
"text": 4
},
{
"at": 5655,
"records": 72,
"added": 0,
"removed": 0,
"attrs": 72,
"text": 0
},
{
"at": 5790,
"records": 72,
"added": 0,
"removed": 0,
"attrs": 72,
"text": 0
},
{
"at": 16256,
"records": 72,
"added": 3,
"removed": 0,
"attrs": 67,
"text": 2
},
{
"at": 6959,
"records": 70,
"added": 0,
"removed": 0,
"attrs": 69,
"text": 1
},
{
"at": 4549,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 4590,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 5282,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 5711,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 6485,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 7777,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 8498,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 8545,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 9500,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 9569,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 11021,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 11795,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
},
{
"at": 17038,
"records": 68,
"added": 0,
"removed": 0,
"attrs": 68,
"text": 0
}
],
"frames": {
"count": 153,
"average": 116.5535947712418,
"p95": 166.69999999999982,
"p99": 316.60000000000014
},
"longTasks": {
"count": 16,
"top": [
{
"startTime": 238.79999999701977,
"duration": 299,
"name": "self"
},
{
"startTime": 542.8999999985099,
"duration": 192,
"name": "self"
},
{
"startTime": 3064.10000000149,
"duration": 177,
"name": "self"
},
{
"startTime": 1632.8999999985099,
"duration": 170,
"name": "self"
},
{
"startTime": 1154.8999999985099,
"duration": 140,
"name": "self"
},
{
"startTime": 2930.2999999970198,
"duration": 133,
"name": "self"
},
{
"startTime": 1505.6999999955297,
"duration": 127,
"name": "self"
},
{
"startTime": 735.6999999955297,
"duration": 117,
"name": "self"
},
{
"startTime": 1295.8999999985099,
"duration": 108,
"name": "self"
},
{
"startTime": 3674.89999999851,
"duration": 83,
"name": "self"
},
{
"startTime": 3387,
"duration": 81,
"name": "self"
},
{
"startTime": 3567.89999999851,
"duration": 59,
"name": "self"
},
{
"startTime": 1909,
"duration": 57,
"name": "self"
},
{
"startTime": 7985.10000000149,
"duration": 55,
"name": "self"
},
{
"startTime": 12968.5,
"duration": 55,
"name": "self"
},
{
"startTime": 931.6999999955297,
"duration": 53,
"name": "self"
}
]
},
"consoleErrors": []
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 875 KiB

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

After

Width:  |  Height:  |  Size: 6.3 MiB

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

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

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

After

Width:  |  Height:  |  Size: 165 KiB

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

After

Width:  |  Height:  |  Size: 160 KiB

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

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

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

After

Width:  |  Height:  |  Size: 86 KiB

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

After

Width:  |  Height:  |  Size: 86 KiB

@@ -0,0 +1,652 @@
{
"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": []
}
@@ -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": []
}
}
@@ -0,0 +1,219 @@
# 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.
@@ -0,0 +1,190 @@
# 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.
@@ -0,0 +1,202 @@
# 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.
@@ -0,0 +1,202 @@
# 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.
@@ -0,0 +1,177 @@
# 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.
@@ -0,0 +1,188 @@
# Issue 006: Spectator Role Handshake Retries
## Summary
The spectator route sometimes attempts `subscribeAll` before the server has accepted the
socket as a spectator. In the 8x throttled profile, the console repeatedly showed:
```txt
Failed to enter spectator mode Error: Spectator role required
```
This did not reproduce in the later 6x runtime audit, so it is intermittent. It is still
worth fixing because it can create startup retries, extra session/auth events, console
noise, and delayed media setup on slow devices.
## Severity
Medium-low.
This is not the main steady-state CPU eater. Treat it as a correctness/startup reliability
issue that can make performance worse under throttling.
## Upstream Likelihood
Low.
This is specific to `/spectate` startup and server role/subscription ordering. The `/`
performance backlog probably will not fix it.
## Affected Files
- `webui/src/hooks/useSpectatorMode.js`
- `webui/src/context/SocketContext.jsx`
- `webui/src/context/SessionContext.jsx`
- `server/src/services/authService/index.js`
- `server/src/services/roverManager/socketHandlers.js`
## Evidence
Artifact:
```sh
perf/results/2026-06-11T03-14-34-168Z/report.json
```
That 8x CPU-throttled profile captured repeated console errors:
```txt
Failed to enter spectator mode Error: Spectator role required
```
Fresh 6x runtime audit:
```txt
consoleErrors: []
auth:role events: 4
```
So the race is not constant, but it exists under some timing conditions.
## Current Code Path
`useSpectatorMode` does this:
```txt
webui/src/hooks/useSpectatorMode.js:18
if (session?.role !== 'spectator') {
await setRole('spectator');
}
webui/src/hooks/useSpectatorMode.js:21
await subscribeAll();
```
The effect reruns when these change:
```txt
webui/src/hooks/useSpectatorMode.js:36
[connected, session?.mode, session?.role, setRole, subscribeAll]
```
The server rejects `subscribeAll` unless the socket role is already spectator:
```txt
server/src/services/roverManager/socketHandlers.js:161
if (socket.data?.role !== 'spectator') {
cb({ error: 'Spectator role required' });
}
```
The auth service can initialize a socket as spectator only if the socket handshake query
requests it:
```txt
server/src/services/authService/index.js:40
const requestedRole = socket.handshake?.query?.role;
server/src/services/authService/index.js:41
const initialRole = requestedRole === 'spectator' ? 'spectator' : 'user';
```
If the spectate page connects as a normal user and then switches role after connect, slow
timing can expose ordering issues.
## Why This Matters
The retry itself is not a huge CPU cost. The damage is indirect:
- extra auth/session events
- repeated `subscribeAll` attempts
- delayed rover room joins
- delayed video/snapshot setup
- noisy console errors during profiling
- worse startup behavior on weak devices
## Fix Strategies
### Option A: Connect `/spectate` With `role=spectator`
Make the spectator route establish its socket with a spectator role query from the start:
```txt
io(..., { query: { role: 'spectator' } })
```
This uses the existing server path in `authService`.
### Option B: Wait For Confirmed Role Before `subscribeAll`
After calling `setRole('spectator')`, wait until `auth:role` or session state confirms
`role === 'spectator'`, then call `subscribeAll`.
Avoid calling `subscribeAll` in the same effect tick if the role update has not propagated.
### Option C: Add A Server-Side Atomic Spectator Enter Event
Create one event:
```txt
session:enterSpectator
```
Server behavior:
1. set socket role to spectator
2. join all visible rover rooms
3. return one ack
This removes client-side ordering risk.
### Option D: Debounce Or Guard Retries
If `subscribeAll` fails with `Spectator role required`, do not retry in a tight loop. Wait
for an explicit role event or connection change.
## Recommended Path
The cleanest fix is Option A plus Option B:
1. Make the `/spectate` socket connect as `role=spectator` when possible.
2. Keep `useSpectatorMode` as a fallback, but call `subscribeAll` only after confirmed
spectator role.
If socket creation is shared in a way that makes route-specific query parameters awkward,
use the atomic server event instead.
## Validation
Run the throttled profile several times:
```sh
CPU_THROTTLE=8 VIEWPORT=390x844 MOBILE=1 \
node perf/live-cpu-profile.mjs https://rover.otter.land/spectate perf/results
```
Expected improvements:
- No `Failed to enter spectator mode` console errors.
- Fewer `auth:role` events during startup.
- More consistent startup timing.
## Risks
- Be careful not to accidentally make ordinary `/` users spectators.
- If using route-specific socket query parameters, verify reconnects preserve the correct
role.
- In lockdown mode, spectator setup must still fail cleanly.
+63
View File
@@ -0,0 +1,63 @@
# `/spectate` CPU Performance Issue Index
This folder breaks the live-site `/spectate` CPU investigation into separate,
work-ready issues. These are intentionally separate from `perf/issues`, which is the
current `/` backlog.
The testing target was:
```sh
https://rover.otter.land/spectate
```
Primary artifact:
```sh
perf/results/2026-06-12T04-55-16-888Z-root-runtime/root-runtime-report.json
```
Secondary artifacts:
```sh
perf/results/2026-06-11T03-14-34-168Z/report.json
perf/results/2026-06-11T03-11-39-596Z-route-sweep/route-sweep-report.json
```
## Ranked Issues
1. [Telemetry fan-out and always-on HUD maps](./001-telemetry-fanout-hud-maps.md)
2. [Multi-rover WebRTC media fan-out](./002-multi-rover-webrtc-media-fanout.md)
3. [Room camera and snapshot object URL churn](./003-room-camera-snapshot-object-url-churn.md)
4. [Broad spectator session rerenders](./004-broad-spectator-session-rerenders.md)
5. [Shared log/session stream pressure](./005-shared-log-session-stream-pressure.md)
6. [Spectator role handshake retries](./006-spectator-role-handshake-retries.md)
## Quick Read
The biggest `/spectate` CPU issue is the number of live visual things being updated at
once. The page receives about 45 `sensorFrame` events/sec in the sampled run, and every
rover card has an always-on HUD map. That produced thousands of SVG `path` mutations in
30 seconds.
The second issue is live media fan-out. The audit saw 6 `RTCPeerConnections`, which is a
lot for low-end laptops and mobile browsers, especially alongside React/HUD updates.
The third issue is snapshot/camera image churn. Room camera frames created 117 `img.src`
mutations and the socket audit saw 11MB of binary payloads in 30 seconds.
Issues 004 and 005 are where the `/` backlog overlaps most. If the `/` work adds better
session selectors, log batching, chat batching, and telemetry visual throttling, revisit
these spectate writeups before implementing them from scratch.
Issue 006 is lower priority because it was intermittent, but it is worth fixing because
failed spectator setup can create extra retries, console noise, and unpredictable startup
work.
## Upstream Labels
Each issue includes an "Upstream Likelihood" section:
- `High`: likely mostly fixed by the earlier `/` work.
- `Medium`: `/` work should help, but `/spectate` still needs targeted changes.
- `Low`: mostly spectate-specific.

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