phmonte / Buildalyzer

A utility to perform design-time builds of .NET projects without having to think too hard about it.
MIT License
589 stars 92 forks source link

Cache dotnet info results per global.json file #227

Closed petertiedemann closed 1 month ago

petertiedemann commented 1 year ago

I ran into this problem with stryker.net (which uses Buildalyzer), getting exceptions like this

Unhandled exception. System.AggregateException: One or more errors occurred. (Could not find build environment) (Could not find build environment) (Could not find build environment) (Could not find build environment) (Could not find build environment) (Could not find build environment) (Could not find build environment) (Could not find build environment)
 ---> System.InvalidOperationException: Could not find build environment
   at Buildalyzer.Environment.EnvironmentFactory.GetBuildEnvironment(String targetFramework, EnvironmentOptions options)
   at Buildalyzer.Environment.EnvironmentFactory.GetBuildEnvironment(String targetFramework)
   at Buildalyzer.ProjectAnalyzer.Build(String targetFramework)
   at Buildalyzer.ProjectAnalyzer.Build()
   at Stryker.Core.Initialisation.ProjectOrchestrator.<>c__DisplayClass7_0.<AnalyzeSolution>b__0(IProjectAnalyzer project) in /_/src/Stryker.Core/Stryker.Core/Initialisation/ProjectOrchestrator.cs:line 124

I was able to reproduce it directly against Buildalyzer (main branch) using this type of test ( no sln included here, but I think any large sln file should do, mine has 278 projects, and was executed on a 5950x CPU with 16c/32t).

        AnalyzerManager manager = new AnalyzerManager(@"C:\path\to\Big.sln", new AnalyzerManagerOptions());
        Parallel.ForEach(manager.Projects.Values, project =>
        {
            Console.Error.WriteLine("Analysing {0}", project.ProjectFile.Path);
            IAnalyzerResults? buildResult = project.Build();
            IAnalyzerResult? projectAnalyzerResult = buildResult.Results.FirstOrDefault();
            if (projectAnalyzerResult is { })
            {
                Console.Error.WriteLine("Analysis of project {0} succeeded", project.ProjectFile.Path);
            }
            else
            {
                Console.Error.WriteLine("Analysis of project {0} failed", project.ProjectFile.Path);
            }
        });

As far as I can tell the problem is that the time-out in DotnetPathResolver.cs for running dotnet --info is "only" 4000ms, and when that time-out is exceeded it fails somewhat silently. In my case I just bumped it to 60000ms.

I wondering if perhaps an optimization is in order to avoid invoking dotnet --info so many times? AFAIK the info will only change if a global.json file is placed in a subfolder above the project file, so it should only be necessary to call dotnet --info once per located global.json (+1 if no global.json exists).

daveaglick commented 6 months ago

I just bumped the timeout to 10 seconds in version 6.0.2 (see #228) and there's now a DOTNET_INFO_WAIT_TIME environment variable that can be used to specify an alternate amount of time to wait as well.

I do really like the idea of caching the dotnet --info call per global.json so I'll leave this open as a general enhancement and try to get something done in that regard.

phmonte commented 3 months ago

@Corniel, I'm going to implement this feature, I thought of a static class to store this cache during execution or use the InMemory cache, do you have any considerations? If you've already started something, I can get another feature too.

Corniel commented 3 months ago

I thought about having it in place why logging/running/tracing the build. I think the scope of one run would be sufficient, but may be a static cache will work better.

phmonte commented 3 months ago

PR with the feature.

phmonte commented 1 month ago

Caching has been implemented, @petertiedemann

I will close this issue, let us know if there is any problem, thank you.