Skip to content

feat(wasm-hv): in-memory CXO telemetry — browser visor reports transport bandwidth+latency to TPD#3362

Open
0pcom wants to merge 13 commits into
skycoin:developfrom
0pcom:feat/wasm-cxo-telemetry
Open

feat(wasm-hv): in-memory CXO telemetry — browser visor reports transport bandwidth+latency to TPD#3362
0pcom wants to merge 13 commits into
skycoin:developfrom
0pcom:feat/wasm-cxo-telemetry

Conversation

@0pcom

@0pcom 0pcom commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

A browser (wasm) visor had no telemetry integration, so its transports never reached TPD /metrics — invisible to rewards. This gives it the same CXO reporting as a native visor, but with an in-memory datastore (no filesystem in a tab; telemetry resets on reload, which is fine — TPD retains published rollups in its window).

The blocker + fix

The CXO/skyobject stack transitively imported bbolt (via the in-repo cxds/idxdb on-disk drives), which won't build for GOOS=js (arch consts + mmap) — so the CXO publisher couldn't run in wasm even with InMemoryDB.

  • Build-tag-split pkg/cxo/data/cxds + idxdb: bbolt on-disk drive.go//go:build !js; drive_js.go stubs provide DriveOptions + NewDrive* (which error — wasm uses the in-memory CXDS/IdxDB via skyobject's InMemoryDB). Shared panicf moved to the untagged memory.go. Native on-disk path unchanged (tests pass).
  • skyobject.NewContainer: skip the DataDir mkdir when InMemoryDB — the default DataDir resolves to /tmp under js/wasm where mkdir is "not implemented on js" (was fatal); in-memory mode never touches those files.

The wasm telemetry publisher (telemetry_js.go)

  • In-memory treestore.Publisher (InMemoryDB: true), feed PK = visor PK.
  • tpM.SetTPDLeafPublisher(pub)transports/<uuid>/entry + /tombstone.
  • Samples transports/<uuid>/current (liveSnapshot: sent/recv + latency min/max/avg) each minute.
  • Announces to TPD every 30 s so its cxo-aggregator subscribes.
  • Wired into bootEdge after the transport manager serves.

Verified live

Reloaded the wasm visor → publisher comes up (feed=<self> → TPD), and after ~2.5 min TPD /metrics shows the wasm visor's 5 transports (swtr, webrtc), each with latency (e.g. webrtc avg 11000, swtr avg 150000) and bandwidth (daily sent/recv) populated. Builds native + js/wasm; cxds/idxdb tests pass.

🤖 Generated with Claude Code

0pcom added 13 commits July 1, 2026 20:38
…ilds under js/wasm

The CXO/skyobject stack transitively imported bbolt (via cxds+idxdb on-disk
drives), which does not build for GOOS=js (arch-specific consts + mmap) — so the
CXO publisher could not run in a wasm visor even with InMemoryDB. Split the
bbolt-backed drive.go behind //go:build !js and add drive_js.go stubs providing
DriveOptions + the NewDrive* constructors (which error, since a wasm visor uses
the in-memory CXDS/IdxDB via skyobject's InMemoryDB path). Moved the shared
panicf helper out of drive.go into the untagged memory.go.

Result: pkg/cxo/treestore (the full publish path) now compiles under js/wasm;
native on-disk path unchanged (cxds/idxdb tests pass). Foundation for wasm-visor
in-memory CXO telemetry -> TPD.
…atency to TPD

The browser visor now runs a CXO TreeStore publisher (InMemoryDB — no filesystem)
that reports each transport's bandwidth + latency to TPD via its cxo-aggregator,
exactly like a native visor (init_stats.go) but without the bbolt stats.Store.
telemetry_js.go: builds the in-memory publisher, wires tpM.SetTPDLeafPublisher
(transports/<uuid>/entry + tombstone), samples transports/<uuid>/current
(liveSnapshot: sent/recv + latency min/max/avg) each minute, and announces to TPD
every 30s so it subscribes. Wired into bootEdge after the transport manager serves.

Also fix skyobject.NewContainer to skip the DataDir mkdir when InMemoryDB — the
default DataDir resolves to /tmp under js/wasm where 'mkdir not implemented on js'
was fatal; in-memory mode never touches those files anyway.

Runtime-verified: publisher comes up (feed=<self> → TPD), builds on native + js/wasm.
… stale deadcode nolint

Compliance pass against the Go coding standard's mechanical review engine
(gofmt / go vet / golangci-lint / native + js-wasm build):

- gofmt the wasm-visor JS-hooks map (alignment drift from telemetry wiring)
- pkg/dmsg/dmsg/util.go: //nolint:gosec G709 — gob frame arrives over a
  Noise-authenticated dmsg session, fixed internal decode target (false positive)
- usermanager cookies: //nolint:gosec G124 — Secure/HttpOnly/SameSite ARE set
  via s.c config accessors gosec's flow analysis can't see through (false positive)
- mux-probe-assert: drop stale ,deadcode from nolint (folded into unused upstream)

All pre-existing findings surfaced only by a newer-than-CI golangci-lint; CI
was already green. No behavioral change.
Routine dependency refresh — all minor/patch bumps, no major-version changes:
grpc 1.81.1→1.82.0, pion/webrtc v4.2.15→v4.2.16 (+sctp/rtp/rtcp/turn),
go-proxyproto 0.12.0→0.14.0, maxminddb v2.4.0→v2.4.1, starlark, genproto,
klauspost/cpuid, gopherjs, lufia/plan9stats, shoenig/go-m1cpu.

Verified: native build, GOOS=js GOARCH=wasm build (wasm-visor + CXO), and
go vet all clean.
…code table

- refresh docs/skywire-goda-graph.svg (goda graph | dot -Tsvg)
- append a Lines of Code section (gocloc, vendor/node_modules/.git excluded):
  1740 Go files / 267,754 LOC; ~403k LOC total across all languages
browse.js: add createChatWindow — a 1:1 skychat client as a WinBox desktop
window (the missing peer to the skynet browser), driving the existing
wasm-visor JS hooks skychatSend(peerPk,text)/skychatMessages(). Distinct
sender PKs from the message buffer surface as clickable chips so an incoming
message from an unknown peer is discoverable. Added to the app menu as 'chat',
gated on skywireVisor.skychatSend (wasm-visor only; native keeps its Angular
skychat tab), exactly like the host window. Rebuild ./skywire re-embeds it
(browse.js is go:embed'd in browseui, not in the wasm.gz blob).

docs/skychat-refactor-rfc.md: design for extracting a shared pkg/skychat core
(one message model + wire codec, federated-only groups, one Transport interface,
history behind a build-tag-split store) so native + wasm stop diverging — the
visorcore convergence pattern. Includes public-group service discovery via a
new servicedisc ServiceTypeSkychat='skychat' (same directory mechanism as
type=proxy), and causal-ish reorder-buffer display ordering.
… step 2)

The skychat length-prefixed frame protocol (4-byte big-endian length + payload,
64 KiB cap, chat-msg/chat-ack envelope) was implemented THREE times — the native
app (framedConn), the browser-tab wasm visor (chatConn), and the group relay
(session.go readFrame/writeFrame) — each with its own constant and write mutex,
free to drift. This collapses them into one codec: pkg/skychat/message.

New package pkg/skychat/message (pure Go, compiles under js/wasm):
- WriteFrame/ReadFrame (io.Writer/io.Reader), MaxFrameSize
- Conn: net.Conn + write mutex, WriteFrame/WriteFrameDeadline/ReadFrame
- Envelope (chat-msg/chat-ack) + TypeMsg/TypeAck + ParseEnvelope/Marshal
- message_test.go: wire round-trip, reject cases, Conn framing-compat, envelope parse

Consumers migrated (wire bytes unchanged):
- cmd/apps/skychat: framedConn is now a type alias for message.Conn; chatEnvelope/
  chatTypeMsg/chatTypeAck alias the message types; tryHandleChatEnvelope uses
  message.ParseEnvelope. Deleted the local frame methods + skychatMaxFrameSize.
- cmd/apps/skychat/group/session.go: relay framing uses message.ReadFrame/WriteFrame;
  deleted local readFrame/writeFrame + relayMaxFrameSize.
- cmd/wasm-visor/skychat_js.go: chatConn/readFrame/writeFrame/skychatMaxFrame/
  chatAckEnvelope replaced with message.Conn + message.Envelope. Blob regenerated.

Validated: message unit tests + full skychat suite (commands/group/pairing) pass;
native + js/wasm build/vet/lint clean; live harness — wasm visor boots, skychat
listens on dmsg:1, desktop chat window opens.
… (step 4)

Measured: a member joining a federated CXO group took ~30-40s to receive its
first message. Root cause: when two members start close together each fails the
other's INITIAL peer-sub dial (peer not listening yet), and the retry cadence
was slow — the in-visor Manager reconnect loop ticks every 30s, and the
standalone CXO-group driver re-attempted every 15s (20s timeout). A group.Session
does not self-retry a peer-sub whose first Connect failed, so first-message
latency was gated by whichever slow driver applied.

Fix (both drivers, same class):
- Manager.runReconnectLoop: adaptive cadence — tick at reconnectWarmupInterval
  (3s) while any peer is still 'warming' (never connected AND under its
  reconnectBackoffFailures1 budget), easing back to reconnectInterval (30s) once
  every peer is attached. New hasWarmingPeer() + peerReconnectFailures() gate it.
  Steady-state groups keep the low-churn 30s cadence; a dead peer stops
  qualifying after its budget and falls to the 5min/30min backoff, so it can't
  pin the loop fast.
- Standalone startCXOGroup driver: 15s->3s ticker, 20s->10s per-attempt timeout.
  It already skips attached peers, so a fast tick idles cheaply post-convergence.

Verified with two standalone CXO-TCP group processes (simultaneous start, the
hard case): member->owner first arrival 2s (was ~30-40s), owner->member 3s. Full
skychat suite + group tests pass; gofmt/vet/lint clean.
Prep for wasm-visor group chat. The group record store was bbolt-backed, and
bbolt can't compile under GOOS=js GOARCH=wasm (arch-const MaxAllocSize + mmap) —
so the group package (and thus group chat) couldn't build for the browser visor.

Split store.go the same way the CXO cxds/idxdb datastore was split:
- store_bbolt.go (//go:build !js): the existing bbolt impl, unchanged.
- store_memory.go (//go:build js): a wire-identical in-memory impl — same *Store
  type + method set, records held as JSON bytes keyed by group ID so value
  semantics (independent copies on read, legacy-Admins normalization, lexicographic
  List order) match bbolt exactly. Ephemeral (resets on tab reload; the federated
  network retains published messages). Designed so an IndexedDB-backed durable
  variant can hydrate/flush this map behind the identical API later.

The group package now compiles under js/wasm; native build + group tests unchanged.
No behavior change on native. Next: plumb InMemoryDB through group.Config and wire
group.Manager into the wasm visor.
…r wasm

Adds Config.InMemoryDB: when set, newPublisher runs the session's CXO tree fully
in memory over the dmsg transport (the native-TCP standalone path already forced
this internally). Relaxes the 'DataDir required' check when InMemoryDB is set.
This lets a browser (js/wasm) visor — which has no filesystem — be a full
federated group member: publish its own feed in memory, subscribe to peers over
dmsg. Native behavior unchanged (InMemoryDB defaults false). Builds native +
js/wasm; group tests pass.
…vity log pane

Two additions to the desktop skychat window (createChatWindow), matching the
browser window's controls:
- Transport selector (dmsg | skynet): skychatSend now takes an optional network
  arg; sendChat dials the chosen appnet.Type and caches conns per (network, peer)
  so the two transports don't clobber each other. Default dmsg (unchanged).
- Activity log pane (🐞): subscribes to the shared window.skywireLog ring
  (same source the 'logs' window reads), filtered to skychat lines, so the
  operator sees the dial/connect/send/receive steps — like the skysocks-lite
  route-setup log in the browser window. sendChat now vlog's those steps.

Verified live via hvinspect on the :8443 harness: selector has dmsg+skynet, log
pane shows 'skychat: dialing dmsg <pk>:1…' after a send. wasm blob regenerated.
docs/wasm-visor-ui-tour.md — a screenshot-driven tour of the browser-tab visor:
dashboard/visor-list, the WinBox mini-desktop app menu, and each window (skynet
browser, skychat with the dmsg/skynet transport selector + activity log pane,
host-content, console REPL, live logs). Captured headless via cmd/hvinspect
against a live 'skywire cli hv serve' harness; includes the regeneration recipe.
Images under docs/img/wasm-visor/.
The browser-tab visor is now a full federated group-chat member. group_js.go
starts a group.Manager over the wasm dmsg client with an in-memory store
(store_memory.go) + in-memory CXO tree (ManagerConfig.InMemoryDB) — no filesystem
needed. It publishes its own member feed and subscribes to peers' feeds over dmsg,
exactly like a native visor; local state is ephemeral (resets on tab reload; the
network retains what was published).

manager.go: plumb InMemoryDB through ManagerConfig → Manager → each opened
group.Config (relaxes the DataDir-required check when set). Native default false.

JS surface on skywireVisor: skychatGroupCreate(name[,mode]) → {id,name,invite},
skychatGroupJoin(inviteLink), skychatGroupSend(id,text), skychatGroupAddMember(
id,pk), skychatGroupList() (JSON), skychatGroupMessages([id]) (JSON). Inbound
group messages surface into an in-memory ring + the visor log.

Builds native + js/wasm; group tests + native full build + js vet + lint clean
(group_js.go lint-clean). Blob regenerated. Next: UI + wasm↔native demo.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant