Repository navigation
fix(templates): find the positions in a template from a line index built once - #289
Merged
Merged
Conversation
The line index that places the values of JSON and TOML documents finds the start of each line once, then the line of an offset by bisection. It moves to source.manager, beside LINE_BREAK and lines_of, whose count of lines it follows, and is exported as LineIndex, so that a template's positions can be found with it.
…e its span A template's includes, extends and loads are read with the span they stand at, as its filters are, instead of their offset in its text. The unread entries take their line from that span, without the text. The positions are still counted by _position.
…ilt once Each position in a template was counted from its start: collecting the filters of a template applying n of them read it n times, in time quadratic in its size (#288). A template of 2 MB applying 160,000 filters on one line took 49 s, where Django compiles and renders it in 2.7 s. The line of each include and extends, and of each load the engine cannot resolve, was counted the same way. The start of each line of a template is now found once, when it is read, and the line of each position by bisection. The positions do not change: templates are read with universal newlines, so their text holds only line feeds, which both count alike.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #288.
Each position in a template was counted from its start.
_positionread the template up to the offset of each filter it applies, of each include and extends, and of each{% load %}the engine cannot resolve. A template applying n filters was therefore read n times, which is quadratic in its size: 160,000 filters on one line (2 MB) took 49 s to collect, where Django compiles and renders the template in 2.7 s.The start of each line of a template is now found once, when it is read, and the line of each position by bisection:
_Linesofsource/positions.py, moves tosource/manager.py, besideLINE_BREAKandlines_of, whose count of lines it follows. It becomesLineIndex, exported bycoretrace_python.source._read_scopesbuilds the template's index once. Its filters, and now its includes, extends and loads, get theirSourceSpanfrom it.project_templatestakes the line of an unread entry from that span._positionis removed.What does not change.
\n, whereLineIndexand_positioncount alike. The filters, their order and the unread entries, lines included, stay the same._read_scopeskeeps its signature.LineIndexis the only new public name. The Enterprise plugins use none of the internals that change.The design and its measurements are in the issue.
refactor(source): share the line index of JSON and TOML positionsLineIndexinsource/manager.py, exported;tests/test_line_index.pyrefactor(templates): give each include, extends and load of a template its span_positionfix(templates): find the positions in a template from a line index built onceLineIndexin_read_scopes,_positionremoved;tests/test_template_positions.pydocs(changelog): announce the positions found in linear timeCHANGELOG.md, under PerformanceResults
Same results. On 436 projects, this branch gives the filters, their order and the unread entries of
main, positions included: 11,648 filters and 3,132 unread entries.{% filter %}blocks, with\n,\r\nand lone\rline ends, and characters outside ASCII.Time.
project_templateson one template, on Python 3.12.3, an AMD Ryzen 9 9950X3D under Ubuntu 24.04 (WSL2), minimum of three runs:main{{ v|upper }}× 10,000, on one line (126 KB){% include v %}× 80,000, one per line{% load missing %}{{ v|upper }}× 80,000, one per line--check) of the 2 MB template: 3.26 s instead of 49.74 s.project_templatestakes 2.72 s on the 36 projects, as onmain(2.69 s).Tests
New files. No existing test changes.
tests/test_line_index.py: the line and column of every offset of a text broken by\n,\r\nand a lone\r, holding a character outside ASCII, and each offset back from its line and column.tests/test_template_positions.py: one template of 200,000 units, collected in a subprocess bounded at 60 s, as the time tests of templates: collecting filter calls takes time exponential in nested includes #279 and templates: the filter walk is exponential in a cycle of includes, and cuts a cycle Django renders through #284 do. Filters on one line, the last at line 1, column 2,599,993; includes named by an expression, then loads of a library not found, one per line, the last unread entry at line 200,000.Before the change.
main:tests/test_line_index.pycannot importLineIndex, and each case oftests/test_template_positions.pyis stopped at 60 s.Mutations. Each of these makes tests fail:
bisect_left, which puts an offset at a line start on the line before:tests/test_line_index.pyand 7 tests of JSON and TOML positions;\n: both tests oftests/test_line_index.py;tests/test_template_filter_calls.pyand the includes case;Verification
Python 3.12 (macOS), on
mainat07adc12:ruff check .andmypy: clean on each commit;main);1b80fce: 978 passed.