v0.11.0 dc50343

memory.go RawHistoryFolder

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}

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