# dial9 Tokio Runtime Profiling

`dial9-tokio-telemetry` records Tokio runtime-level events — poll start/end,
worker park/unpark, task spawn/terminate, and optionally async backtraces of
stalled tasks — into binary trace segments.

It answers questions that Prometheus metrics and `tracing` spans cannot:

- A request took 200 ms. Was a worker thread blocked, or was it waiting on I/O?
- Which task held a worker for 40 ms without yielding?
- Are workers parking because there is no work, or because the queue is starved?

These are the failure modes behind drive stalls, `io_uring`/`O_DIRECT` regressions,
and object-data-cache fill contention. They are invisible to the rest of the obs stack.

## This is a profiler, not telemetry

Three properties follow from that, and all three matter operationally.

**The stock binary cannot run it.** dial9 hooks Tokio's unstable runtime API, which
requires `--cfg tokio_unstable`. Release binaries are built without it, so they neither
depend on Tokio's non-semver surface nor pay its cost. Setting
`RUSTFS_RUNTIME_DIAL9_ENABLED=true` on a stock binary logs a warning and records nothing.

**Traces are written continuously and evicted.** The retained budget is
`MAX_FILE_SIZE × ROTATION_COUNT` (1 GB by default). Once exceeded, the *oldest*
segments are deleted. Under a high poll rate that budget can wrap in minutes,
which means the incident you are chasing may already have been overwritten.
Size the budget against the window you need, and prefer short, targeted runs.

**There is no runtime toggle.** Telemetry is installed when the Tokio runtime is
constructed, so enabling or disabling it requires a process restart.

## Building

```bash
make build-profiling
```

That is `cargo build --release --bin rustfs --features dial9` with
`RUSTFLAGS="--cfg tokio_unstable"`. One optional feature layers on top:

```bash
# Async backtraces of stalled tasks. Linux aarch64/x86/x86_64 only —
# `tokio/taskdump` hard-errors at compile time on other targets, so this
# will not build on macOS.
DIAL9_RUSTFLAGS="--cfg tokio_unstable --cfg tokio_taskdump" \
  DIAL9_FEATURES="dial9-taskdump" make build-profiling
```

`crates/obs/build.rs` fails the build if the `dial9` feature is enabled without
`--cfg tokio_unstable`. This is deliberate: an environment `RUSTFLAGS` *replaces*
the value from `.cargo/config.toml` rather than appending to it, so the flag used
to disappear silently whenever anything else set `RUSTFLAGS`.

For CPU profiling with usable stacks, add `-C force-frame-pointers=yes`.

## Running an investigation

```bash
export RUSTFS_RUNTIME_DIAL9_ENABLED=true
export RUSTFS_RUNTIME_DIAL9_OUTPUT_DIR=/tmp/rustfs-telemetry-investigation
export RUSTFS_RUNTIME_DIAL9_ROTATION_COUNT=3      # 300 MB total, ~short window
export RUSTFS_RUNTIME_DIAL9_TASK_DUMP_ENABLED=true # if built with dial9-taskdump
```

Reproduce the fault, then stop the process **gracefully**. The final buffered
events are flushed when the telemetry guard drops during shutdown; a `SIGKILL`
loses them, and they are usually the interesting ones.

Segments land in `$OUTPUT_DIR/$FILE_PREFIX.N.bin`. Analyse them with upstream's
`TRACE_ANALYSIS_GUIDE.md`.

Turn `RUSTFS_RUNTIME_DIAL9_ENABLED` back off when you are done.

## Configuration

| Variable | Default | Notes |
|---|---|---|
| `RUSTFS_RUNTIME_DIAL9_ENABLED` | `false` | Needs a `dial9` build to take effect |
| `RUSTFS_RUNTIME_DIAL9_OUTPUT_DIR` | `/var/log/rustfs/telemetry` | Must be writable |
| `RUSTFS_RUNTIME_DIAL9_FILE_PREFIX` | `rustfs-tokio` | |
| `RUSTFS_RUNTIME_DIAL9_MAX_FILE_SIZE` | `104857600` | Bytes per segment |
| `RUSTFS_RUNTIME_DIAL9_ROTATION_COUNT` | `10` | Total budget = size × count |
| `RUSTFS_RUNTIME_DIAL9_TASK_DUMP_ENABLED` | `false` | Needs `dial9-taskdump` (Linux only) |
| `RUSTFS_RUNTIME_DIAL9_TASK_DUMP_IDLE_THRESHOLD_MS` | `10` | Mean idle for Poisson sampling |
| `RUSTFS_RUNTIME_DIAL9_S3_BUCKET` | unset | **Not honoured**, see below |
| `RUSTFS_RUNTIME_DIAL9_S3_PREFIX` | unset | **Not honoured**, see below |

Variables whose build feature is absent are ignored, with a warning naming the
feature to rebuild with.

### S3 upload is unavailable

dial9's `worker-s3` feature depends on `aws-sdk-s3-transfer-manager` 0.1.3 (its
latest release), which pins `aws-smithy-http-client` onto `hyper-rustls` 0.24 and
`rustls-webpki` 0.101.7. That webpki carries RUSTSEC-2026-0098, -0099 and -0104,
and the repository's `cargo deny` gate rejects it.

Cargo's feature unification can add features but cannot drop a transitive
dependency, so this cannot be worked around downstream — it needs an upstream
release. RustFS therefore builds no S3 uploader at all. Setting the two variables
above logs a warning and changes nothing; retrieve trace segments from
`OUTPUT_DIR` directly. Tracked as D9-14 in rustfs/backlog#1157.

## Metrics

| Metric | Meaning |
|---|---|
| `rustfs_dial9_supported` | Binary was compiled with telemetry support |
| `rustfs_dial9_configured` | Operator asked for telemetry via the environment |
| `rustfs_dial9_active_sessions` | A session is actually recording (0 or 1) |
| `rustfs_dial9_disk_usage_bytes` | Trace segment bytes on disk, refreshed every 60 s |
| `rustfs_dial9_errors_total` | Telemetry setup failures |

The first three are separate on purpose, because they disagree in exactly the
cases you need to diagnose:

- `configured=1, supported=0` — wrong binary. Rebuild with `make build-profiling`.
- `configured=1, supported=1, active_sessions=0` — telemetry failed to start
  (usually an unwritable `OUTPUT_DIR`) and the process fell back to a standard
  runtime. Check `rustfs_dial9_errors_total` and the startup logs.

The session metrics are not exported at all unless telemetry is running. A counter
pinned at zero reads as "nothing happened", which is worse than a missing series.

### Known gap: writer death is not directly observable

dial9's `RotatingWriter` stops accepting writes (its internal `Finished` state) if
the output directory is removed underneath it or a segment cannot be sealed —
a real risk in containers where `/var/log` gets cleaned. Upstream exposes no way
to observe this: `TelemetryGuard::is_enabled()` reports how the session was *built*,
not whether it is still writing.

There is therefore no `writer_healthy` gauge, because it could only ever be
hard-coded to `1`. Instead, watch `rustfs_dial9_disk_usage_bytes`: a session with
`active_sessions=1` whose disk usage has stopped growing has most likely hit this
state, and needs a restart.
