25
votes

I am benchmarking a Java UDP client that continuously sends datagrams with a payload of 100 bytes as fast as it can. It was implemented using java.nio.*. Tests show that it's able to achieve a steady throughput of 220k datagrams per second. I am not testing with a server; the client just sends the datagrams to some unused port on localhost.

I decided to run the same test in Node.js to compare both technologies and it was surprisingly sad to see that Node.js performed 10 times slower than Java. Let me walk you through my code.

First, I create a UDP socket using Node.js's dgram module:

var client = require('dgram').createSocket("udp4");

Then I create a function that sends a datagram using that socket:

function sendOne() {
    client.send(message, 0, message.length, SERVER_PORT, SERVER_ADDRESS, onSend);
}

The variable message is a buffer created from a string with a hundred characters when the application starts:

var message = new Buffer(/* string with 100 chars */);

The function onSend just increments a variable that holds how many datagrams were sent so far. Next I have a function that constantly calls sendOne() using setImmediate():

function sendForever() {
    sendOne();
    setImmediate(sendForever);
} 

Initially I tried to use process.nextTick(sendForever) but I found out that it always puts itself at the tip of the event queue, even before IO events, as the docs says:

It runs before any additional I/O events (including timers) fire in subsequent ticks of the event loop.

This prevents the send IO events from ever happening, as nextTick is constantly putting sendForever at the tip of the queue at every tick. The queue grows with unread IO events until it makes Node.js crash:

fish: Job 1, 'node client' terminated by signal SIGSEGV (Address boundary error)

On the other hand, setImmediate fires after I/O events callbacks, so that's why I'm using it.

I also create a timer that once every 1 second prints to the console how many datagrams were sent in the last second:

setInterval(printStats, 1000);

And finally I start sending:

sendForever();

Running on the same machine as the Java tests ran, Node.js achieved a steady throughput of 21k datagrams per second, ten times slower than Java.

My first guess was to put two sendOne's for every tick to see if it would double the throughput:

function sendForever() {
    send();
    send();  // second send
    setImmediate(sendForever);
}

But it didn't change the throughput whatsoever.

I have a repository available on GitHub with the complete code:

https://github.com/luciopaiva/udp-perf-js

Simply clone it to your machine, cd into the folder and run:

node client

I want to open a discussion about how this test could be improved in Node.js and if there's some way we can increase Node.js's throughput. Any ideas?

P.S.: for those interested, here is the Java part.

2
@mscdex a while loop would not help, as Node.js wouldn't be able to finish the current tick and also wouldn't be able to process queued IO events... the application would freeze.Lucio Paiva
I belive in this case it's crucial to investigate also the java code. The link to java code you provided in P.S. section does not work. Could you please update it ?Jan Grz
Hey @JanOsch, thanks for noticing it. I have inadvertently removed it from my public account. You can check it there now; it's back online.Lucio Paiva
@rels yes, this could explain the difference for sure. We'd have to test it by confirming that Node.js does not buffer before calling the system (and, by extent, confirm that the underlying buffer that the Java documentation refers to is actually Java's and not the OS's) and modify Node's source to add a buffer and see what happens. Please let me know if you run any tests on it. I'm not working on this problem anymore, but am still curious about what could be wrong.Lucio Paiva
Hmm, I was just wondering if a newer version would perform better. Thanks for sharing, @JrBenitoLucio Paiva

2 Answers

2
votes

That test is overfly flawed. UDP doesn't guarantee the delivery of anything and doesn't guarantee that it would give any error in case of error.

Your application could send 1000k datagram/s at 1GB/s from the Java application, yet 90% of datagrams never reached the destination... the destination might not even be running.

If you want to do any sort of UDP testing, you need two applications, one on each end. Send numbered datagrams 1, 2, 3... and check what's sent and what's received. Note that UDP doesn't guarantee any ordering of messages.

Kernels manage the localhost network in special ways. There are huge buffers dedicated to it and higher limits, no traffic ever goes through any network cards or drivers. It's very different from sending packets for real.

Tests might seem somewhat okay when they're only done on localhost. Expect everything to fail miserably when it's going through any physical infrastructure for real.

PC1 <-----> switch <-----> PC2

Let's say, there are two computers in the same room linked by a switch. It would be no small feat to achieve 10k/s UDP datagrams on that simple setup, without loosing messages randomly.

And that's just two computers in the same room. It can be a lot worse on the Internet and long distance.

1
votes

If all you want is to make the performance test go faster, removing the setImmediate call and executing the next send once the first has completed i.e. in the send callback increased its performance to ~100k requests per second on my slowish laptop.

function send(socket, message) {
  socket.send(message, SERVER_PORT, (err) => {
    send(socket, message);
  });
}

const socket = require('dgram').createSocket('udp4');
const message = new Buffer('dsdsddsdsdsjkdshfsdkjfhdskjfhdskjfhdsfkjsdhfdskjfhdskjfhsdfkjdshfkjdshfkjdsfhdskjfhdskjfhdkj');
send(socket, message);