Add QuickJS as a Javascript engine option
https://bellard.org/quickjs
Some benefits over SM:
-
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. (see https://github.com/apache/couchdb/issues/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. (see https://github.com/apache/couchdb/pull/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 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.
To help users validate that their views will build with QuickJS there is a new scanner application with pluggable module system. One of the modules is a QuickJS compatibility scanner. When enabled (disabled by default), it will scan all the dbs, shards, ddocs and docs and try to run then through both QuickJS and Spidermonkey engines and compare the results. If the results differ, it will report it to the logs.
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: https://github.com/apache/couchdb/issues/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:
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.
Added QuickJS CI job variants which test each Erlang version alongside Spidermonkey. All have nouveau enabled as well.
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?
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.
fair enough.
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_quickjsandjavascript_spidermonkeylanguage 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
Another update:
-
The scanner app moved to it's own PR https://github.com/apache/couchdb/pull/5014. With a sample plugin to detect CouchDB 4.x planned deprecated ddoc features.
-
As requested by a CouchDB user, added the ability to not build with Spidermonkey (completely disable Spidermonkey).
After another update, now ready for review:
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)
Thanks for checking, Ronny!