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 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-24 13:44:31 +09:00
parent a30bbf7567
commit 10df96e6c2

View file

@ -691,6 +691,65 @@ Go には言語レベルの readonly 参照がないため「書き込み禁止
Phase 1 の変更内容を「`entries` の型を `[]*LogEntry` に変更 + `recent()` の戻り値を `[]*LogEntry` に変更」に更新すること。 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()` が修正対象に含まれていない ### G-2. Phase 3-1: `FormatPlanDisplay()` が修正対象に含まれていない
`GetMemoryContext()` 以外にも `FormatPlanDisplay()` が同じ多重 `ReadLongTerm()` 問題を持つ。呼び出しチェーンは以下の通り: `GetMemoryContext()` 以外にも `FormatPlanDisplay()` が同じ多重 `ReadLongTerm()` 問題を持つ。呼び出しチェーンは以下の通り: