Skip to content

Commit 7f5d49b

Browse files
committed
feat: Add MethodCallProfiler for tracking method call statistics in NativeScript iOS Runtime
1 parent 7b4ce04 commit 7f5d49b

5 files changed

Lines changed: 263 additions & 7 deletions

File tree

NativeScript/runtime/MetadataBuilder.mm

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "Helpers.h"
88
#include "InlineFunctions.h"
99
#include "Interop.h"
10+
#include "MethodCallProfiler.h"
1011
#include "NativeScriptException.h"
1112
#include "ObjectManager.h"
1213
#include "Runtime.h"
@@ -748,6 +749,10 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta
748749
CacheItem<MethodMeta>* item =
749750
static_cast<CacheItem<MethodMeta>*>(info.Data().As<External>()->Value());
750751

752+
if (MethodCallProfiler::IsEnabled()) {
753+
MethodCallProfiler::RecordCall(item->className_, item->meta_);
754+
}
755+
751756
bool instanceMethod = info.This()->InternalFieldCount() > 0;
752757
V8FunctionCallbackArgs args(info);
753758

@@ -789,6 +794,10 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta
789794
return;
790795
}
791796

797+
if (MethodCallProfiler::IsEnabled()) {
798+
MethodCallProfiler::RecordCall(item->className_, item->meta_->getter());
799+
}
800+
792801
V8EmptyValueArgs args;
793802
Local<Context> context = isolate->GetCurrentContext();
794803
Local<Value> result = MetadataBuilder::InvokeMethod(context, item->meta_->getter(), receiver,
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#ifndef MethodCallProfiler_h
2+
#define MethodCallProfiler_h
3+
4+
#include <atomic>
5+
6+
#include "Common.h"
7+
#include "Metadata.h"
8+
9+
namespace tns {
10+
11+
class MethodCallProfiler {
12+
public:
13+
static inline bool IsEnabled() {
14+
return enabled_.load(std::memory_order_relaxed);
15+
}
16+
static void Enable();
17+
static void Disable();
18+
static void Reset();
19+
static void RecordCall(const std::string& className, const MethodMeta* meta);
20+
static void RegisterJSAPI(v8::Isolate* isolate,
21+
v8::Local<v8::ObjectTemplate> globalTemplate);
22+
23+
private:
24+
static std::atomic<bool> enabled_;
25+
26+
static void JSStart(const v8::FunctionCallbackInfo<v8::Value>& info);
27+
static void JSStop(const v8::FunctionCallbackInfo<v8::Value>& info);
28+
static void JSReset(const v8::FunctionCallbackInfo<v8::Value>& info);
29+
static void JSReport(const v8::FunctionCallbackInfo<v8::Value>& info);
30+
static void JSAOTConfig(const v8::FunctionCallbackInfo<v8::Value>& info);
31+
};
32+
33+
} // namespace tns
34+
35+
#endif /* MethodCallProfiler_h */
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
#include "MethodCallProfiler.h"
2+
#include <algorithm>
3+
#include <mutex>
4+
#include <sstream>
5+
#include <unordered_map>
6+
#include <vector>
7+
#include "Helpers.h"
8+
9+
using namespace v8;
10+
11+
namespace tns {
12+
13+
std::atomic<bool> MethodCallProfiler::enabled_{false};
14+
15+
struct MethodProfile {
16+
std::string className;
17+
std::string selectorName;
18+
std::string returnType;
19+
std::vector<std::string> argTypes;
20+
uint64_t count = 0;
21+
};
22+
23+
static std::mutex sProfileMutex;
24+
static std::unordered_map<std::string, MethodProfile> sProfiles;
25+
26+
static const char* EncodingToTypeName(BinaryTypeEncodingType type) {
27+
switch (type) {
28+
case BinaryTypeEncodingType::VoidEncoding:
29+
return "void";
30+
case BinaryTypeEncodingType::BoolEncoding:
31+
return "BOOL";
32+
case BinaryTypeEncodingType::IdEncoding:
33+
case BinaryTypeEncodingType::InstanceTypeEncoding:
34+
case BinaryTypeEncodingType::InterfaceDeclarationReference:
35+
return "id";
36+
case BinaryTypeEncodingType::SelectorEncoding:
37+
return "SEL";
38+
case BinaryTypeEncodingType::ClassEncoding:
39+
return "Class";
40+
case BinaryTypeEncodingType::IntEncoding:
41+
return "int";
42+
case BinaryTypeEncodingType::UIntEncoding:
43+
return "uint";
44+
case BinaryTypeEncodingType::LongEncoding:
45+
return "long";
46+
case BinaryTypeEncodingType::ULongEncoding:
47+
return "ulong";
48+
case BinaryTypeEncodingType::LongLongEncoding:
49+
return "longlong";
50+
case BinaryTypeEncodingType::ULongLongEncoding:
51+
return "ulonglong";
52+
case BinaryTypeEncodingType::FloatEncoding:
53+
return "float";
54+
case BinaryTypeEncodingType::DoubleEncoding:
55+
return "double";
56+
case BinaryTypeEncodingType::CharEncoding:
57+
return "char";
58+
case BinaryTypeEncodingType::UCharEncoding:
59+
return "uchar";
60+
case BinaryTypeEncodingType::ShortEncoding:
61+
return "short";
62+
case BinaryTypeEncodingType::UShortEncoding:
63+
return "ushort";
64+
default:
65+
return nullptr;
66+
}
67+
}
68+
69+
void MethodCallProfiler::Enable() { enabled_.store(true, std::memory_order_relaxed); }
70+
71+
void MethodCallProfiler::Disable() { enabled_.store(false, std::memory_order_relaxed); }
72+
73+
void MethodCallProfiler::Reset() {
74+
std::lock_guard<std::mutex> lock(sProfileMutex);
75+
sProfiles.clear();
76+
}
77+
78+
void MethodCallProfiler::RecordCall(const std::string& className, const MethodMeta* meta) {
79+
const char* sel = meta->selectorAsString();
80+
std::string key = className + "\t" + sel;
81+
82+
std::lock_guard<std::mutex> lock(sProfileMutex);
83+
auto it = sProfiles.find(key);
84+
if (it != sProfiles.end()) {
85+
it->second.count++;
86+
return;
87+
}
88+
89+
MethodProfile profile;
90+
profile.className = className;
91+
profile.selectorName = sel;
92+
profile.count = 1;
93+
94+
const auto* encodings = meta->encodings();
95+
const TypeEncoding* enc = encodings->first();
96+
const char* retName = EncodingToTypeName(enc->type);
97+
profile.returnType = retName ? retName : "?";
98+
99+
int paramCount = encodings->count - 1;
100+
for (int i = 0; i < paramCount; i++) {
101+
enc = enc->next();
102+
const char* argName = EncodingToTypeName(enc->type);
103+
profile.argTypes.push_back(argName ? argName : "?");
104+
}
105+
106+
sProfiles.emplace(key, std::move(profile));
107+
}
108+
109+
static std::vector<const MethodProfile*> GetSortedProfiles(int topN) {
110+
std::vector<const MethodProfile*> sorted;
111+
sorted.reserve(sProfiles.size());
112+
for (const auto& pair : sProfiles) {
113+
sorted.push_back(&pair.second);
114+
}
115+
std::sort(sorted.begin(), sorted.end(),
116+
[](const MethodProfile* a, const MethodProfile* b) { return a->count > b->count; });
117+
if (topN > 0 && (int)sorted.size() > topN) {
118+
sorted.resize(topN);
119+
}
120+
return sorted;
121+
}
122+
123+
void MethodCallProfiler::JSStart(const FunctionCallbackInfo<Value>& info) { Enable(); }
124+
125+
void MethodCallProfiler::JSStop(const FunctionCallbackInfo<Value>& info) { Disable(); }
126+
127+
void MethodCallProfiler::JSReset(const FunctionCallbackInfo<Value>& info) { Reset(); }
128+
129+
void MethodCallProfiler::JSReport(const FunctionCallbackInfo<Value>& info) {
130+
Isolate* isolate = info.GetIsolate();
131+
int topN = 50;
132+
if (info.Length() > 0 && info[0]->IsNumber()) {
133+
topN = (int)tns::ToNumber(isolate, info[0]);
134+
}
135+
136+
std::lock_guard<std::mutex> lock(sProfileMutex);
137+
auto sorted = GetSortedProfiles(topN);
138+
139+
std::ostringstream out;
140+
out << "Top " << sorted.size() << " method calls:\n";
141+
for (size_t i = 0; i < sorted.size(); i++) {
142+
const auto& p = *sorted[i];
143+
out << " " << (i + 1) << ". " << p.className << " -[" << p.selectorName << "] " << p.returnType
144+
<< "(";
145+
for (size_t j = 0; j < p.argTypes.size(); j++) {
146+
if (j > 0) out << ", ";
147+
out << p.argTypes[j];
148+
}
149+
out << ") — " << p.count << " calls\n";
150+
}
151+
152+
info.GetReturnValue().Set(tns::ToV8String(isolate, out.str()));
153+
}
154+
155+
void MethodCallProfiler::JSAOTConfig(const FunctionCallbackInfo<Value>& info) {
156+
Isolate* isolate = info.GetIsolate();
157+
int topN = 50;
158+
if (info.Length() > 0 && info[0]->IsNumber()) {
159+
topN = (int)tns::ToNumber(isolate, info[0]);
160+
}
161+
162+
std::lock_guard<std::mutex> lock(sProfileMutex);
163+
auto sorted = GetSortedProfiles(topN);
164+
165+
std::ostringstream out;
166+
out << "[\n";
167+
bool first = true;
168+
for (const auto* p : sorted) {
169+
bool hasUnknownType = (p->returnType == "?");
170+
for (const auto& a : p->argTypes) {
171+
if (a == "?") hasUnknownType = true;
172+
}
173+
if (hasUnknownType) continue;
174+
175+
if (!first) out << ",\n";
176+
first = false;
177+
178+
out << " { \"class\": \"" << p->className << "\", \"selector\": \"" << p->selectorName
179+
<< "\", \"ret\": \"" << p->returnType << "\", \"args\": [";
180+
for (size_t j = 0; j < p->argTypes.size(); j++) {
181+
if (j > 0) out << ", ";
182+
out << "\"" << p->argTypes[j] << "\"";
183+
}
184+
out << "] }";
185+
}
186+
out << "\n]";
187+
188+
info.GetReturnValue().Set(tns::ToV8String(isolate, out.str()));
189+
}
190+
191+
void MethodCallProfiler::RegisterJSAPI(Isolate* isolate, Local<ObjectTemplate> globalTemplate) {
192+
Local<ObjectTemplate> profiler = ObjectTemplate::New(isolate);
193+
profiler->Set(tns::ToV8String(isolate, "start"), FunctionTemplate::New(isolate, JSStart));
194+
profiler->Set(tns::ToV8String(isolate, "stop"), FunctionTemplate::New(isolate, JSStop));
195+
profiler->Set(tns::ToV8String(isolate, "reset"), FunctionTemplate::New(isolate, JSReset));
196+
profiler->Set(tns::ToV8String(isolate, "report"), FunctionTemplate::New(isolate, JSReport));
197+
profiler->Set(tns::ToV8String(isolate, "aotConfig"), FunctionTemplate::New(isolate, JSAOTConfig));
198+
globalTemplate->Set(tns::ToV8String(isolate, "__native_call_profiler"), profiler);
199+
}
200+
201+
} // namespace tns

NativeScript/runtime/Runtime.mm

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "Helpers.h"
99
#include "InlineFunctions.h"
1010
#include "Interop.h"
11+
#include "MethodCallProfiler.h"
1112
#include "NativeScriptException.h"
1213
#include "ObjectManager.h"
1314
#include "PromiseProxy.h"
@@ -22,15 +23,15 @@
2223
#include "DisposerPHV.h"
2324
#include "IsolateWrapper.h"
2425

26+
#include <mutex>
2527
#include <unordered_map>
28+
#include "DevFlags.h"
29+
#include "HMRSupport.h"
2630
#include "ModuleBinding.hpp"
2731
#include "ModuleInternalCallbacks.h"
2832
#include "URLImpl.h"
2933
#include "URLPatternImpl.h"
3034
#include "URLSearchParamsImpl.h"
31-
#include <mutex>
32-
#include "HMRSupport.h"
33-
#include "DevFlags.h"
3435

3536
#define STRINGIZE(x) #x
3637
#define STRINGIZE_VALUE_OF(x) STRINGIZE(x)
@@ -128,7 +129,7 @@ static void InitializeImportMetaObject(Local<Context> context, Local<Module> mod
128129
std::atomic<int> Runtime::nextIsolateId{0};
129130
SimpleAllocator allocator_;
130131
NSDictionary* AppPackageJson = nil;
131-
static std::unordered_map<std::string, id> AppConfigCache; // generic cache for app config values
132+
static std::unordered_map<std::string, id> AppConfigCache; // generic cache for app config values
132133
static std::mutex AppConfigCacheMutex;
133134

134135
// Global flag to track when JavaScript errors occur during execution
@@ -301,8 +302,8 @@ void DisposeIsolateWhenPossible(Isolate* isolate) {
301302
DefineDrainMicrotaskMethod(isolate, globalTemplate);
302303
// queueMicrotask(callback) per spec
303304
{
304-
Local<FunctionTemplate> qmtTemplate = FunctionTemplate::New(
305-
isolate, [](const FunctionCallbackInfo<Value>& info) {
305+
Local<FunctionTemplate> qmtTemplate =
306+
FunctionTemplate::New(isolate, [](const FunctionCallbackInfo<Value>& info) {
306307
auto* isolate = info.GetIsolate();
307308
if (info.Length() < 1 || !info[0]->IsFunction()) {
308309
isolate->ThrowException(Exception::TypeError(
@@ -316,6 +317,7 @@ void DisposeIsolateWhenPossible(Isolate* isolate) {
316317
}
317318
ObjectManager::Init(isolate, globalTemplate);
318319
// SetTimeout::Init(isolate, globalTemplate);
320+
MethodCallProfiler::RegisterJSAPI(isolate, globalTemplate);
319321
MetadataBuilder::RegisterConstantsOnGlobalObject(isolate, globalTemplate, isWorker);
320322

321323
isolate->SetCaptureStackTraceForUncaughtExceptions(true, 100, StackTrace::kOverview);
@@ -488,7 +490,8 @@ void DisposeIsolateWhenPossible(Isolate* isolate) {
488490
result = AppPackageJson[nsKey];
489491
}
490492

491-
// Store in cache (can cache nil as NSNull to differentiate presence if desired; for now, cache as-is)
493+
// Store in cache (can cache nil as NSNull to differentiate presence if desired; for now, cache
494+
// as-is)
492495
{
493496
std::lock_guard<std::mutex> lock(AppConfigCacheMutex);
494497
AppConfigCache[key] = result;

v8ios.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,8 @@
285285
C2DDEB90229EAC8300345BFE /* MetadataBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = C2DDEB67229EAC8100345BFE /* MetadataBuilder.h */; };
286286
CFCFC461946160EDD42B2C8B /* AOTDirectCalls.h in Headers */ = {isa = PBXBuildFile; fileRef = 5E7EEEF53D2575788F33B520 /* AOTDirectCalls.h */; };
287287
1465A61ACE30B5029013B847 /* AOTDirectCalls.mm in Sources */ = {isa = PBXBuildFile; fileRef = ADD755B4B7D89343B5900C60 /* AOTDirectCalls.mm */; };
288+
A1B2C3D4E5F60001PROFHDR /* MethodCallProfiler.h in Headers */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60003PROFHDR /* MethodCallProfiler.h */; };
289+
A1B2C3D4E5F60002PROFSRC /* MethodCallProfiler.mm in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60004PROFSRC /* MethodCallProfiler.mm */; };
288290
C2DDEB91229EAC8300345BFE /* FFICall.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C2DDEB68229EAC8100345BFE /* FFICall.cpp */; };
289291
C2DDEB92229EAC8300345BFE /* WeakRef.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C2DDEB69229EAC8100345BFE /* WeakRef.cpp */; };
290292
C2DDEB93229EAC8300345BFE /* ArgConverter.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2DDEB6A229EAC8200345BFE /* ArgConverter.mm */; };
@@ -793,6 +795,8 @@
793795
C2DDEB67229EAC8100345BFE /* MetadataBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MetadataBuilder.h; sourceTree = "<group>"; };
794796
5E7EEEF53D2575788F33B520 /* AOTDirectCalls.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AOTDirectCalls.h; sourceTree = "<group>"; };
795797
ADD755B4B7D89343B5900C60 /* AOTDirectCalls.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AOTDirectCalls.mm; sourceTree = "<group>"; };
798+
A1B2C3D4E5F60003PROFHDR /* MethodCallProfiler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MethodCallProfiler.h; sourceTree = "<group>"; };
799+
A1B2C3D4E5F60004PROFSRC /* MethodCallProfiler.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MethodCallProfiler.mm; sourceTree = "<group>"; };
796800
C2DDEB68229EAC8100345BFE /* FFICall.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = FFICall.cpp; sourceTree = "<group>"; };
797801
C2DDEB69229EAC8100345BFE /* WeakRef.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = WeakRef.cpp; sourceTree = "<group>"; };
798802
C2DDEB6A229EAC8200345BFE /* ArgConverter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ArgConverter.mm; sourceTree = "<group>"; };
@@ -1462,6 +1466,8 @@
14621466
C2DDEB71229EAC8200345BFE /* MetadataBuilder.mm */,
14631467
5E7EEEF53D2575788F33B520 /* AOTDirectCalls.h */,
14641468
ADD755B4B7D89343B5900C60 /* AOTDirectCalls.mm */,
1469+
A1B2C3D4E5F60003PROFHDR /* MethodCallProfiler.h */,
1470+
A1B2C3D4E5F60004PROFSRC /* MethodCallProfiler.mm */,
14651471
C2DDEB89229EAC8300345BFE /* ModuleInternal.h */,
14661472
AA8C47AF2E27114300649BF5 /* ModuleInternalCallbacks.h */,
14671473
AA8C47B02E27114300649BF5 /* ModuleInternalCallbacks.mm */,
@@ -1618,6 +1624,7 @@
16181624
C2DDEB9D229EAC8300345BFE /* SetTimeout.h in Headers */,
16191625
C2DDEB90229EAC8300345BFE /* MetadataBuilder.h in Headers */,
16201626
CFCFC461946160EDD42B2C8B /* AOTDirectCalls.h in Headers */,
1627+
A1B2C3D4E5F60001PROFHDR /* MethodCallProfiler.h in Headers */,
16211628
C266569F22B282BA00EE15CC /* FunctionReference.h in Headers */,
16221629
C2FEA17022A3C75C00A5C0FC /* InlineFunctions.h in Headers */,
16231630
C2DDEBA1229EAC8300345BFE /* FFICall.h in Headers */,
@@ -2250,6 +2257,7 @@
22502257
C2FEA16F22A3C75C00A5C0FC /* InlineFunctions.cpp in Sources */,
22512258
C2DDEB9A229EAC8300345BFE /* MetadataBuilder.mm in Sources */,
22522259
1465A61ACE30B5029013B847 /* AOTDirectCalls.mm in Sources */,
2260+
A1B2C3D4E5F60002PROFSRC /* MethodCallProfiler.mm in Sources */,
22532261
C266569722AFFFB000EE15CC /* Reference.cpp in Sources */,
22542262
C2DDEB95229EAC8300345BFE /* SetTimeout.cpp in Sources */,
22552263
6573B9EE291FE5B700B0ED7C /* JSIRuntime.m in Sources */,

0 commit comments

Comments
 (0)