Recover main thread producer orphaned by cross-module init order.

ELF init_priority only orders constructors within a single module. All of
a shared object's initializers run before any of the executable's, so an
instrumented dependency .so emitting a zone from its static initializer
creates the main thread producer token against the zero-initialized
s_queue. The queue constructor then resets the producer list, orphaning
that producer: every zone emitted on the main thread from that point on
is enqueued into blocks no consumer ever iterates and silently lost,
while sampling (worker thread producer) keeps working.

Re-link such a producer right after the queue is constructed. In the
common case, where nothing was emitted during shared object init, this
merely constructs the main thread token eagerly.
This commit is contained in:
Bartosz Taudul
2026-06-11 19:30:58 +02:00
parent 7180ea381f
commit dfded9d55d
2 changed files with 31 additions and 0 deletions

View File

@@ -1425,6 +1425,22 @@ std::atomic<int> init_order(102) RpInitLock( 0 );
thread_local bool RpThreadInitDone = false;
thread_local bool RpThreadShutdown = false;
moodycamel::ConcurrentQueue<QueueItem> init_order(103) s_queue( QueuePrealloc );
# ifndef _MSC_VER
// An instrumented shared object may emit zones from its static initializers, which the
// dynamic loader runs before any of the executable's constructors, including the
// priority-ordered constructor of s_queue above. The main thread producer token (s_token)
// is then lazily created against the zero-initialized queue memory, and the queue
// constructor subsequently orphans it, making all zones emitted on the main thread
// invisible to the consumer. Re-adopt such a producer here. If no zones were emitted up
// to this point, this only triggers construction of s_token, which is a no-op repair.
struct EarlyMainThreadTokenRepair
{
EarlyMainThreadTokenRepair() { if( s_token.ptr ) s_queue.readopt_orphaned_producer( s_token.ptr ); }
};
static EarlyMainThreadTokenRepair init_order(104) s_earlyMainThreadTokenRepair;
# endif
std::atomic<uint32_t> init_order(104) s_lockCounter( 0 );
std::atomic<uint8_t> init_order(104) s_gpuCtxCounter( 0 );

View File

@@ -1210,6 +1210,21 @@ private:
return static_cast<ExplicitProducer*>(token.producer);
}
// If a producer token is created before the constructor of a statically allocated
// queue runs (which may happen due to the undefined order of static initialization
// across module boundaries), the constructor will orphan it by resetting the
// producer list. Such a producer is functional, as producer creation works on the
// zero-initialized queue memory, but the consumer is not able to see the data it
// enqueues. This method links the producer back into the list.
bool readopt_orphaned_producer(ExplicitProducer* producer)
{
for (auto ptr = producerListTail.load(std::memory_order_relaxed); ptr != nullptr; ptr = ptr->next_prod()) {
if (ptr == static_cast<ProducerBase*>(producer)) return false;
}
add_producer(static_cast<ProducerBase*>(producer));
return true;
}
private:
//////////////////////////////////