diff --git a/defects/ipfs-cluster-0001/patch/ipfs-cluster-0001.patch b/defects/ipfs-cluster-0001/patch/ipfs-cluster-0001.patch new file mode 100644 index 000000000..610081953 --- /dev/null +++ b/defects/ipfs-cluster-0001/patch/ipfs-cluster-0001.patch @@ -0,0 +1,61 @@ +--- a/allocate.go ++++ b/allocate.go +@@ -123,11 +123,22 @@ func (c *Cluster) filterMetrics(ctx context.Context, mSet api.MetricsSet, numMet + // For a metric/peer to be included in a group, it is necessary that it has + // metrics for all informers. + func (c *Cluster) filterMetrics(ctx context.Context, mSet api.MetricsSet, numMetrics int, currentAllocs, priorityList, blacklist []peer.ID) classifiedMetrics { + curPeersMap := make(map[peer.ID][]api.Metric) + candPeersMap := make(map[peer.ID][]api.Metric) + prioPeersMap := make(map[peer.ID][]api.Metric) + +- // Divide the metric by current/candidate/prio and by peer ++ // Build O(1) lookup sets for peer classification. ++ blacklistSet := make(map[peer.ID]struct{}, len(blacklist)) ++ for _, p := range blacklist { ++ blacklistSet[p] = struct{}{} ++ } ++ currentAllocsSet := make(map[peer.ID]struct{}, len(currentAllocs)) ++ for _, p := range currentAllocs { ++ currentAllocsSet[p] = struct{}{} ++ } ++ prioritySet := make(map[peer.ID]struct{}, len(priorityList)) ++ for _, p := range priorityList { ++ prioritySet[p] = struct{}{} ++ } ++ ++ // Divide our metrics by current/candidate/prio and by peer + for _, metrics := range mSet { + for _, m := range metrics { + switch { +- case containsPeer(blacklist, m.Peer): ++ case peerInSet(blacklistSet, m.Peer): + // discard blacklisted peers + continue + case c.config.PinOnlyOnTrustedPeers && !c.consensus.IsTrustedPeer(ctx, m.Peer): +@@ -136,9 +147,9 @@ func (c *Cluster) filterMetrics(ctx context.Context, mSet api.MetricsSet, numMet + case c.config.PinOnlyOnUntrustedPeers && c.consensus.IsTrustedPeer(ctx, m.Peer): + // discard peers that are trusted + continue +- case containsPeer(currentAllocs, m.Peer): ++ case peerInSet(currentAllocsSet, m.Peer): + curPeersMap[m.Peer] = append(curPeersMap[m.Peer], m) +- case containsPeer(priorityList, m.Peer): ++ case peerInSet(prioritySet, m.Peer): + prioPeersMap[m.Peer] = append(prioPeersMap[m.Peer], m) + default: + candPeersMap[m.Peer] = append(candPeersMap[m.Peer], m) +--- a/util.go ++++ b/util.go +@@ -72,6 +72,12 @@ func containsPeer(list []peer.ID, peer peer.ID) bool { + return false + } + ++// peerInSet checks for peer membership in a pre-built set. O(1) average. ++func peerInSet(set map[peer.ID]struct{}, p peer.ID) bool { ++ _, ok := set[p] ++ return ok ++} ++ + func minInt(x, y int) int { + if x < y { + return x diff --git a/defects/ipfs-cluster-0001/test/go.mod b/defects/ipfs-cluster-0001/test/go.mod new file mode 100644 index 000000000..2eaaa411c --- /dev/null +++ b/defects/ipfs-cluster-0001/test/go.mod @@ -0,0 +1,3 @@ +module ipfscluster0001 + +go 1.21 diff --git a/defects/ipfs-cluster-0001/test/ipfs_cluster_0001_test.go b/defects/ipfs-cluster-0001/test/ipfs_cluster_0001_test.go new file mode 100644 index 000000000..528d45a61 --- /dev/null +++ b/defects/ipfs-cluster-0001/test/ipfs_cluster_0001_test.go @@ -0,0 +1,158 @@ +package ipfscluster0001 + +// Unit test for ipfs-cluster-0001: filterMetrics containsPeer O(N²) +// +// Defect: filterMetrics() calls containsPeer() (linear scan) for blacklist, +// currentAllocs, and priorityList on every metric in our nested loop. +// Complexity: O(M * (B + C + P)) where M = total metrics, B/C/P = list sizes. +// +// Fix: Convert slices to map[string]struct{} sets before our loop. +// Patched complexity: O(M + B + C + P). + +import ( + "fmt" + "testing" +) + +// Simulates our peer.ID as a string (same underlying type in libp2p). +type peerID string + +// --- BEFORE: linear scan (defective) --- + +func containsPeer(list []peerID, peer peerID) bool { + for _, p := range list { + if p == peer { + return true + } + } + return false +} + +func filterMetricsBefore( + metrics []peerID, + blacklist []peerID, + currentAllocs []peerID, + priorityList []peerID, +) (cur, prio, cand []peerID) { + for _, m := range metrics { + switch { + case containsPeer(blacklist, m): + continue + case containsPeer(currentAllocs, m): + cur = append(cur, m) + case containsPeer(priorityList, m): + prio = append(prio, m) + default: + cand = append(cand, m) + } + } + return +} + +// --- AFTER: set lookup (patched) --- + +func peerInSet(set map[peerID]struct{}, p peerID) bool { + _, ok := set[p] + return ok +} + +func toSet(list []peerID) map[peerID]struct{} { + s := make(map[peerID]struct{}, len(list)) + for _, p := range list { + s[p] = struct{}{} + } + return s +} + +func filterMetricsAfter( + metrics []peerID, + blacklist []peerID, + currentAllocs []peerID, + priorityList []peerID, +) (cur, prio, cand []peerID) { + blacklistSet := toSet(blacklist) + currentAllocsSet := toSet(currentAllocs) + prioritySet := toSet(priorityList) + + for _, m := range metrics { + switch { + case peerInSet(blacklistSet, m): + continue + case peerInSet(currentAllocsSet, m): + cur = append(cur, m) + case peerInSet(prioritySet, m): + prio = append(prio, m) + default: + cand = append(cand, m) + } + } + return +} + +// --- Correctness --- + +func TestCorrectness(t *testing.T) { + metrics := []peerID{"p1", "p2", "p3", "p4", "p5", "p6"} + blacklist := []peerID{"p1"} + currentAllocs := []peerID{"p2", "p3"} + priorityList := []peerID{"p4"} + + curB, prioB, candB := filterMetricsBefore(metrics, blacklist, currentAllocs, priorityList) + curA, prioA, candA := filterMetricsAfter(metrics, blacklist, currentAllocs, priorityList) + + if fmt.Sprint(curB) != fmt.Sprint(curA) { + t.Fatalf("current mismatch: before=%v after=%v", curB, curA) + } + if fmt.Sprint(prioB) != fmt.Sprint(prioA) { + t.Fatalf("priority mismatch: before=%v after=%v", prioB, prioA) + } + if fmt.Sprint(candB) != fmt.Sprint(candA) { + t.Fatalf("candidate mismatch: before=%v after=%v", candB, candA) + } + + // Verify expected classification + if fmt.Sprint(curA) != "[p2 p3]" { + t.Fatalf("expected cur=[p2 p3], got %v", curA) + } + if fmt.Sprint(prioA) != "[p4]" { + t.Fatalf("expected prio=[p4], got %v", prioA) + } + if fmt.Sprint(candA) != "[p5 p6]" { + t.Fatalf("expected cand=[p5 p6], got %v", candA) + } +} + +// --- Benchmarks --- + +func makePeers(n int, prefix string) []peerID { + peers := make([]peerID, n) + for i := 0; i < n; i++ { + peers[i] = peerID(fmt.Sprintf("%s-%d", prefix, i)) + } + return peers +} + +func BenchmarkBefore(b *testing.B) { + // 500 metrics, 100 blacklisted, 100 current, 100 priority, 200 candidates + metrics := makePeers(500, "m") + blacklist := makePeers(100, "m") // first 100 overlap + current := makePeers(100, "cur") + priority := makePeers(100, "pri") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + filterMetricsBefore(metrics, blacklist, current, priority) + } +} + +func BenchmarkAfter(b *testing.B) { + metrics := makePeers(500, "m") + blacklist := makePeers(100, "m") + current := makePeers(100, "cur") + priority := makePeers(100, "pri") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + filterMetricsAfter(metrics, blacklist, current, priority) + } +}