Files
gotenberg/pkg/modules/chromium/network_aggregate.go

122 lines
2.9 KiB
Go

package chromium
import (
"net/url"
"sync"
"github.com/chromedp/cdproto/network"
)
// maxTrackedOrigins bounds the distinct origins kept per conversion so a
// pathological page cannot grow the set without limit.
const maxTrackedOrigins = 64
// networkAggregate accumulates per-conversion network activity from Chromium
// DevTools events. It is safe for concurrent use by the chromedp event listener
// goroutine and the conversion goroutine that reads the snapshot afterwards.
type networkAggregate struct {
mu sync.Mutex
requestCount int64
bytesTotal int64
failedCount int64
origins map[string]struct{}
requestURLByID map[network.RequestID]string
heaviestURL string
heaviestBytes int64
}
// networkStats is an immutable snapshot of a [networkAggregate].
type networkStats struct {
requestCount int64
bytesTotal int64
failedCount int64
uniqueOrigins int64
heaviestURL string
heaviestBytes int64
}
func newNetworkAggregate() *networkAggregate {
return &networkAggregate{
origins: make(map[string]struct{}),
requestURLByID: make(map[network.RequestID]string),
}
}
// onResponseReceived records the response origin and remembers the URL for the
// request id, so a later loading-finished event can attribute its bytes.
func (a *networkAggregate) onResponseReceived(ev *network.EventResponseReceived) {
if ev == nil || ev.Response == nil {
return
}
a.mu.Lock()
defer a.mu.Unlock()
if origin := originOf(ev.Response.URL); origin != "" {
if _, ok := a.origins[origin]; !ok && len(a.origins) < maxTrackedOrigins {
a.origins[origin] = struct{}{}
}
}
a.requestURLByID[ev.RequestID] = ev.Response.URL
}
// onLoadingFinished records a successfully completed request and its size,
// tracking the single heaviest resource.
func (a *networkAggregate) onLoadingFinished(ev *network.EventLoadingFinished) {
if ev == nil {
return
}
a.mu.Lock()
defer a.mu.Unlock()
a.requestCount++
size := int64(ev.EncodedDataLength)
a.bytesTotal += size
if size > a.heaviestBytes {
a.heaviestBytes = size
a.heaviestURL = a.requestURLByID[ev.RequestID]
}
}
// onLoadingFailed records a request that failed to complete.
func (a *networkAggregate) onLoadingFailed(ev *network.EventLoadingFailed) {
if ev == nil {
return
}
a.mu.Lock()
defer a.mu.Unlock()
a.requestCount++
a.failedCount++
}
func (a *networkAggregate) snapshot() networkStats {
a.mu.Lock()
defer a.mu.Unlock()
return networkStats{
requestCount: a.requestCount,
bytesTotal: a.bytesTotal,
failedCount: a.failedCount,
uniqueOrigins: int64(len(a.origins)),
heaviestURL: a.heaviestURL,
heaviestBytes: a.heaviestBytes,
}
}
// originOf returns the scheme://host of rawURL, or an empty string when it has
// no host (for example data: or file: URLs).
func originOf(rawURL string) string {
parsed, err := url.Parse(rawURL)
if err != nil || parsed.Host == "" {
return ""
}
return parsed.Scheme + "://" + parsed.Host
}