Repository navigation
Conversation
…ts header per key `key_value_rule` computed `abs_key_parent = header + key_parent` and then looked that path up from the document root twice for every key/value pair: `Flags.is_(abs_key_parent, FROZEN)` and `NestedDict.get_or_create_nest(abs_key_parent)`. Both walk the whole path they are given, so a section with a depth-D header and M keys was parsed in O(M * D) time: a 354 KB document with a 998-deep header and 40000 keys took 5.1 s to parse, while the same keys under a 1-deep header took 0.03 s. Resolve the section's position in the flags tree (`Flags.resolve()`) and its nest once, when the `[header]` or `[[header]]` line is parsed, and look keys up relative to that anchor. The dotted-key checks in the section are anchored in the same way. Error messages still report absolute keys. This is python/cpython#152930 (gh-152930). Signed-off-by: simpleqt <2015900818@qq.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #327 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 4 4
Lines 535 550 +15
Branches 100 103 +3
=========================================
+ Hits 535 550 +15 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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
A key/value pair used to walk the table header of the current section from the
document root twice, for every pair:
_parser.py:444:out.flags.is_(abs_key_parent, Flags.FROZEN)_parser.py:450:out.data.get_or_create_nest(abs_key_parent)with
abs_key_parent = header + key_parent(_parser.py:433).Flags.is_andNestedDict.get_or_create_nestboth walk the path they are given from theirrespective roots, so a section with a depth-
Dheader andMkeys is parsedin
O(M * D)time.MAX_KEY_PARTSboundsDat ~1000, but theO(1000 * M)amplification remains.This is gh-152930 — python/cpython#152930 — where a
CPython maintainer asked for the fix to be done in Tomli first.
Reproducer
354 KB of valid TOML on this machine (CPython 3.11.14, Windows):
master(43a86ad)The header length alone no longer matters: with the same 5000 keys, a depth-1
and a depth-998 header now parse in 0.0134 s and 0.0133 s, where before the
deep header was 45x slower (0.0124 s vs 0.5587 s).
Fix
Resolve the section once, when its
[header]/[[header]]line is parsed:Flags.resolve()returns the section's position in the flags tree andNestedDict.get_or_create_nest()is called for the header itself.loads()keeps the pair and passes it to
key_value_rule(), which then looks upFlags.is_(key, flag, start=header_flags)andget_or_create_nest(key, start=header_nest)relative to it.Anchored lookups see the same flags as lookups from the document root: only
FROZENis ever set recursively, a header can only be entered through thetable rules, which reject an already frozen header or one with a frozen
ancestor, and freezing always happens to keys that the current section header
is a proper prefix of. So neither the header nor its ancestors can become
frozen while the section is being parsed.
Tests
test_many_keys_under_deep_table_header— a deep header with many keysparses into the expected tree.
test_deep_table_header_not_rewalked_per_key— parses the same 5000-key bodyunder a depth-1 and a depth-998 header, alternating between the two and
keeping the fastest of five runs, then requires the ratio to be under 5.
Red on
43a86ad:Green with this change (ratio 0.99):
Ran 20 tests ... OK (skipped=1).A ratio of the two timings is asserted, and both documents are measured
interleaved, so the test does not depend on how fast or how loaded the machine
is; before the fix the ratio is the header depth.
Verification
python -m unittest: 20 tests, OK (1 skipped: the pure-Python 3.15+ lazyimport test).
case names in
tests/files-toml-1.1.0at96fd088(711 are valid UTF-8 andwere parsed; 9 are intentionally non-UTF-8 decoding tests): no valid case
rejected, no invalid case accepted — before and after the change.
43a86adfor 151,519 further documents:mutations of the corpus plus 104,976 exhaustive 4-statement combinations of
table headers, array-of-tables headers, dotted keys, inline tables and frozen
namespaces. Both the parsed value and, for rejected documents,
msg,pos,linenoandcolnowere compared.coverage run -m unittestwithtests/data/*/_externalremoved, like the CIcoverage job: 100% statements and 100% branches.
black26.10.0,isort9.0.2,flake87.4.1 (+ bugbear, builtins,comprehensions) and
mypy2.4.0 instrictmode: clean.Not run locally: the mypyc wheel build and the other Python versions in the CI
matrix.