From 3e5a7b71177512670a415d97d090048ce58f0f05 Mon Sep 17 00:00:00 2001 From: Piyush Katariya Date: Fri, 10 Jul 2026 17:46:03 +0530 Subject: [PATCH] refactor(seahorse): use strings.Builder in compaction string helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compaction helpers assemble prompt text from every message in a chunk using `result += fmt.Sprintf(...)` / `content += s`, which is O(n²): each iteration reallocates and copies the whole accumulated string. Switch them to strings.Builder (amortised O(n)). Output is byte-identical (verified against the previous implementation for n = 0, 1, 7, 200). Functions: formatMessagesForSummary, formatSummariesForCondensation, truncateSummary, truncateCondensedSummaries. Benchmarks (200 messages, go1.26, darwin/arm64, -benchmem): formatMessagesForSummary 375µs -> 53µs 1.78MB -> 95KB/op (7.1x faster, 94.7% less mem) truncateSummary 254µs -> 10µs 1.24MB -> 49KB/op (25x faster, 96% less mem) Adds BenchmarkFormatMessagesForSummary and BenchmarkTruncateSummary. --- pkg/seahorse/short_bench_test.go | 34 ++++++++++++++++++++++++++++++++ pkg/seahorse/short_compaction.go | 25 +++++++++++++---------- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/pkg/seahorse/short_bench_test.go b/pkg/seahorse/short_bench_test.go index b7e47bcffa..2a66f6c51d 100644 --- a/pkg/seahorse/short_bench_test.go +++ b/pkg/seahorse/short_bench_test.go @@ -334,3 +334,37 @@ func BenchmarkBootstrap_500Messages(b *testing.B) { } } } + +// --- Compaction formatting benchmarks --- + +func benchSummaryMessages(n int) []Message { + msgs := make([]Message, n) + base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + for i := range msgs { + msgs[i] = Message{ + Role: "user", + Content: fmt.Sprintf("Conversation message %d with enough content to summarize.", i), + CreatedAt: base.Add(time.Duration(i) * time.Minute), + TokenCount: 15, + } + } + return msgs +} + +func BenchmarkFormatMessagesForSummary(b *testing.B) { + msgs := benchSummaryMessages(200) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = formatMessagesForSummary(msgs) + } +} + +func BenchmarkTruncateSummary(b *testing.B) { + msgs := benchSummaryMessages(200) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = truncateSummary(msgs) + } +} diff --git a/pkg/seahorse/short_compaction.go b/pkg/seahorse/short_compaction.go index 0dfb1330fb..661ea64b07 100644 --- a/pkg/seahorse/short_compaction.go +++ b/pkg/seahorse/short_compaction.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "sort" + "strings" "time" "github.com/sipeed/picoclaw/pkg/logger" @@ -744,20 +745,20 @@ func (e *CompactionEngine) runCondensedLoop(ctx context.Context, convID int64) { // --- Helper functions --- func formatMessagesForSummary(messages []Message) string { - var result string + var b strings.Builder for _, m := range messages { ts := m.CreatedAt.Format("2006-01-02 15:04 MST") content := m.Content if content == "" && len(m.Parts) > 0 { content = partsToReadableContent(m.Parts) } - result += fmt.Sprintf("[%s]\n%s\n\n", ts, content) + fmt.Fprintf(&b, "[%s]\n%s\n\n", ts, content) } - return result + return b.String() } func formatSummariesForCondensation(summaries []Summary) string { - var result string + var b strings.Builder for _, s := range summaries { earliest := "" if s.EarliestAt != nil { @@ -767,9 +768,9 @@ func formatSummariesForCondensation(summaries []Summary) string { if s.LatestAt != nil { latest = s.LatestAt.Format("2006-01-02") } - result += fmt.Sprintf("[%s - %s]\n%s\n\n", earliest, latest, s.Content) + fmt.Fprintf(&b, "[%s - %s]\n%s\n\n", earliest, latest, s.Content) } - return result + return b.String() } func buildLeafSummaryPrompt(sourceText, previousSummary string, targetTokens int) string { @@ -847,14 +848,16 @@ Output requirements: } func truncateSummary(messages []Message) string { - content := "" + var b strings.Builder for _, m := range messages { c := m.Content if c == "" && len(m.Parts) > 0 { c = partsToReadableContent(m.Parts) } - content += c + "\n" + b.WriteString(c) + b.WriteByte('\n') } + content := b.String() if len(content) > 2048 { content = content[:2048] } @@ -863,10 +866,12 @@ func truncateSummary(messages []Message) string { } func truncateCondensedSummaries(summaries []Summary) string { - content := "" + var b strings.Builder for _, s := range summaries { - content += s.Content + "\n" + b.WriteString(s.Content) + b.WriteByte('\n') } + content := b.String() if len(content) > 2048 { content = content[:2048] }