59 lines
2.5 KiB
Diff
59 lines
2.5 KiB
Diff
# UNDF: UNDF-2026-000001138
|
|
# UNDF: (leave blank)
|
|
# CWE-407: Algorithmic Complexity (linear scan inside loop over selected items)
|
|
# OpenShot-Qt query.py: QueryObject.filter() scans ALL objects of a type (O(N))
|
|
# and is called inside loops over selected_clips, selected_effects, etc.
|
|
#
|
|
# Defective path (src/classes/query.py line 116-160,
|
|
# called from src/windows/main_window.py):
|
|
#
|
|
# # actionRemoveClip_trigger (main_window.py ~line 2140):
|
|
# for clip_id in self.selected_clips: # S iterations
|
|
# clips = Clip.filter(id=clip_id) # O(C) linear scan each time
|
|
# for c in clips:
|
|
# c.delete()
|
|
#
|
|
# # QueryObject.filter() (query.py line 128):
|
|
# for child in parent: # scans ALL C clips
|
|
# if child.get("id") == clip_id:
|
|
# ...
|
|
#
|
|
# Complexity: O(S * C) where S = selected clips, C = total clips in project.
|
|
# With 100 selected clips from a 5000-clip project: 500,000 dict lookups.
|
|
#
|
|
# Same pattern in: actionRemoveTransition_trigger, actionRemoveEffect_trigger,
|
|
# keyframe update loops, and other multi-selection operations.
|
|
#
|
|
# Fix: build a {id -> object} dict once before the loop. O(C) build cost,
|
|
# then O(1) per lookup. Total O(C + S) vs O(S * C).
|
|
#
|
|
# Alternatively, add an id-keyed index to QueryObject so filter(id=x) is O(1).
|
|
# The single-call pattern Clip.filter(file_id=f.id) benefits from the index too.
|
|
#
|
|
# Severity: MEDIUM. UI responsiveness degrades quadratically when mass-deleting
|
|
# or mass-moving clips. Affects every multi-selection operation in the editor.
|
|
--- a/src/classes/query.py
|
|
+++ b/src/classes/query.py
|
|
@@ -116,6 +116,15 @@ class QueryObject:
|
|
def filter(OBJECT_TYPE, **kwargs):
|
|
""" Take any arguments given as filters, and find a list of matching objects """
|
|
|
|
+ # Fast path: single id lookup via index avoids O(N) scan
|
|
+ if list(kwargs.keys()) == ["id"] and hasattr(OBJECT_TYPE, "_id_index"):
|
|
+ result = OBJECT_TYPE._id_index.get(kwargs["id"])
|
|
+ return [result] if result is not None else []
|
|
+
|
|
# Get a list of all objects of this type
|
|
parent = get_app().project.get(OBJECT_TYPE.object_key)
|
|
|
|
+# Usage at call sites — replace inner-loop calls with index-based lookup:
|
|
+#
|
|
+# # Before (O(S*C)):
|
|
+# for clip_id in self.selected_clips:
|
|
+# clips = Clip.filter(id=clip_id)
|
|
+#
|
|
+# # After (O(C + S)):
|
|
+# all_clips = {c.id: c for c in Clip.filter()}
|
|
+# for clip_id in self.selected_clips:
|
|
+# c = all_clips.get(clip_id)
|
|
+# if c: c.delete()
|