Optimize memory allocations in BaseClient, SshClient, and SftpClient #1720
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Overview
This PR implements focused memory allocation optimizations in SSH.NET's core client classes to reduce GC pressure and improve performance, particularly for file upload operations.
Changes
1. BaseClient - Timer Callback Optimization
Problem: Timer callback captured
Sessionin a closure, allocating a delegate object on every timer creation.Solution: Use static lambda and pass
thisas state parameter:Impact: Eliminates closure allocation during keep-alive timer creation.
2. SshClient - Cache ReadOnlyCollection
Problem:
ForwardedPortsproperty calledList<T>.AsReadOnly()on every access, allocating a newReadOnlyCollection<T>wrapper each time.Solution: Cache the
ReadOnlyCollectionin a field initialized once in the constructor:Impact: Eliminates repeated allocations on property access.
3. SftpClient - Multiple Optimizations
a. String Interpolation
Problem:
string.Format()creates intermediate objects and has overhead.Solution: Replace with string interpolation in 3 locations:
Impact: Reduces string building overhead.
b. ArrayPool for Upload Buffers (Highest Impact)
Problem:
InternalUploadFileallocated a new byte array for each upload operation, creating significant GC pressure especially for large files or frequent uploads.Solution: Use
ArrayPool<byte>.Sharedto rent/return buffers:Impact: Significant reduction in GC pressure for file upload operations. Buffer is safely returned after
SendRequestcompletes (data is serialized synchronously).Testing
✅ All 2,305 tests passing
✅ Zero test failures introduced
✅ Tested on all target frameworks (.NET 4.6.2, .NET Standard 2.0, .NET 8.0, .NET 9.0)
✅ Zero breaking changes to public APIs
✅ Zero behavior changes
Performance Impact
Code Quality
Additional Notes
During analysis, I found that
PipeStream,Shell, andSshCommandare already well-optimized with efficient patterns like:ArrayBufferfor internal bufferingFuture optimizations could be guided by profiler-identified hot paths in real-world scenarios.
Warning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
07d4be14ce6443bba1362413653ff7e8/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 45595 --endpoint 127.0.0.1:045595 --role client --parentprocessid 14354 --telemetryoptedin false(dns block)0b47405653a04758b4d1c99953cc08cb/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/testhost.dll --port 37513 --endpoint 127.0.0.1:037513 --role client --parentprocessid 5672 --telemetryoptedin false(dns block)21856095fb9f42478f22e95a35d84c1323594e274da64510a2874c67a3a44cf3/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 45511 --endpoint 127.0.0.1:045511 --role client --parentprocessid 12586 --telemetryoptedin false(dns block)272125e1955242628266f05bc18b55d4/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 42499 --endpoint 127.0.0.1:042499 --role client --parentprocessid 8816 --telemetryoptedin false(dns block)284913aa557242659ed389065995d7bb/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 36017 --endpoint 127.0.0.1:036017 --role client --parentprocessid 6917 --telemetryoptedin false(dns block)31ed9d8d1c5d4344a75c8b5954b39895335f3a1356c546afa3e0d23ceb182c61/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 33655 --endpoint 127.0.0.1:033655 --role client --parentprocessid 5665 --telemetryoptedin false(dns block)451ca18cd2674d239cc2c831ba1708d8469dca81b1a24c4fa1abbb5ea221cdb1544f909d291247bda9f5c6607bac35d0/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 42615 --endpoint 127.0.0.1:042615 --role client --parentprocessid 13514 --telemetryoptedin false(dns block)56379a7966934a4382004771bad5fb47/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 43083 --endpoint 127.0.0.1:043083 --role client --parentprocessid 11660 --telemetryoptedin false(dns block)607d5a3a461743228c7a7ca57478c843/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/testhost.dll --port 35597 --endpoint 127.0.0.1:035597 --role client --parentprocessid 5380 --telemetryoptedin false(dns block)6c6dff8763484e18a57b87717a72dd01/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 39011 --endpoint 127.0.0.1:039011 --role client --parentprocessid 10730 --telemetryoptedin false(dns block)7455e65bd1d7456f9833e83fd0bbe0bc/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 43553 --endpoint 127.0.0.1:043553 --role client --parentprocessid 9699 --telemetryoptedin false(dns block)8822efab02c7412791a8540403f94281/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 35469 --endpoint 127.0.0.1:035469 --role client --parentprocessid 7970 --telemetryoptedin false(dns block)8e46b69e229b4b8cabce3a9844006033/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 44399 --endpoint 127.0.0.1:044399 --role client --parentprocessid 15197 --telemetryoptedin false(dns block)8e999d5774df497695e90f7ae722427c/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/testhost.dll --port 37513 --endpoint 127.0.0.1:037513 --role client --parentprocessid 5672 --telemetryoptedin false(dns block)91d6d159d27c4795b609167d9f6f7385/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 44399 --endpoint 127.0.0.1:044399 --role client --parentprocessid 15197 --telemetryoptedin false(dns block)97051edb06f448149af1dab8a2d40e9b/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 36017 --endpoint 127.0.0.1:036017 --role client --parentprocessid 6917 --telemetryoptedin false(dns block)9789d436383b459aa74066b587e0f4bb/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/testhost.dll --port 46823 --endpoint 127.0.0.1:046823 --role client --parentprocessid 9706 --telemetryoptedin false(dns block)9b3cb46c74cc4f87a37242009382ed7f/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 45595 --endpoint 127.0.0.1:045595 --role client --parentprocessid 14354 --telemetryoptedin false(dns block)9e95e92325144a33b879ee1bd53f5dabae20c2a06de843dbbf2e6a33f833bd71/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 35469 --endpoint 127.0.0.1:035469 --role client --parentprocessid 7970 --telemetryoptedin false(dns block)ae5f4b816843485f9e2438f2ead5e164/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 45511 --endpoint 127.0.0.1:045511 --role client --parentprocessid 12586 --telemetryoptedin false(dns block)b9393fd2bd234c23bc2e400f78951f2f/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 42499 --endpoint 127.0.0.1:042499 --role client --parentprocessid 8816 --telemetryoptedin false(dns block)c235cc657e024c87be3f674bff292579/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 43083 --endpoint 127.0.0.1:043083 --role client --parentprocessid 11660 --telemetryoptedin false(dns block)ca3e99fa0cdd450f95d4182bffe72190dbc69966dac24814970f6e72e4c78bcd/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 33655 --endpoint 127.0.0.1:033655 --role client --parentprocessid 5665 --telemetryoptedin false(dns block)de66c9eba5834b60a80a1f2c83cf358e/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 43553 --endpoint 127.0.0.1:043553 --role client --parentprocessid 9699 --telemetryoptedin false(dns block)ecbadcb9d3844fb986d8044d741682c2/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 39011 --endpoint 127.0.0.1:039011 --role client --parentprocessid 10730 --telemetryoptedin false(dns block)fc1bc434703f4507aee5d68c911f60b4/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/testhost.dll --port 35597 --endpoint 127.0.0.1:035597 --role client --parentprocessid 5380 --telemetryoptedin false(dns block)invalid/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net9.0/testhost.dll --port 35013 --endpoint 127.0.0.1:035013 --role client --parentprocessid 5366 --telemetryoptedin false(dns block)/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/testhost.dll --port 35597 --endpoint 127.0.0.1:035597 --role client --parentprocessid 5380 --telemetryoptedin false(dns block)/usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/Renci.SshNet.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/Renci.SshNet.Tests.deps.json /home/REDACTED/work/SSH.NET/SSH.NET/test/Renci.SshNet.Tests/bin/Release/net8.0/testhost.dll --port 37513 --endpoint 127.0.0.1:037513 --role client --parentprocessid 5672 --telemetryoptedin false(dns block)If you need me to access, download, or install something from one of these locations, you can either:
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.