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

Hermes reference tracking #563

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open

Hermes reference tracking #563

wants to merge 21 commits into from

Conversation

brocollie08
Copy link
Contributor

@brocollie08 brocollie08 commented Dec 18, 2024

No longer overloading the global JNI table, tracking references in C++ instead to avoid over-usage crash

Change Type (required)

Indicate the type of change your pull request is:

  • patch
  • minor
  • major
  • N/A

Does your PR have any documentation updates?

  • Updated docs
  • No Update needed
  • Unable to update docs
📦 Published PR as canary version: 0.10.1--canary.563.18922

Try this version out locally by upgrading relevant packages to 0.10.1--canary.563.18922

@brocollie08 brocollie08 added the patch Increment the patch version when merged label Dec 18, 2024
Copy link

codecov bot commented Dec 18, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 89.70%. Comparing base (94d37fc) to head (e65146c).
Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #563      +/-   ##
==========================================
- Coverage   89.70%   89.70%   -0.01%     
==========================================
  Files         331      331              
  Lines       19794    19794              
  Branches     1949     1949              
==========================================
- Hits        17757    17756       -1     
- Misses       2023     2024       +1     
  Partials       14       14              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@brocollie08 brocollie08 changed the title Explicit deconstructors Hermes reference tracking Dec 20, 2024
@brocollie08 brocollie08 marked this pull request as ready for review December 20, 2024 21:45
@brocollie08
Copy link
Contributor Author

/canary

Comment on lines 44 to 45
auto ref = newObjectCxxArgs();
return ref;
Copy link
Member

Choose a reason for hiding this comment

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

Nit: unnecessary local var (applies many places)

Comment on lines 65 to 78
for (auto& ptr : *runtimeScope_->valueScope) {
ptr.second.reset();
}
for (auto& ptr : *runtimeScope_->objectScope) {
ptr.second.reset();
}
for (auto& ptr : *runtimeScope_->functionScope) {
ptr.second.reset();
}
for (auto& ptr : *runtimeScope_->arrayScope) {
ptr.second.reset();
}
for (auto& ptr : *runtimeScope_->symbolScope) {
ptr.second.reset();
Copy link
Member

Choose a reason for hiding this comment

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

This should all be private implementation details of RuntimeScope::release.

Copy link
Member

Choose a reason for hiding this comment

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

Secondarily, this should also probably clear the keys too - as we discovered earlier.

@@ -107,12 +106,12 @@ class JHermesRuntime : public HybridClass<JHermesRuntime, JJSIRuntime> {
private:
friend HybridBase;
std::unique_ptr<HermesRuntime> runtime_;
global_ref<JHermesConfig::jhybridobject> jConfig_;
std::vector<global_ref<JHybridClass::jhybridobject>> scope_;
::global_ref<JHermesConfig::jhybridobject> jConfig_;
Copy link
Member

Choose a reason for hiding this comment

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

::?

Comment on lines -58 to -72
// TODO: From my current understanding, holding a global prevents the class from being
// GC'd when it goes out of scope in Java land, which would prevent the JSI Value reference
// from being reset. I originally wanted to keep a weak_ref, but was getting intermittent
// segfaults from untracked (or incorrectly tracked) references that were then not being
// released because the weak_ref was out of scope. The assumption there was that if the
// weak_ref was out of scope, it was already released. But this doesn't seem to be true,
// otherwise we wouldn't be getting segfaults. Making them global does prevent the segfaults,
// reaffirming the above. Since all the wrapper classes are holding are pointers
// this probably isn't a huge deal, but is certainly an inefficiency we should look
// to remove. I'd like to just create weak_ptrs on the JSI values themselves,
// which requires the holders to use shared_ptrs and expose a getter for accessing.
// Then we'd also need a way to hold arbitrary weak_ptrs. We could also maybe try
// to get weak refs to _just_ the CXX parts of the hybrid class to avoid leaking things
// on the JVM side?
// TODO: Maybe we could do a periodic pruning of the vector to remove obsolete refs?
Copy link
Member

Choose a reason for hiding this comment

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

Beautiful - removing longstanding paragraphs of TODOs are always great to see.

@@ -39,8 +39,7 @@ void JJSIPreparedJavaScript::registerNatives() {
}
Copy link
Member

Choose a reason for hiding this comment

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

Can't put a comment on a diff that isn't modified - comment at the top of this file can now be removed as well:

/**
* IMPORTANT: Our current scoping strategy is to ensure all JHybridClass constructs (JSI Value wrappers)
* are tracked as refs with the runtime that they're created in. This allows the runtime to explicitly
* release any lingering runtime pointer values before releasing the runtime itself. This prevents dangling
* pointers that could cause undefined behavior (likely segfault - or assert if in debug mode).
*
* Currently, this requires careful consideration for every newObjectCxxArgs call. If it creates a wrapper
* around a runtime PointerValue, and we don't have a guarantee it won't go out of scope, then we need to
* make sure it's tracked. TODO: Make it more foolproof, maybe inside the ctors for our wrappers
*/

Comment on lines 129 to 132
explicit JJSIValue(std::shared_ptr<RuntimeScope> scope, Value&& value) : HybridClass(), scope_(scope) {
// internally creates unique ptr
scope->trackValue(this, std::move(value));
}
Copy link
Member

Choose a reason for hiding this comment

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

If/when there is a singular API to track arbitrary runtime types, we should try to capture this as a common super constructor for JJSIValueHybridClass to enforce for any subclass (there are some new ones that may exist in the future).

Comment on lines +155 to +157
~JJSIValue() override {
release();
}
Copy link
Member

Choose a reason for hiding this comment

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

Is this still necessary?

Comment on lines +160 to +161
if (!tracked && value_) value_.reset();
if (scope_) scope_->clearRef(this);
Copy link
Member

Choose a reason for hiding this comment

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

Ah, I get why this is necessary - but maybe the !tracked && value_ case could just make it's own RuntimeScope so that all the APIs still go through a scope and you don't need explicit logic to handle both cases.

if (auto ref = scope_->getValue((void *)this)) {
return *ref;
} else {
throw std::runtime_error("Value reference cleaned up and is no longer in RuntimeScope");
Copy link
Member

Choose a reason for hiding this comment

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

  1. This should probably just go through throwNativeHandleReleasedException("Value"); from below
  2. If you don't go through the helper function, you need to tell the JNI to expect an exception, like throwNativeHandleReleasedException does (and should also probably wrap in a PlayerException - like throwNativeHandleReleasedException does)

if (it != valueScope->end()) {
return valueScope->at(ptr).get();
}
throw std::runtime_error("Value was not found in the RuntimeScope");
Copy link
Member

Choose a reason for hiding this comment

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

Why throw here? The return type is nullable, it should just return nullptr.

Otherwise, change return type to Value& to be more accurate (although, my preference would be keep nullable return type to match standard map APIs and let the caller handle nullptr explicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this would apply to all the pointers, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
patch Increment the patch version when merged
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants