Files
openclaw/scripts/bench-sqlite-state.ts
Vincent Koc 608e365ce4 improve(sqlite): report production workloads independently (#130994)
* perf(scripts): report SQLite scenarios independently

* perf(sqlite): benchmark production state workloads

* test(sqlite): use tracked benchmark temp dirs

* perf(sqlite): harden workload evidence

* fix(perf): require matching SQLite workloads
2026-08-28 01:02:42 +08:00

888 lines
29 KiB
TypeScript

// SQLite state benchmark seeds OpenClaw DBs and reports hot-query proof lines.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import type { DatabaseSync, SQLInputValue } from "node:sqlite";
import { pathToFileURL } from "node:url";
import { expectDefined } from "../packages/normalization-core/src/expect.js";
import { OPENCLAW_AGENT_SCHEMA_VERSION } from "../src/state/openclaw-agent-db-contract.js";
import {
openOpenClawAgentDatabase,
closeOpenClawAgentDatabasesForTest,
} from "../src/state/openclaw-agent-db.js";
import { OPENCLAW_STATE_SCHEMA_VERSION } from "../src/state/openclaw-state-db-contract.js";
import {
closeOpenClawStateDatabaseForTest,
openOpenClawStateDatabase,
} from "../src/state/openclaw-state-db.js";
import { parseStrictIntegerOption } from "./lib/dev-tooling-safety.ts";
import {
collectSqliteQueryPlanEvidence,
type SqliteQueryPlanEvidence,
} from "./lib/sqlite-query-plan-evidence.js";
import {
CliUsageError,
parseSqliteStateBenchmarkCli,
type ProfileId,
} from "./lib/sqlite-state-benchmark-cli.js";
type ProfileConfig = {
agentCacheEntries: number;
agentCount: number;
channelIngressEvents: number;
cronJobs: number;
cronTaskRuns: number;
deliveryQueueEntries: number;
pluginStateEntries: number;
queryRuns: number;
};
type TimedQuery = {
database: "agent" | "state";
id: string;
p50Ms: number;
p95Ms: number;
plan: SqliteQueryPlanEvidence;
runs: number;
rows: number;
sql: string;
};
type BenchmarkReport = {
integrity: {
agent: string[];
state: string;
};
node: string;
schemaVersion: 2;
versions: {
agentSchema: number;
sqlite: string;
stateSchema: number;
};
paths: {
agentDatabases: string[];
artifact: string | null;
stateDatabase: string;
stateDir: string;
};
profile: ProfileId;
queries: TimedQuery[];
rows: {
agentCacheEntries: number;
agentDatabases: number;
channelIngressEvents: number;
cronJobs: number;
cronTaskRuns: number;
deliveryQueueEntries: number;
pluginStateEntries: number;
stateRows: number;
transcriptEvents: number;
};
timingsMs: {
checkpoint: number;
seed: number;
total: number;
};
walBytes: {
agentAfter: number[];
agentBefore: number[];
stateAfter: number;
stateBefore: number;
};
};
const PROFILES: Record<ProfileId, ProfileConfig> = {
smoke: {
agentCacheEntries: 1_000,
agentCount: 2,
channelIngressEvents: 1_000,
cronJobs: 100,
cronTaskRuns: 1_000,
deliveryQueueEntries: 1_000,
pluginStateEntries: 1_000,
queryRuns: 20,
},
default: {
agentCacheEntries: 20_000,
agentCount: 5,
channelIngressEvents: 10_000,
cronJobs: 1_000,
cronTaskRuns: 50_000,
deliveryQueueEntries: 50_000,
pluginStateEntries: 20_000,
queryRuns: 30,
},
large: {
agentCacheEntries: 50_000,
agentCount: 10,
channelIngressEvents: 100_000,
cronJobs: 5_000,
cronTaskRuns: 250_000,
deliveryQueueEntries: 200_000,
pluginStateEntries: 100_000,
queryRuns: 40,
},
};
const SQLITE_PERF_FULL_LOAD_RUNS = 20;
const SQLITE_PERF_TRANSCRIPT_EVENTS = 256;
const SQLITE_PERF_TRANSCRIPT_MESSAGE_BYTES = 4_096;
const SQLITE_PERF_TRANSCRIPT_PAGE_MESSAGES = 256;
const SQLITE_PERF_TRANSCRIPT_SESSION_ID = "perf-history";
const SQLITE_PERF_INGRESS_QUEUE = JSON.stringify(["telegram", "bench-account"]);
const SQLITE_PERF_PLUGIN_ID = "benchmark-plugin";
const SQLITE_PERF_PLUGIN_NAMESPACE = "journal";
const SQLITE_PERF_PLUGIN_NOW = 1_750_000_000_000;
const SQLITE_PERF_CATALOG_SCOPE = "plugin-model-catalog-v1";
const SQLITE_PERF_PAGE_SIZE = 100;
function applyScale(config: ProfileConfig): ProfileConfig {
const scale = parseStrictIntegerOption({
fallback: 1,
label: "SQLITE_PERF_SCALE",
min: 1,
raw: process.env["SQLITE_PERF_SCALE"],
});
if (scale === 1) {
return config;
}
return {
agentCacheEntries: config.agentCacheEntries * scale,
agentCount: config.agentCount,
channelIngressEvents: config.channelIngressEvents * scale,
cronJobs: config.cronJobs * scale,
cronTaskRuns: config.cronTaskRuns * scale,
deliveryQueueEntries: config.deliveryQueueEntries * scale,
pluginStateEntries: config.pluginStateEntries * scale,
queryRuns: config.queryRuns,
};
}
function printUsage(): void {
console.log(`OpenClaw SQLite state benchmark
Usage:
node --import tsx scripts/bench-sqlite-state.ts [options]
Options:
--profile <smoke|default|large> Data volume profile (default: default)
--state-dir <path> Reuse a state directory instead of a temp dir
--output <path> Write machine-readable JSON report
--help Show this text
Environment:
SQLITE_PERF_SCALE=<n> Multiplies row counts for the selected profile
`);
}
function nowMs(): number {
return Number(process.hrtime.bigint()) / 1e6;
}
function fileSize(pathname: string): number {
try {
return fs.statSync(pathname).size;
} catch {
return 0;
}
}
function walSize(pathname: string): number {
return fileSize(`${pathname}-wal`);
}
function stateRowCount(config: ProfileConfig): number {
return (
config.channelIngressEvents +
config.cronJobs +
config.cronTaskRuns +
config.deliveryQueueEntries +
config.pluginStateEntries
);
}
function seedStateDatabase(db: DatabaseSync, config: ProfileConfig): void {
db.exec("BEGIN IMMEDIATE;");
try {
seedCronJobs(db, config.cronJobs);
seedCronTaskRuns(db, config.cronTaskRuns, config.cronJobs);
seedDeliveryQueue(db, config.deliveryQueueEntries);
seedPluginState(db, config.pluginStateEntries);
seedChannelIngress(db, config.channelIngressEvents);
db.exec("COMMIT;");
} catch (err) {
db.exec("ROLLBACK;");
throw err;
}
}
function seedCronJobs(db: DatabaseSync, count: number): void {
const insert = db.prepare(`
INSERT INTO cron_jobs (
store_key, job_id, name, enabled, agent_id, payload_kind,
job_json, state_json, runtime_updated_at_ms, schedule_identity, sort_order, updated_at
) VALUES (?, ?, ?, ?, ?, 'agentTurn', ?, ?, ?, ?, ?, ?)
`);
for (let i = 0; i < count; i += 1) {
const jobId = `job-${String(i).padStart(8, "0")}`;
const storeKey = `/state/cron/jobs-${i % 8}.json`;
const updatedAt = 1_700_000_000_000 + i;
const name = `Benchmark job ${i}`;
const enabled = i % 5 !== 0;
const agentId = `agent-${i % 16}`;
const job = {
id: jobId,
name,
enabled,
createdAtMs: updatedAt - 100_000,
agentId,
sessionKey: `agent:${agentId}:main`,
schedule: {
kind: "every",
everyMs: 60_000 + (i % 120) * 1_000,
anchorMs: updatedAt - 60_000,
},
sessionTarget: "isolated",
wakeMode: "now",
payload: {
kind: "agentTurn",
message: `Benchmark payload ${i}`,
model: "openai/gpt-5.6-luna",
timeoutSeconds: 60,
allowUnsafeExternalContent: false,
lightContext: true,
},
delivery: {
mode: "announce",
channel: "telegram",
to: `chat-${i % 32}`,
accountId: "bench-account",
bestEffort: true,
},
state: {},
};
const state = {
nextRunAtMs: updatedAt + (i % 2_000) * 1_000,
lastRunAtMs: updatedAt - 1_000,
lastRunStatus: "completed",
lastDurationMs: 50 + (i % 500),
consecutiveErrors: 0,
consecutiveSkipped: 0,
scheduleErrorCount: 0,
lastDeliveryStatus: "sent",
lastDelivered: true,
};
insert.run(
storeKey,
jobId,
name,
enabled ? 1 : 0,
agentId,
JSON.stringify(job),
JSON.stringify(state),
updatedAt,
`schedule-${i % 512}`,
i,
updatedAt,
);
}
}
function seedCronTaskRuns(db: DatabaseSync, count: number, cronJobCount: number): void {
const insert = db.prepare(`
INSERT INTO task_runs (
task_id, runtime, source_id, requester_session_key, owner_key, scope_kind,
child_session_key, run_id, task, status, delivery_status, notify_policy,
created_at, started_at, ended_at, last_event_at, error, terminal_summary,
terminal_outcome, detail_json
) VALUES (?, 'cron', ?, '', '', 'system', ?, ?, ?, ?, 'not_applicable', 'silent',
?, ?, ?, ?, ?, ?, ?, ?)
`);
for (let i = 0; i < count; i += 1) {
const jobIndex = i % 4 === 0 ? 0 : i % Math.max(1, cronJobCount);
const jobId = `job-${String(jobIndex).padStart(8, "0")}`;
const ts = 1_700_000_000_000 + i;
const succeeded = i % 17 !== 0;
const runId = `run-${i}`;
const status = succeeded ? "ok" : "error";
const storeKey = `/state/cron/jobs-${i % 8}.json`;
insert.run(
`cron-benchmark-${i}`,
jobId,
`agent:agent-${i % 16}:main`,
runId,
jobId,
succeeded ? "succeeded" : "failed",
ts,
ts,
ts + 20 + (i % 1_000),
ts + 20 + (i % 1_000),
succeeded ? null : `run ${i} failed`,
`run ${i}`,
succeeded ? "succeeded" : null,
JSON.stringify({ kind: "cron-run", storeKey, action: "finished", status, runId }),
);
}
}
function seedDeliveryQueue(db: DatabaseSync, count: number): void {
const insert = db.prepare(`
INSERT INTO delivery_queue_entries (
queue_name, id, status, entry_kind, session_key, channel, target, account_id,
retry_count, last_attempt_at, last_error, recovery_state, platform_send_started_at,
entry_json, enqueued_at, updated_at, failed_at
) VALUES (?, ?, ?, 'message', ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, ?, ?, ?, ?)
`);
for (let i = 0; i < count; i += 1) {
const status = i % 13 === 0 ? "failed" : i % 17 === 0 ? "completed" : "pending";
const queueName = i % 5 === 0 ? "session" : "outbound";
const enqueuedAt = 1_700_000_000_000 + i;
const channel = i % 2 === 0 ? "telegram" : "discord";
const target = `target-${i % 256}`;
const accountId = `account-${i % 8}`;
const sessionKey = `agent:agent-${i % 16}:main`;
insert.run(
queueName,
`delivery-${String(i).padStart(8, "0")}`,
status,
sessionKey,
channel,
target,
accountId,
i % 5,
status === "failed" ? enqueuedAt + 500 : null,
JSON.stringify({
id: `delivery-${String(i).padStart(8, "0")}`,
retryCount: i % 5,
enqueuedAt,
sessionKey,
route: { channel, to: target, accountId },
}),
enqueuedAt,
enqueuedAt + 100,
status === "failed" ? enqueuedAt + 1_000 : null,
);
}
}
function seedPluginState(db: DatabaseSync, count: number): void {
const insert = db.prepare(`
INSERT INTO plugin_state_entries (
plugin_id, namespace, entry_key, value_json, created_at, expires_at
) VALUES (?, ?, ?, ?, ?, ?)
`);
for (let i = 0; i < count; i += 1) {
const concentrated = i < Math.ceil(count * 0.75);
const expiresAt =
i % 10 === 0
? SQLITE_PERF_PLUGIN_NOW - 1_000 - i
: i % 4 === 0
? SQLITE_PERF_PLUGIN_NOW + 1_000 + i
: null;
insert.run(
concentrated ? SQLITE_PERF_PLUGIN_ID : `plugin-${i % 12}`,
concentrated ? SQLITE_PERF_PLUGIN_NAMESPACE : `namespace-${i % 16}`,
`entry-${String(i).padStart(8, "0")}`,
JSON.stringify({ value: i, text: `payload ${i}` }),
1_700_000_000_000 + i,
expiresAt,
);
}
}
function seedChannelIngress(db: DatabaseSync, count: number): void {
const insert = db.prepare(`
INSERT INTO channel_ingress_events (
queue_name, event_id, channel_id, account_id, status, lane_key, payload_json,
metadata_json, received_at, updated_at, claim_token, claim_owner, claimed_at,
attempts, last_attempt_at, last_error, failed_reason, failed_at, completed_at,
completed_metadata_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, ?, NULL)
`);
for (let i = 0; i < count; i += 1) {
const status = i % 17 === 0 ? "claimed" : i % 29 === 0 ? "completed" : "pending";
const timestamp = 1_700_000_000_000 + i;
insert.run(
SQLITE_PERF_INGRESS_QUEUE,
`event-${String(i).padStart(8, "0")}`,
"telegram",
"bench-account",
status,
`lane-${i % 128}`,
JSON.stringify({ messageId: `message-${i}`, text: `message ${i}` }),
JSON.stringify({ source: "benchmark" }),
timestamp,
timestamp,
status === "claimed" ? `claim-${i}` : null,
status === "claimed" ? "benchmark-worker" : null,
status === "claimed" ? timestamp : null,
i % 3,
status === "claimed" ? timestamp : null,
status === "completed" ? timestamp : null,
);
}
}
function seedAgentDatabase(db: DatabaseSync, count: number, agentIndex: number): void {
db.exec("BEGIN IMMEDIATE;");
try {
const insert = db.prepare(`
INSERT INTO cache_entries (scope, key, value_json, blob, expires_at, updated_at)
VALUES (?, ?, ?, NULL, ?, ?)
`);
const catalogEntries = Math.min(count, 64);
for (let i = 0; i < count; i += 1) {
const isCatalog = i < catalogEntries;
insert.run(
isCatalog ? SQLITE_PERF_CATALOG_SCOPE : `runtime-cache-${i % 16}`,
isCatalog
? `plugin-${String(i).padStart(3, "0")}`
: `agent-${agentIndex}-entry-${String(i).padStart(8, "0")}`,
JSON.stringify(
isCatalog
? {
generatedBy: "openclaw-plugin-model-catalog-v1",
models: [{ id: `model-${i}`, name: `Benchmark model ${i}` }],
pluginId: `plugin-${i}`,
}
: { agentIndex, i, value: `cache ${i}` },
),
null,
1_700_000_000_000 + i,
);
}
if (agentIndex === 0) {
seedTranscriptHistory(db);
}
db.exec("COMMIT;");
} catch (err) {
db.exec("ROLLBACK;");
throw err;
}
}
function seedTranscriptHistory(db: DatabaseSync): void {
const sessionKey = "agent:perf-agent-0:history";
db.prepare(
`INSERT INTO session_nodes (session_key, current_session_id, entry_json, updated_at)
VALUES (?, ?, ?, ?)`,
).run(sessionKey, SQLITE_PERF_TRANSCRIPT_SESSION_ID, "{}", 1_700_000_000_000);
db.prepare(
`INSERT INTO session_windows (session_id, session_key, created_at, updated_at)
VALUES (?, ?, ?, ?)`,
).run(SQLITE_PERF_TRANSCRIPT_SESSION_ID, sessionKey, 1_700_000_000_000, 1_700_000_000_000);
const insertEvent = db.prepare(
`INSERT INTO transcript_events (session_id, seq, event_json, created_at)
VALUES (?, ?, ?, ?)`,
);
const insertIdentity = db.prepare(
`INSERT INTO transcript_event_identities
(session_id, event_id, seq, event_type, parent_id, message_idempotency_key, created_at)
VALUES (?, ?, ?, 'message', NULL, NULL, ?)`,
);
const insertActive = db.prepare(
`INSERT INTO session_transcript_active_events
(session_id, active_position, event_seq, message_position)
VALUES (?, ?, ?, ?)`,
);
const messageContent = "x".repeat(SQLITE_PERF_TRANSCRIPT_MESSAGE_BYTES);
for (let seq = 1; seq <= SQLITE_PERF_TRANSCRIPT_EVENTS; seq += 1) {
const eventId = `history-${seq}`;
const message = {
type: "message",
id: eventId,
message: { role: "user", content: messageContent },
};
insertEvent.run(SQLITE_PERF_TRANSCRIPT_SESSION_ID, seq, JSON.stringify(message), seq);
insertIdentity.run(SQLITE_PERF_TRANSCRIPT_SESSION_ID, eventId, seq, seq);
insertActive.run(SQLITE_PERF_TRANSCRIPT_SESSION_ID, seq - 1, seq, seq - 1);
}
db.prepare(
`INSERT INTO session_transcript_index_state
(session_id, indexed_seq, leaf_event_id, active_event_count, active_message_count, updated_at)
VALUES (?, ?, ?, ?, ?, ?)`,
).run(
SQLITE_PERF_TRANSCRIPT_SESSION_ID,
SQLITE_PERF_TRANSCRIPT_EVENTS,
`history-${SQLITE_PERF_TRANSCRIPT_EVENTS}`,
SQLITE_PERF_TRANSCRIPT_EVENTS,
SQLITE_PERF_TRANSCRIPT_EVENTS,
1_700_000_000_000,
);
}
function readIntegrity(db: DatabaseSync): string {
const row = db.prepare("PRAGMA integrity_check").get() as { integrity_check?: unknown };
return typeof row.integrity_check === "string" ? row.integrity_check : "missing";
}
function readSqliteVersion(db: DatabaseSync): string {
const row = db.prepare("SELECT sqlite_version() AS version").get() as { version?: unknown };
return typeof row.version === "string" ? row.version : "unknown";
}
function checkpoint(db: DatabaseSync): void {
db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").all();
}
function percentile(values: number[], pct: number): number {
if (values.length === 0) {
return 0;
}
const sorted = values.toSorted((left, right) => left - right);
const index = Math.min(sorted.length - 1, Math.ceil((pct / 100) * sorted.length) - 1);
return Number(expectDefined(sorted[index], `SQLite benchmark percentile ${pct}`).toFixed(3));
}
function readQueryPlan(
db: DatabaseSync,
sql: string,
params: SQLInputValue[],
): SqliteQueryPlanEvidence {
const raw = (
db.prepare(`EXPLAIN QUERY PLAN ${sql}`).all(...params) as Array<{ detail?: unknown }>
).map((row) => (typeof row.detail === "string" ? row.detail : JSON.stringify(row.detail ?? "")));
return collectSqliteQueryPlanEvidence(raw);
}
function runTimedQuery(params: {
database: "agent" | "state";
db: DatabaseSync;
fullLoad?: boolean;
id: string;
queryParams: SQLInputValue[];
requestedRuns: number;
sql: string;
}): TimedQuery {
const runs = params.fullLoad
? Math.min(params.requestedRuns, SQLITE_PERF_FULL_LOAD_RUNS)
: params.requestedRuns;
const statement = params.db.prepare(params.sql);
const samples: number[] = [];
let rows = statement.all(...params.queryParams).length;
for (let i = 0; i < runs; i += 1) {
const started = nowMs();
rows = statement.all(...params.queryParams).length;
samples.push(nowMs() - started);
}
return {
database: params.database,
id: params.id,
p50Ms: percentile(samples, 50),
p95Ms: percentile(samples, 95),
plan: readQueryPlan(params.db, params.sql, params.queryParams),
runs,
rows,
sql: params.sql,
};
}
function taskRunSelectSql(where: string): string {
return `SELECT
task_id, runtime, task_kind, source_id, requester_session_key, owner_key, scope_kind,
child_session_key, parent_flow_id, parent_task_id, agent_id, requester_agent_id,
run_id, label, task, status, delivery_status, notify_policy, created_at, started_at,
ended_at, last_event_at, cleanup_after, tool_use_count, last_tool_name, error,
progress_summary, terminal_summary, terminal_outcome, detail_json
FROM task_runs
WHERE ${where}
ORDER BY created_at ASC, task_id ASC`;
}
function runHotQueries(params: {
agentDb: DatabaseSync;
config: ProfileConfig;
stateDb: DatabaseSync;
}): TimedQuery[] {
const transcriptPositions = Array.from(
{ length: SQLITE_PERF_TRANSCRIPT_PAGE_MESSAGES },
(_, index) => SQLITE_PERF_TRANSCRIPT_EVENTS - SQLITE_PERF_TRANSCRIPT_PAGE_MESSAGES + index,
);
const transcriptPlaceholders = transcriptPositions.map(() => "?").join(", ");
return [
runTimedQuery({
database: "state",
db: params.stateDb,
id: "cron.store.load",
queryParams: ["/state/cron/jobs-0.json"],
requestedRuns: params.config.queryRuns,
sql: `SELECT *
FROM cron_jobs
WHERE store_key = ?
ORDER BY sort_order ASC, updated_at ASC, job_id ASC`,
}),
runTimedQuery({
database: "state",
db: params.stateDb,
fullLoad: true,
id: "task-runs.cron.list",
queryParams: ["cron"],
requestedRuns: params.config.queryRuns,
sql: taskRunSelectSql("runtime = ?"),
}),
runTimedQuery({
database: "state",
db: params.stateDb,
fullLoad: true,
id: "task-runs.cron-source.list",
queryParams: ["cron", "job-00000000"],
requestedRuns: params.config.queryRuns,
sql: taskRunSelectSql("runtime = ? AND source_id = ?"),
}),
runTimedQuery({
database: "state",
db: params.stateDb,
fullLoad: true,
id: "delivery.pending.load",
queryParams: ["outbound", "pending"],
requestedRuns: params.config.queryRuns,
sql: `SELECT id, entry_json, enqueued_at, retry_count, last_attempt_at, last_error,
platform_send_started_at, recovery_state
FROM delivery_queue_entries
WHERE queue_name = ? AND status = ?
ORDER BY enqueued_at ASC, id ASC`,
}),
runTimedQuery({
database: "state",
db: params.stateDb,
id: "ingress.pending.first-page",
queryParams: [SQLITE_PERF_INGRESS_QUEUE, "pending", SQLITE_PERF_PAGE_SIZE],
requestedRuns: params.config.queryRuns,
sql: `SELECT *
FROM channel_ingress_events
WHERE queue_name = ? AND status = ?
ORDER BY received_at ASC, event_id ASC
LIMIT ?`,
}),
runTimedQuery({
database: "state",
db: params.stateDb,
id: "ingress.pending.seek-page",
queryParams: [
SQLITE_PERF_INGRESS_QUEUE,
"pending",
1_700_000_000_500,
1_700_000_000_500,
"event-00000500",
SQLITE_PERF_PAGE_SIZE,
],
requestedRuns: params.config.queryRuns,
sql: `SELECT *
FROM channel_ingress_events
WHERE queue_name = ? AND status = ?
AND (received_at > ? OR (received_at = ? AND event_id > ?))
ORDER BY received_at ASC, event_id ASC
LIMIT ?`,
}),
runTimedQuery({
database: "state",
db: params.stateDb,
id: "ingress.pending.id-page",
queryParams: [SQLITE_PERF_INGRESS_QUEUE, "pending", SQLITE_PERF_PAGE_SIZE],
requestedRuns: params.config.queryRuns,
sql: `SELECT *
FROM channel_ingress_events
WHERE queue_name = ? AND status = ?
ORDER BY event_id ASC
LIMIT ?`,
}),
runTimedQuery({
database: "state",
db: params.stateDb,
id: "ingress.pending.id-seek-page",
queryParams: [SQLITE_PERF_INGRESS_QUEUE, "pending", "event-00000500", SQLITE_PERF_PAGE_SIZE],
requestedRuns: params.config.queryRuns,
sql: `SELECT *
FROM channel_ingress_events
WHERE queue_name = ? AND status = ? AND event_id > ?
ORDER BY event_id ASC
LIMIT ?`,
}),
runTimedQuery({
database: "state",
db: params.stateDb,
fullLoad: true,
id: "plugin-state.namespace.live",
queryParams: [SQLITE_PERF_PLUGIN_ID, SQLITE_PERF_PLUGIN_NAMESPACE, SQLITE_PERF_PLUGIN_NOW],
requestedRuns: params.config.queryRuns,
sql: `SELECT plugin_id, namespace, entry_key, value_json, created_at, expires_at
FROM plugin_state_entries
WHERE plugin_id = ? AND namespace = ?
AND (expires_at IS NULL OR expires_at > ?)
ORDER BY created_at ASC, entry_key ASC`,
}),
runTimedQuery({
database: "agent",
db: params.agentDb,
id: "agent-cache.plugin-model-catalog.list",
queryParams: [SQLITE_PERF_CATALOG_SCOPE],
requestedRuns: params.config.queryRuns,
sql: `SELECT key, value_json
FROM cache_entries
WHERE scope = ?
ORDER BY key ASC`,
}),
runTimedQuery({
database: "agent",
db: params.agentDb,
id: "transcript.tail.metadata",
queryParams: [SQLITE_PERF_TRANSCRIPT_SESSION_ID, ...transcriptPositions],
requestedRuns: params.config.queryRuns,
sql: `SELECT active.message_position,
LENGTH(CAST(event.event_json AS BLOB)) + 1 AS serialized_bytes
FROM session_transcript_active_events AS active
JOIN transcript_events AS event
ON event.session_id = active.session_id AND event.seq = active.event_seq
WHERE active.session_id = ?
AND active.message_position IN (${transcriptPlaceholders})
ORDER BY active.message_position DESC`,
}),
runTimedQuery({
database: "agent",
db: params.agentDb,
id: "transcript.tail.payload",
queryParams: [SQLITE_PERF_TRANSCRIPT_SESSION_ID, ...transcriptPositions],
requestedRuns: params.config.queryRuns,
sql: `SELECT active.message_position, event.event_json
FROM session_transcript_active_events AS active
JOIN transcript_events AS event
ON event.session_id = active.session_id AND event.seq = active.event_seq
WHERE active.session_id = ?
AND active.message_position IN (${transcriptPlaceholders})
ORDER BY active.message_position ASC`,
}),
];
}
function printProofLines(report: BenchmarkReport): void {
const p95 = Math.max(...report.queries.map((query) => query.p95Ms));
console.log(`SQLITE_PERF_PROFILE=${report.profile}`);
console.log(`SQLITE_PERF_STATE_ROWS=${report.rows.stateRows}`);
console.log(`SQLITE_PERF_AGENT_ROWS=${report.rows.agentCacheEntries}`);
console.log(`SQLITE_PERF_TRANSCRIPT_ROWS=${report.rows.transcriptEvents}`);
console.log(`SQLITE_PERF_INTEGRITY=${report.integrity.state}`);
console.log(`SQLITE_PERF_WAL_BYTES_BEFORE=${report.walBytes.stateBefore}`);
console.log(`SQLITE_PERF_WAL_BYTES_AFTER=${report.walBytes.stateAfter}`);
console.log(`SQLITE_PERF_QUERY_P95_MS=${p95.toFixed(3)}`);
for (const query of report.queries) {
console.log(`SQLITE_PERF_SCENARIO ${JSON.stringify(query)}`);
}
if (report.paths.artifact) {
console.log(`SQLITE_PERF_ARTIFACT=${report.paths.artifact}`);
}
}
function main(): void {
const argv = process.argv.slice(2);
const cli = parseSqliteStateBenchmarkCli(argv);
if (cli.help) {
printUsage();
return;
}
const { options } = cli;
const config = applyScale(PROFILES[options.profile]);
const stateDir =
options.stateDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-perf-"));
const env = { OPENCLAW_STATE_DIR: stateDir };
const started = nowMs();
try {
const stateDatabase = openOpenClawStateDatabase({ env });
const agentDatabases = Array.from({ length: config.agentCount }, (_, index) =>
openOpenClawAgentDatabase({ agentId: `perf-agent-${index}`, env }),
);
const seedStarted = nowMs();
seedStateDatabase(stateDatabase.db, config);
const perAgentEntries = Math.ceil(config.agentCacheEntries / config.agentCount);
agentDatabases.forEach((database, index) =>
seedAgentDatabase(database.db, perAgentEntries, index),
);
const seedMs = nowMs() - seedStarted;
const stateWalBefore = walSize(stateDatabase.path);
const agentWalBefore = agentDatabases.map((database) => walSize(database.path));
const stateIntegrity = readIntegrity(stateDatabase.db);
const agentIntegrity = agentDatabases.map((database) => readIntegrity(database.db));
const queries = runHotQueries({
agentDb: agentDatabases[0]?.db ?? stateDatabase.db,
config,
stateDb: stateDatabase.db,
});
const checkpointStarted = nowMs();
checkpoint(stateDatabase.db);
agentDatabases.forEach((database) => checkpoint(database.db));
const checkpointMs = nowMs() - checkpointStarted;
const report: BenchmarkReport = {
integrity: {
agent: agentIntegrity,
state: stateIntegrity,
},
node: process.version,
schemaVersion: 2,
paths: {
agentDatabases: agentDatabases.map((database) => database.path),
artifact: options.output,
stateDatabase: stateDatabase.path,
stateDir,
},
profile: options.profile,
queries,
versions: {
agentSchema: OPENCLAW_AGENT_SCHEMA_VERSION,
sqlite: readSqliteVersion(stateDatabase.db),
stateSchema: OPENCLAW_STATE_SCHEMA_VERSION,
},
rows: {
agentCacheEntries: perAgentEntries * config.agentCount,
agentDatabases: config.agentCount,
channelIngressEvents: config.channelIngressEvents,
cronJobs: config.cronJobs,
cronTaskRuns: config.cronTaskRuns,
deliveryQueueEntries: config.deliveryQueueEntries,
pluginStateEntries: config.pluginStateEntries,
stateRows: stateRowCount(config),
transcriptEvents: SQLITE_PERF_TRANSCRIPT_EVENTS,
},
timingsMs: {
checkpoint: Number(checkpointMs.toFixed(3)),
seed: Number(seedMs.toFixed(3)),
total: Number((nowMs() - started).toFixed(3)),
},
walBytes: {
agentAfter: agentDatabases.map((database) => walSize(database.path)),
agentBefore: agentWalBefore,
stateAfter: walSize(stateDatabase.path),
stateBefore: stateWalBefore,
},
};
if (options.output) {
fs.mkdirSync(path.dirname(options.output), { recursive: true });
fs.writeFileSync(options.output, `${JSON.stringify(report, null, 2)}\n`, "utf8");
}
printProofLines(report);
} finally {
closeOpenClawAgentDatabasesForTest();
closeOpenClawStateDatabaseForTest();
if (!options.stateDir) {
fs.rmSync(stateDir, { recursive: true, force: true });
}
}
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
try {
main();
} catch (error) {
if (error instanceof CliUsageError) {
console.error(`error: ${error.message}`);
process.exit(2);
}
throw error;
}
}