Repository navigation
Description
File cleanup can perform blocking I/O while the collector has every Haxe thread stopped. An unreachable output file with buffered bytes reaches fio::finalize -> fio::destroy -> fclose. If flushing waits for a pipe reader or slow storage, the entire GC pause includes that wait.
Reproduced at 920fa85 on Linux x86_64 through WSL, Haxe 4.3.3, g++ 13.3.0, default collector, with HXCPP_GC_PROFILE_COLLECT for phase timing.
A local FIFO provides a controlled flush delay:
- Create a FIFO, open its read end in nonblocking mode, set pipe capacity to 4096 bytes with
F_SETPIPE_SZ, and fill it with 4096 bytes through a temporary native writer. - On a joined, attached native worker, call Haxe
sys.io.File.writeon the FIFO and write one additional byte without closing the output. Joining the worker removes its stack roots. - Start an ordinary native reader thread, independent of the Haxe scheduler. It waits for the chosen delay, drains the full pipe, then reads the final buffered byte.
- Force
cpp.vm.Gc.run(true)and measure its duration. Check that the reader received all 4097 bytes, including the final marker, and remove the FIFO.
Two fresh processes produced:
| Reader delay | Collection duration, run 1 | Collection duration, run 2 |
|---|---|---|
| 0 ms | 0.4939 ms | 0.4214 ms |
| 100 ms | 100.5191 ms | 100.5876 ms |
| 200 ms | 200.5090 ms | 200.4808 ms |
All six data checks passed. For the 200 ms trials, the collector reports 200.08 ms and 200.03 ms in fin, with one finalizer. Setup reports 0.00 ms, and reclaim is below 0.21 ms. This isolates finalizer I/O rather than a large live graph or slow marking. The delays are deliberately controlled, not claims about typical disk or pipe latency.
MarkAll calls hx::RunFinalizers before Collect clears gPauseForCollect and resumes other threads. Putting fclose in a GC-free zone inside this callback alone would not release the collection's global stop.
Consider detaching the file's native cleanup payload while stopped and performing the potentially blocking flush/close after mutators resume. Keep the native stream owned until cleanup completes, prevent double close with the explicit close path, preserve buffered data, and define completion behavior for an explicit Gc.run. This should be scoped to native resource cleanup; moving arbitrary Haxe finalizers out of the existing phase has different rooting and resurrection implications. A bounded queue also needs a policy for cleanup that remains blocked.
Applications should explicitly close output files, but fallback cleanup should not turn one abandoned stream's flush into a pause for every independent worker. The runtime can address this without compiler or stdlib changes.
A local conventional test/regression/FileFinalizerFlush checks that abandoned buffered files are flushed and released, and that a reachable file remains writable across collection. It passes on Windows x64 with default and moving GC, and on Linux x86_64 with default GC. This is correctness coverage for a future change, not a timing assertion. The Linux FIFO probe is separate. No runtime implementation changes were made.
Related: #302 concerns scanning finalizer registrations during minor collections. This report concerns executing a dead file's blocking cleanup during the global pause.
Activity
Close the files the collector finds dead on another thread