Skip to content
Conduit

skill

alwavelets

riadhbenabdelhamid/alwaveletscommunitylistedunreviewed

Set up

Skills are installed by placing their folder where your agent loads skills.

The download comes from github.com/riadhbenabdelhamid/alwavelets itself, not from our server, so what you get is exactly what its author published.

Install instructions come from the README

Skills do not use an MCP config. Follow the steps in the README below.

README

synced Oct 21, 2026

Alwavelets — Terminal VCD/FST Waveform Viewer

License: AGPL v3

A dependency-free terminal waveform viewer for digital-design simulation traces. Runs over SSH / tmux / a bare console — no display server required.

The mark is a symmetric burst of square waves over a >_ prompt — the shape of a signal, drawn the way this tool draws one. It ships in two tints so it holds contrast on either GitHub theme (assets/alwavelets-logo.png for dark, …-logo-light.png for light). The in-app ◆ diamond icon used for desktop entries is separate, generated from scratch by tools/make_icon.py — pure stdlib, original artwork, no third-party assets.

Features

  • Formats: VCD (streaming, background, and disk-backed loading for large files; aliased identifier codes and b-form 1-bit values included) and FST read natively — zlib, LZ4 and FastLZ chains and hierarchies, gzip-wrapped files, aliases, $dumpoff blackouts — with pure-Python block decoders, so Verilator, GHDL, Icarus and vcd2fst output opens with no extra package (fst2vcd is only a fallback for anything the reader rejects). Values are bit-packed in memory (1-bit signals and all buses up to 64 bits), cutting resident size several-fold on bus-heavy traces.
  • Navigation: fuzzy signal finder (/ filters every signal in the file live, fzf-style), edge-to-edge jumps, multi-cursor measurement, markers, zoom-to-fit / zoom-to-selection, mouse drag-to-zoom, time goto, vim-style motions.
  • Display: binary / hex / dec / unsigned radixes, analog rendering, per-signal color & height, IEEE-754 float decode, scope folding, themes, and sub-cell (2× horizontal resolution) waveforms for finer clock timing (w), collapsible signal groups — nestable via group parent/child — (fold with Tab), and true-color themes (#rrggbb in theme JSON; quantizes gracefully on 256-color terminals — see theme_nord.json).
  • Analysis: frequency & transition detection, cross-signal expression search (when valid && state==3 && addr>=0x40), glitch / x-hazard detection, pattern/trigger search, protocol decoders (SPI, I²C, UART, AXI, Wishbone, AHB), RISC-V instruction trace decoding, formal counter-example & power-activity analysis.
  • Persistence: whole-session save/restore (;session writes a sidecar that reopens the trace with the same layout, groups, radixes, colors, zoom and theme), plus bookmarks, annotations/notes, signal tags, and saved arrangements stored as JSON sidecar files next to the trace; GTKWave .gtkw import/export for interop (loadgtkw/savegtkw); a persistent parse cache for instant re-opens.
  • Export: CSV, SVG, PDF, screenshot, and a pixel-accurate image shown inline on kitty / iTerm2 / sixel terminals (falls back to a saved PNG, and to the normal character-cell rendering, everywhere else — never hinders portability).
  • Scripting: a small TCL-like command engine and an in-app command line.

The compact title and shortcut bars leave more room for waveforms. The guide shows actions for the focused pane and selected signals, using your configured keys. It fits shortcuts to the terminal width; 0 / 9 page through them, F2 hides the guide, and ? opens quick help. The dark theme uses quiet panel colors and a subtle A–B measurement region. Sidebar labels truncate at character-cell boundaries, and the status bar keeps complete readouts together while giving loading, errors, and agent-control activity priority on narrow terminals.

Press F4 or Ctrl-P (or enter ;actions) to search actions and commands. Type words such as fit cursors, workmode, or export; use ↑/↓ to choose, Enter to run, and Esc to return. The palette shows shortcuts and a description; commands that need arguments prompt with their syntax.

At 96 columns or wider, the hierarchy browser, signal labels/values, and waveforms have separate columns. On smaller terminals, Tab opens the hierarchy as an overlay; Tab or Esc returns to waveforms. The active pane has a ▸ in its header. s toggles hierarchy browsing, and ( / ) adjust the signal-label width. Waveforms receive keyboard focus at startup.

F8 / F9 (or ;back / ;forward) step through the last 50 time ranges. Zoom, pan, time jumps, and search jumps that move the view are recorded. Going back preserves the cursors and stops live following; navigating somewhere new clears the forward history. Signal editing still has its separate u undo.

F6 (;inspect) toggles a signal inspector below the waveforms. It shows the focused signal's path, width, type, radix, cursor A/B values, visible transition count, and cursor time difference. Reads run in the background for that signal only. Missing lazy-loaded data is labelled as unavailable without loading extra ranges. Long paths wrap across two rows; values fit the available terminal width.

F7 (;overview) toggles a one-row timeline above the ruler. The highlighted interval shows the visible portion of the trace; A, B, and marker ticks show reference positions. Click to move the view while keeping its duration. The overview uses time metadata and stays provisional while the extent is loading. Both optional panels start hidden, work in all three workmodes, and save with ;session. Config fields inspector and overview set their startup defaults.

F10 (or ;signal-menu) opens a small action menu beside the focused signal. Right-click a signal's label to open it there. Choose a number format, color, analog height, bus expansion, grouping, or removal; ↑/↓ choose, Enter applies, and Esc returns or closes. Right-clicking an unselected label selects that target for the menu; right-clicking a selected label keeps the selection. Right-clicking waveforms still places markers.

1 shows and selects cursor A; 2 shows and selects cursor B. c hides or shows only the selected cursor, preserving its position and the other cursor's visibility. To hide both, press 1, c, 2, c. Left/right move the selected cursor when visible and scroll the view when it is hidden. The A–B measurement appears only while both cursors are visible.

F12 (or ;markers, or Manage markers in the action palette) opens a time-sorted marker list. Inside the list, use ↑/↓, PgUp/PgDn, or Home/End to select; Enter jumps the active cursor to that marker, makes it visible, and closes the list. r renames, Delete removes one, m adds at the cursor, and M clears all. u / Ctrl-R undo/redo marker edits; Esc closes the list. You can also click a row, double-click to jump, or click the controls. Inside the list, / opens a filter prompt: enter a label or time and press Enter to apply; Esc cancels the prompt. Matching ignores case, and every space-separated term must occur in the label, displayed time, or raw tick count. The title shows matching/total counts. Ctrl-U in the list resets the filter; an empty filter also shows all markers. Filtering preserves the markers themselves: Delete removes the selected match, while M still clears all markers, including those hidden by the filter. In label prompts, empty Enter leaves no label and Esc cancels. Markers are saved with ;session. In the waveform view, , / . jump to the previous / next marker and reveal the active cursor. Each jump briefly shows the marker's position in the list, label, and time; wrapped means navigation passed an end of the list. Open F12 again to select in the list.

Select signals with Space or V, then use R, v, or H to change their radix, color, or analog height together. Cycling uses the focused selected signal's setting (or the first selected signal by ID when focus is elsewhere) and applies that same setting to the selection. The menu offers explicit choices and a Default option to clear overrides. With no selection, these actions affect the focused signal. Digital rows retain their fixed height.

Floating-point radixes include f16, bf16 (bfloat16), f32, and f64. Choose BF16 in the signal menu or use ;radix bf16 for the global setting. On a 16-bit bus with BF16 selected, F expands its sign, exponent, mantissa, and decoded value. The bf16 setting also works in config, saved sessions, and snapshot exports.

Two configurable number formats are also available: fixed and float. See the custom number formats guide for a walkthrough, bit-pattern examples, and the configuration reference. In F10 → Number format, use Configure custom fixed… or Configure custom float…, then select the corresponding format to apply it to the selected/focused signals. The prompt starts with the current settings; edit them, press Enter to accept, or Esc to cancel. Both slots also participate in r/R cycling. Settings changes support undo/redo and update every signal using that slot within the viewer.

;fixed 3 4 signed
;radix fixed
;float 4 3 auto ieee
;radix float

Fixed-point values use two's complement when signed; integer bits exclude the sign bit. Use unsigned to omit the sign. The displayed value is the decoded integer divided by 2^fraction_bits, with exact decimal output. Total widths from 1 to 1024 bits are supported, including zero fraction bits. The default slot is signed, with 7 integer and 8 fraction bits (16 bits total).

Custom floats have one sign bit, an implicit leading significand bit for normal numbers, and subnormal values when the exponent is zero. Configure exponent and stored mantissa widths, followed optionally by a bias (auto means 2^(exponent_bits-1)-1) and a special-value policy:

  • ieee: the maximum exponent encodes infinity or NaN.
  • finite: the maximum exponent encodes ordinary finite values.
  • nan: only the maximum exponent and maximum mantissa encode NaN; the remaining patterns are finite. This policy needs at least one mantissa bit.

The default float slot uses 5 exponent bits, 10 mantissa bits, automatic bias, and ieee. Exponents may have 1–15 bits, mantissas may have zero bits, and the total width must not exceed 1024 bits. Explicit biases range from -65535 to 65535. These formats describe individual words; any external scaling must be supplied separately. Analog display retains its existing numeric interpretation.

To debug an encoding, focus its signal and press F, choose F10 → Expand components, or enter ;expand-fields. The chosen radix determines the lanes:

RadixComponent lanes
f16 / bf16 / f32 / f64 / custom float.sign, .exp, .mant, .value
Custom fixed.sign (signed only), .integer, .fraction, .value

Zero-width fields are omitted. Exponent and integer fields start in unsigned decimal; sign, mantissa, and fraction fields start in binary. These are the raw stored fields: signed fixed 11101000 with I=3/F=4 splits into 1 | 110 | 1000 and decodes to -1.5, using two's complement. The .value lane plots the decoded number. x expands a bus or component into individual bits.

Press F on the parent again to collapse all its component and bit lanes. Expansion shares the signal's history and follows newly loaded samples. After editing a custom definition, collapse and expand again to refresh its lanes. Component lanes are temporary; expand them again after restoring a saved view or reloading the trace.

Signal widths must match the configured total; a width! label reports a mismatch. Short VCD binary values are zero-extended, and unknown X/Z values are preserved. Sessions and arrangements store both slot definitions and signal assignments. Text/SVG snapshots using a saved arrangement honor those settings; an explicit snapshot radix overrides the saved signal assignments.

u / ;undo reverses an edit; Ctrl-R / ;redo reapplies it to the original targets, even after changing focus or selection. Each bulk formatting/removal operation is one history entry. Editing history holds up to 100 entries and covers signal formatting, analog toggles, add/remove, reordering, grouping, and marker creation, renaming, deletion, and clear-all. A new edit clears redo; navigation preserves it. Restoring a session/arrangement or reloading a trace clears editing history. History itself is not saved in sessions.

An empty view explains whether no signals are selected, scopes are folded, or the trace is still loading, and shows the relevant selection/navigation keys.

Three selectable workmodes combine a palette with presentation defaults:

ModeAppearance
calmMuted waveform colors, compact guide, quieter panels, scope separators
classic (default)Vivid waveform colors, dense shortcuts and technical statistics, contiguous signal rows
minimalMonochrome, plain panel bars, no row striping or scope separators, guide hidden

Press F3 to cycle modes, or enter ;workmode calm, ;workmode classic, or ;workmode minimal. The title shows the active mode even when the guide is hidden; F2 toggles the guide in any mode. Switching modes reapplies the palette, guide, scope-separator, and zero-spacing defaults while keeping the displayed signals, zoom, cursors, and per-signal settings. ;theme and \ still change only the palette. ;session saves the mode and any subsequent appearance adjustments for that trace.

For clearer separation between neighboring signals, press | to cycle Compact → Blank gap → Divider, or choose directly:

;spacing blank
;spacing divider
;spacing compact

Blank gap adds one empty terminal row so a signal's low level cannot touch the next signal's high level. Divider adds a faint dotted line in that row. Both leave fewer signals visible; Compact remains the default. Dividers sit below the complete signal, including tall analog plots and comparison lanes. The Q grid setting can also add horizontal guides in gaps. Use ;session to save your choice. Older fractional spacing settings retain their full-row gap with a dotted divider; saved two/three-row gaps also survive.

Clock edges snap to the nearest terminal column. At zoom levels where a half-period spans a fractional number of columns, equal pulses can still differ by one column in width. Press w to select Hi-res (sub-cell) for half-column resolution, or zoom in for more detail. Dense activity that cannot be resolved is shown with a busy glyph; displayed pulse widths are approximate, while cursor time measurements use the trace timestamps.

Keyboard zoom keeps the visible active cursor at its screen position; mouse-wheel zoom keeps the time under the pointer in place. With a hidden or off-screen cursor, keyboard zoom uses the center of the view. Positions round to trace ticks, and zooming out stops the left edge at time zero. The optional grid (Q) follows the ruler ticks, dims stable waveform cells, and draws dotted marks in empty cells while preserving transitions and labels.

Requirements

  • Python 3.8+ with the standard library curses module (Unix/Linux/macOS; Windows is not supported because curses is not in the stdlib there).

No third-party packages are required.

Usage

python3 alwavelets.py <file.vcd>
python3 alwavelets.py <file.vcd> --signals 'clk,data*,alu.*'
python3 alwavelets.py --demo            # generate and open a built-in example

Or from a Python REPL, with no curses terminal involved:

>>> import alwavelets
>>> vcd = alwavelets.demo()      # prints a waveform, returns the parsed trace

Flags:

FlagMeaning
--signals, -sComma-separated name/glob filters
--syncForce synchronous parse (disable background loading)
--storage auto|memory|diskUse disk above 50 MiB, for large declaration counts, or large FST expansion; override either way
--memory-mib NDecoded waveform page-cache budget, default 256 MiB
--loaduncheckedStart with no signals shown (pick them in the B tree browser)
--expandedStart with all scope modules expanded
--workmode calm|classic|minimalChoose UI presentation, overriding config and saved appearance
--demoGenerate and view a demo trace (written under a per-user temp directory)
--mcpRun as an MCP server (stdio) instead of the viewer
--no-cacheDisable persistent waveform and instruction caches
--cache-dir PATHCache directory; overrides ALWAVELETS_CACHE_DIR and the default XDG location
--waveform-cache-mib NSaved waveform cache retention quota in MiB, unlimited by default; 0 disables reuse and saving
--list-waveform-cachePrint cache entries, sizes, busy status, usage, and free disk space as JSON, then exit
--remove-waveform-cache NAMERemove one idle entry by its exact listed name; repeat for multiple entries
--purge-waveform-cacheRemove all idle waveform caches and checkpoints, then exit
--purge-partial-indexesRemove only idle incomplete VCD/FST indexes, then exit
--analysis-cache-mib NSaved instruction-index quota, default 8192 MiB; 0 disables reuse and saving
--purge-analysis-cacheRemove saved instruction indexes and exit
--no-configIgnore ~/.config/alwavelets/config.json
--no-sessionIgnore a saved session sidecar (open the trace fresh). --expanded / --loadunchecked imply this
--controlExpose a token-authenticated control socket so an authorized local agent can drive this live session (see SKILL.md)
--list-sessionsPrint controllable running viewers as JSON, then exit
--jobs, -jOpt-in parallel parse workers for a large (>16 MB) synchronous parse (e.g. --sync --jobs 8)

Parallel workers use forkserver where supported and spawn elsewhere, so they do not inherit the viewer's threads and locks. Library scripts requesting parallel parsing must guard their entry point with if __name__ == '__main__':. The low-level parse_parallel_file API accepts an optional mp_context; parse_file falls back to sequential parsing when workers cannot start or fail. Worker startup and merging add overhead, so --jobs remains opt-in and applies to in-memory VCD parsing (--storage memory --sync --jobs 8).

Very large traces

VCD and FST files above 50 MiB automatically use an indexed SQLite trace store. Smaller VCDs also use disk when their declaration count predicts large metadata allocations, even if their waveform history is short. For smaller native FSTs, automatic selection also examines uncompressed block sizes, timestamp counts, and declaration counts. Gzip-wrapped FSTs use disk storage conservatively, since inspecting their inner block layout requires decompression. --storage memory remains an explicit override. Every declared signal remains available. --signals, --loadunchecked, and tree selection remain optional display controls; disk mode does not require a signal shortlist. To force disk storage or choose a smaller page cache:

alwavelets huge.vcd --storage disk --memory-mib 64
alwavelets huge.fst --storage disk --loadunchecked

The first open scans the source and batches declaration writes. Short histories (up to eight events and 512 encoded bytes) stay in the signal catalog; longer histories use independently compressed pages. VCD events and FST initial values use bounded batches with bulk metadata lookups and periodic publication. VCD batches preserve same-timestamp event order, and longer histories retain their page buffers across input batches. Declaration serialization reuses common type/scope fields while preserving existing saved-session fingerprints. Pages, summaries, and timestamp/count updates also use bounded write batches. Buffered histories keep their own event offsets and last timestamps. Completed pages carry that state in the bounded write queue, and each drain updates both metadata fields together. A read-through cache holds at most 1,024 stored positions; it does not write metadata on eviction. Checkpoints still flush all buffered state before committing, and existing complete and partial indexes remain compatible.

FST timestamp deltas expand in bounded pages as signal chains request them, so the first waveform batches can publish before the whole timestamp table has been converted. Expanded pages are retained for later signals and spill to disk under the same memory budget. Every declared timestamp is still validated before its block completes, including timestamps unused by any signal.

The first available histories publish promptly. Completed pages and progress normally publish every quarter second; later partial pages can accumulate for about a second, or until page/memory limits require a flush. Finishing a load always publishes all remaining values. Existing prepared indexes remain usable. Opening a native VCD/FST now shows a loading screen before storage detection, decompression, declaration parsing, or scope-index upgrades. Preparation runs on a worker; the screen shows the current stage, available byte/scope counts, and elapsed time. Press Esc, q, or Ctrl-C to cancel. Cleanup finishes on the worker, including temporary files and any unclaimed trace. Existing durable checkpoints remain available for resuming; preparation before the first checkpoint must be repeated. The same opening screen supports --storage memory, including eager cache reads and writes. --sync, demo generation, and converted legacy formats retain their synchronous startup path. The screen transitions into the viewer when metadata is ready, and disk waveforms become available as indexing progresses. Cached opens wake the UI immediately when ready, without a fixed polling delay. Signal selection remains optional. Rows and cursor values load on a background worker; panning discards obsolete requests. Complete analyses wait until indexing finishes. The index includes signal/time lookups and a hierarchy of exact min/max summaries, so overview scaling need not decode the whole history. Reopening an unchanged source opens the signal catalog and fetches declarations and values on demand, without reconstructing every signal object. Changed sources and incompatible index formats rebuild the index.

FST disk ingestion keeps ordinary bus values packed through page writing, including buses wider than 64 bits, and computes numeric ranges while decoding. Uncommon logic characters keep their text representation. Public value reads, rendering, and exports still return the same logic strings. New FST indexes use version 8; complete and resumable version-7 indexes remain readable, and resuming one upgrades its version at the next checkpoint. Older readers rebuild version-8 indexes. Eager parse caches and VCD indexes retain their existing formats.

VCD disk indexing with caching enabled saves a resumable checkpoint after the header, then roughly every five seconds or 64 MiB of input. Reopening after a close, cancellation, or process interruption continues at the last checkpoint; the status bar shows Resuming and progress includes the saved work. Checkpoints atomically save all preceding events, parser state, and unfinished summary groups. Work since the last checkpoint is replayed. Source identity, size, and modification/change times must still match; otherwise indexing starts over. Selective signal loading remains optional, and all declarations are retained across checkpoints.

FST disk indexing also saves resumable checkpoints with caching enabled. It records the next block, signal chain, and position within a chain alongside the waveform pages, counts, and unfinished summaries. Checkpoints follow roughly five seconds of indexing or 64 MiB of decoded chain data, checked at completed batches; declaration, initial-frame, and final-sort boundaries are saved too. Reopening skips completed chains and reuses the declaration catalog. The current chain rebuilds its decompression history up to the saved byte position, then continues streaming; saved events are not decoded or written again. Its timestamp/offset tables are decompressed again, and timestamp expansion rebuilds the prefix needed at the saved position before proceeding on demand. Gzip-wrapped FSTs must also be unwrapped again. An interrupted blackout block or final sort is replayed from its preceding checkpoint. This reduces repeated work after interruption; it does not remove the first-load scan or guarantee a checkpoint during a long decompression step.

FST shared-chain decoding now uses the correct one-based handle targets. This fixes missing or incorrect histories when distinct signals share compressed chains. FST parse caches, disk indexes, and incomplete checkpoints from the older decoder rebuild on their next open; VCD caches retain their version.

Caches default to ${XDG_CACHE_HOME:-~/.cache}/alwavelets. Set --cache-dir or ALWAVELETS_CACHE_DIR to put waveform and instruction caches on another drive. Explicitly choosing a directory also places temporary waveform indexes there when possible; decompression spill files still use the system temporary directory (TMPDIR). Existing caches are not moved automatically.

Waveform cache retention is unlimited by default. Set --waveform-cache-mib N to evict the least recently used inactive entries on opens, VCD checkpoints, completion, and close. This covers eager parse caches, completed disk indexes, incomplete VCD/FST checkpoints, and their SQLite sidecars. It excludes small lock files, temporary work files, and instruction caches (which have a separate --analysis-cache-mib quota). Open viewers and builders are protected and can exceed the retention quota; it is not a hard disk-usage limit. A completed index larger than the quota remains usable until its last viewer closes. Zero disables waveform cache reuse and saving without deleting existing files.

For example, use a cache on a larger drive with a 20 GiB retention quota:

alwavelets trace.vcd --storage disk --cache-dir /large-drive/alwavelets --waveform-cache-mib 20480
alwavelets --cache-dir /large-drive/alwavelets --list-waveform-cache
alwavelets --cache-dir /large-drive/alwavelets --remove-waveform-cache 'NAME_FROM_LIST'

--purge-waveform-cache removes all idle waveform entries; --purge-partial-indexes removes only idle incomplete indexes. Both skip files held by open viewers. --no-cache disables both saving and resuming. If another viewer owns the partial index or its cache directory is unavailable, opening uses temporary storage. Small lock files remain in the cache directory so concurrent opens and cleanup can coordinate safely.

If disk space or a filesystem quota runs out, indexing reports the affected directory. When a VCD or FST checkpoint has already been committed, the saved data stays readable and survives closing the viewer, even if it exceeds the retention quota. Free space and reopen the unchanged source to resume; choosing another cache directory starts a new index. Later cache maintenance may evict the idle checkpoint under the normal retention policy. For FST, saved histories can cover different time ranges per signal while indexing is incomplete.

Time-only queries read just the kind/timestamp prefix of a compressed page, without decoding its values. A later value read replaces that cache entry with the complete page; both forms share the same bounded cache. Rendering and event exports request complete pages when they need values immediately. Small inline histories are still read whole. Existing disk indexes need no rebuild, and signal selection remains optional. Prefix reads validate the header and timestamp length; the full page's zlib checksum is checked when values are read.

Zoomed-out digital rendering samples shared column boundaries once: a 160-column row needs 161 boundaries, or 321 at subcell resolution. Disk queries combine each boundary's event ordinal and preceding value, reuse the active page, and avoid decoding a following page for queries in a gap. Expanded bus bits extract only sampled values, including for in-memory traces. Duplicate timestamps, half-open column edges, labels and glyphs retain their existing behavior. Boundary batches use one published history and check cancellation between samples. Temporary sample counts scale with screen width; their values and labels still require space proportional to signal width.

Related rows share raw boundary samples through a separate cache capped at the smaller of 1 MiB per trace or one-eighth of the decoded-page budget. It retains timestamps, ordinals and raw values, including an allowance for entry overhead; it retains no decoded pages. Values too large for an entry are not cached. A publication or reorder changes the trace generation, invalidating these samples before the next batch reads them. Left boundaries used for waveform edges and inclusive point samples used for bus labels remain distinct, so exact-time delta events keep their existing meaning. Bus labels are read in one batch, and analog rows reuse their scaling range for the displayed limits. The page-cache budget is unchanged; the sample cache is additional bounded memory.

Pan, zoom, resize and waveform-style changes cancel obsolete background rows before the UI reads signal metadata. Scrolling removes pending offscreen rows; new versions of a row replace its queued work. Requests use a bounded queue that releases cancelled closures immediately, and each drawing pass submits its rows before starting new worker jobs. Cancellation belongs to the query, so file loading and unrelated readers continue. It is cooperative: an ongoing SQLite operation or page decompression must return before it can be observed. Large individual values can therefore still delay cancellation. Signal selection remains optional and existing indexes need no rebuild.

Pages are evicted from a bounded cache while browsing. The budget covers decoded timestamps and values, not total process memory: Python, scopes, display edits, SQLite, builder buffers, and active query pages need additional memory. A separate cache of at most 1,024 recent page locations lets nearby cursor, timestamp and ordinal queries reuse a decoded page without repeating SQLite lookups. These locations retain no decoded arrays and are valid only while the page remains cached and the published history is unchanged. FST value chains stream through bounded decompression history and read buffers without a separate decompressed-chain file. Frames, hierarchies, handle geometry, block/chain indexes, and timestamp tables use bounded buffers that spill to temporary files. zlib, LZ4, FastLZ, and outer gzip decompression process small chunks and check cancellation. A single declaration name or signal value can still exceed the working-buffer budget. There is no new native dependency. Disk mode trades a slower first scan and disk space for low memory use and quick subsequent opens. --no-cache still uses temporary disk storage, removed on close. Declarations, declaration order, and direct scope membership live in SQLite during both the first parse and cached reopening. Signal objects use a bounded cache; held objects and alias masters retain their identity across eviction. Event counts, ingestion timestamps, and pending summary groups also use bounded caches. Scope parents, child order, subtree counts, and ordinal bounds also live in SQLite, with a bounded node cache. Cached opens do not rebuild the hierarchy. Older complete and resumable indexes get a transactional scope metadata upgrade without rescanning waveform histories; cancelling that upgrade leaves the existing index usable. Building and traversing deeply nested scopes does not depend on Python's recursion limit. Display order uses compact catalog ranges, with space proportional to the number of runs and explicit edits. Default module folding and saved views store a fold level plus exceptions, rather than every folded scope name. Existing sessions with explicit fold lists remain readable. Highly fragmented selections, explicit browser expansions, group memberships, or collecting every signal/scope can still require memory proportional to those edits. Long hierarchical names also increase index size. All declarations remain available without selecting a shortlist.

The signal browser resolves both scope headers and signal rows on demand. Expanding a scope does not create one UI entry per child scope or signal; the row index grows with explicit expansions. The split panel reads the selected scope directly; subtree counts are persisted and checkmarks are cached. Home/End and Page Up/Down navigate both tree views. Scope selection and checkmarks use catalog ranges. Contiguous scopes can be toggled without visiting each signal; interleaved declarations may require a scan to find their runs. Reordering, undo, and small display groups preserve the surrounding ranges. Finder navigation snapshots also share these runs.

The / finder reads names without materializing the catalog. Disk-backed and large in-memory traces rank fuzzy matches on a cancellable background worker; typing again discards the previous query. Results are stored on disk and paged, with no match-count limit. Empty queries show declaration order immediately. Home/End jump to the first/last result. Full fuzzy matching still scans candidate names, so completing a broad search can take time while the interface remains responsive. Small in-memory traces continue to filter immediately.

During background disk indexing, ;cancel stops the load; closing the viewer also cancels it. Partial indexes are never treated as complete; cached VCD checkpoints remain available for resuming. Temporary spill files close on completion, cancellation, or failure. Library callers can pass a cancellation event to FSTReader.parse(cancel=event) or storage.open_trace(cancel=event). FST metadata is parsed before the viewer opens; library cancellation also covers this stage. The prepared reader shares metadata with the viewer and retains unwrapped input for background indexing, so declarations and outer gzip data are processed once. Value chains and timestamp tables feed bounded batches; same-timestamp transitions remain distinct and in their original order. Disk spilling reduces memory at the cost of additional I/O and can make the first parse slower. Eager FST parsing still retains decoded history.

Expression searches stream only the referenced signals. when/match searches run in the background in disk mode, with progress and a cancel command. Derived signal output also uses temporary pages. Clock measurement uses a disk sort for its exact median. CSV, SVG, PDF, and PNG file writers stream their input and output. PNG rasterization retains pixels rather than a complete event list, including when narrow pulses share a pixel. Protocol decoders expose iterators; AXI spills unmatched responses to a temporary disk queue.

Viewer protocol decoding, bus decoding, and instruction-trace building run in the background with progress and cancel. Their complete results are indexed in SQLite files, including when the source uses in-memory storage. Overlays read the visible time range on a worker and cache only the painted terminal cells. Changing the viewport cancels obsolete overlay reads. Successful decodes replace prior results; cancellation or failure leaves prior results available. Replacing/clearing results, reloading, or closing the trace releases their temporary storage. No frame-count limit is imposed.

Completed instruction indexes are saved under the user cache directory and reused by ;trace on subsequent opens. Reuse requires matching source identity, size and timestamps, signal tags, decoder settings, Python and implementation versions. Changed or incomplete indexes rebuild; custom decoder classes and sources without a stable parse-time identity use temporary storage. Only completed indexes are published, and concurrent publishers coordinate through a cache lock. Cache errors leave the temporary index usable.

Saved instruction indexes have a separate 8 GiB quota, evicting the least recently used first. Larger individual indexes remain temporary. Building and publishing need additional temporary disk space, including a copy of the index; the quota covers completed cache files. --no-cache disables reuse and saving. Use ;analysis-cache purge in the viewer, or alwavelets --purge-analysis-cache, to reclaim saved instruction indexes. Open analyses remain usable after eviction or purge.

Instruction searches also run in the background. PC, hart, thread ID, exact mnemonic, time, register-write, and memory-address filters use indexed fields; matches retain record IDs on disk instead of duplicating instruction payloads. The first 100 matches become navigation markers. ;results opens a paged browser of the complete selection alongside the waveform; moving between rows updates the waveform cursor, including matches sharing a timestamp. Use Up/Down or n/p, Page Up/Down, Home/End, and g to jump to a row. Enter or Escape closes the browser. These commands also work from the command prompt:

results goto 1000000
results next
results prev
results export /path/to/all-matches.csv

Export writes every match in capture order, runs in the background, and accepts ;cancel. Cancellation or errors preserve any existing destination. Summary queries keep the last match selection available; a new filter replaces it. Regex mnemonic searches and custom Python predicates can require a scan. cancel stops scans, SQLite sorting, and index construction; a single user-supplied predicate must return before cancellation can proceed.

The trace query prompt accepts pc <lo> [hi], time <lo> <hi>, hart <id> [start end], threadid <id>, and mn= <exact-name>. Existing thread <id> remains an alias for hart selection, and mn <regex> keeps its regex behavior. IPC, fairness, starvation, divergence, heatmaps, and register snapshots process history incrementally. The register panel reads indexed snapshots on a background worker. Aggregate memory scales with the number of harts (and heatmap dimensions), rather than retired instructions. Press p for the register panel: it shows x0–x31, ABI names, complete hex values, and the current PC/instruction on a cleared background. Columns adapt to terminal size; PgUp/PgDn or the mouse wheel over the panel reach the remaining registers when space is limited. Cursor navigation still updates the snapshot. p or Esc closes the panel. Unknown values show --------; loading and missing instruction data are labeled explicitly.

The panel docks right by default. While it is open, use the commands at its top (click them or press the number): 3 Right, 4 Bottom, 5 Top, 6 Left, 7 Overlay. Narrow panels use arrows for the four dock directions. Docking reserves space for the panel so the waveform, ruler, and cursors remain visible beside it; overlay draws over the waveform. Moving or closing the panel preserves cursor times and the displayed time range. The session remembers the chosen position.

Try the included register-panel demo:

python3 alwavelets.py demo_register_panel.vcd --expanded --no-session

Press p, then enter ;goto 6500ns and press Enter. The synthetic RV32 writeback signals are tagged automatically; no manual setup is needed. The panel retains earlier register writes while the waveform shows each individual writeback. Values in the panel are hexadecimal.

Cursor commandWhat to look for in the panel
;goto 0nsx0 is zero; other registers are unknown (--------).
;goto 6200nsAll 32 registers are visible/available; hart 1 has x1=00000201 through x31=0000021f.
;goto 6500nsHart 0: a0=00000005, a1=00000007, a2=0000000c; the footer shows the addition.
;goto 6600ns, then ;goto 6700nsTwo identical addi instructions update a0 to 6, then 7.
;goto 6800nsNo retirement: register values and the last retired instruction stay unchanged.
;goto 7100nst0=ffffffff, t1=80000000, t2=7fffffff; full-width values remain readable.
;goto 7200nsA nop retires without changing registers; x0 stays zero.
;goto 7500nsHart 1: a0=100, a1=200, a2=300 (hex 64, c8, 12c).
;goto 7600nsBack to hart 0: its a0=7 and a2=12 were preserved; a3=19 (00000013).
;goto 7700nsHart 1 has its own a3=400 (00000190).

At a retirement time the newly written register is highlighted. Resize the terminal to try the adaptive columns and PgUp/PgDn paging. Regenerate the file with python3 tools/generate_register_demo.py.

The historical ipc formula remains count divided by elapsed trace ticks; it does not normalize by a separately tagged clock.

Explicit materialization such as signal.changes, render_svg(), and decoder decode()/decode_all() result lists still uses memory proportional to the requested output. Library InstructionTrace.build() defaults to memory; pass storage='disk' for indexed storage whose open handle is tied to the source trace's lifetime. Pass use_cache=False to disable instruction-cache reuse and saving for that build. Use trace.select(...) as a context manager for paged matches, or trace.iter_matching(...) for streaming iteration. Existing find_*, query(), and thread-extraction methods still return lists for compatibility. Prefer the streaming APIs for large captures; individual transactions still retain their payloads. Waveform file exports from the viewer remain synchronous. Live tailing requires --storage memory; disk indexes are snapshots.

Library callers can explicitly select storage and should close traces:

from alwavelets import parse_file
from alwavelets.exporting import write_signal_csv, write_svg
from alwavelets.graphics import write_waveform_png
from alwavelets.decoders import SPIDecoder

with parse_file("huge.vcd", storage="disk", memory_mib=64) as trace:
    clock = trace.signals[trace.signal_order[0]]
    data = trace.signals[trace.signal_order[1]]
    write_signal_csv(clock, "clock.csv")
    write_waveform_png(trace, "clock.png", [clock.full_name])
    write_svg(trace, "clock.svg", [clock.full_name])
    for frame in SPIDecoder(clock, data).iter_decode(0, trace.max_time):
        print(frame.t_start, frame.label)

SPI/I²C/UART iterators accept a time range; AXI/Wishbone/AHB provide iter_decode(), and instruction traces provide iter_entries(trace, tags). Streaming decoders and CSV/SVG/PNG writers accept cancel=threading.Event(). PNG reports progress per signal row; CSV reports processed timestamps. Cancelled or failed exports leave an existing destination path intact. Caller-supplied streams may already contain partial output.

External LXT/LXT2/GHW/VZT converters and the FST converter fallback write to temporary files instead of capturing the converted trace in RAM. The converted VCD follows the selected storage policy. Converted disk indexes are temporary and removed when the trace closes; keep a converted VCD to reuse its index.

VCDFile.parse() remains the explicit eager reader. The old lazy reader remains available through its low-level API and the benchmark's lazy mode.

Press ? inside the viewer for quick help, or F1 for the full manual. For a browsable reference of every key, command, sidecar file and the MCP integration, read the complete field guide — also in the repo at docs/index.html, self-contained and usable offline.

Lazy views preserve the value entering a window and all changes at the same timestamp. Analysis commands load their requested interval before returning results; a whole-trace search can therefore load substantially more data than the visible window. Commands report when the initial background load is still in progress instead of searching incomplete data.

Sessions and arrangements preserve hierarchical references, declared ranges, widths, and types, so layouts survive regenerated VCD identifier codes. Disk-backed views save compact version-3 display ranges and a declaration-layout fingerprint. They require the same declaration identities and order; a changed layout is reported and the saved view is left unapplied. Versions 1 and 2 remain readable: version 2 resolves names and reports missing or ambiguous references, while version 1 uses original identifiers. Older waveform indexes compute the fingerprint with a bounded scan when first needed in that open session; newly built indexes store it. Saved time positions are restored after background loading establishes the trace extent.

Diff comparison normalizes both traces to the same time units and compares their overlapping time extent, including values held through the end of that interval. Separately declared bits and non-zero-based or ascending bus ranges retain their indices in signal names and expanded bit labels.

Configuration

An optional ~/.config/alwavelets/config.json (or $XDG_CONFIG_HOME) sets startup defaults and remaps keys. All fields are optional; a malformed file is ignored. keys maps an action name to a single character.

{
  "workmode": "classic",
  "theme": "phosphor",
  "radix": "hex",
  "custom_radixes": {
    "fixed": {"integer_bits": 7, "fraction_bits": 8, "signed": true},
    "float": {"exponent_bits": 5, "mantissa_bits": 10, "bias": null, "specials": "ieee"}
  },
  "wave_style": 2,
  "menu_bar": false,
  "grid": 1,
  "name_width": 28,
  "keys": { "quit": "Q", "zoom_in": "=", "toggle_cursor": " " }
}

workmode sets the starting presentation. Explicit theme and menu_bar settings override its defaults (the example uses Classic's layout with Phosphor colors). A saved session restores its own appearance; --workmode takes precedence over both. For example:

alwavelets trace.fst --workmode minimal

MCP server (agentic workflows)

Alwavelets can act as a Model Context Protocol server so an agent/app can request a waveform snapshot — a list of signals over a time window — and display it in a box. It speaks JSON-RPC over stdio and needs no extra dependencies:

python3 alwavelets.py --mcp        # or: python3 -m alwavelets.mcp_server

Example MCP client config (e.g. Claude Desktop's mcpServers):

{
  "mcpServers": {
    "alwavelets": { "command": "python3", "args": ["/path/to/alwavelets.py", "--mcp"] }
  }
}

Tools:

ToolReturns
render_waveform(file, signals?, start?, end?, width?, radix?, arrangement?)Unicode-text snapshot (signal names + waveforms + ruler)
render_waveform_svg(file, signals?, start?, end?, width?, radix?, theme?, arrangement?)SVG snapshot as inline image content (base64)
render_waveform_png(file, signals?, start?, end?, width?, radix?, arrangement?)Pixel-accurate PNG snapshot as inline image content (base64)
measure(file, signals, start?, end?)Per-signal transition count, period/frequency, and the edge-to-edge delta between the first two signals
list_signals(file, pattern?, limit?)Matching signal names
find_when(file, expr, start?, end?, limit?)Times where a boolean expression over signals holds ("valid && state==3 && addr>=0x40")
find_glitches(file, signals?, start?, end?, max_pulse?)Waveform anomalies: x-hazards and zero-width/runt glitches

Those are headless — they parse a trace of their own. Five more attach to a running viewer started with --control, so an MCP client can drive the session you have open (they proxy the socket below, so there is one implementation behind two transports):

ToolDoes
list_viewer_sessions()Which live viewers can be driven
viewer_state(session?)Read window, cursors, focused/displayed signals, groups, markers
viewer_render(session?, width?)Render what the user is seeing right now
viewer_command(session?, command)Run one ; command (when valid && ready, group A/B, zoom, stats…)
viewer_key(session?, keys)Keystrokes for key-only actions (x, a, w, tab, u)

session is a pid or trace filename; it is inferred when only one viewer is open, and the tools refuse to guess when several are.

signals is a list of names or globs ("clk", "top.alu.*"); times are ticks or unit strings ("500ns", "1.5us") and default to the full trace extent. theme is dark/light/phosphor or a theme-JSON path; arrangement is a saved-view .json that supplies the signal list + radix (so a snapshot can match a view you saved in the viewer). The same renderers are available in-process via alwavelets.render_text / alwavelets.render_svg / alwavelets.measure.

Agent control of a live session

The MCP server above is headless — it parses a file of its own and knows nothing about what you have on screen. The control channel is the other half: start a viewer with --control and an authorized local agent can drive that exact session while you watch — group signals, jump the cursor to where an expression holds, zoom, restyle — and read back both the view state and the waveform you are seeing.

python3 alwavelets.py bench.vcd --control     # in your terminal / tmux pane
python3 alwavelets.py --list-sessions         # how an agent finds it

If your agent speaks MCP, use the viewer_* tools above and you are done. This socket is the direct route for agents that don't, or that want a persistent connection. SKILL.md is the agent-facing contract (frontmatter + protocol, ops, command vocabulary, state schema and worked recipes) — point your agent at it. In Python:

from alwavelets.control import list_sessions, ControlClient

session = next(s for s in list_sessions() if not s["stale"])
with ControlClient(session) as c:
    c.command("group Interface/Handshake")   # organize the view
    c.command("when valid && ready")         # cursor lands on the first match
    print(c.render(width=100)["text"])       # see what the user sees

Safety: the socket only exists with --control; it and its session descriptor are 0600 and every request carries a per-session token; commands run with your authority; and the viewer shows a ◆AGENT indicator in the status bar whenever an agent is connected and acting.

Project layout

alwavelets.py is a thin launcher; the implementation is the alwavelets/ package:

ModuleResponsibility
coreData structures (VCDSignal, Radix, …) and value formatting
parsersVCD parsing (sync / background / lazy / follow), scope tree, parse_file dispatcher
value_streamShared incremental value, comment, and dump-state decoding
fstNative FST reader with pure-Python LZ4 and FastLZ block decoders
fst_streamForward FST chain decompression and bounded storage for blocks and numeric tables
fst_valuesPacked FST bus batches and numeric ranges, with text fallback for uncommon states
hierarchy, scope_storePaged scope metadata, compact folds, and virtual browser rows
cachePersistent on-disk parse cache (instant re-open of unchanged traces)
storagePaged VCD/FST storage, LRU decoding cache, persistent summary hierarchy
catalog, finder_searchDisk-backed declarations and scope membership; cancellable, paged fuzzy ranking
backgroundBounded UI read queue and cancellable analysis jobs
instruction_storeIndexed instruction fields and paged record-ID selections
instruction_analysisStreaming instruction filters, statistics, and register snapshots
analysis_cacheReusable instruction indexes, validation, quotas, and eviction
ui_resultsBackground paging, waveform jumps, and full-selection CSV export
parallelOpt-in multi-process parsing of large VCDs (--jobs N)
exprCross-signal boolean expression search + derived signals (when/derive)
gtkwGTKWave .gtkw save-file import/export (interop)
configOptional ~/.config/alwavelets/config.json (defaults + keybindings)
decodersProtocol (SPI/I²C/UART), RISC-V, and bus (AXI/Wishbone/AHB) decode
renderingPure waveform draw functions
analysisFormal counter-example and power-activity analysis
persistenceJSON sidecar stores (tags, annotations, bookmarks)
tclSmall scripting/command engine
manualIn-app help/manual text
uiThe Alwavelets curses TUI
snapshotHeadless text/SVG waveform snapshots
graphicsPure-Python pixel rasteriser + kitty/iTerm2/sixel image blit
mcp_serverMCP (JSON-RPC over stdio) server exposing snapshots
controlAgent control channel for a live session (socket + client)
cliDemo generation, bootstrap, and main()

Development

Tests live in tests/ and exercise the pure-Python layer (parsing, decoding, rendering, persistence, and the keymap dispatch) without a curses terminal:

pip install pytest ruff
python3 -m pytest        # tests
ruff check .             # bug-finding lint (pyflakes + syntax)

The suite includes a VCD parser fuzzer (random mutations must never crash the parser), a lazy-chunk parity check (the on-demand loader must agree with a full parse), and an FST parity check: tests/fixtures/fst_ref.vcd converted by vcd2fst with each compression (-4, -F, -Z, -c) must decode to exactly what the VCD parser reads. Both lint and tests run in CI (.github/workflows/ci.yml) on every push, across Python 3.8–3.13. Ruff is configured in ruff.toml for bug-finding rules only (E9 + F), not full style enforcement.

pip install . installs the package and an alwavelets console command (pyproject.toml; no runtime dependencies).

Performance benchmarks

The stdlib-only harness generates deterministic scalar, bus, real, mixed, and sparse VCD workloads. Each sample runs in a fresh interpreter, bypasses the parse cache, and verifies rendered views across loading modes. Reports include raw samples, medians, input hashes, Python/platform metadata, and Git revision.

python3 tools/benchmark.py --size-mib 8 --repeat 5 --output before.json
python3 tools/benchmark.py --size-mib 8 --repeat 5 --baseline before.json --output after.json
python3 tools/benchmark.py --scenarios mixed --size-mib 64 --modes full lazy parallel --jobs 4
python3 tools/benchmark.py --scenarios mixed --size-mib 1024 --modes disk --memory-mib 8 --repeat 1
python3 tools/benchmark.py --input /path/to/design.vcd --repeat 5

Use --repo /path/to/checkout to measure another revision with the same harness. Compare matching inputs on the same machine and Python version, with other heavy workloads stopped. Timing is informational, not a machine-dependent CI pass/fail threshold; ordinary tests check benchmark determinism and reader parity.

  • load(s) measures a full parse or, in lazy mode, header parsing plus indexing. Disk mode measures building a fresh complete index; this includes neither a persistent-cache hit nor the UI's progressive display before indexing ends.
  • first(s) includes loading and rendering the initial 16-signal text view. It is a headless proxy for useful first display, not curses startup latency.
  • seek(ms) measures loading/rendering a previously unvisited window at 90% of the trace; pan(ms) is the median of three further jumps.
  • main RSS(MiB) is the main parsing process's peak resident memory, including imports and rendering. Parallel workers are excluded, so it is not total parallel memory usage. It is unavailable on platforms without resource.

Generation and imports are outside the timed region. The OS page cache is not flushed; "cold" refers to Alwavelets' loaded windows, not cold disk I/O. Use --size-mib and --signals to test larger files and hierarchy widths; synthetic results should be supplemented with representative project traces.

To exercise complete workflows and many declarations in fresh processes:

python3 tools/stress_workflows.py --size-gib 10 --signals 100000 --memory-mib 8 --output stress.json

This generates a 4096-bit bus with a clock, verifies indexed values, streams PNG/CSV exports and paged SPI decoding, and checks reopening without loading pages. Separate workloads check 100,000 signal declarations and one million records each for frames, transactions, and instructions (--result-frames N). Reports include process peak RSS before and after reopening, page-cache usage, index size, and timings. The reopening measurement retains the first closed trace object, so its metadata is still resident. The generated traces and indexes require temporary disk space and are removed after the run.

Instruction query stress checks build ten million synthetic retirements, validate full and selective result counts, and exercise per-hart statistics and register reconstruction in a fresh process:

python3 tools/stress_instructions.py --records 10000000 --output instructions.json
python3 tools/stress_instructions.py --records 10000000 --cache-roundtrip --output instruction-cache.json

The report includes index size, query/build times, and total process peak RSS. The synthetic workload includes repeated timestamps and unsigned 64-bit PCs. The cache roundtrip additionally checks persistence, reopening, final-page access, and purge. It needs space for both the temporary and saved index.

FST stress checks generate compressed traces without external tools: a single chain with ten million timestamps, 100,000 declarations, and a wide initial frame. Optional block checks expand 1 GiB through each supported chain codec:

python3 tools/stress_fst.py --events 10000000 --signals 100000 --memory-mib 8 --output fst.json
python3 tools/stress_fst.py --block-mib 1024 --output fst-blocks.json
python3 tools/stress_fst.py --background --wrapped --output fst-startup.json

Fresh reader processes verify values and report peak RSS, parse time, index size, and automatic storage selection. --repo /path/to/checkout compares another revision on the same generated workload. Block measurements include decompression and sampled reads, not full waveform indexing. Temporary files are removed after each run; disk space must accommodate the expanded blocks. --background also measures when metadata is ready and when the first signal has readable waveform data (not terminal rendering time). --wrapped repeats the trace checks with outer gzip compression.

Hierarchy UI stress checks cover flat and nested layouts with 100,000 and one million signals, including expansion, arbitrary scrolling, selection, and split-panel drawing:

python3 tools/stress_tree.py --output tree.json
python3 tools/stress_tree.py --repo /path/to/baseline --output tree-baseline.json

Each sample runs in a fresh process using real declaration objects and UI methods with a recording terminal surface. Results exclude file parsing, waveform rendering, and terminal I/O. The allocation metric covers rebuilding the expanded tree; process RSS also includes declarations and selection state. Slow baseline operations stop at --timeout seconds and are reported as timeouts rather than completed measurements.

For real million-declaration VCD/FST files, including one initial value per signal, cold indexing, cached reopening, browsing, and narrow/broad finder queries:

python3 tools/stress_catalog.py --signals 1000000 --output catalog.json
python3 tools/stress_catalog.py --repo /path/to/baseline --output catalog-baseline.json

Cold and cached phases run in separate processes. Reports include index size, peak RSS, finder submission latency, and total ranking time. The cached phase uses the real viewer constructor with all signals selected and a recording screen; terminal I/O and waveform painting are excluded. --formats vcd or --formats fst restricts the generated inputs.

A local million-signal comparison of ae4ed59 against 312c8c6, with an 8 MiB waveform cache, measured the following (timings depend on the machine and storage):

MeasurementBeforeBatched ingestion + inline histories
FST cold indexing69.3 s19.4 s
VCD cold indexing55.5 s32.4 s
FST index size323 MiB160 MiB
VCD index size323 MiB164 MiB
Cold VCD/FST peak RSS39–41 MiB28–37 MiB
Cached reopen6–7 ms6–7 ms

These synthetic traces contain one initial value per signal; gains depend on history length and value width. At ae4ed59, headless browsing/search with all signals selected used about 128 MiB. Fuzzy ranking completed in 1.5–2.3 seconds in the background, and the first all-selected scope checkmark paint took about 0.5 seconds. Compact display ranges address this remaining selection overhead.

A separate background FST run with 100,000 signals made metadata available in 1.1 seconds and the first waveform readable in 1.6 seconds, before indexing finished at 2.0 seconds. A ten-million-event scalar FST indexed in 8.9 seconds with 25 MiB peak RSS. These startup measurements exclude terminal rendering.

Selection/session checks use real FSTs, prebuilt indexes, fresh viewer processes, and a recording screen. They exercise scope toggles, distant navigation, move/delete undo, a small nested group, and session serialization/restoration:

python3 tools/stress_selection.py --signals 1000000 10000000 --output selection.json
python3 tools/stress_selection.py --signals 1000000 --repo /path/to/baseline --output selection-baseline.json

A local run comparing ae4ed59 to compact display ranges measured:

MeasurementBefore, 1M signalsCompact, 1M signalsCompact, 10M signals
Viewer construction507 msbelow 1 msbelow 1 ms
First scope paint571 msbelow 1 msbelow 1 ms
Remove/add entire scope566–833 msbelow 1 msbelow 1 ms
Session serialization29.8 sbelow 1 msbelow 1 ms
Session JSON91.7 MBbelow 1 KiBbelow 1 KiB
Session restoration36.3 sbelow 1 msbelow 1 ms
Peak RSS, including session roundtrip1,350 MiB26 MiB27 MiB

These are flat, all-selected catalogs with one value per signal. Preparation, terminal I/O, and waveform painting are excluded. The JSON metric uses compact formatting; the application's indented default session also stays below 1 KiB. Fragmented orders, explicit scope expansions, or large groups need more memory/work. The separate million-signal FST browsing/finder workflow used 29 MiB with compact display ranges, versus about 128 MiB at ae4ed59. Cached reopening stayed near 6 ms. At a79ca5e, cold FST indexing measured 24.7 seconds in this run.

Scope scaling checks use real VCD and FST files with one scalar per child scope, fresh processes, an 8 MiB page cache, and the real viewer with a recording screen:

python3 tools/stress_scopes.py --scopes 100000 --output scopes-100k.json
python3 tools/stress_scopes.py --scopes 1000000 --output scopes-1m.json
python3 tools/stress_scopes.py --scopes 100000 --repo /path/to/baseline --output scopes-before.json

A local comparison against 473389c measured these VCD/FST ranges:

MeasurementBefore, 100k scopesPaged, 100k scopesPaged, 1M scopes
Cold indexing1.8–3.0 s2.8–4.2 s30.2–45.9 s
Cold peak RSS86 MiB30 MiB30 MiB
Cached reopen263–291 ms4 ms8 ms
Viewer construction78–111 msbelow 1 msbelow 1 ms
Expand top / jump to last scope / paint99–107 msbelow 1 msbelow 1 ms
Default module foldingexceeded 10 s2–3 ms18 ms
Browser peak RSS107 MiB27 MiB27 MiB
Folded session JSONnot measured545 bytes546 bytes

Generation and terminal I/O are excluded. Default folding on the baseline was stopped after ten seconds, so no folded-session size is reported for that run. Persisting scope metadata increases the 100k-scope index from about 22 MiB to 32–33 MiB and adds first-open work. Subsequent opens reuse that metadata. These are wide hierarchies with short histories; explicit expansions and fragmented selections still have their own costs. Regression tests also cover a hierarchy 1,800 levels deep, declaration-order parity, legacy index upgrades, cancellation, and saved-view restoration.

Startup timing now separates the loading screen, completed metadata, usable browser, first readable value, and finished indexing:

python3 tools/stress_startup.py --scopes 100000 --formats vcd fst wrapped --output startup.json
python3 tools/stress_startup.py --scopes 1000000 --output startup-1m.json
python3 tools/stress_startup.py --scopes 100000 --repo /path/to/baseline --output startup-before.json

A local comparison with aec9eb5, using 100k scopes and VCD, FST, and gzip-wrapped FST, measured:

MeasurementBeforeBackground preparation
First screen on cold open2.2–3.3 sbelow 1 ms
Metadata ready on cold open2.2–3.3 s2.3–3.4 s
First readable value2.5–3.6 s2.5–3.7 s
Full indexing2.8–4.2 s2.9–4.3 s
Cached browser ready3.5–4.8 ms4.8–6.0 ms

At 8d71c5a, a one-million-scope run painted the loading screen below 1 ms, metadata took 25.5 s (VCD) / 37.0 s (FST), and full indexing took 33.2 / 45.7 s with about 32 MiB peak RSS. Cached browser startup took 10–13 ms. Preparation is now visible and cancellable throughout that wait; its parsing and indexing work still needs to finish. The benchmark uses fresh processes, an 8 MiB page cache, real files, and a recording screen. Generation, imports, curses setup, and terminal I/O are excluded. First-value timing is the first successful value query after browser setup. Stage start times are included in the JSON for profiling declaration parsing, scope construction, and decompression separately.

Scope construction now resolves ancestors and writes nodes in batches of up to 256 pending paths, flushing earlier at an estimated 256 KiB of queued metadata. Subtree counting retains up to eight pages of 256 child records plus an iterative ancestor stack. Index sorts also observe cancellation. The on-disk schema, declaration order, optional signal selection, and saved checkpoints are preserved.

A paired local startup comparison against 8d71c5a, using the same generated workloads and settings above, measured:

MeasurementPrevious VCD / FSTBatched VCD / FST
Metadata ready, 100k scopes2.36 / 3.51 s1.62 / 2.82 s
Scope preparation, 1M scopes16.63 / 16.21 s7.25 / 7.29 s
Metadata ready, 1M scopes25.41 / 37.42 s16.25 / 28.52 s
Full indexing, 1M scopes32.06 / 46.47 s22.69 / 37.46 s
Peak RSS, 1M scopes31.8 / 31.3 MiB33.0 / 31.6 MiB

Scope preparation includes node construction, subtree counts/order, and indexes. These are individual local runs with wide hierarchies and short histories; other layouts and storage devices will differ. The million-scope cached browser still opened in 10–12 ms. FST declaration decoding took about 21 seconds in both versions.

FST hierarchy searches now reuse their buffered pages, and handle records are written in bounded batches during both preparation and resume. A 100k-scope profile of the 2.68 MB decompressed hierarchy reduced temporary-file reads from 19.18 GB across 292,723 calls to 5.09 MB across 78 calls. These are bytes returned by file reads, including repeated reads served by the OS cache, not physical disk traffic.

A local million-scope FST comparison against 6b37c6e, using the startup tool and the same settings, measured:

MeasurementPreviousBuffered hierarchy / batched handles
Declaration parsing21.24 s10.98 s
Metadata ready28.50 s18.17 s
First readable value31.18 s20.92 s
Full indexing39.37 s28.70 s
Peak RSS31.5 MiB31.6 MiB

To compare equivalent long histories, a separate tool generates a VCD and converts that exact input using GTKWave's vcd2fst. It opens every signal in fresh processes, checks every event against the generator, and compares waveform digests across formats and cached opens. Conversion and verification are outside the timings. The converter is only a benchmark-generation dependency.

python3 tools/stress_formats.py --signals 128 --ticks 10000 --mixed --output formats.json
python3 tools/stress_formats.py --signals 128 --ticks 10000 --mixed --codec Z --output formats-zlib.json
python3 tools/stress_formats.py --signals 128 --ticks 32768 --mixed --baseline /path/to/old-checkout --output formats-long.json

With 1.28M events across 128 scalar, 16/128-bit bus and real signals, plus an alias, a local comparison against 4f78e81 produced identical waveform digests:

MeasurementVCDPrevious LZ4 FSTBatched LZ4 FST
Source size55.61 MB3.47 MB3.47 MB
Metadata ready58 ms60 ms61 ms
First readable value337 ms329 ms331 ms
Full indexing2.58 s3.43 s2.60 s
Process CPU2.34 s3.15 s2.36 s
Peak RSS30.8 MiB27.7 MiB27.7 MiB
Cached reopen2.8 ms2.9 ms2.9 ms

These single local runs use an 8 MiB page budget and exclude terminal rendering. FST disk decoding now fills event batches directly and reads timestamps from bounded 64 KiB numeric pages, avoiding per-event generator yields and general record lookups. LZ4/FastLZ output reuses the bounded decompression history to combine tiny writes, and short matches copy only the referenced bytes. Large metadata blocks and timestamp tables still spill to disk; signal selection stays optional. That batching change preserved existing cache and checkpoint formats.

The mixed workload takes about 24% less FST indexing time, reaching roughly VCD parity. A separate 1.28M-event scalar comparison measured 0.96 → 0.51 s for FST versus 1.10 s for VCD, at 26.7 MiB FST peak RSS. Those scalar signals deliberately share identical histories, exercising shared compressed chains as well as a declaration alias. Profiling the mixed trace found less than 1 ms spent repeating shared-chain decompression, so no extra chain cache was added.

Increasing the mixed trace to 4.19M events (182.59 MB VCD / 11.38 MB FST) measured 11.17 → 8.28 s FST indexing versus 8.49 s VCD, again with identical waveform digests. FST peak RSS remained 28.2 MiB, with the same 8 MiB page budget. A separate 20M-event scalar chain at 3213fc1, including a 160 MB timestamp table that spills to disk, indexed in 17.50 → 8.95 s at 26.5 MiB peak RSS. Its first readable waveform arrived in 3.05 → 2.94 s; full timestamp-table expansion still preceded chain decoding. That stress check verifies sampled values and unchanged event counts, while the matched-format runs verify every event.

Incremental timestamp expansion now removes that initial conversion delay. A paired background run against 3213fc1 on the same 20M-event scalar FST, using an 8 MiB page budget, measured:

MeasurementPreviousIncremental timestamps
First readable waveform2.97 s0.30 s
Full indexing9.08 s8.67 s
Peak RSS29.6 MiB26.7 MiB
Index size32.66 MB32.66 MB

Timestamp deltas expand in chunks of at most 8,192 entries, and subsequent signals reuse the numeric pages. The decompressed delta buffer remains open until the block finishes; both it and the expanded table can spill to disk. Raw timestamp data is still decompressed before use. Sparse jumps and resume positions require expanding the preceding deltas. Existing cache and checkpoint formats remain compatible.

The matched 1.28M-event mixed workload still indexed in roughly 2.6 s at 27.5 MiB peak RSS, with identical complete waveform digests across VCD, FST, baseline/current readers and cached opens. These are individual local runs; the startup gain depends on timestamp-table size and signal activity.

Regression coverage includes LZ4, FastLZ and zlib; memory/disk loading; page jumps, same-timestamp changes, wide and real values; cancellation and exact checkpoint resume. Decoders before the shared-chain correction in 4f78e81 fail waveform verification on these workloads and cannot serve as timing baselines.

Value-chain decompression now also advances on demand, so long bus and real histories can publish their first waveform before the entire chain is expanded. The same raw, zlib, LZ4 and FastLZ decoders serve both forward chain reads and buffered metadata reads. Completed chains must pass tail, size and checksum validation before a checkpoint can skip them, including when resumed at the last event or when event decoding stops early.

python3 tools/stress_fst_chains.py --events 1000000 --baseline /path/to/baseline --output chains-bus.json
python3 tools/stress_fst_chains.py --events 1000000 --kind real --baseline /path/to/baseline --output chains-real.json

Paired local runs against 5818f66, with LZ4 compression and an 8 MiB page budget, verified every event in a million-event signal and its alias:

Measurement256-bit bus before → afterReal before → after
First readable waveform0.80 → 0.29 s1.39 → 0.29 s
Full indexing3.84 → 3.84 s2.85 → 2.91 s
Temporary-buffer writes54.18 → 8.00 MB18.00 → 9.00 MB
Peak RSS30.6 → 29.0 MiB26.4 → 26.9 MiB

The largest individual chains were 23.75 MB and 9.00 MB respectively. Temporary-buffer writes count bytes passed to decompression/table spill files, including their initial buffered prefix; they exclude waveform-index writes and do not measure physical storage traffic. Inputs, index sizes and complete waveform digests matched. Generation, conversion, verification and terminal rendering are outside the timed region.

These single runs show earlier first waveforms and less temporary storage work, with roughly unchanged total indexing time. Compression history and unconsumed output remain bounded; an individual very wide value can still require a larger read. Metadata and timestamp preparation still precede chain decoding, and a resume still has to decompress the chain prefix. Signal selection remains optional. Those streaming changes preserved cache and checkpoint formats. Regression tests cover publication before decompression completes, cancellation, corruption after a readable prefix, and exact resume across all supported chain codecs.

Packed bus ingestion subsequently targets total indexing time. Profiling a 200,000-event, 1,024-bit counter trace at b75fedf attributed roughly 20% of indexing time to JSON encoding, numeric conversion and page compression. FST bus batches now pass two-bit logic digits directly to the page writer, with numeric extrema computed during decoding. This removes the intermediate value strings, their JSON encoding for wide buses, and the later summary scan. The page reader also uses built-in byte translation and formatting for binary-only wide values.

python3 tools/stress_fst_chains.py --events 1000000 --width 256 --baseline /path/to/baseline --output packed-256.json
python3 tools/stress_fst_chains.py --events 1000000 --width 1024 --baseline /path/to/baseline --output packed-1024.json
python3 tools/stress_fst_chains.py --events 200000 --width 1024 --pattern dense --baseline /path/to/baseline --output packed-dense.json

Paired local runs against b75fedf used LZ4 FSTs, an 8 MiB page budget, and one bus plus an alias. Both patterns include X/Z events; the dense pattern uses deterministic high-entropy bits throughout the bus.

WorkloadFull indexing before → afterCPU before → afterIndex before → afterPeak RSS before → after
1M events, 256-bit counter3.99 → 3.02 s3.40 → 2.58 s7.43 → 7.43 MB30.9 → 27.8 MiB
1M events, 1,024-bit counter6.50 → 3.83 s5.77 → 3.35 s19.31 → 9.80 MB28.2 → 27.8 MiB
200k events, 1,024-bit dense2.53 → 1.17 s2.32 → 1.12 s50.21 → 34.28 MB28.1 → 27.6 MiB

Every event in the signal and alias matched, with identical waveform digests. First values remained available in about 0.3 seconds. Timings exclude generation, conversion, verification and terminal rendering. The dense run's separate full verification, including reads, expected-value generation and hashing, took 2.46 → 2.36 seconds. These are individual local runs, not universal speedups. Uncommon states fall back to text within a batch, so gains depend on the values and widths present. Batch limits remain bounded, except for an individual value larger than a page. Regression coverage includes unaligned widths, high bits, mixed X/Z and uncommon states, same-tick changes, exact resume, version-7 indexes, numeric summaries, rendering and CSV exports. Signal selection remains optional.

The separate 1.28M-event mixed scalar/bus/real workload indexed in 2.53 → 2.05 s, with 27.4 → 27.3 MiB peak RSS. Cached reopening took about 3 ms, and complete waveform digests matched across VCD/FST, both readers and cached opens.

LZ4/FastLZ token decoding now keeps its input cursor, output counters and copy history local to the decoder loop. Small literals and matches no longer call the general byte cursor and output writer for each operation. Input refills remain at most 64 KiB, and long literals, matches and length extensions retain bounded work between cancellation checks. Streaming, decompressed-size checks, tail validation, cache formats and exact checkpoint positions are preserved. No native dependency is added, and signal selection remains optional.

On the same 200,000-event, 1,024-bit counter trace, profiling recorded 10.57M → 4.52M function calls. Separate, unprofiled runs against 7078534 measured:

WorkloadCodecFull indexing before → afterCPU before → after
1M counter eventsLZ43.70 → 2.60 s3.51 → 2.52 s
1M counter eventsFastLZ3.82 → 2.72 s3.75 → 2.64 s
1M counter eventszlib2.21 → 2.23 s2.13 → 2.17 s
200k dense eventsLZ41.17 → 1.13 s1.10 → 1.06 s
200k dense eventsFastLZ1.54 → 1.29 s1.46 → 1.21 s
200k dense eventszlib1.23 → 1.23 s1.16 → 1.17 s

All six cases used a 1,024-bit bus, an alias, X/Z events and an 8 MiB page budget. Every event matched, and index sizes and decompression spill-write counts stayed identical. Current peak RSS ranged from 27.5 to 28.5 MiB, with first waveforms in about 0.28 seconds. These single paired runs show larger gains on the counter trace than on dense data; zlib remains a largely unchanged control. Generation, conversion, complete waveform verification and terminal rendering are excluded from indexing times.

python3 tools/stress_fst_chains.py --events 1000000 --width 1024 --codec 4 --baseline /path/to/baseline --output decode-lz4.json
python3 tools/stress_fst_chains.py --events 1000000 --width 1024 --codec F --baseline /path/to/baseline --output decode-fastlz.json
python3 tools/stress_fst_chains.py --events 1000000 --width 1024 --codec Z --baseline /path/to/baseline --output decode-zlib.json

Use --events 200000 --pattern dense for the dense cases. Additional regression tests force tiny input windows, cross every token field through refills, exercise maximum distances after history trimming, compare overlapping copies with a byte-at-a-time oracle, reject truncated prefixes and incorrect sizes, and check cancellation during long length extensions and immediately after a yield.

FST bus batches now size pages by their packed stride plus timestamps and allocation slack. A 1,024-bit bus holds up to 240 events per batch instead of 60 under the previous text estimate. If an uncommon state requires strings, the decoder emits the packed prefix first, then uses a smaller text budget that also accounts for non-ASCII replacement characters. A checkpoint before that split saves the next uncommitted event's position, even if its bytes have already been read. Page/event limits, cancellation and optional signal selection are preserved; a single value larger than a page remains the size exception. Existing indexes and checkpoint formats remain compatible.

Paired local runs against 83917ad, using LZ4, an 8 MiB page budget, one bus and its alias, measured:

WorkloadFull indexing before → afterPages before → afterIndex before → afterCold-page lookup median before → after
1M events, 256-bit counter1.80 → 1.65 s4,903 → 1,2227.43 → 5.87 MB0.061 → 0.085 ms
1M events, 1,024-bit counter2.63 → 2.32 s16,668 → 4,1719.80 → 9.17 MB0.058 → 0.078 ms
200k events, 1,024-bit dense1.17 → 0.99 s3,334 → 83434.28 → 31.40 MB0.094 → 0.220 ms

Every event matched, including X/Z values and aliases. Current peak RSS was 28.2–28.7 MiB, first waveforms appeared in about 0.28 seconds, and decompression spill-write counts were unchanged. Larger pages reduce ingestion overhead but increase the work for a cold single-value lookup, especially on dense data; the dense run's lookup p95 rose from 0.149 to 0.384 ms. The benchmark performs 1,000 deterministic random lookups with the decoded-page cache cleared before each query; SQLite and OS caches may remain warm. Loading measurements exclude generation, conversion, verification and these queries. These individual local runs are workload-specific, not universal speedups.

python3 tools/stress_fst_chains.py --events 1000000 --width 256 --baseline /path/to/baseline --output batches-256.json
python3 tools/stress_fst_chains.py --events 1000000 --width 1024 --baseline /path/to/baseline --output batches-1024.json
python3 tools/stress_fst_chains.py --events 200000 --width 1024 --pattern dense --baseline /path/to/baseline --output batches-dense.json

Regression tests cover packed capacity, fallback memory bounds, oversized individual values, cancellation after a packed prefix, exact restart at every batch boundary, repeated timestamps, multi-byte deltas and cached reopening.

The separate 1.28M-event mixed scalar/bus/real workload was essentially unchanged in indexing time (1.51 → 1.49 s), while its FST index shrank from 6.28 to 6.01 MB. VCD loading remained about 2.55 seconds, and cached reopening about 3 ms. Complete waveform digests matched across both formats, readers and cached opens.

Disk-backed reads now locate a value's page directly, using page metadata to avoid decoding a following page when the query falls in a gap. A bounded cache of recent page locations also serves timestamp bisections and ordinal reads, including aliases. Cache checks respect published counts and the different left/right boundaries required when a timestamp spans multiple pages. Short history updates, publication, sorting, rollback and eviction cannot expose stale or unpublished values. Existing index formats remain compatible and signal selection remains optional.

tools/stress_queries.py measures this read path in fresh processes for each repository and format. Each case contains 200,000 events and an alias, uses an 8 MiB decoded-page budget, verifies every event, and compares rendered-frame digests. Paired local runs against e8c26e3 measured:

WorkloadFormatCold random median before → afterNearby cursor median before → after30 pan/zoom frames, total before → after
Dense 1,024-bit busVCD218 → 196 µs30.6 → 0.92 µs677 → 498 ms
Dense 1,024-bit busFST214 → 193 µs32.1 → 2.25 µs485 → 299 ms
ScalarVCD93.4 → 72.2 µs30.2 → 0.95 µs261 → 32.6 ms
ScalarFST94.0 → 73.6 µs30.1 → 0.95 µs265 → 37.3 ms
RealVCD140 → 120 µs30.3 → 0.94 µs312 → 165 ms
RealFST118 → 95.8 µs30.3 → 0.94 µs207 → 47.4 ms

Cold queries clear the decoded-page cache before each of 1,000 deterministic random lookups; SQLite and OS caches may remain warm. Nearby cursor queries make 4,000 reads across a repeated 256-tick region, alternating the signal and its alias. Viewports cover five zoom levels and six pan positions at 160 columns; timing includes waveform rendering but excludes terminal output. Generation, indexing and full-history verification are outside the query timings. Waveform and frame digests matched across both formats and readers, and decoded-page memory stayed within budget. Cold dense reads still pay for decompressing larger pages; the largest gains come from nearby reads. These are individual local runs, not guaranteed speedups on every trace.

python3 tools/stress_queries.py --baseline /path/to/baseline --output queries-bus.json
python3 tools/stress_queries.py --kind scalar --baseline /path/to/baseline --output queries-scalar.json
python3 tools/stress_queries.py --kind real --baseline /path/to/baseline --output queries-real.json

Timestamp-only page reads extend this path without changing the index format. The query benchmark also measures cold timestamp bisections and a sweep of 1,000 evenly spaced timestamps. It reports timestamp-only and full-page decodes separately, plus the cached bytes and pages remaining after each stage. Paired local runs against 1466711, with the same 200,000-event workloads and 8 MiB budget, measured:

WorkloadFormatCold timestamp median before → afterTimestamp sweep total before → after
Dense 1,024-bit busVCD199 → 39.7 µs222 → 45.8 ms
Dense 1,024-bit busFST192 → 44.4 µs176 → 41.6 ms
ScalarVCD79.4 → 70.9 µs5.75 → 5.53 ms
ScalarFST80.0 → 71.8 µs5.72 → 5.51 ms
RealVCD130 → 45.3 µs37.2 → 13.2 ms
RealFST103 → 43.4 µs35.4 → 15.9 ms

These timestamp stages decoded no value arrays. At the end of the dense-bus sweep, VCD retained 1,000 timestamp pages in 0.84 MiB, versus 129 full pages in 8.00 MiB before; FST retained 834 timestamp pages in 1.92 MiB, versus 131 full pages in 7.97 MiB. Full pages replace timestamp entries when needed and stay under the same budget. Wide values benefit most because their value payloads dominate page size; scalar timestamp gains are smaller.

Value-heavy rendering still needs complete pages. The dense-bus 30-frame totals were 490 → 483 ms for VCD and 296 → 309 ms for FST; scalar totals were about 32 → 35 ms. This change targets timestamp queries and their cache footprint, with no consistent rendering speedup in these runs. All waveform and frame digests matched across formats and readers. These are individual local runs, with the decoded-page cache cleared before each cold query but SQLite and OS caches potentially warm. Checksum validation of the unread value tail is deferred until a full-page read.

Shared viewport boundaries now reduce repeated searches during digital rendering. Disk-backed batches return each boundary's ordinal and preceding value together; in-memory expanded bits also avoid extracting every visible bus event. tools/stress_queries.py can render several independent signals and expanded bit lanes in each frame, using --signals, --expanded-bits, and --subcell. Event verification and frame hashing remain outside timed regions.

Paired local runs against bd468d3 used an 8 MiB page-cache budget, 160 columns, five zoom levels and six pan positions. The following times cover 30 frames; each frame renders every row in its workload:

WorkloadFormatFrame median before → afterFrame p95 before → after30-frame total before → after
Four scalars, 50k events each (4 rows)VCD3.59 → 1.05 ms7.86 → 3.69 ms115 → 37.8 ms
Four scalars, 50k events each (4 rows)FST3.83 → 1.13 ms8.31 → 3.81 ms118 → 37.4 ms
Four 1,024-bit buses + two bits each, 50k events each (12 rows)VCD301 → 71.7 ms1,204 → 607 ms16.2 → 7.58 s
Four 1,024-bit buses + two bits each, 50k events each (12 rows)FST189 → 17.1 ms989 → 558 ms12.1 → 6.27 s
Two 256-bit buses + four subcell bits each, 20k events each (10 rows)VCD380 → 10.2 ms610 → 72.1 ms9.91 → 0.973 s
Two 256-bit buses + four subcell bits each, 20k events each (10 rows)FST242 → 11.2 ms432 → 13.9 ms6.75 → 0.301 s

Expanded bits benefit most from removing repeated searches over page-backed window views. Full-page decoding can still dominate wide-bus rows: a separate single-bus run took 611 → 642 ms for VCD and 427 → 371 ms for FST across 30 frames. Benefits depend on layout, cache reuse and signal width. Decoded pages stayed within budget; the 12-row run used about 35 MiB peak process RSS. All complete waveform and frame digests matched across formats and readers. An additional comparison preserved 4,608 rendered views covering aliases, expanded bits, exact time scaling, large timestamps, both bit styles and all bus radixes. Existing indexes remain compatible and signal selection stays optional. These are individual local measurements; terminal output is excluded.

python3 tools/stress_queries.py --kind scalar --events 50000 --signals 4 --baseline /path/to/baseline --output viewport-scalars.json
python3 tools/stress_queries.py --events 50000 --signals 4 --expanded-bits 2 --baseline /path/to/baseline --output viewport-bits.json
python3 tools/stress_queries.py --events 20000 --signals 2 --width 256 --expanded-bits 4 --subcell --baseline /path/to/baseline --output viewport-subcell.json

tools/stress_navigation.py measures the viewer itself with a recording terminal, including cursor reads, background row requests and applying completed results. Each round starts with empty decoded-page caches, issues eight pan/zoom changes with a 5 ms pause between drawing passes, then waits for the final 64-tick viewport. A separate uninterrupted viewport measures rendering cost. Generation, indexing, physical terminal I/O and the normal input polling delay are excluded. SQLite and OS caches may be warm.

Paired local runs against cd1cb36 used 50,000 events per signal, 160 waveform columns, five rounds and an 8 MiB page-cache budget. Submission time includes the navigation action and its immediate drawing pass; completion time starts at the final input and ends when its rows and cursor values have been applied:

WorkloadFormatSubmission median before → afterFinal viewport median before → afterUninterrupted viewport median before → after
Four 1,024-bit buses + two bits each (12 rows)VCD805 → 1.17 ms127 → 30.5 ms771 → 838 ms
Four 1,024-bit buses + two bits each (12 rows)FST119 → 1.23 ms63.2 → 24.7 ms202 → 215 ms
Four scalarsVCD2.99 → 0.42 ms5.37 → 2.12 ms11.1 → 10.8 ms
Four scalarsFST3.04 → 0.41 ms5.31 → 2.12 ms11.3 → 10.8 ms
Four real-valued signalsVCD4.70 → 0.62 ms10.3 → 6.83 ms49.9 → 46.4 ms
Four real-valued signalsFST4.97 → 0.59 ms11.8 → 6.31 ms52.6 → 50.4 ms

Cancelling obsolete rows and deferring new worker jobs until metadata reads finish removes the largest submission stalls. The 12-row submission p95 fell from 933 to 1.55 ms for VCD and from 152 to 1.56 ms for FST. This is a navigation responsiveness improvement: uninterrupted wide-bus rendering was 6–9% slower in these runs. Cooperative checks cannot interrupt an active SQLite operation or decompression, and these measurements do not establish a worst-case latency. The 12-row run stayed below its page-cache budget with about 40 MiB peak process RSS. Final glyphs, attributes, raw labels, analog limits and recording-terminal digests matched across repositories and formats. Queue tests separately verify that cancelled closures are released immediately and that loading and unrelated readers remain usable.

python3 tools/stress_navigation.py --signals 4 --expanded-bits 2 --baseline /path/to/baseline --output navigation-bus.json
python3 tools/stress_navigation.py --kind scalar --signals 4 --baseline /path/to/baseline --output navigation-scalar.json
python3 tools/stress_navigation.py --kind real --signals 4 --baseline /path/to/baseline --output navigation-real.json

Shared samples and batched label reads were compared separately with df82e0d. The navigation benchmark now checks both the uninterrupted wide viewport and the final zoomed viewport, clears page and sample caches between rounds, and reports decoded pages separately for uninterrupted renders and navigation bursts. Five-round local runs with the same 160 columns and 8 MiB page budget measured:

WorkloadFormatUninterrupted viewport median before → afterPages decoded across five uninterrupted viewports before → after
Four 1,024-bit buses + two bits each, 50k events each (12 rows)VCD829 → 388 ms12,280 → 6,180
Four 1,024-bit buses + two bits each, 50k events each (12 rows)FST219 → 152 ms2,140 → 2,140
Two 256-bit buses + four subcell bits each, 20k events each (10 rows)VCD75.4 → 52.9 ms520 → 520
Two 256-bit buses + four subcell bits each, 20k events each (10 rows)FST49.7 → 29.4 ms150 → 150
Four scalars, 50k events eachVCD11.5 → 11.9 ms200 → 200
Four scalars, 50k events eachFST11.0 → 11.1 ms200 → 200
Four real-valued signals, 50k events eachVCD46.3 → 45.8 ms635 → 620
Four real-valued signals, 50k events eachFST57.2 → 48.7 ms780 → 780

Sharing raw samples saves value extraction and searches even when decoded pages already fit in the page cache. VCD's 12-row workload also avoided many repeated page decodes. Median submission latency stayed around 1.2 ms for that workload; its final viewport completed in 33.6 → 19.0 ms for VCD and 26.2 → 17.6 ms for FST. Raw samples stayed below 1 MiB, decoded pages stayed within 8 MiB, and peak process RSS was about 40 MiB. Scalars saw little change; analog gains were smaller and varied between formats. Navigation bursts can decode more pages when a faster worker reaches more rows before cancellation, so their page counts are reported separately. These are individual local measurements, with imports, indexing, terminal I/O and the normal input polling delay excluded. Stable and final glyphs, attributes, labels and analog limits matched across both formats and repositories. Signal selection remains optional and index formats are unchanged.

python3 tools/stress_navigation.py --signals 4 --expanded-bits 2 --baseline /path/to/df82e0d --output shared-bus.json
python3 tools/stress_navigation.py --events 20000 --signals 2 --width 256 --expanded-bits 4 --subcell --baseline /path/to/df82e0d --output shared-subcell.json

Cold VCD ingestion checks cover a million declarations and mixed scalar, 16/128-bit bus, and real-valued histories in eight nested scopes with an alias:

python3 tools/stress_vcd.py --signals 1000000 --events 10000000 --output vcd.json
python3 tools/stress_vcd.py --signals 1000000 --events 10000000 --repo /path/to/baseline --output vcd-before.json
python3 tools/stress_vcd.py --cases mixed --active-signals 4096 --events 1000000 --output vcd-interleaved.json

A local paired run against a79ca5e, using the same source files in fresh processes with an 8 MiB page cache, measured:

VCD workloadPrevious loadBatched loadPrevious peak RSSBatched peak RSS
1M signals, one initial value each50.5 s19.0 s34.6 MiB26.6 MiB
128 signals, 10M mixed events66.7 s29.6 s29.3 MiB29.5 MiB
4,096 signals, 1M mixed events75.2 s33.7 s34.5 MiB41.9 MiB

Process CPU time fell from 38.9 to 13.5 seconds, 42.8 to 24.9 seconds, and 36.9 to 20.0 seconds respectively. The first readable value in the million-signal trace arrived at 8.8 seconds instead of 13.6; in mixed traces it arrived within 0.5 seconds. These synthetic measurements exclude generation and terminal rendering, and wall times vary with system load and storage. More interleaved histories can keep more buffers active between publications: the last case used more RSS but its index shrank from 76.9 to 57.1 MB. Doubling that case to 2M events in a separate run used 41.8 MiB peak RSS. The memory option limits the decoded-page cache, not total process RSS. All declarations remain available; signal selection is optional.

Page-writing improvements were separately compared with e595a14 using the same VCD generator, paired fresh processes, and an 8 MiB page cache:

VCD workloadLoad before → afterRegular pages before → afterIndex MB before → afterPeak RSS MiB before → after
4,096 signals, 1M mixed events31.8 s → 24.1 s255,112 → 191,99457.6 → 45.442.2 → 41.8
128 signals, 10M mixed events33.1 s → 28.5 s31,404 → 26,34955.5 → 53.130.0 → 30.0
1M signals, one initial value each16.3 s → 16.8 s0 → 0187.8 → 187.926.8 → 26.7

The interleaved case reduced process CPU time from 19.9 to 14.6 seconds; CPU time in the other two cases was essentially unchanged. First values in both mixed traces were readable within 0.5 seconds, including metadata parsing. The initial value-only case already uses inline storage, so page batching offers little benefit there. The benchmark now reports page counts, events per page, and summary row counts. These local results exclude generation and rendering; wall times depend on system load and storage. A separate paired ten-million-event FST check measured 11.1 seconds before and 11.3 seconds after, with the same index size.

Paired metadata writes were compared with 0e474d8 using the same fresh-process VCD benchmark and an 8 MiB page cache:

VCD workloadLoad before → afterCPU before → afterPeak RSS MiB before → after
4,096 signals, 1M mixed events21.5 s → 17.7 s14.3 s → 11.5 s42.0 → 41.1
128 signals, 10M mixed events39.5 s → 31.6 s24.3 s → 24.3 s29.9 → 30.0
1M signals, one initial value each22.2 s → 23.2 s13.5 s → 13.3 s26.4 → 26.7

The interleaved workload reduced CPU time by about 20%; its index shrank from 44.5 to 43.7 MB. First values remained readable within 0.6 seconds in both mixed cases. The narrow and initial-value-only cases showed little CPU benefit; their wall-time differences should not be treated as a reliable speedup. These measurements disable persistent caching, exclude generation/rendering, and vary with system load and storage. A separate 10M-event FST comparison measured 11.5 seconds before and 11.1 seconds after, with an unchanged index size.

With persistent caching and checkpoints enabled, a separate synchronous cold load of 4,096 signals and 1M mixed events took 13.8 → 9.9 seconds, with CPU time 12.5 → 8.9 seconds and peak RSS 40.9 → 41.1 MiB. Both runs built fresh indexes; neither reused saved data. A checkpoint produced by 0e474d8 was also resumed and its complete histories compared against the eager parser.

To measure interruption and resume with the default checkpoint cadence:

python3 tools/stress_resume.py --events 4000000 --output resume.json

This runs a cold load, an interrupted load, and a resumed load in separate processes with isolated caches. A local 128-signal, 4M-event mixed VCD run (174 MB, 8 MiB page cache) took 10.5 seconds for a full load. Interrupting at 60% left a checkpoint at 39%, preserving 1.55M events. Reopening finished in 6.6 seconds, with 29.6 MiB peak RSS versus 30.0 MiB for the full load. The remaining work after the checkpoint was replayed. Timings exclude generation and validation, include disk writes, and depend on storage and system load; the OS page cache is not flushed. This measures saved work after interruption, not faster initial parsing. Short traces may finish before an event checkpoint.

For the equivalent FST check, including a single chain large enough to require checkpoints within it:

python3 tools/stress_fst_resume.py --events 20000000 --stop-fraction 0.8 --output fst-resume.json
python3 tools/stress_fst_resume.py --events 20000000 --stop-fraction 0.8 --wrapped --output fst-wrapped-resume.json

The tool validates event counts and sampled values in fresh processes, with isolated caches and the production checkpoint cadence. It reports saved events, resume position, index size, elapsed/CPU time, and peak RSS. As with the VCD benchmark, source generation and validation are outside the timed region, disk writes are included, and the OS page cache is not flushed.

With incremental timestamps, a 20M-event run took 8.25 s from scratch. Interrupting at 80% preserved 12.36M events, and reopening completed in 4.61 s, at about 26 MiB peak RSS. The later interruption allows the normal five-second checkpoint to occur; a 60% interruption in a faster run can leave no saved events. Check the tool's saved_events result when interpreting resume timings.

A local 20M-event scalar FST run before the decoding optimizations above, with an 8 MiB page cache, took 16.0 seconds from scratch. Interrupting around 60% preserved 11.1M events; reopening finished in 8.6 seconds, with about 25 MiB peak RSS in each phase. In a separate paired 10M-event cold-load check, 93a1ae1 took 8.02 seconds and the resumable reader took 7.94 seconds (CPU: 7.66 and 7.67 seconds). These are local measurements, not speed guarantees; the benefit depends on how much work was checkpointed.

License

Alwavelets is dual-licensed:

  • Open source — AGPL-3.0-or-later. Free to use, study, modify, and share under the GNU Affero General Public License v3.0. Note the network clause: if you run a modified version to provide a service over a network, you must offer that version's source to its users.
  • Commercial. If AGPL copyleft (including the network/SaaS clause) doesn't fit — e.g. you want to embed Alwavelets in a proprietary product or hosted service without releasing your source — a separate commercial license is available. Contact riadh.benabdelhamid@gmail.com.

Copyright © 2026 Riadh Ben Abdelhamid.

↑↓ navigate openesc close