Skip to content
Merged

Perf #218

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- `promote_inline` and `promote_array` now preserve entry key and value
formatting

## [2.0.2] - 2026-07-10

### Fixed
Expand Down
73 changes: 43 additions & 30 deletions benchmarks/bench_mutate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

"""Manipulation-throughput benchmark.

Times common edit workflows over a parsed document — append AoT
entry, deep set, scalar update, structural replace — followed by
re-render. The aim is to surface regressions in the mutation path,
which the parse-only benchmarks do not exercise.
Times common edits over freshly parsed documents, with setup outside the
timed region. Parse/render workflows are labelled explicitly. The aim is to
surface regressions in the mutation path, which the parse-only benchmarks do
not exercise.

Usage:

Expand Down Expand Up @@ -105,10 +105,8 @@ def _bench_with_setup(
) -> None:
"""Like :func:`_bench`, but ``setup()`` runs *outside* the timer.

Used where the closure under test needs a freshly parsed document
each iteration (mutation is not idempotent) but a large trailing
document body would otherwise let one-time parse cost swamp the
mutation cost being measured.
Used where the closure under test needs fresh state each iteration
because mutation is not idempotent.
"""
timings: list[float] = []
for _ in range(repeats):
Expand Down Expand Up @@ -152,12 +150,10 @@ def update_scalar() -> None:
doc["project"]["version"] = "9.9.9"
tomlrt.dumps(doc)

def append_aot_entry() -> None:
doc = tomlrt.loads(aot_src)
def append_aot_entry(doc: Document) -> None:
aot = doc.aot("items")
for i in range(50):
aot.append({"name": f"new-{i}", "value": 1000 + i})
tomlrt.dumps(doc)

def append_non_tail_aot_entry(doc: Document) -> None:
aot = doc.aot("items")
Expand All @@ -175,34 +171,26 @@ def promote_large_inline_array(doc: Document) -> None:
def overwrite_section_inside_aot_entry(doc: Document) -> None:
doc.aot("a")[0].table("b")["c"] = 5

def deep_set_new_section() -> None:
doc = tomlrt.loads(section_src)
def deep_set_new_section(doc: Document) -> None:
for i in range(20):
doc.install(("new", f"s{i}"), tomlrt.Table.section({"x": i}))
tomlrt.dumps(doc)

def bulk_kv_update() -> None:
doc = tomlrt.loads(section_src)
def bulk_kv_update(doc: Document) -> None:
for s in range(50):
sec = doc.table(f"s{s}")
for k in range(20):
sec[f"k{k}"] = k * 10
tomlrt.dumps(doc)

def build_section_new_keys() -> None:
doc = tomlrt.loads("[t]\n")
def build_section_new_keys(doc: Document) -> None:
sec = doc["t"]
for i in range(1000):
sec[f"k{i}"] = i
tomlrt.dumps(doc)

def delete_kvs() -> None:
doc = tomlrt.loads(section_src)
def delete_kvs(doc: Document) -> None:
for s in range(50):
sec = doc.table(f"s{s}")
del sec["k0"]
del sec["k1"]
tomlrt.dumps(doc)

def render_only() -> None:
tomlrt.dumps(doc_pyproject)
Expand All @@ -211,8 +199,13 @@ def render_only() -> None:

_bench("parse + render: pyproject.toml", parse_and_render_pyproject, repeats=500)
_bench("render only: pyproject.toml", render_only, repeats=2000)
_bench("update scalar in pyproject", update_scalar, repeats=500)
_bench("append 50 AoT entries (base 500)", append_aot_entry, repeats=200)
_bench("parse + update + render: pyproject", update_scalar, repeats=500)
_bench_with_setup(
"append 50 AoT entries (base 500)",
lambda: tomlrt.loads(aot_src),
append_aot_entry,
repeats=200,
)
_bench_with_setup(
"append 20 entries, non-tail AoT (20k trailing)",
lambda: tomlrt.loads(aot_trailing_src),
Expand All @@ -226,7 +219,7 @@ def render_only() -> None:
repeats=50,
)
_bench_with_setup(
"promote inline table with 1k fields",
"promote array: one table with 1k fields",
lambda: tomlrt.loads(large_inline_array_src),
promote_large_inline_array,
repeats=100,
Expand All @@ -237,10 +230,30 @@ def render_only() -> None:
overwrite_section_inside_aot_entry,
repeats=50,
)
_bench("install 20 new sections", deep_set_new_section, repeats=200)
_bench("bulk update 1000 KVs", bulk_kv_update, repeats=100)
_bench("build section with 1000 new keys", build_section_new_keys, repeats=100)
_bench("delete 100 KVs", delete_kvs, repeats=200)
_bench_with_setup(
"install 20 new sections (base 1k KVs)",
lambda: tomlrt.loads(section_src),
deep_set_new_section,
repeats=200,
)
_bench_with_setup(
"bulk update 1000 KVs",
lambda: tomlrt.loads(section_src),
bulk_kv_update,
repeats=100,
)
_bench_with_setup(
"build section with 1000 new keys",
lambda: tomlrt.loads("[t]\n"),
build_section_new_keys,
repeats=100,
)
_bench_with_setup(
"delete 100 KVs (base 1k KVs)",
lambda: tomlrt.loads(section_src),
delete_kvs,
repeats=200,
)


if __name__ == "__main__":
Expand Down
21 changes: 17 additions & 4 deletions src/tomlrt/_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -1054,13 +1054,16 @@ def promote_inline(self, key: str) -> Table:
f"comments that would be lost"
)
raise TOMLError(msg)
value = cur._value # noqa: SLF001
assert isinstance(value, InlineTableValue)
entries = _layout_ops.prepare_promoted_inline_entries(value.items)
# Transfer the existing KV slot's leading + eol to the header.
saved_leading, saved_eol = _direct_kv_trivia(self, key)
snapshot = cur.to_dict()
_layout_ops.delete_key(self, key)
self[key] = Table.section(snapshot)
self[key] = Table.section()
result = dict.__getitem__(self, key)
assert isinstance(result, Table)
_layout_ops.populate_promoted_inline_entries(result, entries)
new_header = result._header_ref.slot if result._header_ref else None # noqa: SLF001
if isinstance(new_header, StructuralHeaderSlot):
if saved_leading is not None:
Expand Down Expand Up @@ -1110,13 +1113,23 @@ def promote_array(self, key: str) -> AoT:
f"comments that would be lost"
)
raise TOMLError(msg)
snapshot = cur.to_list()
value = cur._value # noqa: SLF001
assert isinstance(value, ArrayValue)
entries: list[list[tuple[InlineTableEntry, Value]]] = []
for item in value.items:
assert isinstance(item.value, InlineTableValue)
entries.append(
_layout_ops.prepare_promoted_inline_entries(item.value.items)
)
# Carry the original KV slot's leading/eol onto the promoted AoT.
saved_leading, saved_eol = _direct_kv_trivia(self, key)
_layout_ops.delete_key(self, key)
self[key] = AoT(snapshot)
self[key] = AoT()
result = dict.__getitem__(self, key)
assert isinstance(result, AoT)
for body in entries:
entry = _layout_ops.add_aot_entry(result, None)
_layout_ops.populate_promoted_inline_entries(entry, body)
# Apply saved leading to the first entry header and saved eol to
# the last entry's last slot.
if saved_leading is not None and len(result) > 0:
Expand Down
Loading