cal_searching_got_instance_cb() scans entire search_hit_cache GSList with g_slist_find_custom() for each new calendar instance: O(1)+O(2)+...+O(N) = O(N^2). Fix: parallel GHashTable for O(1) membership test. 433x op-count speedup at N=1000. MOADs 0002/0003/0004/0005 CLEAN.
130 lines
3.8 KiB
Python
130 lines
3.8 KiB
Python
"""
|
|
Unit test for evolution-0002: Calendar search_hit_cache dedup O(N^2) fix.
|
|
|
|
Simulates cal_searching_got_instance_cb() collecting unique time_t values
|
|
into a dedup'd cache. Measures op-count for defective (GSList linear scan)
|
|
vs fixed (GHashTable O(1)) implementations.
|
|
|
|
Asserts speedup > 3x at N=100 and N=1000.
|
|
"""
|
|
import time
|
|
import sys
|
|
|
|
PASS = True
|
|
|
|
|
|
def collect_instances_defective(instances):
|
|
"""
|
|
Defective: linear scan for each new instance.
|
|
Equivalent to g_slist_find_custom on a growing GSList.
|
|
Returns (cache_list, op_count).
|
|
"""
|
|
cache = []
|
|
ops = 0
|
|
for val in instances:
|
|
# Scan entire cache for duplicate — O(k) per insert
|
|
found = False
|
|
for c in cache:
|
|
ops += 1
|
|
if c == val:
|
|
found = True
|
|
break
|
|
if not found:
|
|
cache.append(val)
|
|
return cache, ops
|
|
|
|
|
|
def collect_instances_fixed(instances):
|
|
"""
|
|
Fixed: O(1) hash set membership check.
|
|
Equivalent to g_hash_table_contains before g_slist_append.
|
|
Returns (cache_list, op_count).
|
|
"""
|
|
cache = []
|
|
cache_set = set()
|
|
ops = 0
|
|
for val in instances:
|
|
ops += 1 # One hash lookup per insert
|
|
if val not in cache_set:
|
|
cache_set.add(val)
|
|
cache.append(val)
|
|
return cache, ops
|
|
|
|
|
|
def run_test(label, n_instances, expected_speedup):
|
|
global PASS
|
|
|
|
# Generate N unique timestamps (weekly recurring event instances)
|
|
# Mix in some duplicates: same event across two calendars = same timestamp
|
|
unique = list(range(n_instances))
|
|
# 20% duplicates simulating overlapping calendar subscriptions
|
|
duplicates = unique[: n_instances // 5]
|
|
instances = unique + duplicates
|
|
|
|
# Shuffle deterministically for reproducibility
|
|
# (keep order as-is; order doesn't change big-O result)
|
|
|
|
_, ops_defective = collect_instances_defective(instances)
|
|
cache_fixed, ops_fixed = collect_instances_fixed(instances)
|
|
|
|
ratio = ops_defective / max(ops_fixed, 1)
|
|
|
|
status = "PASS" if ratio >= expected_speedup else "FAIL"
|
|
if status == "FAIL":
|
|
PASS = False
|
|
|
|
print(
|
|
f"[{status}] {label}: N={n_instances} | "
|
|
f"defective={ops_defective} ops | fixed={ops_fixed} ops | ratio={ratio:.1f}x "
|
|
f"(expected >= {expected_speedup}x)"
|
|
)
|
|
|
|
# Verify correctness: both produce same unique set
|
|
cache_defective, _ = collect_instances_defective(instances)
|
|
if sorted(cache_defective) != sorted(cache_fixed):
|
|
print(f" [FAIL] correctness: cache contents differ!")
|
|
PASS = False
|
|
else:
|
|
print(f" correctness: caches match ({len(cache_fixed)} unique timestamps)")
|
|
|
|
|
|
def run_wall_time_test(label, n_instances):
|
|
"""Wall-clock speedup check for large N."""
|
|
unique = list(range(n_instances))
|
|
duplicates = unique[: n_instances // 5]
|
|
instances = unique + duplicates
|
|
|
|
t0 = time.perf_counter()
|
|
collect_instances_defective(instances)
|
|
t1 = time.perf_counter()
|
|
collect_instances_fixed(instances)
|
|
t2 = time.perf_counter()
|
|
|
|
defective_ms = (t1 - t0) * 1000
|
|
fixed_ms = (t2 - t1) * 1000
|
|
ratio = defective_ms / max(fixed_ms, 1e-9)
|
|
print(
|
|
f" wall-clock {label}: defective={defective_ms:.2f}ms fixed={fixed_ms:.2f}ms ratio={ratio:.1f}x"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("evolution-0002: Calendar search_hit_cache dedup O(N^2) fix")
|
|
print("=" * 65)
|
|
|
|
run_test("N=50 op-count", 50, 3.0)
|
|
run_test("N=100 op-count", 100, 10.0)
|
|
run_test("N=500 op-count", 500, 50.0)
|
|
run_test("N=1000 op-count", 1000, 100.0)
|
|
|
|
print()
|
|
run_wall_time_test("N=1000 wall-clock", 1000)
|
|
run_wall_time_test("N=5000 wall-clock", 5000)
|
|
|
|
print()
|
|
if PASS:
|
|
print("ALL TESTS PASSED")
|
|
sys.exit(0)
|
|
else:
|
|
print("SOME TESTS FAILED")
|
|
sys.exit(1)
|