DEV Community

Cover image for ProgpJS: a fast javascript engine for Go!
Johan PIQUET
Johan PIQUET

Posted on • Updated on

ProgpJS: a fast javascript engine for Go!

Introduction

Hello I'm Johan author of ProgpJS. It's my first open-source project, and it's why I'm very proud to write about it today.

What is ProgpJS? You can see it like Node.js, but written in Go. Go is a language created by Google for server applications requiring very high speed and stability, which is why I choose this solution for my own projects and it's why I used this language to create ProgpJS.

How fast is ProgpJS? Near 2x faster than Node.js! His speed is on par with DenoJS and BunJS, while ProgpJS can be much (much) faster when you mix Go code and javascript code. Here I will show you how to start with ProgpJS and how to create a function able to handle near 180.000 req/sec on a simple Mac Book Air! ... where Node.js can't go over 80.000 req/sec.

The first thing to known about ProgpJS is that it's very simple to use! With ProgpJS you write simple Go functions and the engine handle all the technical stuffs for you! The second thing to know is that ProgpJS uses two execution mode: dynamic mode (which is the default one) and compiled mode. Compile mode means that the engine will generate highly optimized code in order to make thing a lot faster. Here we will use compiled mode in order to build something fast.

What we will learn in this tutorial?

Here we will use ProgpJS as a toolkit for Go sofware and no as a standalone solution. We will start with the compiled mode and write a simple http server responding "hello world". We will write two versions of this server: the first one will only use javascript and the second one will use Go to check a cache before calling our javascript.

We will learn how to start with ProgpJS, how to expose a Go function to javascript and take a first look on how to embed ProgpJS in our own Go project.

Some benchmarks

Here is a little benchmark allowing you to feel how fast ProgpJS is. This benchmark test a simple http server responding "hello world". It's not a real world benchmark but it allows to feel how fast is ProgpJS internal pipeline. And I think it's a good introduction to make you want to know a little more about ProgpJS!

Round 1, where 10 clients are bombarding the server at full speed:

  • #1 - BunJS with 139663 req/sec
  • #2 - ProgpJS with 128473 req/sec
  • #3 - DenoJS with 114776 req/sec
  • #4 - NodeJS with 81374 req/sec

Round 2, this time 500 clients are bombarding:

  • #1 - BunJS with 135905 req/sec
  • #2 - DenoJS with 120302 req/sec
  • #4 - ProgpJS with 94261 req/sec
  • #5 - NodeJS ? ... is scratching (near the 250 clients)

Round 3, 1500 clients:

  • #1 - DenoJS with 99199 req/sec
  • #2 - ProgpJS with 79421 req/sec

Here ProgpJS don't lose any connection, all call respond with a code 200. When a server is saturating we get more and more code 500, but here ProgpJS he is comfortable. He is slower than DenoJS in this type of benchmark du to the fact that Go uses of "virtual threads" and a garbage collector. In fact micro-call are the nemesis of ProgpJS when using
pure javascript solutions, while it's his good point when mixing javascript and Go.

Round 4, 1500 clients while using a mixe between Go and javascript:

  • #1 - ProgpJS with 173946 req/sec
  • #2 - DenoJS with 99199 req/sec

As you can see the benchmark is much better! Here we use a Go function managing a cache, only calling our javascript if the cache is empty. It's what we will build in this tutorial. This possibility to easily mix javascript and Go code is something that only ProgpJS, and being able to do that is why I created ProgpJS. In this tutorial you will see how it's easy to add a Go function called from javascript, it's very easy!

Prerequisites

  • The current version of ProgpJS doesn't support Windows, but you can use WSL2 (which is mainly a virtual machine hosting a linux). It works with Apple Silicon, Linux x64 and Linux ARM.
  • You must install Go, version 1.21. It probably supports older versions, but it's not tested with.
  • Git must be installed in order to clone projects.

Installing the sample project

The github for ProgpJS is here. It contains a lot of projects.

The more important one are this projects:

  • progpjs: which is the high level API allowing you to use ProgpJS.
  • progpAPI: here it's the low level API.
  • progpV8Engine: is a wrapper around the v8 engine.
  • progpQuickjsEngine: will host the wrapper for the QuickJS engine.
  • modules: contains the main javascript modules and the Go code implementing them.
  • httpServer: the very fast http server build for ProgpJS.
  • samples: a sample project, heavily commented.

Ok here we will start by cloning the "samples" project. It uses git submodules, it's why we won't use a simple "git clone" but this command:

git clone --recurse-submodules https://github.com/progpjs/samples

Once cloned a directory samples is created and contains four things:

  • sampleProject which is a project contains samples about how to use ProgpJS.
  • progpjs.progpV8Engine which contains the wrapper around V8 engine, which is what execute our javascript.
  • go.work which allow to create a "workspace".
  • index.ts, which is our javascript script (here it's typescript in fact).

A workspace is a Go feature. It's allows us to replace some dependencies by our own version. Dependencies are like Node.js modules (folder nodes_modules) but for Go. ProgpJS compiled mode needs to use a workspace because theb egine will automatically update two files inside the progpV8Engine directory: generated.go and generated.cpp. Since it's no more the mainstream version, then we need to use a workspace in order to work with our own version.

The dynamic mode don't use code generation and allow to use ProgpJS as a simple Go modules. I will show you how to use it later in a second tutorial. But here we use the compiled mode, which is the more complexe one.

The script index.ts

The file index.ts is the script called by our sample project when starting. It's typescript and not javascript. ProgpJS support typescript natively, without compilation step.
It also support simple javascript files (.js), ReactJS files (.jsx) and Typescript for ReactJS (.tsx) file.

Here our index.ts start a http server listening at url http://localhost:8000. His content is this:

import {HttpServer} from "@progp/http"
let server = new HttpServer(8000), host = server.getHost("localhost");
host.GET("/", async req => req.returnHtml(200, "Hello world"));
server.start();
Enter fullscreen mode Exit fullscreen mode

Running our project

Starting our Go project is simple, you only have to use the command go run . as here:

cd ./samples/sampleProject

go run .

When launched for the first time, a message tell you that the engine has updated his own source code, asking you to restart.
As you see you don't need an extra build step in order to update the generated code, it's done for you.

Javascript binding code has been updated.

A restart is required.

If you have this messager it's mean that all is ok and you are using compile mode. Now we restart our app.

go run .

In result, it shows you this message Server started at http://localhost:8000. If you click on the link a web page open and print a simple "Hello World". If you want to test ProgpJS http stack performances, you can use a tool named bombardier. Here we use it with 500 concurrent connections, which is a lot:

bombardier -c500 -n1000000 http://localhost:8000

On my Macbook Air M1 the result is 89166, which mean that our server is able to respond to near 90.000 req/sec. It's an average, sometime it's faster and sometime slower.
Each page is processed in 0.01ms, it's why each little thing executing on my computer can hinder the performance and it's why there is a big difference between the max speed
(170.000 req/sec) and the average speed (90.000 req/sec).

Statistics        Avg      Stdev        Max
  Reqs/sec     89166.95   17658.39  170446.13
  Latency        5.56ms     4.55ms   163.58ms
  HTTP codes:
    1xx - 0, 2xx - 1000000, 3xx - 0, 4xx - 0, 5xx - 0
    others - 0
  Throughput:    16.46MB/s
Enter fullscreen mode Exit fullscreen mode

We want more!

Here ProgpJS demonstrates that he is very fast despite a heavy bombardment. And as you can see all call responded with a code 200, which mean that ProgpJS isn't saturated at all.
It's handsome for a pure javascript solution :-) but our project need somethat able to handle more micro-call in order to reduce our server costs. It's why we use ProgpJS ... and it's why we will write a Go function processing himself the http call while call our javascript to feed a cache.It will be a very basic version acting as a sample.

To do that we will start by creating a Go function which will be called by our javascript. In order to make thing simpler for this tutorial and help you add your own functions, I already added a placeholder where a sample function is added. You can found it in this file sampleProject/modSamples/jsMyFunctions.go. Here this file content is:

func jsMyFunctions(group *progpAPI.FunctionGroup) {
    group.AddFunction("testMyFunction", "JsTestMyFunction", JsTestMyFunction)

    // If you want to add more than one function:
   // group.AddFunction("mySecondFunction", "JsMySecondFunction", JsMySecondFunction)
}

func JsTestMyFunction() {}
Enter fullscreen mode Exit fullscreen mode

From javascript to Go

The function jsMyFunctions is called by our own code in order to declare our Go functions exposed to javascript. Our Go function is named JsTestMyFunction while the javascript function will be named testMyFunction. You only have to do testMyFunction() from javascript to call this Go function.

From Go to javascript

Now we will upgrade our project in order to be able to call a javascript function from our Go code. The first thing to do, is allow our Go function to received a reference to a javascript function. It's the function that we will call when our cache is empty. In the javascript side, we will have something like testMyFunction(() => { console.log("Hi!") }).

With ProgpJS the only thing you have to do is add or remove parameters to you Go function. You old Go function was:

func JsTestMyFunction() {}
Enter fullscreen mode Exit fullscreen mode

and now it's

func JsTestMyFunction(callback progpAPI.JsFunction) { }
Enter fullscreen mode Exit fullscreen mode

There isn't no oher thing to do inder order to received and call a javascript function from Go!

Since we need to send a javascript function to our Go function, we will update our Go code in order to accept a javascript function as our first parameter. Our Go function JsTestMyFunction will become:

// ProgJS will automatically fill the param "callback" with the function send by our javascript call.
func JsTestMyFunction(callback progpAPI.JsFunction) {
    // For sample, here we call our javascript function.
    //
    // If we use this javascript:  testMyFunction(() => { console.log("Hi!") })
    // then the console will print "Hi!" once our callback done.
    //
    callback.CallWithUndefined();
}
Enter fullscreen mode Exit fullscreen mode

The only thing we need to do, is adding a parameter to our function, and it will automatically receive the value used in the javascript side.

If you have an error after compiling, about generated.go or generated.cpp, then execute the script reset_generated_code.sh. Why this error? It's because the generated code is build for our the previous version of our function. So if your function header is updated (the header, not the body) then your the generated code is mismatching and the project can't compile. The reset script will only replace generated.go and generated.cpp by a blank version.

Sending more data from javascript to Go

Now we will add two other informations: the url of our webpage and the name of the cache entry. Our Go function becomes:

func JsTestMyFunction(url string, cacheKey string, callback progpAPI.JsFunction) { }
Enter fullscreen mode Exit fullscreen mode

As for our previous change the only thing required is to add parameters in our Go function, the engine will automatically handle the technical stuffs and generate the C++ code for us.

./reset_generated_code.sh
go run .

Adding our cache logic

Now we will start to add our cache logic: bind a listener to our url, check a cache and call our javascript to get the content if not in cache. Our Go function become something like this:

func JsTestMyFunction(url string, cacheKey string, callback progpAPI.JsFunction) {
    // Get our server. This server has already been created by our javascript code
    // so we only need to know his port to get it.
    //
    server := httpServer.GetHttpServer(8000)

    // Get our host. ProgpJS http server don't listen to any hostname
    // unlike node.js server. It allows you to have one server responding
    // to more than url while using the same port. It's very useful since
    // most of the time the opened ports are limited and need firewall rule.
    //
    host := server.GetHost("localhost")

    // Now we bind a listener tou our url.
    // host.GET allows to say what function must be executed when our url is called.
    //
    host.GET(url, func(call httpServer.HttpRequest) error {
        // TODO: check the cache, call our js, ...
        return nil
    })
}
Enter fullscreen mode Exit fullscreen mode

It would be more simple to directly send the host objet from javascript to Go, since we already
have a reference to it. But here for this tutorial I don't use, the goal being to introduce some mechanisms.

Sending a Go object to javascript

Here we will call our javascript and sending him a reference to the Go object containing our http call informations. It's possible to send simple Go object to Javascript
and read them from our script, but here we will send the object call which is to complex and too big. It's why we will not send the object himself but a reference to this object.
It's what is named a "shared resource" : something shared between Go and Javascript. It's basically a pointer (for thos knowing Go or C++ pointers).

The idea behind shared resources is to bind an number to a Go object (for exemple object number 14). From there, when our javascript call one of our Go function
it can say "it's about the object with number 14". We don't send the object himself, but his number which allow to known of what object we are speaking.

In order to use shared resource, we need something named a shared resource container. The main goal of this containers is to allows to automatically dispose our shared resources and avoid memory bleeding. ProgpJS add functionnalities for resources management allowing to have more control on this resources shared between Go and Javascript.

Getting this shared resource manager is easy: we only need to add one parameter to our function:

func JsTestMyFunction(url string, cacheKey string, callback progpAPI.JsFunction)

became

func JsTestMyFunction(rc *progpAPI.SharedResourceContainer, url string, cacheKey string, callback progpAPI.JsFunction)

Here rc is a "virtual parameter": it exists in the Go side but not in the javascript side. The engine known that it's a special parameter and it will automatically fill his value for you.

Our final function

He is the final version of our function. I put the full file content and you can direcly copy/past it.

package modSamples

import (
  "github.com/progpjs/httpServer/v2"
  "github.com/progpjs/progpAPI/v2"
)

func jsMyFunctions(group *progpAPI.FunctionGroup) {
  group.AddFunction("testMyFunction", "JsTestMyFunction", JsTestMyFunction)
}

var gCache = make(map[string]string)

func JsTestMyFunction(rc *progpAPI.SharedResourceContainer, url string, cacheKey string, callback progpAPI.JsFunction) {
  server := httpServer.GetHttpServer(8000)
  host := server.GetHost("localhost")

  host.GET(url, func(call httpServer.HttpRequest) error {
    if cache, ok := gCache[cacheKey]; ok {
      call.ReturnString(200, cache)
      return nil
    }

    // Will allows to know what has been sent.
    spy := httpServer.NewHttpRequestResponseSpy(call)

    // Create a shared resource which can be sent
    // to javascript and from javascript to Go.
    req := rc.NewSharedResource(spy, nil)

    // We call our javascript function.
    callback.CallWithResource1(req)

    // End we wait his response.
    // Javascript is asynchrone, so we can't only take our function response.
    //
    call.WaitResponse()

    // Code 200 = ok.
    //
    if spy.StatusCode == 200 {
      gCache[cacheKey] = spy.ResponseText
    }

    // Nil mean no error here.
    return nil
  })
}
Enter fullscreen mode Exit fullscreen mode

Our javascript file index.ts became:

import {HttpServer, asHttpRequest} from "@progp/http"
let server = new HttpServer(8000);
server.getHost("localhost");
testMyFunction("/", "cache1", asHttpRequest(async req => req.returnHtml(200, "Hello world")));
server.start();
Enter fullscreen mode Exit fullscreen mode

Here the function asHttpRequest allows to build an object of type HttpRequest wrapping our resource (which is a reference to our Go object containing information about the http call).

Testing our new function

Suspense! What will be the score of our new server implementation? It's a x2! The previous score was near 90.000 req/sec, and now we have a little more than 170.000 req/sec while being bombarded by 500 clients.

./reset_generated_code.sh
go run .
bombardier -c500 -n1000000 http://localhost:8000

Statistics        Avg      Stdev        Max
  Reqs/sec    173946.73   29320.96  437320.44
  Latency        2.88ms    10.63ms   411.93ms
  HTTP codes:
    1xx - 0, 2xx - 1000000, 3xx - 0, 4xx - 0, 5xx - 0
    others - 0
  Throughput:    34.40MB/s
Enter fullscreen mode Exit fullscreen mode

To go further

This first tutorial allowed you to see how easy it is to call a Go function from javascript while being (very) fast. We have go through a lot of mechanisms, but the good news is that we have seen the most important one.

The next tutorial will focus on the "dynamic" and the "plugin" execution modes, which allows to avoid the 5 to 10 seconds required by Go to compile our project when we are doing a code update. It's very slow to compile because of the code library containing the V8 engine: it's size is near 100Mo which is enormous.

So we will see how to make dev faster, and we will see how to embed ProgpJS in your own project. Also, we will see how to embed our script inside our executable in order to build an application without dependencies.

Starring my GitHub project

If you appreciate my works then you can help me by starring my GitHub project link. It allow me to know if my project is useful and I must make evolve it.

You can also write me comment allowing me to know what you love about ProgpJS.

Discord Server

IF you want to speak with me about ProgpJS, feel free to contact me on discord: The project has a Discord server allowing us to share about ProgpJS.
Here is the link: discord link

Top comments (15)

Collapse
 
aungmyatmoe profile image
Aung Myat Moe • Edited

Yet another JS runtime dropped, looking forward this one!
I assume this one have very small amount of maintainer and background support. However, this showcase is so far so good in my experience. I have take a look to the source code, Wala, we can make some funny things with this runtime cheer!

Collapse
 
claratrcs profile image
claratrcs

Hello Jo! Happy to see it's finally out!

Collapse
 
johanpiquet profile image
Johan PIQUET

Hi Clara, happy to meet you here!

Collapse
 
aditya_raj_1010 profile image
A.R

In the context of building complex, scalable web applications, elucidate the strategies and best practices for incorporating "ProgpJS," a fast JavaScript engine designed for Go. Discuss how the integration affects the overall project architecture, focusing on topics such as managing shared state, communication protocols between Go and JavaScript components, and potential bottlenecks in performance. Furthermore, explore the implications on debugging and profiling techniques, and elaborate on how this integration aligns with modern trends in microservices and serverless architectures. Consider real-world scenarios where the interplay between Go and ProgpJS provides distinct advantages and challenges in comparison to traditional web development approaches.

"followback for more insightfull discussion"

Collapse
 
johanpiquet profile image
Johan PIQUET

thank you for your advice...there is a lot to do

Collapse
 
aditya_raj_1010 profile image
A.R

"followback for more insightfull discussion"

Collapse
 
rudransh61 profile image
Rudransh Bhardwaj

Broo yooooo thatd coolll

Collapse
 
aditya_raj_1010 profile image
A.R • Edited

Beyond the benchmark results, could you shed light on the optimization strategies employed within ProgpJS to achieve its impressive performance, particularly in scenarios involving mixed Go and JavaScript code?
followback for more insightful discussion

Collapse
 
johanpiquet profile image
Johan PIQUET

It comes from a lot of technicals stuffs, the main points been to reduce call between Go and C++, optimize threads / mutex management, reduce memory allocations and benchmark a lot in order to detect what are good or bad idea for performance. It's technical. The code seem simple when you read it and it is, but there is a lot of little details doing a big difference in performance. For exemple I don't use object pool, which seem to great better for performance but aren't at all when the server process a lot of requests.

Collapse
 
goodevilgenius profile image
Dan Jones

Could this be used in a go application to allow end users to add custom plugins, written in JavaScript?

I'm thinking something where they write some custom JavaScript, that hooks into my go code. I'd have a configuration file where they could add the paths to their js files, or possibly a predefined path where they add js files as plugins.

Would something like that be possible?

Collapse
 
johanpiquet profile image
Johan PIQUET

Hello Dan, I'm sorry but I don't understand what you want to do ...

Collapse
 
sarahpetitsoleil profile image
SarahPetitSoleil

Hi, thank johan! ... I get an error after the second launch about generated.cpp. Why?

Collapse
 
johanpiquet profile image
Johan PIQUET

Hi Sarah. It's because the generated code is about your previous version of the function JsTestMyFunction. Call arguments have been updated and now the generated code don't match anymore. I will add a script "reset_generated_code.sh" in the project root in order to restore a neutral version.

Collapse
 
johanpiquet profile image
Johan PIQUET

Ok the script is added. Feel free to ask me if you have another difficulties.

Thread Thread
 
sarahpetitsoleil profile image
SarahPetitSoleil

Thank you Johan!

Some comments may only be visible to logged-in visitors. Sign in to view all comments.