detached 81c09fd

perf(cef): opt-in BeginFrame from the frame clock and pin the OSR rate (#429)

* perf(cef): drive BeginFrame from the frame clock and allow pinning the rate

Pins the bridge revision that carries the presenter knobs, and applies the two
Dumber-side frame-path changes:

- CEF produces frames on BeginFrame ticks driven by the GTK frame clock instead
  of its own timer, so frame production follows the widget's cadence;
- `DUMBER_CEF_WINDOWLESS_FRAME_RATE` pins the OSR frame rate and takes
  precedence over the configured value and over adaptive monitor polling, so a
  run can be held at one cadence without editing the config file.

Non-positive and unparsable frame rates fall back to the configured behaviour,
and `DUMBER_CEF_EXTERNAL_BEGIN_FRAME=0` restores CEF's own timer.

The rate is parsed with a 32-bit bound rather than converted from Atoi, which
the linter rejects as a potential overflow.

This is not an ownership change: the bridge still presents a borrowed DMA-BUF
that CEF releases to its pool when the callback returns.

* chore(deps): pin the render-path bridge revision

Updates purego-cef2gtk to the revision that gates GtkGraphicsOffload on
the runtime GTK version, reports the effective offload, import-priority
and retire-limit settings in Diagnostics, and delivers batched wheel
motion once per frame tick.

* fix(cef): keep the external BeginFrame opt-in

The env flag had been inverted into a default-on switch. Restore the
canonical opt-in semantics: only an explicit true value drives CEF
BeginFrame from the GTK frame clock, while unset, empty, invalid and
false values keep CEF's own cadence. The tests now exercise
externalBeginFrameEnabled itself instead of the generic boolean helper.

The pinned OSR rate override stays: a valid positive
DUMBER_CEF_WINDOWLESS_FRAME_RATE wins over the configured value and
disables adaptive monitor-refresh polling, with invalid, non-positive and
overflowing values falling back to the configured behaviour. That pin
selects how often CEF is asked to capture frames; it is independent of
the external BeginFrame request rate.

The render diagnostic snapshot now also reports the effective GDK
offload request, import priority and retire limit.

* chore(deps): repin the bridge to the reviewed render-path revision

Moves the pin to the bridge revision that reports the offload request and
its installation separately, keeps offload recovery to the constructor, and
delivers the accrued wheel share when a stall retires a burst. The stale
go.sum lines for superseded revisions are dropped.

* fix(cef): log the pinned OSR rate and cover the BeginFrame call site

A pinned OSR rate silently suspended adaptive polling without saying so in
the startup log, so the cef: configured engine line now reports
windowless_frame_rate_pinned next to the rate it resolves to.

The BeginFrame opt-in was only ever asserted through the boolean helper,
which is how an inverted default shipped; a call-site test now drives
configureNativePopupWindow over absent, empty, false, invalid and true
values and checks the WindowInfo field itself. The render diagnostic
snapshot also reports gdk_offload_installed next to the request.

* chore(deps): pin purego-cef2gtk v0.11.0

The released tag carries the same content as the pseudo-version this branch
pinned (the bridge pull request was squash-merged), so this swaps a
branch-scoped revision for the released one without changing behaviour.

* feat(cef): enable external BeginFrame by default

The embedder's frame clock is the cadence the compositor samples, and the
tick implementation already exists, so CEF now waits for a BeginFrame
request on each tick instead of running its own timer. The environment
variable stays as the opt-out: 0, false, no or off restores CEF's internal
cadence.

This is a deliberate default under observation rather than a measured win.
The tick loop requests a frame on every tick regardless of the configured
capture rate, and no frame-timing measurement shows an improvement over
CEF's own timer yet, so the default is being dogfooded and can be reverted
to opt-in without touching the implementation. The comment on the helper
and the call-site test say so, and both now assert the default rather than
an opt-in.
commit
81c09fd1c5fd5608be698f9b56692adb04875e16
author
bnema · · 2026-09-11 10:24 UTC
committer
GitHub · · 2026-09-11 10:24 UTC
parent
62dc92c
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
8 files · +180 -12
M go.mod +1 -1
@@ -7,7 +7,7 @@ require (
77 github.com/andybalholm/brotli v1.2.3
88 github.com/bnema/purego v0.11.0-bnema.4
99 github.com/bnema/purego-cef v0.14.2
10- github.com/bnema/purego-cef2gtk v0.10.2-0.20260911042437-e9e81371d3b8
10+ github.com/bnema/purego-cef2gtk v0.11.0
1111 github.com/bnema/purego-pipewire v0.1.6
1212 github.com/bnema/purego-sqlite v0.1.5
1313 github.com/bnema/purego-webp v0.2.1
@@ -7,7 +7,7 @@ require (
7 github.com/andybalholm/brotli v1.2.37 github.com/andybalholm/brotli v1.2.3
8 github.com/bnema/purego v0.11.0-bnema.48 github.com/bnema/purego v0.11.0-bnema.4
9 github.com/bnema/purego-cef v0.14.29 github.com/bnema/purego-cef v0.14.2
10- github.com/bnema/purego-cef2gtk v0.10.2-0.20260911042437-e9e81371d3b810+ github.com/bnema/purego-cef2gtk v0.11.0
11 github.com/bnema/purego-pipewire v0.1.611 github.com/bnema/purego-pipewire v0.1.6
12 github.com/bnema/purego-sqlite v0.1.512 github.com/bnema/purego-sqlite v0.1.5
13 github.com/bnema/purego-webp v0.2.113 github.com/bnema/purego-webp v0.2.1
M go.sum +2 -2
@@ -14,8 +14,8 @@ github.com/bnema/purego v0.11.0-bnema.4 h1:uQjx9QtM945rD6HkwYzPGuVN7wKr5HrFKIWE3
1414 github.com/bnema/purego v0.11.0-bnema.4/go.mod h1:LHW8Sb4QO18dQaLVRU8tZCGM0SjsC+agFW/3M3nCUtg=
1515 github.com/bnema/purego-cef v0.14.2 h1:0tKXT00gFvWUsL3A4sX4kzD+3XRmPOif0ccwO9iokqU=
1616 github.com/bnema/purego-cef v0.14.2/go.mod h1:r+qCMwzuI+wq3xo189x3NAWWpALWoWwjtk+8wVCK1Eg=
17-github.com/bnema/purego-cef2gtk v0.10.2-0.20260911042437-e9e81371d3b8 h1:9BtmuhH5raXQKXwI0+owr/iD1oT0hkjCHhS/WUPUiXk=
18-github.com/bnema/purego-cef2gtk v0.10.2-0.20260911042437-e9e81371d3b8/go.mod h1:1tIL/HpDXXwNSIgtLVgumjApAEP7noO0DcHPhmEcgcU=
17+github.com/bnema/purego-cef2gtk v0.11.0 h1:imn2AzfqXijtWw1NXE4E7niIja7itKQATDS/JPAEGPw=
18+github.com/bnema/purego-cef2gtk v0.11.0/go.mod h1:1tIL/HpDXXwNSIgtLVgumjApAEP7noO0DcHPhmEcgcU=
1919 github.com/bnema/purego-pipewire v0.1.6 h1:olbxeiqIJZXil0pus5Uzh9psH/KTI9GovqTCuPXTquU=
2020 github.com/bnema/purego-pipewire v0.1.6/go.mod h1:57aFYl3Wll3XbrwblQjmX8R1k7RkcZQwfQTGQWcKbdc=
2121 github.com/bnema/purego-sqlite v0.1.5 h1:L4KtDQR0++TZqHVnuzCPsrw55s1ww/nXGEpdrkHsM4Y=
@@ -14,8 +14,8 @@ github.com/bnema/purego v0.11.0-bnema.4 h1:uQjx9QtM945rD6HkwYzPGuVN7wKr5HrFKIWE3
14 github.com/bnema/purego v0.11.0-bnema.4/go.mod h1:LHW8Sb4QO18dQaLVRU8tZCGM0SjsC+agFW/3M3nCUtg=14 github.com/bnema/purego v0.11.0-bnema.4/go.mod h1:LHW8Sb4QO18dQaLVRU8tZCGM0SjsC+agFW/3M3nCUtg=
15 github.com/bnema/purego-cef v0.14.2 h1:0tKXT00gFvWUsL3A4sX4kzD+3XRmPOif0ccwO9iokqU=15 github.com/bnema/purego-cef v0.14.2 h1:0tKXT00gFvWUsL3A4sX4kzD+3XRmPOif0ccwO9iokqU=
16 github.com/bnema/purego-cef v0.14.2/go.mod h1:r+qCMwzuI+wq3xo189x3NAWWpALWoWwjtk+8wVCK1Eg=16 github.com/bnema/purego-cef v0.14.2/go.mod h1:r+qCMwzuI+wq3xo189x3NAWWpALWoWwjtk+8wVCK1Eg=
17-github.com/bnema/purego-cef2gtk v0.10.2-0.20260911042437-e9e81371d3b8 h1:9BtmuhH5raXQKXwI0+owr/iD1oT0hkjCHhS/WUPUiXk=17+github.com/bnema/purego-cef2gtk v0.11.0 h1:imn2AzfqXijtWw1NXE4E7niIja7itKQATDS/JPAEGPw=
18-github.com/bnema/purego-cef2gtk v0.10.2-0.20260911042437-e9e81371d3b8/go.mod h1:1tIL/HpDXXwNSIgtLVgumjApAEP7noO0DcHPhmEcgcU=18+github.com/bnema/purego-cef2gtk v0.11.0/go.mod h1:1tIL/HpDXXwNSIgtLVgumjApAEP7noO0DcHPhmEcgcU=
19 github.com/bnema/purego-pipewire v0.1.6 h1:olbxeiqIJZXil0pus5Uzh9psH/KTI9GovqTCuPXTquU=19 github.com/bnema/purego-pipewire v0.1.6 h1:olbxeiqIJZXil0pus5Uzh9psH/KTI9GovqTCuPXTquU=
20 github.com/bnema/purego-pipewire v0.1.6/go.mod h1:57aFYl3Wll3XbrwblQjmX8R1k7RkcZQwfQTGQWcKbdc=20 github.com/bnema/purego-pipewire v0.1.6/go.mod h1:57aFYl3Wll3XbrwblQjmX8R1k7RkcZQwfQTGQWcKbdc=
21 github.com/bnema/purego-sqlite v0.1.5 h1:L4KtDQR0++TZqHVnuzCPsrw55s1ww/nXGEpdrkHsM4Y=21 github.com/bnema/purego-sqlite v0.1.5 h1:L4KtDQR0++TZqHVnuzCPsrw55s1ww/nXGEpdrkHsM4Y=
M internal/infrastructure/cef/adaptive_frame_rate.go +10 -0
@@ -23,6 +23,16 @@ func adaptiveFrameRateForRefresh(refreshRateMilliHz int, fallbackFPS, maxFPS int
2323 })
2424 }
2525
26+// resolveWindowlessFrameRate applies a pinned rate on top of the configured OSR
27+// frame rate. A pinned rate wins over both the configured value and the adaptive
28+// monitor polling, which is suspended so the pinned cadence is what CEF keeps.
29+func resolveWindowlessFrameRate(configRate int32, adaptive bool) (int32, bool) {
30+ if pinned, ok := windowlessFrameRateOverride(); ok {
31+ return pinned, false
32+ }
33+ return normalizedWindowlessFrameRate(configRate), adaptive
34+}
35+
2636 func (wv *WebView) scheduleStartAdaptiveFrameRatePolling() {
2737 if wv == nil || !wv.adaptiveWindowlessFrameRate {
2838 return
@@ -23,6 +23,16 @@ func adaptiveFrameRateForRefresh(refreshRateMilliHz int, fallbackFPS, maxFPS int
23 })23 })
24 }24 }
25 25
26+// resolveWindowlessFrameRate applies a pinned rate on top of the configured OSR
27+// frame rate. A pinned rate wins over both the configured value and the adaptive
28+// monitor polling, which is suspended so the pinned cadence is what CEF keeps.
29+func resolveWindowlessFrameRate(configRate int32, adaptive bool) (int32, bool) {
30+ if pinned, ok := windowlessFrameRateOverride(); ok {
31+ return pinned, false
32+ }
33+ return normalizedWindowlessFrameRate(configRate), adaptive
34+}
35+
26 func (wv *WebView) scheduleStartAdaptiveFrameRatePolling() {36 func (wv *WebView) scheduleStartAdaptiveFrameRatePolling() {
27 if wv == nil || !wv.adaptiveWindowlessFrameRate {37 if wv == nil || !wv.adaptiveWindowlessFrameRate {
28 return38 return
M internal/infrastructure/cef/engine_init.go +3 -2
@@ -50,7 +50,7 @@ func NewEngine(
5050 logger.Warn().Str("state_root", stateRoot).Msg("cef: applying next-start GPU safety recovery")
5151 }
5252 cleanStaleSingletonLocks(logger, stateRoot)
53- windowlessFrameRate := normalizedWindowlessFrameRate(cfg.WindowlessFrameRate)
53+ windowlessFrameRate, adaptiveWindowlessFrameRate := resolveWindowlessFrameRate(cfg.WindowlessFrameRate, cfg.AdaptiveWindowlessFrameRate)
5454 renderStackPlan, err := resolveCEFRenderStackPlan(cfg.RenderStack)
5555 if err != nil {
5656 return nil, err
@@ -89,6 +89,7 @@ func NewEngine(
8989
9090 logger.Info().
9191 Int32("windowless_frame_rate", windowlessFrameRate).
92+ Bool("windowless_frame_rate_pinned", windowlessFrameRatePinned()).
9293 Str("render_stack", string(renderStackPlan.Stack)).
9394 Str("render_backend", renderStackPlan.Backend.String()).
9495 Str("angle_backend", renderStackPlan.ANGLEBackend).
@@ -109,7 +110,7 @@ func NewEngine(
109110 ctx,
110111 eng,
111112 webViewFactoryOptions{
112- adaptiveWindowlessFrameRate: cfg.AdaptiveWindowlessFrameRate,
113+ adaptiveWindowlessFrameRate: adaptiveWindowlessFrameRate,
113114 windowlessFrameRate: windowlessFrameRate,
114115 windowlessFrameRateMax: cfg.WindowlessFrameRateMax,
115116 inputConfig: cfg.Input,
@@ -50,7 +50,7 @@ func NewEngine(
50 logger.Warn().Str("state_root", stateRoot).Msg("cef: applying next-start GPU safety recovery")50 logger.Warn().Str("state_root", stateRoot).Msg("cef: applying next-start GPU safety recovery")
51 }51 }
52 cleanStaleSingletonLocks(logger, stateRoot)52 cleanStaleSingletonLocks(logger, stateRoot)
53- windowlessFrameRate := normalizedWindowlessFrameRate(cfg.WindowlessFrameRate)53+ windowlessFrameRate, adaptiveWindowlessFrameRate := resolveWindowlessFrameRate(cfg.WindowlessFrameRate, cfg.AdaptiveWindowlessFrameRate)
54 renderStackPlan, err := resolveCEFRenderStackPlan(cfg.RenderStack)54 renderStackPlan, err := resolveCEFRenderStackPlan(cfg.RenderStack)
55 if err != nil {55 if err != nil {
56 return nil, err56 return nil, err
@@ -89,6 +89,7 @@ func NewEngine(
89 89
90 logger.Info().90 logger.Info().
91 Int32("windowless_frame_rate", windowlessFrameRate).91 Int32("windowless_frame_rate", windowlessFrameRate).
92+ Bool("windowless_frame_rate_pinned", windowlessFrameRatePinned()).
92 Str("render_stack", string(renderStackPlan.Stack)).93 Str("render_stack", string(renderStackPlan.Stack)).
93 Str("render_backend", renderStackPlan.Backend.String()).94 Str("render_backend", renderStackPlan.Backend.String()).
94 Str("angle_backend", renderStackPlan.ANGLEBackend).95 Str("angle_backend", renderStackPlan.ANGLEBackend).
@@ -109,7 +110,7 @@ func NewEngine(
109 ctx,110 ctx,
110 eng,111 eng,
111 webViewFactoryOptions{112 webViewFactoryOptions{
112- adaptiveWindowlessFrameRate: cfg.AdaptiveWindowlessFrameRate,113+ adaptiveWindowlessFrameRate: adaptiveWindowlessFrameRate,
113 windowlessFrameRate: windowlessFrameRate,114 windowlessFrameRate: windowlessFrameRate,
114 windowlessFrameRateMax: cfg.WindowlessFrameRateMax,115 windowlessFrameRateMax: cfg.WindowlessFrameRateMax,
115 inputConfig: cfg.Input,116 inputConfig: cfg.Input,
M internal/infrastructure/cef/env.go +39 -1
@@ -2,11 +2,13 @@ package cef
22
33 import (
44 "os"
5+ "strconv"
56 "strings"
67 )
78
89 const (
910 cefExternalBeginFrameEnvVar = "DUMBER_CEF_EXTERNAL_BEGIN_FRAME"
11+ cefWindowlessFrameRateEnvVar = "DUMBER_CEF_WINDOWLESS_FRAME_RATE"
1012 cefEnableWebAuthnUnsafeEnvVar = "DUMBER_CEF_ENABLE_WEBAUTHN_UNSAFE"
1113 cefChromiumFlagsEnvVar = "DUMBER_CEF_CHROMIUM_FLAGS"
1214 cefEnableVAAPIEnvVar = "DUMBER_CEF_ENABLE_VAAPI"
@@ -28,8 +30,44 @@ func envBoolEnabled(envVar string) bool {
2830 }
2931 }
3032
33+// externalBeginFrameEnabled reports whether CEF produces frames on BeginFrame
34+// ticks driven by the GTK frame clock instead of its own timer. It is enabled by
35+// default: the embedder's frame clock is the cadence the compositor samples, so
36+// asking CEF for a frame on each tick aligns production with presentation. A
37+// falsy value restores CEF's internal cadence.
38+//
39+// This is an experimental default under observation: the tick loop requests a
40+// frame on every tick regardless of the configured capture rate, and no
41+// frame-timing measurement yet shows an improvement over CEF's own timer.
3142 func externalBeginFrameEnabled() bool {
32- return envBoolEnabled(cefExternalBeginFrameEnvVar)
43+ switch strings.ToLower(strings.TrimSpace(os.Getenv(cefExternalBeginFrameEnvVar))) {
44+ case "0", "false", "no", "off":
45+ return false
46+ default:
47+ return true
48+ }
49+}
50+
51+// windowlessFrameRateOverride returns an explicitly pinned OSR frame rate. A
52+// pinned rate takes precedence over the configured value and disables adaptive
53+// monitor-refresh polling, so a run can be compared at a single cadence.
54+func windowlessFrameRateOverride() (int32, bool) {
55+ raw := strings.TrimSpace(os.Getenv(cefWindowlessFrameRateEnvVar))
56+ if raw == "" {
57+ return 0, false
58+ }
59+ rate, err := strconv.ParseInt(raw, 10, 32)
60+ if err != nil || rate <= 0 {
61+ return 0, false
62+ }
63+ return int32(rate), true
64+}
65+
66+// windowlessFrameRatePinned reports whether the environment pins the OSR frame
67+// rate, which also suspends adaptive monitor-refresh polling.
68+func windowlessFrameRatePinned() bool {
69+ _, pinned := windowlessFrameRateOverride()
70+ return pinned
3371 }
3472
3573 func cefWebAuthnUnsafeEnabled() bool {
@@ -2,11 +2,13 @@ package cef
2 2
3 import (3 import (
4 "os"4 "os"
5+ "strconv"
5 "strings"6 "strings"
6 )7 )
7 8
8 const (9 const (
9 cefExternalBeginFrameEnvVar = "DUMBER_CEF_EXTERNAL_BEGIN_FRAME"10 cefExternalBeginFrameEnvVar = "DUMBER_CEF_EXTERNAL_BEGIN_FRAME"
11+ cefWindowlessFrameRateEnvVar = "DUMBER_CEF_WINDOWLESS_FRAME_RATE"
10 cefEnableWebAuthnUnsafeEnvVar = "DUMBER_CEF_ENABLE_WEBAUTHN_UNSAFE"12 cefEnableWebAuthnUnsafeEnvVar = "DUMBER_CEF_ENABLE_WEBAUTHN_UNSAFE"
11 cefChromiumFlagsEnvVar = "DUMBER_CEF_CHROMIUM_FLAGS"13 cefChromiumFlagsEnvVar = "DUMBER_CEF_CHROMIUM_FLAGS"
12 cefEnableVAAPIEnvVar = "DUMBER_CEF_ENABLE_VAAPI"14 cefEnableVAAPIEnvVar = "DUMBER_CEF_ENABLE_VAAPI"
@@ -28,8 +30,44 @@ func envBoolEnabled(envVar string) bool {
28 }30 }
29 }31 }
30 32
33+// externalBeginFrameEnabled reports whether CEF produces frames on BeginFrame
34+// ticks driven by the GTK frame clock instead of its own timer. It is enabled by
35+// default: the embedder's frame clock is the cadence the compositor samples, so
36+// asking CEF for a frame on each tick aligns production with presentation. A
37+// falsy value restores CEF's internal cadence.
38+//
39+// This is an experimental default under observation: the tick loop requests a
40+// frame on every tick regardless of the configured capture rate, and no
41+// frame-timing measurement yet shows an improvement over CEF's own timer.
31 func externalBeginFrameEnabled() bool {42 func externalBeginFrameEnabled() bool {
32- return envBoolEnabled(cefExternalBeginFrameEnvVar)43+ switch strings.ToLower(strings.TrimSpace(os.Getenv(cefExternalBeginFrameEnvVar))) {
44+ case "0", "false", "no", "off":
45+ return false
46+ default:
47+ return true
48+ }
49+}
50+
51+// windowlessFrameRateOverride returns an explicitly pinned OSR frame rate. A
52+// pinned rate takes precedence over the configured value and disables adaptive
53+// monitor-refresh polling, so a run can be compared at a single cadence.
54+func windowlessFrameRateOverride() (int32, bool) {
55+ raw := strings.TrimSpace(os.Getenv(cefWindowlessFrameRateEnvVar))
56+ if raw == "" {
57+ return 0, false
58+ }
59+ rate, err := strconv.ParseInt(raw, 10, 32)
60+ if err != nil || rate <= 0 {
61+ return 0, false
62+ }
63+ return int32(rate), true
64+}
65+
66+// windowlessFrameRatePinned reports whether the environment pins the OSR frame
67+// rate, which also suspends adaptive monitor-refresh polling.
68+func windowlessFrameRatePinned() bool {
69+ _, pinned := windowlessFrameRateOverride()
70+ return pinned
33 }71 }
34 72
35 func cefWebAuthnUnsafeEnabled() bool {73 func cefWebAuthnUnsafeEnabled() bool {
M internal/infrastructure/cef/env_test.go +8 -6
@@ -2,32 +2,34 @@ package cef
22
33 import "testing"
44
5-func TestExternalBeginFrameEnabledIsOptIn(t *testing.T) {
5+func TestExternalBeginFrameEnabledIsOnByDefault(t *testing.T) {
66 tests := []struct {
77 name string
88 value string
99 want bool
1010 }{
11+ {name: "unset", value: "", want: true},
12+ {name: "blank", value: " ", want: true},
1113 {name: "1", value: "1", want: true},
1214 {name: "true", value: "true", want: true},
1315 {name: "yes", value: "yes", want: true},
1416 {name: "on", value: "on", want: true},
1517 {name: "TRUE", value: "TRUE", want: true},
1618 {name: " true ", value: " true ", want: true},
17- {name: "", value: "", want: false},
18- {name: "false", value: "false", want: false},
19+ {name: "random enables", value: "random", want: true},
1920 {name: "0", value: "0", want: false},
21+ {name: "false", value: "false", want: false},
2022 {name: "no", value: "no", want: false},
2123 {name: "off", value: "off", want: false},
22- {name: "random", value: "random", want: false},
24+ {name: "OFF", value: "OFF", want: false},
2325 }
2426
2527 for _, tt := range tests {
2628 t.Run(tt.name, func(t *testing.T) {
2729 t.Setenv(cefExternalBeginFrameEnvVar, tt.value)
2830
29- if got := envBoolEnabled(cefExternalBeginFrameEnvVar); got != tt.want {
30- t.Fatalf("envBoolEnabled(%q) = %v, want %v", tt.value, got, tt.want)
31+ if got := externalBeginFrameEnabled(); got != tt.want {
32+ t.Fatalf("externalBeginFrameEnabled(%q) = %v, want %v", tt.value, got, tt.want)
3133 }
3234 })
3335 }
@@ -2,32 +2,34 @@ package cef
2 2
3 import "testing"3 import "testing"
4 4
5-func TestExternalBeginFrameEnabledIsOptIn(t *testing.T) {5+func TestExternalBeginFrameEnabledIsOnByDefault(t *testing.T) {
6 tests := []struct {6 tests := []struct {
7 name string7 name string
8 value string8 value string
9 want bool9 want bool
10 }{10 }{
11+ {name: "unset", value: "", want: true},
12+ {name: "blank", value: " ", want: true},
11 {name: "1", value: "1", want: true},13 {name: "1", value: "1", want: true},
12 {name: "true", value: "true", want: true},14 {name: "true", value: "true", want: true},
13 {name: "yes", value: "yes", want: true},15 {name: "yes", value: "yes", want: true},
14 {name: "on", value: "on", want: true},16 {name: "on", value: "on", want: true},
15 {name: "TRUE", value: "TRUE", want: true},17 {name: "TRUE", value: "TRUE", want: true},
16 {name: " true ", value: " true ", want: true},18 {name: " true ", value: " true ", want: true},
17- {name: "", value: "", want: false},19+ {name: "random enables", value: "random", want: true},
18- {name: "false", value: "false", want: false},
19 {name: "0", value: "0", want: false},20 {name: "0", value: "0", want: false},
21+ {name: "false", value: "false", want: false},
20 {name: "no", value: "no", want: false},22 {name: "no", value: "no", want: false},
21 {name: "off", value: "off", want: false},23 {name: "off", value: "off", want: false},
22- {name: "random", value: "random", want: false},24+ {name: "OFF", value: "OFF", want: false},
23 }25 }
24 26
25 for _, tt := range tests {27 for _, tt := range tests {
26 t.Run(tt.name, func(t *testing.T) {28 t.Run(tt.name, func(t *testing.T) {
27 t.Setenv(cefExternalBeginFrameEnvVar, tt.value)29 t.Setenv(cefExternalBeginFrameEnvVar, tt.value)
28 30
29- if got := envBoolEnabled(cefExternalBeginFrameEnvVar); got != tt.want {31+ if got := externalBeginFrameEnabled(); got != tt.want {
30- t.Fatalf("envBoolEnabled(%q) = %v, want %v", tt.value, got, tt.want)32+ t.Fatalf("externalBeginFrameEnabled(%q) = %v, want %v", tt.value, got, tt.want)
31 }33 }
32 })34 })
33 }35 }
M internal/infrastructure/cef/render_diagnostics.go +4 -0
@@ -102,6 +102,10 @@ func (wv *WebView) logRenderDiagnosticSnapshot(
102102 Int("pending_reschedules", diag.PendingReschedules).
103103 Int("pending_schedule_failures", diag.PendingScheduleFailures).
104104 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).
105109 Int32("surface_width", width).
106110 Int32("surface_height", height).
107111 Float32("surface_scale", scale).
@@ -102,6 +102,10 @@ func (wv *WebView) logRenderDiagnosticSnapshot(
102 Int("pending_reschedules", diag.PendingReschedules).102 Int("pending_reschedules", diag.PendingReschedules).
103 Int("pending_schedule_failures", diag.PendingScheduleFailures).103 Int("pending_schedule_failures", diag.PendingScheduleFailures).
104 Int("pending_idle_callbacks", diag.PendingIdleCallbacks).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).
105 Int32("surface_width", width).109 Int32("surface_width", width).
106 Int32("surface_height", height).110 Int32("surface_height", height).
107 Float32("surface_scale", scale).111 Float32("surface_scale", scale).
A internal/infrastructure/cef/render_experiment_test.go +113 -0
@@ -0,0 +1,113 @@
1+package cef
2+
3+import (
4+ "strconv"
5+ "testing"
6+
7+ purecef "github.com/bnema/purego-cef/cef"
8+)
9+
10+func TestWindowlessFrameRatePinnedFollowsTheOverride(t *testing.T) {
11+ cases := []struct {
12+ value string
13+ want bool
14+ }{
15+ {value: "", want: false},
16+ {value: "60", want: true},
17+ {value: " 144 ", want: true},
18+ {value: "0", want: false},
19+ {value: "-60", want: false},
20+ {value: "many", want: false},
21+ {value: strconv.FormatInt(int64(1)<<40, 10), want: false},
22+ }
23+ for _, testCase := range cases {
24+ t.Run("pin:"+testCase.value, func(t *testing.T) {
25+ t.Setenv(cefWindowlessFrameRateEnvVar, testCase.value)
26+ if got := windowlessFrameRatePinned(); got != testCase.want {
27+ t.Fatalf("windowlessFrameRatePinned(%q) = %v, want %v", testCase.value, got, testCase.want)
28+ }
29+ })
30+ }
31+}
32+
33+func TestConfigureNativePopupWindowEnablesExternalBeginFrameByDefault(t *testing.T) {
34+ // The default is only worth asserting through a real call site: an inverted
35+ // default shipped before because only the boolean helper was covered.
36+ cases := []struct {
37+ value string
38+ want int32
39+ }{
40+ {value: "", want: 1},
41+ {value: "1", want: 1},
42+ {value: "true", want: 1},
43+ {value: "on", want: 1},
44+ {value: "random", want: 1},
45+ {value: "0", want: 0},
46+ {value: "false", want: 0},
47+ {value: "off", want: 0},
48+ }
49+ for _, testCase := range cases {
50+ t.Run("call-site:"+testCase.value, func(t *testing.T) {
51+ t.Setenv(cefExternalBeginFrameEnvVar, testCase.value)
52+ windowInfo := purecef.NewWindowInfo()
53+ settings := purecef.NewBrowserSettings()
54+
55+ configureNativePopupWindow(&windowInfo, &settings, 60)
56+
57+ if windowInfo.ExternalBeginFrameEnabled != testCase.want {
58+ t.Fatalf("ExternalBeginFrameEnabled with %q = %d, want %d",
59+ testCase.value, windowInfo.ExternalBeginFrameEnabled, testCase.want)
60+ }
61+ })
62+ }
63+}
64+
65+func TestResolveWindowlessFrameRatePrefersAPinnedRate(t *testing.T) {
66+ t.Setenv(cefWindowlessFrameRateEnvVar, "")
67+
68+ rate, adaptive := resolveWindowlessFrameRate(0, true)
69+ if rate != defaultCEFWindowlessFrameRate || !adaptive {
70+ t.Fatalf("unpinned resolve = (%d, %v), want (%d, true)", rate, adaptive, defaultCEFWindowlessFrameRate)
71+ }
72+
73+ rate, adaptive = resolveWindowlessFrameRate(90, true)
74+ if rate != 90 || !adaptive {
75+ t.Fatalf("configured resolve = (%d, %v), want (90, true)", rate, adaptive)
76+ }
77+
78+ rate, adaptive = resolveWindowlessFrameRate(90, false)
79+ if rate != 90 || adaptive {
80+ t.Fatalf("non-adaptive resolve = (%d, %v), want (90, false)", rate, adaptive)
81+ }
82+
83+ t.Setenv(cefWindowlessFrameRateEnvVar, "144")
84+ rate, adaptive = resolveWindowlessFrameRate(0, true)
85+ if rate != 144 || adaptive {
86+ t.Fatalf("pinned resolve = (%d, %v), want (144, false)", rate, adaptive)
87+ }
88+
89+ t.Setenv(cefWindowlessFrameRateEnvVar, "not-a-rate")
90+ rate, adaptive = resolveWindowlessFrameRate(0, true)
91+ if rate != defaultCEFWindowlessFrameRate || !adaptive {
92+ t.Fatalf("unparsable pin resolve = (%d, %v), want the configured fallback", rate, adaptive)
93+ }
94+
95+ t.Setenv(cefWindowlessFrameRateEnvVar, "0")
96+ rate, adaptive = resolveWindowlessFrameRate(60, true)
97+ if rate != 60 || !adaptive {
98+ t.Fatalf("zero pin resolve = (%d, %v), want (60, true)", rate, adaptive)
99+ }
100+
101+ t.Setenv(cefWindowlessFrameRateEnvVar, "-60")
102+ rate, adaptive = resolveWindowlessFrameRate(60, true)
103+ if rate != 60 || !adaptive {
104+ t.Fatalf("negative pin resolve = (%d, %v), want (60, true)", rate, adaptive)
105+ }
106+
107+ overflow := strconv.FormatInt(int64(1)<<40, 10)
108+ t.Setenv(cefWindowlessFrameRateEnvVar, overflow)
109+ rate, adaptive = resolveWindowlessFrameRate(60, true)
110+ if rate != 60 || !adaptive {
111+ t.Fatalf("overflowing pin resolve = (%d, %v), want (60, true)", rate, adaptive)
112+ }
113+}
@@ -0,0 +1,113 @@
1+package cef
2+
3+import (
4+ "strconv"
5+ "testing"
6+
7+ purecef "github.com/bnema/purego-cef/cef"
8+)
9+
10+func TestWindowlessFrameRatePinnedFollowsTheOverride(t *testing.T) {
11+ cases := []struct {
12+ value string
13+ want bool
14+ }{
15+ {value: "", want: false},
16+ {value: "60", want: true},
17+ {value: " 144 ", want: true},
18+ {value: "0", want: false},
19+ {value: "-60", want: false},
20+ {value: "many", want: false},
21+ {value: strconv.FormatInt(int64(1)<<40, 10), want: false},
22+ }
23+ for _, testCase := range cases {
24+ t.Run("pin:"+testCase.value, func(t *testing.T) {
25+ t.Setenv(cefWindowlessFrameRateEnvVar, testCase.value)
26+ if got := windowlessFrameRatePinned(); got != testCase.want {
27+ t.Fatalf("windowlessFrameRatePinned(%q) = %v, want %v", testCase.value, got, testCase.want)
28+ }
29+ })
30+ }
31+}
32+
33+func TestConfigureNativePopupWindowEnablesExternalBeginFrameByDefault(t *testing.T) {
34+ // The default is only worth asserting through a real call site: an inverted
35+ // default shipped before because only the boolean helper was covered.
36+ cases := []struct {
37+ value string
38+ want int32
39+ }{
40+ {value: "", want: 1},
41+ {value: "1", want: 1},
42+ {value: "true", want: 1},
43+ {value: "on", want: 1},
44+ {value: "random", want: 1},
45+ {value: "0", want: 0},
46+ {value: "false", want: 0},
47+ {value: "off", want: 0},
48+ }
49+ for _, testCase := range cases {
50+ t.Run("call-site:"+testCase.value, func(t *testing.T) {
51+ t.Setenv(cefExternalBeginFrameEnvVar, testCase.value)
52+ windowInfo := purecef.NewWindowInfo()
53+ settings := purecef.NewBrowserSettings()
54+
55+ configureNativePopupWindow(&windowInfo, &settings, 60)
56+
57+ if windowInfo.ExternalBeginFrameEnabled != testCase.want {
58+ t.Fatalf("ExternalBeginFrameEnabled with %q = %d, want %d",
59+ testCase.value, windowInfo.ExternalBeginFrameEnabled, testCase.want)
60+ }
61+ })
62+ }
63+}
64+
65+func TestResolveWindowlessFrameRatePrefersAPinnedRate(t *testing.T) {
66+ t.Setenv(cefWindowlessFrameRateEnvVar, "")
67+
68+ rate, adaptive := resolveWindowlessFrameRate(0, true)
69+ if rate != defaultCEFWindowlessFrameRate || !adaptive {
70+ t.Fatalf("unpinned resolve = (%d, %v), want (%d, true)", rate, adaptive, defaultCEFWindowlessFrameRate)
71+ }
72+
73+ rate, adaptive = resolveWindowlessFrameRate(90, true)
74+ if rate != 90 || !adaptive {
75+ t.Fatalf("configured resolve = (%d, %v), want (90, true)", rate, adaptive)
76+ }
77+
78+ rate, adaptive = resolveWindowlessFrameRate(90, false)
79+ if rate != 90 || adaptive {
80+ t.Fatalf("non-adaptive resolve = (%d, %v), want (90, false)", rate, adaptive)
81+ }
82+
83+ t.Setenv(cefWindowlessFrameRateEnvVar, "144")
84+ rate, adaptive = resolveWindowlessFrameRate(0, true)
85+ if rate != 144 || adaptive {
86+ t.Fatalf("pinned resolve = (%d, %v), want (144, false)", rate, adaptive)
87+ }
88+
89+ t.Setenv(cefWindowlessFrameRateEnvVar, "not-a-rate")
90+ rate, adaptive = resolveWindowlessFrameRate(0, true)
91+ if rate != defaultCEFWindowlessFrameRate || !adaptive {
92+ t.Fatalf("unparsable pin resolve = (%d, %v), want the configured fallback", rate, adaptive)
93+ }
94+
95+ t.Setenv(cefWindowlessFrameRateEnvVar, "0")
96+ rate, adaptive = resolveWindowlessFrameRate(60, true)
97+ if rate != 60 || !adaptive {
98+ t.Fatalf("zero pin resolve = (%d, %v), want (60, true)", rate, adaptive)
99+ }
100+
101+ t.Setenv(cefWindowlessFrameRateEnvVar, "-60")
102+ rate, adaptive = resolveWindowlessFrameRate(60, true)
103+ if rate != 60 || !adaptive {
104+ t.Fatalf("negative pin resolve = (%d, %v), want (60, true)", rate, adaptive)
105+ }
106+
107+ overflow := strconv.FormatInt(int64(1)<<40, 10)
108+ t.Setenv(cefWindowlessFrameRateEnvVar, overflow)
109+ rate, adaptive = resolveWindowlessFrameRate(60, true)
110+ if rate != 60 || !adaptive {
111+ t.Fatalf("overflowing pin resolve = (%d, %v), want (60, true)", rate, adaptive)
112+ }
113+}

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