Skip to main content

pulse-viewers

Inventzia's Pulse Run Viewer

Viewing utilities for Pulse applications.

This repository hosts the viewers built over the artifacts a Pulse run leaves behind. It is a home for several of them; today it holds one, the Events Viewer, with the shared contracts and readers that any further viewer builds on. The design rationale is in docs/viewer.md.

The run browser listing runs by health, and a run's event stream open in its own window

The run browser (top) lists every run under the output root with its health verdict; each run opens in its own window (bottom). Here a real-time run shows its two heartbeats, an actor's echo, and the engine's own lifecycle — EngineStatus events on engine.status — interleaved in one stream, coloured by type.

Part of the Inventzia Pulse ecosystem

Pulse provides typed events, an event-driven engine for replay, simulation and real-time applications, and desktop tools for inspecting recorded runs. Use the components you need.

Package Purpose Choose it when…
pulse-data Shared typed events, schemas and serialization for Python and Java You need Pulse data types or want to define extensions
pulse-beacon Event-driven execution with Python/Java interoperability and run recording You want to build and run an application
pulse-viewers Desktop tools for browsing runs and inspecting recorded events You want to examine or follow a recording

Beacon depends on Data; Viewers reads Beacon's recorded files and can run independently, including on another machine — which is why Data and Beacon release together on one version while Viewers versions on its own.

New to Pulse? The ecosystem overview has a quickstart that runs an example and opens its recording.

Quickstart

pip install pulse-viewers
pulse-events-viewer

That opens the run browser. Point it at your Pulse output folder with Change output folder... — it remembers the choice — or set PULSE_OUTPUT beforehand. Runs appear newest first with their health; pick one and it opens in its own window. A run still being written is followed live.

No Pulse runs yet? The viewer works on any recording, and one can be generated without a JVM:

python -m inventzia.pulse.viewers.contract.event_record /tmp/sample.jsonl
pulse-events-viewer /tmp/sample.jsonl

Requirements: Python 3.11+ and a desktop session (the viewer is a Qt application). It needs neither a JVM nor pulse-beacon — it reads what a run has already written.

Installing the other components

# Build and run applications from Python; installs pulse-data too.
# Requires Java 17+.
pip install "pulse-beacon[jpype]"

# Inspect recordings on a desktop; no Java required.
pip install pulse-viewers
pulse-events-viewer

# Use only the shared event types and serialization.
pip install pulse-data

What a Pulse viewer is

Every viewer here follows the same three rules. They are what keep a viewer from becoming a second engine.

1. A viewer is an out-of-process reader. It never links pulse-beacon, never starts a JVM, and never participates in a run. Qt stays out of pulse-data and pulse-beacon entirely — the dependency never points that way.

2. A viewer reads a run's recorded artifacts, not the engine's internals or its human logs. So the artifact format is the real interface, and it comes first. Each run writes a standardized directory (see pulse-output.md):

$PULSE_OUTPUT/<tier>/<app>/<runId>/
    run.json        manifest: mode, window, provenance, status and counts
    events.jsonl    the event recording
    console.log     the run's human log

tier is historical for deterministic replays and live for real-time runs.

3. A viewer reports a run's health honestly. run.json carries the engine outcome (runStatus) and the recording outcome (recordingStatus) as separate fields, because either can fail while the other succeeds, plus counts that always balance (observed = events + overflow + serializationErrors + abandoned). A viewer must surface a partial or failed recording as such, never present it as complete.

Shared contents

Everything installable lives under src/inventzia/pulse/viewers/, a PEP 420 namespace package alongside inventzia.pulse.data and inventzia.pulse.beacon.

  • run_browser.py: the run picker, shared by every viewer — choosing a run is not specific to any one of them. Lists what list_runs() finds under the output root and classifies each run's health (ok / partial / broken / running / unknown) from its manifest, keeping the engine outcome and the recording outcome separate. Its logic half imports no Qt, so python -m inventzia.pulse.viewers.run_browser [--root PATH] gives a console listing anywhere.
  • live_tail.py: the reader for a recording still being written — partial trailing line, truncation, rotation, malformed records. Qt-free, so it can be driven by a timer or a test.
  • contract/run_layout.py: the Pulse output layout in one place — resolve $PULSE_OUTPUT, create and finalize run directories, read manifests, and list_runs() to enumerate every run under the root, newest first. The Java mirror of this lives in pulse-beacon as RunLayout; the two are kept byte-for-byte compatible.
  • contract/event_record.py: the Python side of the recording contract — build, write, read and validate records, plus make_sample() to emit a demo recording so a viewer can be developed without a JVM.
  • contract/event-record.schema.json: the authoritative record contract (JSON Schema 2020-12), the kinds header / event / trailer. Shipped as package data so validation works from an installed wheel.

Install from a checkout

pip install -e .              # plus `pytest` to run the suite
conda env update -f py_environment.yml     # or enrich the shared `pulse` conda env

pulse-viewers depends on neither pulse-data nor pulse-beacon: a viewer reads a run's artifacts as plain JSON and never links the engine, so the only dependencies are PySide6 and jsonschema.


Viewers

Events Viewer (pulse-events-viewer)

Reads one run's event recording and shows the dispatch stream.

pulse-events-viewer                     # browse $PULSE_OUTPUT and pick a run
pulse-events-viewer --root PATH         # browse a specific output root
pulse-events-viewer recording.jsonl     # open one recording directly

With no argument it opens the run browser, which is the application's hub rather than a one-shot picker: it stays open, and each run you open gets its own detached window, so several runs can be compared side by side. Closing a viewer leaves the browser and the other viewers alone, and closing the browser leaves the viewers open; Run browser... in any viewer brings it back.

A run is chosen by what it is — app, time, health — rather than by remembering a path. The chosen run's verdict travels into its window's header bar, where the engine outcome and the recording outcome stay separate and a partial capture is labelled partial. A recording opened as a bare .jsonl has no manifest, so its health comes from the trailer instead.

Only the two columns that carry meaning are coloured: the tier (grey for historical, blue for live) and the health verdict. Both palettes follow the desktop theme.

Following a live run. A run whose manifest still says running is being written now, so opening it starts following: new events are taken up as they arrive, the filters grow with them, and the follow stops by itself when the recorder writes its trailer. Follow pauses and resumes it, and any finished recording can be followed on demand. The status line reports rows, unreadable lines and re-opens, so a lossy or damaged tail is visible rather than silent. The reader (live_tail.py, Qt-free) handles a partial trailing line, truncation, rotation and malformed records — see docs/viewer.md section 5.

Engine lifecycle in the stream. When a run records the engine's status topic, its transitions (BLANK -> INITIALIZED, PRESTART -> STARTED, ... STOPPED -> COMPLETE, for the engine and each gateway) arrive as ordinary EngineStatus events, filterable and colourable like any other type.

Deleting a run. Delete run... removes the selected run directory after confirming. It refuses anything that is not a tier/app/runId directory inside the output root.

The output root can be chosen in the browser itself (Change output folder...) and is remembered between sessions, so nothing has to be configured before the first launch; a remembered choice takes precedence over $PULSE_OUTPUT, and an explicit --root over both. Open recording file... opens a single events.jsonl from anywhere.

It validates the recording (diagnostics to the console, run metadata in the header bar), shows the events newest-first by dispatch seq, colours each data type, and opens the selected event's payload in a detail tree. Filter by topic, key, type, a seq range, or free text; click a column header to sort (sorting by type groups by type and orders by seq within). Bounded retention, batched inserts, and lazy payload parsing are in place for the larger recordings of later phases.

There is no synthetic fallback: if a recording is needed without a JVM, generate one explicitly with python -m inventzia.pulse.viewers.contract.event_record, which writes into the platform's temp directory (pass a path to choose your own).

The recording contract

A recording is JSONL, one JSON object per line:

  • one header line: run metadata (runId, operating mode, window, type-universe fingerprint and provider ids from RunInfo, source identity);
  • then event lines, ordered by seq (the dispatch sequence, the authoritative order);
  • then a trailer line: completion status and the accounting totals.

Each event carries seq, observedAt, eventTime, topic, key, typeId, and payload (the datum's fields as a JSON object). typeId is flat so the viewer can filter and colour without parsing the payload; unknown types stay fully inspectable as generic JSON.

Producing a recording

Python (no JVM), for viewer development:

python -m inventzia.pulse.viewers.contract.event_record [outfile.jsonl]

Java: the recorder now ships in pulse-beacon as com.inventzia.pulse.beacon.core.gateway.recording.EventRecorderGateway, driven by the launcher-side run package, so a normal run writes its recording into the output layout with no extra wiring. reference/EventRecorderGateway.java is the original draft, kept outside the package for reference only — pulse-beacon is authoritative and the two have diverged.

Two limits remain while the recorder is a subscriber sink rather than an engine tap (Stage A):

  • Beacon routing is one subscriber per (topic, key), so the recorder cannot be registered on a route that already has a sink. Use it only on routes with no other consumer.
  • Its seq is recorder-local (the order this gateway received events on its subscribed routes), not the engine's global dispatch sequence.

Both facts are written into run.json's recording descriptor, so a reader never mistakes a partial recording for the complete engine stream. The global sequence and all-routes capture arrive with the engine event tap (Stage B).

Why not the existing JsonlWriterGateway

JsonlWriterGateway.onEvent writes bare codec.toJson(payload) with no topic, no type discriminator, and no time, and it serializes on the dispatch thread. Mixed event types cannot be told apart from that file, and it does not meet the recorder performance contract. Its format is left unchanged (backward compatible); recording is a new, separate writer.

Next

recording contract -> offline viewer -> run browser -> live file following is done, through live following. What comes next is driven by what the recording itself can offer: pulse-beacon's Stage B engine event tap will give a global dispatch sequence across all routes, replacing the selected-routes, recorder-local sequence this version reads (both declared in every run.json, so the viewer already reports which it is looking at).


Development

pip install -e . pytest
pytest                      # Qt runs offscreen; conftest.py sets QT_QPA_PLATFORM

The suite is built around a corpus of deliberately damaged runs — a crashed engine, a failed recorder, a lossy capture, counts that contradict themselves, a run that never finalized, one with no manifest, one with no events at all. A viewer's job is to open the run someone is worried about, so those are the cases worth testing; two healthy runs would prove very little. Colour is checked by measuring WCAG contrast in both light and dark palettes rather than by eye, and live following is driven against a file being written underneath the reader.

CI runs the suite on Python 3.11 and 3.12, then builds the distribution and smoke-tests the installed wheel from a directory with no source tree in sight — so an accidental source-path dependency fails there rather than in someone's install.


License

Dual-licensed: GNU Affero General Public License v3.0 (see LICENSE-AGPL-3.0) or a commercial license from Inventzia Science and Technology Ltd. (see LICENSE-COMMERCIAL.txt and COMMERCIAL.md).

Third-party components are recorded in NOTICE. Packaging note: PySide6 is LGPL, and those obligations attach only to distributing it — declaring it as a dependency (a wheel on PyPI, a conda recipe) ships a name, not the library, so nothing is owed; bundling Qt's binaries into a frozen build (PyInstaller and friends) does convey it, and then the licence texts, the notice, and the recipient's right to relink against their own Qt all apply. Also prefer non-Qt charting (pyqtgraph, matplotlib): some Qt modules, Qt Charts among them, are GPL-or-commercial rather than LGPL, and GPL would reach our own code.

Contributions require a DCO sign-off (git commit -s); see CLA.md. Security reports: SECURITY.md.

Release files for pulse-viewers 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pulse-viewers 0.1.0
File Size Uploaded
pulse_viewers-0.1.0.tar.gz 64.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pulse-viewers 0.1.0
File Interpreter ABI Platform
pulse_viewers-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 118.7 kB

Release files / pulse_viewers-0.1.0.tar.gz

Download URL pulse_viewers-0.1.0.tar.gz
Size 64.5 kB
Tags Source
SHA-256 checksum
How to use checksums
521bb1ab8c55c1d95c92547a7ecef838f0ddac715b10684feabe2d62c116d224
BLAKE2b-256 checksum
How to use checksums
195a5329f87e7288b4296bd1115c7a4202e4ccab9bf71f38b96a6ddef403fcf8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.15

Release files / pulse_viewers-0.1.0-py3-none-any.whl

Download URL pulse_viewers-0.1.0-py3-none-any.whl
Size 54.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
058b3180b036ff5c386fe6faa46f033d8c17fcb25b470de479a4dfcd0626f907
BLAKE2b-256 checksum
How to use checksums
00b798b6233e1fef292b331a00412ab01d0f8e7c29f544abb50c413f74ace759
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.15

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page