renpy-0001: ShownImageInfo.choose_image() list membership O(I*A*(R+O)), 5.9x
This commit is contained in:
parent
9fac7766ba
commit
bf67964b10
2 changed files with 154 additions and 0 deletions
35
defects/renpy-0001/patch/renpy-0001.patch
Normal file
35
defects/renpy-0001/patch/renpy-0001.patch
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# UNDF: UNDF-2026-000000969
|
||||||
|
--- a/renpy/display/image.py
|
||||||
|
+++ b/renpy/display/image.py
|
||||||
|
@@ -980,10 +980,10 @@
|
||||||
|
defaults = f(name)
|
||||||
|
|
||||||
|
# The list of attributes a matching image may have.
|
||||||
|
- optional = list(defaults) if defaults else []
|
||||||
|
+ optional = set(defaults) if defaults else set()
|
||||||
|
|
||||||
|
# The list of attributes a matching image must have.
|
||||||
|
- required = []
|
||||||
|
+ required = set()
|
||||||
|
|
||||||
|
for i in name[1:]:
|
||||||
|
if i[0] == "-":
|
||||||
|
@@ -991,16 +991,16 @@
|
||||||
|
|
||||||
|
if i in optional:
|
||||||
|
- optional.remove(i)
|
||||||
|
+ optional.discard(i)
|
||||||
|
|
||||||
|
if i in required:
|
||||||
|
- required.remove(i)
|
||||||
|
+ required.discard(i)
|
||||||
|
|
||||||
|
else:
|
||||||
|
- required.append(i)
|
||||||
|
+ required.add(i)
|
||||||
|
|
||||||
|
return self.choose_image(nametag, required, optional, name)
|
||||||
|
|
||||||
|
def choose_image(self, tag, required, optional, exception_name):
|
||||||
|
# The longest length of an image that matches.
|
||||||
|
max_len = -1
|
||||||
119
defects/renpy-0001/test/renpy-0001-test.py
Normal file
119
defects/renpy-0001/test/renpy-0001-test.py
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
#!/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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue