Skip to content

Fix VTIME_PROFILING-only part of the code so that TimeTrace::printPretty() does not crash in the timer thread when something else is happening in the main thread - #928

Open
quickbeam123 wants to merge 1 commit into
masterfrom
martin-timetrace-race
Open

Conversation

@quickbeam123

Copy link
Copy Markdown
Collaborator

More details in the commit message ...

limitReached() runs on timer_thread and calls env.statistics->print(), which
reaches TimeTrace::printPretty() (Shell/Statistics.cpp:339), while the main
thread is still proving and still mutating the time trace. Three of the
interactions were memory-unsafe:

- printPrettyRec() sorted `children` *in place*. That is a
  Lib::Stack<unique_ptr<Node>> which the main thread's ScopedTimer constructor
  scans and appends to; a concurrent append reallocates the array std::sort is
  writing into, and the main thread can meanwhile read a moved-from unique_ptr.
- printPretty() iterates `_stack` twice while every TIME_TRACE scope pushes and
  pops it, ~10^9 times per run, reallocating it under the iterator.
- Node carried USE_ALLOCATOR(Node), and Lib::Stack::expand uses ALLOC_KNOWN;
  both route to GLOBAL_SMALL_OBJECT_ALLOCATOR, which is plain free lists with no
  synchronisation (Lib/Allocator.hpp). Node::flatten() therefore allocated dozens
  of nodes on timer_thread while the main thread allocated clauses from the same
  free lists. This corrupts the heap silently and faults later, which is why it
  only appeared under load.

Fix: freeze the trace rather than stop the thread, so the main thread needs no
new check anywhere. _enabled becomes atomic and limitReached() clears it (plus a
one-tick settle) before reporting; ScopedTimer remembers in _active whether it
actually pushed, so a frozen trace is never written to again, not even by
already-open scopes. printPrettyRec() orders a local vector<Node*>, and the whole
TimeTrace subsystem now uses std::vector and the system allocator, so
timer_thread never enters the prover's pool.

Storing _active also fixes a latent bug: the destructor used to re-read _enabled,
so any flip between construction and destruction unbalanced _stack and
mis-attributed every subsequent scope.

limitReached() additionally flushes std::cout, which terminateImmediately()
(std::_Exit) does not -- the tail of the report was lost whenever stdout was a
file rather than a tty.

Reproducer: a full TPTP sweep (26504 problems, -i 100000 -tstat on, 120 jobs in
parallel) produced 2450 logs containing "Aborted by signal" -- 2120 SIGSEGV plus
SIGBUS/SIGABRT -- roughly one in five of the instruction-limited runs, with 2239
further logs losing their profile section to truncated or interleaved output. In
all 2450 the abort line lands *inside* the time-trace output (1676 inside the
flattened profile, 774 inside the trace tree, none before it). Count these with
`grep -la`: plain `grep -l` skips the interleaved logs as binary and undercounts
to 1900. No small deterministic reproducer; the same code path is reached
locally by `vampire -tstat on -t 3 <any problem that will not finish>`, but an
idle machine does not hit the race often enough to be a test.

Verified: unit tests 100/100, checks/sanity clean against a release build, and
every node's `cnt` on AGT001+1.p identical before and after (the flattened
profile's ordering of near-equal entries varies run to run in the old binary
too).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@MichaelRawson MichaelRawson left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The "set a flag and wait for a bit" approach feels like a mutex done badly. Did you try a mutex? Otherwise no objection.

Comment thread Lib/Timer.cpp

// terminateImmediately is std::_Exit, which does not flush: without this the tail of
// the report is lost whenever stdout is a (fully buffered) file rather than a tty.
std::cout.flush();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We could consider putting this into terminateImmediately.

Comment thread Debug/TimeProfiling.cpp
// Order a local copy rather than sorting `children` in place: this is called on the
// timer thread while the main thread may still be scanning and appending to
// `children` (Lib/Timer.cpp, limitReached()), and an in-place sort would move
// elements under it. Also makes printing idempotent.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The whole point of acquiring the lock is that this part is not multi-threaded, no?

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.

2 participants