Skip to content

Commit d9522dc

Browse files
committed
perf(data,embed,log): drop allocs across read paths + log formatter
Three lifts found by running the full bench against v0.10.0 and looking properly at what's still bleeding, not just one pattern. * Data.resolve — replace SplitN("/") with Index + string-slicing for the mount-name + relative-path split. Zero alloc instead of allocating a 2-element []string. Every Data.ReadFile / ReadString / List / ListNames / Mounts call benefits. * Embed.path — skip PathJoin when basedir is "" or ".". The canonical Mount(fsys, ".") shape is hot and that PathJoin alloc was per-call. * log writeKV — replace Sprintf("%q") / Sprintf("%v") with strconv.AppendQuote / AppendInt / AppendUint / AppendBool / AppendFloat into a stack-allocated 64-byte scratch. Zero alloc per keyval for string / int / int64 / uint / uint64 / bool / float64 (the >99% of real log values). Non-trivial types still go through Sprintf to preserve behaviour. Bench impact (Apple M3 Ultra): Data.ReadFile_Hit 299 ns / 9 allocs → 187 ns / 5 allocs 1.6x Data.ReadString_Hit 355 ns / 11 allocs → 207 ns / 7 allocs 1.7x Data.List 484 ns / 13 allocs → 375 ns / 9 allocs 1.3x Embed.ReadFile 251 ns / 8 allocs → 162 ns / 5 allocs 1.5x Embed.ReadString 274 ns / 10 allocs → 186 ns / 7 allocs 1.5x Embed.Sub 187 ns / 5 allocs → 73 ns / 3 allocs 2.6x Log.Info_TwoKeyvals 273 ns / 5 allocs → 188 ns / 2 allocs 1.5x Log.Warn_SixKeyvals 585 ns / 11 allocs → 345 ns / 2 allocs 1.7x Log.Security 305 ns / 6 allocs → 202 ns / 2 allocs 1.5x Log.LogErr_Log 416 ns / 9 allocs → 290 ns / 4 allocs 1.4x
1 parent 0a8b115 commit d9522dc

3 files changed

Lines changed: 46 additions & 11 deletions

File tree

data.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,18 @@ func (d *Data) New(opts Options) Result {
7171
}
7272

7373
// resolve splits a path like "brain/coding.md" into mount name + relative path.
74+
// Uses Index + string-slicing so the split is zero-alloc — SplitN
75+
// would allocate a []string of length two plus the slice header.
7476
func (d *Data) resolve(path string) (*Embed, string) {
75-
parts := SplitN(path, "/", 2)
76-
if len(parts) < 2 {
77+
i := Index(path, "/")
78+
if i < 0 {
7779
return nil, ""
7880
}
79-
r := d.Get(parts[0])
81+
r := d.Get(path[:i])
8082
if !r.OK {
8183
return nil, ""
8284
}
83-
return r.Value.(*Embed), parts[1]
85+
return r.Value.(*Embed), path[i+1:]
8486
}
8587

8688
// ReadFile reads a file by full path.

embed.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,19 @@ func MountEmbed(efs embed.FS, basedir string) Result {
390390
}
391391

392392
func (s *Embed) path(name string) Result {
393-
joined := PathToSlash(PathJoin(s.basedir, name))
393+
// Fast paths for common mount shapes:
394+
// - basedir "" or "." → name is already the embed-FS path; no
395+
// PathJoin alloc required. (Mount(fsys, ".") is the typical
396+
// consumer shape and hits this branch on every Read/Open.)
397+
// - basedir non-empty → fall through to PathJoin which builds
398+
// the joined path.
399+
var joined string
400+
switch s.basedir {
401+
case "", ".":
402+
joined = name
403+
default:
404+
joined = PathToSlash(PathJoin(s.basedir, name))
405+
}
394406
if HasPrefix(joined, "..") || Contains(joined, "/../") || HasSuffix(joined, "/..") {
395407
return Result{E("embed.path", Concat("path traversal rejected: ", name), nil), false}
396408
}

log.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package core
88
import (
99
"bytes"
1010
goio "io"
11+
"strconv"
1112
)
1213

1314
// Level defines logging verbosity.
@@ -248,6 +249,11 @@ func (l *Log) log(level Level, prefix, msg string, keyvals ...any) {
248249
}
249250
}
250251

252+
// Scratch buffer reused across keyvals for strconv.AppendX calls.
253+
// On stack — no heap alloc unless writeKV's closure escapes (it
254+
// doesn't; it only escapes within this function's lifetime).
255+
var scratch [64]byte
256+
251257
writeKV := func(key any, val any) {
252258
line.WriteByte(' ')
253259
// Fast path: key is already a string (the >99% case for structured logs).
@@ -266,12 +272,27 @@ func (l *Log) log(level Level, prefix, msg string, keyvals ...any) {
266272
line.WriteString(keyStr)
267273
line.WriteByte('=')
268274
}
269-
// Value formatting: %q for strings (escapes + quotes), %v for the rest.
270-
// Stays on Sprintf for non-trivial types so behaviour matches the
271-
// previous formatter byte-for-byte.
272-
if s, ok := val.(string); ok {
273-
line.WriteString(Sprintf("%q", s))
274-
} else {
275+
// Value formatting: byte-level fast paths for the common types
276+
// (string / int / uint / bool / float64) drive directly into
277+
// the line buffer via strconv.AppendX — zero alloc per keyval.
278+
// All other types fall through to Sprintf("%v"), matching the
279+
// previous behaviour for less common values.
280+
switch v := val.(type) {
281+
case string:
282+
line.Write(strconv.AppendQuote(scratch[:0], v))
283+
case int:
284+
line.Write(strconv.AppendInt(scratch[:0], int64(v), 10))
285+
case int64:
286+
line.Write(strconv.AppendInt(scratch[:0], v, 10))
287+
case uint:
288+
line.Write(strconv.AppendUint(scratch[:0], uint64(v), 10))
289+
case uint64:
290+
line.Write(strconv.AppendUint(scratch[:0], v, 10))
291+
case bool:
292+
line.Write(strconv.AppendBool(scratch[:0], v))
293+
case float64:
294+
line.Write(strconv.AppendFloat(scratch[:0], v, 'g', -1, 64))
295+
default:
275296
line.WriteString(Sprintf("%v", val))
276297
}
277298
}

0 commit comments

Comments
 (0)