1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
|
package graphql_test
import (
"context"
"fmt"
"testing"
graphql "github.com/graph-gophers/graphql-go"
)
// This benchmark compares query execution when resolvers do NOT call the
// selection helpers vs when they call SelectedFieldNames at object boundaries.
// It documents the lazy overhead of computing child field selections.
const lazyBenchSchema = `
schema { query: Query }
type Query { hero: Human }
type Human { id: ID! name: String friends: [Human!]! }
`
// Simple in-memory data graph.
type human struct {
id, name string
friends []*human
}
// Build a small graph once outside the benchmark loops.
var benchHero *human
func init() {
// Create 5 friends (no recursive friends to keep size stable).
friends := make([]*human, 5)
for i := range friends {
friends[i] = &human{id: fmt.Sprintf("F%d", i), name: "Friend"}
}
benchHero = &human{id: "H1", name: "Hero", friends: friends}
}
// Baseline resolvers (do NOT invoke selection helpers).
type (
rootBaseline struct{}
humanResolverBaseline struct{ h *human }
)
func (r *rootBaseline) Hero(ctx context.Context) *humanResolverBaseline {
return &humanResolverBaseline{h: benchHero}
}
func (h *humanResolverBaseline) ID() graphql.ID { return graphql.ID(h.h.id) }
func (h *humanResolverBaseline) Name() *string { return &h.h.name }
func (h *humanResolverBaseline) Friends(ctx context.Context) []*humanResolverBaseline {
out := make([]*humanResolverBaseline, len(h.h.friends))
for i, f := range h.h.friends {
out[i] = &humanResolverBaseline{h: f}
}
return out
}
// Instrumented resolvers (CALL selection helpers once per object-level resolver).
type (
rootWithSel struct{}
humanResolverWithSel struct{ h *human }
)
func (r *rootWithSel) Hero(ctx context.Context) *humanResolverWithSel {
// Selection list for hero object (id, name, friends)
_ = graphql.SelectedFieldNames(ctx)
return &humanResolverWithSel{h: benchHero}
}
func (h *humanResolverWithSel) ID(ctx context.Context) graphql.ID { // leaf: expecting empty slice
return graphql.ID(h.h.id)
}
func (h *humanResolverWithSel) Name(ctx context.Context) *string { // leaf
return &h.h.name
}
func (h *humanResolverWithSel) Friends(ctx context.Context) []*humanResolverWithSel {
// Selection list on list field: children of Human inside list items.
_ = graphql.SelectedFieldNames(ctx)
out := make([]*humanResolverWithSel, len(h.h.friends))
for i, f := range h.h.friends {
// For each friend object we also call once at the object resolver boundary.
out[i] = &humanResolverWithSel{h: f}
}
return out
}
// Query used for both benchmarks.
const lazyBenchQuery = `query { hero { id name friends { id name } } }`
func BenchmarkFieldSelections_NoUsage(b *testing.B) {
schema := graphql.MustParseSchema(lazyBenchSchema, &rootBaseline{})
ctx := context.Background()
b.ReportAllocs()
for b.Loop() {
_ = schema.Exec(ctx, lazyBenchQuery, "", nil)
}
}
func BenchmarkFieldSelections_Disabled_NoUsage(b *testing.B) {
schema := graphql.MustParseSchema(lazyBenchSchema, &rootBaseline{}, graphql.DisableFieldSelections())
ctx := context.Background()
b.ReportAllocs()
for b.Loop() {
_ = schema.Exec(ctx, lazyBenchQuery, "", nil)
}
}
func BenchmarkFieldSelections_WithSelectedFieldNames(b *testing.B) {
schema := graphql.MustParseSchema(lazyBenchSchema, &rootWithSel{})
ctx := context.Background()
b.ReportAllocs()
for b.Loop() {
_ = schema.Exec(ctx, lazyBenchQuery, "", nil)
}
}
|