1package cef
2
3import (
4 "sync/atomic"
5 "time"
6
7 purecef "github.com/bnema/purego-cef/cef"
8
9 "github.com/bnema/dumber/internal/logging"
10)
11
12const (
13 cefHeartbeatInterval = 1 * time.Second
14 cefHeartbeatLogEvery = 10 * time.Second
15 cefHeartbeatStopTimeout = 2 * time.Second
16)
17
18type cefThreadHeartbeatState struct {
19 seq atomic.Uint64
20 lastPostUnixNS atomic.Int64
21 lastAckUnixNS atomic.Int64
22 lastLatencyNS atomic.Int64
23 lastPostResult atomic.Int32
24 lastLogUnixNS atomic.Int64
25 inFlight atomic.Bool
26}
27
28func (s *cefThreadHeartbeatState) snapshot(now time.Time) cefHeartbeatSnapshot {
29 if s == nil {
30 return cefHeartbeatSnapshot{}
31 }
32 lastPost := unixNSTime(s.lastPostUnixNS.Load())
33 lastAck := unixNSTime(s.lastAckUnixNS.Load())
34 snap := cefHeartbeatSnapshot{
35 Seq: s.seq.Load(),
36 PostResult: s.lastPostResult.Load(),
37 Latency: time.Duration(s.lastLatencyNS.Load()),
38 LastPostTime: lastPost,
39 LastAckTime: lastAck,
40 InFlight: s.inFlight.Load(),
41 }
42 if !lastPost.IsZero() {
43 snap.LastPostAge = now.Sub(lastPost)
44 }
45 if !lastAck.IsZero() {
46 snap.LastAckAge = now.Sub(lastAck)
47 }
48 return snap
49}
50
51type cefHeartbeatSnapshot struct {
52 Seq uint64
53 PostResult int32
54 Latency time.Duration
55 LastPostTime time.Time
56 LastAckTime time.Time
57 LastPostAge time.Duration
58 LastAckAge time.Duration
59 InFlight bool
60}
61
62func (e *Engine) startCEFHeartbeat() {
63 if e == nil || e.ctx == nil || e.cefHeartbeatStop != nil {
64 return
65 }
66 stop := make(chan struct{})
67 done := make(chan struct{})
68 e.cefHeartbeatStop = stop
69 e.cefHeartbeatDone = done
70 go e.cefHeartbeatLoop(stop, done)
71}
72
73func (e *Engine) stopCEFHeartbeat() {
74 if e == nil || e.cefHeartbeatStop == nil {
75 return
76 }
77 stop := e.cefHeartbeatStop
78 done := e.cefHeartbeatDone
79 e.cefHeartbeatStop = nil
80 e.cefHeartbeatDone = nil
81 close(stop)
82 if done == nil {
83 return
84 }
85 select {
86 case <-done:
87 case <-time.After(cefHeartbeatStopTimeout):
88 if e.ctx != nil {
89 logging.FromContext(e.ctx).Warn().Msg("cef: heartbeat stop timed out")
90 }
91 }
92}
93
94func (e *Engine) cefHeartbeatLoop(stop <-chan struct{}, done chan<- struct{}) {
95 defer close(done)
96 ticker := time.NewTicker(cefHeartbeatInterval)
97 defer ticker.Stop()
98 for {
99 select {
100 case <-stop:
101 return
102 case now := <-ticker.C:
103 e.postCEFThreadHeartbeat("ui", purecef.ThreadIDTidUi, &e.cefUIHeartbeat, now)
104 e.postCEFThreadHeartbeat("io", purecef.ThreadIDTidIo, &e.cefIOHeartbeat, now)
105 }
106 }
107}
108
109func (e *Engine) postCEFThreadHeartbeat(name string, threadID purecef.ThreadID, state *cefThreadHeartbeatState, now time.Time) {
110 if e == nil || state == nil {
111 return
112 }
113 seq := state.seq.Add(1)
114 postedAt := now
115 state.lastPostUnixNS.Store(postedAt.UnixNano())
116 state.inFlight.Store(true)
117 task := cefNewTask(cefTaskFunc(func() {
118 ackedAt := time.Now()
119 state.lastAckUnixNS.Store(ackedAt.UnixNano())
120 state.lastLatencyNS.Store(ackedAt.Sub(postedAt).Nanoseconds())
121 state.inFlight.Store(false)
122 }))
123 if task == nil {
124 state.lastPostResult.Store(0)
125 state.inFlight.Store(false)
126 return
127 }
128 result := cefPostTask(threadID, task)
129 state.lastPostResult.Store(result)
130 if result != 1 {
131 state.inFlight.Store(false)
132 }
133 lastLog := unixNSTime(state.lastLogUnixNS.Load())
134 if result != 1 || lastLog.IsZero() || now.Sub(lastLog) >= cefHeartbeatLogEvery {
135 state.lastLogUnixNS.Store(now.UnixNano())
136 snap := state.snapshot(now)
137 logging.FromContext(e.ctx).Debug().
138 Str("thread", name).
139 Uint64("seq", seq).
140 Int32("post_result", result).
141 Int64("last_ack_age_ms", durationMillis(snap.LastAckAge)).
142 Int64("latency_ms", durationMillis(snap.Latency)).
143 Bool("in_flight", snap.InFlight).
144 Msg("cef: thread heartbeat")
145 }
146}
147
148func (e *Engine) cefHeartbeatSnapshots(now time.Time) (cefHeartbeatSnapshot, cefHeartbeatSnapshot) {
149 if e == nil {
150 return cefHeartbeatSnapshot{}, cefHeartbeatSnapshot{}
151 }
152 return e.cefUIHeartbeat.snapshot(now), e.cefIOHeartbeat.snapshot(now)
153}
154
155func (wv *WebView) cefHeartbeatSnapshots(now time.Time) (cefHeartbeatSnapshot, cefHeartbeatSnapshot) {
156 if wv == nil || wv.engine == nil {
157 return cefHeartbeatSnapshot{}, cefHeartbeatSnapshot{}
158 }
159 return wv.engine.cefHeartbeatSnapshots(now)
160}
161
162func unixNSTime(ns int64) time.Time {
163 if ns <= 0 {
164 return time.Time{}
165 }
166 return time.Unix(0, ns)
167}
168
169func durationMillis(d time.Duration) int64 {
170 if d <= 0 {
171 return 0
172 }
173 return d.Milliseconds()
174}