Did nix just mass-spawn 200 build processes across 47 derivations, max out your cores, eat 32GB of RAM, and then disappear before you could figure out which nix build caused it? Did the OOM killer take out your browser, your IDE, and your will to live?
Have you ever kill -9'd a nix-daemon fork and prayed? Have you ever wanted to freeze a build, check what it's doing, then let it continue? Have you ever wished you could see which user launched that 3-hour kernel rebuild on your shared builder?
Do you run a fleet of remote builders and have no idea what's actually happening on them? Does Hydra tell you a build failed 40 minutes ago but not what went wrong in real time?
nix-analytics is htop for nix. nom, but global, interactive, and with cgroup-level control. A daemon that hooks directly into the nix process via a logger plugin, captures every build event as it happens, and serves it to a terminal UI where you can monitor, freeze, kill, nice, memory-limit, and inspect builds.
Three components:
- C++ plugin (
libnix-analytics.so) -- loaded into the nix process viaplugin-files, wraps the nix Logger, sends structured JSON events over a Unix socket - Rust daemon (
nix-analyticsd) -- receives events, discovers cgroups, polls remote builders, serves a control socket - TUI (
nix-analytics) -- connects to the daemon, renders build state, lets you interact with builds
The plugin captures activity start/stop, phase changes, log lines, progress, post-build-hook output, and remote dispatch events. The daemon correlates these with cgroup data to give you per-build CPU, memory, freeze state, and process trees. The TUI gives you vim-style keybindings to navigate, filter, kill, freeze, and yank build info.
Add the flake and enable the module:
# flake.nix
{
inputs.nix-analytics.url = "github:DieracDelta/nix-btm-mark-2";
outputs = { self, nixpkgs, nix-analytics, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
modules = [
nix-analytics.nixosModules.default
{
services.nix-analytics = {
enable = true;
package = nix-analytics.packages.x86_64-linux.default;
pluginPackage = nix-analytics.packages.x86_64-linux.nix-analytics-plugin;
};
}
];
};
};
}Rebuild, then:
# launch the TUI
nix-analytics
# or query from scripts
nix-analytics-ctl list-builds
nix-analytics-ctl get-history
nix-analytics-ctl snapshotThis sets up everything: the plugin loads into nix-daemon, cgroups are enabled, the daemon runs as a systemd service, sockets live in /run/nix-analytics/.
If you run nix build as root directly (not through nix-daemon), the plugin still loads and sends events. The daemon still discovers cgroups -- it finds them via /proc/<pid>/cgroup for each nix process, which works regardless of whether builds appear under a systemd service cgroup or a user session scope.
Same NixOS module config as above. The only difference is that builds show up under your session's cgroup hierarchy instead of nix-daemon.service.
Rootless nix can't write to /run/nix-analytics/ and the daemon can't run as a system service. You need to point everything at a user-writable directory.
The daemon, TUI, and ctl tool all respect the NIX_ANALYTICS_SOCKET_DIR environment variable. The resolution order is:
NIX_ANALYTICS_SOCKET_DIR(explicit override)$XDG_RUNTIME_DIR/nix-analytics(user-scoped default)/run/nix-analytics(system default)
For rootless nix, set the env var and tell the plugin where to send events:
# In your shell profile
export NIX_ANALYTICS_SOCKET_DIR="$XDG_RUNTIME_DIR/nix-analytics"# In ~/.config/nix/nix.conf
plugin-files = /path/to/libnix-analytics.so
use-cgroups = true
analytics-socket = /run/user/1000/nix-analytics/events.sockThen run the daemon manually (or as a systemd user service):
nix-analyticsdCgroup control caveat: kill/freeze/nice/memory-limit require write access to cgroup files. Under rootless nix, this depends on whether systemd delegates cgroup control to your user. If it doesn't, you can still monitor builds -- you just can't control them via cgroups. The daemon falls back to PID-based signals (SIGKILL/SIGSTOP/SIGCONT) when cgroups aren't available.
The module exposes a socketDir option:
services.nix-analytics = {
enable = true;
socketDir = "/run/custom-analytics";
};This sets NIX_ANALYTICS_SOCKET_DIR on the systemd service and system-wide session variables, creates the directory via tmpfiles, and configures the plugin's analytics-socket in nix.conf.
| Key | Action |
|---|---|
j/k |
Navigate up/down |
Ctrl-u/Ctrl-d |
Half page up/down |
gg/G |
Jump to top/bottom |
K |
Action prompt (then k=kill, f=freeze, u=unfreeze) |
V |
Visual mode (select range) |
y |
Yank prompt (copy build field to clipboard) |
p |
Process view (per-build process tree) |
d |
Dependency tree view |
l |
Toggle log panel |
h |
Toggle history |
r |
Toggle remote machines |
f |
Filter builds |
/ |
Search (in dep tree) |
n/N |
Nice build / search next |
c |
CPU limit |
m |
Memory limit |
a |
Toggle show all activity types |
z then c/o/a |
Fold close/open/toggle |
q |
Quit |
nix develop --command bash -c "cargo test --workspace"This runs all unit and integration tests (~142 tests covering protocol encoding, state management, control server request handling, cgroup parsing, machine parsing, UI rendering, and full pipeline integration).
nix build .#checks.x86_64-linux.vm-e2e --print-build-logsThis spins up two NixOS VMs:
- Standard multi-user -- default
/run/nix-analytics/sockets, verifies empty state, triggers a build, checks history, validatesuser_uidis populated from cgroup discovery - Custom socket directory --
socketDir = "/run/custom-analytics", verifies the daemon binds to the right place, ctl can query it, and builds are tracked
# Build everything
nix build
# Run all checks (unit tests + VM test)
nix flake checkThree VM scenarios for interactive testing:
# Standard multi-user (nix-daemon)
nix run --impure .#dev-vm
# Nix as root, no daemon
nix run --impure .#dev-vm-root
# Daemonless / rootless (user-scoped sockets)
nix run --impure .#dev-vm-daemonlessSSH in with ssh -p 2222 root@localhost (password: root).
dev-vm (default): Standard NixOS with nix-daemon. Run nix build normally, view builds in nix-analytics.
dev-vm-root: nix-daemon is disabled. Run nix build as root directly. The plugin still loads and sends events to the system nix-analyticsd service.
dev-vm-daemonless: Both nix-daemon and the system nix-analyticsd are disabled. Run start-daemon to launch nix-analyticsd with user-scoped sockets, then daemonless-build --expr '...' to trigger builds. Demonstrates the full rootless flow.
All three support hot-reload: build on the host, then run reload (default/root) or start-dev-daemon (daemonless) in the VM.
# Terminal 1
export NIX_ANALYTICS_SOCKET_DIR=/tmp/test-analytics
cargo run --bin nix-analyticsd
# Terminal 2
export NIX_ANALYTICS_SOCKET_DIR=/tmp/test-analytics
cargo run --bin nix-analytics-ctl -- list-builds
# Terminal 3
export NIX_ANALYTICS_SOCKET_DIR=/tmp/test-analytics
cargo run --bin nix-analyticsnix develop
cargo watch -x checkThe devshell includes the Rust toolchain, rust-analyzer, meson/ninja for the C++ plugin, and cargo-watch.
There is no system cargo -- all cargo commands must go through nix develop.