Skip to content

Commit d792fe5

Browse files
authored
feat: expose timing of planning in the report (#2312)
1 parent 9f9faa0 commit d792fe5

File tree

5 files changed

+113
-41
lines changed

5 files changed

+113
-41
lines changed

router/core/plan_generator.go

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"net/http"
99
"os"
10+
"time"
1011

1112
"github.com/wundergraph/cosmo/router/pkg/metric"
1213

@@ -53,6 +54,26 @@ type Planner struct {
5354
operationValidator *astvalidation.OperationValidator
5455
}
5556

57+
type OperationTimes struct {
58+
ParseTime time.Duration
59+
NormalizeTime time.Duration
60+
ValidateTime time.Duration
61+
PlanTime time.Duration
62+
}
63+
64+
func (ot *OperationTimes) TotalTime() time.Duration {
65+
return ot.ParseTime + ot.NormalizeTime + ot.ValidateTime + ot.PlanTime
66+
}
67+
68+
func (ot OperationTimes) Merge(other OperationTimes) OperationTimes {
69+
return OperationTimes{
70+
ParseTime: ot.ParseTime + other.ParseTime,
71+
NormalizeTime: ot.NormalizeTime + other.NormalizeTime,
72+
ValidateTime: ot.ValidateTime + other.ValidateTime,
73+
PlanTime: ot.PlanTime + other.PlanTime,
74+
}
75+
}
76+
5677
type PlanOutputFormat string
5778

5879
const (
@@ -75,57 +96,74 @@ func NewPlanner(planConfiguration *plan.Configuration, definition *ast.Document,
7596
}
7697

7798
// PlanOperation creates a query plan from an operation file in a pretty-printed text or JSON format
78-
func (pl *Planner) PlanOperation(operationFilePath string, outputFormat PlanOutputFormat) (string, error) {
79-
operation, err := pl.ParseAndPrepareOperation(operationFilePath)
99+
func (pl *Planner) PlanOperation(operationFilePath string, outputFormat PlanOutputFormat) (string, OperationTimes, error) {
100+
operation, opTimes, err := pl.ParseAndPrepareOperation(operationFilePath)
80101
if err != nil {
81-
return "", err
102+
return "", opTimes, err
82103
}
83104

84-
rawPlan, err := pl.PlanPreparedOperation(operation)
105+
rawPlan, opTimes2, err := pl.PlanPreparedOperation(operation)
106+
opTimes = opTimes.Merge(opTimes2)
85107
if err != nil {
86-
return "", fmt.Errorf("failed to plan operation: %w", err)
108+
return "", opTimes, fmt.Errorf("failed to plan operation: %w", err)
87109
}
88110

89111
switch outputFormat {
90112
case PlanOutputFormatText:
91-
return rawPlan.PrettyPrint(), nil
113+
return rawPlan.PrettyPrint(), opTimes, nil
92114
case PlanOutputFormatJSON:
93115
marshal, err := json.Marshal(rawPlan)
94116
if err != nil {
95-
return "", fmt.Errorf("failed to marshal raw plan: %w", err)
117+
return "", opTimes, fmt.Errorf("failed to marshal raw plan: %w", err)
96118
}
97-
return string(marshal), nil
119+
return string(marshal), opTimes, nil
98120
}
99121

100-
return "", fmt.Errorf("invalid outputFormat specified: %q", outputFormat)
122+
return "", opTimes, fmt.Errorf("invalid outputFormat specified: %q", outputFormat)
101123
}
102124

103125
// ParseAndPrepareOperation parses, normalizes and validates the operation
104-
func (pl *Planner) ParseAndPrepareOperation(operationFilePath string) (*ast.Document, error) {
126+
func (pl *Planner) ParseAndPrepareOperation(operationFilePath string) (*ast.Document, OperationTimes, error) {
127+
start := time.Now()
105128
operation, err := pl.parseOperation(operationFilePath)
129+
parseTime := time.Since(start)
106130
if err != nil {
107-
return nil, &PlannerOperationValidationError{err: err}
131+
return nil, OperationTimes{ParseTime: parseTime}, &PlannerOperationValidationError{err: err}
108132
}
109133

110-
return pl.PrepareOperation(operation)
134+
operation, opTimes, err := pl.PrepareOperation(operation)
135+
opTimes.ParseTime = parseTime
136+
if err != nil {
137+
return nil, opTimes, err
138+
}
139+
140+
return operation, opTimes, nil
111141
}
112142

113143
// PrepareOperation normalizes and validates the operation
114-
func (pl *Planner) PrepareOperation(operation *ast.Document) (*ast.Document, error) {
144+
func (pl *Planner) PrepareOperation(operation *ast.Document) (*ast.Document, OperationTimes, error) {
115145
operationName := findOperationName(operation)
116146
if operationName == nil {
117-
return nil, &PlannerOperationValidationError{err: errors.New("operation name not found")}
147+
return nil, OperationTimes{}, &PlannerOperationValidationError{err: errors.New("operation name not found")}
118148
}
119149

120-
if err := pl.normalizeOperation(operation, operationName); err != nil {
121-
return nil, &PlannerOperationValidationError{err: err}
150+
opTimes := OperationTimes{}
151+
152+
start := time.Now()
153+
err := pl.normalizeOperation(operation, operationName)
154+
opTimes.NormalizeTime = time.Since(start)
155+
if err != nil {
156+
return nil, opTimes, &PlannerOperationValidationError{err: err}
122157
}
123158

124-
if err := pl.validateOperation(operation); err != nil {
125-
return nil, &PlannerOperationValidationError{err: err}
159+
start = time.Now()
160+
err = pl.validateOperation(operation)
161+
opTimes.ValidateTime = time.Since(start)
162+
if err != nil {
163+
return nil, opTimes, &PlannerOperationValidationError{err: err}
126164
}
127165

128-
return operation, nil
166+
return operation, opTimes, nil
129167
}
130168

131169
func (pl *Planner) normalizeOperation(operation *ast.Document, operationName []byte) (err error) {
@@ -160,7 +198,7 @@ func (pl *Planner) normalizeOperation(operation *ast.Document, operationName []b
160198
}
161199

162200
// PlanPreparedOperation creates a query plan from a normalized and validated operation
163-
func (pl *Planner) PlanPreparedOperation(operation *ast.Document) (planNode *resolve.FetchTreeQueryPlanNode, err error) {
201+
func (pl *Planner) PlanPreparedOperation(operation *ast.Document) (planNode *resolve.FetchTreeQueryPlanNode, opTimes OperationTimes, err error) {
164202
defer func() {
165203
if r := recover(); r != nil {
166204
err = fmt.Errorf("panic during plan generation: %v", r)
@@ -172,25 +210,30 @@ func (pl *Planner) PlanPreparedOperation(operation *ast.Document) (planNode *res
172210
operationName := findOperationName(operation)
173211

174212
if operationName == nil {
175-
return nil, errors.New("operation name not found")
213+
return nil, opTimes, errors.New("operation name not found")
176214
}
177215

178216
// create and postprocess the plan
217+
start := time.Now()
179218
preparedPlan := pl.planner.Plan(operation, pl.definition, string(operationName), &report, plan.IncludeQueryPlanInResponse())
219+
opTimes.PlanTime = time.Since(start)
180220
if report.HasErrors() {
181-
return nil, errors.New(report.Error())
221+
return nil, opTimes, errors.New(report.Error())
182222
}
223+
183224
post := postprocess.NewProcessor()
184225
post.Process(preparedPlan)
226+
// measure postprocessing time as part of planning time
227+
opTimes.PlanTime = time.Since(start)
185228

186229
switch p := preparedPlan.(type) {
187230
case *plan.SynchronousResponsePlan:
188-
return p.Response.Fetches.QueryPlan(), nil
231+
return p.Response.Fetches.QueryPlan(), opTimes, nil
189232
case *plan.SubscriptionResponsePlan:
190-
return p.Response.Response.Fetches.QueryPlan(), nil
233+
return p.Response.Response.Fetches.QueryPlan(), opTimes, nil
191234
}
192235

193-
return &resolve.FetchTreeQueryPlanNode{}, nil
236+
return &resolve.FetchTreeQueryPlanNode{}, opTimes, nil
194237
}
195238

196239
func (pl *Planner) validateOperation(operation *ast.Document) (err error) {

router/core/plan_generator_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func TestPlanOperationPanic(t *testing.T) {
2929
}
3030

3131
assert.NotPanics(t, func() {
32-
_, err = planner.PlanPreparedOperation(invalidOperation)
32+
_, _, err = planner.PlanPreparedOperation(invalidOperation)
3333
assert.Error(t, err)
3434
})
3535
}

router/internal/planningbenchmark/benchmark_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ func TestPlanning(t *testing.T) {
3636
pl, err := pg.GetPlanner()
3737
require.NoError(t, err)
3838

39-
opDoc, err := pl.ParseAndPrepareOperation(cfg.OperationPath)
39+
opDoc, _, err := pl.ParseAndPrepareOperation(cfg.OperationPath)
4040
require.NoError(t, err)
4141

4242
start := time.Now()
43-
p, err := pl.PlanPreparedOperation(opDoc)
43+
p, _, err := pl.PlanPreparedOperation(opDoc)
4444
require.NoError(t, err)
4545
t.Logf("Planning completed in %v", time.Since(start))
4646

@@ -69,12 +69,12 @@ func BenchmarkPlanning(b *testing.B) {
6969

7070
for b.Loop() {
7171
b.StopTimer()
72-
opDoc, err := pl.ParseAndPrepareOperation(cfg.OperationPath)
72+
opDoc, _, err := pl.ParseAndPrepareOperation(cfg.OperationPath)
7373
require.NoError(b, err)
7474
b.SetBytes(int64(len(opDoc.Input.RawBytes)))
7575
b.StartTimer()
7676

77-
_, err = pl.PlanPreparedOperation(opDoc)
77+
_, _, err = pl.PlanPreparedOperation(opDoc)
7878
require.NoError(b, err)
7979
}
8080
}

router/pkg/plan_generator/plan_generator.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ type QueryPlanResults struct {
4242
}
4343

4444
type QueryPlanResult struct {
45-
FileName string `json:"file_name,omitempty"`
46-
Plan string `json:"plan,omitempty"`
47-
Error string `json:"error,omitempty"`
48-
Warning string `json:"warning,omitempty"`
45+
FileName string `json:"file_name,omitempty"`
46+
Plan string `json:"plan,omitempty"`
47+
Error string `json:"error,omitempty"`
48+
Warning string `json:"warning,omitempty"`
49+
Timings core.OperationTimes `json:"timings,omitempty"`
4950
}
5051

5152
func PlanGenerator(ctx context.Context, cfg QueryPlanConfig) error {
@@ -145,10 +146,11 @@ func PlanGenerator(ctx context.Context, cfg QueryPlanConfig) error {
145146

146147
queryFilePath := filepath.Join(queriesPath, queryFile.Name())
147148

148-
outContent, err := planner.PlanOperation(queryFilePath, cfg.OutputFormat)
149+
outContent, opTimes, err := planner.PlanOperation(queryFilePath, cfg.OutputFormat)
149150
res := QueryPlanResult{
150-
FileName: queryFile.Name(),
151-
Plan: outContent,
151+
FileName: queryFile.Name(),
152+
Plan: outContent,
153+
Timings: opTimes,
152154
}
153155
if err != nil {
154156
if _, ok := err.(*core.PlannerOperationValidationError); ok {

router/pkg/plan_generator/plan_generator_test.go

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ package plan_generator
33
import (
44
"context"
55
"encoding/json"
6-
"github.com/wundergraph/cosmo/router/core"
76
"os"
87
"path"
98
"path/filepath"
109
"runtime"
1110
"strings"
1211
"testing"
1312

13+
"github.com/wundergraph/cosmo/router/core"
14+
1415
"github.com/stretchr/testify/assert"
1516
"github.com/stretchr/testify/require"
1617
"go.uber.org/zap"
@@ -210,7 +211,16 @@ func TestPlanGenerator(t *testing.T) {
210211
assert.NoError(t, err)
211212
resultsExpected, err := os.ReadFile(path.Join(getTestDataDir(), "plans", "base", ReportFileName))
212213
assert.NoError(t, err)
213-
assert.Equal(t, string(resultsExpected), string(results))
214+
resultsStruct := QueryPlanResults{}
215+
json.Unmarshal(results, &resultsStruct)
216+
resultsExpectedStruct := QueryPlanResults{}
217+
json.Unmarshal(resultsExpected, &resultsExpectedStruct)
218+
require.Len(t, resultsStruct.Plans, len(resultsExpectedStruct.Plans))
219+
for i := range resultsStruct.Plans {
220+
assert.Equal(t, resultsStruct.Plans[i].Plan, resultsExpectedStruct.Plans[i].Plan)
221+
assert.Equal(t, resultsStruct.Plans[i].Error, resultsExpectedStruct.Plans[i].Error)
222+
assert.Equal(t, resultsStruct.Plans[i].Warning, resultsExpectedStruct.Plans[i].Warning)
223+
}
214224
})
215225

216226
t.Run("will not fail on warnings and results should return the warnings and generate results file", func(t *testing.T) {
@@ -241,7 +251,16 @@ func TestPlanGenerator(t *testing.T) {
241251
assert.NoError(t, err)
242252
resultsExpected, err := os.ReadFile(path.Join(getTestDataDir(), "plans", "base", ReportFileName))
243253
assert.NoError(t, err)
244-
assert.Equal(t, string(resultsExpected), string(results))
254+
resultsStruct := QueryPlanResults{}
255+
json.Unmarshal(results, &resultsStruct)
256+
resultsExpectedStruct := QueryPlanResults{}
257+
json.Unmarshal(resultsExpected, &resultsExpectedStruct)
258+
require.Len(t, resultsStruct.Plans, len(resultsExpectedStruct.Plans))
259+
for i := range resultsStruct.Plans {
260+
assert.Equal(t, resultsStruct.Plans[i].Plan, resultsExpectedStruct.Plans[i].Plan)
261+
assert.Equal(t, resultsStruct.Plans[i].Error, resultsExpectedStruct.Plans[i].Error)
262+
assert.Equal(t, resultsStruct.Plans[i].Warning, resultsExpectedStruct.Plans[i].Warning)
263+
}
245264
})
246265

247266
t.Run("will not fail on warnings and files should have warnings and generate files", func(t *testing.T) {
@@ -270,7 +289,15 @@ func TestPlanGenerator(t *testing.T) {
270289
assert.NoError(t, err)
271290
expected, err := os.ReadFile(path.Join(getTestDataDir(), "plans", "base", filename))
272291
assert.NoError(t, err)
273-
assert.Equal(t, string(expected), string(queryPlan))
292+
resultsStruct := QueryPlanResults{}
293+
json.Unmarshal(queryPlan, &resultsStruct)
294+
resultsExpectedStruct := QueryPlanResults{}
295+
json.Unmarshal(expected, &resultsExpectedStruct)
296+
for i := range resultsStruct.Plans {
297+
assert.Equal(t, resultsStruct.Plans[i].Plan, resultsExpectedStruct.Plans[i].Plan)
298+
assert.Equal(t, resultsStruct.Plans[i].Error, resultsExpectedStruct.Plans[i].Error)
299+
assert.Equal(t, resultsStruct.Plans[i].Warning, resultsExpectedStruct.Plans[i].Warning)
300+
}
274301
})
275302
}
276303
})

0 commit comments

Comments
 (0)