From 6b1e34e8d6af665654133c7b1c35ca7b659ff162 Mon Sep 17 00:00:00 2001 From: Jens Neuse Date: Mon, 29 Sep 2025 11:57:57 +0200 Subject: [PATCH 1/9] chore: cleanup old arena --- arena.go | 128 ----------------------------- arena_test.go | 77 ------------------ arena_timing_test.go | 44 ---------- handy.go | 26 ++---- parser_test.go | 6 +- parser_timing_test.go | 184 ------------------------------------------ pool.go | 65 --------------- pool_test.go | 50 ------------ 8 files changed, 8 insertions(+), 572 deletions(-) delete mode 100644 arena.go delete mode 100644 arena_test.go delete mode 100644 arena_timing_test.go delete mode 100644 pool.go delete mode 100644 pool_test.go diff --git a/arena.go b/arena.go deleted file mode 100644 index fc3b600..0000000 --- a/arena.go +++ /dev/null @@ -1,128 +0,0 @@ -package astjson - -import ( - "strconv" -) - -// Arena may be used for fast creation and re-use of Values. -// -// Typical Arena lifecycle: -// -// 1. Construct Values via the Arena and Value.Set* calls. -// 2. Marshal the constructed Values with Value.MarshalTo call. -// 3. Reset all the constructed Values at once by Arena.Reset call. -// 4. Go to 1 and re-use the Arena. -// -// It is unsafe calling Arena methods from concurrent goroutines. -// Use per-goroutine Arenas or ArenaPool instead. -type Arena struct { - b []byte - c cache -} - -// Reset resets all the Values allocated by a. -// -// Values previously allocated by a cannot be used after the Reset call. -func (a *Arena) Reset() { - a.b = a.b[:0] - a.c.reset() -} - -// NewObject returns new empty object value. -// -// New entries may be added to the returned object via Set call. -// -// The returned object is valid until Reset is called on a. -func (a *Arena) NewObject() *Value { - v := a.c.getValue() - v.t = TypeObject - v.o.reset() - return v -} - -// NewArray returns new empty array value. -// -// New entries may be added to the returned array via Set* calls. -// -// The returned array is valid until Reset is called on a. -func (a *Arena) NewArray() *Value { - v := a.c.getValue() - v.t = TypeArray - v.a = v.a[:0] - return v -} - -// NewString returns new string value containing s. -// -// The returned string is valid until Reset is called on a. -func (a *Arena) NewString(s string) *Value { - v := a.c.getValue() - v.t = TypeString - bLen := len(a.b) - a.b = escapeString(a.b, s) - v.s = b2s(a.b[bLen+1 : len(a.b)-1]) - v.s = unescapeStringBestEffort(v.s) - return v -} - -// NewStringBytes returns new string value containing b. -// -// The returned string is valid until Reset is called on a. -func (a *Arena) NewStringBytes(b []byte) *Value { - v := a.c.getValue() - v.t = TypeString - bLen := len(a.b) - a.b = escapeString(a.b, b2s(b)) - v.s = b2s(a.b[bLen+1 : len(a.b)-1]) - v.s = unescapeStringBestEffort(v.s) - return v -} - -// NewNumberFloat64 returns new number value containing f. -// -// The returned number is valid until Reset is called on a. -func (a *Arena) NewNumberFloat64(f float64) *Value { - v := a.c.getValue() - v.t = TypeNumber - bLen := len(a.b) - a.b = strconv.AppendFloat(a.b, f, 'g', -1, 64) - v.s = b2s(a.b[bLen:]) - return v -} - -// NewNumberInt returns new number value containing n. -// -// The returned number is valid until Reset is called on a. -func (a *Arena) NewNumberInt(n int) *Value { - v := a.c.getValue() - v.t = TypeNumber - bLen := len(a.b) - a.b = strconv.AppendInt(a.b, int64(n), 10) - v.s = b2s(a.b[bLen:]) - return v -} - -// NewNumberString returns new number value containing s. -// -// The returned number is valid until Reset is called on a. -func (a *Arena) NewNumberString(s string) *Value { - v := a.c.getValue() - v.t = TypeNumber - v.s = s - return v -} - -// NewNull returns null value. -func (a *Arena) NewNull() *Value { - return valueNull -} - -// NewTrue returns true value. -func (a *Arena) NewTrue() *Value { - return valueTrue -} - -// NewFalse return false value. -func (a *Arena) NewFalse() *Value { - return valueFalse -} diff --git a/arena_test.go b/arena_test.go deleted file mode 100644 index 49da9a5..0000000 --- a/arena_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package astjson - -import ( - "fmt" - "testing" - "time" -) - -func TestArena(t *testing.T) { - t.Run("serial", func(t *testing.T) { - var a Arena - for i := 0; i < 10; i++ { - if err := testArena(&a); err != nil { - t.Fatal(err) - } - a.Reset() - } - }) - t.Run("concurrent", func(t *testing.T) { - var ap ArenaPool - workers := 4 - ch := make(chan error, workers) - for i := 0; i < workers; i++ { - go func() { - a := ap.Get() - defer ap.Put(a) - var err error - for i := 0; i < 10; i++ { - if err = testArena(a); err != nil { - break - } - } - ch <- err - }() - } - for i := 0; i < workers; i++ { - select { - case err := <-ch: - if err != nil { - t.Fatal(err) - } - case <-time.After(time.Second): - t.Fatalf("timeout") - } - } - }) -} - -func testArena(a *Arena) error { - o := a.NewObject() - o.Set("nil1", a.NewNull()) - o.Set("nil2", nil) - o.Set("false", a.NewFalse()) - o.Set("true", a.NewTrue()) - ni := a.NewNumberInt(123) - o.Set("ni", ni) - o.Set("nf", a.NewNumberFloat64(1.23)) - o.Set("ns", a.NewNumberString("34.43")) - s := a.NewString("foo") - o.Set("str1", s) - o.Set("str2", a.NewStringBytes([]byte("xx"))) - - aa := a.NewArray() - aa.SetArrayItem(0, s) - aa.Set("1", ni) - o.Set("a", aa) - obj := a.NewObject() - obj.Set("s", s) - o.Set("obj", obj) - - str := o.String() - strExpected := `{"nil1":null,"nil2":null,"false":false,"true":true,"ni":123,"nf":1.23,"ns":34.43,"str1":"foo","str2":"xx","a":["foo",123],"obj":{"s":"foo"}}` - if str != strExpected { - return fmt.Errorf("unexpected json\ngot\n%s\nwant\n%s", str, strExpected) - } - return nil -} diff --git a/arena_timing_test.go b/arena_timing_test.go deleted file mode 100644 index 92eba95..0000000 --- a/arena_timing_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package astjson - -import ( - "sync/atomic" - "testing" -) - -func BenchmarkArenaTypicalUse(b *testing.B) { - // Determine the length of created object - var aa Arena - obj := benchCreateArenaObject(&aa) - objLen := len(obj.String()) - b.SetBytes(int64(objLen)) - b.ReportAllocs() - b.RunParallel(func(pb *testing.PB) { - var buf []byte - var a Arena - var sink int - for pb.Next() { - obj := benchCreateArenaObject(&a) - buf = obj.MarshalTo(buf[:0]) - a.Reset() - sink += len(buf) - } - atomic.AddUint64(&Sink, uint64(sink)) - }) -} - -func benchCreateArenaObject(a *Arena) *Value { - o := a.NewObject() - o.Set("key1", a.NewNumberInt(123)) - o.Set("key2", a.NewNumberFloat64(-1.23)) - - // Create a string only once and use multuple times as a performance optimization. - s := a.NewString("foobar") - aa := a.NewArray() - for i := 0; i < 10; i++ { - aa.SetArrayItem(i, s) - } - o.Set("key3", aa) - return o -} - -var Sink uint64 diff --git a/handy.go b/handy.go index c54550d..7dd234b 100644 --- a/handy.go +++ b/handy.go @@ -1,7 +1,5 @@ package astjson -var handyPool ParserPool - // GetString returns string value for the field identified by keys path // in JSON data. // @@ -11,15 +9,13 @@ var handyPool ParserPool // // Parser is faster for obtaining multiple fields from JSON. func GetString(data []byte, keys ...string) string { - p := handyPool.Get() + var p Parser v, err := p.ParseBytes(data) if err != nil { - handyPool.Put(p) return "" } sb := v.GetStringBytes(keys...) str := string(sb) - handyPool.Put(p) return str } @@ -32,10 +28,9 @@ func GetString(data []byte, keys ...string) string { // // Parser is faster for obtaining multiple fields from JSON. func GetBytes(data []byte, keys ...string) []byte { - p := handyPool.Get() + var p Parser v, err := p.ParseBytes(data) if err != nil { - handyPool.Put(p) return nil } sb := v.GetStringBytes(keys...) @@ -46,7 +41,6 @@ func GetBytes(data []byte, keys ...string) []byte { b = append(b, sb...) } - handyPool.Put(p) return b } @@ -59,14 +53,12 @@ func GetBytes(data []byte, keys ...string) []byte { // // Parser is faster for obtaining multiple fields from JSON. func GetInt(data []byte, keys ...string) int { - p := handyPool.Get() + var p Parser v, err := p.ParseBytes(data) if err != nil { - handyPool.Put(p) return 0 } n := v.GetInt(keys...) - handyPool.Put(p) return n } @@ -79,14 +71,12 @@ func GetInt(data []byte, keys ...string) int { // // Parser is faster for obtaining multiple fields from JSON. func GetFloat64(data []byte, keys ...string) float64 { - p := handyPool.Get() + var p Parser v, err := p.ParseBytes(data) if err != nil { - handyPool.Put(p) return 0 } f := v.GetFloat64(keys...) - handyPool.Put(p) return f } @@ -99,14 +89,12 @@ func GetFloat64(data []byte, keys ...string) float64 { // // Parser is faster for obtaining multiple fields from JSON. func GetBool(data []byte, keys ...string) bool { - p := handyPool.Get() + var p Parser v, err := p.ParseBytes(data) if err != nil { - handyPool.Put(p) return false } b := v.GetBool(keys...) - handyPool.Put(p) return b } @@ -118,14 +106,12 @@ func GetBool(data []byte, keys ...string) bool { // // Parser is faster when multiple fields must be checked in the JSON. func Exists(data []byte, keys ...string) bool { - p := handyPool.Get() + var p Parser v, err := p.ParseBytes(data) if err != nil { - handyPool.Put(p) return false } ok := v.Exists(keys...) - handyPool.Put(p) return ok } diff --git a/parser_test.go b/parser_test.go index 1d87ea0..1b71d11 100644 --- a/parser_test.go +++ b/parser_test.go @@ -435,9 +435,9 @@ func TestVisitNil(t *testing.T) { } func TestValueGet(t *testing.T) { - var pp ParserPool - p := pp.Get() + var p Parser + v, err := p.ParseBytes([]byte(`{"xx":33.33,"foo":[123,{"bar":["baz"],"x":"y"}], "": "empty-key", "empty-value": ""}`)) if err != nil { t.Fatalf("unexpected error: %s", err) @@ -513,8 +513,6 @@ func TestValueGet(t *testing.T) { t.Fatalf("expecting nil value for nonexisting path. Got %#v", vv) } }) - - pp.Put(p) } func TestParserParse(t *testing.T) { diff --git a/parser_timing_test.go b/parser_timing_test.go index 849792a..f87b7e6 100644 --- a/parser_timing_test.go +++ b/parser_timing_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "os" - "strings" "testing" ) @@ -63,113 +62,6 @@ func benchmarkParseRawNumber(b *testing.B, s string) { }) } -func BenchmarkObjectGet(b *testing.B) { - for _, itemsCount := range []int{10, 100, 1000, 10000, 100000} { - b.Run(fmt.Sprintf("items_%d", itemsCount), func(b *testing.B) { - for _, lookupsCount := range []int{0, 1, 2, 4, 8, 16, 32, 64} { - b.Run(fmt.Sprintf("lookups_%d", lookupsCount), func(b *testing.B) { - benchmarkObjectGet(b, itemsCount, lookupsCount) - }) - } - }) - } -} - -func benchmarkObjectGet(b *testing.B, itemsCount, lookupsCount int) { - b.StopTimer() - var ss []string - for i := 0; i < itemsCount; i++ { - s := fmt.Sprintf(`"key_%d": "value_%d"`, i, i) - ss = append(ss, s) - } - s := "{" + strings.Join(ss, ",") + "}" - key := fmt.Sprintf("key_%d", len(ss)/2) - expectedValue := fmt.Sprintf("value_%d", len(ss)/2) - b.StartTimer() - b.ReportAllocs() - b.SetBytes(int64(len(s))) - - b.RunParallel(func(pb *testing.PB) { - p := benchPool.Get() - for pb.Next() { - v, err := p.Parse(s) - if err != nil { - panic(fmt.Errorf("unexpected error: %s", err)) - } - o := v.GetObject() - for i := 0; i < lookupsCount; i++ { - sb := o.Get(key).GetStringBytes() - if string(sb) != expectedValue { - panic(fmt.Errorf("unexpected value; got %q; want %q", sb, expectedValue)) - } - } - } - benchPool.Put(p) - }) -} - -func BenchmarkMarshalTo(b *testing.B) { - b.Run("small", func(b *testing.B) { - benchmarkMarshalTo(b, smallFixture) - }) - b.Run("medium", func(b *testing.B) { - benchmarkMarshalTo(b, mediumFixture) - }) - b.Run("large", func(b *testing.B) { - benchmarkMarshalTo(b, largeFixture) - }) - b.Run("canada", func(b *testing.B) { - benchmarkMarshalTo(b, canadaFixture) - }) - b.Run("citm", func(b *testing.B) { - benchmarkMarshalTo(b, citmFixture) - }) - b.Run("twitter", func(b *testing.B) { - benchmarkMarshalTo(b, twitterFixture) - }) -} - -func benchmarkMarshalTo(b *testing.B, s string) { - p := benchPool.Get() - v, err := p.Parse(s) - if err != nil { - panic(fmt.Errorf("unexpected error: %s", err)) - } - - b.ReportAllocs() - b.SetBytes(int64(len(s))) - b.RunParallel(func(pb *testing.PB) { - var b []byte - for pb.Next() { - // It is ok calling v.MarshalTo from concurrent - // goroutines, since MarshalTo doesn't modify v. - b = v.MarshalTo(b[:0]) - } - }) - benchPool.Put(p) -} - -func BenchmarkParse(b *testing.B) { - b.Run("small", func(b *testing.B) { - benchmarkParse(b, smallFixture) - }) - b.Run("medium", func(b *testing.B) { - benchmarkParse(b, mediumFixture) - }) - b.Run("large", func(b *testing.B) { - benchmarkParse(b, largeFixture) - }) - b.Run("canada", func(b *testing.B) { - benchmarkParse(b, canadaFixture) - }) - b.Run("citm", func(b *testing.B) { - benchmarkParse(b, citmFixture) - }) - b.Run("twitter", func(b *testing.B) { - benchmarkParse(b, twitterFixture) - }) -} - var ( // small, medium and large fixtures are from https://github.com/buger/jsonparser/blob/f04e003e4115787c6272636780bc206e5ffad6c4/benchmark/benchmark.go smallFixture = getFromFile("testdata/small.json") @@ -190,82 +82,6 @@ func getFromFile(filename string) string { return string(data) } -func benchmarkParse(b *testing.B, s string) { - b.Run("stdjson-map", func(b *testing.B) { - benchmarkStdJSONParseMap(b, s) - }) - b.Run("stdjson-struct", func(b *testing.B) { - benchmarkStdJSONParseStruct(b, s) - }) - b.Run("stdjson-empty-struct", func(b *testing.B) { - benchmarkStdJSONParseEmptyStruct(b, s) - }) - b.Run("fastjson", func(b *testing.B) { - benchmarkFastJSONParse(b, s) - }) - b.Run("fastjson-get", func(b *testing.B) { - benchmarkFastJSONParseGet(b, s) - }) -} - -func benchmarkFastJSONParse(b *testing.B, s string) { - b.ReportAllocs() - b.SetBytes(int64(len(s))) - b.RunParallel(func(pb *testing.PB) { - p := benchPool.Get() - for pb.Next() { - v, err := p.Parse(s) - if err != nil { - panic(fmt.Errorf("unexpected error: %s", err)) - } - if v.Type() != TypeObject { - panic(fmt.Errorf("unexpected value type; got %s; want %s", v.Type(), TypeObject)) - } - } - benchPool.Put(p) - }) -} - -func benchmarkFastJSONParseGet(b *testing.B, s string) { - b.ReportAllocs() - b.SetBytes(int64(len(s))) - b.RunParallel(func(pb *testing.PB) { - p := benchPool.Get() - var n int - for pb.Next() { - v, err := p.Parse(s) - if err != nil { - panic(fmt.Errorf("unexpected error: %s", err)) - } - n += v.GetInt("sid") - n += len(v.GetStringBytes("uuid")) - p := v.Get("person") - if p != nil { - n++ - } - c := v.Get("company") - if c != nil { - n++ - } - u := v.Get("users") - if u != nil { - n++ - } - a := v.GetArray("features") - n += len(a) - a = v.GetArray("topicSubTopics") - n += len(a) - o := v.Get("search_metadata") - if o != nil { - n++ - } - } - benchPool.Put(p) - }) -} - -var benchPool ParserPool - func benchmarkStdJSONParseMap(b *testing.B, s string) { b.ReportAllocs() b.SetBytes(int64(len(s))) diff --git a/pool.go b/pool.go deleted file mode 100644 index 02a7880..0000000 --- a/pool.go +++ /dev/null @@ -1,65 +0,0 @@ -package astjson - -import ( - "sync" -) - -// ParserPool may be used for pooling Parsers for similarly typed JSONs. -type ParserPool struct { - pool sync.Pool -} - -// Get returns a Parser from pp. -// -// The Parser must be Put to pp after use. -func (pp *ParserPool) Get() *Parser { - v := pp.pool.Get() - if v == nil { - return &Parser{} - } - return v.(*Parser) -} - -// Put returns p to pp. -// -// p and objects recursively returned from p cannot be used after p -// is put into pp. -func (pp *ParserPool) Put(p *Parser) { - pp.pool.Put(p) -} - -// PutIfSizeLessThan PutIfLessThan Put returns p to pp only if the number of values in the cache is less than maxSize. -// If set to <= 0, no size limit is applied. -// -// p and objects recursively returned from p cannot be used after p is put into pp or released -func (pp *ParserPool) PutIfSizeLessThan(p *Parser, maxSize int) { - // Release the parser if the cache is too big - if maxSize > 0 && cap(p.c.vs) > maxSize { - return - } - - pp.pool.Put(p) -} - -// ArenaPool may be used for pooling Arenas for similarly typed JSONs. -type ArenaPool struct { - pool sync.Pool -} - -// Get returns an Arena from ap. -// -// The Arena must be Put to ap after use. -func (ap *ArenaPool) Get() *Arena { - v := ap.pool.Get() - if v == nil { - return &Arena{} - } - return v.(*Arena) -} - -// Put returns a to ap. -// -// a and objects created by a cannot be used after a is put into ap. -func (ap *ArenaPool) Put(a *Arena) { - ap.pool.Put(a) -} diff --git a/pool_test.go b/pool_test.go deleted file mode 100644 index 0111b4e..0000000 --- a/pool_test.go +++ /dev/null @@ -1,50 +0,0 @@ -// Pool is no-op under race detector, so all these tests do not work. -//go:build !race - -package astjson - -import ( - "fmt" - "sync" - "testing" -) - -func TestParserPool(t *testing.T) { - var pp ParserPool - for i := 0; i < 10; i++ { - p := pp.Get() - if _, err := p.Parse("null"); err != nil { - t.Fatalf("cannot parse null: %s", err) - } - pp.Put(p) - } -} - -func TestParserPoolMaxSize(t *testing.T) { - var numNew, numNewLimit int - ppr := &ParserPool{ - sync.Pool{New: func() interface{} { numNew++; return new(Parser) }}, - } - pprLimit := &ParserPool{ - sync.Pool{New: func() interface{} { numNewLimit++; return new(Parser) }}, - } - - parse := func(ppr *ParserPool, maxSize int, index int) { - var json = fmt.Sprintf(`{"%d":"test"}`, index) - pr := ppr.Get() - _, _ = pr.Parse(json) - ppr.PutIfSizeLessThan(pr, maxSize) - } - for i := 0; i < 10; i++ { - parse(ppr, 0, i) - parse(pprLimit, 1, i) - } - - if numNew != 1 { - t.Fatalf("Expected exactly 1 calls to Pool New with no Max Size (not %d)", numNew) - } - - if numNewLimit != 10 { - t.Fatalf("Expected exactly 10 calls to Pool with a Max Size (not %d)", numNewLimit) - } -} From 56b3fd1c9ad50ef3dc60a366c9ba80cb8edf9486 Mon Sep 17 00:00:00 2001 From: Jens Neuse Date: Mon, 29 Sep 2025 12:04:30 +0200 Subject: [PATCH 2/9] chore: merge main --- parser_timing_test.go | 190 ------------------------------------------ 1 file changed, 190 deletions(-) diff --git a/parser_timing_test.go b/parser_timing_test.go index 916e341..66559a5 100644 --- a/parser_timing_test.go +++ b/parser_timing_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "os" - "strings" "testing" ) @@ -63,119 +62,6 @@ func benchmarkParseRawNumber(b *testing.B, s string) { }) } -func BenchmarkObjectGet(b *testing.B) { - for _, itemsCount := range []int{10, 100, 1000, 10000, 100000} { - b.Run(fmt.Sprintf("items_%d", itemsCount), func(b *testing.B) { - for _, lookupsCount := range []int{0, 1, 2, 4, 8, 16, 32, 64} { - b.Run(fmt.Sprintf("lookups_%d", lookupsCount), func(b *testing.B) { - benchmarkObjectGet(b, itemsCount, lookupsCount) - }) - } - }) - } -} - -func benchmarkObjectGet(b *testing.B, itemsCount, lookupsCount int) { - var benchPool ParserPool - b.StopTimer() - var ss []string - for i := 0; i < itemsCount; i++ { - s := fmt.Sprintf(`"key_%d": "value_%d"`, i, i) - ss = append(ss, s) - } - s := "{" + strings.Join(ss, ",") + "}" - key := fmt.Sprintf("key_%d", len(ss)/2) - expectedValue := fmt.Sprintf("value_%d", len(ss)/2) - b.StartTimer() - b.ReportAllocs() - b.SetBytes(int64(len(s))) - - b.RunParallel(func(pb *testing.PB) { - p := benchPool.Get() - for pb.Next() { - v, err := p.Parse(s) - if err != nil { - panic(fmt.Errorf("unexpected error: %s", err)) - } - o := v.GetObject() - for i := 0; i < lookupsCount; i++ { - sb := o.Get(key).GetStringBytes() - if string(sb) != expectedValue { - panic(fmt.Errorf("unexpected value; got %q; want %q", sb, expectedValue)) - } - } - } - benchPool.Put(p) - }) -} - -func BenchmarkMarshalTo(b *testing.B) { - b.Run("small", func(b *testing.B) { - benchmarkMarshalTo(b, smallFixture) - }) - b.Run("medium", func(b *testing.B) { - benchmarkMarshalTo(b, mediumFixture) - }) - b.Run("large", func(b *testing.B) { - benchmarkMarshalTo(b, largeFixture) - }) - b.Run("canada", func(b *testing.B) { - benchmarkMarshalTo(b, canadaFixture) - }) - b.Run("citm", func(b *testing.B) { - benchmarkMarshalTo(b, citmFixture) - }) - b.Run("twitter", func(b *testing.B) { - benchmarkMarshalTo(b, twitterFixture) - }) - b.Run("20mb", func(b *testing.B) { - benchmarkMarshalTo(b, huge20MbFixture) - }) -} - -var benchPoolMarshalTo ParserPool - -func benchmarkMarshalTo(b *testing.B, s string) { - p := benchPoolMarshalTo.Get() - v, err := p.Parse(s) - if err != nil { - panic(fmt.Errorf("unexpected error: %s", err)) - } - - b.ReportAllocs() - b.SetBytes(int64(len(s))) - b.RunParallel(func(pb *testing.PB) { - var b []byte - for pb.Next() { - // It is ok calling v.MarshalTo from concurrent - // goroutines, since MarshalTo doesn't modify v. - b = v.MarshalTo(b[:0]) - } - }) - benchPoolMarshalTo.Put(p) -} - -func BenchmarkParse(b *testing.B) { - b.Run("small", func(b *testing.B) { - benchmarkParse(b, smallFixture) - }) - b.Run("medium", func(b *testing.B) { - benchmarkParse(b, mediumFixture) - }) - b.Run("large", func(b *testing.B) { - benchmarkParse(b, largeFixture) - }) - b.Run("canada", func(b *testing.B) { - benchmarkParse(b, canadaFixture) - }) - b.Run("citm", func(b *testing.B) { - benchmarkParse(b, citmFixture) - }) - b.Run("twitter", func(b *testing.B) { - benchmarkParse(b, twitterFixture) - }) -} - var ( // small, medium and large fixtures are from https://github.com/buger/jsonparser/blob/f04e003e4115787c6272636780bc206e5ffad6c4/benchmark/benchmark.go smallFixture = getFromFile("testdata/small.json") @@ -199,82 +85,6 @@ func getFromFile(filename string) string { return string(data) } -func benchmarkParse(b *testing.B, s string) { - b.Run("stdjson-map", func(b *testing.B) { - benchmarkStdJSONParseMap(b, s) - }) - b.Run("stdjson-struct", func(b *testing.B) { - benchmarkStdJSONParseStruct(b, s) - }) - b.Run("stdjson-empty-struct", func(b *testing.B) { - benchmarkStdJSONParseEmptyStruct(b, s) - }) - b.Run("fastjson", func(b *testing.B) { - benchmarkFastJSONParse(b, s) - }) - b.Run("fastjson-get", func(b *testing.B) { - benchmarkFastJSONParseGet(b, s) - }) -} - -func benchmarkFastJSONParse(b *testing.B, s string) { - var benchPool ParserPool - b.ReportAllocs() - b.SetBytes(int64(len(s))) - b.RunParallel(func(pb *testing.PB) { - p := benchPool.Get() - for pb.Next() { - v, err := p.Parse(s) - if err != nil { - panic(fmt.Errorf("unexpected error: %s", err)) - } - if v.Type() != TypeObject { - panic(fmt.Errorf("unexpected value type; got %s; want %s", v.Type(), TypeObject)) - } - } - benchPool.Put(p) - }) -} - -func benchmarkFastJSONParseGet(b *testing.B, s string) { - var benchPool ParserPool - b.ReportAllocs() - b.SetBytes(int64(len(s))) - b.RunParallel(func(pb *testing.PB) { - p := benchPool.Get() - var n int - for pb.Next() { - v, err := p.Parse(s) - if err != nil { - panic(fmt.Errorf("unexpected error: %s", err)) - } - n += v.GetInt("sid") - n += len(v.GetStringBytes("uuid")) - p := v.Get("person") - if p != nil { - n++ - } - c := v.Get("company") - if c != nil { - n++ - } - u := v.Get("users") - if u != nil { - n++ - } - a := v.GetArray("features") - n += len(a) - a = v.GetArray("topicSubTopics") - n += len(a) - o := v.Get("search_metadata") - if o != nil { - n++ - } - } - benchPool.Put(p) - }) -} - func benchmarkStdJSONParseMap(b *testing.B, s string) { b.ReportAllocs() b.SetBytes(int64(len(s))) From 7d4d045c439bdd4f2d4524c6034af753e744d246 Mon Sep 17 00:00:00 2001 From: Jens Neuse Date: Mon, 29 Sep 2025 12:10:54 +0200 Subject: [PATCH 3/9] chore: remove cache --- parser.go | 93 ++++++++++-------------------------------------------- scanner.go | 6 +--- 2 files changed, 17 insertions(+), 82 deletions(-) diff --git a/parser.go b/parser.go index 38b4929..44e2679 100644 --- a/parser.go +++ b/parser.go @@ -37,8 +37,6 @@ func NewParseError(err error) *ParseError { type Parser struct { // b contains working copy of the string to be parsed. b []byte - // c is a cache for json values. - c *cache } // Parse parses s containing JSON. @@ -49,13 +47,8 @@ type Parser struct { func (p *Parser) Parse(s string) (*Value, error) { s = skipWS(s) p.b = append(p.b[:0], s...) - if p.c == nil { - p.c = &cache{vs: make([]Value, 4)} - } else { - p.c.reset() - } - v, tail, err := parseValue(b2s(p.b), p.c, 0) + v, tail, err := parseValue(b2s(p.b), 0) if err != nil { return nil, NewParseError(fmt.Errorf("cannot parse JSON: %s; unparsed tail: %q", err, startEndString(tail))) } @@ -69,9 +62,8 @@ func (p *Parser) Parse(s string) (*Value, error) { func (p *Parser) ParseWithoutCache(s string) (*Value, error) { s = skipWS(s) p.b = append(p.b[:0], s...) - p.c = nil - v, tail, err := parseValue(b2s(p.b), p.c, 0) + v, tail, err := parseValue(b2s(p.b), 0) if err != nil { return nil, NewParseError(fmt.Errorf("cannot parse JSON: %s; unparsed tail: %q", err, startEndString(tail))) } @@ -95,59 +87,6 @@ func (p *Parser) ParseBytesWithoutCache(b []byte) (*Value, error) { return p.ParseWithoutCache(b2s(b)) } -type cache struct { - vs []Value - nx *cache // next - lt *cache // last -} - -func (c *cache) reset() { - c.vs = c.vs[:0] - c.lt = nil - if c.nx != nil { - c.nx.reset() - } -} - -const ( - preAllocatedCacheSize = 341 // 32kb class size - maxAllocatedCacheSize = 10922 // 1MB -) - -func (c *cache) getValue() *Value { - if c == nil { - return &Value{} - } - readSrc := c - if readSrc.lt != nil { - readSrc = readSrc.lt - } - switch { - case cap(readSrc.vs) == 0: - // initial state - readSrc.vs = make([]Value, 1, preAllocatedCacheSize) - - case cap(readSrc.vs) > len(readSrc.vs): - readSrc.vs = readSrc.vs[:len(readSrc.vs)+1] - - default: - if readSrc.nx == nil { - nextLen := len(readSrc.vs) * 2 - if nextLen > maxAllocatedCacheSize { - nextLen = maxAllocatedCacheSize - } - readSrc.nx = &cache{ - vs: make([]Value, 0, nextLen), - } - } - c.lt = readSrc.nx - readSrc = readSrc.nx - readSrc.vs = readSrc.vs[:len(readSrc.vs)+1] - } - // Do not reset the value, since the caller must properly init it. - return &readSrc.vs[len(readSrc.vs)-1] -} - func skipWS(s string) string { if len(s) == 0 || s[0] > 0x20 { // Fast path. @@ -176,7 +115,7 @@ type kv struct { // MaxDepth is the maximum depth for nested JSON. const MaxDepth = 300 -func parseValue(s string, c *cache, depth int) (*Value, string, error) { +func parseValue(s string, depth int) (*Value, string, error) { if len(s) == 0 { return nil, s, fmt.Errorf("cannot parse empty string") } @@ -186,14 +125,14 @@ func parseValue(s string, c *cache, depth int) (*Value, string, error) { } if s[0] == '{' { - v, tail, err := parseObject(s[1:], c, depth) + v, tail, err := parseObject(s[1:], depth) if err != nil { return nil, tail, fmt.Errorf("cannot parse object: %s", err) } return v, tail, nil } if s[0] == '[' { - v, tail, err := parseArray(s[1:], c, depth) + v, tail, err := parseArray(s[1:], depth) if err != nil { return nil, tail, fmt.Errorf("cannot parse array: %s", err) } @@ -204,7 +143,7 @@ func parseValue(s string, c *cache, depth int) (*Value, string, error) { if err != nil { return nil, tail, fmt.Errorf("cannot parse string: %s", err) } - v := c.getValue() + v := &Value{} v.t = TypeString v.s = unescapeStringBestEffort(ss) return v, tail, nil @@ -225,7 +164,7 @@ func parseValue(s string, c *cache, depth int) (*Value, string, error) { if len(s) < len("null") || s[:len("null")] != "null" { // Try parsing NaN if len(s) >= 3 && strings.EqualFold(s[:3], "nan") { - v := c.getValue() + v := &Value{} v.t = TypeNumber v.s = s[:3] return v, s[3:], nil @@ -239,26 +178,26 @@ func parseValue(s string, c *cache, depth int) (*Value, string, error) { if err != nil { return nil, tail, fmt.Errorf("cannot parse number: %s", err) } - v := c.getValue() + v := &Value{} v.t = TypeNumber v.s = ns return v, tail, nil } -func parseArray(s string, c *cache, depth int) (*Value, string, error) { +func parseArray(s string, depth int) (*Value, string, error) { s = skipWS(s) if len(s) == 0 { return nil, s, fmt.Errorf("missing ']'") } if s[0] == ']' { - v := c.getValue() + v := &Value{} v.t = TypeArray v.a = v.a[:0] return v, s[1:], nil } - a := c.getValue() + a := &Value{} a.t = TypeArray a.a = a.a[:0] for { @@ -266,7 +205,7 @@ func parseArray(s string, c *cache, depth int) (*Value, string, error) { var err error s = skipWS(s) - v, s, err = parseValue(s, c, depth) + v, s, err = parseValue(s, depth) if err != nil { return nil, s, fmt.Errorf("cannot parse array value: %s", err) } @@ -288,20 +227,20 @@ func parseArray(s string, c *cache, depth int) (*Value, string, error) { } } -func parseObject(s string, c *cache, depth int) (*Value, string, error) { +func parseObject(s string, depth int) (*Value, string, error) { s = skipWS(s) if len(s) == 0 { return nil, s, fmt.Errorf("missing '}'") } if s[0] == '}' { - v := c.getValue() + v := &Value{} v.t = TypeObject v.o.reset() return v, s[1:], nil } - o := c.getValue() + o := &Value{} o.t = TypeObject o.o.reset() for { @@ -325,7 +264,7 @@ func parseObject(s string, c *cache, depth int) (*Value, string, error) { // Parse value s = skipWS(s) - kv.v, s, err = parseValue(s, c, depth) + kv.v, s, err = parseValue(s, depth) if err != nil { return nil, s, fmt.Errorf("cannot parse object value: %s", err) } diff --git a/scanner.go b/scanner.go index 641bade..fc7fe5f 100644 --- a/scanner.go +++ b/scanner.go @@ -25,9 +25,6 @@ type Scanner struct { // v contains the last parsed JSON value. v *Value - - // c is used for caching JSON values. - c cache } // Init initializes sc with the given s. @@ -64,8 +61,7 @@ func (sc *Scanner) Next() bool { return false } - sc.c.reset() - v, tail, err := parseValue(sc.s, &sc.c, 0) + v, tail, err := parseValue(sc.s, 0) if err != nil { sc.err = err return false From 557ce21b2dae00e3ac3facddd845a9cff54fca80 Mon Sep 17 00:00:00 2001 From: Jens Neuse Date: Mon, 29 Sep 2025 12:12:22 +0200 Subject: [PATCH 4/9] chore: remove cache --- handy.go | 10 ---------- parser.go | 19 ------------------- parser_test.go | 24 ------------------------ 3 files changed, 53 deletions(-) diff --git a/handy.go b/handy.go index 7dd234b..30a6012 100644 --- a/handy.go +++ b/handy.go @@ -123,11 +123,6 @@ func Parse(s string) (*Value, error) { return p.Parse(s) } -func ParseWithoutCache(s string) (*Value, error) { - var p Parser - return p.ParseWithoutCache(s) -} - // MustParse parses json string s. // // The function panics if s cannot be parsed. @@ -148,11 +143,6 @@ func ParseBytes(b []byte) (*Value, error) { return p.ParseBytes(b) } -func ParseBytesWithoutCache(b []byte) (*Value, error) { - var p Parser - return p.ParseBytesWithoutCache(b) -} - // MustParseBytes parses b containing json. // // The function panics if b cannot be parsed. diff --git a/parser.go b/parser.go index 44e2679..1903044 100644 --- a/parser.go +++ b/parser.go @@ -59,21 +59,6 @@ func (p *Parser) Parse(s string) (*Value, error) { return v, nil } -func (p *Parser) ParseWithoutCache(s string) (*Value, error) { - s = skipWS(s) - p.b = append(p.b[:0], s...) - - v, tail, err := parseValue(b2s(p.b), 0) - if err != nil { - return nil, NewParseError(fmt.Errorf("cannot parse JSON: %s; unparsed tail: %q", err, startEndString(tail))) - } - tail = skipWS(tail) - if len(tail) > 0 { - return nil, NewParseError(fmt.Errorf("unexpected tail: %q", startEndString(tail))) - } - return v, nil -} - // ParseBytes parses b containing JSON. // // The returned Value is valid until the next call to Parse*. @@ -83,10 +68,6 @@ func (p *Parser) ParseBytes(b []byte) (*Value, error) { return p.Parse(b2s(b)) } -func (p *Parser) ParseBytesWithoutCache(b []byte) (*Value, error) { - return p.ParseWithoutCache(b2s(b)) -} - func skipWS(s string) string { if len(s) == 0 || s[0] > 0x20 { // Fast path. diff --git a/parser_test.go b/parser_test.go index 24c1b41..a83f078 100644 --- a/parser_test.go +++ b/parser_test.go @@ -1263,30 +1263,6 @@ func testParseGetSerial(s string) error { return nil } -func TestParseBytesWithoutCache(t *testing.T) { - var p Parser - v, err := p.ParseBytesWithoutCache([]byte(`{"foo": "bar"}`)) - if err != nil { - t.Fatalf("cannot parse json: %s", err) - } - sb := v.GetStringBytes("foo") - if string(sb) != "bar" { - t.Fatalf("unexpected value for key=%q; got %q; want %q", "foo", sb, "bar") - } -} - -func TestParseWithoutCache(t *testing.T) { - var p Parser - v, err := p.ParseWithoutCache(`{"foo": "bar"}`) - if err != nil { - t.Fatalf("cannot parse json: %s", err) - } - sb := v.GetStringBytes("foo") - if string(sb) != "bar" { - t.Fatalf("unexpected value for key=%q; got %q; want %q", "foo", sb, "bar") - } -} - func TestMarshalTo(t *testing.T) { fileData := getFromFile("testdata/bunchFields.json") var p Parser From 2616cd7eec3a5b55d88249d23a1ab604e7ddd25a Mon Sep 17 00:00:00 2001 From: Jens Neuse Date: Thu, 9 Oct 2025 09:06:16 +0200 Subject: [PATCH 5/9] feat: better arena implementation --- go.mod | 7 +- go.sum | 6 +- handy.go | 14 +++ mergevalues.go | 26 +++-- mergevalues_test.go | 70 ++++++------ parser.go | 254 +++++++++++++++++------------------------ parser_test.go | 32 +++++- scanner.go | 2 +- update.go | 25 ++-- update_example_test.go | 11 +- update_test.go | 18 +-- util.go | 6 +- 12 files changed, 243 insertions(+), 228 deletions(-) diff --git a/go.mod b/go.mod index 47acb85..4a34a44 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,11 @@ module github.com/wundergraph/astjson -go 1.21 +go 1.25 -require github.com/stretchr/testify v1.9.0 +require ( + github.com/stretchr/testify v1.11.1 + github.com/wundergraph/go-arena v0.0.0-20251008210416-55cb97e6f68f +) require ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index 60ce688..d241c92 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/wundergraph/go-arena v0.0.0-20251008210416-55cb97e6f68f h1:5snewyMaIpajTu4wj22L/DgrGimICqXtUVjkZInBH3Y= +github.com/wundergraph/go-arena v0.0.0-20251008210416-55cb97e6f68f/go.mod h1:ROOysEHWJjLQ8FSfNxZCziagb7Qw2nXY3/vgKRh7eWw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/handy.go b/handy.go index 30a6012..1d62a96 100644 --- a/handy.go +++ b/handy.go @@ -1,5 +1,9 @@ package astjson +import ( + "github.com/wundergraph/go-arena" +) + // GetString returns string value for the field identified by keys path // in JSON data. // @@ -123,6 +127,11 @@ func Parse(s string) (*Value, error) { return p.Parse(s) } +func ParseWithArena(a arena.Arena, s string) (*Value, error) { + var p Parser + return p.ParseWithArena(a, s) +} + // MustParse parses json string s. // // The function panics if s cannot be parsed. @@ -143,6 +152,11 @@ func ParseBytes(b []byte) (*Value, error) { return p.ParseBytes(b) } +func ParseBytesWithArena(a arena.Arena, b []byte) (*Value, error) { + var p Parser + return p.ParseBytesWithArena(a, b) +} + // MustParseBytes parses b containing json. // // The function panics if b cannot be parsed. diff --git a/mergevalues.go b/mergevalues.go index 632bda4..08a1d38 100644 --- a/mergevalues.go +++ b/mergevalues.go @@ -3,6 +3,8 @@ package astjson import ( "bytes" "errors" + + "github.com/wundergraph/go-arena" ) var ( @@ -11,7 +13,7 @@ var ( ErrMergeUnknownType = errors.New("cannot merge unknown type") ) -func MergeValues(a, b *Value) (v *Value, changed bool, err error) { +func MergeValues(ar arena.Arena, a, b *Value) (v *Value, changed bool, err error) { if a == nil { return b, true, nil } @@ -33,22 +35,22 @@ func MergeValues(a, b *Value) (v *Value, changed bool, err error) { case TypeObject: ao, _ := a.Object() bo, _ := b.Object() - ao.unescapeKeys() - bo.unescapeKeys() + ao.unescapeKeys(ar) + bo.unescapeKeys(ar) for i := range bo.kvs { k := bo.kvs[i].k r := bo.kvs[i].v l := ao.Get(k) if l == nil { - ao.Set(k, r) + ao.Set(ar, k, r) continue } - n, changed, err := MergeValues(l, r) + n, changed, err := MergeValues(ar, l, r) if err != nil { return nil, false, err } if changed { - ao.Set(k, n) + ao.Set(ar, k, n) } } return a, false, nil @@ -65,7 +67,7 @@ func MergeValues(a, b *Value) (v *Value, changed bool, err error) { return nil, false, ErrMergeDifferingArrayLengths } for i := range aa { - n, changed, err := MergeValues(aa[i], ba[i]) + n, changed, err := MergeValues(ar, aa[i], ba[i]) if err != nil { return nil, false, err } @@ -108,18 +110,18 @@ func MergeValues(a, b *Value) (v *Value, changed bool, err error) { } } -func MergeValuesWithPath(a, b *Value, path ...string) (v *Value, changed bool, err error) { +func MergeValuesWithPath(ar arena.Arena, a, b *Value, path ...string) (v *Value, changed bool, err error) { if len(path) == 0 { - return MergeValues(a, b) + return MergeValues(ar, a, b) } root := &Value{ t: TypeObject, } current := root for i := 0; i < len(path)-1; i++ { - current.Set(path[i], &Value{t: TypeObject}) + current.Set(ar, path[i], &Value{t: TypeObject}) current = current.Get(path[i]) } - current.Set(path[len(path)-1], b) - return MergeValues(a, root) + current.Set(ar, path[len(path)-1], b) + return MergeValues(ar, a, root) } diff --git a/mergevalues_test.go b/mergevalues_test.go index 00e56d9..e779974 100644 --- a/mergevalues_test.go +++ b/mergevalues_test.go @@ -11,7 +11,7 @@ func TestMergeValues(t *testing.T) { t.Run("left nil", func(t *testing.T) { t.Parallel() b := MustParse(`{"b":2}`) - merged, changed, err := MergeValues(nil, b) + merged, changed, err := MergeValues(nil, nil, b) require.NoError(t, err) require.Equal(t, true, changed) out := merged.MarshalTo(nil) @@ -22,7 +22,7 @@ func TestMergeValues(t *testing.T) { t.Run("right nil", func(t *testing.T) { t.Parallel() a := MustParse(`{"a":1}`) - merged, changed, err := MergeValues(a, nil) + merged, changed, err := MergeValues(nil, a, nil) require.NoError(t, err) require.Equal(t, false, changed) out := merged.MarshalTo(nil) @@ -33,7 +33,7 @@ func TestMergeValues(t *testing.T) { t.Run("type mismatch err", func(t *testing.T) { t.Parallel() a, b := MustParse(`{"a":1}`), MustParse(`{"a":true}`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.Equal(t, ErrMergeDifferentTypes, err) require.Nil(t, merged) require.Equal(t, false, changed) @@ -41,7 +41,7 @@ func TestMergeValues(t *testing.T) { t.Run("bool type mismatch ok", func(t *testing.T) { t.Parallel() a, b := MustParse(`true`), MustParse(`false`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.NoError(t, err) require.Equal(t, true, changed) out := merged.MarshalTo(nil) @@ -50,7 +50,7 @@ func TestMergeValues(t *testing.T) { t.Run("bool type mismatch ok reverse", func(t *testing.T) { t.Parallel() a, b := MustParse(`false`), MustParse(`true`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.NoError(t, err) require.Equal(t, true, changed) out := merged.MarshalTo(nil) @@ -59,7 +59,7 @@ func TestMergeValues(t *testing.T) { t.Run("integers", func(t *testing.T) { t.Parallel() a, b := MustParse(`1`), MustParse(`2`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.NoError(t, err) require.Equal(t, true, changed) out := merged.MarshalTo(nil) @@ -68,7 +68,7 @@ func TestMergeValues(t *testing.T) { t.Run("integers reverse", func(t *testing.T) { t.Parallel() a, b := MustParse(`2`), MustParse(`1`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.NoError(t, err) require.Equal(t, true, changed) out := merged.MarshalTo(nil) @@ -77,7 +77,7 @@ func TestMergeValues(t *testing.T) { t.Run("integers equal", func(t *testing.T) { t.Parallel() a, b := MustParse(`1`), MustParse(`1`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.NoError(t, err) require.Equal(t, false, changed) out := merged.MarshalTo(nil) @@ -86,7 +86,7 @@ func TestMergeValues(t *testing.T) { t.Run("floats", func(t *testing.T) { t.Parallel() a, b := MustParse(`1.1`), MustParse(`2.2`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.NoError(t, err) require.Equal(t, true, changed) out := merged.MarshalTo(nil) @@ -95,7 +95,7 @@ func TestMergeValues(t *testing.T) { t.Run("floats reverse", func(t *testing.T) { t.Parallel() a, b := MustParse(`2.2`), MustParse(`1.1`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.NoError(t, err) require.Equal(t, true, changed) out := merged.MarshalTo(nil) @@ -104,7 +104,7 @@ func TestMergeValues(t *testing.T) { t.Run("floats equal", func(t *testing.T) { t.Parallel() a, b := MustParse(`1.1`), MustParse(`1.1`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.NoError(t, err) require.Equal(t, false, changed) out := merged.MarshalTo(nil) @@ -113,7 +113,7 @@ func TestMergeValues(t *testing.T) { t.Run("arrays", func(t *testing.T) { t.Parallel() a, b := MustParse(`[1,2]`), MustParse(`[3,4]`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.NoError(t, err) require.Equal(t, false, changed) out := merged.MarshalTo(nil) @@ -122,7 +122,7 @@ func TestMergeValues(t *testing.T) { t.Run("left array empty", func(t *testing.T) { t.Parallel() a, b := MustParse(`[]`), MustParse(`[1,2]`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.NoError(t, err) require.Equal(t, true, changed) out := merged.MarshalTo(nil) @@ -131,7 +131,7 @@ func TestMergeValues(t *testing.T) { t.Run("right array empty", func(t *testing.T) { t.Parallel() a, b := MustParse(`[1,2]`), MustParse(`[]`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.NoError(t, err) require.Equal(t, false, changed) out := merged.MarshalTo(nil) @@ -140,7 +140,7 @@ func TestMergeValues(t *testing.T) { t.Run("err differing array lengths", func(t *testing.T) { t.Parallel() a, b := MustParse(`[1,2]`), MustParse(`[3]`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.Equal(t, ErrMergeDifferingArrayLengths, err) require.Nil(t, merged) require.Equal(t, false, changed) @@ -148,7 +148,7 @@ func TestMergeValues(t *testing.T) { t.Run("err merging array item", func(t *testing.T) { t.Parallel() a, b := MustParse(`[1,2]`), MustParse(`[3,"a"]`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.Error(t, err) require.Nil(t, merged) require.Equal(t, false, changed) @@ -156,7 +156,7 @@ func TestMergeValues(t *testing.T) { t.Run("false false", func(t *testing.T) { t.Parallel() a, b := MustParse(`false`), MustParse(`false`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.NoError(t, err) require.Equal(t, false, changed) out := merged.MarshalTo(nil) @@ -165,7 +165,7 @@ func TestMergeValues(t *testing.T) { t.Run("true true", func(t *testing.T) { t.Parallel() a, b := MustParse(`true`), MustParse(`true`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.NoError(t, err) require.Equal(t, false, changed) out := merged.MarshalTo(nil) @@ -174,7 +174,7 @@ func TestMergeValues(t *testing.T) { t.Run("null null", func(t *testing.T) { t.Parallel() a, b := MustParse(`null`), MustParse(`null`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.NoError(t, err) require.Equal(t, false, changed) out := merged.MarshalTo(nil) @@ -183,19 +183,19 @@ func TestMergeValues(t *testing.T) { t.Run("null not null", func(t *testing.T) { t.Parallel() a, b := MustParse(`null`), MustParse(`1`) - _, _, err := MergeValues(a, b) + _, _, err := MergeValues(nil, a, b) require.Error(t, err) }) t.Run("null not null reverse", func(t *testing.T) { t.Parallel() a, b := MustParse(`1`), MustParse(`null`) - _, _, err := MergeValues(a, b) + _, _, err := MergeValues(nil, a, b) require.Error(t, err) }) t.Run("array objects", func(t *testing.T) { t.Parallel() a, b := MustParse(`[{"a":1,"b":2},{"x":1}]`), MustParse(`[{"a":2,"b":3,"c":4},{"y":1}]`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.NoError(t, err) require.Equal(t, false, changed) out := merged.MarshalTo(nil) @@ -204,7 +204,7 @@ func TestMergeValues(t *testing.T) { t.Run("objects", func(t *testing.T) { t.Parallel() a, b := MustParse(`{"a":{"b":1}}`), MustParse(`{"a":{"c":2}}`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.NoError(t, err) require.Equal(t, false, changed) out := merged.MarshalTo(nil) @@ -213,7 +213,7 @@ func TestMergeValues(t *testing.T) { t.Run("strings", func(t *testing.T) { t.Parallel() a, b := MustParse(`"a"`), MustParse(`"b"`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.NoError(t, err) require.Equal(t, true, changed) out := merged.MarshalTo(nil) @@ -222,7 +222,7 @@ func TestMergeValues(t *testing.T) { t.Run("strings equal", func(t *testing.T) { t.Parallel() a, b := MustParse(`"a"`), MustParse(`"a"`) - merged, changed, err := MergeValues(a, b) + merged, changed, err := MergeValues(nil, a, b) require.NoError(t, err) require.Equal(t, false, changed) out := merged.MarshalTo(nil) @@ -231,13 +231,13 @@ func TestMergeValues(t *testing.T) { t.Run("with path", func(t *testing.T) { t.Parallel() a, b := MustParse(`{"a":{"b":1}}`), MustParse(`{"c":2}`) - merged, changed, err := MergeValuesWithPath(a, b, "a") + merged, changed, err := MergeValuesWithPath(nil, a, b, "a") require.NoError(t, err) require.Equal(t, false, changed) out := merged.MarshalTo(nil) require.Equal(t, `{"a":{"b":1,"c":2}}`, string(out)) e := MustParse(`{"e":true}`) - merged, changed, err = MergeValuesWithPath(merged, e, "a", "d") + merged, changed, err = MergeValuesWithPath(nil, merged, e, "a", "d") require.NoError(t, err) require.Equal(t, false, changed) out = merged.MarshalTo(out[:0]) @@ -246,7 +246,7 @@ func TestMergeValues(t *testing.T) { t.Run("with empty path", func(t *testing.T) { t.Parallel() a, b := MustParse(`{"a":1}`), MustParse(`{"b":2}`) - merged, changed, err := MergeValuesWithPath(a, b) + merged, changed, err := MergeValuesWithPath(nil, a, b) require.NoError(t, err) require.Equal(t, false, changed) out := merged.MarshalTo(nil) @@ -258,7 +258,7 @@ func TestMergeValues(t *testing.T) { t.Parallel() left := MustParse(`{"a":{"b":1,"c":2,"e":[],"f":[1],"h":[1,2,3]}}`) right := MustParse(`{"a":{"b":2,"d":3,"e":[1,2,3],"g":[1],"h":[4,5,6]}}`) - out, _, err := MergeValues(left, right) + out, _, err := MergeValues(nil, left, right) require.NoError(t, err) require.Equal(t, `{"a":{"b":2,"c":2,"e":[1,2,3],"f":[1],"h":[4,5,6],"d":3,"g":[1]}}`, out.String()) }) @@ -266,35 +266,35 @@ func TestMergeValues(t *testing.T) { t.Parallel() left := MustParse(`null`) right := MustParse(`true`) - _, _, err := MergeValues(left, right) + _, _, err := MergeValues(nil, left, right) require.Error(t, err) }) t.Run("true null", func(t *testing.T) { t.Parallel() left := MustParse(`true`) right := MustParse(`null`) - _, _, err := MergeValues(left, right) + _, _, err := MergeValues(nil, left, right) require.Error(t, err) }) t.Run("nested null true", func(t *testing.T) { t.Parallel() left := MustParse(`{"a":null}`) right := MustParse(`{"a":true}`) - _, _, err := MergeValues(left, right) + _, _, err := MergeValues(nil, left, right) require.Error(t, err) }) t.Run("nested true null", func(t *testing.T) { t.Parallel() left := MustParse(`{"a":true}`) right := MustParse(`{"a":null}`) - _, _, err := MergeValues(left, right) + _, _, err := MergeValues(nil, left, right) require.Error(t, err) }) t.Run("nested null into nested object", func(t *testing.T) { t.Parallel() left := MustParse(`{"a":{"b":"c"}}`) right := MustParse(`{"a":null}`) - out, _, err := MergeValues(left, right) + out, _, err := MergeValues(nil, left, right) require.NoError(t, err) require.Equal(t, `{"a":{"b":"c"}}`, out.String()) }) @@ -302,7 +302,7 @@ func TestMergeValues(t *testing.T) { t.Parallel() left := MustParse(`{"a":null}`) right := MustParse(`{"a":{"b":"c"}}`) - _, _, err := MergeValues(left, right) + _, _, err := MergeValues(nil, left, right) require.Error(t, err) }) } diff --git a/parser.go b/parser.go index 1903044..de1ce44 100644 --- a/parser.go +++ b/parser.go @@ -7,6 +7,7 @@ import ( "unicode/utf16" "github.com/wundergraph/astjson/fastfloat" + "github.com/wundergraph/go-arena" ) type ParseError struct { @@ -35,7 +36,6 @@ func NewParseError(err error) *ParseError { // Parser cannot be used from concurrent goroutines. // Use per-goroutine parsers or ParserPool instead. type Parser struct { - // b contains working copy of the string to be parsed. b []byte } @@ -45,18 +45,13 @@ type Parser struct { // // Use Scanner if a stream of JSON values must be parsed. func (p *Parser) Parse(s string) (*Value, error) { - s = skipWS(s) p.b = append(p.b[:0], s...) + return p.parse(nil, b2s(p.b)) +} - v, tail, err := parseValue(b2s(p.b), 0) - if err != nil { - return nil, NewParseError(fmt.Errorf("cannot parse JSON: %s; unparsed tail: %q", err, startEndString(tail))) - } - tail = skipWS(tail) - if len(tail) > 0 { - return nil, NewParseError(fmt.Errorf("unexpected tail: %q", startEndString(tail))) - } - return v, nil +func (p *Parser) ParseWithArena(a arena.Arena, s string) (*Value, error) { + p.b = append(p.b[:0], s...) + return p.parse(a, b2s(p.b)) } // ParseBytes parses b containing JSON. @@ -65,7 +60,25 @@ func (p *Parser) Parse(s string) (*Value, error) { // // Use Scanner if a stream of JSON values must be parsed. func (p *Parser) ParseBytes(b []byte) (*Value, error) { - return p.Parse(b2s(b)) + return p.parse(nil, b2s(b)) +} + +func (p *Parser) ParseBytesWithArena(a arena.Arena, b []byte) (*Value, error) { + return p.parse(a, b2s(b)) +} + +func (p *Parser) parse(a arena.Arena, s string) (*Value, error) { + s = skipWS(s) + + v, tail, err := parseValue(a, s, 0) + if err != nil { + return nil, NewParseError(fmt.Errorf("cannot parse JSON: %s; unparsed tail: %q", err, startEndString(tail))) + } + tail = skipWS(tail) + if len(tail) > 0 { + return nil, NewParseError(fmt.Errorf("unexpected tail: %q", startEndString(tail))) + } + return v, nil } func skipWS(s string) string { @@ -96,7 +109,7 @@ type kv struct { // MaxDepth is the maximum depth for nested JSON. const MaxDepth = 300 -func parseValue(s string, depth int) (*Value, string, error) { +func parseValue(a arena.Arena, s string, depth int) (*Value, string, error) { if len(s) == 0 { return nil, s, fmt.Errorf("cannot parse empty string") } @@ -106,14 +119,14 @@ func parseValue(s string, depth int) (*Value, string, error) { } if s[0] == '{' { - v, tail, err := parseObject(s[1:], depth) + v, tail, err := parseObject(a, s[1:], depth) if err != nil { return nil, tail, fmt.Errorf("cannot parse object: %s", err) } return v, tail, nil } if s[0] == '[' { - v, tail, err := parseArray(s[1:], depth) + v, tail, err := parseArray(a, s[1:], depth) if err != nil { return nil, tail, fmt.Errorf("cannot parse array: %s", err) } @@ -124,9 +137,9 @@ func parseValue(s string, depth int) (*Value, string, error) { if err != nil { return nil, tail, fmt.Errorf("cannot parse string: %s", err) } - v := &Value{} + v := arena.Allocate[Value](a) v.t = TypeString - v.s = unescapeStringBestEffort(ss) + v.s = unescapeStringBestEffort(a, ss) return v, tail, nil } if s[0] == 't' { @@ -145,7 +158,7 @@ func parseValue(s string, depth int) (*Value, string, error) { if len(s) < len("null") || s[:len("null")] != "null" { // Try parsing NaN if len(s) >= 3 && strings.EqualFold(s[:3], "nan") { - v := &Value{} + v := arena.Allocate[Value](a) v.t = TypeNumber v.s = s[:3] return v, s[3:], nil @@ -159,38 +172,43 @@ func parseValue(s string, depth int) (*Value, string, error) { if err != nil { return nil, tail, fmt.Errorf("cannot parse number: %s", err) } - v := &Value{} + v := arena.Allocate[Value](a) v.t = TypeNumber v.s = ns return v, tail, nil } -func parseArray(s string, depth int) (*Value, string, error) { +func parseArray(a arena.Arena, s string, depth int) (*Value, string, error) { s = skipWS(s) if len(s) == 0 { return nil, s, fmt.Errorf("missing ']'") } if s[0] == ']' { - v := &Value{} + v := arena.Allocate[Value](a) v.t = TypeArray v.a = v.a[:0] return v, s[1:], nil } - a := &Value{} - a.t = TypeArray - a.a = a.a[:0] + arr := arena.Allocate[Value](a) + arr.t = TypeArray + arr.a = arr.a[:0] for { var v *Value var err error s = skipWS(s) - v, s, err = parseValue(s, depth) + v, s, err = parseValue(a, s, depth) if err != nil { return nil, s, fmt.Errorf("cannot parse array value: %s", err) } - a.a = append(a.a, v) + if arr.a == nil { + arr.a = arena.AllocateSlice[*Value](a, 1, 1) + arr.a[0] = v + } else { + arr.a = arena.SliceAppend(a, arr.a, v) + } s = skipWS(s) if len(s) == 0 { @@ -202,31 +220,31 @@ func parseArray(s string, depth int) (*Value, string, error) { } if s[0] == ']' { s = s[1:] - return a, s, nil + return arr, s, nil } return nil, s, fmt.Errorf("missing ',' after array value") } } -func parseObject(s string, depth int) (*Value, string, error) { +func parseObject(a arena.Arena, s string, depth int) (*Value, string, error) { s = skipWS(s) if len(s) == 0 { return nil, s, fmt.Errorf("missing '}'") } if s[0] == '}' { - v := &Value{} + v := arena.Allocate[Value](a) v.t = TypeObject v.o.reset() return v, s[1:], nil } - o := &Value{} + o := arena.Allocate[Value](a) o.t = TypeObject o.o.reset() for { var err error - kv := o.o.getKV() + kv := o.o.getKV(a) // Parse key. s = skipWS(s) @@ -245,7 +263,7 @@ func parseObject(s string, depth int) (*Value, string, error) { // Parse value s = skipWS(s) - kv.v, s, err = parseValue(s, depth) + kv.v, s, err = parseValue(a, s, depth) if err != nil { return nil, s, fmt.Errorf("cannot parse object value: %s", err) } @@ -328,82 +346,86 @@ func escapeStringSlowPath(dst []byte, s string) []byte { return dst } -func unescapeStringBestEffort(s string) string { +func unescapeStringBestEffort(a arena.Arena, s string) string { n := strings.IndexByte(s, '\\') if n < 0 { // Fast path - nothing to unescape. return s } - // Slow path - unescape string. - b := s2b(s) // It is safe to do, since s points to a byte slice in Parser.b. - b = b[:n] + // Estimate capacity to avoid frequent reallocations + estimatedCap := len(s) + 4 + b := arena.AllocateSlice[byte](a, 0, estimatedCap) + + // Add the initial part before the first escape + b = arena.SliceAppend(a, b, []byte(s[:n])...) s = s[n+1:] + for len(s) > 0 { ch := s[0] s = s[1:] switch ch { case '"': - b = append(b, '"') + b = arena.SliceAppend(a, b, '"') case '\\': - b = append(b, '\\') + b = arena.SliceAppend(a, b, '\\') case '/': - b = append(b, '/') + b = arena.SliceAppend(a, b, '/') case 'b': - b = append(b, '\b') + b = arena.SliceAppend(a, b, '\b') case 'f': - b = append(b, '\f') + b = arena.SliceAppend(a, b, '\f') case 'n': - b = append(b, '\n') + b = arena.SliceAppend(a, b, '\n') case 'r': - b = append(b, '\r') + b = arena.SliceAppend(a, b, '\r') case 't': - b = append(b, '\t') + b = arena.SliceAppend(a, b, '\t') case 'u': if len(s) < 4 { // Too short escape sequence. Just store it unchanged. - b = append(b, "\\u"...) + b = arena.SliceAppend(a, b, []byte("\\u")...) break } xs := s[:4] x, err := strconv.ParseUint(xs, 16, 16) if err != nil { // Invalid escape sequence. Just store it unchanged. - b = append(b, "\\u"...) + b = arena.SliceAppend(a, b, []byte("\\u")...) break } s = s[4:] if !utf16.IsSurrogate(rune(x)) { - b = append(b, string(rune(x))...) + b = arena.SliceAppend(a, b, []byte(string(rune(x)))...) break } // Surrogate. // See https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Surrogates if len(s) < 6 || s[0] != '\\' || s[1] != 'u' { - b = append(b, "\\u"...) - b = append(b, xs...) + b = arena.SliceAppend(a, b, []byte("\\u")...) + b = arena.SliceAppend(a, b, []byte(xs)...) break } x1, err := strconv.ParseUint(s[2:6], 16, 16) if err != nil { - b = append(b, "\\u"...) - b = append(b, xs...) + b = arena.SliceAppend(a, b, []byte("\\u")...) + b = arena.SliceAppend(a, b, []byte(xs)...) break } r := utf16.DecodeRune(rune(x), rune(x1)) - b = append(b, string(r)...) + b = arena.SliceAppend(a, b, []byte(string(r))...) s = s[6:] default: // Unknown escape sequence. Just store it unchanged. - b = append(b, '\\', ch) + b = arena.SliceAppend(a, b, '\\', ch) } n = strings.IndexByte(s, '\\') if n < 0 { - b = append(b, s...) + b = arena.SliceAppend(a, b, []byte(s)...) break } - b = append(b, s[:n]...) + b = arena.SliceAppend(a, b, []byte(s[:n])...) s = s[n+1:] } return b2s(b) @@ -487,43 +509,31 @@ func parseRawNumber(s string) (string, string, error) { // Object cannot be used from concurrent goroutines. // Use per-goroutine parsers or ParserPool instead. type Object struct { - kvs []kv + kvs []*kv keysUnescaped bool - nx *Object - lt *Object } func (o *Object) reset() { o.kvs = o.kvs[:0] o.keysUnescaped = false - o.lt = nil - if o.nx != nil { - o.nx.reset() - } } // MarshalTo appends marshaled o to dst and returns the result. func (o *Object) MarshalTo(dst []byte) []byte { dst = append(dst, '{') - srcKV := o - lastN := o.Len() - n := 0 - for srcKV != nil { - for _, kv := range srcKV.kvs { - if srcKV.keysUnescaped { - dst = escapeString(dst, kv.k) - } else { - dst = append(dst, '"') - dst = append(dst, kv.k...) - dst = append(dst, '"') - } - dst = append(dst, ':') - dst = kv.v.MarshalTo(dst) - if n++; n != lastN { - dst = append(dst, ',') - } + for i, kv := range o.kvs { + if o.keysUnescaped { + dst = escapeString(dst, kv.k) + } else { + dst = append(dst, '"') + dst = append(dst, kv.k...) + dst = append(dst, '"') + } + dst = append(dst, ':') + dst = kv.v.MarshalTo(dst) + if i != len(o.kvs)-1 { + dst = append(dst, ',') } - srcKV = srcKV.nx } dst = append(dst, '}') return dst @@ -540,68 +550,27 @@ func (o *Object) String() string { return b2s(b) } -const ( - preAllocatedObjectKVs = 170 // 8kb class - maxAllocatedObjectKVS = 21845 // 1MB class -) - -func (o *Object) getKV() *kv { - kvSrc := o - if kvSrc.lt != nil { - kvSrc = kvSrc.lt - } - switch { - case cap(kvSrc.kvs) == 0: - // initial state - kvSrc.kvs = append(kvSrc.kvs, kv{}) - - case cap(kvSrc.kvs) > len(kvSrc.kvs): - kvSrc.kvs = kvSrc.kvs[:len(kvSrc.kvs)+1] - - default: - if cap(kvSrc.kvs) < preAllocatedObjectKVs { - kvSrc.kvs = append(kvSrc.kvs, kv{}) - break - } - // new chain - if kvSrc.nx == nil { - nextLen := len(kvSrc.kvs) * 2 - if nextLen > maxAllocatedObjectKVS { - nextLen = maxAllocatedObjectKVS - } - kvSrc.nx = &Object{ - kvs: make([]kv, 0, nextLen), - } - } - kvSrc = kvSrc.nx - o.lt = kvSrc - kvSrc.kvs = kvSrc.kvs[:len(kvSrc.kvs)+1] +func (o *Object) getKV(a arena.Arena) *kv { + if o.kvs == nil { + o.kvs = arena.AllocateSlice[*kv](a, 0, 1) } - - return &kvSrc.kvs[len(kvSrc.kvs)-1] + o.kvs = arena.SliceAppend(a, o.kvs, arena.Allocate[kv](a)) + return o.kvs[len(o.kvs)-1] } -func (o *Object) unescapeKeys() { +func (o *Object) unescapeKeys(a arena.Arena) { if o.keysUnescaped { return } - kvs := o.kvs - for i := range kvs { - kv := &kvs[i] - kv.k = unescapeStringBestEffort(kv.k) - } - if o.nx != nil { - o.nx.unescapeKeys() + for i := range o.kvs { + o.kvs[i].k = unescapeStringBestEffort(a, o.kvs[i].k) } o.keysUnescaped = true } // Len returns the number of items in the o. func (o *Object) Len() int { - if o.nx == nil { - return len(o.kvs) - } - return len(o.kvs) + o.nx.Len() + return len(o.kvs) } // Get returns the value for the given key in the o. @@ -610,6 +579,11 @@ func (o *Object) Len() int { // // The returned value is valid until Parse is called on the Parser returned o. func (o *Object) Get(key string) *Value { + + if o == nil { + return nil + } + if !o.keysUnescaped && strings.IndexByte(key, '\\') < 0 { // Fast path - try searching for the key without object keys unescaping. for _, kv := range o.kvs { @@ -617,28 +591,16 @@ func (o *Object) Get(key string) *Value { return kv.v } } - if o.nx != nil { - if v := o.nx.Get(key); v != nil { - return v - } - } } // Slow path - unescape object keys. - o.unescapeKeys() + o.unescapeKeys(nil) for _, kv := range o.kvs { if kv.k == key { return kv.v } } - - if o.nx != nil { - if v := o.nx.Get(key); v != nil { - return v - } - } - return nil } @@ -651,15 +613,11 @@ func (o *Object) Visit(f func(key []byte, v *Value)) { return } - o.unescapeKeys() + o.unescapeKeys(nil) for _, kv := range o.kvs { f(s2b(kv.k), kv.v) } - - if o.nx != nil { - o.nx.Visit(f) - } } // Value represents any JSON value. diff --git a/parser_test.go b/parser_test.go index a83f078..1f1bf90 100644 --- a/parser_test.go +++ b/parser_test.go @@ -6,6 +6,8 @@ import ( "strings" "testing" "time" + + "github.com/wundergraph/go-arena" ) func TestParseRawNumber(t *testing.T) { @@ -95,7 +97,7 @@ func testUnescapeStringBestEffort(t *testing.T, s, expectedS string) { // unescapeString modifies the original s, so call it // on a byte slice copy. b := append([]byte{}, s...) - us := unescapeStringBestEffort(b2s(b)) + us := unescapeStringBestEffort(nil, b2s(b)) if us != expectedS { t.Fatalf("unexpected unescaped string; got %q; want %q", us, expectedS) } @@ -1286,3 +1288,31 @@ func TestMarshalTo(t *testing.T) { t.Fatalf("expected 871 fields, got %d", o.Len()) } } + +func BenchmarkParse(b *testing.B) { + fileData := getFromFile("testdata/twitter.json") + var p Parser + b.SetBytes(int64(len(fileData))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err := p.Parse(fileData); err != nil { + b.Fatalf("cannot parse json: %s", err) + } + } +} + +func BenchmarkParseArena(b *testing.B) { + fileData := getFromFile("testdata/twitter.json") + var p Parser + a := arena.NewMonotonicArena(arena.WithMinBufferSize(1024 * 1024 * 2)) + b.SetBytes(int64(len(fileData))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err := p.ParseWithArena(a, fileData); err != nil { + b.Fatalf("cannot parse json: %s", err) + } + a.Reset() + } +} diff --git a/scanner.go b/scanner.go index fc7fe5f..bd5474f 100644 --- a/scanner.go +++ b/scanner.go @@ -61,7 +61,7 @@ func (sc *Scanner) Next() bool { return false } - v, tail, err := parseValue(sc.s, 0) + v, tail, err := parseValue(nil, sc.s, 0) if err != nil { sc.err = err return false diff --git a/update.go b/update.go index c064fde..fd3c2bb 100644 --- a/update.go +++ b/update.go @@ -3,6 +3,8 @@ package astjson import ( "strconv" "strings" + + "github.com/wundergraph/go-arena" ) // Del deletes the entry with the given key from o. @@ -21,7 +23,7 @@ func (o *Object) Del(key string) { } // Slow path - unescape object keys before item search. - o.unescapeKeys() + o.unescapeKeys(nil) for i, kv := range o.kvs { if kv.k == key { @@ -52,26 +54,25 @@ func (v *Value) Del(key string) { // Set sets (key, value) entry in the o. // // The value must be unchanged during o lifetime. -func (o *Object) Set(key string, value *Value) { +func (o *Object) Set(a arena.Arena, key string, value *Value) { if o == nil { return } if value == nil { value = valueNull } - o.unescapeKeys() + o.unescapeKeys(a) // Try substituting already existing entry with the given key. for i := range o.kvs { - kv := &o.kvs[i] - if kv.k == key { - kv.v = value + if o.kvs[i].k == key { + o.kvs[i].v = value return } } // Add new entry. - kv := o.getKV() + kv := o.getKV(a) kv.k = key kv.v = value } @@ -79,12 +80,12 @@ func (o *Object) Set(key string, value *Value) { // Set sets (key, value) entry in the array or object v. // // The value must be unchanged during v lifetime. -func (v *Value) Set(key string, value *Value) { +func (v *Value) Set(a arena.Arena, key string, value *Value) { if v == nil { return } if v.t == TypeObject { - v.o.Set(key, value) + v.o.Set(a, key, value) return } if v.t == TypeArray { @@ -92,19 +93,19 @@ func (v *Value) Set(key string, value *Value) { if err != nil || idx < 0 { return } - v.SetArrayItem(idx, value) + v.SetArrayItem(a, idx, value) } } // SetArrayItem sets the value in the array v at idx position. // // The value must be unchanged during v lifetime. -func (v *Value) SetArrayItem(idx int, value *Value) { +func (v *Value) SetArrayItem(a arena.Arena, idx int, value *Value) { if v == nil || v.t != TypeArray { return } for idx >= len(v.a) { - v.a = append(v.a, valueNull) + v.a = arena.SliceAppend(a, v.a, valueNull) } v.a[idx] = value } diff --git a/update_example_test.go b/update_example_test.go index 3dee915..eedc34b 100644 --- a/update_example_test.go +++ b/update_example_test.go @@ -5,6 +5,7 @@ import ( "log" "github.com/wundergraph/astjson" + "github.com/wundergraph/go-arena" ) func ExampleObject_Del() { @@ -49,17 +50,17 @@ func ExampleValue_Del() { func ExampleValue_Set() { v := astjson.MustParse(`{"foo":1,"bar":[2,3]}`) - + a := arena.NewMonotonicArena() // Replace `foo` value with "xyz" - v.Set("foo", astjson.MustParse(`"xyz"`)) + v.Set(a, "foo", astjson.MustParse(`"xyz"`)) // Add "newv":123 - v.Set("newv", astjson.MustParse(`123`)) + v.Set(a, "newv", astjson.MustParse(`123`)) fmt.Printf("%s\n", v) // Replace `bar.1` with {"x":"y"} - v.Get("bar").Set("1", astjson.MustParse(`{"x":"y"}`)) + v.Get("bar").Set(a, "1", astjson.MustParse(`{"x":"y"}`)) // Add `bar.3="qwe" - v.Get("bar").Set("3", astjson.MustParse(`"qwe"`)) + v.Get("bar").Set(a, "3", astjson.MustParse(`"qwe"`)) fmt.Printf("%s\n", v) // Output: diff --git a/update_test.go b/update_test.go index 94be4ae..487d84e 100644 --- a/update_test.go +++ b/update_test.go @@ -2,11 +2,14 @@ package astjson import ( "testing" + + "github.com/wundergraph/go-arena" ) func TestObjectDelSet(t *testing.T) { var p Parser var o *Object + a := arena.NewMonotonicArena() o.Del("xx") @@ -33,7 +36,7 @@ func TestObjectDelSet(t *testing.T) { // Set new value vNew := MustParse(`{"foo":[1,2,3]}`) - o.Set("new_key", vNew) + o.Set(a, "new_key", vNew) // Delete item with escaped key o.Del("fo\no") @@ -50,11 +53,12 @@ func TestObjectDelSet(t *testing.T) { // Set and Del function as no-op on nil value o = nil o.Del("x") - o.Set("x", MustParse(`[3]`)) + o.Set(a, "x", MustParse(`[3]`)) } func TestValueDelSet(t *testing.T) { var p Parser + ar := arena.NewMonotonicArena() v, err := p.Parse(`{"xx": 123, "x": [1,2,3]}`) if err != nil { t.Fatalf("unexpected error during parse: %s", err) @@ -80,14 +84,14 @@ func TestValueDelSet(t *testing.T) { // Update the first element in the array vNew := MustParse(`"foobar"`) - va.Set("0", vNew) + va.Set(ar, "0", vNew) // Add third element to the array vNew = MustParse(`[3]`) - va.Set("3", vNew) + va.Set(ar, "3", vNew) // Add invalid array index to the array - va.Set("invalid", MustParse(`"nonsense"`)) + va.Set(ar, "invalid", MustParse(`"nonsense"`)) str := v.String() strExpected := `{"x":["foobar",3,null,[3]]}` @@ -98,8 +102,8 @@ func TestValueDelSet(t *testing.T) { // Set and Del function as no-op on nil value v = nil v.Del("x") - v.Set("x", MustParse(`[]`)) - v.SetArrayItem(1, MustParse(`[]`)) + v.Set(ar, "x", MustParse(`[]`)) + v.SetArrayItem(ar, 1, MustParse(`[]`)) } func TestValue_AppendArrayItems(t *testing.T) { diff --git a/util.go b/util.go index a30ddef..70b2489 100644 --- a/util.go +++ b/util.go @@ -32,7 +32,7 @@ func AppendToArray(array, value *Value) { return } items, _ := array.Array() - array.SetArrayItem(len(items), value) + array.SetArrayItem(nil, len(items), value) } func SetValue(v *Value, value *Value, path ...string) { @@ -41,11 +41,11 @@ func SetValue(v *Value, value *Value, path ...string) { v = v.Get(path[i]) if v == nil { child := MustParse(`{}`) - parent.Set(path[i], child) + parent.Set(nil, path[i], child) v = child } } - v.Set(path[len(path)-1], value) + v.Set(nil, path[len(path)-1], value) } func SetNull(v *Value, path ...string) { From 119349832a9f22b46f7f2dd39bc79a8b82631b39 Mon Sep 17 00:00:00 2001 From: Jens Neuse Date: Thu, 9 Oct 2025 09:37:43 +0200 Subject: [PATCH 6/9] refactor: fix lint issues, update linter --- .github/workflows/ci.yml | 8 ++--- parser.go | 7 +++-- parser_timing_test.go | 55 --------------------------------- validate.go | 2 +- values.go | 66 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 75 insertions(+), 63 deletions(-) create mode 100644 values.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61d57b4..a6a447b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - go: [ '1.21' ] + go: [ '1.25' ] os: [ubuntu-latest, windows-latest] steps: - name: Set git to use LF @@ -37,14 +37,14 @@ jobs: steps: - name: Check out code into the Go module directory uses: actions/checkout@v3 - - name: Set up Go 1.21 + - name: Set up Go 1.25 uses: actions/setup-go@v4 with: - go-version: 1.21 + go-version: 1.25 - name: Run linters uses: golangci/golangci-lint-action@v3 with: - version: v1.55.2 + version: v2.5.0 args: --timeout=3m ci: name: CI Success diff --git a/parser.go b/parser.go index de1ce44..23d9935 100644 --- a/parser.go +++ b/parser.go @@ -753,18 +753,19 @@ func (v *Value) Get(keys ...string) *Value { return nil } for _, key := range keys { - if v.t == TypeObject { + switch v.t { + case TypeObject: v = v.o.Get(key) if v == nil { return nil } - } else if v.t == TypeArray { + case TypeArray: n, err := strconv.Atoi(key) if err != nil || n < 0 || n >= len(v.a) { return nil } v = v.a[n] - } else { + default: return nil } } diff --git a/parser_timing_test.go b/parser_timing_test.go index 66559a5..1199c78 100644 --- a/parser_timing_test.go +++ b/parser_timing_test.go @@ -1,7 +1,6 @@ package astjson import ( - "encoding/json" "fmt" "os" "testing" @@ -72,9 +71,6 @@ var ( canadaFixture = getFromFile("testdata/canada.json") citmFixture = getFromFile("testdata/citm_catalog.json") twitterFixture = getFromFile("testdata/twitter.json") - - // 20mb is a huge (stressful) fixture from https://examplefile.com/code/json/20-mb-json - huge20MbFixture = getFromFile("testdata/20mb.json") ) func getFromFile(filename string) string { @@ -84,54 +80,3 @@ func getFromFile(filename string) string { } return string(data) } - -func benchmarkStdJSONParseMap(b *testing.B, s string) { - b.ReportAllocs() - b.SetBytes(int64(len(s))) - bb := s2b(s) - b.RunParallel(func(pb *testing.PB) { - var m map[string]interface{} - for pb.Next() { - if err := json.Unmarshal(bb, &m); err != nil { - panic(fmt.Errorf("unexpected error: %s", err)) - } - } - }) -} - -func benchmarkStdJSONParseStruct(b *testing.B, s string) { - b.ReportAllocs() - b.SetBytes(int64(len(s))) - bb := s2b(s) - b.RunParallel(func(pb *testing.PB) { - var m struct { - Sid int - UUID string - Person map[string]interface{} - Company map[string]interface{} - Users []interface{} - Features []map[string]interface{} - TopicSubTopics map[string]interface{} - SearchMetadata map[string]interface{} - } - for pb.Next() { - if err := json.Unmarshal(bb, &m); err != nil { - panic(fmt.Errorf("unexpected error: %s", err)) - } - } - }) -} - -func benchmarkStdJSONParseEmptyStruct(b *testing.B, s string) { - b.ReportAllocs() - b.SetBytes(int64(len(s))) - bb := s2b(s) - b.RunParallel(func(pb *testing.PB) { - var m struct{} - for pb.Next() { - if err := json.Unmarshal(bb, &m); err != nil { - panic(fmt.Errorf("unexpected error: %s", err)) - } - } - }) -} diff --git a/validate.go b/validate.go index e2e42fe..967664d 100644 --- a/validate.go +++ b/validate.go @@ -215,7 +215,7 @@ func validateString(s string) (string, string, error) { switch ch { case '"', '\\', '/', 'b', 'f', 'n', 'r', 't': // Valid escape sequences - see http://json.org/ - break + continue case 'u': if len(rs) < 4 { return rs, tail, fmt.Errorf(`too short escape sequence: \u%s`, rs) diff --git a/values.go b/values.go new file mode 100644 index 0000000..d275c0f --- /dev/null +++ b/values.go @@ -0,0 +1,66 @@ +package astjson + +import ( + "fmt" + + "github.com/wundergraph/go-arena" +) + +func StringValue(a arena.Arena, s string) *Value { + v := arena.Allocate[Value](a) + v.t = TypeString + v.s = s + return v +} + +func StringValueBytes(a arena.Arena, b []byte) *Value { + v := arena.Allocate[Value](a) + v.t = TypeString + v.s = b2s(b) + return v +} + +func IntValue(a arena.Arena, i int) *Value { + v := arena.Allocate[Value](a) + v.t = TypeNumber + v.s = fmt.Sprintf("%d", i) + return v +} + +func FloatValue(a arena.Arena, f float64) *Value { + v := arena.Allocate[Value](a) + v.t = TypeNumber + v.s = fmt.Sprintf("%g", f) + return v +} + +func NumberValue(a arena.Arena, s string) *Value { + v := arena.Allocate[Value](a) + v.t = TypeNumber + v.s = s + return v +} + +func TrueValue(a arena.Arena) *Value { + v := arena.Allocate[Value](a) + v.t = TypeTrue + return v +} + +func FalseValue(a arena.Arena) *Value { + v := arena.Allocate[Value](a) + v.t = TypeFalse + return v +} + +func ObjectValue(a arena.Arena) *Value { + v := arena.Allocate[Value](a) + v.t = TypeObject + return v +} + +func ArrayValue(a arena.Arena) *Value { + v := arena.Allocate[Value](a) + v.t = TypeArray + return v +} From f1b4b272fa3e6a0475852759033c9b3f584ae5a4 Mon Sep 17 00:00:00 2001 From: Jens Neuse Date: Thu, 9 Oct 2025 09:46:16 +0200 Subject: [PATCH 7/9] chore: update CI configuration and dependencies --- .github/workflows/ci.yml | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6a447b..2d883c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,10 @@ on: push: branches: - master + +permissions: + contents: read + jobs: test: name: Build and test (go ${{ matrix.go }} / ${{ matrix.os }}) @@ -20,9 +24,9 @@ jobs: git config --global core.autocrlf false git config --global core.eol lf - name: Check out code into the Go module directory - uses: actions/checkout@v3 + uses: actions/checkout@v5 - name: Set up Go ${{ matrix.go }} - uses: actions/setup-go@v4 + uses: actions/setup-go@v6 with: go-version: ^${{ matrix.go }} id: go @@ -32,20 +36,17 @@ jobs: run: go test -v ./... -race lint: - name: Linters + name: lint runs-on: ubuntu-latest steps: - - name: Check out code into the Go module directory - uses: actions/checkout@v3 - - name: Set up Go 1.25 - uses: actions/setup-go@v4 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: - go-version: 1.25 - - name: Run linters - uses: golangci/golangci-lint-action@v3 + go-version: stable + - name: golangci-lint + uses: golangci/golangci-lint-action@v8 with: - version: v2.5.0 - args: --timeout=3m + version: v2.1 ci: name: CI Success if: ${{ always() }} From 5bcda71a5d28e9e43efd79ea07f673f6eeb1dd0c Mon Sep 17 00:00:00 2001 From: Jens Neuse Date: Thu, 9 Oct 2025 09:51:37 +0200 Subject: [PATCH 8/9] test: add unit test for Del method with nil arena handling --- .github/workflows/ci.yml | 2 +- update.go | 1 + update_test.go | 31 +++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d883c5..0cefa11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v8 with: - version: v2.1 + version: v2.5.0 ci: name: CI Success if: ${{ always() }} diff --git a/update.go b/update.go index fd3c2bb..18899b8 100644 --- a/update.go +++ b/update.go @@ -23,6 +23,7 @@ func (o *Object) Del(key string) { } // Slow path - unescape object keys before item search. + // Note: Passing nil arena is safe - go-arena falls back to heap allocation when arena is nil. o.unescapeKeys(nil) for i, kv := range o.kvs { diff --git a/update_test.go b/update_test.go index 487d84e..feb68cd 100644 --- a/update_test.go +++ b/update_test.go @@ -118,3 +118,34 @@ func TestValue_AppendArrayItems(t *testing.T) { t.Fatalf("unexpected output; got %q; want %q", out, `[1,2,3,4,5,6]`) } } + +func TestObjectDelWithNilArena(t *testing.T) { + // Test that Del method works correctly when unescapeKeys is called with nil arena + var p Parser + v, err := p.Parse(`{"fo\no": "bar", "x": [1,2,3], "escaped\\key": "value"}`) + if err != nil { + t.Fatalf("unexpected error during parse: %s", err) + } + o, err := v.Object() + if err != nil { + t.Fatalf("cannot obtain object: %s", err) + } + + // This should trigger the slow path and call unescapeKeys(nil) + // The go-arena library should handle nil arena gracefully + o.Del("fo\no") + if o.Len() != 2 { + t.Fatalf("unexpected number of items left; got %d; want %d", o.Len(), 2) + } + + // Test with another escaped key + o.Del("escaped\\key") + if o.Len() != 1 { + t.Fatalf("unexpected number of items left; got %d; want %d", o.Len(), 1) + } + + // Verify the remaining key + if o.Get("x") == nil { + t.Fatalf("expected key 'x' to still exist") + } +} From d990a3dff59d868b7266d69fa8a81e45f607df49 Mon Sep 17 00:00:00 2001 From: Jens Neuse Date: Thu, 9 Oct 2025 11:28:08 +0200 Subject: [PATCH 9/9] test: add benchmarks for JSON parsing and marshalling performance --- parser_timing_test.go | 237 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) diff --git a/parser_timing_test.go b/parser_timing_test.go index 1199c78..a69d8f4 100644 --- a/parser_timing_test.go +++ b/parser_timing_test.go @@ -1,8 +1,10 @@ package astjson import ( + "encoding/json" "fmt" "os" + "strings" "testing" ) @@ -71,6 +73,9 @@ var ( canadaFixture = getFromFile("testdata/canada.json") citmFixture = getFromFile("testdata/citm_catalog.json") twitterFixture = getFromFile("testdata/twitter.json") + + // 20mb is a huge (stressful) fixture from https://examplefile.com/code/json/20-mb-json + huge20MbFixture = getFromFile("testdata/20mb.json") ) func getFromFile(filename string) string { @@ -80,3 +85,235 @@ func getFromFile(filename string) string { } return string(data) } + +func BenchmarkObjectGet(b *testing.B) { + for _, itemsCount := range []int{10, 100, 1000, 10000, 100000} { + b.Run(fmt.Sprintf("items_%d", itemsCount), func(b *testing.B) { + for _, lookupsCount := range []int{0, 1, 2, 4, 8, 16, 32, 64} { + b.Run(fmt.Sprintf("lookups_%d", lookupsCount), func(b *testing.B) { + benchmarkObjectGet(b, itemsCount, lookupsCount) + }) + } + }) + } +} + +func benchmarkObjectGet(b *testing.B, itemsCount, lookupsCount int) { + var benchPool Parser + b.StopTimer() + var ss []string + for i := 0; i < itemsCount; i++ { + s := fmt.Sprintf(`"key_%d": "value_%d"`, i, i) + ss = append(ss, s) + } + s := "{" + strings.Join(ss, ",") + "}" + key := fmt.Sprintf("key_%d", len(ss)/2) + expectedValue := fmt.Sprintf("value_%d", len(ss)/2) + b.StartTimer() + b.ReportAllocs() + b.SetBytes(int64(len(s))) + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + v, err := benchPool.Parse(s) + if err != nil { + panic(fmt.Errorf("unexpected error: %s", err)) + } + o := v.GetObject() + for i := 0; i < lookupsCount; i++ { + sb := o.Get(key).GetStringBytes() + if string(sb) != expectedValue { + panic(fmt.Errorf("unexpected value; got %q; want %q", sb, expectedValue)) + } + } + } + }) +} + +func BenchmarkMarshalTo(b *testing.B) { + b.Run("small", func(b *testing.B) { + benchmarkMarshalTo(b, smallFixture) + }) + b.Run("medium", func(b *testing.B) { + benchmarkMarshalTo(b, mediumFixture) + }) + b.Run("large", func(b *testing.B) { + benchmarkMarshalTo(b, largeFixture) + }) + b.Run("canada", func(b *testing.B) { + benchmarkMarshalTo(b, canadaFixture) + }) + b.Run("citm", func(b *testing.B) { + benchmarkMarshalTo(b, citmFixture) + }) + b.Run("twitter", func(b *testing.B) { + benchmarkMarshalTo(b, twitterFixture) + }) + b.Run("20mb", func(b *testing.B) { + benchmarkMarshalTo(b, huge20MbFixture) + }) +} + +var benchPoolMarshalTo Parser + +func benchmarkMarshalTo(b *testing.B, s string) { + v, err := benchPoolMarshalTo.Parse(s) + if err != nil { + panic(fmt.Errorf("unexpected error: %s", err)) + } + + b.ReportAllocs() + b.SetBytes(int64(len(s))) + b.RunParallel(func(pb *testing.PB) { + var b []byte + for pb.Next() { + // It is ok calling v.MarshalTo from concurrent + // goroutines, since MarshalTo doesn't modify v. + b = v.MarshalTo(b[:0]) + } + }) +} + +func BenchmarkParseComparison(b *testing.B) { + b.Run("small", func(b *testing.B) { + benchmarkParse(b, smallFixture) + }) + b.Run("medium", func(b *testing.B) { + benchmarkParse(b, mediumFixture) + }) + b.Run("large", func(b *testing.B) { + benchmarkParse(b, largeFixture) + }) + b.Run("canada", func(b *testing.B) { + benchmarkParse(b, canadaFixture) + }) + b.Run("citm", func(b *testing.B) { + benchmarkParse(b, citmFixture) + }) + b.Run("twitter", func(b *testing.B) { + benchmarkParse(b, twitterFixture) + }) +} + +func benchmarkParse(b *testing.B, s string) { + b.Run("stdjson-map", func(b *testing.B) { + benchmarkStdJSONParseMap(b, s) + }) + b.Run("stdjson-struct", func(b *testing.B) { + benchmarkStdJSONParseStruct(b, s) + }) + b.Run("stdjson-empty-struct", func(b *testing.B) { + benchmarkStdJSONParseEmptyStruct(b, s) + }) + b.Run("fastjson", func(b *testing.B) { + benchmarkFastJSONParse(b, s) + }) + b.Run("fastjson-get", func(b *testing.B) { + benchmarkFastJSONParseGet(b, s) + }) +} + +func benchmarkFastJSONParse(b *testing.B, s string) { + var benchPool Parser + b.ReportAllocs() + b.SetBytes(int64(len(s))) + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + v, err := benchPool.Parse(s) + if err != nil { + panic(fmt.Errorf("unexpected error: %s", err)) + } + if v.Type() != TypeObject { + panic(fmt.Errorf("unexpected value type; got %s; want %s", v.Type(), TypeObject)) + } + } + }) +} + +func benchmarkFastJSONParseGet(b *testing.B, s string) { + var benchPool Parser + b.ReportAllocs() + b.SetBytes(int64(len(s))) + b.RunParallel(func(pb *testing.PB) { + var n int + for pb.Next() { + v, err := benchPool.Parse(s) + if err != nil { + panic(fmt.Errorf("unexpected error: %s", err)) + } + n += v.GetInt("sid") + n += len(v.GetStringBytes("uuid")) + p := v.Get("person") + if p != nil { + n++ + } + c := v.Get("company") + if c != nil { + n++ + } + u := v.Get("users") + if u != nil { + n++ + } + a := v.GetArray("features") + n += len(a) + a = v.GetArray("topicSubTopics") + n += len(a) + o := v.Get("search_metadata") + if o != nil { + n++ + } + } + }) +} + +func benchmarkStdJSONParseMap(b *testing.B, s string) { + b.ReportAllocs() + b.SetBytes(int64(len(s))) + bb := s2b(s) + b.RunParallel(func(pb *testing.PB) { + var m map[string]interface{} + for pb.Next() { + if err := json.Unmarshal(bb, &m); err != nil { + panic(fmt.Errorf("unexpected error: %s", err)) + } + } + }) +} + +func benchmarkStdJSONParseStruct(b *testing.B, s string) { + b.ReportAllocs() + b.SetBytes(int64(len(s))) + bb := s2b(s) + b.RunParallel(func(pb *testing.PB) { + var m struct { + Sid int + UUID string + Person map[string]interface{} + Company map[string]interface{} + Users []interface{} + Features []map[string]interface{} + TopicSubTopics map[string]interface{} + SearchMetadata map[string]interface{} + } + for pb.Next() { + if err := json.Unmarshal(bb, &m); err != nil { + panic(fmt.Errorf("unexpected error: %s", err)) + } + } + }) +} + +func benchmarkStdJSONParseEmptyStruct(b *testing.B, s string) { + b.ReportAllocs() + b.SetBytes(int64(len(s))) + bb := s2b(s) + b.RunParallel(func(pb *testing.PB) { + var m struct{} + for pb.Next() { + if err := json.Unmarshal(bb, &m); err != nil { + panic(fmt.Errorf("unexpected error: %s", err)) + } + } + }) +}