RPC round trip, 50 sequential calls (ms)
min p50 p95 max avg
Pigeon 48.864 50.003 51.051 51.305 49.995
Socket 50.115 64.895 66.901 67.524 59.646
Client -> server burst, 250 messages
Pigeon 250/250 in 4.517ms ( 55349 msg/s)
Socket 250/250 in 2.504ms ( 99828 msg/s)
Server -> client burst, 250 messages
Pigeon 250/250 in 4.254ms ( 58763 msg/s)
Socket 250/250 in 2.378ms ( 105139 msg/s)
Wire volume, idle traffic subtracted (client-side Stats counters)
-- RPC stage, 50 calls --
Pigeon up 7.56 KB ( 154.8 B/msg) down 0.00 KB ( 0.0 B/msg)
Socket up 10.04 KB ( 205.6 B/msg) down 0.00 KB ( 0.0 B/msg)
-- uplink stage, 250 messages --
Pigeon up 41.42 KB ( 169.7 B/msg) down 0.00 KB ( 0.0 B/msg)
Socket up 36.80 KB ( 150.7 B/msg) down 0.00 KB ( 0.0 B/msg)
-- downlink stage, 250 messages --
Pigeon up 0.50 KB ( 2.0 B/msg) down 0.00 KB ( 0.0 B/msg)
Socket up 0.21 KB ( 0.9 B/msg) down 0.00 KB ( 0.0 B/msg)
Measured packet sizes per remote event (PacketSizeCounter)
via event count total min avg max
client -> server, weighed on the server (includes the 5 RPC warmups)
BenchSocket Bench/Blast 1 169 B 169 169.0 169
BenchSocket Bench/Up 250 41500 B 166 166.0 166
BenchSocket Bench/UpReset 1 29 B 29 29.0 29
BenchSocket CallRequest:Bench/Echo 55 11550 B 210 210.0 210
BenchSocket CallRequest:Bench/UpReport 1 81 B 81 81.0 81
Pigeon BenchPigeon 1 188 B 188 188.0 188
Pigeon BenchPigeon 55 10010 B 182 182.0 182
Pigeon BenchPigeon 1 55 B 55 55.0 55
Pigeon BenchPigeon 250 46250 B 185 185.0 185
Pigeon BenchPigeon 1 51 B 51 51.0 51
Pigeon BenchPigeon 1 46 B 46 46.0 46
server -> client, weighed on this client
BenchSocket Bench/Down 250 40750 B 163 163.0 163
BenchSocket CallResponse 56 10767 B 97 192.3 194
Pigeon <rpc reply> 57 10180 B 40 178.6 1615
Pigeon BenchPigeon 250 45500 B 182 182.0 182
Current benchmarks against the base socket module
here is the code that ran
SERVER
--!strict
-------------------------------------------------------------------- SERVICES
const Players = game:GetService("Players")
const ReplicatedStorage = game:GetService("ReplicatedStorage")
-------------------------------------------------------------------- MODULES
const Pigeon = require(ReplicatedStorage.Shared.Pigeon)
const Sockets = require(ReplicatedStorage.Shared.Sockets)
const PacketSizeCounter = require(ReplicatedStorage.Shared.PacketSizeCounter)
const Wire = require(ReplicatedStorage.Shared.Pigeon.Wire)
-------------------------------------------------------------------- TYPES
export type Uplink = {
Count:number,
First:number,
Last:number
}
export type PacketRow = {
Transport:string,
Event:string,
Count:number,
Bytes:number,
Min:number,
Max:number
}
export type PacketLedger = {[string]:PacketRow}
-------------------------------------------------------------------- VARS
const PigeonBench = Pigeon.new("BenchPigeon")
const SocketBench = Sockets.new("BenchSocket")
const PigeonUplinks:{[Player]:Uplink} = {}
const SocketUplinks:{[Player]:Uplink} = {}
const Packets:PacketLedger = {}
-------------------------------------------------------------------- PACKET METER
const Watched:{[Instance]:boolean} = {}
const function MeasurePacket(RunContext:"Client" | "Server", Packed:{n:number, [number]:any}):number
local Dense = true
for Index = 1, Packed.n do
if Packed[Index] == nil then
Dense = false
break
end
end
if Dense then
return PacketSizeCounter.GetPacketSize({
RunContext = RunContext,
RemoteType = "RemoteEvent",
PacketData = Packed
})
end
local Total = PacketSizeCounter.BaseRemoteOverhead
if RunContext == "Client" then
Total += PacketSizeCounter.ClientToServerOverhead
end
for Index = 1, Packed.n do
Total += PacketSizeCounter.GetDataByteSize(Packed[Index])
end
return Total
end
const function TransportOf(Remote:Instance):string
if Remote.Name:sub(1, 9) == "PigeonRef" then
return "Pigeon"
end
return Remote.Name
end
const function EventOf(Packed:{n:number, [number]:any}):string
const First = Packed[1]
if type(First) == "string" then
if First == "CallRequest" and type(Packed[2]) == "string" then
return `CallRequest:{Packed[2]}`
end
return First
end
if typeof(First) == "buffer" then
const Ok, Kind, Event = pcall(Wire.unpack, First)
if not Ok then
return "<pigeon>"
end
if type(Event) == "string" and Event ~= "" then
return Event
end
if Kind == Wire.RESPOND then
return "<rpc reply>"
end
if Kind == Wire.AUTH then
return "<auth>"
end
return "<pigeon>"
end
return "<unknown>"
end
const function RecordPacket(Remote:Instance, Packed:{n:number, [number]:any})
const Transport = TransportOf(Remote)
const Event = EventOf(Packed)
const Key = `{Transport}|{Event}`
const Bytes = MeasurePacket("Client", Packed)
local Row = Packets[Key]
if not Row then
Row = {
Transport = Transport,
Event = Event,
Count = 0,
Bytes = 0,
Min = math.huge,
Max = 0
}
Packets[Key] = Row
end
Row.Count += 1
Row.Bytes += Bytes
Row.Min = math.min(Row.Min, Bytes)
Row.Max = math.max(Row.Max, Bytes)
end
const function WatchRemote(Descendant:Instance)
if Watched[Descendant] then
return
end
if not (Descendant:IsA("RemoteEvent") or Descendant:IsA("UnreliableRemoteEvent")) then
return
end
Watched[Descendant] = true
const Remote = Descendant :: RemoteEvent
Remote.OnServerEvent:Connect(function(_Player:Player, ...:any)
RecordPacket(Descendant, table.pack(...))
end)
end
ReplicatedStorage.DescendantAdded:Connect(WatchRemote)
for _, Descendant in ipairs(ReplicatedStorage:GetDescendants()) do
WatchRemote(Descendant)
end
-------------------------------------------------------------------- HELPERS
const function FreshUplink():Uplink
return {
Count = 0,
First = 0,
Last = 0
}
end
const function Record(Uplinks:{[Player]:Uplink}, Player:Player)
local Link = Uplinks[Player]
if not Link then
Link = FreshUplink()
Uplinks[Player] = Link
end
local Now = os.clock()
if Link.Count == 0 then
Link.First = Now
end
Link.Count += 1
Link.Last = Now
end
const function Report(Uplinks:{[Player]:Uplink}, Player:Player):(number, number)
local Link = Uplinks[Player]
if not Link or Link.Count == 0 then
return 0, 0
end
return Link.Count, (Link.Last - Link.First) * 1000
end
-------------------------------------------------------------------- PIGEON SIDE
PigeonBench:When("Bench/Echo", function(_Player:Player, Payload:any)
return Payload
end)
PigeonBench:On("Bench/Up", function(Player:Player, _Index:number, _Payload:any)
Record(PigeonUplinks, Player)
end)
PigeonBench:On("Bench/UpReset", function(Player:Player)
PigeonUplinks[Player] = FreshUplink()
end)
PigeonBench:When("Bench/UpReport", function(Player:Player)
return Report(PigeonUplinks, Player)
end)
PigeonBench:When("Bench/PacketReport", function(_Player:Player)
return Packets
end)
PigeonBench:On("Bench/PacketReset", function(_Player:Player)
table.clear(Packets)
end)
PigeonBench:On("Bench/Blast", function(Player:Player, Count:number, Payload:any)
for Index = 1, Count do
PigeonBench:BroadcastTo(Player, "Bench/Down", Index, Payload)
end
end)
-------------------------------------------------------------------- SOCKET SIDE
SocketBench:On("Bench/Echo", function(_Player:Player, Payload:any)
return Payload
end)
SocketBench:On("Bench/Up", function(Player:Player, _Index:number, _Payload:any)
Record(SocketUplinks, Player)
end)
SocketBench:On("Bench/UpReset", function(Player:Player)
SocketUplinks[Player] = FreshUplink()
end)
SocketBench:On("Bench/UpReport", function(Player:Player)
local Count, Elapsed = Report(SocketUplinks, Player)
return {Count = Count, Elapsed = Elapsed}
end)
SocketBench:On("Bench/Blast", function(Player:Player, Count:number, Payload:any)
for Index = 1, Count do
SocketBench:EmitTo(Player, "Bench/Down", Index, Payload)
end
end)
SocketBench:initialize()
-------------------------------------------------------------------- CLEANUP
Players.PlayerRemoving:Connect(function(Player:Player)
PigeonUplinks[Player] = nil
SocketUplinks[Player] = nil
end)
local Metered = 0
for _ in pairs(Watched) do
Metered += 1
end
print(string.format(
"[Bench] server ready - Pigeon and Socket handlers installed, metering %d remotes",
Metered
))
Client
--!strict
-------------------------------------------------------------------- SERVICES
const ReplicatedStorage = game:GetService("ReplicatedStorage")
const RunService = game:GetService("RunService")
const Stats = game:GetService("Stats")
-------------------------------------------------------------------- MODULES
const Pigeon = require(ReplicatedStorage.Shared.Pigeon)
const Sockets = require(ReplicatedStorage.Shared.Sockets)
const PacketSizeCounter = require(ReplicatedStorage.Shared.PacketSizeCounter)
const Wire = require(ReplicatedStorage.Shared.Pigeon.Wire)
-------------------------------------------------------------------- CONFIG
const WARMUP = 5
const RPC_COUNT = 50
const BURST_COUNT = 250
const SETTLE = 1
const BASELINE_TIME = 3
const DRAIN_TIMEOUT = 10
const SOCKET_TIMEOUT = 5
const PAYLOAD = {
Id = 1337,
Name = "BenchmarkPayload",
Position = {12.5, 64.0, -88.25},
Flags = {Alive = true, Ready = false},
Tags = {"alpha", "beta", "gamma"}
}
-------------------------------------------------------------------- TYPES
export type Sample = {
Min:number,
P50:number,
P95:number,
Max:number,
Avg:number
}
export type Traffic = {
Sent:number,
Received:number,
Duration:number
}
export type PacketRow = {
Transport:string,
Event:string,
Count:number,
Bytes:number,
Min:number,
Max:number
}
export type PacketLedger = {[string]:PacketRow}
-------------------------------------------------------------------- VARS
const PigeonBench = Pigeon.new("BenchPigeon"):Init()
const SocketBench = Sockets.new("BenchSocket")
const Downlink = {
Count = 0,
First = 0,
Last = 0
}
const Idle = {
Sent = 0,
Received = 0
}
const Packets:PacketLedger = {}
-------------------------------------------------------------------- PACKET METER
const Watched:{[Instance]:boolean} = {}
const function MeasurePacket(RunContext:"Client" | "Server", Packed:{n:number, [number]:any}):number
local Dense = true
for Index = 1, Packed.n do
if Packed[Index] == nil then
Dense = false
break
end
end
if Dense then
return PacketSizeCounter.GetPacketSize({
RunContext = RunContext,
RemoteType = "RemoteEvent",
PacketData = Packed
})
end
local Total = PacketSizeCounter.BaseRemoteOverhead
if RunContext == "Client" then
Total += PacketSizeCounter.ClientToServerOverhead
end
for Index = 1, Packed.n do
Total += PacketSizeCounter.GetDataByteSize(Packed[Index])
end
return Total
end
const function TransportOf(Remote:Instance):string
if Remote.Name:sub(1, 9) == "PigeonRef" then
return "Pigeon"
end
return Remote.Name
end
const function EventOf(Packed:{n:number, [number]:any}):string
const First = Packed[1]
if type(First) == "string" then
-- Sockets tunnels RPCs as ("CallRequest", eventName, requestId, ...).
if First == "CallRequest" and type(Packed[2]) == "string" then
return `CallRequest:{Packed[2]}`
end
return First
end
if typeof(First) == "buffer" then
const Ok, Kind, Event = pcall(Wire.unpack, First)
if not Ok then
return "<pigeon>"
end
if type(Event) == "string" and Event ~= "" then
return Event
end
if Kind == Wire.RESPOND then
return "<rpc reply>"
end
if Kind == Wire.AUTH then
return "<auth>"
end
return "<pigeon>"
end
return "<unknown>"
end
const function RecordPacket(Remote:Instance, Packed:{n:number, [number]:any})
const Transport = TransportOf(Remote)
const Event = EventOf(Packed)
const Key = `{Transport}|{Event}`
const Bytes = MeasurePacket("Server", Packed)
local Row = Packets[Key]
if not Row then
Row = {
Transport = Transport,
Event = Event,
Count = 0,
Bytes = 0,
Min = math.huge,
Max = 0
}
Packets[Key] = Row
end
Row.Count += 1
Row.Bytes += Bytes
Row.Min = math.min(Row.Min, Bytes)
Row.Max = math.max(Row.Max, Bytes)
end
const function WatchRemote(Descendant:Instance)
if Watched[Descendant] then
return
end
if not (Descendant:IsA("RemoteEvent") or Descendant:IsA("UnreliableRemoteEvent")) then
return
end
Watched[Descendant] = true
const Remote = Descendant :: RemoteEvent
Remote.OnClientEvent:Connect(function(...:any)
RecordPacket(Descendant, table.pack(...))
end)
end
ReplicatedStorage.DescendantAdded:Connect(WatchRemote)
for _, Descendant in ipairs(ReplicatedStorage:GetDescendants()) do
WatchRemote(Descendant)
end
-------------------------------------------------------------------- TRAFFIC METER
const function OpenMeter():() -> Traffic
local Sent = 0
local Received = 0
local Started = os.clock()
local Connection = RunService.Heartbeat:Connect(function(Delta:number)
Sent += Stats.DataSendKbps * Delta
Received += Stats.DataReceiveKbps * Delta
end)
return function():Traffic
Connection:Disconnect()
return {
Sent = Sent,
Received = Received,
Duration = os.clock() - Started
}
end
end
const function Net(Measured:Traffic):Traffic
return {
Sent = math.max(0, Measured.Sent - (Idle.Sent * Measured.Duration)),
Received = math.max(0, Measured.Received - (Idle.Received * Measured.Duration)),
Duration = Measured.Duration
}
end
const function Baseline()
local Close = OpenMeter()
task.wait(BASELINE_TIME)
local Measured = Close()
Idle.Sent = Measured.Sent / Measured.Duration
Idle.Received = Measured.Received / Measured.Duration
end
-------------------------------------------------------------------- HELPERS
const function Summarise(Samples:{number}):Sample
table.sort(Samples)
local Total = 0
for _, Value in ipairs(Samples) do
Total += Value
end
local Count = #Samples
local Median = math.max(1, math.floor(Count * 0.50))
local Tail = math.max(1, math.floor(Count * 0.95))
return {
Min = Samples[1],
P50 = Samples[Median],
P95 = Samples[Tail],
Max = Samples[Count],
Avg = Total / Count
}
end
const function Row(Label:string, Result:Sample):string
return string.format(
" %-8s %9.3f %9.3f %9.3f %9.3f %9.3f",
Label, Result.Min, Result.P50, Result.P95, Result.Max, Result.Avg
)
end
const function ResetDownlink()
Downlink.Count = 0
Downlink.First = 0
Downlink.Last = 0
end
const function OnDown()
local Now = os.clock()
if Downlink.Count == 0 then
Downlink.First = Now
end
Downlink.Count += 1
Downlink.Last = Now
end
const function DrainDownlink(Expected:number):(number, number)
local Deadline = os.clock() + DRAIN_TIMEOUT
while Downlink.Count < Expected and os.clock() < Deadline do
task.wait(0.05)
end
task.wait(0.25)
return Downlink.Count, (Downlink.Last - Downlink.First) * 1000
end
-------------------------------------------------------------------- STAGES
const function RunRPC(Label:string, Call:(any) -> any):(Sample, Traffic)
for _ = 1, WARMUP do
Call(PAYLOAD)
end
local Close = OpenMeter()
local Samples:{number} = {}
for _ = 1, RPC_COUNT do
local Started = os.clock()
local Reply = Call(PAYLOAD)
table.insert(Samples, (os.clock() - Started) * 1000)
if Reply == nil then
warn(string.format("[Bench] %s dropped an RPC reply", Label))
end
end
return Summarise(Samples), Net(Close())
end
const function RunUplink(Label:string, Reset:() -> (), Send:(number) -> (), Fetch:() -> (number, number)):(number, number, Traffic)
Reset()
task.wait(SETTLE)
local Close = OpenMeter()
for Index = 1, BURST_COUNT do
Send(Index)
end
task.wait(SETTLE)
local Used = Net(Close())
local Received, Elapsed = Fetch()
if Received < BURST_COUNT then
warn(string.format("[Bench] %s uplink lost %d of %d", Label, BURST_COUNT - Received, BURST_COUNT))
end
return Received, Elapsed, Used
end
const function RunDownlink(Label:string, Blast:(number) -> ()):(number, number, Traffic)
ResetDownlink()
task.wait(SETTLE)
local Close = OpenMeter()
Blast(BURST_COUNT)
local Received, Elapsed = DrainDownlink(BURST_COUNT)
local Used = Net(Close())
if Received < BURST_COUNT then
warn(string.format("[Bench] %s downlink lost %d of %d", Label, BURST_COUNT - Received, BURST_COUNT))
end
return Received, Elapsed, Used
end
-------------------------------------------------------------------- REPORTING
const function Throughput(Received:number, Elapsed:number):number
if Elapsed <= 0 then
return 0
end
return Received / (Elapsed / 1000)
end
const function BurstLine(Label:string, Received:number, Elapsed:number):string
return string.format(
" %-8s %4d/%d in %8.3fms (%9.0f msg/s)",
Label, Received, BURST_COUNT, Elapsed, Throughput(Received, Elapsed)
)
end
const function BytesPer(Kilobytes:number, Count:number):number
if Count <= 0 then
return 0
end
return (Kilobytes * 1024) / Count
end
const function TrafficLine(Label:string, Used:Traffic, Count:number):string
return string.format(
" %-8s up %8.2f KB (%7.1f B/msg) down %8.2f KB (%7.1f B/msg)",
Label,
Used.Sent, BytesPer(Used.Sent, Count),
Used.Received, BytesPer(Used.Received, Count)
)
end
const function PacketLines(Ledger:PacketLedger):{string}
const Keys:{string} = {}
for Key in pairs(Ledger) do
table.insert(Keys, Key)
end
table.sort(Keys)
if #Keys == 0 then
return {" (no packets seen)"}
end
const Lines:{string} = {}
for _, Key in ipairs(Keys) do
const Row = Ledger[Key]
table.insert(Lines, string.format(
" %-8s %-26s %5d %8d B %6d %8.1f %6d",
Row.Transport, Row.Event, Row.Count, Row.Bytes,
Row.Min, Row.Bytes / Row.Count, Row.Max
))
end
return Lines
end
-------------------------------------------------------------------- RUN
const function RunBenchmark()
task.wait(2)
SocketBench:initialize()
task.wait(SETTLE)
print(string.format(
"[Bench] measuring idle bandwidth for %ds ...",
BASELINE_TIME
))
Baseline()
print(string.format(
"[Bench] starting - %d RPCs, %d message bursts",
RPC_COUNT, BURST_COUNT
))
table.clear(Packets)
PigeonBench:Emit("Bench/PacketReset")
task.wait(SETTLE)
---------------------------------------------------------------- RPC
local PigeonRPC, PigeonRPCNet = RunRPC("Pigeon", function(Payload:any):any
return PigeonBench:Call("Bench/Echo", Payload)
end)
local SocketRPC, SocketRPCNet = RunRPC("Socket", function(Payload:any):any
return SocketBench:Call("Bench/Echo", SOCKET_TIMEOUT, Payload)
end)
---------------------------------------------------------------- UPLINK
local PigeonUp, PigeonUpMs, PigeonUpNet = RunUplink("Pigeon", function()
PigeonBench:Emit("Bench/UpReset")
end, function(Index:number)
PigeonBench:Emit("Bench/Up", Index, PAYLOAD)
end, function():(number, number)
local Count, Elapsed = PigeonBench:Call("Bench/UpReport")
if type(Count) ~= "number" or type(Elapsed) ~= "number" then
return 0, 0
end
return Count, Elapsed
end)
local SocketUp, SocketUpMs, SocketUpNet = RunUplink("Socket", function()
SocketBench:Emit("Bench/UpReset")
end, function(Index:number)
SocketBench:Emit("Bench/Up", Index, PAYLOAD)
end, function():(number, number)
local Result = SocketBench:Call("Bench/UpReport", SOCKET_TIMEOUT)
if type(Result) ~= "table" then
return 0, 0
end
return Result.Count, Result.Elapsed
end)
---------------------------------------------------------------- DOWNLINK
PigeonBench:On("Bench/Down", OnDown)
local PigeonDown, PigeonDownMs, PigeonDownNet = RunDownlink("Pigeon", function(Count:number)
PigeonBench:Emit("Bench/Blast", Count, PAYLOAD)
end)
PigeonBench:Off("Bench/Down")
SocketBench:On("Bench/Down", OnDown)
local SocketDown, SocketDownMs, SocketDownNet = RunDownlink("Socket", function(Count:number)
SocketBench:Emit("Bench/Blast", Count, PAYLOAD)
end)
SocketBench:Off("Bench/Down")
---------------------------------------------------------------- REPORT
const DownPackets:PacketLedger = table.clone(Packets)
local UpPackets = PigeonBench:Call("Bench/PacketReport")
if type(UpPackets) ~= "table" then
UpPackets = {}
end
local Lines = {
string.format("RPC round trip, %d sequential calls (ms)", RPC_COUNT),
" min p50 p95 max avg",
Row("Pigeon", PigeonRPC),
Row("Socket", SocketRPC),
"",
string.format("Client -> server burst, %d messages", BURST_COUNT),
BurstLine("Pigeon", PigeonUp, PigeonUpMs),
BurstLine("Socket", SocketUp, SocketUpMs),
"",
string.format("Server -> client burst, %d messages", BURST_COUNT),
BurstLine("Pigeon", PigeonDown, PigeonDownMs),
BurstLine("Socket", SocketDown, SocketDownMs),
"",
"Wire volume, idle traffic subtracted (client-side Stats counters)",
string.format(" -- RPC stage, %d calls --", RPC_COUNT),
TrafficLine("Pigeon", PigeonRPCNet, RPC_COUNT),
TrafficLine("Socket", SocketRPCNet, RPC_COUNT),
string.format(" -- uplink stage, %d messages --", BURST_COUNT),
TrafficLine("Pigeon", PigeonUpNet, BURST_COUNT),
TrafficLine("Socket", SocketUpNet, BURST_COUNT),
string.format(" -- downlink stage, %d messages --", BURST_COUNT),
TrafficLine("Pigeon", PigeonDownNet, BURST_COUNT),
TrafficLine("Socket", SocketDownNet, BURST_COUNT),
"",
"Measured packet sizes per remote event (PacketSizeCounter)",
string.format(
" %-8s %-26s %5s %8s %6s %8s %6s",
"via", "event", "count", "total", "min", "avg", "max"
),
string.format(" client -> server, weighed on the server (includes the %d RPC warmups)", WARMUP),
}
const function Append(More:{string})
table.move(More, 1, #More, #Lines + 1, Lines)
end
Append(PacketLines(UpPackets))
table.insert(Lines, " server -> client, weighed on this client")
Append(PacketLines(DownPackets))
table.insert(Lines, "")
print(table.concat(Lines, "\n"))
end
task.spawn(RunBenchmark)