|
Uni20
Asynchronous tensor and runtime library.
|
TaskRegistry is the async runtime introspection layer used for deadlock and lifecycle diagnostics.
This document explains what it tracks, how to enable it, and how to interpret presentation reports and Graphviz snapshots.
-DUNI20_DEBUG_ASYNC_TASKS=ON-DUNI20_DEBUG_DAG=ON-DUNI20_ENABLE_STACKTRACE=ON when supported; this is the fresh-build defaultCMake defaults UNI20_ENABLE_STACKTRACE to the result of a compile-and-link probe. If unavailable, it defaults to OFF and the build continues with degraded output and an explicit warning. Forcing it ON with an unsupported toolchain is a configuration error; setting it OFF explicitly disables otherwise available stacktrace capture.
UNI20_DEBUG_DAG enables async value nodes and coroutine argument dependency capture. It requires the task registry and therefore enables UNI20_DEBUG_ASYNC_TASKS.
For a complete build-capability check and guided demonstration, build and run:
GCC 14 is a useful baseline for libstdc++ std::stacktrace; CMake still performs the authoritative compile-and-link capability probe for the selected compiler and standard library.
Environment variable: UNI20_DEBUG_ASYNC_TASKS
0, none, off, false, no -> no dump1, basic, on, true, yes -> basic diagnostics2, full, all, verbose -> full diagnosticsUnset or unknown values currently default to basic.
The registry is observational. It stores task identities, transitions, stack information, and dependency associations, but it never owns a coroutine frame or keeps one alive. A registry entry for a suspended task does not mean that the scheduler owns or has queued that task.
Each task record includes:
constructed, running, suspended, leaked)Each epoch context record includes:
Dump output correlates tasks with epochs at dump time by scanning epoch snapshots:
This gives useful suspension context without requiring high-overhead always-on edge tracking.
NodeInfo allocations currently survive for the process lifetime. They retain diagnostic metadata, not ownership of tensor values or coroutine frames. This policy is intentional; see sanitizer boundaries for the narrow leak-checking suppression. There is no metadata cleanup API.
Reclamation is deferred. Cleaning up only at program exit would provide little practical benefit. A useful future operation would reclaim metadata during a long calculation without adding reference counting to every diagnostic edge or disturbing the execution being debugged.
A diagnostic allocation arena would make allocation and bulk release simple, but switching allocation to a new arena does not make the old one disposable. Live Async<T> values, epochs, coroutine dependency lists and in-progress snapshots can still refer to old nodes. Scheduler completion alone does not end these lifetimes. Keeping arenas alive through shared ownership would introduce arena-wide reference counting; one long-lived value could retain the entire old arena. That is not the preferred direction merely to remove sanitizer reports.
Two options remain worth investigating if diagnostic memory growth becomes a practical problem:
The second option is the more useful candidate for a continuing calculation, not an approved design. It requires an audit of all references, including coroutine promises, epochs, aliases and temporary snapshot state. Node names must not accidentally keep dead nodes alive; stale label entries would need removal. Diagnostic pointers held outside the registry also need an explicit lifetime contract. Cleanup must exclude concurrent submission, resumption, async-object mutation and diagnostic capture across the affected runtime; suspended tasks remain live references. These requirements cannot currently be replaced by a call to one scheduler's run_all().
Any implementation should demonstrate that live values and subsequent snapshots remain valid, repeated cleanup reclaims dead metadata, and normal debug execution does not acquire per-edge ownership overhead. No allocation arena, collection mechanism or new synchronization is implemented by this proposal.
TaskRegistry::graphviz_dot() returns a Graphviz DOT document for the current async runtime context. Internally this is a renderer over a structured TaskRegistry::GraphSnapshot plus TaskRegistry::GraphDiagnostics, so callers can inspect the same data model without parsing DOT. The graph includes:
UNI20_DEBUG_DAG=ONReadBuffer/WriteBuffer parametersco_await edges for awaitables that expose async value metadataUNI20_HAS_STACKTRACE=1Async value node labels show storage identity and current construction state. storage=0x... is the shared_storage<T> control-block identity; state=constructed means a T object exists at snapshot time, state=unconstructed means the storage exists but does not currently hold a constructed T, and state=invalid means no storage control block was available. Shape-like objects that expose mdspan().extents() or extents() may also show value=shape=(...); built-in scalar values show their value. Other constructed objects omit the value=... line by default. Custom types can provide a one-line uni20_async_debug_value(T const&) overload in their own namespace to override the default.
Async values and coroutine tasks can be labelled explicitly for diagnostic output:
These labels are optional. Unlabelled nodes keep the generic data N and task N labels, and labels do not affect scheduling, dependency tracking, or coroutine lifetime.
When stacktrace support is available, task and epoch labels include compact source locations such as created_at=examples/foo.cpp:31, scheduled_at=examples/foo.cpp:42, and awaiting_at=examples/foo.cpp:57. Concrete co_await edges can also show the source line where the dependency was observed. Full stacktraces are kept out of visible labels and emitted as Graphviz tooltips. DOT tooltip lines are wrapped to 100 display cells because xdot does not wrap its Pango tooltip labels itself. The number of frames serialized into snapshots and DOT tooltips is controlled by TaskRegistry::StacktraceOptions, which can be set programmatically with TaskRegistry::set_stacktrace_options(...) or reset from environment defaults with TaskRegistry::reset_stacktrace_options(). Set max_frames=0 to keep compact locations while omitting full stacktrace text; set it to std::numeric_limits<std::size_t>::max() for uncapped traces. This provenance is best-effort and compiler/debug-info dependent. Without stacktrace support, DAG structure and diagnostics still work; the provenance fields are simply empty and omitted from DOT labels.
Diagnostic annotations are intentionally conservative. A missing-writer annotation means a task is currently blocked reading an epoch with no visible writer task or writer activity in the registry snapshot. A dependency-cycle annotation is inferred from current blocked-read wait edges plus tracked producer edges; it identifies a likely cycle to inspect, not a proof of terminal font- or scheduler-dependent behavior.
Use TaskRegistry::graphviz_dot_best_effort() or TaskRegistry::dump_graphviz_file_best_effort(path) for deadlock/debugger paths. The best-effort path avoids indefinite waits on registry and epoch locks; if a lock is busy, the DOT output marks that part of the snapshot as unavailable.
Use TaskRegistry::snapshot() when program code wants structured task, epoch, and async-value records. TaskRegistry::diagnose_snapshot(snapshot) derives blocked task, missing-writer, and dependency-cycle annotations from that immutable snapshot. task_registry_report(snapshot, diagnostics) produces a presentation-native terminal/plain-text report from the same records; it does not require parsing DOT. TaskRegistry::graphviz_dot(snapshot, diagnostics) renders a pre-captured snapshot, which is useful when another subsystem needs both structured diagnostics and a DOT artifact from the same point in time.
An exception escaping an AsyncTask or CudaTask coroutine reaches the shared TaskPromiseBase::unhandled_exception() implementation while that task is still present in the registry. The promise marks the task failed and records the exception summary before propagating the exception to writer sinks. Automatic diagnostics are debug-registry-only and opt-in:
The policy is process-wide rather than thread-local because a coroutine may resume on a different worker thread. Only the coroutine that originates an exception triggers automatic capture; downstream coroutines that merely rethrow the injected exception are still marked failed in later live snapshots but do not each write a duplicate file. “Unhandled” here has the standard coroutine meaning of escaping one coroutine body. The exception may still be intentionally observed later at an Async<T> boundary.
The snapshot APIs are intended for diagnostics at normal program checkpoints, deadlock handlers, debugger calls, or controlled interruption paths. They are not async-signal-safe signal handlers.
For normal program control, call one of:
TaskRegistry::graphviz_dot()TaskRegistry::dump_graphviz_file(path)TaskRegistry::request_graphviz_dump() followed by TaskRegistry::service_debug_requests()DebugScheduler, TbbScheduler, and TbbNumaScheduler service queued dump requests at scheduler progress points. This lets instrumentation request a dump without performing file I/O on the requesting path. A caller can still service explicitly when it needs a specific output directory or deterministic timing.
For external triggers, start the optional diagnostics service:
The service owns a background thread. It writes best-effort DOT files when a request file appears, when the configured signal is received, or when program code calls request_graphviz_dump(). A signal is consumed with sigtimedwait on Linux rather than by doing work inside a signal handler. Setting UNI20_DEBUG_DAG_SIGNAL automatically starts the service during program initialization, allowing the signal to be blocked before namespace-scope users and worker threads are created. Without that environment variable, callers must start the service explicitly before creating threads that should inherit the blocked signal mask.
Default output and service settings can be configured with environment variables:
| Variable | Meaning |
|---|---|
UNI20_DEBUG_DAG_OUTPUT_DIR | Default DOT output directory; defaults to /tmp |
UNI20_DEBUG_DAG_FILE_PREFIX | Default DOT filename prefix; defaults to uni20-dag |
UNI20_DEBUG_DAG_DUMP_ON_EXCEPTION | Enable a live registry report and DOT snapshot when an exception originates in an async coroutine; defaults to off |
UNI20_DEBUG_DAG_REQUEST_FILE | Control file consumed by start_diagnostics_service() |
UNI20_DEBUG_DAG_SIGNAL | Signal for the service, e.g. SIGUSR1, USR2, or a number; setting it starts the service during program initialization |
UNI20_DEBUG_DAG_POLL_MS | Diagnostics-service poll interval in milliseconds |
UNI20_DEBUG_DAG_STACKTRACE_FRAMES | Maximum stack frames retained in snapshots and DOT tooltips; 0 hides trace text, all/full/unlimited/max disables the cap |
UNI20_DEBUG_DAG_STACKTRACE_INTERNAL_FRAMES | Whether stacktrace text includes internal Uni20/libstdc++ frames; defaults to true |
TaskRegistry::default_graphviz_dump_path() produces paths of the form <dir>/<prefix>.<pid>.<sequence>.dot.
xdot can display wrapped C++ templates and references correctlyPresentation output queries the actual destination stream. A terminal uses its detected width; redirected output uses UNI20_FALLBACK_TERMINAL_WIDTH (132 by default) unless COLUMNS overrides it. UNI20_COLOR, NO_COLOR, UNI20_GLYPHS, and UNI20_CHARSET provide the normal presentation controls.
| API | Purpose |
|---|---|
TaskRegistry::dump() | full global dump (deadlock triage path) |
TaskRegistry::dump_epoch_context(epoch, reason) | focused dump for one epoch |
TaskRegistry::snapshot() | captures the current task/epoch/value graph as structured records |
TaskRegistry::snapshot_best_effort() | captures a structured snapshot without indefinite lock waits |
TaskRegistry::diagnose_snapshot(snapshot) | derives blocked-task, missing-writer, and cycle annotations |
task_registry_report(snapshot, diagnostics) | builds a presentation-native report from a captured snapshot |
TaskRegistry::graphviz_dot() | returns current task/epoch/value DAG as Graphviz DOT |
TaskRegistry::graphviz_dot_best_effort() | returns a partial DOT snapshot without indefinite lock waits |
TaskRegistry::graphviz_dot(snapshot, diagnostics) | renders a captured snapshot and annotations as DOT |
TaskRegistry::dump_graphviz(stream) | writes DOT to a C stream, stderr by default |
TaskRegistry::dump_graphviz_file(path) | writes DOT to a file |
TaskRegistry::dump_graphviz_file_best_effort(path) | writes a best-effort DOT snapshot to a file |
TaskRegistry::coroutine_exception_diagnostics_options() | returns the active process-wide exception diagnostics policy |
TaskRegistry::set_coroutine_exception_diagnostics_options(options) | enables, disables, or redirects automatic exception diagnostics |
TaskRegistry::reset_coroutine_exception_diagnostics_options() | restores the environment-derived exception diagnostics policy |
TaskRegistry::default_stacktrace_options() | returns stacktrace formatting defaults from environment variables |
TaskRegistry::stacktrace_options() | returns active stacktrace formatting options |
TaskRegistry::set_stacktrace_options(options) | updates active stacktrace formatting options |
TaskRegistry::reset_stacktrace_options() | resets stacktrace formatting options from environment variables |
TaskRegistry::name_task(handle, label) | assigns an optional diagnostic label to a task node |
TaskRegistry::name_async_value(node, label) | assigns an optional diagnostic label to an async value node |
TaskRegistry::request_graphviz_dump() | queues a dump request for scheduler/service processing |
TaskRegistry::service_debug_requests(options) | writes queued requests on the calling thread |
TaskRegistry::start_diagnostics_service(options) | starts background signal/control-file servicing |
TaskRegistry::stop_diagnostics_service() | stops the background diagnostics service |
TaskRegistry::dump_mode() | current runtime mode |
State hooks are invoked from promise/task runtime paths, so transitions are tied to coroutine-handle operations, not high-level wrapper object usage.
UNI20_DEBUG_ASYNC_TASKS=fullcreated_at, scheduled_at, and awaiting_at locations in DOT labelsco_await elements in xdot to inspect full captured provenanceIf output volume is too large during exception handling paths, use focused epoch dumps (dump_epoch_context) at throw sites and reserve full dump for deadlock endpoints.
dag_debug_examples.mdruntime_model.mdexceptions_and_cancellation.mdschedulers.mdquick_reference.md