Skip to content

Cache GetFileVersion to avoid redundant per-project version reads in GenerateDepsFile - #55033

Merged
baronfel merged 2 commits into
mainfrom
dev/veronikao/generatedepsfile-fileversion-cache
Jun 29, 2026
Merged

baronfel merged 2 commits into
mainfrom
dev/veronikao/generatedepsfile-fileversion-cache

Conversation

@OvesN

@OvesN OvesN commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

FileUtilities.GetFileVersion re-opens and re-parses the Win32 version resource of the same
runtime files for every project that references them. The same framework/package assemblies
are referenced by many projects, so this is highly redundant I/O and allocation during
GenerateDepsFile (which calls it per resolved runtime file when IncludeRuntimeFileVersions
is set).

This adds a path + last-write-time cache, mirroring the existing cache already used for
assembly versions (GetAssemblyVersion). It is a ConcurrentDictionary, so it is safe
under multithreaded (-mt) builds, and the cached entries are immutable.

Why it helps -mt in particular

The cache is static, so in -mt (single process) it is shared across all projects in the
build, eliminating far more redundant reads than in multi-process -m, where each worker
process keeps its own cache.

Measurements (OrchardCore Cms.Web Rebuild, 192 GenerateDepsFile calls)

  • GenerateDepsFile task time: -mt ≈ −65% (59 → 20 ms avg), -m ≈ −14% (74 → 64 ms avg).
  • Deps-attributed allocation: ≈ −33% (174 → 116 MB); GetFileVersion drops out of the hot path.
  • The produced deps.json is byte-identical (verified by hash) — pure speedup, no behavior change.

Follow-ups (draft)

  • Add a unit test (cache hit on unchanged write time; re-read when the file's write time changes).

GetFileVersion re-opened and re-parsed the Win32 version resource of every runtime
file for every project. The same framework/package assemblies are referenced by many
projects, so this is highly redundant. Add a path + last-write-time cache mirroring the
existing assembly-version cache (thread-safe ConcurrentDictionary).

Because the cache is static, in -mt (single process) it is shared across all projects in
the build, so it removes far more redundant I/O than in multi-process -m (per-worker
caches). Measured on OrchardCore.Cms.Web Rebuild (192 deps calls, IL task):
GenerateDepsFile avg -mt 59->20ms (~65%), -m 74->64ms (~14%); deps.json byte-identical.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ViktorHofer
ViktorHofer requested a review from dsplaisted June 29, 2026 09:18
@OvesN
OvesN marked this pull request as ready for review June 29, 2026 09:21
Copilot AI review requested due to automatic review settings June 29, 2026 09:21

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.

Pull request overview

This PR improves build performance in GenerateDepsFile scenarios by caching Win32 file version reads (FileUtilities.GetFileVersion) so that the same runtime file’s version resource is not repeatedly opened and parsed across many projects in a single MSBuild process.

Changes:

  • Introduced a static ConcurrentDictionary cache keyed by file path plus last-write-time UTC for file version lookups.
  • Updated GetFileVersion to return cached values when the file timestamp is unchanged, and to refresh the cache when it changes.

Comment on lines 15 to +35
public static Version? GetFileVersion(string? sourcePath)
{
if (sourcePath != null)
{
var fvi = FileVersionInfo.GetVersionInfo(sourcePath);
DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(sourcePath);

if (s_fileVersionCache.TryGetValue(sourcePath, out var cacheEntry)
&& lastWriteTimeUtc == cacheEntry.LastKnownWriteTimeUtc)
{
return cacheEntry.Version;
}

Version? version = null;
var fvi = FileVersionInfo.GetVersionInfo(sourcePath);
if (fvi != null)
{
return new Version(fvi.FileMajorPart, fvi.FileMinorPart, fvi.FileBuildPart, fvi.FilePrivatePart);
version = new Version(fvi.FileMajorPart, fvi.FileMinorPart, fvi.FileBuildPart, fvi.FilePrivatePart);
}

s_fileVersionCache[sourcePath] = (lastWriteTimeUtc, version);
return version;
Comment on lines +12 to +13
private static readonly ConcurrentDictionary<string, (DateTime LastKnownWriteTimeUtc, Version? Version)> s_fileVersionCache
= new(StringComparer.OrdinalIgnoreCase);
@ViktorHofer

Copy link
Copy Markdown
Member

@dsplaisted / @ericstj (hope it's ok to continue to ask you questions on GH) any reason why we didn't cache the file version information before in the GenerateDepsFile task? Just trying to see if there are any hidden risks in doing that.

@baronfel

Copy link
Copy Markdown
Member

@ViktorHofer almost certainly just a performance/impact question - we weren't tracking how frequently the operation was done. This kind of metadata caching seems completely reasonable to do - 100% safe in the case of a single build, potentially less safe across builds but still very likely to be so.

Comment thread src/Tasks/Common/FileUtilities.cs
@baronfel

Copy link
Copy Markdown
Member

/ba-g flaky test run on the linux-amd64 leg, no reason to believe its related to this change.

@baronfel
baronfel enabled auto-merge (squash) June 29, 2026 15:28
@baronfel
baronfel merged commit 99e9db0 into main Jun 29, 2026
37 of 39 checks passed
@baronfel
baronfel deleted the dev/veronikao/generatedepsfile-fileversion-cache branch June 29, 2026 15:34
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 11.0-preview7 milestone Jun 30, 2026
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.

5 participants