Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ RUN pnpm build

EXPOSE 3000

CMD ["node", "--no-node-snapshot", "--import", "tsx", "benchmarks/dynamic-apps/src/server.ts", "--host", "0.0.0.0"]
CMD ["node", "--import", "tsx", "benchmarks/dynamic-apps/src/server.ts", "--host", "0.0.0.0"]
27 changes: 21 additions & 6 deletions benchmarks/dynamic-apps/RESULTS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
# Direct-isolate Dynamic Apps benchmark
# agentOS inline Dynamic Apps benchmark

Status: local correctness implemented; deployed performance qualification is
pending.

The active architecture keeps the durable per-app Rivet actor for deployment,
then caches one agentOS VM per immutable release and executes the exported ESM
dispatcher through headless JavaScript evaluation. `pooled` mode resets and
reinitializes retained contexts; `ephemeral` mode requests a fresh context from
the cached VM.

No agentOS inline latency numbers are published yet. In particular, the old
guest-server and native-isolate results below must not be reused as inline
evaluation latency or as evidence that the new performance gates pass.

## Historical comparison: direct-isolate runtime

Qualified 2026-08-26 (US/Pacific). The rewritten architecture keeps the
durable per-app Rivet actor for releases and invalidation, but removes scaler
Expand Down Expand Up @@ -26,7 +41,7 @@ direct isolate pool 2, actor-worker cache 4, and actor-worker heap limit 96 MiB.
Every Cloud mutation targeted only
`dynamic-apps-ben-562e-production-sqac`.

## Runtime hardening qualification (2026-08-30)
## Historical runtime hardening qualification (2026-08-30)

The local stress suite now exercises multi-app cache churn, large payload
bursts, release invalidation, cold-cache fan-out, queue overflow, oversized
Expand Down Expand Up @@ -75,7 +90,7 @@ from the same release. Its warm actor action cases were 100% successful at
23.42 ms sequential p50 and 61.27 ms concurrency-16 p50; those numbers include
the local Engine path, unlike the lower-level worker timing above.

## Decision
## Historical decision

**Use the direct local-isolate path for ordinary request/response. Keep Rivet
actors for durable state and app-defined actor semantics.**
Expand All @@ -92,7 +107,7 @@ Public Rivet Run ingress from this client is roughly 190–220 ms at low load, s
outer latency hides the architectural difference. Server timing headers are
the relevant comparison.

## Final Rivet Compute direct results
## Historical Rivet Compute direct results

The workload is a zero-dependency JSON `fetch()` handler. Initialization and
warm-up requests are excluded from the steady samples.
Expand Down Expand Up @@ -141,7 +156,7 @@ The small pool deliberately trades burst-tail CPU for bounded idle memory.
Even with 21.23% overflow creation at concurrency 32, the stated server target
(p50 <= 25 ms, p95 <= 50 ms) passed and memory returned to its steady bound.

## App-defined actor results
## Historical app-defined actor results

The actor fixture uses ordinary RivetKit state, actions, an event subscription,
and a direct HTTP handler in the same deployment. Correctness passed locally
Expand All @@ -164,7 +179,7 @@ about 5.5x the complete fresh-isolate server p50. Public ingress adds another
roughly 225 ms at the median. This confirms that the earlier actor-heavy design
was benchmarking network topology rather than a useful V8 warm-cache advantage.

## Local direct baseline
## Historical local direct baseline

The same direct executor on a local Engine completed a 10,000-request pool-2,
concurrency-2 run at 100% success:
Expand Down
4 changes: 2 additions & 2 deletions benchmarks/dynamic-apps/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
"@rivet-dev/agentos-toolchain": "0.2.15",
"@rivet-dev/dynamic-apps": "workspace:*",
"hono": "^4.12.9",
"rivetkit": "0.0.0-fix-rivetkit-wasm-serve-config.e2b11f9"
"rivetkit": "2.3.11"
},
"devDependencies": {
"@rivetkit/engine-cli": "0.0.0-fix-rivetkit-wasm-serve-config.e2b11f9",
"@rivetkit/engine-cli": "2.3.11",
"@types/node": "^22.19.15",
"get-port": "^7.1.0",
"tsx": "^4.20.6",
Expand Down
30 changes: 15 additions & 15 deletions benchmarks/dynamic-apps/src/cloud-stress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
type StressMode = "ramp" | "soak" | "both";

interface StressCase {
name: "warm" | "snapshot" | "actor";
name: "pooled" | "ephemeral" | "actor";
path: string;
concurrency: number;
echoRequestId: boolean;
Expand Down Expand Up @@ -103,10 +103,10 @@ export async function runCloudStress(
try {
if (config.mode === "ramp" || config.mode === "both") {
result.ramps.push(
await runRamp(config, "warm", (concurrency) => [
await runRamp(config, "pooled", (concurrency) => [
{
name: "warm",
path: "/bench/warm",
name: "pooled",
path: "/bench/pooled",
concurrency,
echoRequestId: true,
},
Expand Down Expand Up @@ -271,20 +271,20 @@ async function runStage(
}

function mixedCases(concurrency: number): StressCase[] {
const warm = Math.max(1, Math.floor(concurrency * 0.8));
const snapshot = Math.max(1, Math.floor(concurrency * 0.1));
const actor = Math.max(1, concurrency - warm - snapshot);
const pooled = Math.max(1, Math.floor(concurrency * 0.8));
const ephemeral = Math.max(1, Math.floor(concurrency * 0.1));
const actor = Math.max(1, concurrency - pooled - ephemeral);
return [
{
name: "warm",
path: "/bench/warm",
concurrency: warm,
name: "pooled",
path: "/bench/pooled",
concurrency: pooled,
echoRequestId: true,
},
{
name: "snapshot",
path: "/bench/snapshot",
concurrency: snapshot,
name: "ephemeral",
path: "/bench/ephemeral",
concurrency: ephemeral,
echoRequestId: true,
},
{
Expand Down Expand Up @@ -357,7 +357,7 @@ async function setup(baseUrl: string): Promise<unknown> {
);
for (let index = 0; index < 10; index += 1) {
const response = await fetch(
`${baseUrl}/bench/warm?requestId=warmup-${index}`,
`${baseUrl}/bench/pooled?requestId=warmup-${index}`,
{
signal: AbortSignal.timeout(60_000),
},
Expand All @@ -372,7 +372,7 @@ async function setup(baseUrl: string): Promise<unknown> {
body.requestId !== `warmup-${index}`
) {
throw new Error(
`direct warmup ${index} failed with HTTP ${response.status}`,
`direct pooled warmup ${index} failed with HTTP ${response.status}`,
);
}
}
Expand Down
54 changes: 21 additions & 33 deletions benchmarks/dynamic-apps/src/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,15 @@ export function createBenchmarkApplication(): Hono {
...process.env,
DYNAMIC_APPS_TIMING_HEADERS: "1",
});
const warm = new DynamicAppsExecutor({
const pooled = new DynamicAppsExecutor({
...baseConfig,
isolateMode: "prewarm",
isolatePoolSize: integerEnv("BENCH_WARM_POOL_SIZE", 8, 1, 128),
executionMode: "pooled",
contextPoolSize: integerEnv("BENCH_CONTEXT_POOL_SIZE", 8, 1, 128),
});
const snapshot = new DynamicAppsExecutor({
const ephemeral = new DynamicAppsExecutor({
...baseConfig,
isolateMode: "snapshot",
isolatePoolSize: 0,
});
const fresh = new DynamicAppsExecutor({
...baseConfig,
isolateMode: "fresh",
isolatePoolSize: 0,
executionMode: "ephemeral",
contextPoolSize: 0,
});
const client = createClient() as unknown as StateClient &
BenchmarkDeploymentClient;
Expand Down Expand Up @@ -126,7 +121,7 @@ export function createBenchmarkApplication(): Hono {
const direct = (
executor: DynamicAppsExecutor,
prefix: string,
architecture: "warm" | "snapshot" | "fresh",
architecture: "pooled" | "ephemeral",
request: Request,
) => {
const url = new URL(request.url);
Expand All @@ -138,21 +133,17 @@ export function createBenchmarkApplication(): Hono {
return response;
});
};
app.all("/bench/warm", (c) => direct(warm, "/bench/warm", "warm", c.req.raw));
app.all("/bench/warm/*", (c) =>
direct(warm, "/bench/warm", "warm", c.req.raw),
);
app.all("/bench/fresh", (c) =>
direct(fresh, "/bench/fresh", "fresh", c.req.raw),
app.all("/bench/pooled", (c) =>
direct(pooled, "/bench/pooled", "pooled", c.req.raw),
);
app.all("/bench/fresh/*", (c) =>
direct(fresh, "/bench/fresh", "fresh", c.req.raw),
app.all("/bench/pooled/*", (c) =>
direct(pooled, "/bench/pooled", "pooled", c.req.raw),
);
app.all("/bench/snapshot", (c) =>
direct(snapshot, "/bench/snapshot", "snapshot", c.req.raw),
app.all("/bench/ephemeral", (c) =>
direct(ephemeral, "/bench/ephemeral", "ephemeral", c.req.raw),
);
app.all("/bench/snapshot/*", (c) =>
direct(snapshot, "/bench/snapshot", "snapshot", c.req.raw),
app.all("/bench/ephemeral/*", (c) =>
direct(ephemeral, "/bench/ephemeral", "ephemeral", c.req.raw),
);

app.all("/bench/actor/resolve", async () => {
Expand Down Expand Up @@ -229,7 +220,7 @@ export function createBenchmarkApplication(): Hono {
const second = await counter.add(3);
const current = await counter.inspect();
await counter.dispose();
const directResponse = await warm.request(
const directResponse = await pooled.request(
ACTOR_BENCHMARK_APP_ID,
new Request("http://dynamic-app.test/"),
);
Expand Down Expand Up @@ -280,16 +271,14 @@ export function createBenchmarkApplication(): Hono {
memory: process.memoryUsage(),
},
cpuParallelism: availableParallelism(),
warm: warm.diagnostics(),
snapshot: snapshot.diagnostics(),
fresh: fresh.diagnostics(),
pooled: pooled.diagnostics(),
ephemeral: ephemeral.diagnostics(),
paths: [
"/bench/noop",
"/bench/actor/resolve",
"/bench/actor/action",
"/bench/warm",
"/bench/snapshot",
"/bench/fresh",
"/bench/pooled",
"/bench/ephemeral",
"POST /bench/actor-app/setup",
"POST /bench/actor-app/verify",
"/bench/actor-app/action",
Expand Down Expand Up @@ -386,8 +375,7 @@ export function actorApplicationClientConfig(
poolName: deployment.pool,
...(deployment.token || endpointToken || process.env.RIVET_TOKEN
? {
token:
deployment.token ?? endpointToken ?? process.env.RIVET_TOKEN,
token: deployment.token ?? endpointToken ?? process.env.RIVET_TOKEN,
}
: {}),
};
Expand Down
9 changes: 8 additions & 1 deletion benchmarks/dynamic-apps/src/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,20 @@ export async function deployBenchmarkFixture(
main: "index.js",
}),
"index.js": `
import { basename } from "node:path";

export default {
fetch(request) {
const url = new URL(request.url);
if (url.searchParams.get("logs") === "1") {
console.log("benchmark stdout");
console.error("benchmark stderr");
}
return Response.json({
ok: true,
workload: "basic-request-response",
requestId: url.searchParams.get("requestId"),
node: { platform: process.platform, file: basename(import.meta.filename) },
});
},
};
Expand Down Expand Up @@ -61,7 +68,7 @@ export async function deployActorBenchmarkFixture(
type: "module",
main: "index.js",
dependencies: {
rivetkit: "0.0.0-fix-rivetkit-wasm-serve-config.e2b11f9",
rivetkit: "2.3.11",
},
}),
"index.js": `
Expand Down
3 changes: 0 additions & 3 deletions benchmarks/dynamic-apps/src/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,6 @@ export async function runLoadTest(
}
for (const [phase, header] of Object.entries({
bundleLoad: "x-agentos-app-bundle-load-ms",
isolateStart: "x-agentos-app-isolate-start-ms",
processReady: "x-agentos-app-process-ready-ms",
dispatch: "x-agentos-app-dispatch-ms",
appReleaseLookup: "x-agentos-app-release-lookup-ms",
appRequestBody: "x-agentos-app-request-body-ms",
appScalerAcquire: "x-agentos-app-scaler-acquire-ms",
Expand Down
Loading
Loading