Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
98 changes: 98 additions & 0 deletions src/MSBuild.Benchmarks/MSBuildServerStartupBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Globalization;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;

namespace MSBuild.Benchmarks;

/// <summary>
/// Measures an end-to-end cold server-backed build. Each invocation uses a unique handshake salt and disables
/// node reuse so it cannot reuse an existing server and the server exits after the build.
/// </summary>
public class MSBuildServerStartupBenchmark
{
private const string MSBuildPathEnvironmentVariable = "MSBUILD_SERVER_BENCHMARK_PATH";

private string _msbuildPath = null!;
private string _projectPath = null!;
private string _processIdPath = null!;

[GlobalSetup]
public void GlobalSetup()
{
_msbuildPath = Environment.GetEnvironmentVariable(MSBuildPathEnvironmentVariable)
?? throw new InvalidOperationException(
$"Set {MSBuildPathEnvironmentVariable} to the MSBuild executable produced by the same build as this benchmark.");

if (!File.Exists(_msbuildPath))
{
throw new FileNotFoundException("The MSBuild server benchmark executable was not found.", _msbuildPath);
}

_projectPath = Path.Combine(Path.GetTempPath(), $"msbuild-server-benchmark-{Guid.NewGuid():N}.proj");
_processIdPath = Path.ChangeExtension(_projectPath, ".pid");
File.WriteAllText(
_projectPath,
"""
<Project>
<Target Name="Build">
<WriteLinesToFile
File="$(BenchmarkProcessIdFile)"
Lines="$([System.Diagnostics.Process]::GetCurrentProcess().Id)"
Overwrite="true" />
</Target>
</Project>
""");
}

[GlobalCleanup]
public void GlobalCleanup()
{
File.Delete(_projectPath);
File.Delete(_processIdPath);
}

[Benchmark]
public void ColdServerBuild() => RunBuild();

private void RunBuild()
{
File.Delete(_processIdPath);

ProcessStartInfo startInfo = new()
{
FileName = _msbuildPath,
Arguments = $"\"{_projectPath}\" -nologo -verbosity:quiet -mt -nodeReuse:false -p:BenchmarkProcessIdFile=\"{_processIdPath}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
};
startInfo.Environment["MSBUILDUSESERVER"] = "1";
startInfo.Environment["MSBUILDNODEHANDSHAKESALT"] = Guid.NewGuid().ToString("N");
startInfo.Environment["MSBUILDENABLEALLPROPERTYFUNCTIONS"] = "1";

using Process process = Process.Start(startInfo)
?? throw new InvalidOperationException("Failed to start MSBuild.");
Task<string> outputTask = process.StandardOutput.ReadToEndAsync();
Task<string> errorTask = process.StandardError.ReadToEndAsync();
process.WaitForExit();
string output = outputTask.GetAwaiter().GetResult();
string error = errorTask.GetAwaiter().GetResult();
Comment on lines +65 to +84

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.

We're moving to target .NET 11 in Viktor's branch - suggest having the benchmarks target that and using the new One-shot process APIs to make sure you're not artificially stalling the stdout/stderr-draining worker threads.


if (process.ExitCode != 0)
{
throw new InvalidOperationException(
$"MSBuild exited with code {process.ExitCode}.{Environment.NewLine}{output}{Environment.NewLine}{error}");
}

int buildProcessId = int.Parse(File.ReadAllText(_processIdPath), CultureInfo.InvariantCulture);
if (buildProcessId == process.Id)
{
throw new InvalidOperationException("MSBuild fell back to an in-process build instead of using the server.");
}
}
}
45 changes: 45 additions & 0 deletions src/MSBuild.UnitTests/XMake_BinlogSwitch_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Build.CommandLine.Experimental;
using Microsoft.Build.Execution;
using Microsoft.Build.Shared;
using Microsoft.Build.UnitTests.Shared;
using Shouldly;
using Xunit;
Expand Down Expand Up @@ -206,6 +209,48 @@ public void LoggingArgsEnvVarDefaultLevelEmitsWarnings()
output.ShouldContain("MSB1070");
}

[Fact]
public void LoggingArgsEnvVarWarningUsesConfiguredUILanguage()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
!EncodingUtilities.CurrentPlatformIsWindowsAndOfficiallySupportsUTF8Encoding())
{
return;
}

var directory = _env.CreateFolder();
string content = ObjectModelHelpers.CleanupFileContents("<Project><Target Name='t' /></Project>");
string projectPath = directory.CreateFile("my.proj", content).Path;

_env.SetEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "ja");
_env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", "-maxcpucount:4");

string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out bool successfulExit, _output);
successfulExit.ShouldBeTrue(output);

CultureInfo originalUICulture = CultureInfo.CurrentUICulture;
string expectedMessage;
string englishMessage;
try
{
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("ja");
expectedMessage = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(
"LoggingArgsEnvVarUnsupportedArgument",
"-maxcpucount:4");
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en");
englishMessage = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(
"LoggingArgsEnvVarUnsupportedArgument",
"-maxcpucount:4");
}
finally
{
CultureInfo.CurrentUICulture = originalUICulture;
}

expectedMessage.ShouldNotBe(englishMessage);
output.ShouldContain(expectedMessage);
}

/// <summary>
/// Test that empty or whitespace MSBUILD_LOGGING_ARGS is ignored.
/// </summary>
Expand Down
Loading