Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NativeScript/runtime/ArgConverter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <Foundation/Foundation.h>
#include <sstream>
#include "DictionaryAdapter.h"
#include "ExternalMemory.h"
#include "Helpers.h"
#include "Interop.h"
#include "NSExceptionSupport.h"
Expand Down Expand Up @@ -638,6 +639,7 @@
if (!resultIsOwned) {
[result retain];
}
ExternalMemory::ChargeEstimatedSize(isolate, thiz);
}
}

Expand Down
2 changes: 2 additions & 0 deletions NativeScript/runtime/Caches.h
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ class Caches {
robin_hood::unordered_map<const void*,
std::shared_ptr<v8::Persistent<v8::Object>>>
PointerInstances;
// ExternalMemory's size estimator per class, resolved on first sight.
robin_hood::unordered_map<Class, uint8_t> ExternalSizeEstimators;

// Live IsolateTracked instances (URL, URLSearchParams, URLPattern). Their
// weak-callback finalizers never fire at isolate disposal, so teardown
Expand Down
8 changes: 8 additions & 0 deletions NativeScript/runtime/DataWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#include "Common.h"
#include "ConcurrentQueue.h"
#include "ExternalMemory.h"
#include "Metadata.h"
#include "libffi.h"

Expand Down Expand Up @@ -163,8 +164,15 @@ class BaseDataWrapper {

void GcUnprotect() { this->gcProtected_ = false; }

ExternalMemoryCharge* ExternalCharge() { return this->externalCharge_.get(); }

void SetExternalCharge(std::unique_ptr<ExternalMemoryCharge> charge) {
this->externalCharge_ = std::move(charge);
}

private:
bool gcProtected_;
std::unique_ptr<ExternalMemoryCharge> externalCharge_;
};

class EnumDataWrapper : public BaseDataWrapper {
Expand Down
63 changes: 63 additions & 0 deletions NativeScript/runtime/ExternalMemory.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#ifndef ExternalMemory_h
#define ExternalMemory_h

#include <cstddef>

#include "v8-external-memory-accounter.h"
#include "v8.h"

namespace tns {

class BaseDataWrapper;

// Native bytes a wrapper keeps alive, reported to V8 so the collector weighs
// them when scheduling collections. The wrapper owns it, so the bytes are
// returned whenever the wrapper is deleted, from whichever path deletes it.
//
// Releasing is safe from any thread and after the isolate is gone: it pins
// the isolate's gate instead of taking its Locker (the decrease is a single
// atomic in V8 and never collects), and once the gate is closed the isolate
// is being disposed and the bytes are dropped with it.
class ExternalMemoryCharge {
public:
ExternalMemoryCharge(v8::Isolate* isolate, int gateId)
: isolate_(isolate), gateId_(gateId) {}
~ExternalMemoryCharge();
ExternalMemoryCharge(const ExternalMemoryCharge&) = delete;
ExternalMemoryCharge& operator=(const ExternalMemoryCharge&) = delete;

size_t Bytes() const { return bytes_; }
v8::Isolate* Isolate() const { return isolate_; }

// Isolate thread only: an increase can run a collection before returning.
void Set(size_t bytes);

private:
v8::Isolate* isolate_;
int gateId_;
size_t bytes_ = 0;
v8::ExternalMemoryAccounter accounter_;
};

namespace ExternalMemory {

// Replaces the bytes `wrapper` reports for `isolate`; 0 drops the charge.
// Isolate thread only. A wrapper charged for another isolate keeps its charge.
void SetSize(v8::Isolate* isolate, BaseDataWrapper* wrapper, size_t bytes);

// Charges an estimate of the native footprint of the ObjC or CF object that
// `value` wraps, unless it already carries a charge or its class has no
// estimator. Only call it for objects JS plausibly holds the last reference
// to: collecting the wrapper of an object that native code also retains frees
// nothing, so charging it only buys collections that cannot pay off.
void ChargeEstimatedSize(v8::Isolate* isolate, v8::Local<v8::Value> value);

// Forwards the system's memory pressure events to every live isolate.
// Process-wide and idempotent.
void StartMemoryPressureMonitoring();

} // namespace ExternalMemory

} // namespace tns

#endif /* ExternalMemory_h */
214 changes: 214 additions & 0 deletions NativeScript/runtime/ExternalMemory.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
#include "ExternalMemory.h"

#include <CoreFoundation/CoreFoundation.h>
#include <Foundation/Foundation.h>
#include <dispatch/dispatch.h>
#include <dlfcn.h>
#include <objc/message.h>
#include <objc/runtime.h>

#include "Caches.h"
#include "DataWrapper.h"
#include "Helpers.h"
#include "IsolateWrapper.h"
#include "NSDataAdapter.h"
#include "Runtime.h"

using namespace v8;

namespace tns {

ExternalMemoryCharge::~ExternalMemoryCharge() {
if (bytes_ == 0) {
return;
}
// A closed gate means the isolate is being disposed, which drops its
// external memory total with it.
if (IsolateGates::TryPin(gateId_)) {
accounter_.Decrease(isolate_, bytes_);
IsolateGates::Unpin(gateId_);
}
}

void ExternalMemoryCharge::Set(size_t bytes) {
if (bytes > bytes_) {
accounter_.Increase(isolate_, bytes - bytes_);
} else if (bytes < bytes_) {
accounter_.Decrease(isolate_, bytes_ - bytes);
}
bytes_ = bytes;
}

namespace ExternalMemory {

namespace {

// Values of Caches::ExternalSizeEstimators.
enum Estimator : uint8_t {
kNone,
kNSData,
kUIImage,
// Toll-free bridged CF instances all share one class, so the estimator is
// picked per object from its CFTypeID.
kCFType,
};

typedef CFTypeID (*TypeIDFn)(void);
typedef size_t (*SizeOfFn)(CFTypeRef);

// Resolved at runtime so the runtime does not link CoreGraphics or CoreVideo
// itself; apps that never load them simply get no estimate.
struct CFEstimators {
TypeIDFn cgImageTypeID = nullptr;
SizeOfFn cgImageBytesPerRow = nullptr;
SizeOfFn cgImageHeight = nullptr;
TypeIDFn pixelBufferTypeID = nullptr;
SizeOfFn pixelBufferDataSize = nullptr;
};

const CFEstimators& GetCFEstimators() {
static CFEstimators estimators = [] {
CFEstimators e;
e.cgImageTypeID = (TypeIDFn)dlsym(RTLD_DEFAULT, "CGImageGetTypeID");
e.cgImageBytesPerRow = (SizeOfFn)dlsym(RTLD_DEFAULT, "CGImageGetBytesPerRow");
e.cgImageHeight = (SizeOfFn)dlsym(RTLD_DEFAULT, "CGImageGetHeight");
e.pixelBufferTypeID = (TypeIDFn)dlsym(RTLD_DEFAULT, "CVPixelBufferGetTypeID");
e.pixelBufferDataSize = (SizeOfFn)dlsym(RTLD_DEFAULT, "CVPixelBufferGetDataSize");
return e;
}();
return estimators;
}

size_t CGImageSize(CFTypeRef image) {
const CFEstimators& e = GetCFEstimators();
if (image == nullptr || e.cgImageBytesPerRow == nullptr || e.cgImageHeight == nullptr) {
return 0;
}
return e.cgImageBytesPerRow(image) * e.cgImageHeight(image);
}

Estimator ResolveEstimator(Class klass) {
// NSDataAdapter exposes bytes of a JS buffer that V8 already accounts for.
if ([klass isSubclassOfClass:[NSDataAdapter class]]) {
return kNone;
}
if ([klass isSubclassOfClass:[NSData class]]) {
return kNSData;
}
static Class uiImageClass = objc_getClass("UIImage");
if (uiImageClass != nil && [klass isSubclassOfClass:uiImageClass]) {
return kUIImage;
}
static Class cfTypeClass = objc_getClass("__NSCFType");
if (cfTypeClass != nil && klass == cfTypeClass) {
return kCFType;
}
return kNone;
}

size_t Estimate(Estimator estimator, id obj) {
switch (estimator) {
case kNSData:
return [(NSData*)obj length];
case kUIImage: {
// UIKit is not linked by the runtime, so its selectors are registered
// rather than declared. A symbol image rasterizes on -CGImage, and one
// backed by a CIImage has no bitmap until it is rendered (NULL here).
static SEL isSymbolImage = sel_registerName("isSymbolImage");
static SEL cgImage = sel_registerName("CGImage");
if ([obj respondsToSelector:isSymbolImage] &&
((BOOL (*)(id, SEL))objc_msgSend)(obj, isSymbolImage)) {
return 0;
}
return CGImageSize(((CFTypeRef (*)(id, SEL))objc_msgSend)(obj, cgImage));
}
case kCFType: {
const CFEstimators& e = GetCFEstimators();
CFTypeID type = CFGetTypeID((CFTypeRef)obj);
if (e.cgImageTypeID != nullptr && type == e.cgImageTypeID()) {
return CGImageSize((CFTypeRef)obj);
}
if (e.pixelBufferTypeID != nullptr && e.pixelBufferDataSize != nullptr &&
type == e.pixelBufferTypeID()) {
return e.pixelBufferDataSize((CFTypeRef)obj);
}
return 0;
}
case kNone:
return 0;
}
return 0;
}

} // namespace

void SetSize(Isolate* isolate, BaseDataWrapper* wrapper, size_t bytes) {
ExternalMemoryCharge* charge = wrapper->ExternalCharge();
if (charge == nullptr) {
if (bytes == 0) {
return;
}
auto created =
std::make_unique<ExternalMemoryCharge>(isolate, Caches::Get(isolate)->getGateId());
charge = created.get();
wrapper->SetExternalCharge(std::move(created));
} else if (charge->Isolate() != isolate) {
return;
}
charge->Set(bytes);
}

void ChargeEstimatedSize(Isolate* isolate, Local<Value> value) {
BaseDataWrapper* wrapper = tns::GetValue(isolate, value);
if (wrapper == nullptr || wrapper->Type() != WrapperType::ObjCObject ||
wrapper->ExternalCharge() != nullptr) {
return;
}
ObjCDataWrapper* objcWrapper = static_cast<ObjCDataWrapper*>(wrapper);
id obj = objcWrapper->Data();
if (obj == nil) {
return;
}

auto& estimators = Caches::Get(isolate)->ExternalSizeEstimators;
Class klass = objcWrapper->Klass();
auto it = estimators.find(klass);
Estimator estimator;
if (it != estimators.end()) {
estimator = static_cast<Estimator>(it->second);
} else {
estimator = ResolveEstimator(klass);
estimators.emplace(klass, estimator);
}
if (estimator == kNone) {
return;
}

SetSize(isolate, wrapper, Estimate(estimator, obj));
}

void StartMemoryPressureMonitoring() {
static dispatch_source_t source;
static dispatch_once_t once;
dispatch_once(&once, ^{
source = dispatch_source_create(DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, 0,
DISPATCH_MEMORYPRESSURE_NORMAL | DISPATCH_MEMORYPRESSURE_WARN |
DISPATCH_MEMORYPRESSURE_CRITICAL,
dispatch_get_global_queue(QOS_CLASS_UTILITY, 0));
dispatch_source_set_event_handler(source, ^{
unsigned long status = dispatch_source_get_data(source);
MemoryPressureLevel level = MemoryPressureLevel::kNone;
if (status & DISPATCH_MEMORYPRESSURE_CRITICAL) {
level = MemoryPressureLevel::kCritical;
} else if (status & DISPATCH_MEMORYPRESSURE_WARN) {
level = MemoryPressureLevel::kModerate;
}
Runtime::NotifyMemoryPressure(level);
});
dispatch_resume(source);
});
}

} // namespace ExternalMemory

} // namespace tns
2 changes: 2 additions & 0 deletions NativeScript/runtime/Interop.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ class Interop {
v8::Local<v8::Object> interop);
static void RegisterAdoptFunction(v8::Local<v8::Context> context,
v8::Local<v8::Object> interop);
static void RegisterExternalSizeFunctions(v8::Local<v8::Context> context,
v8::Local<v8::Object> interop);
static void RegisterSizeOfFunction(v8::Local<v8::Context> context,
v8::Local<v8::Object> interop);
static void RegisterEscapeExceptionFunction(v8::Local<v8::Context> context,
Expand Down
10 changes: 10 additions & 0 deletions NativeScript/runtime/Interop.mm
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "Constants.h"
#include "DictionaryAdapter.h"
#include "ExtVector.h"
#include "ExternalMemory.h"
#include "Helpers.h"
#include "NSDataAdapter.h"
#include "NativeScriptException.h"
Expand Down Expand Up @@ -1841,6 +1842,15 @@ inline bool isBool() {
methodCall.context_, methodCall.typeEncoding_, &call, marshalToPrimitive, nullptr, false,
methodCall.ownsReturnedObject_, methodCall.returnsUnmanaged_, methodCall.isInitializer_);

// Owned returns, initializers and class factory methods hand out objects JS
// most likely holds the last reference to. Instance getters do not: their
// results usually stay retained by the receiver.
bool returnsFreshObject = methodCall.ownsReturnedObject_ || methodCall.isInitializer_ ||
(!methodCall.isPrimitiveFunction_ && !isInstanceMethod);
if (returnsFreshObject && !result.IsEmpty() && result->IsObject()) {
ExternalMemory::ChargeEstimatedSize(v8::Isolate::GetCurrent(), result);
}

return result;
}

Expand Down
Loading
Loading