Skip to content

Commit

Permalink
feat: improve performance and memory usage of loader & resolbable (#851)
Browse files Browse the repository at this point in the history
  • Loading branch information
jensneuse authored Jul 17, 2024
1 parent 699eb81 commit 27670b7
Show file tree
Hide file tree
Showing 17 changed files with 902 additions and 554 deletions.
2 changes: 1 addition & 1 deletion execution/engine/execution_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,7 @@ func TestExecutionEngine_Execute(t *testing.T) {
},
},
},
expectedResponse: `{"errors":[{"message":"Failed to fetch from Subgraph at Path 'query', Reason: no data or errors in response."},{"message":"Cannot return null for non-nullable field 'Query.hero'.","path":["hero"]}],"data":null}`,
expectedResponse: `{"errors":[{"message":"Failed to fetch from Subgraph at Path 'query', Reason: invalid JSON."}],"data":null}`,
}))

t.Run("execute operation and apply input coercion for lists without variables", runWithoutError(ExecutionEngineTestCase{
Expand Down
2 changes: 2 additions & 0 deletions go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer5
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/barkyq/fastjson v0.0.0-20230118153732-bb1076612fd9 h1:gsI0tqI5IvQ3xIH4oXttNu0EMcBEUR+1RmfgayyGjVE=
github.com/barkyq/fastjson v0.0.0-20230118153732-bb1076612fd9/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
Expand Down
4 changes: 4 additions & 0 deletions v2/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/buger/jsonparser v1.1.1
github.com/cespare/xxhash/v2 v2.2.0
github.com/davecgh/go-spew v1.1.1
github.com/goccy/go-json v0.10.2
github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.6.0
Expand All @@ -24,6 +25,7 @@ require (
github.com/stretchr/testify v1.9.0
github.com/tidwall/gjson v1.17.0
github.com/tidwall/sjson v1.2.5
github.com/valyala/fastjson v1.6.4
github.com/vektah/gqlparser/v2 v2.5.11
go.uber.org/atomic v1.11.0
go.uber.org/zap v1.26.0
Expand Down Expand Up @@ -68,3 +70,5 @@ require (
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace github.com/valyala/fastjson v1.6.4 => github.com/barkyq/fastjson v0.0.0-20230118153732-bb1076612fd9
2 changes: 2 additions & 0 deletions v2/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/barkyq/fastjson v0.0.0-20230118153732-bb1076612fd9 h1:gsI0tqI5IvQ3xIH4oXttNu0EMcBEUR+1RmfgayyGjVE=
github.com/barkyq/fastjson v0.0.0-20230118153732-bb1076612fd9/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
Expand Down
2 changes: 1 addition & 1 deletion v2/pkg/astjson/astjson.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ func (j *JSON) ParseArray(input []byte) (err error) {

func (j *JSON) AppendAnyJSONBytes(input []byte) (ref int, err error) {
if j.storage == nil {
j.storage = make([]byte, 0, 4*1024)
j.storage = make([]byte, 0, len(input))
}
start := len(j.storage)
j.storage = append(j.storage, input...)
Expand Down
116 changes: 116 additions & 0 deletions v2/pkg/astjson/astjson_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (

"github.com/buger/jsonparser"
"github.com/stretchr/testify/assert"
"github.com/valyala/fastjson"
"github.com/wundergraph/graphql-go-tools/v2/pkg/fastjsonext"
)

func TestJSON_ParsePrint(t *testing.T) {
Expand Down Expand Up @@ -401,6 +403,116 @@ func BenchmarkJSON_ParsePrint(b *testing.B) {
}
}

func BenchmarkFastJSON(b *testing.B) {
var p fastjson.Parser
input := []byte(`{"data":{"_entities":[{"stock":8},{"stock":2},{"stock":5}]}}`)
expectedOut := []byte(`{"_entities":[{"stock":8},{"stock":2},{"stock":5}]}`)
res := make([]byte, 0, 1024)
b.SetBytes(int64(len(input)))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
v, err := p.ParseBytes(input)
if err != nil {
b.Fatal(err)
}
out := v.Get("data")
res = out.MarshalTo(res)
if !bytes.Equal(expectedOut, res) {
b.Fatal("not equal")
}
res = res[:0]
}
}

func TestFastJsonMerge(t *testing.T) {
a, err := fastjson.ParseBytes([]byte(`{"a":1,"b":2}`))
assert.NoError(t, err)
b, err := fastjson.ParseBytes([]byte(`{"c":3}`))
assert.NoError(t, err)
merged, _ := fastjsonext.MergeValues(a, b)
out := merged.MarshalTo(nil)
assert.Equal(t, `{"a":1,"b":2,"c":3}`, string(out))
}

func TestFastJsonMergeNested(t *testing.T) {
a, err := fastjson.ParseBytes([]byte(`{"a":1,"b":2,"c":{"d":4,"e":4}}`))
assert.NoError(t, err)
b, err := fastjson.ParseBytes([]byte(`{"c":{"e":5}}`))
assert.NoError(t, err)
merged, _ := fastjsonext.MergeValues(a, b)
out := merged.MarshalTo(nil)
assert.Equal(t, `{"a":1,"b":2,"c":{"d":4,"e":5}}`, string(out))
}

func BenchmarkFastParse(b *testing.B) {
var p fastjson.Parser

b.SetBytes(int64(len(bigJSON)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v, err := p.ParseBytes(bigJSON)
if err != nil {
b.Fatal(err)
}
if v == nil {
b.Fatal("nil")
}
}
}

func BenchmarkParse(b *testing.B) {
fs := &JSON{}
b.SetBytes(int64(len(bigJSON)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
fs.Reset()
ref, err := fs.AppendAnyJSONBytes(bigJSON)
if err != nil {
b.Fatal(err)
}
if ref == -1 {
b.Fatal("nil")
}
}
}

func BenchmarkFastJsonMerge(t *testing.B) {
var (
p1, p2, p3 fastjson.Parser
out = make([]byte, 0, 1024)
)
first := []byte(`{"a":1,"b":2,"c":{"d":4,"e":5,"f":6,"g":7,"h":8,"i":9,"j":10,"k":11,"l":12,"m":13,"n":14,"o":15,"p":16,"q":17,"r":18,"s":19,"t":20,"u":21,"v":22,"w":23,"x":24,"y":25,"z":26}}`)
second := []byte(`{"c":{"e":5,"f":6,"g":7,"h":8,"i":9,"j":10,"k":11,"l":12,"m":13,"n":14,"o":15,"p":16,"q":17,"r":18,"s":19,"t":20,"u":21,"v":22,"w":23,"x":24,"y":25,"z":26}}`)
third := []byte(`{"c":{"e":6,"f":7,"g":8,"h":9,"i":10,"j":11,"k":true,"l":13,"m":"Cosmo Rocks!","n":15,"o":16,"p":17,"q":18,"r":19,"s":20,"t":21,"u":22,"v":23,"w":24,"x":25,"y":26,"z":28}}`)
expected := []byte(`{"a":1,"b":2,"c":{"d":4,"e":6,"f":7,"g":8,"h":9,"i":10,"j":11,"k":11,"l":13,"m":13,"n":15,"o":16,"p":17,"q":18,"r":19,"s":20,"t":21,"u":22,"v":23,"w":24,"x":25,"y":26,"z":28}}`)
t.SetBytes(int64(len(first) + len(second) + len(third)))
t.ReportAllocs()
t.ResetTimer()
for i := 0; i < t.N; i++ {
a, err := p1.ParseBytes(first)
if err != nil {
t.Fatal(err)
}
b, err := p2.ParseBytes(second)
if err != nil {
t.Fatal(err)
}
c, err := p3.ParseBytes(third)
if err != nil {
t.Fatal(err)
}
ab, _ := fastjsonext.MergeValues(a, b)
abc, _ := fastjsonext.MergeValues(ab, c)
out = abc.MarshalTo(out[:0])
if !bytes.Equal(expected, out) {
t.Fatal("not equal")
}

}
}

func BenchmarkJSON_MergeNodesNested(b *testing.B) {
js := &JSON{}
first := []byte(`{"a":1,"b":2,"c":{"d":4,"e":5,"f":6,"g":7,"h":8,"i":9,"j":10,"k":11,"l":12,"m":13,"n":14,"o":15,"p":16,"q":17,"r":18,"s":19,"t":20,"u":21,"v":22,"w":23,"x":24,"y":25,"z":26}}`)
Expand Down Expand Up @@ -467,3 +579,7 @@ func BenchmarkJSON_MergeNodesWithPath(b *testing.B) {
}
}
}

var (
bigJSON = []byte(`{"data":{"employees":[{"id":1,"details":{"forename":"Jens","surname":"Neuse"}},{"id":2,"details":{"forename":"Dustin","surname":"Deus"}},{"id":3,"details":{"forename":"Stefan","surname":"Avram"}},{"id":4,"details":{"forename":"Björn","surname":"Schwenzer"}},{"id":5,"details":{"forename":"Sergiy","surname":"Petrunin"}},{"id":7,"details":{"forename":"Suvij","surname":"Surya"}},{"id":8,"details":{"forename":"Nithin","surname":"Kumar"}},{"id":10,"details":{"forename":"Eelco","surname":"Wiersma"}},{"id":11,"details":{"forename":"Alexandra","surname":"Neuse"}},{"id":12,"details":{"forename":"David","surname":"Stutt"}}]}}`)
)
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ var (
SelectResponseErrorsPath: []string{"errors"},
}
SingleEntityPostProcessingConfiguration = resolve.PostProcessingConfiguration{
SelectResponseDataPath: []string{"data", "_entities", "[0]"},
SelectResponseDataPath: []string{"data", "_entities", "0"},
SelectResponseErrorsPath: []string{"errors"},
}
)
Expand Down
26 changes: 25 additions & 1 deletion v2/pkg/engine/datasource/httpclient/nethttpclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

"github.com/buger/jsonparser"
"github.com/wundergraph/graphql-go-tools/v2/pkg/lexer/literal"
"github.com/wundergraph/graphql-go-tools/v2/pkg/pool"
)

const (
Expand Down Expand Up @@ -132,7 +133,18 @@ func releaseBuffer(buf *bytes.Buffer) {
requestBufferPool.Put(buf)
}

type bodyHashContextKey struct{}

func BodyHashFromContext(ctx context.Context) (uint64, bool) {
value := ctx.Value(bodyHashContextKey{})
if value == nil {
return 0, false
}
return value.(uint64), true
}

func makeHTTPRequest(client *http.Client, ctx context.Context, url, method, headers, queryParams []byte, body io.Reader, enableTrace bool, out *bytes.Buffer, contentType string) (err error) {

request, err := http.NewRequestWithContext(ctx, string(method), string(url), body)
if err != nil {
return err
Expand Down Expand Up @@ -243,7 +255,11 @@ func makeHTTPRequest(client *http.Client, ctx context.Context, url, method, head

func Do(client *http.Client, ctx context.Context, requestInput []byte, out *bytes.Buffer) (err error) {
url, method, body, headers, queryParams, enableTrace := requestInputParams(requestInput)

h := pool.Hash64.Get()
_, _ = h.Write(body)
bodyHash := h.Sum64()
pool.Hash64.Put(h)
ctx = context.WithValue(ctx, bodyHashContextKey{}, bodyHash)
return makeHTTPRequest(client, ctx, url, method, headers, queryParams, bytes.NewReader(body), enableTrace, out, ContentTypeJSON)
}

Expand All @@ -256,6 +272,10 @@ func DoMultipartForm(

url, method, body, headers, queryParams, enableTrace := requestInputParams(requestInput)

h := pool.Hash64.Get()
defer pool.Hash64.Put(h)
_, _ = h.Write(body)

formValues := map[string]io.Reader{
"operations": bytes.NewReader(body),
}
Expand All @@ -273,6 +293,7 @@ func DoMultipartForm(
fileMap = fmt.Sprintf(`%s, "%d" : ["variables.files.%d"]`, fileMap, i, i)
}
key := fmt.Sprintf("%d", i)
_, _ = h.WriteString(file.Path())
temporaryFile, err := os.Open(file.Path())
tempFiles = append(tempFiles, temporaryFile)
if err != nil {
Expand All @@ -299,6 +320,9 @@ func DoMultipartForm(
}
}()

bodyHash := h.Sum64()
ctx = context.WithValue(ctx, bodyHashContextKey{}, bodyHash)

return makeHTTPRequest(client, ctx, url, method, headers, queryParams, multipartBody, enableTrace, out, contentType)
}

Expand Down
15 changes: 8 additions & 7 deletions v2/pkg/engine/resolve/authorization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import (
"context"
"encoding/json"
"errors"
"github.com/stretchr/testify/require"
"io"
"sync/atomic"
"testing"

"github.com/stretchr/testify/require"

"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -87,19 +88,19 @@ func TestAuthorization(t *testing.T) {
return nil, nil
}, func(ctx *Context, dataSourceID string, object json.RawMessage, coordinate GraphCoordinate) (result *AuthorizationDeny, err error) {
if dataSourceID == "reviews" && coordinate.TypeName == "User" && coordinate.FieldName == "reviews" {
assert.Equal(t, `{"id":"1234","username":"Me","__typename":"User"}`, string(object))
assert.Equal(t, `{"id":"1234","username":"Me","__typename":"User","reviews":[{"body":"A highly effective form of birth control.","product":{"upc":"top-1","__typename":"Product","data":{"name":"Trilby"}}},{"body":"Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.","product":{"upc":"top-2","__typename":"Product","data":{"name":"Fedora"}}}]}`, string(object))
assertions.Add(1)
}
if dataSourceID == "reviews" && coordinate.TypeName == "Review" && coordinate.FieldName == "body" {
assert.Equal(t, `{"body":"A highly effective form of birth control."}`, string(object))
assert.Equal(t, `{"body":"A highly effective form of birth control.","product":{"upc":"top-1","__typename":"Product","data":{"name":"Trilby"}}}`, string(object))
assertions.Add(1)
}
if dataSourceID == "reviews" && coordinate.TypeName == "Review" && coordinate.FieldName == "product" {
assert.Equal(t, `{"body":"A highly effective form of birth control."}`, string(object))
assert.Equal(t, `{"body":"A highly effective form of birth control.","product":{"upc":"top-1","__typename":"Product","data":{"name":"Trilby"}}}`, string(object))
assertions.Add(1)
}
if dataSourceID == "products" && coordinate.TypeName == "Product" && coordinate.FieldName == "name" {
assert.Equal(t, `{"upc":"top-1","__typename":"Product"}`, string(object))
assert.Equal(t, `{"upc":"top-1","__typename":"Product","data":{"name":"Trilby"}}`, string(object))
assertions.Add(1)
}
return nil, nil
Expand Down Expand Up @@ -621,7 +622,7 @@ func generateTestFederationGraphQLResponse(t *testing.T, ctrl *gomock.Controller
DataSource: reviewsService,
PostProcessing: PostProcessingConfiguration{
SelectResponseErrorsPath: []string{"errors"},
SelectResponseDataPath: []string{"data", "_entities", "[0]"},
SelectResponseDataPath: []string{"data", "_entities", "0"},
},
},
},
Expand Down Expand Up @@ -907,7 +908,7 @@ func generateTestFederationGraphQLResponseWithoutAuthorizationRules(t *testing.T
FetchConfiguration: FetchConfiguration{
DataSource: reviewsService,
PostProcessing: PostProcessingConfiguration{
SelectResponseDataPath: []string{"data", "_entities", "[0]"},
SelectResponseDataPath: []string{"data", "_entities", "0"},
},
},
},
Expand Down
Loading

0 comments on commit 27670b7

Please sign in to comment.