Skip to content

Commit 0b23c5d

Browse files
committed
storage: use AppendString for zero-alloc panic-message rendering
Follow-up to #243. Allocation-efficient variant of describeScmerValue: - AppendString with a heap-backed 256-byte scratch buffer so primitive values (string/symbol/int/float/bool/nil) render with zero extra allocations; slice/dict values still allocate inside AppendString. - Truncation now respects UTF-8 rune boundaries via utf8.RuneStart so we never leave a half-encoded code point in the panic message. Panic output is unchanged for ASCII values; multi-byte sequences that fell exactly on the 200-byte cut no longer produce invalid UTF-8.
1 parent 906149f commit 0b23c5d

1 file changed

Lines changed: 22 additions & 6 deletions

File tree

storage/storage.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import "time"
2626
import "strconv"
2727
import "reflect"
2828
import "strings"
29+
import "unicode/utf8"
2930
import units "github.com/docker/go-units"
3031
import "github.com/launix-de/memcp/scm"
3132
import "github.com/launix-de/go-mysqlstack/sqldb"
@@ -253,17 +254,32 @@ func scmerSlice(v scm.Scmer) ([]scm.Scmer, bool) {
253254
}
254255

255256
// describeScmerValue renders v for use in a panic message. Long values
256-
// (entire codegen'd expressions) are truncated so the panic stays readable.
257+
// (entire codegen'd expressions) are truncated at a UTF-8 rune boundary
258+
// so the panic stays readable and never leaves a half-encoded code point.
259+
//
260+
// Uses AppendString with a heap-backed 256-byte scratch buffer so primitive
261+
// values (string/symbol/int/float/bool/nil) render without an extra heap
262+
// allocation. Larger values (slices, dicts) still allocate inside
263+
// AppendString — panics are rare, so we accept that cost.
257264
func describeScmerValue(v scm.Scmer) string {
258-
const maxLen = 200
265+
const maxBytes = 200
259266
if v.IsNil() {
260267
return "nil"
261268
}
262-
s := scm.String(v)
263-
if len(s) > maxLen {
264-
return s[:maxLen] + "…"
269+
// make'd slice is heap-allocated so the unsafe.String view returned by
270+
// AppendString for tagInt / tagFloat stays live as long as the result.
271+
buf := make([]byte, 0, 256)
272+
s, _ := v.AppendString(buf)
273+
if len(s) <= maxBytes {
274+
return s
275+
}
276+
// Back off from maxBytes to the previous rune boundary; max UTF-8 rune
277+
// is 4 bytes so this costs at most 3 iterations.
278+
cut := maxBytes
279+
for cut > 0 && !utf8.RuneStart(s[cut]) {
280+
cut--
265281
}
266-
return s
282+
return s[:cut] + "…"
267283
}
268284

269285
func mustScmerSlice(v scm.Scmer, ctx string) []scm.Scmer {

0 commit comments

Comments
 (0)