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

Switch QuickJS to QuickJS-NG #369

Merged
merged 37 commits into from
Nov 5, 2024
Merged

Conversation

richarddd
Copy link
Contributor

Changes QuickJS to QuickJS NG.

Significant changes:

  1. Better performance thanks to Poly IC cache (10% increase)
  2. More APIs (ArrayBuffer.transfom etc)
  3. A lot of engine bug fixes
  4. Classes (Class IDs) are now registered per JSRuntime and not statically across all JSRuntime instances
  5. Removal of BigFloat & BigInt is always enabled
  6. Operator overloading removed
  7. Simplification of JS allocator. State is now managed internally
  8. Less patches required as column numbers, export names etc are all exposed now.
  9. Support for MSVC
  10. Lots of safety fixes (valgrind, asan enabled in upstream ci)

Status:
Project currently builds with a lot of successful tests but some tests are failing due to memory leaks and invalid allocations:

  1. (signal: 11, SIGSEGV: invalid memory reference)
  2. Assertion failed: (list_empty(&rt->gc_obj_list))

@DelSkayn feel free to check out this branch or provide feedback here. LMKWYT!
I might need some support ironing out the last memory reference/leak bugs

@saghul
Copy link

saghul commented Oct 4, 2024

Hey folks! Very excited to see this! Let me know if there is anything I can do to help on the QuickJS-ng side of things :-)

I see there are still a couple of patches applied, feel free to send them over and we can discuss them!

2. Assertion failed: (list_empty(&rt->gc_obj_list))

This is a leak. You can enable leak dumping and it should tell you what it is that got leaked.

@richarddd
Copy link
Contributor Author

Hey folks! Very excited to see this! Let me know if there is anything I can do to help on the QuickJS-ng side of things :-)

Fantastic, thanks for the support @saghul 🎉

I see there are still a couple of patches applied, feel free to send them over and we can discuss them!

Let's do it, we can probably expose JS_GetFunctionProto. I'm curious about the check_stack_overflow.patch as the only difference seems to be:

return unlikely(js_get_stack_pointer() - alloca_size < rt->stack_limit);

vs.

return unlikely(js_get_stack_pointer() < rt->stack_limit + alloca_size);

I don't know why this was added. The first expression makes the comparison after subtracting the allocation size from the current stack pointer. The second expression makes the comparison by adjusting the stack limit instead, then comparing the current stack pointer directly.

  1. Assertion failed: (list_empty(&rt->gc_obj_list))

This is a leak. You can enable leak dumping and it should tell you what it is that got leaked.

Yep. However it's seems to be affected by races. Also might be a bug in the refactoring of allocator. When for example running each test in class.rs individually, they work. When running the whole test suite, it fails with either memory leak or invalid memory reference.

Here are some leak information:
Object leaks:
ADDRESS REFS SHRF PROTO CLASS PROPS
0x148907510 1 [js_context]
0x147f11d30 1 0* 0x148907c00 RustFunction { }

@saghul
Copy link

saghul commented Oct 4, 2024

Yep. However it's seems to be affected by races.

What kind of races? Are you running multiple threads using the same runtime?

@richarddd
Copy link
Contributor Author

Yep. However it's seems to be affected by races.

What kind of races? Are you running multiple threads using the same runtime?

No this is the thing that's strange. They all run in separate runtimes. The only tests that are failing now are

mod test {

Running each test individually works, running them together, fails with either memory reference error or leaking objects.

@richarddd
Copy link
Contributor Author

Running with all dump-flags i'll get this output JS reference/memory leak:
running 3 tests

Freeing cycles:
       ADDRESS REFS SHRF          PROTO      CLASS PROPS
   0x129e0d060    3   1     0x129f07c00   Function { length: 0, name: 13"" }
   0x129e0d130    2  53*    0x129f07c00   Function { length: 0, name: 12"" }
   0x129e0d270    2   1     0x129f07b30     Object { Object: [Function 0x129e0d4b0], Function: [Function 0x129e0e470], Error: [Function 0x129e0e560], EvalError: [Function 0x129e0eab0], RangeError: [Function 0x129e0eb70], ReferenceError: [Function 0x129e0ecb0], SyntaxError: [Function 0x129e0ed30], TypeError: [Function 0x129e0ec20], URIError: [Function 0x129e0ef10], InternalError: [Function 0x129e0efc0], AggregateError: [Function 0x129e0ede0], Iterator: [Function 0x129e0f640], Array: [Function 0x129e103c0], parseInt: [Function 0x129e10a90], parseFloat: [Function 0x129e11020], isNaN: [autoinit 0x129f07510 2 0x100bc1400], isFinite: [autoinit 0x129f07510 2 0x100bc1420], queueMicrotask: [autoinit 0x129f07510 2 0x100bc1440], decodeURI: [autoinit 0x129f07510 2 0x100bc1460], decodeURIComponent: [autoinit 0x129f07510 2 0x100bc1480], encodeURI: [autoinit 0x129f07510 2 0x100bc14a0], encodeURIComponent: [autoinit 0x129f07510 2 0x100bc14c0], escape: [autoinit 0x129f07510 2 0x100bc14e0], unescape: [autoinit 0x129f07510 2 0x100bc1500], Infinity: inf, NaN: nan, undefined: undefined, Symbol.toStringTag: [autoinit 0x129f07510 2 0x100bc1580], Number: [Function 0x129e112c0], Boolean: [Function 0x129e11c90], String: [Function 0x129e11ce0], Math: [autoinit 0x129f07510 2 0x100bc1ef0], Reflect: [autoinit 0x129f07510 2 0x100bc24b0], Symbol: [Function 0x129e13300], eval: [Function 0x129e139d0], globalThis: [Object 0x129e0d270], Date: [Function 0x129e142f0], RegExp: [Function 0x129e15800], JSON: [autoinit 0x129f07510 2 0x100bbfc80], Proxy: [Function 0x129e15530], Map: [Function 0x129e15f60], Set: [Function 0x129e0fb20], WeakMap: [Function 0x129e16890], WeakSet: [Function 0x129e113f0], ArrayBuffer: [Function 0x129e17170], SharedArrayBuffer: [Function 0x129e17010], Uint8ClampedArray: [Function 0x129e183a0], Int8Array: [Function 0x129e184c0], Uint8Array: [Function 0x129e18660], Int16Array: [Function 0x129e18760], Uint16Array: [Function 0x129e18860], Int32Array: [Function 0x129e18960], Uint32Array: [Function 0x129e18a60], BigInt64Array: [Function 0x129e18b60], BigUint64Array: [Function 0x129e18c60], Float16Array: [Function 0x129e18d60], Float32Array: [Function 0x129e18e60], Float64Array: [Function 0x129e18f60], DataView: [Function 0x129e19770], Atomics: [autoinit 0x129f07510 2 0x100bc27d0], Promise: [Function 0x129e19830], BigInt: [Function 0x129e1a620], WeakRef: [Function 0x129e1a910], FinalizationRegistry: [Function 0x129e1aaf0], performance: [Object 0x129e16970] }
   0x129e0d2e0    1   1*            0x0     Object {  }
   0x129e0d4b0    1   1*    0x129f07c00   Function { length: 1, name: 2"Object", prototype: [Object 0x129f07b30], create: [autoinit 0x129f07510 2 0x100bbda40], getPrototypeOf: [autoinit 0x129f07510 2 0x100bbda60], setPrototypeOf: [autoinit 0x129f07510 2 0x100bbda80], defineProperty: [autoinit 0x129f07510 2 0x100bbdaa0], defineProperties: [autoinit 0x129f07510 2 0x100bbdac0], getOwnPropertyNames: [autoinit 0x129f07510 2 0x100bbdae0], getOwnPropertySymbols: [autoinit 0x129f07510 2 0x100bbdb00], groupBy: [autoinit 0x129f07510 2 0x100bbdb20], keys: [autoinit 0x129f07510 2 0x100bbdb40], values: [autoinit 0x129f07510 2 0x100bbdb60], entries: [autoinit 0x129f07510 2 0x100bbdb80], isExtensible: [autoinit 0x129f07510 2 0x100bbdba0], preventExtensions: [autoinit 0x129f07510 2 0x100bbdbc0], getOwnPropertyDescriptor: [autoinit 0x129f07510 2 0x100bbdbe0], getOwnPropertyDescriptors: [autoinit 0x129f07510 2 0x100bbdc00], is: [autoinit 0x129f07510 2 0x100bbdc20], assign: [autoinit 0x129f07510 2 0x100bbdc40], seal: [autoinit 0x129f07510 2 0x100bbdc60], freeze: [autoinit 0x129f07510 2 0x100bbdc80], isSealed: [autoinit 0x129f07510 2 0x100bbdca0], isFrozen: [autoinit 0x129f07510 2 0x100bbdcc0], fromEntries: [autoinit 0x129f07510 2 0x100bbdce0], hasOwn: [autoinit 0x129f07510 2 0x100bbdd00] }
Object leaks:
Freeing    0x129e0de30    1  52*    0x129f07c00   Function { length:    0x12aa09d80    0  54*    0x129e0c9c0   Function { length: 00, name: "get __proto__" }
Freeing 0"get __proto__"
       ADDRESS REFS SHRF          PROTO      CLASS PROPS
   0x12a9070f0    1 [js_context]
   0x12a907360    1   0*    0x12a9077e0 RustFunction {  }
Assertion failed: (list_empty(&rt->gc_obj_list)), function JS_Fr, name: 2"<eval>" }
eeRuntime, file quickjs.c, line 2073.
Freeing [bytecode <eval>]
proto__" }
860    1  51*    0x129f07c00   Function { length: 1, name: "set __proto__" }
Freeing 0"set __proto__"
   0x129e0e220    1  50*    0x129f07c00   Function { length: 0, name: "get fileName" }
Freeing 0"get fileName"
   0x129e0e0c0    1  49*    0x129f07c00   Function { length: 0, name: "get lineNumber" }
Freeing 0"get lineNumber"
   0x129e0e190    1  48*    0x129f07c00   Function { length: 0, name: "get columnNumber" }
Freeing 0"get columnNumber"
   0x129e0e6d0    1  47*    0x129f07c00   Function { length: 0, name: "get stackTraceLimit" }
Freeing 0"get stackTraceLimit"
   0x129e0e760    1  46*    0x129f07c00   Function { length: 1, name: "set stackTraceLimit" }
Freeing 0"set stackTraceLimit"
   0x129e0e8a0    1  45*    0x129f07c00   Function { length: 0, name: "get prepareStackTrace" }
Freeing 0"get prepareStackTrace"

And i have attached dump for SIGSEGV: invalid memory reference.
mem-ref-dump.txt

@richarddd
Copy link
Contributor Author

richarddd commented Oct 7, 2024

@DelSkayn memory reference error occurs when running finalizer for RustFunction here. Switching to QuickJS-NG could offer substantial benefits compared to regular QuickJS, and I’d love to hear your thoughts.

/// FFI finalizer, destroying the object once it is delete by the Gc.
pub(crate) unsafe extern "C" fn finalizer<'js, C: JsClass<'js>>(
    rt: *mut qjs::JSRuntime,
    val: qjs::JSValue,
) {
    let ptr = qjs::JS_GetOpaque(val, C::class_id().get(rt)).cast::<JsCell<C>>();
    debug_assert!(!ptr.is_null());
    let inst = Box::from_raw(ptr);
    mem::drop(inst);  // <---- causes memory reference error when class is is RustFunction
}

@DelSkayn
Copy link
Owner

DelSkayn commented Oct 7, 2024

I am fine with switching to NG!
It seems the merging NG and the base rquickjs will take a while longer so I have no objections.
The only issue is fixing the segfaults and memory issues.

I have actually been looking into these issues but haven't been able to locate the exact problem.
So far the issue only seems to show up when running multiple tests which is weird since each of those tests should be isolated.
I have tried to replicate the problem by running the failing tests on multiple threads but that doesn't work.
Very weird.

@richarddd
Copy link
Contributor Author

richarddd commented Oct 7, 2024

I am fine with switching to NG! It seems the merging NG and the base rquickjs will take a while longer so I have no objections. The only issue is fixing the segfaults and memory issues.

That's awesome! Thank you!

I have actually been looking into these issues but haven't been able to locate the exact problem. So far the issue only seems to show up when running multiple tests which is weird since each of those tests should be isolated. I have tried to replicate the problem by running the failing tests on multiple threads but that doesn't work. Very weird.

I can consistency replicate the memory reference issue by moving all class tests to a single function.

It's now consistency failing when finalizer runs for RustFunction for the second time. Not quite sure why this happens (should it even run twice?) but then the pointer to the instance is null:

Screenshot 2024-10-07 at 4 26 57 PM
mod tests
#[cfg(test)]
mod test {
    use std::sync::{
        atomic::{AtomicBool, Ordering}, Arc
    };

    use crate::{
        class::{ClassId, JsClass, Readable, Trace, Tracer, Writable},
        function::This,
        test_with,
        value::Constructor,
        Class, Context, FromJs, Function, IntoJs, Object, Runtime,
    };

    /// Test circular references.
    #[test]
    fn class_tests() {


        pub struct Container<'js> {
            inner: Vec<Class<'js, Container<'js>>>,
            test: Arc<AtomicBool>,
        }

        impl<'js> Drop for Container<'js> {
            fn drop(&mut self) {
                self.test.store(true, Ordering::SeqCst);
            }
        }

        impl<'js> Trace<'js> for Container<'js> {
            fn trace<'a>(&self, tracer: Tracer<'a, 'js>) {
                self.inner.iter().for_each(|x| x.trace(tracer))
            }
        }

        impl<'js> JsClass<'js> for Container<'js> {
            const NAME: &'static str = "Container";

            type Mutable = Writable;

            fn class_id() -> &'static crate::class::ClassId {
                static ID: ClassId = ClassId::new();
                &ID
            }

            fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<crate::Object<'js>>> {
                Ok(Some(Object::new(ctx.clone())?))
            }

            fn constructor(
                _ctx: &crate::Ctx<'js>,
            ) -> crate::Result<Option<crate::value::Constructor<'js>>> {
                Ok(None)
            }
        }

        let rt = Runtime::new().unwrap();
        let ctx = Context::full(&rt).unwrap();

        let drop_test = Arc::new(AtomicBool::new(false));

        ctx.with(|ctx| {
            let cls = Class::instance(
                ctx.clone(),
                Container {
                    inner: Vec::new(),
                    test: drop_test.clone(),
                },
            )
            .unwrap();
            let cls_clone = cls.clone();
            cls.borrow_mut().inner.push(cls_clone);
        });
        rt.run_gc();
        assert!(drop_test.load(Ordering::SeqCst));
        ctx.with(|ctx| {
            let cls = Class::instance(
                ctx.clone(),
                Container {
                    inner: Vec::new(),
                    test: drop_test.clone(),
                },
            )
            .unwrap();
            let cls_clone = cls.clone();
            cls.borrow_mut().inner.push(cls_clone);
            ctx.globals().set("t", cls).unwrap();
        });

        #[derive(Clone, Copy)]
        pub struct Vec3 {
            x: f32,
            y: f32,
            z: f32,
        }

        impl Vec3 {
            pub fn new(x: f32, y: f32, z: f32) -> Self {
                Vec3 { x, y, z }
            }

            pub fn add(self, v: Vec3) -> Self {
                Vec3 {
                    x: self.x + v.x,
                    y: self.y + v.y,
                    z: self.z + v.z,
                }
            }
        }

        impl<'js> Trace<'js> for Vec3 {
            fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
        }

        impl<'js> FromJs<'js> for Vec3 {
            fn from_js(ctx: &crate::Ctx<'js>, value: crate::Value<'js>) -> crate::Result<Self> {
                Ok(*Class::<Vec3>::from_js(ctx, value)?.try_borrow()?)
            }
        }

        impl<'js> IntoJs<'js> for Vec3 {
            fn into_js(self, ctx: &crate::Ctx<'js>) -> crate::Result<crate::Value<'js>> {
                Class::instance(ctx.clone(), self).into_js(ctx)
            }
        }

        impl<'js> JsClass<'js> for Vec3 {
            const NAME: &'static str = "Vec3";

            type Mutable = Writable;

            fn class_id() -> &'static crate::class::ClassId {
                static ID: ClassId = ClassId::new();
                &ID
            }

            fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<crate::Object<'js>>> {
                let proto = Object::new(ctx.clone())?;
                let func =
                    Function::new(ctx.clone(), |this: This<Vec3>, other: Vec3| this.add(other))?
                        .with_name("add")?;

                proto.set("add", func)?;
                Ok(Some(proto))
            }

            fn constructor(
                ctx: &crate::Ctx<'js>,
            ) -> crate::Result<Option<crate::value::Constructor<'js>>> {
                let constr =
                    Constructor::new_class::<Vec3, _, _>(ctx.clone(), |x: f32, y: f32, z: f32| {
                        Vec3::new(x, y, z)
                    })?;

                Ok(Some(constr))
            }
        }

        test_with(|ctx| {
            Class::<Vec3>::define(&ctx.globals()).unwrap();

            let v = ctx
                .eval::<Vec3, _>(
                    r"
                let a = new Vec3(1,2,3);
                let b = new Vec3(4,2,8);
                a.add(b)
            ",
                )
                .unwrap();

            approx::assert_abs_diff_eq!(v.x, 5.0);
            approx::assert_abs_diff_eq!(v.y, 4.0);
            approx::assert_abs_diff_eq!(v.z, 11.0);

            let name: String = ctx.eval("new Vec3(1,2,3).constructor.name").unwrap();
            assert_eq!(name, Vec3::NAME);
        });


        pub struct X;

        impl<'js> Trace<'js> for X {
            fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
        }

        impl<'js> JsClass<'js> for X {
            const NAME: &'static str = "X";

            type Mutable = Readable;

            fn class_id() -> &'static ClassId {
                static ID: ClassId = ClassId::new();
                &ID
            }

            fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<Object<'js>>> {
                Object::new(ctx.clone()).map(Some)
            }

            fn constructor(_ctx: &crate::Ctx<'js>) -> crate::Result<Option<Constructor<'js>>> {
                Ok(None)
            }
        }

        test_with(|ctx| {
            Class::<X>::register(&ctx).unwrap();
            Class::<X>::register(&ctx).unwrap();
        })
    }
}

@richarddd
Copy link
Contributor Author

Some more investigation shows something wrong with the atoms. For example for working constructor tests i can see these objects beeing marked as "should be freed" by GC cycle:

0x152a085a0    1   1*            0x0     Object { a: [Vec3 0x152a15e10], b: [Vec3 0x152a120e0] }

However when running all tests together they are instead log as:

0x60000277ba70    1   1*            0x0     Object { a: [RustFunction 0x6000027769e0], b: [RustFunction 0x600002776a30] }

Where the b object is a dangling pointer. I suspect this might be something where the prototype get's overwritten or some issues with the memory allocator.

@DelSkayn
Copy link
Owner

DelSkayn commented Oct 8, 2024

I found the issue. The problem is that class id's are no longer a global value in quickjs-ng. The current class functionality are designed around this being a single global value.

When a class is registered for the first time, during execution, in a runtime we store the new class id returned in a static global associated to the type. When we convert to a class from an object we then use this stored id to check if the object is of the right type and we use it to check if a type is already registered.

With the class id's no longer being global but instead local to the runtime we now store conflicting id's.

In your reproduction RustFunction and Vec3 have the same class id because RustFunction was registered by the first run test and Vec3 is only registered in the second. In the second test the RustFunction class then doesn't request a new class id since that is already initialized and then registers itself under a class id which is not yet returned. Vec3 then gets the same class id as RustFunction and it thinks it is already registered so we don't set the right finalizer. This causes Vec3 object to be stored with the finalizer from RustFunction which causes the segfault when the runtime tries to drop the Vec3 object.

If we want to fix this we will have to redesign how types are registered.

@saghul
Copy link

saghul commented Oct 8, 2024

Nice find!

Global class IDs were broken since the beginning, specially if you had multiple runtimes registering classes with globals as used by quickjs-libc.c for example.

IMHO, ng or not, it's probably a good idea to make sure the registered class IDs are per runtime.

@richarddd
Copy link
Contributor Author

Awesome finding. I was suspecting this as well but could not figure out why. Your explanation was the missing piece of the puzzle. I’ll refactor the class id implementation and test later today!

@richarddd richarddd marked this pull request as ready for review October 8, 2024 19:34
@richarddd
Copy link
Contributor Author

Ok should be in a pretty good state to review now. What I see now is some Windows failings, which may be related to non-updated bindings.

core/src/class/id.rs Outdated Show resolved Hide resolved
@richarddd
Copy link
Contributor Author

@DelSkayn something super strange is occurring in CI which i can't explain. When building with --target x86_64-pc-windows-gnu the build script get's the env TARGET set to x86_64-pc-windows-msvc. To prove this i added an ENV to the build script with the target from the matrix and then in build.rs logging TARGET vs BUILD_TARGET (in parethenasis) and they differ:
ci-issue

@DelSkayn
Copy link
Owner

DelSkayn commented Oct 10, 2024

I have implemented a rework of the class system in #373 which doesn't require a hashmap lookup for every time we try to convert a quickjs object to a rust class, just two checks. We only need a hashmap to lookup class prototypes. It also removes the rust ClassId completely which fixes an outstanding issue with the class-id globals and generic types. This approach should also work without adjustment for QuickJS-NG.

Let me know what you think.

@richarddd
Copy link
Contributor Author

I have implemented a rework of the class system in #373 which doesn't require a hashmap lookup for every time we try to convert a quickjs object to a rust class, just two checks. We only need a hashmap to lookup class prototypes. It also removes the rust ClassId completely which fixes an outstanding issue with the class-id globals and generic types. This approach should also work without adjustment for QuickJS-NG.

Let me know what you think.

Cool! I will take a look. I just pushed an update where i use tinyvec to store an array of classIds (since it's low integer incremented by 1) and we can use that as backing storage. There is no heap allocation unless we register more than 1024 classes per runtime and lookups are very fast since they're based on indexes instead of hashes. This solves a lot of issues as well.

@richarddd
Copy link
Contributor Author

richarddd commented Oct 15, 2024

@DelSkayn the only thing that breaks now is the ptr_32_nan_boxing.rs. Can you please have a look? I think we should get this in and rework class system later. I'm pretty happy with the ClassId implementation in this latest push. No hashes, constant time lookup with only stack allocations unless we have more than 1000 classes. In that case it's regular Vec lookup complexity and allocations :)

@richarddd
Copy link
Contributor Author

@DelSkayn F.Y.I I have now rebased on the new class system

.github/workflows/ci.yml Outdated Show resolved Hide resolved
core/src/allocator/rust.rs Outdated Show resolved Hide resolved
core/src/runtime/raw.rs Show resolved Hide resolved
core/src/runtime/raw.rs Show resolved Hide resolved
sys/src/bindings/x86_64-unknown-linux-gnu.rs Show resolved Hide resolved
core/src/context/builder.rs Show resolved Hide resolved
Copy link
Owner

@DelSkayn DelSkayn left a comment

Choose a reason for hiding this comment

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

Looks good, thanks for all the work!

@DelSkayn DelSkayn merged commit 2b83f23 into DelSkayn:master Nov 5, 2024
27 checks passed
@saghul
Copy link

saghul commented Nov 5, 2024

Great work @richarddd ! 🙌

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.

4 participants