6 — Operating & going deeper
You can now model, write, query, and run a world. This last chapter is about living with a running engine: seeing inside it, giving it sensible memory budgets, handling the errors it can throw — and knowing where to go when this guide runs out.
It's deliberately light. The theme throughout Typhon holds here too: the engine does the heavy lifting (it instruments and budgets itself); your job is to turn on what you want to see and set a few caps. None of this is required to build a working app — reach for it when you're moving from "it runs" to "it runs in production."
1. 🔭 Seeing inside a running engine
Three independent surfaces, each opt-in, each costing nothing until you switch it on.
Metrics & health (OpenTelemetry)
Every long-lived part of the engine — page cache, WAL, transaction pool, scheduler — continuously reports its own memory, capacity, and throughput into an internal resource graph. You don't build any of it; you just expose it. One DI call wires the graph to OpenTelemetry meters plus health checks:
services.AddTyphonObservabilityBridge(o =>
{
o.SnapshotInterval = TimeSpan.FromSeconds(5); // how often metrics refresh
});
After that, the Typhon.Resources meter feeds your existing OTel/Prometheus pipeline, and a health check reports the worst subsystem (a single saturated resource is never lost in a sea of green). You read dashboards; the engine fills them.
The profiler
For deep, per-tick performance work there's an in-process profiler that captures a rich trace (spans for commits, queries, tick phases, lock contention, even off-CPU time) and a Workbench UI to explore it. The key fact for using it: it's config-driven, zero host code. Drop a typhon.telemetry.json next to your binary and the engine self-wires the profiler on the next run:
{ "Typhon": { "Profiler": { "Enabled": true, "Storage": { "PageCache": { "Enabled": true } } } } }
Categories are a tree — enable a parent to enable its children. You pick at deployment time what to capture; the rest stays off.
💡 Why on/off is a restart, not a runtime toggle. Typhon's telemetry gates are
static readonly bools resolved once at startup, so the JIT deletes every disabled instrumentation branch at compile time — a switched-off span isn't a cheapif, it's no code at all. That's the trade: you lose runtime toggling, and in exchange the engine pays literally zero CPU for telemetry it isn't emitting. For a microsecond-latency engine that's the right side of the bet — you don't want a hot path checking a config flag a million times a second.
The Workbench
The profiler captures; the Workbench is how you look. It's a local, browser-based UI — an ASP.NET Core server plus a single-page app — that opens what the engine produces and turns it into something you can read. Think "DataGrip for Typhon," not "a profiler with a few extra tabs": data browsing, schema inspection, query analysis, and timeline profiling live under one roof.
It opens three kinds of thing, and the views light up according to which one you picked:
- A trace file (
.typhon-trace) — a recorded run, opened offline. The full performance story. - A live engine — attach to a running process over TCP and watch ticks stream in as they happen.
- A database (a
.typhondirectory) — open the on-disk store directly to inspect its schema and physical layout, no trace needed.
What you get today, organised by what you're trying to understand:
Understand a run's performance (trace or live attach):
- Profiler timeline — the tick-by-tick view: per-tick duration, system spans, phases, overload level, metronome wait, with CPU samples and off-CPU (thread-scheduling) gaps overlaid on the right thread's lane. Pan and zoom over the whole run.
- Top Spans / Call Tree — the heaviest spans in the window, and the captured CPU-sample call tree for whatever you've selected — with click-through to the originating source line (Source Preview).
- Critical Path — the chain of systems that actually determined a tick's length, so you tune what matters instead of guessing.
- System DAG — the scheduler's dependency graph as a canvas, phase swim-lanes and derived edges, nodes coloured by their measured cost (mean / p50 / p95 / p99 / max over a chosen window).
Understand the queries (trace):
- Query Catalog, Query Plan Tree, and Execution Inspector — every query definition seen in the run, its plan (structural or with per-execution stats overlaid), and a drill-down into individual executions' phase breakdowns.
Understand the data model & storage (a .typhon database):
- Component / Archetype browsers and the Schema Inspector — survey every registered component (size, field count, entity count, indexes), inspect struct layout, indexes, and the systems that read/write or react to a component.
- Database File Map — a zoomable Hilbert-curve map of the actual data file: per-page fill, write-age, residency, segment/chunk structure, with search and pathology highlighting (e.g. under-filled pages). The physical truth of where your bytes live.
- Resource Tree — the engine's live resource graph (cache, WAL, pools…) as a navigable tree.
The shell ties it together: a dockable, persisted multi-panel workspace, a Ctrl+K-style command palette (commands and a #-prefixed jump-to-resource search), a Detail pane, and a Logs panel. A live attach session can also be saved to a self-contained .typhon-replay for later offline study.
How to open it. Start it with pwsh -File wb-dev.ps1 start from the repo root (it launches the backend on :5200 and the UI on :5173), browse to http://localhost:5173, and from the welcome screen pick Open .typhon File, Open .typhon-trace, or Attach to Engine. For an attach session, run your engine with the profiler's TCP exporter enabled (typhon.telemetry.json, above) and point the Workbench at the port; for a trace, just open the .typhon-trace the run left behind.
⚠️ The performance views need profiler data — a
.typhon-tracefrom a profiler-enabled run, or a live attach to an engine started with the TCP exporter on. Open a.typhondatabase (directory) with the profiler off and you still get the schema and File-Map views, but the timeline has nothing to show.
2. 🎚️ Resource budgets
The engine manages its own memory: a paged, memory-mapped store with a cache, the WAL stage, transaction pools. You never allocate a page. What you do set is a few caps, at engine-build time, and the engine lives within them.
.AddScopedManagedPagedMemoryMappedFile(o =>
{
o.DatabaseName = "skirmish";
o.DatabaseDirectory = ".";
o.DatabaseCacheSize = 512UL * 1024 * 1024; // page-cache size in bytes
})
.AddScopedDatabaseEngine(o =>
{
o.Resources.TotalMemoryBudgetBytes = 4L << 30; // overall budget — call o.Resources.Validate() yourself to enforce it
o.Wal = new WalWriterOptions(); // enable the WAL (durability)
});
The defaults are intentionally small — a 2 MB cache out of the box — so development exercises the cache machinery instead of hiding behind RAM. Size it up for real workloads. Call options.Resources.Validate() yourself after configuring — it throws if the fixed allocations (cache + WAL + buffers) don't fit inside TotalMemoryBudgetBytes. The engine does not call this automatically at boot — an oversized configuration is silently accepted unless you validate it yourself.
💡 Cache size is not a database-size cap.
DatabaseCacheSizebounds the resident working set, not how much you can store — the on-disk database can be many times the cache; cold pages live on disk and page in on demand (persistent data, indexes, and the entity map all page out — only Transient components stay RAM-resident). Size the cache for throughput/latency, not capacity. This is the SQL/SQLite model, and it's what sets Typhon apart from in-memory ECS frameworks.
When a bounded resource fills, each one has a baked-in policy: a cache evicts, the WAL waits (applying backpressure to commits), and hard client-facing limits fail fast with a ResourceExhaustedException (which is transient — see §3). You don't choose these per resource; they're semantic properties of each subsystem.
💡 Why you don't get a hundred tuning knobs. The budget surface is deliberately tiny because the engine knows its own access patterns better than a config file does. You declare the envelope (how much memory it may use, where the data lives, whether durability is on); it self-manages the rest. If you ever need to see where the budget is going, that's exactly what the resource graph in §1 shows you — observe first, then resize the one cap that matters.
3. 🧯 Error-handling ground rules
Typhon's error model is small and has three rules worth internalising before you write a catch.
1. The engine throws; it never retries. When an operation can't proceed — a lock timed out, the cache is under backpressure, a WAL write failed — it raises an exception and stops. Retry policy is yours: a game server skips the tick, a batch job waits and tries again, a test asserts it never happens. None of that belongs inside the engine.
2. Catch TyphonException, route on IsTransient. Every engine exception derives from TyphonException and carries an ErrorCode plus a transience hint:
try
{
using var tx = dbe.CreateQuickTransaction();
tx.Spawn<Unit>(/* … */);
tx.Commit();
}
catch (TyphonException ex) when (ex.IsTransient)
{
// the resource may free up — backoff / retry / drop (your call)
}
catch (TyphonException ex)
{
// terminal — log with ex.ErrorCode, alert, give up
throw;
}
IsTransient is true for the things that might pass on a second look — timeouts (TyphonTimeoutException and friends) and ResourceExhaustedException — and false for the terminal ones (corruption, a fatal WAL write, a schema mismatch).
3. The ones you'll actually meet. Most engine exceptions are operational edge cases. The handful that come from your own modeling are worth recognising:
| Exception | You'll hit it when… |
|---|---|
UniqueConstraintViolationException |
inserting a duplicate key into a [Index] (unique) field (ch.2) |
SchemaValidationException / SchemaDowngradeException |
reopening a database with a changed struct and no revision bump, or with an older app than wrote it (ch.2) |
TyphonTimeoutException |
a lock or transaction blew its deadline under contention — transient |
⚠️ Typhon's
TyphonTimeoutExceptiondoes not derive fromSystem.TimeoutException— catch the Typhon type. And don't write speculativecatchblocks for exceptions the docs list as reserved but not yet thrown (TransactionConflictException, etc.) — they'd be dead code until those types ship.
You won't see the engine's hot-path Result<,> return type (used internally for "not found" / "not visible" so those routine outcomes don't pay for a thrown exception) — it surfaces as ordinary return values like a query that simply yields nothing.
4. 🗺️ Going deeper — the map
This guide is the task-oriented layer. When you need struct layouts, algorithms, invariants, or the why behind a design, the in-depth overview is the contributor/power-user reference. The mapping from a guide chapter to where it goes deep:
| When you want… | Go to |
|---|---|
| The MVCC revision chain, visibility predicate | 05-revision |
| ECS storage internals, generated accessors, archetype layout | 06-ecs |
| Schema diffing & the migration engine | 04-schema |
| Index structures (B+Tree) and the query planner | 03-indexing · 09-querying |
| The spatial grid + R-tree, broad-phase, margins | 07-spatial |
| Transaction/UoW mechanics, conflict detection | 08-transactions |
| The scheduler in full — DAG derivation, overload, fence | 10-runtime |
| WAL, checkpoint, crash recovery (FPI) | 11-durability |
| The telemetry pipeline & profiler internals | 12-observability |
| The resource graph & budgets in detail | 13-resources |
| The full exception hierarchy & error codes | 14-errors |
| Page cache, memory-mapped store, foundation primitives | 02-storage · 01-foundation |
🎓 You've finished the guide
You can now declare a data model, choose the right storage mode per component, write and read it transactionally, query it (one-shot and reactive), run systems over it every tick in parallel, and operate the result. That's the whole arc — from your first spawned entity to a real-time engine you can observe and tune.
Where to go from here:
- Build something — the loop from ch.1 is a real, runnable starting point; grow it.
- Go deep when you hit a wall — the map in §4 points at the exact reference chapter.
🧩 Key concepts & types
Concepts: DatabaseEngine · Page cache & paged store · WAL & checkpoint.
Exact calls: AddTyphonObservabilityBridge / ObservabilityBridgeOptions · typhon.telemetry.json (profiler config) · .typhon-trace (recorded run) / .typhon-replay (saved attach session) · the Workbench (tools/Typhon.Workbench, http://localhost:5173) · DatabaseCacheSize · DatabaseEngineOptions.Resources (TotalMemoryBudgetBytes) · WalWriterOptions · TyphonException (ErrorCode / IsTransient) · TyphonTimeoutException · ResourceExhaustedException · UniqueConstraintViolationException · SchemaValidationException.