Skip to content

Commit cc98e80

Browse files
committed
fix(templates): place the blocks of a chain in time linear in its blocks
Each block placed looked up the blocks it holds among all the blocks of its template, so placing a template's blocks took time quadratic in their number: a 501 KB template of 16,000 empty blocks took more than 8 s to collect, where main takes 0.07 s, and the full analysis 11.6 s instead of 0.5 s. The blocks each block holds, and those standing in no block, are now indexed once per chain, while the blocks are listed. The same 16,000 blocks are placed in 0.08 s, and the full analysis takes 0.3 s. What is placed does not change.
1 parent 33b25c7 commit cc98e80

2 files changed

Lines changed: 33 additions & 5 deletions

File tree

‎src/coretrace_python/taint/templates.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,11 +1099,12 @@ def _page(name: str, readable: Callable[[str | None], _Template | None]) -> _Ren
10991099
break
11001100
current = parent.name
11011101
defining: dict[str, list[int]] = {}
1102+
held: dict[tuple[int, str | None], list[str]] = {}
11021103
for level, (_, template) in enumerate(chain):
11031104
for block in template.blocks:
11041105
defining.setdefault(block, []).append(level)
1105-
root = chain[-1][1]
1106-
pending = [block for block in root.blocks if root.enclosing.get(block) is None] if known else list(defining)
1106+
held.setdefault((level, template.enclosing.get(block)), []).append(block)
1107+
pending = list(held.get((len(chain) - 1, None), ())) if known else list(defining)
11071108
placed: set[str] = set()
11081109
shown: set[tuple[int, str]] = set()
11091110
while pending:
@@ -1113,9 +1114,8 @@ def _page(name: str, readable: Callable[[str | None], _Template | None]) -> _Ren
11131114
placed.add(block)
11141115
for level in defining.get(block, []):
11151116
shown.add((level, block))
1116-
template = chain[level][1]
1117-
pending.extend(inner for inner in template.blocks if template.enclosing.get(inner) == block)
1118-
if block not in template.extended:
1117+
pending.extend(held.get((level, block), ()))
1118+
if block not in chain[level][1].extended:
11191119
break
11201120

11211121
def renders(level: int, blocks: tuple[str, ...]) -> bool:

‎tests/test_template_block_placement.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212

1313
from __future__ import annotations
1414

15+
import json
16+
import subprocess
17+
import sys
1518
from pathlib import Path
1619

1720
import pytest
@@ -22,6 +25,7 @@
2225
from coretrace_python.semantic.symbols import SymbolId
2326
from coretrace_python.taint.templates import project_templates
2427

28+
TIMEOUT = 60
2529
FILTERS = "python.django.template.defaultfilters"
2630
STRIPTAGS = f"{FILTERS}.striptags"
2731
ADVISORY = Advisory(
@@ -207,3 +211,27 @@ def test_a_chain_lists_its_filters_in_the_order_of_a_walk_through_its_extends(tm
207211
)
208212

209213
assert described(root, "page.html") == [("striptags", "b.html", 1, 6, ("v",)), ("upper", "a.html", 1, 6, ("v",))]
214+
215+
216+
COUNT_FILTERS = (
217+
"import json, sys\n"
218+
"from pathlib import Path\n"
219+
"from coretrace_python.taint.templates import project_templates\n"
220+
"print(json.dumps({name: len(found) for name, found in project_templates(Path(sys.argv[1])).filters.items()}))\n"
221+
)
222+
223+
224+
def test_the_blocks_of_a_template_are_placed_in_linear_time(tmp_path: Path) -> None:
225+
# Looking up the blocks a placed block holds among all the template's blocks made
226+
# placing 50,000 blocks take minutes. Only the last one applies a filter.
227+
blocks = "".join(f"{{% block b{i} %}}{{% endblock %}}" for i in range(50_000))
228+
root = write(tmp_path, {"page.html": blocks + "{% block last %}{{ v|upper }}{% endblock %}"})
229+
try:
230+
completed = subprocess.run(
231+
[sys.executable, "-c", COUNT_FILTERS, str(root)], capture_output=True, text=True, check=False, timeout=TIMEOUT
232+
)
233+
except subprocess.TimeoutExpired:
234+
pytest.fail(f"placing the blocks did not end within {TIMEOUT} s")
235+
236+
assert completed.returncode == 0, completed.stderr
237+
assert json.loads(completed.stdout) == {"page.html": 1}

0 commit comments

Comments
 (0)