v0.33.1 9577fee

render_diagnostics.go RawHistoryFolder

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}

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