Skip to content

Commit 8715768

Browse files
docs(audience-session): plain-language comment pass
Rewrites jargon-heavy comments (off-thread, serialise, wire-ordered, fence, CAS, Timer thread) into plain language that reads without a threading background. No code changes. All 220 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ab6d35e commit 8715768

3 files changed

Lines changed: 35 additions & 23 deletions

File tree

src/Packages/Audience/Runtime/Core/Session.cs

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ namespace Immutable.Audience
1212
internal delegate void TrackDelegate(string eventName, Dictionary<string, object> properties);
1313

1414
// Unity session lifecycle. Emits session_start / session_heartbeat / session_end.
15-
// duration is engagement time (excludes pause). Heartbeat fires off-thread;
16-
// public methods run on the caller's thread. _track fires outside _lock.
15+
// duration is engagement time (excludes pause). The heartbeat runs on a
16+
// background thread; other methods run on the thread that called them. The
17+
// track callback is invoked with the internal lock released.
1718
//
18-
// Serialisation: Start / End / Dispose are NOT reentrant-safe from multiple
19-
// threads. Callers serialise them (ImmutableAudience holds _initLock across
20-
// Init / SetConsent / Shutdown / Reset, which are the only public entry points
21-
// that touch a Session). Pause / Resume / OnHeartbeat are thread-safe.
19+
// Start / End / Dispose are not safe to call from multiple threads at once.
20+
// Callers run them one at a time (ImmutableAudience holds its init lock while
21+
// calling Init / SetConsent / Shutdown / Reset — the only public entry points
22+
// that touch a Session). Pause / Resume / OnHeartbeat are safe to call from
23+
// any thread.
2224
internal sealed class Session : IDisposable
2325
{
2426
internal const int HeartbeatIntervalMs = 60_000;
@@ -63,9 +65,10 @@ internal Session(
6365
// Starts a session. Fires session_start and arms the heartbeat timer.
6466
internal void Start()
6567
{
66-
// Phase 1: drain old timer outside _lock (callback re-enters _lock).
67-
// Old state left intact so a trailing callback emits for the old session
68-
// — wire-ordered before the new session_start.
68+
// Phase 1: shut down the old timer with the internal lock released
69+
// (the callback takes that lock itself). Old state left intact so a
70+
// trailing callback sends a heartbeat for the old session — the
71+
// backend receives it before the new session_start.
6972
Timer? oldTimer;
7073
lock (_lock)
7174
{
@@ -216,7 +219,8 @@ internal void OnHeartbeat()
216219
lock (_lock)
217220
{
218221
if (_disposed || _sessionId == null) return;
219-
// Paused sessions don't ship heartbeats (timer still fires; this gates _track).
222+
// A paused session doesn't send heartbeats. The timer keeps
223+
// firing internally; this check stops the event from going out.
220224
if (_pausedAt.HasValue) return;
221225
sessionId = _sessionId!;
222226

@@ -244,8 +248,10 @@ internal void OnHeartbeat()
244248
SafeTrack("session_heartbeat", properties);
245249
}
246250

247-
// Guards _track from escaping. Heartbeat is a Timer callback (process
248-
// kill on .NET 5+); Start/End are caller-thread (would bubble to Init/Shutdown).
251+
// Stops exceptions from the track callback from reaching upstream.
252+
// Heartbeat runs on a background timer — an uncaught exception there
253+
// crashes the game on modern .NET. Start / End run on the caller's
254+
// thread, where it would bubble into Init / Shutdown.
249255
private void SafeTrack(string eventName, Dictionary<string, object> properties)
250256
{
251257
try
@@ -258,7 +264,8 @@ private void SafeTrack(string eventName, Dictionary<string, object> properties)
258264
}
259265
}
260266

261-
// Guards the studio-supplied snapshot from escaping to the timer thread.
267+
// Stops exceptions from the studio-supplied snapshot callback from
268+
// reaching the background timer.
262269
private Dictionary<string, object>? SafePerformanceSnapshot()
263270
{
264271
if (_performanceSnapshot == null) return null;
@@ -288,7 +295,8 @@ private void DrainHeartbeatTimer()
288295
using var waited = new ManualResetEvent(false);
289296
try
290297
{
291-
// Dispose(wh)=false: already disposed, wh never signals — skip the wait.
298+
// Timer was already disposed. The signal handle won't fire, so
299+
// don't wait for it.
292300
if (!timer.Dispose(waited))
293301
return;
294302

src/Packages/Audience/Runtime/ImmutableAudience.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ namespace Immutable.Audience
1212
// Entry point for the Immutable Audience SDK.
1313
public static class ImmutableAudience
1414
{
15-
// Reference fields are written inside _initLock; readers fence off the volatile _initialized load.
16-
// _consent, _session are written in _initLock, read outside → volatile.
17-
// _userId is written outside the lock (Identify, Reset) → volatile.
15+
// Reference fields are written inside _initLock; readers check the
16+
// `volatile _initialized` flag first so they never see a half-initialised state.
17+
// _consent and _session are written only inside _initLock but read outside,
18+
// so they stay `volatile` to make writes visible across threads.
19+
// _userId is written outside the lock (Identify, Reset) — `volatile` for the same reason.
1820
private static AudienceConfig? _config;
1921
private static DiskStore? _store;
2022
private static EventQueue? _queue;
@@ -610,7 +612,8 @@ private static bool CanTrack()
610612
return _initialized && _consent.CanTrack();
611613
}
612614

613-
// Shallow-copy so post-call mutation can't race the drain-thread serialiser.
615+
// Copy the dictionary so the caller editing it later can't corrupt the
616+
// message while the background thread is writing it to disk.
614617
private static Dictionary<string, object>? SnapshotCallerDict(Dictionary<string, object>? src) =>
615618
src != null ? new Dictionary<string, object>(src) : null;
616619

@@ -625,8 +628,8 @@ private static void Enqueue(Dictionary<string, object>? msg)
625628

626629
private static void SendBatch()
627630
{
628-
// CAS the gate; a previous tick still runningskip (including reschedule,
629-
// which the in-flight tick handles in its finally).
631+
// If a previous send is still running, skip this one. That send
632+
// will reschedule the next tick when it finishes.
630633
if (Interlocked.CompareExchange(ref _sendInFlight, 1, 0) != 0)
631634
return;
632635

src/Packages/Audience/Runtime/Utility/Log.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ internal static void Warn(string message) =>
2222

2323
private static void Emit(string line)
2424
{
25-
// Swallow Writer/Console throws so Log.Warn/Debug is never-throwing.
26-
// SDK safety wrappers log from inside their own catches; a throwing
27-
// Writer would otherwise reach the Timer thread (process kill on .NET 5+).
25+
// Swallow anything the Writer or Console throws so Log.Warn and
26+
// Log.Debug never throw themselves. If they did, an exception from
27+
// logging inside a catch block would reach the background timer
28+
// and crash the game on modern .NET.
2829
try
2930
{
3031
if (Writer != null)

0 commit comments

Comments
 (0)