From 3fce1a938d7bc887a6b64e4a394e74051146bde9 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Fri, 7 Aug 2026 00:21:16 -0700 Subject: [PATCH] perf(weed/topology): preallocate the heartbeat volume conversion slice (#10607) * test(weed/topology): benchmark the per-heartbeat volume sync A volume server re-sends its entire volume list every VolumePulsePeriod, so SyncDataNodeRegistration is the master's steady-state per-server cost. Give it a benchmark so allocation regressions show up. * perf(weed/topology): preallocate the heartbeat volume conversion slice The slice grows to one entry per volume on the data node, so at 100k volumes the doubling copies allocate 60MB of garbage per heartbeat. The final length is known up front. BenchmarkSyncDataNodeRegistration/100000Volumes 199670102 B/op -> 137725872 B/op --- weed/topology/topology.go | 2 +- .../topology/topology_heartbeat_bench_test.go | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 weed/topology/topology_heartbeat_bench_test.go diff --git a/weed/topology/topology.go b/weed/topology/topology.go index e898ec7dc..1374ed4eb 100644 --- a/weed/topology/topology.go +++ b/weed/topology/topology.go @@ -587,7 +587,7 @@ func (t *Topology) ListDCAndRacks() (dcs map[NodeId][]NodeId) { func (t *Topology) SyncDataNodeRegistration(volumes []*master_pb.VolumeInformationMessage, dn *DataNode) (newVolumes, deletedVolumes []storage.VolumeInfo) { // convert into in memory struct storage.VolumeInfo - var volumeInfos []storage.VolumeInfo + volumeInfos := make([]storage.VolumeInfo, 0, len(volumes)) for _, v := range volumes { if vi, err := storage.NewVolumeInfo(v); err == nil { volumeInfos = append(volumeInfos, vi) diff --git a/weed/topology/topology_heartbeat_bench_test.go b/weed/topology/topology_heartbeat_bench_test.go new file mode 100644 index 000000000..dc605f6c9 --- /dev/null +++ b/weed/topology/topology_heartbeat_bench_test.go @@ -0,0 +1,52 @@ +package topology + +import ( + "fmt" + "testing" + + "github.com/seaweedfs/seaweedfs/weed/pb/master_pb" +) + +func benchHeartbeatMessages(count int) []*master_pb.VolumeInformationMessage { + messages := make([]*master_pb.VolumeInformationMessage, 0, count) + for i := 0; i < count; i++ { + messages = append(messages, &master_pb.VolumeInformationMessage{ + Id: uint32(i), + Size: 1024 * 1024, + Collection: "benchcollection", + FileCount: 100, + DeleteCount: 1, + DeletedByteCount: 1024, + ReplicaPlacement: 0, + Version: 3, + CompactRevision: 1, + ModifiedAtSecond: 1700000000, + }) + } + return messages +} + +// A volume server re-sends its whole volume list on every heartbeat, so this is +// the master's steady-state cost per volume server every VolumePulsePeriod. +func benchSyncDataNodeRegistration(b *testing.B, count int) { + topo := NewTopology("bench", nil, 32*1024*1024*1024, 5, false) + dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1"). + GetOrCreateDataNode("127.0.0.1", 8080, 18080, "", "", map[string]uint32{"": uint32(count) * 2}) + topo.SyncDataNodeRegistration(benchHeartbeatMessages(count), dn) + + messages := benchHeartbeatMessages(count) + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + topo.SyncDataNodeRegistration(messages, dn) + } +} + +func BenchmarkSyncDataNodeRegistration(b *testing.B) { + for _, count := range []int{1000, 100000} { + b.Run(fmt.Sprintf("%dVolumes", count), func(b *testing.B) { + benchSyncDataNodeRegistration(b, count) + }) + } +}