Skip to content

Commit 0c849bc

Browse files
authored
Merge pull request #9 from pingcap/reduce-alloc-print-stack
reduce allocations when printing stack traces (pkg#149)
2 parents e41050e + dc8ffe7 commit 0c849bc

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

@@ -72,30 +74,37 @@ func (f Frame) line() int {
7274
// GOPATH separated by \n\t (<funcname>\n\t<path>)
7375
// %+v equivalent to %+s:%d
7476
func (f Frame) Format(s fmt.State, verb rune) {
77+
f.format(s, s, verb)
78+
}
79+
80+
// format allows stack trace printing calls to be made with a bytes.Buffer.
81+
func (f Frame) format(w io.Writer, s fmt.State, verb rune) {
7582
switch verb {
7683
case 's':
7784
switch {
7885
case s.Flag('+'):
7986
pc := f.pc()
8087
fn := runtime.FuncForPC(pc)
8188
if fn == nil {
82-
io.WriteString(s, "unknown")
89+
io.WriteString(w, "unknown")
8390
} else {
8491
file, _ := fn.FileLine(pc)
85-
fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file)
92+
io.WriteString(w, fn.Name())
93+
io.WriteString(w, "\n\t")
94+
io.WriteString(w, file)
8695
}
8796
default:
88-
io.WriteString(s, path.Base(f.file()))
97+
io.WriteString(w, path.Base(f.file()))
8998
}
9099
case 'd':
91-
fmt.Fprintf(s, "%d", f.line())
100+
io.WriteString(w, strconv.Itoa(f.line()))
92101
case 'n':
93102
name := runtime.FuncForPC(f.pc()).Name()
94-
io.WriteString(s, funcname(name))
103+
io.WriteString(w, funcname(name))
95104
case 'v':
96-
f.Format(s, 's')
97-
io.WriteString(s, ":")
98-
f.Format(s, 'd')
105+
f.format(w, s, 's')
106+
io.WriteString(w, ":")
107+
f.format(w, s, 'd')
99108
}
100109
}
101110

@@ -111,23 +120,50 @@ type StackTrace []Frame
111120
//
112121
// %+v Prints filename, function, and line number for each Frame in the stack.
113122
func (st StackTrace) Format(s fmt.State, verb rune) {
123+
var b bytes.Buffer
114124
switch verb {
115125
case 'v':
116126
switch {
117127
case s.Flag('+'):
118-
for _, f := range st {
119-
fmt.Fprintf(s, "\n%+v", f)
128+
b.Grow(len(st) * stackMinLen)
129+
for _, fr := range st {
130+
b.WriteByte('\n')
131+
fr.format(&b, s, verb)
120132
}
121133
case s.Flag('#'):
122-
fmt.Fprintf(s, "%#v", []Frame(st))
134+
fmt.Fprintf(&b, "%#v", []Frame(st))
123135
default:
124-
fmt.Fprintf(s, "%v", []Frame(st))
136+
st.formatSlice(&b, s, verb)
125137
}
126138
case 's':
127-
fmt.Fprintf(s, "%s", []Frame(st))
139+
st.formatSlice(&b, s, verb)
140+
}
141+
io.Copy(s, &b)
142+
}
143+
144+
// formatSlice will format this StackTrace into the given buffer as a slice of
145+
// Frame, only valid when called with '%s' or '%v'.
146+
func (st StackTrace) formatSlice(b *bytes.Buffer, s fmt.State, verb rune) {
147+
b.WriteByte('[')
148+
if len(st) == 0 {
149+
b.WriteByte(']')
150+
return
151+
}
152+
153+
b.Grow(len(st) * (stackMinLen / 4))
154+
st[0].format(b, s, verb)
155+
for _, fr := range st[1:] {
156+
b.WriteByte(' ')
157+
fr.format(b, s, verb)
128158
}
159+
b.WriteByte(']')
129160
}
130161

162+
// stackMinLen is a best-guess at the minimum length of a stack trace. It
163+
// doesn't need to be exact, just give a good enough head start for the buffer
164+
// to avoid the expensive early growth.
165+
const stackMinLen = 96
166+
131167
// stack represents a stack of program counters.
132168
type stack []uintptr
133169

@@ -136,10 +172,14 @@ func (s *stack) Format(st fmt.State, verb rune) {
136172
case 'v':
137173
switch {
138174
case st.Flag('+'):
175+
var b bytes.Buffer
176+
b.Grow(len(*s) * stackMinLen)
139177
for _, pc := range *s {
140178
f := Frame(pc)
141-
fmt.Fprintf(st, "\n%+v", f)
179+
b.WriteByte('\n')
180+
f.format(&b, st, 'v')
142181
}
182+
io.Copy(st, &b)
143183
}
144184
}
145185
}

0 commit comments

Comments
 (0)