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