GithubHelp home page GithubHelp logo

Possible memory leak about hermes HOT 30 CLOSED

lucaswitch avatar lucaswitch commented on April 28, 2024
Possible memory leak

from hermes.

Comments (30)

neildhar avatar neildhar commented on April 28, 2024 1

It is possible that the issue you're observing is a known leak in WeakMap, which was discussed in #1147 and fixed in e7b2abe.

Could you try calling gc() from JS periodically and see if that brings the memory consumption under control?

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

After running the application for 13 hours this crash is ocurring.
Screenshot 2023-12-29 at 09 49 48

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

Screenshot 2023-12-29 at 09 54 04
Also can see this on the stack trace, not sure how to reproduce it yet cause i can't find the actual cause of the crash on the js code.

from hermes.

tmikov avatar tmikov commented on April 28, 2024

The error in the previous screenshot seems to be out of memory?

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

@tmikov yes, also the cpu goes all the way up when the crash begins.

from hermes.

tmikov avatar tmikov commented on April 28, 2024

Everybody is on a break now, but we will take a more serious look after the holidays.

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

@tmikov thank you for replying and happy holidays!

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

Any updates on this stacktrace?
@tmikov

from hermes.

neildhar avatar neildhar commented on April 28, 2024

Hey @lucaswitch, we'd need more information, like a minimal repro to offer more insight here. It looks like you may be running into two separate problems, the first is a crash somewhere in the GC, and the second is an OOM.

The OOM is likely caused by a large number of allocations, and you can try debugging it by connecting to the application in DevTools and taking a heap snapshot or an allocation timeline profile. That should help you pin down where the allocations are coming from. It is also worth monitoring the runtime's reporting memory consumption by calling HermesInternal.getInstrumentedStats() in JS.

The crash is trickier and may indicate a bug in Hermes, unless Hermes is being used incorrectly (e.g. from multiple threads simultaneously). Are you able to share a minimised JS repro that results in the crash?

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

Thank you for replying, I'm also debugging for possible causes that could create the OOM or any kind of over memory/cpu usage.
I will try to create a reproducer once i figure this out. The problem is i can't figure out where it is and sometimes it needs a couple hours of use to reproduce the crash.

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

@neildhar Does this getInstrumentedStats looks normal:

{"js_VMExperiments": 0, "js_allocatedBytes": 26423128, "js_gcCPUTime": 0.123, "js_gcTime": 0.4050000000000003, "js_heapSize": 33554432, "js_mallocSizeEstimate": 0, "js_markStackOverflows": 0, "js_numGCs": 224, "js_totalAllocatedBytes": 903991048, "js_vaSize": 33554432}

js_totalAllocatedBytes looks too big but not sure if it's to be that way on debug mode.

Is there a documentation about the getInstrumentedStats attributes meaning?

from hermes.

tmikov avatar tmikov commented on April 28, 2024

32MB heap, looks totally normal.

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

So i think the oom is solved(we did a full refactor of the app), but the crash sadly persists the same.

Also now i'm keeping eye on the getInstrumentedStats all the time and seems normal(only js_totalAllocatedBytes gets quite big)

I will consider that some module could be messing up the runtime for now.
Is there some kind of object that could provide which native modules are using jsi at the moment of a crash?

I'm looking some variables at the moment of the crash to indicate which module could be causing this crash then i can take a deeper look at It. And then make a repro(or remove it).

Looking after those stacktrace i could not find enough information that could lead a third party module.

The stacktrace changes a little bit between crashes except on LINE facebook::jsi::RuntimeDecorator::call (decorator.h:340) and above.
I can always see the variables and debug into it, and thats what ive doing.

What we know at this moment:
The methodId and moduleId at the moment of the crash are always between these values:

moduleId: JSTimers and methodId callTimers

moduleId RCTEventEmitter and methodId ReceiveTouches

moduleId RCTEventEmitter and methodId emit

Can you provide some help to find It on how to find which module it's being executed?

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

The issue also only happens on iOS 16 and 17. Our app works with user health monitoring so we had to shutdown the iOS version to solve that issue due to thousands of this crash.

from hermes.

tmikov avatar tmikov commented on April 28, 2024

@lucaswitch

Is there a documentation about the getInstrumentedStats attributes meaning?

@neildhar can probably provide more detail, but most of the fields are documented here:

struct HeapInfo {

The other fields just show the total number of GCs and how much time in milliseconds they took.

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

@lucaswitch

Is there a documentation about the getInstrumentedStats attributes meaning?

@neildhar can probably provide more detail, but most of the fields are documented here:

struct HeapInfo {

The other fields just show the total number of GCs and how much time in milliseconds they took.

Thank you this header file contains all that i need about the getInstrumentedStats.

Can you provide some help which files contains the information about which react-native native module is calling the jsi environment?

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

Found something! This code from watermelon database heavily access the jsi runtime.
But i don't think there is any issue with that since it does not open another thread.
Here is the code:

#include "Database.h"
#include "DatabasePlatform.h"
#include "JSIHelpers.h"

namespace watermelondb {

using platform::consoleError;
using platform::consoleLog;

void Database::install(jsi::Runtime *runtime) {
    jsi::Runtime &rt = *runtime;
    auto globalObject = rt.global();
    createMethod(rt, globalObject, "nativeWatermelonCreateAdapter", 2, [runtime](jsi::Runtime &rt, const jsi::Value *args) {
        std::string dbPath = args[0].getString(rt).utf8(rt);
        bool usesExclusiveLocking = args[1].getBool();

        jsi::Object adapter(rt);

        std::shared_ptr<Database> database = std::make_shared<Database>(runtime, dbPath, usesExclusiveLocking);
        adapter.setProperty(rt, "database", jsi::Object::createFromHostObject(rt, database));

        // FIXME: Important hack!
        // Without any hacks, JSI Watermelon crashes on Android/Hermes on app reload in development:
        // (This doesn't happen on iOS/JSC)
        //   abort 0x00007d0bd27cff2f
        //   __fortify_fatal(char const*, ...) 0x00007d0bd27d20c1
        //   HandleUsingDestroyedMutex(pthread_mutex_t*, char const*) 0x00007d0bd283b020
        //   pthread_mutex_lock 0x00007d0bd283aef4
        //   pthreadMutexEnter sqlite3.c:26320
        //   sqlite3_mutex_enter sqlite3.c:25775
        //   sqlite3_next_stmt sqlite3.c:84221
        //   watermelondb::SqliteDb::~SqliteDb() Sqlite.cpp:57
        // It appears that the Unix thread on which Database is set up is already destroyed by the
        // time destructor is called. AFAIU destructors on objects that are managed by JSI runtime
        // *should* be safe in this respect, but maybe they're not/there's a bug...
        //
        // For future debuggers, the flow goes like this:
        //  - ReactInstanceManager.runCreateReactContextOnNewThread()
        //       this sets up new instance
        //  - ReactInstanceManager.tearDownReactContext()
        //  - ReactContext.destroy()
        //  - CatalystInstanceImpl.destroy()
        //       this notifies listeners that the app is about to be destroyed
        //  - mHybridData.resetNative()
        //  - ~CatalystInstanceImpl()
        //  - ~Instance()
        //  - NativeToJSBridge.destroy()
        //  - m_executor = nullptr
        //  - ~Runtime()
        //  - ...
        //  - ~Database()
        //
        // First attempt to work around this issue was by disabling sqlite3's threadsafety (which caused
        // pthread apis to be called, leading to a crash), since we're only using it from one thread
        // but predictably that caused new issues.
        // When using headless JS, this issue would occur:
        //    Failed to get a row for query - sqlite error 11 (database disk image is malformed)
        // (Not exactly sure why, seems like headless JS reuses the same catalyst instance...)
        //
        // Current workaround is to tap into CatalystInstanceImpl.destroy() to destroy the database
        // before it's destructed via normal C++ rules. There's no clean API for our JSI setup, so
        // we route via NativeModuleRegistry onCatalystInstanceDestroy -> DatabaseBridge ->
        // WatermelonJSI via reflection (and switch to the currect thread - important!) and then to
        // individual Database objects via this listener callback. It's ugly, but should work.
        //
        // 2023 update: Check if the above is still true, given https://github.com/Nozbe/WatermelonDB/issues/1474
        // showed that the true cause of the pthread_mutex_lock crash is something else.
        // On the other hand, it's still true that invalidation happens asynchronously and could happen
        // after new bridge is already set up, which could cause locking issues (and a case was found on iOS where
        // this does happen)
        std::weak_ptr<Database> weakDatabase = database;
        platform::onDestroy([weakDatabase]() {
            if (auto databaseToDestroy = weakDatabase.lock()) {
                consoleLog("Destroying database due to RCTBridge invalidation");
                databaseToDestroy->destroy();
            }
        });

        createMethod(rt, adapter, "initialize", 2, [database](jsi::Runtime &rt, const jsi::Value *args) {
            jsi::String dbName = args[0].getString(rt);
            int expectedVersion = (int)args[1].getNumber();

            int databaseVersion = database->getUserVersion();

            jsi::Object response(rt);

            if (databaseVersion == expectedVersion) {
                database->initialized_ = true;
                response.setProperty(rt, "code", "ok");
            } else if (databaseVersion == 0) {
                response.setProperty(rt, "code", "schema_needed");
            } else if (databaseVersion < expectedVersion) {
                response.setProperty(rt, "code", "migrations_needed");
                response.setProperty(rt, "databaseVersion", databaseVersion);
            } else {
                consoleLog("Database has newer version (" + std::to_string(databaseVersion) +
                           ") than what the app supports (" + std::to_string(expectedVersion) + "). Will reset database.");
                response.setProperty(rt, "code", "schema_needed");
            }

            return response;
        });
        createMethod(rt, adapter, "setUpWithSchema", 3, [database](jsi::Runtime &rt, const jsi::Value *args) {
            jsi::String dbName = args[0].getString(rt);
            jsi::String schema = args[1].getString(rt);
            int schemaVersion = (int)args[2].getNumber();

            try {
                database->unsafeResetDatabase(schema, schemaVersion);
            } catch (const std::exception &ex) {
                consoleError("Failed to set up the database correctly - " + std::string(ex.what()));
                std::abort();
            }

            database->initialized_ = true;
            return jsi::Value::undefined();
        });
        createMethod(rt, adapter, "setUpWithMigrations", 4, [database](jsi::Runtime &rt, const jsi::Value *args) {
            jsi::String dbName = args[0].getString(rt);
            jsi::String migrationSchema = args[1].getString(rt);
            int fromVersion = (int)args[2].getNumber();
            int toVersion = (int)args[3].getNumber();

            try {
                database->migrate(migrationSchema, fromVersion, toVersion);
            } catch (const std::exception &ex) {
                consoleError("Failed to migrate the database correctly - " + std::string(ex.what()));
                return makeError(rt, ex.what());
            }

            database->initialized_ = true;
            return jsi::Value::undefined();
        });
        createMethod(rt, adapter, "find", 2, [database](jsi::Runtime &rt, const jsi::Value *args) {
            assert(database->initialized_);
            jsi::String tableName = args[0].getString(rt);
            jsi::String id = args[1].getString(rt);
            return database->find(tableName, id);
        });
        createMethod(rt, adapter, "query", 3, [database](jsi::Runtime &rt, const jsi::Value *args) {
            assert(database->initialized_);
            jsi::String tableName = args[0].getString(rt);
            jsi::String sql = args[1].getString(rt);
            jsi::Array arguments = args[2].getObject(rt).getArray(rt);
            return database->query(tableName, sql, arguments);
        });
        createMethod(rt, adapter, "queryAsArray", 3, [database](jsi::Runtime &rt, const jsi::Value *args) {
            assert(database->initialized_);
            jsi::String tableName = args[0].getString(rt);
            jsi::String sql = args[1].getString(rt);
            jsi::Array arguments = args[2].getObject(rt).getArray(rt);
            return database->queryAsArray(tableName, sql, arguments);
        });
        createMethod(rt, adapter, "queryIds", 2, [database](jsi::Runtime &rt, const jsi::Value *args) {
            assert(database->initialized_);
            jsi::String sql = args[0].getString(rt);
            jsi::Array arguments = args[1].getObject(rt).getArray(rt);
            return database->queryIds(sql, arguments);
        });
        createMethod(rt, adapter, "unsafeQueryRaw", 2, [database](jsi::Runtime &rt, const jsi::Value *args) {
            assert(database->initialized_);
            jsi::String sql = args[0].getString(rt);
            jsi::Array arguments = args[1].getObject(rt).getArray(rt);
            return database->unsafeQueryRaw(sql, arguments);
        });
        createMethod(rt, adapter, "count", 2, [database](jsi::Runtime &rt, const jsi::Value *args) {
            assert(database->initialized_);
            jsi::String sql = args[0].getString(rt);
            jsi::Array arguments = args[1].getObject(rt).getArray(rt);
            return database->count(sql, arguments);
        });
        createMethod(rt, adapter, "batch", 1, [database](jsi::Runtime &rt, const jsi::Value *args) {
            assert(database->initialized_);
            jsi::Array operations = args[0].getObject(rt).getArray(rt);
            database->batch(operations);
            return jsi::Value::undefined();
        });
        createMethod(rt, adapter, "batchJSON", 1, [database](jsi::Runtime &rt, const jsi::Value *args) {
            assert(database->initialized_);
            database->batchJSON(args[0].getString(rt));
            return jsi::Value::undefined();
        });
        createMethod(rt, adapter, "getLocal", 1, [database](jsi::Runtime &rt, const jsi::Value *args) {
            assert(database->initialized_);
            jsi::String key = args[0].getString(rt);
            return database->getLocal(key);
        });
        createMethod(rt, adapter, "unsafeLoadFromSync", 4, [database](jsi::Runtime &rt, const jsi::Value *args) {
            assert(database->initialized_);
            auto jsonId = (int) args[0].getNumber();
            auto schema = args[1].getObject(rt);
            auto preamble = args[2].getString(rt).utf8(rt);
            auto postamble = args[3].getString(rt).utf8(rt);
            return database->unsafeLoadFromSync(jsonId, schema, preamble, postamble);
        });
        createMethod(rt, adapter, "unsafeExecuteMultiple", 1, [database](jsi::Runtime &rt, const jsi::Value *args) {
            assert(database->initialized_);
            auto sqlString = args[0].getString(rt).utf8(rt);
            database->executeMultiple(sqlString);
            return jsi::Value::undefined();
        });
        createMethod(rt, adapter, "unsafeResetDatabase", 2, [database](jsi::Runtime &rt, const jsi::Value *args) {
            assert(database->initialized_);
            jsi::String schema = args[0].getString(rt);
            int schemaVersion = (int)args[1].getNumber();

            try {
                database->unsafeResetDatabase(schema, schemaVersion);
                return jsi::Value::undefined();
            } catch (const std::exception &ex) {
                consoleError("Failed to reset database correctly - " + std::string(ex.what()));
                // Partially reset database is likely corrupted, so it's probably less bad to crash
                std::abort();
            }
        });
        createMethod(rt, adapter, "unsafeClose", 0, [database](jsi::Runtime &rt, const jsi::Value *args) {
            assert(database->initialized_);
            database->destroy();
            database->initialized_ = false;
            return jsi::Value::undefined();
        });

        return adapter;
    });

    // TODO: Use the onMemoryAlert hook!
}


} // namespace watermelondb

from hermes.

neildhar avatar neildhar commented on April 28, 2024

which react-native native module is calling the jsi environment

That would fall outside Hermes unfortunately. It is possible that RN has some logging that logs transitions into native code from JS, but we can't provide much input there.

Your best bet may be to attach a debugger and set breakpoints on entry into the Hermes runtime to determine if anything is potentially corrupting runtime state.

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

Can you provide some interesting points on React-jsi directory to place the debugger on?
Having a hard time to where to place it

from hermes.

tmikov avatar tmikov commented on April 28, 2024

This is probably a good place, you should get a hit whenever native calls into JavaScript:

jsi::Value HermesRuntimeImpl::call(

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

Screenshot 2024-01-22 at 11 30 53
Is it normal to have this quantity of leak warnings on debug mode?

from hermes.

neildhar avatar neildhar commented on April 28, 2024

@lucaswitch According to Apple's documentation it looks like the leak detector works by scanning the application memory for pointers to regions that were allocated with malloc.

The behaviour you're seeing is expected, since we recently made a change to how the pointers are encoded in the heap, which would result in the leaks tool not being able to find them. However, this isn't a real leak, and Hermes should still release the memory whenever the ArrayBuffer is garbage collected.

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

Thank you for the fast response! Ok i will keep looking around on jsi modules.

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

The stacktrace changed a bit. Removed a lot of the application deps.
Screenshot 2024-01-23 at 14 27 30

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

Done! @neildhar. As the #1147 i placed it every 100ms.
Screenshot 2024-01-23 at 15 28 19

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

Another crash, also i can see that the Module being called is the JSTimers.
Screenshot 2024-01-23 at 15 37 00
Screenshot 2024-01-23 at 15 37 14
Screenshot 2024-01-23 at 15 38 09
Screenshot 2024-01-23 at 15 44 47

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

Also i figure out that this is the current third party modules accessing the jsi functions.

image

from hermes.

tmikov avatar tmikov commented on April 28, 2024

@lucaswitch the top of the stack looks a deadlock exception thrown by the sampling profiler mutex. Very interesting. Can we see the message of that exception?

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

Here is another one maybe related to #1203.
Also because our app uses redux-saga it relies heavily on generator functions. I can see on the stacktrace some breadcrumbs that could be that: hermes::vm::GeneratorInnerFunction

Also i was not able to get the exception pointer value...

Screenshot 2024-01-24 at 09 54 02
Screenshot 2024-01-24 at 10 04 35
Screenshot 2024-01-24 at 10 05 51
Screenshot 2024-01-24 at 10 17 21

from hermes.

lucaswitch avatar lucaswitch commented on April 28, 2024

Solved! Closing as this issue is not a hermes engine issue.
It was a private react-native module with c++ responsible for doing fast fourrier transform that was causing the issue(maybe some wrong offset access or any kind o dynamic resource allocation).
After a full rewrite of this module in plain javascript the crash was gone.

Thank you for all the support on this issue. @tmikov @neildhar
Very much appreciated.

from hermes.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.