Skip to content

Commit

Permalink
create byte buffer pool (#9)
Browse files Browse the repository at this point in the history
* create byte buffer pool

* fix buffer size

* add better test for allocations

* add pool size

* log if allocation happened in pool
  • Loading branch information
awalterschulze authored Jan 30, 2025
1 parent 025644a commit 847a1ce
Show file tree
Hide file tree
Showing 15 changed files with 256 additions and 39 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ bench:
memprofile:
rm mem.out || true
rm profile*.* || true
go test -test.v -test.run=XXX -test.bench=. -test.memprofile=mem.out ./json
go test -test.v -test.run=XXX -test.bench=BenchmarkAlloc -test.memprofile=mem.out ./json
go tool pprof -alloc_space -png mem.out

gofmt:
Expand Down
55 changes: 50 additions & 5 deletions json/alloc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,22 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package json_test
package json

import (
"fmt"
"math/rand"
"testing"
"time"

jsonparser "github.com/katydid/parser-go-json/json"
"github.com/katydid/parser-go-json/json/pool"
)

func DisabledTestNoAllocs(t *testing.T) {
func TestNoAllocsOnAverage(t *testing.T) {
num := 100
r := rand.New(rand.NewSource(time.Now().UnixNano()))
js := randJsons(r, num)
jparser := jsonparser.NewJsonParser()
jparser := NewJsonParser()

const runsPerTest = 100
checkNoAllocs := func(f func()) func(t *testing.T) {
Expand All @@ -48,11 +48,56 @@ func DisabledTestNoAllocs(t *testing.T) {
}
}

func TestNotASingleAllocAfterWarmUp(t *testing.T) {
num := 100
r := rand.New(rand.NewSource(time.Now().UnixNano()))
js := randJsons(r, num)
pool := pool.New()
jparser := NewJsonParser()
jparser.(*jsonParser).pool = pool

// warm up buffer pool
for i := 0; i < num; i++ {
if err := jparser.Init(js[i%num]); err != nil {
t.Fatal(err)
}
walk(jparser)
}
originalPoolSize := pool.Size()

const runsPerTest = 1
checkNoAllocs := func(f func()) func(t *testing.T) {
return func(t *testing.T) {
t.Helper()
if allocs := testing.AllocsPerRun(runsPerTest, f); allocs != 0 {
t.Errorf("got %v allocs, want 0 allocs, pool allocs = %v", allocs, pool.Size()-originalPoolSize)
}
}
}
for i := 0; i < num; i++ {
t.Run(fmt.Sprintf("%d", i), checkNoAllocs(func() {
if err := jparser.Init(js[i]); err != nil {
t.Fatal(err)
}
walk(jparser)
}))
}
}

func BenchmarkAlloc(b *testing.B) {
num := 1000
r := rand.New(rand.NewSource(time.Now().UnixNano()))
js := randJsons(r, num)
jparser := jsonparser.NewJsonParser()
jparser := NewJsonParser()

// exercise buffer pool
for i := 0; i < num; i++ {
if err := jparser.Init(js[i%num]); err != nil {
b.Fatal(err)
}
walk(jparser)
}

b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := jparser.Init(js[i%num]); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion json/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package json_test
package json

import (
"fmt"
Expand Down
8 changes: 3 additions & 5 deletions json/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package json_test
package json

import (
"testing"

jsonparser "github.com/katydid/parser-go-json/json"
)

func testValue(t *testing.T, input, output string) {
t.Helper()
parser := jsonparser.NewJsonParser()
parser := NewJsonParser()
if err := parser.Init([]byte(input)); err != nil {
t.Errorf("init error: %v", err)
return
Expand Down Expand Up @@ -52,7 +50,7 @@ func testSame(t *testing.T, input string) {

func testError(t *testing.T, s string) {
t.Helper()
parser := jsonparser.NewJsonParser()
parser := NewJsonParser()
if err := parser.Init([]byte(s)); err != nil {
t.Logf("PASS: given <%s> error: %v", s, err)
return
Expand Down
12 changes: 8 additions & 4 deletions json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"bytes"
"io"

"github.com/katydid/parser-go-json/json/pool"
"github.com/katydid/parser-go-json/json/strconv"
"github.com/katydid/parser-go/parser"
)
Expand Down Expand Up @@ -83,10 +84,10 @@ func skipSpace(buf []byte) int {
return len(buf)
}

func unquote(s []byte) (string, error) {
func unquote(pool pool.Pool, s []byte) (string, error) {
var ok bool
var t string
s, ok = unquoteBytes(s)
s, ok = unquoteBytes(pool, s)
t = castToString(s)
if !ok {
return "", errUnquote
Expand Down Expand Up @@ -196,7 +197,7 @@ func (s *jsonParser) scanName() error {
if err := s.incOffset(n); err != nil {
return err
}
s.name, err = unquote(s.buf[startOffset:s.offset])
s.name, err = unquote(s.pool, s.buf[startOffset:s.offset])
if err != nil {
return err
}
Expand Down Expand Up @@ -551,7 +552,7 @@ func (s *jsonParser) String() (string, error) {
if v[0] != '"' {
return "", parser.ErrNotString
}
res, err := unquote(v)
res, err := unquote(s.pool, v)
if err != nil {
return "", err
}
Expand All @@ -578,6 +579,7 @@ type JsonParser interface {
// NewJsonParser returns a new JSON parser.
func NewJsonParser() JsonParser {
return &jsonParser{
pool: pool.New(),
state: state{
firstObjectValue: true,
},
Expand All @@ -591,6 +593,7 @@ func (s *jsonParser) Init(buf []byte) error {
buf: buf,
}
s.stack = s.stack[:0]
s.pool.FreeAll()
if err := s.skipSpace(); err != nil {
return err
}
Expand Down Expand Up @@ -627,6 +630,7 @@ func (s *jsonParser) Reset() error {
type jsonParser struct {
state
stack []state
pool pool.Pool
}

type state struct {
Expand Down
17 changes: 8 additions & 9 deletions json/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package json_test
package json

import (
"encoding/json"
"testing"

jsonparser "github.com/katydid/parser-go-json/json"
"github.com/katydid/parser-go/parser/debug"
)

func TestDebug(t *testing.T) {
p := jsonparser.NewJsonParser()
p := NewJsonParser()
data, err := json.Marshal(debug.Input)
if err != nil {
t.Fatal(err)
Expand All @@ -38,7 +37,7 @@ func TestDebug(t *testing.T) {
}

func TestRandomDebug(t *testing.T) {
p := jsonparser.NewJsonParser()
p := NewJsonParser()
data, err := json.Marshal(debug.Input)
if err != nil {
t.Fatal(err)
Expand All @@ -62,7 +61,7 @@ func TestEscapedChar(t *testing.T) {
t.Fatal(err)
}
t.Logf("%s", string(data))
parser := jsonparser.NewJsonParser()
parser := NewJsonParser()
if err := parser.Init(data); err != nil {
t.Fatal(err)
}
Expand All @@ -80,7 +79,7 @@ func TestMultiLineArray(t *testing.T) {
s := `{
"A":[1]
}`
parser := jsonparser.NewJsonParser()
parser := NewJsonParser()
if err := parser.Init([]byte(s)); err != nil {
t.Fatal(err)
}
Expand All @@ -93,7 +92,7 @@ func TestMultiLineArray(t *testing.T) {

func TestIntWithExponent(t *testing.T) {
s := `{"A":1e+08}`
parser := jsonparser.NewJsonParser()
parser := NewJsonParser()
if err := parser.Init([]byte(s)); err != nil {
t.Fatal(err)
}
Expand All @@ -116,7 +115,7 @@ func TestIntWithExponent(t *testing.T) {

func TestTooLargeNumber(t *testing.T) {
input := `123456789.123456789e+123456789`
parser := jsonparser.NewJsonParser()
parser := NewJsonParser()
if err := parser.Init([]byte(input)); err != nil {
t.Fatalf("init error: %v", err)
}
Expand Down Expand Up @@ -157,7 +156,7 @@ func TestValues(t *testing.T) {

func testWalk(t *testing.T, s string) {
t.Helper()
parser := jsonparser.NewJsonParser()
parser := NewJsonParser()
if err := parser.Init([]byte(s)); err != nil {
t.Error(err)
return
Expand Down
2 changes: 1 addition & 1 deletion json/jsontext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// license that can be found in the LICENSE file.

// Original these tests were copied from https://github.com/go-json-experiment/json/blob/master/jsontext/decode_test.go
package json_test
package json

import (
"testing"
Expand Down
52 changes: 52 additions & 0 deletions json/pool/basic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2025 Walter Schulze
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package pool

type pool struct {
free [][]byte
busy [][]byte
}

func New() Pool {
return &pool{
free: make([][]byte, 0),
busy: make([][]byte, 0),
}
}

func (p *pool) FreeAll() {
p.free = append(p.free, p.busy...)
p.busy = p.busy[:0]
}

func (p *pool) Alloc(size int) []byte {
for i := 0; i < len(p.free); i++ {
if len(p.free[i]) >= size {
buf := p.free[i]
p.free[i] = p.free[len(p.free)-1]
p.free = p.free[:len(p.free)-1]
p.busy = append(p.busy, buf)
return buf[:size]
}
}
// always allocate a big buffer, so hits when searching are very likely
buf := make([]byte, max(size*2, 1000))
p.busy = append(p.busy, buf)
return buf[:size]
}

func (p *pool) Size() int {
return len(p.free) + len(p.busy)
}
21 changes: 21 additions & 0 deletions json/pool/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2025 Walter Schulze
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package pool

type Pool interface {
FreeAll()
Alloc(size int) []byte
Size() int
}
31 changes: 31 additions & 0 deletions json/pool/none.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2025 Walter Schulze
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package pool

type none struct{}

func None() Pool {
return &none{}
}

func (p *none) FreeAll() {}

func (p *none) Alloc(size int) []byte {
return make([]byte, size)
}

func (p *none) Size() int {
return 0
}
Loading

0 comments on commit 847a1ce

Please sign in to comment.