From 13a86874dc71d3bf016425cd9d7e8da6def6c5bb Mon Sep 17 00:00:00 2001 From: Keerecles Date: Thu, 19 Jun 2025 09:44:44 +0800 Subject: [PATCH] profile tool Signed-off-by: Keerecles Change-Id: Ibcf8be531dbcb675011de78aeb8122b2e63d6969 --- arkui-plugins/common/debug.ts | 3 +- arkui-plugins/common/program-visitor.ts | 11 -- arkui-plugins/memo-plugins/index.ts | 18 +- arkui-plugins/test/package.json | 8 +- arkui-plugins/ui-plugins/index.ts | 22 ++- koala-wrapper/native/BUILD.gn | 1 + koala-wrapper/native/include/memoryTracker.h | 52 +++++ koala-wrapper/native/src/bridges.cc | 20 ++ koala-wrapper/native/src/memoryTracker.cc | 178 ++++++++++++++++++ koala-wrapper/src/Es2pandaNativeModule.ts | 12 ++ .../src/arkts-api/utilities/performance.ts | 123 +++++++++++- 11 files changed, 420 insertions(+), 28 deletions(-) create mode 100644 koala-wrapper/native/include/memoryTracker.h create mode 100644 koala-wrapper/native/src/memoryTracker.cc diff --git a/arkui-plugins/common/debug.ts b/arkui-plugins/common/debug.ts index f39940e8f..467572560 100644 --- a/arkui-plugins/common/debug.ts +++ b/arkui-plugins/common/debug.ts @@ -19,8 +19,9 @@ import * as arkts from '@koalaui/libarkts'; const isDebugLog: boolean = false; const isDebugDump: boolean = false; const isPerformance: boolean = false; +const enableMemoryTracker: boolean = false; arkts.Performance.getInstance().skip(!isPerformance); - +arkts.Performance.getInstance().enableMemoryTracker(enableMemoryTracker); export function getEnumName(enumType: any, value: number): string | undefined { return enumType[value]; } diff --git a/arkui-plugins/common/program-visitor.ts b/arkui-plugins/common/program-visitor.ts index 3196b403b..55ac1aad4 100644 --- a/arkui-plugins/common/program-visitor.ts +++ b/arkui-plugins/common/program-visitor.ts @@ -174,7 +174,6 @@ export class ProgramVisitor extends AbstractVisitor { const visited = new Set(); const queue: arkts.Program[] = programQueue; this.getLegacyModule(); - arkts.Performance.getInstance().createEvent(`${this.state}-external-source`); while (queue.length > 0) { const currProgram = queue.shift()!; if (visited.has(currProgram.peer) || currProgram.isASTLowered()) { @@ -197,16 +196,13 @@ export class ProgramVisitor extends AbstractVisitor { this.visitNextProgramInQueue(queue, visited, externalSource); } } - arkts.Performance.getInstance().stopEvent(`${this.state}-external-source`, false); } programVisitor(program: arkts.Program): arkts.Program { this.visitExternalSources(program, [program]); - arkts.Performance.getInstance().createEvent(`${this.state}-source`); let programScript = program.astNode; programScript = this.visitor(programScript, program, this.externalSourceName); - arkts.Performance.getInstance().stopEvent(`${this.state}-source`, false); const visitorsToReset = flattenVisitorsInHooks(this.hooks, this.state); visitorsToReset.forEach((visitor) => visitor.reset()); @@ -247,7 +243,6 @@ export class ProgramVisitor extends AbstractVisitor { } visitor(node: arkts.AstNode, program?: arkts.Program, externalSourceName?: string): arkts.EtsScript { - arkts.Performance.getInstance().createEvent(`${this.state}-${externalSourceName ?? 'SOURCE'}`); let hook: ProgramHookLifeCycle | undefined; let script: arkts.EtsScript = node as arkts.EtsScript; @@ -264,9 +259,7 @@ export class ProgramVisitor extends AbstractVisitor { } this.visitTransformer(transformer, script, externalSourceName, program); transformer.reset(); - arkts.Performance.getInstance().createEvent('set-all-parent'); arkts.setAllParents(script); - arkts.Performance.getInstance().stopEvent('set-all-parent', false); if (!transformer.isExternal) { debugDump( script.dumpSrc(), @@ -282,8 +275,6 @@ export class ProgramVisitor extends AbstractVisitor { // post-run visitors hook = isExternal ? this.hooks?.external : this.hooks?.source; this.postVisitor(hook, node, program, externalSourceName); - - arkts.Performance.getInstance().stopEvent(`${this.state}-${externalSourceName ?? 'SOURCE'}`, false); return script; } @@ -303,12 +294,10 @@ export class ProgramVisitor extends AbstractVisitor { externalSourceName?: string, program?: arkts.Program ): arkts.EtsScript { - arkts.Performance.getInstance().createEvent(transformer.constructor.name); transformer.isExternal = !!externalSourceName; transformer.externalSourceName = externalSourceName; transformer.program = program; const newScript = transformer.visitor(script) as arkts.EtsScript; - arkts.Performance.getInstance().stopEvent(transformer.constructor.name, false); return newScript; } } diff --git a/arkui-plugins/memo-plugins/index.ts b/arkui-plugins/memo-plugins/index.ts index 72c2b577d..243d8b85f 100644 --- a/arkui-plugins/memo-plugins/index.ts +++ b/arkui-plugins/memo-plugins/index.ts @@ -36,6 +36,8 @@ export function unmemoizeTransform(): Plugins { function checkedTransform(this: PluginContext): arkts.EtsScript | undefined { console.log('[MEMO PLUGIN] AFTER CHECKED ENTER'); + arkts.Performance.getInstance().memoryTrackerReset(); + arkts.Performance.getInstance().startMemRecord('Node:UIPlugin:Memo-AfterCheck'); const contextPtr = this.getContextPtr() ?? arkts.arktsGlobal.compilerContext?.peer; if (!!contextPtr) { let program = arkts.getOrUpdateGlobalContext(contextPtr).program; @@ -52,7 +54,7 @@ function checkedTransform(this: PluginContext): arkts.EtsScript | undefined { arkts.Performance.getInstance().createEvent('memo-checked'); program = checkedProgramVisit(program, this); script = program.astNode; - arkts.Performance.getInstance().stopEvent('memo-checked', false); + arkts.Performance.getInstance().stopEvent('memo-checked', true); debugLog('[AFTER MEMO SCRIPT] script: ', script.dumpSrc()); debugDump( script.dumpSrc(), @@ -61,14 +63,18 @@ function checkedTransform(this: PluginContext): arkts.EtsScript | undefined { cachePath, program.fileNameWithExtension ); + + arkts.Performance.getInstance().memoryTrackerGetDelta('UIPlugin:Memo-AfterCheck'); + arkts.Performance.getInstance().memoryTrackerReset(); + arkts.Performance.getInstance().stopMemRecord('Node:UIPlugin:Memo-AfterCheck'); + arkts.Performance.getInstance().startMemRecord('Node:ArkTS:Recheck'); arkts.Performance.getInstance().createEvent('memo-recheck'); arkts.recheckSubtree(script); - arkts.Performance.getInstance().stopEvent('memo-recheck', false); - arkts.Performance.getInstance().clearAllEvents(false); - arkts.Performance.getInstance().visualizeEvents(true); - arkts.Performance.getInstance().clearHistory(); - arkts.Performance.getInstance().clearTotalDuration(); + arkts.Performance.getInstance().stopEvent('memo-recheck', true); this.setArkTSAst(script); + arkts.Performance.getInstance().memoryTrackerGetDelta('ArkTS:Recheck'); + arkts.Performance.getInstance().stopMemRecord('Node:ArkTS:Recheck'); + arkts.Performance.getInstance().memoryTrackerPrintCurrent('UIPlugin:End'); console.log('[MEMO PLUGIN] AFTER CHECKED EXIT'); return script; } diff --git a/arkui-plugins/test/package.json b/arkui-plugins/test/package.json index 34fcd06f2..46947c865 100644 --- a/arkui-plugins/test/package.json +++ b/arkui-plugins/test/package.json @@ -1,7 +1,6 @@ { "name": "arkui-plugins-test", "version": "1.0.0", - "description": "", "private": true, "scripts": { "compile:ohos": "node $INIT_CWD/../../../../arkcompiler/ets_frontend/ets2panda/driver/build-system/dist/entry.js ./demo/hello_world/build_config.json", @@ -10,6 +9,13 @@ "clean:localtest": "rm -rf dist", "clean:test": "rm -rf generated", "clean:all": "npm run clean:localtest && npm run clean:test", + "mem": "rm -rf dist && node localtest_config.js && LD_LIBRARY_PATH=$INIT_CWD/../../../../out/sdk/ohos-sdk/linux/ets/ets1.2/build-tools/ets2panda/lib LD_BIND_NOW=1 /usr/local/valgrind-3.25.1/bin/valgrind --tool=massif --pages-as-heap=yes --heap=yes --alloc-fn='libes2panda_public.so*' --alloc-fn='*es2panda.node*' node $INIT_CWD/../../../../out/sdk/ohos-sdk/linux/ets/ets1.2/build-tools/driver/build-system/dist/entry.js ./demo/localtest/build_config.json", + "local": "rm -rf dist && node localtest_config.js && LD_LIBRARY_PATH=$INIT_CWD/../../../../out/sdk/ohos-sdk/linux/ets/ets1.2/build-tools/ets2panda/lib node $INIT_CWD/../../../../out/sdk/ohos-sdk/linux/ets/ets1.2/build-tools/driver/build-system/dist/entry.js ./demo/localtest/build_config.json", + "inspect": "rm -rf dist && node localtest_config.js && LD_LIBRARY_PATH=$INIT_CWD/../../../../out/sdk/ohos-sdk/linux/ets/ets1.2/build-tools/ets2panda/lib node --inspect-brk --expose_gc --no-turbo-inlining $INIT_CWD/../../../../out/sdk/ohos-sdk/linux/ets/ets1.2/build-tools/driver/build-system/dist/entry.js ./demo/localtest/build_config.json", + "clinic_mem": "rm -rf dist && node localtest_config.js && LD_LIBRARY_PATH=$INIT_CWD/../../../../out/sdk/ohos-sdk/linux/ets/ets1.2/build-tools/ets2panda/lib clinic heapprofiler --node-arguments='--heapsnapshot-on-signal' -- node $INIT_CWD/../../../../out/sdk/ohos-sdk/linux/ets/ets1.2/build-tools/driver/build-system/dist/entry.js ./demo/localtest/build_config.json", + "mem1": "rm -rf dist && node localtest_config.js && LD_LIBRARY_PATH=$INIT_CWD/../../../../out/sdk/ohos-sdk/linux/ets/ets1.2/build-tools/ets2panda/lib clinic flame --mem --collect-only -- node --expose_gc --heap-prof --trace-gc --track-heap-objects $INIT_CWD/../../../../out/sdk/ohos-sdk/linux/ets/ets1.2/build-tools/driver/build-system/dist/entry.js ./demo/localtest/build_config.json", + "mem2": "rm -rf dist && node localtest_config.js && LD_LIBRARY_PATH=$INIT_CWD/../../../../out/sdk/ohos-sdk/linux/ets/ets1.2/build-tools/ets2panda/lib clinic flame --mem-prof -- node --no-node-snapshot --no-deprecation $INIT_CWD/../../../../out/sdk/ohos-sdk/linux/ets/ets1.2/build-tools/driver/build-system/dist/entry.js ./demo/localtest/build_config.json", + "valgrind": "rm -rf dist && node localtest_config.js && npm run compile:plugins && LD_LIBRARY_PATH=$INIT_CWD/../../../../out/sdk/ohos-sdk/linux/ets/ets1.2/build-tools/ets2panda/lib LD_BIND_NOW=1 /usr/local/valgrind-3.25.1/bin/valgrind --tool=massif --pages-as-heap=yes --heap=yes --alloc-fn='libes2panda_public.so*' --alloc-fn='*es2panda.node*' node $INIT_CWD/../../../../out/sdk/ohos-sdk/linux/ets/ets1.2/build-tools/driver/build-system/dist/entry.js ./demo/localtest/build_config.json", "test": "npm run clean:all && npm run compile:plugins && cd .. && npm run test", "test:gdb": "npm run clean:all && npm run compile:plugins && cd .. && npm run test:gdb", "localtest": "rm -rf dist && node localtest_config.js && npm run compile:plugins && LD_LIBRARY_PATH=$INIT_CWD/../../../../out/sdk/ohos-sdk/linux/ets/ets1.2/build-tools/ets2panda/lib node $INIT_CWD/../../../../out/sdk/ohos-sdk/linux/ets/ets1.2/build-tools/driver/build-system/dist/entry.js ./demo/localtest/build_config.json", diff --git a/arkui-plugins/ui-plugins/index.ts b/arkui-plugins/ui-plugins/index.ts index 6f0abaaa8..6c780b2f6 100644 --- a/arkui-plugins/ui-plugins/index.ts +++ b/arkui-plugins/ui-plugins/index.ts @@ -35,6 +35,9 @@ export function uiTransform(): Plugins { function parsedTransform(this: PluginContext): arkts.EtsScript | undefined { let script: arkts.EtsScript | undefined; console.log('[UI PLUGIN] AFTER PARSED ENTER'); + arkts.Performance.getInstance().memoryTrackerPrintCurrent('ArkTS:Parse'); + arkts.Performance.getInstance().memoryTrackerReset(); + arkts.Performance.getInstance().startMemRecord('Node:UIPlugin:AfterParse'); const contextPtr = this.getContextPtr() ?? arkts.arktsGlobal.compilerContext?.peer; if (!!contextPtr) { let program = arkts.getOrUpdateGlobalContext(contextPtr).program; @@ -51,7 +54,7 @@ function parsedTransform(this: PluginContext): arkts.EtsScript | undefined { arkts.Performance.getInstance().createEvent('ui-parsed'); program = parsedProgramVisit(program, this); script = program.astNode; - arkts.Performance.getInstance().stopEvent('ui-parsed', false); + arkts.Performance.getInstance().stopEvent('ui-parsed', true); debugLog('[AFTER PARSED SCRIPT] script: ', script.dumpSrc()); debugDump( script.dumpSrc(), @@ -61,6 +64,9 @@ function parsedTransform(this: PluginContext): arkts.EtsScript | undefined { program.fileNameWithExtension ); this.setArkTSAst(script); + arkts.Performance.getInstance().memoryTrackerGetDelta('UIPlugin:AfterParse'); + arkts.Performance.getInstance().memoryTrackerReset(); + arkts.Performance.getInstance().stopMemRecord('Node:UIPlugin:AfterParse'); console.log('[UI PLUGIN] AFTER PARSED EXIT'); return script; } @@ -89,6 +95,10 @@ function parsedProgramVisit(program: arkts.Program, context: PluginContext): ark function checkedTransform(this: PluginContext): arkts.EtsScript | undefined { let script: arkts.EtsScript | undefined; console.log('[UI PLUGIN] AFTER CHECKED ENTER'); + arkts.Performance.getInstance().memoryTrackerPrintCurrent('ArkTS:Check'); + arkts.Performance.getInstance().memoryTrackerGetDelta('ArkTS:Check'); + arkts.Performance.getInstance().memoryTrackerReset(); + arkts.Performance.getInstance().startMemRecord('Node:UIPlugin:UI-AfterCheck'); const contextPtr = this.getContextPtr() ?? arkts.arktsGlobal.compilerContext?.peer; if (!!contextPtr) { let program = arkts.getOrUpdateGlobalContext(contextPtr).program; @@ -105,7 +115,7 @@ function checkedTransform(this: PluginContext): arkts.EtsScript | undefined { arkts.Performance.getInstance().createEvent('ui-checked'); program = checkedProgramVisit(program, this); script = program.astNode; - arkts.Performance.getInstance().stopEvent('ui-checked', false); + arkts.Performance.getInstance().stopEvent('ui-checked', true); debugLog('[AFTER STRUCT SCRIPT] script: ', script.dumpSrc()); debugDump( script.dumpSrc(), @@ -116,12 +126,10 @@ function checkedTransform(this: PluginContext): arkts.EtsScript | undefined { ); arkts.Performance.getInstance().createEvent('ui-recheck'); arkts.recheckSubtree(script); - arkts.Performance.getInstance().stopEvent('ui-recheck', false); - arkts.Performance.getInstance().clearAllEvents(false); - arkts.Performance.getInstance().visualizeEvents(true); - arkts.Performance.getInstance().clearHistory(); - arkts.Performance.getInstance().clearTotalDuration(); + arkts.Performance.getInstance().stopEvent('ui-recheck', true); this.setArkTSAst(script); + arkts.Performance.getInstance().memoryTrackerGetDelta('UIPlugin:UI-AfterCheck'); + arkts.Performance.getInstance().stopMemRecord('Node:UIPlugin:UI-AfterCheck'); console.log('[UI PLUGIN] AFTER CHECKED EXIT'); return script; } diff --git a/koala-wrapper/native/BUILD.gn b/koala-wrapper/native/BUILD.gn index 1f7e783fa..f3055944c 100644 --- a/koala-wrapper/native/BUILD.gn +++ b/koala-wrapper/native/BUILD.gn @@ -26,6 +26,7 @@ shared_library("es2panda") { "./src/bridges.cc", "./src/common.cc", "./src/generated/bridges.cc", + "./src/memoryTracker.cc" ] include_dirs = [ diff --git a/koala-wrapper/native/include/memoryTracker.h b/koala-wrapper/native/include/memoryTracker.h new file mode 100644 index 000000000..3f0dc71eb --- /dev/null +++ b/koala-wrapper/native/include/memoryTracker.h @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef KOALA_MEMORY_TRACKER +#define KOALA_MEMORY_TRACKER + +#include +#include + +// 内存统计结构体 +struct MemoryStats { + size_t currentRss = 0; // 当前驻留集大小 (字节) + size_t peakRss = 0; // 峰值驻留集大小 (字节) + size_t currentVss = 0; // 当前虚拟内存大小 (字节) + size_t pageFaultsMinor = 0; // 小页错误次数 + size_t pageFaultsMajor = 0; // 大页错误次数 +}; + +class MemoryTracker { +public: + MemoryTracker() + { + Reset(); + } + + void Reset(); + MemoryStats GetDelta(); + + template + MemoryStats MeasureMemory(Func&& func); + + void Report(MemoryStats stats); + +private: + MemoryStats baseline; +}; + +MemoryStats GetMemoryStats(); + +#endif diff --git a/koala-wrapper/native/src/bridges.cc b/koala-wrapper/native/src/bridges.cc index 440bb5ba4..0936041a4 100644 --- a/koala-wrapper/native/src/bridges.cc +++ b/koala-wrapper/native/src/bridges.cc @@ -18,6 +18,7 @@ #include #include #include +#include "memoryTracker.h" std::set globalStructInfo; std::mutex g_structMutex; @@ -657,3 +658,22 @@ void impl_LogDiagnosticWithSuggestion(KNativePointer context, KNativePointer dia GetImpl()->LogDiagnosticWithSuggestion(_context, _diagnosticInfo, _suggestionInfo, _range); } KOALA_INTEROP_V4(LogDiagnosticWithSuggestion, KNativePointer, KNativePointer, KNativePointer, KNativePointer); + +MemoryTracker tracker; +void impl_MemoryTrackerReset(KNativePointer context) +{ + tracker.Reset(); +} +KOALA_INTEROP_V1(MemoryTrackerReset, KNativePointer); + +void impl_MemoryTrackerGetDelta(KNativePointer context) +{ + tracker.Report(tracker.GetDelta()); +} +KOALA_INTEROP_V1(MemoryTrackerGetDelta, KNativePointer); + +void impl_MemoryTrackerPrintCurrent(KNativePointer context) +{ + tracker.Report(GetMemoryStats()); +} +KOALA_INTEROP_V1(MemoryTrackerPrintCurrent, KNativePointer); diff --git a/koala-wrapper/native/src/memoryTracker.cc b/koala-wrapper/native/src/memoryTracker.cc new file mode 100644 index 000000000..c12fcd248 --- /dev/null +++ b/koala-wrapper/native/src/memoryTracker.cc @@ -0,0 +1,178 @@ +/** + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "memoryTracker.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 + #include + #include +#elif defined(__APPLE__) + #include + #include +#elif defined(__linux__) + #include + #include +#endif + +constexpr const char* UNIT_K = "kB"; +constexpr size_t BYTES_PER_KB = 1024; +constexpr const char* MEMORY_STATUS_FILE = "/proc/self/status"; +const std::regex VM_RSS_REGEX(R"#(VmRSS:\s*(\d+)\s*([kKmMgG]?B))#", + std::regex_constants::ECMAScript | std::regex_constants::icase); +const std::regex VM_SIZE_REGEX(R"#(VmSize:\s*(\d+)\s*([kKmMgG]?B))#", + std::regex_constants::ECMAScript | std::regex_constants::icase); + +constexpr int MATCH_GROUP_VALUE = 1; +constexpr int MATCH_GROUP_UNIT = 2; +constexpr int MATCH_GROUP_SIZE = 3; + +#if defined(_WIN32) +MemoryStats GetMemoryStats() +{ + MemoryStats stats = {0, 0, 0, 0, 0}; + PROCESS_MEMORY_COUNTERS_EX pmc; + if (GetProcessMemoryInfo(GetCurrentProcess(), + reinterpret_cast(&pmc), sizeof(pmc))) { + stats.currentRss = pmc.WorkingSetSize; + stats.peakRss = pmc.PeakWorkingSetSize; + stats.currentVss = pmc.PrivateUsage; // 私有内存使用量 + stats.pageFaultsMinor = pmc.PageFaultCount; + // Windows API不直接提供主缺页错误计数 + stats.pageFaultsMajor = 0; + } + return stats; +} + +#elif defined(__APPLE__) +MemoryStats GetMemoryStats() +{ + MemoryStats stats = {0, 0, 0, 0, 0}; + struct rusage ru; + if (getrusage(RUSAGE_SELF, &ru) == 0) { + stats.currentRss = 0; // macOS需要专用API获取当前内存 + stats.peakRss = ru.ru_maxrss; // macOS返回字节 + stats.pageFaultsMinor = ru.ru_minflt; + stats.pageFaultsMajor = ru.ru_majflt; + + // 获取当前内存使用 (macOS专用API) + task_basic_info info; + mach_msg_type_number_t count = TASK_BASIC_INFO_64_COUNT; + if (task_info(mach_task_self(), TASK_BASIC_INFO_64, + (task_info_t)&info, &count) == KERN_SUCCESS) { + stats.currentRss = info.resident_size; // 物理内存使用量 + stats.currentVss = info.virtual_size; // 虚拟内存总量 + } + } + return stats; +} + +#elif defined(__linux__) +MemoryStats GetMemoryStats() +{ + MemoryStats stats = {0, 0, 0, 0, 0}; + struct rusage ru; + if (getrusage(RUSAGE_SELF, &ru) == 0) { + stats.peakRss = static_cast(ru.ru_maxrss) * BYTES_PER_KB; // KB -> 字节 + stats.pageFaultsMinor = ru.ru_minflt; + stats.pageFaultsMajor = ru.ru_majflt; + } + std::ifstream statusFile(MEMORY_STATUS_FILE); + if (!statusFile) { + return stats; + } + std::string line; + std::smatch matches; + while (std::getline(statusFile, line)) { + if (std::regex_match(line, matches, VM_RSS_REGEX) && matches.size() >= MATCH_GROUP_SIZE) { + stats.currentRss = std::stoull(matches[MATCH_GROUP_VALUE].str()); + std::string unit = matches[MATCH_GROUP_UNIT].str(); + if (unit == UNIT_K) { + stats.currentRss *= BYTES_PER_KB; + } + } else if (std::regex_match(line, matches, VM_SIZE_REGEX) && matches.size() >= MATCH_GROUP_SIZE) { + stats.currentVss = std::stoull(matches[MATCH_GROUP_VALUE].str()); + std::string unit = matches[MATCH_GROUP_UNIT].str(); + if (unit == UNIT_K) { + stats.currentVss *= BYTES_PER_KB; + } + } + } + return stats; +} +#endif + +void MemoryTracker::Reset() +{ + baseline = GetMemoryStats(); +} + +MemoryStats MemoryTracker::GetDelta() +{ + MemoryStats current = GetMemoryStats(); + MemoryStats delta = { + current.currentRss - baseline.currentRss, + current.peakRss - baseline.peakRss, + current.currentVss - baseline.currentVss, + current.pageFaultsMinor - baseline.pageFaultsMinor, + current.pageFaultsMajor - baseline.pageFaultsMajor + }; + return delta; +} + +template +MemoryStats MemoryTracker::MeasureMemory(Func&& func) +{ + Reset(); + auto preStats = GetMemoryStats(); + func(); + auto postStats = GetMemoryStats(); + + return { + postStats.currentRss - preStats.currentRss, + postStats.peakRss - preStats.peakRss, + postStats.currentVss - preStats.currentVss, + postStats.pageFaultsMinor - preStats.pageFaultsMinor, + postStats.pageFaultsMajor - preStats.pageFaultsMajor + }; +} + +void MemoryTracker::Report(MemoryStats stats) +{ + auto formatBytes = [](size_t bytes) -> std::string { + const double kb = BYTES_PER_KB; + const double mb = kb * BYTES_PER_KB; + const double gb = mb * BYTES_PER_KB; + + if (bytes > gb) return std::to_string(bytes / gb) + " GB"; + if (bytes > mb) return std::to_string(bytes / mb) + " MB"; + if (bytes > kb) return std::to_string(bytes / kb) + " KB"; + return std::to_string(bytes) + " B"; + }; + + std::cout << "Current RSS: " << formatBytes(stats.currentRss) << "\n"; + std::cout << "Peak RSS : " << formatBytes(stats.peakRss) << "\n"; + std::cout << "VSS : " << formatBytes(stats.currentVss) << "\n"; + std::cout << "FaultsMinor: " << stats.pageFaultsMinor << "\n"; + std::cout << "FaultsMajor: " << stats.pageFaultsMajor << "\n"; + return; +} \ No newline at end of file diff --git a/koala-wrapper/src/Es2pandaNativeModule.ts b/koala-wrapper/src/Es2pandaNativeModule.ts index 83a90fea2..9435c1a3f 100644 --- a/koala-wrapper/src/Es2pandaNativeModule.ts +++ b/koala-wrapper/src/Es2pandaNativeModule.ts @@ -934,6 +934,18 @@ export class Es2pandaNativeModule { _SetUpSoPath(soPath: string): void { throw new Error('Not implemented'); } + + _MemoryTrackerReset(context: KNativePointer): void { + throw new Error('CallExpressionIsTrailingCallConst was not overloaded by native module initialization'); + } + + _MemoryTrackerGetDelta(context: KNativePointer): void { + throw new Error('CallExpressionIsTrailingCallConst was not overloaded by native module initialization'); + } + + _MemoryTrackerPrintCurrent(context: KNativePointer): void { + throw new Error('CallExpressionIsTrailingCallConst was not overloaded by native module initialization'); + } } export function initEs2panda(): Es2pandaNativeModule { diff --git a/koala-wrapper/src/arkts-api/utilities/performance.ts b/koala-wrapper/src/arkts-api/utilities/performance.ts index c938de34c..9aaa414e8 100644 --- a/koala-wrapper/src/arkts-api/utilities/performance.ts +++ b/koala-wrapper/src/arkts-api/utilities/performance.ts @@ -13,6 +13,22 @@ * limitations under the License. */ +import * as process from 'process'; +import { global as localGlobal} from '../static/global'; + +const BYTES_PER_KIBIBYTE = 1024; + +interface MemoryContext { + startTime: number; + startMemory: { + rss: number; + heapTotal: number; + heapUsed: number; + external: number; + arrayBuffers: number; + }; +} + interface Event { name: string, startTime: number, @@ -46,12 +62,14 @@ export class Performance { private scopes: string[]; private shouldSkip: boolean; private totalDuration: number; - + private memoryContexts = new Map(); + private memoryTrackerEnable: boolean; private constructor() { this.events = new Map(); this.historyEvents = new Map(); this.scopes = []; this.shouldSkip = true; + this.memoryTrackerEnable = false; this.totalDuration = 0; } @@ -66,6 +84,10 @@ export class Performance { this.shouldSkip = shouldSkip; } + enableMemoryTracker(enableMemoryTracker: boolean = false): void { + this.memoryTrackerEnable = enableMemoryTracker; + } + createEvent(name: string): Event { if (this.shouldSkip) { return { name: '', startTime: 0 }; @@ -175,7 +197,7 @@ export class Performance { result += `${indent}- ${child.name}: ${formatTime(duration)}(${round(duration)}), ${count}\n`; result += _result; }); - + return [result, children.length]; } @@ -187,4 +209,101 @@ export class Performance { console.log(`[PERFORMANCE] ===== FINAL RESULT ====`); } } + + startMemRecord(label: string = `measurement-${Date.now()}`): void { + // 强制进行垃圾回收(需要 Node.js 启动时添加 --expose-gc 参数) + if (!this.memoryTrackerEnable) { + return; + } + if (global.gc) { + (global as any).gc(); + } + const startMemory = process.memoryUsage(); + this.memoryContexts.set(label, { + startTime: Date.now(), + startMemory: { + rss: startMemory.rss / (BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE), + heapTotal: startMemory.heapTotal / (BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE), + heapUsed: startMemory.heapUsed / (BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE), + external: startMemory.external / (BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE), + arrayBuffers: (startMemory.arrayBuffers || 0) / (BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE) + } + }); + + return; + } + + stopMemRecord(label: string = `measurement-${Date.now()}`, runGc: boolean = false): void { + if (!this.memoryTrackerEnable) { + return; + } + const context = this.memoryContexts.get(label); + + if (!context) { + console.error(`未找到标签为 "${label}" 的内存测量上下文`); + return; + } + + // 可选:在测量结束前执行垃圾回收 + if (runGc && global.gc) { + (global as any).gc(); + } + + // 记录结束时的内存使用情况 + const endTime = Date.now(); + const endMemory = process.memoryUsage(); + + // 计算内存使用增量 + const memoryDiff = { + rss: endMemory.rss / (BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE) - context.startMemory.rss, + heapTotal: endMemory.heapTotal / (BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE) - context.startMemory.heapTotal, + heapUsed: endMemory.heapUsed / (BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE) - context.startMemory.heapUsed, + external: endMemory.external / (BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE) - context.startMemory.external, + arrayBuffers: ((endMemory.arrayBuffers || 0) / (BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE)) - context.startMemory.arrayBuffers + }; + const duration = endTime - context.startTime; + + console.log('[PERFORMANCE]', `内存测量结果 [标签: ${label}]`); + console.log('[PERFORMANCE]', `执行时间: ${duration}ms`); + console.log('---------------------------------------------------------------'); + console.log('[PERFORMANCE]', `内存类型 | 开始值(MB) | 结束值(MB) | 增量(MB)`); + console.log('---------------------------------------------------------------'); + console.log('[PERFORMANCE]', `RSS | ${context.startMemory.rss.toFixed(2)} | ${(endMemory.rss / (BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE)).toFixed(2)} | ${memoryDiff.rss.toFixed(2)}`); + console.log('[PERFORMANCE]', `Heap Total | ${context.startMemory.heapTotal.toFixed(2)} | ${(endMemory.heapTotal / (BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE)).toFixed(2)} | ${memoryDiff.heapTotal.toFixed(2)}`); + console.log('[PERFORMANCE]', `Heap Used | ${context.startMemory.heapUsed.toFixed(2)} | ${(endMemory.heapUsed / (BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE)).toFixed(2)} | ${memoryDiff.heapUsed.toFixed(2)}`); + console.log('[PERFORMANCE]', `External | ${context.startMemory.external.toFixed(2)} | ${(endMemory.external / (BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE)).toFixed(2)} | ${memoryDiff.external.toFixed(2)}`); + if (endMemory.arrayBuffers !== undefined) { + console.log(`Array Buffers | ${context.startMemory.arrayBuffers.toFixed(2)} | ${((endMemory.arrayBuffers || 0) / (BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE)).toFixed(2)} | ${memoryDiff.arrayBuffers.toFixed(2)}`); + } + console.log('---------------------------------------------------------------'); + this.memoryContexts.delete(label); + return; + } + + memoryTrackerReset(): void { + if (!this.memoryTrackerEnable) { + return; + } + localGlobal.es2panda._MemoryTrackerReset(localGlobal.context); + } + + memoryTrackerGetDelta(tag: string): void { + if (!this.memoryTrackerEnable) { + return; + } + console.log('---------------------------------------------------------------'); + console.log('[PERFORMANCE] Increamental memory:', tag); + localGlobal.es2panda._MemoryTrackerGetDelta(localGlobal.context); + console.log('---------------------------------------------------------------'); + } + + memoryTrackerPrintCurrent(tag: string): void { + if (!this.memoryTrackerEnable) { + return; + } + console.log('---------------------------------------------------------------'); + console.log('[PERFORMANCE] Current total memory:', tag); + localGlobal.es2panda._MemoryTrackerPrintCurrent(localGlobal.context); + console.log('---------------------------------------------------------------'); + } } \ No newline at end of file -- Gitee