Skip to main content

reset_thread_engine

Function reset_thread_engine 

Source
pub fn reset_thread_engine()
Expand description

Free this thread’s accumulated engine state — the three State singletons (STATE, STD_STATE, STY_STATE) and the string-interner arena — returning them to a fresh baseline.

Why this exists. The engine’s roots (STATE, arena::ARENA, …) are #[thread_local] attribute statics. Unlike the thread_local! macro, the attribute does not run destructors on thread exit, so a thread that builds a full engine and then exits leaks it (~110 MB for a typical document). The single-conversion latexml_oxide binary never notices — it runs one conversion and the process exits. But any process that runs many conversions across many threads (notably the test harness, where libtest spawns a fresh thread per test) accumulates one leaked engine per conversion (measured: ~4.9 GB across 50_structure, which then trips the per-process RSS fuse in stomach::check_timeout). Resetting between conversions frees that memory before the thread exits (peak fell ~4.9 GB → ~2.9 GB at -j20).

Why reset the interner here. The interner could be kept across conversions — that is the faithful daemon design (Perl keeps its symbol table and resets only the binding stack via pushDaemonFrame/popDaemonFrame in LaTeXML.pm), and re-interning the same ~110k base symbols next conversion is deduped. But that only pays off when the same thread handles multiple conversions. The test harness gets a fresh thread per test, so its interner can never be reused — keeping it would just leak it on thread exit. So we reset it too. A future thread-reusing daemon should instead keep the interner by calling state::reset_thread_state alone (State only).

Soundness. Resetting the interner invalidates every live SymStr on the thread, so this is sound only between fully independent conversions — when the prior conversion’s output has already been serialized to owned data and nothing will read a pre-reset symbol again. The test harness satisfies this (each test serializes to owned Strings, then resets before its thread exits). It does not reclaim libxml2’s process-global C state (parser dictionaries) — that residual (~24 MB/test) is left as-is rather than risk the global xmlCleanupParser.

THREAD-LIFECYCLE CONTRACT (PR_READINESS review): several SymStr-holding statics are NOT reset here — pin! call-site OnceCells (no registry; unresettable by design), gullet DEFERRED_COMMANDS, dump-reader CURRENT_LOAD_CTX, package-local caches. They are safe only because no thread READS a pre-reset SymStr after arena::reset(): libtest runs one test per thread, and the persistent cortex_worker never resets. Any future daemon/thread-pool that resets AND reuses a thread resurrects the phantom-symbol-aliasing bug class (see the REPORT-map fix, 7b64a48ad1) in an unfixable form — redesign the pin! cache before doing that.