I think it is important to make some examples when we compare languages and Runtimes, or libraries and protocols, as I did for my post on GPRC. This time is the time of NodeJS and Java. How did I made the test? Easy, I took a well known algorithm that computes in a huge amount of time and that is really easy to implement in exactly the same way in the two languages, in my case the insertion sort, I created a webapp in the two languages (with Express for Node and Spring Boot for Java) and I run in a single thread and in a multithread mode. For Node I used the latest version, currently 18.2.0, and I used the worker_threads module to boost it at its maximum. For Java I used Java 12, there is the 17 but I had few issues in configuring it with intellij and I had already spent lot of time in learning basics of Node.

To complete the test I also dockerised the two apps and run them in a container, to appreciate the differences. First of all, the code:

'use strict';

const express = require('express');
const { Worker } = require('worker_threads');
if (!Date.ts) {
    Date.ts = function() { return new Date().getTime(); }
}

// Constants
const PORT = 8080;
const HOST = '0.0.0.0';

// App
const app = express();

app.use(
  express.urlencoded({
    extended: true,
  })
);
//app.use(express.raw({type:'text/plain'}))
app.use(express.json());


var farmprocstart = 0;
var farmprocend = 0;


app.post('/', (req, res) => {
  console.log(req.body);
  console.time("Execution");
  var result = insertion_sort.insertion_sort(req.body.test.map((x) => x));
  console.timeEnd("Execution");
  console.time("ExecutionFarm");
  farmprocstart = Date.ts();
  start_farm(req.body.test);
  console.timeEnd("ExecutionFarm");
  res.send('PONG: '+result);
});

var insertion_sort = function(arr) {
  for (let i = 1; i < arr.length; i++) {
      // Start comparing current element with every element before it
      for (let j = i - 1; j > -1; j--) {
        
          // Swap elements as required
          if (arr[j + 1] < arr[j]) {
              [arr[j + 1], arr[j]] = [arr[j], arr[j + 1]];
          }
      }
  }
  return arr;
}

var start_farm = function(arr) {
  for (let i = 0; i < 200; i++) {
    start_worker(arr.map((x) => x), i);
  }
}

var start_worker = function(arr, id) {
  const worker = new Worker('./insertion_worker.js', { 
      workerData: arr
  });
  worker.once('message', (arr) => {
    farmprocend = Date.ts();
    console.log(farmprocend - farmprocstart);
  });
}


app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);

It is quite self-explanatory, it is not a post on Node. The following is the Worker.

'use strict';

const { parentPort, workerData } = require('worker_threads');

const arr = workerData;
for (let i = 1; i < arr.length; i++) {
  // Start comparing current element with every element before it
  for (let j = i - 1; j > -1; j--) {
    
      // Swap elements as required
      if (arr[j + 1] < arr[j]) {
          [arr[j + 1], arr[j]] = [arr[j], arr[j + 1]];
      }
  }
}
// Send the hashedArray to the parent thread
parentPort.postMessage(arr);
process.exit()

I totally know I could have reused the algorithm for the insertion sort, I was just bothered to use/understand javascript 😛

{
  "name": "docker_web_app",
  "version": "1.0.0",
  "description": "Node.js on Docker",
  "author": "First Last <first.last@example.com>",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "^4.16.1"
  }
}

This is to package all the required libraries with NPM. Last but not least, the Dockerfile

FROM node:18.2-alpine3.15

# Create app directory
WORKDIR /usr/src/app

# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./

RUN npm install
# If you are building your code for production
# RUN npm ci --only=production

# Bundle app source
COPY . .

EXPOSE 8080

ENTRYPOINT ["node", "app.js"]

Ok, that’s it! I won’t go for all the required files for Spring Boot, I used JIB for packaging it. I just put the RestController

package name.lorenzani.andrea.easy_node.Controller;

import name.lorenzani.andrea.easy_node.model.Input;
import name.lorenzani.andrea.easy_node.worker.Worker;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.CompletableFuture;

@RestController
public class HomeController {

    @GetMapping("/ciao")
    public String index() {
        return "Greetings from Spring Boot!";
    }

    @PostMapping(path = "/", consumes = MediaType.ALL_VALUE)
    public Integer[] create(@RequestBody(required = true) Input data) {
        Integer[] original = data.getTest();
        long startExec = System.currentTimeMillis();
        Integer[] firstInput = original.clone();
        Integer[] result =  Worker.sort(firstInput);
        long endExec = System.currentTimeMillis();
        CompletableFuture[] allTheCompl = new CompletableFuture[200];
        for(int i=0; i<200; i++) {
            allTheCompl[i] = CompletableFuture.supplyAsync(() -> Worker.sort(original.clone()));
        }
        CompletableFuture.allOf(allTheCompl).thenRun(() -> {
            long endAll = System.currentTimeMillis();
            System.out.println("Total computation: "+(endAll-startExec));
            System.out.println("Single computation: "+(endExec-startExec));
            System.out.println("Thread computation: "+(endAll-endExec));
        });
        return result;
    }

}

and the Worker class, and I let you taste analogies with the worker in Node.

public class Worker {

    public static Integer[] sort(Integer[] input) {
        for (int i = 1; i < input.length; i++) {
            // Start comparing current element with every element before it
            for (int j = i - 1; j > -1; j--) {

                // Swap elements as required
                if (input[j + 1] < input[j]) {
                    Integer swap = input[j+1];
                    input[j+1] = input[j];
                    input[j] = swap;
                }
            }
        }
        return input;

    }
}

I ran the two codes and I have to say that what I noticed immediately is the huge amount of time to spawn the 200 workers. That literally froze the app, that was not responding for 10 seconds in my machine and for even 50 seconds running on containers, while Java was replying immediately.

Ok, those are my results:

TestNode ServerJava Server
Docker Single Threaded105 ms106 ms
Docker Multithreaded34.5 sec9.8 sec
Docker Response Time13.50 sec302 ms
Test result Node vs Java

I think this is nothing astonishing. Follow me for other wonderful tests.

Stay tuned!

Share