Skip to content

test(runtime): add persistence observer streaming benchmarks - #4077

Open
salignatmoandal wants to merge 1 commit into
docker:mainfrom
salignatmoandal:bench/persistence-observer-streaming
Open

test(runtime): add persistence observer streaming benchmarks#4077
salignatmoandal wants to merge 1 commit into
docker:mainfrom
salignatmoandal:bench/persistence-observer-streaming

Conversation

@salignatmoandal

Copy link
Copy Markdown

Summary

Adds characterization tests and benchmarks for PersistenceObserver streaming persistence — the path that mirrors assistant token deltas (AgentChoice / AgentChoiceReasoning) into a single growing message row in the session store.

This establishes a baseline before any future optimization (e.g. debounced flushes) and documents the per-chunk store write contract that the observer currently implements.

Context

During a streaming assistant turn, the runtime emits one AgentChoiceEvent per delta. PersistenceObserver.persistStreamingContent keeps a single in-flight row and:

  1. INSERT (AddMessage) on the first chunk
  2. UPDATE (UpdateMessage) on every subsequent chunk
  3. UPDATE again on MessageAddedEvent to finalise the row with the canonical payload

This behaviour is easy to regress when refactoring persistence or the store layer, but was not previously covered by a focused unit test or benchmark.

Changes

New file: pkg/runtime/persistence_observer_bench_test.go

Tests

Test What it pins
TestPersistenceObserver_UpdateCountPerChunk 1 AddMessage + (N-1) UpdateMessage calls for N streaming chunks, plus one final UpdateMessage on MessageAdded
TestPersistenceObserver_StreamingContentAccumulates Mid-stream reload shows accumulated assistant text ("hel" + "lo""hello")

Both tests use a countingStore wrapper around InMemorySessionStore to assert store call counts without mocking the observer.

Benchmarks

Each iteration simulates a long assistant turn: 500 AgentChoice deltas + 1 MessageAdded finalisation (streamingBenchChunks = 500).

Benchmark Store Purpose
BenchmarkPersistenceObserver_StreamingChunks InMemorySessionStore CPU / alloc cost of observer logic + in-memory clone path
BenchmarkPersistenceObserver_StreamingChunks_SQLite SQLite :memory: Realistic cost including json.Marshal + UPDATE session_items per chunk

Sample results (darwin/arm64, Apple M3 Pro)

BenchmarkPersistenceObserver_StreamingChunks-12          ~2.0 ms/op    502 KB/op    2515 allocs/op
BenchmarkPersistenceObserver_StreamingChunks_SQLite-12   ~3.1 ms/op   1558 KB/op    8714 allocs/op

Per chunk (500 chunks/iter): ~4 µs in-memory, ~6 µs SQLite, ~5 vs ~17 allocs.

SQLite is ~1.5× slower and ~3× more alloc-heavy — expected given JSON marshal + SQL per update.

Known benchmark caveats

  • In-memory drift: the in-memory benchmark reuses the same session across b.N iterations, so UpdateMessage eventually scans an ever-growing message list (O(messages) per update). Later iterations are slower than the first. SQLite stays flat because updates are keyed by message_id.
  • Migration logs: SQLite setup calls NewSQLiteSessionStoreFromDB, which logs migration info before b.ResetTimer() — noisy stdout but not included in ns/op.

These caveats are acceptable for a baseline but worth keeping in mind when comparing future numbers.

Why now?

Streaming persistence is on the hot path for every assistant response when a session store is configured. Having explicit tests + benchmarks makes it safer to:

  • change the flush strategy (per-chunk vs debounced)
  • optimize persistStreamingContent (avoid strings.Builder.String() copies on every delta)
  • compare in-memory vs SQLite store performance under realistic chunk counts

Test plan

  • go test ./pkg/runtime -run TestPersistenceObserver_UpdateCountPerChunk -v
  • go test ./pkg/runtime -run TestPersistenceObserver_StreamingContentAccumulates -v
  • go test ./pkg/runtime -run=^$ -bench=BenchmarkPersistenceObserver -benchmem -count=1
  • task test (full suite)
  • task lint

@salignatmoandal
salignatmoandal requested a review from a team as a code owner August 30, 2026 20:58
@salignatmoandal
salignatmoandal force-pushed the bench/persistence-observer-streaming branch from 36cfbd2 to 084471c Compare August 30, 2026 21:00
@aheritier aheritier added area/runtime Runtime engine, agent loop execution, tool dispatch, loop detection kind/test Test-only changes status/needs-signed-commits Some commits in the PR are signed with a valid SSH/GPG key labels Aug 30, 2026
@aheritier

Copy link
Copy Markdown
Collaborator

👋 Some commits in this PR are not signed and verified by GitHub. Please sign your commits with a GPG or SSH key registered in your GitHub account, then force-push.

Commits that are not verified: 084471c

See GitHub's guide on signing commits for setup instructions. I've added status/needs-signed-commits; it will be removed automatically once every commit in this PR carries a valid GitHub-verified signature.

@salignatmoandal
salignatmoandal force-pushed the bench/persistence-observer-streaming branch from 084471c to 5cbf63a Compare August 30, 2026 21:02
Document the per-chunk AddMessage/UpdateMessage contract and establish
in-memory vs SQLite baselines for streaming assistant persistence.
@salignatmoandal
salignatmoandal force-pushed the bench/persistence-observer-streaming branch from 5cbf63a to e40f6b6 Compare August 30, 2026 21:13
@salignatmoandal

Copy link
Copy Markdown
Author

Hi @aheritier, thanks for the heads-up.

I've signed the commit with my SSH signing key and force-pushed (e40f6b6). GitHub now shows it as verified on my side.

Happy to adjust anything else if needed.

@aheritier aheritier removed the status/needs-signed-commits Some commits in the PR are signed with a valid SSH/GPG key label Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/runtime Runtime engine, agent loop execution, tool dispatch, loop detection kind/test Test-only changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants