Skip to content
wkhadgarPublic

About

ZView - A real-time thread (and more!) viewer for Zephyr RTOS. Just like htop, but for Zephyr

Topics

Resources

Code of conduct

Contributing

Stars

54 stars

Watchers

0 watching

Forks

Repository files navigation

ZView, a Zephyr RTOS runtime visualizer

Real-time system observability for Zephyr RTOS, delivered over SWD.

Stop guessing your stack margins and heap health. ZView shows your Zephyr application's runtime from the kernel's own structures, read over SWD, with no code added to the firmware.

No UART, no RTT, and no manual code changes. Just Kconfig and your probe.

Looking for the standalone pip installation? See README_pip.md.


Prerequisites

ZView reads what the build left in the ELF, so any build works, and a few Kconfig options add to what it can show. They ship as a snippet:

west build -S zview
Setting the options by hand

A build that does not go through west build can carry the same options in its own prj.conf:

## prj.conf
CONFIG_THREAD_MONITOR=y                    # Enables threads created at runtime
CONFIG_THREAD_STACK_INFO=y                 # Enables stack sizes
CONFIG_INIT_STACKS=y                       # Enables stack watermarks
CONFIG_THREAD_NAME=y                       # Enables thread name display
CONFIG_THREAD_RUNTIME_STATS=y              # Enables CPU usage tracking
CONFIG_SYS_HEAP_RUNTIME_STATS=y            # Enables heap runtime stats and fragmentation map
CONFIG_MEM_SLAB_TRACE_MAX_UTILIZATION=y    # Enables the memory slab peak

Note: snippets need Zephyr v3.4.0 or newer. On an older tree, set the options in prj.conf instead.

Installation

Add this snippet to your west manifest:

manifest:
  projects:
    - name: zview
      url: https://github.com/wkhadgar/zview
      revision: main
      path: modules/tools/zview
      west-commands: scripts/west-commands.yml

Then fetch it with:

west update zview

How to Use

If your board is already flashed and probed, ZView can often infer the ELF path and runner from the build directory, so just run:

# From project root
west zview

Point it at another build directory, or at another image of a sysbuild build, with -d and --domain:

west zview -d build/other --domain mcuboot

Or pass arguments explicitly:

# Example: nRF5340 DK via JLink
west zview live -e build/zephyr/zephyr.elf -r jlink -t nRF5340_xxAA

Commands

ZView is invoked through one of four commands. Bare zview with no command runs live.

Command Purpose TUI
live Attach to a probe and render the TUI. Default when no command is given. yes
record Capture a live session to a .ndjson.gz recording file and exit. no
replay Render the TUI from a previously captured recording. yes
dump Emit a single polling frame and exit. no

Common arguments

Argument Used by Description
-e, --elf-file all Path to the firmware .elf file (e.g. build/zephyr/zephyr.elf).
-d, --build-dir all Application build directory to read unset -e, -r, -t and --frequency from.
--domain all Sysbuild domain to view (default: the build's default domain).
-r, --runner live, record, dump Debug runner: jlink, pyocd, nrfutil, or gdb.
-t, --runner-target live, record, dump MCU descriptor for the chosen runner (see below).
--frequency live, record, dump SWD clock of -r pyocd and -r jlink, in Hz or with a k or M suffix (default: 4M on pyocd, the J-Link's own on jlink). Lower it if long wires give read errors.
--period live, record, dump Minimum polling period in seconds (default: 0.10). A frame that takes longer is followed by the next one right away.
--usage-window live, replay Sum the CPU and load shares over the last N polls, up to 64 (default: 1, each poll alone).
--match all Read only the rows this line matches, the line / edits (default: *). --match '*, heaps:!*' reads everything but the heaps.

Patterns follow two rules. Excludes always win: threads:worker*, !worker_3 skips worker_3. A group's own includes replace the bare ones: *, heaps:min_* narrows only the heaps.

The groups are threads, heaps, semaphores, mutexes, msgqs, mem_slabs and workqs. Reading fewer rows makes each frame shorter. A recording keeps the selection it was made with, and a replay reads the same rows. On replay and dump -i the line filters only what is shown, and replay starts from the recording's own line. A pattern that matches no row is reported, in the message log on live and replay, and on stderr otherwise.

Command-specific arguments

Argument Command Description
-o, --output record Recording target path (.ndjson.gz).
--duration record Recording upper bound, in seconds.
--frames record Recording upper bound, in data frames.
-i, --input replay, dump Recording source path (.ndjson.gz).
--no-pacing replay Drain the recording as fast as possible instead of honoring its wall-clock cadence.
--frame dump Which polling frame to emit (1-indexed; default: 2, the first with CPU shares).
--json dump Emit the frame as JSON on stdout.
Finding the right value for -t

The -t argument is the MCU descriptor name as expected by your chosen runner. How to find it depends on the runner:

JLink (-r jlink) Use the device name from the J-Link Supported Devices list.

# Example: Nordic nRF5340 DK
west zview live -e build/zephyr/zephyr.elf -r jlink -t nRF5340_xxAA

pyOCD (-r pyocd) Run pyocd list --targets to see the available target names for your installed packs.

pyocd list --targets          # find your target name

# Example: STM32F401 Nucleo
west zview live -e build/zephyr/zephyr.elf -r pyocd -t stm32f401xe

GDB server (-r gdb) Pass the host:port of your GDB server instead of a device name.

west zview live -e build/zephyr/zephyr.elf -r gdb -t localhost:1234

Tip: The runner and target are often already defined in the build directory's runners.yaml (generated by west build). In that case you can omit -r and -t entirely and ZView will pick them up automatically.

Navigation

ZView acts as a TUI. Navigate with UP and DOWN arrows from the default view:

  • ?: Open the contextual help overlay listing every binding active in the current view.
  • m: Open the message log, holding the last reported messages with the time each arrived. The status row is reclaimed by the poll heartbeat, so this is where messages stay readable.
  • ENTER: Get details for the selected thread or kernel object (hit ENTER again, or Esc, to return).
  • S / I: Sort the data and invert the sorting order.
  • k: Access the Kernel Objects view listing semaphores, mutexes, message queues, memory slabs, heaps and work queues (hit k again to return).
  • x: Mute the selected thread or object. It stays listed, dimmed, with its last values, and is no longer read. A muted thread's usage counter still is, since each CPU share is of every thread's cycles. ENTER on a muted row unmutes it, so its detail view is tracked.
  • /: Edit the row patterns on the line --match takes: GLOB, GROUP:GLOB, or !GLOB and GROUP:!GLOB to exclude, comma separated. ENTER applies the line, Esc drops it.
  • + / -: In the thread views, sum the CPU and load shares over twice or half the polls, from 1 up to 64. The bottom bar shows the count above 1.
  • r: Soft refresh, re-walks the kernel thread list and clears runtime baselines.
  • R: Full reload, tears down the polling thread, disconnects the probe, re-parses the ELF, reconnects, checks the target runs that ELF, and resumes. After a rebuild, press it once the target runs the new image. It also unmutes every row.

The bottom bar names the groups turned off, the patterns and the muted rows. On a replay, x and / change what is shown, not what is read.

See your threads

Every running thread, at a glance. Sort by any column, invert the order, and pop the help overlay any time.

Thread list navigation

See a thread's own view

A thread carries its CPU and load graphs, building up live as it is polled.

Thread detail

Watch your kernel objects

Press k for every statically declared k_sem, k_mutex, k_msgq, k_mem_slab, k_heap and k_work_q: semaphore counts with their waiter lists, mutexes with the thread that holds each lock, queues with their fill level, slabs and heaps with what they have handed out, and work queues with the work waiting to run. The aggregate row on top sums the objects listed, and takes a type's own wording when / leaves only that type read.

Kernel objects list

ENTER opens the selected object on its own history.

See each object's view

A mutex carries its owner, lock depth, the threads queued behind it, and a per-frame lock strip that separates free from held alone from held with waiters.

Mutex detail

A semaphore plots its count against the limit over time, so a sweep from empty to full and the burst that drains it read visually.

Semaphore detail

A queue plots its depth against its capacity, with the threads parked on it: senders when it is full, receivers when it is empty.

Message queue detail

A slab plots the blocks it has out against the blocks it owns, with its peak marked on the bar if the build sets CONFIG_MEM_SLAB_TRACE_MAX_UTILIZATION.

Memory slab detail

A heap plots the bytes it has out against the bytes it accounts for, and opens onto a map of its allocated chunks in address order, with the threads waiting for memory beside it.

Heap detail

A work queue plots its backlog beside the items waiting to run and the threads draining it.

Work queue detail

Offline workflows

ZView can record a live session to disk, replay it later without a probe, or emit a single frame as JSON for CI.

Record a live session:

Record a session

# 30 s of live polling from a JLink probe, saved to disk
west zview record -e build/zephyr/zephyr.elf -r jlink -t nRF5340_xxAA \
  -o capture.ndjson.gz --duration 30

Bound the recording by either --duration (seconds) or --frames (number of data frames).

Replay it later, no hardware needed:

Replay a recording

# Feed the recording into the TUI
west zview replay -e build/zephyr/zephyr.elf -i capture.ndjson.gz

By default the replay honors the recording's original cadence. Pass --no-pacing to drain it as fast as possible.

The ELF is still required: DWARF offsets are resolved at replay time and are not stored in the recording.

CI-friendly single-frame snapshot:

Dump as JSON

# One polling frame from a live probe, dumped as JSON on stdout
west zview dump -e build/zephyr/zephyr.elf -r jlink -t nRF5340_xxAA --json \
  | jq '.threads[] | select(.runtime.stack_watermark_percent > 80)'

dump accepts either a live source (-r/-t) or a recording (-i). Omit --json for a human-readable one-shot dump. Use --frame N to emit the Nth polling frame instead of the second, the first with CPU shares.

Note: Status messages (probe connect, ELF load) are emitted on stderr, so stdout stays clean for piping into jq or similar.


Advanced

QEMU/GDB Targets

If debugserver is available:

west debugserver

Or launch a GDB server from QEMU directly with -s, if you have no runners.yaml (e.g. mps2/an385):

qemu-system-arm \
  -machine mps2-an385 \
  -cpu cortex-m3 \
  -kernel build/zephyr/zephyr.elf \
  -nographic \
  -serial mon:stdio \
  -s

Then attach ZView:

west zview live -r gdb -t localhost:1234

Note: Due to GDB server limitations, when using -r gdb the target is briefly halted prior to memory reads. Be aware of this side effect.

Kernel object coverage

Only statically declared objects are listed. K_SEM_DEFINE, K_MUTEX_DEFINE, K_MSGQ_DEFINE, K_MEM_SLAB_DEFINE, K_HEAP_DEFINE and friends define globals, which ZView finds while parsing the ELF: at the address their DWARF location gives, or through the symbol table for one the debug information only declares. An object created at runtime is no global and never appears in the view. Nor does one the linker drops: an object no code touches is garbage collected out of the image, however statically it was declared.

Note: Waiter lists need a sys_dlist_t wait queue, Zephyr's default (CONFIG_WAITQ_SIMPLE). Under CONFIG_WAITQ_SCALABLE the queue is a red-black tree, and the waiters column reads ? instead of a count.

How it works

ZView achieves a minimal footprint by avoiding on-target processing or UART/Shell output. It utilizes the debug probe's ability to read memory through the AHB-AP (a MEM-AP on the system bus) without halting the CPU. By parsing the ELF file, ZView identifies kernel object locations, performs analysis of stack watermarks, thread CPU usages, and executes a deterministic walk of physical heap chunks to map memory fragmentation.

Every value is sampled at the polling period, so state that appears and disappears between two polls is never seen: a lock taken and released, a thread that starts and exits, an allocation made and freed. The high-water marks are the exception, since a stack watermark and a slab's peak usage hold their maximum between reads. Zephyr counts a thread's cycles only as it is switched out, so a poll alone shows each thread's run at its switch: a poll faster than the run reads it idle, then the whole run at once. --usage-window and + / - sum the CPU and load shares over more polls.

Note: The idle thread is implicit and only expresses itself on the used CPU %, when available.

Feel free to open an issue if you feel like this has some potential!

About

ZView - A real-time thread (and more!) viewer for Zephyr RTOS. Just like htop, but for Zephyr

Topics

Resources

Code of conduct

Contributing

Stars

54 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages