ArcaneClient::init opens a play session alongside the ownership check. It measures how long the player plays and, when they allow it, samples the frame rate. You do not call anything to start it.
What runs in the background
One thread, namedarcane-session. It sleeps on a condition variable, wakes about once a minute, posts a heartbeat of roughly 200 bytes to the Arcane desktop app on 127.0.0.1:39284, and goes back to sleep. There is no busy loop, no fine-grained timer, and no second thread. It is also the thread that polls lobby events, once a game has asked for them.
The session never opens the
arcane-powered:// deep link. If the desktop app is not running, tracking degrades quietly; it never interrupts the player.
Playtime
Playtime is the wall of time betweeninit and the end of the session, measured with a monotonic clock (Instant). Changing the system clock cannot inflate or rewind it. It is not paused when the player alt-tabs or opens a menu — session time is process time.
Every heartbeat carries the cumulative seconds since init, not a delta, so a lost or replayed heartbeat cannot corrupt the total.
FPS sampling
Frame rate is sampled, not counted continuously. While sampling is on, the session thread opens a 30-second window 60 seconds after the session starts — long enough to skip loading — and then every 5 minutes. Each closed window becomes one sample:- Outside a window,
frame()is a single atomic load — the counter does not even move. - A window with no frames produces no sample. A game that never calls
frame()reports nothing. - Each sample keeps its
sample_iduntil a heartbeat is acknowledged, so a retry is deduplicated rather than double-counted. resolutionandgraphics_presetare whatever you last passed toset_graphics, and are omitted when you never call it.
Sampling is the player’s choice, not yours. The Arcane desktop app has a “Share performance data” setting (on by default), and its answer travels in the
session/start and heartbeat responses. When it is off, no window is opened at all. A player toggling it mid-game takes effect on the next heartbeat.Tracking states
session() returns a SessionSnapshot — a copy of the state, taken from memory:
Lobby events and the tick
The session thread carries one more job, and only if you ask for it: polling the Arcane desktop app for lobby events.- Nothing is polled until the game calls
client.p2p()for the first time. That call arms it. - Once armed, the thread asks for events on every tick: every 5 seconds while this client is in an open lobby — created or joined, and not yet left, closed, or ended by a
LobbyClosedevent — and every 60 seconds otherwise. - Heartbeats keep their own 60-second schedule either way. A faster lobby tick does not mean a faster heartbeat.
- If the Arcane desktop app predates the lobby routes, polling stops silently and for good, and
session().lobby_eventsreadsUnavailable.
Ending the session
shutdown consumes the client. Dropping the last clone does the same thing best-effort, so it is optional — it just makes the moment explicit and gives the report a deadline you control. ArcaneClient is Clone and the clones share one session: it ends when the last one goes away.
In C, arcane_sdk_shutdown() does exactly this.
What is lost offline
If the Arcane desktop app is never reachable during the whole session, that session’s playtime is lost. The SDK keeps no buffer on disk — the loopback API is the only bus. Heartbeats missed in the middle of a session cost nothing: the next one carries the cumulative total, and unacknowledged FPS samples are re-sent with the samesample_id.
If the desktop app stops answering entirely, it expires the session on its side after three missed heartbeats (180 seconds) and keeps what it already had.