Skip to content

Commit 1f1a5fe

Browse files
authored
Use an array buffer for the sftp packet stream (#1649)
The sftp packet stream runs within but independently of the channel data stream, meaning a channel data packet can contain multiple sftp packets, or an sftp packet can be split across multiple channel data packets. Normally the packets are sized such there is a 1-to-1 relationship for efficiency. When this doesn't happen the library falls back to buffering via a List<byte>, which is not so efficient. This change uses an array-based buffer instead. In a sample download which hit this fallback I see about a 20% reduction in memory allocated.
1 parent caac95c commit 1f1a5fe

File tree

1 file changed

+40
-87
lines changed

1 file changed

+40
-87
lines changed

src/Renci.SshNet/Sftp/SftpSession.cs

Lines changed: 40 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Buffers.Binary;
23
using System.Collections.Generic;
34
using System.Diagnostics;
45
using System.Globalization;
@@ -22,8 +23,8 @@ internal sealed class SftpSession : SubsystemSession, ISftpSession
2223

2324
private readonly Dictionary<uint, SftpRequest> _requests = new Dictionary<uint, SftpRequest>();
2425
private readonly ISftpResponseFactory _sftpResponseFactory;
25-
private readonly List<byte> _data = new List<byte>(32 * 1024);
2626
private readonly Encoding _encoding;
27+
private System.Net.ArrayBuffer _buffer = new(32 * 1024);
2728
private EventWaitHandle _sftpVersionConfirmed = new AutoResetEvent(initialState: false);
2829
private IDictionary<string, string> _supportedExtensions;
2930

@@ -303,125 +304,77 @@ protected override void OnChannelOpen()
303304

304305
protected override void OnDataReceived(byte[] data)
305306
{
306-
const int packetLengthByteCount = 4;
307-
const int sftpMessageTypeByteCount = 1;
308-
const int minimumChannelDataLength = packetLengthByteCount + sftpMessageTypeByteCount;
307+
ArraySegment<byte> d = new(data);
309308

310-
var offset = 0;
311-
var count = data.Length;
312-
313-
// improve performance and reduce GC pressure by not buffering channel data if the received
314-
// chunk contains the complete packet data.
315-
//
316-
// for this, the buffer should be empty and the chunk should contain at least the packet length
317-
// and the type of the SFTP message
318-
if (_data.Count == 0)
309+
// If the buffer is empty then skip a copy and read packets
310+
// directly out of the given data.
311+
if (_buffer.ActiveLength == 0)
319312
{
320-
while (count >= minimumChannelDataLength)
313+
while (d.Count >= 4)
321314
{
322-
// extract packet length
323-
var packetDataLength = data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 |
324-
data[offset + 3];
325-
326-
var packetTotalLength = packetDataLength + packetLengthByteCount;
315+
var packetLength = BinaryPrimitives.ReadInt32BigEndian(d);
327316

328-
// check if complete packet data (or more) is available
329-
if (count >= packetTotalLength)
317+
if (d.Count - 4 < packetLength)
330318
{
331-
// load and process SFTP message
332-
if (!TryLoadSftpMessage(data, offset + packetLengthByteCount, packetDataLength))
333-
{
334-
return;
335-
}
336-
337-
// remove processed bytes from the number of bytes to process as the channel
338-
// data we received may contain (part of) another message
339-
count -= packetTotalLength;
340-
341-
// move offset beyond bytes we just processed
342-
offset += packetTotalLength;
319+
break;
343320
}
344-
else
321+
322+
if (!TryLoadSftpMessage(d.Slice(4, packetLength)))
345323
{
346-
// we don't have a complete message
347-
break;
324+
// An error occured.
325+
return;
348326
}
349-
}
350327

351-
// check if there is channel data left to process or buffer
352-
if (count == 0)
353-
{
354-
return;
328+
d = d.Slice(4 + packetLength);
355329
}
356330

357-
// check if we processed part of the channel data we received
358-
if (offset > 0)
359-
{
360-
// add (remaining) channel data to internal data holder
361-
var remainingChannelData = new byte[count];
362-
Buffer.BlockCopy(data, offset, remainingChannelData, 0, count);
363-
_data.AddRange(remainingChannelData);
364-
}
365-
else
331+
if (d.Count > 0)
366332
{
367-
// add (remaining) channel data to internal data holder
368-
_data.AddRange(data);
333+
// Now buffer the remainder.
334+
_buffer.EnsureAvailableSpace(d.Count);
335+
d.AsSpan().CopyTo(_buffer.AvailableSpan);
336+
_buffer.Commit(d.Count);
369337
}
370338

371-
// skip further processing as we'll need a new chunk to complete the message
372339
return;
373340
}
374341

375-
// add (remaining) channel data to internal data holder
376-
_data.AddRange(data);
342+
// The buffer already had some data. Append the new data and
343+
// proceed with reading out packets.
344+
_buffer.EnsureAvailableSpace(d.Count);
345+
d.AsSpan().CopyTo(_buffer.AvailableSpan);
346+
_buffer.Commit(d.Count);
377347

378-
while (_data.Count >= minimumChannelDataLength)
348+
while (_buffer.ActiveLength >= 4)
379349
{
380-
// extract packet length
381-
var packetDataLength = _data[0] << 24 | _data[1] << 16 | _data[2] << 8 | _data[3];
350+
d = new ArraySegment<byte>(
351+
_buffer.DangerousGetUnderlyingBuffer(),
352+
_buffer.ActiveStartOffset,
353+
_buffer.ActiveLength);
382354

383-
var packetTotalLength = packetDataLength + packetLengthByteCount;
355+
var packetLength = BinaryPrimitives.ReadInt32BigEndian(d);
384356

385-
// check if complete packet data is available
386-
if (_data.Count < packetTotalLength)
357+
if (d.Count - 4 < packetLength)
387358
{
388-
// wait for complete message to arrive first
389359
break;
390360
}
391361

392-
// create buffer to hold packet data
393-
var packetData = new byte[packetDataLength];
362+
// Note: the packet data in the buffer is safe to read from
363+
// only for the duration of this load. If it needs to be stored,
364+
// callees should make their own copy.
365+
_ = TryLoadSftpMessage(d.Slice(4, packetLength));
394366

395-
// copy packet data and bytes for length to array
396-
_data.CopyTo(packetLengthByteCount, packetData, 0, packetDataLength);
397-
398-
// remove loaded data and bytes for length from _data holder
399-
if (_data.Count == packetTotalLength)
400-
{
401-
// the only buffered data is the data we're processing
402-
_data.Clear();
403-
}
404-
else
405-
{
406-
// remove only the data we're processing
407-
_data.RemoveRange(0, packetTotalLength);
408-
}
409-
410-
// load and process SFTP message
411-
if (!TryLoadSftpMessage(packetData, 0, packetDataLength))
412-
{
413-
break;
414-
}
367+
_buffer.Discard(4 + packetLength);
415368
}
416369
}
417370

418-
private bool TryLoadSftpMessage(byte[] packetData, int offset, int count)
371+
private bool TryLoadSftpMessage(ArraySegment<byte> packetData)
419372
{
420373
// Create SFTP message
421-
var response = _sftpResponseFactory.Create(ProtocolVersion, packetData[offset], _encoding);
374+
var response = _sftpResponseFactory.Create(ProtocolVersion, packetData.Array[packetData.Offset], _encoding);
422375

423376
// Load message data into it
424-
response.Load(packetData, offset + 1, count - 1);
377+
response.Load(packetData.Array, packetData.Offset + 1, packetData.Count - 1);
425378

426379
try
427380
{

0 commit comments

Comments
 (0)