Skip to content

Engine Lifecycle: Design-Time, Init-Time, Runtime

UTLXe engine feature. This chapter covers the lifecycle of transformations in the UTLXe production engine — the three phases, execution strategies, and worker threading model. The CLI (utlx) collapses all three phases into a single invocation: parse, execute, serialize, done.

Every UTL-X transformation goes through three distinct phases. Understanding them is key to performance tuning, debugging, and choosing the right execution strategy.

Phase 1: Design-Time

Where: developer's laptop, VS Code + utlxd daemon.

When: while writing the transformation.

What happens:

  1. Developer writes a .utlx file

  2. The daemon (utlxd) parses the source in real-time (grammar → AST)

  3. The IDE shows syntax errors, autocompletion, and type hints

  4. Live preview: the transformation runs on sample data with every keystroke

  5. Optionally: schema-to-schema mapping with the visual mapper

Design-time is human time — minutes to hours per transformation. The optimization here is a good IDE, schema-driven development, and reusable patterns. AI-assisted generation (Chapter 45) can reduce this significantly.

Artifacts produced:

  • .utlx file — the transformation source

  • transform.yaml — engine configuration (strategy, validation policy, schemas)

  • Schema files — input/output validation contracts (optional)

Phase 2: Init-Time (Load and Compile)

Where: UTLXe engine, at startup or on hot-reload.

When: when a transformation is loaded — at startup from a bundle, or dynamically via the API.

What happens:

  1. Parse .utlx source → AST (same parser as design-time)

  2. Resolve schemas (if referenced in transform.yaml)

  3. Select execution strategy:

StrategyWhat init doesInit cost
TEMPLATEParse to AST — ready to interpretInstant (milliseconds)
COPYBuild skeleton DOM from output schema, pre-allocate object poolFast (tens of milliseconds)
COMPILEDCompile AST → JVM bytecode via ASM libraryPaid once at init-time (seconds), before the engine reports ready; then cached. Not a first-message cost.
COPY + COMPILEDBuild skeleton DOM and compile fill logic to JVM bytecodePaid once at init-time (seconds), before ready; then cached. Not a first-message cost.
AUTOSchema present → COPY, no schema → TEMPLATEDepends on schema
  1. Register transformation in the registry (ConcurrentHashMap)

  2. Transformation is now ready to accept messages

Init-time is the most important phase for performance thinking. A slow init is paid once. A slow runtime is paid per message — thousands or millions of times. Invest init-time to save runtime.

Compilation is eager: COMPILED transformations are turned into bytecode while the bundle loads (or at upload time), not on the first message. The engine reports ready only once every transformation is compiled, so the first message already hits compiled code — there is no "slow first message". The COMPILED init cost is a one-time startup cost (and, in a scaled deployment, a per-replica startup cost), never a runtime latency spike.

Compilation Cache

The COMPILED strategy generates JVM bytecode from the AST using the ASM bytecode library — the same technology Java itself uses. The generated bytecode is cached by a SHA-256 hash of the .utlx source. If the source hasn't changed, the cached bytecode is reused — no recompilation.

This means (all at load/init-time, before the engine reports ready — never on a message):

  • First load of a new transformation: seconds (compilation)

  • Subsequent loads of the same transformation: instant (cache hit)

  • Changing one character in the source: recompiles (new hash)

Object Pool (COPY Strategy)

The COPY strategy pre-builds a skeleton UDM tree from the output schema at init-time. At runtime, each message gets a clone of the skeleton with values filled in — faster than building the output tree from scratch.

The skeleton is pooled (ConcurrentLinkedQueue, 32 slots). Workers take a skeleton from the pool, fill it, serialize the output, and return the skeleton. This avoids garbage collection pressure from creating and discarding thousands of UDM trees per second.

Phase 3: Runtime (Execute)

Where: UTLXe engine, per incoming message.

When: every time a message arrives — HTTP request, Dapr input binding, gRPC call, stdin.

What happens:

  1. Parse input data (XML/JSON/CSV/YAML → UDM)

  2. Pre-validate against input schema (if configured)

  3. Execute the transformation:

StrategyHow it executes
TEMPLATEWalk the AST, evaluate each expression (tree-walking interpreter)
COPYClone skeleton, fill values from input UDM (fast path, no interpretation)
COMPILEDCall generated JVM bytecode (JIT-optimized by HotSpot)
  1. Post-validate against output schema (if configured)

  2. Serialize output (UDM → target format)

  3. Return result

Runtime cost: 0.01ms (simple pass-through) to 50ms (complex transformation with large messages). The strategy choice determines the constant factor; the message size determines the linear factor.

How Strategies Compare

StrategyInit costRuntime per messageBest for
TEMPLATEInstantSlower (interpret AST)Development, simple transforms, low volume
COPYFast (build skeleton)Fast (clone + fill)Schema-driven, high throughput, predictable structures
COMPILEDSeconds at startup (one-time, before ready)Fastest (JVM bytecode)Maximum throughput, complex logic, hot paths
COPY + COMPILEDSeconds at startup (one-time, before ready)Fastest possibleProduction: skeleton for structure, bytecode for logic
AUTODependsDependsProduction default — lets the engine choose

The COPY + COMPILED Hybrid

The fastest execution path combines both strategies:

  • COPY builds the output skeleton at init-time — the structure is pre-allocated

  • COMPILED generates bytecode for the fill logic — expressions are JVM-optimized

  • Runtime: clone the skeleton (fast) + run compiled fill code (fast) = fastest possible path

This is not a separate strategy: value — it is what COPY does automatically: selecting COPY (or letting AUTO choose it) also compiles the fill logic to bytecode where possible. This combined path is what carries transformations that process 86,000+ messages per second.

The Three Phases Visualized

DESIGN-TIME (human)
  Developer → VS Code → utlxd → .utlx file + schemas
  Cost: minutes to hours, paid once per transformation

        │ .utlx file + transform.yaml + schemas
        ▼

INIT-TIME (engine startup)
  UTLXe loads .utlx → parse → compile/prepare → register
  Cost: milliseconds to seconds, paid once per engine start

  TEMPLATE: AST ready (instant)
  COPY:     build skeleton + object pool (fast)
  COMPILED: AST → bytecode via ASM (one-time at startup, before ready; cached after)

        │ registered transformation, ready for messages
        ▼

RUNTIME (per message)
  Message → parse → validate → transform → serialize → output
  Cost: 0.01-50ms per message, paid millions of times

  8-128 workers process messages concurrently
  Back-pressure: queue full → caller waits

Worker Threading Model

UTLXe uses a fixed-size worker thread pool for concurrent message processing:

Message Queue → [Worker 1] → transform → output
              → [Worker 2] → transform → output
              → [Worker 3] → transform → output
              → ...
              → [Worker N] → transform → output

Each worker is independent — no shared mutable state between workers. A transformation's execution is completely isolated: each worker has its own environment, its own UDM tree, its own output buffer.

Back-Pressure

The worker pool uses an ArrayBlockingQueue with CallerRunsPolicy:

  • Messages enter the queue

  • Workers take messages from the queue and process them

  • When the queue is full (all workers busy, queue at capacity): the calling thread executes the work itself

  • This prevents unbounded queue growth (no out-of-memory risk)

  • Natural flow control: the producer (message broker) slows down when workers are saturated

Tuning Workers

The --workers flag controls the pool size:

bash
utlxe --bundle transforms/ --workers 32

Rules of thumb:

  • Start with 8 workers per vCPU. A 4-vCPU container: 32 workers.

  • Increase for I/O-heavy transformations — if workers wait on downstream HTTP calls, more workers keep the CPU busy while others wait.

  • Decrease for memory-heavy transformations — large XML documents (10MB+) consume significant heap per worker. Fewer workers = less concurrent memory pressure.

  • Monitor: watch utlxe_active_workers (Prometheus gauge) and queue depth. If workers are always at capacity, add more. If they're mostly idle, reduce to save memory.

Thread Safety

UTL-X transformations are inherently thread-safe because they are purely functional:

  • No mutable variables (all bindings are let, not var)

  • No side effects (no file I/O, no database writes, no network calls from within the transformation)

  • No shared state (each execution gets its own environment)

  • $input is read-only (the UDM tree is never modified)

This means any transformation can run safely on any number of concurrent workers without locks, synchronization, or race conditions. The engine guarantees isolation; the language guarantees purity.

Released under AGPL-3.0.