perf: lazy extension VM init, incremental startup maintenance, and UI optimizations

- Defer extension VM initialization until first use with lockReadyVM() pattern to eliminate TOCTOU races and reduce startup overhead
- Add validateExtensionLoad() to catch JS errors at install time without keeping VM alive
- Teardown VM on extension disable to free resources; re-init lazily on re-enable
- Replace full orphan cleanup with incremental cursor-based pagination across launches
- Batch DB writes (upsertBatch, replaceAll) with transactions for atomicity
- Parse JSON natively on Kotlin side to avoid double-serialization over MethodChannel
- Add identity-based memoization caches for unified items and path match keys in queue tab
- Use ValueListenableBuilder for targeted embedded cover refreshes instead of full setState
- Extract shared widgets (_buildAlbumGridItemCore, _buildFilterButton, _navigateWithUnfocus)
- Use libraryCollectionsProvider selector and MediaQuery.paddingOf for fewer rebuilds
- Simplify supporter chip tiers and localize remaining hardcoded strings
This commit is contained in:
zarzet committed 2026-03-25 19:55:02 +07:00
1 parent da9d64ccfd
commit 03fd734048
18 files changed
+1279 -1133

No files matched your search

+6 -16
View File
@@ -2187,12 +2187,6 @@ func LoadExtensionFromPath(filePath string) (string, error) {
return "", err
}
settingsStore := GetExtensionSettingsStore()
settings := settingsStore.GetAll(ext.ID)
if len(settings) > 0 {
manager.InitializeExtension(ext.ID, settings)
}
result := map[string]interface{}{
"id": ext.ID,
"name": ext.Manifest.Name,
@@ -2226,12 +2220,6 @@ func UpgradeExtensionFromPath(filePath string) (string, error) {
return "", err
}
settingsStore := GetExtensionSettingsStore()
settings := settingsStore.GetAll(ext.ID)
if len(settings) > 0 {
manager.InitializeExtension(ext.ID, settings)
}
result := map[string]interface{}{
"id": ext.ID,
"display_name": ext.Manifest.DisplayName,
@@ -3324,12 +3312,14 @@ func callExtensionFunctionJSON(extensionID, functionName string, timeout time.Du
if !ext.Enabled {
return "", fmt.Errorf("extension '%s' is disabled", extensionID)
}
vm, err := ext.lockReadyVM()
if err != nil {
return "", err
}
defer ext.VMMu.Unlock()
// Goja runtime is not thread-safe; guard direct extension.*() calls with VMMu
// to avoid races with other provider calls (e.g. getAlbum/getPlaylist).
ext.VMMu.Lock()
defer ext.VMMu.Unlock()
script := fmt.Sprintf(`
(function() {
if (typeof extension !== 'undefined' && typeof extension.%s === 'function') {
@@ -3339,7 +3329,7 @@ func callExtensionFunctionJSON(extensionID, functionName string, timeout time.Du
})()
`, functionName, functionName)
result, err := RunWithTimeoutAndRecover(ext.VM, script, timeout)
result, err := RunWithTimeoutAndRecover(vm, script, timeout)
if err != nil {
return "", fmt.Errorf("%s failed: %w", functionName, err)
}
+241 -114
View File
@@ -44,16 +44,76 @@ func compareVersions(v1, v2 string) int {
}
type LoadedExtension struct {
ID string `json:"id"`
Manifest *ExtensionManifest `json:"manifest"`
VM *goja.Runtime `json:"-"`
VMMu sync.Mutex `json:"-"`
runtime *ExtensionRuntime
Enabled bool `json:"enabled"`
Error string `json:"error,omitempty"`
DataDir string `json:"data_dir"`
SourceDir string `json:"source_dir"`
IconPath string `json:"icon_path"`
ID string `json:"id"`
Manifest *ExtensionManifest `json:"manifest"`
VM *goja.Runtime `json:"-"`
VMMu sync.Mutex `json:"-"`
runtime *ExtensionRuntime
initialized bool
Enabled bool `json:"enabled"`
Error string `json:"error,omitempty"`
DataDir string `json:"data_dir"`
SourceDir string `json:"source_dir"`
IconPath string `json:"icon_path"`
}
func getExtensionInitSettings(extensionID string) map[string]interface{} {
settings := GetExtensionSettingsStore().GetAll(extensionID)
if len(settings) == 0 {
return settings
}
filtered := make(map[string]interface{}, len(settings))
for key, value := range settings {
if strings.HasPrefix(key, "_") {
continue
}
filtered[key] = value
}
return filtered
}
func ensureRuntimeReadyLocked(ext *LoadedExtension, applyStoredSettings bool) error {
if ext.VM == nil || ext.runtime == nil {
if err := initializeVMLocked(ext); err != nil {
ext.Error = err.Error()
ext.Enabled = false
return err
}
}
if applyStoredSettings && !ext.initialized {
settings := getExtensionInitSettings(ext.ID)
if len(settings) > 0 {
if err := initializeExtensionWithSettingsLocked(ext, settings); err != nil {
teardownVMLocked(ext)
ext.Error = err.Error()
ext.Enabled = false
return err
}
} else {
ext.initialized = true
}
}
ext.Error = ""
return nil
}
func (ext *LoadedExtension) ensureRuntimeReady() error {
ext.VMMu.Lock()
defer ext.VMMu.Unlock()
return ensureRuntimeReadyLocked(ext, true)
}
func (ext *LoadedExtension) lockReadyVM() (*goja.Runtime, error) {
ext.VMMu.Lock()
if err := ensureRuntimeReadyLocked(ext, true); err != nil {
ext.VMMu.Unlock()
return nil, err
}
return ext.VM, nil
}
type ExtensionManager struct {
@@ -220,10 +280,10 @@ func (m *ExtensionManager) LoadExtensionFromFile(filePath string) (*LoadedExtens
SourceDir: extDir,
}
if err := m.initializeVM(ext); err != nil {
if err := validateExtensionLoad(ext); err != nil {
ext.Error = err.Error()
ext.Enabled = false
GoLog("[Extension] Failed to initialize VM for %s: %v\n", manifest.Name, err)
GoLog("[Extension] Failed to validate extension %s: %v\n", manifest.Name, err)
}
m.extensions[manifest.Name] = ext
@@ -232,7 +292,10 @@ func (m *ExtensionManager) LoadExtensionFromFile(filePath string) (*LoadedExtens
return ext, nil
}
func (m *ExtensionManager) initializeVM(ext *LoadedExtension) error {
func initializeVMLocked(ext *LoadedExtension) error {
ext.VM = nil
ext.runtime = nil
ext.initialized = false
vm := goja.New()
ext.VM = vm
@@ -279,6 +342,136 @@ func (m *ExtensionManager) initializeVM(ext *LoadedExtension) error {
return nil
}
func (m *ExtensionManager) initializeVM(ext *LoadedExtension) error {
ext.VMMu.Lock()
defer ext.VMMu.Unlock()
return initializeVMLocked(ext)
}
func initializeExtensionWithSettingsLocked(
ext *LoadedExtension,
settings map[string]interface{},
) error {
if ext.VM == nil {
return fmt.Errorf("Extension failed to load. Please reinstall the extension")
}
settingsJSON, err := json.Marshal(settings)
if err != nil {
return fmt.Errorf("Failed to save settings")
}
script := fmt.Sprintf(`
(function() {
var settings = %s;
if (typeof extension !== 'undefined' && typeof extension.initialize === 'function') {
try {
extension.initialize(settings);
return { success: true };
} catch (e) {
return { success: false, error: e.toString() };
}
}
return { success: true, message: 'no initialize function' };
})()
`, string(settingsJSON))
result, err := ext.VM.RunString(script)
if err != nil {
ext.Error = fmt.Sprintf("initialize failed: %v", err)
ext.Enabled = false
GoLog("[Extension] Initialize error for %s: %v\n", ext.ID, err)
return err
}
if result != nil && !goja.IsUndefined(result) {
exported := result.Export()
if resultMap, ok := exported.(map[string]interface{}); ok {
if success, ok := resultMap["success"].(bool); ok && !success {
errMsg := "unknown error"
if e, ok := resultMap["error"].(string); ok {
errMsg = e
}
ext.Error = errMsg
ext.Enabled = false
GoLog("[Extension] Initialize failed for %s: %s\n", ext.ID, errMsg)
return fmt.Errorf("initialize failed: %s", errMsg)
}
}
}
ext.initialized = true
GoLog("[Extension] Initialized %s\n", ext.ID)
return nil
}
func runCleanupLocked(ext *LoadedExtension) error {
if ext.VM != nil {
script := `
(function() {
if (typeof extension !== 'undefined' && typeof extension.cleanup === 'function') {
try {
extension.cleanup();
return { success: true };
} catch (e) {
return { success: false, error: e.toString() };
}
}
return { success: true, message: 'no cleanup function' };
})()
`
result, err := ext.VM.RunString(script)
if err != nil {
return err
}
if result != nil && !goja.IsUndefined(result) {
exported := result.Export()
if resultMap, ok := exported.(map[string]interface{}); ok {
if success, ok := resultMap["success"].(bool); ok && !success {
errMsg := "unknown error"
if e, ok := resultMap["error"].(string); ok {
errMsg = e
}
return fmt.Errorf("cleanup failed: %s", errMsg)
}
}
}
if result != nil && !goja.IsUndefined(result) && !goja.IsNull(result) {
GoLog("[Extension] Cleanup called for %s\n", ext.ID)
}
}
return nil
}
func teardownVMLocked(ext *LoadedExtension) {
if err := runCleanupLocked(ext); err != nil {
GoLog("[Extension] Error calling cleanup for %s: %v\n", ext.ID, err)
}
if ext.runtime != nil {
if err := ext.runtime.flushStorageNow(); err != nil {
GoLog("[Extension] Failed to flush storage for %s: %v\n", ext.ID, err)
}
ext.runtime.closeStorageFlusher()
}
ext.runtime = nil
ext.VM = nil
ext.initialized = false
}
func validateExtensionLoad(ext *LoadedExtension) error {
ext.VMMu.Lock()
defer ext.VMMu.Unlock()
if err := initializeVMLocked(ext); err != nil {
return err
}
teardownVMLocked(ext)
return nil
}
func (m *ExtensionManager) UnloadExtension(extensionID string) error {
m.mu.Lock()
defer m.mu.Unlock()
@@ -288,21 +481,9 @@ func (m *ExtensionManager) UnloadExtension(extensionID string) error {
return fmt.Errorf("Extension not found")
}
if ext.VM != nil {
cleanup, err := ext.VM.RunString("typeof extension !== 'undefined' && typeof extension.cleanup === 'function' ? extension.cleanup() : null")
if err != nil {
GoLog("[Extension] Error calling cleanup for %s: %v\n", extensionID, err)
} else if cleanup != nil && !goja.IsUndefined(cleanup) && !goja.IsNull(cleanup) {
GoLog("[Extension] Cleanup called for %s\n", extensionID)
}
}
if ext.runtime != nil {
if err := ext.runtime.flushStorageNow(); err != nil {
GoLog("[Extension] Failed to flush storage for %s: %v\n", extensionID, err)
}
ext.runtime.closeStorageFlusher()
ext.runtime = nil
}
ext.VMMu.Lock()
teardownVMLocked(ext)
ext.VMMu.Unlock()
delete(m.extensions, extensionID)
GoLog("[Extension] Unloaded extension: %s\n", extensionID)
@@ -341,7 +522,21 @@ func (m *ExtensionManager) SetExtensionEnabled(extensionID string, enabled bool)
return fmt.Errorf("Extension not found")
}
ext.Enabled = enabled
if enabled {
ext.Enabled = true
if err := ext.ensureRuntimeReady(); err != nil {
store := GetExtensionSettingsStore()
ext.Enabled = false
_ = store.Set(extensionID, "_enabled", false)
return err
}
} else {
ext.Enabled = false
ext.Error = ""
ext.VMMu.Lock()
teardownVMLocked(ext)
ext.VMMu.Unlock()
}
GoLog("[Extension] %s %s\n", extensionID, map[bool]string{true: "enabled", false: "disabled"}[enabled])
store := GetExtensionSettingsStore()
@@ -436,10 +631,10 @@ func (m *ExtensionManager) loadExtensionFromDirectory(dirPath string) (*LoadedEx
}
}
if err := m.initializeVM(ext); err != nil {
if err := validateExtensionLoad(ext); err != nil {
ext.Error = err.Error()
ext.Enabled = false
GoLog("[Extension] Failed to initialize VM for %s: %v\n", manifest.Name, err)
GoLog("[Extension] Failed to validate extension %s: %v\n", manifest.Name, err)
}
m.extensions[manifest.Name] = ext
@@ -590,10 +785,14 @@ func (m *ExtensionManager) UpgradeExtension(filePath string) (*LoadedExtension,
SourceDir: extDir,
}
if err := m.initializeVM(ext); err != nil {
if wasEnabled {
if err := ext.ensureRuntimeReady(); err != nil {
GoLog("[Extension] Failed to initialize upgraded extension %s: %v\n", newManifest.Name, err)
}
} else if err := validateExtensionLoad(ext); err != nil {
ext.Error = err.Error()
ext.Enabled = false
GoLog("[Extension] Failed to initialize VM for %s: %v\n", newManifest.Name, err)
GoLog("[Extension] Failed to validate upgraded extension %s: %v\n", newManifest.Name, err)
}
m.mu.Lock()
@@ -790,56 +989,13 @@ func (m *ExtensionManager) InitializeExtension(extensionID string, settings map[
return fmt.Errorf("Extension not found")
}
if ext.VM == nil {
return fmt.Errorf("Extension failed to load. Please reinstall the extension")
}
ext.VMMu.Lock()
defer ext.VMMu.Unlock()
settingsJSON, err := json.Marshal(settings)
if err != nil {
return fmt.Errorf("Failed to save settings")
}
script := fmt.Sprintf(`
(function() {
var settings = %s;
if (typeof extension !== 'undefined' && typeof extension.initialize === 'function') {
try {
extension.initialize(settings);
return { success: true };
} catch (e) {
return { success: false, error: e.toString() };
}
}
return { success: true, message: 'no initialize function' };
})()
`, string(settingsJSON))
result, err := ext.VM.RunString(script)
if err != nil {
ext.Error = fmt.Sprintf("initialize failed: %v", err)
ext.Enabled = false
GoLog("[Extension] Initialize error for %s: %v\n", extensionID, err)
if err := ensureRuntimeReadyLocked(ext, false); err != nil {
return err
}
if result != nil && !goja.IsUndefined(result) {
exported := result.Export()
if resultMap, ok := exported.(map[string]interface{}); ok {
if success, ok := resultMap["success"].(bool); ok && !success {
errMsg := "unknown error"
if e, ok := resultMap["error"].(string); ok {
errMsg = e
}
ext.Error = errMsg
ext.Enabled = false
GoLog("[Extension] Initialize failed for %s: %s\n", extensionID, errMsg)
return fmt.Errorf("initialize failed: %s", errMsg)
}
}
}
GoLog("[Extension] Initialized %s\n", extensionID)
return nil
return initializeExtensionWithSettingsLocked(ext, settings)
}
func (m *ExtensionManager) CleanupExtension(extensionID string) error {
@@ -854,41 +1010,12 @@ func (m *ExtensionManager) CleanupExtension(extensionID string) error {
if ext.VM == nil {
return nil
}
script := `
(function() {
if (typeof extension !== 'undefined' && typeof extension.cleanup === 'function') {
try {
extension.cleanup();
return { success: true };
} catch (e) {
return { success: false, error: e.toString() };
}
}
return { success: true, message: 'no cleanup function' };
})()
`
result, err := ext.VM.RunString(script)
if err != nil {
ext.VMMu.Lock()
defer ext.VMMu.Unlock()
if err := runCleanupLocked(ext); err != nil {
GoLog("[Extension] Cleanup error for %s: %v\n", extensionID, err)
return err
}
if result != nil && !goja.IsUndefined(result) {
exported := result.Export()
if resultMap, ok := exported.(map[string]interface{}); ok {
if success, ok := resultMap["success"].(bool); ok && !success {
errMsg := "unknown error"
if e, ok := resultMap["error"].(string); ok {
errMsg = e
}
GoLog("[Extension] Cleanup failed for %s: %s\n", extensionID, errMsg)
return fmt.Errorf("cleanup failed: %s", errMsg)
}
}
}
GoLog("[Extension] Cleaned up %s\n", extensionID)
return nil
}
@@ -917,8 +1044,8 @@ func (m *ExtensionManager) InvokeAction(extensionID string, actionName string) (
return nil, fmt.Errorf("extension not found: %s", extensionID)
}
if ext.VM == nil {
return nil, fmt.Errorf("extension VM not initialized")
if err := ext.ensureRuntimeReady(); err != nil {
return nil, err
}
if !ext.Enabled {
+56 -28
View File
@@ -125,6 +125,15 @@ func NewExtensionProviderWrapper(ext *LoadedExtension) *ExtensionProviderWrapper
}
}
func (p *ExtensionProviderWrapper) lockReadyVM() error {
vm, err := p.extension.lockReadyVM()
if err != nil {
return err
}
p.vm = vm
return nil
}
func (p *ExtensionProviderWrapper) SearchTracks(query string, limit int) (*ExtSearchResult, error) {
if !p.extension.Manifest.IsMetadataProvider() {
return nil, fmt.Errorf("extension '%s' is not a metadata provider", p.extension.ID)
@@ -133,8 +142,9 @@ func (p *ExtensionProviderWrapper) SearchTracks(query string, limit int) (*ExtSe
if !p.extension.Enabled {
return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID)
}
p.extension.VMMu.Lock()
if err := p.lockReadyVM(); err != nil {
return nil, err
}
defer p.extension.VMMu.Unlock()
script := fmt.Sprintf(`
@@ -192,8 +202,9 @@ func (p *ExtensionProviderWrapper) GetTrack(trackID string) (*ExtTrackMetadata,
if !p.extension.Enabled {
return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID)
}
p.extension.VMMu.Lock()
if err := p.lockReadyVM(); err != nil {
return nil, err
}
defer p.extension.VMMu.Unlock()
script := fmt.Sprintf(`
@@ -240,8 +251,9 @@ func (p *ExtensionProviderWrapper) GetAlbum(albumID string) (*ExtAlbumMetadata,
if !p.extension.Enabled {
return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID)
}
p.extension.VMMu.Lock()
if err := p.lockReadyVM(); err != nil {
return nil, err
}
defer p.extension.VMMu.Unlock()
script := fmt.Sprintf(`
@@ -291,8 +303,9 @@ func (p *ExtensionProviderWrapper) GetArtist(artistID string) (*ExtArtistMetadat
if !p.extension.Enabled {
return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID)
}
p.extension.VMMu.Lock()
if err := p.lockReadyVM(); err != nil {
return nil, err
}
defer p.extension.VMMu.Unlock()
script := fmt.Sprintf(`
@@ -345,8 +358,10 @@ func (p *ExtensionProviderWrapper) EnrichTrack(track *ExtTrackMetadata) (*ExtTra
if !p.extension.Enabled {
return track, nil
}
p.extension.VMMu.Lock()
if err := p.lockReadyVM(); err != nil {
GoLog("[Extension] EnrichTrack init error for %s: %v\n", p.extension.ID, err)
return track, nil
}
defer p.extension.VMMu.Unlock()
trackJSON, err := json.Marshal(track)
@@ -405,8 +420,9 @@ func (p *ExtensionProviderWrapper) CheckAvailability(isrc, trackName, artistName
if !p.extension.Enabled {
return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID)
}
p.extension.VMMu.Lock()
if err := p.lockReadyVM(); err != nil {
return nil, err
}
defer p.extension.VMMu.Unlock()
script := fmt.Sprintf(`
@@ -452,8 +468,9 @@ func (p *ExtensionProviderWrapper) GetDownloadURL(trackID, quality string) (*Ext
if !p.extension.Enabled {
return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID)
}
p.extension.VMMu.Lock()
if err := p.lockReadyVM(); err != nil {
return nil, err
}
defer p.extension.VMMu.Unlock()
script := fmt.Sprintf(`
@@ -501,8 +518,13 @@ func (p *ExtensionProviderWrapper) Download(trackID, quality, outputPath string,
if !p.extension.Enabled {
return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID)
}
p.extension.VMMu.Lock()
if err := p.lockReadyVM(); err != nil {
return &ExtDownloadResult{
Success: false,
ErrorMessage: err.Error(),
ErrorType: "init_error",
}, nil
}
defer p.extension.VMMu.Unlock()
p.vm.Set("__onProgress", func(call goja.FunctionCall) goja.Value {
@@ -1626,8 +1648,9 @@ func (p *ExtensionProviderWrapper) CustomSearch(query string, options map[string
if !p.extension.Enabled {
return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID)
}
p.extension.VMMu.Lock()
if err := p.lockReadyVM(); err != nil {
return nil, err
}
defer p.extension.VMMu.Unlock()
if options == nil {
@@ -1707,8 +1730,9 @@ func (p *ExtensionProviderWrapper) HandleURL(url string) (*ExtURLHandleResult, e
if !p.extension.Enabled {
return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID)
}
p.extension.VMMu.Lock()
if err := p.lockReadyVM(); err != nil {
return nil, err
}
defer p.extension.VMMu.Unlock()
script := fmt.Sprintf(`
@@ -1792,8 +1816,9 @@ func (p *ExtensionProviderWrapper) MatchTrack(sourceTrack map[string]interface{}
if !p.extension.Enabled {
return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID)
}
p.extension.VMMu.Lock()
if err := p.lockReadyVM(); err != nil {
return nil, err
}
defer p.extension.VMMu.Unlock()
sourceJSON, _ := json.Marshal(sourceTrack)
@@ -1862,8 +1887,9 @@ func (p *ExtensionProviderWrapper) PostProcess(filePath string, metadata map[str
if !p.extension.Enabled {
return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID)
}
p.extension.VMMu.Lock()
if err := p.lockReadyVM(); err != nil {
return &PostProcessResult{Success: false, Error: err.Error()}, nil
}
defer p.extension.VMMu.Unlock()
metadataJSON, _ := json.Marshal(metadata)
@@ -1924,8 +1950,9 @@ func (p *ExtensionProviderWrapper) PostProcessV2(input PostProcessInput, metadat
if !p.extension.Enabled {
return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID)
}
p.extension.VMMu.Lock()
if err := p.lockReadyVM(); err != nil {
return &PostProcessResult{Success: false, Error: err.Error()}, nil
}
defer p.extension.VMMu.Unlock()
metadataJSON, _ := json.Marshal(metadata)
@@ -2182,8 +2209,9 @@ func (p *ExtensionProviderWrapper) FetchLyrics(trackName, artistName, albumName
if !p.extension.Enabled {
return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID)
}
p.extension.VMMu.Lock()
if err := p.lockReadyVM(); err != nil {
return nil, err
}
defer p.extension.VMMu.Unlock()
// Use global variables to avoid JS injection issues with special characters in track/artist names