From abae66ee1a0efe4cb7191fad06c995903d588549 Mon Sep 17 00:00:00 2001 From: nielash Date: Fri, 7 Aug 2026 09:15:13 -0400 Subject: [PATCH] accounting: fix memory leak from stats groups on long-running rcd Before this change, `NewStats` stored the context it was created from on the `StatsInfo`. Stats groups are never freed -- they are only evicted once there are `--max-stats-groups` of them -- so each one kept its context, and everything reachable from it, alive for the life of the process. As the rc creates a group per call, that included the call's filters and their compiled regexps. The context was only ever used to get `ci.StatsFileNameLength` from the config. `StatsInfo` already stores that same `*fs.ConfigInfo`, read from the same context in `NewStats`. This change fixes the issue by passing the stored ci to `transferMap.String` and dropping the context from `StatsInfo`. --- fs/accounting/stats.go | 6 ++---- fs/accounting/transfermap.go | 4 +--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/fs/accounting/stats.go b/fs/accounting/stats.go index 8dc14f236..a1467b957 100644 --- a/fs/accounting/stats.go +++ b/fs/accounting/stats.go @@ -33,7 +33,6 @@ var MaxCompletedTransfers = 100 // to correctly count the updated fields type StatsInfo struct { mu sync.RWMutex - ctx context.Context ci *fs.ConfigInfo bytes int64 errors int64 @@ -85,7 +84,6 @@ type averageValues struct { func NewStats(ctx context.Context) *StatsInfo { ci := fs.GetConfig(ctx) s := &StatsInfo{ - ctx: ctx, ci: ci, checking: newTransferMap(ci.Checkers, "checking"), transferring: newTransferMap(ci.Transfers, "transferring"), @@ -518,10 +516,10 @@ func (s *StatsInfo) String() string { // Add per transfer stats if required if !s.ci.StatsOneLine { if !s.checking.empty() { - _, _ = fmt.Fprintf(buf, "Checking:\n%s\n", s.checking.String(s.ctx, s.inProgress, s.transferring)) + _, _ = fmt.Fprintf(buf, "Checking:\n%s\n", s.checking.String(s.ci, s.inProgress, s.transferring)) } if !s.transferring.empty() { - _, _ = fmt.Fprintf(buf, "Transferring:\n%s\n", s.transferring.String(s.ctx, s.inProgress, nil)) + _, _ = fmt.Fprintf(buf, "Transferring:\n%s\n", s.transferring.String(s.ci, s.inProgress, nil)) } } diff --git a/fs/accounting/transfermap.go b/fs/accounting/transfermap.go index 1f8f6c752..8e05142b1 100644 --- a/fs/accounting/transfermap.go +++ b/fs/accounting/transfermap.go @@ -1,7 +1,6 @@ package accounting import ( - "context" "fmt" "maps" "sort" @@ -91,10 +90,9 @@ func (tm *transferMap) _sortedSlice() []*Transfer { // String returns string representation of map items excluding any in // exclude (if set). -func (tm *transferMap) String(ctx context.Context, progress *inProgress, exclude *transferMap) string { +func (tm *transferMap) String(ci *fs.ConfigInfo, progress *inProgress, exclude *transferMap) string { tm.mu.RLock() defer tm.mu.RUnlock() - ci := fs.GetConfig(ctx) stringList := make([]string, 0, len(tm.items)) for _, tr := range tm._sortedSlice() { var what = tr.what