From 45de0b2e1127c269a680cad4e2f1c27d95f67747 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 11:34:32 +0000 Subject: [PATCH] Use ArrayPool for upload buffer in SftpClient to reduce allocations Co-authored-by: WojciechNagorski <17333903+WojciechNagorski@users.noreply.github.com> --- src/Renci.SshNet/SftpClient.cs | 174 +++++++++++++++++---------------- 1 file changed, 91 insertions(+), 83 deletions(-) diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs index 95bdfae0..c9d633cf 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -2419,103 +2419,111 @@ namespace Renci.SshNet ulong offset = 0; - // create buffer of optimal length - var buffer = new byte[_sftpSession.CalculateOptimalWriteLength(_bufferSize, handle)]; + // create buffer of optimal length using ArrayPool + var bufferLength = (int)_sftpSession.CalculateOptimalWriteLength(_bufferSize, handle); + var buffer = ArrayPool.Shared.Rent(bufferLength); - var expectedResponses = 0; - - // We will send out all the write requests without waiting for each response. - // Afterwards, we may wait on this handle until all responses are received - // or an error has occurred. - using var mres = new ManualResetEventSlim(initialState: false); - - ExceptionDispatchInfo? exception = null; - - while (true) + try { - var bytesRead = isAsync -#if NET - ? await input.ReadAsync(buffer, cancellationToken).ConfigureAwait(false) -#else - ? await input.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false) -#endif - : input.Read(buffer, 0, buffer.Length); + var expectedResponses = 0; - if (bytesRead == 0) + // We will send out all the write requests without waiting for each response. + // Afterwards, we may wait on this handle until all responses are received + // or an error has occurred. + using var mres = new ManualResetEventSlim(initialState: false); + + ExceptionDispatchInfo? exception = null; + + while (true) { - break; + var bytesRead = isAsync +#if NET + ? await input.ReadAsync(buffer.AsMemory(0, bufferLength), cancellationToken).ConfigureAwait(false) +#else + ? await input.ReadAsync(buffer, 0, bufferLength, cancellationToken).ConfigureAwait(false) +#endif + : input.Read(buffer, 0, bufferLength); + + if (bytesRead == 0) + { + break; + } + + if (asyncResult is not null && asyncResult.IsUploadCanceled) + { + break; + } + + exception?.Throw(); + + var writtenBytes = offset + (ulong)bytesRead; + + _ = Interlocked.Increment(ref expectedResponses); + mres.Reset(); + + _sftpSession.RequestWrite(handle, offset, buffer, offset: 0, bytesRead, wait: null, s => + { + var setHandle = false; + + try + { + if (Sftp.SftpSession.GetSftpException(s) is Exception ex) + { + exception = ExceptionDispatchInfo.Capture(ex); + } + + if (exception is not null) + { + setHandle = true; + return; + } + + Debug.Assert(s.StatusCode == StatusCode.Ok); + + asyncResult?.Update(writtenBytes); + + // Call callback to report number of bytes written + if (uploadCallback is not null) + { + // Execute callback on different thread + ThreadAbstraction.ExecuteThread(() => uploadCallback(writtenBytes)); + } + } + finally + { + if (Interlocked.Decrement(ref expectedResponses) == 0 || setHandle) + { + mres.Set(); + } + } + }); + + offset += (ulong)bytesRead; } - if (asyncResult is not null && asyncResult.IsUploadCanceled) + // Make sure the read of exception cannot be executed ahead of + // the read of expectedResponses so that we do not miss an + // exception. + + if (Volatile.Read(ref expectedResponses) != 0) { - break; + if (isAsync) + { + await _sftpSession.WaitOnHandleAsync(mres.WaitHandle, _operationTimeout, cancellationToken).ConfigureAwait(false); + } + else + { + _sftpSession.WaitOnHandle(mres.WaitHandle, _operationTimeout); + } } exception?.Throw(); - - var writtenBytes = offset + (ulong)bytesRead; - - _ = Interlocked.Increment(ref expectedResponses); - mres.Reset(); - - _sftpSession.RequestWrite(handle, offset, buffer, offset: 0, bytesRead, wait: null, s => - { - var setHandle = false; - - try - { - if (Sftp.SftpSession.GetSftpException(s) is Exception ex) - { - exception = ExceptionDispatchInfo.Capture(ex); - } - - if (exception is not null) - { - setHandle = true; - return; - } - - Debug.Assert(s.StatusCode == StatusCode.Ok); - - asyncResult?.Update(writtenBytes); - - // Call callback to report number of bytes written - if (uploadCallback is not null) - { - // Execute callback on different thread - ThreadAbstraction.ExecuteThread(() => uploadCallback(writtenBytes)); - } - } - finally - { - if (Interlocked.Decrement(ref expectedResponses) == 0 || setHandle) - { - mres.Set(); - } - } - }); - - offset += (ulong)bytesRead; } - - // Make sure the read of exception cannot be executed ahead of - // the read of expectedResponses so that we do not miss an - // exception. - - if (Volatile.Read(ref expectedResponses) != 0) + finally { - if (isAsync) - { - await _sftpSession.WaitOnHandleAsync(mres.WaitHandle, _operationTimeout, cancellationToken).ConfigureAwait(false); - } - else - { - _sftpSession.WaitOnHandle(mres.WaitHandle, _operationTimeout); - } + ArrayPool.Shared.Return(buffer); } - exception?.Throw(); - if (isAsync) { await _sftpSession.RequestCloseAsync(handle, cancellationToken).ConfigureAwait(false);