33package server
44
55import (
6+ "bytes"
67 "crypto/sha256"
78 "encoding/base64"
89 "encoding/hex"
@@ -11,6 +12,7 @@ import (
1112 "log/slog"
1213 "os"
1314 "path/filepath"
15+ "sync"
1416 "time"
1517
1618 "github.com/TeoSlayer/pilotprotocol/internal/fsutil"
@@ -20,6 +22,19 @@ import (
2022 "github.com/TeoSlayer/pilotprotocol/pkg/urlvalidate"
2123)
2224
25+ // flushSaveBufPool reuses the bytes buffer that backs the snapshot JSON
26+ // across save ticks. Pre-grown to 128 MB so the first save at fleet
27+ // scale (~50-100 MB JSON) doesn't grow it further. After the first
28+ // save the pool always returns a buffer at peak capacity, so subsequent
29+ // saves do zero allocation in the encode path. Eliminates the ~1 GB
30+ // live `bytes.growSlice` heap that was driving GC STW pauses.
31+ var flushSaveBufPool = sync.Pool {
32+ New : func () interface {} {
33+ b := make ([]byte , 0 , 128 * 1024 * 1024 )
34+ return & b
35+ },
36+ }
37+
2338// rawNodeCopy holds raw node fields copied under RLock (no encoding).
2439// base64/time.Format happens outside the lock to minimize lock hold time.
2540type rawNodeCopy struct {
@@ -383,22 +398,47 @@ func (s *Server) flushSave() error {
383398 }
384399 s .auditMu .Unlock ()
385400
386- // Compute checksum: marshal once without checksum (omitempty omits it), hash,
387- // then inject the checksum into the JSON without a second marshal.
401+ // Compute checksum: encode once without checksum (omitempty omits it), hash,
402+ // then inject the checksum into the JSON without a second encode.
403+ //
404+ // 2026-05-14: switched from json.Marshal (one-shot allocate) to json.Encoder
405+ // writing into a pooled bytes.Buffer. At 170k+ active nodes the snapshot is
406+ // ~50-100 MB and was the dominant heap allocator (~1 GB live, GC pressure
407+ // causing kernel UDP drops during STW pauses). The pooled buffer is reused
408+ // across save ticks — the underlying slice grows once and stays at peak,
409+ // no per-tick allocation thereafter.
388410 snap .Checksum = ""
389- data , err := json .Marshal (snap )
390- if err != nil {
391- slog .Error ("registry save marshal error" , "err" , err )
392- return fmt .Errorf ("marshal snapshot: %w" , err )
411+ bp := flushSaveBufPool .Get ().(* []byte )
412+ buf := bytes .NewBuffer ((* bp )[:0 ])
413+ enc := json .NewEncoder (buf )
414+ enc .SetEscapeHTML (false )
415+ if err := enc .Encode (snap ); err != nil {
416+ * bp = buf .Bytes ()[:0 ]
417+ flushSaveBufPool .Put (bp )
418+ slog .Error ("registry save encode error" , "err" , err )
419+ return fmt .Errorf ("encode snapshot: %w" , err )
420+ }
421+ data := buf .Bytes ()
422+ // json.Encoder.Encode appends a newline; drop it to match prior Marshal output.
423+ if len (data ) > 0 && data [len (data )- 1 ] == '\n' {
424+ data = data [:len (data )- 1 ]
393425 }
394426 hash := sha256 .Sum256 (data )
395427 checksum := hex .EncodeToString (hash [:])
396- // Insert "checksum":"<hex>" before the closing brace. json.Marshal of a struct
397- // always produces a JSON object ending with '}' (no trailing whitespace ).
428+ // Insert "checksum":"<hex>" before the closing brace. json.Encoder of a struct
429+ // always produces a JSON object ending with '}' (after newline trim ).
398430 if len (data ) == 0 || data [len (data )- 1 ] != '}' {
399- return fmt .Errorf ("marshal snapshot: unexpected JSON format (expected trailing '}')" )
431+ * bp = buf .Bytes ()[:0 ]
432+ flushSaveBufPool .Put (bp )
433+ return fmt .Errorf ("encode snapshot: unexpected JSON format (expected trailing '}')" )
400434 }
401435 data = append (data [:len (data )- 1 ], []byte (`,"checksum":"` + checksum + `"}` )... )
436+ defer func () {
437+ // Return the (possibly grown) underlying buffer to the pool. AtomicWrite
438+ // has copied data to disk by this point, so it is safe to release.
439+ * bp = data [:0 ]
440+ flushSaveBufPool .Put (bp )
441+ }()
402442
403443 // Persist to disk atomically
404444 if s .storePath != "" {
0 commit comments