Skip to content

Commit

Permalink
opt: reduce ShardsInfo memory usage with bitmap and sorted slice (#7974)
Browse files Browse the repository at this point in the history
* opt: reduce ShardsInfo memory usage with bitmap and sorted slice

- Replace map[ShardId]*ShardInfo with sorted []ShardInfo slice
- Add ShardBits (uint32) bitmap for O(1) existence checks
- Use binary search for O(log n) lookups by shard ID
- Maintain sorted order for efficient iteration
- Add comprehensive unit tests and benchmarks

Memory savings:
- Map overhead: ~48 bytes per entry eliminated
- Pointers: 8 bytes per entry eliminated
- Total: ~56 bytes per shard saved

Performance improvements:
- Has(): O(1) using bitmap
- Size(): O(log n) using binary search (was O(1), acceptable tradeoff)
- Count(): O(1) using popcount on bitmap
- Iteration: Faster due to cache locality

* refactor: add methods to ShardBits type

- Add Has(), Set(), Clear(), and Count() methods to ShardBits
- Simplify ShardsInfo methods by using ShardBits methods
- Improves code readability and encapsulation

* opt: use ShardBits directly in ShardsCountFromVolumeEcShardInformationMessage

Avoid creating a full ShardsInfo object just to count shards.
Directly cast vi.EcIndexBits to ShardBits and use Count() method.

* opt: use strings.Builder in ShardsInfo.String() for efficiency

* refactor: change AsSlice to return []ShardInfo (values instead of pointers)

This completes the memory optimization by avoiding unnecessary pointer slices and potential allocations.

* refactor: rename ShardsCountFromVolumeEcShardInformationMessage to GetShardCount

* fix: prevent deadlock in Add and Subtract methods

Copy shards data from 'other' before releasing its lock to avoid
potential deadlock when a.Add(b) and b.Add(a) are called concurrently.

The previous implementation held other's lock while calling si.Set/Delete,
which acquires si's lock. This could deadlock if two goroutines tried to
add/subtract each other concurrently.

* opt: avoid unnecessary locking in constructor functions

ShardsInfoFromVolume and ShardsInfoFromVolumeEcShardInformationMessage
now build shards slice and bitmap directly without calling Set(), which
acquires a lock on every call. Since the object is local and not yet
shared, locking is unnecessary and adds overhead.

This improves performance during object construction.

* fix: rename 'copy' variable to avoid shadowing built-in function

The variable name 'copy' in TestShardsInfo_Copy shadowed the built-in
copy() function, which is confusing and bad practice. Renamed to 'siCopy'.

* opt: use math/bits.OnesCount32 and reorganize types

1. Replace manual popcount loop with math/bits.OnesCount32 for better
   performance and idiomatic Go code
2. Move ShardSize type definition to ec_shards_info.go for better code
   organization since it's primarily used there

* refactor: Set() now accepts ShardInfo for future extensibility

Changed Set(id ShardId, size ShardSize) to Set(shard ShardInfo) to
support future additions to ShardInfo without changing the API.

This makes the code more extensible as new fields can be added to
ShardInfo (e.g., checksum, location, etc.) without breaking the Set API.

* refactor: move ShardInfo and ShardSize to separate file

Created ec_shard_info.go to hold the basic shard types (ShardInfo and
ShardSize) for better code organization and separation of concerns.

* refactor: add ShardInfo constructor and helper functions

Added NewShardInfo() constructor and IsValid() method to better
encapsulate ShardInfo creation and validation. Updated code to use
the constructor for cleaner, more maintainable code.

* fix: update remaining Set() calls to use NewShardInfo constructor

Fixed compilation errors in storage and shell packages where Set() calls
were not updated to use the new NewShardInfo() constructor.

* fix: remove unreachable code in filer backup commands

Removed unreachable return statements after infinite loops in
filer_backup.go and filer_meta_backup.go to fix compilation errors.

* fix: rename 'new' variable to avoid shadowing built-in

Renamed 'new' to 'result' in MinusParityShards, Plus, and Minus methods
to avoid shadowing Go's built-in new() function.

* fix: update remaining test files to use NewShardInfo constructor

Fixed Set() calls in command_volume_list_test.go and
ec_rebalance_slots_test.go to use NewShardInfo() constructor.
  • Loading branch information
Chris Lu authored and GitHub committed Jan 6, 2026
1 parent a75cc09 commit e10f11b
Show file tree
Hide file tree
Showing 17 changed files with 786 additions and 300 deletions.
1 change: 0 additions & 1 deletion weed/command/filer_backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ func runFilerBackup(cmd *Command, args []string) bool {
time.Sleep(1747 * time.Millisecond)
}
}
return false
}

const (
Expand Down
1 change: 0 additions & 1 deletion weed/command/filer_meta_backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ func runFilerMetaBackup(cmd *Command, args []string) bool {
time.Sleep(1747 * time.Millisecond)
}
}
return false
}

func (metaBackup *FilerMetaBackupOptions) initStore(v *viper.Viper) error {
Expand Down
2 changes: 1 addition & 1 deletion weed/shell/command_cluster_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ func (sp *ClusterStatusPrinter) printVolumeInfo() {
for _, eci := range di.EcShardInfos {
vid := needle.VolumeId(eci.Id)
ecVolumeIds[vid] = true
ecShards += erasure_coding.ShardsCountFromVolumeEcShardInformationMessage(eci)
ecShards += erasure_coding.GetShardCount(eci)
}
}
}
Expand Down
10 changes: 5 additions & 5 deletions weed/shell/command_ec_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ func swap(data []*CandidateEcNode, i, j int) {

func countShards(ecShardInfos []*master_pb.VolumeEcShardInformationMessage) (count int) {
for _, eci := range ecShardInfos {
count += erasure_coding.ShardsCountFromVolumeEcShardInformationMessage(eci)
count += erasure_coding.GetShardCount(eci)
}
return
}
Expand All @@ -441,7 +441,7 @@ func (ecNode *EcNode) localShardIdCount(vid uint32) int {
for _, diskInfo := range ecNode.info.DiskInfos {
for _, eci := range diskInfo.EcShardInfos {
if vid == eci.Id {
return erasure_coding.ShardsCountFromVolumeEcShardInformationMessage(eci)
return erasure_coding.GetShardCount(eci)
}
}
}
Expand Down Expand Up @@ -602,7 +602,7 @@ func (ecNode *EcNode) addEcVolumeShards(vid needle.VolumeId, collection string,
si := erasure_coding.ShardsInfoFromVolumeEcShardInformationMessage(ecsi)
oldShardCount := si.Count()
for _, shardId := range shardIds {
si.Set(shardId, 0)
si.Set(erasure_coding.NewShardInfo(shardId, 0))
}
ecsi.EcIndexBits = si.Bitmap()
ecsi.ShardSizes = si.SizesInt64()
Expand All @@ -621,7 +621,7 @@ func (ecNode *EcNode) addEcVolumeShards(vid needle.VolumeId, collection string,
if !foundVolume {
si := erasure_coding.NewShardsInfo()
for _, id := range shardIds {
si.Set(id, 0)
si.Set(erasure_coding.NewShardInfo(id, 0))
}
diskInfo.EcShardInfos = append(diskInfo.EcShardInfos, &master_pb.VolumeEcShardInformationMessage{
Id: uint32(vid),
Expand Down Expand Up @@ -1107,7 +1107,7 @@ func (ecb *ecBalancer) doBalanceEcRack(ecRack *EcRack) error {
return
}
for _, ecShardInfo := range diskInfo.EcShardInfos {
count += erasure_coding.ShardsCountFromVolumeEcShardInformationMessage(ecShardInfo)
count += erasure_coding.GetShardCount(ecShardInfo)
}
return ecNode.info.Id, count
})
Expand Down
2 changes: 1 addition & 1 deletion weed/shell/command_ec_rebuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ func (erb *ecRebuilder) countLocalShards(node *EcNode, collection string, volume
for _, diskInfo := range node.info.DiskInfos {
for _, ecShardInfo := range diskInfo.EcShardInfos {
if ecShardInfo.Collection == collection && needle.VolumeId(ecShardInfo.Id) == volumeId {
return erasure_coding.ShardsCountFromVolumeEcShardInformationMessage(ecShardInfo)
return erasure_coding.GetShardCount(ecShardInfo)
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions weed/shell/command_volume_balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ func capacityByMaxVolumeCount(diskType types.DiskType) CapacityFunc {
}
var ecShardCount int
for _, ecShardInfo := range diskInfo.EcShardInfos {
ecShardCount += erasure_coding.ShardsCountFromVolumeEcShardInformationMessage(ecShardInfo)
ecShardCount += erasure_coding.GetShardCount(ecShardInfo)
}
return float64(diskInfo.MaxVolumeCount) - float64(ecShardCount)/erasure_coding.DataShardsCount
}
Expand All @@ -269,7 +269,7 @@ func capacityByFreeVolumeCount(diskType types.DiskType) CapacityFunc {
}
var ecShardCount int
for _, ecShardInfo := range diskInfo.EcShardInfos {
ecShardCount += erasure_coding.ShardsCountFromVolumeEcShardInformationMessage(ecShardInfo)
ecShardCount += erasure_coding.GetShardCount(ecShardInfo)
}
return float64(diskInfo.MaxVolumeCount-diskInfo.VolumeCount) - float64(ecShardCount)/erasure_coding.DataShardsCount
}
Expand Down
2 changes: 1 addition & 1 deletion weed/shell/command_volume_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func parseOutput(output string) *master_pb.TopologyInfo {
shardsInfo := erasure_coding.NewShardsInfo()
for _, shardId := range strings.Split(shards, ",") {
sid, _ := strconv.Atoi(shardId)
shardsInfo.Set(erasure_coding.ShardId(sid), 0)
shardsInfo.Set(erasure_coding.NewShardInfo(erasure_coding.ShardId(sid), 0))
}
ecShard.EcIndexBits = shardsInfo.Bitmap()
ecShard.ShardSizes = shardsInfo.SizesInt64()
Expand Down
8 changes: 4 additions & 4 deletions weed/shell/ec_rebalance_slots_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func TestECRebalanceWithLimitedSlots(t *testing.T) {
shardCount := 0
for _, diskInfo := range node.info.DiskInfos {
for _, ecShard := range diskInfo.EcShardInfos {
shardCount += erasure_coding.ShardsCountFromVolumeEcShardInformationMessage(ecShard)
shardCount += erasure_coding.GetShardCount(ecShard)
}
}
t.Logf(" Node %s (rack %s): %d shards, %d free slots",
Expand All @@ -56,7 +56,7 @@ func TestECRebalanceWithLimitedSlots(t *testing.T) {
for _, node := range ecNodes {
for _, diskInfo := range node.info.DiskInfos {
for _, ecShard := range diskInfo.EcShardInfos {
totalEcShards += erasure_coding.ShardsCountFromVolumeEcShardInformationMessage(ecShard)
totalEcShards += erasure_coding.GetShardCount(ecShard)
}
}
}
Expand Down Expand Up @@ -122,7 +122,7 @@ func TestECRebalanceZeroFreeSlots(t *testing.T) {
shardCount := 0
for _, diskInfo := range node.info.DiskInfos {
for _, ecShard := range diskInfo.EcShardInfos {
shardCount += erasure_coding.ShardsCountFromVolumeEcShardInformationMessage(ecShard)
shardCount += erasure_coding.GetShardCount(ecShard)
}
}
t.Logf(" Node %s: %d shards, %d free slots, volumeCount=%d, max=%d",
Expand Down Expand Up @@ -228,7 +228,7 @@ func buildEcShards(volumeIds []uint32) []*master_pb.VolumeEcShardInformationMess
for _, vid := range volumeIds {
si := erasure_coding.NewShardsInfo()
for _, id := range erasure_coding.AllShardIds() {
si.Set(id, 1234)
si.Set(erasure_coding.NewShardInfo(id, 1234))
}
shards = append(shards, &master_pb.VolumeEcShardInformationMessage{
Id: vid,
Expand Down
23 changes: 23 additions & 0 deletions weed/storage/erasure_coding/ec_shard_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package erasure_coding

// ShardSize represents the size of a shard in bytes
type ShardSize int64

// ShardInfo holds information about a single shard
type ShardInfo struct {
Id ShardId
Size ShardSize
}

// NewShardInfo creates a new ShardInfo with the given ID and size
func NewShardInfo(id ShardId, size ShardSize) ShardInfo {
return ShardInfo{
Id: id,
Size: size,
}
}

// IsValid checks if the shard info has a valid ID
func (si ShardInfo) IsValid() bool {
return si.Id < MaxShardCount
}
Loading

0 comments on commit e10f11b

Please sign in to comment.