perf(webkit): cache compiled content filters for instant reuse across WebViews
- commit
- 6defb29587f67b6092b6593514e7e64ab9c43274
- author
- bnema · · 2025-11-29 11:26 UTC
- parent
- 422848a
- signature
- GPG Unverified
2B82207D370127A0The key is unknown
1 file · +104 -11
M pkg/webkit/content_blocking.go +104 -11
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "fmt" | |
| 6 | 6 | "log" | |
| 7 | 7 | "path/filepath" | |
| 8 | + | "sync" | |
| 8 | 9 | ||
| 9 | 10 | webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6" | |
| 10 | 11 | "github.com/diamondburned/gotk4/pkg/gio/v2" | |
| @@ -15,6 +16,11 @@ import ( | |||
| 15 | 16 | type ContentBlockingManager struct { | |
| 16 | 17 | filterStore *webkit.UserContentFilterStore | |
| 17 | 18 | storagePath string | |
| 19 | + | ||
| 20 | + | // Cache compiled filter to avoid recompilation | |
| 21 | + | mu sync.RWMutex | |
| 22 | + | compiledFilter *webkit.UserContentFilter | |
| 23 | + | filterID string | |
| 18 | 24 | } | |
| 19 | 25 | ||
| 20 | 26 | // NewContentBlockingManager creates a new content blocking manager | |
| @@ -33,13 +39,103 @@ func NewContentBlockingManager(storagePath string) (*ContentBlockingManager, err | |||
| 33 | 39 | }, nil | |
| 34 | 40 | } | |
| 35 | 41 | ||
| 36 | - | // ApplyFiltersFromJSON compiles and applies WebKit JSON filter rules to a UserContentManager | |
| 37 | - | // The jsonRules parameter should be a JSON array of WebKit content blocker rules | |
| 42 | + | // CompileFilters compiles JSON filter rules and caches the result for reuse. | |
| 43 | + | // This should be called once during startup. Subsequent WebViews use ApplyCompiledFilter. | |
| 44 | + | func (cbm *ContentBlockingManager) CompileFilters(identifier string, jsonRules []byte, onComplete func(error)) { | |
| 45 | + | if len(jsonRules) == 0 { | |
| 46 | + | if onComplete != nil { | |
| 47 | + | onComplete(fmt.Errorf("empty filter rules")) | |
| 48 | + | } | |
| 49 | + | return | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | log.Printf("[webkit] Compiling content filters (identifier: %s, rules size: %d bytes)", identifier, len(jsonRules)) | |
| 53 | + | ||
| 54 | + | // Store identifier for later use | |
| 55 | + | cbm.mu.Lock() | |
| 56 | + | cbm.filterID = identifier | |
| 57 | + | cbm.mu.Unlock() | |
| 58 | + | ||
| 59 | + | // Convert []byte to *glib.Bytes for WebKit API | |
| 60 | + | gBytes := glib.NewBytesWithGo(jsonRules) | |
| 61 | + | ||
| 62 | + | // Compile asynchronously - does not block main thread | |
| 63 | + | ctx := context.Background() | |
| 64 | + | cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) { | |
| 65 | + | filter, err := cbm.filterStore.SaveFinish(result) | |
| 66 | + | if err != nil { | |
| 67 | + | log.Printf("[webkit] Failed to compile content filter: %v", err) | |
| 68 | + | if onComplete != nil { | |
| 69 | + | onComplete(fmt.Errorf("failed to compile filter: %w", err)) | |
| 70 | + | } | |
| 71 | + | return | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | if filter == nil { | |
| 75 | + | log.Printf("[webkit] Filter compilation returned nil") | |
| 76 | + | if onComplete != nil { | |
| 77 | + | onComplete(fmt.Errorf("filter compilation returned nil")) | |
| 78 | + | } | |
| 79 | + | return | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // Cache the compiled filter | |
| 83 | + | cbm.mu.Lock() | |
| 84 | + | cbm.compiledFilter = filter | |
| 85 | + | cbm.mu.Unlock() | |
| 86 | + | ||
| 87 | + | log.Printf("[webkit] Filter compilation complete: %s", identifier) | |
| 88 | + | if onComplete != nil { | |
| 89 | + | onComplete(nil) | |
| 90 | + | } | |
| 91 | + | }) | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | // ApplyCompiledFilter applies the pre-compiled filter to a UserContentManager. | |
| 95 | + | // This is instant since the filter is already compiled. | |
| 96 | + | func (cbm *ContentBlockingManager) ApplyCompiledFilter(ucm *webkit.UserContentManager) error { | |
| 97 | + | if ucm == nil { | |
| 98 | + | return fmt.Errorf("UserContentManager is nil") | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | cbm.mu.RLock() | |
| 102 | + | filter := cbm.compiledFilter | |
| 103 | + | cbm.mu.RUnlock() | |
| 104 | + | ||
| 105 | + | if filter == nil { | |
| 106 | + | return fmt.Errorf("no compiled filter available") | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | ucm.AddFilter(filter) | |
| 110 | + | return nil | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | // IsFilterCompiled returns true if filters have been compiled and are ready to apply. | |
| 114 | + | func (cbm *ContentBlockingManager) IsFilterCompiled() bool { | |
| 115 | + | cbm.mu.RLock() | |
| 116 | + | defer cbm.mu.RUnlock() | |
| 117 | + | return cbm.compiledFilter != nil | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | // ApplyFiltersFromJSON compiles and applies WebKit JSON filter rules to a UserContentManager. | |
| 121 | + | // DEPRECATED: Use CompileFilters + ApplyCompiledFilter for better performance. | |
| 122 | + | // This method is kept for backward compatibility. | |
| 38 | 123 | func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentManager, identifier string, jsonRules []byte) error { | |
| 39 | 124 | if ucm == nil { | |
| 40 | 125 | return fmt.Errorf("UserContentManager is nil") | |
| 41 | 126 | } | |
| 42 | 127 | ||
| 128 | + | // Check if already compiled | |
| 129 | + | cbm.mu.RLock() | |
| 130 | + | if cbm.compiledFilter != nil { | |
| 131 | + | filter := cbm.compiledFilter | |
| 132 | + | cbm.mu.RUnlock() | |
| 133 | + | ucm.AddFilter(filter) | |
| 134 | + | log.Printf("[webkit] Applied cached content filter to UCM") | |
| 135 | + | return nil | |
| 136 | + | } | |
| 137 | + | cbm.mu.RUnlock() | |
| 138 | + | ||
| 43 | 139 | if len(jsonRules) == 0 { | |
| 44 | 140 | return fmt.Errorf("empty filter rules") | |
| 45 | 141 | } | |
| @@ -47,18 +143,12 @@ func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentM | |||
| 47 | 143 | log.Printf("[webkit] Applying content filters (identifier: %s, rules size: %d bytes)", identifier, len(jsonRules)) | |
| 48 | 144 | ||
| 49 | 145 | // Convert []byte to *glib.Bytes for WebKit API | |
| 50 | - | // NewBytesWithGo keeps the Go slice alive for the GBytes lifetime | |
| 51 | 146 | gBytes := glib.NewBytesWithGo(jsonRules) | |
| 52 | 147 | ||
| 53 | - | // Save filters to store asynchronously | |
| 54 | - | // This compiles the JSON rules into WebKit's internal format | |
| 55 | 148 | ctx := context.Background() | |
| 56 | - | ||
| 57 | - | // Use a channel to wait for the async operation | |
| 58 | 149 | done := make(chan error, 1) | |
| 59 | 150 | ||
| 60 | 151 | cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) { | |
| 61 | - | // This callback runs when compilation is complete | |
| 62 | 152 | filter, err := cbm.filterStore.SaveFinish(result) | |
| 63 | 153 | if err != nil { | |
| 64 | 154 | log.Printf("[webkit] Failed to compile content filter: %v", err) | |
| @@ -72,8 +162,12 @@ func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentM | |||
| 72 | 162 | return | |
| 73 | 163 | } | |
| 74 | 164 | ||
| 75 | - | // Add the compiled filter to the UserContentManager | |
| 76 | - | // This must be done on the main GTK thread | |
| 165 | + | // Cache for future use | |
| 166 | + | cbm.mu.Lock() | |
| 167 | + | cbm.compiledFilter = filter | |
| 168 | + | cbm.filterID = identifier | |
| 169 | + | cbm.mu.Unlock() | |
| 170 | + | ||
| 77 | 171 | RunOnMainThread(func() { | |
| 78 | 172 | ucm.AddFilter(filter) | |
| 79 | 173 | log.Printf("[webkit] Successfully added content filter: %s", identifier) | |
| @@ -81,7 +175,6 @@ func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentM | |||
| 81 | 175 | }) | |
| 82 | 176 | }) | |
| 83 | 177 | ||
| 84 | - | // Wait for completion | |
| 85 | 178 | return <-done | |
| 86 | 179 | } | |
| 87 | 180 | ||
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "log" | 6 | "log" | ||
| 7 | "path/filepath" | 7 | "path/filepath" | ||
| 8 | + | "sync" | |||
| 8 | 9 | ||||
| 9 | webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6" | 10 | webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6" | ||
| 10 | "github.com/diamondburned/gotk4/pkg/gio/v2" | 11 | "github.com/diamondburned/gotk4/pkg/gio/v2" | ||
| @@ -15,6 +16,11 @@ import ( | |||||
| 15 | type ContentBlockingManager struct { | 16 | type ContentBlockingManager struct { | ||
| 16 | filterStore *webkit.UserContentFilterStore | 17 | filterStore *webkit.UserContentFilterStore | ||
| 17 | storagePath string | 18 | storagePath string | ||
| 19 | + | ||||
| 20 | + | // Cache compiled filter to avoid recompilation | |||
| 21 | + | mu sync.RWMutex | |||
| 22 | + | compiledFilter *webkit.UserContentFilter | |||
| 23 | + | filterID string | |||
| 18 | } | 24 | } | ||
| 19 | 25 | ||||
| 20 | // NewContentBlockingManager creates a new content blocking manager | 26 | // NewContentBlockingManager creates a new content blocking manager | ||
| @@ -33,13 +39,103 @@ func NewContentBlockingManager(storagePath string) (*ContentBlockingManager, err | |||||
| 33 | }, nil | 39 | }, nil | ||
| 34 | } | 40 | } | ||
| 35 | 41 | ||||
| 36 | - | // ApplyFiltersFromJSON compiles and applies WebKit JSON filter rules to a UserContentManager | 42 | + | // CompileFilters compiles JSON filter rules and caches the result for reuse. |
| 37 | - | // The jsonRules parameter should be a JSON array of WebKit content blocker rules | 43 | + | // This should be called once during startup. Subsequent WebViews use ApplyCompiledFilter. |
| 44 | + | func (cbm *ContentBlockingManager) CompileFilters(identifier string, jsonRules []byte, onComplete func(error)) { | |||
| 45 | + | if len(jsonRules) == 0 { | |||
| 46 | + | if onComplete != nil { | |||
| 47 | + | onComplete(fmt.Errorf("empty filter rules")) | |||
| 48 | + | } | |||
| 49 | + | return | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | log.Printf("[webkit] Compiling content filters (identifier: %s, rules size: %d bytes)", identifier, len(jsonRules)) | |||
| 53 | + | ||||
| 54 | + | // Store identifier for later use | |||
| 55 | + | cbm.mu.Lock() | |||
| 56 | + | cbm.filterID = identifier | |||
| 57 | + | cbm.mu.Unlock() | |||
| 58 | + | ||||
| 59 | + | // Convert []byte to *glib.Bytes for WebKit API | |||
| 60 | + | gBytes := glib.NewBytesWithGo(jsonRules) | |||
| 61 | + | ||||
| 62 | + | // Compile asynchronously - does not block main thread | |||
| 63 | + | ctx := context.Background() | |||
| 64 | + | cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) { | |||
| 65 | + | filter, err := cbm.filterStore.SaveFinish(result) | |||
| 66 | + | if err != nil { | |||
| 67 | + | log.Printf("[webkit] Failed to compile content filter: %v", err) | |||
| 68 | + | if onComplete != nil { | |||
| 69 | + | onComplete(fmt.Errorf("failed to compile filter: %w", err)) | |||
| 70 | + | } | |||
| 71 | + | return | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | if filter == nil { | |||
| 75 | + | log.Printf("[webkit] Filter compilation returned nil") | |||
| 76 | + | if onComplete != nil { | |||
| 77 | + | onComplete(fmt.Errorf("filter compilation returned nil")) | |||
| 78 | + | } | |||
| 79 | + | return | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | // Cache the compiled filter | |||
| 83 | + | cbm.mu.Lock() | |||
| 84 | + | cbm.compiledFilter = filter | |||
| 85 | + | cbm.mu.Unlock() | |||
| 86 | + | ||||
| 87 | + | log.Printf("[webkit] Filter compilation complete: %s", identifier) | |||
| 88 | + | if onComplete != nil { | |||
| 89 | + | onComplete(nil) | |||
| 90 | + | } | |||
| 91 | + | }) | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | // ApplyCompiledFilter applies the pre-compiled filter to a UserContentManager. | |||
| 95 | + | // This is instant since the filter is already compiled. | |||
| 96 | + | func (cbm *ContentBlockingManager) ApplyCompiledFilter(ucm *webkit.UserContentManager) error { | |||
| 97 | + | if ucm == nil { | |||
| 98 | + | return fmt.Errorf("UserContentManager is nil") | |||
| 99 | + | } | |||
| 100 | + | ||||
| 101 | + | cbm.mu.RLock() | |||
| 102 | + | filter := cbm.compiledFilter | |||
| 103 | + | cbm.mu.RUnlock() | |||
| 104 | + | ||||
| 105 | + | if filter == nil { | |||
| 106 | + | return fmt.Errorf("no compiled filter available") | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | ucm.AddFilter(filter) | |||
| 110 | + | return nil | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | // IsFilterCompiled returns true if filters have been compiled and are ready to apply. | |||
| 114 | + | func (cbm *ContentBlockingManager) IsFilterCompiled() bool { | |||
| 115 | + | cbm.mu.RLock() | |||
| 116 | + | defer cbm.mu.RUnlock() | |||
| 117 | + | return cbm.compiledFilter != nil | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | // ApplyFiltersFromJSON compiles and applies WebKit JSON filter rules to a UserContentManager. | |||
| 121 | + | // DEPRECATED: Use CompileFilters + ApplyCompiledFilter for better performance. | |||
| 122 | + | // This method is kept for backward compatibility. | |||
| 38 | func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentManager, identifier string, jsonRules []byte) error { | 123 | func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentManager, identifier string, jsonRules []byte) error { | ||
| 39 | if ucm == nil { | 124 | if ucm == nil { | ||
| 40 | return fmt.Errorf("UserContentManager is nil") | 125 | return fmt.Errorf("UserContentManager is nil") | ||
| 41 | } | 126 | } | ||
| 42 | 127 | ||||
| 128 | + | // Check if already compiled | |||
| 129 | + | cbm.mu.RLock() | |||
| 130 | + | if cbm.compiledFilter != nil { | |||
| 131 | + | filter := cbm.compiledFilter | |||
| 132 | + | cbm.mu.RUnlock() | |||
| 133 | + | ucm.AddFilter(filter) | |||
| 134 | + | log.Printf("[webkit] Applied cached content filter to UCM") | |||
| 135 | + | return nil | |||
| 136 | + | } | |||
| 137 | + | cbm.mu.RUnlock() | |||
| 138 | + | ||||
| 43 | if len(jsonRules) == 0 { | 139 | if len(jsonRules) == 0 { | ||
| 44 | return fmt.Errorf("empty filter rules") | 140 | return fmt.Errorf("empty filter rules") | ||
| 45 | } | 141 | } | ||
| @@ -47,18 +143,12 @@ func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentM | |||||
| 47 | log.Printf("[webkit] Applying content filters (identifier: %s, rules size: %d bytes)", identifier, len(jsonRules)) | 143 | log.Printf("[webkit] Applying content filters (identifier: %s, rules size: %d bytes)", identifier, len(jsonRules)) | ||
| 48 | 144 | ||||
| 49 | // Convert []byte to *glib.Bytes for WebKit API | 145 | // Convert []byte to *glib.Bytes for WebKit API | ||
| 50 | - | // NewBytesWithGo keeps the Go slice alive for the GBytes lifetime | |||
| 51 | gBytes := glib.NewBytesWithGo(jsonRules) | 146 | gBytes := glib.NewBytesWithGo(jsonRules) | ||
| 52 | 147 | ||||
| 53 | - | // Save filters to store asynchronously | |||
| 54 | - | // This compiles the JSON rules into WebKit's internal format | |||
| 55 | ctx := context.Background() | 148 | ctx := context.Background() | ||
| 56 | - | ||||
| 57 | - | // Use a channel to wait for the async operation | |||
| 58 | done := make(chan error, 1) | 149 | done := make(chan error, 1) | ||
| 59 | 150 | ||||
| 60 | cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) { | 151 | cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) { | ||
| 61 | - | // This callback runs when compilation is complete | |||
| 62 | filter, err := cbm.filterStore.SaveFinish(result) | 152 | filter, err := cbm.filterStore.SaveFinish(result) | ||
| 63 | if err != nil { | 153 | if err != nil { | ||
| 64 | log.Printf("[webkit] Failed to compile content filter: %v", err) | 154 | log.Printf("[webkit] Failed to compile content filter: %v", err) | ||
| @@ -72,8 +162,12 @@ func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentM | |||||
| 72 | return | 162 | return | ||
| 73 | } | 163 | } | ||
| 74 | 164 | ||||
| 75 | - | // Add the compiled filter to the UserContentManager | 165 | + | // Cache for future use |
| 76 | - | // This must be done on the main GTK thread | 166 | + | cbm.mu.Lock() |
| 167 | + | cbm.compiledFilter = filter | |||
| 168 | + | cbm.filterID = identifier | |||
| 169 | + | cbm.mu.Unlock() | |||
| 170 | + | ||||
| 77 | RunOnMainThread(func() { | 171 | RunOnMainThread(func() { | ||
| 78 | ucm.AddFilter(filter) | 172 | ucm.AddFilter(filter) | ||
| 79 | log.Printf("[webkit] Successfully added content filter: %s", identifier) | 173 | log.Printf("[webkit] Successfully added content filter: %s", identifier) | ||
| @@ -81,7 +175,6 @@ func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentM | |||||
| 81 | }) | 175 | }) | ||
| 82 | }) | 176 | }) | ||
| 83 | 177 | ||||
| 84 | - | // Wait for completion | |||
| 85 | return <-done | 178 | return <-done | ||
| 86 | } | 179 | } | ||
| 87 | 180 | ||||