detached 900aa67

fix: guard queued viewport sync host

commit
900aa670406b1583841857af94acab97a931e763
author
brice · · 2026-05-16 15:20 UTC
parent
51fc2d4
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +98 -12
M internal/infrastructure/cef/webview.go +12 -4
@@ -1620,7 +1620,7 @@ func (wv *WebView) updateHoverURI(uri string) {
16201620 // These values are empirically tuned for CEF's async zoom application.
16211621 func (wv *WebView) scheduleZoomRefresh() {
16221622 for _, delayMs := range [...]int64{16, 48} {
1623- purecef.PostDelayedTask(purecef.ThreadIDTidUi, purecef.NewTask(cefTaskFunc(func() {
1623+ task := cefNewTask(cefTaskFunc(func() {
16241624 if wv.destroyed.Load() {
16251625 return
16261626 }
@@ -1631,7 +1631,11 @@ func (wv *WebView) scheduleZoomRefresh() {
16311631 return
16321632 }
16331633 host.Invalidate(purecef.PaintElementTypePetView)
1634- })), delayMs)
1634+ }))
1635+ if task == nil {
1636+ continue
1637+ }
1638+ cefPostDelayedTask(purecef.ThreadIDTidUi, task, delayMs)
16351639 }
16361640 }
16371641
@@ -1644,7 +1648,7 @@ func (wv *WebView) scheduleZoomRefresh() {
16441648 // These values are empirically tuned for CEF's async zoom application.
16451649 func (wv *WebView) scheduleZoomReadback(expectedFactor, expectedLevel float64) {
16461650 for _, delayMs := range [...]int64{0, 64} {
1647- purecef.PostDelayedTask(purecef.ThreadIDTidUi, purecef.NewTask(cefTaskFunc(func() {
1651+ task := cefNewTask(cefTaskFunc(func() {
16481652 if wv.destroyed.Load() {
16491653 return
16501654 }
@@ -1665,7 +1669,11 @@ func (wv *WebView) scheduleZoomReadback(expectedFactor, expectedLevel float64) {
16651669 Float64("actual_cef_level", actualLevel).
16661670 Float64("osr_backing_scale", backingScale).
16671671 Msg("cef: zoom level readback")
1668- })), delayMs)
1672+ }))
1673+ if task == nil {
1674+ continue
1675+ }
1676+ cefPostDelayedTask(purecef.ThreadIDTidUi, task, delayMs)
16691677 }
16701678 }
16711679
@@ -1620,7 +1620,7 @@ func (wv *WebView) updateHoverURI(uri string) {
1620 // These values are empirically tuned for CEF's async zoom application.1620 // These values are empirically tuned for CEF's async zoom application.
1621 func (wv *WebView) scheduleZoomRefresh() {1621 func (wv *WebView) scheduleZoomRefresh() {
1622 for _, delayMs := range [...]int64{16, 48} {1622 for _, delayMs := range [...]int64{16, 48} {
1623- purecef.PostDelayedTask(purecef.ThreadIDTidUi, purecef.NewTask(cefTaskFunc(func() {1623+ task := cefNewTask(cefTaskFunc(func() {
1624 if wv.destroyed.Load() {1624 if wv.destroyed.Load() {
1625 return1625 return
1626 }1626 }
@@ -1631,7 +1631,11 @@ func (wv *WebView) scheduleZoomRefresh() {
1631 return1631 return
1632 }1632 }
1633 host.Invalidate(purecef.PaintElementTypePetView)1633 host.Invalidate(purecef.PaintElementTypePetView)
1634- })), delayMs)1634+ }))
1635+ if task == nil {
1636+ continue
1637+ }
1638+ cefPostDelayedTask(purecef.ThreadIDTidUi, task, delayMs)
1635 }1639 }
1636 }1640 }
1637 1641
@@ -1644,7 +1648,7 @@ func (wv *WebView) scheduleZoomRefresh() {
1644 // These values are empirically tuned for CEF's async zoom application.1648 // These values are empirically tuned for CEF's async zoom application.
1645 func (wv *WebView) scheduleZoomReadback(expectedFactor, expectedLevel float64) {1649 func (wv *WebView) scheduleZoomReadback(expectedFactor, expectedLevel float64) {
1646 for _, delayMs := range [...]int64{0, 64} {1650 for _, delayMs := range [...]int64{0, 64} {
1647- purecef.PostDelayedTask(purecef.ThreadIDTidUi, purecef.NewTask(cefTaskFunc(func() {1651+ task := cefNewTask(cefTaskFunc(func() {
1648 if wv.destroyed.Load() {1652 if wv.destroyed.Load() {
1649 return1653 return
1650 }1654 }
@@ -1665,7 +1669,11 @@ func (wv *WebView) scheduleZoomReadback(expectedFactor, expectedLevel float64) {
1665 Float64("actual_cef_level", actualLevel).1669 Float64("actual_cef_level", actualLevel).
1666 Float64("osr_backing_scale", backingScale).1670 Float64("osr_backing_scale", backingScale).
1667 Msg("cef: zoom level readback")1671 Msg("cef: zoom level readback")
1668- })), delayMs)1672+ }))
1673+ if task == nil {
1674+ continue
1675+ }
1676+ cefPostDelayedTask(purecef.ThreadIDTidUi, task, delayMs)
1669 }1677 }
1670 }1678 }
1671 1679
M internal/infrastructure/cef/webview_viewport.go +10 -3
@@ -269,10 +269,17 @@ func (wv *WebView) notifyViewportSyncOnCEFUIThread(host viewportSyncBrowserHost,
269269 return
270270 }
271271 task := cefNewTask(cefTaskFunc(func() {
272- notifyBrowserViewportSync(host, visible)
273- if wv != nil {
274- wv.reapplyCurrentZoomForBackingScale("viewport-sync")
272+ if wv == nil || wv.destroyed.Load() {
273+ return
274+ }
275+ wv.mu.RLock()
276+ currentHost := wv.host
277+ wv.mu.RUnlock()
278+ if currentHost != host {
279+ return
275280 }
281+ notifyBrowserViewportSync(host, visible)
282+ wv.reapplyCurrentZoomForBackingScale("viewport-sync")
276283 }))
277284 if task == nil {
278285 return
@@ -269,10 +269,17 @@ func (wv *WebView) notifyViewportSyncOnCEFUIThread(host viewportSyncBrowserHost,
269 return269 return
270 }270 }
271 task := cefNewTask(cefTaskFunc(func() {271 task := cefNewTask(cefTaskFunc(func() {
272- notifyBrowserViewportSync(host, visible)272+ if wv == nil || wv.destroyed.Load() {
273- if wv != nil {273+ return
274- wv.reapplyCurrentZoomForBackingScale("viewport-sync")274+ }
275+ wv.mu.RLock()
276+ currentHost := wv.host
277+ wv.mu.RUnlock()
278+ if currentHost != host {
279+ return
275 }280 }
281+ notifyBrowserViewportSync(host, visible)
282+ wv.reapplyCurrentZoomForBackingScale("viewport-sync")
276 }))283 }))
277 if task == nil {284 if task == nil {
278 return285 return
M internal/infrastructure/cef/webview_viewport_test.go +76 -5
@@ -10,7 +10,8 @@ import (
1010
1111 type viewportSyncOrderHost struct {
1212 purecef.BrowserHost
13- calls []string
13+ calls []string
14+ zoomLevel float64
1415 }
1516
1617 func (h *viewportSyncOrderHost) WasHidden(state int32) {
@@ -32,6 +33,15 @@ func (h *viewportSyncOrderHost) Invalidate(_ purecef.PaintElementType) {
3233 h.calls = append(h.calls, "Invalidate")
3334 }
3435
36+func (h *viewportSyncOrderHost) SetZoomLevel(level float64) {
37+ h.calls = append(h.calls, "SetZoomLevel")
38+ h.zoomLevel = level
39+}
40+
41+func (h *viewportSyncOrderHost) GetZoomLevel() float64 {
42+ return h.zoomLevel
43+}
44+
3545 func TestNotifyBrowserViewportSync_VisibleCallsFullSequence(t *testing.T) {
3646 host := &viewportSyncOrderHost{}
3747
@@ -61,21 +71,82 @@ func TestNotifyViewportSyncOnCEFUIThread_PostsCEFWork(t *testing.T) {
6171 var scheduled purecef.Task
6272 cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
6373 require.Equal(t, purecef.ThreadIDTidUi, threadID)
64- require.Zero(t, delayMs)
6574 require.NotNil(t, task)
66- scheduled = task
75+ if delayMs == 0 && scheduled == nil {
76+ scheduled = task
77+ }
6778 return 1
6879 }
6980
7081 host := &viewportSyncOrderHost{}
71- wv := &WebView{}
82+ wv := &WebView{ctx: context.Background(), host: host}
7283 wv.notifyViewportSyncOnCEFUIThread(host, true)
7384
7485 require.Empty(t, host.calls)
7586 require.NotNil(t, scheduled)
7687
7788 scheduled.Execute()
78- require.Equal(t, []string{"WasHidden", "NotifyScreenInfoChanged", "WasResized", "Invalidate"}, host.calls)
89+ require.Equal(t, []string{
90+ "WasHidden",
91+ "NotifyScreenInfoChanged",
92+ "WasResized",
93+ "Invalidate",
94+ "SetZoomLevel",
95+ "NotifyScreenInfoChanged",
96+ }, host.calls)
97+}
98+
99+func TestNotifyViewportSyncOnCEFUIThread_SkipsStaleHost(t *testing.T) {
100+ oldNewTask := cefNewTask
101+ oldPostDelayedTask := cefPostDelayedTask
102+ defer func() {
103+ cefNewTask = oldNewTask
104+ cefPostDelayedTask = oldPostDelayedTask
105+ }()
106+
107+ cefNewTask = func(task purecef.Task) purecef.Task { return task }
108+
109+ var scheduled purecef.Task
110+ cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
111+ scheduled = task
112+ return 1
113+ }
114+
115+ capturedHost := &viewportSyncOrderHost{}
116+ currentHost := &viewportSyncOrderHost{}
117+ wv := &WebView{ctx: context.Background(), host: currentHost}
118+ wv.notifyViewportSyncOnCEFUIThread(capturedHost, true)
119+
120+ require.NotNil(t, scheduled)
121+ scheduled.Execute()
122+ require.Empty(t, capturedHost.calls)
123+ require.Empty(t, currentHost.calls)
124+}
125+
126+func TestNotifyViewportSyncOnCEFUIThread_SkipsDestroyedWebView(t *testing.T) {
127+ oldNewTask := cefNewTask
128+ oldPostDelayedTask := cefPostDelayedTask
129+ defer func() {
130+ cefNewTask = oldNewTask
131+ cefPostDelayedTask = oldPostDelayedTask
132+ }()
133+
134+ cefNewTask = func(task purecef.Task) purecef.Task { return task }
135+
136+ var scheduled purecef.Task
137+ cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
138+ scheduled = task
139+ return 1
140+ }
141+
142+ host := &viewportSyncOrderHost{}
143+ wv := &WebView{ctx: context.Background(), host: host}
144+ wv.destroyed.Store(true)
145+ wv.notifyViewportSyncOnCEFUIThread(host, true)
146+
147+ require.NotNil(t, scheduled)
148+ scheduled.Execute()
149+ require.Empty(t, host.calls)
79150 }
80151
81152 func TestScheduleResizeRepaintPulse_CoalescesToLatestSequence(t *testing.T) {
@@ -10,7 +10,8 @@ import (
10 10
11 type viewportSyncOrderHost struct {11 type viewportSyncOrderHost struct {
12 purecef.BrowserHost12 purecef.BrowserHost
13- calls []string13+ calls []string
14+ zoomLevel float64
14 }15 }
15 16
16 func (h *viewportSyncOrderHost) WasHidden(state int32) {17 func (h *viewportSyncOrderHost) WasHidden(state int32) {
@@ -32,6 +33,15 @@ func (h *viewportSyncOrderHost) Invalidate(_ purecef.PaintElementType) {
32 h.calls = append(h.calls, "Invalidate")33 h.calls = append(h.calls, "Invalidate")
33 }34 }
34 35
36+func (h *viewportSyncOrderHost) SetZoomLevel(level float64) {
37+ h.calls = append(h.calls, "SetZoomLevel")
38+ h.zoomLevel = level
39+}
40+
41+func (h *viewportSyncOrderHost) GetZoomLevel() float64 {
42+ return h.zoomLevel
43+}
44+
35 func TestNotifyBrowserViewportSync_VisibleCallsFullSequence(t *testing.T) {45 func TestNotifyBrowserViewportSync_VisibleCallsFullSequence(t *testing.T) {
36 host := &viewportSyncOrderHost{}46 host := &viewportSyncOrderHost{}
37 47
@@ -61,21 +71,82 @@ func TestNotifyViewportSyncOnCEFUIThread_PostsCEFWork(t *testing.T) {
61 var scheduled purecef.Task71 var scheduled purecef.Task
62 cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {72 cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
63 require.Equal(t, purecef.ThreadIDTidUi, threadID)73 require.Equal(t, purecef.ThreadIDTidUi, threadID)
64- require.Zero(t, delayMs)
65 require.NotNil(t, task)74 require.NotNil(t, task)
66- scheduled = task75+ if delayMs == 0 && scheduled == nil {
76+ scheduled = task
77+ }
67 return 178 return 1
68 }79 }
69 80
70 host := &viewportSyncOrderHost{}81 host := &viewportSyncOrderHost{}
71- wv := &WebView{}82+ wv := &WebView{ctx: context.Background(), host: host}
72 wv.notifyViewportSyncOnCEFUIThread(host, true)83 wv.notifyViewportSyncOnCEFUIThread(host, true)
73 84
74 require.Empty(t, host.calls)85 require.Empty(t, host.calls)
75 require.NotNil(t, scheduled)86 require.NotNil(t, scheduled)
76 87
77 scheduled.Execute()88 scheduled.Execute()
78- require.Equal(t, []string{"WasHidden", "NotifyScreenInfoChanged", "WasResized", "Invalidate"}, host.calls)89+ require.Equal(t, []string{
90+ "WasHidden",
91+ "NotifyScreenInfoChanged",
92+ "WasResized",
93+ "Invalidate",
94+ "SetZoomLevel",
95+ "NotifyScreenInfoChanged",
96+ }, host.calls)
97+}
98+
99+func TestNotifyViewportSyncOnCEFUIThread_SkipsStaleHost(t *testing.T) {
100+ oldNewTask := cefNewTask
101+ oldPostDelayedTask := cefPostDelayedTask
102+ defer func() {
103+ cefNewTask = oldNewTask
104+ cefPostDelayedTask = oldPostDelayedTask
105+ }()
106+
107+ cefNewTask = func(task purecef.Task) purecef.Task { return task }
108+
109+ var scheduled purecef.Task
110+ cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
111+ scheduled = task
112+ return 1
113+ }
114+
115+ capturedHost := &viewportSyncOrderHost{}
116+ currentHost := &viewportSyncOrderHost{}
117+ wv := &WebView{ctx: context.Background(), host: currentHost}
118+ wv.notifyViewportSyncOnCEFUIThread(capturedHost, true)
119+
120+ require.NotNil(t, scheduled)
121+ scheduled.Execute()
122+ require.Empty(t, capturedHost.calls)
123+ require.Empty(t, currentHost.calls)
124+}
125+
126+func TestNotifyViewportSyncOnCEFUIThread_SkipsDestroyedWebView(t *testing.T) {
127+ oldNewTask := cefNewTask
128+ oldPostDelayedTask := cefPostDelayedTask
129+ defer func() {
130+ cefNewTask = oldNewTask
131+ cefPostDelayedTask = oldPostDelayedTask
132+ }()
133+
134+ cefNewTask = func(task purecef.Task) purecef.Task { return task }
135+
136+ var scheduled purecef.Task
137+ cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
138+ scheduled = task
139+ return 1
140+ }
141+
142+ host := &viewportSyncOrderHost{}
143+ wv := &WebView{ctx: context.Background(), host: host}
144+ wv.destroyed.Store(true)
145+ wv.notifyViewportSyncOnCEFUIThread(host, true)
146+
147+ require.NotNil(t, scheduled)
148+ scheduled.Execute()
149+ require.Empty(t, host.calls)
79 }150 }
80 151
81 func TestScheduleResizeRepaintPulse_CoalescesToLatestSequence(t *testing.T) {152 func TestScheduleResizeRepaintPulse_CoalescesToLatestSequence(t *testing.T) {

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