Skip to content

Platform runtime

PAM Native 0.6 adds bounded platform primitives that share the retained runtime instead of building separate execution loops. Every coded kind, state, type, priority, and opcode is an integer-backed enum.

IdlCompiler validates sequential module, method, and field IDs and generates fingerprinted PHP, Kotlin, Swift, and Rust contracts:

$artifacts = IdlCompiler::compile(file_get_contents('bridge.pam-idl.json'));

Schemas are limited to 1 MiB, 256 modules, 256 methods per module, and 128 fields per method.

$products = new AsyncResource(
fn (CancellationToken $token) => $repository->products($token),
key: 'products',
);
$products->load(TaskPriority::UserBlocking);
return Suspense::make(
$products->value(),
content: fn (array $items) => ProductGrid::make($items),
fallback: ProductSkeleton::make(),
);

Immediate, user-blocking, render, normal, background, and idle tasks run in priority order. Coalescing cancels obsolete work and draining respects a frame budget.

$opacity = Worklet::input()
->interpolate(0, 200, 1, 0)
->clamp(0, 1);

PNW1 worklets are bounded numeric programs. They cannot call PHP, allocate objects, access global state, or perform I/O.

VirtualizedList, VirtualGrid, and SectionList use Rust layout and native recycling. Heterogeneous cells, grids, inverted/horizontal layouts, stable keys, authored or estimated sizes, bounded prefetch, initial index, and end-reached events are supported without per-frame PHP traffic.

BackgroundJobs stores idempotent work in OfflineMutationQueue. Its snapshot can be persisted with PAM storage or Nitro and restored after process death.

$jobs->dispatch(
'messages.sync',
uniqueKey: 'conversation:42',
payload: ['conversationId' => 42],
);

The queue has typed queued, sending, applied, retry, conflict, and failed states, plus capped exponential backoff, 256 KiB payloads, and 10,000 entries.

return Canvas::make()
->roundedRectangle(8, 8, 120, 48, 12, 0xFF6750A4)
->circle(180, 32, 24, 0xFFFFD23F)
->line(8, 80, 220, 80, 4, 0xFF111111);

Android and UIKit render retained vector commands using platform-accelerated drawing. Geometry must be finite; command count and payload size are bounded.

Server-driven documents are data, not downloadable PHP:

$tree = ServerDrivenUi::render(
$document,
actions: fn (string $name): ?Closure => match ($name) {
'offer.open' => $this->openOffer(...),
default => null,
},
);

Only allowlisted native nodes, numeric styles, and locally resolved actions are accepted. A document cannot name or call PHP classes or functions. Documents are limited to 1 MiB, 10,000 nodes, 64 levels, and 16 KiB text values.

PAM owns, builds, checksums, and versions the PHP runtime used by a Native app. The project declares the supported series and release channel in pam-native.json; development, profiling, and release builds then resolve the same runtime lock.

{
"runtime": {
"php": "8.5",
"channel": "stable"
}
}
Terminal window
pam mobile runtime:list
pam mobile runtime:use 8.5
pam mobile runtime:info
pam mobile runtime:update

The resolved artifact is written to .pam-native/runtime.lock.json. Runtime IDs have the form <php-version>-r<revision>: a PHP security release changes the PHP version, while a PAM build or flag change increments the revision. Keep pam-native.json in version control and preserve the generated lock as a CI artifact. PAM Native consumes the runtime; it does not publish a separate PHP binary.

Development, production, and benchmark modes

Section titled “Development, production, and benchmark modes”

Set PAM_NATIVE_MODE before starting or building the application:

Value Behavior
development Runtime diagnostics and flexible compiler checks.
production Strict compilation with profiling removed from the hot path.
benchmark Strict compilation with performance measurements enabled.

PAM_NATIVE_STRICT=1 enables production compiler checks during development. Strict compilation rejects dynamic component properties and runtime code evaluation that would prevent deterministic dependency analysis.

PAM treats performance as a set of cooperating contracts:

  • binary bridge batches, pooled ABI buffers, and append-only typed IDL avoid per-command JSON and recycle native allocations;
  • retained node identity applies property and structure patches only where the tree changed;
  • the scheduler uses the physical 60/90/120 Hz refresh rate as its deadline and protects input work through priority, cancellation, and coalescing;
  • virtualized lists use stable keys, variable extents, velocity-biased overscan, and logarithmic window lookup to keep 100,000-item data sets bounded;
  • every queue and cache has a limit, and platform memory-pressure signals can reclaim retained resources.

Component-local state and Store reads are recorded against the component being rendered. A write invalidates only its subscribers, so clean components keep their previous element subtree and descendant lifecycle state. Components that use untracked mutable public fields retain full-render behavior for compatibility.

Incremental layout marks changed node IDs and their ancestor paths dirty. The Rust engine recalculates the affected container, prunes clean subtrees with the same retained frame, and removes stale geometry when visibility changes. Fixed-size text boxes can skip intrinsic measurement when their resolved geometry cannot change.

Compile every .pam.php component during the release build:

Terminal window
vendor/bin/pam-native-optimize src build/pam-cache

The optimizer emits versioned component metadata, a SHA-256 manifest, and a relocatable pam-preload.php. Loading the preload primes OPcache and generates constructor factories and prop schemas before the first application frame, so template parsing and component reflection leave the startup hot path. Cache format versions invalidate stale metadata safely.

The bounded Profiler retains at most 512 spans for php.render, php.encode, and scheduler.task, including duration, timestamp, and integer priority. Android adds decode/mount trace sections, frame metrics, startup macrobenchmarks, and Baseline Profile journeys; iOS exposes renderer metrics to the runtime callback and Instruments.

The Rust observatory reports frame deadline misses, retained bytes, buffer reuse, node counts, coalesced commands, and P95 decode, reconcile, layout, and encode latency through the stable C ABI. Both DevTools overlays consume those same counters.

After a confirmed native commit, the runtime publishes a throttled atomic checkpoint. A rejected commit never replaces lastFrame, preserving the last known-good native hierarchy. Stable failure fingerprints track consecutive errors; three consecutive failures enter safe mode instead of creating a restart loop.

When an incremental patch is invalid, the encoder preserves PHP component identity and cached subtrees, discards only the previous wire snapshot, and immediately emits one complete tree. This recoverable path does not open an error overlay. If the complete tree is also rejected, the error remains visible and the checkpoint does not advance.

The Native CI contract covers PHP SDK and lifecycle tests, deterministic 1,000-frame tree fuzzing, a 1,001-node encoder budget, Rust release-mode gates, Android API 26/36 instrumentation and macrobenchmarks, iOS simulator coverage for a bounded 100,000-item window, and optimized release builds.

Local thresholds can be overridden with PAM_PERF_FIRST_FRAME_MS and PAM_PERF_STEADY_FRAME_MS. Reproduce the native engine gate with:

Terminal window
cargo run --release -p pam-native-engine --example benchmark -- --check

Its full-tree and patch ceilings use PAM_PERF_ENGINE_FULL_TREE_NS and PAM_PERF_ENGINE_PATCH_NS. Device macrobenchmarks remain the source of truth for startup, frame timing, and memory.