diff --git a/src/NativeScript/Calling/FunctionWrapper.mm b/src/NativeScript/Calling/FunctionWrapper.mm index f49bc3a92..dfabdd72b 100644 --- a/src/NativeScript/Calling/FunctionWrapper.mm +++ b/src/NativeScript/Calling/FunctionWrapper.mm @@ -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); @@ -114,6 +117,8 @@ __block std::unique_ptr 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); @@ -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; @@ -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(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; diff --git a/src/NativeScript/TNSRuntime.h b/src/NativeScript/TNSRuntime.h index fc1fed6af..1978f36e8 100644 --- a/src/NativeScript/TNSRuntime.h +++ b/src/NativeScript/TNSRuntime.h @@ -39,7 +39,9 @@ FOUNDATION_EXTERN void TNSSetUncaughtErrorHandler(TNSUncaughtErrorHandler handle - (JSValueRef)convertObject:(id)object; -- (id)appPackageJson; +- (NSDictionary*)appPackageJson; + +- (void)tryCollectGarbage; @end @interface TNSWorkerRuntime : TNSRuntime diff --git a/src/NativeScript/TNSRuntime.mm b/src/NativeScript/TNSRuntime.mm index 1fbfc352f..973dbb7c3 100644 --- a/src/NativeScript/TNSRuntime.mm +++ b/src/NativeScript/TNSRuntime.mm @@ -31,6 +31,8 @@ #import "TNSRuntime.h" #include "Workers/JSWorkerGlobalObject.h" +#include + using namespace JSC; using namespace NativeScript; @@ -47,7 +49,21 @@ @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 @@ -55,6 +71,12 @@ @implementation TNSRuntime @synthesize appPackageJsonData; +@synthesize gcThrottleTimeValue; + +@synthesize memoryCheckIntervalValue; + +@synthesize freeMemoryRatioValue; + static WTF::Lock _runtimesLock; static NSPointerArray* _runtimes; @@ -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; @@ -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(vm_stat.free_count + vm_stat.inactive_count); + double used = static_cast(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>(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)