Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions documentation/specs/multithreading/multithreaded-msbuild.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,17 @@ deactivate Thread1_Project1
## MSBuild Server integration

To avoid regressing CLI incremental build performance, it is essential to fully support the MSBuild server feature in multithreaded mode. Out-of-process nodes offer significant benefits by preserving caches across build command executions when node reuse is enabled. However, caches in the entry process are lost at the end of each build unless the MSBuild server feature is used. In multiprocess execution, that is a fraction of the caches and processes, but with multithreading, it is the entire process. As a result, opting into multithreading on the command line should automatically enable MSBuild server.

### `-mt` implies MSBuild Server

When `-mt` (`-multithreaded`) is on the command line and `MSBUILDUSESERVER` is unset, MSBuild Server is engaged automatically. Explicit `MSBUILDUSESERVER=0` opts out and takes precedence over the implicit `-mt` opt-in.

| `MSBUILDUSESERVER` | `-mt` build | Server engaged? | Telemetry `ServerEnableReason` |
|---|---|---|---|
| `1` | (any) | Yes | `EnvVar` |
| `0` | (any) | No (explicit opt-out) | β€” |
| unset | yes | **Yes** | `ImpliedByMt` |
| unset | no | No | β€” |

The multithreaded determination is computed once (a lightweight command-line scan plus the `MSBUILDFORCEMULTITHREADED` opt-in) and drives both this server opt-in and, when the server is engaged for a multithreaded build, the server process's [Server GC](../../MSBuild-Server.md#garbage-collection) selection.

11 changes: 11 additions & 0 deletions src/Framework/Telemetry/BuildTelemetry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ internal class BuildTelemetry : TelemetryBase, IActivityTelemetryDataHolder
/// </summary>
public string? ServerFallbackReason { get; set; }

/// <summary>
/// Why MSBuild server was engaged for this invocation. One of:
/// "EnvVar" β€” MSBUILDUSESERVER=1 was set (explicit opt-in)
/// "ImpliedByMt" β€” this is a multithreaded (-mt) build and MSBUILDUSESERVER was unset
/// null β€” server was not engaged
/// Lets dashboards measure adoption of the implicit -mt-implies-server path separately
/// from the explicit env-var path.
/// </summary>
Comment thread
JanProvaznik marked this conversation as resolved.
public string? ServerEnableReason { get; set; }

/// <summary>
/// Version of MSBuild.
/// </summary>
Expand Down Expand Up @@ -200,6 +210,7 @@ public override IDictionary<string, string> GetProperties()
AddIfNotNull(InitialMSBuildServerState);
AddIfNotNull(ProjectPath != null ? Path.GetFileName(ProjectPath) : null, nameof(ProjectPath));
AddIfNotNull(ServerFallbackReason);
AddIfNotNull(ServerEnableReason);
AddIfNotNull(SanitizeBuildTarget(BuildTarget), nameof(BuildTarget));
AddIfNotNull(BuildEngineVersion?.ToString(), nameof(BuildEngineVersion));
AddIfNotNull(BuildSuccess?.ToString(), nameof(BuildSuccess));
Expand Down
31 changes: 31 additions & 0 deletions src/MSBuild.UnitTests/MSBuildServer_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,37 @@ public void ServerShouldStartWhenBuildIsInteractive()
serverIsDown.ShouldBeTrue();
}
Comment thread
JanProvaznik marked this conversation as resolved.

[Fact]
public void ServerStartsWhenMtPresentEvenWithoutEnvVar()
{
// Regression test for the "-mt implies MSBuild Server" routing decision
// (investigation #9379, ShouldUseMSBuildServer / IsMultiThreadedRequested).
// When MSBUILDUSESERVER is unset and the user passes -mt, the client should engage
// the server automatically. Verified by running two builds back-to-back and asserting
// the server process PID is the SAME for both β€” server reuse is the unique signature
// of MSBuild server engagement (a non-server build would always get a fresh worker PID).
TransientTestFile project = _env.CreateFile("testProject.proj", printPidContents);
// Explicitly clear MSBUILDUSESERVER so we test the -mt-implies-server path.
_env.SetEnvironmentVariable("MSBUILDUSESERVER", null);

// Make sure we start with no server running.
MSBuildClient.ShutdownServer(CancellationToken.None);
Comment thread
JanProvaznik marked this conversation as resolved.
Outdated

string output1 = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path + " -mt", out bool success1, false, _output);
success1.ShouldBeTrue();
int serverPid1 = ParseNumber(output1, "Server ID is ");

string output2 = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path + " -mt", out bool success2, false, _output);
success2.ShouldBeTrue();
int serverPid2 = ParseNumber(output2, "Server ID is ");

Comment thread
JanProvaznik marked this conversation as resolved.
serverPid1.ShouldBe(serverPid2, "When -mt implies server, two consecutive builds should reuse the same server process. PIDs were " + serverPid1 + " and " + serverPid2 + ".");

_env.WithTransientProcess(serverPid1);
// Clean up the server we spun up.
MSBuildClient.ShutdownServer(CancellationToken.None);
}

[Fact]
public void PropertyMSBuildStartupDirectoryOnServer()
{
Expand Down
139 changes: 130 additions & 9 deletions src/MSBuild/XMake.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,14 +309,26 @@ public static int Main(string[] args)
DumpCounters(true /* initialize only */);
}

// Determine once whether this is a multithreaded (/mt) build. This single value drives
// two related decisions:
// (1) whether to engage MSBuild Server implicitly (this PR: -mt implies server), and
// (2) whether the server, once engaged, is launched with Server GC (the base PR).
// Computed with a cheap command-line scan (plus the MSBUILDFORCEMULTITHREADED opt-in) so
// the common no-server path does not pay for the full GatherAllSwitches parse here.
bool multiThreaded = IsMultiThreadedRequested(args);
Comment thread
JanProvaznik marked this conversation as resolved.
Outdated

int exitCode;
if (
Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName) == "1" &&
ShouldUseMSBuildServer(multiThreaded, out string serverEnableReason) &&
!Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout &&
CanRunServerBasedOnCommandLineSwitches(args, out bool multiThreaded))
CanRunServerBasedOnCommandLineSwitches(args))
{
Console.CancelKeyPress += Console_CancelKeyPress;

if (KnownTelemetry.PartialBuildTelemetry != null)
{
KnownTelemetry.PartialBuildTelemetry.ServerEnableReason = serverEnableReason;
}

// Use the client app to execute build in msbuild server. Opt-in feature.
exitCode = ((s_initialized && MSBuildClientApp.Execute(args, multiThreaded, s_buildCancellationSource.Token) == ExitType.Success) ? 0 : 1);
Expand All @@ -343,10 +355,9 @@ public static int Main(string[] args)
/// <remarks>
/// Will not throw. If arguments processing fails, we will not run it on server - no reason as it will not run any build anyway.
/// </remarks>
private static bool CanRunServerBasedOnCommandLineSwitches(string[] commandLine, out bool multiThreaded)
private static bool CanRunServerBasedOnCommandLineSwitches(string[] commandLine)
{
bool canRunServer = true;
multiThreaded = false;
try
{
commandLineParser.GatherAllSwitches(
Expand All @@ -362,11 +373,6 @@ private static bool CanRunServerBasedOnCommandLineSwitches(string[] commandLine,
{
commandLineSwitches = CombineSwitchesRespectingPriority(switchesFromAutoResponseFile, switchesNotFromAutoResponseFile, fullCommandLine);
}

// Determine multithreaded mode from the fully-parsed switches (which include any
// response-file-provided switches) using the same logic as the in-proc build path.
multiThreaded = IsMultiThreadedEnabled(commandLineSwitches);

string projectFile = ProcessProjectSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Project], commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.IgnoreProjectExtensions], Directory.GetFiles);
if (commandLineSwitches[CommandLineSwitches.ParameterlessSwitch.Help] ||
commandLineSwitches.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.NodeMode) ||
Expand Down Expand Up @@ -394,6 +400,121 @@ private static bool CanRunServerBasedOnCommandLineSwitches(string[] commandLine,
return canRunServer;
}

/// <summary>
/// Returns true if MSBuild Server should be used for this invocation.
/// </summary>
/// <remarks>
/// Decision tree:
/// <list type="bullet">
/// <item><c>MSBUILDUSESERVER=1</c> β†’ use server (existing explicit opt-in).</item>
/// <item><c>MSBUILDUSESERVER=0</c> β†’ do NOT use server (explicit opt-out, takes precedence over -mt).</item>
/// <item><c>MSBUILDUSESERVER</c> unset AND this is a multithreaded (<c>-mt</c>) build β†’ use server.
/// Rationale: <c>-mt</c> users already accept process-shared state (in-proc thread workers
/// instead of multi-process worker nodes), so server reuse barely adds risk and recovers the
/// per-invocation JIT/SDK-resolution warm-up cost that <c>-mt</c> would otherwise pay every
/// build (because <c>-mt</c> shares the entry process with the build instead of the workers).
/// See <see href="https://github.com/dotnet/msbuild/issues/9379">#9379</see>.</item>
/// <item>Otherwise β†’ no server (existing default).</item>
/// </list>
/// </remarks>
/// <param name="multiThreaded">Whether this is a multithreaded (/mt) build, as determined by
/// <see cref="IsMultiThreadedRequested"/>.</param>
/// <param name="serverEnableReason">Telemetry-friendly reason: "EnvVar", "ImpliedByMt", or empty when not enabled.</param>
/// <returns>True if server should be used.</returns>
private static bool ShouldUseMSBuildServer(bool multiThreaded, out string serverEnableReason)
{
serverEnableReason = string.Empty;
string envVar = Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName);

if (envVar == "1")
{
serverEnableReason = "EnvVar";
return true;
}

// Explicit opt-out: MSBUILDUSESERVER=0 always wins, even if -mt is on the command line.
if (envVar == "0")
{
Comment thread
JanProvaznik marked this conversation as resolved.
return false;
}

// Implicit opt-in via -mt.
if (multiThreaded)
{
Comment thread
JanProvaznik marked this conversation as resolved.
serverEnableReason = "ImpliedByMt";
return true;
}
Comment thread
JanProvaznik marked this conversation as resolved.
Outdated

return false;
}

/// <summary>
/// Determines whether this invocation requests a multithreaded (/mt) build. This single
/// determination feeds both the implicit server opt-in (<see cref="ShouldUseMSBuildServer"/>)
/// and the server's Server GC decision (passed to <see cref="MSBuildClientApp.Execute(string[], bool, System.Threading.CancellationToken)"/>).
/// </summary>
/// <remarks>
/// Uses a lightweight command-line scan (plus the <c>MSBUILDFORCEMULTITHREADED</c> opt-in) rather
/// than the full <c>GatherAllSwitches</c> parse: this runs on every invocation (including the
/// common no-server path), and the full parse is expensive and already runs later for the actual
/// build. Intentionally conservative β€” returns false on any parse problem, which only declines the
/// implicit server opt-in; the explicit <c>MSBUILDUSESERVER=1</c> path is unaffected. A consequence
/// is that <c>-mt</c> supplied via a response file is not detected here.
/// </remarks>
/// <param name="args">Raw command-line arguments.</param>
/// <returns>True if this is a multithreaded build.</returns>
private static bool IsMultiThreadedRequested(string[] args)
{
if (Traits.Instance.ForceMultiThreaded)
{
Comment thread
JanProvaznik marked this conversation as resolved.
Outdated
return true;
}

if (args is null)
{
return false;
}

foreach (string arg in args)
{
if (string.IsNullOrEmpty(arg) || arg.Length < 2)
{
continue;
Comment thread
JanProvaznik marked this conversation as resolved.
Outdated
}

// Switches start with - or / (and on Unix, - is the only convention).
char prefix = arg[0];
if (prefix != '-' && prefix != '/')
{
continue;
}

// Strip leading - or /, then split on : to get the switch name (ignore any value/parameters).
string body = arg.Substring(1);
int colonIndex = body.IndexOf(':');
Comment thread
JanProvaznik marked this conversation as resolved.
Outdated
string switchName = colonIndex >= 0 ? body.Substring(0, colonIndex) : body;

if (switchName.Equals("mt", StringComparison.OrdinalIgnoreCase) ||
switchName.Equals("multithreaded", StringComparison.OrdinalIgnoreCase))
{
// Honor an explicit ":false" value if present (matches ProcessBooleanSwitch semantics).
if (colonIndex >= 0)
{
string value = body.Substring(colonIndex + 1);
if (value.Equals("false", StringComparison.OrdinalIgnoreCase) ||
value.Equals("0", StringComparison.OrdinalIgnoreCase))
Comment thread
JanProvaznik marked this conversation as resolved.
Outdated
{
return false;
}
}

return true;
}
}

return false;
}

/// <summary>
/// Append output file with elapsedTime
/// </summary>
Expand Down
Loading