119 lines
4.3 KiB
Python
119 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Unit test for renpy-0001: ShownImageInfo.choose_image required/optional list
|
|
membership O(I*A*(R+O)) -> set membership O(I*A)
|
|
|
|
Defect: In renpy/display/image.py, ShownImageInfo.apply_attributes() builds
|
|
`required` and `optional` as Python lists, then passes them to choose_image().
|
|
Inside choose_image(), the inner loop checks `i in required` and `i in optional`
|
|
for every attribute of every registered image for a tag. With lists, each `in`
|
|
check is O(N), making the total O(I * A * (R + O)) where I = images per tag,
|
|
A = attributes per image, R = len(required), O = len(optional).
|
|
|
|
Fix: Change `required` and `optional` from lists to sets. Membership test
|
|
becomes O(1), total becomes O(I * A). Use set.add/discard instead of
|
|
list.append/remove.
|
|
|
|
File: renpy/display/image.py
|
|
Method: ShownImageInfo.apply_attributes / ShownImageInfo.choose_image
|
|
Lines: 984-1002 (apply_attributes), 1011-1032 (choose_image inner loop)
|
|
"""
|
|
|
|
import time
|
|
|
|
|
|
def simulate_defective(image_attrs_by_tag, required_list, optional_list):
|
|
"""
|
|
Simulates choose_image with required and optional as lists.
|
|
O(I * A * (R + O)) due to list membership checks.
|
|
"""
|
|
matches = []
|
|
for attrs in image_attrs_by_tag:
|
|
if not all((i in required_list) or (i in optional_list) for i in attrs):
|
|
continue
|
|
num_required = 0
|
|
for i in attrs:
|
|
if i in required_list:
|
|
num_required += 1
|
|
if num_required != len(required_list):
|
|
continue
|
|
matches.append(attrs)
|
|
return matches
|
|
|
|
|
|
def simulate_fixed(image_attrs_by_tag, required_set, optional_set):
|
|
"""
|
|
Simulates choose_image with required and optional as sets.
|
|
O(I * A) due to set membership checks.
|
|
"""
|
|
matches = []
|
|
for attrs in image_attrs_by_tag:
|
|
if not all((i in required_set) or (i in optional_set) for i in attrs):
|
|
continue
|
|
num_required = 0
|
|
for i in attrs:
|
|
if i in required_set:
|
|
num_required += 1
|
|
if num_required != len(required_set):
|
|
continue
|
|
matches.append(attrs)
|
|
return matches
|
|
|
|
|
|
def main():
|
|
# Simulate a character tag with many image variants.
|
|
# Visual novel characters can have dozens of emotion/pose/outfit combos.
|
|
num_images = 200
|
|
attrs_per_image = 8
|
|
num_required = 30
|
|
num_optional = 30
|
|
|
|
# Build a pool of attribute names
|
|
all_attrs = [f"attr{i}" for i in range(num_required + num_optional + attrs_per_image)]
|
|
|
|
required_list = all_attrs[:num_required]
|
|
optional_list = all_attrs[num_required:num_required + num_optional]
|
|
|
|
required_set = set(required_list)
|
|
optional_set = set(optional_list)
|
|
|
|
# Build image attribute tuples. Each image has some required, some optional,
|
|
# and some unique attrs.
|
|
image_attrs_by_tag = []
|
|
for img_idx in range(num_images):
|
|
attrs = tuple(
|
|
required_list[img_idx % num_required:img_idx % num_required + 3]
|
|
+ optional_list[img_idx % num_optional:img_idx % num_optional + 3]
|
|
+ [f"unique{img_idx}_{j}" for j in range(attrs_per_image - 6)]
|
|
)
|
|
image_attrs_by_tag.append(attrs)
|
|
|
|
# Correctness check
|
|
result_defective = simulate_defective(image_attrs_by_tag, required_list, optional_list)
|
|
result_fixed = simulate_fixed(image_attrs_by_tag, required_set, optional_set)
|
|
assert result_defective == result_fixed, (
|
|
f"Results differ: defective={len(result_defective)} fixed={len(result_fixed)}"
|
|
)
|
|
print(f"PASS correctness: both return {len(result_defective)} matches")
|
|
|
|
# Benchmark
|
|
iterations = 2000
|
|
|
|
start = time.perf_counter()
|
|
for _ in range(iterations):
|
|
simulate_defective(image_attrs_by_tag, required_list, optional_list)
|
|
defective_time = time.perf_counter() - start
|
|
|
|
start = time.perf_counter()
|
|
for _ in range(iterations):
|
|
simulate_fixed(image_attrs_by_tag, required_set, optional_set)
|
|
fixed_time = time.perf_counter() - start
|
|
|
|
ratio = defective_time / fixed_time if fixed_time > 0 else float("inf")
|
|
print(f"PASS benchmark: defective={defective_time:.3f}s fixed={fixed_time:.3f}s ratio={ratio:.1f}x")
|
|
assert ratio > 2.0, f"Expected at least 2x improvement, got {ratio:.1f}x"
|
|
print(f"PASS ratio > 2x confirmed ({ratio:.1f}x)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|