Task 84 — ProposalStore caching, async I/O, and hot-path allocations
Phase: 5
Initiative: 46
Status: done
Owner: —
Slug: proposal-store-hotpath-alloc
Goal
Две perf-проблемы:
- ProposalStore.ListAll (
Reflection/ProposalStore.cs:75-97): читает все JSON files синхронно под globallock, потом сортирует в памяти.GetBySkill,GetRecent,CountToday— каждый re-read’ает все файлы. O(files) sync I/O под lock на каждый call. - Hot-path allocations: нет
ArrayPool/MemoryPool/ObjectPoolusage;new StringBuilder()per request в context-build hot path (LayeredMemoryManager.cs:41,ContextBuilder.cs:92,TraceSummarizer.cs:19,AgentCore.cs:1023,MemoryManager.cs:33,164,ResponseAggregator.cs:208).
Acceptance criteria
Sub-tasks
Reflection/ProposalStore.cs:75-97— кешировать file list сFileSystemWatcherinvalidation:List<Proposal>cached in-memory, watcher на_proposalsDirinvalidate на Created/Changed/Deleted/Renamed.- Конвертировать
ListAllвasync Task<List<Proposal>>— использоватьFile.ReadAllTextAsyncвместоFile.ReadAllText. - Убрать global
lock—ConcurrentDictionaryилиvolatilesnapshot +ImmutableListswap on invalidation. GetBySkill,GetRecent,CountToday— использовать cached snapshot, не re-read files.- Ввести
Microsoft.Extensions.ObjectPoolдляStringBuilderpooling:_sbPool = new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy()). Memory/Layers/LayeredMemoryManager.cs:41—var sb = _sbPool.Get(); try { ... } finally { _sbPool.Return(sb); sb.Clear(); }.Context/ContextBuilder.cs:92,Context/Summarizer/TraceSummarizer.cs:19,Agent/AgentCore.cs:1023,Agent/MemoryManager.cs:33,164,Mesh/Aggregation/ResponseAggregator.cs:208— аналогично: pooledStringBuilder.Hercules.csproj— добавитьMicrosoft.Extensions.ObjectPoolесли ещё нет.- Для больших buffer allocations (serialization, file reads): рассмотреть
ArrayPool<byte>.Shared.Rent(size)+Return. (не применимо — JSON files маленькие, File.ReadAllTextAsync справляется; см. Implementation notes) - Unit-тест:
ListAllпосле FileSystemWatcher event → cache invalidated, re-read. - Unit-тест: 1000 context-build iterations → pooled StringBuilder: GC.GetTotalMemory delta < non-pooled baseline. (200 iterations TraceSummarizer как proxy для hot-path)
- Benchmark (optional):
dotnet run --benchmark context-build— compare before/after. (не сделано — BenchmarkDotNet не в стеке; functional test покрывает) dotnet build+dotnet testpass.
Implementation notes
- Approach: keep ProposalStore API backwards-compatible.
ListAllremains sync (read from snapshot),ListAllAsyncadded for explicit disk rescan.FileSystemWatcheris subscribed in constructor and debounced (250ms) to coalesce bursts; periodic 60s safety refresh. - Snapshot:
ImmutableList<Proposal> _snapshotswapped atomically viaImmutableInterlocked.Update. Read path is lock-free.Save/Deleteupdate snapshot eagerly so reads never block on watcher. _lockremoved from read path; onlyFile.WriteAllText/File.Deleteguarded by_writeLockto avoid torn writes.ListAllreturns clonedProposal(deep-copyList<string>fields) so callers cannot mutate the cached entry.- StringBuilder pool: per-class
ObjectPool<StringBuilder>viaDefaultObjectPoolProvider.CreateStringBuilderPool()(4KB retained capacity, 32 initial).Get → try/finally → Returnpattern in 7 call sites:LayeredMemoryManager.BuildContextBlockAsyncContextBuilder.BuildContextAsyncTraceSummarizer.SummarizeAgentCore.BuildSystemPromptMemoryManager.BuildContextBlockMemoryManager.ParseSectionsResponseAggregator.AggregateWithLlmJudgeAsync
ArrayPool<byte>skipped: file reads useFile.ReadAllTextAsync+ small JSON payloads, no proven hot-spot.- Disposal:
ProposalStoreimplementsIDisposableto stop watcher and dispose timer/semaphore. - Tests:
tests/Hercules.Agent.Tests/Reflection/ProposalStoreCachingTests.cs(12 tests, includes FileSystemWatcher invalidation on Add/Delete),tests/Hercules.Agent.Tests/Reflection/StringBuilderPoolTests.cs(4 tests).
Validation
dotnet build src/agent/Hercules.csproj— 0 errors, 15 warnings (все pre-existing)dotnet build tests/Hercules.Agent.Tests/Hercules.Agent.Tests.csproj— 0 errorsdotnet testfiltered to 28 tests for ProposalStore / StringBuilder pool — 28/28 passeddotnet testfiltered to 188 tests across affected components (ContextBuilder, TraceSummarizer, MemoryManager, ResponseAggregator, LayeredMemory, AgentCore, MaintenanceWorkflow, ReflectionProposal) — 188/188 passeddotnet testfull suite — 1912 passed, 9 pre-existing failures (OtelServiceTests, RedisTaskQueueTests, NumericValidatorTests — все воспроизводятся на чистом HEAD без моих изменений)
Scope / Likely files
src/agent/Reflection/ProposalStore.cs, src/agent/Memory/Layers/LayeredMemoryManager.cs, src/agent/Context/ContextBuilder.cs, src/agent/Context/Summarizer/TraceSummarizer.cs, src/agent/Agent/AgentCore.cs, src/agent/Agent/MemoryManager.cs, src/agent/Mesh/Aggregation/ResponseAggregator.cs, src/agent/Hercules.csproj
Dependencies
- блокирует / опирается на: task_017 — safe-self-improvement
- блокирует / опирается на: task_027 — context-assembly
- блокирует / опирается на: task_011 — layered-memory
Risks / Rollback
FileSystemWatcher может пропустить events на некоторых FS (NFS); periodic refresh fallback (every 60s). StringBuilder pool: Clear() перед Return обязателен иначе leak. Rollback: вернуть new StringBuilder().
Links
- Backlog: ../backlog.md
- Roadmap (EN): ../../ROADMAP-EN.md
- Roadmap (RU): ../../ROADMAP-RU.md