Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
17 changes: 17 additions & 0 deletions documentation/specs/multithreading/multithreaded-msbuild.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,20 @@ 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. Setting `MSBUILDUSESERVER` to any explicit value opts out of the implicit path: `1` keeps the server on, and any other value (e.g. `0`, `false`) keeps it off β€” only `1` engages the server, matching the pre-existing rule. `-mt` is itself experimental and opt-in, so the implicit server engagement is not separately gated.

| `MSBUILDUSESERVER` | `-mt` on command line | Server engaged? | Telemetry `ServerEnableReason` |
|---|---|---|---|
| `1` | (any) | Yes | `EnvVar` |
| any other non-empty value (`0`, `false`, …) | (any) | No (explicit opt-out) | β€” |
| unset | yes | **Yes** | `ImpliedByMt` |
| unset | no | No | β€” |

Two `-mt` determinations are used, with distinct purposes:
- A cheap command-line scan (`IsMultiThreadedRequested`) drives the *implicit server opt-in* above. It runs on every invocation, so it avoids the full command-line parse; consequently `-mt` from a response file does not trigger the implicit opt-in, and `MSBUILDFORCEMULTITHREADED` (a force/testing knob) does **not** by itself engage the server.
Comment thread
JanProvaznik marked this conversation as resolved.
Outdated
- The authoritative full parse (`IsMultiThreadedEnabled`, which expands response files and honors `MSBUILDFORCEMULTITHREADED`) decides, once the server is engaged, whether the server process is launched with [Server GC](../../MSBuild-Server.md#garbage-collection).


3 changes: 2 additions & 1 deletion src/Build/BackEnd/Client/MSBuildClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,8 @@ private ServerNodeBuildCommand GetServerNodeBuildCommand()
: new PartialBuildTelemetry(
startedAt: KnownTelemetry.PartialBuildTelemetry.StartAt.GetValueOrDefault(),
initialServerState: KnownTelemetry.PartialBuildTelemetry.InitialMSBuildServerState,
serverFallbackReason: KnownTelemetry.PartialBuildTelemetry.ServerFallbackReason);
serverFallbackReason: KnownTelemetry.PartialBuildTelemetry.ServerFallbackReason,
serverEnableReason: KnownTelemetry.PartialBuildTelemetry.ServerEnableReason);

return new ServerNodeBuildCommand(
_commandLine,
Expand Down
1 change: 1 addition & 0 deletions src/Build/BackEnd/Node/OutOfProcServerNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ private void HandleServerNodeBuildCommand(ServerNodeBuildCommand command)
buildTelemetry.StartAt = command.PartialBuildTelemetry.StartedAt;
buildTelemetry.InitialMSBuildServerState = command.PartialBuildTelemetry.InitialServerState;
buildTelemetry.ServerFallbackReason = command.PartialBuildTelemetry.ServerFallbackReason;
buildTelemetry.ServerEnableReason = command.PartialBuildTelemetry.ServerEnableReason;
}

// Also try our best to increase chance custom Loggers which use Console static members will work as expected.
Expand Down
7 changes: 6 additions & 1 deletion src/Build/BackEnd/Node/PartialBuildTelemetry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ internal sealed class PartialBuildTelemetry : ITranslatable
private DateTime _startedAt = default;
private string? _initialServerState = default;
private string? _serverFallbackReason = default;
private string? _serverEnableReason = default;

public PartialBuildTelemetry(DateTime startedAt, string? initialServerState, string? serverFallbackReason)
public PartialBuildTelemetry(DateTime startedAt, string? initialServerState, string? serverFallbackReason, string? serverEnableReason)
{
_startedAt = startedAt;
_initialServerState = initialServerState;
_serverFallbackReason = serverFallbackReason;
_serverEnableReason = serverEnableReason;
}

/// <summary>
Expand All @@ -35,11 +37,14 @@ private PartialBuildTelemetry()

public string? ServerFallbackReason => _serverFallbackReason;

public string? ServerEnableReason => _serverEnableReason;

public void Translate(ITranslator translator)
{
translator.Translate(ref _startedAt);
translator.Translate(ref _initialServerState);
translator.Translate(ref _serverFallbackReason);
translator.Translate(ref _serverEnableReason);
}

internal static PartialBuildTelemetry FactoryForDeserialization(ITranslator translator)
Expand Down
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
34 changes: 34 additions & 0 deletions src/MSBuild.UnitTests/MSBuildServer_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,40 @@ 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, and isolate this
// test's server with a unique handshake salt so it can't reuse/shut down an unrelated server.
_env.SetEnvironmentVariable("MSBUILDUSESERVER", null);
_env.SetEnvironmentVariable("MSBUILDNODEHANDSHAKESALT", Guid.NewGuid().ToString("N"));

// Make sure we start with no server running.
MSBuildClient.ShutdownServer(CancellationToken.None);

string output1 = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path + " -mt", out bool success1, false, _output);
success1.ShouldBeTrue();
int serverPid1 = ParseNumber(output1, "Server ID is ");
// Register cleanup before any assertion so the server does not leak if an assertion throws.
_env.WithTransientProcess(serverPid1);

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 + ".");

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

[Fact]
public void PropertyMSBuildStartupDirectoryOnServer()
{
Expand Down
55 changes: 55 additions & 0 deletions src/MSBuild.UnitTests/XMake_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3217,6 +3217,61 @@ public void MSBuildForceMultiThreadedEnvironmentVariableNonOneValueDoesNotEnable
MSBuildApp.IsMultiThreadedEnabled(switches).ShouldBeFalse();
}

[Theory]
[InlineData(new[] { "proj.proj", "-mt" }, true)]
[InlineData(new[] { "proj.proj", "/mt" }, true)]
[InlineData(new[] { "proj.proj", "-multithreaded" }, true)]
[InlineData(new[] { "proj.proj", "-mt:true" }, true)]
[InlineData(new[] { "proj.proj", "-mt:false" }, false)]
[InlineData(new[] { "proj.proj", "-mt:0" }, false)]
[InlineData(new[] { "proj.proj", "-mt:" }, true)]
[InlineData(new[] { "proj.proj" }, false)]
[InlineData(new[] { "proj.proj", "-multiproc" }, false)]
// Last occurrence wins, matching MSBuild's boolean-switch semantics.
[InlineData(new[] { "proj.proj", "-mt:false", "-mt" }, true)]
[InlineData(new[] { "proj.proj", "-mt", "-mt:false" }, false)]
public void IsMultiThreadedRequestedDetectsCommandLineSwitch(string[] args, bool expected)
{
// MSBUILDFORCEMULTITHREADED must NOT influence the command-line detection used for the
// implicit server opt-in.
using TestEnvironment testEnvironment = TestEnvironment.Create();
testEnvironment.SetEnvironmentVariable("MSBUILDFORCEMULTITHREADED", null);

MSBuildApp.IsMultiThreadedRequested(args).ShouldBe(expected);
}

[Fact]
public void IsMultiThreadedRequestedIgnoresForceMultiThreadedEnvVar()
{
// The force/testing knob should not by itself imply the command-line -mt switch.
using TestEnvironment testEnvironment = TestEnvironment.Create();
testEnvironment.SetEnvironmentVariable("MSBUILDFORCEMULTITHREADED", "1");

MSBuildApp.IsMultiThreadedRequested(["proj.proj"]).ShouldBeFalse();
}

[Theory]
// MSBUILDUSESERVER value, -mt on command line, expected server use, expected reason.
[InlineData("1", false, true, "EnvVar")]
[InlineData("1", true, true, "EnvVar")]
[InlineData("0", true, false, "")]
[InlineData("0", false, false, "")]
[InlineData("false", true, false, "")] // any explicit non-"1" value opts out
[InlineData("true", true, false, "")]
[InlineData(null, true, true, "ImpliedByMt")]
[InlineData(null, false, false, "")]
[InlineData("", true, true, "ImpliedByMt")] // empty is treated as unset
public void ShouldUseMSBuildServerDecisionTree(string useServerValue, bool isMtOnCommandLine, bool expectedUseServer, string expectedReason)
{
using TestEnvironment testEnvironment = TestEnvironment.Create();
testEnvironment.SetEnvironmentVariable("MSBUILDUSESERVER", useServerValue);

bool useServer = MSBuildApp.ShouldUseMSBuildServer(isMtOnCommandLine, out string reason);

useServer.ShouldBe(expectedUseServer);
reason.ShouldBe(expectedReason);
}

private string CopyMSBuild()
{
string dest = null;
Expand Down
Loading
Loading