Parcourir la source

Integration benchmark tests (#1298)

* Integration benchmark tests

* Fixes

* Write with encoding

* Update src/Renci.SshNet/Common/SshDataStream.cs

Co-authored-by: Rob Hague <rob.hague00@gmail.com>

* Add more benchmarks

---------

Co-authored-by: Rob Hague <rob.hague00@gmail.com>
Wojciech Nagórski il y a 1 an
Parent
commit
0371af9aaf

+ 22 - 0
Renci.SshNet.sln

@@ -88,6 +88,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "svg", "svg", "{92E7B1B8-4C7
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Renci.SshNet.Benchmarks", "test\Renci.SshNet.Benchmarks\Renci.SshNet.Benchmarks.csproj", "{CF6CA77F-E4B8-4522-B267-E3F555E2E7B1}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Renci.SshNet.IntegrationBenchmarks", "test\Renci.SshNet.IntegrationBenchmarks\Renci.SshNet.IntegrationBenchmarks.csproj", "{6DFC1807-3F44-4302-A302-43F7D887C4E0}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -196,6 +198,26 @@ Global
 		{CF6CA77F-E4B8-4522-B267-E3F555E2E7B1}.Release|x64.Build.0 = Release|Any CPU
 		{CF6CA77F-E4B8-4522-B267-E3F555E2E7B1}.Release|x86.ActiveCfg = Release|Any CPU
 		{CF6CA77F-E4B8-4522-B267-E3F555E2E7B1}.Release|x86.Build.0 = Release|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Debug|ARM.ActiveCfg = Debug|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Debug|ARM.Build.0 = Debug|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Debug|x64.Build.0 = Debug|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Debug|x86.Build.0 = Debug|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Release|Any CPU.Build.0 = Release|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Release|ARM.ActiveCfg = Release|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Release|ARM.Build.0 = Release|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Release|x64.ActiveCfg = Release|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Release|x64.Build.0 = Release|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Release|x86.ActiveCfg = Release|Any CPU
+		{6DFC1807-3F44-4302-A302-43F7D887C4E0}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 9 - 1
src/Renci.SshNet/Common/SshDataStream.cs

@@ -126,9 +126,17 @@ namespace Renci.SshNet.Common
             {
                 throw new ArgumentNullException(nameof(encoding));
             }
-
+#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
+            ReadOnlySpan<char> value = s;
+            var count = encoding.GetByteCount(value);
+            var bytes = count <= 256 ? stackalloc byte[count] : new byte[count];
+            encoding.GetBytes(value, bytes);
+            Write((uint) count);
+            Write(bytes);
+#else
             var bytes = encoding.GetBytes(s);
             WriteBinary(bytes, 0, bytes.Length);
+#endif
         }
 
         /// <summary>

+ 1 - 0
test/Renci.SshNet.Benchmarks/Renci.SshNet.Benchmarks.csproj

@@ -17,4 +17,5 @@
   <ItemGroup>
     <EmbeddedResource Include="..\Data\*" LinkBase="Data" />
   </ItemGroup>
+
 </Project>

+ 72 - 0
test/Renci.SshNet.IntegrationBenchmarks/.editorconfig

@@ -0,0 +1,72 @@
+[*.cs]
+
+#### Sonar rules ####
+
+# S125: Sections of code should not be commented out
+https://rules.sonarsource.com/csharp/RSPEC-125/
+dotnet_diagnostic.S125.severity = suggestion
+
+# S1118: Utility classes should not have public constructors
+# https://rules.sonarsource.com/csharp/RSPEC-1118/
+dotnet_diagnostic.S1118.severity = suggestion
+
+# S1450: Private fields only used as local variables in methods should become local variables
+# https://rules.sonarsource.com/csharp/RSPEC-1450/
+dotnet_diagnostic.S1450.severity = suggestion
+
+# S4144: Methods should not have identical implementations
+# https://rules.sonarsource.com/csharp/RSPEC-4144/
+dotnet_diagnostic.S4144.severity = suggestion
+
+#### SYSLIB diagnostics ####
+
+
+#### StyleCop Analyzers rules ####
+
+# SA1028: Code must not contain trailing whitespace
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1028.md
+dotnet_diagnostic.SA1028.severity = suggestion
+
+# SA1414: Tuple types in signatures should have element names
+https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1414.md
+dotnet_diagnostic.SA1414.severity = suggestion
+
+# SA1400: Access modifier must be declared
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1400.md
+dotnet_diagnostic.SA1400.severity = suggestion
+
+# SA1401: Fields must be private
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md
+dotnet_diagnostic.SA1401.severity = suggestion
+
+# SA1411: Attribute constructor must not use unnecessary parenthesis
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1411.md
+dotnet_diagnostic.SA1411.severity = suggestion
+
+# SA1505: Opening braces must not be followed by blank line
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1505.md
+dotnet_diagnostic.SA1505.severity = suggestion
+
+# SA1512: Single line comments must not be followed by blank line
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1512.md
+dotnet_diagnostic.SA1512.severity = suggestion
+
+# SA1513: Closing brace must be followed by blank line
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1513.md
+dotnet_diagnostic.SA1513.severity = suggestion
+
+#### Meziantou.Analyzer rules ####
+
+# MA0003: Add parameter name to improve readability
+# https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0003.md
+dotnet_diagnostic.MA0003.severity = suggestion
+
+# MA0053: Make class sealed
+# https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0053.md
+dotnet_diagnostic.MA0053.severity = suggestion
+
+#### .NET Compiler Platform analysers rules ####
+
+# CA2000: Dispose objects before losing scope
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2000
+dotnet_diagnostic.CA2000.severity = suggestion

+ 19 - 0
test/Renci.SshNet.IntegrationBenchmarks/IntegrationBenchmarkBase.cs

@@ -0,0 +1,19 @@
+using Renci.SshNet.IntegrationTests.TestsFixtures;
+
+namespace Renci.SshNet.IntegrationBenchmarks
+{
+    public class IntegrationBenchmarkBase
+    {
+#pragma warning disable CA1822 // Mark members as static
+        public async Task GlobalSetup()
+        {
+            await InfrastructureFixture.Instance.InitializeAsync().ConfigureAwait(false);
+        }
+
+        public async Task GlobalCleanup()
+        {
+            await InfrastructureFixture.Instance.DisposeAsync().ConfigureAwait(false);
+        }
+#pragma warning restore CA1822 // Mark members as static
+    }
+}

+ 12 - 0
test/Renci.SshNet.IntegrationBenchmarks/Program.cs

@@ -0,0 +1,12 @@
+using BenchmarkDotNet.Running;
+
+namespace Renci.SshNet.IntegrationBenchmarks
+{
+    public static class Program
+    {
+        public static void Main(string[] args)
+        {
+            _ = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
+        }
+    }
+}

+ 25 - 0
test/Renci.SshNet.IntegrationBenchmarks/Renci.SshNet.IntegrationBenchmarks.csproj

@@ -0,0 +1,25 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net8.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="BenchmarkDotNet" Version="0.13.9" />
+    <!-- <PackageReference Include="SSH.NET" Version="2023.0.0" /> -->
+    <PackageReference Include="SSH.NET" Version="2023.0.0" ExcludeAssets="All" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\Renci.SshNet\Renci.SshNet.csproj" />
+    <ProjectReference Include="..\Renci.SshNet.IntegrationTests\Renci.SshNet.IntegrationTests.csproj" />
+  </ItemGroup>
+    
+  <ItemGroup>
+    <EmbeddedResource Include="..\Data\*" LinkBase="Data" />
+  </ItemGroup>
+
+</Project>

+ 79 - 0
test/Renci.SshNet.IntegrationBenchmarks/ScpClientBenchmark.cs

@@ -0,0 +1,79 @@
+using System.Text;
+using BenchmarkDotNet.Attributes;
+
+using Renci.SshNet.IntegrationTests.TestsFixtures;
+
+namespace Renci.SshNet.IntegrationBenchmarks
+{
+    [MemoryDiagnoser]
+    [SimpleJob]
+    public class ScpClientBenchmark : IntegrationBenchmarkBase
+    {
+        private readonly InfrastructureFixture _infrastructureFixture;
+
+        private readonly string _file = $"/tmp/{Guid.NewGuid()}.txt";
+        private ScpClient? _scpClient;
+        private MemoryStream? _uploadStream;
+
+        public ScpClientBenchmark()
+        {
+            _infrastructureFixture = InfrastructureFixture.Instance;
+        }
+
+        [GlobalSetup]
+        public async Task Setup()
+        {
+            await GlobalSetup().ConfigureAwait(false);
+            _scpClient = new ScpClient(_infrastructureFixture.SshServerHostName, _infrastructureFixture.SshServerPort, _infrastructureFixture.User.UserName, _infrastructureFixture.User.Password);
+            await _scpClient.ConnectAsync(CancellationToken.None).ConfigureAwait(false);
+
+            var fileContent = "File content !@#$%^&*()_+{}:,./<>[];'\\|";
+            _uploadStream = new MemoryStream(Encoding.UTF8.GetBytes(fileContent));
+        }
+
+        [GlobalCleanup]
+        public async Task Cleanup()
+        {
+            await GlobalCleanup().ConfigureAwait(false);
+            await _uploadStream!.DisposeAsync().ConfigureAwait(false);
+        }
+
+        [Benchmark]
+        public void Connect()
+        {
+            using var scpClient = new ScpClient(_infrastructureFixture.SshServerHostName, _infrastructureFixture.SshServerPort, _infrastructureFixture.User.UserName, _infrastructureFixture.User.Password);
+            scpClient.Connect();
+        }
+
+        [Benchmark]
+        public async Task ConnectAsync()
+        {
+            using var scpClient = new ScpClient(_infrastructureFixture.SshServerHostName, _infrastructureFixture.SshServerPort, _infrastructureFixture.User.UserName, _infrastructureFixture.User.Password);
+            await scpClient.ConnectAsync(CancellationToken.None).ConfigureAwait(false);
+        }
+
+        [Benchmark]
+        public string ConnectUploadAndDownload()
+        {
+            using var scpClient = new ScpClient(_infrastructureFixture.SshServerHostName, _infrastructureFixture.SshServerPort, _infrastructureFixture.User.UserName, _infrastructureFixture.User.Password);
+            scpClient.Connect();
+            _uploadStream!.Position = 0;
+            scpClient.Upload(_uploadStream, _file);
+            using var downloadStream = new MemoryStream();
+            scpClient.Download(_file, downloadStream);
+
+            return Encoding.UTF8.GetString(downloadStream.ToArray());
+        }
+
+        [Benchmark]
+        public string UploadAndDownload()
+        {
+            _uploadStream!.Position = 0;
+            _scpClient!.Upload(_uploadStream, _file);
+            using var downloadStream = new MemoryStream();
+            _scpClient.Download(_file, downloadStream);
+
+            return Encoding.UTF8.GetString(downloadStream.ToArray());
+        }
+    }
+}

+ 78 - 0
test/Renci.SshNet.IntegrationBenchmarks/SftpClientBenchmark.cs

@@ -0,0 +1,78 @@
+using System.Text;
+
+using BenchmarkDotNet.Attributes;
+
+using Renci.SshNet.IntegrationTests.TestsFixtures;
+using Renci.SshNet.Sftp;
+
+namespace Renci.SshNet.IntegrationBenchmarks
+{
+    [MemoryDiagnoser]
+    [SimpleJob]
+    public class SftpClientBenchmark : IntegrationBenchmarkBase
+    {
+        private readonly InfrastructureFixture _infrastructureFixture;
+        private readonly string _file = $"/tmp/{Guid.NewGuid()}.txt";
+
+        private SftpClient? _sftpClient;
+        private MemoryStream? _uploadStream;
+
+        public SftpClientBenchmark()
+        {
+            _infrastructureFixture = InfrastructureFixture.Instance;
+        }
+
+        [GlobalSetup]
+        public async Task Setup()
+        {
+            await GlobalSetup().ConfigureAwait(false);
+            _sftpClient = new SftpClient(_infrastructureFixture.SshServerHostName, _infrastructureFixture.SshServerPort, _infrastructureFixture.User.UserName, _infrastructureFixture.User.Password);
+            await _sftpClient.ConnectAsync(CancellationToken.None).ConfigureAwait(false);
+
+            var fileContent = "File content !@#$%^&*()_+{}:,./<>[];'\\|";
+            _uploadStream = new MemoryStream(Encoding.UTF8.GetBytes(fileContent));
+        }
+
+        [GlobalCleanup]
+        public async Task Cleanup()
+        {
+            await GlobalCleanup().ConfigureAwait(false);
+            await _uploadStream!.DisposeAsync().ConfigureAwait(false);
+        }
+
+        [Benchmark]
+        public void Connect()
+        {
+            using var sftpClient = new SftpClient(_infrastructureFixture.SshServerHostName, _infrastructureFixture.SshServerPort, _infrastructureFixture.User.UserName, _infrastructureFixture.User.Password);
+            sftpClient.Connect();
+        }
+
+        [Benchmark]
+        public async Task ConnectAsync()
+        {
+            using var sftpClient = new SftpClient(_infrastructureFixture.SshServerHostName, _infrastructureFixture.SshServerPort, _infrastructureFixture.User.UserName, _infrastructureFixture.User.Password);
+            await sftpClient.ConnectAsync(CancellationToken.None).ConfigureAwait(false);
+        }
+
+        public IEnumerable<ISftpFile> ListDirectory()
+        {
+            return _sftpClient!.ListDirectory("/root");
+        }
+
+        public IAsyncEnumerable<ISftpFile> ListDirectoryAsync()
+        {
+            return _sftpClient!.ListDirectoryAsync("/root", CancellationToken.None);
+        }
+
+        [Benchmark]
+        public string UploadAndDownload()
+        {
+            _uploadStream!.Position = 0;
+            _sftpClient!.UploadFile(_uploadStream, _file);
+            using var downloadStream = new MemoryStream();
+            _sftpClient.DownloadFile(_file, downloadStream);
+
+            return Encoding.UTF8.GetString(downloadStream.ToArray());
+        }
+    }
+}

+ 69 - 0
test/Renci.SshNet.IntegrationBenchmarks/SshClientBenchmark.cs

@@ -0,0 +1,69 @@
+using BenchmarkDotNet.Attributes;
+
+using Renci.SshNet.IntegrationTests.TestsFixtures;
+
+namespace Renci.SshNet.IntegrationBenchmarks
+{
+    [MemoryDiagnoser]
+    [SimpleJob]
+    public class SshClientBenchmark : IntegrationBenchmarkBase
+    {
+        private readonly InfrastructureFixture _infrastructureFixture;
+        private SshClient? _sshClient;
+
+        public SshClientBenchmark()
+        {
+            _infrastructureFixture = InfrastructureFixture.Instance;
+        }
+
+        [GlobalSetup]
+        public async Task Setup()
+        {
+            await GlobalSetup().ConfigureAwait(false);
+            _sshClient = new SshClient(_infrastructureFixture.SshServerHostName, _infrastructureFixture.SshServerPort, _infrastructureFixture.User.UserName, _infrastructureFixture.User.Password);
+            await _sshClient.ConnectAsync(CancellationToken.None).ConfigureAwait(false);
+        }
+
+        [GlobalCleanup]
+        public async Task Cleanup()
+        {
+            await GlobalCleanup().ConfigureAwait(false);
+        }
+
+        [Benchmark]
+        public void Connect()
+        {
+            using var sshClient = new SshClient(_infrastructureFixture.SshServerHostName, _infrastructureFixture.SshServerPort, _infrastructureFixture.User.UserName, _infrastructureFixture.User.Password);
+            sshClient.Connect();
+        }
+
+        [Benchmark]
+        public async Task ConnectAsync()
+        {
+            using var sshClient = new SshClient(_infrastructureFixture.SshServerHostName, _infrastructureFixture.SshServerPort, _infrastructureFixture.User.UserName, _infrastructureFixture.User.Password);
+            await sshClient.ConnectAsync(CancellationToken.None).ConfigureAwait(false);
+        }
+
+        [Benchmark]
+        public string ConnectAndRunCommand()
+        {
+            using var sshClient = new SshClient(_infrastructureFixture.SshServerHostName, _infrastructureFixture.SshServerPort, _infrastructureFixture.User.UserName, _infrastructureFixture.User.Password);
+            sshClient.Connect();
+            return sshClient.RunCommand("echo $'test !@#$%^&*()_+{}:,./<>[];\\|'").Result;
+        }
+
+        [Benchmark]
+        public async Task<string> ConnectAsyncAndRunCommand()
+        {
+            using var sshClient = new SshClient(_infrastructureFixture.SshServerHostName, _infrastructureFixture.SshServerPort, _infrastructureFixture.User.UserName, _infrastructureFixture.User.Password);
+            await sshClient.ConnectAsync(CancellationToken.None).ConfigureAwait(false);
+            return sshClient.RunCommand("echo $'test !@#$%^&*()_+{}:,./<>[];\\|'").Result;
+        }
+
+        [Benchmark]
+        public string RunCommand()
+        {
+            return _sshClient!.RunCommand("echo $'test !@#$%^&*()_+{}:,./<>[];\\|'").Result;
+        }
+    }
+}