Skip to content

Commit dccdedc

Browse files
authored
Build the read-ahead mechanism into SftpFileStream (#1705)
* Build the read-ahead mechanism into SftpFileStream This change unifies the SFTP download implementations that exist via DownloadFile and via SftpFileStream, by rewriting SftpFileStream to perform the same "read-aheads" as DownloadFile. This brings the performance of downloads via SftpFileStream in line with DownloadFile, such that the latter is now effectively SftpFileStream.CopyTo. It also brings the recently added DownloadFileAsync up to speed since that was implemented via SftpFileStream.CopyToAsync. The methodology is a mix of the previous one and that within OpenSSH: the first call to SftpFileStream.Read sends one read request to the server. The second sends two and when not interrupted by Write or similar, the number of in-flight read requests continues to scale up in this fashion. I have measured CopyTo to be 3-20x faster than before, depending on file size and server round-trip time. * Check CanSeek in ReadAllBytes * Squeeze out some performance
1 parent c5c6f28 commit dccdedc

File tree

38 files changed

+979
-4717
lines changed

38 files changed

+979
-4717
lines changed

src/Renci.SshNet/Common/Extensions.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Globalization;
44
#if !NET
55
using System.IO;
6+
using System.Threading.Tasks;
67
#endif
78
using System.Net;
89
using System.Net.Sockets;
@@ -398,6 +399,29 @@ internal static void ReadExactly(this Stream stream, byte[] buffer, int offset,
398399
totalRead += read;
399400
}
400401
}
402+
403+
internal static Task<T> WaitAsync<T>(this Task<T> task, CancellationToken cancellationToken)
404+
{
405+
if (task.IsCompleted || !cancellationToken.CanBeCanceled)
406+
{
407+
return task;
408+
}
409+
410+
return WaitCore();
411+
412+
async Task<T> WaitCore()
413+
{
414+
TaskCompletionSource<T> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
415+
416+
using var reg = cancellationToken.Register(
417+
() => tcs.TrySetCanceled(cancellationToken),
418+
useSynchronizationContext: false);
419+
420+
var completedTask = await Task.WhenAny(task, tcs.Task).ConfigureAwait(false);
421+
422+
return await completedTask.ConfigureAwait(false);
423+
}
424+
}
401425
#endif
402426
}
403427
}

src/Renci.SshNet/IServiceFactory.cs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,6 @@ internal partial interface IServiceFactory
8383
/// <exception cref="SshConnectionException">No key exchange algorithm is supported by both client and server.</exception>
8484
IKeyExchange CreateKeyExchange(IDictionary<string, Func<IKeyExchange>> clientAlgorithms, string[] serverAlgorithms);
8585

86-
/// <summary>
87-
/// Creates an <see cref="ISftpFileReader"/> for the specified file and with the specified
88-
/// buffer size.
89-
/// </summary>
90-
/// <param name="fileName">The file to read.</param>
91-
/// <param name="sftpSession">The SFTP session to use.</param>
92-
/// <param name="bufferSize">The size of buffer.</param>
93-
/// <returns>
94-
/// An <see cref="ISftpFileReader"/>.
95-
/// </returns>
96-
ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSession, uint bufferSize);
97-
9886
/// <summary>
9987
/// Creates a new <see cref="ISftpResponseFactory"/> instance.
10088
/// </summary>

src/Renci.SshNet/ServiceFactory.cs

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
using System.Net.Sockets;
55
using System.Text;
66

7-
using Microsoft.Extensions.Logging;
8-
97
using Renci.SshNet.Common;
108
using Renci.SshNet.Connection;
119
using Renci.SshNet.Messages.Transport;
@@ -118,51 +116,6 @@ public INetConfSession CreateNetConfSession(ISession session, int operationTimeo
118116
return new NetConfSession(session, operationTimeout);
119117
}
120118

121-
/// <summary>
122-
/// Creates an <see cref="ISftpFileReader"/> for the specified file and with the specified
123-
/// buffer size.
124-
/// </summary>
125-
/// <param name="fileName">The file to read.</param>
126-
/// <param name="sftpSession">The SFTP session to use.</param>
127-
/// <param name="bufferSize">The size of buffer.</param>
128-
/// <returns>
129-
/// An <see cref="ISftpFileReader"/>.
130-
/// </returns>
131-
public ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSession, uint bufferSize)
132-
{
133-
const int defaultMaxPendingReads = 10;
134-
135-
// Issue #292: Avoid overlapping SSH_FXP_OPEN and SSH_FXP_LSTAT requests for the same file as this
136-
// causes a performance degradation on Sun SSH
137-
var openAsyncResult = sftpSession.BeginOpen(fileName, Flags.Read, callback: null, state: null);
138-
var handle = sftpSession.EndOpen(openAsyncResult);
139-
140-
var statAsyncResult = sftpSession.BeginLStat(fileName, callback: null, state: null);
141-
142-
long? fileSize;
143-
int maxPendingReads;
144-
145-
var chunkSize = sftpSession.CalculateOptimalReadLength(bufferSize);
146-
147-
// fallback to a default maximum of pending reads when remote server does not allow us to obtain
148-
// the attributes of the file
149-
try
150-
{
151-
var fileAttributes = sftpSession.EndLStat(statAsyncResult);
152-
fileSize = fileAttributes.Size;
153-
maxPendingReads = Math.Min(100, (int)Math.Ceiling((double)fileAttributes.Size / chunkSize) + 1);
154-
}
155-
catch (SshException ex)
156-
{
157-
fileSize = null;
158-
maxPendingReads = defaultMaxPendingReads;
159-
160-
sftpSession.SessionLoggerFactory.CreateLogger<ServiceFactory>().LogInformation(ex, "Failed to obtain size of file. Allowing maximum {MaxPendingReads} pending reads", maxPendingReads);
161-
}
162-
163-
return sftpSession.CreateFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize);
164-
}
165-
166119
/// <summary>
167120
/// Creates a new <see cref="ISftpResponseFactory"/> instance.
168121
/// </summary>

src/Renci.SshNet/Sftp/ISftpFileReader.cs

Lines changed: 0 additions & 23 deletions
This file was deleted.

src/Renci.SshNet/Sftp/ISftpSession.cs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,10 @@ internal interface ISftpSession : ISubsystemSession
6767
/// Asynchronously performs a <c>SSH_FXP_FSTAT</c> request.
6868
/// </summary>
6969
/// <param name="handle">The handle.</param>
70-
/// <param name="nullOnError">If set to <see langword="true"/>, <see langword="null"/> is returned in case of an error.</param>
7170
/// <returns>
7271
/// The file attributes.
7372
/// </returns>
74-
SftpFileAttributes RequestFStat(byte[] handle, bool nullOnError);
73+
SftpFileAttributes RequestFStat(byte[] handle);
7574

7675
/// <summary>
7776
/// Asynchronously performs a <c>SSH_FXP_FSTAT</c> request.
@@ -522,19 +521,5 @@ void RequestWrite(byte[] handle,
522521
/// Currently, we do not take the remote window size into account.
523522
/// </remarks>
524523
uint CalculateOptimalWriteLength(uint bufferSize, byte[] handle);
525-
526-
/// <summary>
527-
/// Creates an <see cref="ISftpFileReader"/> for reading the content of the file represented by a given <paramref name="handle"/>.
528-
/// </summary>
529-
/// <param name="handle">The handle of the file to read.</param>
530-
/// <param name="sftpSession">The SFTP session.</param>
531-
/// <param name="chunkSize">The maximum number of bytes to read with each chunk.</param>
532-
/// <param name="maxPendingReads">The maximum number of pending reads.</param>
533-
/// <param name="fileSize">The size of the file or <see langword="null"/> when the size could not be determined.</param>
534-
/// <returns>
535-
/// An <see cref="ISftpFileReader"/> for reading the content of the file represented by the
536-
/// specified <paramref name="handle"/>.
537-
/// </returns>
538-
ISftpFileReader CreateFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSize);
539524
}
540525
}

0 commit comments

Comments
 (0)