blob: 5832696c600ce45b2e169cd4d95f908a6bc30aea [file]
/*
* 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, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createSessionStore } from '@maka/storage/session-store';
import { ClientSessionSubscription } from '../dist/client/session-subscription.js';
import { SESSION_CONTINUITY_SCHEMA_VERSION } from '../dist/protocol/index.js';
import {
createSessionTranscriptBootstrap,
readSessionTranscriptPage,
} from '../dist/server/session-transcript-pager.js';
const BOOTSTRAP_BYTES = 16 * 1024;
const RTT_MS = Number.parseInt(process.env.MAKA_TRANSCRIPT_BENCHMARK_RTT_MS ?? '20', 10);
const cases = [
{ name: '5k-small-messages', messages: 5_000, textBytes: 96 },
{ name: '15MiB-single-message', messages: 1, textBytes: 15 * 1024 * 1024 },
{ name: '17MiB-transcript', totalBytes: 17 * 1024 * 1024, textBytes: 4 * 1024 },
{ name: '64MiB-transcript', totalBytes: 64 * 1024 * 1024, textBytes: 4 * 1024 },
];
const results = [];
for (const fixture of cases) results.push(await runFixture(fixture));
console.table(results);
async function runFixture(fixture) {
const messages = buildMessages(fixture);
const root = await mkdtemp(join(tmpdir(), 'maka-transcript-benchmark-'));
const store = createSessionStore(root);
const setupAt = performance.now();
const session = await store.create({
cwd: root,
backend: 'fake',
llmConnectionSlug: 'fake',
model: 'fake-model',
permissionMode: 'ask',
name: fixture.name,
labels: [],
});
await store.appendMessages(session.id, messages);
const setupMs = performance.now() - setupAt;
const reader = sqliteReader(store);
try {
const openedAt = performance.now();
const throughSequence = await reader.readDurableHighWater(session.id);
const { bootstrap, state } = await createSessionTranscriptBootstrap({
reader,
sessionId: session.id,
subscriptionId: `benchmark-${fixture.name}`,
throughSequence,
rootTurn: null,
activeAssistantStreams: [],
maxBytes: BOOTSTRAP_BYTES,
});
const bootstrapCpuMs = performance.now() - openedAt;
let pageRequests = 0;
let transferredRawBytes = bootstrap.durable.rawBytes + bootstrap.overlay.rawBytes;
const subscription = new ClientSessionSubscription(
{
hostEpoch: 'benchmark-host',
subscriptionId: state.subscriptionId,
nextSequence: 1,
activeAssistantStreams: [],
transcript: bootstrap,
snapshot: {
schemaVersion: SESSION_CONTINUITY_SCHEMA_VERSION,
session: {
sessionId: state.sessionId,
metadataRevision: 1,
status: 'active',
createdAt: 1,
isArchived: false,
},
projectionRevision: 1,
rootTurn: null,
goal: null,
queue: {
hostEpoch: 'benchmark-host',
queueRevision: 0,
steering: [],
followup: [],
},
interactions: { pending: [] },
},
},
async () => undefined,
async (request) => {
pageRequests += 1;
const page = await readSessionTranscriptPage({ reader, state, request });
transferredRawBytes += page.rawBytes;
return page;
},
);
const materializeAt = performance.now();
const materialized = await subscription.loadTranscript((value) => value);
const materializeCpuMs = performance.now() - materializeAt;
if (materialized.length !== messages.length) {
throw new Error(
`${fixture.name} materialized ${materialized.length}/${messages.length} messages`,
);
}
const wireRequests = 1 + pageRequests;
return {
fixture: fixture.name,
messages: messages.length,
rawMiB: decimalMiB(transferredRawBytes),
bootstrapKiB: decimalKiB(bootstrap.durable.rawBytes + bootstrap.overlay.rawBytes),
wireRequests,
pageRequests,
setupMs: setupMs.toFixed(1),
bootstrapCpuMs: bootstrapCpuMs.toFixed(1),
materializeCpuMs: materializeCpuMs.toFixed(1),
modeledRttFloorMs: wireRequests * RTT_MS,
};
} finally {
await store.close?.();
await rm(root, { recursive: true, force: true });
}
}
function buildMessages(fixture) {
const messages = [];
let encodedBytes = 0;
const targetCount = fixture.messages ?? Number.POSITIVE_INFINITY;
while (
messages.length < targetCount &&
(fixture.totalBytes === undefined || encodedBytes < fixture.totalBytes)
) {
const index = messages.length;
const message = {
type: 'user',
id: `message-${index}`,
turnId: `turn-${index}`,
ts: index + 1,
text: 'x'.repeat(fixture.textBytes),
};
messages.push(message);
encodedBytes += Buffer.byteLength(JSON.stringify(message), 'utf8');
}
return messages;
}
function sqliteReader(store) {
return {
readDurableHighWater: (sessionId) => store.readTranscriptHighWaterSnapshot(sessionId),
readDurablePage: (sessionId, request) => store.readTranscriptPageSnapshot(sessionId, request),
readDurableMessagesById: (sessionId, messageIds, throughSequence) =>
store.readTranscriptMessagesSnapshot(sessionId, messageIds, throughSequence),
readActiveOverlay: async () => [],
};
}
function decimalMiB(bytes) {
return (bytes / (1024 * 1024)).toFixed(2);
}
function decimalKiB(bytes) {
return (bytes / 1024).toFixed(2);
}