See what your application is doing.
Local runtime tracing, performance analysis, and debugging for
Python · JavaScript / TypeScript · Instrumented C
Follow execution from a request to the functions, files, and database calls behind it. Explore a live call tree, inspect exceptions in context, find expensive work, and return to a recording later. Everything stays on your machine in SQLite.
Requires Python 3.11+. Node tracing also requires Node.js 22.15+ and npm. C tracing requires a GCC-compatible compiler and binutils; on Windows, use MSYS2 UCRT64 GCC.
./scripts/setup.ps1
./scripts/run.ps1 trace python examples/python/workload.pyOr install directly from this checkout:
python -m venv .venv
# Activate .venv using your shell, then:
python -m pip install -e .
npm ci
eventhorizon trace python examples/python/workload.pyThe terminal explorer opens in an interactive terminal. Redirected output automatically uses a readable summary. Add --headless to record without the interface.
Examples are grouped under examples/python, examples/js, and examples/c. The workload examples finish normally; the error examples deliberately fail.
| Scenario | Python | JavaScript / TypeScript | C |
|---|---|---|---|
| Finishes normally | python/workload.py |
js/workload.cjs, js/workload.ts |
c/workload.c |
| Fails after successful work | python/runtime_error.py |
js/runtime_error.cjs |
c/crash.c |
| Invalid syntax; no application code runs | python/syntax_error.py |
js/syntax_error.cjs |
c/syntax_error.c (build fails) |
| Immediate runtime failure | — | — | c/startup_error.c |
| Runs until interrupted | python/server.py |
js/server.cjs |
c/long_running.c |
Run the following commands from the repository root, one scenario at a time. Close the explorer with q before starting the next scenario.
./scripts/run.ps1 trace python examples/python/workload.py
# Optional workload arguments:
./scripts/run.ps1 trace python examples/python/workload.py --iterations 12 --delay 0.05
# Deliberate failures: after successful work, then during parsing.
./scripts/run.ps1 trace python examples/python/runtime_error.py
./scripts/run.ps1 trace python examples/python/syntax_error.py
./scripts/run.ps1 trace python examples/python/server.pyWhile the Python server is running, use another PowerShell terminal:
Invoke-RestMethod http://127.0.0.1:8765/health
Invoke-RestMethod http://127.0.0.1:8765/work
Invoke-WebRequest http://127.0.0.1:8765/error # Deliberate HTTP 500; server continues.
Invoke-RestMethod http://127.0.0.1:8765/health./scripts/run.ps1 trace node examples/js/workload.cjs
./scripts/run.ps1 trace node examples/js/workload.ts
# Deliberate failures: after successful work, then during parsing.
./scripts/run.ps1 trace node examples/js/runtime_error.cjs
./scripts/run.ps1 trace node examples/js/syntax_error.cjs
./scripts/run.ps1 trace node examples/js/server.cjsWhile the JavaScript server is running, use another PowerShell terminal:
Invoke-RestMethod http://127.0.0.1:8766/health
Invoke-RestMethod http://127.0.0.1:8766/work
Invoke-WebRequest http://127.0.0.1:8766/error # Deliberate HTTP 500; server continues.
Invoke-RestMethod http://127.0.0.1:8766/healthBuild each example before tracing its executable:
./scripts/run.ps1 build-c examples/c/workload.c -o .eventhorizon/workload.exe
./scripts/run.ps1 trace native .eventhorizon/workload.exe
# Fatal signal after successful work; unfinished calls are expected.
./scripts/run.ps1 build-c examples/c/crash.c -o .eventhorizon/crash.exe
./scripts/run.ps1 trace native .eventhorizon/crash.exe
# Immediate nonzero exit at runtime.
./scripts/run.ps1 build-c examples/c/startup_error.c -o .eventhorizon/startup-error.exe
./scripts/run.ps1 trace native .eventhorizon/startup-error.exe
# Expected compiler failure; there is no executable to trace.
./scripts/run.ps1 build-c examples/c/syntax_error.c -o .eventhorizon/syntax-error.exe
# Continuous worker; stop with q in the explorer.
./scripts/run.ps1 build-c examples/c/long_running.c -o .eventhorizon/worker.exe
./scripts/run.ps1 trace native .eventhorizon/worker.exeIn the TUI, Ctrl+X or Stop ends a launched server/worker and keeps its recording open for inspection; the target cannot be resumed. q stops the target and quits the explorer. In headless mode, use Ctrl+C. The C crash example deliberately raises a fatal signal; unfinished calls are expected. C syntax errors occur during compilation, so startup_error.c separately demonstrates failure at runtime before useful work.
./scripts/run.ps1 trace --headless python examples/python/workload.py
./scripts/run.ps1 sessions
./scripts/run.ps1 show latest
./scripts/run.ps1 show latest --tree
./scripts/run.ps1 show latest --replay 2
./scripts/run.ps1 export latest --format json -o .eventhorizon/example-trace.json
./scripts/run.ps1 export latest --format folder -o .eventhorizon/example-trace-itemslatest selects the most recently started session. The JSON command writes one complete blob; the folder command writes indexed JSON files per item and requires a new or empty destination. Add --headless before python, node, or native in any trace command above to record without opening the explorer.
eventhorizon trace python app.py --your-app-argument
eventhorizon trace node server.js
eventhorizon trace node server.mjs
eventhorizon trace node examples/js/workload.ts
eventhorizon build-c examples/c/workload.c -o .eventhorizon/workload.exe
eventhorizon trace native .eventhorizon/workload.exeUse an executable path appropriate for your platform; .exe is only needed on Windows. C sources link against the bundled helper automatically. Add compiler flags with --cflag=-pthread, for example. Existing native builds can compile their own sources with -g -finstrument-functions, compile eventhorizon/backends/native.c separately with -fno-instrument-functions, and link both. Windows needs -lws2_32; Unix needs -pthread and a non-PIE executable for address resolution.
Tracing defaults to files under the current directory. Set --root when launching from elsewhere. Collector options go before the runtime; arguments after the script belong to your application.
eventhorizon trace --root ./src --exclude '*cache*' python src/app.py
eventhorizon trace --mode filtered --include '*process_order*' python examples/python/workload.py
eventhorizon trace --libraries --depth 32 --sample 5 node server.js
eventhorizon trace --mode sample python examples/python/workload.py
eventhorizon trace --mode sample node examples/js/workload.ts--include and --exclude accept repeatable glob fragments matching file, module, or function names. --sample N records every Nth eligible call; it is call thinning, not a CPU profiler. --mode sample instead collects nominal 10 ms statistical stack samples: Python samples wall stacks, while Node uses the V8 profiler. Samples are estimates, not exact call counts. Native hooks support call thinning and depth limits; native name filters apply at collection after symbol resolution.
| View | What it shows |
|---|---|
| Calls | Expandable execution order, durations, and observed exceptions |
| Hotspots | Call counts, inclusive/self time, percentiles, CPU where available |
| Samples | Sampled locations and estimated time in statistical mode |
| Timeline | Calls placed on a common time axis, with thread and task context |
| Requests | Inbound/outbound HTTP and custom request spans; Enter opens the call branch |
| Events / Errors | Correlated I/O, logs, exceptions, stacks, and source locations |
| Output | Last 500 chunks of the launched process's stdout/stderr; live only, never saved or exported |
| Bookmarks | Saved developer annotations attached to calls or the session |
| Compare | Mean latency and call-count changes against another session |
| Diagnostics | Capabilities, collection loss, incomplete calls, and process metrics |
| Key | Action |
|---|---|
| Arrow keys / Enter | Navigate and expand calls; inspect a table row |
/ |
Search |
| Space | Pause the display; collection continues |
| Ctrl+X / click Stop | End the launched target; keep the explorer open |
b |
Annotate the selected call; Enter saves |
e / click Export |
Choose an export format and destination in a popup |
r |
Toggle replay of a completed recording |
[ / ] |
Newer / older history page |
| Escape | Clear search, thread/task selection, and request scope |
d / q |
Toggle details / quit |
Drag to select text in calls, tables, output, or details; Ctrl+C or right-click copies the selection. The view holds steady while text is selected; click to clear the selection and resume live display updates. Collection continues throughout. The details pane has both scrollbars and does not wrap long lines. Terminals that reserve right-click for their own menu may require Shift+right-click or Ctrl+C. Theme choices made through the command palette (Ctrl+P → theme) are saved in your per-user EventHorizon configuration and restored next launch.
Live updates preserve scroll positions, expanded calls, selections, and detail context. Output follows new lines while you are at the bottom; scrolling up keeps your place. Stopped targets are saved with a stopped session status. For cooperative listeners, Stop ends collection; the independently launched application keeps running.
If a program fails before recording any calls, empty tabs are hidden and the explorer opens Errors or Output when available. Populated events, samples, bookmarks, and diagnostics remain accessible. Failures after successful calls keep the normal views, and an empty search does not hide tabs.
JSON in output, details, and diagnostics is colored. Ctrl+click or double-click a source file path in details or a Source column to open it for editing; its visual appearance stays unchanged. On Windows, the configured Edit application is used, with Notepad as a fallback. Requests clicks inspect a row; Enter opens its call branch. Leaving Calls clears that request scope.
Search plain text or combine name:, file:, thread:, task:, status:, category:, trace:, and min: (milliseconds). Events also accept kind: and level:. For example, category:http min:20 finds slow HTTP calls; kind:exception filters the event stream. Use the thread/task selector to isolate concurrent work.
The live interface reads 1,000 calls and 1,000 events per page. Hotspots and distributions describe that visible window; the window is labeled in the header. CLI summaries, comparisons, and exports use the full retained recording. A parent outside the page or tracing filter appears as a separate visible root. Collection detail, depth, and sampling are selected when launching; view filters can change during recording.
Sessions are recorded automatically in .eventhorizon/traces.sqlite relative to your working directory. Supply --db PATH before the command to use another database.
eventhorizon sessions
eventhorizon show latest
eventhorizon show latest --tree
eventhorizon show latest --replay 2
eventhorizon compare BEFORE_ID AFTER_ID
eventhorizon note SESSION_ID "Investigate the slow inventory request"
eventhorizon note SESSION_ID "Retry path" --span SPAN_ID
eventhorizon export latest --format json -o trace.json
eventhorizon export latest --format folder -o trace-items
eventhorizon export latest --format jsonl -o events.jsonl
eventhorizon export latest --format chrome -o timeline.json
eventhorizon export latest --format text -o report.txtSession IDs may be unambiguous prefixes. JSON includes session metadata, spans, raw normalized events, and annotations. JSONL streams raw events. Chrome trace JSON opens in compatible trace viewers such as Perfetto. Replay reads the saved SQLite recording; exported JSON is portable evidence, not an import format.
In the explorer, click Export in the footer or press e, choose a format, and edit the prefilled destination before confirming. Changing formats updates the suggested filename unless you have entered a custom path. Escape cancels; export errors stay in the popup so you can correct the destination.
--format json creates one complete JSON blob. --format folder requires a new or empty folder and creates session.json, one JSON file per span/event/note under spans/, events/, and notes/, plus index.json linking the items. The index is written last so an unfinished export does not appear complete.
Comparison flags a potential regression when mean observed latency rises by at least 20% and 1 ms; change the percentage with --threshold. It matches function name, source path, line, and category. Code moves can appear as added/removed functions. Results are observations, not statistical proof of a regression; use comparable workloads and tracing settings.
| Backend | Automatic visibility | Explicit limits |
|---|---|---|
| Python | Python calls/returns, exceptions including handled exceptions, asyncio tasks, new threads, file opens, sqlite3, http.client, http.server, logging |
Native extension internals, arbitrary database/HTTP clients, and threads already running before cooperative connection need their own integration. File payload reads/writes are not copied. |
| Node.js / TypeScript | Loaded CJS/ESM/TypeScript source functions, Promise results, async context, thrown exceptions, http/https, sync and Promise file operations, console |
Constructors, generators, eval, native internals, browser JS, native fetch, and database clients need explicit instrumentation. Async return tracing changes Promise identity and attaches rejection observers. TypeScript is stripped, not type-checked; JSX requires a precompiled application. |
| C | Functions compiled with GCC hooks, per-thread call stacks, named manual spans, live symbols and source locations | Uninstrumented libraries, arbitrary attachment, arguments/return values, C++ exception unwinding, longjmp, and asynchronous correlation are not automatic. Use unstripped debug symbols. ELF PIE/shared-library symbolization is outside the native launch backend. |
Node requires the npm dependencies installed in this source checkout. Source instrumentation executes applications; it is not a sandbox. Python and Node dependencies are included only with --libraries or an explicit include where supported. The profiler's own implementation is excluded. Handled exceptions remain available in the exception explorer; calls are marked failed when an exception escapes them.
Timing: durations use monotonic clocks anchored to wall time in each process. Self wall time subtracts the union of observed direct child intervals, so overlapping work is never subtracted twice. Inclusive totals can exceed session elapsed time through concurrency and recursion. CPU counters remain separate; Python thread CPU resolution depends on the OS, and Node/C function CPU time is unavailable. Process RSS, CPU utilization, and cumulative CPU time are sampled separately. Cross-process ordering is approximate; clock anchors and an estimated 1 ms uncertainty are recorded. HTTP durations end at response headers for clients and request handling/response completion for supported servers, not after consuming an entire response body.
To trace a process you control without relaunching it, start a listener:
eventhorizon listenIt prints a local address, token, and configuration. In the running Python application, connect deliberately and wrap operations as needed:
from eventhorizon.sdk import connect, span, annotate
connection = connect("127.0.0.1:PORT", "TOKEN", root="/path/to/project")
with span("checkout", category="request", order_id=42):
# Your application code
annotate("Inspect the retry decision")
connection.close() # Stop tracing and flush pending events when finished.Automatic tracing starts in the connecting thread and threads subsequently created. It cannot reconstruct earlier execution. In Node, applications launched with the collector's EH_ADDRESS, EH_TOKEN, and EH_CONFIG environment variables can preload eventhorizon/backends/node.cjs plus --import eventhorizon/backends/register.mjs. A running Node process can require the collector after setting those variables and use globalThis[Symbol.for('eventhorizon')].span(name, callback) for cooperative spans. Existing loaded code is not retroactively rewritten. C hooks use the same launch environment. HTTP integrations propagate W3C traceparent IDs between supported components. There is no arbitrary PID injection.
Argument values, returns, launch arguments after the entry point, and free-form log/error messages are off by default. Enable them deliberately with --values. Sensitive named fields and command switches such as passwords, tokens, cookies, credentials, and authorization are masked; HTTP query strings, fragments, headers, bodies, and SQL parameters are not recorded. Free-form strings can still contain secrets when value capture is enabled. Paths, function names, source locations, developer annotations, and your application's ordinary terminal output may also contain sensitive information.
Collection uses authenticated, loopback-only UDP and bounded queues, keeping the interface off the target's execution path. UDP is best-effort: sequence gaps, producer drops, queue drops, retention drops, malformed events, and unfinished spans are reported rather than silently treated as complete execution. Abrupt exits can lose final packets and summaries. Reported producer overhead measures transport/enqueue work, not the full instrumentation cost. Detailed tracing can substantially slow an application; use filters or statistical mode for lower overhead.
Defaults: 8,192 queued events · 500,000 stored events per session · 30 retained sessions. Change them with --buffer, --max-events, and --max-sessions. Old completed sessions are removed when a new recording starts; active sessions are retained. SQLite reuses freed pages. Quit stops processes launched by this recording; cooperative listener shutdown leaves independently owned applications running.
./scripts/diagnose.ps1
./scripts/release.ps1The release command archives the current committed source into dist/; it does not publish or push. Run setup after extracting it. The bundled workloads exercise concurrent requests, file operations, database calls, recoverable exceptions, and native recursion. They are runnable examples, not a test suite.