v0.4.0 6203a22

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}
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}

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