event: make logfmt output much faster
name old time/op new time/op delta
EventLogNoExporter-16 421ns ± 2% 429ns ± 4% ~ (p=0.059 n=8+9)
EventLogNoop-16 1.59µs ± 1% 1.63µs ± 6% +2.58% (p=0.041 n=8+10)
EventLogDiscard-16 15.1µs ± 2% 8.7µs ± 2% -42.61% (p=0.000 n=9+9)
EventLogSourceDiscard-16 29.6µs ± 3% 22.2µs ± 2% -25.26% (p=0.000 n=9+10)
EventLogfDiscard-16 18.4µs ± 3% 11.9µs ± 4% -35.47% (p=0.000 n=9+10)
EventTraceNoop-16 4.26µs ± 3% 4.32µs ± 3% +1.48% (p=0.046 n=9+10)
EventTraceDiscard-16 25.4µs ± 6% 16.2µs ± 4% -36.12% (p=0.000 n=10+10)
EventMetricNoop-16 2.96µs ± 4% 2.99µs ± 2% ~ (p=0.161 n=9+9)
EventMetricDiscard-16 49.3µs ± 3% 31.0µs ± 6% -37.09% (p=0.000 n=10+10)
Change-Id: Ia35a336811b2805bb387c2d8e164c8b9a39533df
Reviewed-on: https://go-review.googlesource.com/c/exp/+/329794
Trust: Ian Cottrell <iancottrell@google.com>
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/event/adapter/logfmt/logfmt.go b/event/adapter/logfmt/logfmt.go
index 2ca6e38..32eadbe 100644
--- a/event/adapter/logfmt/logfmt.go
+++ b/event/adapter/logfmt/logfmt.go
@@ -10,7 +10,7 @@
"fmt"
"io"
"strconv"
- "unicode"
+ "time"
"unicode/utf8"
"golang.org/x/exp/event"
@@ -48,7 +48,10 @@
func (p *Printer) Event(w io.Writer, ev *event.Event) {
p.needSep = false
if !ev.At.IsZero() {
- p.Label(w, event.Bytes("time", ev.At.AppendFormat(p.buf[:0], TimeFormat)))
+ p.separator(w)
+ io.WriteString(w, `time="`)
+ p.time(w, ev.At)
+ io.WriteString(w, `"`)
}
if !p.SuppressNamespace && ev.Namespace != "" {
@@ -56,7 +59,9 @@
}
if ev.Parent != 0 {
- p.Label(w, event.Bytes("parent", strconv.AppendUint(p.buf[:0], ev.Parent, 10)))
+ p.separator(w)
+ io.WriteString(w, `parent=`)
+ w.Write(strconv.AppendUint(p.buf[:0], ev.Parent, 10))
}
for _, l := range ev.Labels {
if l.Name == "" {
@@ -66,42 +71,39 @@
}
if ev.TraceID != 0 {
- p.Label(w, event.Uint64("trace", ev.TraceID))
+ p.separator(w)
+ io.WriteString(w, `trace=`)
+ w.Write(strconv.AppendUint(p.buf[:0], ev.TraceID, 10))
}
if ev.Kind == event.EndKind {
- p.Label(w, event.Value("end", nil))
+ p.separator(w)
+ io.WriteString(w, `end`)
}
io.WriteString(w, "\n")
}
+func (p *Printer) separator(w io.Writer) {
+ if p.needSep {
+ io.WriteString(w, " ")
+ }
+ p.needSep = true
+}
+
func (p *Printer) Label(w io.Writer, l event.Label) {
if l.Name == "" {
return
}
- if p.needSep {
- io.WriteString(w, " ")
- }
- p.needSep = true
+ p.separator(w)
p.Ident(w, l.Name)
if l.HasValue() {
io.WriteString(w, "=")
switch {
case l.IsString():
- s := l.String()
- if p.QuoteValues || stringNeedQuote(s) {
- p.quoteString(w, s)
- } else {
- io.WriteString(w, s)
- }
+ p.string(w, l.String())
case l.IsBytes():
- buf := l.Bytes()
- if p.QuoteValues || bytesNeedQuote(buf) {
- p.quoteBytes(w, buf)
- } else {
- w.Write(buf)
- }
+ p.bytes(w, l.Bytes())
case l.IsInt64():
w.Write(strconv.AppendInt(p.buf[:0], l.Int64(), 10))
case l.IsUint64():
@@ -115,17 +117,21 @@
io.WriteString(w, "false")
}
default:
- if p.w.Cap() == 0 {
- // we rely on the inliner to cause this to not allocate
- p.w = *bytes.NewBuffer(p.buf[:0])
- }
- fmt.Fprint(&p.w, l.Interface())
- b := p.w.Bytes()
- p.w.Reset()
- if p.QuoteValues || bytesNeedQuote(b) {
- p.quoteBytes(w, b)
- } else {
- w.Write(b)
+ v := l.Interface()
+ switch v := v.(type) {
+ case string:
+ p.string(w, v)
+ case fmt.Stringer:
+ p.string(w, v.String())
+ default:
+ if p.w.Cap() == 0 {
+ // we rely on the inliner to cause this to not allocate
+ p.w = *bytes.NewBuffer(p.buf[:0])
+ }
+ fmt.Fprint(&p.w, v)
+ b := p.w.Bytes()
+ p.w.Reset()
+ p.bytes(w, b)
}
}
}
@@ -139,6 +145,14 @@
p.quoteString(w, s)
}
+func (p *Printer) string(w io.Writer, s string) {
+ if p.QuoteValues || stringNeedQuote(s) {
+ p.quoteString(w, s)
+ } else {
+ io.WriteString(w, s)
+ }
+}
+
func (p *Printer) quoteString(w io.Writer, s string) {
io.WriteString(w, `"`)
written := 0
@@ -157,6 +171,14 @@
io.WriteString(w, `"`)
}
+func (p *Printer) bytes(w io.Writer, buf []byte) {
+ if p.QuoteValues || stringNeedQuote(string(buf)) {
+ p.quoteBytes(w, buf)
+ } else {
+ w.Write(buf)
+ }
+}
+
// Bytes writes a byte array in string form to the printer.
func (p *Printer) quoteBytes(w io.Writer, buf []byte) {
io.WriteString(w, `"`)
@@ -178,33 +200,44 @@
io.WriteString(w, `"`)
}
+// time writes a timstamp in the same format as
+func (p *Printer) time(w io.Writer, t time.Time) {
+ year, month, day := t.Date()
+ hour, minute, second := t.Clock()
+ p.padInt(w, int64(year), 4)
+ io.WriteString(w, `/`)
+ p.padInt(w, int64(month), 2)
+ io.WriteString(w, `/`)
+ p.padInt(w, int64(day), 2)
+ io.WriteString(w, ` `)
+ p.padInt(w, int64(hour), 2)
+ io.WriteString(w, `:`)
+ p.padInt(w, int64(minute), 2)
+ io.WriteString(w, `:`)
+ p.padInt(w, int64(second), 2)
+}
+
+func (p *Printer) padInt(w io.Writer, v int64, width int) {
+ b := strconv.AppendInt(p.buf[:0], int64(v), 10)
+ if len(b) < width {
+ io.WriteString(w, "0000"[:width-len(b)])
+ }
+ w.Write(b)
+}
+
func stringNeedQuote(s string) bool {
if len(s) == 0 {
return true
}
- for _, r := range s {
- if runeForcesQuote(r) {
+ for i := 0; i < len(s); i++ {
+ c := s[i]
+ if c >= utf8.RuneSelf || c == ' ' || c == '"' || c == '\n' || c == '\\' {
return true
}
}
return false
}
-func bytesNeedQuote(buf []byte) bool {
- for offset := 0; offset < len(buf); {
- r, size := utf8.DecodeRune(buf[offset:])
- offset += size
- if runeForcesQuote(r) {
- return true
- }
- }
- return false
-}
-
-func runeForcesQuote(r rune) bool {
- return !unicode.IsLetter(r) && !unicode.IsNumber(r)
-}
-
func quoteRune(r rune) string {
switch r {
case '"':
diff --git a/event/adapter/logfmt/logfmt_test.go b/event/adapter/logfmt/logfmt_test.go
index 45379ce..072caaa 100644
--- a/event/adapter/logfmt/logfmt_test.go
+++ b/event/adapter/logfmt/logfmt_test.go
@@ -37,7 +37,7 @@
}, {
name: "namespace",
event: event.Event{Namespace: "golang.org/x/exp/event"},
- expect: `in="golang.org/x/exp/event"`,
+ expect: `in=golang.org/x/exp/event`,
}, {
name: "at",
event: event.Event{At: at},
@@ -177,7 +177,7 @@
Namespace: "golang.org/x/exp/event",
Labels: []event.Label{event.String("msg", "some text")},
},
- before: `in="golang.org/x/exp/event" msg="some text"`,
+ before: `in=golang.org/x/exp/event msg="some text"`,
after: `msg="some text"`,
}} {
t.Run(test.name, func(t *testing.T) {