You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/09/21 13:50:30 UTC
(maka) branch main updated: perf(runtime-host): baseline materialized Session copy cost (#5526)
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/main by this push:
new 879e0a4bc2 perf(runtime-host): baseline materialized Session copy cost (#5526)
879e0a4bc2 is described below
commit 879e0a4bc2786524c568fd28f4c0e4d73eb81aa2
Author: KunLi <90...@users.noreply.github.com>
AuthorDate: Mon Sep 21 21:50:25 2026 +0800
perf(runtime-host): baseline materialized Session copy cost (#5526)
* perf(runtime-host): baseline session copy cost
* chore: normalize ASF headers
* fix(runtime-host): stabilize session copy benchmark
---------
Co-authored-by: likun <ku...@alva.xyz>
---
docs/architecture/session-copy-cost-baseline.md | 94 +++++++++
packages/runtime-host/package.json | 1 +
.../scripts/session-copy-cost-benchmark.mjs | 211 +++++++++++++++++++++
3 files changed, 306 insertions(+)
diff --git a/docs/architecture/session-copy-cost-baseline.md b/docs/architecture/session-copy-cost-baseline.md
new file mode 100644
index 0000000000..cc434e2c08
--- /dev/null
+++ b/docs/architecture/session-copy-cost-baseline.md
@@ -0,0 +1,94 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+# Session copy cost baseline
+
+This document records the current storage boundary for Session branch/revision
+copies and defines the reproducible baseline for issue #5439.
+
+## Current boundary
+
+The Host treats a branch or revision as a materialized, self-contained Session.
+`packages/runtime-host/src/server/session-revision-coordinator.ts` captures a
+settled source slice, creates the target Session, then publishes copied state
+across the following stores:
+
+| Domain | Current copy behavior | Identity after copy |
+| --- | --- | --- |
+| Session header | New target row; source metadata is projected into it | New Session id |
+| transcript messages | Runtime ledger is replayed into target projections | New message/run/event ids where the domain owns identity |
+| Runtime Events | Immutable events are cloned into the target Session | New target Session/run/event identities |
+| AgentRun operational events | Copied for the selected run closure | New target run/event identities |
+| model-call facts | Rebuilt from the copied run ledger and projection transitions | New target ledger facts; source usage facts are not shared |
+| task ledger / Todo | Target Todo state is initialized from the selected branch boundary | New target Session ownership |
+| artifacts | Payloads are copied into target-owned artifact records | New artifact ids and target paths |
+| context-offload references | Reference rows are copied, while the content-addressed `blobId` is reused | New target reference; shared blob payload |
+| linked child Sessions | Rejected, preserved with validated external ids, or snapshotted depending on copy kind | Explicitly controlled per reference mode |
+
+The important existing sharing boundary is context offload: `copyReferences`
+creates target-Session references without duplicating the blob bytes. The
+ordinary conversation copy path does not currently apply that ownership/payload
+split to transcript, Runtime Events, model-call facts, Todo state, or artifact
+payloads.
+
+## Measurement
+
+Run the benchmark after building the workspaces:
+
+```sh
+npm run build
+npm --workspace @maka/runtime-host run benchmark:session-copy
+```
+
+The benchmark creates real settled turns through the Runtime Host, invokes the
+production `session.branch.create` handler, and reports:
+
+- median copy latency in milliseconds after one warmup copy and five measured
+ samples;
+- storage bytes added under the temporary interactive root;
+- source and child projected message counts;
+- source and child invocation counts;
+- message-count amplification.
+
+The default fixtures use 4, 16, 64, and 128 settled turns. Each turn contains a
+256-byte user payload and uses the deterministic FakeBackend. This is a
+transcript/runtime-ledger baseline; it intentionally does not claim artifact or
+context-offload coverage until fixtures for those payload classes are added.
+
+The byte delta is measured after the warmup copy and immediately before and
+after the measured copies, then reported per measured copy. It includes SQLite
+journaling and metadata writes, so the number is an operational write-volume
+proxy rather than a logical row-size estimate. Run the same command on a clean
+machine and record the output with the commit, Node version, filesystem, and
+platform.
+
+## Interpretation
+
+This baseline answers the first question in #5439: whether the current copy
+cost grows with the retained history and whether the dominant growth is in
+runtime/transcript state. It does not choose a shared-prefix design. A future
+prototype must add comparable measurements for:
+
+1. artifact payloads and `session_file` references;
+2. context-offload references and shared blob bytes;
+3. export/import closure materialization;
+4. parent continuation, parent deletion, repair, and restart stability.
+
+The baseline should be kept as the materialized-copy control group when those
+experiments land.
diff --git a/packages/runtime-host/package.json b/packages/runtime-host/package.json
index d0c8f08a9e..f285d6a564 100644
--- a/packages/runtime-host/package.json
+++ b/packages/runtime-host/package.json
@@ -24,6 +24,7 @@
"scripts": {
"clean": "node ../../scripts/clean-paths.mjs dist tsconfig.tsbuildinfo",
"build": "tsc -p tsconfig.json",
+ "benchmark:session-copy": "node scripts/session-copy-cost-benchmark.mjs",
"benchmark:transcript": "node scripts/transcript-data-plane-benchmark.mjs",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test:dist": "node --test \"dist/**/*.test.js\""
diff --git a/packages/runtime-host/scripts/session-copy-cost-benchmark.mjs b/packages/runtime-host/scripts/session-copy-cost-benchmark.mjs
new file mode 100644
index 0000000000..f82a8d92f1
--- /dev/null
+++ b/packages/runtime-host/scripts/session-copy-cost-benchmark.mjs
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { performance } from 'node:perf_hooks';
+import { mkdtemp, readdir, rm, stat } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { randomUUID } from 'node:crypto';
+import { FakeBackend } from '@maka/runtime/test-only/fake-backend';
+import { SessionManager } from '@maka/runtime/session-manager';
+import { resolveStorageRoot, tryAcquireInteractiveRootOwner } from '@maka/storage/root-authority';
+import { createExecutionRuntimeHostComposition } from '../dist/server/execution-composition.js';
+
+const HISTORY_SIZES = [4, 16, 64, 128];
+const COPY_SAMPLE_COUNT = 5;
+const COPY_RETRY_ATTEMPTS = 20;
+const COPY_RETRY_DELAY_MS = 25;
+const CONNECTION_ID = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
+const operationContext = {
+ hostEpoch: 'session-copy-cost-benchmark',
+ connectionId: 'benchmark-client',
+ principal: 'local_os_user',
+ acquireResidency: () => ({ release() {} }),
+};
+
+const results = [];
+for (const historySize of HISTORY_SIZES) {
+ results.push(await runFixture(historySize));
+}
+console.table(results);
+
+async function runFixture(historySize) {
+ const root = await mkdtemp(join(tmpdir(), 'maka-session-copy-benchmark-'));
+ const capability = await resolveStorageRoot({ path: root, kind: 'interactive' });
+ const owner = await tryAcquireInteractiveRootOwner(capability);
+ if (!owner) throw new Error(`Could not acquire benchmark storage root: ${root}`);
+
+ let composition;
+ let manager;
+ const originalRecover = SessionManager.prototype.recoverInterruptedSessionsStrict;
+ SessionManager.prototype.recoverInterruptedSessionsStrict = async function (stores) {
+ manager = this;
+ return originalRecover.call(this, stores);
+ };
+ try {
+ composition = await createExecutionRuntimeHostComposition(
+ {
+ owner,
+ hostEpoch: operationContext.hostEpoch,
+ acquireResidency: () => ({ release() {} }),
+ retainUntilProcessExit: () => undefined,
+ requestDrain: () => undefined,
+ },
+ {},
+ {
+ primaryBackendFactory: (context) => new FakeBackend(context),
+ },
+ );
+ await composition.recover();
+
+ if (!manager) throw new Error('Execution composition did not construct a SessionManager');
+ const source = await manager.createSession({
+ cwd: root,
+ llmConnectionId: CONNECTION_ID,
+ llmConnectionSlug: 'fake',
+ model: 'fake-model',
+ permissionMode: 'bypass',
+ name: `copy-cost-${historySize}`,
+ });
+ let sourceTurnId;
+ for (let index = 0; index < historySize; index += 1) {
+ sourceTurnId = `turn-${index}`;
+ const started = await composition.handlers['turn.start'](
+ {
+ sessionId: source.id,
+ turnId: sourceTurnId,
+ content: { text: `benchmark turn ${index} ${'x'.repeat(256)}` },
+ },
+ operationContext,
+ );
+ if (!started.ok) throw new Error(`Turn ${index} failed to start: ${started.reason}`);
+ await waitForCompletedTurn(manager, source.id, sourceTurnId);
+ }
+
+ const sourceSummary = (await manager.listSessions()).find((item) => item.id === source.id);
+ if (!sourceSummary || !sourceTurnId) throw new Error('Benchmark source did not settle');
+
+ await copySession({ composition, manager, sourceSessionId: source.id, sourceTurnId });
+ const beforeBytes = await directoryBytes(root);
+ const copyDurations = [];
+ let targetSessionId;
+ for (let sample = 0; sample < COPY_SAMPLE_COUNT; sample += 1) {
+ targetSessionId = randomUUID();
+ const startedAt = performance.now();
+ await copySession({
+ composition,
+ manager,
+ sourceSessionId: source.id,
+ sourceTurnId,
+ targetSessionId,
+ });
+ copyDurations.push(performance.now() - startedAt);
+ }
+ const afterBytes = await directoryBytes(root);
+ const sourceMessages = await manager.getMessages(source.id);
+ const childMessages = await manager.getMessages(targetSessionId);
+ const sourceInvocations = await manager.listInvocations(source.id);
+ const childInvocations = await manager.listInvocations(targetSessionId);
+
+ return {
+ historySize,
+ copyMsMedian: median(copyDurations).toFixed(1),
+ addedMiB: ((afterBytes - beforeBytes) / COPY_SAMPLE_COUNT / (1024 * 1024)).toFixed(3),
+ sourceMessages: sourceMessages.length,
+ childMessages: childMessages.length,
+ sourceRuns: sourceInvocations.length,
+ childRuns: childInvocations.length,
+ amplification: (childMessages.length / Math.max(1, sourceMessages.length)).toFixed(2),
+ };
+ } finally {
+ SessionManager.prototype.recoverInterruptedSessionsStrict = originalRecover;
+ composition?.beginDrain();
+ await composition?.close();
+ await owner.close();
+ await rm(root, { recursive: true, force: true });
+ }
+}
+
+async function copySession({
+ composition,
+ manager,
+ sourceSessionId,
+ sourceTurnId,
+ targetSessionId = randomUUID(),
+}) {
+ let lastOutcome;
+ for (let attempt = 0; attempt < COPY_RETRY_ATTEMPTS; attempt += 1) {
+ const sourceRecord = await manager.deps.store.readHeaderRecordSnapshot(sourceSessionId);
+ const copied = await composition.handlers['session.branch.create'](
+ {
+ sourceSessionId,
+ targetSessionId,
+ sourceTurnId,
+ expectedSourceRevision: sourceRecord.revision,
+ },
+ operationContext,
+ );
+ if (copied.ok && copied.result.kind === 'committed') return copied.result;
+
+ lastOutcome = copied;
+ const retryable =
+ (copied.ok && copied.result.kind === 'source_revision_conflict') ||
+ (!copied.ok && copied.error.code === 'session_busy');
+ if (!retryable) break;
+ await sleep(COPY_RETRY_DELAY_MS);
+ }
+ throw new Error(`Session copy failed after retries: ${JSON.stringify(lastOutcome)}`);
+}
+
+async function waitForCompletedTurn(manager, sessionId, turnId) {
+ const deadline = performance.now() + 10_000;
+ while (performance.now() < deadline) {
+ const messages = await manager.getMessages(sessionId);
+ const state = messages.findLast(
+ (message) => message.type === 'turn_state' && message.turnId === turnId,
+ );
+ if (state?.status === 'completed') return;
+ if (state?.status === 'failed' || state?.status === 'aborted') {
+ throw new Error(`Benchmark turn ${turnId} ended as ${state.status}`);
+ }
+ await new Promise((resolve) => setTimeout(resolve, 5));
+ }
+ throw new Error(`Timed out waiting for benchmark turn ${turnId}`);
+}
+
+function median(values) {
+ const sorted = [...values].sort((left, right) => left - right);
+ return sorted[Math.floor(sorted.length / 2)];
+}
+
+function sleep(delayMs) {
+ return new Promise((resolve) => setTimeout(resolve, delayMs));
+}
+
+async function directoryBytes(root) {
+ const entries = await readdir(root, { withFileTypes: true });
+ let total = 0;
+ for (const entry of entries) {
+ if (entry.name === '.maka-storage-root.json') continue;
+ const path = join(root, entry.name);
+ const info = await stat(path);
+ total += info.isDirectory() ? await directoryBytes(path) : info.size;
+ }
+ return total;
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org