Skip to content

Commit

Permalink
chore: only compute error context when needed
Browse files Browse the repository at this point in the history
JSONry error messages have helpful context about where the error
occurred. This is now only computed if there was an error, resulting in
performance improvements when there are no errors.
  • Loading branch information
blgm committed Feb 27, 2021
1 parent 07abfda commit c59764d
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 178 deletions.
6 changes: 3 additions & 3 deletions PERFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ relative performance between the two. In the benchamark test:

- Unmarshal
- JSONry takes 2.5 times as long as `encoding/json`
- JSONry allocates 6 times as much memory as `enconding/json`
- JSONry allocates 5 times as much memory as `enconding/json`

- Marshal
- JSONry takes 11 times as long as `encoding/json`
- JSONry allocates 21 times as much memory as `enconding/json`
- JSONry takes 10 times as long as `encoding/json`
- JSONry allocates 17 times as much memory as `enconding/json`

- Version: `go version go1.14.3 darwin/amd64`
- Command: `go test -run none -bench . -benchmem -benchtime 10s`
117 changes: 98 additions & 19 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,54 +5,60 @@ import (
"fmt"
"reflect"

"code.cloudfoundry.org/jsonry/internal/context"
"code.cloudfoundry.org/jsonry/internal/errorcontext"
)

type unsupportedType struct {
context context.Context
typ reflect.Type
typ reflect.Type
}

func newUnsupportedTypeError(ctx context.Context, t reflect.Type) error {
func newUnsupportedTypeError(t reflect.Type) error {
return &unsupportedType{
context: ctx,
typ: t,
typ: t,
}
}

func (u unsupportedType) Error() string {
return fmt.Sprintf(`unsupported type "%s" at %s`, u.typ, u.context)
return u.message(errorcontext.ErrorContext{})
}

func (u unsupportedType) message(ctx errorcontext.ErrorContext) string {
return fmt.Sprintf(`unsupported type "%s" at %s`, u.typ, ctx)
}

type unsupportedKeyType struct {
context context.Context
typ reflect.Type
typ reflect.Type
}

func newUnsupportedKeyTypeError(ctx context.Context, t reflect.Type) error {
func newUnsupportedKeyTypeError(t reflect.Type) error {
return &unsupportedKeyType{
context: ctx,
typ: t,
typ: t,
}
}

func (u unsupportedKeyType) Error() string {
return fmt.Sprintf(`maps must only have string keys for "%s" at %s`, u.typ, u.context)
return u.message(errorcontext.ErrorContext{})
}

func (u unsupportedKeyType) message(ctx errorcontext.ErrorContext) string {
return fmt.Sprintf(`maps must only have string keys for "%s" at %s`, u.typ, ctx)
}

type conversionError struct {
context context.Context
value interface{}
value interface{}
}

func newConversionError(ctx context.Context, value interface{}) error {
func newConversionError(value interface{}) error {
return &conversionError{
context: ctx,
value: value,
value: value,
}
}

func (c conversionError) Error() string {
return c.message(errorcontext.ErrorContext{})
}

func (c conversionError) message(ctx errorcontext.ErrorContext) string {
var t string
switch c.value.(type) {
case nil:
Expand All @@ -68,5 +74,78 @@ func (c conversionError) Error() string {
msg = fmt.Sprintf(`%stype "%s" `, msg, t)
}

return msg + "into " + c.context.String()
return msg + "into " + ctx.String()
}

type foreignError struct {
msg string
cause error
}

func newForeignError(msg string, cause error) error {
return foreignError{
msg: msg,
cause: cause,
}
}

func (e foreignError) Error() string {
return e.message(errorcontext.ErrorContext{})
}

func (e foreignError) message(ctx errorcontext.ErrorContext) string {
return fmt.Sprintf("%s at %s: %s", e.msg, ctx, e.cause)
}

type contextError struct {
cause error
context errorcontext.ErrorContext
}

func (c contextError) Error() string {
if e, ok := c.cause.(interface {
message(errorcontext.ErrorContext) string
}); ok {
return e.message(c.context)
}
return c.cause.Error()
}

func wrapErrorWithFieldContext(err error, fieldName string, fieldType reflect.Type) error {
switch e := err.(type) {
case contextError:
e.context = e.context.WithField(fieldName, fieldType)
return e
default:
return contextError{
cause: err,
context: errorcontext.ErrorContext{}.WithField(fieldName, fieldType),
}
}
}

func wrapErrorWithIndexContext(err error, index int, elementType reflect.Type) error {
switch e := err.(type) {
case contextError:
e.context = e.context.WithIndex(index, elementType)
return e
default:
return contextError{
cause: err,
context: errorcontext.ErrorContext{}.WithIndex(index, elementType),
}
}
}

func wrapErrorWithKeyContext(err error, keyName string, valueType reflect.Type) error {
switch e := err.(type) {
case contextError:
e.context = e.context.WithKey(keyName, valueType)
return e
default:
return contextError{
cause: err,
context: errorcontext.ErrorContext{}.WithKey(keyName, valueType),
}
}
}
63 changes: 0 additions & 63 deletions internal/context/context.go

This file was deleted.

67 changes: 67 additions & 0 deletions internal/errorcontext/errorcontext.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Package errorcontext represents a marshalling or unmarshalling call stack for use in error messages
package errorcontext

import (
"fmt"
"reflect"
)

type sort uint

const (
field sort = iota
index
key
)

type ErrorContext []segment

func (ctx ErrorContext) WithField(n string, t reflect.Type) ErrorContext {
return ctx.push(segment{sort: field, name: n, typ: t})
}

func (ctx ErrorContext) WithIndex(i int, t reflect.Type) ErrorContext {
return ctx.push(segment{sort: index, index: i, typ: t})
}

func (ctx ErrorContext) WithKey(k string, t reflect.Type) ErrorContext {
return ctx.push(segment{sort: key, name: k, typ: t})
}

func (ctx ErrorContext) String() string {
switch len(ctx) {
case 0:
return "root path"
case 1:
return ctx.leaf().String()
default:
return fmt.Sprintf("%s path %s", ctx.leaf(), ctx.path())
}
}

func (ctx ErrorContext) leaf() segment {
return ctx[len(ctx)-1]
}

func (ctx ErrorContext) path() string {
var path string
for _, s := range ctx {
switch s.sort {
case index:
path = fmt.Sprintf("%s[%d]", path, s.index)
case field:
if len(path) > 0 {
path = path + "."
}
path = path + s.name
case key:
path = fmt.Sprintf(`%s["%s"]`, path, s.name)
}
}

return path
}

func (ctx ErrorContext) push(s segment) ErrorContext {
return append([]segment{s}, ctx...)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package context_test
package errorcontext_test

import (
"testing"
Expand All @@ -9,5 +9,5 @@ import (

func TestContext(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "JSONry Internal Context Suite")
RunSpecs(t, "JSONry Internal ErrorContext Suite")
}

0 comments on commit c59764d

Please sign in to comment.