mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-08-01 16:28:36 -04:00
**Broadcast** now encodes the wire payload just once and sends the same bytes to every recipient, before we did one redundant compression per connection. This primarily reduces CPU load on the server/host. **Chunking** Large messages are now compressed before chunking rather than after. Resulting in slightly smaller payloads. The receiver now decompresses a single reassembled payload instead of decompressing every chunk independently, significantly reducing CPU load on receiving clients. **Refactor** Chunking and compression are now low-level wire concerns handled by Connection rather than being mixed into the high-level message types. The old `InternalMessageType.Chunk` enum is removed; chunk framing uses a dedicated wire flag byte alongside `FlagRaw` and `FlagCompressed`. **Results (Chunking changes)** Synthetic data, results on real payloads may differ. Benchmarks (1000 GOs / 2000 components, ~1MB payload, 500 iterations): Wire size (chunk-first-then-compress): 275KB Wire size (compress-first): 259KB (5.7% smaller) Send chunk-first: 0.85 ms/op (old) Send compress-first: 0.88 ms/op (new) Recv chunk-first: 1.16 ms/op (old) Recv compress-first: 0.34 ms/op (new, 3.4x faster)
237 lines
4.7 KiB
C#
237 lines
4.7 KiB
C#
using System.Runtime.InteropServices;
|
|
using System.Text.Json;
|
|
|
|
namespace Sandbox.Network;
|
|
|
|
internal class StringTable
|
|
{
|
|
/// <summary>
|
|
/// The maximum size of a snapshot entry in megabytes.
|
|
/// </summary>
|
|
private const int MaxSnapshotEntrySize = 8;
|
|
|
|
public readonly Dictionary<string, Entry> Entries = new();
|
|
|
|
public string Name { get; }
|
|
public bool Compressed { get; }
|
|
public bool HasChanged => changed.Count > 0 || removed.Count > 0;
|
|
|
|
/// <summary>
|
|
/// Called when a table entry is changed or added from the network
|
|
/// </summary>
|
|
public Action<Entry> OnChangeOrAdd;
|
|
|
|
/// <summary>
|
|
/// The table has been fully replaced. Anything or everything could have changed.
|
|
/// </summary>
|
|
public Action OnSnapshot;
|
|
|
|
/// <summary>
|
|
/// Called when a table entry is removed by the network
|
|
/// </summary>
|
|
public Action<Entry> OnRemoved;
|
|
|
|
/// <summary>
|
|
/// Called after a network update of the table is complete. Not called after snapshots!
|
|
/// </summary>
|
|
public Action PostNetworkUpdate;
|
|
|
|
readonly HashSet<string> changed = new();
|
|
readonly HashSet<string> removed = new();
|
|
|
|
public StringTable( string name, bool compressed )
|
|
{
|
|
Name = name;
|
|
Compressed = compressed;
|
|
}
|
|
|
|
internal Entry Remove( string name )
|
|
{
|
|
if ( !Entries.Remove( name, out var e ) )
|
|
return default;
|
|
|
|
Log.Trace( $"Removed Key {name}" );
|
|
removed.Add( name );
|
|
changed.Remove( name );
|
|
return e;
|
|
}
|
|
|
|
internal void Reset()
|
|
{
|
|
Log.Trace( "Table Reset" );
|
|
Entries.Clear();
|
|
removed.Clear();
|
|
changed.Clear();
|
|
}
|
|
|
|
internal Entry Set<T>( string name, T assemblyBytes ) where T : unmanaged
|
|
{
|
|
var size = Marshal.SizeOf<T>();
|
|
var arr = new byte[size];
|
|
MemoryMarshal.Write( arr, assemblyBytes );
|
|
|
|
return Set( name, arr );
|
|
}
|
|
|
|
internal Entry Set( string name, byte[] assemblyBytes )
|
|
{
|
|
if ( Entries.TryGetValue( name, out var entry ) )
|
|
{
|
|
entry.Data = assemblyBytes;
|
|
Log.Trace( $"Updated {name} [{assemblyBytes.Length}]" );
|
|
}
|
|
else
|
|
{
|
|
entry = new Entry { Name = name, Data = assemblyBytes };
|
|
Entries[name] = entry;
|
|
Log.Trace( $"Added {name} [{assemblyBytes.Length}]" );
|
|
}
|
|
|
|
changed.Add( name );
|
|
removed.Remove( name );
|
|
|
|
return entry;
|
|
}
|
|
|
|
public class Entry
|
|
{
|
|
public string Name { get; set; }
|
|
public byte[] Data { get; set; }
|
|
|
|
/// <summary>
|
|
/// Read an unmanaged struct from the data
|
|
/// </summary>
|
|
internal T Read<T>() where T : unmanaged
|
|
{
|
|
using var bytes = ByteStream.CreateReader( Data );
|
|
return bytes.Read<T>();
|
|
}
|
|
|
|
internal string ReadAsString()
|
|
{
|
|
return System.Text.Encoding.UTF8.GetString( Data );
|
|
}
|
|
|
|
internal T ReadJson<T>()
|
|
{
|
|
var json = ReadAsString();
|
|
if ( string.IsNullOrWhiteSpace( json ) ) return default;
|
|
return JsonSerializer.Deserialize<T>( json );
|
|
}
|
|
}
|
|
|
|
internal void SendSnapshot( Connection source )
|
|
{
|
|
var bs = ByteStream.Create( 1024 );
|
|
bs.Write( InternalMessageType.TableSnapshot );
|
|
bs.Write( Name );
|
|
|
|
BuildSnapshotMessage( ref bs );
|
|
source.SendStream( bs );
|
|
|
|
bs.Dispose();
|
|
}
|
|
|
|
internal void ReadSnapshot( ByteStream bs )
|
|
{
|
|
while ( true )
|
|
{
|
|
var key = bs.Read<string>();
|
|
if ( key == null ) break;
|
|
var data = bs.ReadArraySpan<byte>( 1024 * 1024 * MaxSnapshotEntrySize );
|
|
Set( key, data.ToArray() );
|
|
}
|
|
|
|
OnSnapshot?.Invoke();
|
|
}
|
|
|
|
internal void BuildSnapshotMessage( ref ByteStream bs )
|
|
{
|
|
foreach ( var entry in Entries )
|
|
{
|
|
bs.Write( entry.Value.Name );
|
|
bs.WriteArray( entry.Value.Data );
|
|
}
|
|
|
|
bs.Write( (string)null );
|
|
}
|
|
|
|
internal void BuildUpdateMessage( ref ByteStream bs )
|
|
{
|
|
bs.Write( removed.Count );
|
|
bs.Write( changed.Count );
|
|
|
|
foreach ( var entry in removed )
|
|
{
|
|
bs.Write( entry );
|
|
}
|
|
|
|
foreach ( var entry in changed )
|
|
{
|
|
var e = Entries[entry];
|
|
|
|
bs.Write( e.Name );
|
|
bs.Write( e.Data.Length );
|
|
bs.Write( e.Data );
|
|
}
|
|
}
|
|
|
|
internal void ReadUpdate( ByteStream bs )
|
|
{
|
|
var removed = bs.Read<int>();
|
|
var changed = bs.Read<int>();
|
|
|
|
for ( var i = 0; i < removed; i++ )
|
|
{
|
|
var e = Remove( bs.Read<string>() );
|
|
if ( e is not null )
|
|
{
|
|
OnRemoved?.Invoke( e );
|
|
}
|
|
}
|
|
|
|
for ( var i = 0; i < changed; i++ )
|
|
{
|
|
var key = bs.Read<string>();
|
|
var len = bs.Read<int>();
|
|
if ( len == 0 )
|
|
{
|
|
var e = Set( key, [] );
|
|
if ( e is not null )
|
|
{
|
|
OnChangeOrAdd?.Invoke( e );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
using var data = bs.ReadByteStream( len );
|
|
|
|
var e = Set( key, data.ToArray() );
|
|
if ( e is not null )
|
|
{
|
|
OnChangeOrAdd?.Invoke( e );
|
|
}
|
|
}
|
|
}
|
|
|
|
PostNetworkUpdate?.Invoke();
|
|
}
|
|
|
|
internal void ClearChanges()
|
|
{
|
|
changed.Clear();
|
|
removed.Clear();
|
|
}
|
|
|
|
internal Entry SetJson<T>( string key, T value )
|
|
{
|
|
var json = JsonSerializer.Serialize( value );
|
|
return SetString( key, json );
|
|
}
|
|
|
|
internal Entry SetString( string key, string value )
|
|
{
|
|
return Set( key, System.Text.Encoding.UTF8.GetBytes( value ) );
|
|
}
|
|
}
|