Skip to content

fix: a run of many files no longer runs out of memory while it saves its manifest - #212

Merged
donislawdev merged 4 commits into
mainfrom
fix/run-memory-2026-11-02
Nov 3, 2026
Merged

donislawdev merged 4 commits into
mainfrom
fix/run-memory-2026-11-02

Conversation

@donislawdev

@donislawdev donislawdev commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

What

  • The manifest is written entry by entry, with the same bytes. Encode (internal/manifest/encode.go) takes the document with no entries and cuts it where the list of files is. It writes that head, then each entry, then the tail, through a buffered writer. This is the layout size.go already counts. A manifest with no entries is still encoded whole, so a list that was never made stays null.
  • The plan list is reserved once instead of grown file by file (engine.planCapacity). The reservation never goes past what the memory setting allows the plan. A run past the file ceiling gets no reservation, and the planning loop refuses it as before.

Why

Saving the manifest put the whole document together in memory, plus an indented copy, after the last file and while every entry was still held. Measured with a probe that splits a run into planning, writing the files and saving the manifest, at 100 000 text files:

before after
peak while saving the manifest 882 MB 351 MB, no more than writing the files
peak while planning 251 MB 127 MB
manifest length 84 170 700 B 84 170 700 B

In a Docker container limited to 1 GB, tfg generate --format txt --size 4kb --count 200000 was killed for lack of memory after writing its last file. It left 200 000 files and no manifest, so tfg cleanup had nothing to remove them by. With this change the same run exits 0 and writes its manifest.

Guards

  • TestTheManifestIsWrittenAsEncodingJSONWritesIt checks that the bytes from Encode equal what encoding/json produces for the same value: no entries, a list that was never made, one entry, many unlike entries, and two thousand entries with escaped text.
  • TestTheManifestIsNotHeldWholeToBeWritten fails if the document reaches its writer in one piece. Going back to json.NewEncoder turns it red while the bytes guard stays green.
  • TestAPlanIsReservedRatherThanGrown checks that a plan's capacity equals its length. Without the reservation, a plan of 1000 files had room for 1359.
  • Five mutation entries cover these guards. Capping the reservation at the memory setting has no guard: removing the cap only raises the peak memory, and the test process shares one heap with every other guard.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Manifest generation now writes entries incrementally while preserving the existing output format, reducing memory use for large manifests.
    • Planning now reserves capacity for expected files, helping large runs complete within memory limits.
    • A run processing 200,000 text files can now complete in a 1 GB container.

donislawdev and others added 2 commits November 2, 2026 19:07
Saving the manifest built the whole document in memory, and an indented
copy of it, while every entry was still held. At 100 000 text files the
save peaked at 9.3 KB a file against 4.0 KB for writing every file, and
a run of 200 000 in a container of 1 GB was killed after its last file
with no manifest written. Encode now writes the document with no entries
cut where its list is, and the entries between, one at a time. A guard
holds the bytes to encoding/json's own rendering and another fails when
the document reaches its writer in one piece.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Grown by append, a plan of a million text files took 1.9-2.2 GB from the
system for 0.94 GB of plan, because every doubling leaves the last array
to the collector. Reserved, it took 1.2 GB with five collections instead
of fourteen. The reservation never passes the memory setting's ceiling
for the plan and is not made for a run past the file ceiling, which the
loop refuses as before. A guard holds the plan's capacity to its length.

Mutation entries for this and for the manifest written an entry at a
time, and the changelog entry for both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI (base), Organization UI (inherited)
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 24133cdb-6f8f-432c-a4ba-3aa98c8ec75c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI (base), Organization UI (inherited)
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: e1e2a0d1-bdb6-4fac-aa10-4f6d36100cec
Commits

Reviewing files that changed from the base of the PR and between 4510c97 and 5947b5d.

Files selected for processing: 10
  • CHANGELOG.md
  • internal/engine/engine.go
  • internal/engine/planmemory.go
  • internal/guard/manifestreach_test.go
  • internal/guard/manifeststream_test.go
  • internal/guard/mutation/entries.py
  • internal/guard/planreserve_test.go
  • internal/manifest/encode.go
  • internal/manifest/manifest.go
  • internal/manifest/size.go
Files with no reviewable changes: 1
  • internal/manifest/manifest.go

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Recent review details
Context from checks skipped due to timeout: 12
  • GitHub Check: part 0
  • GitHub Check: CodeRabbit
  • GitHub Check: reference tools actually installed
  • GitHub Check: semgrep
  • GitHub Check: bill of materials
  • GitHub Check: test on ubuntu-latest
  • GitHub Check: coverage gate
  • GitHub Check: linters
  • GitHub Check: test on windows-latest
  • GitHub Check: test on macos-latest
  • GitHub Check: the installer installs and leaves
  • GitHub Check: the Chocolatey packages install and leave
Additional context used
Code guidelines: 1
CONTRIBUTING.md — configured
Path-based instructions: 14
Applies to text shown to the user (labels, buttons, tooltips, placeholders, dialogs, errors, status messages, empty states, translations).

CodeRabbit configuration file

Files:

  • internal/manifest/size.go
  • internal/guard/planreserve_test.go
  • internal/engine/planmemory.go
  • internal/engine/engine.go
  • internal/guard/mutation/entries.py
  • internal/guard/manifestreach_test.go
  • internal/guard/manifeststream_test.go
  • internal/manifest/encode.go
Verify tests check real behavior and would fail if the implementation were broken.

CodeRabbit configuration file

Files:

  • internal/guard/planreserve_test.go
  • internal/guard/manifestreach_test.go
  • internal/guard/manifeststream_test.go
These are end-user desktop applications.

CodeRabbit configuration file

Files:

  • internal/manifest/size.go
  • internal/guard/planreserve_test.go
  • internal/engine/planmemory.go
  • internal/engine/engine.go
  • internal/guard/mutation/entries.py
  • internal/guard/manifestreach_test.go
  • internal/guard/manifeststream_test.go
  • internal/manifest/encode.go
Performance is a known weak spot of these projects.

CodeRabbit configuration file

Files:

  • internal/manifest/size.go
  • internal/guard/planreserve_test.go
  • internal/engine/planmemory.go
  • internal/engine/engine.go
  • internal/guard/mutation/entries.py
  • internal/guard/manifestreach_test.go
  • internal/guard/manifeststream_test.go
  • internal/manifest/encode.go
Applies only to code that builds or styles a GUI.

CodeRabbit configuration file

Files:

  • internal/manifest/size.go
  • internal/guard/planreserve_test.go
  • internal/engine/planmemory.go
  • internal/engine/engine.go
  • internal/guard/mutation/entries.py
  • internal/guard/manifestreach_test.go
  • internal/guard/manifeststream_test.go
  • internal/manifest/encode.go
User-facing changelog.

CodeRabbit configuration file

Files:

  • CHANGELOG.md
Domain: test file generator (Go; `tfg` CLI and `tfg-gui` Fyne window over one engine).

CodeRabbit configuration file

Files:

  • internal/manifest/size.go
  • internal/guard/planreserve_test.go
  • internal/engine/planmemory.go
  • internal/engine/engine.go
  • internal/guard/mutation/entries.py
  • internal/guard/manifestreach_test.go
  • internal/guard/manifeststream_test.go
  • internal/manifest/encode.go
SECURITY, HIGH PRIORITY.

CodeRabbit configuration file

Files:

  • internal/manifest/size.go
  • internal/guard/planreserve_test.go
  • internal/engine/planmemory.go
  • internal/engine/engine.go
  • internal/guard/mutation/entries.py
  • internal/guard/manifestreach_test.go
  • internal/guard/manifeststream_test.go
  • internal/manifest/encode.go
These apps are QA/developer tools.

CodeRabbit configuration file

Files:

  • internal/manifest/size.go
  • internal/guard/planreserve_test.go
  • internal/engine/planmemory.go
  • internal/engine/engine.go
  • internal/guard/mutation/entries.py
  • internal/guard/manifestreach_test.go
  • internal/guard/manifeststream_test.go
  • internal/manifest/encode.go
Go code.

CodeRabbit configuration file

Files:

  • internal/manifest/size.go
  • internal/guard/planreserve_test.go
  • internal/engine/planmemory.go
  • internal/engine/engine.go
  • internal/guard/manifestreach_test.go
  • internal/guard/manifeststream_test.go
  • internal/manifest/encode.go
Check that documentation matches the actual code in this PR: commands, flags, config keys, file paths, build steps and examples must exist.

CodeRabbit configuration file

Files:

  • CHANGELOG.md
Python code.

CodeRabbit configuration file

Files:

  • internal/guard/mutation/entries.py
All code in this repository is written by an AI coding agent (Claude Code).

CodeRabbit configuration file

Files:

  • internal/manifest/size.go
  • internal/guard/planreserve_test.go
  • internal/engine/planmemory.go
  • CHANGELOG.md
  • internal/engine/engine.go
  • internal/guard/mutation/entries.py
  • internal/guard/manifestreach_test.go
  • internal/guard/manifeststream_test.go
  • internal/manifest/encode.go
Source excerpt: **Words a user reads are English, with a flat hyphen and no semicolons.**

CodeRabbit inference engine (CONTRIBUTING.md)

Files:

  • CHANGELOG.md
Learnings: 2
Common learnings
Learnt from: CR
Repo: donislawdev/TestingFilesGenerator PR: 0
File: CONTRIBUTING.md:0-0
Timestamp: 2026-10-20T18:12:35.072Z
Learning: Source excerpt:
# Contributing

## What a change has to satisfy

- **Every change in behaviour comes with a test that fails without it.** A green
  suite is not evidence on its own. The question to answer in the pull request
  is "which test would go red if this change were undone". Each test in
  `internal/guard` is also broken on purpose by an entry in
  `internal/guard/mutation/entries.py`, and the `mutation` check on a pull
  request runs the entries the change reaches. A new test needs an entry there,
  or a line in `internal/guard/mutationcoverage_test.go` saying why it has none.
Learning: 2026-11-02T16:20:03.196Z
Learnt from: CR
Repo: donislawdev/TestingFilesGenerator PR: 0
File: CONTRIBUTING.md:0-0
Timestamp: 2026-11-02T16:20:03.196Z
Learning: Applies to CHANGELOG.md : Source excerpt:
# Contributing

## What a change has to satisfy

- **Words a user reads are English, with a flat hyphen and no semicolons.** That
  covers the README, the changelog, `--help`, every message, and every comment
  in the code. A test enforces the punctuation.

Applied to files:

  • CHANGELOG.md
Walkthrough

Nonempty manifests are encoded entry by entry while preserving their JSON bytes. Planning reserves output-slice capacity based on target counts and the memory budget.

Changes: Manifest and planning memory usage

Layer / File(s) Summary
Stream manifest encoding
internal/manifest/encode.go, internal/manifest/manifest.go, internal/manifest/size.go, internal/guard/manifeststream_test.go, internal/guard/manifestreach_test.go, internal/guard/mutation/entries.py, CHANGELOG.md
Manifest.Encode writes nonempty manifests incrementally and retains whole-document encoding for empty manifests. Tests compare encoded bytes with indented JSON, check write sizes, and verify the manifest-size calculation.
Reserve planned-file capacity
internal/engine/planmemory.go, internal/engine/engine.go, internal/guard/planreserve_test.go, internal/guard/mutation/entries.py
planCapacity calculates reservation capacity from target sizes and the memory budget. PlanContext uses that capacity, and a test checks the resulting plan length and slice capacity.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Encode as Manifest.Encode
  participant Shell as shell
  participant Whole as encodeWhole
  participant JSON as json.Marshal
  participant Buffer as bufio.Writer
  participant Writer as io.Writer
  Caller->>Encode: Encode(writer)
  Encode->>Shell: Build empty-files document shell
  Shell->>Whole: Encode manifest copy with empty files
  Whole-->>Shell: Indented JSON bytes
  Shell-->>Encode: Document head and tail
  loop Each file entry
    Encode->>JSON: Marshal entry
    JSON-->>Encode: Entry bytes
    Encode->>Buffer: Write head or entry bytes
  end
  Encode->>Buffer: Write document tail and flush
  Buffer->>Writer: Write buffered bytes
  Writer-->>Buffer: Write result
  Buffer-->>Encode: Flush result
  Encode-->>Caller: Return encoding or write error
Loading

Possibly related PRs

Suggested labels: bug, performance

Merge Risk: Moderate · previous review 5947b

An invalid large run can consume substantial memory before reporting why it cannot proceed, and may exhaust a constrained container. Defer or bound the reservation before merging.

Pre-merge checks: Passed 11 · Failed 3

Failed Failed checks: 3 warnings

Check name Status Explanation Resolution
Tests For Changed Behavior Warning The PR adds tests for manifest byte preservation, streaming writes, and exact plan capacity under the normal budget. It does not test the new memory-bounded reservation branch in planCapacity (`plan… Add deterministic tests for planCapacity or an equivalent engine-level behavior. Cover a budget whose ceiling / sizeof(PlannedFile) is below the target count and assert that the initial plan capacity is capped. Also cover the zero-budge…
Desktop Robustness Warning The new non-empty Manifest.Encode path performs one marshal/write loop per manifest entry, but it accepts only io.Writer and has no progress or cancellation hook. In the GUI, startRun reports pr… Thread a context and manifest-save progress callback through SaveRecord, Reservation.Save, and Manifest.Encode. Check cancellation between entries and report entry/byte progress to the GUI. Preserve the temporary-file and publish sequ…
Clear User-Facing Text Warning The PR adds a user-visible defect message in internal/manifest/encode.go: a manifest with no entries holds %d lists of files one level in, not one. The CLI exposes this through `cannot write the m… Replace the defect text with: manifest encoding found %d lists for "files" instead of one; this is an internal error. Report it with the command output.
Passed Passed checks: 11 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly describes the user-visible fix: large runs no longer exhaust memory while saving the manifest. It is specific and within the length limit.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
No Secrets Or Debug Leftovers Passed The pull-request diff adds no CLAUDE.md, CLAUDE.local.md, AGENTS.md, .claude/, or .env files. Added lines contain no detected credentials, private keys, URLs, absolute user paths, IP addresses, person…
No Hardcoded Ui Styling Passed The pull request does not add or change GUI code. The authoritative diff contains only CHANGELOG.md and internal Go/Python engine, manifest, and test files. No XAML, Slint, Fyne, Tkinter, or WPF UI co…
No Obvious Performance Problems Passed No clear performance problem matches the check. Manifest.Encode emits each file entry through a 64 KiB buffered writer, so it avoids whole-document accumulation; each entry is processed once. `planC…
Safe File Parsing Passed No unsafe file parsing was introduced. The PR exports only manifest JSON through json.NewEncoder and per-entry json.MarshalIndent in Manifest.Encode; it does not parse XML/CSV/XLSX/YAML, evaluat…
System Changes Are Reversible Passed PASS: The pull request changes manifest JSON encoding, plan-slice capacity, tests, and changelog text. The reviewed diff contains no changes to network filters or rules, proxies, firewalls, system tim…
No Resource Leaks Passed No resource leak is introduced. Manifest.Encode uses a bounded 64 KiB bufio.Writer and temporary bytes.Buffer data; it does not own or retain the caller's writer. saveReserved closes the manif…
Scope, Duplication And Docs Passed The PR scope matches the title and description: it streams manifest entries and reserves plan capacity. The added guards and mutation cases support those two changes. `Manifest.Encode(io.Writer) error…
Full details: No Secrets Or Debug Leftovers

Explanation

The pull-request diff adds no CLAUDE.md, CLAUDE.local.md, AGENTS.md, .claude/, or .env files. Added lines contain no detected credentials, private keys, URLs, absolute user paths, IP addresses, personal email addresses, or debug output/calls. The fmt usage is limited to test formatting and Sprintf construction.

Full details: No Hardcoded Ui Styling

Explanation

The pull request does not add or change GUI code. The authoritative diff contains only CHANGELOG.md and internal Go/Python engine, manifest, and test files. No XAML, Slint, Fyne, Tkinter, or WPF UI code is changed, so the hardcoded UI styling condition does not apply.

Full details: No Obvious Performance Problems

Explanation

No clear performance problem matches the check. Manifest.Encode emits each file entry through a 64 KiB buffered writer, so it avoids whole-document accumulation; each entry is processed once. planCapacity makes one linear pass over target sizes and reserves the plan once, while the planning loop remains linear. No UI-thread I/O, per-item external call, or O(n²) collection processing was introduced.

Full details: Safe File Parsing

Explanation

No unsafe file parsing was introduced. The PR exports only manifest JSON through json.NewEncoder and per-entry json.MarshalIndent in Manifest.Encode; it does not parse XML/CSV/XLSX/YAML, evaluate formulas, resolve entities, deserialize arbitrary types, or add path handling. JSON string escaping remains provided by encoding/json, and the diff preserves existing manifest path validation.

Full details: System Changes Are Reversible

Explanation

PASS: The pull request changes manifest JSON encoding, plan-slice capacity, tests, and changelog text. The reviewed diff contains no changes to network filters or rules, proxies, firewalls, system time, process hooking or injection, Windows services, registry, or drivers. Reversibility safeguards are therefore not applicable.

Full details: No Resource Leaks

Explanation

No resource leak is introduced. Manifest.Encode uses a bounded 64 KiB bufio.Writer and temporary bytes.Buffer data; it does not own or retain the caller's writer. saveReserved closes the manifest file through core.Finish on encode, sync, and close-error paths. planCapacity creates one bounded plan slice, and failed planning releases it with the local context. No handlers, timers, goroutines, processes, or unbounded caches were added.

Full details: Scope, Duplication And Docs

Explanation

The PR scope matches the title and description: it streams manifest entries and reserves plan capacity. The added guards and mutation cases support those two changes. Manifest.Encode(io.Writer) error keeps the same signature, and the manifest format remains unchanged through byte-for-byte tests against encoding/json; no CLI, configuration, build, or requirement changes were introduced. CHANGELOG.md documents the user-visible memory fix. No duplicate implementation or bypassed architecture layer was found.

Full details: Tests For Changed Behavior

Explanation

The PR adds tests for manifest byte preservation, streaming writes, and exact plan capacity under the normal budget. It does not test the new memory-bounded reservation branch in planCapacity (planmemory.go:38-40). TestAPlanIsReservedRatherThanGrown plans only 1,000 files with default settings, so it exercises the uncapped return n path. Existing memory tests check refusal, not the reserved capacity. The PR description also explicitly states that the memory-setting cap has no guard. No test assertions were deleted or weakened; the existing manifest-size test was refactored with its assertion preserved.

Resolution

Add deterministic tests for planCapacity or an equivalent engine-level behavior. Cover a budget whose ceiling / sizeof(PlannedFile) is below the target count and assert that the initial plan capacity is capped. Also cover the zero-budget and over-MaxFilesPerRun paths if those branches remain part of the implementation.

Full details: Desktop Robustness

Explanation

The new non-empty Manifest.Encode path performs one marshal/write loop per manifest entry, but it accepts only io.Writer and has no progress or cancellation hook. In the GUI, startRun reports progress only from engine.Run; it then calls saveRecord/engine.SaveRecord, which runs this new loop. Closing the window cancels the run context, but Encode never observes it, so a 200,000-entry save can continue without feedback while Stop waits. The reservation still writes to a temporary file, syncs it, and publishes it atomically.

Resolution

Thread a context and manifest-save progress callback through SaveRecord, Reservation.Save, and Manifest.Encode. Check cancellation between entries and report entry/byte progress to the GUI. Preserve the temporary-file and publish sequence. If cancellation occurs after files exist, either finish and publish the manifest before returning or retain a valid partial-run manifest; do not leave written files without their record.

Full details: Clear User-Facing Text

Explanation

The PR adds a user-visible defect message in internal/manifest/encode.go: a manifest with no entries holds %d lists of files one level in, not one. The CLI exposes this through cannot write the manifest or cannot render the manifest. It describes the internal state but gives the user no action. The changelog text is otherwise clear.

Resolution

Replace the defect text with: manifest encoding found %d lists for "files" instead of one; this is an internal error. Report it with the command output.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@coderabbitai coderabbitai Bot added bug Something isn't working performance labels Nov 2, 2026
Comment thread internal/engine/engine.go Resolved
donislawdev and others added 2 commits November 2, 2026 22:31
Reserved before the first target, a million files asking for a size
their format cannot make were refused at the first file after taking
441-450 MB, against 27-30 MB before. The size of the reservation is
still worked out before the targets are walked, and the list is now
taken when the first file has passed every check, with Ctrl+C asked
just before. That refusal is back to 26-28 MB, and a plan of a million
text files still peaks at 1.2 GB rather than 2.

A guard holds what planning allocates when the first file is refused.
A mutation entry for the context check is narrowed to its own place,
since the new check made its pattern appear twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…loop

The block that takes the list at the first file was nested three deep
inside the loop over files, which made it the fiftieth function at that
depth against a ceiling of 49. The ceiling is a ratchet, so the block
moved into startList instead of the number moving up. Behaviour is the
same: the list is taken once, at the first file that passed its checks,
with Ctrl+C asked first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@donislawdev
donislawdev merged commit 2c1785f into main Nov 3, 2026
26 checks passed
@donislawdev
donislawdev deleted the fix/run-memory-2026-11-02 branch November 3, 2026 07:22
donislawdev added a commit that referenced this pull request Nov 3, 2026
Both sides held #212's commits. The one conflict was in the mutation
entries, where this branch had rewritten an entry #212 added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant