Skip to content

Commit 24c1f32

Browse files
lawrencejonesisaacseymour
authored andcommitted
Optimize code generation with parallel file writing
tl;dr: make codegen much faster through parallel writing of files We have a _very large_ API design, with over 100 services, which generates almost 3k files with `goa generate`. As our API has grown, running `goa generate` has got really slow. This commit adds timing data to the `--debug` output. Then, the lowest-hanging-fruit optimisation has been applied: writing all the output files in parallel. --- As a recap, the generation process has several stages: 1. **Compile temporary binary**: Goa creates a temporary Go program that imports the design package (which triggers package initialization and DSL execution via blank import) 2. **Execute binary**: The binary runs through multiple phases: - Package initialization (runs DSL definitions) - `eval.RunDSL()` - processes the DSL in 4 phases (execute, prepare, validate, finalize) - `generator.Generate()` - produces the actual Go files Measuring codegen for the incident-io codebase on 3,036 generated files: **Total time: 61 seconds** Breakdown: - build.Import: 117ms - NewGenerator (packages.Load): 52ms - Write (generate main.go): 14ms - Compile (go get + go build): 3.6s - packages.Load: 47ms - go get: 514ms - go build: 3.0s - **Run (execute binary): 52.2s** ⚠️ 85% of total time - Check eval.Context.Errors: <1ms - eval.RunDSL(): 105ms - **generator.Generate(): 51.6s** ⚠️ - Stage 1 (Compute design roots): <1ms - Stage 2 (Compute gen package): 33ms - Stage 3 (Retrieve generators): <1ms - Stage 4 (Pre-generation plugins): <1ms - **Stage 5 (Generate files): 26.2s** (3 generators, sequential) - Generator 0: 7.2s → 1,438 files - Generator 1: 18.8s → 1,594 files - Generator 2: 0.2s → 4 files - Stage 6 (Post-generation plugins): <1ms - **Stage 7 (Write files): 32.1s** ⚠️ Biggest bottleneck (52% of generation) - Stage 8 (Compute filenames): 2ms This commit tries optimising the biggest stage of this process and changes file rendering from sequential loop to parallel worker pool: ```go // Before: Sequential (32.1s for 3,036 files) for _, f := range genfiles { filename, err := f.Render(dir) // ... } // After: Parallel with runtime.NumCPU() workers numWorkers := runtime.NumCPU() // Worker pool processes files concurrently ``` Which changed execution time from **61 seconds total** to **34 seconds total**, for an overall speedup of 1.8x. While here, I also tried parallelising Stage 5 (generator functions) but hit infinite recursion in `AsObject()` when handling circular type references concurrently. This is where I'd go for the next biggest speed-up.
1 parent aabae26 commit 24c1f32

File tree

6 files changed

+239
-35
lines changed

6 files changed

+239
-35
lines changed

cmd/goa/gen.go

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"runtime"
1414
"strconv"
1515
"strings"
16+
"time"
1617

1718
"goa.design/goa/v3/codegen"
1819
"golang.org/x/tools/go/packages"
@@ -44,7 +45,7 @@ type Generator struct {
4445
}
4546

4647
// NewGenerator creates a Generator.
47-
func NewGenerator(cmd, path, output string) *Generator {
48+
func NewGenerator(cmd, path, output string, debug bool) *Generator {
4849
bin := "goa"
4950
if runtime.GOOS == "windows" {
5051
bin += ".exe"
@@ -55,7 +56,11 @@ func NewGenerator(cmd, path, output string) *Generator {
5556
{
5657
version = 2
5758
matched := false
59+
startPkgLoad := time.Now()
5860
pkgs, _ := packages.Load(&packages.Config{Mode: packages.NeedFiles | packages.NeedModule}, path)
61+
if debug {
62+
fmt.Fprintf(os.Stderr, "[TIMING] packages.Load (design files) took %v\n", time.Since(startPkgLoad))
63+
}
5964
fset := token.NewFileSet()
6065
p := regexp.MustCompile(`goa.design/goa/v(\d+)/dsl`)
6166
for _, pkg := range pkgs {
@@ -132,6 +137,7 @@ func (g *Generator) Write(_ bool) error {
132137
codegen.SimpleImport("sort"),
133138
codegen.SimpleImport("strconv"),
134139
codegen.SimpleImport("strings"),
140+
codegen.SimpleImport("time"),
135141
codegen.SimpleImport("goa.design/goa/" + ver + "codegen"),
136142
codegen.SimpleImport("goa.design/goa/" + ver + "codegen/generator"),
137143
codegen.SimpleImport("goa.design/goa/" + ver + "eval"),
@@ -154,23 +160,36 @@ func (g *Generator) Write(_ bool) error {
154160
}
155161

156162
// Compile compiles the generator.
157-
func (g *Generator) Compile() error {
163+
func (g *Generator) Compile(debug bool) error {
158164
// We first need to go get the generated package to make sure that all
159165
// dependencies are added to go.sum prior to compiling.
166+
startLoad := time.Now()
160167
pkgs, err := packages.Load(&packages.Config{Mode: packages.NeedName}, fmt.Sprintf(".%c%s", filepath.Separator, g.tmpDir))
161168
if err != nil {
162169
return err
163170
}
164171
if len(pkgs) != 1 {
165172
return fmt.Errorf("expected to find one package in %s", g.tmpDir)
166173
}
174+
if debug {
175+
fmt.Fprintf(os.Stderr, "[TIMING] packages.Load (temp dir) took %v\n", time.Since(startLoad))
176+
}
177+
167178
if !g.hasVendorDirectory {
179+
startGet := time.Now()
168180
if err := g.runGoCmd("get", pkgs[0].PkgPath); err != nil {
169181
return err
170182
}
183+
if debug {
184+
fmt.Fprintf(os.Stderr, "[TIMING] go get took %v\n", time.Since(startGet))
185+
}
171186
}
172187

188+
startBuild := time.Now()
173189
err = g.runGoCmd("build", "-o", g.bin)
190+
if debug {
191+
fmt.Fprintf(os.Stderr, "[TIMING] go build took %v\n", time.Since(startBuild))
192+
}
174193

175194
// If we're in vendor context we check the error string to see if it's an issue of unsatisfied dependencies
176195
if err != nil && g.hasVendorDirectory {
@@ -183,7 +202,7 @@ func (g *Generator) Compile() error {
183202
}
184203

185204
// Run runs the compiled binary and return the output lines.
186-
func (g *Generator) Run() ([]string, error) {
205+
func (g *Generator) Run(debug bool) ([]string, error) {
187206
var cmdl string
188207
{
189208
args := make([]string, len(os.Args)-1)
@@ -210,7 +229,7 @@ func (g *Generator) Run() ([]string, error) {
210229
cmdl = fmt.Sprintf("$ %s%s", rawcmd, cmdl)
211230
}
212231

213-
args := []string{"--version=" + strconv.Itoa(g.DesignVersion), "--output=" + g.Output, "--cmd=" + cmdl}
232+
args := []string{"--version=" + strconv.Itoa(g.DesignVersion), "--output=" + g.Output, "--cmd=" + cmdl, "--debug=" + strconv.FormatBool(debug)}
214233
cmd := exec.Command(filepath.Join(g.tmpDir, g.bin), args...)
215234
out, err := cmd.CombinedOutput()
216235
if err != nil {
@@ -291,6 +310,7 @@ const mainT = `func main() {
291310
out = flag.String("output", "", "")
292311
version = flag.String("version", "", "")
293312
cmdl = flag.String("cmd", "", "")
313+
debug = flag.Bool("debug", false, "")
294314
ver int
295315
)
296316
{
@@ -311,15 +331,31 @@ const mainT = `func main() {
311331
ver = v
312332
}
313333
334+
startBinary := time.Now()
335+
if *debug {
336+
fmt.Fprintf(os.Stderr, "[TIMING] [binary] Starting generated binary execution\n")
337+
}
338+
314339
if ver > goa.Major {
315340
fail("cannot run goa %s on design using goa v%s\n", goa.Version(), *version)
316341
}
342+
343+
startCheckErrors := time.Now()
317344
if err := eval.Context.Errors; err != nil {
318345
fail(err.Error())
319346
}
347+
if *debug {
348+
fmt.Fprintf(os.Stderr, "[TIMING] [binary] Check eval.Context.Errors took %v\n", time.Since(startCheckErrors))
349+
}
350+
351+
startRunDSL := time.Now()
320352
if err := eval.RunDSL(); err != nil {
321353
fail(err.Error())
322354
}
355+
if *debug {
356+
fmt.Fprintf(os.Stderr, "[TIMING] [binary] eval.RunDSL() took %v\n", time.Since(startRunDSL))
357+
}
358+
323359
{{- range .CleanupDirs }}
324360
if err := os.RemoveAll({{ printf "%q" . }}); err != nil {
325361
fail(err.Error())
@@ -328,11 +364,16 @@ const mainT = `func main() {
328364
{{- if gt .DesignVersion 2 }}
329365
codegen.DesignVersion = ver
330366
{{- end }}
331-
outputs, err := generator.Generate(*out, {{ printf "%q" .Command }})
367+
368+
startGenerate := time.Now()
369+
outputs, err := generator.Generate(*out, {{ printf "%q" .Command }}, *debug)
332370
if err != nil {
333371
fail(err.Error())
334372
}
335-
373+
if *debug {
374+
fmt.Fprintf(os.Stderr, "[TIMING] [binary] generator.Generate() took %v\n", time.Since(startGenerate))
375+
fmt.Fprintf(os.Stderr, "[TIMING] [binary] Total binary execution took %v\n", time.Since(startBinary))
376+
}
336377
fmt.Println(strings.Join(outputs, "\n"))
337378
}
338379

cmd/goa/main.go

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"go/build"
66
"os"
77
"strings"
8+
"time"
89

910
"flag"
1011

@@ -77,29 +78,55 @@ var (
7778

7879
func generate(cmd, path, output string, debug bool) error {
7980
var (
80-
files []string
81-
err error
82-
tmp *Generator
81+
files []string
82+
err error
83+
tmp *Generator
84+
startTotal, startImport, startNewGen, startWrite, startCompile, startRun time.Time
8385
)
8486

87+
startTotal = time.Now()
88+
if debug {
89+
fmt.Fprintf(os.Stderr, "[TIMING] Starting goa generation\n")
90+
}
91+
92+
startImport = time.Now()
8593
if _, err = build.Import(path, ".", 0); err != nil {
8694
goto fail
8795
}
96+
if debug {
97+
fmt.Fprintf(os.Stderr, "[TIMING] build.Import took %v\n", time.Since(startImport))
98+
}
8899

89-
tmp = NewGenerator(cmd, path, output)
100+
startNewGen = time.Now()
101+
tmp = NewGenerator(cmd, path, output, debug)
102+
if debug {
103+
fmt.Fprintf(os.Stderr, "[TIMING] NewGenerator took %v\n", time.Since(startNewGen))
104+
}
90105

106+
startWrite = time.Now()
91107
if err = tmp.Write(debug); err != nil {
92108
goto fail
93109
}
110+
if debug {
111+
fmt.Fprintf(os.Stderr, "[TIMING] Write (generate main.go) took %v\n", time.Since(startWrite))
112+
}
94113

95-
if err = tmp.Compile(); err != nil {
114+
startCompile = time.Now()
115+
if err = tmp.Compile(debug); err != nil {
96116
goto fail
97117
}
118+
if debug {
119+
fmt.Fprintf(os.Stderr, "[TIMING] Compile (go get + go build) took %v\n", time.Since(startCompile))
120+
}
98121

99-
if files, err = tmp.Run(); err != nil {
122+
startRun = time.Now()
123+
if files, err = tmp.Run(debug); err != nil {
100124
goto fail
101125
}
102-
126+
if debug {
127+
fmt.Fprintf(os.Stderr, "[TIMING] Run (execute binary) took %v\n", time.Since(startRun))
128+
fmt.Fprintf(os.Stderr, "[TIMING] Total generation time: %v\n", time.Since(startTotal))
129+
}
103130
fmt.Println(strings.Join(files, "\n"))
104131
if !debug {
105132
tmp.Remove()

0 commit comments

Comments
 (0)