Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use a union struct for a Field to reduce allocations #13

Merged
merged 5 commits into from
Apr 6, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 65 additions & 85 deletions field.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,153 +20,133 @@

package zap

import "time"
import (
"fmt"
"math"
"time"
)

// A Field is a deferred marshaling operation used to add a key-value pair to
// a logger's context. Keys and values are appropriately escaped for the current
// encoding scheme (e.g., JSON).
type Field interface {
addTo(encoder) error
}
type fieldType int

// A FieldCloser closes a nested field.
type FieldCloser interface {
CloseField()
}

const (
unknownType fieldType = iota
boolType
floatType
intType
int64Type
stringType
marshalerType
)

// A Field is a deferred marshaling operation used to add a key-value pair to
// a logger's context. Keys and values are appropriately escaped for the current
// encoding scheme (e.g., JSON).
type Field struct {
key string
fieldType fieldType
ival int64
str string
obj Marshaler
}

// Bool constructs a Field with the given key and value.
func Bool(key string, val bool) Field {
return boolField{key, val}
return Field{key: key, fieldType: boolType, ival: 1}
}

// Float64 constructs a Field with the given key and value. The floating-point
// value is encoded using strconv.FormatFloat's 'g' option (exponential notation
// for large exponents, grade-school notation otherwise).
func Float64(key string, val float64) Field {
return float64Field{key, val}
return Field{key: key, fieldType: floatType, ival: int64(math.Float64bits(val))}
}

// Int constructs a Field with the given key and value.
func Int(key string, val int) Field {
return int64Field{key, int64(val)}
return Field{key: key, fieldType: intType, ival: int64(val)}
}

// Int64 constructs a Field with the given key and value.
func Int64(key string, val int64) Field {
return int64Field{key, val}
return Field{key: key, fieldType: int64Type, ival: val}
}

// String constructs a Field with the given key and value.
func String(key string, val string) Field {
return stringField{key, val}
return Field{key: key, fieldType: stringType, str: val}
}

// Time constructs a Field with the given key and value. It represents a
// time.Time as nanoseconds since epoch.
func Time(key string, val time.Time) Field {
return timeField{key, val}
return Int64(key, val.UnixNano())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do this, we should remove all the time-related code from encoders. I was on the fence about whether we'd want this to be encoding-specific or not.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I wasn't sure about this either, let's remove for now. If we really want encoder to control time, then we can serialize to unixnano and deserialize before calling the encoder method.

}

// Err constructs a Field that stores err.Error() under the key "error".
func Err(err error) Field {
return stringField{"error", err.Error()}
return String("error", err.Error())
}

// Duration constructs a Field with the given key and value. It represents
// durations as an integer number of nanoseconds.
func Duration(key string, val time.Duration) Field {
return int64Field{key, int64(val)}
return Int64(key, int64(val))
}

// Object constructs a field with the given key and zap.Marshaler. It provides a
// flexible, but still type-safe and efficient, way to add user-defined types to
// the logging context.
func Object(key string, val Marshaler) Field {
return marshalerField{key, val}
return Field{key: key, fieldType: marshalerType, obj: val}
}

// Nest takes a key and a variadic number of Fields and creates a nested
// namespace.
func Nest(key string, fields ...Field) Field {
return nestedField{key, fields}
}

type boolField struct {
key string
val bool
}

func (b boolField) addTo(enc encoder) error {
enc.AddBool(b.key, b.val)
return nil
}

type float64Field struct {
key string
val float64
}

func (f float64Field) addTo(enc encoder) error {
enc.AddFloat64(f.key, f.val)
return nil
}

type int64Field struct {
key string
val int64
}

func (i int64Field) addTo(enc encoder) error {
enc.AddInt64(i.key, i.val)
return nil
}

type stringField struct {
key string
val string
}

func (s stringField) addTo(enc encoder) error {
enc.AddString(s.key, s.val)
return nil
}

type timeField struct {
key string
val time.Time
}

func (t timeField) addTo(enc encoder) error {
enc.AddTime(t.key, t.val)
return Field{key: key, fieldType: marshalerType, obj: multiFields(fields)}
}

func (f Field) addTo(kv KeyValue) error {
switch f.fieldType {
case boolType:
kv.AddBool(f.key, f.ival == 1)
case floatType:
kv.AddFloat64(f.key, math.Float64frombits(uint64(f.ival)))
case intType:
kv.AddInt(f.key, int(f.ival))
case int64Type:
kv.AddInt64(f.key, f.ival)
case stringType:
kv.AddString(f.key, f.str)
case marshalerType:
closer := kv.Nest(f.key)
err := f.obj.MarshalLog(kv)
closer.CloseField()
return err
default:
panic(fmt.Sprintf("unknown field type found: %v", f))
}
return nil
}

type marshalerField struct {
key string
val Marshaler
}

func (m marshalerField) addTo(enc encoder) error {
closer := enc.Nest(m.key)
err := m.val.MarshalLog(enc)
closer.CloseField()
return err
}
type multiFields []Field

type nestedField struct {
key string
vals []Field
func (fs multiFields) MarshalLog(kv KeyValue) error {
return addFields(kv, []Field(fs))
}

func (n nestedField) addTo(enc encoder) error {
closer := enc.Nest(n.key)
func addFields(kv KeyValue, fields []Field) error {
var errs multiError
for _, f := range n.vals {
if err := f.addTo(enc); err != nil {
for _, f := range fields {
if err := f.addTo(kv); err != nil {
errs = append(errs, err)
}
}
closer.CloseField()
if len(errs) > 0 {
return errs
}
Expand Down
52 changes: 40 additions & 12 deletions field_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package zap

import (
"errors"
"sync"
"testing"
"time"

Expand All @@ -47,57 +48,84 @@ func assertFieldJSON(t testing.TB, expected string, field Field) {
"Unexpected JSON output after applying field %+v.", field)
}

func assertCanBeReused(t testing.TB, field Field) {
var wg sync.WaitGroup

for i := 0; i < 100; i++ {
enc := newJSONEncoder()
defer enc.Free()

// Ensure using the field in multiple encoders in separate goroutines
// does not cause any races or panics.
wg.Add(1)
go func() {
defer wg.Done()
assert.NotPanics(t, func() {
field.addTo(enc)
}, "Reusing a field should not cause issues")
}()
}

wg.Wait()
}

func TestBoolField(t *testing.T) {
assertFieldJSON(t, `"foo":true`, Bool("foo", true))
assertCanBeReused(t, Bool("foo", true))
}

func TestFloat64Field(t *testing.T) {
assertFieldJSON(t, `"foo":1.314`, Float64("foo", 1.314))
assertCanBeReused(t, Float64("foo", 1.314))
}

func TestIntField(t *testing.T) {
assertFieldJSON(t, `"foo":1`, Int("foo", 1))
assertCanBeReused(t, Int("foo", 1))
}

func TestInt64Field(t *testing.T) {
assertFieldJSON(t, `"foo":1`, Int64("foo", int64(1)))
assertCanBeReused(t, Int64("foo", int64(1)))
}

func TestStringField(t *testing.T) {
assertFieldJSON(t, `"foo":"bar"`, String("foo", "bar"))
assertCanBeReused(t, String("foo", "bar"))
}

func TestTimeField(t *testing.T) {
assertFieldJSON(t, `"foo":0`, Time("foo", time.Unix(0, 0)))
assertCanBeReused(t, Time("foo", time.Unix(0, 0)))
}

func TestErrField(t *testing.T) {
assertFieldJSON(t, `"error":"fail"`, Err(errors.New("fail")))
assertCanBeReused(t, Err(errors.New("fail")))
}

func TestDurationField(t *testing.T) {
assertFieldJSON(t, `"foo":1`, Duration("foo", time.Nanosecond))
assertCanBeReused(t, Duration("foo", time.Nanosecond))
}

func TestObjectField(t *testing.T) {
assertFieldJSON(t, `"foo":{"name":"phil"}`, Object("foo", fakeUser{"phil"}))
// Marshaling the user failed, so we expect an empty object.
assertFieldJSON(t, `"foo":{}`, Object("foo", fakeUser{"fail"}))

assertFieldJSON(t, `"foo":{"name":"phil"}`, Object("foo", fakeUser{"phil"}))
assertCanBeReused(t, Object("foo", fakeUser{"phil"}))
}

func TestNestField(t *testing.T) {
assertFieldJSON(
t,
`"foo":{"name":"phil","age":42}`,
Nest("foo",
String("name", "phil"),
Int("age", 42),
),
assertFieldJSON(t, `"foo":{"name":"phil","age":42}`,
Nest("foo", String("name", "phil"), Int("age", 42)),
)
assertFieldJSON(
t,
// Marshaling the user failed, so we expect an empty object.
`"foo":{"user":{}}`,
// Marshaling the user failed, so we expect an empty object.
assertFieldJSON(t, `"foo":{"user":{}}`,
Nest("foo", Object("user", fakeUser{"fail"})),
)

nest := Nest("foo", String("name", "phil"), Int("age", 42))
assertCanBeReused(t, nest)
}
17 changes: 1 addition & 16 deletions json_encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,6 @@ func (enc *jsonEncoder) AddInt64(key string, val int64) {
enc.bytes = strconv.AppendInt(enc.bytes, val, 10)
}

// AddTime adds a string key and time.Time value to the encoder's fields. The key
// is JSON-escaped, and the time is encoded as nanoseconds since epoch.
func (enc *jsonEncoder) AddTime(key string, val time.Time) {
enc.AddInt64(key, val.UnixNano())
}

// AddFloat64 adds a string key and float64 value to the encoder's fields. The
// key is JSON-escaped, and the floating-point value is encoded using
// strconv.FormatFloat's 'g' option (exponential notation for large exponents,
Expand Down Expand Up @@ -136,16 +130,7 @@ func (enc *jsonEncoder) Clone() encoder {

// AddFields applies the passed fields to this encoder.
func (enc *jsonEncoder) AddFields(fields []Field) error {
var errs multiError
for _, f := range fields {
if err := f.addTo(enc); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errs
}
return nil
return addFields(enc, fields)
}

// WriteMessage writes a complete log message to the supplied writer, including
Expand Down
Loading