Concurrency testing started out simple enough – launch multiple instances of the client app and see how well the server can handle them all. First I used a simple DOS script that would launch 10 clients each time:
for /l %%i in (0,1,10) do (start AsyncSocketClient.exe -port 8989 -address 127.0.0.1)
Launching the client app this many times turns out to be a bad idea since each instance of the app consumes about 8 MB of memory, but even so, the server can handle 100 clients all connecting at once just fine:
Upon further review, the SendMessageToClient function or the callback in the UI thread isn’t optimized properly. A stress test has been built into the client app which creates 100 or 1000 sockets and connects them to the server immediately (no sleep), simulating a massive influx of clients. This heavy load experiment piqued my interest, so I had a look into the SendMessageToClient function to see if it could be optimized. In the callback, we are updating a listbox and textbox on the server’s UI thread. Avoiding these two updates causes the server to perform much better, and that’s fine because we can omit the Forms-based UI and simply write strings to the console, as most servers do.
We can clearly see that the server is able to handle over 10,000 connections without a hiccup:
However, once the number of client connections exceeds 16,000, we start noticing issues again:
As it turns out, it was the string concatenation in the server’s UI thread that caused the exponential performance drop. That should have been obvious sooner.
It also looks like the 16,000 figure is exactly the number of connections my test machine can handle before it completely bombs out, where even websites won’t load! Closing a few clients releases a few connections, and websites can load again. Here are some test results:
Simulating 1000 client connections over 16 iterations... 1) 485.0278 ms 2) 452.0259 ms 3) 495.0283 ms 4) 476.0272 ms 5) 472.027 ms 6) 477.0273 ms 7) 522.0299 ms 8) 516.0295 ms 9) 457.0261 ms 10) 506.029 ms 11) 474.0271 ms 12) 496.0283 ms 13) 545.0312 ms 14) 516.0295 ms 15) 517.0296 ms 16) 540.0309 ms All iterations complete. Total duration: 7949.4547 ms
Not bad but I think we can do better! The server can now push a host system to the port limit without bogging down. Here’s over 64,000 clients connected:
The time it took the client (stress test) and the server to connect that many clients was 81 seconds, with the server process consuming just 238 MB of RAM. Next I’d like to test broadcasting messages to multiple clients and observe the performance/latency. I’d also like to do some real-world simulations (DoS) by running the stress test over the internet.
Head on over to Part II of this series where I will do some comparisons against Node.js and Apache.