Skip to main content
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.
Add one line to your render loop and you also get FPS:

What runs in the background

One thread, named arcane-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 between init 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_id until a heartbeat is acknowledged, so a retry is deduplicated rather than double-counted.
  • resolution and graphics_preset are whatever you last passed to set_graphics, and are omitted when you never call it.
Arcane averages the samples per hardware configuration and shows them on the store page. The SDK only produces the numbers.
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 LobbyClosed event — 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_events reads Unavailable.

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 same sample_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.

Next steps