Skip to content

fix(streaming): scale reconstructed timestamps by the device's own clock frequency (closes #36) - #37

Merged
cptkoolbeenz merged 2 commits into
mainfrom
fix/streaming-timestamp-frequency
Jul 27, 2026
Merged

cptkoolbeenz merged 2 commits into
mainfrom
fix/streaming-timestamp-frequency

Conversation

@cptkoolbeenz

Copy link
Copy Markdown
Member

What

Ports the applicable half of upstream desktop #770 (b469ca1) — the streaming timeline was reconstructed against the wrong clock.

Closes #36.

Bug

AbstractStreamingDevice.ProcessStreamMessage calls _timestampProcessor.ProcessTimestamp(deviceId, ...) but never calls SetTimestampFrequency, so Core's processor reconstructs against its 20 ns fallback tick (50 MHz, the legacy clock) while current firmware runs the streaming timestamp timer at 42 MHz. The result is a ~16% timeline compression that drifts without bound (each timestamp is the previous plus elapsed ticks) and is persisted to the DB (TimestampTicks).

Fix

Apply the device's real TimestampFrequency to the processor once per session, guarded by a new _timestampFrequencyApplied flag reset alongside both _timestampProcessor.ResetAll() sites (InitializeStreaming + StopStreaming — Core drops per-device frequencies on reset). Applied in ProcessStreamMessage where the device-id key is known to match the one ProcessTimestamp uses.

SetTimestampFrequency(string, uint) has existed in Core since 1.0.0 (#236), so this is independent of the Core 1.3.0 bump. The port's own uses of TimestampFrequency (leftover-frame detection at IsLeftoverFrameFromPreviousSession / ValidateFirstFramesWithoutReference) are untouched — they already used the real frequency and operate on a separate path.

Not ported (N/A)

#770's other half (coreSample.Timestamp → _currentFrameTimestamp) does not apply: the port self-decodes frames in ProcessStreamMessage and already stamps every channel sample with the single per-frame messageTimestamp (it never adopted Core's per-channel OnCoreChannelSampleReceived pipeline), so there is no coreSample.Timestamp divergence.

Validation

Desktop head builds 0 errors. The streaming pipeline (discover → connect → stream 16ch@100Hz → plot) was validated live on-device as part of the Core 1.3.0 work; this change adds one guarded SetTimestampFrequency call on that same path. Part of the core/desktop → Avalonia sync sweep.

…ock frequency

Ports the applicable half of upstream desktop #770. ProcessStreamMessage called
_timestampProcessor.ProcessTimestamp but never SetTimestampFrequency, so Core's
processor reconstructed the timeline against its 20 ns fallback tick (50 MHz)
while current firmware runs the streaming timestamp timer at 42 MHz -- a ~16%
compression that drifts without bound and lands in the DB (TimestampTicks).

Apply the device's real TimestampFrequency to the processor once per session,
guarded by a _timestampFrequencyApplied flag reset alongside both ResetAll()
sites (InitializeStreaming + StopStreaming), which per Core's contract also drop
per-device frequencies. SetTimestampFrequency has existed since Core 1.0.0, so
this is independent of the Core 1.3.0 bump.

#770's other half (coreSample.Timestamp -> _currentFrameTimestamp) does not
apply: the port self-decodes frames and already stamps every channel sample with
the single per-frame messageTimestamp (it never adopted Core's per-channel
OnCoreChannelSampleReceived pipeline), so there is no divergence to fix.

Desktop head builds 0 errors.

Closes #36.

Claude-Session: https://claude.ai/code/session_019fhAAGrHshwojdLJ99DUhc
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix streaming timestamps by applying device clock frequency to TimestampProcessor

🐞 Bug fix 🕐 10-20 Minutes

Grey Divider

AI Description

• Apply device-reported timestamp clock frequency when reconstructing streaming timestamps.
• Reset per-session frequency application state when streaming starts/stops.
• Prevent ~16% timeline compression/drift caused by Core’s 50MHz fallback tick.
Diagram

graph TD
  Dev{{"Device firmware"}} --> Core["CoreDevice status"] --> ASD["ProcessStreamMessage"] --> TP["TimestampProcessor"] --> DB[("DB: TimestampTicks")]

  subgraph Legend
    direction LR
    _ext{{"External"}} ~~~ _cmp["Component"] ~~~ _db[("Database")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Apply frequency during InitializeStreaming()
  • ➕ Avoids a per-message guard branch in the hot path
  • ➕ Keeps streaming-session setup in one place
  • ➖ Requires a guaranteed correct device-id key at init time (risk of mismatch vs message.DeviceSn keying)
  • ➖ TimestampFrequency may still be 0 at init if status hasn’t arrived yet, requiring additional waiting/refresh logic
2. Fix in Core: have TimestampProcessor read frequency from device metadata automatically
  • ➕ Eliminates duplicated ‘apply frequency’ responsibilities across clients
  • ➕ Hardens the API against future call-site omissions
  • ➖ Requires a Core change/release and coordination across consumers
  • ➖ May be non-trivial if Core intentionally keeps TimestampProcessor stateless or caller-configured

Recommendation: Current approach is the best fit for this repo: applying SetTimestampFrequency in ProcessStreamMessage guarantees the correct per-device key (from the message) and ensures the frequency is present before the first ProcessTimestamp call. Resetting the guard alongside both ResetAll() sites matches Core’s contract and avoids stale/cleared frequency state across sessions.

Files changed (1) +21 / -0

Bug fix (1) +21 / -0
AbstractStreamingDevice.csApply device TimestampFrequency to Core timestamp reconstruction per session +21/-0

Apply device TimestampFrequency to Core timestamp reconstruction per session

• Introduces a per-session guard (_timestampFrequencyApplied) and applies the device’s TimestampFrequency to the Core TimestampProcessor before processing stream timestamps. Resets the guard whenever the processor is ResetAll()’d during InitializeStreaming and StopStreaming to ensure correct behavior across streaming sessions.

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs

@qodo-code-review

qodo-code-review Bot commented Jul 27, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Unsynced session frequency state 🐞 Bug ☼ Reliability
Description
_timestampFrequencyApplied and _timestampProcessor are accessed from the fire-and-forget inbound
message pipeline while InitializeStreaming/StopStreaming reset them, with no shared lock/session
token to establish ordering. This can race such that a new session observes a stale "applied" state
(or sees ResetAll after applying), leaving the processor on fallback frequency for that session and
reintroducing incorrect timestamp reconstruction.
Code

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[R596-606]

+        // Apply the device's real streaming-clock frequency to Core's processor once per session.
+        // Without it the processor reconstructs against its 20 ns fallback tick (50 MHz) while current
+        // firmware runs the timer at 42 MHz, compressing the reconstructed timeline ~16% and drifting
+        // without bound (each timestamp is the previous plus elapsed ticks). Applied here, where the
+        // device-id key is known to match the one ProcessTimestamp uses, and re-applied per session
+        // because InitializeStreaming's ResetAll() drops per-device frequencies with the baselines.
+        if (!_timestampFrequencyApplied && TimestampFrequency != 0)
+        {
+            _timestampProcessor.SetTimestampFrequency(deviceId, TimestampFrequency);
+            _timestampFrequencyApplied = true;
+        }
Relevance

⭐⭐⭐ High

Team previously accepted adding locks/tokens to prevent cross-thread races on shared session state.

PR-#26

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Inbound messages are dispatched via a non-awaited async call, so stream processing can overlap with
UI-driven start/stop that resets the processor. The new flag is neither volatile nor protected by a
shared synchronization mechanism, while other similar cross-thread flags in this class are volatile,
indicating multi-threaded access patterns.

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[528-543]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[587-616]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[1079-1158]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[99-120]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs[280-319]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ProcessStreamMessage()` may run concurrently with `InitializeStreaming()` / `StopStreaming()` because inbound messages are routed via a fire-and-forget async handler. The new `_timestampFrequencyApplied` gate and `_timestampProcessor.ResetAll()` are not synchronized, so the frequency application can be skipped or mis-ordered relative to resets.

### Issue Context
- `OnInboundMessageReceived` calls `_protocolHandler.HandleAsync(...)` without awaiting.
- `ProcessStreamMessage` reads/writes `_timestampFrequencyApplied` and calls `_timestampProcessor.SetTimestampFrequency`/`ProcessTimestamp`.
- `InitializeStreaming` and `StopStreaming` call `_timestampProcessor.ResetAll()` and set `_timestampFrequencyApplied = false`.

### Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[96-120]
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[528-543]
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[587-616]
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[1079-1158]

### Suggested fix
1. Introduce a private lock object (e.g., `_timestampSync = new object();`).
2. Wrap **all** uses/mutations of `_timestampProcessor` and `_timestampFrequencyApplied` under this lock:
  - In `ProcessStreamMessage`: lock around the `if (!_timestampFrequencyApplied ...) { SetTimestampFrequency... }` block **and** the subsequent `ProcessTimestamp` call.
  - In `InitializeStreaming` and `StopStreaming`: lock around `ResetAll()` and `_timestampFrequencyApplied = false`.
3. (Optional) If locking `ProcessTimestamp` is too contentious, use a session-generation counter instead (increment on ResetAll; store the generation used to apply frequency), but keep the ordering explicit and thread-safe.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +596 to +606
// Apply the device's real streaming-clock frequency to Core's processor once per session.
// Without it the processor reconstructs against its 20 ns fallback tick (50 MHz) while current
// firmware runs the timer at 42 MHz, compressing the reconstructed timeline ~16% and drifting
// without bound (each timestamp is the previous plus elapsed ticks). Applied here, where the
// device-id key is known to match the one ProcessTimestamp uses, and re-applied per session
// because InitializeStreaming's ResetAll() drops per-device frequencies with the baselines.
if (!_timestampFrequencyApplied && TimestampFrequency != 0)
{
_timestampProcessor.SetTimestampFrequency(deviceId, TimestampFrequency);
_timestampFrequencyApplied = true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Unsynced session frequency state 🐞 Bug ☼ Reliability

_timestampFrequencyApplied and _timestampProcessor are accessed from the fire-and-forget inbound
message pipeline while InitializeStreaming/StopStreaming reset them, with no shared lock/session
token to establish ordering. This can race such that a new session observes a stale "applied" state
(or sees ResetAll after applying), leaving the processor on fallback frequency for that session and
reintroducing incorrect timestamp reconstruction.
Agent Prompt
### Issue description
`ProcessStreamMessage()` may run concurrently with `InitializeStreaming()` / `StopStreaming()` because inbound messages are routed via a fire-and-forget async handler. The new `_timestampFrequencyApplied` gate and `_timestampProcessor.ResetAll()` are not synchronized, so the frequency application can be skipped or mis-ordered relative to resets.

### Issue Context
- `OnInboundMessageReceived` calls `_protocolHandler.HandleAsync(...)` without awaiting.
- `ProcessStreamMessage` reads/writes `_timestampFrequencyApplied` and calls `_timestampProcessor.SetTimestampFrequency`/`ProcessTimestamp`.
- `InitializeStreaming` and `StopStreaming` call `_timestampProcessor.ResetAll()` and set `_timestampFrequencyApplied = false`.

### Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[96-120]
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[528-543]
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[587-616]
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[1079-1158]

### Suggested fix
1. Introduce a private lock object (e.g., `_timestampSync = new object();`).
2. Wrap **all** uses/mutations of `_timestampProcessor` and `_timestampFrequencyApplied` under this lock:
   - In `ProcessStreamMessage`: lock around the `if (!_timestampFrequencyApplied ...) { SetTimestampFrequency... }` block **and** the subsequent `ProcessTimestamp` call.
   - In `InitializeStreaming` and `StopStreaming`: lock around `ResetAll()` and `_timestampFrequencyApplied = false`.
3. (Optional) If locking `ProcessTimestamp` is too contentious, use a session-generation counter instead (increment on ResetAll; store the generation used to apply frequency), but keep the ordering explicit and thread-safe.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

The flag is written by ProcessStreamMessage (transport thread) and reset by
InitializeStreaming/StopStreaming (UI thread); volatile matches the class's other
cross-thread streaming-state flags (_checkForLeftoverFrames,
_pendingFirstFrameValidation) so the transport thread promptly observes the reset.

Claude-Session: https://claude.ai/code/session_019fhAAGrHshwojdLJ99DUhc
@cptkoolbeenz

Copy link
Copy Markdown
Member Author

Re: "Unsynced session frequency state" — addressed in 82caeda + a follow-up ticket.

  • Fixed the concrete inconsistency: _timestampFrequencyApplied is now volatile, matching the class's other cross-thread streaming-state flags (_checkForLeftoverFrames, _pendingFirstFrameValidation). That was the specific evidence cited (a cross-thread flag that wasn't volatile while its siblings are), and it closes the visibility gap so the transport thread promptly observes the UI thread's reset.

  • Residual ordering race → #39: the deeper concern (a ResetAll on the UI thread interleaving with SetTimestampFrequency on the transport thread) is pre-existing — _timestampProcessor was already accessed cross-thread (ProcessTimestamp on the transport thread vs ResetAll on the UI thread) before this PR — and it faithfully matches upstream desktop's shipped #770 design, which also uses a plain non-locked flag. A proper lock/session-token serialization of the 100 Hz streaming hot path (cf. feat(mobile): logged-session plot viewer + delete/delete-all (#7 phase-2) #26) is a deliberate threading change and a maintainer decision, not part of a faithful #770 port, so it's tracked separately in reliability: serialize cross-thread access to _timestampProcessor in AbstractStreamingDevice #39 rather than expanded here.

@cptkoolbeenz

Copy link
Copy Markdown
Member Author

/agentic_review

@cptkoolbeenz

Copy link
Copy Markdown
Member Author

/improve

@qodo-code-review

Copy link
Copy Markdown

PR Code Suggestions ✨

Warning

/improve is deprecated. Use /agentic_review instead (removal date not yet scheduled).

CategorySuggestion                                                                                                                                    Impact
Possible issue
Synchronize timestamp state transitions

Introduce a lock to ensure thread-safe and atomic operations between timestamp
frequency application, processing, and state resets.

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs [10-43]

-private volatile bool _timestampFrequencyApplied;
+private readonly object _timestampProcessorLock = new();
+private bool _timestampFrequencyApplied;
 
 ...
 
-if (!_timestampFrequencyApplied && TimestampFrequency != 0)
+TimestampResult timestampResult;
+lock (_timestampProcessorLock)
 {
-    _timestampProcessor.SetTimestampFrequency(deviceId, TimestampFrequency);
-    _timestampFrequencyApplied = true;
+    if (!_timestampFrequencyApplied && TimestampFrequency != 0)
+    {
+        _timestampProcessor.SetTimestampFrequency(deviceId, TimestampFrequency);
+        _timestampFrequencyApplied = true;
+    }
+
+    timestampResult = _timestampProcessor.ProcessTimestamp(deviceId, message.MsgTimeStamp);
 }
-
-// Use Core's TimestampProcessor for rollover handling
-var timestampResult = _timestampProcessor.ProcessTimestamp(deviceId, message.MsgTimeStamp);
 
 ...
 
-_timestampProcessor.ResetAll();
-_timestampFrequencyApplied = false;
+lock (_timestampProcessorLock)
+{
+    _timestampProcessor.ResetAll();
+    _timestampFrequencyApplied = false;
+}

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: Identifies a valid race condition where concurrent threads could observe inconsistent state between the volatile flag and the timestamp processor during a reset.

High
  • More

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 82caeda

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.

fix(streaming): scale reconstructed timestamps by the device's own clock frequency

1 participant