Skip to content

Share an operation budget across canvas preparation and writing (#500) - #578

Merged
yhay81 merged 1 commit into
mainfrom
claude/500-canvas-operation-budget
Nov 2, 2026
Merged

yhay81 merged 1 commit into
mainfrom
claude/500-canvas-operation-budget

Conversation

@yhay81

@yhay81 yhay81 commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Closes #500

Builds on #505 (_OperationBudget, SharedBudget, begin_canvas/abort_canvas) and #503. Of the codex/500-canvas-operation-budget draft (5f1add2), this ports the font operation= keyword and the idea of _Document::budget() returning the canvas operation. The rest was already merged through #505 and #503, or is written anew here.

What changed

  • A canvas is one operation. Canvas.__enter__ opens it with begin_canvas, which takes its limits from the document's DocumentLimits once. A discarded or refused canvas is abort_canvased: nothing is written and no undo step is recorded. end_transaction refuses to commit a cancelled or expired operation.
  • Reads and writes inside a canvas charge it.
    • _Document::budget() now returns this thread's innermost canvas operation when one is open, or a new one otherwise. So Objects reads, image encoding (pixmap_png), Page reads and writes, all on the owner thread, accumulate against the canvas.
    • Calls with max_* options go through _Document::call_budget. Inside a canvas an option can only tighten the operation (SharedBudget::tightened), and None keeps the operation's limit. Outside a canvas the behaviour is unchanged: the option replaces the limit, and None removes it.
  • Text.
    • _Font.encode, _Font.layout and _Font.shape take operation=. They charge the operation and observe its deadline and cancellation, and when the call ends they return the memory they charged.
    • The canvas operation leaves text_input_size and text_line_count to each call's max_text_size, as before. shape charges its index buffer before allocating it.
    • Canvas.text counts lines without splitting (_line_count, which matches str.splitlines exactly), checks the line limit, and charges the lines before splitlines(). Canvas.textbox charges the normalized copies before making them. Line splitting stays in Python.
  • Buffers and commit.
    • Every buffered content fragment goes through _append. It checks the canvas's running content size against content_input_size and charges memory_size (which polls) before appending.
    • An opacity group returns its buffer once its Form is written.
    • _commit checks the framed size and charges the joined copies before joining.
    • What the canvas charged is released before it ends, so a canvas nested in another operation does not leak into it.

Acceptance

  • One budget from the document limits per canvas. Validation, buffers, layout and encoding, generated content and writes use it, and input-sized allocations are charged first. Tests: test_a_canvas_charges_one_operation_from_the_document_limits, test_font_calls_observe_the_canvas_operation, test_text_layout_observes_the_operation_memory, test_the_line_limit_counts_before_splitting.
  • Charges accumulate within a canvas and do not leak across canvases. Low memory and element limits refuse with a typed LimitError, with no write and no undo step. Tests: test_charges_accumulate_*, test_draws_that_fit_alone_are_refused_together, test_a_low_memory_limit_refuses_* (path, text, textbox), test_the_element_limit_refuses_without_a_write, test_a_call_inside_a_canvas_tightens_the_operation.
  • The deadline and cancellation are observed during preparation and during writing: test_cancellation_stops_preparation, test_cancellation_stops_writing, test_the_deadline_stops_preparation_and_writing.
  • Content size is checked before buffering and before the join at commit: test_content_size_is_checked_before_buffering, test_content_size_is_checked_before_joining_at_commit, test_an_opacity_group_returns_its_buffer.
  • Regressions:
    • existing canvas path, image, text, embedded, source-budget and page-Form tests pass;
    • str.splitlines compatibility (CRLF, \v, \f, \x1c–\x1e, \x85, 
, 
, trailing breaks, empty lines) is tested, although splitting stays in Python.
  • python3 tools/check.py --locked passes locally (pytest, ruff, ruff format, ty, cargo fmt, clippy, cargo test). Current-head CI: see checks.

🤖 Generated with Claude Code

A canvas runs as one operation from its document's limits. Reads and
writes on the owner thread, image encoding, and font encode, layout and
shape charge it; call options can only tighten it. The canvas checks its
content size and charges its buffers before appending or joining them,
counts text lines before splitting, and aborts without a write or undo
step on refusal, cancellation, or deadline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yhay81
yhay81 merged commit b729a25 into main Nov 2, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Share an operation budget across canvas preparation and writing

1 participant