From 10df96e6c2dd2a0d19032a7bf8e6bc16b9dfdc18 Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:44:31 +0900 Subject: [PATCH] docs: add visitor pattern to G-1 for zero-copy log filtering Introduce visit(fn func(*LogEntry) bool) on ring buffer. Passes pointer under RLock directly to callback; no intermediate snapshot slice. RecentLogs() copies only entries that pass the filter (not all 100). Sanitization (Caller strip, SanitizeFields) still requires one copy per returned entry - unavoidable. Keep recent() for test compatibility. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 51db88659..15867ce3c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -691,6 +691,65 @@ Go には言語レベルの readonly 参照がないため「書き込み禁止 Phase 1 の変更内容を「`entries` の型を `[]*LogEntry` に変更 + `recent()` の戻り値を `[]*LogEntry` に変更」に更新すること。 +#### さらに踏み込む: visitor パターンで中間スライスも排除 + +`*LogEntry` 保持案はポインタコピーは残る。`RecentLogs()` の実装を見ると、さらに削減できる余地がある。 + +``` +現状: + ring(100件) → recent(0) → snapshot[]LogEntry(100件全コピー) + → filter/sanitize → result[M件] + ↑フィルタで弾く分まで全コピーするのが無駄 + +visitor パターン: + ring(100件) → visit(callback) ─ フィルタを通ったM件だけコピー → result[M件] + RLock 中に *LogEntry を直接渡す、中間スライス不要 +``` + +```go +// ring buffer に visit() を追加 +func (rb *logRingBuffer) visit(fn func(*LogEntry) bool) { + rb.mu.RLock() + defer rb.mu.RUnlock() + n := rb.count + start := (rb.head - n + len(rb.entries)) % len(rb.entries) + for i := n - 1; i >= 0; i-- { // 新しい順にイテレート + if !fn(&rb.entries[(start+i)%len(rb.entries)]) { + return + } + } +} + +// RecentLogs: 中間スライスなし、フィルタを通った分だけコピー +func RecentLogs(minLevel LogLevel, component string, limit int) []LogEntry { + result := make([]LogEntry, 0, limit) + ringBuf.visit(func(e *LogEntry) bool { + if len(result) >= limit { + return false + } + if lvl, ok := levelFromName[e.Level]; ok && lvl < minLevel { + return true // コピーせずスキップ + } + if component != "" && e.Component != component { + return true // コピーせずスキップ + } + sanitized := *e // フィルタを通った分だけコピー + sanitized.Caller = "" + sanitized.Fields = SanitizeFields(e.Fields) + result = append(result, sanitized) + return true + }) + // visit が新しい順にイテレートするので reverse 不要 + return result +} +``` + +**避けられないコピー**: `e.Caller = ""` と `SanitizeFields()` でエントリを変更するため、返すエントリは必ず新しい値として生成が必要。コピー自体はゼロにならないが、**フィルタで弾くエントリのコピーは完全になくなる**。 + +**制約**: `visit()` は RLock を保持したまま callback を呼ぶ。callback 内でポインタを保存・持ち出してはいけない(`*e` として即コピーするのは安全)。 + +**最終的な Phase 1 の変更内容**: `recent()` を残しつつ `visit()` を追加 → `RecentLogs()` を `visit()` ベースに書き換え。`recent()` は既存テストとの互換のため残す。 + ### G-2. Phase 3-1: `FormatPlanDisplay()` が修正対象に含まれていない `GetMemoryContext()` 以外にも `FormatPlanDisplay()` が同じ多重 `ReadLongTerm()` 問題を持つ。呼び出しチェーンは以下の通り: