Skip to content

Reduce tar extraction overhead and improve async archive creation - #135357

Open
JeremyKuhne wants to merge 3 commits into
dotnet:mainfrom
JeremyKuhne:perf/tar-filesystem-overhead
Open

JeremyKuhne wants to merge 3 commits into
dotnet:mainfrom
JeremyKuhne:perf/tar-filesystem-overhead

Conversation

@JeremyKuhne

@JeremyKuhne JeremyKuhne commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Rebuilt on main at 699100ec729dddc77dcafa770b3b9e177357ec41, including #135748 (df0dd45e1302190770b48ce9676d237224f8d2e3). The measurements below compare that updated main baseline with this PR, not with the implementation before #135748.

This version builds on the new symlink-validation design instead of restoring the earlier PR's empty-destination descendant-check bypass. No public API is added.

Changes

  • Restore extracted regular-file timestamps through the existing output handle, avoiding another file open. Flush buffered async output with the caller's cancellation token before exposing the handle.
  • Reduce output buffer sizes for tiny regular files without disabling the buffering needed by short-reading streams. Preserve sparse-file handling.
  • Further reduce the remaining hard-link validation overhead: lazily reuse physical-root resolution only when the root and all probed ancestors are ordinary directories, use fresh non-link metadata probes, and remove duplicate logical-path normalization.
  • Keep descendant checks fresh for every hard-link target, including in prepopulated destinations. Linked roots/ancestors retain fallback resolution. Preserve main's deferred symlinks and intended preexisting-symlink behavior.
  • Batch file-backed archive output with a 32 KiB buffer, rather than the previous PR version's 64 KiB. Use Windows sequential-read hints for source files. Caller-provided streams are unchanged.
  • Cover buffered timestamps, post-copy cancellation, descendant changes between entries, and retargeted linked roots/ancestors. The new Windows reparse-point tests use junctions so they execute without symbolic-link privilege.

Performance beyond #135748

Fresh BenchmarkDotNet Release measurements on Windows 11 / Ryzen 7 9700X and WSL2 Ubuntu on native ext4. Baseline and PR use the same locally built 12.0.0-dev runtime; only the platform Tar assembly differs. All 60 benchmark rows completed with populated time/allocation results and verified loaded-assembly hashes. Every output is checked outside timing for contents, populations, timestamps under the frozen validation policy, and applicable hard-link identities/groups.

Cells are sync / async. Times are milliseconds; negative percentages mean less time or cumulative managed allocation per archive, not lower peak memory.

Scenario Main ms PR ms Time change Allocation change
Windows SDK tar extraction 3430.33 / 3460.13 2924.13 / 3125.71 -14.8% / -9.7% -26.1% / -23.2%
Windows SDK tar.gz extraction 4168.50 / 4552.21 3786.83 / 4229.47 -9.2% / -7.1% -22.1% / -16.3%
Windows Runtime tar extraction 120.90 / 135.66 125.11 / 131.60 +3.5% / -3.0% -5.2% / -3.5%
Windows Runtime tar.gz extraction 246.76 / 308.20 236.10 / 305.33 -4.3% / -0.9% -3.4% / -1.1%
Windows Small extraction 15.50 / 17.28 12.65 / 14.58 -18.4% / -15.6% -54.2% / -41.5%
Windows Tiny extraction 1988.06 / 2101.16 1756.25 / 1830.26 -11.7% / -12.9% -58.3% / -54.5%
Windows Mixed extraction 130.49 / 145.41 122.89 / 132.66 -5.8% / -8.8% -39.6% / -35.1%
Windows Large extraction 24.94 / 33.87 25.39 / 32.79 +1.8% / -3.2% -2.0% / +1.8%
Windows SDK tar creation 515.59 / 1000.59 503.21 / 930.36 -2.4% / -7.0% +0.2% / -3.0%
Windows Runtime tar creation 37.89 / 63.33 37.29 / 57.87 -1.6% / -8.6% +6.8% / +5.8%
Windows Small creation 2.04 / 3.80 1.78 / 3.48 -12.6% / -8.3% +42.2% / +25.2%
Windows Tiny creation 156.92 / 407.90 150.14 / 356.77 -4.3% / -12.5% +0.4% / -0.2%
Windows Mixed creation 16.23 / 33.74 15.85 / 31.22 -2.3% / -7.4% +5.9% / +3.3%
Windows Large creation 23.03 / 34.82 23.29 / 35.06 +1.2% / +0.7% +237.0% / +133.3%
Linux SDK tar extraction 2340.16 / 2702.05 2291.38 / 2632.12 -2.1% / -2.6% -28.2% / -26.2%

SDK extraction allocation falls 27.57 / 32.39 -> 20.39 / 24.88 MiB on Windows and 24.96 / 27.70 -> 17.93 / 20.45 MiB on Linux. The real archives are the pinned .NET 11 RC SDK (~726 MB payload) and Runtime (~87 MB); creation repacks their verified trees as uncompressed tar. Small is 32 256-byte files, Tiny is 4,096 256-byte files, Mixed is 256 files totaling ~17 MiB, and Large is four 16 MiB files.

These are observed means, not universal latency guarantees. In particular, Runtime tar sync extraction's +3.5% mean has substantial dispersion in the PR run (44.7 ms standard deviation), and the short Large controls are effectively flat. The fixed creation buffer adds 28 KiB per synchronous archive; that explains the large relative allocation percentages for inexpensive creation cases. Linux's modest elapsed-time changes should not be overinterpreted.

Windows extraction uses seven measured iterations, ColdStart, zero BDN warmups; real extraction has one setup extraction/verification. Real creation uses ten measured iterations and one setup creation. Synthetic creation uses 15 measured iterations and three warmups. Linux SDK extraction uses ten measured iterations and one setup extraction. The OS file cache is warm, storage caches were not flushed, and platform runs were sequential. Short-iteration BDN warnings remain applicable. These are JIT measurements, not NativeAOT or macOS measurements.

Why 32 KiB rather than 64 KiB or larger?

The current implementation was swept at 4, 8, 16, 32, 64, 128, 256, 512, 1024, and 2048 KiB. A 100-row BDN screen was followed by 693 independently verified, rotating/reversed-order observations, excluding 20 primers per arm/case. The warm paired results are:

Buffer comparison Workload Mean paired time change
4 -> 32 KiB Windows Tiny async creation -10.7%
4 -> 32 KiB Linux Tiny async creation -4.8%
32 -> 64 KiB Windows SDK async creation -1.9%, about 17 ms
32 -> 64 KiB Windows Tiny async creation About -1% to -2% across two runs
32 -> 64 KiB Linux Tiny async creation No clear improvement
64 -> 512 KiB Windows SDK async creation -8.5%

32 KiB captures most of the tiny-file benefit with a smaller per-archive allocation. Larger buffers can further improve SDK and large-file throughput, but 128 KiB and above put their arrays on the large object heap. This PR chooses the smaller default rather than maximizing every large-archive result. Callers prioritizing that throughput can supply their own more heavily buffered stream.

Linux SDK creation is not included: the strict oracle rejects five omitted SDK dotfiles under the existing enumeration behavior. No product or oracle change was made to accept that omission. Linux buffer-size comparisons use the verified synthetic workloads instead.

Validation and limits

Passed on Windows, with analyzers enabled:

.\.dotnet\dotnet.exe build src\libraries\System.Formats.Tar\src\System.Formats.Tar.csproj -c Release /p:RunAnalyzersInBuild=true
.\.dotnet\dotnet.exe build src\libraries\System.Formats.Tar\tests\System.Formats.Tar.Tests.csproj -c Release /t:Test /p:Outerloop=true /p:RunAnalyzersInBuild=true
  • Product build: all three TFMs, zero warnings/errors.
  • Tests: 7,050 passed, 31 existing symbolic-link-capability skips, zero failures. All six new junction-based validation cases execute and pass.

Passed on Linux, with analyzers enabled:

.dotnet/dotnet build src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj -c Release /p:RunAnalyzersInBuild=true
.dotnet/dotnet build src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj -c Release /t:Test /p:Outerloop=true /p:RunAnalyzersInBuild=true
  • Product build: all three TFMs, zero warnings/errors.
  • Tests: 6,428 passed, eight privileged-only skips, zero failures. A separate privileged selection expands those methods to 54 passing cases.
  • All six new symbolic-link validation cases execute and pass.
  • The three post-copy cancellation regression cases fail against main and pass with token forwarding. Short-read extraction and timestamp boundaries were also validated.

The root cache assumes that ordinary roots and their ancestors are not replaced externally during extraction; it is not handle-anchored namespace-race protection. Descendants are still checked freshly, and linked roots/ancestors are not cached. No macOS execution or new NativeAOT measurement is claimed.

Related to #135128.

Note

This description was generated with GitHub Copilot.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-formats-tar
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Security-sensitive cross-platform path handling changed substantially, and macOS execution was not validated.

0 open findings

What changed in this PR

Optimizes tar extraction and file-backed archive creation while preserving extraction security and metadata behavior.

Changes:

  • Reduces path-resolution calls and per-entry allocations.
  • Improves buffering, async flushing, timestamps, and sequential reads.
  • Adds cross-platform regression and cancellation coverage.
File Description
TarFile.ExtractToDirectory.Stream.Tests.cs Covers empty roots and late links.
TarFile.ExtractToDirectory.File.Tests.Windows.cs Tests Windows reparse-root handling.
TarFile.ExtractToDirectory.File.Tests.Unix.cs Tests Unix links and permissions.
TarFile.ExtractToDirectory.File.Tests.cs Adds root-retargeting test support.
TarEntry.ExtractToFile.Tests.cs Tests cancellation and timestamps.
TarWriter.Windows.cs Adds sequential-read hints.
TarFile.cs Adds output buffering and extraction context.
TarEntry.Windows.cs Optimizes Windows path resolution.
TarEntry.Unix.cs Optimizes Unix path resolution.
TarEntry.cs Reworks containment checks and file extraction.
System.Formats.Tar.csproj Includes required Windows interop sources.

🧠 Review effort: Balanced

Rebuild the optimizations on main including dotnet#135748. Preserve deferred symlink validation and fresh descendant checks while lazily reusing ordinary roots for hard-link targets and avoiding path-probe allocations. Reuse output handles for timestamps, forward flush cancellation, and right-size tiny-file buffers.

Use 32 KiB owned archive output buffers after the measured size sweep, retaining Windows sequential-read hints. Add timestamp, cancellation, descendant mutation, and linked-root retargeting coverage, including Windows junctions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Descendant-change regression coverage must exercise validation after the ordinary-root cache has been populated.

1 open issue

Review effort: Balanced

Prime the ordinary-root cache with a successful hard link before changing the descendant directory, then verify the initial hard link keeps its contents. Both sync and async cases detect a generated cache-bypass mutant and pass against the normal product on Windows and Linux.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@rzikm rzikm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems reasonable in general, I left couple of comments

@rzikm

rzikm commented Nov 4, 2026

Copy link
Copy Markdown
Member

@JeremyKuhne have you had time to take a look at my comments?

@JeremyKuhne

Copy link
Copy Markdown
Member Author

@JeremyKuhne have you had time to take a look at my comments?

@rzikm Sorry, missed the notification- thanks for the heads up I'll look today! :)

Disable source-file buffering on Windows and Unix, cache trusted extraction roots once, and replace concurrent-mutation tests with static linked-root coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants