Skip to content

Commit e413b2a

Browse files
Bump golang version, add io.StringWriter and improve performance (#33)
* feat: add support for io.StringWriter The interface was added to the io package in go 1.12. Related to: - https://go.dev/doc/go1.12#iopkgio - golang/go@33d531d * chore: support go 1.25 and up Following the same policy as golang. See https://go.dev/doc/devel/release#policy * chore: ran go fmt * chore: benchmark * refactor: invoke middleware func once This removes allocation for each Write call * refactor: reduce allocation by defining types A type is now created for each possible interface combination. Allowing Wrap to so a single allocation. * chore: combine the if statements * fix: capture metrics for WriteString * chore: add httpFlushError support --------- Co-authored-by: Felix Geisendörfer <felix@felixge.de>
1 parent 9a9390b commit e413b2a

13 files changed

Lines changed: 42759 additions & 10635 deletions

.github/workflows/main.yaml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ jobs:
55
strategy:
66
matrix:
77
go:
8-
- 1.18.x
9-
- 1.17.x
10-
- 1.16.x
8+
- 1.26.x
9+
- 1.25.x
1110
os:
1211
- ubuntu-latest
1312
name: ${{ matrix.os }}/go${{ matrix.go }}

capture_metrics.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ func (m *Metrics) CaptureMetrics(w http.ResponseWriter, fn func(http.ResponseWri
6969
}
7070
},
7171

72+
WriteString: func(next WriteStringFunc) WriteStringFunc {
73+
return func(s string) (int, error) {
74+
n, err := next(s)
75+
76+
m.Written += int64(n)
77+
headerWritten = true
78+
return n, err
79+
}
80+
},
81+
7282
ReadFrom: func(next ReadFromFunc) ReadFromFunc {
7383
return func(src io.Reader) (int64, error) {
7484
n, err := next(src)
@@ -101,3 +111,11 @@ type deadliner interface {
101111
type fullDuplexEnabler interface {
102112
EnableFullDuplex() error
103113
}
114+
115+
// httpFlushError defines a method introduced in go 1.20. The standard
116+
// library seems not to provide an interface we can import, hence its definition
117+
// here.
118+
// See https://github.com/golang/go/blob/go1.20/src/net/http/responsecontroller.go#L50
119+
type httpFlushError interface {
120+
FlushError() error
121+
}

bench_test.go renamed to capture_metrics_bench_test.go

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,6 @@ func BenchmarkCaptureMetricsTwice(b *testing.B) {
1818
benchmark(b, 2)
1919
}
2020

21-
func BenchmarkWrap(b *testing.B) {
22-
b.StopTimer()
23-
doneCh := make(chan struct{}, 1)
24-
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
25-
b.StartTimer()
26-
for i := 0; i < b.N; i++ {
27-
Wrap(w, Hooks{})
28-
}
29-
doneCh <- struct{}{}
30-
})
31-
s := httptest.NewServer(h)
32-
defer s.Close()
33-
if _, err := http.Get(s.URL); err != nil {
34-
b.Fatal(err)
35-
}
36-
<-doneCh
37-
}
38-
3921
func benchmark(b *testing.B, wrappings int) {
4022
dummyH := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
4123
h := dummyH

capture_metrics_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ func TestCaptureMetrics(t *testing.T) {
6161
WantWritten: 17,
6262
WantCode: http.StatusOK,
6363
},
64+
{
65+
Name: "string writer",
66+
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
67+
_, _ = io.WriteString(w, "write string")
68+
}),
69+
WantWritten: int64(len("write string")),
70+
WantCode: http.StatusOK,
71+
},
6472
{
6573
Name: "empty panic",
6674
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -104,7 +112,7 @@ func TestCaptureMetrics(t *testing.T) {
104112
}
105113
if err == nil {
106114
defer res.Body.Close()
107-
}
115+
}
108116
m := <-ch
109117
if m.Code != test.WantCode {
110118
t.Errorf("test %d: got=%d want=%d", i, m.Code, test.WantCode)

codegen/main.go

Lines changed: 133 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,25 @@ type Build struct {
1515
}
1616

1717
func (b *Build) MustBuild() {
18-
prefix := "wrap_generated_"
19-
b.Implementation().MustWriteFile(prefix + b.Suffix + ".go")
20-
b.Tests().MustWriteFile(prefix + b.Suffix + "_test.go")
18+
prefix := "wrap_generated"
19+
if b.Suffix != "" {
20+
prefix += "_" + b.Suffix
21+
}
22+
23+
b.Implementation().MustWriteFile(prefix + ".go")
24+
b.Tests().MustWriteFile(prefix + "_test.go")
2125
}
2226

2327
func (b *Build) writeHeader(g *Generator) {
24-
g.Printf(`
25-
// +build %s
26-
// Code generated by "httpsnoop/codegen"; DO NOT EDIT.
28+
if b.Tags != "" {
29+
g.Printf(`// +build %s
30+
`, b.Tags)
31+
}
32+
g.buf.WriteString(`// Code generated by "httpsnoop/codegen"; DO NOT EDIT.
2733
2834
package httpsnoop
2935
30-
`, b.Tags)
36+
`)
3137
}
3238

3339
func (b *Build) Implementation() *Generator {
@@ -89,64 +95,110 @@ type Hooks struct {
8995
// hooks can be used.
9096
`, strings.Join(docList, "\n"))
9197
g.Printf("func Wrap(w http.ResponseWriter, hooks Hooks) http.ResponseWriter {\n")
92-
g.Printf("rw := &rw{w: w, h: hooks}\n")
98+
g.Printf("state := &rwState{w: w}\n")
99+
100+
// Precompute hook chains once per Wrap call and
101+
// build a uint8 combo index so the switch compiles to a jump table.
102+
g.Printf("var combo uint16\n")
103+
for _, fn := range ifaces[0].Funcs {
104+
g.Printf("if hooks.%s != nil {\n", fn.Name)
105+
g.Printf("state.%s = hooks.%s(w.%s)\n", fieldName(fn.Name), fn.Name, fn.Name)
106+
g.Printf("}\n")
107+
}
108+
93109
for i, iface := range subIfaces {
94-
g.Printf("_, i%d := w.(%s)\n", i, iface.Name)
110+
g.Printf("if t%[1]d, i%[1]d := w.(%s); i%[1]d {\n", i, iface.Name)
111+
bit := len(subIfaces) - i - 1
112+
g.Printf("combo |= 1<<%d\n", bit)
113+
for _, fn := range iface.Funcs {
114+
g.Printf("if hooks.%s != nil {\n", fn.Name)
115+
g.Printf("state.%s = hooks.%s(t%d.%s)\n", fieldName(fn.Name), fn.Name, i, fn.Name)
116+
g.Printf("}\n")
117+
}
118+
g.Printf("}\n")
95119
}
96-
g.Printf("switch {\n")
120+
121+
g.Printf("switch combo {\n")
97122
combinations := 1 << uint(len(subIfaces))
98-
for i := 0; i < combinations; i++ {
99-
conditions := make([]string, len(subIfaces))
100-
fields := make([]string, 0, len(subIfaces))
101-
fields = append(fields, "Unwrapper", "http.ResponseWriter")
102-
for j, iface := range subIfaces {
103-
ok := i&(1<<uint(len(subIfaces)-j-1)) > 0
104-
if !ok {
105-
conditions[j] = "!"
106-
} else {
107-
fields = append(fields, iface.Name)
108-
}
109-
conditions[j] += fmt.Sprintf("i%d", j)
110-
}
111-
values := make([]string, len(fields))
112-
for i := range fields {
113-
values[i] = "rw"
114-
}
115-
g.Printf("// combination %d/%d\n", i+1, combinations)
116-
g.Printf("case %s:\n", strings.Join(conditions, "&&"))
117-
fieldsS, valuesS := strings.Join(fields, "\n"), strings.Join(values, ",")
118-
g.Printf("return struct{\n%s\n}{%s}\n", fieldsS, valuesS)
123+
for c := 0; c < combinations; c++ {
124+
g.Printf("case %d: return (*rw%d)(state)\n", c, c)
119125
}
120126
g.Printf("}\n")
121127
g.Printf("panic(\"unreachable\")")
122-
g.Printf("}\n")
123-
124-
// rw struct
125-
g.Printf(`
126-
type rw struct {
127-
w http.ResponseWriter
128-
h Hooks
129-
}
128+
g.Printf("}\n\n")
130129

131-
func (w *rw) Unwrap() http.ResponseWriter {
132-
return w.w
133-
}
130+
// rwState holds the underlying writer plus the precomputed hooks.
131+
// All variant types are type-definitions over rwState, so a single *rwState
132+
// allocation can be reinterpreted as any variant via pointer conversion.
133+
g.Printf("type rwState struct {\n")
134+
g.Printf("w http.ResponseWriter\n")
135+
for _, iface := range ifaces {
136+
for _, fn := range iface.Funcs {
137+
g.Printf("%s %s\n", fieldName(fn.Name), fn.Type())
138+
}
139+
}
140+
g.Printf("}\n\n")
134141

135-
`)
142+
// do<Name> helpers on *rwState
143+
// These actual dispatch logic, defined once and called by the variant types.
136144
for _, iface := range ifaces {
137145
for _, fn := range iface.Funcs {
138-
g.Printf("func (w *rw) %s(%s) (%s) {\n", fn.Name, fn.Args, fn.Returns)
139-
g.Printf("f := w.w.(%s).%s\n", iface.Name, fn.Name)
140-
g.Printf("if w.h.%s != nil {\n", fn.Name)
141-
g.Printf("f = w.h.%s(f)\n", fn.Name)
142-
g.Printf("}\n")
146+
g.Printf("func (r *rwState) do%s(%s) (%s) {\n", fn.Name, fn.Args, fn.Returns)
147+
g.Printf("if r.%s != nil {\n", fieldName(fn.Name))
143148
if fn.Returns != "" {
144-
g.Printf("return ")
149+
g.Printf("return r.%s(%s)\n", fieldName(fn.Name), fn.Args.Names())
150+
} else {
151+
g.Printf("r.%s(%s)\n", fieldName(fn.Name), fn.Args.Names())
152+
g.Printf("return\n")
145153
}
146-
g.Printf("f(%s)\n", fn.Args.Names())
147154
g.Printf("}\n")
148-
g.Printf("\n")
155+
156+
receiver := "r.w"
157+
if iface.Name != "http.ResponseWriter" {
158+
receiver = fmt.Sprintf("r.w.(%s)", iface.Name)
159+
}
160+
if fn.Returns != "" {
161+
g.Printf("return %s.%s(%s)\n", receiver, fn.Name, fn.Args.Names())
162+
} else {
163+
g.Printf("%s.%s(%s)\n", receiver, fn.Name, fn.Args.Names())
164+
}
165+
g.Printf("}\n\n")
166+
}
167+
}
168+
169+
// Variant types, each is a type with the same memory layout as rwState,
170+
// but exposing exactly the method set required by its combination of interfaces.
171+
// This allows (*rwN)(state) to be a zero-cost pointer conversion.
172+
emitVariantMethod := func(c int, fn *InterfaceFunc) {
173+
g.Printf("func (w *rw%d) %s(%s) (%s) {\n", c, fn.Name, fn.Args, fn.Returns)
174+
if fn.Returns != "" {
175+
g.Printf("return (*rwState)(w).do%s(%s)\n", fn.Name, fn.Args.Names())
176+
} else {
177+
g.Printf("(*rwState)(w).do%s(%s)\n", fn.Name, fn.Args.Names())
149178
}
179+
g.Printf("}\n")
180+
}
181+
for c := 0; c < combinations; c++ {
182+
supported := []string{"http.ResponseWriter"}
183+
for j, iface := range subIfaces {
184+
if c&(1<<uint(len(subIfaces)-j-1)) > 0 {
185+
supported = append(supported, iface.Name)
186+
}
187+
}
188+
g.Printf("// combination %d/%d: %s\n", c+1, combinations, strings.Join(supported, ", "))
189+
g.Printf("type rw%d rwState\n", c)
190+
g.Printf("func (w *rw%d) Unwrap() http.ResponseWriter { return w.w }\n", c)
191+
for _, fn := range ifaces[0].Funcs {
192+
emitVariantMethod(c, fn)
193+
}
194+
for j, iface := range subIfaces {
195+
if c&(1<<uint(len(subIfaces)-j-1)) > 0 {
196+
for _, fn := range iface.Funcs {
197+
emitVariantMethod(c, fn)
198+
}
199+
}
200+
}
201+
g.Printf("\n")
150202
}
151203
g.Printf(`
152204
type Unwrapper interface {
@@ -159,9 +211,8 @@ func Unwrap(w http.ResponseWriter) http.ResponseWriter {
159211
if rw, ok := w.(Unwrapper); ok {
160212
// recurse until rw.Unwrap() returns a non-Unwrapper
161213
return Unwrap(rw.Unwrap())
162-
} else {
163-
return w
164214
}
215+
return w
165216
}
166217
`)
167218
return &g
@@ -263,12 +314,19 @@ func (fn *InterfaceFunc) Type() string {
263314
return fn.Name + "Func"
264315
}
265316

317+
func fieldName(s string) string {
318+
if s == "" {
319+
return s
320+
}
321+
return strings.ToLower(s[:1]) + s[1:]
322+
}
323+
266324
type Generator struct {
267325
buf bytes.Buffer
268326
}
269327

270328
func (g *Generator) Printf(s string, args ...interface{}) {
271-
fmt.Fprintf(&g.buf, s, args...)
329+
_, _ = fmt.Fprintf(&g.buf, s, args...)
272330
}
273331

274332
func (g *Generator) WriteFile(name string) error {
@@ -311,6 +369,12 @@ func main() {
311369
{"Flush", nil, ""},
312370
},
313371
},
372+
{
373+
Name: "httpFlushError", // Introduced in Go 1.20.
374+
Funcs: []*InterfaceFunc{
375+
{"FlushError", nil, "error"},
376+
},
377+
},
314378
{
315379
Name: "http.CloseNotifier",
316380
Funcs: []*InterfaceFunc{
@@ -342,33 +406,32 @@ func main() {
342406
{"EnableFullDuplex", nil, "error"},
343407
},
344408
},
409+
{
410+
Name: "http.Pusher",
411+
Funcs: []*InterfaceFunc{
412+
{"Push", FuncArgs{
413+
{"target", "string"},
414+
{"opts", "*http.PushOptions"},
415+
}, "error"},
416+
},
417+
}, {
418+
Name: "io.StringWriter",
419+
Funcs: []*InterfaceFunc{
420+
{"WriteString", FuncArgs{{"s", "string"}}, "int, error"},
421+
},
422+
},
345423
}
346424
builds := []Build{
347425
{
348-
Suffix: "lt_1.8",
349-
Tags: "!go1.8",
350426
Interfaces: ifaces,
351427
},
352-
{
353-
Suffix: "gteq_1.8",
354-
Tags: "go1.8",
355-
Interfaces: append(ifaces, &Interface{
356-
Name: "http.Pusher",
357-
Funcs: []*InterfaceFunc{
358-
{"Push", FuncArgs{
359-
{"target", "string"},
360-
{"opts", "*http.PushOptions"},
361-
}, "error"},
362-
},
363-
}),
364-
},
365428
}
366429
for _, build := range builds {
367430
build.MustBuild()
368431
}
369432
}
370433

371434
func fatalf(s string, args ...interface{}) {
372-
fmt.Fprintf(os.Stderr, s+"\n", args...)
435+
_, _ = fmt.Fprintf(os.Stderr, s+"\n", args...)
373436
os.Exit(1)
374437
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module github.com/felixge/httpsnoop
22

3-
go 1.13
3+
go 1.25

0 commit comments

Comments
 (0)