Background
graphdb.ChannelUpdateInfo stores its two freshness values as lnwire.Timestamp:
type ChannelUpdateInfo struct {
ShortChannelID lnwire.ShortChannelID
Version lnwire.GossipVersion
Node1Freshness lnwire.Timestamp
Node2Freshness lnwire.Timestamp
}
lnwire.Timestamp is an interface, and the concrete v1 value is lnwire.UnixTimestamp, which is a uint64. A uint64 does not fit in an interface's data word, so every assignment boxes onto the heap.
Cost
Measured with runtime.MemStats over the exact construction processChanRangeReply performs:
|
bytes/channel |
heap allocs/channel |
| zero timestamps |
48.1 |
0 |
| both timestamps set |
64.0 |
2 |
So the boxing costs 16 B/channel plus two allocations, a 33% increase over the struct itself.
Where it bites
GossipSyncer.bufferedChanRangeReplies holds one ChannelUpdateInfo per short channel ID for the duration of a query_channel_range sync. Two things multiply it:
- Every connected peer buffers independently. All syncers start in
syncingChans and issue their own range query; NumActiveSyncers (default 3) only governs whether a syncer sends a GossipTimestampRange, not whether it performs the range query.
DefaultNumRestrictedSlots is 100, so ~100 inbound peers can be buffering at once, and outbound connections are unrestricted.
With the 100k per-query SCID cap added in #10992, the worst case is roughly 610 MiB steady and 880 MiB peak across 100 peers, of which about 153 MiB is purely the interface boxing.
Proposal
Store the freshness values concretely in the hot struct. Either:
- Replace the interface fields with an explicit
{version, value uint64} struct, keeping lnwire.Timestamp at the API boundary where polymorphism is actually needed, or
- Give
ChannelUpdateInfo a v1-specific representation, since Version already discriminates.
Expected result: 64 → 48 B/channel (25% reduction) and 200k fewer heap allocations per saturated sync, with no change to the wire format or database encoding.
Notes
This is not a correctness issue and needs no backport. It surfaced while reviewing the memory bounds in #10992; the fix was left out of that PR to keep it focused on the bound itself.
Background
graphdb.ChannelUpdateInfostores its two freshness values aslnwire.Timestamp:lnwire.Timestampis an interface, and the concrete v1 value islnwire.UnixTimestamp, which is auint64. Auint64does not fit in an interface's data word, so every assignment boxes onto the heap.Cost
Measured with
runtime.MemStatsover the exact constructionprocessChanRangeReplyperforms:So the boxing costs 16 B/channel plus two allocations, a 33% increase over the struct itself.
Where it bites
GossipSyncer.bufferedChanRangeRepliesholds oneChannelUpdateInfoper short channel ID for the duration of aquery_channel_rangesync. Two things multiply it:syncingChansand issue their own range query;NumActiveSyncers(default 3) only governs whether a syncer sends aGossipTimestampRange, not whether it performs the range query.DefaultNumRestrictedSlotsis 100, so ~100 inbound peers can be buffering at once, and outbound connections are unrestricted.With the 100k per-query SCID cap added in #10992, the worst case is roughly 610 MiB steady and 880 MiB peak across 100 peers, of which about 153 MiB is purely the interface boxing.
Proposal
Store the freshness values concretely in the hot struct. Either:
{version, value uint64}struct, keepinglnwire.Timestampat the API boundary where polymorphism is actually needed, orChannelUpdateInfoa v1-specific representation, sinceVersionalready discriminates.Expected result: 64 → 48 B/channel (25% reduction) and 200k fewer heap allocations per saturated sync, with no change to the wire format or database encoding.
Notes
This is not a correctness issue and needs no backport. It surfaced while reviewing the memory bounds in #10992; the fix was left out of that PR to keep it focused on the bound itself.