Deeper parallel execution plan¶
Note
Historical design record. Written between 7 and 12 October 2026, while the work was under way. The scheduling, bounded page and file workers and declared independent documents it describes are implemented, and the tables of starting seams describe the code before that work. It is kept for its rationale and acceptance criteria and is not updated with the code. For shipped behavior, read the workflow reference and the bounded benchmark.
Extend the existing local scheduler first to overlap independent PDF work, then to compare pages and format files within one shared resource budget. Keep the baseline, transformation, prepared-build, and archive-rebuild gates ordered. Declared independent documents now use the same shared budget, each with its own workspace and verified source package. Publication waits for every document.
The scheduling and declared-document delivery steps below are implemented: shared admission and
runtime accounting, dependency-driven single-document PDF work, and bounded
page/file workers. Whole-job deadlines, immutable PDF inputs, coordinator-only
mutation and cancellation draining are included. The
bounded benchmark records measured results and
limitations; default CPU/build/render limits remain 2/1/1. A shared parsed-PDF
evidence cache and broader performance corpora remain future work. The independent
document release check is PKG301; scheduling itself adds no check codes.
This document retains the design rationale and acceptance criteria. It refines the
architecture and
requirements; the
workflow reference remains the description of shipped behavior. No distributed
queue, web server, network lookup, new PDF parser, or additional tool installation
is needed for the steps below.
Starting seams and constraints¶
The following table records the implementation before these changes. Current behavior is described above and in the workflow reference; these are the issues the plan addressed.
Code seam |
Observed behavior |
Consequence for parallel execution |
|---|---|---|
|
Schedules baseline compilation, source analysis, bibliography checks and configured manuscript checks; awaits the whole group before PDF work |
PDF checks cannot begin as soon as the baseline alone finishes |
|
Validate dependency cycles and CPU/memory admission; distinguish successful prerequisites ( |
Reuse these semantics; add tool-class capacity and explicit artifact success rather than replacing the scheduler |
|
Default |
Reservations are declarations, not measured peaks; they need calibration before larger fan-out |
|
Uses a fresh project/output tree, runs ordered |
A normal coroutine return currently counts as scheduler success even when no usable build artifact exists |
|
Runs PDF properties, geometry and inventories sequentially |
Independent inventory tools can overlap after valid input/page preflight |
|
Prepares each side sequentially, then renders each page and side sequentially |
Page pairs offer bounded independent work, but current |
|
Processes files serially; each file has two ordered formatter passes; returns proposed contents |
Parallelize file-level computation while retaining both passes and coordinator-only application |
|
Holds mutable |
Parallel workers need stable per-job tool state and the same epoch, not independently created timestamps |
|
Check a shared version dictionary before awaiting a probe; findings retain the same dictionary object |
Concurrent probes can duplicate work, and earlier findings can change when later tools are recorded |
|
Kills a process group on timeout/cancellation and samples group RSS, process count and workspace size |
Preserve this boundary; individual command limits alone do not bound the aggregate of concurrent commands and Python buffers |
|
Source, bibliography and manuscript parsing run in worker threads against the imported snapshot |
Cancelling an await does not stop its running thread; temporary-directory cleanup must wait for readers to stop |
|
Findings are sorted, but stages arrive in completion order and the finding key omits stage/document/page |
Define deterministic result ordering before adding more tasks |
These are code-derived constraints, not measured performance findings. Existing scheduler and workflow tests establish bounded overlap, basic failure propagation and controlled serial equivalence. They do not establish end-to-end speedup, aggregate runtime memory enforcement, parallel rendering, or formatter isolation.
Task graph and required gates¶
Use stable task identifiers derived from stage, document, operation and, where applicable, relative file path or numeric page. Display descriptive task names. Do not derive identity or output paths from task start/completion order.
flowchart TD
input["Import and freeze inputs"] --> source["Source analysis"]
input --> bibliography["Bibliography analysis"]
input --> baseline["Baseline build"]
baseline --> preflight["Baseline PDF preflight"]
preflight --> inspect["Independent PDF inventories"]
preflight --> reference["Supplied-PDF comparison, when requested"]
source --> plan["Reconcile evidence and resolve change plan"]
bibliography --> plan
inspect --> plan
reference --> plan
plan --> changes["Apply ordered project transformations"]
changes --> format["Compute independent formatting patches"]
format --> freeze["Apply patches and freeze prepared sources"]
freeze --> prepared["Prepared build"]
prepared --> preserve["Compare baseline and prepared PDF"]
preserve --> archive["Write exact archive and extract afresh"]
archive --> final["Archive build"]
final --> finalInspect["Final PDF inspection"]
final --> finalCompare["Prepared and archive comparison"]
final --> finalReference["Supplied-PDF and archive comparison"]
finalInspect --> release["Collect required results and publish"]
finalCompare --> release
finalReference --> release
The diagram shows successful artifact flow. Diagnostic collection depends on completion and still runs after failed or blocked nodes. An omitted optional branch has an explicit not-requested state and cannot become a missing required dependency. A supplied-PDF comparison remains required when requested, matching the current CLI behavior.
Implement the graph in this order:
Add baseline PDF inspection and the optional baseline/reference comparison as consumers of a successful baseline artifact in the initial graph. Source and bibliography/manuscript checks continue independently. Planning waits for all applicable evidence, including observed dependencies; no cleanup or rewrite runs early.
After the archive build, run final inspection, prepared/archive comparison and the optional reference/archive comparison concurrently. Publication waits for all required checks, not the first successful branch.
Split inspection into preflight and independent font, text, attachment and image inventory operations. Reuse one validated properties/geometry result for the relevant immutable PDF. A new checker should consume this evidence when compatible instead of invoking the same parser or tool again.
Split each comparison into side preflight/text extraction, ordered comparison assembly, bounded page-pair work, and a final collector. Geometry validation precedes rendering; compare only common pages and report missing pages explicitly. Preserve the current 144 DPI, exact RGB comparison, page/dimension caps and text normalization. Concurrency must not weaken comparison policy.
The outer workflow can retain explicit phase boundaries and several scheduler calls. Within a phase, downstream tasks must start when their own prerequisites finish. Page count determines a later bounded set of leaf operations; do not require a general dynamic workflow engine to express it.
Never overlap a build or archive reader with mutations to its input tree. Keep
passes within one latexmk build ordered. Dry runs still stop before prepared and
archive verification. Scoped review decisions are supplied in configuration;
a later interactive approval flow must hold no execution capacity while waiting.
One resource budget for every expensive operation¶
Introduce one per-job admission controller in scheduler.py, shared by phase
schedulers and adapter helpers. Reserve CPU units, memory and optional build or
render slots atomically; release them in finally after children have exited and
owned buffers have been released. Reject requests larger than any configured
total before launching work. No adapter creates a separate unconstrained pool.
A leaf operation owns one lease. A composite PDF/formatting task that merely
coordinates leaves owns no CPU or tool lease while waiting for them. A build leaf
can hold its lease for copy, ordered tool execution and artifact validation; its
ToolRunner calls use that lease instead of reserving it again. This avoids a
parent holding all capacity while its children wait for that same capacity.
Budget |
Proposed behavior |
|---|---|
CPU ( |
Retain |
Builds ( |
Add an explicit cap, initially one. It becomes useful when multiple declared roots/packages are supported; it does not parallelize passes inside one build. |
Rendering ( |
Add an explicit cap, initially one until measured. A page-pair leaf renders its two sides sequentially, so one lease means at most one active renderer; different page pairs can overlap when enabled. |
Memory ( |
Count parent/parser/buffer reservations plus command-group limits. A command’s configured RSS cap cannot exceed the lease backing it. Report estimates separately from sampled observations. |
Per-operation limits |
Preserve wall time, captured-output, process-count, file-size and workspace limits. Add an explicit overall job deadline so hundreds of individually bounded pages cannot extend execution indefinitely. Queue wait counts toward the job deadline, not the command’s execution timeout. |
Disk and queued artifacts |
Retain existing per-workspace limits and enforce a job-wide temporary-byte ceiling. Bound active/ready raster buffers; do not retain every page image in memory or fill the disk before collecting results. |
At the current maximum of 16 million pixels, two RGB rasters contain about
96 million bytes before Python object overhead. _read_raster also copies slices
while decoding, and Poppler needs additional memory. A 64 MiB generic task
reservation is therefore not an adequate page-pair budget. Calculate raster
storage from validated dimensions, add measured decoding/tool headroom, and admit
only as many page pairs as fit. Stream or release each completed pair after its
small comparison result is recorded.
Replace one full ps scan per active tool with one job monitor when concurrency
grows. Track registered process groups, the coordinating process and any parser
workers without double counting; retain per-command failures and add an explicit
job-budget failure. Measure monitor overhead. Sampling remains a reactive bound
with an overshoot interval, not an operating-system hard RSS guarantee. Keep
single-thread environment settings for supported subprocess libraries; record
and budget tools that still create their own workers.
Formatting and parser execution¶
Refactor format_project into project enumeration, one per-file formatter worker,
and deterministic result assembly. Each worker reads one frozen source file and
writes only its own workspace. The first pass must finish before its second
idempotence pass. Return relative path, original bytes or an equivalent local
precondition, proposed bytes, diff and findings. The coordinator verifies that
each target still matches the expected input, rejects duplicate/overlapping
targets and applies all accepted results in sorted order after required workers
finish. Any required failure prevents partial patch application.
Keep cleanup, DOI normalization, flattening and reviewed cross-file citation
changes in their existing ordered transformation chain. Schedule formatting on
the resulting frozen paths, then apply patches with _apply_contents. Readers
start only after this coordinator step. Formatter-directive removal, if later
implemented, follows formatting. No formatter worker renames files or updates
cross-file references.
Keep tool orchestration in asyncio and use the existing isolated subprocesses;
wrapping Poppler or tex-fmt in Python process pools adds no useful computation
parallelism. Initially retain bounded threads for source/bibliography analysis
and file I/O. More Python threads do not establish CPU scaling for Python parsing.
Share immutable parse results between rules before adding parser workers.
Make thread cancellation cooperative at bounded file/record boundaries and keep the snapshot alive until active readers stop. Preserve the underlying worker future so cancelling its asyncio waiter cannot lose lifecycle ownership. If profiling shows a significant CPU-bound parser bottleneck, evaluate a small spawn-based process worker design with serializable immutable inputs/results, memory accounting and explicit stop/join behavior. A Python 3.11 process-pool future alone cannot terminate already running parsing. Do not introduce that complexity without a measured benefit or a required hard cancellation boundary.
Result, failure and cancellation semantics¶
Add an explicit adapter-to-scheduler outcome contract. In particular, preserve
the diagnostics in BuildResult(success=False) while marking its artifact
producer unsuccessful; dependent PDF tasks must never run merely because the
coroutine returned normally. Keep execution success separate from check success:
a completed comparison that detects changed pages has valid diagnostic output,
but its failed preservation finding blocks the release gate.
The coordinator alone mutates Report, applies changes and publishes output.
Workers return values and structured events. Sort final findings by stable
document, stage, path, page, line, registered code where applicable, descriptive
rule name and a deterministic tie-breaker. Sort changes, tool metadata and final
stage records independently of completion order; keep live event ordering and
timings separate. Do not turn ordinary inventories or progress events into coded
checks. Registered diagnostic codes retain their descriptive rule names and
existing severity/status/evidence semantics.
Failures block only consumers that need the unsuccessful artifact. Independent analysis and diagnostic assembly continue. Required missing tools, timeouts, resource violations and incomplete checks block publication; optional unavailable work stays visible without erasing other findings. On the first cancellation, stop admission, mark queued work cancelled, stop active tool process groups, signal parser workers, await cleanup, and release leases exactly once. Preserve completed findings and explicit incomplete task states. Repeated cancellation must not interrupt mandatory child reaping or allow temporary-directory cleanup while a worker still owns it. Never publish an incomplete package.
Delivery steps and acceptance checks¶
Step |
Concrete files to change |
Required evidence before continuing |
|---|---|---|
Establish safe scheduling contracts |
|
Atomic shared admission, unsatisfiable request rejection, no nested-lease deadlock, failed build artifact propagation, stable tool context, cancellation cleanup and honest memory-accounting status |
Extend the single-document graph |
|
PDF work begins after its build without waiting for unrelated analysis; independent final branches overlap; release still waits for every required branch |
Add bounded page and file workers |
|
Unique outputs/temp state, bounded memory/disk, ordered formatter passes, no partial mutation, serial/concurrent result equivalence and actual sandbox isolation |
Measure and tune |
|
Recorded end-to-end and stage timings, observed resource peaks, unchanged findings/prepared contents, supported conclusions or an explicit no-benefit result |
Support declared independent documents (implemented) |
|
Explicit file selections, isolated workspaces and independently rebuilt exact packages; no package is published if another fails; shared budgets and deterministic collection |
Each registered diagnostic code needs a meaningful behavioral unit test in its owning component, not just a registry-membership assertion. Exercise the triggering fixture and assert the code plus relevant status, severity, location and evidence; include valid or ambiguous controls where applicable. New scheduler/runtime codes need concrete failure or cancellation fixtures. For concurrency-sensitive paths, run controlled fixtures serially and concurrently and compare the same coded diagnostic behavior, including inconclusive states. Pure inventory/progress messages do not count toward this coverage requirement.
Use event/barrier-controlled tool doubles, not timing guesses, to test overlap and ordering. Exercise page results completed out of order, equal job names in separate workspaces, one missing renderer, failed build values, interrupted tool version probes, first/second formatter-pass failures, resource exhaustion and cancellation with queued plus active work. Assert no reader sees partially applied transformations and no blocked/cancelled job releases output. For fixed prepared inputs, serial and concurrent packaging must produce identical ZIP bytes; findings and changes must match apart from documented runtime fields.
Run the existing unit suite plus changed-file lint, formatting and type checks.
Keep Click/Rich CLI tests isolated with CliRunner or captured stdout/stderr;
verify exit codes and JSON-only stdout separately from Rich progress on stderr.
Do not run concurrent CliRunner invocations in the same process, where their
temporary stream/environment changes would share state.
Real sandbox/build tests are a separate gate: use an environment where macOS
sandbox-exec can start, preserve unsupported-platform failures, and label absent
tools or sandbox refusal as skipped/blocked rather than a pass. No automatic
installation or unsandboxed fallback is part of this plan.
Benchmark design and decision rules¶
Start with a declared bounded corpus: a small single-file article; a multi-file project; a many-page/figure-heavy PDF; a bibliography-heavy project; and many independent formatting files. Include mixed page dimensions and resource-limit fixtures. Use the existing nested-paper example as a smoke fixture, not as a representative performance corpus. Test multiple documents/packages only after that product capability exists.
Compare jobs=1 with explicit bounded settings such as two and four CPU units,
keeping build/render caps, input bytes, TeX engine, tool versions, source-date
epoch, comparison policy and transformations controlled and reported. Change one
parallelization step at a time. First run one pilot per corpus/settings pair;
estimate and approve a finite local benchmark time budget before larger repeats.
Within that budget, use at least three measured repetitions per selected pair,
alternate execution order, and record warm/cold cache conditions. Do not purge
user caches. Report individual timings and medians/ranges; small samples do not
support latency guarantees.
Record end-to-end time, phase/task execution and queue wait, time to first useful finding, command counts, tool-probe counts, active CPU/build/render reservations, sampled aggregate/command/parent memory peaks, temporary disk high-water mark, monitor overhead and cancellation latency. Mark unavailable metrics explicitly. Compare findings, outcome, prepared contents and deterministic archive bytes alongside timing. Separate tool-double orchestration results from real tool runs.
The hypotheses are that independent PDF inventories reduce waiting, page-pair work helps long comparisons, and per-file formatting helps larger projects. Process startup, copying, memory pressure or the ordered build chain may dominate instead. Retain serial mode and current defaults unless measured improvements justify a change without correctness, resource or cancellation regressions. Remove or defer complexity whose benefit is absent on the declared corpus; do not generalize results to other toolchains, document types or machines.