perf(extensions): reuse isolated goja download runtimes via per-extension pool

This commit is contained in:
zarzet
2026-07-14 09:09:39 +07:00
parent e302fac390
commit 08007f493c
2 changed files with 101 additions and 12 deletions
+97
View File
@@ -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)
}
+4 -12
View File
@@ -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"