Hello there!
I’d really appreciate your help with this, I can’t figure it out and I suspect it’s more of a “feature” than a bug.
I guess the people more likely to explain this, would be the Xojo engineers who know how the Xojo Framework works under the hood.
The situation is as follows:
There’s this webapi-oriented server that I’m making, which you can find here
It is a ground-up implementation of the HTTP protocol, using a ServerSocket and SSLSockets.
One of its main features, is that every new socket fires up a thread that has access to that socket, so it can do its I/O stuff.
In this particular application, it’s doing filesystem access and manipulation via a web api. Uploads, downloads, renames, deletes, pretty straightforward…
Unsurprisingly, it implements file download functionality: The GET method of the /files/ endpoint opens a BinaryStream to a file and passes chunks of data from the file stream to the socket, until EOF.
I’ve tried different variations for the code to do that, and I’ve concluded that this appears to be the most optimal, in terms of performance/concurrency:
(This is just the loop where TX happens. The method is in endpoint_files.GET if you care to dive into it)
try
stream = BinaryStream.Open(file)
Readable = true // it is readable
WorkerThread.SocketRef.PrepareResponseHeaders_SendBinaryFile(FileSize , file.Name)
WorkerThread.SocketRef.RespondOK(true)
while not stream.EndOfFile
chunk = stream.Read(ipsc_Lib.SocketChunkSize * n) // adjust n to taste, currently 4
WorkerThread.YieldToNext
if not WorkerThread.SocketRef.IsConnected then exit while // freezes on connection drops without it, in this exact place
WorkerThread.SocketRef.Write(chunk)
WorkerThread.BytesSent = WorkerThread.BytesSent + chunk.Bytes
WorkerThread.SocketRef.Flush // without this, it is all one big data packet
WorkerThread.YieldToNext
wend
Catch e as IOException
stream.Close
if not Readable or WorkerThread.BytesSent = 0 then // nothing has been sent, we can respond in error
WorkerThread.SocketRef.RespondInError(423 , "Unreadable file , IO error " + e.ErrorNumber.ToString) // locked
Return
end if
// we got an io error while we had already started sending an OK response
// we just kill the connection and hope the client can detect it's incomplete
WorkerThread.SocketRef.Disconnect
WorkerThread.SocketRef.Close
Return
end try
When the server and clients are both on the localhost (or on different gigabit ethernet-connected devices), it works blazingly fast, for multiple concurrent downloads.
The problem manifests clearly when one of the concurrent downloads takes place via a connection that’s much slower than the rest.
What happens then, is that the slowest transfer, sets the pace for all active transfers at the time.
A way to demonstrate that is by printing a timestamped message every time a socket’s SendComplete event fires.
In the first example, we have two open sockets (840,792) sending an 845MB file to clients that also run on the same machine as the server.
The big number is a milliseconds timestamp. Notice that the interval between two SendCompletes is around 20ms for both sockets.
ipsc_Connection.SendComplete : 1768851272 : connection 840 - UserAborted = False
ipsc_Connection.SendComplete : 1768851300 : connection 792 - UserAborted = False
ipsc_Connection.SendComplete : 1768851301 : connection 840 - UserAborted = False
ipsc_Connection.SendComplete : 1768851322 : connection 792 - UserAborted = False
ipsc_Connection.SendComplete : 1768851322 : connection 840 - UserAborted = False
ipsc_Connection.SendComplete : 1768851342 : connection 792 - UserAborted = False
In the second example, we also have two open sockets sending the same file.
The first one (852) is with a client that runs on the same machine. That should have been a fast one.
The second (720), is with a client that runs on a laptop connected via wifi to the home LAN. A much slower link that is.
What happens, is that while the slow transfer is active, they both run on pretty much the same pace.
When the slow one finishes or gets aborted, the fast one skyrockets, as it normally would.
ipsc_Connection.SendComplete : 1772574921 : connection 852 - UserAborted = False
ipsc_Connection.SendComplete : 1772576537 : connection 720 - UserAborted = False
ipsc_Connection.SendComplete : 1772576537 : connection 852 - UserAborted = False
ipsc_Connection.SendComplete : 1772577772 : connection 720 - UserAborted = False
ipsc_Connection.SendComplete : 1772577777 : connection 852 - UserAborted = False
ipsc_Connection.SendComplete : 1772578738 : connection 720 - UserAborted = False
ipsc_Connection.SendComplete : 1772578739 : connection 852 - UserAborted = False
ipsc_Connection.SendComplete : 1772579723 : connection 720 - UserAborted = False
Exact same thing happens when more than 2 connections are involved.
Now, I was under the impression that every Socket works independently of the rest, but this is obviously not the case.
I guess there can 3+1 causes:
- My code (obviously)
- The threading system
- The sockets system
- a good combination of some/all of the above
Could someone be kind enough to shed some light on the situation?
Windows 10, Xojo 2021R2 btw.
Thanks!
George