1package cef
2
3import (
4 "time"
5
6 purecef "github.com/bnema/purego-cef/cef"
7 cef2gtk "github.com/bnema/purego-cef2gtk"
8
9 "github.com/bnema/dumber/internal/application/port"
10 "github.com/bnema/dumber/internal/logging"
11)
12
13const (
14 renderStallRecoveryCooldown = 10 * time.Second
15 // renderRecoveryVerifyDelay is how long a render pulse gets to produce a
16 // new accelerated paint before its outcome is evaluated.
17 renderRecoveryVerifyDelay = 2 * time.Second
18)
19
20func (e *Engine) logAllWebViewRenderSnapshots(now time.Time, _ port.WebViewID, reason string, classification renderStallClassification) {
21 if e == nil {
22 return
23 }
24 e.activeWebViews.Range(func(_, value any) bool {
25 wv, ok := value.(*WebView)
26 if !ok || wv == nil || wv.viewBridge == nil || wv.destroyed.Load() {
27 return true
28 }
29 diag := wv.viewBridge.Diagnostics()
30 lastPaintAge := time.Duration(0)
31 if lastPaint := latestDiagnosticEventTime(diag, "accelerated-paint"); !lastPaint.IsZero() {
32 lastPaintAge = now.Sub(lastPaint)
33 }
34 wv.mu.RLock()
35 browser := wv.browser
36 uri := wv.uri
37 title := wv.title
38 isLoading := wv.isLoading
39 pendingURI := wv.pendingURI
40 wv.mu.RUnlock()
41 wv.logRenderDiagnosticSnapshot(
42 now, reason, classification, wv.viewBridge, diag, lastPaintAge,
43 browser, uri, title, isLoading, pendingURI, wv.audioPlaying.Load(),
44 )
45 return true
46 })
47}
48
49func (wv *WebView) logRenderDiagnosticSnapshot(
50 now time.Time,
51 reason string,
52 classification renderStallClassification,
53 bridge *Cef2gtkAdapter,
54 diag cef2gtk.Diagnostics,
55 lastPaintAge time.Duration,
56 browser purecef.Browser,
57 uri, title string,
58 isLoading bool,
59 pendingURI string,
60 audioPlaying bool,
61) {
62 if wv == nil || wv.ctx == nil {
63 return
64 }
65 var browserID int32
66 if browser != nil {
67 browserID = browser.GetIdentifier()
68 }
69 width, height := int32(0), int32(0)
70 scale := float32(0)
71 if bridge != nil {
72 width, height = bridge.Size()
73 scale = bridge.DeviceScaleFactor()
74 }
75 uiHeartbeat, ioHeartbeat := wv.cefHeartbeatSnapshots(now)
76 profile, profileAge, hasProfile := wv.latestCEF2GTKProfileSnapshot(now)
77 logging.FromContext(wv.ctx).Warn().
78 Str("reason", reason).
79 Str("classification", classification.Category).
80 Bool("cef_ui_blocked", classification.CEFUIBlocked).
81 Bool("cef_io_alive", classification.CEFIOAlive).
82 Uint64("webview_id", uint64(wv.id)).
83 Int32("browser_id", browserID).
84 Str("uri", logging.TruncateURL(uri, logging.PermissionLogURLMaxLen)).
85 Str("title", title).
86 Bool("is_loading", isLoading).
87 Str("pending_uri", logging.TruncateURL(pendingURI, logging.PermissionLogURLMaxLen)).
88 Bool("audio_playing", audioPlaying).
89 Uint64("audio_packets", wv.audioPacketCount.Load()).
90 Uint64("audio_writes", wv.audioWriteCount.Load()).
91 Int64("last_accelerated_paint_age_ms", durationMillis(lastPaintAge)).
92 Str("render_backend", diag.Backend).
93 Int("accelerated_paints", diag.AcceleratedPaints).
94 Int("unsupported_paints", diag.UnsupportedPaints).
95 Int("accelerated_paint_errors", diag.AcceleratedPaintErrors).
96 Int("import_failures", diag.ImportFailures).
97 Int("render_failures", diag.RenderFailures).
98 Int("textures_built", diag.TexturesBuilt).
99 Int("texture_build_failures", diag.TextureBuildFailures).
100 Int("fd_dup_failures", diag.FDDupFailures).
101 Int("unsupported_formats", diag.UnsupportedFormats).
102 Int("paintable_swaps", diag.PaintableSwaps).
103 Bool("pending_frame", diag.PendingFrame).
104 Bool("pending_scheduled", diag.PendingScheduled).
105 Int64("pending_age_ms", durationMillis(diag.PendingAge)).
106 Uint("pending_source_id", diag.PendingSourceID).
107 Int("pending_reschedules", diag.PendingReschedules).
108 Int("pending_schedule_failures", diag.PendingScheduleFailures).
109 Int("pending_idle_callbacks", diag.PendingIdleCallbacks).
110 Bool("gdk_offload_requested", diag.OffloadRequested).
111 Bool("gdk_offload_installed", diag.OffloadInstalled).
112 Int("gdk_import_priority", diag.ImportPriority).
113 Int("gdk_retire_limit", diag.RetireLimit).
114 Int32("surface_width", width).
115 Int32("surface_height", height).
116 Float32("surface_scale", scale).
117 Bool("external_begin_frame", externalBeginFrameEnabled()).
118 Bool("profile_snapshot_available", hasProfile).
119 Int64("profile_snapshot_age_ms", durationMillis(profileAge)).
120 Uint64("profile_frames_received", profile.FramesReceived).
121 Uint64("profile_frames_queued", profile.FramesQueued).
122 Uint64("profile_frames_rendered", profile.FramesRendered).
123 Uint64("profile_import_failures", profile.ImportFailures).
124 Uint64("profile_render_failures", profile.RenderFailures).
125 Uint32("profile_gc_delta", profile.GC.NumGCDelta).
126 Int64("cef_ui_heartbeat_ack_age_ms", durationMillis(uiHeartbeat.LastAckAge)).
127 Int64("cef_ui_heartbeat_latency_ms", durationMillis(uiHeartbeat.Latency)).
128 Int32("cef_ui_heartbeat_post_result", uiHeartbeat.PostResult).
129 Bool("cef_ui_heartbeat_in_flight", uiHeartbeat.InFlight).
130 Int64("cef_io_heartbeat_ack_age_ms", durationMillis(ioHeartbeat.LastAckAge)).
131 Int64("cef_io_heartbeat_latency_ms", durationMillis(ioHeartbeat.Latency)).
132 Int32("cef_io_heartbeat_post_result", ioHeartbeat.PostResult).
133 Bool("cef_io_heartbeat_in_flight", ioHeartbeat.InFlight).
134 Msg("cef: render diagnostic snapshot")
135}
136
137func (wv *WebView) maybeRunRenderStallRecovery(now time.Time, _ *Cef2gtkAdapter, paintsBefore int) {
138 if wv == nil || wv.ctx == nil || wv.destroyed.Load() || !renderStallRecoveryEnabled() {
139 return
140 }
141 wv.renderStallMu.Lock()
142 last := wv.renderStallRecoveryLastAt
143 if !last.IsZero() && now.Sub(last) < renderStallRecoveryCooldown {
144 wv.renderStallMu.Unlock()
145 return
146 }
147 wv.renderStallMu.Unlock()
148
149 target, ok := wv.renderPulseTarget()
150 if !ok {
151 return
152 }
153 browserID := target.browserID
154 logging.FromContext(wv.ctx).Warn().
155 Uint64("webview_id", uint64(wv.id)).
156 Int32("browser_id", browserID).
157 Int("accelerated_paints_before", paintsBefore).
158 Msg("cef: render stall recovery pulse scheduled")
159 const msg = "cef: render stall recovery pulse"
160 posted := wv.postRenderPulseTask(msg, func(host purecef.BrowserHost, _ *Cef2gtkAdapter) {
161 // Hidden views legitimately produce no paints and the pulse would
162 // mark them visible, so only pulse views CEF believes are shown.
163 if wv.effectivelyHidden() {
164 logging.FromContext(wv.ctx).Warn().
165 Uint64("webview_id", uint64(wv.id)).
166 Int32("browser_id", browserID).
167 Msg(msg + " skipped: view hidden")
168 return
169 }
170 pulseRender(host)
171 logging.FromContext(wv.ctx).Warn().
172 Uint64("webview_id", uint64(wv.id)).
173 Int32("browser_id", browserID).
174 Msg(msg + " executed")
175 })
176 if !posted {
177 return
178 }
179 wv.renderStallMu.Lock()
180 wv.renderStallRecoveryLastAt = now
181 wv.renderStallMu.Unlock()
182 cefScheduleAfter(renderRecoveryVerifyDelay, func() {
183 bridge := wv.liveBridge()
184 if bridge == nil || wv.ctx == nil {
185 return
186 }
187 after := bridge.Diagnostics().AcceleratedPaints
188 logging.FromContext(wv.ctx).Warn().
189 Uint64("webview_id", uint64(wv.id)).
190 Int32("browser_id", browserID).
191 Int("accelerated_paints_before", paintsBefore).
192 Int("accelerated_paints_after", after).
193 Bool("recovered", after > paintsBefore).
194 Msg("cef: render stall recovery pulse result")
195 })
196}
197
198// renderPulseTargetInfo is a consistent snapshot of the state a render pulse
199// needs, captured under wv.mu.
200type renderPulseTargetInfo struct {
201 host purecef.BrowserHost
202 bridge *Cef2gtkAdapter
203 browserID int32
204 hasBrowser bool
205}
206
207// renderPulseTarget snapshots a view that can receive a render pulse. It
208// reports false when the view is destroyed, has no CEF host yet, or has no
209// live bridge. The bridge is cleared under wv.mu by teardown, so it must only
210// be read through this snapshot or liveBridge.
211func (wv *WebView) renderPulseTarget() (renderPulseTargetInfo, bool) {
212 if wv == nil || wv.ctx == nil || wv.destroyed.Load() {
213 return renderPulseTargetInfo{}, false
214 }
215 wv.mu.RLock()
216 host := wv.host
217 browser := wv.browser
218 bridge := wv.viewBridge
219 wv.mu.RUnlock()
220 if host == nil || bridge == nil {
221 return renderPulseTargetInfo{}, false
222 }
223 info := renderPulseTargetInfo{host: host, bridge: bridge}
224 if browser != nil {
225 info.browserID = browser.GetIdentifier()
226 info.hasBrowser = true
227 }
228 return info, true
229}
230
231// liveBridge returns the view bridge, or nil once the view is destroyed or its
232// bridge torn down. Safe from any goroutine.
233func (wv *WebView) liveBridge() *Cef2gtkAdapter {
234 if wv == nil || wv.destroyed.Load() {
235 return nil
236 }
237 wv.mu.RLock()
238 defer wv.mu.RUnlock()
239 return wv.viewBridge
240}
241
242// pulseRender asks CEF to produce a fresh OSR frame.
243func pulseRender(host purecef.BrowserHost) {
244 host.Invalidate(purecef.PaintElementTypePetView)
245 host.WasHidden(1)
246 host.WasHidden(0)
247 host.NotifyScreenInfoChanged()
248 host.WasResized()
249 host.Invalidate(purecef.PaintElementTypePetView)
250}
251
252// postRenderPulseTask posts body to the CEF UI thread. The host and bridge are
253// revalidated when the task executes; body only runs for a live view. msg is
254// the log message prefix. It reports whether CEF accepted the task.
255func (wv *WebView) postRenderPulseTask(msg string, body func(host purecef.BrowserHost, bridge *Cef2gtkAdapter)) bool {
256 target, ok := wv.renderPulseTarget()
257 if !ok {
258 return false
259 }
260 host := target.host
261 task := cefNewTask(cefTaskFunc(func() {
262 bridge := wv.liveBridge()
263 if bridge == nil {
264 return
265 }
266 wv.mu.RLock()
267 currentHost := wv.host
268 wv.mu.RUnlock()
269 if currentHost != host {
270 return
271 }
272 body(host, bridge)
273 }))
274 if task == nil {
275 return false
276 }
277 result := cefPostTask(purecef.ThreadIDTidUi, task)
278 logging.FromContext(wv.ctx).Warn().
279 Uint64("webview_id", uint64(wv.id)).
280 Int32("browser_id", target.browserID).
281 Int32("post_result", result).
282 Msg(msg + " posted")
283 return result == 1
284}