Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add QuickJS as a Javascript engine option #4627

Merged
merged 1 commit into from May 15, 2024
Merged

Add QuickJS as a Javascript engine option #4627

merged 1 commit into from May 15, 2024

Conversation

nickva
Copy link
Contributor

@nickva nickva commented May 30, 2023

https://bellard.org/quickjs

Some benefits over Mozilla Spidermonkey:

  • Small. We're using 6 or so C files vs 700+ SM91 C++ files.

  • Built with Apache CouchDB as opposed having to maintain a separate SM package, like for RHEL9, for instance, where they dropped support for SM already: RHEL 9 Support for CouchDB #4154.

  • Embedding friendly. Designed from ground-up for embedding. SM has been updating the C++ API such that we have to keep copy-pasting new versions of our C++ code every year or so: try compiling for sm 102 #4305.

  • Easy to modify to accept Spidermonkey 1.8.5 top level functions for map/reduce code so we don't have have to parse the JS, AST transform it, and then re-compile it.

  • Better sandboxing. Creating a whole JSRuntime takes only 300 microseconds, so we can afford to do that on reset. JSRuntimes cannot share JS data or object between them.

  • Seems to be faster in preliminary benchmarking with small concurrent VDU and view builds:
    https://gist.github.com/nickva/ed239651114794ebb138b1f16c5f6758

    • 4x faster than SM 1.8.5
    • 5x faster than SM 91
    • 6x reduced memory usage per couchjs process (5MB vs 30MB)
  • Allows compiling JS bytecode ahead of time a C array of bytes.

QuickJS can be built alongside Spidermonkey and toggled on/off at runtime:

./configure --dev --js-engine=quickjs

This makes it the default engine. But Spidermonkey can still be set in the config option.

[couchdb]
js_engine = spidermonkey | quickjs

To test individual views, without switching the default use the javascript_quickjs language in the design docs. To keep using Spidermonkey engine after switching the default, can use javascript_spidermonkey language in design docs. However, language selection will reset the view and the view will have to be rebuilt.

It's also possible to build without Spidermonkey support completely by using:

./configure --disable-spidermonkey

Issue: #4448

Some basic benchmarking

Summary:

  • 3x faster than SM 1.8.5
  • 4x faster than SM 91
  • 6x reduced memory usage per couchjs process (5MB vs 30MB).

Setup Details

MacOS Intel, Erlang 24, ./dev/run -n1

Benchmark script: https://gist.github.com/nickva/c0cbf6a556cc2dc7dd6ee79f504f5f84.

  • 20 concurrent workers create a db and:
    • Insert 100 docs, 10 ddoc(views) and query the views
    • Repeat that 3 times in a row

Results measured by the zsh time command as % time ./stampede_dbs_ddocs_vdus.py .... CPU usage is reported for the client not the couchjs process or Erlang VM. Some example of resource usage is provided below as btop screenshots.

QuickJS

./stampede_dbs_ddocs_vdus.py -x 10 -n 100 -w 20 -t 3  1.10s user 0.22s system 7% cpu 18.754 total
./stampede_dbs_ddocs_vdus.py -x 10 -n 100 -w 20 -t 3  1.05s user 0.21s system 6% cpu 18.438 total
./stampede_dbs_ddocs_vdus.py -x 10 -n 100 -w 20 -t 3  1.06s user 0.21s system 6% cpu 18.591 total
./stampede_dbs_ddocs_vdus.py -x 10 -n 100 -w 20 -t 3  1.06s user 0.20s system 6% cpu 18.279 total
./stampede_dbs_ddocs_vdus.py -x 10 -n 100 -w 20 -t 3  1.09s user 0.22s system 6% cpu 18.822 total

Spidermonkey 1.8.5

./stampede_dbs_ddocs_vdus.py -x 10 -n 100 -w 20 -t 3  1.33s user 0.25s system 2% cpu 1:06.80 total
./stampede_dbs_ddocs_vdus.py -x 10 -n 100 -w 20 -t 3  1.31s user 0.24s system 2% cpu 1:05.80 total
./stampede_dbs_ddocs_vdus.py -x 10 -n 100 -w 20 -t 3  1.32s user 0.24s system 2% cpu 1:04.34 total
./stampede_dbs_ddocs_vdus.py -x 10 -n 100 -w 20 -t 3  1.31s user 0.23s system 2% cpu 1:03.00 total
./stampede_dbs_ddocs_vdus.py -x 10 -n 100 -w 20 -t 3  1.34s user 0.25s system 2% cpu 1:13.93 total

Spidermonkey 91

./stampede_dbs_ddocs_vdus.py -x 10 -n 100 -w 20 -t 3  1.11s user 0.21s system 1% cpu 1:31.13 total
./stampede_dbs_ddocs_vdus.py -x 10 -n 100 -w 20 -t 3  1.09s user 0.20s system 1% cpu 1:24.22 total
./stampede_dbs_ddocs_vdus.py -x 10 -n 100 -w 20 -t 3  1.10s user 0.21s system 1% cpu 1:35.53 total
./stampede_dbs_ddocs_vdus.py -x 10 -n 100 -w 20 -t 3  1.06s user 0.20s system 1% cpu 1:20.57 total
./stampede_dbs_ddocs_vdus.py -x 10 -n 100 -w 20 -t 3  1.07s user 0.20s system 1% cpu 1:22.06 total

Using btop locally with one Spidermonkey run and two QuickJS runs:

240767354-aad22ad0-3e96-4aa8-a840-aa8e44288a16 240767364-e9736666-5fee-4ade-b8bb-e8c1f22969b5

Memory usage 30-32MB RSS for Spidermonkey and 4-5MB for QuickJS. The time it took to run the benchmark can seen in the CPU usage graph.

@nickva
Copy link
Contributor Author

nickva commented May 31, 2023

Screen Shot 2023-05-30 at 8 13 16 PM

Added QuickJS CI job variants which test each Erlang version alongside Spidermonkey. All have nouveau enabled as well.

@rnewson
Copy link
Member

rnewson commented May 31, 2023

looks really good. what would a NIF look like? I mentioned elsewhere that rustler would be a nice way to do that safely. I think you said 100% NIF is conditional on dropping _list/_show support?

@nickva
Copy link
Contributor Author

nickva commented May 31, 2023

A NIF would be easier to do if we didn't have the getRow/sendRow sub-protocol for list/show, where we have to call back into Erlang, wait for a row, then return back to Javascript. As a driver it's just simpler to do with print and readline.

Even then, it might still make sense to retain external driver vs NIF duality, as it is possible to apply more restrictive isolation policies (seccomp, apparmor, selinux, etc) to the OS process. With all the main.js compiled in as bytecode to a C array, the OS process can even be blocked from opening files: it only gets to talk to outside via standard-in/out file descriptors it received on startup. That would be one of the tradeoffs - more isolation but lower speed.

Rustler might improve the situation if there was a Rust-only JS engine which doesn't wrap a C or C++ one. Then, we could have a NIF and feel like we're not making as much of a safety vs speed tradeoff. I've been keeping an eye on https://github.com/Starlight-JS/Starlight and https://github.com/boa-dev/boa. Boa one looks more promising.

@rnewson
Copy link
Member

rnewson commented May 31, 2023

fair enough.

@nickva
Copy link
Contributor Author

nickva commented Feb 29, 2024

Some progress update:

  • Updated QuickJS with the latest from upstream. We carried a few patches (a CVE patch, FreeBSD compat, and some others). All those have been merged upstream now. We only have our simple Spidermonkey 1.8.5 compat patch.

  • For compatibility, added javascript_quickjs and javascript_spidermonkey language variants. This is to allow testing individual views using QuickJS even if the default is kept as Spidermonkey. Or after switching the default to QuickJS, to keep some views using Spidermonkey as a temporary measure.

  • As discussed during one of the CouchDB meetings, implemented a scanner application to help users scan their cluster automatically for QuickJS compatibility. The scanner app can run a bunch of plugins while traversing over all the dbs, shards, ddoc, etc. To start implemented the QuickJS compat plugin only, but other plugins are possible: ddoc feature detection, finding tags or fields in ddocs, etc. The QuickJS plugin, when enabled, will trawl through all the dbs, shards and ddocs and try to compile them with both QuickJS and Spidermonkey. If results differ it will report it in the logs as a warning.

TODO:

  • The idea so to add some doc sampling and actually some docs to the view functions.
  • Write some tests for scanner app

configure Outdated Show resolved Hide resolved
@nickva nickva force-pushed the qjs branch 5 times, most recently from 441fca1 to d55a944 Compare March 14, 2024 22:25
@nickva
Copy link
Contributor Author

nickva commented May 1, 2024

After another update, now ready for review:

  • Added Windows support using MSYS2 glazier PR
  • A Scanner plugin.
  • Documentation
  • CI jobs with QuickJS as the default

@big-r81
Copy link
Contributor

big-r81 commented May 2, 2024

Hey Nick,

thats really great work. I installed the MSYS2 choco package to the Win-CI and was able to compile CouchDB with QuickJS on Windows.

We need an additional small change (PR) in CouchDB Glazier that this works out-of-the-box.

==> couch_quickjs (compile)
make[1]: Entering directory '/c/Users/couchdb/Documents/couchdb/src/couch_quickjs/quickjs'
mkdir -p .obj .obj/examples .obj/tests
gcc -g -Wall -MMD -MF .obj/qjsc.o.d -Wno-array-bounds -Wno-format-truncation -fwrapv  -D_GNU_SOURCE -DCONFIG_VERSION=\"2024-02-14\" -DCONFIG_BIGNUM -DCONFIG_CC=\"gcc\" -DCONFIG_PREFIX=\"/usr/local\" -O2 -c -o .obj/qjsc.o qjsc.c
gcc -g -Wall -MMD -MF .obj/quickjs.o.d -Wno-array-bounds -Wno-format-truncation -fwrapv  -D_GNU_SOURCE -DCONFIG_VERSION=\"2024-02-14\" -DCONFIG_BIGNUM -O2 -c -o .obj/quickjs.o quickjs.c
gcc -g -Wall -MMD -MF .obj/libregexp.o.d -Wno-array-bounds -Wno-format-truncation -fwrapv  -D_GNU_SOURCE -DCONFIG_VERSION=\"2024-02-14\" -DCONFIG_BIGNUM -O2 -c -o .obj/libregexp.o libregexp.c
gcc -g -Wall -MMD -MF .obj/libunicode.o.d -Wno-array-bounds -Wno-format-truncation -fwrapv  -D_GNU_SOURCE -DCONFIG_VERSION=\"2024-02-14\" -DCONFIG_BIGNUM -O2 -c -o .obj/libunicode.o libunicode.c
gcc -g -Wall -MMD -MF .obj/cutils.o.d -Wno-array-bounds -Wno-format-truncation -fwrapv  -D_GNU_SOURCE -DCONFIG_VERSION=\"2024-02-14\" -DCONFIG_BIGNUM -O2 -c -o .obj/cutils.o cutils.c
gcc -g -Wall -MMD -MF .obj/quickjs-libc.o.d -Wno-array-bounds -Wno-format-truncation -fwrapv  -D_GNU_SOURCE -DCONFIG_VERSION=\"2024-02-14\" -DCONFIG_BIGNUM -O2 -c -o .obj/quickjs-libc.o quickjs-libc.c
gcc -g -Wall -MMD -MF .obj/libbf.o.d -Wno-array-bounds -Wno-format-truncation -fwrapv  -D_GNU_SOURCE -DCONFIG_VERSION=\"2024-02-14\" -DCONFIG_BIGNUM -O2 -c -o .obj/libbf.o libbf.c
gcc -g -o qjsc .obj/qjsc.o .obj/quickjs.o .obj/libregexp.o .obj/libunicode.o .obj/cutils.o .obj/quickjs-libc.o .obj/libbf.o -lm -ldl -lpthread
make[1]: Leaving directory '/c/Users/couchdb/Documents/couchdb/src/couch_quickjs/quickjs'
Compiled src/couch_quickjs.erl
c:/Users/couchdb/Documents/couchdb/src/couch_quickjs/src/couch_quickjs_scanner_plugin.erl:14:2: Warning: behaviour couch_scanner_plugin undefined
Compiled src/couch_quickjs_scanner_plugin.erl
Compiling quickjs/quickjs.c
Compiling quickjs/libregexp.c
Compiling quickjs/libunicode.c
Compiling quickjs/cutils.c
Compiling quickjs/libbf.c
Compiling quickjs/quickjs-libc.c
Compiling c_src/couchjs.c
Compiling c_src/couchjs_mainjs_bytecode.c
Compiling c_src/couchjs_coffee_bytecode.c

Running the tests:

==> couch_quickjs (eunit)
======================== EUnit ========================
module 'couch_quickjs_scanner_plugin'
  module 'couch_quickjs_scanner_plugin_tests'
    couch_quickjs_scanner_plugin_tests:24: -couch_quickjs_scanner_plugin_test_/0-fun-0- (t_basic)...[0.008 s] ok
    [done in 0.011 s]
  [done in 9.601 s]
module 'couch_quickjs'
  module 'couch_quickjs_tests'
=======================================================
  Test passed.
Cover analysis: c:/Users/couchdb/Documents/couchdb/src/couch_quickjs/.eunit/index.html

Code Coverage:
couch_quickjs                :  77%
couch_quickjs_scanner_plugin :   1%

Total                        : 6%
==> rel (eunit)
==> couchdb (eunit)

@nickva
Copy link
Contributor Author

nickva commented May 2, 2024

Thanks for checking, Ronny!

Copy link
Contributor

@big-r81 big-r81 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Nick,

a few small remarks after a quick read. Great work so far!

configure Outdated Show resolved Hide resolved
configure.ps1 Outdated Show resolved Hide resolved
src/chttpd/src/chttpd_node.erl Show resolved Hide resolved
src/couch_quickjs/test/couch_quickjs_tests.erl Outdated Show resolved Hide resolved
src/docs/src/config/couchdb.rst Outdated Show resolved Hide resolved
src/docs/src/config/quickjs.rst Outdated Show resolved Hide resolved
src/docs/src/config/quickjs.rst Outdated Show resolved Hide resolved
src/docs/src/config/quickjs.rst Show resolved Hide resolved
Copy link
Contributor

@jaydoane jaydoane left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just leaving a couple comments, but looking great so far!

test/elixir/test/view_errors_test.exs Outdated Show resolved Hide resolved
src/couch_quickjs/test/couch_quickjs_tests.erl Outdated Show resolved Hide resolved
@nickva nickva force-pushed the qjs branch 8 times, most recently from 2211e0a to cc87df1 Compare May 10, 2024 17:13
@nickva nickva force-pushed the qjs branch 7 times, most recently from 626d9cc to 0810831 Compare May 13, 2024 06:10
LICENSE Outdated Show resolved Hide resolved
Copy link
Contributor

@jaydoane jaydoane left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks awesome!

Sorry if it's in the code and I missed it, but my only question is whether users of the scanner plugin might want to be warned when they exceed various max_ limits?

Some benefits over Mozilla Spidermonkey:

 * Small. We're using 6 or so C files vs 700+ SM91 C++ files.

 * Built with Apache CouchDB as opposed having to maintain a separate SM
   package, like for RHEL9, for instance, where they dropped support for SM
   already [1].

 * Embedding friendly. Designed from ground-up for embedding. SM has been
   updating the C++ API such that we have to keep copy-pasting new versions of
   our C++ code every year or so [2].

 * Easy to modify to accept Spidermonkey 1.8.5 top level functions for
   map/reduce code so we don't have have to parse the JS, AST transform it, and
   then re-compile it.

 * Better sandboxing. Creating a whole JSRuntime takes only 300 microseconds, so
   we can afford to do that on reset. JSRuntimes cannot share JS data or object
   between them.

 * Seems to be faster in preliminary benchmarking with small
   concurrent VDU and view builds:
     https://gist.github.com/nickva/ed239651114794ebb138b1f16c5f6758
   Results seem promising:
     - 4x faster than SM 1.8.5
     - 5x faster than SM 91
     - 6x reduced memory usage per couchjs process (5MB vs 30MB)

 * Allows compiling JS bytecode ahead of time a C array of bytes.

QuickJS can be built alongside Spidermonkey and toggled on/off at runtime:

```
./configure --dev --js-engine=quickjs
```

This makes it the default engine. But Spidermonkey can still be set in the
config option.

```
[couchdb]
js_engine = spidermonkey | quickjs
```

To test individual views, without switching the default use the
`javascript_quickjs` language in the design docs. To keep using Spidermonkey
engine after switching the default, can use `javascript_spidermonkey` language
in design docs. However, language selection will reset the view and the view
will have to be rebuilt.

It's also possible to build without Spidermonkey support completely by using:
```
./configure --disable-spidermonkey
```

Issue: #4448

[1] #4154
[2] #4305
@nickva nickva merged commit f82be73 into main May 15, 2024
24 checks passed
@nickva nickva deleted the qjs branch May 15, 2024 06:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants