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