Skip to content

[Opt-in] Parallelize Targets when building a solution#7512

Merged
Forgind merged 9 commits intodotnet:mainfrom
yuehuang010:dev/yuehuang/main_solution
Jul 18, 2022
Merged

[Opt-in] Parallelize Targets when building a solution#7512
Forgind merged 9 commits intodotnet:mainfrom
yuehuang010:dev/yuehuang/main_solution

Conversation

@yuehuang010
Copy link
Copy Markdown
Contributor

@yuehuang010 yuehuang010 commented Apr 1, 2022

Fixes

#5072 (comment)

Context

  1. When building a SLN, a metaproj is used to represent the build behavior. When there are multiple targets (ex clean;build), the current behavior is to run all of first Target in the projects, then run second Target. To improve the parallelism, the solution can pass both target to the project. Each project can start the second target without waiting for all of the first Target to finish.
    When the feature is enabled via environment variable, MSBuildSolutionBatchTargets, Solution Generator will create a "SlnProjectResolveProjectReference" target to build all the project/targets. All targets will depend on this new target.

  2. Add support for "SkipNonexistentProjects" as a metadata in MSBuild task. This allow the removal of it as a parameter during solution generation.

Testing

Add unit tests.

Before:

image

After:

image


if (FileSystems.Default.FileExists(projectPath) || (_skipNonexistentProjects == SkipNonexistentProjectsBehavior.Build))
// Try to get the behavior from metadata if it is undefined.
var skipNonexistPropjects = _skipNonexistentProjects;
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.

It looks like this was essentially copied from SolutionProjectGenerator—is it possible to merge them? Are there major differences? Why are they separate in the first place?

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.

I don't know why there are two copies of MSBuild.cs and MSBuild_Tests.cs.

When tests are running, they resolve to BackEnd.

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 story here is that long ago, the MSBuild task was just a task that used the IBuildEngine APIs like any other task would be able to do. At some later time, as a performance optimization, the MSBuild task was pulled into the engine itself, and task resolution short-circuits to prefer that version.

The one in Tasks exists only because it was a public non-sealed type, so user types might have inherited from it. I generally wouldn't worry about adding new features to it, since it should be almost entirely dead code.

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.

When we talked about this problem, I expected something along the lines of synthesizing a target like

<MSBuild Projects="@(Projects)" Targets="Foo;Bar" />

Instead of the existing mechanism of synthesizing a Foo that calls all Foo and a Bar that calls all bar, then calling them in sequence.

Is that viable?

@yuehuang010
Copy link
Copy Markdown
Contributor Author

yuehuang010 commented Apr 18, 2022

When we talked about this problem, I expected something along the lines of synthesizing a target like

<MSBuild Projects="@(Projects)" Targets="Foo;Bar" />

Instead of the existing mechanism of synthesizing a Foo that calls all Foo and a Bar that calls all bar, then calling them in sequence.

Is that viable?

How would the target interface with the entry point work? @rainersigwald

@rainersigwald
Copy link
Copy Markdown
Member

How would the target interface with the entry point work?

Sorry, missed this. I don't think I understand the question.

I was thinking of something like synthesizing a new target based on the targetNames passed to the SolutionProjectGenerator constructor, and having it be, basically,

  <Target Name="Build_A_B_C">
    <MSBuild BuildInParallel="True"
             SkipNonexistentProjects="%(ProjectReference.SkipNonexistentProjects)"
             Projects="@(ProjectReference)"
             Targets="A;B;C"
             Properties="BuildingSolutionFile=true; CurrentSolutionConfigurationContents=$(CurrentSolutionConfigurationContents); SolutionDir=$(SolutionDir); SolutionExt=$(SolutionExt); SolutionFileName=$(SolutionFileName); SolutionName=$(SolutionName); SolutionPath=$(SolutionPath)" />
  </Target>

@yuehuang010
Copy link
Copy Markdown
Contributor Author

How would the target interface with the entry point work?

Sorry, missed this. I don't think I understand the question.

I was thinking of something like synthesizing a new target based on the targetNames passed to the SolutionProjectGenerator constructor, and having it be, basically,

  <Target Name="Build_A_B_C">
    <MSBuild BuildInParallel="True"
             SkipNonexistentProjects="%(ProjectReference.SkipNonexistentProjects)"
             Projects="@(ProjectReference)"
             Targets="A;B;C"
             Properties="BuildingSolutionFile=true; CurrentSolutionConfigurationContents=$(CurrentSolutionConfigurationContents); SolutionDir=$(SolutionDir); SolutionExt=$(SolutionExt); SolutionFileName=$(SolutionFileName); SolutionName=$(SolutionName); SolutionPath=$(SolutionPath)" />
  </Target>

@rainersigwald, it is doing creating a new single target with the MSBuild which invokes all targets. The question is how would you expected the initial targets to invoke this new target?

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.

Sorry for the delay on this. You're right that the general approach matches what I was thinking--not sure what confused me there.

  1. Can you elaborate on how metadata-SkipNonexistentProjects is tied to the solution-batching stuff?

  2. Looking at the metaproject, I see

  <Target Name="foo" DependsOnTargets="SlnProjectResolveProjectReference" />
  <Target Name="bar" DependsOnTargets="SlnProjectResolveProjectReference" />
  <Target Name="SlnProjectResolveProjectReference">
    <MSBuild ToolsVersion="$(ProjectToolsVersion)" BuildInParallel="True" Projects="@(ProjectReference)" Targets="foo;bar" />
  </Target>

Replacing

  <Target Name="foo">
    <MSBuild BuildInParallel="True" Projects="@(ProjectReference)" Targets="foo" Properties="BuildingSolutionFile=true; CurrentSolutionConfigurationContents=$(CurrentSolutionConfigurationContents); SolutionDir=$(SolutionDir); SolutionExt=$(SolutionExt); SolutionFileName=$(SolutionFileName); SolutionName=$(SolutionName); SolutionPath=$(SolutionPath)" />
  </Target>
  <Target Name="bar">
    <MSBuild BuildInParallel="True" Projects="@(ProjectReference)" Targets="bar" Properties="BuildingSolutionFile=true; CurrentSolutionConfigurationContents=$(CurrentSolutionConfigurationContents); SolutionDir=$(SolutionDir); SolutionExt=$(SolutionExt); SolutionFileName=$(SolutionFileName); SolutionName=$(SolutionName); SolutionPath=$(SolutionPath)" />
  </Target>

Can you pass along the properties? Otherwise I think it'll go wrong in Build on non-default solution configurations.


if (FileSystems.Default.FileExists(projectPath) || (_skipNonexistentProjects == SkipNonexistentProjectsBehavior.Build))
// Try to get the behavior from metadata if it is undefined.
var skipNonexistPropjects = _skipNonexistentProjects;
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 story here is that long ago, the MSBuild task was just a task that used the IBuildEngine APIs like any other task would be able to do. At some later time, as a performance optimization, the MSBuild task was pulled into the engine itself, and task resolution short-circuits to prefer that version.

The one in Tasks exists only because it was a public non-sealed type, so user types might have inherited from it. I generally wouldn't worry about adding new features to it, since it should be almost entirely dead code.

@yuehuang010
Copy link
Copy Markdown
Contributor Author

yuehuang010 commented May 9, 2022

  • Can you elaborate on how metadata-SkipNonexistentProjects is tied to the solution-batching stuff?

It is related as the SkipNonexistentProjects is splitting the build into two batches. This batch is serializing them which this will optimize away.

  • Looking at the metaproject,

This isn't actually needed as ProjectReference item already has those Properties as metadata.

@yuehuang010
Copy link
Copy Markdown
Contributor Author

@rainersigwald Ping.

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.

Sorry for the delay!

It is related as the SkipNonexistentProjects is splitting the build into two batches. This batch is serializing them which this will optimize away.

Ah, and this is happening inside solution builds because anything with a solution build dependency generates a metaproject that gets SkipNonexistent.

This isn't actually needed as ProjectReference item already has those Properties as metadata.

I'd really like to leave this in case a user injects a target that changes the properties, since it's been done that way for the normal build targets for a very long time.

@yuehuang010
Copy link
Copy Markdown
Contributor Author

@rainersigwald Ping.

Copy link
Copy Markdown
Contributor

@Forgind Forgind left a comment

Choose a reason for hiding this comment

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

Tested this out, and it works!

}
else
{
task.SetParameter("Properties", SolutionProperties);
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.

This feels like taking something global and making it local to every project. Is that wrong? I would've imagined properties would flow automatically from the metaproj down to each individual project, but its absence here makes me wonder if we should continue to leave it empty.

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.

@rainersigwald suggested to add it.

This isn't actually needed as ProjectReference item already has those Properties as metadata.

I'd really like to leave this in case a user injects a target that changes the properties, since it's been done that way for the normal build targets for a very long time.

@Forgind Forgind added the merge-when-branch-open PRs that are approved, except that there is a problem that means we are not merging stuff right now. label Jul 18, 2022
@Forgind Forgind merged commit c849248 into dotnet:main Jul 18, 2022
@yuehuang010 yuehuang010 deleted the dev/yuehuang/main_solution branch February 22, 2024 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-when-branch-open PRs that are approved, except that there is a problem that means we are not merging stuff right now.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants