128 lines
3.7 KiB
Python
128 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Unit test for go-libp2p-0001: identify protocol diff() uses
|
|
slices.Contains inside loop = O(N^2) protocol comparison.
|
|
Fix: use map[protocol.ID]struct{} for O(1) lookups = O(N) total.
|
|
|
|
File: p2p/protocol/identify/id.go
|
|
Function: diff(a, b []protocol.ID) (added, removed []protocol.ID)
|
|
"""
|
|
|
|
import time
|
|
import unittest
|
|
|
|
|
|
# --- DEFECTIVE VERSION: O(N^2) ---
|
|
|
|
def diff_defective(a, b):
|
|
"""Original: slices.Contains(a, x) inside for-range b loop."""
|
|
added = []
|
|
removed = []
|
|
for x in b:
|
|
if x not in a: # linear scan = O(N) per element
|
|
added.append(x)
|
|
for x in a:
|
|
if x not in b: # linear scan = O(N) per element
|
|
removed.append(x)
|
|
return added, removed
|
|
|
|
|
|
# --- PATCHED VERSION: O(N) ---
|
|
|
|
def diff_patched(a, b):
|
|
"""Patched: build sets for O(1) lookups."""
|
|
a_set = set(a)
|
|
b_set = set(b)
|
|
added = [x for x in b if x not in a_set]
|
|
removed = [x for x in a if x not in b_set]
|
|
return added, removed
|
|
|
|
|
|
class TestDiffCorrectness(unittest.TestCase):
|
|
"""Verify patched version matches defective version output."""
|
|
|
|
def _check(self, a, b, want_add, want_rem):
|
|
add_d, rem_d = diff_defective(a, b)
|
|
add_p, rem_p = diff_patched(a, b)
|
|
self.assertEqual(add_d, add_p, "added mismatch between versions")
|
|
self.assertEqual(rem_d, rem_p, "removed mismatch between versions")
|
|
self.assertEqual(add_p, want_add)
|
|
self.assertEqual(rem_p, want_rem)
|
|
|
|
def test_no_change(self):
|
|
self._check(
|
|
["/ipfs/id/1.0.0", "/ipfs/ping/1.0.0"],
|
|
["/ipfs/id/1.0.0", "/ipfs/ping/1.0.0"],
|
|
[], []
|
|
)
|
|
|
|
def test_one_added(self):
|
|
self._check(
|
|
["/ipfs/id/1.0.0"],
|
|
["/ipfs/id/1.0.0", "/ipfs/ping/1.0.0"],
|
|
["/ipfs/ping/1.0.0"], []
|
|
)
|
|
|
|
def test_one_removed(self):
|
|
self._check(
|
|
["/ipfs/id/1.0.0", "/ipfs/ping/1.0.0"],
|
|
["/ipfs/id/1.0.0"],
|
|
[], ["/ipfs/ping/1.0.0"]
|
|
)
|
|
|
|
def test_both_added_and_removed(self):
|
|
self._check(
|
|
["/proto/a", "/proto/b"],
|
|
["/proto/b", "/proto/c"],
|
|
["/proto/c"], ["/proto/a"]
|
|
)
|
|
|
|
def test_empty_to_many(self):
|
|
self._check(
|
|
[],
|
|
["/p/1", "/p/2", "/p/3"],
|
|
["/p/1", "/p/2", "/p/3"], []
|
|
)
|
|
|
|
def test_many_to_empty(self):
|
|
self._check(
|
|
["/p/1", "/p/2", "/p/3"],
|
|
[],
|
|
[], ["/p/1", "/p/2", "/p/3"]
|
|
)
|
|
|
|
|
|
class TestDiffPerformance(unittest.TestCase):
|
|
"""Benchmark: patched should be significantly faster at scale."""
|
|
|
|
def test_performance_scaling(self):
|
|
sizes = [10, 100, 500, 1000]
|
|
for n in sizes:
|
|
# Build two protocol lists with ~50% overlap
|
|
a = [f"/proto/{i}" for i in range(n)]
|
|
b = [f"/proto/{i}" for i in range(n // 2, n + n // 2)]
|
|
|
|
iters = 200
|
|
|
|
# Benchmark defective (list-based O(N^2))
|
|
start = time.perf_counter()
|
|
for _ in range(iters):
|
|
diff_defective(a, b)
|
|
defective_time = time.perf_counter() - start
|
|
|
|
# Benchmark patched (set-based O(N))
|
|
start = time.perf_counter()
|
|
for _ in range(iters):
|
|
diff_patched(a, b)
|
|
patched_time = time.perf_counter() - start
|
|
|
|
ratio = defective_time / patched_time if patched_time > 0 else float('inf')
|
|
print(f" N={n:4d} defective={defective_time:.4f}s patched={patched_time:.4f}s ratio={ratio:.1f}x")
|
|
|
|
if n >= 100:
|
|
self.assertGreater(ratio, 2.0,
|
|
f"N={n}: expected patched to be at least 2x faster, got {ratio:.1f}x")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|