From edbf653b04d37405ff00af3f8384d2c96ce525cb Mon Sep 17 00:00:00 2001 From: Martin Bektchiev Date: Tue, 8 Jan 2019 15:38:39 +0200 Subject: [PATCH] feat(bridge): Provide automatic garbage collection triggering 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 --- src/NativeScript/Calling/FunctionWrapper.mm | 56 +++++---- src/NativeScript/TNSRuntime.h | 4 +- src/NativeScript/TNSRuntime.mm | 125 +++++++++++++++++++- 3 files changed, 153 insertions(+), 32 deletions(-) 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)