Skip to content

Commit 4abd2fd

Browse files
authored
feat(go): track chromium memory usage (#820)
1 parent 203cca5 commit 4abd2fd

File tree

4 files changed

+462
-45
lines changed

4 files changed

+462
-45
lines changed

pkg/metrics/registry.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ func NewRegistry() *prometheus.Registry {
2929
service.MetricBrowserRenderCSVDuration,
3030
service.MetricBrowserRenderDuration,
3131
service.MetricBrowserInstancesActive,
32+
service.MetricProcessMaxMemory,
33+
service.MetricProcessPeakMemoryAverage,
34+
service.MetricProcessPeakMemoryInstant,
3235
)
3336
return registry
3437
}

pkg/service/browser.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ var (
7272
var ErrInvalidBrowserOption = errors.New("invalid browser option")
7373

7474
type BrowserService struct {
75-
cfg config.BrowserConfig
75+
cfg config.BrowserConfig
76+
processes *ProcessStatService
7677

7778
// log is the base logger for the service.
7879
log *slog.Logger
@@ -83,8 +84,9 @@ type BrowserService struct {
8384
// The options are not validated on creation, rather on request.
8485
func NewBrowserService(cfg config.BrowserConfig) *BrowserService {
8586
return &BrowserService{
86-
cfg: cfg,
87-
log: slog.With("service", "browser"),
87+
cfg: cfg,
88+
processes: NewProcessStatService(),
89+
log: slog.With("service", "browser"),
8890
}
8991
}
9092

@@ -239,6 +241,7 @@ func (s *BrowserService) Render(ctx context.Context, url string, printer Printer
239241

240242
fileChan := make(chan []byte, 1) // buffered: we don't want the browser to stick around while we try to export this value.
241243
actions := []chromedp.Action{
244+
observingAction("trackProcess", trackProcess(browserCtx, s.processes)),
242245
observingAction("network.Enable", network.Enable()), // required by waitForReady
243246
observingAction("fetch.Enable", fetch.Enable()), // required by handleNetworkEvents
244247
observingAction("SetPageScaleFactor", emulation.SetPageScaleFactor(cfg.PageScaleFactor)),
@@ -318,6 +321,7 @@ func (s *BrowserService) RenderCSV(ctx context.Context, url, renderKey, domain,
318321
s.handleNetworkEvents(browserCtx)
319322

320323
actions := []chromedp.Action{
324+
observingAction("trackProcess", trackProcess(browserCtx, s.processes)),
321325
observingAction("network.Enable", network.Enable()),
322326
observingAction("setHeaders", setHeaders(browserCtx, headers)),
323327
observingAction("setCookies", setCookies([]*network.SetCookieParams{
@@ -1090,3 +1094,17 @@ func (at *atomicTime) Load() time.Time {
10901094
func (at *atomicTime) Store(t time.Time) {
10911095
at.Value.Store(t)
10921096
}
1097+
1098+
func trackProcess(browserCtx context.Context, processes *ProcessStatService) chromedp.Action {
1099+
return chromedp.ActionFunc(func(ctx context.Context) error {
1100+
cdpCtx := chromedp.FromContext(ctx)
1101+
proc := cdpCtx.Browser.Process()
1102+
if proc == nil {
1103+
// no process to track.
1104+
return nil
1105+
}
1106+
1107+
processes.TrackProcess(browserCtx, int32(proc.Pid))
1108+
return nil
1109+
})
1110+
}

pkg/service/process.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package service
2+
3+
import (
4+
"context"
5+
"errors"
6+
"log/slog"
7+
"sync"
8+
"time"
9+
10+
"github.com/prometheus/client_golang/prometheus"
11+
"github.com/shirou/gopsutil/v4/process"
12+
)
13+
14+
var (
15+
MetricProcessMaxMemory = prometheus.NewGauge(prometheus.GaugeOpts{
16+
Name: "process_max_memory",
17+
ConstLabels: prometheus.Labels{
18+
"unit": "bytes",
19+
},
20+
Help: "Maximum memory used by the Chromium process in bytes. This is the max of all tracked processes.",
21+
})
22+
MetricProcessPeakMemoryAverage = prometheus.NewGauge(prometheus.GaugeOpts{
23+
Name: "process_peak_memory_avg",
24+
ConstLabels: prometheus.Labels{
25+
"unit": "bytes",
26+
},
27+
Help: "Peak memory used by the Chromium process in bytes. This is a slow-moving average of all tracked processes.",
28+
})
29+
MetricProcessPeakMemoryInstant = prometheus.NewHistogram(prometheus.HistogramOpts{
30+
Name: "process_peak_memory",
31+
ConstLabels: prometheus.Labels{
32+
"unit": "bytes",
33+
},
34+
Help: "Peak memory used by any Chromium process in bytes. This is marked for every process.",
35+
Buckets: []float64{
36+
kibibyte,
37+
mibibyte,
38+
16 * mibibyte,
39+
32 * mibibyte,
40+
64 * mibibyte,
41+
128 * mibibyte,
42+
256 * mibibyte,
43+
368 * mibibyte,
44+
512 * mibibyte,
45+
768 * mibibyte,
46+
gibibyte,
47+
gibibyte + 512*mibibyte,
48+
2 * gibibyte,
49+
4 * gibibyte,
50+
6 * gibibyte,
51+
8 * gibibyte,
52+
},
53+
})
54+
)
55+
56+
const (
57+
kibibyte = 1024
58+
mibibyte = 1024 * kibibyte
59+
gibibyte = 1024 * mibibyte
60+
)
61+
62+
type ProcessStatService struct {
63+
mu sync.Mutex
64+
// MaxMemory is the number of bytes a Chromium process uses at absolute max.
65+
// This is the max of all processes.
66+
MaxMemory int64
67+
// PeakMemory is the number of bytes a Chromium process uses at peak.
68+
// It is a slow-moving average of all values tracked.
69+
PeakMemory int64
70+
71+
log *slog.Logger
72+
}
73+
74+
func NewProcessStatService() *ProcessStatService {
75+
return &ProcessStatService{
76+
log: slog.With("service", "process_stat"),
77+
}
78+
}
79+
80+
// TrackProcess starts a new goroutine to keep track of the process.
81+
func (p *ProcessStatService) TrackProcess(ctx context.Context, pid int32) {
82+
go func() {
83+
logger := p.log.With("pid", pid)
84+
85+
proc, err := process.NewProcessWithContext(ctx, pid)
86+
if errors.Is(err, context.Canceled) {
87+
return
88+
} else if err != nil {
89+
logger.Warn("failed to find process to track", "err", err)
90+
return
91+
}
92+
93+
peakMemory := 0
94+
defer func() {
95+
// We only do the lock once per process. This reduces contention significantly.
96+
p.mu.Lock()
97+
defer p.mu.Unlock()
98+
99+
if p.PeakMemory == 0 {
100+
p.PeakMemory = int64(peakMemory)
101+
} else {
102+
const decay = 5
103+
p.PeakMemory = (p.PeakMemory*(decay-1) + int64(peakMemory)) / decay
104+
}
105+
p.MaxMemory = max(p.MaxMemory, int64(peakMemory))
106+
107+
MetricProcessMaxMemory.Set(float64(p.MaxMemory))
108+
MetricProcessPeakMemoryAverage.Set(float64(p.PeakMemory))
109+
MetricProcessPeakMemoryInstant.Observe(float64(peakMemory))
110+
}()
111+
112+
for {
113+
select {
114+
case <-ctx.Done():
115+
return
116+
case <-time.After(500 * time.Millisecond):
117+
}
118+
119+
mem, err := proc.MemoryInfoWithContext(ctx)
120+
if errors.Is(err, context.Canceled) || errors.Is(err, process.ErrorProcessNotRunning) {
121+
return
122+
} else if err != nil {
123+
logger.Warn("failed to find memory info about process", "err", err)
124+
return
125+
}
126+
127+
peakMemory = max(peakMemory, int(mem.RSS))
128+
}
129+
}()
130+
}

0 commit comments

Comments
 (0)