Fix GUI freeze on stop and prevent corrupted XDF footers - #146
sappelhoff wants to merge 4 commits into
Conversation
01f99fe to
c5d3038
Compare
|
Thanks for tackling the stop latency. I recommend addressing these before merging:
Review scope: source inspection against the PR head and liblsl v1.18.0.b3, plus the isolated condition-variable reproduction; I did not run the full integration script. |
…ples Follow-up to the interruptible-teardown work, addressing review feedback. Shutdown flags are now published under the mutex that the condition variable predicates read them under. Setting an atomic outside that mutex and then notifying leaves a window in which a waiter that has just evaluated its predicate as false enters the wait and misses the notification, so the offset thread could still sleep out its full five-second interval. The blocking calls that a stop could not interrupt are now issued in short slices that observe the shutdown flag: - stream_inlet::info() was called twice with the default infinite timeout. close_stream() only stops the data receiver, so an unreachable metadata endpoint blocked a stop indefinitely. The info is now fetched once and reused for the header and the nominal rate. - open_stream() could hold a stop for up to max_open_wait. - time_correction() could hold it for the full query timeout. - The watchlist resolver blocked for a whole resolve_interval; it now resolves briefly and waits out the rest interruptibly. - The phase gates could park a stream for max_headers_wait with no way out, so a stream could lose its footer waiting for one that had hung. Joining is bounded for the first time: try_join_once() called std::thread::join(), which has no timeout, so polling it could never enforce max_join_wait. Threads are now paired with a future that becomes ready when the body returns, which can be waited on with a deadline. Closing the inlets the moment stop is pressed discards everything still buffered in them; a recording of 40 markers came back with 2. Inlets are now closed only after the stream threads have been given a grace period to drain and write their footers, and the transfer loop does a final non-blocking pull on the way out, so a stop no longer costs samples that had already arrived. Also fixed along the way: record_offsets() wrote uninitialised offset and timestamp values into the file when a time correction query timed out; the inlet bookkeeping leaked a registration on every exception path; and a stream that failed mid-recording was left without a footer although its header was already on disk. scripts/test_recording_teardown.py now covers a plain stop, a stop before the first sample, a stop while subscribing to a source that has gone away, repeated start/stop cycles, and that no buffered sample is lost. It checks the exit status and the footers of every stream, holds one stated teardown budget instead of documenting one and asserting another, and runs on all three platforms in CI.
|
Thanks — all three held up, and chasing the second one turned up a data-loss regression in my own patch. Pushed as 54ffd28. 1. Missed shutdown notificationsCorrect. The phase gates test 2. Unreachable-stream hangAlso correct, and it went further than the two points you named.
On the joins: right, 3. ValidationRewritten into five cases, run on all three platforms in CI, checking exit status and the footer of every stream (sample count against the data, timestamps parseable) rather than just the EEG one. The budget is one stated number, 1.0 s, in both the docstring and the assertion, overridable with The regression this turned upClosing every inlet the moment stop is pressed discards whatever is still buffered in them. The new case sends 40 markers and stops; against the previous head of this branch 2 of 40 were recorded. Teardown is now staged — Three unrelated defects fixed while in there: What the tests do not showTwo limits worth being explicit about:
Left alone, deliberately
|
Problem
When stopping a recording,
recording::~recording()executes synchronously on the Qt UI thread. Becauserecord_offsets()used an uninterruptible 5-second sleep (std::this_thread::sleep_for(offset_interval)) andboundary_threadjoined with a 15-second timeout, the Qt event loop blocked, causing Windows to mark the window as "(Not Responding)" for 5–15+ seconds. Forcefully terminating the application when frozen aborted before stream footers could be written, resulting in corrupted XDF footers and truncated files.Solution
std::condition_variable shutdown_cv_inrecordingand replacedstd::this_thread::sleep_for/sleep_untilacrossrecord_offsets(),record_boundaries(), andtyped_transfer_loop()with condition variable waits. When shutdown is triggered, all worker threads wake up in < 1 ms.lsl::stream_inletinstances and invokein->close_stream()on shutdown to abort any blocking TCP socket calls or unreachable remote network routes immediately.in->pull_sample()timeout from 4.0s to 0.1s so silent or late-starting streams checkshutdown_rapidly.scripts/test_recording_teardown.pyto verify thatLabRecorderCLIstops in < 0.5s and produces fully valid, uncorrupted XDF files with intact footers.Verification
.xdfparses cleanly withpyxdf.