Tracing Dispatcher
A CoroutineDispatcher that wraps delegate and emits a TraceEvent.Queued, TraceEvent.Started and TraceEvent.Finished event to sink for every block dispatched through it. label identifies this dispatcher in the events.
Diagnostic tool, opt-in, not meant for production by default. When the sink is disabled for TraceLevel.Dispatch the original block is forwarded to the delegate untouched: no wrapper and no allocation. Coroutine name and thread identity are read only when the sink is enabled for TraceLevel.Coroutine. Every sink.emit is guarded: a faulty sink can never break dispatch or change the outcome of the block.
Every dispatched runnable gets a process-unique dispatchId, shared by its queued, started and finished events. When the dispatch context carries a TraceSpan, its id, parent id and name are attached to the three events; without one they are null. Reading it and allocating an id happen only when the sink is enabled.
Timestamps are nanoseconds on a monotonic base: the elapsed time since this dispatcher was created. Only differences between timestamps are meaningful.
What it observes, and what it does not. It observes dispatched segments, not all execution. A segment is the run of code between two suspension points that went through dispatch. Not visible:
Dispatchers.Unconfined, and any delegate whoseisDispatchNeededis false (for exampleDispatchers.Main.immediatewhen already on the main thread): the block runs inline and never reaches dispatch.the first segment of
launch(start = CoroutineStart.UNDISPATCHED).withContextto the same dispatcher, which continues inline.the timer callback of Delay itself, which is not a dispatch (the resume that follows is).
Wrapping Dispatchers.Unconfined is pointless, and leaves a dangling TraceEvent.Queued on yield() because the delegate swallows the block. In general consumers must tolerate a TraceEvent.Queued without matching TraceEvent.Started and TraceEvent.Finished. A coroutine cancelled before its first run still emits Started and Finished for a segment that did no user work.
Wrapping also loses the identity of the delegate (the wrapper is not a MainCoroutineDispatcher) and its elasticity and limitedParallelism hints, as InstrumentedDispatcher documents. Calling limitedParallelism on the wrapper creates a view that is not traced separately.
It forwards Delay: when delegate has its own delay implementation (for example a TestDispatcher, whose virtual time must be honored), timers are scheduled on it; otherwise the standard default delay is used. Either way the coroutine resumes through this dispatcher, so the resume is observed.
Example:
val sink = RingBufferSink(256)
val io = TracingDispatcher(Dispatchers.ioCompat, "io", sink)
withContext(io + CoroutineName("fetch")) { api.fetch() }
sink.snapshot().forEach(::println) // Queued, Started, FinishedThread-safety: stateless apart from the immutable creation timestamp; safe to share across coroutines and threads provided sink is thread-safe.
Requires opting in to ExperimentalStructuredCoroutines.
Parameters
dispatcher that actually runs the blocks.
name carried by every emitted TraceEvent.dispatcher.
receiver of the events.