JS_Engine

JS_Engine - Extends for QuickJS

Learning the awesome QuickJS by extending it with below functionalities:

  • Divide the 5.4W LoC quickjs.c into multiple small files, makes the code easy to browser and navigate
  • A debugger which supports inline breakpoints and includes web interfaces which is easy to integrate with the Debug Adapter Protocol
  • Dump the GC managed objects and view the results in the Chrome devtools

Debugger

The debugger can be tasted by following steps:

Click to expand
  1. Build our SlowJS:

    cmake -S . --preset=default
    cmake --build --preset=qjs

    the location of the built stuff is ./build/qjs/qjs

  2. Make up a file tmp_test.js to test:

    function add(a, b) {
      const c = a + b;
      return c;
    }
    
    function sub(a, b) {
      const c = a - b;
      return c;
    }
    
    function doSth(a, b) {
      return add(a, b) + sub(a, b);
    }
    
    print(doSth(1, 2));
  3. Start the debugger:

    ./build/qjs/qjs --debug 8097
  4. Connect to the debugger:

    nc 0.0.0.0 8097

    We use nc to communicate with the debugger server, then we can paste come commands to perform debug

  5. Call the debugger to launch a new session:

    { "type": "launch", "data": { "file": "./tmp_test.js" } }

    Paste above json into the nc REPL and press ENTER

  6. Set breakpoints:

    {
      "type": "setBreakpoint",
      "data": { "file": "./tmp_test.js", "line": 3, "col": 0 }
    }
    {
      "type": "setBreakpoint",
      "data": { "file": "./tmp_test.js", "line": 8, "col": 0 }
    }
  7. Start to run our test script:

    { "type": "run" }
  8. Now the debugger is paused at the first breakpoint, we can list the stack frames:

    { "type": "listStackframes" }

    the output looks like:

    {
      "type": "listStackframes",
      "data": [
        {
          "name": "add",
          "file": "./tmp_test.js",
          "line": 1
        },
        {
          "name": "doSth",
          "file": "./tmp_test.js",
          "line": 11
        },
        {
          "name": "<eval>",
          "file": "./tmp_test.js",
          "line": 1
        }
      ]
    }
  9. We can resume the debugger by issuing below command:

    { "type": "continue" }
  10. Now the debugger is paused at the second breakpoint, we can print the variable in the topmost stack frame:

    { "type": "dumpStackframe", "data": { "i": 0 } }

    the output looks like:

    {
      "type": "dumpStackframe",
      "data": {
        "args": [
          {
            "name": "a",
            "value": 1
          },
          {
            "name": "b",
            "value": 2
          }
        ],
        "vars": [
          {
            "name": "c",
            "value": -1
          }
        ],
        "closure_vars": [],
        "name": "sub",
        "file": "./tmp_test.js",
        "line": 6
      }
    }
  11. We can use the continue command resume the debugger again:

    { "type": "continue" }
  12. Now the test script is done and the debugger server prints the final results:

    new sess thread is running...
    2

GC Dump

The GC dump functionality can be tasted by following steps:

Click to expand
  1. Build our SlowJS:

    cmake -S . --preset=default
    cmake --build --preset=qjs

    the location of the built stuff is ./build/qjs/qjs

  2. Make up a file tmp_test.js to test:

    var o = {
      a: { a1: { a2: 1 } },
      b: { b1: { b2: 1 } },
      c: function () {
        return 1;
      },
      d: new ArrayBuffer((1 << 20) * 50, 0),
      e: new Uint16Array((1 << 20) * 50, 0),
    };
    
    __js_gcdump_objects();
    print(o); // retain the obj to prevent it from being freed
  3. Run the test script:

    ./build/qjs/qjs tmp_test.js
  4. The output file will have name looks like:

    Heap.20230318.130209.224.heapsnapshot
    

    the filename is in this pattern:

    Heap.date.time.ms.heapsnapshot
    
  5. Import the output file into Chrome devtools:

  6. Then we can dig into the heap:

Development

It's better to glance over the available options before you perform the actual build:

cmake -B build -LH
  • -B stands for the building directory
  • -L stands for listing all the options
  • -H stands for printing the help messages along with the options

above command will print the available options and their help messages, use them like this:

cmake -B build -S . -G Ninja -D QJS_DUMP_BYTECODE=1
  • -S stands for the source directory
  • -D stands for specifying an options in a key=value pattern

then choose one of below sections to run in project root directory

Debug build

cmake -S . --preset=default
cmake --build --preset=qjs

Release build

cmake -S . --preset=default -D CMAKE_BUILD_TYPE=Release
cmake --build --preset=qjs

Tests

cmake -S . --preset=default
cmake --build --preset=run-tests

Microbench

cmake -S . --preset=default -D CMAKE_BUILD_TYPE=Release
cmake --build --preset=run-microbench

Test262

cmake -S . --preset=default -D CMAKE_BUILD_TYPE=Release
cmake --build --preset=run-test262

# Result: 302/75790 errors, 1396 excluded, 7712 skipped, 302 new

Presets

You can also choose the presets listed in CMakePresets.json to run:

# Use a config preset
cmake -S . --preset=default

# Use a build preset
cmake --build --preset=run-tests