1package cef
2
3import (
4 "sync"
5 "time"
6
7 "github.com/rs/zerolog"
8)
9
10// startupTrace records the accelerated CEF/DMABUF/GTK path from process entry
11// to the first GTK presentation. It deliberately rejects unknown, duplicate,
12// and out-of-order milestones so collected cold-start measurements remain
13// truthful.
14type startupTrace struct {
15 mu sync.Mutex
16 now func() time.Time
17 t0 time.Time
18 milestones []startupMilestone
19 logger *zerolog.Logger
20 buffered []startupMilestone
21 backend string
22 incompleteReason string
23 summaryEmitted bool
24}
25
26type startupMilestone struct {
27 Name string
28 Elapsed time.Duration
29 Delta time.Duration
30}
31
32var startupMilestoneOrder = []string{
33 "process_entry",
34 "config_complete",
35 "cef_library_load_begin",
36 "cef_initialized",
37 "browser_create_requested",
38 "first_accelerated_paint_received",
39 "first_dmabuf_texture_swap",
40 "first_gtk_presentation",
41}
42
43var processStartupTrace struct {
44 sync.Mutex
45 trace *startupTrace
46}
47
48func newStartupTrace(now func() time.Time, processEntry time.Time) *startupTrace {
49 return &startupTrace{
50 now: now,
51 t0: processEntry,
52 milestones: make([]startupMilestone, 0, len(startupMilestoneOrder)),
53 buffered: make([]startupMilestone, 0, len(startupMilestoneOrder)),
54 }
55}
56
57// ActivateStartupTrace starts the CEF-only first-presentation trace after GUI
58// engine selection. The supplied timestamps are captured by the application at
59// process entry and configuration completion, before CEF initialization.
60func ActivateStartupTrace(processEntry, configComplete time.Time, logger *zerolog.Logger) {
61 if processEntry.IsZero() || configComplete.IsZero() {
62 return
63 }
64
65 processStartupTrace.Lock()
66 defer processStartupTrace.Unlock()
67 if processStartupTrace.trace != nil {
68 return
69 }
70
71 trace := newStartupTrace(time.Now, processEntry)
72 if !trace.markAt("process_entry", processEntry) || !trace.markAt("config_complete", configComplete) {
73 return
74 }
75 trace.SetLogger(logger)
76 processStartupTrace.trace = trace
77}
78
79func activeStartupTrace() *startupTrace {
80 processStartupTrace.Lock()
81 defer processStartupTrace.Unlock()
82 return processStartupTrace.trace
83}
84
85func (st *startupTrace) SetLogger(logger *zerolog.Logger) {
86 if st == nil || logger == nil {
87 return
88 }
89 st.mu.Lock()
90 defer st.mu.Unlock()
91 st.logger = logger
92 for _, milestone := range st.buffered {
93 st.emitMilestone(milestone)
94 }
95 st.buffered = nil
96 st.emitSummaryLocked()
97}
98
99func (st *startupTrace) SetBackend(backend string) {
100 if st == nil {
101 return
102 }
103 st.mu.Lock()
104 defer st.mu.Unlock()
105 st.backend = backend
106}
107
108func (st *startupTrace) SetIncompleteReason(reason string) {
109 if st == nil {
110 return
111 }
112 st.mu.Lock()
113 defer st.mu.Unlock()
114 if st.incompleteReason == "" {
115 st.incompleteReason = reason
116 }
117}
118
119// Mark accepts exactly the next non-presentation startup milestone.
120func (st *startupTrace) Mark(name string) bool {
121 if name == "first_gtk_presentation" {
122 return false
123 }
124 return st.markAt(name, st.currentTime())
125}
126
127// MarkGTKAfterPaint records the final milestone from the CEF-to-GTK bridge's
128// first frame-clock after-paint callback.
129func (st *startupTrace) MarkGTKAfterPaint() bool {
130 return st.markAt("first_gtk_presentation", st.currentTime())
131}
132
133func (st *startupTrace) currentTime() time.Time {
134 if st == nil || st.now == nil {
135 return time.Time{}
136 }
137 return st.now()
138}
139
140func (st *startupTrace) markAt(name string, at time.Time) bool {
141 if st == nil || st.now == nil || at.IsZero() {
142 return false
143 }
144 st.mu.Lock()
145 defer st.mu.Unlock()
146 if at.Before(st.t0) || len(st.milestones) >= len(startupMilestoneOrder) || name != startupMilestoneOrder[len(st.milestones)] {
147 return false
148 }
149
150 milestone := startupMilestone{Name: name, Elapsed: at.Sub(st.t0)}
151 previousMilliseconds := int64(0)
152 if len(st.milestones) > 0 {
153 previousMilliseconds = st.milestones[len(st.milestones)-1].Elapsed.Milliseconds()
154 }
155 milestone.Delta = time.Duration(milestone.Elapsed.Milliseconds()-previousMilliseconds) * time.Millisecond
156 st.milestones = append(st.milestones, milestone)
157 if st.logger == nil {
158 st.buffered = append(st.buffered, milestone)
159 } else {
160 st.emitMilestone(milestone)
161 }
162 st.emitSummaryLocked()
163 return true
164}
165
166func (st *startupTrace) emitMilestone(m startupMilestone) {
167 if st.logger == nil {
168 return
169 }
170 st.logger.Debug().
171 Str("milestone", m.Name).
172 Int64("t_ms", m.Elapsed.Milliseconds()).
173 Int64("delta_ms", m.Delta.Milliseconds()).
174 Msg("startup_trace: milestone")
175}
176
177func (st *startupTrace) emitSummaryLocked() {
178 if st.summaryEmitted || st.logger == nil || len(st.milestones) != len(startupMilestoneOrder) {
179 return
180 }
181 st.summaryEmitted = true
182 st.logger.Info().
183 Str("backend", st.backend).
184 Str("incomplete_reason", st.incompleteReason).
185 Int64("total_ms", st.milestones[len(st.milestones)-1].Elapsed.Milliseconds()).
186 Interface("milestones", st.milestones).
187 Msg("startup_trace: first presentation")
188}
189
190func (st *startupTrace) Enabled() bool { return st != nil && st.now != nil }
191
192func (st *startupTrace) TotalElapsed() time.Duration {
193 if st == nil || st.now == nil {
194 return 0
195 }
196 st.mu.Lock()
197 defer st.mu.Unlock()
198 return st.now().Sub(st.t0)
199}