Skip to content

Commit

Permalink
feat(bridge): Provide automatic garbage collection triggering
Browse files Browse the repository at this point in the history
Introduce settings similar to the ones in {N} Android Runtime for
configuring automatic GC triggering by the runtime. They are
specified in `app/package.json` as values in the `ios` section:

* `gcThrottleTime` - number of milliseconds which must have passed
since the last automatic GC in order to trigger one during a call from
JS to native
* `memoryCheckInterval` - number of milliseconds which must have
passed since the last automatic GC in order to check the memory
usage on the device and trigger GC if free memory is below the specified
threshold
* `freeMemoryRatio` - a floating-point value from 0 to 1 which sets the
threshold below which a GC will be triggered when the above check is made.

implements #1035
  • Loading branch information
mbektchiev committed Jan 17, 2019
1 parent c3de1e2 commit edbf653
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 32 deletions.
56 changes: 27 additions & 29 deletions src/NativeScript/Calling/FunctionWrapper.mm
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@
ReleasePoolHolder releasePoolHolder(execState);

JSC::VM& vm = execState->vm();

[[TNSRuntime current] tryCollectGarbage];

auto scope = DECLARE_THROW_SCOPE(vm);

callee->preCall(execState, invocation);
Expand Down Expand Up @@ -114,6 +117,8 @@
__block std::unique_ptr<FFICall::Invocation> invocation(new FFICall::Invocation(call));
ReleasePoolHolder releasePoolHolder(execState);

JSC::VM& vm = execState->vm();

Register* fakeCallFrame = new Register[CallFrame::headerSizeInRegisters + execState->argumentCount() + 1];
ExecState* fakeExecState = ExecState::create(fakeCallFrame);

Expand All @@ -128,10 +133,9 @@
ASSERT(fakeExecState->argumentCount() == arguments.size());

{
JSC::VM& vm = execState->vm();
auto scope = DECLARE_THROW_SCOPE(vm);

TopCallFrameSetter frameSetter(execState->vm(), fakeExecState);
TopCallFrameSetter frameSetter(vm, fakeExecState);
call->preCall(fakeExecState, *invocation);
if (Exception* exception = scope.exception()) {
delete[] fakeCallFrame;
Expand All @@ -145,48 +149,42 @@
__block TNSRuntime* runtime = [TNSRuntime current];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
JSC::VM& vm = fakeExecState->vm();
auto scope = DECLARE_CATCH_SCOPE(vm);

NSException* nsexception = nullptr;
JSLockHolder lockHolder(vm);

[[TNSRuntime current] tryCollectGarbage];

// we no longer have a valid caller on the stack, what with being async and all
fakeExecState->setCallerFrame(fakeExecState->lexicalGlobalObject()->globalExec());
TopCallFrameSetter frameSetter(vm, fakeExecState);

@try {
// Native call is made outside of the VM lock by design.
// For more information see https://github.com/NativeScript/ios-runtime/issues/215 and it's corresponding PR.
// This creates a racing condition which might corrupt the internal state of the VM but
// a fix for it is outside of this PR's scope, so I'm leaving it like it has always been.
JSLock::DropAllLocks locksDropper(fakeExecState);

ffi_call(call->cif().get(), FFI_FN(invocation->function), invocation->resultBuffer(), reinterpret_cast<void**>(invocation->_buffer + call->argsArrayOffset()));

} @catch (NSException* ex) {
nsexception = ex;
auto throwScope = DECLARE_THROW_SCOPE(vm);
throwVMError(fakeExecState, throwScope, JSValue(createErrorFromNSException(runtime, fakeExecState, ex)));
}

// Native call is made outside of the VM lock by design.
// For more information see https://github.com/NativeScript/ios-runtime/issues/215 and it's corresponding PR.
// This creates a racing condition which might corrupt the internal state of the VM but
// a fix for it is outside of this PR's scope, so I'm leaving it like it has always been.
JSLockHolder lockHolder(vm);

// we no longer have a valid csaller on the stack, what with being async and all
fakeExecState->setCallerFrame(fakeExecState->lexicalGlobalObject()->globalExec());

JSValue result;
JSValue jsexception = jsNull();
{
TopCallFrameSetter frameSetter(vm, fakeExecState);
result = call->returnType().read(fakeExecState, invocation->_buffer + call->returnOffset(), call->returnTypeCell().get());
JSValue result = call->returnType().read(fakeExecState, invocation->_buffer + call->returnOffset(), call->returnTypeCell().get());

call->postCall(fakeExecState, *invocation);
call->postCall(fakeExecState, *invocation);

if (Exception* ex = scope.exception()) {
scope.clearException();
jsexception = ex->value();
} else if (nsexception != nullptr) {
jsexception = JSValue(createErrorFromNSException(runtime, fakeExecState, nsexception));
}
}
if (jsexception != jsNull()) {
if (Exception* ex = scope.exception()) {
scope.clearException();

CallData rejectCallData;
CallType rejectCallType = JSC::getCallData(vm, deferred->reject(), rejectCallData);

MarkedArgumentBuffer rejectArguments;
rejectArguments.append(jsexception);
rejectArguments.append(ex->value());
JSC::call(fakeExecState->lexicalGlobalObject()->globalExec(), deferred->reject(), rejectCallType, rejectCallData, jsUndefined(), rejectArguments);
} else {
CallData resolveCallData;
Expand Down
4 changes: 3 additions & 1 deletion src/NativeScript/TNSRuntime.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ FOUNDATION_EXTERN void TNSSetUncaughtErrorHandler(TNSUncaughtErrorHandler handle

- (JSValueRef)convertObject:(id)object;

- (id)appPackageJson;
- (NSDictionary*)appPackageJson;

- (void)tryCollectGarbage;
@end

@interface TNSWorkerRuntime : TNSRuntime
Expand Down
125 changes: 123 additions & 2 deletions src/NativeScript/TNSRuntime.mm
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
#import "TNSRuntime.h"
#include "Workers/JSWorkerGlobalObject.h"

#include <mach/mach_host.h>

using namespace JSC;
using namespace NativeScript;

Expand All @@ -47,14 +49,34 @@

@interface TNSRuntime ()

@property(nonatomic, retain) id appPackageJsonData;
@property(nonatomic, retain) NSDictionary* appPackageJsonData;

@property double* gcThrottleTimeValue;

@property double* memoryCheckIntervalValue;

@property double* freeMemoryRatioValue;

- (double)gcThrottleTime;

- (double)memoryCheckInterval;

- (double)freeMemoryRatio;

- (double)readDoubleFromPackageJsonIos:(NSString*)key;

@end

@implementation TNSRuntime

@synthesize appPackageJsonData;

@synthesize gcThrottleTimeValue;

@synthesize memoryCheckIntervalValue;

@synthesize freeMemoryRatioValue;

static WTF::Lock _runtimesLock;
static NSPointerArray* _runtimes;

Expand Down Expand Up @@ -191,7 +213,7 @@ - (JSValueRef)convertObject:(id)object {
return toRef(self->_globalObject->globalExec(), toValue(self->_globalObject->globalExec(), object));
}

- (id)appPackageJson {
- (NSDictionary*)appPackageJson {

if (self->appPackageJsonData != nil) {
return self->appPackageJsonData;
Expand All @@ -207,6 +229,105 @@ - (id)appPackageJson {
return self->appPackageJsonData;
}

- (double)gcThrottleTime {

if (self->gcThrottleTimeValue != nullptr) {
return *self->gcThrottleTimeValue;
}

self->gcThrottleTimeValue = new double([self readDoubleFromPackageJsonIos:@"gcThrottleTime"]);

return *self->gcThrottleTimeValue;
}

- (double)memoryCheckInterval {

if (self->memoryCheckIntervalValue != nullptr) {
return *self->memoryCheckIntervalValue;
}

self->memoryCheckIntervalValue = new double([self readDoubleFromPackageJsonIos:@"memoryCheckInterval"]);

return *self->memoryCheckIntervalValue;
}

- (double)freeMemoryRatio {

if (self->freeMemoryRatioValue != nullptr) {
return *self->freeMemoryRatioValue;
}

self->freeMemoryRatioValue = new double([self readDoubleFromPackageJsonIos:@"freeMemoryRatio"]);

return *self->freeMemoryRatioValue;
}

- (double)readDoubleFromPackageJsonIos:(NSString*)key {
double res = 0;
if (auto packageJson = [self appPackageJson]) {
if (NSDictionary* ios = packageJson[@"ios"]) {
if (id value = ios[key]) {
if ([value respondsToSelector:@selector(doubleValue)]) {
res = [value doubleValue];
} else {
NSLog(@"\"%@\" setting from package.json cannot be converted to double: %@", key, value);
}
}
}
}

return res;
}

double getSystemFreeMemoryRatio() {
mach_port_t host_port = mach_host_self();
;
mach_msg_type_number_t host_size = sizeof(vm_statistics_data_t) / sizeof(integer_t);
vm_statistics_data_t vm_stat;
if (host_statistics(host_port, HOST_VM_INFO, (host_info_t)&vm_stat, &host_size) != KERN_SUCCESS) {
NSLog(@"Failed to fetch vm statistics");
return 0;
}

double free = static_cast<double>(vm_stat.free_count + vm_stat.inactive_count);
double used = static_cast<double>(vm_stat.active_count + vm_stat.wire_count);
double total = free + used;

return free / total;
}

- (void)tryCollectGarbage {
using namespace std;
using namespace std::chrono;

static auto previousGcTime = steady_clock::now();

auto triggerGc = ^{
JSLockHolder locker(self->_vm.get());
self->_vm->heap.collectAsync(CollectionScope::Full);
previousGcTime = steady_clock::now();
};
auto elapsedMs = duration_cast<duration<double, milli>>(steady_clock::now() - previousGcTime).count();

if (auto gcThrottleTimeMs = [self gcThrottleTime]) {
if (elapsedMs > gcThrottleTimeMs) {
triggerGc();
return;
}
}

if (auto gcMemCheckIntervalMs = [self memoryCheckInterval]) {
if (elapsedMs > gcMemCheckIntervalMs) {
if (auto freeMemoryRatio = [self freeMemoryRatio]) {
if (getSystemFreeMemoryRatio() < freeMemoryRatio) {
triggerGc();
return;
}
}
}
}
}

- (void)dealloc {
[self->_applicationPath release];
#if PLATFORM(IOS)
Expand Down

0 comments on commit edbf653

Please sign in to comment.