perf: reduce []byte/string conversions and add ASCII fast paths

- bytes.NewReader instead of strings.NewReader(string(...)) in web search
- Consolidate multiple string(body) calls into single variable
- sb.Write(paramsJSON) instead of string conversion in CLI provider
- ASCII fast paths in Truncate/wrapLine before []rune conversion
- Byte-length truncation for ASCII-only branch names
- Extract runeDisplayWidth to avoid per-rune string(r) in wrap loop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-24 15:43:41 +09:00
parent 66c5f2d897
commit 77e0862827
5 changed files with 45 additions and 31 deletions

View file

@ -1036,28 +1036,32 @@ func formatMarkdownTable(lines []string) string {
return strings.TrimRight(b.String(), "\n") return strings.TrimRight(b.String(), "\n")
} }
func runeDisplayWidth(r rune) int {
switch {
case r == '\u200d' || r == '\u200c' || r == '\ufe0f':
return 0
case unicode.Is(unicode.Mn, r):
return 0
case isEmojiRune(r):
return 3
case unicode.In(r,
unicode.Han,
unicode.Hiragana,
unicode.Katakana,
unicode.Hangul):
return 2
case (r >= 0x3000 && r <= 0x303F) || (r >= 0xFF00 && r <= 0xFFEF):
// CJK symbols/punctuation and half/fullwidth forms.
return 2
default:
return 1
}
}
func displayWidth(s string) int { func displayWidth(s string) int {
w := 0 w := 0
for _, r := range s { for _, r := range s {
switch { w += runeDisplayWidth(r)
case r == '\u200d' || r == '\u200c' || r == '\ufe0f':
continue
case unicode.Is(unicode.Mn, r):
continue
case isEmojiRune(r):
w += 3
case unicode.In(r,
unicode.Han,
unicode.Hiragana,
unicode.Katakana,
unicode.Hangul):
w += 2
case (r >= 0x3000 && r <= 0x303F) || (r >= 0xFF00 && r <= 0xFFEF):
// CJK symbols/punctuation and half/fullwidth forms.
w += 2
default:
w++
}
} }
return w return w
} }
@ -1092,7 +1096,7 @@ func wrapByDisplayWidth(s string, maxWidth int) []string {
flush() flush()
continue continue
} }
rw := displayWidth(string(r)) rw := runeDisplayWidth(r)
if rw == 0 { if rw == 0 {
cur.WriteRune(r) cur.WriteRune(r)
continue continue

View file

@ -67,12 +67,11 @@ func SanitizeBranchName(task string) string {
s = "worktree" s = "worktree"
} }
// Truncate to 40 chars // Truncate to 40 chars (ASCII fast path: branch names are ASCII after sanitization)
runes := []rune(s) if len(s) > 40 {
if len(runes) > 40 { s = s[:40]
runes = runes[:40]
} }
s = strings.TrimRight(string(runes), "-") s = strings.TrimRight(s, "-")
return "plan/" + s return "plan/" + s
} }

View file

@ -130,7 +130,9 @@ func (p *ClaudeCliProvider) buildToolsPrompt(tools []ToolDefinition) string {
} }
if len(tool.Function.Parameters) > 0 { if len(tool.Function.Parameters) > 0 {
paramsJSON, _ := json.Marshal(tool.Function.Parameters) paramsJSON, _ := json.Marshal(tool.Function.Parameters)
sb.WriteString(fmt.Sprintf("Parameters:\n```json\n%s\n```\n", string(paramsJSON))) sb.WriteString("Parameters:\n```json\n")
sb.Write(paramsJSON)
sb.WriteString("\n```\n")
} }
sb.WriteString("\n") sb.WriteString("\n")
} }

View file

@ -278,7 +278,7 @@ func (p *PerplexitySearchProvider) Search(ctx context.Context, query string, cou
return "", fmt.Errorf("failed to marshal request: %w", err) return "", fmt.Errorf("failed to marshal request: %w", err)
} }
req, err := http.NewRequestWithContext(ctx, "POST", searchURL, strings.NewReader(string(payloadBytes))) req, err := http.NewRequestWithContext(ctx, "POST", searchURL, bytes.NewReader(payloadBytes))
if err != nil { if err != nil {
return "", fmt.Errorf("failed to create request: %w", err) return "", fmt.Errorf("failed to create request: %w", err)
} }
@ -534,6 +534,7 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]any) *ToolRe
var text, extractor string var text, extractor string
bodyStr := string(body)
if strings.Contains(contentType, "application/json") { if strings.Contains(contentType, "application/json") {
var jsonData any var jsonData any
if err := json.Unmarshal(body, &jsonData); err == nil { if err := json.Unmarshal(body, &jsonData); err == nil {
@ -541,15 +542,15 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]any) *ToolRe
text = string(formatted) text = string(formatted)
extractor = "json" extractor = "json"
} else { } else {
text = string(body) text = bodyStr
extractor = "raw" extractor = "raw"
} }
} else if strings.Contains(contentType, "text/html") || len(body) > 0 && } else if strings.Contains(contentType, "text/html") || len(body) > 0 &&
(strings.HasPrefix(string(body), "<!DOCTYPE") || strings.HasPrefix(strings.ToLower(string(body)), "<html")) { (strings.HasPrefix(bodyStr, "<!DOCTYPE") || strings.HasPrefix(strings.ToLower(bodyStr), "<html")) {
text = t.extractText(string(body)) text = t.extractText(bodyStr)
extractor = "text" extractor = "text"
} else { } else {
text = string(body) text = bodyStr
extractor = "raw" extractor = "raw"
} }

View file

@ -47,6 +47,10 @@ func TailPad(s string, n, wrapWidth int) string {
// wrapLine splits a single line into segments of at most width runes. // wrapLine splits a single line into segments of at most width runes.
// An empty line produces one empty string (preserving blank lines). // An empty line produces one empty string (preserving blank lines).
func wrapLine(line string, width int) []string { func wrapLine(line string, width int) []string {
// ASCII fast path: byte length == rune length for pure ASCII
if len(line) <= width {
return []string{line}
}
runes := []rune(line) runes := []rune(line)
if len(runes) <= width { if len(runes) <= width {
return []string{line} return []string{line}
@ -97,6 +101,10 @@ func Truncate(s string, maxLen int) string {
if maxLen <= 0 { if maxLen <= 0 {
return "" return ""
} }
// ASCII fast path: byte length == rune length for pure ASCII
if len(s) <= maxLen {
return s
}
runes := []rune(s) runes := []rune(s)
if len(runes) <= maxLen { if len(runes) <= maxLen {
return s return s