Conversation
goccy
commented
Mar 3, 2026
- Optimize WASM plugin execution with compilation caching
- If result.Files does not exist, we will not run the code generator plugin
There was a problem hiding this comment.
Pull request overview
This PR optimizes WASM plugin execution by caching compiled WASM modules and their runtimes so that the expensive compilation step is performed only once per plugin path during the lifetime of a Generator instance. It also skips code generator plugin execution entirely when result.Files is empty.
Changes:
- Introduced
wasmPlugin(compiled module + runtime, reusable) andwasmPluginCache(path-keyed map) inwasm.go, replacing the previous one-shotevalCodeGeneratorPluginfunction. - Extended
Generatorwith a long-livedwasmPluginCache, added aClosemethod, and threaded the cache through allevalAllCodeGenerationPlugincall sites. - Added an early-return in
CreateCodeGeneratorResponsewhenresult.Filesis empty, avoiding unnecessary plugin invocations.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
generator/wasm.go |
New wasmPlugin / wasmPluginCache types replacing the old evalCodeGeneratorPlugin function; handles compile-once-execute-many pattern |
generator/generator.go |
Adds wasmPluginCache field to Generator, a Close method for cleanup, passes cache to all evalAllCodeGenerationPlugin calls, and adds early-exit guard on empty result.Files |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This comment has been minimized.
This comment has been minimized.
Include opt.Sha256 in the cache key so that the same path with different hash values does not bypass hash verification. Return errors from Close methods using errors.Join instead of silently discarding them. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
generator/generator.go
Outdated
| fmt.Fprint(os.Stderr, validator.Format(outs)) | ||
| } | ||
| if len(result.Files) == 0 { | ||
| return nil, nil |
There was a problem hiding this comment.
Since returning nil can make protoc treat the plugin as not supporting FEATURE_PROTO3_OPTIONAL, should we return an empty response with SupportedFeatures set?
- Use content-derived sha256 as cache key instead of user-provided value - Make wasmPluginCache thread-safe with sync.RWMutex - Add defer g.Close(ctx) in CLI entry point - Return SupportedFeatures even when result.Files is empty Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func (c *wasmPluginCache) getOrCreate(ctx context.Context, opt *WasmPluginOption) (*wasmPlugin, error) { | ||
| wasmFile, err := os.ReadFile(opt.Path) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("grpc-federation: failed to read plugin file: %s: %w", opt.Path, err) | ||
| } | ||
| hash := sha256.Sum256(wasmFile) | ||
| gotHash := hex.EncodeToString(hash[:]) | ||
| if opt.Sha256 != "" && opt.Sha256 != gotHash { | ||
| return nil, fmt.Errorf( | ||
| `grpc-federation: expected plugin sha256 value is [%s] but got [%s]`, | ||
| opt.Sha256, | ||
| gotHash, | ||
| ) | ||
| } | ||
| cacheKey := opt.Path + ":" + gotHash | ||
|
|
||
| c.mu.RLock() | ||
| if wp, ok := c.plugins[cacheKey]; ok { | ||
| c.mu.RUnlock() | ||
| return wp, nil | ||
| } | ||
| c.mu.RUnlock() |
There was a problem hiding this comment.
The getOrCreate method reads the entire WASM file from disk and computes its SHA-256 hash on every call, even when the plugin is already present in the cache. The cache lookup only happens after the file I/O and hashing, meaning every invocation incurs the cost of a full file read and SHA-256 computation regardless of whether the compiled module is already cached.
For a hot path (e.g., watch mode processing many files), this means repeated disk reads of potentially large WASM binaries. The optimisation intended by compilation caching is significantly undermined by this per-call file read.
Consider checking the cache keyed by opt.Path first (with a read lock), and only reading the file and computing the hash when a cache miss occurs. Alternatively, if a constant hash per path is acceptable, the SHA-256 could be cached alongside the plugin to avoid repeated computation.
| protoPath = args[0] | ||
| } | ||
| g := generator.New(cfg) | ||
| defer g.Close(ctx) |
There was a problem hiding this comment.
The error returned by g.Close(ctx) is silently discarded because defer does not propagate the return value. If closing a cached WASM runtime fails (e.g., due to a resource leak), the caller has no way to detect or log that failure.
Consider logging the error from Close if it is non-nil, similar to how other error conditions are handled in _main. For example: defer func() { if err := g.Close(ctx); err != nil { log.Printf("failed to close generator: %+v", err) } }().
| defer g.Close(ctx) | |
| defer func() { | |
| if err := g.Close(ctx); err != nil { | |
| log.Printf("failed to close generator: %+v", err) | |
| } | |
| }() |
This comment has been minimized.
This comment has been minimized.
generator/wasm.go
Outdated
| type wasmPlugin struct { | ||
| runtime wazero.Runtime | ||
| compiled wazero.CompiledModule | ||
| counter atomic.Int64 |
There was a problem hiding this comment.
Can you add a comment explaining what this counter is used for, please?
Code Metrics Report
Details | | main (5eeb102) | #363 (d80eab1) | +/- |
|---------------------|----------------|----------------|-------|
- | Coverage | 67.9% | 67.8% | -0.2% |
| Files | 84 | 84 | 0 |
| Lines | 15582 | 15614 | +32 |
+ | Covered | 10587 | 10588 | +1 |
- | Code to Test Ratio | 1:0.4 | 1:0.4 | -0.1 |
| Code | 48318 | 48381 | +63 |
| Test | 20407 | 20407 | 0 |
- | Test Execution Time | 6m51s | 6m58s | +7s |Code coverage of files in pull request scope (31.0% → 29.4%)
Reported by octocov |