From 08007f493c58948f1cd123f80b46b2be7a5f5ca8 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 13 Jul 2026 20:17:49 +0700 Subject: [PATCH] perf(extensions): reuse isolated goja download runtimes via per-extension pool --- go_backend/extension_manager.go | 97 ++++++++++++++++++++++++ go_backend/extension_provider_wrapper.go | 16 +--- 2 files changed, 101 insertions(+), 12 deletions(-) diff --git a/go_backend/extension_manager.go b/go_backend/extension_manager.go index 61b9b806..15cf9332 100644 --- a/go_backend/extension_manager.go +++ b/go_backend/extension_manager.go @@ -62,6 +62,14 @@ type loadedExtension struct { DataDir string `json:"data_dir"` SourceDir string `json:"source_dir"` IconPath string `json:"icon_path"` + + isolatedPoolMu sync.Mutex + isolatedPool []*isolatedRuntimeHandle +} + +type isolatedRuntimeHandle struct { + vm *goja.Runtime + runtime *extensionRuntime } func getExtensionInitSettings(extensionID string) map[string]any { @@ -442,6 +450,94 @@ func newIsolatedExtensionRuntime(ext *loadedExtension) (*goja.Runtime, *extensio return vm, runtime, nil } +// A goja runtime plus an executed extension program is several MB of live +// heap; rebuilding one per download multiplies that by the number of tracks. +// Extensions already serve many calls on the persistent shared VM, so reusing +// an initialized isolated runtime for consecutive downloads is the same +// lifecycle contract. +const maxIdleIsolatedRuntimes = 1 + +// acquireIsolatedExtensionRuntime pops an idle pooled runtime or builds one. +func acquireIsolatedExtensionRuntime(ext *loadedExtension) (*goja.Runtime, *extensionRuntime, error) { + ext.isolatedPoolMu.Lock() + if n := len(ext.isolatedPool); n > 0 { + handle := ext.isolatedPool[n-1] + ext.isolatedPool = ext.isolatedPool[:n-1] + ext.isolatedPoolMu.Unlock() + return handle.vm, handle.runtime, nil + } + ext.isolatedPoolMu.Unlock() + + ext.VMMu.Lock() + defer ext.VMMu.Unlock() + return newIsolatedExtensionRuntime(ext) +} + +// releaseIsolatedExtensionRuntime pools a healthy runtime for reuse or tears +// it down. Pass healthy=false after an interrupt/timeout/script error, whose +// VM state can't be trusted for reuse. +func releaseIsolatedExtensionRuntime(ext *loadedExtension, vm *goja.Runtime, runtime *extensionRuntime, healthy bool) { + if runtime != nil { + if err := runtime.flushStorageNow(); err != nil { + GoLog("[Extension:%s] isolated download storage flush failed: %v\n", ext.ID, err) + } + } + + if healthy && vm != nil && runtime != nil && ext.Enabled { + ext.isolatedPoolMu.Lock() + if len(ext.isolatedPool) < maxIdleIsolatedRuntimes { + ext.isolatedPool = append(ext.isolatedPool, &isolatedRuntimeHandle{vm: vm, runtime: runtime}) + ext.isolatedPoolMu.Unlock() + return + } + ext.isolatedPoolMu.Unlock() + } + + if cleanupErr := runCleanupOnVM(vm); cleanupErr != nil { + GoLog("[Extension:%s] isolated download cleanup failed: %v\n", ext.ID, cleanupErr) + } + if runtime != nil { + runtime.closeStorageFlusher() + } +} + +// drainIsolatedRuntimePool tears down idle isolated runtimes. Called on +// extension teardown and on app-wide memory release. +func drainIsolatedRuntimePool(ext *loadedExtension) { + ext.isolatedPoolMu.Lock() + pool := ext.isolatedPool + ext.isolatedPool = nil + ext.isolatedPoolMu.Unlock() + + for _, handle := range pool { + if cleanupErr := runCleanupOnVM(handle.vm); cleanupErr != nil { + GoLog("[Extension:%s] isolated pool cleanup failed: %v\n", ext.ID, cleanupErr) + } + if handle.runtime != nil { + if err := handle.runtime.flushStorageNow(); err != nil { + GoLog("[Extension:%s] isolated pool storage flush failed: %v\n", ext.ID, err) + } + handle.runtime.closeStorageFlusher() + } + } +} + +// drainAllIsolatedRuntimePools releases every extension's idle isolated +// runtimes (memory-pressure hook). +func drainAllIsolatedRuntimePools() { + m := getExtensionManager() + m.mu.RLock() + exts := make([]*loadedExtension, 0, len(m.extensions)) + for _, ext := range m.extensions { + exts = append(exts, ext) + } + m.mu.RUnlock() + + for _, ext := range exts { + drainIsolatedRuntimePool(ext) + } +} + func (m *extensionManager) initializeVM(ext *loadedExtension) error { ext.VMMu.Lock() defer ext.VMMu.Unlock() @@ -568,6 +664,7 @@ func runCleanupOnVM(vm *goja.Runtime) error { } func teardownVMLocked(ext *loadedExtension) { + drainIsolatedRuntimePool(ext) if err := runCleanupLocked(ext); err != nil { GoLog("[Extension] Error calling cleanup for %s: %v\n", ext.ID, err) } diff --git a/go_backend/extension_provider_wrapper.go b/go_backend/extension_provider_wrapper.go index 171dba8c..5bc0ac47 100644 --- a/go_backend/extension_provider_wrapper.go +++ b/go_backend/extension_provider_wrapper.go @@ -485,9 +485,7 @@ func (p *extensionProviderWrapper) Download(trackID, quality, outputPath, itemID perf := newExtensionCallPerf(p.extension.ID, "download") defer perf.finish() initStartedAt := time.Now() - p.extension.VMMu.Lock() - vm, runtime, err := newIsolatedExtensionRuntime(p.extension) - p.extension.VMMu.Unlock() + vm, runtime, err := acquireIsolatedExtensionRuntime(p.extension) perf.recordInit(time.Since(initStartedAt)) if err != nil { return &ExtDownloadResult{ @@ -496,16 +494,9 @@ func (p *extensionProviderWrapper) Download(trackID, quality, outputPath, itemID ErrorType: "init_error", }, nil } + vmHealthy := false defer func() { - if cleanupErr := runCleanupOnVM(vm); cleanupErr != nil { - GoLog("[Extension:%s] isolated download cleanup failed: %v\n", p.extension.ID, cleanupErr) - } - if runtime != nil { - if flushErr := runtime.flushStorageNow(); flushErr != nil { - GoLog("[Extension:%s] isolated download storage flush failed: %v\n", p.extension.ID, flushErr) - } - runtime.closeStorageFlusher() - } + releaseIsolatedExtensionRuntime(p.extension, vm, runtime, vmHealthy) }() if runtime != nil { runtime.setActiveDownloadItemID(itemID) @@ -546,6 +537,7 @@ func (p *extensionProviderWrapper) Download(trackID, quality, outputPath, itemID result, err := RunWithTimeoutAndRecover(vm, script, ExtDownloadTimeout) perf.recordJS(time.Since(jsStartedAt)) perf.recordPayload(result) + vmHealthy = err == nil if err != nil { errMsg := err.Error() errType := "script_error"