detached 6defb29

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 Unverified2B82207D370127A0 The key is unknown
1 file · +104 -11
M pkg/webkit/content_blocking.go +104 -11
@@ -5,6 +5,7 @@ import (
55 "fmt"
66 "log"
77 "path/filepath"
8+ "sync"
89
910 webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
1011 "github.com/diamondburned/gotk4/pkg/gio/v2"
@@ -15,6 +16,11 @@ import (
1516 type ContentBlockingManager struct {
1617 filterStore *webkit.UserContentFilterStore
1718 storagePath string
19+
20+ // Cache compiled filter to avoid recompilation
21+ mu sync.RWMutex
22+ compiledFilter *webkit.UserContentFilter
23+ filterID string
1824 }
1925
2026 // NewContentBlockingManager creates a new content blocking manager
@@ -33,13 +39,103 @@ func NewContentBlockingManager(storagePath string) (*ContentBlockingManager, err
3339 }, nil
3440 }
3541
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.
38123 func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentManager, identifier string, jsonRules []byte) error {
39124 if ucm == nil {
40125 return fmt.Errorf("UserContentManager is nil")
41126 }
42127
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+
43139 if len(jsonRules) == 0 {
44140 return fmt.Errorf("empty filter rules")
45141 }
@@ -47,18 +143,12 @@ func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentM
47143 log.Printf("[webkit] Applying content filters (identifier: %s, rules size: %d bytes)", identifier, len(jsonRules))
48144
49145 // Convert []byte to *glib.Bytes for WebKit API
50- // NewBytesWithGo keeps the Go slice alive for the GBytes lifetime
51146 gBytes := glib.NewBytesWithGo(jsonRules)
52147
53- // Save filters to store asynchronously
54- // This compiles the JSON rules into WebKit's internal format
55148 ctx := context.Background()
56-
57- // Use a channel to wait for the async operation
58149 done := make(chan error, 1)
59150
60151 cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) {
61- // This callback runs when compilation is complete
62152 filter, err := cbm.filterStore.SaveFinish(result)
63153 if err != nil {
64154 log.Printf("[webkit] Failed to compile content filter: %v", err)
@@ -72,8 +162,12 @@ func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentM
72162 return
73163 }
74164
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+
77171 RunOnMainThread(func() {
78172 ucm.AddFilter(filter)
79173 log.Printf("[webkit] Successfully added content filter: %s", identifier)
@@ -81,7 +175,6 @@ func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentM
81175 })
82176 })
83177
84- // Wait for completion
85178 return <-done
86179 }
87180
@@ -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.UserContentFilterStore17 filterStore *webkit.UserContentFilterStore
17 storagePath string18 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 manager26 // NewContentBlockingManager creates a new content blocking manager
@@ -33,13 +39,103 @@ func NewContentBlockingManager(storagePath string) (*ContentBlockingManager, err
33 }, nil39 }, nil
34 }40 }
35 41
36-// ApplyFiltersFromJSON compiles and applies WebKit JSON filter rules to a UserContentManager42+// CompileFilters compiles JSON filter rules and caches the result for reuse.
37-// The jsonRules parameter should be a JSON array of WebKit content blocker rules43+// 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 API145 // 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 return162 return
73 }163 }
74 164
75- // Add the compiled filter to the UserContentManager165+ // Cache for future use
76- // This must be done on the main GTK thread166+ 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 <-done178 return <-done
86 }179 }
87 180

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close