diff --git a/Renci.SshNet.sln b/Renci.SshNet.sln index 0ca62c338..40103f6d6 100644 --- a/Renci.SshNet.sln +++ b/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 diff --git a/src/Renci.SshNet/Common/SshDataStream.cs b/src/Renci.SshNet/Common/SshDataStream.cs index 9e2a28051..5fb40adcf 100644 --- a/src/Renci.SshNet/Common/SshDataStream.cs +++ b/src/Renci.SshNet/Common/SshDataStream.cs @@ -126,9 +126,17 @@ public void Write(string s, Encoding encoding) { throw new ArgumentNullException(nameof(encoding)); } - +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + ReadOnlySpan 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 } /// diff --git a/test/Renci.SshNet.Benchmarks/Renci.SshNet.Benchmarks.csproj b/test/Renci.SshNet.Benchmarks/Renci.SshNet.Benchmarks.csproj index e9e964377..244b4d722 100644 --- a/test/Renci.SshNet.Benchmarks/Renci.SshNet.Benchmarks.csproj +++ b/test/Renci.SshNet.Benchmarks/Renci.SshNet.Benchmarks.csproj @@ -17,4 +17,5 @@ + diff --git a/test/Renci.SshNet.IntegrationBenchmarks/.editorconfig b/test/Renci.SshNet.IntegrationBenchmarks/.editorconfig new file mode 100644 index 000000000..e903a76d8 --- /dev/null +++ b/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 diff --git a/test/Renci.SshNet.IntegrationBenchmarks/IntegrationBenchmarkBase.cs b/test/Renci.SshNet.IntegrationBenchmarks/IntegrationBenchmarkBase.cs new file mode 100644 index 000000000..5df3990c1 --- /dev/null +++ b/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 + } +} diff --git a/test/Renci.SshNet.IntegrationBenchmarks/Program.cs b/test/Renci.SshNet.IntegrationBenchmarks/Program.cs new file mode 100644 index 000000000..58c23eb50 --- /dev/null +++ b/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); + } + } +} diff --git a/test/Renci.SshNet.IntegrationBenchmarks/Renci.SshNet.IntegrationBenchmarks.csproj b/test/Renci.SshNet.IntegrationBenchmarks/Renci.SshNet.IntegrationBenchmarks.csproj new file mode 100644 index 000000000..2af6c25ce --- /dev/null +++ b/test/Renci.SshNet.IntegrationBenchmarks/Renci.SshNet.IntegrationBenchmarks.csproj @@ -0,0 +1,25 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/test/Renci.SshNet.IntegrationBenchmarks/ScpClientBenchmark.cs b/test/Renci.SshNet.IntegrationBenchmarks/ScpClientBenchmark.cs new file mode 100644 index 000000000..1e7b9368b --- /dev/null +++ b/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()); + } + } +} diff --git a/test/Renci.SshNet.IntegrationBenchmarks/SftpClientBenchmark.cs b/test/Renci.SshNet.IntegrationBenchmarks/SftpClientBenchmark.cs new file mode 100644 index 000000000..b3d4ad17e --- /dev/null +++ b/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 ListDirectory() + { + return _sftpClient!.ListDirectory("/root"); + } + + public IAsyncEnumerable 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()); + } + } +} diff --git a/test/Renci.SshNet.IntegrationBenchmarks/SshClientBenchmark.cs b/test/Renci.SshNet.IntegrationBenchmarks/SshClientBenchmark.cs new file mode 100644 index 000000000..0ce126e0e --- /dev/null +++ b/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 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; + } + } +}