From b121a5da679f96ec78ce5f199804671ea240fb66 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 6 Mar 2026 14:19:38 -0500 Subject: [PATCH] fix: cache esbuild transform results per VM to avoid repeated TTY syscalls Each call to esbuild.Transform() internally creates a new stderr logger that unconditionally calls GetTerminalInfo/IoctlGetTermios to check if stderr is a TTY. Since vm.compile() is called hundreds of times during generation (once per template rendered plus setup calls), and many calls receive the same source string, this accumulates significant CPU overhead. Cache esbuild.TransformResult per VM instance keyed by (name, src) to avoid redundant transform calls. The cache is bounded by the number of unique template sources and requires no eviction or synchronization since VMs are single-goroutine, where the number of template sources is not likely to be a concern in practice. --- internal/vm/vm.go | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/internal/vm/vm.go b/internal/vm/vm.go index 4589507..0bd4720 100644 --- a/internal/vm/vm.go +++ b/internal/vm/vm.go @@ -8,6 +8,7 @@ import ( "regexp" "strconv" "strings" + "sync" "time" "github.com/dop251/goja" @@ -36,10 +37,17 @@ const ( var lineNumberRegex = regexp.MustCompile(` \(*([^ ]+):([0-9]+):([0-9]+)\([0-9]+\)`) +type transformCacheKey struct { + name string + src string +} + // VM is a wrapper around the goja runtime. type VM struct { *goja.Runtime globalSourceMapCache map[string]*sourcemap.Consumer + transformCache map[transformCacheKey]*esbuild.TransformResult + transformCacheMutex sync.RWMutex } // Options represents options for running a script. @@ -82,7 +90,11 @@ func New(randSource RandSource) (*VM, error) { }) } - return &VM{Runtime: g, globalSourceMapCache: make(map[string]*sourcemap.Consumer)}, nil + return &VM{ + Runtime: g, + globalSourceMapCache: make(map[string]*sourcemap.Consumer), + transformCache: make(map[transformCacheKey]*esbuild.TransformResult), + }, nil } // Run runs a script in the VM. @@ -181,14 +193,33 @@ func (v *VM) GetRuntime() *goja.Runtime { return v.Runtime } -func (v *VM) compile(name string, src string, strict bool) (*program, error) { - // transform src with esbuild -- this ensures we handle typescript +func (v *VM) cachedTransform(name string, src string) *esbuild.TransformResult { + key := transformCacheKey{name: name, src: src} + + v.transformCacheMutex.RLock() + if cached, ok := v.transformCache[key]; ok { + v.transformCacheMutex.RUnlock() + return cached + } + v.transformCacheMutex.RUnlock() + result := esbuild.Transform(src, esbuild.TransformOptions{ Target: esbuild.ES2015, Loader: esbuild.LoaderTS, Sourcemap: esbuild.SourceMapExternal, Sourcefile: name, }) + + v.transformCacheMutex.Lock() + v.transformCache[key] = &result + v.transformCacheMutex.Unlock() + + return &result +} + +func (v *VM) compile(name string, src string, strict bool) (*program, error) { + // transform src with esbuild -- this ensures we handle typescript + result := v.cachedTransform(name, src) if len(result.Errors) > 0 { msg := "" for _, errMsg := range result.Errors {