Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Startup performance measure tool #851

Merged
merged 11 commits into from
Aug 14, 2020
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<OutputType>Exe</OutputType>
<AssemblyOriginatorKeyFile>dotvvmwizard.snk</AssemblyOriginatorKeyFile>
<SignAssembly>true</SignAssembly>
<PackAsTool>true</PackAsTool>
<ToolCommandName>dotvvm-startup-perf</ToolCommandName>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<PackageId>DotVVM.Tools.StartupPerf</PackageId>
<PackageVersion>2.4.0.2-preview02-76679</PackageVersion>
<Authors>RIGANTI</Authors>
<Description>Command-line tool for measuring startup performance of DotVVM apps.</Description>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>dotvvm;asp.net;mvvm;owin;dotnetcore;dnx;cli</PackageTags>
<PackageIconUrl>https://dotvvm.com/Content/images/icons/icon-blue-64x64.png</PackageIconUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="system.commandline" Version="2.0.0-beta1.20253.1" />
</ItemGroup>
</Project>
31 changes: 31 additions & 0 deletions src/DotVVM.Framework.StartupPerfTests/FileSystemHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.IO;

namespace DotVVM.Framework.StartupPerfTests
{
static internal class FileSystemHelper
{
public static string CreateTempDir()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
return tempDir;
}

public static bool RemoveDir(string dir)
{
try
{
if (Directory.Exists(dir))
{
Directory.Delete(dir, true);
}
return true;
}
catch (Exception)
Copy link
Member

Choose a reason for hiding this comment

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

We should only catch DirectoryNotFoundException. If there is another problem (like, file with the same name exists, runtime error, ... it should still fail)

Copy link
Member Author

Choose a reason for hiding this comment

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

The catch was here actually for IOException and UnauthorizedAccessException - I am using it only to delete a temp folder and in general, it's not a big deal if it fails.
I've added a retry logic - sometimes it failed to delete it because some files were still locked.

{
return false;
}
}
}
}
38 changes: 38 additions & 0 deletions src/DotVVM.Framework.StartupPerfTests/NetworkingHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Net;
using System.Net.Sockets;

namespace DotVVM.Framework.StartupPerfTests
{
public static class NetworkingHelpers
{
public static int FindRandomPort()
{
int port;
var random = new Random();
do
{
port = 60000 + random.Next(5000);
} while (!TestPort(port));

return port;
}

private static bool TestPort(int port)
{
using (var client = new TcpClient())
{
try
{
client.Connect(new IPEndPoint(IPAddress.Loopback, port));
client.Close();
return false;
}
catch (Exception)
{
return true;
}
}
}
}
}
63 changes: 63 additions & 0 deletions src/DotVVM.Framework.StartupPerfTests/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Diagnostics;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading;
using Process = System.Diagnostics.Process;

namespace DotVVM.Framework.StartupPerfTests
{
class Program
{
private static TextWriter logWriter;

static void Main(string[] args)
{
var rootCommand = new RootCommand
{
new Argument<FileInfo>(
"project",
"Path to the project file"
) { Arity = ArgumentArity.ExactlyOne },
new Option<TestTarget>(
new [] { "-t", "--type" },
"Type of the project - use 'owin' or 'aspnetcore'"
) { Required = true },
new Option<int>(
new [] { "-r", "--repeat" },
() => 1,
"How many times the operation should be repeated."
),
new Option<string>(
new [] { "-u", "--url" },
() => "",
"Relative URL in the app that should be tested."
),
new Option<bool>(
new [] { "-v", "--verbose" },
() => false,
"Diagnostics output"
)
};
rootCommand.Handler = CommandHandler.Create<FileInfo, TestTarget, int, string, bool>((project, type, repeat, url, verbose) =>
{
new StartupPerformanceTest(project, type, repeat, url, verbose).HandleCommand();
});
rootCommand.Invoke(args);
}
}

public enum TestTarget
{
Owin,
AspNetCore
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"profiles": {
"DotVVM.Framework.StartupPerfTests": {
"commandName": "Project",
"commandLineArgs": "-t owin -r 1"
}
}
}
153 changes: 153 additions & 0 deletions src/DotVVM.Framework.StartupPerfTests/StartupPerformanceTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading;

namespace DotVVM.Framework.StartupPerfTests
{
public class StartupPerformanceTest
{
private readonly FileInfo project;
private readonly TestTarget type;
private readonly int repeat;
private readonly string url;
private readonly bool verbose;

public StartupPerformanceTest(FileInfo project, TestTarget type, int repeat, string url, bool verbose)
{
this.project = project;
this.type = type;
this.repeat = repeat;
this.url = url;
this.verbose = verbose;
}


public void HandleCommand()
{
// test project existence
var projectPath = project.FullName;
if (!project.Exists)
{
throw new Exception($"The project {projectPath} doesn't exist!");
}

// find a random port
var port = NetworkingHelpers.FindRandomPort();
var urlToTest = $"http://localhost:{port}/{url.TrimStart('/')}";

// prepare directories
var dir = Path.GetDirectoryName(projectPath);
TraceOutput($"Project dir: {dir}");
var tempDir = FileSystemHelper.CreateTempDir();
TraceOutput($"Temp dir: {tempDir}");

long measuredTime = 0;
if (type == TestTarget.Owin)
{
// OWIN
TraceOutput($"Publishing...");
RunProcessAndWait(@"c:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe", @$"""{Path.GetFileName(projectPath)}"" /p:DeployOnBuild=true /t:WebPublish /p:WebPublishMethod=FileSystem /p:publishUrl=""{tempDir}""", dir);

for (var i = 0; i < repeat; i++)
{
TraceOutput($"Attempt #{i + 1}");
measuredTime += RunProcessAndWaitForHealthCheck(@"C:\Program Files (x86)\IIS Express\iisexpress.exe", $@"""/path:{dir}"" /port:{port}", dir, urlToTest);
}
}
else if (type == TestTarget.AspNetCore)
{
// ASP.NET Core
TraceOutput($"Publishing...");
RunProcessAndWait(@"dotnet", $@"publish -c Release -o ""{tempDir}""", dir);

for (var i = 0; i < repeat; i++)
{
TraceOutput($"Attempt #{i + 1}");
measuredTime += RunProcessAndWaitForHealthCheck(@"dotnet", $@"""./{Path.GetFileNameWithoutExtension(projectPath)}.dll"" --urls {urlToTest}", tempDir, urlToTest);
Copy link
Member

Choose a reason for hiding this comment

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

Unfortunately, the .NET API for starting processes is terrible, I'd recommend using https://github.com/madelson/MedallionShell, it handles the argument escaping well.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the tip, it is a nice library.

}
}
else
{
throw new NotSupportedException();
}

TraceOutput($"Average time: {measuredTime / repeat}");

TraceOutput($"Removing temp dir...");
FileSystemHelper.RemoveDir(tempDir);
TraceOutput($"Done");
}

private void RunProcessAndWait(string path, string arguments, string workingDirectory)
{
TraceOutput($"Running {path} {arguments}");

var psi = new ProcessStartInfo(path, arguments) {
CreateNoWindow = true,
UseShellExecute = false,
WindowStyle = ProcessWindowStyle.Hidden,
WorkingDirectory = workingDirectory
};
var process = Process.Start(psi);
process.WaitForExit();
if (process.ExitCode != 0)
{
throw new Exception($"Process exited with code {process.ExitCode}!");
}
}

private long RunProcessAndWaitForHealthCheck(string path, string arguments, string workingDirectory, string urlToTest)
{
var psi = new ProcessStartInfo(path, arguments) {
CreateNoWindow = true,
UseShellExecute = false,
WindowStyle = ProcessWindowStyle.Hidden,
WorkingDirectory = workingDirectory
};
var process = Process.Start(psi);

var sw = new Stopwatch();
sw.Start();

retry:
try
{
var wc = new WebClient();
var response = wc.DownloadString(urlToTest);
}
catch (WebException ex) when (ex.InnerException is HttpRequestException hrex && (hrex.InnerException is IOException || hrex.InnerException is SocketException))
{
Thread.Sleep(100);
goto retry;
Copy link
Member

Choose a reason for hiding this comment

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

image

🙂

Copy link
Member Author

Choose a reason for hiding this comment

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

Added the timeout option (with a default value of 10 seconds). If the HTTP port doesn't open within the timeout, an exception is thrown.

}

if (process.HasExited)
{
throw new Exception("The process has exited!");
Copy link
Member

Choose a reason for hiding this comment

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

The check should be inside the retry loop

}

var time = sw.ElapsedMilliseconds;
ImportantOutput(time.ToString());

process.Kill();
return time;
}

public void ImportantOutput(string message)
{
Console.WriteLine(message);
}

public void TraceOutput(string message)
{
if (verbose)
{
Console.WriteLine(message);
}
}
}
}
16 changes: 16 additions & 0 deletions src/DotVVM.Framework.StartupPerfTests/TestBasicSamples.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Prepare temp content directory
$temp = [System.IO.Path]::GetTempPath()
if (Test-Path $temp/DotVVM.Samples.Common) {
rmdir $temp/DotVVM.Samples.Common -Recurse -Force
}
mkdir $temp/DotVVM.Samples.Common | out-null
copy ../DotVVM.Samples.Common/Content -Destination $temp/DotVVM.Samples.Common -Recurse -Force
copy ../DotVVM.Samples.Common/Scripts -Destination $temp/DotVVM.Samples.Common -Recurse -Force
copy ../DotVVM.Samples.Common/Views -Destination $temp/DotVVM.Samples.Common -Recurse -Force
copy ../DotVVM.Samples.Common/sampleConfig.json -Destination $temp/DotVVM.Samples.Common -Force

# Run OWIN tests
./bin/Debug/netcoreapp3.0/DotVVM.Framework.StartupPerfTests.exe ../DotVVM.Samples.BasicSamples.Owin/DotVVM.Samples.BasicSamples.Owin.csproj -t owin -v -r 5

# Run ASP.NET Core tests
./bin/Debug/netcoreapp3.0/DotVVM.Framework.StartupPerfTests.exe ../DotVVM.Samples.BasicSamples.AspNetCoreLatest/DotVVM.Samples.BasicSamples.AspNetCoreLatest.csproj -t aspnetcore -v -r 5
Binary file not shown.
15 changes: 15 additions & 0 deletions src/DotVVM.sln
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotVVM.Framework.Api.Swashb
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotVVM.Samples.BasicSamples.AspNetCoreLatest", "DotVVM.Samples.BasicSamples.AspNetCoreLatest\DotVVM.Samples.BasicSamples.AspNetCoreLatest.csproj", "{C10AC84A-B618-4729-8F83-0464E32BA6B5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotVVM.Framework.StartupPerfTests", "DotVVM.Framework.StartupPerfTests\DotVVM.Framework.StartupPerfTests.csproj", "{18A866FA-3ED8-4345-8AAD-93000CC03CF0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -436,6 +438,18 @@ Global
{C10AC84A-B618-4729-8F83-0464E32BA6B5}.Release|x64.Build.0 = Release|Any CPU
{C10AC84A-B618-4729-8F83-0464E32BA6B5}.Release|x86.ActiveCfg = Release|Any CPU
{C10AC84A-B618-4729-8F83-0464E32BA6B5}.Release|x86.Build.0 = Release|Any CPU
{18A866FA-3ED8-4345-8AAD-93000CC03CF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{18A866FA-3ED8-4345-8AAD-93000CC03CF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{18A866FA-3ED8-4345-8AAD-93000CC03CF0}.Debug|x64.ActiveCfg = Debug|Any CPU
{18A866FA-3ED8-4345-8AAD-93000CC03CF0}.Debug|x64.Build.0 = Debug|Any CPU
{18A866FA-3ED8-4345-8AAD-93000CC03CF0}.Debug|x86.ActiveCfg = Debug|Any CPU
{18A866FA-3ED8-4345-8AAD-93000CC03CF0}.Debug|x86.Build.0 = Debug|Any CPU
{18A866FA-3ED8-4345-8AAD-93000CC03CF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{18A866FA-3ED8-4345-8AAD-93000CC03CF0}.Release|Any CPU.Build.0 = Release|Any CPU
{18A866FA-3ED8-4345-8AAD-93000CC03CF0}.Release|x64.ActiveCfg = Release|Any CPU
{18A866FA-3ED8-4345-8AAD-93000CC03CF0}.Release|x64.Build.0 = Release|Any CPU
{18A866FA-3ED8-4345-8AAD-93000CC03CF0}.Release|x86.ActiveCfg = Release|Any CPU
{18A866FA-3ED8-4345-8AAD-93000CC03CF0}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -464,6 +478,7 @@ Global
{7E3DCB6E-CEE8-4760-B19A-E23250DB7CC1} = {B760209E-57D7-4AEF-A726-69F73BBE52C0}
{BD72F6FC-A6CA-4B10-8098-3D27693E25BA} = {BA93154A-93F1-4302-A994-824693AE7C46}
{C10AC84A-B618-4729-8F83-0464E32BA6B5} = {13011738-A4B3-47B0-9E50-C0323EB19F76}
{18A866FA-3ED8-4345-8AAD-93000CC03CF0} = {85FD5610-F03A-4BB7-806E-5EE790042331}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BD6D794B-1553-4534-83EA-14041C0F7F34}
Expand Down
1 change: 1 addition & 0 deletions src/Tools/build/publish.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ $packages = @(
[pscustomobject]@{ Package = "DotVVM.Owin"; Directory = "DotVVM.Framework.Hosting.Owin" },
[pscustomobject]@{ Package = "DotVVM.AspNetCore"; Directory = "DotVVM.Framework.Hosting.AspNetCore" },
[pscustomobject]@{ Package = "DotVVM.CommandLine"; Directory = "DotVVM.CommandLine" },
[pscustomobject]@{ Package = "DotVVM.Tools.StartupPerf"; Directory = "DotVVM.Framework.StartupPerfTests" },
[pscustomobject]@{ Package = "DotVVM.Compiler.Light"; Directory = "DotVVM.Compiler.Light" },
[pscustomobject]@{ Package = "DotVVM.Api.Swashbuckle.AspNetCore"; Directory = "DotVVM.Framework.Api.Swashbuckle.AspNetCore" },
[pscustomobject]@{ Package = "DotVVM.Api.Swashbuckle.Owin"; Directory = "DotVVM.Framework.Api.Swashbuckle.Owin" }
Expand Down