From e448c314a0700d84d0686e21c33bb501fbcdd6fc Mon Sep 17 00:00:00 2001 From: amvtek Date: Thu, 25 Jun 2026 14:26:44 +0300 Subject: [PATCH 1/5] Change Program Counter caching approach. --- pkg/raised/error.go | 293 +++++++++++++++++++++++--------------------- 1 file changed, 154 insertions(+), 139 deletions(-) diff --git a/pkg/raised/error.go b/pkg/raised/error.go index 1a14080..454fc34 100644 --- a/pkg/raised/error.go +++ b/pkg/raised/error.go @@ -153,26 +153,7 @@ func (self *errTrace) Error() string { return "" } - // determine caching keys - var start int - if self.next > traceSize { - start = traceSize - 1 - } else { - start = self.next - 1 - } - k1 := self.pcs[start] - k2 := self.msgs[start] - - smr, rs := summaryCache.Get(k1, k2) - switch rs { - case cchMiss: - self._summary = self.genSummary() - case cchMissCacheNew: - self._summary = self.genSummary() - summaryCache.Set(k1, k2, self._summary) - case cchHit: - self._summary = smr - } + self._summary = self.genSummary() return self._summary } @@ -191,20 +172,7 @@ func (self *errTrace) Trace() string { return "" } - // determine caching keys - k1 := traceL1Key{turnCount: self.next, pcs: self.pcs} - k2 := traceL2Key{cause: self.causeString(), msgs: self.msgs} - - trc, rs := traceCache.Get(k1, k2) - switch rs { - case cchMiss: - self._trace = self.genTrace() - case cchMissCacheNew: - self._trace = self.genTrace() - traceCache.Set(k1, k2, self._trace) - case cchHit: - self._trace = trc - } + self._trace = self.genTrace() return self._trace } @@ -284,9 +252,10 @@ func (self *errTrace) genSummary() string { msg := self.msgs[start] // retrieve file/line string - fls := getFileLines(self.pcs[start : 1+start])[0] + var info pcInfo + loadPCInfo(self.pcs[start], &info) - return msg + fls + return msg + info.tfl } // genTrace renders the full traceback string across all recorded steps. @@ -311,20 +280,31 @@ func (self *errTrace) genTrace() string { start = maxstart } - // render full error trace - // note that the trace is "compressed" if it has more than traceSize turns + // prepare lines buffer (used to avoid allocating) + var line string + var lns [4 + 2*traceSize]string + var size int + lines := lns[:0] + + line = "Traceback [most recent call first]:\n" + size += len(line) + lines = append(lines, line) + var omitfmt string - var tb strings.Builder - tb.WriteString("Traceback [most recent call first]:\n") - fls := getFileLines(self.pcs[:1+start]) + var info pcInfo msgs := self.msgs + pcs := self.pcs for i := start; i >= 0; i -= 1 { + // buffer trace msg + line = msgs[i] + size += len(line) + lines = append(lines, line) - // write trace msg - tb.WriteString(msgs[i]) - - // write trace file/line - tb.WriteString(fls[i]) + // buffer trace file/line + loadPCInfo(pcs[i], &info) + line = info.tfl + size += len(line) + lines = append(lines, line) // insert misscount if it is > 0 if i == misspos { @@ -333,12 +313,26 @@ func (self *errTrace) genTrace() string { } else { omitfmt = " [%d omissions...]\n" } - tb.WriteString(fmt.Sprintf(omitfmt, misscount)) + line = fmt.Sprintf(omitfmt, misscount) // 1 extra alloc, could be avoided... + size += len(line) + lines = append(lines, line) } } if cause != msgs[0] { - tb.WriteString("Caused by:\n ") - tb.WriteString(cause) + line = "Caused by:\n " + size += len(line) + lines = append(lines, line) + + line = cause + size += len(line) + lines = append(lines, line) + } + + // render full error trace + var tb strings.Builder + tb.Grow(size) // only 1 alloc + for _, ln := range lines { + tb.WriteString(ln) } return tb.String() @@ -409,59 +403,6 @@ func (self *errTraceSnapshot) entryPoint() uintptr { } } -// getFileLines resolves a slice of PCs to formatted file/line strings. -func getFileLines(pcs []uintptr) []string { - if 0 == len(pcs) { - return nil - } - - fls := make([]string, 0, len(pcs)) - - frames := runtime.CallersFrames(pcs) - var frame runtime.Frame - var more bool - var filename, fileline, funcname, pkgpath string - var dotpos int - for { - frame, more = frames.Next() - - // extract base filename - filename = filepath.Base(frame.File) - - // funcname in canonical form module/package.funcname[.component] - // funcname may have more than 1 component if func is instantiated dynamically by a factory - funcname = frame.Function - - // extract pkgpath in canonical form module/package - dotpos = strings.LastIndex(funcname, "/") - if dotpos < 0 { - dotpos = 0 - } - dotpos += strings.Index(funcname[dotpos:], ".") - if dotpos >= 0 { - pkgpath = funcname[:dotpos] - - // filename is in canonical form module/package/filename - // module is relative to current buildModPath - filename, _ = strings.CutPrefix(path.Join(pkgpath, filename), buildModPath) - - fileline = fmt.Sprintf(filelineFmt, filename, frame.Line) - - } else { - - fileline = missFileline - } - - fls = append(fls, fileline) - - if !more { - break - } - } - - return fls -} - // buildModPath is the module path prefix stripped from file names in traces. var buildModPath string @@ -496,7 +437,8 @@ func addCallerInfo[K ~int](err *errTrace, flk K, msg string, skip int) { err.msgs[pos] = strings.TrimSpace(msg) // record module "entry point" - if err.epc == 0 && isLocal(pc) { + local := isLocal(pc) + if local && err.epc == 0 { err.epc = pc } @@ -507,21 +449,11 @@ func addCallerInfo[K ~int](err *errTrace, flk K, msg string, skip int) { err.next += 1 } -// isLocal returns true if pc is a program counter within "project" module. -// TODO: we need an heuristic in case module is undefined. -func isLocal(pc uintptr) bool { - // FuncForPC does not alloc where as CallersFrames do - if fn := runtime.FuncForPC(pc - 1); fn != nil && pc > 0 { - return strings.HasPrefix(fn.Name(), buildModPath) - } - return false -} - // --- // program counter resolution caching // pcCache stores resolved program counters keyed by pckey, shared across -// all TraceAt call sites. +// all Trace call sites. var pcCache sync.Map // pckey is the cache key for getPC. @@ -564,21 +496,120 @@ func getPC[K ~int](ck pckey[K]) uintptr { return pc } -// --- -// Error() & Trace() caching +// pcInfoCache is a process-wide store mapping program counter values to their +// resolved pcInfo. +var pcInfoCache sync.Map + +// pcInfo holds the resolved file/line information for a single program counter, +// cached to amortize the cost of runtime.CallersFrames across repeated lookups +// of the same PC. +type pcInfo struct { + // pc is the program counter this entry was resolved from. + pc uintptr + + // trace fileline string (optimised for reading) + tfl string + + // hash fileline slice (used for error key hashing) + hfl []byte + + // true if file is part of project + local bool +} + +// isLocal returns true if pc is a program counter within "project" module. +// isLocal has side effects, it stores file/line information in pcInfoCache. +func isLocal(pc uintptr) bool { + if 0 == pc { + return false + } + + var info pcInfo + var ok bool + + val, found := pcInfoCache.Load(pc) + if found { + info, ok = val.(pcInfo) + if !ok { + info.pc = 0 + pcInfoCache.CompareAndDelete(pc, val) + } + } + if 0 == info.pc { + info.pc = pc + + pcs := []uintptr{pc} + frames := runtime.CallersFrames(pcs) + frame, _ := frames.Next() + + // extract base filename + basename := filepath.Base(frame.File) + + // funcname in canonical form module/package.funcname[.component] + // funcname may have more than 1 component if func is instantiated dynamically by a factory + funcname := frame.Function + + // extract pkgpath in canonical form module/package + dotpos := strings.LastIndex(funcname, "/") + if dotpos < 0 { + dotpos = 0 + } + dotpos += strings.Index(funcname[dotpos:], ".") + if dotpos >= 0 { + pkgpath := funcname[:dotpos] + filename := path.Join(pkgpath, basename) + + info.hfl = fmt.Appendf(nil, filelineFmt, filename, frame.Line) + + // filename is in canonical form module/package/filename + // module is relative to current buildModPath + filename, local := strings.CutPrefix(filename, buildModPath) + info.local = local + info.tfl = fmt.Sprintf(filelineFmt, filename, frame.Line) + + } else { + + info.tfl = missFileline + info.hfl = []byte(missFileline) + } + val, _ = pcInfoCache.LoadOrStore(pc, info) + info = val.(pcInfo) // if val was again invalid this would indicate a big issue, better panic in this case + } + + return info.local + +} + +// noPCInfo is the sentinel pcInfo returned by loadPCInfo when a program counter +// cannot be resolved or is not present in pcInfoCache. +var noPCInfo = pcInfo{tfl: missFileline, hfl: []byte(missFileline)} + +// loadPCInfo retrieves the cached pcInfo for pc into dst. +// Returns true if a valid entry was found in pcInfoCache. +func loadPCInfo(pc uintptr, dst *pcInfo) bool { + var ok, valid bool + info := noPCInfo + + val, ok := pcInfoCache.Load(pc) + if ok { + info, valid = val.(pcInfo) + if !valid { + pcInfoCache.CompareAndDelete(pc, val) + info = noPCInfo + ok = false + } + } + *dst = info + + return ok +} // tickInterval is the time window duration used by the package-level scClock, // controlling how quickly summaryCache and traceCache slot states evolve. const tickInterval = 16 * time.Second // ticks is the shared clock driving summaryCache and traceCache. -// summaryCache keys on (mostRecentPC, mostRecentMsg) and caches Error() output. -// traceCache keys on (pcs+turnCount, cause+msgs) and caches Trace() output. -var ( - ticks = &scClock{} - summaryCache = &timedCache[uintptr, string, string]{clock: ticks} - traceCache = &timedCache[traceL1Key, traceL2Key, string]{clock: ticks} -) +var ticks = &scClock{} func init() { err := ticks.Init(tickInterval) @@ -591,19 +622,3 @@ func init() { // the module entry point, the recorded propagation PCs, and the total Trace // call count. type L1Key = [2 + traceSize]uintptr - -// traceL1Key is the stable outer key for traceCache, derived from the recorded -// PCs and total turn count. Two errTrace values sharing this key took the same -// code path and map to the same cacheSlot. -type traceL1Key struct { - pcs [traceSize]uintptr - turnCount int -} - -// traceL2Key is the inner key within a traceCache cacheSlot, derived from the -// cause string and message array. It discriminates errors that share a code path -// but differ in cause or per-step messages. -type traceL2Key struct { - cause string - msgs [traceSize]string -} From 50143ac5a809b7da57c2cd0a89e895d38daeff53 Mon Sep 17 00:00:00 2001 From: amvtek Date: Thu, 25 Jun 2026 14:28:04 +0300 Subject: [PATCH 2/5] Load file/line info from Program Counter cache. --- pkg/raised/keying.go | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/pkg/raised/keying.go b/pkg/raised/keying.go index 332e6c1..2bd2430 100644 --- a/pkg/raised/keying.go +++ b/pkg/raised/keying.go @@ -11,6 +11,13 @@ import ( // keySize is the number of bytes in an ErrorKey. const keySize = 16 +var ( + tagCause = []byte("CS") + tagNext = []byte("NX") + tagFilelineCount = []byte("FLC") + tagFileline = []byte("FLN") +) + // ErrorKey is a fixed-size hash derived from an error's propagation path and // terminal error identity. Two errors sharing the same ErrorKey represent the // same problem: identical code path and equivalent root cause. @@ -164,14 +171,14 @@ func (self *sentinelErrorKeyer[T]) Key(err error) (ErrorKey, bool) { // --- // cause component - hs.Write([]byte("CS")) + hs.Write(tagCause) binary.BigEndian.PutUint64(ib[:], uint64(len(k2))) hs.Write(ib[:]) hs.Write([]byte(k2)) // --- // next component - hs.Write([]byte("NX")) + hs.Write(tagNext) binary.BigEndian.PutUint64(ib[:], uint64(snp.next)) hs.Write(ib[:]) @@ -187,23 +194,25 @@ func (self *sentinelErrorKeyer[T]) Key(err error) (ErrorKey, bool) { flc = traceSize } flc += 1 // pc are read from k1 which is [epc|pcs...|next] - hs.Write([]byte("FLC")) + hs.Write(tagFilelineCount) binary.BigEndian.PutUint64(ib[:], uint64(flc)) hs.Write(ib[:]) // hash each fileline in ert code path // flc allows excluding next which is not a valid pc - fls := getFileLines(k1[:flc]) - for _, fln := range fls { - hs.Write([]byte("FLN")) - binary.BigEndian.PutUint64(ib[:], uint64(len(fln))) + var info pcInfo + for i := range flc { + loadPCInfo(k1[i], &info) + hs.Write(tagFileline) + binary.BigEndian.PutUint64(ib[:], uint64(len(info.hfl))) hs.Write(ib[:]) - hs.Write([]byte(fln)) + hs.Write(info.hfl) } // --- // copy hash in erk - copy(erk[:], hs.Sum(nil)) + var buf [32]byte + copy(erk[:], hs.Sum(buf[:0])) if rs == cchMissCacheNew { self.tc.Set(k1, k2, erk) From 81b2d040049145640f9807518ea5bf3eae3e994e Mon Sep 17 00:00:00 2001 From: amvtek Date: Fri, 26 Jun 2026 06:27:48 +0300 Subject: [PATCH 3/5] Improve labelling of design benchmarks. --- pkg/raised/design_test.go | 198 +++++++++++++++++++++++++++----------- 1 file changed, 143 insertions(+), 55 deletions(-) diff --git a/pkg/raised/design_test.go b/pkg/raised/design_test.go index 54339c8..5873d0e 100644 --- a/pkg/raised/design_test.go +++ b/pkg/raised/design_test.go @@ -44,10 +44,6 @@ func TestTmp_Join(t *testing.T) { } } -func TestDesign_Lab(t *testing.T) { - t.Logf("f03() -> %+v", f03()) -} - var errOne = NewSentinel("ERROR(1): 1 more time...") func f01() error { @@ -63,14 +59,10 @@ func f03() error { } // ==================================================================================== -// Propagating fmt.Errorf vs using raised.TraceAt +// Propagating fmt.Errorf vs raised.Trace vs raised.TraceAt (aka PCCache) vs github.com/pkg/errors (aka Rce) // // To run those benchmarks use: // go test ./pkg/raised -bench Design_prop -benchmem -// -// Those benchmarks show that propagating an error using raised.Trace requires a single alloc -// Whereas fmt.Errorf number of allocs grow linearily with trace size. -// raised Error rendering time performance is also much better than what fmt.Errorf allows. func BenchmarkDesign_propErrorfs64r4(b *testing.B) { ef := makeErrorfPropChain(64, 4) @@ -93,148 +85,244 @@ func BenchmarkDesign_propRaiseds64r4(b *testing.B) { } } -func BenchmarkDesign_propRaisedPCCaches64r4(b *testing.B) { - ef := makeRaisedPCCachePropChain(64, 4) +func BenchmarkDesign_propPCCaches64r4(b *testing.B) { + ef := makePCCachePropChain(64, 4) for b.Loop() { _ = ef() } } -func BenchmarkDesign_propErrorfTraces64r4(b *testing.B) { - ef := makeErrorfPropChain(64, 4) +func BenchmarkDesign_propErrorfs64r8(b *testing.B) { + ef := makeErrorfPropChain(64, 8) for b.Loop() { - _ = fmt.Sprintf("%+v", ef()) // using fmt results in 1 extra alloc... + _ = ef() } } -func BenchmarkDesign_propRceTraces64r4(b *testing.B) { - ef := makeRcePropChain(64, 4) +func BenchmarkDesign_propRces64r8(b *testing.B) { + ef := makeRcePropChain(64, 8) for b.Loop() { - _ = fmt.Sprintf("%+v", ef()) + _ = ef() } } -func BenchmarkDesign_propRaisedTraces64r4(b *testing.B) { - ef := makeRaisedPropChain(64, 4, errPropSentinel) +func BenchmarkDesign_propRaiseds64r8(b *testing.B) { + ef := makeRaisedPropChain(64, 8, errPropSentinel) for b.Loop() { - _ = fmt.Sprintf("%+v", ef()) // using fmt results in 1 extra alloc... + _ = ef() } } -func BenchmarkDesign_propRaisedPCCacheTraces64r4(b *testing.B) { - ef := makeRaisedPCCachePropChain(64, 4) +func BenchmarkDesign_propPCCaches64r8(b *testing.B) { + ef := makePCCachePropChain(64, 8) for b.Loop() { - _ = fmt.Sprintf("%+v", ef()) // using fmt results in 1 extra alloc... + _ = ef() } } -func BenchmarkDesign_propErrorfs64r8(b *testing.B) { - ef := makeErrorfPropChain(64, 8) +func BenchmarkDesign_propErrorfs256r16(b *testing.B) { + ef := makeErrorfPropChain(256, 16) for b.Loop() { _ = ef() } } -func BenchmarkDesign_propRces64r8(b *testing.B) { - ef := makeRcePropChain(64, 8) +func BenchmarkDesign_propRces256r16(b *testing.B) { + ef := makeRcePropChain(256, 16) for b.Loop() { _ = ef() } } -func BenchmarkDesign_propRaiseds64r8(b *testing.B) { - ef := makeRaisedPropChain(64, 8, errPropSentinel) +func BenchmarkDesign_propRaiseds256r16(b *testing.B) { + ef := makeRaisedPropChain(256, 16, errPropSentinel) for b.Loop() { _ = ef() } } -func BenchmarkDesign_propRaisedPCCaches64r8(b *testing.B) { - ef := makeRaisedPCCachePropChain(64, 8) +func BenchmarkDesign_propPCCaches256r16(b *testing.B) { + ef := makePCCachePropChain(256, 16) for b.Loop() { _ = ef() } } -func BenchmarkDesign_propErrorfTraces64r8(b *testing.B) { +// ==================================================================================== +// Rendering errors fmt.Errorf vs raised.Trace vs raised.TraceAt (aka PCCache) vs github.com/pkg/errors (aka Rce) +// +// To run those benchmarks use: +// go test ./pkg/raised -bench Design_error -benchmem + +func BenchmarkDesign_errorErrorfs64r4(b *testing.B) { + ef := makeErrorfPropChain(64, 4) + for b.Loop() { + _ = ef().Error() + } +} + +func BenchmarkDesign_errorRces64r4(b *testing.B) { + ef := makeRcePropChain(64, 4) + for b.Loop() { + _ = ef().Error() + } +} + +func BenchmarkDesign_errorRaiseds64r4(b *testing.B) { + ef := makeRaisedPropChain(64, 4, errPropSentinel) + for b.Loop() { + _ = ef().Error() + } +} + +func BenchmarkDesign_errorPCCaches64r4(b *testing.B) { + ef := makePCCachePropChain(64, 4) + for b.Loop() { + _ = ef().Error() + } +} + +func BenchmarkDesign_errorErrorfs64r8(b *testing.B) { ef := makeErrorfPropChain(64, 8) for b.Loop() { - _ = fmt.Sprintf("%+v", ef()) // using fmt results in 1 extra alloc... + _ = ef().Error() } } -func BenchmarkDesign_propRceTraces64r8(b *testing.B) { +func BenchmarkDesign_errorRces64r8(b *testing.B) { ef := makeRcePropChain(64, 8) for b.Loop() { - _ = fmt.Sprintf("%+v", ef()) // using fmt results in 1 extra alloc... + _ = ef().Error() } } -func BenchmarkDesign_propRaisedTraces64r8(b *testing.B) { +func BenchmarkDesign_errorRaiseds64r8(b *testing.B) { ef := makeRaisedPropChain(64, 8, errPropSentinel) for b.Loop() { - _ = fmt.Sprintf("%+v", ef()) // using fmt results in 1 extra alloc... + _ = ef().Error() } } -func BenchmarkDesign_propRaisedPCCacheTraces64r8(b *testing.B) { - ef := makeRaisedPCCachePropChain(64, 8) +func BenchmarkDesign_errorPCCaches64r8(b *testing.B) { + ef := makePCCachePropChain(64, 8) for b.Loop() { - _ = fmt.Sprintf("%+v", ef()) // using fmt results in 1 extra alloc... + _ = ef().Error() } } -func BenchmarkDesign_propErrorfs256r16(b *testing.B) { +func BenchmarkDesign_errorErrorfs256r16(b *testing.B) { ef := makeErrorfPropChain(256, 16) for b.Loop() { - _ = ef() + _ = ef().Error() } } -func BenchmarkDesign_propRces256r16(b *testing.B) { +func BenchmarkDesign_errorRces256r16(b *testing.B) { ef := makeRcePropChain(256, 16) for b.Loop() { - _ = ef() + _ = ef().Error() } } -func BenchmarkDesign_propRaiseds256r16(b *testing.B) { +func BenchmarkDesign_errorRaiseds256r16(b *testing.B) { ef := makeRaisedPropChain(256, 16, errPropSentinel) for b.Loop() { - _ = ef() + _ = ef().Error() } } -func BenchmarkDesign_propRaisedPCCaches256r16(b *testing.B) { - ef := makeRaisedPCCachePropChain(256, 16) +func BenchmarkDesign_errorPCCaches256r16(b *testing.B) { + ef := makePCCachePropChain(256, 16) for b.Loop() { - _ = ef() + _ = ef().Error() + } +} + +// ==================================================================================== +// Rendering traces fmt.Errorf vs raised.Trace vs raised.TraceAt (aka PCCache) vs github.com/pkg/errors (aka Rce) +// +// To run those benchmarks use: +// go test ./pkg/raised -bench Design_trace -benchmem + +func BenchmarkDesign_traceErrorfs64r4(b *testing.B) { + ef := makeErrorfPropChain(64, 4) + for b.Loop() { + _ = fmt.Sprintf("%+v", ef()) // using fmt results in 1 extra alloc... + } +} + +func BenchmarkDesign_traceRces64r4(b *testing.B) { + ef := makeRcePropChain(64, 4) + for b.Loop() { + _ = fmt.Sprintf("%+v", ef()) + } +} + +func BenchmarkDesign_traceRaiseds64r4(b *testing.B) { + ef := makeRaisedPropChain(64, 4, errPropSentinel) + for b.Loop() { + _ = fmt.Sprintf("%+v", ef()) // using fmt results in 1 extra alloc... + } +} + +func BenchmarkDesign_tracePCCaches64r4(b *testing.B) { + ef := makePCCachePropChain(64, 4) + for b.Loop() { + _ = fmt.Sprintf("%+v", ef()) // using fmt results in 1 extra alloc... + } +} + +func BenchmarkDesign_traceErrorfs64r8(b *testing.B) { + ef := makeErrorfPropChain(64, 8) + for b.Loop() { + _ = fmt.Sprintf("%+v", ef()) // using fmt results in 1 extra alloc... + } +} + +func BenchmarkDesign_traceRces64r8(b *testing.B) { + ef := makeRcePropChain(64, 8) + for b.Loop() { + _ = fmt.Sprintf("%+v", ef()) // using fmt results in 1 extra alloc... + } +} + +func BenchmarkDesign_traceRaiseds64r8(b *testing.B) { + ef := makeRaisedPropChain(64, 8, errPropSentinel) + for b.Loop() { + _ = fmt.Sprintf("%+v", ef()) // using fmt results in 1 extra alloc... + } +} + +func BenchmarkDesign_tracePCCaches64r8(b *testing.B) { + ef := makePCCachePropChain(64, 8) + for b.Loop() { + _ = fmt.Sprintf("%+v", ef()) // using fmt results in 1 extra alloc... } } -func BenchmarkDesign_propErrorfTraces256r16(b *testing.B) { +func BenchmarkDesign_traceErrorfs256r16(b *testing.B) { ef := makeErrorfPropChain(256, 16) for b.Loop() { _ = fmt.Sprintf("%+v", ef()) // using fmt results in 1 extra alloc... } } -func BenchmarkDesign_propRceTraces256r16(b *testing.B) { +func BenchmarkDesign_traceRces256r16(b *testing.B) { ef := makeRcePropChain(256, 16) for b.Loop() { _ = fmt.Sprintf("%+v", ef()) // using fmt results in 1 extra alloc... } } -func BenchmarkDesign_propRaisedTraces256r16(b *testing.B) { +func BenchmarkDesign_traceRaiseds256r16(b *testing.B) { ef := makeRaisedPropChain(256, 16, errPropSentinel) for b.Loop() { _ = fmt.Sprintf("%+v", ef()) // using fmt results in 1 extra alloc... } } -func BenchmarkDesign_propRaisedPCCacheTraces256r16(b *testing.B) { - ef := makeRaisedPCCachePropChain(256, 16) +func BenchmarkDesign_tracePCCaches256r16(b *testing.B) { + ef := makePCCachePropChain(256, 16) for b.Loop() { _ = fmt.Sprintf("%+v", ef()) // using fmt results in 1 extra alloc... } @@ -433,7 +521,7 @@ func makeRcePropChain(strsz int, chnsz int) errfunc { return erf } -func makeRaisedPCCachePropChain(strsz int, chnsz int) errfunc { +func makePCCachePropChain(strsz int, chnsz int) errfunc { makeNextFunc := func(fls int, prev errfunc) errfunc { msg := fmt.Sprintf("[%d]: %s", fls, rndString(strsz)) switch fls % 4 { From 89ee7800f8085356a25c32231b4773eef2641434 Mon Sep 17 00:00:00 2001 From: amvtek Date: Fri, 26 Jun 2026 09:23:17 +0300 Subject: [PATCH 4/5] Move TraceAt to test section. --- pkg/raised/error.go | 24 ------------------------ pkg/raised/only_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/pkg/raised/error.go b/pkg/raised/error.go index 454fc34..5b07aec 100644 --- a/pkg/raised/error.go +++ b/pkg/raised/error.go @@ -72,30 +72,6 @@ func Trace(err error, msg string, args ...any) Error { return rv } -// TraceAt behaves like Trace but caches the call site PC using flk as a -// lookup key, avoiding repeated runtime.Callers calls on hot paths. -// flk must be a non-zero integer constant unique within the calling package. -// If args are provided, msg is used as a format string. -// Returns nil if err is nil. -func TraceAt[K ~int](flk K, err error, msg string, args ...any) Error { - if nil == err { - return nil - } - if len(args) > 0 { - msg = fmt.Sprintf(msg, args...) - } - rv, ok := err.(*errTrace) - if !ok { - rv = &errTrace{cause: err} - } else { - // rv not created here, hence we lock - rv.mut.Lock() - defer rv.mut.Unlock() - } - addCallerInfo(rv, flk, msg, 1) - return rv -} - // errTrace is an error used to track the propagation of a root error... // It records the propagation path as a fixed-size sequence of (PC, message) // pairs; middle entries are compressed out when traceSize is exceeded, diff --git a/pkg/raised/only_test.go b/pkg/raised/only_test.go index 63c2e8b..794a54f 100644 --- a/pkg/raised/only_test.go +++ b/pkg/raised/only_test.go @@ -57,3 +57,30 @@ func rndString(sz int) string { return rawB64.EncodeToString(buf) } + +// ==================================================================================== +// code below was part of package public interface + +// TraceAt behaves like Trace but caches the call site PC using flk as a +// lookup key, avoiding repeated runtime.Callers calls on hot paths. +// flk must be a non-zero integer constant unique within the calling package. +// If args are provided, msg is used as a format string. +// Returns nil if err is nil. +func TraceAt[K ~int](flk K, err error, msg string, args ...any) Error { + if nil == err { + return nil + } + if len(args) > 0 { + msg = fmt.Sprintf(msg, args...) + } + rv, ok := err.(*errTrace) + if !ok { + rv = &errTrace{cause: err} + } else { + // rv not created here, hence we lock + rv.mut.Lock() + defer rv.mut.Unlock() + } + addCallerInfo(rv, flk, msg, 1) + return rv +} From 460f472b504e0221f250b622ccaf753d35063155 Mon Sep 17 00:00:00 2001 From: amvtek Date: Fri, 26 Jun 2026 09:34:22 +0300 Subject: [PATCH 5/5] Provide Trace/Tracef functions. --- pkg/raised/error.go | 24 ++++++++++++++++++++++-- pkg/raised/error_test.go | 2 +- pkg/raised/keying.go | 2 +- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/pkg/raised/error.go b/pkg/raised/error.go index 5b07aec..8af2c78 100644 --- a/pkg/raised/error.go +++ b/pkg/raised/error.go @@ -49,11 +49,31 @@ type Error interface { } // Trace records a propagation step for err, attaching msg and the call site PC -// to the trace. If err is already a traced error it is extended in place; +// to the trace. If err is already a traced error it is extended in place, +// otherwise a new trace is created with err as its cause. +// Returns nil if err is nil. +func Trace(err error, msg string) Error { + if nil == err { + return nil + } + rv, ok := err.(*errTrace) + if !ok { + rv = &errTrace{cause: err} + } else { + // rv not created here, hence we lock + rv.mut.Lock() + defer rv.mut.Unlock() + } + addCallerInfo(rv, 0, msg, 1) + return rv +} + +// Tracef records a propagation step for err, attaching msg and the call site PC +// to the trace. If err is already a traced error it is extended in place, // otherwise a new trace is created with err as its cause. // If args are provided, msg is used as a format string. // Returns nil if err is nil. -func Trace(err error, msg string, args ...any) Error { +func Tracef(err error, msg string, args ...any) Error { if nil == err { return nil } diff --git a/pkg/raised/error_test.go b/pkg/raised/error_test.go index e4d4317..22f8346 100644 --- a/pkg/raised/error_test.go +++ b/pkg/raised/error_test.go @@ -127,7 +127,7 @@ func TestError_Show(t *testing.T) { } func TestError_FormatArgs(t *testing.T) { - err := Trace(errTraceSentinel, "value is %d", 42) + err := Tracef(errTraceSentinel, "value is %d", 42) if !strings.Contains(err.Error(), "value is 42") { t.Errorf("expected Error() to contain %q, got %q", "value is 42", err.Error()) diff --git a/pkg/raised/keying.go b/pkg/raised/keying.go index 2bd2430..8a68215 100644 --- a/pkg/raised/keying.go +++ b/pkg/raised/keying.go @@ -113,7 +113,7 @@ func NewSentinelErrorKeyer[T any](hf HashFunc, ukl UnstableKeyListener) (ErrorKe } h := hf() if h.Size() < keySize { - return nil, Trace(ErrInvalidHash, "insufficient hash size %d < %d", h.Size(), keySize) + return nil, Tracef(ErrInvalidHash, "insufficient hash size %d < %d", h.Size(), keySize) } sk := sentinelErrorKeyer[T]{hf: hf, tc: &keyCache{clock: ticks}, ukl: ukl}