1//go:build webkit_cgo
2
3package webkit
4
5import (
6 "fmt"
7 "log"
8 "os"
9 "strconv"
10 "strings"
11 "time"
12)
13
14// ProcessMemoryInfo contains memory information for a WebKit process
15type ProcessMemoryInfo struct {
16 PID int `json:"pid"`
17 VmRSS int64 `json:"vm_rss"` // Resident Set Size in KB
18 VmSize int64 `json:"vm_size"` // Virtual Memory Size in KB
19 VmPeak int64 `json:"vm_peak"` // Peak Virtual Memory in KB
20 ProcessName string `json:"process_name"`
21}
22
23// WebViewMemoryManager handles memory monitoring and lifecycle management for WebViews
24type WebViewMemoryManager struct {
25 views map[uintptr]*WebView
26 enableMonitoring bool
27 recycleThreshold int
28 monitoringInterval time.Duration
29 stopMonitoring chan struct{}
30 lastLoggedMemory int64
31}
32
33// NewWebViewMemoryManager creates a new memory manager
34func NewWebViewMemoryManager(enableMonitoring bool, recycleThreshold int, monitoringInterval time.Duration) *WebViewMemoryManager {
35 return &WebViewMemoryManager{
36 views: make(map[uintptr]*WebView),
37 enableMonitoring: enableMonitoring,
38 recycleThreshold: recycleThreshold,
39 monitoringInterval: monitoringInterval,
40 stopMonitoring: make(chan struct{}),
41 }
42}
43
44// RegisterWebView registers a WebView for monitoring
45func (mgr *WebViewMemoryManager) RegisterWebView(view *WebView) {
46 if mgr == nil || view == nil {
47 return
48 }
49 mgr.views[view.id] = view
50
51 if mgr.enableMonitoring && len(mgr.views) == 1 {
52 // Start monitoring when first view is registered
53 go mgr.startMemoryMonitoring()
54 }
55}
56
57// UnregisterWebView removes a WebView from monitoring
58func (mgr *WebViewMemoryManager) UnregisterWebView(viewID uintptr) {
59 if mgr == nil {
60 return
61 }
62 delete(mgr.views, viewID)
63
64 if len(mgr.views) == 0 {
65 // Stop monitoring when no views left
66 close(mgr.stopMonitoring)
67 mgr.stopMonitoring = make(chan struct{})
68 }
69}
70
71// startMemoryMonitoring starts the background memory monitoring routine
72func (mgr *WebViewMemoryManager) startMemoryMonitoring() {
73 if mgr == nil || mgr.monitoringInterval <= 0 {
74 return
75 }
76
77 ticker := time.NewTicker(mgr.monitoringInterval)
78 defer ticker.Stop()
79
80 for {
81 select {
82 case <-ticker.C:
83 mgr.checkMemoryUsage()
84 case <-mgr.stopMonitoring:
85 return
86 }
87 }
88}
89
90// checkMemoryUsage monitors memory usage of WebKit processes
91func (mgr *WebViewMemoryManager) checkMemoryUsage() {
92 if mgr == nil {
93 return
94 }
95
96 processes, err := mgr.getWebKitProcesses()
97 if err != nil {
98 if mgr.enableMonitoring {
99 log.Printf("[webkit] Error monitoring processes: %v", err)
100 }
101 return
102 }
103
104 totalMemory := int64(0)
105 for _, proc := range processes {
106 totalMemory += proc.VmRSS
107 }
108
109 // Only log if memory usage changed by more than 10MB (10240 KB)
110 const memoryChangeThreshold = 10240 // 10MB in KB
111 if mgr.enableMonitoring {
112 memoryDiff := totalMemory - mgr.lastLoggedMemory
113 if memoryDiff < 0 {
114 memoryDiff = -memoryDiff
115 }
116
117 if mgr.lastLoggedMemory == 0 || memoryDiff >= memoryChangeThreshold {
118 log.Printf("[webkit] Memory usage: %d processes, total %.1f MB RSS",
119 len(processes), float64(totalMemory)/1024.0)
120 mgr.lastLoggedMemory = totalMemory
121 }
122 }
123
124 // Check for recycling needs
125 for _, view := range mgr.views {
126 if view.memStats != nil && mgr.recycleThreshold > 0 {
127 if view.memStats.pageLoadCount >= mgr.recycleThreshold {
128 if mgr.enableMonitoring {
129 log.Printf("[webkit] WebView %d needs recycling (%d pages loaded)",
130 view.id, view.memStats.pageLoadCount)
131 }
132 }
133 }
134 }
135}
136
137// getWebKitProcesses returns memory info for all WebKit-related processes
138func (mgr *WebViewMemoryManager) getWebKitProcesses() ([]ProcessMemoryInfo, error) {
139 var processes []ProcessMemoryInfo
140
141 // Find all WebKit processes by scanning /proc
142 procDir, err := os.ReadDir("/proc")
143 if err != nil {
144 return nil, fmt.Errorf("failed to read /proc: %w", err)
145 }
146
147 for _, entry := range procDir {
148 if !entry.IsDir() {
149 continue
150 }
151
152 pid, err := strconv.Atoi(entry.Name())
153 if err != nil {
154 continue // Not a PID directory
155 }
156
157 // Check if this is a WebKit process
158 cmdlinePath := fmt.Sprintf("/proc/%d/cmdline", pid)
159 cmdlineBytes, err := os.ReadFile(cmdlinePath)
160 if err != nil {
161 continue // Process might have exited
162 }
163
164 cmdline := string(cmdlineBytes)
165 if !strings.Contains(cmdline, "WebKit") && !strings.Contains(cmdline, "dumber") {
166 continue // Not a WebKit or dumber process
167 }
168
169 // Get memory information
170 statusPath := fmt.Sprintf("/proc/%d/status", pid)
171 statusData, err := os.ReadFile(statusPath)
172 if err != nil {
173 continue
174 }
175
176 memInfo := mgr.parseMemoryStatus(string(statusData), pid)
177 if memInfo != nil {
178 // Extract process name from cmdline
179 parts := strings.Split(cmdline, "\x00")
180 if len(parts) > 0 {
181 memInfo.ProcessName = parts[0]
182 }
183 processes = append(processes, *memInfo)
184 }
185 }
186
187 return processes, nil
188}
189
190// parseMemoryStatus parses /proc/[pid]/status for memory information
191func (mgr *WebViewMemoryManager) parseMemoryStatus(status string, pid int) *ProcessMemoryInfo {
192 lines := strings.Split(status, "\n")
193 info := &ProcessMemoryInfo{PID: pid}
194
195 for _, line := range lines {
196 if strings.HasPrefix(line, "VmRSS:") {
197 if val := mgr.extractMemoryValue(line); val > 0 {
198 info.VmRSS = val
199 }
200 } else if strings.HasPrefix(line, "VmSize:") {
201 if val := mgr.extractMemoryValue(line); val > 0 {
202 info.VmSize = val
203 }
204 } else if strings.HasPrefix(line, "VmPeak:") {
205 if val := mgr.extractMemoryValue(line); val > 0 {
206 info.VmPeak = val
207 }
208 }
209 }
210
211 // Only return if we found at least VmRSS
212 if info.VmRSS > 0 {
213 return info
214 }
215
216 return nil
217}
218
219// extractMemoryValue extracts memory value in KB from status lines like "VmRSS: 12345 kB"
220func (mgr *WebViewMemoryManager) extractMemoryValue(line string) int64 {
221 parts := strings.Fields(line)
222 if len(parts) >= 2 {
223 val, err := strconv.ParseInt(parts[1], 10, 64)
224 if err == nil {
225 return val
226 }
227 }
228 return 0
229}
230
231// GetTotalMemoryUsage returns total memory usage of all WebKit processes in MB
232func (mgr *WebViewMemoryManager) GetTotalMemoryUsage() (float64, error) {
233 if mgr == nil {
234 return 0, fmt.Errorf("memory manager is nil")
235 }
236
237 processes, err := mgr.getWebKitProcesses()
238 if err != nil {
239 return 0, err
240 }
241
242 totalKB := int64(0)
243 for _, proc := range processes {
244 totalKB += proc.VmRSS
245 }
246
247 return float64(totalKB) / 1024.0, nil
248}
249
250// GetDetailedMemoryInfo returns detailed memory information for all WebKit processes
251func (mgr *WebViewMemoryManager) GetDetailedMemoryInfo() ([]ProcessMemoryInfo, error) {
252 if mgr == nil {
253 return nil, fmt.Errorf("memory manager is nil")
254 }
255
256 return mgr.getWebKitProcesses()
257}
258
259// ShouldRecycleWebView checks if a WebView should be recycled based on memory pressure
260func (mgr *WebViewMemoryManager) ShouldRecycleWebView(view *WebView) bool {
261 if mgr == nil || view == nil || view.memStats == nil {
262 return false
263 }
264
265 if mgr.recycleThreshold > 0 && view.memStats.pageLoadCount >= mgr.recycleThreshold {
266 return true
267 }
268
269 return false
270}
271
272// Global memory manager instance
273var globalMemoryManager *WebViewMemoryManager
274
275// InitializeGlobalMemoryManager initializes the global memory manager
276func InitializeGlobalMemoryManager(enableMonitoring bool, recycleThreshold int, monitoringInterval time.Duration) {
277 if monitoringInterval == 0 {
278 monitoringInterval = 30 * time.Second // Default 30 seconds
279 }
280 globalMemoryManager = NewWebViewMemoryManager(enableMonitoring, recycleThreshold, monitoringInterval)
281}
282
283// GetGlobalMemoryManager returns the global memory manager
284func GetGlobalMemoryManager() *WebViewMemoryManager {
285 return globalMemoryManager
286}