Skip to content
This repository was archived by the owner on Dec 1, 2021. It is now read-only.

Commit e1ac100

Browse files
committed
reduce allocations when printing stack traces (#149)
1 parent 30136e2 commit e1ac100

File tree

2 files changed

+102
-15
lines changed

2 files changed

+102
-15
lines changed

bench_test.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func yesErrors(at, depth int) error {
2525

2626
// GlobalE is an exported global to store the result of benchmark results,
2727
// preventing the compiler from optimising the benchmark functions away.
28-
var GlobalE error
28+
var GlobalE interface{}
2929

3030
func BenchmarkErrors(b *testing.B) {
3131
type run struct {
@@ -61,3 +61,50 @@ func BenchmarkErrors(b *testing.B) {
6161
})
6262
}
6363
}
64+
65+
func BenchmarkStackFormatting(b *testing.B) {
66+
type run struct {
67+
stack int
68+
format string
69+
}
70+
runs := []run{
71+
{10, "%s"},
72+
{10, "%v"},
73+
{10, "%+v"},
74+
{30, "%s"},
75+
{30, "%v"},
76+
{30, "%+v"},
77+
{60, "%s"},
78+
{60, "%v"},
79+
{60, "%+v"},
80+
}
81+
82+
var stackStr string
83+
for _, r := range runs {
84+
name := fmt.Sprintf("%s-stack-%d", r.format, r.stack)
85+
b.Run(name, func(b *testing.B) {
86+
err := yesErrors(0, r.stack)
87+
b.ReportAllocs()
88+
b.ResetTimer()
89+
for i := 0; i < b.N; i++ {
90+
stackStr = fmt.Sprintf(r.format, err)
91+
}
92+
b.StopTimer()
93+
})
94+
}
95+
96+
for _, r := range runs {
97+
name := fmt.Sprintf("%s-stacktrace-%d", r.format, r.stack)
98+
b.Run(name, func(b *testing.B) {
99+
err := yesErrors(0, r.stack)
100+
st := err.(*fundamental).stack.StackTrace()
101+
b.ReportAllocs()
102+
b.ResetTimer()
103+
for i := 0; i < b.N; i++ {
104+
stackStr = fmt.Sprintf(r.format, st)
105+
}
106+
b.StopTimer()
107+
})
108+
}
109+
GlobalE = stackStr
110+
}

stack.go

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package errors
22

33
import (
4+
"bytes"
45
"fmt"
56
"io"
67
"path"
78
"runtime"
9+
"strconv"
810
"strings"
911
)
1012

@@ -50,30 +52,37 @@ func (f Frame) line() int {
5052
// GOPATH separated by \n\t (<funcname>\n\t<path>)
5153
// %+v equivalent to %+s:%d
5254
func (f Frame) Format(s fmt.State, verb rune) {
55+
f.format(s, s, verb)
56+
}
57+
58+
// format allows stack trace printing calls to be made with a bytes.Buffer.
59+
func (f Frame) format(w io.Writer, s fmt.State, verb rune) {
5360
switch verb {
5461
case 's':
5562
switch {
5663
case s.Flag('+'):
5764
pc := f.pc()
5865
fn := runtime.FuncForPC(pc)
5966
if fn == nil {
60-
io.WriteString(s, "unknown")
67+
io.WriteString(w, "unknown")
6168
} else {
6269
file, _ := fn.FileLine(pc)
63-
fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file)
70+
io.WriteString(w, fn.Name())
71+
io.WriteString(w, "\n\t")
72+
io.WriteString(w, file)
6473
}
6574
default:
66-
io.WriteString(s, path.Base(f.file()))
75+
io.WriteString(w, path.Base(f.file()))
6776
}
6877
case 'd':
69-
fmt.Fprintf(s, "%d", f.line())
78+
io.WriteString(w, strconv.Itoa(f.line()))
7079
case 'n':
7180
name := runtime.FuncForPC(f.pc()).Name()
72-
io.WriteString(s, funcname(name))
81+
io.WriteString(w, funcname(name))
7382
case 'v':
74-
f.Format(s, 's')
75-
io.WriteString(s, ":")
76-
f.Format(s, 'd')
83+
f.format(w, s, 's')
84+
io.WriteString(w, ":")
85+
f.format(w, s, 'd')
7786
}
7887
}
7988

@@ -89,23 +98,50 @@ type StackTrace []Frame
8998
//
9099
// %+v Prints filename, function, and line number for each Frame in the stack.
91100
func (st StackTrace) Format(s fmt.State, verb rune) {
101+
var b bytes.Buffer
92102
switch verb {
93103
case 'v':
94104
switch {
95105
case s.Flag('+'):
96-
for _, f := range st {
97-
fmt.Fprintf(s, "\n%+v", f)
106+
b.Grow(len(st) * stackMinLen)
107+
for _, fr := range st {
108+
b.WriteByte('\n')
109+
fr.format(&b, s, verb)
98110
}
99111
case s.Flag('#'):
100-
fmt.Fprintf(s, "%#v", []Frame(st))
112+
fmt.Fprintf(&b, "%#v", []Frame(st))
101113
default:
102-
fmt.Fprintf(s, "%v", []Frame(st))
114+
st.formatSlice(&b, s, verb)
103115
}
104116
case 's':
105-
fmt.Fprintf(s, "%s", []Frame(st))
117+
st.formatSlice(&b, s, verb)
118+
}
119+
io.Copy(s, &b)
120+
}
121+
122+
// formatSlice will format this StackTrace into the given buffer as a slice of
123+
// Frame, only valid when called with '%s' or '%v'.
124+
func (st StackTrace) formatSlice(b *bytes.Buffer, s fmt.State, verb rune) {
125+
b.WriteByte('[')
126+
if len(st) == 0 {
127+
b.WriteByte(']')
128+
return
129+
}
130+
131+
b.Grow(len(st) * (stackMinLen / 4))
132+
st[0].format(b, s, verb)
133+
for _, fr := range st[1:] {
134+
b.WriteByte(' ')
135+
fr.format(b, s, verb)
106136
}
137+
b.WriteByte(']')
107138
}
108139

140+
// stackMinLen is a best-guess at the minimum length of a stack trace. It
141+
// doesn't need to be exact, just give a good enough head start for the buffer
142+
// to avoid the expensive early growth.
143+
const stackMinLen = 96
144+
109145
// stack represents a stack of program counters.
110146
type stack []uintptr
111147

@@ -114,10 +150,14 @@ func (s *stack) Format(st fmt.State, verb rune) {
114150
case 'v':
115151
switch {
116152
case st.Flag('+'):
153+
var b bytes.Buffer
154+
b.Grow(len(*s) * stackMinLen)
117155
for _, pc := range *s {
118156
f := Frame(pc)
119-
fmt.Fprintf(st, "\n%+v", f)
157+
b.WriteByte('\n')
158+
f.format(&b, st, 'v')
120159
}
160+
io.Copy(st, &b)
121161
}
122162
}
123163
}

0 commit comments

Comments
 (0)