CreatePublisher builds both signature template formatters per destination:
WithSignatureFormatter(NewSignatureFormatter(d.signatureContentTemplate)),
WithHeaderFormatter(NewHeaderFormatter(d.signatureHeaderTemplate)),
Both template strings are provider fields — the same value for every destination. Each constructor calls sprig.TxtFuncMap(), building a map of ~200 functions that the parsed template then retains. Every cached publisher ends up holding private copies of two identical function maps.
Built unconditionally, including for destinations with signatures disabled.
Cost
Measured on an M1 Pro. Per formatter, to build: ~50 µs, ~53 KB, 50–70 allocs. Retained heap, measured after GC across 2,000 publishers' worth of formatters: 43.6 KB per cached publisher.
| cached destinations |
retained |
| 100 |
~4 MB |
| 1,000 |
~44 MB |
| 10,000 (publisher cache cap) |
~436 MB |
Noise at a few hundred destinations. At the cache ceiling it's likely the single largest consumer.
Proposed change
Build the formatters once in the provider's New(), replacing the stored template strings:
// today
signatureContentTemplate string
signatureHeaderTemplate string
// instead
signatureFormatter SignatureFormatter
headerFormatter HeaderFormatter
CreatePublisher passes them through, leaving secrets as the only genuinely per-destination input:
sm := NewSignatureManager(
secrets,
WithSignatureFormatter(d.signatureFormatter),
WithHeaderFormatter(d.headerFormatter),
WithEncoder(d.encoder),
WithAlgorithm(d.algorithm),
)
Sharing is safe: both formatters hold a single *template.Template and Format executes into a local buffer, and text/template is documented safe for parallel execution once parsed. No synchronization needed.
GetEncoder and GetAlgorithm are worth hoisting in the same pass — both stateless, both derived from provider config.
destwebhookstandard has the same shape and needs the same treatment.
Related
CreatePublisherbuilds both signature template formatters per destination:Both template strings are provider fields — the same value for every destination. Each constructor calls
sprig.TxtFuncMap(), building a map of ~200 functions that the parsed template then retains. Every cached publisher ends up holding private copies of two identical function maps.Built unconditionally, including for destinations with signatures disabled.
Cost
Measured on an M1 Pro. Per formatter, to build: ~50 µs, ~53 KB, 50–70 allocs. Retained heap, measured after GC across 2,000 publishers' worth of formatters: 43.6 KB per cached publisher.
Noise at a few hundred destinations. At the cache ceiling it's likely the single largest consumer.
Proposed change
Build the formatters once in the provider's
New(), replacing the stored template strings:CreatePublisherpasses them through, leavingsecretsas the only genuinely per-destination input:Sharing is safe: both formatters hold a single
*template.TemplateandFormatexecutes into a local buffer, andtext/templateis documented safe for parallel execution once parsed. No synchronization needed.GetEncoderandGetAlgorithmare worth hoisting in the same pass — both stateless, both derived from provider config.destwebhookstandardhas the same shape and needs the same treatment.Related