Skip to content

Commit 6f12799

Browse files
committed
Use an array buffer for the sftp packet stream
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 03e2821 commit 6f12799

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)