Skip to content

refactor(snapshots): reduce per-file heap allocations during snapshot (minor perf improvement) - #5571

Open
jkowalski wants to merge 1 commit into
kopia:masterfrom
jkowalski:reduce-per-file-allocs
Open

refactor(snapshots): reduce per-file heap allocations during snapshot (minor perf improvement)#5571
jkowalski wants to merge 1 commit into
kopia:masterfrom
jkowalski:reduce-per-file-allocs

Conversation

@jkowalski

Copy link
Copy Markdown
Contributor

Snapshotting a directory tree performs a small number of heap allocations for every single file, independent of its size. This change removes two of those per-file allocations. They are individually tiny, but they scale linearly with the number of files, so on large trees they add up to a measurable reduction in total allocation count.

The two changes:

  1. localfs: use DirEntry.Info() instead of os.Lstat(prefix + name)

    While iterating a directory, toDirEntryOrNil() called
    os.Lstat(prefix + n) for every entry, which required building the
    full path string on each iteration. DirEntry.Info() returns the same
    information (it is an lstat of the entry) without the caller having to
    construct the path, and it also correctly handles the case where the
    entry is removed between ReadDir() and the stat. This removes one
    string concatenation per directory entry.

  2. upload: use the bare filename as the object writer description

    The uploader built the object writer description as "FILE:" + name
    (and "SYMLINK:" / "STREAMFILE:" for the other types), allocating a
    new concatenated string per file. The description is only used in
    error and log messages, where the bare path is already clear and the
    surrounding error context identifies the file. Passing the name
    directly removes three more concatenations.

Measured impact

Benchmarked by snapshotting two datasets with otherwise-identical baseline and patched binaries, reading runtime.MemStats around the upload:

  • The Kopia source tree (~34,500 files): allocation count dropped by ~34,000 mallocs (~1.6%), about one fewer allocation per file.
  • A synthetic tree of 1,000,000 small files: allocation count dropped by ~950,000 mallocs (~2.0%), again about one fewer allocation per file.

Total bytes allocated and peak heap are effectively unchanged: these were small, short-lived objects, so removing them reduces GC work (fewer objects to scan and collect) but not the overall memory footprint, which is dominated by content hashing, compression, and the repository index.

The changes are behavior-preserving; the existing test suites for fs/localfs and snapshot/upload pass.

Snapshotting a directory tree performs a small number of heap
allocations for every single file, independent of its size. This change
removes two of those per-file allocations. They are individually tiny,
but they scale linearly with the number of files, so on large trees they
add up to a measurable reduction in total allocation count.

The two changes:

1. localfs: use DirEntry.Info() instead of os.Lstat(prefix + name)

   While iterating a directory, toDirEntryOrNil() called
   os.Lstat(prefix + n) for every entry, which required building the
   full path string on each iteration. DirEntry.Info() returns the same
   information (it is an lstat of the entry) without the caller having to
   construct the path, and it also correctly handles the case where the
   entry is removed between ReadDir() and the stat. This removes one
   string concatenation per directory entry.

2. upload: use the bare filename as the object writer description

   The uploader built the object writer description as "FILE:" + name
   (and "SYMLINK:" / "STREAMFILE:" for the other types), allocating a
   new concatenated string per file. The description is only used in
   error and log messages, where the bare path is already clear and the
   surrounding error context identifies the file. Passing the name
   directly removes three more concatenations.

Measured impact

Benchmarked by snapshotting two datasets with otherwise-identical
baseline and patched binaries, reading runtime.MemStats around the
upload:

  - The Kopia source tree (~34,500 files): allocation count dropped by
    ~34,000 mallocs (~1.6%), about one fewer allocation per file.
  - A synthetic tree of 1,000,000 small files: allocation count dropped
    by ~950,000 mallocs (~2.0%), again about one fewer allocation per
    file.

Total bytes allocated and peak heap are effectively unchanged: these were
small, short-lived objects, so removing them reduces GC work (fewer
objects to scan and collect) but not the overall memory footprint, which
is dominated by content hashing, compression, and the repository index.

The changes are behavior-preserving; the existing test suites for
fs/localfs and snapshot/upload pass.
@jkowalski
jkowalski requested review from a team and a balanced review from Copilot August 18, 2026 01:05
@jkowalski jkowalski changed the title performance(upload): reduce per-file heap allocations during snapshot refactor(upload): reduce per-file heap allocations during snapshot Aug 18, 2026
@jkowalski jkowalski changed the title refactor(upload): reduce per-file heap allocations during snapshot refactor(snapshots): reduce per-file heap allocations during snapshot Aug 18, 2026
@jkowalski jkowalski changed the title refactor(snapshots): reduce per-file heap allocations during snapshot refactor(snapshots): reduce per-file heap allocations during snapshot (minor perf improvement) Aug 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Reduces per-file allocations during snapshot uploads.

Changes:

  • Uses DirEntry.Info() during directory iteration.
  • Removes object-type prefixes from writer descriptions.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
fs/localfs/local_fs_os.go Changes directory-entry metadata lookup.
snapshot/upload/upload.go Uses filenames directly as writer descriptions.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread fs/localfs/local_fs_os.go
Comment on lines +99 to +102
// DirEntry.Info() is equivalent to Lstat(prefix+n) but is implemented by
// the os package without exposing the path; it also handles the
// entry-deleted-between-ReadDir-and-Info case.
switch fi, err := dirEntry.Info(); {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is actually fine, we're not requiring fresh lstat.

@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.84%. Comparing base (cb455c6) to head (315373c).
⚠️ Report is 1045 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5571      +/-   ##
==========================================
+ Coverage   75.86%   77.84%   +1.98%     
==========================================
  Files         470      553      +83     
  Lines       37301    31746    -5555     
==========================================
- Hits        28299    24714    -3585     
+ Misses       7071     4966    -2105     
- Partials     1931     2066     +135     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jkowalski
jkowalski enabled auto-merge (squash) August 19, 2026 14:33
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.

3 participants