Skip to content

Deep clone TaskRegistry and Toolset#8973

Closed
JanKrivanek wants to merge 7 commits intodotnet:mainfrom
JanKrivanek:proto/TaskRegistryDeepCopy
Closed

Deep clone TaskRegistry and Toolset#8973
JanKrivanek wants to merge 7 commits intodotnet:mainfrom
JanKrivanek:proto/TaskRegistryDeepCopy

Conversation

@JanKrivanek
Copy link
Copy Markdown
Member

@JanKrivanek JanKrivanek commented Jun 28, 2023

Fixes ADO#1801351 and ADO#1801341 and possibly ADO#1824802

Credit for rootcausing: @rokonec

Context

TaskRegistry (and as Toolset as well) were not properly clonned during clonning ProjectInstance data - that could cause unintended sharing of TaskRegistry data between multiple independent evaluation/build threads - allowing concurrent access to TaskRegistry and its objects, while such concurency is not supported.

Changes Made

Made TaskRegistry and Toolset classes DeepClonable and properly cloning them during ProjectInstance clonning

Testing - Perf

No observable impact.

  • OrchardCore and Console, build command: msbuild.exe /nodeReuse:false /bl
Scenario Mean Duration
Orchard, MSBuild main, clean build, /bl 00:01:54.28
Orchard, MSBuild curr branch, clean build, /bl 00:02:01.36
Orchard, MSBuild main, rebuild, /bl 00:01:44.46
Orchard, MSBuild curr branch, rebuild, /bl 00:01:41.81
Orchard, MSBuild main, rebuild 00:01:36.55
Orchard, MSBuild curr branch, rebuild 00:01:39.83
--- ---
Console, MSBuild main, clean build 00:00:01.75
Console, MSBuild curr branch, clean build 00:00:01.71
Console, MSBuild main, rebuild 00:00:01.34
Console, MSBuild curr branch, rebuild 00:00:01.31

Comment thread src/Build/Definition/Toolset.cs Outdated
Copy link
Copy Markdown
Member

@rokonec rokonec left a comment

Choose a reason for hiding this comment

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

Good job.
This would be better in another PR, but I believe that we shall alter our tests so it would catch common failure mode when people add field/property and forget to Clone it.
We have unit tests comparer but those will most probably be forgotten to update as well.

I had success in pasty with AutoFixture for seeding data and Fluent Assertion equivalency tests

Comment thread src/Build/Definition/Toolset.cs
Comment thread src/Build/Definition/Toolset.cs
Comment thread src/Build/Instance/ProjectInstance.cs Outdated
Comment thread src/Build/Instance/ProjectInstance.cs Outdated
Comment thread src/Build/Definition/ProjectImportPathMatch.cs Outdated
ProjectInstance first = GetSampleProjectInstance();
ProjectInstance second = first.DeepCopy();

// Task registry object should be immutable
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.

Was this assertion always wrong? Have we added mutability where we shouldn't have, or just "in a way that requires we change this"?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Unfortunately this is older then GH history so I can only provide guessing - and my guess is the ProjectInstance and owned object graph (including TaskRegistry) was always meant to be instantiated once and then used only sequantially. So immutability and deepclonning was never strongly followed - as it was mostly ok in majority of cases (a specific set of VS actions and conditions needs to be made - e.g. in-proc build or cancel and rebuild - to hit the unwanted sharing of TaskRegistry/Toolset).
TaskRegistry looks almost immutable, with exception of the internal matching caches, lazy initialized state of held TaskFactoryWrapper instances (the reason for the 3rd state corruption perf issue), plus something else (I forgot now) - so the path of DeepCopying turned out to be easier and safer than attempting to make TaskRegistry and Toolset truly immutable

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

tl;dr; the added mutability shouldn't be added without adding deep cloning IMHO

Copy link
Copy Markdown
Member

@rainersigwald rainersigwald left a comment

Choose a reason for hiding this comment

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

Looking at this more, this feels wrong to me. I expect the Toolset and TaskRegistry to be essentially singletons (since we no longer respect a file's toolset specification), and now there will be many of them, causing us to have a lower cache hit rate on lookups.

Do we understand yet what's causing the multithreading in task execution? Or is it one thread executing tasks and one thread evaluating projects? Or what?

Comment on lines +14 to +16
public static PropertyDictionary<ProjectPropertyInstance>? DeepClone(
this PropertyDictionary<ProjectPropertyInstance>? properties)
=> properties == null ? null : new(properties.Select<ProjectPropertyInstance, ProjectPropertyInstance>(p => p.DeepClone()));
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.

The extra copies seem ok based on your results so far, but we could also consider using our ImmutableDictionary, our wrapper CopyOnWriteDictionary, or a similar type to get copy-on-write behavior and minimize allocations (since it seems modifications are rare).

}

internal SubToolset DeepClone()
=> new SubToolset(_subToolsetVersion, _properties?.DeepClone());
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.

What causes mutation of _properties? It feels like it should be static for a subtoolset, and indeed that subtoolset instances should be single-instanced rather than cloned themselves.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

👍
The code could (and likely should) verify the IsImmutable member of all _properties and skip clonning if they are all true.
Without that unfortunately the code gives no guarantees.

Or possibly the SubToolset itself should be created immutable (with properties flipped to immutable) - as currently this is not a case

@JanKrivanek
Copy link
Copy Markdown
Member Author

Looking at this more, this feels wrong to me. I expect the Toolset and TaskRegistry to be essentially singletons (since we no longer respect a file's toolset specification), and now there will be many of them, causing us to have a lower cache hit rate on lookups.

Do we understand yet what's causing the multithreading in task execution? Or is it one thread executing tasks and one thread evaluating projects? Or what?

Copying ProjectInstances is exposed publicly (with coment mentioning it's deep copy) - so there might be scenarios we don't know about. Overlaping build and evaluation seems to be one of them (as copy ctor is invoked between eval and build). The mutability concern and need for deep copy was voiced in the original code as well: https://github.com/dotnet/msbuild/pull/8973/files#diff-40f77147ccc6867c10324ec4ddf8110ede2e43499d939b73ef83c7f5febb7a75L564

However, let's make sure we have better understanding and better consensus on solution - so keeping this PR open for now.

@JanKrivanek JanKrivanek marked this pull request as draft July 12, 2023 16:03
@JanKrivanek
Copy link
Copy Markdown
Member Author

Upon detailed investigation with @rainersigwald and @rokonec we'll fix this differently: TaskRegistry is expected to be shareable and logicaly immutable, however it has internal caches, that can be altered on access. so TaskRegistry internal caches will be made thread safe, the actual task registrations are not expected to be changed after initialization - so safeguard code will be added to catch unintended state changes

@JanKrivanek
Copy link
Copy Markdown
Member Author

Superseded by #9032

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