fix: guard queued viewport sync host
- commit
- 900aa670406b1583841857af94acab97a931e763
- author
- brice · · 2026-05-16 15:20 UTC
- parent
- 51fc2d4
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 1620 | 1620 | // These values are empirically tuned for CEF's async zoom application. | |
| 1621 | 1621 | func (wv *WebView) scheduleZoomRefresh() { | |
| 1622 | 1622 | for _, delayMs := range [...]int64{16, 48} { | |
| 1623 | - | purecef.PostDelayedTask(purecef.ThreadIDTidUi, purecef.NewTask(cefTaskFunc(func() { | |
| 1623 | + | task := cefNewTask(cefTaskFunc(func() { | |
| 1624 | 1624 | if wv.destroyed.Load() { | |
| 1625 | 1625 | return | |
| 1626 | 1626 | } | |
| @@ -1631,7 +1631,11 @@ func (wv *WebView) scheduleZoomRefresh() { | |||
| 1631 | 1631 | return | |
| 1632 | 1632 | } | |
| 1633 | 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 | 1648 | // These values are empirically tuned for CEF's async zoom application. | |
| 1645 | 1649 | func (wv *WebView) scheduleZoomReadback(expectedFactor, expectedLevel float64) { | |
| 1646 | 1650 | for _, delayMs := range [...]int64{0, 64} { | |
| 1647 | - | purecef.PostDelayedTask(purecef.ThreadIDTidUi, purecef.NewTask(cefTaskFunc(func() { | |
| 1651 | + | task := cefNewTask(cefTaskFunc(func() { | |
| 1648 | 1652 | if wv.destroyed.Load() { | |
| 1649 | 1653 | return | |
| 1650 | 1654 | } | |
| @@ -1665,7 +1669,11 @@ func (wv *WebView) scheduleZoomReadback(expectedFactor, expectedLevel float64) { | |||
| 1665 | 1669 | Float64("actual_cef_level", actualLevel). | |
| 1666 | 1670 | Float64("osr_backing_scale", backingScale). | |
| 1667 | 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 | ||
| @@ -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 | return | 1625 | return | ||
| 1626 | } | 1626 | } | ||
| @@ -1631,7 +1631,11 @@ func (wv *WebView) scheduleZoomRefresh() { | |||||
| 1631 | return | 1631 | 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 | return | 1653 | 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, | |||
| 269 | 269 | return | |
| 270 | 270 | } | |
| 271 | 271 | 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 | |
| 275 | 280 | } | |
| 281 | + | notifyBrowserViewportSync(host, visible) | |
| 282 | + | wv.reapplyCurrentZoomForBackingScale("viewport-sync") | |
| 276 | 283 | })) | |
| 277 | 284 | if task == nil { | |
| 278 | 285 | return | |
| @@ -269,10 +269,17 @@ func (wv *WebView) notifyViewportSyncOnCEFUIThread(host viewportSyncBrowserHost, | |||||
| 269 | return | 269 | 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 | return | 285 | return | ||
M internal/infrastructure/cef/webview_viewport_test.go +76 -5
| @@ -10,7 +10,8 @@ import ( | |||
| 10 | 10 | ||
| 11 | 11 | type viewportSyncOrderHost struct { | |
| 12 | 12 | purecef.BrowserHost | |
| 13 | - | calls []string | |
| 13 | + | calls []string | |
| 14 | + | zoomLevel float64 | |
| 14 | 15 | } | |
| 15 | 16 | ||
| 16 | 17 | func (h *viewportSyncOrderHost) WasHidden(state int32) { | |
| @@ -32,6 +33,15 @@ func (h *viewportSyncOrderHost) Invalidate(_ purecef.PaintElementType) { | |||
| 32 | 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 | 45 | func TestNotifyBrowserViewportSync_VisibleCallsFullSequence(t *testing.T) { | |
| 36 | 46 | host := &viewportSyncOrderHost{} | |
| 37 | 47 | ||
| @@ -61,21 +71,82 @@ func TestNotifyViewportSyncOnCEFUIThread_PostsCEFWork(t *testing.T) { | |||
| 61 | 71 | var scheduled purecef.Task | |
| 62 | 72 | cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 { | |
| 63 | 73 | require.Equal(t, purecef.ThreadIDTidUi, threadID) | |
| 64 | - | require.Zero(t, delayMs) | |
| 65 | 74 | require.NotNil(t, task) | |
| 66 | - | scheduled = task | |
| 75 | + | if delayMs == 0 && scheduled == nil { | |
| 76 | + | scheduled = task | |
| 77 | + | } | |
| 67 | 78 | return 1 | |
| 68 | 79 | } | |
| 69 | 80 | ||
| 70 | 81 | host := &viewportSyncOrderHost{} | |
| 71 | - | wv := &WebView{} | |
| 82 | + | wv := &WebView{ctx: context.Background(), host: host} | |
| 72 | 83 | wv.notifyViewportSyncOnCEFUIThread(host, true) | |
| 73 | 84 | ||
| 74 | 85 | require.Empty(t, host.calls) | |
| 75 | 86 | require.NotNil(t, scheduled) | |
| 76 | 87 | ||
| 77 | 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 | 152 | func TestScheduleResizeRepaintPulse_CoalescesToLatestSequence(t *testing.T) { | |
| @@ -10,7 +10,8 @@ import ( | |||||
| 10 | 10 | ||||
| 11 | type viewportSyncOrderHost struct { | 11 | type viewportSyncOrderHost struct { | ||
| 12 | purecef.BrowserHost | 12 | purecef.BrowserHost | ||
| 13 | - | calls []string | 13 | + | 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.Task | 71 | 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 = task | 75 | + | if delayMs == 0 && scheduled == nil { |
| 76 | + | scheduled = task | |||
| 77 | + | } | |||
| 67 | return 1 | 78 | 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) { | ||