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 renderStallRecoveryCooldown = 10 * time.Second
14
15func (e *Engine) logAllWebViewRenderSnapshots(now time.Time, _ port.WebViewID, reason string, classification renderStallClassification) {
16 if e == nil {
17 return
18 }
19 e.activeWebViews.Range(func(_, value any) bool {
20 wv, ok := value.(*WebView)
21 if !ok || wv == nil || wv.viewBridge == nil || wv.destroyed.Load() {
22 return true
23 }
24 diag := wv.viewBridge.Diagnostics()
25 lastPaintAge := time.Duration(0)
26 if lastPaint := latestDiagnosticEventTime(diag, "accelerated-paint"); !lastPaint.IsZero() {
27 lastPaintAge = now.Sub(lastPaint)
28 }
29 wv.mu.RLock()
30 browser := wv.browser
31 uri := wv.uri
32 title := wv.title
33 isLoading := wv.isLoading
34 pendingURI := wv.pendingURI
35 wv.mu.RUnlock()
36 wv.logRenderDiagnosticSnapshot(
37 now, reason, classification, wv.viewBridge, diag, lastPaintAge,
38 browser, uri, title, isLoading, pendingURI, wv.audioPlaying.Load(),
39 )
40 return true
41 })
42}
43
44func (wv *WebView) logRenderDiagnosticSnapshot(
45 now time.Time,
46 reason string,
47 classification renderStallClassification,
48 bridge *Cef2gtkAdapter,
49 diag cef2gtk.Diagnostics,
50 lastPaintAge time.Duration,
51 browser purecef.Browser,
52 uri, title string,
53 isLoading bool,
54 pendingURI string,
55 audioPlaying bool,
56) {
57 if wv == nil || wv.ctx == nil {
58 return
59 }
60 var browserID int32
61 if browser != nil {
62 browserID = browser.GetIdentifier()
63 }
64 width, height := int32(0), int32(0)
65 scale := float32(0)
66 if bridge != nil {
67 width, height = bridge.Size()
68 scale = bridge.DeviceScaleFactor()
69 }
70 uiHeartbeat, ioHeartbeat := wv.cefHeartbeatSnapshots(now)
71 profile, profileAge, hasProfile := wv.latestCEF2GTKProfileSnapshot(now)
72 logging.FromContext(wv.ctx).Warn().
73 Str("reason", reason).
74 Str("classification", classification.Category).
75 Bool("cef_ui_blocked", classification.CEFUIBlocked).
76 Bool("cef_io_alive", classification.CEFIOAlive).
77 Uint64("webview_id", uint64(wv.id)).
78 Int32("browser_id", browserID).
79 Str("uri", logging.TruncateURL(uri, logging.PermissionLogURLMaxLen)).
80 Str("title", title).
81 Bool("is_loading", isLoading).
82 Str("pending_uri", logging.TruncateURL(pendingURI, logging.PermissionLogURLMaxLen)).
83 Bool("audio_playing", audioPlaying).
84 Uint64("audio_packets", wv.audioPacketCount.Load()).
85 Uint64("audio_writes", wv.audioWriteCount.Load()).
86 Int64("last_accelerated_paint_age_ms", durationMillis(lastPaintAge)).
87 Str("render_backend", diag.Backend).
88 Int("accelerated_paints", diag.AcceleratedPaints).
89 Int("unsupported_paints", diag.UnsupportedPaints).
90 Int("accelerated_paint_errors", diag.AcceleratedPaintErrors).
91 Int("import_failures", diag.ImportFailures).
92 Int("render_failures", diag.RenderFailures).
93 Int("textures_built", diag.TexturesBuilt).
94 Int("texture_build_failures", diag.TextureBuildFailures).
95 Int("fd_dup_failures", diag.FDDupFailures).
96 Int("unsupported_formats", diag.UnsupportedFormats).
97 Int("paintable_swaps", diag.PaintableSwaps).
98 Bool("pending_frame", diag.PendingFrame).
99 Bool("pending_scheduled", diag.PendingScheduled).
100 Int64("pending_age_ms", durationMillis(diag.PendingAge)).
101 Uint("pending_source_id", diag.PendingSourceID).
102 Int("pending_reschedules", diag.PendingReschedules).
103 Int("pending_schedule_failures", diag.PendingScheduleFailures).
104 Int("pending_idle_callbacks", diag.PendingIdleCallbacks).
105 Bool("gdk_offload_requested", diag.OffloadRequested).
106 Bool("gdk_offload_installed", diag.OffloadInstalled).
107 Int("gdk_import_priority", diag.ImportPriority).
108 Int("gdk_retire_limit", diag.RetireLimit).
109 Int32("surface_width", width).
110 Int32("surface_height", height).
111 Float32("surface_scale", scale).
112 Bool("external_begin_frame", externalBeginFrameEnabled()).
113 Bool("profile_snapshot_available", hasProfile).
114 Int64("profile_snapshot_age_ms", durationMillis(profileAge)).
115 Uint64("profile_frames_received", profile.FramesReceived).
116 Uint64("profile_frames_queued", profile.FramesQueued).
117 Uint64("profile_frames_rendered", profile.FramesRendered).
118 Uint64("profile_import_failures", profile.ImportFailures).
119 Uint64("profile_render_failures", profile.RenderFailures).
120 Uint32("profile_gc_delta", profile.GC.NumGCDelta).
121 Int64("cef_ui_heartbeat_ack_age_ms", durationMillis(uiHeartbeat.LastAckAge)).
122 Int64("cef_ui_heartbeat_latency_ms", durationMillis(uiHeartbeat.Latency)).
123 Int32("cef_ui_heartbeat_post_result", uiHeartbeat.PostResult).
124 Bool("cef_ui_heartbeat_in_flight", uiHeartbeat.InFlight).
125 Int64("cef_io_heartbeat_ack_age_ms", durationMillis(ioHeartbeat.LastAckAge)).
126 Int64("cef_io_heartbeat_latency_ms", durationMillis(ioHeartbeat.Latency)).
127 Int32("cef_io_heartbeat_post_result", ioHeartbeat.PostResult).
128 Bool("cef_io_heartbeat_in_flight", ioHeartbeat.InFlight).
129 Msg("cef: render diagnostic snapshot")
130}
131
132func (wv *WebView) maybeRunRenderStallRecovery(now time.Time, _ *Cef2gtkAdapter, paintsBefore int) {
133 if wv == nil || wv.ctx == nil || wv.destroyed.Load() || !renderStallRecoveryEnabled() {
134 return
135 }
136 wv.renderStallMu.Lock()
137 last := wv.renderStallRecoveryLastAt
138 if !last.IsZero() && now.Sub(last) < renderStallRecoveryCooldown {
139 wv.renderStallMu.Unlock()
140 return
141 }
142 wv.renderStallMu.Unlock()
143
144 wv.mu.RLock()
145 host := wv.host
146 browser := wv.browser
147 wv.mu.RUnlock()
148 if host == nil {
149 return
150 }
151 browserID := int32(0)
152 if browser != nil {
153 browserID = browser.GetIdentifier()
154 }
155 logging.FromContext(wv.ctx).Warn().
156 Uint64("webview_id", uint64(wv.id)).
157 Int32("browser_id", browserID).
158 Int("accelerated_paints_before", paintsBefore).
159 Msg("cef: render stall recovery pulse scheduled")
160 task := cefNewTask(cefTaskFunc(func() {
161 if wv == nil || wv.viewBridge == nil || wv.destroyed.Load() || wv.ctx == nil || host == nil {
162 return
163 }
164 host.Invalidate(purecef.PaintElementTypePetView)
165 host.WasHidden(1)
166 host.WasHidden(0)
167 host.NotifyScreenInfoChanged()
168 host.WasResized()
169 host.Invalidate(purecef.PaintElementTypePetView)
170 logging.FromContext(wv.ctx).Warn().
171 Uint64("webview_id", uint64(wv.id)).
172 Int32("browser_id", browserID).
173 Msg("cef: render stall recovery pulse executed")
174 }))
175 if task == nil {
176 return
177 }
178 result := cefPostTask(purecef.ThreadIDTidUi, task)
179 logging.FromContext(wv.ctx).Warn().
180 Uint64("webview_id", uint64(wv.id)).
181 Int32("browser_id", browserID).
182 Int32("post_result", result).
183 Msg("cef: render stall recovery pulse posted")
184 if result != 1 {
185 return
186 }
187 wv.renderStallMu.Lock()
188 wv.renderStallRecoveryLastAt = now
189 wv.renderStallMu.Unlock()
190 cefScheduleAfter(2*time.Second, func() {
191 if wv == nil || wv.viewBridge == nil || wv.destroyed.Load() || wv.ctx == nil {
192 return
193 }
194 after := wv.viewBridge.Diagnostics().AcceleratedPaints
195 logging.FromContext(wv.ctx).Warn().
196 Uint64("webview_id", uint64(wv.id)).
197 Int32("browser_id", browserID).
198 Int("accelerated_paints_before", paintsBefore).
199 Int("accelerated_paints_after", after).
200 Bool("recovered", after > paintsBefore).
201 Msg("cef: render stall recovery pulse result")
202 })
203}