mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-04-19 13:59:22 -04:00
* Fix GCHandle leak when passing data to steam sockect
In 519ad5d815 we started passing the same byte array to N connections.
Our handle bookkeeping would pin the array N times but inly free it once so N-1 handles and the buffer would leak.
We now avoid pinning and GCHandles altogether and do a cheap mem copy instead of handle bookkeeping.
* Avoid a few more allocations in receive path
116 lines
4.6 KiB
C#
116 lines
4.6 KiB
C#
using Sandbox.Compression;
|
|
using Sandbox.Network;
|
|
using System.Buffers.Binary;
|
|
using System.IO;
|
|
|
|
namespace Sandbox;
|
|
|
|
public abstract partial class Connection
|
|
{
|
|
// Transport-level wire flags — the first byte of every packet sent or received.
|
|
internal const byte FlagRaw = 0; // [FlagRaw:1][data:N] — no origLen needed
|
|
internal const byte FlagCompressed = 1; // [FlagCompressed:1][origLen:4][lz4:N]
|
|
internal const byte FlagChunk = 2; // [FlagChunk:1][index:4][total:4][data:N]
|
|
|
|
private const int MinimumCompressionByteCount = 128;
|
|
|
|
/// <summary>
|
|
/// Upper bound on decompressed payload size. Any <see cref="FlagCompressed"/> packet claiming a
|
|
/// larger original length is treated as corrupt/malicious and rejected.
|
|
/// </summary>
|
|
private const int MaxDecompressedByteSize = 256 * 1024 * 1024;
|
|
|
|
// Static decode buffer — all decoding is synchronous on the network tick thread,
|
|
// so a single shared buffer with grow-to-high-water-mark is safe and zero-GC after warmup.
|
|
static byte[] _decodeBuffer;
|
|
|
|
/// <summary>
|
|
/// Encode a <see cref="ByteStream"/> into a wire-ready packet, applying LZ4 when beneficial.
|
|
/// Wire format: <c>[FlagRaw][data]</c> or <c>[FlagCompressed][origLen:4][lz4data]</c>.
|
|
/// Returns a heap-allocated <see cref="byte"/>[] that transports may store beyond the call.
|
|
/// </summary>
|
|
internal static byte[] Encode( ByteStream stream )
|
|
{
|
|
var src = stream.ToSpan();
|
|
|
|
if ( src.Length > MinimumCompressionByteCount )
|
|
{
|
|
var compressed = LZ4.CompressBlock( src );
|
|
|
|
if ( compressed.Length < src.Length )
|
|
{
|
|
var outputSize = 1 + sizeof( int ) + compressed.Length;
|
|
var output = GC.AllocateUninitializedArray<byte>( outputSize );
|
|
output[0] = FlagCompressed;
|
|
BinaryPrimitives.WriteInt32LittleEndian( output.AsSpan( 1 ), src.Length );
|
|
compressed.CopyTo( output.AsSpan( 1 + sizeof( int ) ) );
|
|
return output;
|
|
}
|
|
}
|
|
|
|
var result = GC.AllocateUninitializedArray<byte>( 1 + src.Length );
|
|
result[0] = FlagRaw;
|
|
src.CopyTo( result.AsSpan( 1 ) );
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decode a wire payload (<see cref="FlagRaw"/> or <see cref="FlagCompressed"/>).
|
|
/// <see cref="FlagChunk"/> packets must be fully reassembled by <see cref="OnRawPacketReceived"/>
|
|
/// before reaching this method.
|
|
/// Returns a span into either the source data (raw) or the static <see cref="_decodeBuffer"/> (compressed).
|
|
/// The span is only valid until the next call to <see cref="Decode"/>.
|
|
/// </summary>
|
|
internal static ReadOnlySpan<byte> Decode( ReadOnlySpan<byte> data )
|
|
{
|
|
if ( data.Length < 1 )
|
|
return ReadOnlySpan<byte>.Empty;
|
|
|
|
switch ( data[0] )
|
|
{
|
|
case FlagRaw:
|
|
return data.Slice( 1 );
|
|
case FlagCompressed:
|
|
{
|
|
const int headerSize = 1 + sizeof( int ); // flag + origLen
|
|
if ( data.Length < headerSize )
|
|
throw new InvalidDataException( $"Compressed packet too short ({data.Length}b, need {headerSize}b)" );
|
|
|
|
var origLen = BinaryPrimitives.ReadInt32LittleEndian( data.Slice( 1, sizeof( int ) ) );
|
|
if ( origLen <= 0 || origLen > MaxDecompressedByteSize )
|
|
throw new InvalidDataException( $"Compressed origLen {origLen} out of range (1..{MaxDecompressedByteSize})" );
|
|
|
|
// Grow to high-water mark; never returned — zero GC after warmup.
|
|
if ( _decodeBuffer == null || _decodeBuffer.Length < origLen )
|
|
_decodeBuffer = GC.AllocateUninitializedArray<byte>( origLen );
|
|
|
|
var lz4Data = data.Slice( headerSize );
|
|
int written = LZ4.DecompressBlock( lz4Data, _decodeBuffer );
|
|
|
|
if ( written != origLen )
|
|
throw new InvalidDataException( $"LZ4 decompressed {written}b but header claimed {origLen}b" );
|
|
|
|
Networking.TryRecordMessage( _decodeBuffer.AsSpan( 0, origLen ) );
|
|
return _decodeBuffer.AsSpan( 0, origLen );
|
|
}
|
|
default:
|
|
throw new InvalidOperationException( $"Unknown wire flag {data[0]}" );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Build a transport-level chunk packet: <c>[FlagChunk:1][index:4][total:4][data:N]</c>.
|
|
/// Chunks carry raw slices of an already-encoded (possibly compressed) payload.
|
|
/// Chunk header is 9 bytes vs the old 14-byte envelope.
|
|
/// </summary>
|
|
internal static byte[] BuildChunkPacket( byte[] encoded, int offset, int length, int chunkIndex, int totalChunks )
|
|
{
|
|
var result = GC.AllocateUninitializedArray<byte>( 1 + sizeof( uint ) + sizeof( uint ) + length ); // 9-byte header
|
|
result[0] = FlagChunk;
|
|
BinaryPrimitives.WriteUInt32LittleEndian( result.AsSpan( 1 ), (uint)chunkIndex );
|
|
BinaryPrimitives.WriteUInt32LittleEndian( result.AsSpan( 5 ), (uint)totalChunks );
|
|
encoded.AsSpan( offset, length ).CopyTo( result.AsSpan( 9 ) );
|
|
return result;
|
|
}
|
|
}
|