已合并
add hybrid binary dump and offline snapshot merger #14820
add hybrid binary dump and offline snapshot merger #14820
已合并
yangxiaoshuai2022创建于 6月18日
47 个文件变更+6441-232
MBUILD.gn+2-0
@@ -1374,6 +1374,7 @@ if (!is_mingw && !is_mac && target_os != "ios") {
1374 "ecmascript/dfx/cpu_profiler/sampling_processor.cpp",1374 "ecmascript/dfx/cpu_profiler/sampling_processor.cpp",
1375 "ecmascript/dfx/cpu_profiler/samples_record.cpp",1375 "ecmascript/dfx/cpu_profiler/samples_record.cpp",
1376 "ecmascript/dfx/hprof/file_stream.cpp",1376 "ecmascript/dfx/hprof/file_stream.cpp",
1377+ "ecmascript/dfx/hprof/heap_dump_session.cpp",
1377 "ecmascript/dfx/hprof/heap_marker.cpp",1378 "ecmascript/dfx/hprof/heap_marker.cpp",
1378 "ecmascript/dfx/hprof/heap_profiler.cpp",1379 "ecmascript/dfx/hprof/heap_profiler.cpp",
1379 "ecmascript/dfx/hprof/heap_profiler_interface.cpp",1380 "ecmascript/dfx/hprof/heap_profiler_interface.cpp",
@@ -1390,6 +1391,7 @@ if (!is_mingw && !is_mac && target_os != "ios") {
1390 ]1391 ]
1391 if (ark_js_hybrid) {1392 if (ark_js_hybrid) {
1392 ecma_profiler_source += [1393 ecma_profiler_source += [
1394+ "ecmascript/dfx/hprof/dynamic_dump.cpp",
1393 "ecmascript/dfx/hprof/hybrid/hybrid_heap_profiler.cpp",1395 "ecmascript/dfx/hprof/hybrid/hybrid_heap_profiler.cpp",
1394 "ecmascript/dfx/hprof/hybrid/hybrid_heap_snapshot.cpp",1396 "ecmascript/dfx/hprof/hybrid/hybrid_heap_snapshot.cpp",
1395 ]1397 ]
Mbundle.json+9-0
@@ -101,6 +101,15 @@
101 "header_base": "//arkcompiler/ets_runtime/ecmascript/napi/include"101 "header_base": "//arkcompiler/ets_runtime/ecmascript/napi/include"
102 }102 }
103 },103 },
104+ {
105+ "name": "//arkcompiler/ets_runtime/ecmascript/dfx/hprof/rawheap_translate:rawheap_translate_static",
106+ "header": {
107+ "header_files": [
108+ "rawheap_translate.h"
109+ ],
110+ "header_base": "//arkcompiler/ets_runtime/ecmascript/dfx/hprof/rawheap_translate"
111+ }
112+ },
104 {113 {
105 "name": "//arkcompiler/ets_runtime/compiler_service:libcompiler_service",114 "name": "//arkcompiler/ets_runtime/compiler_service:libcompiler_service",
106 "header": {115 "header": {
Mecmascript/cross_vm/cross_vm_operator.cpp+14-1
@@ -19,6 +19,9 @@
19#include "ecmascript/cross_vm/unified_gc/unified_gc_marker.h"19#include "ecmascript/cross_vm/unified_gc/unified_gc_marker.h"
20#include "ecmascript/cross_vm/heap_hybrid-inl.h"20#include "ecmascript/cross_vm/heap_hybrid-inl.h"
21#include "ecmascript/cross_vm/unified_gc/unified_gc.h"21#include "ecmascript/cross_vm/unified_gc/unified_gc.h"
22+#if defined(ECMASCRIPT_SUPPORT_HEAPPROFILER)
23+#include "ecmascript/dfx/hprof/dynamic_dump.h"
24+#endif
22#include "ecmascript/ecma_string.h"25#include "ecmascript/ecma_string.h"
23#include "ecmascript/ecma_vm.h"26#include "ecmascript/ecma_vm.h"
24#include "ecmascript/interpreter/frame_handler.h"27#include "ecmascript/interpreter/frame_handler.h"
@@ -249,4 +252,14 @@ const void *CrossVMOperator::EcmaVMInterfaceImpl::GetEcmaVM() const
249{252{
250 return static_cast<const void *>(vm_);253 return static_cast<const void *>(vm_);
251}254}
252-} // namespace panda::ecmascript255+ 
256+std::unique_ptr<AbstractDumper> CrossVMOperator::EcmaVMInterfaceImpl::CreateHeapDumper(const DumpRequest &request)
257+{
258+#if defined(ECMASCRIPT_SUPPORT_HEAPPROFILER)
259+ return DynamicDump::Create(vm_, request);
260+#else
261+ (void)request;
262+ return nullptr;
263+#endif
264+}
265+} // namespace panda::ecmascript
Mecmascript/cross_vm/cross_vm_operator.h+6-1
@@ -23,9 +23,13 @@
23#include "hybrid/ecma_vm_interface.h"23#include "hybrid/ecma_vm_interface.h"
24#include "hybrid/hybrid_frame_info.h"24#include "hybrid/hybrid_frame_info.h"
25#include "hybrid/sts_vm_interface.h"25#include "hybrid/sts_vm_interface.h"
26+#include "profiler/heap_dump.h"
26 27 
27namespace panda::ecmascript {28namespace panda::ecmascript {
28 29 
30+using common::dump::AbstractDumper;
31+using common::dump::DumpRequest;
32+ 
29using JSTaggedType = uint64_t;33using JSTaggedType = uint64_t;
30class EcmaVM;34class EcmaVM;
31 35 
@@ -74,6 +78,7 @@ private:
74 bool ForEachDynamicFrame(void *currFrameSP, void *toFrameSP,78 bool ForEachDynamicFrame(void *currFrameSP, void *toFrameSP,
75 const std::function<void(const void *)> &cb) const override;79 const std::function<void(const void *)> &cb) const override;
76 const void *GetEcmaVM() const override;80 const void *GetEcmaVM() const override;
81+ std::unique_ptr<AbstractDumper> CreateHeapDumper(const DumpRequest &request) override;
77 82 
78 private:83 private:
79 [[maybe_unused]] EcmaVM *vm_ {nullptr};84 [[maybe_unused]] EcmaVM *vm_ {nullptr};
@@ -86,4 +91,4 @@ private:
86 91 
87} // namespace panda::ecmascript92} // namespace panda::ecmascript
88 93 
89-#endif // ETS_RUNTIME_ECMASCRIPT_CROSS_VM_CROSS_VM_OPERATOR_H94+#endif // ETS_RUNTIME_ECMASCRIPT_CROSS_VM_CROSS_VM_OPERATOR_H
Mecmascript/dfx/hprof/AGENTS.md+7-1
@@ -71,7 +71,13 @@ Tests are organized into multiple test suites in `tests/BUILD.gn`:
71- **HeapTrackerFirstTest/SecondTest/ThirdTest**71- **HeapTrackerFirstTest/SecondTest/ThirdTest**
72- **HeapSamplingTest**72- **HeapSamplingTest**
73- **HProfTest**73- **HProfTest**
74-- **RawHeapTranslateTest**74+- **RawHeapTranslateTest** - CLI entry and metadata parsing tests
75+- **RawHeapStaticSnapshotTest** - static binary snapshot format tests
76+ (wire-format parsing, class/instance/array records, merge, xref)
77+- **RawHeapFormatContractTest** - static assertions bridging common.h
78+ and dump_format.h format constants (only built when ark_js_hybrid=true)
79+- **HybridHeapSnapshotTest** - hybrid heap snapshot construction and
80+ serialization tests (mock STS, entry ID map, XRef edge emission)
75- **JSMetadataTest**81- **JSMetadataTest**
76- **LocalHandleLeakDetectTest** / **GlobalHandleLeakDetectTest**82- **LocalHandleLeakDetectTest** / **GlobalHandleLeakDetectTest**
77 83 
Aecmascript/dfx/hprof/dynamic_dump.cpp+376-0
@@ -0,0 +1,376 @@
1+/*
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS of ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+#include "ecmascript/dfx/hprof/dynamic_dump.h"
17+#include "ecmascript/dfx/hprof/file_stream.h"
18+#include "common_components/heap/heap.h"
19+#include "ecmascript/base/config.h"
20+#include "ecmascript/checkpoint/thread_state_transition.h"
21+#include "ecmascript/runtime_lock.h"
22+#if defined(ENABLE_DUMP_IN_FAULTLOG)
23+#include "faultloggerd_client.h"
24+#endif
25+ 
26+#include <unistd.h>
27+ 
28+#include "ecmascript/ecma_vm.h"
29+#include "ecmascript/js_thread.h"
30+#include "ecmascript/mem/heap.h"
31+#include "ecmascript/mem/shared_heap/shared_concurrent_sweeper.h"
32+#include "ecmascript/runtime.h"
33+ 
34+namespace panda::ecmascript {
35+ 
36+using common::dump::DumpExecutionMode;
37+using common::dump::DumpIdentity;
38+using common::dump::DumpReason;
39+using common::dump::DumpScope;
40+ 
41+class DynamicDump::RuntimeScope final {
42+public:
43+ RuntimeScope(EcmaVM *vm, bool runtimeAlreadySuspended, bool isProcessDump) : ownerPid_(getpid())
44+ {
45+ if (!runtimeAlreadySuspended) {
46+ SuspendRuntime();
47+ }
48+ PrepareHeaps(vm, isProcessDump, runtimeAlreadySuspended);
49+ }
50+ 
51+ ~RuntimeScope()
52+ {
53+ if (getpid() != ownerPid_) {
54+ // The child inherited guards for parent-owned runtime state.
55+ // Do not resume threads or unlock copied synchronization objects.
56+ (void)externalSuspendGuard_.release();
57+ (void)suspendGuard_.release();
58+ (void)externalRuntimeLock_.release();
59+ (void)runtimeLock_.release();
60+ (void)managedScope_.release();
61+ return;
62+ }
63+ 
64+ externalSuspendGuard_.reset();
65+ suspendGuard_.reset();
66+ externalRuntimeLock_.reset();
67+ runtimeLock_.reset();
68+ managedScope_.reset();
69+ }
70+ 
71+private:
72+ void SuspendRuntime()
73+ {
74+ JSThread *current = JSThread::GetCurrent();
75+ auto &suspensionMutex = SharedHeap::GetInstance()->GetSuspensionRequestMutex();
76+ if (current == nullptr) {
77+ externalRuntimeLock_ = std::make_unique<LockHolder>(suspensionMutex);
78+ externalSuspendGuard_ = std::make_unique<SuspendAllScopeFromExternal>(nullptr);
79+ return;
80+ }
81+ 
82+ managedScope_ = std::make_unique<ThreadManagedScope<JSThread>>(current);
83+ runtimeLock_ = std::make_unique<RuntimeLockHolder>(current, suspensionMutex);
84+ suspendGuard_ = std::make_unique<SuspendAllScope<JSThread>>(current);
85+ }
86+ 
87+ static void PrepareHeaps(EcmaVM *vm, bool isProcessDump, bool fromSharedGC)
88+ {
89+ if (g_isEnableCMCGC) {
90+ common::Heap::GetHeap().WaitForGCFinish();
91+ return;
92+ }
93+ 
94+ auto prepareLocalHeaps = [vm, isProcessDump]() {
95+ if (!isProcessDump) {
96+ vm->GetHeap()->Prepare();
97+ return;
98+ }
99+ Runtime::GetInstance()->GCIterateThreadList([](JSThread *jsThread) {
100+ const_cast<Heap *>(jsThread->GetEcmaVM()->GetHeap())->Prepare();
101+ });
102+ };
103+ auto prepareSharedHeap = [vm, fromSharedGC]() {
104+ if (fromSharedGC) {
105+ SharedHeap::GetInstance()->PrepareByJSThread(vm->GetAssociatedJSThread(), true);
106+ return;
107+ }
108+ JSThread *current = JSThread::GetCurrent();
109+ if (current == nullptr) {
110+ SharedHeap::GetInstance()->Prepare(true);
111+ return;
112+ }
113+ SharedHeap::GetInstance()->PrepareByJSThread(current, true);
114+ };
115+ 
116+ if (fromSharedGC) {
117+ // The shared-GC OOM route reaches this scope while the shared heap
118+ // owns the stop-the-world state, so preserve its preparation order.
119+ prepareSharedHeap();
120+ prepareLocalHeaps();
121+ } else {
122+ prepareLocalHeaps();
123+ prepareSharedHeap();
124+ }
125+ Runtime::GetInstance()->GCIterateThreadList([](JSThread *jsThread) {
126+ ASSERT(jsThread->IsSuspended() || jsThread->HasLaunchedSuspendAll());
127+ const_cast<Heap *>(jsThread->GetEcmaVM()->GetHeap())->FillBumpPointerForTlab();
128+ ASSERT(!jsThread->IsConcurrentCopying());
129+ });
130+ }
131+ 
132+ pid_t ownerPid_ {-1};
133+ std::unique_ptr<ThreadManagedScope<JSThread>> managedScope_;
134+ std::unique_ptr<RuntimeLockHolder> runtimeLock_;
135+ std::unique_ptr<LockHolder> externalRuntimeLock_;
136+ std::unique_ptr<SuspendAllScope<JSThread>> suspendGuard_;
137+ std::unique_ptr<SuspendAllScopeFromExternal> externalSuspendGuard_;
138+};
139+ 
140+class DynamicDump::CrossThreadExecutionScope final {
141+public:
142+ explicit CrossThreadExecutionScope(JSThread *thread) : thread_(thread)
143+ {
144+ ASSERT(thread_ != nullptr);
145+ enabledByScope_ = thread_->CheckMultiThread();
146+ if (enabledByScope_) {
147+ thread_->SetCrossThreadExecution(true);
148+ }
149+ }
150+ 
151+ ~CrossThreadExecutionScope()
152+ {
153+ if (enabledByScope_) {
154+ thread_->SetCrossThreadExecution(false);
155+ }
156+ }
157+ 
158+ NO_COPY_SEMANTIC(CrossThreadExecutionScope);
159+ NO_MOVE_SEMANTIC(CrossThreadExecutionScope);
160+ 
161+private:
162+ JSThread *thread_;
163+ bool enabledByScope_ {false};
164+};
165+ 
166+DynamicDump::DynamicDump(EcmaVM *vm, const DumpRequest &request)
167+ : vm_(vm), request_(request)
168+{
169+}
170+ 
171+bool DynamicDump::IsDynamicOOM() const
172+{
173+ return request_.reason == DumpReason::DYNAMIC_LOCAL_OOM ||
174+ request_.reason == DumpReason::DYNAMIC_SHARED_OOM ||
175+ request_.reason == DumpReason::DYNAMIC_SHARED_GC_OOM;
176+}
177+ 
178+std::unique_ptr<AbstractDumper> DynamicDump::Create(EcmaVM *vm, const DumpRequest &request)
179+{
180+ if (vm == nullptr) {
181+ LOG_ECMA(ERROR) << "[HybDump][Dyn] Dumper creation failed: VM unavailable";
182+ return nullptr;
183+ }
184+ auto dumper = std::make_unique<DynamicDump>(vm, request);
185+ LOG_ECMA(INFO) << "[HybDump][Dyn] Dumper created";
186+ return dumper;
187+}
188+ 
189+DynamicDump::~DynamicDump()
190+{
191+ // Resume the dynamic runtime before releasing any dump resources.
192+ runtimeScope_.reset();
193+ // Order: rawHeapDump (uses stream) -> close fd -> snapshot/stringTable.
194+ delete rawHeapDump_;
195+ rawHeapDump_ = nullptr;
196+ fdStream_.reset();
197+ snapshot_.reset();
198+ stringTable_.reset();
199+ if (heapProfilerOwnership_ == HeapProfilerOwnership::STANDALONE) {
200+ HeapProfilerInterface::DestroyInstance(heapProfiler_);
201+ } else if (heapProfilerOwnership_ == HeapProfilerOwnership::VM) {
202+ HeapProfilerInterface::Destroy(vm_);
203+ }
204+ heapProfiler_ = nullptr;
205+ LOG_ECMA(INFO) << "[HybDump][Dyn] Dumper destroyed";
206+}
207+ 
208+void DynamicDump::CompleteCrossRuntimeGC()
209+{
210+ // XGC resumes every local heap independently and may leave asynchronous
211+ // reclamation tasks running. Wait for all of them before Shared GC consumes
212+ // local-to-shared remembered sets; otherwise it can read a slot while its
213+ // local object is being converted to a FreeObject.
214+ Runtime::GetInstance()->GCIterateThreadList([](JSThread *jsThread) {
215+ const_cast<Heap *>(jsThread->GetEcmaVM()->GetHeap())->WaitClearTaskFinished();
216+ });
217+}
218+ 
219+void DynamicDump::TriggerGC()
220+{
221+ JSThread *thread = JSThread::GetCurrent();
222+ if (thread == nullptr || thread != vm_->GetAssociatedJSThread()) {
223+ LOG_ECMA(ERROR) << "[HybDump][Dyn] GC failed: current thread does not own target VM";
224+ return;
225+ }
226+ ThreadManagedScope<JSThread> managedScope(thread);
227+ if (g_isEnableCMCGC) {
228+ common::BaseRuntime::RequestGC(common::GC_REASON_BACKUP, false, common::GC_TYPE_FULL);
229+ return;
230+ }
231+ vm_->GetHeap()->CollectGarbage(TriggerGCType::FULL_GC);
232+ SharedHeap *sHeap = SharedHeap::GetInstance();
233+ sHeap->CollectGarbage<TriggerGCType::SHARED_GC, GCReason::OTHER>(thread);
234+ sHeap->GetSweeper()->WaitAllTaskFinished();
235+}
236+ 
237+void DynamicDump::PrepareSession()
238+{
239+ if (runtimeScope_ != nullptr) {
240+ return;
241+ }
242+ LOG_ECMA(INFO) << "[HybDump][Dyn] Session prepare begin";
243+ runtimeScope_ = std::make_unique<RuntimeScope>(vm_, request_.reason == DumpReason::DYNAMIC_SHARED_GC_OOM,
244+ request_.policy.scope == DumpScope::PROCESS);
245+ if (!InitializeHeapProfiler()) {
246+ LOG_ECMA(ERROR) << "[HybDump][Dyn] Session prepare failed: heap profiler unavailable";
247+ return;
248+ }
249+ LOG_ECMA(INFO) << "[HybDump][Dyn] Session prepare end";
250+}
251+ 
252+bool DynamicDump::InitializeHeapProfiler()
253+{
254+ if (heapProfiler_ != nullptr) {
255+ return true;
256+ }
257+ if (request_.reason == DumpReason::DYNAMIC_SHARED_GC_OOM) {
258+ heapProfiler_ = static_cast<HeapProfiler *>(HeapProfilerInterface::CreateNewInstance(vm_));
259+ if (heapProfiler_ != nullptr) {
260+ heapProfilerOwnership_ = HeapProfilerOwnership::STANDALONE;
261+ }
262+ } else {
263+ if (IsDynamicOOM() && vm_->GetHeapProfile() != nullptr) {
264+ LOG_ECMA(ERROR) << "[HybDump][Dyn] Heap profiler creation failed: already active";
265+ return false;
266+ }
267+ bool createHeapProfiler = vm_->GetHeapProfile() == nullptr;
268+ heapProfiler_ = static_cast<HeapProfiler *>(HeapProfilerInterface::GetInstance(vm_));
269+ if (createHeapProfiler && heapProfiler_ != nullptr) {
270+ heapProfilerOwnership_ = HeapProfilerOwnership::VM;
271+ }
272+ }
273+ return heapProfiler_ != nullptr;
274+}
275+ 
276+bool DynamicDump::AcquireOutput()
277+{
278+ if (fdStream_ != nullptr) {
279+ return true;
280+ }
281+ if (!request_.identity.IsValid()) {
282+ LOG_ECMA(ERROR) << "[HybDump][Dyn] Output fd acquire failed: invalid dump identity";
283+ return false;
284+ }
285+ LOG_ECMA(INFO) << "[HybDump][Dyn] Output fd acquire begin";
286+#if defined(ENABLE_DUMP_IN_FAULTLOG)
287+ FaultLoggerdRequest fdRequest = {};
288+ fdRequest.type = static_cast<int32_t>(FaultLoggerType::JS_RAW_SNAPSHOT);
289+ fdRequest.pid = request_.identity.GetPid();
290+ fdRequest.tid = request_.policy.scope == DumpScope::PROCESS ? DumpIdentity::UNSPECIFIED_ID
291+ : request_.identity.GetTid();
292+ fdRequest.time = request_.identity.GetTimestampMillis();
293+ int fd = RequestFileDescriptorEx(&fdRequest);
294+#else
295+ int fd = -1;
296+#endif
297+ if (fd < 0) {
298+ LOG_ECMA(ERROR) << "[HybDump][Dyn] Output fd acquire failed: faultlogger request failed";
299+ return false;
300+ }
301+ LOG_ECMA(INFO) << "[HybDump][Dyn] Output fd acquired: fd=" << fd;
302+ fdStream_ = std::make_unique<FileDescriptorStream>(fd);
303+ return true;
304+}
305+ 
306+DumpSnapShotOption DynamicDump::CreateDumpOption() const
307+{
308+ DumpSnapShotOption option;
309+ bool isDynamicOOM = IsDynamicOOM();
310+ option.dumpFormat = DumpFormat::BINARY;
311+ option.isFullGC = false;
312+ option.isSimplify = isDynamicOOM;
313+ option.isSync = request_.policy.executionMode == DumpExecutionMode::IN_PROCESS;
314+ option.isBeforeFill = false;
315+ option.isDumpOOM = isDynamicOOM;
316+ option.isForSharedOOM = request_.reason == DumpReason::DYNAMIC_SHARED_OOM ||
317+ request_.reason == DumpReason::DYNAMIC_SHARED_GC_OOM;
318+ option.isProcDump = request_.policy.scope == DumpScope::PROCESS;
319+ // DynamicDump is only used by the hybrid coordinator. OOM rawheap IDs must
320+ // therefore be address-resolvable for the static-side XRef records.
321+ option.isForHybridXRef = isDynamicOOM;
322+ option.spaceType = request_.oom.spaceType;
323+ option.heapType = request_.oom.heapType;
324+ return option;
325+}
326+ 
327+bool DynamicDump::CreateRawHeapDump()
328+{
329+ if (rawHeapDump_ != nullptr) {
330+ return true;
331+ }
332+ if (heapProfiler_ == nullptr) {
333+ LOG_ECMA(ERROR) << "[HybDump][Dyn] Raw heap dumper creation failed: heap profiler unavailable";
334+ return false;
335+ }
336+ if (fdStream_ == nullptr) {
337+ LOG_ECMA(ERROR) << "[HybDump][Dyn] Raw heap dumper creation failed: output unavailable";
338+ return false;
339+ }
340+ 
341+ EntryIdMap *entryIdMap = heapProfiler_->GetEntryIdMap();
342+ DumpSnapShotOption option = CreateDumpOption();
343+ stringTable_ = std::make_unique<StringHashMap>(vm_);
344+ snapshot_ = std::make_unique<HeapSnapshot>(vm_, stringTable_.get(), option, false, entryIdMap);
345+ RawHeapDumpCropLevel cropLevel = Runtime::GetInstance()->GetRawHeapDumpCropLevel();
346+ if (cropLevel == RawHeapDumpCropLevel::LEVEL_V2) {
347+ rawHeapDump_ = new RawHeapDumpV2(vm_, fdStream_.get(), snapshot_.get(), entryIdMap, option);
348+ } else {
349+ rawHeapDump_ = new RawHeapDumpV1(vm_, fdStream_.get(), snapshot_.get(), entryIdMap, option);
350+ }
351+ return true;
352+}
353+ 
354+bool DynamicDump::Execute()
355+{
356+ if (rawHeapDump_ == nullptr) {
357+ LOG_ECMA(ERROR) << "[HybDump][Dyn] Dump failed: output is not open";
358+ return false;
359+ }
360+ LOG_ECMA(INFO) << "[HybDump][Dyn] Dump begin";
361+ 
362+ rawHeapDump_->BinaryDump();
363+ LOG_ECMA(INFO) << "[HybDump][Dyn] Dump end: success=true, objects=" << rawHeapDump_->GetObjectCount();
364+ return true;
365+}
366+ 
367+DumpResult DynamicDump::Dump()
368+{
369+ CrossThreadExecutionScope executionScope(vm_->GetAssociatedJSThread());
370+ if (!AcquireOutput() || !CreateRawHeapDump()) {
371+ return {{0, 0}, false};
372+ }
373+ return {{0, 0}, Execute()};
374+}
375+ 
376+} // namespace panda::ecmascript
Aecmascript/dfx/hprof/dynamic_dump.h+126-0
@@ -0,0 +1,126 @@
1+/*
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS of ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+#ifndef PANDA_ECMASCRIPT_DFX_HPROF_DYNAMIC_DUMP_H
17+#define PANDA_ECMASCRIPT_DFX_HPROF_DYNAMIC_DUMP_H
18+ 
19+#include "ecmascript/dfx/hprof/heap_dump_session.h"
20+#include "ecmascript/dfx/hprof/rawheap_dump.h" // RawHeapDump, ObjectMarker
21+#include "ecmascript/dfx/hprof/heap_profiler_interface.h" // DumpSnapShotOption, DumpFormat
22+#include "ecmascript/dfx/hprof/string_hashmap.h" // StringHashMap
23+#include "ecmascript/dfx/hprof/heap_snapshot.h" // HeapSnapshot
24+#include "ecmascript/dfx/hprof/heap_profiler.h" // EntryIdMap
25+#include "profiler/heap_dump.h"
26+ 
27+#include <memory>
28+ 
29+namespace panda::ecmascript {
30+ 
31+using common::dump::AbstractDumper;
32+using common::dump::DumpRequest;
33+using common::dump::DumpResult;
34+ 
35+/**
36+ * @brief Owns the dynamic side of a hybrid binary heap dump.
37+ *
38+ * This class enables the dynamic (JS/ArkTS) binary dump to participate in
39+ * HeapDumpCoordinator's unified lifecycle orchestration while preserving the
40+ * existing V1/V2 rawheap binary format.
41+ *
42+ * Runtime suspension, descriptor acquisition, stream/writer construction, and
43+ * V1/V2 serialization remain internal to this participant.
44+ *
45+ * The participant is created for an explicit EcmaVM through EcmaVMInterface.
46+ * It does not participate in any process-global factory registration.
47+ */
48+class DynamicDump : public AbstractDumper {
49+public:
50+ enum class HeapProfilerOwnership : uint8_t {
51+ NONE,
52+ VM,
53+ STANDALONE,
54+ };
55+ 
56+ DynamicDump(EcmaVM *vm, const DumpRequest &request);
57+ ~DynamicDump() override;
58+ 
59+ static std::unique_ptr<AbstractDumper> Create(EcmaVM *vm, const DumpRequest &request);
60+ 
61+ // -- AbstractDumper interface --
62+ 
63+ /**
64+ * @brief Full GC: local heap CollectGarbage(FULL_GC) +
65+ * shared heap CollectGarbage(SHARED_GC) + WaitAllTaskFinished.
66+ */
67+ void TriggerGC() override;
68+ 
69+ /// Waits for asynchronous local-heap reclamation started by XGC.
70+ void CompleteCrossRuntimeGC() override;
71+ 
72+ void PrepareSession() override;
73+ bool AcquireOutput() override;
74+ DumpResult Dump() override;
75+ 
76+ /**
77+ * @brief Return opaque EcmaVM* for XRef context.
78+ * The ETS coordinator obtains the VM through the runtime-neutral dumper
79+ * contract without exposing dynamic runtime types.
80+ */
81+ void *GetCurrentVM() override
82+ {
83+ return static_cast<void *>(vm_);
84+ }
85+ 
86+ /**
87+ * @brief Resolve a JS heap address to its dynamic node ID without mutating
88+ * the ID map.
89+ * Returns 0 when the address was not included in the dump.
90+ */
91+ uint32_t GetNodeId(uint64_t addr) const override
92+ {
93+ return (rawHeapDump_ != nullptr) ? rawHeapDump_->FindNodeId(addr) : 0;
94+ }
95+ 
96+ /// Enable cross-thread execution in the child process after fork.
97+ void PrepareForkChild() override
98+ {
99+ vm_->GetAssociatedJSThread()->SetCrossThreadExecution(true);
100+ }
101+ 
102+private:
103+ class RuntimeScope;
104+ class CrossThreadExecutionScope;
105+ 
106+ bool InitializeHeapProfiler();
107+ bool IsDynamicOOM() const;
108+ DumpSnapShotOption CreateDumpOption() const;
109+ bool CreateRawHeapDump();
110+ bool Execute();
111+ 
112+ HeapDumpSession dumpSession_;
113+ EcmaVM *vm_;
114+ RawHeapDump *rawHeapDump_ {nullptr}; // owned, deleted before its dependencies
115+ std::unique_ptr<FileDescriptorStream> fdStream_; // owns fd; reset after rawHeapDump_
116+ std::unique_ptr<StringHashMap> stringTable_; // owned, destroyed after rawHeapDump_
117+ std::unique_ptr<HeapSnapshot> snapshot_; // owned, destroyed after rawHeapDump_
118+ HeapProfiler *heapProfiler_ {nullptr};
119+ HeapProfilerOwnership heapProfilerOwnership_ {HeapProfilerOwnership::NONE};
120+ DumpRequest request_ {};
121+ std::unique_ptr<RuntimeScope> runtimeScope_;
122+};
123+ 
124+} // namespace panda::ecmascript
125+ 
126+#endif // PANDA_ECMASCRIPT_DFX_HPROF_DYNAMIC_DUMP_H
Mecmascript/dfx/hprof/file_stream.cpp+1-1
@@ -106,7 +106,7 @@ void FileDescriptorStream::EndOfStream()
106 106 
107bool FileDescriptorStream::Good()107bool FileDescriptorStream::Good()
108{108{
109- return fd_ > 0;109+ return fd_ >= 0;
110}110}
111 111 
112// Writes the chunk of data into the stream112// Writes the chunk of data into the stream
Aecmascript/dfx/hprof/heap_dump_session.cpp+46-0
@@ -0,0 +1,46 @@
1+/*
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+#include "ecmascript/dfx/hprof/heap_dump_session.h"
17+ 
18+#include "ecmascript/daemon/daemon_thread.h"
19+#include "ecmascript/runtime_lock.h"
20+ 
21+#include <unistd.h>
22+ 
23+namespace panda::ecmascript {
24+ 
25+Mutex HeapDumpSession::mutex_;
26+ 
27+HeapDumpSession::HeapDumpSession() : ownerPid_(getpid())
28+{
29+ JSThread *current = JSThread::GetCurrent();
30+ if (current == nullptr) {
31+ mutex_.Lock();
32+ } else if (current->IsDaemonThread()) {
33+ RuntimeLock(static_cast<DaemonThread *>(current), mutex_);
34+ } else {
35+ RuntimeLock(current, mutex_);
36+ }
37+}
38+ 
39+HeapDumpSession::~HeapDumpSession()
40+{
41+ if (getpid() == ownerPid_) {
42+ mutex_.Unlock();
43+ }
44+}
45+ 
46+} // namespace panda::ecmascript
Aecmascript/dfx/hprof/heap_dump_session.h+49-0
@@ -0,0 +1,49 @@
1+/*
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+#ifndef PANDA_ECMASCRIPT_DFX_HPROF_HEAP_DUMP_SESSION_H
17+#define PANDA_ECMASCRIPT_DFX_HPROF_HEAP_DUMP_SESSION_H
18+ 
19+#include "ecmascript/platform/mutex.h"
20+ 
21+namespace panda::ecmascript {
22+ 
23+/**
24+ * @brief Serializes parent-process preparation for dynamic heap dumps.
25+ *
26+ * A dump-all request can schedule the main hybrid runtime and dynamic-only
27+ * worker runtimes on different event loops. Their GC, suspension and heap
28+ * preparation must not overlap. A contending JS thread enters a non-running
29+ * state while waiting, so it cannot block a shared GC that needs to suspend
30+ * all JS threads. Forked children operate on private snapshots.
31+ */
32+class HeapDumpSession final {
33+public:
34+ HeapDumpSession();
35+ ~HeapDumpSession();
36+ 
37+ HeapDumpSession(const HeapDumpSession &) = delete;
38+ HeapDumpSession &operator=(const HeapDumpSession &) = delete;
39+ HeapDumpSession(HeapDumpSession &&) = delete;
40+ HeapDumpSession &operator=(HeapDumpSession &&) = delete;
41+ 
42+private:
43+ static Mutex mutex_;
44+ int ownerPid_ {-1};
45+};
46+ 
47+} // namespace panda::ecmascript
48+ 
49+#endif // PANDA_ECMASCRIPT_DFX_HPROF_HEAP_DUMP_SESSION_H
Mecmascript/dfx/hprof/heap_profiler.cpp+1-26
@@ -43,8 +43,6 @@
43 43 
44namespace panda::ecmascript {44namespace panda::ecmascript {
45 45 
46-bool HeapProfiler::oomDumpActive_ = false;
47- 
48std::pair<bool, NodeId> EntryIdMap::FindId(JSTaggedType addr)46std::pair<bool, NodeId> EntryIdMap::FindId(JSTaggedType addr)
49{47{
50 auto it = idMap_.find(addr);48 auto it = idMap_.find(addr);
@@ -186,7 +184,7 @@ void HeapProfiler::DumpHeapSnapshotForOOM([[maybe_unused]] const DumpSnapShotOpt
186 doDumpOption.isBeforeFill = false;184 doDumpOption.isBeforeFill = false;
187#endif185#endif
188 int32_t fd;186 int32_t fd;
189- if (doDumpOption.isDumpOOM && doDumpOption.dumpFormat == DumpFormat::BINARY) {187+ if (doDumpOption.dumpFormat == DumpFormat::BINARY) {
190 fd = RequestFileDescriptor(static_cast<int32_t>(FaultLoggerType::JS_RAW_SNAPSHOT));188 fd = RequestFileDescriptor(static_cast<int32_t>(FaultLoggerType::JS_RAW_SNAPSHOT));
191 } else {189 } else {
192 fd = RequestFileDescriptor(static_cast<int32_t>(FaultLoggerType::JS_HEAP_SNAPSHOT));190 fd = RequestFileDescriptor(static_cast<int32_t>(FaultLoggerType::JS_HEAP_SNAPSHOT));
@@ -222,11 +220,6 @@ static void InitFork()
222 220 
223void HeapProfiler::DumpHeapSnapshotFromSharedGCForOOM(Stream *stream, const DumpSnapShotOption &dumpOption)221void HeapProfiler::DumpHeapSnapshotFromSharedGCForOOM(Stream *stream, const DumpSnapShotOption &dumpOption)
224{222{
225- if (!TryStartOOMDump()) {
226- LOG_ECMA(WARN) << "OOM dump already in progress, skip dump";
227- return;
228- }
229- 
230 SharedHeap::GetInstance()->PrepareByJSThread(vm_->GetAssociatedJSThread(), true);223 SharedHeap::GetInstance()->PrepareByJSThread(vm_->GetAssociatedJSThread(), true);
231 if (dumpOption.isProcDump) {224 if (dumpOption.isProcDump) {
232 Runtime::GetInstance()->GCIterateThreadList([&](JSThread *thread) {225 Runtime::GetInstance()->GCIterateThreadList([&](JSThread *thread) {
@@ -551,12 +544,6 @@ bool HeapProfiler::DumpHeapSnapshot(Stream *stream, const DumpSnapShotOption &du
551 if (dumpOption.isBeforeFill) {544 if (dumpOption.isBeforeFill) {
552 FillIdMap();545 FillIdMap();
553 }546 }
554- if (dumpOption.isDumpOOM) {
555- if (!TryStartOOMDump()) {
556- LOG_ECMA(WARN) << "OOM dump already in progress, skip dump";
557- return false;
558- }
559- }
560 // fork for hidumper or oom547 // fork for hidumper or oom
561 pid = ForkAndPerformDump(stream, dumpOption, progress);548 pid = ForkAndPerformDump(stream, dumpOption, progress);
562 if (pid < 0) {549 if (pid < 0) {
@@ -968,18 +955,6 @@ void HeapProfiler::StorePotentiallyLeakHandles(const uintptr_t handle)
968 }955 }
969}956}
970 957 
971-bool HeapProfiler::TryStartOOMDump()
972-{
973- bool result = oomDumpActive_;
974- oomDumpActive_ = true;
975- return !result;
976-}
977- 
978-void HeapProfiler::ResetOOMDump()
979-{
980- oomDumpActive_ = false;
981-}
982- 
983#if defined(ENABLE_HITRACE_LOCAL_HANDLE_DETECT) && defined(ENABLE_BACKTRACE_LOCAL)958#if defined(ENABLE_HITRACE_LOCAL_HANDLE_DETECT) && defined(ENABLE_BACKTRACE_LOCAL)
984void HeapProfiler::DumpHandleLeakRecords()959void HeapProfiler::DumpHandleLeakRecords()
985{960{
Mecmascript/dfx/hprof/heap_profiler.h+13-3
@@ -48,6 +48,19 @@ public:
48 static constexpr uint64_t SEQ_STEP = 2;48 static constexpr uint64_t SEQ_STEP = 2;
49 std::pair<bool, NodeId> FindId(JSTaggedType addr);49 std::pair<bool, NodeId> FindId(JSTaggedType addr);
50 NodeId FindOrInsertNodeId(JSTaggedType addr);50 NodeId FindOrInsertNodeId(JSTaggedType addr);
51+ // Read-only nodeId probe: returns the assigned nodeId or 0 if `addr` was
52+ // never registered. Unlike FindId (which advances nextId_ on a miss), this
53+ // is non-mutating and safe for non-dumping lookups such as XRef resolution
54+ // (HeapDumpCoordinator::CollectAndWriteXRef converts a JS heap address to the
55+ // dynamic nodeId so the XRef record is symmetric with the static staNodeId).
56+ // Returns uint32_t: only the low 32 bits are meaningful (the translator
57+ // strips the high hash bits), mirroring the static-side ObjectIdMap::NodeId.
58+ // 0 is a safe sentinel: entryIds start at 3 (nextId_ init) and stride by 2.
59+ uint32_t FindNodeId(JSTaggedType addr) const
60+ {
61+ auto it = idMap_.find(addr);
62+ return (it != idMap_.end()) ? static_cast<uint32_t>(it->second) : 0;
63+ }
51 bool InsertId(JSTaggedType addr, NodeId id);64 bool InsertId(JSTaggedType addr, NodeId id);
52 bool EraseId(JSTaggedType addr);65 bool EraseId(JSTaggedType addr);
53 bool Move(JSTaggedType oldAddr, JSTaggedType forwardAddr);66 bool Move(JSTaggedType oldAddr, JSTaggedType forwardAddr);
@@ -126,8 +139,6 @@ public:
126 bool StartHeapSampling(uint64_t samplingInterval, int stackDepth = 128) override;139 bool StartHeapSampling(uint64_t samplingInterval, int stackDepth = 128) override;
127 void StopHeapSampling() override;140 void StopHeapSampling() override;
128 const struct SamplingInfo *GetAllocationProfile() override;141 const struct SamplingInfo *GetAllocationProfile() override;
129- static bool TryStartOOMDump();
130- static void ResetOOMDump();
131 size_t GetIdCount() override142 size_t GetIdCount() override
132 {143 {
133 return entryIdMap_->GetIdCount();144 return entryIdMap_->GetIdCount();
@@ -168,7 +179,6 @@ public:
168 std::unordered_map<uintptr_t, uint64_t> GetHandleNodeIdMap() override;179 std::unordered_map<uintptr_t, uint64_t> GetHandleNodeIdMap() override;
169 180 
170private:181private:
171- static bool oomDumpActive_; // don't dump again while OOM dump is in progress.
172 /**182 /**
173 * trigger full gc to make sure no unreachable objects in heap183 * trigger full gc to make sure no unreachable objects in heap
174 */184 */
Mecmascript/dfx/hprof/heap_profiler_interface.cpp+18-0
@@ -20,6 +20,8 @@
20 20 
21namespace panda::ecmascript {21namespace panda::ecmascript {
22 22 
23+std::atomic<bool> HeapProfilerInterface::oomDumpActive_ {false};
24+ 
23HeapProfilerInterface *HeapProfilerInterface::GetInstance(EcmaVM *vm)25HeapProfilerInterface *HeapProfilerInterface::GetInstance(EcmaVM *vm)
24{26{
25 return vm->GetOrNewHeapProfile();27 return vm->GetOrNewHeapProfile();
@@ -40,9 +42,25 @@ void HeapProfilerInterface::DestroyInstance(HeapProfilerInterface *heapProfiler)
40 delete heapProfiler;42 delete heapProfiler;
41}43}
42 44 
45+bool HeapProfilerInterface::TryStartOOMDump()
46+{
47+ bool expected = false;
48+ return oomDumpActive_.compare_exchange_strong(expected, true);
49+}
50+ 
51+void HeapProfilerInterface::ResetOOMDump()
52+{
53+ oomDumpActive_.store(false);
54+}
55+ 
43void HeapProfilerInterface::DumpHeapSnapshotForCMCOOM(void *thread)56void HeapProfilerInterface::DumpHeapSnapshotForCMCOOM(void *thread)
44{57{
45#if defined(ECMASCRIPT_SUPPORT_SNAPSHOT) && defined(ENABLE_DUMP_IN_FAULTLOG)58#if defined(ECMASCRIPT_SUPPORT_SNAPSHOT) && defined(ENABLE_DUMP_IN_FAULTLOG)
59+ if (!TryStartOOMDump()) {
60+ LOG_ECMA(INFO) << "DumpHeapSnapshotForCMCOOM, OOM dump already triggered.";
61+ return;
62+ }
63+ 
46 EcmaVM *vm = Runtime::GetInstance()->GetMainThread()->GetEcmaVM();64 EcmaVM *vm = Runtime::GetInstance()->GetMainThread()->GetEcmaVM();
47 if (thread != nullptr) {65 if (thread != nullptr) {
48 vm = reinterpret_cast<JSThread *>(thread)->GetEcmaVM();66 vm = reinterpret_cast<JSThread *>(thread)->GetEcmaVM();
Mecmascript/dfx/hprof/heap_profiler_interface.h+16-0
@@ -16,6 +16,7 @@
16#ifndef ECMASCRIPT_DFX_HPROF_HEAP_PROFILER_INTERFACE_H16#ifndef ECMASCRIPT_DFX_HPROF_HEAP_PROFILER_INTERFACE_H
17#define ECMASCRIPT_DFX_HPROF_HEAP_PROFILER_INTERFACE_H17#define ECMASCRIPT_DFX_HPROF_HEAP_PROFILER_INTERFACE_H
18 18 
19+#include <atomic>
19#include <functional>20#include <functional>
20#include <memory>21#include <memory>
21#include <unordered_map>22#include <unordered_map>
@@ -46,6 +47,7 @@ struct DumpSnapShotOption {
46 bool isClearNodeIdCache = false; // whether clear node id map cache after dump47 bool isClearNodeIdCache = false; // whether clear node id map cache after dump
47 bool dumpDynamicHeap = false; // whether to dump dynamic (ArkTS) heap48 bool dumpDynamicHeap = false; // whether to dump dynamic (ArkTS) heap
48 bool dumpStaticHeap = false; // whether to dump static heap49 bool dumpStaticHeap = false; // whether to dump static heap
50+ bool isForHybridXRef = false; // whether dynamic node IDs are needed by a hybrid XRef dump
49 std::string spaceType = ""; // space type for raw heap dump51 std::string spaceType = ""; // space type for raw heap dump
50 std::string heapType = ""; // heap type: local heap or shared heap52 std::string heapType = ""; // heap type: local heap or shared heap
51 LanguageEnv languageEnv = LanguageEnv::DYNAMIC; // language environment selection53 LanguageEnv languageEnv = LanguageEnv::DYNAMIC; // language environment selection
@@ -68,6 +70,17 @@ public:
68 70 
69 static void DumpHeapSnapshotForCMCOOM(void *thread);71 static void DumpHeapSnapshotForCMCOOM(void *thread);
70 72 
73+ /**
74+ * Claim the process-wide dynamic OOM dump attempt.
75+ *
76+ * Production code never releases this one-shot claim because cascading
77+ * OOM notifications must not start another dump.
78+ */
79+ static bool TryStartOOMDump();
80+ 
81+ /** Reset the OOM dump claim for isolated tests. */
82+ static void ResetOOMDump();
83+ 
71 HeapProfilerInterface() = default;84 HeapProfilerInterface() = default;
72 virtual ~HeapProfilerInterface() = default;85 virtual ~HeapProfilerInterface() = default;
73 86 
@@ -94,6 +107,9 @@ public:
94 107 
95 NO_MOVE_SEMANTIC(HeapProfilerInterface);108 NO_MOVE_SEMANTIC(HeapProfilerInterface);
96 NO_COPY_SEMANTIC(HeapProfilerInterface);109 NO_COPY_SEMANTIC(HeapProfilerInterface);
110+ 
111+private:
112+ static std::atomic<bool> oomDumpActive_;
97};113};
98} // namespace panda::ecmascript114} // namespace panda::ecmascript
99#endif // ECMASCRIPT_DFX_HPROF_HEAP_PROFILER_INTERFACE_H115#endif // ECMASCRIPT_DFX_HPROF_HEAP_PROFILER_INTERFACE_H
Mecmascript/dfx/hprof/hybrid/hybrid_heap_profiler.cpp+46-2
@@ -14,12 +14,14 @@
14 */14 */
15 15 
16#include <csignal>16#include <csignal>
17+#include <ctime>
17#include <sys/prctl.h>18#include <sys/prctl.h>
18#include <sys/syscall.h>19#include <sys/syscall.h>
19#include <sys/wait.h>20#include <sys/wait.h>
20#include <unistd.h>21#include <unistd.h>
21 22 
22#include "ecmascript/checkpoint/thread_state_transition.h"23#include "ecmascript/checkpoint/thread_state_transition.h"
24+#include "ecmascript/cross_vm/cross_vm_operator.h"
23#include "ecmascript/dfx/hprof/hybrid/hybrid_heap_profiler.h"25#include "ecmascript/dfx/hprof/hybrid/hybrid_heap_profiler.h"
24#include "ecmascript/dfx/hprof/heap_snapshot_json_serializer.h"26#include "ecmascript/dfx/hprof/heap_snapshot_json_serializer.h"
25#include "ecmascript/dfx/hprof/hybrid/hybrid_heap_snapshot.h"27#include "ecmascript/dfx/hprof/hybrid/hybrid_heap_snapshot.h"
@@ -30,11 +32,17 @@
30#include "ecmascript/mem/shared_heap/shared_concurrent_sweeper.h"32#include "ecmascript/mem/shared_heap/shared_concurrent_sweeper.h"
31#include "ecmascript/runtime.h"33#include "ecmascript/runtime.h"
32#include "ecmascript/runtime_lock.h"34#include "ecmascript/runtime_lock.h"
35+#include "libpandabase/utils/time.h"
33#if defined(ENABLE_DUMP_IN_FAULTLOG)36#if defined(ENABLE_DUMP_IN_FAULTLOG)
34#include "faultloggerd_client.h"37#include "faultloggerd_client.h"
35#endif38#endif
36 39 
37namespace panda::ecmascript {40namespace panda::ecmascript {
41+ 
42+using common::dump::DumpExecutionMode;
43+using common::dump::DumpRequest;
44+using common::dump::DumpScope;
45+ 
38HybridHeapProfiler *HybridHeapProfiler::GetInstance()46HybridHeapProfiler *HybridHeapProfiler::GetInstance()
39{47{
40 if (!Runtime::HasInstance()) {48 if (!Runtime::HasInstance()) {
@@ -133,10 +141,10 @@ void HybridHeapProfiler::WaitForJSGCFinish(const EcmaVM *vm, const DumpSnapShotO
133 141 
134static void HybridWaitProcess(pid_t pid)142static void HybridWaitProcess(pid_t pid)
135{143{
136- time_t startTime = time(nullptr);144+ std::time_t startTime = std::time(nullptr);
137 constexpr int dumpTimeOut = 300;145 constexpr int dumpTimeOut = 300;
138 constexpr int defaultSleepTime = 100000;146 constexpr int defaultSleepTime = 100000;
139- while (time(nullptr) <= startTime + dumpTimeOut) {147+ while (std::time(nullptr) <= startTime + dumpTimeOut) {
140 int status = 0;148 int status = 0;
141 pid_t p = waitpid(pid, &status, WNOHANG);149 pid_t p = waitpid(pid, &status, WNOHANG);
142 if (p < 0) {150 if (p < 0) {
@@ -272,6 +280,42 @@ int32_t HybridHeapProfiler::AcquireDumpStream(const DumpSnapShotOption &dumpOpti
272#endif280#endif
273}281}
274 282 
283+bool HybridHeapProfiler::BinaryDump(EcmaVM *vm, DumpSnapShotOption &dumpOption)
284+{
285+ if (!HasSTSInterface()) {
286+ LOG_ECMA(ERROR) << "[HybDump][Dyn] Request rejected: static interface unavailable";
287+ return false;
288+ }
289+ dumpOption.dumpDynamicHeap = vm != nullptr;
290+ dumpOption.dumpStaticHeap = stsInterface_->IsCurrentThreadAttached();
291+ LOG_ECMA(INFO) << "[HybDump][Dyn] Request ready: dynamic="
292+ << (dumpOption.dumpDynamicHeap ? "true" : "false")
293+ << ", static=" << (dumpOption.dumpStaticHeap ? "true" : "false")
294+ << ", gc=" << (dumpOption.isFullGC ? "true" : "false")
295+ << ", scope=" << (dumpOption.isProcDump ? "process" : "vm")
296+ << ", mode=" << (dumpOption.isSync ? "in_process" : "fork_once");
297+ if (!dumpOption.dumpDynamicHeap && !dumpOption.dumpStaticHeap) {
298+ return false;
299+ }
300+ 
301+ DumpRequest request;
302+ request.policy.triggerGC = dumpOption.isFullGC;
303+ request.policy.scope = dumpOption.isProcDump ? DumpScope::PROCESS : DumpScope::VM;
304+ request.policy.executionMode = dumpOption.isSync ? DumpExecutionMode::IN_PROCESS : DumpExecutionMode::FORK_ONCE;
305+ request.identity = {static_cast<int32_t>(getpid()), static_cast<int32_t>(JSThread::GetCurrentThreadId()),
306+ panda::time::GetCurrentTimeInMillis(true)};
307+ arkplatform::EcmaVMInterface *ecmaInterface = nullptr;
308+ if (dumpOption.dumpDynamicHeap) {
309+ auto *crossVMOperator = vm->GetCrossVMOperator();
310+ ecmaInterface = crossVMOperator == nullptr ? nullptr : crossVMOperator->GetEcmaVMInterface();
311+ if (ecmaInterface == nullptr) {
312+ LOG_ECMA(ERROR) << "[HybDump][Dyn] Request rejected: dynamic interface unavailable";
313+ return false;
314+ }
315+ }
316+ return stsInterface_->ExecuteHeapDump(request, ecmaInterface, dumpOption.dumpStaticHeap);
317+}
318+ 
275bool HybridHeapProfiler::SetAppFreezeFilter()319bool HybridHeapProfiler::SetAppFreezeFilter()
276{320{
277 AppFreezeFilterCallback callback = Runtime::GetInstance()->GetAppFreezeFilterCallback();321 AppFreezeFilterCallback callback = Runtime::GetInstance()->GetAppFreezeFilterCallback();
Mecmascript/dfx/hprof/hybrid/hybrid_heap_profiler.h+2-0
@@ -42,6 +42,8 @@ public:
42 42 
43 bool Dump(EcmaVM *vm, Stream *stream, DumpSnapShotOption &dumpOption);43 bool Dump(EcmaVM *vm, Stream *stream, DumpSnapShotOption &dumpOption);
44 44 
45+ bool BinaryDump(EcmaVM *vm, DumpSnapShotOption &dumpOption);
46+ 
45 EntryIdMap *GetEntryIdMap()47 EntryIdMap *GetEntryIdMap()
46 {48 {
47 return &entryIdMap_;49 return &entryIdMap_;
Mecmascript/dfx/hprof/hybrid/hybrid_heap_snapshot.h+2-2
@@ -79,8 +79,8 @@ private:
79 bool isSimplify_ {false};79 bool isSimplify_ {false};
80 80 
81 // xref maps (populated in BuildUp, used in FillEdges)81 // xref maps (populated in BuildUp, used in FillEdges)
82- std::unordered_map<uint64_t, uint64_t> jsToEts_;82+ arkplatform::STSVMInterface::XRefMap jsToEts_;
83- std::unordered_map<uint64_t, uint64_t> etsToJs_;83+ arkplatform::STSVMInterface::XRefMap etsToJs_;
84};84};
85 85 
86} // namespace panda::ecmascript86} // namespace panda::ecmascript
Mecmascript/dfx/hprof/rawheap_dump.cpp+37-10
@@ -150,22 +150,33 @@ void ObjectMarker::MarkRootObjects()
150 150 
151RawHeapDump::RawHeapDump(const EcmaVM *vm, Stream *stream, HeapSnapshot *snapshot,151RawHeapDump::RawHeapDump(const EcmaVM *vm, Stream *stream, HeapSnapshot *snapshot,
152 EntryIdMap *entryIdMap, const DumpSnapShotOption &dumpOption)152 EntryIdMap *entryIdMap, const DumpSnapShotOption &dumpOption)
153- : vm_(vm), dumpOption_(&dumpOption), snapshot_(snapshot), entryIdMap_(entryIdMap),153+ : vm_(vm), dumpOption_(dumpOption), snapshot_(snapshot), entryIdMap_(entryIdMap),
154- writer_(stream), marker_(vm, &dumpOption)154+ writer_(stream), marker_(vm, &dumpOption_)
155{155{
156 startTime_ = std::chrono::steady_clock::now();156 startTime_ = std::chrono::steady_clock::now();
157}157}
158 158 
159RawHeapDump::~RawHeapDump()159RawHeapDump::~RawHeapDump()
160{160{
161- writer_.EndOfWriteBinBlock();161+ // Finalize() performs the flush / success log / HiSysEvent at a defined
162+ // point (end of BinaryDump). The destructor must NOT repeat those side
163+ // effects: in the hybrid fork model the parent process never calls
164+ // BinaryDump but still destroys this instance, and flushing the parent's
165+ // stale buffer / emitting a duplicate OOM event would corrupt the child's
166+ // output and double-report. Only release in-memory state that is safe to
167+ // drop in either process.
162 secIndexVec_.clear();168 secIndexVec_.clear();
169+}
170+ 
171+void RawHeapDump::Finalize()
172+{
173+ writer_.EndOfWriteBinBlock();
163 auto endTime = std::chrono::steady_clock::now();174 auto endTime = std::chrono::steady_clock::now();
164 double duration = std::chrono::duration<double>(endTime - startTime_).count();175 double duration = std::chrono::duration<double>(endTime - startTime_).count();
165 LOG_ECMA(INFO) << "rawheap dump success, cost " << duration << "s, " << "file size " << GetRawHeapFileOffset();176 LOG_ECMA(INFO) << "rawheap dump success, cost " << duration << "s, " << "file size " << GetRawHeapFileOffset();
166- if (dumpOption_->isDumpOOM) {177+ if (dumpOption_.isDumpOOM) {
167 SEND_HISYSEVENT(ARKTS_RUNTIME, ARK_STATS_OOM, STATISTIC, "STATUS", 0, "MESSAGE", "OK",178 SEND_HISYSEVENT(ARKTS_RUNTIME, ARK_STATS_OOM, STATISTIC, "STATUS", 0, "MESSAGE", "OK",
168- "OOM_TYPE", dumpOption_->isForSharedOOM ? "SHARED_OOM" : "LOCAL_OOM",179+ "OOM_TYPE", dumpOption_.isForSharedOOM ? "SHARED_OOM" : "LOCAL_OOM",
169 "OBJ_COUNT", GetObjectCount(),180 "OBJ_COUNT", GetObjectCount(),
170 "STR_COUNT", GetEcmaStringTable()->GetCapcity(),181 "STR_COUNT", GetEcmaStringTable()->GetCapcity(),
171 "HEAP_SIZE", marker_.GetHeapSize(),182 "HEAP_SIZE", marker_.GetHeapSize(),
@@ -224,6 +235,9 @@ void RawHeapDump::BinaryDump()
224 DumpObjectMemory();235 DumpObjectMemory();
225 236 
226 DumpSectionIndex();237 DumpSectionIndex();
238+ // Flush the writer buffer and emit the success/OOM events at a defined
239+ // point rather than from the destructor (see ~RawHeapDump).
240+ Finalize();
227}241}
228 242 
229void RawHeapDump::IterateMarkedObjects(const std::function<void(JSTaggedType)> &visitor)243void RawHeapDump::IterateMarkedObjects(const std::function<void(JSTaggedType)> &visitor)
@@ -293,8 +307,8 @@ void RawHeapDump::DumpVersion(const std::string &version)
293 307 
294void RawHeapDump::DumpMetadataFields()308void RawHeapDump::DumpMetadataFields()
295{309{
296- DumpStringField(dumpOption_->spaceType, 32, "space type"); // 32: space type size310+ DumpStringField(dumpOption_.spaceType, 32, "space type"); // 32: space type size
297- DumpStringField(dumpOption_->heapType, 16, "heap type"); // 16: heap type size311+ DumpStringField(dumpOption_.heapType, 16, "heap type"); // 16: heap type size
298 DumpStringField("dynamic", 8, "vm type"); // 8: vm type size312 DumpStringField("dynamic", 8, "vm type"); // 8: vm type size
299}313}
300 314 
@@ -326,8 +340,10 @@ void RawHeapDump::DumpSectionIndex()
326*/340*/
327NodeId RawHeapDump::GenerateNodeId(JSTaggedType addr)341NodeId RawHeapDump::GenerateNodeId(JSTaggedType addr)
328{342{
329- NodeId nodeId = dumpOption_->isDumpOOM ? entryIdMap_->GetNextId() : entryIdMap_->FindOrInsertNodeId(addr);343+ NodeId nodeId = dumpOption_.isDumpOOM && !dumpOption_.isForHybridXRef
330- if (!dumpOption_->isJSLeakWatcher) {344+ ? entryIdMap_->GetNextId()
345+ : entryIdMap_->FindOrInsertNodeId(addr);
346+ if (!dumpOption_.isJSLeakWatcher) {
331 return nodeId;347 return nodeId;
332 }348 }
333 349 
@@ -336,6 +352,17 @@ NodeId RawHeapDump::GenerateNodeId(JSTaggedType addr)
336 return (static_cast<uint64_t>(hash) << 32) | (nodeId & 0xFFFFFFFF); // 32: 32-bits means a half of uint64_t352 return (static_cast<uint64_t>(hash) << 32) | (nodeId & 0xFFFFFFFF); // 32: 32-bits means a half of uint64_t
337}353}
338 354 
355+uint32_t RawHeapDump::FindNodeId(uint64_t addr) const
356+{
357+ if (entryIdMap_ == nullptr) {
358+ return 0;
359+ }
360+ // EntryIdMap keys by JSTaggedType (uint64_t); jsAddr from STS XRef maps is
361+ // the same tagged value the dumper registered (the legacy hybrid path also
362+ // resolves jsToEts by the dynamic node's address). FindNodeId is read-only.
363+ return entryIdMap_->FindNodeId(static_cast<JSTaggedType>(addr));
364+}
365+ 
339void RawHeapDump::WriteChunk(char *data, size_t size)366void RawHeapDump::WriteChunk(char *data, size_t size)
340{367{
341 writer_.WriteBinBlock(data, size);368 writer_.WriteBinBlock(data, size);
@@ -410,7 +437,7 @@ void RawHeapDump::WriteGlobalRefGroup()
410 }437 }
411 entries.push_back({reinterpret_cast<uintptr_t>(ref), value.GetRawData()});438 entries.push_back({reinterpret_cast<uintptr_t>(ref), value.GetRawData()});
412 };439 };
413- if (dumpOption_->isProcDump) {440+ if (dumpOption_.isProcDump) {
414 Runtime::GetInstance()->GCIterateThreadList([&](JSThread *thread) {441 Runtime::GetInstance()->GCIterateThreadList([&](JSThread *thread) {
415 thread->IterateGlobalRefMappings(collector);442 thread->IterateGlobalRefMappings(collector);
416 });443 });
Mecmascript/dfx/hprof/rawheap_dump.h+22-1
@@ -102,11 +102,32 @@ public:
102 102 
103 void BinaryDump();103 void BinaryDump();
104 104 
105+ /**
106+ * @brief Flush the binary writer buffer and emit the success log / OOM
107+ * HiSysEvent. Called at a defined point (end of BinaryDump) rather than
108+ * from the destructor.
109+ *
110+ * In the hybrid fork model the parent process never calls BinaryDump but
111+ * still destroys the RawHeapDump instance; performing the flush / event
112+ * emission in the destructor would flush the parent's stale buffer into
113+ * the child's output file and double-report the OOM event. The destructor
114+ * therefore only clears in-memory state, and all side effects live here.
115+ */
116+ void Finalize();
117+ 
105 uint32_t GetRawHeapFileOffset()118 uint32_t GetRawHeapFileOffset()
106 {119 {
107 return static_cast<uint32_t>(writer_.GetCurrentFileSize());120 return static_cast<uint32_t>(writer_.GetCurrentFileSize());
108 }121 }
109 122 
123+ /**
124+ * @brief Read-only nodeId lookup for a heap address (no insertion, no
125+ * counter advance). Used by DynamicDump::GetNodeId so XRef records carry
126+ * the dynamic nodeId instead of the raw address.
127+ * @return nodeId assigned to addr (low 32 bits), or 0 if addr was not dumped.
128+ */
129+ uint32_t FindNodeId(uint64_t addr) const;
130+ 
110 uint32_t GetObjectCount()131 uint32_t GetObjectCount()
111 {132 {
112 return static_cast<uint32_t>(marker_.GetMarkedObjects());133 return static_cast<uint32_t>(marker_.GetMarkedObjects());
@@ -190,7 +211,7 @@ private:
190 // Update string table for SourceTextModule's EcmaModuleRecordName and EcmaModuleFilename211 // Update string table for SourceTextModule's EcmaModuleRecordName and EcmaModuleFilename
191 void UpdateSourceTextModuleStringTable(JSTaggedType addr, int &strCnt);212 void UpdateSourceTextModuleStringTable(JSTaggedType addr, int &strCnt);
192 213 
193- const DumpSnapShotOption *dumpOption_ {};214+ DumpSnapShotOption dumpOption_ {};
194 HeapSnapshot *snapshot_ {nullptr};215 HeapSnapshot *snapshot_ {nullptr};
195 EntryIdMap *entryIdMap_ {nullptr};216 EntryIdMap *entryIdMap_ {nullptr};
196 BinaryWriter writer_;217 BinaryWriter writer_;
Mecmascript/dfx/hprof/rawheap_translate/BUILD.gn+31-12
@@ -13,23 +13,42 @@
13 13 
14import("//arkcompiler/ets_runtime/js_runtime_config.gni")14import("//arkcompiler/ets_runtime/js_runtime_config.gni")
15 15 
16-config("rawheap_translate_config") {16+config("rawheap_translate_public_config") {
17- include_dirs = [ "$js_root" ]17+ include_dirs = [ "$js_root/ecmascript/dfx/hprof/rawheap_translate" ]
18}18}
19 19 
20-ohos_executable("rawheap_translator") {20+rawheap_translate_sources = [
21- sources = [21+ "metadata_parse.cpp",
22- "main.cpp",22+ "rawheap_translate.cpp",
23- "metadata_parse.cpp",23+ "serializer.cpp",
24- "rawheap_translate.cpp",24+ "snapshot_merger.cpp",
25- "serializer.cpp",25+ "static_rawheap_translate.cpp",
26- "string_hashmap.cpp",26+ "string_hashmap.cpp",
27- "utils.cpp",27+ "utils.cpp",
28- ]28+]
29+ 
30+ohos_static_library("rawheap_translate_static") {
31+ sources = rawheap_translate_sources
29 32 
30 cflags_cc = [ "-std=c++17" ]33 cflags_cc = [ "-std=c++17" ]
31 34 
32- configs = [ ":rawheap_translate_config" ]35+ public_configs = [ ":rawheap_translate_public_config" ]
36+ 
37+ external_deps = [
38+ "bounds_checking_function:libsec_static",
39+ "cJSON:cjson_static",
40+ ]
41+ 
42+ part_name = "ets_runtime"
43+ subsystem_name = "arkcompiler"
44+}
45+ 
46+ohos_executable("rawheap_translator") {
47+ sources = [ "main.cpp" ]
48+ 
49+ cflags_cc = [ "-std=c++17" ]
50+ 
51+ deps = [ ":rawheap_translate_static" ]
33 52 
34 if (!ark_standalone_build) {53 if (!ark_standalone_build) {
35 branch_protector_ret = "pac_ret"54 branch_protector_ret = "pac_ret"
Mecmascript/dfx/hprof/rawheap_translate/common.h+130-1
@@ -16,6 +16,7 @@
16#ifndef RAWHEAP_TRANSLATE_COMMON_H16#ifndef RAWHEAP_TRANSLATE_COMMON_H
17#define RAWHEAP_TRANSLATE_COMMON_H17#define RAWHEAP_TRANSLATE_COMMON_H
18 18 
19+#include <array>
19#include <cstdint>20#include <cstdint>
20#include <vector>21#include <vector>
21#include <string>22#include <string>
@@ -27,11 +28,23 @@ using StringKey = size_t;
27using StringId = uint32_t;28using StringId = uint32_t;
28 29 
29static constexpr NodeType DEFAULT_NODETYPE = 8; // 8: means default node type30static constexpr NodeType DEFAULT_NODETYPE = 8; // 8: means default node type
31+static constexpr NodeType SYNTHETIC_NODETYPE = 9; // 9: means SYNTHETIC node type
30static constexpr NodeType FRAMEWORK_NODETYPE = 14;32static constexpr NodeType FRAMEWORK_NODETYPE = 14;
31static constexpr NodeType ROOT = 15;33static constexpr NodeType ROOT = 15;
32static constexpr NodeType HEAP_NUMBER = 7;34static constexpr NodeType HEAP_NUMBER = 7;
33static constexpr NodeType STRING = 2;35static constexpr NodeType STRING = 2;
34-enum class EdgeType { CONTEXT, ELEMENT, PROPERTY, INTERNAL, HIDDEN, SHORTCUT, WEAK, DEFAULT = PROPERTY };36+// Per-record-tag node types restored by the static rawheap parser. These mirror
37+// the binary serializer's node_types list (serializer.cpp): array=1, object=3.
38+// CLASS is appended to that list at index 16 (additive — does not shift the
39+// existing framework/handle slots). The dynamic parser derives node types from
40+// metadata instead, but the static side has no JSType, so the record tag that
41+// populated the record (TAG_STATIC_CLASS_DUMP / INSTANCE_DUMP / ARRAY_DUMP) is
42+// the only signal available.
43+static constexpr NodeType ARRAY_NODETYPE = 1; // 1: "array" in node_types
44+static constexpr NodeType OBJECT_NODETYPE = 3; // 3: "object" in node_types
45+static constexpr NodeType CLOSURE_NODETYPE = 5; // 5: "closure" in node_types (synthetic method node)
46+static constexpr NodeType CLASS_NODETYPE = 16; // 16: "class" appended to node_types
47+enum class EdgeType { CONTEXT, ELEMENT, PROPERTY, INTERNAL, HIDDEN, SHORTCUT, WEAK, XREF, DEFAULT = PROPERTY };
35 48 
36static constexpr int VIRTUAL_NODE_SIZE = 1; // The virtual node size is fixed at 149static constexpr int VIRTUAL_NODE_SIZE = 1; // The virtual node size is fixed at 1
37 50 
@@ -107,8 +120,14 @@ struct Edge {
107 Node *to {nullptr};120 Node *to {nullptr};
108 uint32_t nameOrIndex = 0;121 uint32_t nameOrIndex = 0;
109 EdgeType type = EdgeType::DEFAULT;122 EdgeType type = EdgeType::DEFAULT;
123+ // Source node (set by the static parser so edges can be sorted into the
124+ // the .heapsnapshot grouping contract: node i owns the next edgeCount[i] edges). V1/V2
125+ // leave this null (their edges are already inserted in source order).
126+ Node *from {nullptr};
110 127 
111 Edge(Node *node, uint32_t index, EdgeType edgeType) : to(node), nameOrIndex(index), type(edgeType) {}128 Edge(Node *node, uint32_t index, EdgeType edgeType) : to(node), nameOrIndex(index), type(edgeType) {}
129+ Edge(Node *fromNode, Node *toNode, uint32_t index, EdgeType edgeType)
130+ : to(toNode), nameOrIndex(index), type(edgeType), from(fromNode) {}
112};131};
113 132 
114static constexpr uint8_t ZERO_VALUE = 0x02U; // 0000 0010133static constexpr uint8_t ZERO_VALUE = 0x02U; // 0000 0010
@@ -125,5 +144,115 @@ static constexpr uint8_t DOUB_VALUE = 0x06U; // 0000 0110
125// is disabled. Translator treats it as "tracking off, no payload follows".144// is disabled. Translator treats it as "tracking off, no payload follows".
126// Shared between rawheap_dump.cpp (writer) and rawheap_translate.cpp (reader).145// Shared between rawheap_dump.cpp (writer) and rawheap_translate.cpp (reader).
127constexpr uint32_t GLOBAL_REF_TRACK_OFF_MARK = 0xFFFFFFFFU;146constexpr uint32_t GLOBAL_REF_TRACK_OFF_MARK = 0xFFFFFFFFU;
147+// ---- Static binary snapshot format constants ----
148+//
149+// IMPORTANT: These constants mirror the authoritative definitions in
150+// plugins/ets/runtime/tooling/hprof/session/dump_format.h. The rawheap_translate
151+// module is an offline CLI tool that cannot depend on the ETS runtime implementation,
152+// so it must maintain its own copies. When updating any value here,
153+// you MUST also update the corresponding value in dump_format.h (and
154+// vice versa) to keep the two in sync. A mismatch will produce a
155+// format that the parser/writer on the other side cannot correctly
156+// interpret.
157+//
158+// Naming conventions:
159+// TAG_* → same name and value as ark::tooling::hprof::TAG_*
160+// StaFieldType → mirrors ark::tooling::hprof::FieldType (same numeric values,
161+// different name because this module does not use the
162+// ark::tooling::hprof namespace)
163+// XREF_* → same name and value as ark::tooling::hprof::XREF_DIR_*
164+// STATIC_* → mirrors ark::tooling::hprof::HYBRID_DUMP_* / *_BODY_SIZE
165+ 
166+// Record tags
167+static constexpr uint8_t TAG_STRING_IN_UTF8 = 0x01;
168+static constexpr uint8_t TAG_LOAD_CLASS = 0x02;
169+static constexpr uint8_t TAG_STATIC_CLASS_DUMP = 0x0B;
170+static constexpr uint8_t TAG_ROOT_RECORD = 0x10;
171+static constexpr uint8_t TAG_STATIC_INSTANCE_DUMP = 0x14;
172+static constexpr uint8_t TAG_STATIC_ARRAY_DUMP = 0x15;
173+static constexpr uint8_t TAG_STATIC_STRING_DUMP = 0x16; // string objects with UTF-8 content
174+static constexpr uint8_t TAG_XREF_EDGE = 0x30;
175+static constexpr uint8_t TAG_HEAP_SUMMARY = 0xFE;
176+static constexpr uint8_t TAG_PARTIAL_MARKER = 0xFF;
177+ 
178+// Root types (within TAG_ROOT_RECORD body)
179+static constexpr uint8_t ROOT_TYPE_STATIC_OBJECT = 0x00;
180+ 
181+// Field types (mirrors ark::tooling::hprof::FieldType numeric values)
182+enum class StaFieldType : uint8_t {
183+ UNKNOWN = 0x00, BOOLEAN = 0x01, CHAR = 0x02, FLOAT = 0x03,
184+ DOUBLE = 0x04, BYTE = 0x05, SHORT = 0x06, INT = 0x07,
185+ LONG = 0x08, OBJECT = 0x09, ARRAY = 0x0A, TAGGED = 0x0B,
186+ WEAK_OBJECT = 0x0C,
187+};
188+ 
189+// Static runtime coretypes::TaggedValue special payloads. Tagged heap objects
190+// and primitives are normalized by the writer to OBJECT/WEAK_OBJECT or their
191+// concrete primitive type; TAGGED normally carries one of these raw markers.
192+// Unknown raw markers are retained as stable hexadecimal synthetic values.
193+inline constexpr uint64_t STATIC_TAGGED_HOLE = 0x00ULL;
194+inline constexpr uint64_t STATIC_TAGGED_NULL = 0x02ULL;
195+inline constexpr uint64_t STATIC_TAGGED_FALSE = 0x06ULL;
196+inline constexpr uint64_t STATIC_TAGGED_TRUE = 0x07ULL;
197+inline constexpr uint64_t STATIC_TAGGED_UNDEFINED = 0x0AULL;
198+inline constexpr uint64_t STATIC_TAGGED_EXCEPTION = 0x12ULL;
199+ 
200+// XRef direction
201+static constexpr uint8_t XREF_DYN_TO_STA = 0;
202+static constexpr uint8_t XREF_STA_TO_DYN = 1;
203+static constexpr uint8_t XREF_BIDIR = 2;
204+ 
205+// Header / record layout sizes
206+//
207+// The header starts with an 8-byte version string matching the V1/V2 convention.
208+// Old V1/V2 tools that encounter a V3 file read "3.0.0" as the version,
209+// try ParseRawheap, see VERSION(2,0,0) < Version(3,0,0) → gracefully exit.
210+// New tools route every compatible 3.x.x version to the static parser.
211+static constexpr size_t STATIC_VERSION_SIZE = 8; // "3.0.0\0\0\0"
212+static constexpr uint32_t STATIC_HEADER_SIZE = 33; // version(8)+id(4)+ts(8)+lang(1)+hdr(4)+rec(4)+flags(4)
213+static constexpr uint32_t STATIC_IDENTIFIER_SIZE = 4; // 4-byte object identifier (u32 nodeId, even numbers)
214+inline constexpr uint8_t STATIC_LANGUAGE_STATIC = 1;
215+inline constexpr uint8_t STATIC_LANGUAGE_HYBRID = 2;
216+inline constexpr uint32_t STATIC_SUPPORTED_FEATURE_FLAGS = 0;
217+static constexpr int STATIC_SNAPSHOT_MAJOR_VERSION = 3;
218+static constexpr size_t STATIC_RECORD_HDR_SIZE = 17; // tag(1)+time(8)+length(4)+count(4)
219+ 
220+// Offsets within the record header (STATIC_RECORD_HDR_SIZE bytes).
221+static constexpr size_t STATIC_RECORD_HDR_TAG_OFF = 0; // tag: u8
222+static constexpr size_t STATIC_RECORD_HDR_TIME_OFF = 1; // timestamp: u64 (8 bytes)
223+static constexpr size_t STATIC_RECORD_HDR_LENGTH_OFF = 9; // body length: u32 (4 bytes)
224+static constexpr size_t STATIC_RECORD_HDR_COUNT_OFF = 13; // item count: u32 (4 bytes)
225+static constexpr size_t STATIC_ROOT_BODY_SIZE = 5; // rootType(1)+objNodeId(4)
226+static constexpr size_t STATIC_XREF_BODY_SIZE = 9; // dynNodeId(4)+staNodeId(4)+dir(1)
227+// objNodeId(4)+classNodeId(4)+stackTrace(4)+instSize(4)+arrayLen(4)+elemType(1)
228+static constexpr size_t STATIC_ARRAY_PREFIX_BODY_SIZE = 21;
229+ 
230+// STATIC_STRING_DUMP fixed prefix: objId(4)+classObjId(4)+instSize(4)+valueLen(4) = 16,
231+// followed by valueLen bytes of UTF-8 content.
232+static constexpr size_t STATIC_STRING_PREFIX_BODY_SIZE = 16;
233+static constexpr size_t STATIC_STRING_OBJADDR_OFF = 0;
234+static constexpr size_t STATIC_STRING_CLASSOBJ_OFF = STATIC_IDENTIFIER_SIZE; // 4
235+static constexpr size_t STATIC_STRING_INSTSIZE_OFF = 2 * STATIC_IDENTIFIER_SIZE; // 8
236+static constexpr size_t STATIC_STRING_VALUELEN_OFF = 3 * STATIC_IDENTIFIER_SIZE; // 12
237+ 
238+// Field offsets within the array prefix body (relative to the start of the prefix).
239+static constexpr size_t STATIC_ARRAY_CLASS_OFFSET = STATIC_IDENTIFIER_SIZE; // 4: classNodeId field offset
240+static constexpr size_t STATIC_ARRAY_INSTSIZE_OFFSET =
241+ 2 * STATIC_IDENTIFIER_SIZE + sizeof(uint32_t); // 12: instanceSize field offset
242+static constexpr size_t STATIC_ARRAY_LENGTH_OFFSET =
243+ 2 * STATIC_IDENTIFIER_SIZE + 2 * sizeof(uint32_t); // 16: arrayLen field offset
244+static constexpr size_t STATIC_ARRAY_ELEM_TYPE_OFFSET =
245+ STATIC_ARRAY_LENGTH_OFFSET + sizeof(uint32_t); // 20: elemType field offset
246+ 
247+// Bit-shift helpers for byte-to-integer conversion.
248+static constexpr size_t BITS_PER_BYTE = 8;
249+ 
250+// Version emitted by the current static/hybrid writer. It uses the same 8-byte
251+// convention as V1/V2. The writer emits 3.0.0, while the reader accepts every
252+// compatible 3.x.x version.
253+inline constexpr std::array<char, STATIC_VERSION_SIZE> STATIC_SNAPSHOT_VERSION = {
254+ '3', '.', '0', '.', '0', '\0', '\0', '\0'
255+};
256+ 
128} // namespace rawheap_translate257} // namespace rawheap_translate
129#endif // RAWHEAP_TRANSLATE_COMMON_H258#endif // RAWHEAP_TRANSLATE_COMMON_H
Mecmascript/dfx/hprof/rawheap_translate/main.cpp+85-46
@@ -13,78 +13,117 @@
13 * limitations under the License.13 * limitations under the License.
14 */14 */
15 15 
16-#include "ecmascript/dfx/hprof/rawheap_translate/metadata_parse.h"16+#include "metadata_parse.h"
17-#include "ecmascript/dfx/hprof/rawheap_translate/rawheap_translate.h"17+#include "rawheap_translate.h"
18-#include "ecmascript/dfx/hprof/rawheap_translate/serializer.h"18+#include "serializer.h"
19+#include "utils.h"
20+ 
21+#include <cstdlib>
19 22 
20namespace rawheap_translate {23namespace rawheap_translate {
24+// Argument index constants for argv positional access.
25+static constexpr int ARG_INDEX_INPUT = 1; // argv[1]: first .rawheap (single or dynamic)
26+static constexpr int ARG_INDEX_SINGLE_OUTPUT = 2; // argv[2]: single-file optional .heapsnapshot
27+static constexpr int ARG_INDEX_STATIC = 2; // argv[2]: static .rawheap (two-file mode)
28+static constexpr int ARG_INDEX_TWO_FILE_OUTPUT = 3; // argv[3]: two-file optional .heapsnapshot
29+ 
30+// Minimum argc thresholds for each mode.
31+static constexpr int MIN_ARGC_SINGLE = 2; // single-file: <rawheap>
32+static constexpr int MIN_ARGC_SINGLE_OUTPUT = 3; // single-file + output: <rawheap> <heapsnapshot>
33+static constexpr int MIN_ARGC_TWO_FILE = 3; // two-file: <dynamic> <static>
34+static constexpr int MIN_ARGC_TWO_FILE_OUTPUT = 4; // two-file + output: <dynamic> <static> <heapsnapshot>
35+ 
21std::string RAWHEAP_TRANSLATE_HELPER =36std::string RAWHEAP_TRANSLATE_HELPER =
22- "Usage: rawheap_translator <filename.rawheap> <filename.heapsnapshot>\n"37+ "Usage:\n"
23- "at least 1 argv provide, you can also extend to include <filename.heapsnapshot>, "38+ " Single-file mode:\n"
24- "if output file not available, an automatic one will be generated after all.";39+ " rawheap_translator <filename.rawheap> [filename.heapsnapshot]\n"
40+ " Two-file hybrid mode:\n"
41+ " rawheap_translator <dynamic.rawheap> <static.rawheap> [filename.heapsnapshot]\n"
42+ "\n"
43+ "In single-file mode, the input .rawheap is translated to a .heapsnapshot.\n"
44+ "In two-file mode, a dynamic (V1/V2) rawheap and a static binary snapshot\n"
45+ "are merged into a single .heapsnapshot.\n"
46+ "If the output file name is not provided, an automatic one will be generated.";
25 47 
26-bool ParseArgs(const int argc, const char **argv, std::string &input, std::string &output)48+// Parse single-file arguments: 1 .rawheap + optional .heapsnapshot.
49+bool ParseArgsSingle(const int argc, const char **argv, std::string &input, std::string &output)
27{50{
28- const int minArgc = 2; // 2: at least 1 argv provide, including <filename.rawheap>51+ std::string rawheapPath = argv[ARG_INDEX_INPUT];
29- const int maxArgc = 3; // 3: also extend to include output file <filename.heapsnapshot>52+ std::string userOutput = (argc >= MIN_ARGC_SINGLE_OUTPUT) ? argv[ARG_INDEX_SINGLE_OUTPUT] : "";
30- if (argc < minArgc || argc > maxArgc) {53+ if (!GenerateOutputNameFromInput(userOutput, output)) {
31- std::cout << "Input error!\n" << RAWHEAP_TRANSLATE_HELPER << std::endl;54+ std::cout << "Generate dump file name failed!\n";
32 return false;55 return false;
33 }56 }
57+ input = rawheapPath;
58+ return true;
59+}
34 60 
35- int newArgc = 1;61+// Parse two-file arguments: 2 .rawheap + optional .heapsnapshot.
36- std::string rawheapPathOrVersionCheck = argv[newArgc];62+bool ParseArgsTwoFile(const int argc, const char **argv, std::string &dynamicInput,
37- if (rawheapPathOrVersionCheck == "--version" || rawheapPathOrVersionCheck == "-v") {63+ std::string &staticInput, std::string &output)
38- std::cout << VERSION.ToString() << std::endl;64+{
65+ if (argc < MIN_ARGC_TWO_FILE) {
39 return false;66 return false;
40 }67 }
41- 68+ std::string dynamicPath = argv[ARG_INDEX_INPUT];
42- if (rawheapPathOrVersionCheck == "--help" || rawheapPathOrVersionCheck == "-h") {69+ std::string staticPath = argv[ARG_INDEX_STATIC];
43- std::cout << RAWHEAP_TRANSLATE_HELPER << std::endl;70+ if (!EndsWith(dynamicPath, ".rawheap") || !EndsWith(staticPath, ".rawheap")) {
44 return false;71 return false;
45 }72 }
46- 73+ std::string userOutput = (argc >= MIN_ARGC_TWO_FILE_OUTPUT) ? argv[ARG_INDEX_TWO_FILE_OUTPUT] : "";
47- if (!EndsWith(rawheapPathOrVersionCheck, ".rawheap")) {74+ if (!GenerateOutputNameFromInput(userOutput, output)) {
48- std::cout << "The second argument must be rawheap file!" << std::endl75+ std::cout << "Generate dump file name failed!\n";
49- << RAWHEAP_TRANSLATE_HELPER << std::endl;
50 return false;76 return false;
51 }77 }
52- 78+ dynamicInput = dynamicPath;
53- newArgc++;79+ staticInput = staticPath;
54- std::string outputPath {};
55- if (newArgc < argc) {
56- outputPath = argv[newArgc];
57- if (!EndsWith(outputPath, ".heapsnapshot")) {
58- std::cout << "The last argument must be heapsnapshot file!" << std::endl
59- << RAWHEAP_TRANSLATE_HELPER << std::endl;
60- return false;
61- }
62- } else {
63- if (!GenerateDumpFileName(outputPath)) {
64- std::cout << "Generate dump file name failed!\n";
65- return false;
66- }
67- }
68- 
69- input = rawheapPathOrVersionCheck;
70- output = outputPath;
71 return true;80 return true;
72}81}
73 82 
74int Main(const int argc, const char **argv)83int Main(const int argc, const char **argv)
75{84{
76- std::string rawheapPath;85+ if (argc < MIN_ARGC_SINGLE) {
77- std::string snapshotPath;86+ std::cout << "Input error!\n" << RAWHEAP_TRANSLATE_HELPER << std::endl;
78- if (!ParseArgs(argc, argv, rawheapPath, snapshotPath)) {
79 return 0;87 return 0;
80 }88 }
81 89 
82- RawHeap::TranslateRawheap(rawheapPath, snapshotPath);90+ std::string firstArg = argv[ARG_INDEX_INPUT];
91+ if (firstArg == "--version" || firstArg == "-v") {
92+ std::cout << VERSION.ToString() << std::endl;
93+ return 0;
94+ }
95+ if (firstArg == "--help" || firstArg == "-h") {
96+ std::cout << RAWHEAP_TRANSLATE_HELPER << std::endl;
97+ return 0;
98+ }
99+ 
100+ // Try two-file mode first (two .rawheap inputs).
101+ std::string dynamicInput;
102+ std::string staticInput;
103+ std::string outputPath;
104+ if (ParseArgsTwoFile(argc, argv, dynamicInput, staticInput, outputPath)) {
105+ return RawHeap::TranslateRawheap(dynamicInput, staticInput, outputPath) ? EXIT_SUCCESS : EXIT_FAILURE;
106+ }
107+ 
108+ // Fall back to single-file mode.
109+ if (!EndsWith(firstArg, ".rawheap")) {
110+ std::cout << "Input error!\n" << RAWHEAP_TRANSLATE_HELPER << std::endl;
111+ return 0;
112+ }
113+ std::string singleInput;
114+ if (!ParseArgsSingle(argc, argv, singleInput, outputPath)) {
115+ std::cout << "Input error!\n" << RAWHEAP_TRANSLATE_HELPER << std::endl;
116+ return 0;
117+ }
118+ RawHeap::TranslateRawheap(singleInput, outputPath);
83 return 0;119 return 0;
84}120}
85 121 
86} // namespace rawheap_translate122} // namespace rawheap_translate
123+ 
124+#ifndef RAWHEAP_TRANSLATOR_UNITTEST
87int main(int argc, const char **argv)125int main(int argc, const char **argv)
88{126{
89 return rawheap_translate::Main(argc, argv);127 return rawheap_translate::Main(argc, argv);
90-}128+}
129+#endif
Mecmascript/dfx/hprof/rawheap_translate/metadata_parse.cpp+1-1
@@ -13,7 +13,7 @@
13 * limitations under the License.13 * limitations under the License.
14 */14 */
15 15 
16-#include "ecmascript/dfx/hprof/rawheap_translate/metadata_parse.h"16+#include "metadata_parse.h"
17#include <algorithm>17#include <algorithm>
18 18 
19namespace rawheap_translate {19namespace rawheap_translate {
Mecmascript/dfx/hprof/rawheap_translate/metadata_parse.h+2-2
@@ -17,8 +17,8 @@
17#define METADATA_JSON_PARSE_H17#define METADATA_JSON_PARSE_H
18 18 
19#include "cJSON.h"19#include "cJSON.h"
20-#include "ecmascript/dfx/hprof/rawheap_translate/common.h"20+#include "common.h"
21-#include "ecmascript/dfx/hprof/rawheap_translate/utils.h"21+#include "utils.h"
22 22 
23namespace rawheap_translate {23namespace rawheap_translate {
24class MetaParser {24class MetaParser {
Mecmascript/dfx/hprof/rawheap_translate/rawheap_translate.cpp+175-16
@@ -13,11 +13,16 @@
13 * limitations under the License.13 * limitations under the License.
14 */14 */
15 15 
16+#include <algorithm>
16#include <chrono>17#include <chrono>
18+#include <cstring>
17#include <sstream>19#include <sstream>
18-#include "ecmascript/dfx/hprof/rawheap_translate/rawheap_translate.h"20+ 
19-#include "ecmascript/dfx/hprof/rawheap_translate/serializer.h"21+#include "rawheap_translate.h"
20-#include "ecmascript/dfx/hprof/rawheap_translate/utils.h"22+#include "serializer.h"
23+#include "static_rawheap_translate.h"
24+#include "snapshot_merger.h"
25+#include "utils.h"
21 26 
22namespace rawheap_translate {27namespace rawheap_translate {
23RawHeap::~RawHeap()28RawHeap::~RawHeap()
@@ -25,14 +30,10 @@ RawHeap::~RawHeap()
25 for (auto node : nodes_) {30 for (auto node : nodes_) {
26 delete node;31 delete node;
27 }32 }
28- 
29 for (auto edge : edges_) {33 for (auto edge : edges_) {
30 delete edge;34 delete edge;
31 }35 }
32- 
33 delete strTable_;36 delete strTable_;
34- nodes_.clear();
35- edges_.clear();
36}37}
37 38 
38bool RawHeap::TranslateRawheap(const std::string &inputPath, const std::string &outputPath)39bool RawHeap::TranslateRawheap(const std::string &inputPath, const std::string &outputPath)
@@ -42,7 +43,39 @@ bool RawHeap::TranslateRawheap(const std::string &inputPath, const std::string &
42 if (!file.Initialize(inputPath)) {43 if (!file.Initialize(inputPath)) {
43 return false;44 return false;
44 }45 }
46+ // Static binary snapshot files carry their own root framework; V1/V2
47+ // rawheap files go through the metadata + trailer + Parse/Translate path.
48+ if (IsStaticSnapshotFormat(file)) {
49+ return TranslateStaticSnapshot(file, inputPath, outputPath, start);
50+ }
51+ return TranslateDynamicRawheap(file, inputPath, outputPath, start);
52+}
45 53 
54+bool RawHeap::TranslateStaticSnapshot(FileReader &file, const std::string &inputPath,
55+ const std::string &outputPath,
56+ std::chrono::steady_clock::time_point start)
57+{
58+ StaticRawheapTranslate parser;
59+ parser.EnableRootFramework();
60+ uint64_t fileSize = FileReader::GetFileSize(inputPath);
61+ if (!parser.Parse(file, fileSize) || !parser.Translate()) {
62+ return false;
63+ }
64+ StreamWriter writer;
65+ if (!writer.Initialize(outputPath)) {
66+ return false;
67+ }
68+ HeapSnapshotJSONSerializer::Serialize(&parser, &writer);
69+ auto end = std::chrono::steady_clock::now();
70+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
71+ LOG_INFO_ << "file save to " << outputPath << ", cost " << duration << "ms";
72+ return true;
73+}
74+ 
75+bool RawHeap::TranslateDynamicRawheap(FileReader &file, const std::string &inputPath,
76+ const std::string &outputPath,
77+ std::chrono::steady_clock::time_point start)
78+{
46 uint64_t fileSize = FileReader::GetFileSize(inputPath);79 uint64_t fileSize = FileReader::GetFileSize(inputPath);
47 if (!file.CheckAndGetHeaderAt(fileSize - sizeof(uint64_t), 0)) {80 if (!file.CheckAndGetHeaderAt(fileSize - sizeof(uint64_t), 0)) {
48 LOG_ERROR_ << "Read rawheap file header failed!";81 LOG_ERROR_ << "Read rawheap file header failed!";
@@ -78,8 +111,117 @@ bool RawHeap::TranslateRawheap(const std::string &inputPath, const std::string &
78 HeapSnapshotJSONSerializer::Serialize(rawheap, &writer);111 HeapSnapshotJSONSerializer::Serialize(rawheap, &writer);
wanghuan2022
wanghuan2022wanghuan20227月8日

rawheap_translate.cpp:108 — 日志单位不一致

TranslateDynamicRawheap 日志写 cost << std::to_string(duration) << 's',但 durationmilliseconds 计数(std::chrono::duration_cast<std::chrono::milliseconds>),单位标为 's'(秒)是错误的。

建议:

  • 改为 cost << duration << "ms"
  • 或将 duration 转换为秒
likedislike
yangxiaoshuai2022
yangxiaoshuai2022
7月28日 评论:
79 delete rawheap;112 delete rawheap;
80 auto end = std::chrono::steady_clock::now();113 auto end = std::chrono::steady_clock::now();
81- int duration = (int)std::chrono::duration<double>(end - start).count();114+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
82- LOG_INFO_ << "file save to " << outputPath << ", cost " << std::to_string(duration) << 's';115+ LOG_INFO_ << "file save to " << outputPath << ", cost " << duration << "ms";
116+ return true;
117+}
118+ 
119+bool RawHeap::IsStaticSnapshotFormat(FileReader &file)
120+{
121+ std::string versionString = ReadVersion(file);
122+ (void)file.Seek(0);
123+ if (versionString.empty()) {
124+ return false;
125+ }
126+ Version version;
127+ return version.Parse(versionString) && version.GetMajor() == STATIC_SNAPSHOT_MAJOR_VERSION;
128+}
129+ 
130+// Parse + translate the dynamic (V1/V2 rawheap) side of a two-file merge.
131+// Returns an owning pointer on success (caller deletes), nullptr on failure.
132+RawHeap *RawHeap::ParseDynamicRawheap(const std::string &dynamicPath)
133+{
134+ FileReader dynamicFile;
135+ if (!dynamicFile.Initialize(dynamicPath)) {
136+ LOG_ERROR_ << "failed to open dynamic rawheap file: " << dynamicPath;
137+ return nullptr;
138+ }
139+ uint64_t dynamicFileSize = FileReader::GetFileSize(dynamicPath);
140+ if (!dynamicFile.CheckAndGetHeaderAt(dynamicFileSize - sizeof(uint64_t), 0)) {
141+ LOG_ERROR_ << "dynamic rawheap file header check failed";
142+ return nullptr;
143+ }
144+ if (IsStaticSnapshotFormat(dynamicFile)) {
145+ LOG_ERROR_ << "dynamic file is a static binary snapshot; two-file mode "
146+ "expects a V1/V2 rawheap for the dynamic side";
147+ return nullptr;
148+ }
149+ MetaParser metaParser;
150+ if (!ParseMetaData(dynamicFile, &metaParser)) {
151+ return nullptr;
152+ }
153+ Version version;
154+ if (!version.Parse(RawHeap::ReadVersion(dynamicFile))) {
155+ return nullptr;
156+ }
157+ RawHeap *dynamic = ParseRawheap(version, &metaParser);
158+ if (dynamic == nullptr) {
159+ return nullptr;
160+ }
161+ if (!dynamic->Parse(dynamicFile, dynamicFile.GetHeaderLeft()) || !dynamic->Translate()) {
162+ delete dynamic;
163+ return nullptr;
164+ }
165+ return dynamic;
166+}
167+ 
168+// Parse the static binary snapshot file into the provided parser. The path
169+// must be non-empty; returns false (and logs) on open/format/parse failure.
170+bool RawHeap::ParseStaticSnapshot(const std::string &staticPath, StaticRawheapTranslate &staticParser)
171+{
172+ FileReader staticFile;
173+ if (!staticFile.Initialize(staticPath)) {
174+ LOG_ERROR_ << "failed to open static snapshot file: " << staticPath;
175+ return false;
176+ }
177+ if (!IsStaticSnapshotFormat(staticFile)) {
178+ LOG_ERROR_ << "static file is not a static binary snapshot";
179+ return false;
180+ }
181+ uint64_t staticFileSize = FileReader::GetFileSize(staticPath);
182+ return staticParser.Parse(staticFile, staticFileSize);
183+}
184+ 
185+bool RawHeap::TranslateRawheap(
186+ const std::string &dynamicPath,
187+ const std::string &staticPath,
188+ const std::string &outputPath)
189+{
190+ auto start = std::chrono::steady_clock::now();
191+ 
192+ // 1. Parse + translate the dynamic (V1/V2 rawheap) file.
193+ RawHeap *dynamic = ParseDynamicRawheap(dynamicPath);
194+ if (dynamic == nullptr) {
195+ return false;
196+ }
197+ 
198+ // 2-3. Parse the static snapshot (if provided) and merge it in.
199+ if (!staticPath.empty()) {
200+ StaticRawheapTranslate staticParser;
201+ if (!ParseStaticSnapshot(staticPath, staticParser)) {
202+ delete dynamic;
203+ return false;
204+ }
205+ SnapshotMerger merger;
206+ if (!merger.Merge(*dynamic, staticParser)) {
207+ delete dynamic;
208+ return false;
209+ }
210+ }
211+ 
212+ // 4. Serialize the (merged) graph and free the owning dynamic pointer.
213+ StreamWriter writer;
214+ if (!writer.Initialize(outputPath)) {
215+ LOG_ERROR_ << "failed to initialize output writer: " << outputPath;
216+ delete dynamic;
217+ return false;
218+ }
219+ HeapSnapshotJSONSerializer::Serialize(dynamic, &writer);
220+ delete dynamic;
221+ 
222+ auto end = std::chrono::steady_clock::now();
223+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
224+ LOG_INFO_ << "hybrid file saved to " << outputPath << ", cost " << duration << "ms";
83 return true;225 return true;
84}226}
85 227 
@@ -218,6 +360,27 @@ void RawHeap::InsertEdge(Node *toNode, uint32_t indexOrStrId, EdgeType type)
218 edges_.push_back(edge);360 edges_.push_back(edge);
219}361}
220 362 
363+void RawHeap::InsertEdge(Node *fromNode, Node *toNode, uint32_t indexOrStrId, EdgeType type)
364+{
365+ Edge *edge = new Edge(fromNode, toNode, indexOrStrId, type);
366+ edges_.push_back(edge);
367+}
368+ 
369+void RawHeap::SortEdgesByFrom()
370+{
371+ // Stable sort by source node index so the flat edges_ vector becomes
372+ // grouped: [node0's edges, node1's edges, ...]. The .heapsnapshot format
373+ // requires this (node i owns the next edgeCount[i] edges). Edges with
374+ // from==nullptr (V1/V2 path, already grouped) sort first and keep relative
375+ // order due to stable_sort.
376+ std::stable_sort(edges_.begin(), edges_.end(),
377+ [](const Edge *a, const Edge *b) {
378+ uint32_t ia = (a->from != nullptr) ? a->from->index : 0;
379+ uint32_t ib = (b->from != nullptr) ? b->from->index : 0;
380+ return ia < ib;
381+ });
382+}
383+ 
221StringId RawHeap::InsertAndGetStringId(const std::string &str)384StringId RawHeap::InsertAndGetStringId(const std::string &str)
222{385{
223 return strTable_->InsertStrAndGetStringId(str);386 return strTable_->InsertStrAndGetStringId(str);
@@ -244,10 +407,6 @@ void RawHeap::CreateHashEdge(Node *node)
244 InsertEdge(hashNode, hashStrId, EdgeType::DEFAULT);407 InsertEdge(hashNode, hashStrId, EdgeType::DEFAULT);
245 primitiveNodes_.push_back(hashNode);408 primitiveNodes_.push_back(hashNode);
246 node->edgeCount++;409 node->edgeCount++;
247- 
248-#ifdef OHOS_UNIT_TEST
249- hashSet_.insert(hash);
250-#endif
251}410}
252 411 
253void RawHeap::AddPrimitiveNodes()412void RawHeap::AddPrimitiveNodes()
@@ -271,7 +430,7 @@ void RawHeap::CreateRootNode(Node *root, const std::string &name, size_t count)
271void RawHeap::CreateMetadataNode(Node *metadataNode)430void RawHeap::CreateMetadataNode(Node *metadataNode)
272{431{
273 metadataNode->nodeId = 0;432 metadataNode->nodeId = 0;
274- metadataNode->type = 8; // 8 is native nodetype433+ metadataNode->type = DEFAULT_NODETYPE;
275 metadataNode->strId = InsertAndGetStringId("HeapMetadata");434 metadataNode->strId = InsertAndGetStringId("HeapMetadata");
276 metadataNode->edgeCount = 0;435 metadataNode->edgeCount = 0;
277 metadataNode->size = VIRTUAL_NODE_SIZE;436 metadataNode->size = VIRTUAL_NODE_SIZE;
@@ -794,7 +953,7 @@ void RawHeapTranslateV1::AddSyntheticRootNode(std::vector<uint64_t> &roots)
794{953{
795 Node *syntheticRoot = CreateNode();954 Node *syntheticRoot = CreateNode();
796 syntheticRoot->nodeId = 1; // 1: means root node955 syntheticRoot->nodeId = 1; // 1: means root node
797- syntheticRoot->type = 9; // 9: means SYNTHETIC node type956+ syntheticRoot->type = SYNTHETIC_NODETYPE; // SYNTHETIC node type
798 syntheticRoot->strId = InsertAndGetStringId("SyntheticRoot");957 syntheticRoot->strId = InsertAndGetStringId("SyntheticRoot");
799 syntheticRoot->edgeCount = roots.size();958 syntheticRoot->edgeCount = roots.size();
800 959 
@@ -1661,7 +1820,7 @@ void RawHeapTranslateV2::AddHandleRootEdges(const std::vector<uint32_t> &handleR
1661void RawHeapTranslateV2::AddSyntheticRootNode(std::vector<uint32_t> &roots)1820void RawHeapTranslateV2::AddSyntheticRootNode(std::vector<uint32_t> &roots)
1662{1821{
1663 syntheticRoot_->nodeId = 1; // 1: means root node1822 syntheticRoot_->nodeId = 1; // 1: means root node
1664- syntheticRoot_->type = 9; // 9: means SYNTHETIC node type1823+ syntheticRoot_->type = SYNTHETIC_NODETYPE; // SYNTHETIC node type
1665 syntheticRoot_->strId = InsertAndGetStringId("SyntheticRoot");1824 syntheticRoot_->strId = InsertAndGetStringId("SyntheticRoot");
1666 syntheticRoot_->edgeCount = roots.size();1825 syntheticRoot_->edgeCount = roots.size();
1667 StringId strId = InsertAndGetStringId("-subroot-");1826 StringId strId = InsertAndGetStringId("-subroot-");
Mecmascript/dfx/hprof/rawheap_translate/rawheap_translate.h+57-10
@@ -16,9 +16,11 @@
16#ifndef RAWHEAP_TRANSLATE_H16#ifndef RAWHEAP_TRANSLATE_H
17#define RAWHEAP_TRANSLATE_H17#define RAWHEAP_TRANSLATE_H
18 18 
19-#include "ecmascript/dfx/hprof/rawheap_translate/common.h"19+#include <chrono>
20-#include "ecmascript/dfx/hprof/rawheap_translate/metadata_parse.h"20+ 
21-#include "ecmascript/dfx/hprof/rawheap_translate/string_hashmap.h"21+#include "common.h"
22+#include "metadata_parse.h"
23+#include "string_hashmap.h"
22 24 
23namespace panda::test {25namespace panda::test {
24class HeapDumpTestHelper;26class HeapDumpTestHelper;
@@ -27,6 +29,8 @@ class RawHeapTranslateV2TestHelper;
27};29};
28 30 
29namespace rawheap_translate {31namespace rawheap_translate {
32+class SnapshotMerger;
33+class StaticRawheapTranslate;
30class RawHeap {34class RawHeap {
31public:35public:
32 RawHeap() : strTable_(new StringHashMap())36 RawHeap() : strTable_(new StringHashMap())
@@ -40,10 +44,28 @@ public:
40 virtual bool Translate() = 0;44 virtual bool Translate() = 0;
41 45 
42 static bool TranslateRawheap(const std::string &inputPath, const std::string &outputPath);46 static bool TranslateRawheap(const std::string &inputPath, const std::string &outputPath);
47+ 
48+ /**
49+ * @brief Translate a dynamic (V1/V2 rawheap) file plus a static binary
50+ * snapshot file into one combined .heapsnapshot.
51+ *
52+ * The dynamic file is parsed+translated by the existing V1/V2 toolchain;
53+ * the static file is parsed by StaticRawheapTranslate; the two graphs are
54+ * merged by SnapshotMerger (string pools deduplicated by content, object
55+ * addresses live in disjoint virtual-machine spaces, XRef edges spliced
56+ * across virtual-machine boundaries).
57+ */
58+ static bool TranslateRawheap(
59+ const std::string &dynamicPath,
60+ const std::string &staticPath,
61+ const std::string &outputPath);
43 static bool ParseMetaData(FileReader &file, MetaParser *parser);62 static bool ParseMetaData(FileReader &file, MetaParser *parser);
44 static RawHeap *ParseRawheap(const Version &version, MetaParser *metaParser);63 static RawHeap *ParseRawheap(const Version &version, MetaParser *metaParser);
45 static std::string ReadVersion(FileReader &file);64 static std::string ReadVersion(FileReader &file);
46 65 
66+ /** Read-only probe: true for a static/hybrid V3 rawheap. */
67+ static bool IsStaticSnapshotFormat(FileReader &file);
68+ 
47 std::vector<Node *>* GetNodes();69 std::vector<Node *>* GetNodes();
48 std::vector<Edge *>* GetEdges();70 std::vector<Edge *>* GetEdges();
49 size_t GetNodeCount();71 size_t GetNodeCount();
@@ -51,15 +73,26 @@ public:
51 StringHashMap* GetStringTable();73 StringHashMap* GetStringTable();
52 std::string GetVersion();74 std::string GetVersion();
53 75 
76+ // Graph-construction primitives. Declared public so that the merger and
77+ // tests can build/extend a graph without subclassing; the V1/V2 parsers
78+ // and the static parser also use them internally.
79+ Node *CreateNode();
80+ StringId InsertAndGetStringId(const std::string &str);
81+ 
54 std::string spaceType_;82 std::string spaceType_;
55 std::string heapType_;83 std::string heapType_;
56 std::string vmType_;84 std::string vmType_;
57 85 
58protected:86protected:
59- Node *CreateNode();
60 Node *CreateNodeAt(size_t pos);87 Node *CreateNodeAt(size_t pos);
61 void InsertEdge(Node *toNode, uint32_t indexOrStrId, EdgeType type);88 void InsertEdge(Node *toNode, uint32_t indexOrStrId, EdgeType type);
62- StringId InsertAndGetStringId(const std::string &str);89+ // Variant that records the source node on the edge (edge->from). Used by
90+ // the static parser so SortEdgesByFrom can group edges by source node.
91+ void InsertEdge(Node *fromNode, Node *toNode, uint32_t indexOrStrId, EdgeType type);
92+ // Sort edges_ by source node index (edge->from->index), stable. Required by
93+ // the .heapsnapshot grouping contract (node i owns the next edgeCount[i]
94+ // edges). Edges with from==nullptr sort first. Call after BuildGraph.
95+ void SortEdgesByFrom();
63 void SetVersion(const std::string &version);96 void SetVersion(const std::string &version);
64 void CreateHashEdge(Node *node);97 void CreateHashEdge(Node *node);
65 void AddPrimitiveNodes();98 void AddPrimitiveNodes();
@@ -71,6 +104,23 @@ protected:
71 void DoAddGlobalHandleObjectNodes(const std::unordered_map<uint64_t, uint64_t> &globalRefEntries);104 void DoAddGlobalHandleObjectNodes(const std::unordered_map<uint64_t, uint64_t> &globalRefEntries);
72 105 
73 static bool ReadSectionInfo(FileReader &file, uint32_t offset, std::vector<uint32_t> &section);106 static bool ReadSectionInfo(FileReader &file, uint32_t offset, std::vector<uint32_t> &section);
107+ // Two-file (hybrid) helpers: split out of TranslateRawheap(dynamic, static, out)
108+ // so each step stays small. ParseDynamicRawheap returns an owning pointer
109+ // (caller deletes); ParseStaticSnapshot fills the provided parser. Both log
110+ // the failure reason and return nullptr/false on error.
111+ static RawHeap *ParseDynamicRawheap(const std::string &dynamicPath);
112+ static bool ParseStaticSnapshot(const std::string &staticPath, StaticRawheapTranslate &staticParser);
113+ 
114+ // Single-file helpers: split out of TranslateRawheap(input, out) so each
115+ // format path stays small. Both consume the already-opened `file` (its
116+ // cursor is left at 0 after the IsStaticSnapshotFormat probe) and measure
117+ // duration from `start`, which the caller captures before opening the file.
118+ static bool TranslateStaticSnapshot(FileReader &file, const std::string &inputPath,
119+ const std::string &outputPath,
120+ std::chrono::steady_clock::time_point start);
121+ static bool TranslateDynamicRawheap(FileReader &file, const std::string &inputPath,
122+ const std::string &outputPath,
123+ std::chrono::steady_clock::time_point start);
74 124 
75 std::unordered_map<StringId, uint64_t> refAddrStrIdMap_ {}; // strId -> refAddr (virtual nodes)125 std::unordered_map<StringId, uint64_t> refAddrStrIdMap_ {}; // strId -> refAddr (virtual nodes)
76 126 
@@ -82,11 +132,8 @@ private:
82 std::string version_;132 std::string version_;
wanghuan2022
wanghuan2022wanghuan20227月8日

rawheap_translate.h:128 — hashSet_ 从 #ifdef OHOS_UNIT_TEST 移出,生产构建承担不必要的内存开销

std::unordered_set<uint32_t> hashSet_ 原仅在单元测试构建中存在,现在无条件包含。每次 CreateHashEdge 都执行 hashSet_.insert(hash),生产路径中 hashSet_ 只写不读。对百万级 edge 的 dump 增加约 8MB 无用内存和 CPU 开销。

建议:

  • 保留 #ifdef OHOS_UNIT_TEST 包裹
  • 或改为 #if defined(OHOS_UNIT_TEST) || defined(ARK_DEBUG) 等调试宏
  • 生产构建不应承担此开销
likedislike
yangxiaoshuai2022
yangxiaoshuai2022
28 天前 评论:
83 uint32_t nodeIndex_ {0};133 uint32_t nodeIndex_ {0};
84 134 
85-#ifdef OHOS_UNIT_TEST
86- std::unordered_set<uint32_t> hashSet_ {};
87-#endif
88- 
89 friend class panda::test::HeapDumpTestHelper;135 friend class panda::test::HeapDumpTestHelper;
136+ friend class SnapshotMerger;
90};137};
91 138 
92class RawHeapTranslateV1 : public RawHeap {139class RawHeapTranslateV1 : public RawHeap {
@@ -247,4 +294,4 @@ private:
247 friend class panda::test::RawHeapTranslateV2TestHelper;294 friend class panda::test::RawHeapTranslateV2TestHelper;
248};295};
249} // namespace rawheap_translate296} // namespace rawheap_translate
250-#endif // RAWHEAP_TRANSLATE_H297+#endif // RAWHEAP_TRANSLATE_H
Mecmascript/dfx/hprof/rawheap_translate/serializer.cpp+13-8
@@ -13,7 +13,8 @@
13 * limitations under the License.13 * limitations under the License.
14 */14 */
15 15 
16-#include "ecmascript/dfx/hprof/rawheap_translate/serializer.h"16+#include "serializer.h"
17+#include "securec.h"
17 18 
18namespace rawheap_translate {19namespace rawheap_translate {
19bool StreamWriter::Initialize(const std::string &filePath)20bool StreamWriter::Initialize(const std::string &filePath)
@@ -100,7 +101,7 @@ void HeapSnapshotJSONSerializer::SerializeSnapshotHeader(RawHeap *rawheap, Strea
100 // NOLINTNEXTLINE(modernize-raw-string-literal)101 // NOLINTNEXTLINE(modernize-raw-string-literal)
101 writer->WriteString("\"number\",\"native\",\"synthetic\",\"concatenated string\",\"slicedstring\",\"symbol\",");102 writer->WriteString("\"number\",\"native\",\"synthetic\",\"concatenated string\",\"slicedstring\",\"symbol\",");
102 // NOLINTNEXTLINE(modernize-raw-string-literal)103 // NOLINTNEXTLINE(modernize-raw-string-literal)
103- writer->WriteString("\"bigint\",\"framework\",\"handle\"],");104+ writer->WriteString("\"bigint\",\"framework\",\"handle\",\"class\"],");
104 // NOLINTNEXTLINE(modernize-raw-string-literal)105 // NOLINTNEXTLINE(modernize-raw-string-literal)
105 writer->WriteString("\"string\",\"number\",\"number\",\"number\",\"number\",\"number\",\"number\"],\n"); // 4.106 writer->WriteString("\"string\",\"number\",\"number\",\"number\",\"number\",\"number\",\"number\"],\n"); // 4.
106 // NOLINTNEXTLINE(modernize-raw-string-literal)107 // NOLINTNEXTLINE(modernize-raw-string-literal)
@@ -108,7 +109,10 @@ void HeapSnapshotJSONSerializer::SerializeSnapshotHeader(RawHeap *rawheap, Strea
108 // NOLINTNEXTLINE(modernize-raw-string-literal)109 // NOLINTNEXTLINE(modernize-raw-string-literal)
109 writer->WriteString("\"edge_types\":[[\"context\",\"element\",\"property\",\"internal\",\"hidden\",\"shortcut\",");110 writer->WriteString("\"edge_types\":[[\"context\",\"element\",\"property\",\"internal\",\"hidden\",\"shortcut\",");
110 // NOLINTNEXTLINE(modernize-raw-string-literal)111 // NOLINTNEXTLINE(modernize-raw-string-literal)
111- writer->WriteString("\"weak\"],\"string_or_number\",\"node\"],\n"); // 6.112+ writer->WriteString("\"weak\",\"xref\"],\"string_or_number\",\"node\"],\n"); // 6.
113+ // "xref" at index 7 mirrors EdgeType::XREF (common.h) — without it, any
114+ // cross-VM xref edge emitted by the hybrid merger would carry an
115+ // out-of-range type field in the .heapsnapshot meta.
112 // NOLINTNEXTLINE(modernize-raw-string-literal)116 // NOLINTNEXTLINE(modernize-raw-string-literal)
113 writer->WriteString("\"trace_function_info_fields\":[\"function_id\",\"name\",\"script_name\",\"script_id\",");117 writer->WriteString("\"trace_function_info_fields\":[\"function_id\",\"name\",\"script_name\",\"script_id\",");
114 // NOLINTNEXTLINE(modernize-raw-string-literal)118 // NOLINTNEXTLINE(modernize-raw-string-literal)
@@ -193,7 +197,7 @@ void HeapSnapshotJSONSerializer::SerializeStringTable(RawHeap *rawheap, StreamWr
193 writer->WriteString("\"\",\n");197 writer->WriteString("\"\",\n");
194 writer->WriteString("\"GC roots\",\n");198 writer->WriteString("\"GC roots\",\n");
195 // StringId Range from 3199 // StringId Range from 3
196- size_t capcity = stringTable->GetCapcity();200+ size_t capcity = stringTable->GetCapacity();
197 if (capcity <= 0) {201 if (capcity <= 0) {
198 return;202 return;
199 }203 }
@@ -220,6 +224,7 @@ void HeapSnapshotJSONSerializer::SerializeString(const char *str, StreamWriter *
220 }224 }
221 const char *s = str;225 const char *s = str;
222 while (*s != '\0') {226 while (*s != '\0') {
227+ const auto ch = static_cast<unsigned char>(*s);
223 if (*s == '\"' || *s == '\\') {228 if (*s == '\"' || *s == '\\') {
224 writer->WriteChar('\\');229 writer->WriteChar('\\');
225 writer->WriteChar(*s);230 writer->WriteChar(*s);
@@ -239,12 +244,12 @@ void HeapSnapshotJSONSerializer::SerializeString(const char *str, StreamWriter *
239 } else if (*s == '\t') {244 } else if (*s == '\t') {
240 writer->WriteString("\\t");245 writer->WriteString("\\t");
241 s++;246 s++;
242- } else if (*s > ASCII_US && *s < ASCII_DEL) {247+ } else if (ch > ASCII_US && ch < ASCII_DEL) {
243 writer->WriteChar(*s);248 writer->WriteChar(*s);
244 s++;249 s++;
245- } else if (*s <= ASCII_US || *s == ASCII_DEL) {250+ } else if (ch <= ASCII_US || ch == ASCII_DEL) {
246- // special char convert to \u unicode251+ // Escape JSON control characters.
247- SerializeUnicodeChar(static_cast<uint32_t>(*s), writer);252+ SerializeUnicodeChar(ch, writer);
248 s++;253 s++;
249 } else {254 } else {
250 writer->WriteChar(*s);255 writer->WriteChar(*s);
Mecmascript/dfx/hprof/rawheap_translate/serializer.h+2-2
@@ -17,8 +17,8 @@
17#define RAWHEAP_TRANSLATE_SERIALIZER_H17#define RAWHEAP_TRANSLATE_SERIALIZER_H
18 18 
19#define NODE_FIELD_COUNT 819#define NODE_FIELD_COUNT 8
20-#include "ecmascript/dfx/hprof/rawheap_translate/rawheap_translate.h"20+#include "rawheap_translate.h"
21-#include "ecmascript/dfx/hprof/rawheap_translate/utils.h"21+#include "utils.h"
22 22 
23 23 
24namespace rawheap_translate {24namespace rawheap_translate {
Aecmascript/dfx/hprof/rawheap_translate/snapshot_merger.cpp+299-0
@@ -0,0 +1,299 @@
1+/*
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+#include "snapshot_merger.h"
17+ 
18+#include <unordered_set>
19+ 
20+namespace rawheap_translate {
21+ 
22+// ---- Merge (main entry point) ----
23+ 
24+bool SnapshotMerger::Merge(RawHeap &dynamic, StaticRawheapTranslate &sp)
25+{
26+ strIdMap_.clear();
27+ duplicateNodeIds_ = 0;
28+ xrefUnresolved_ = 0;
29+ auto *targetNodes = dynamic.GetNodes();
30+ if (targetNodes->empty()) {
31+ LOG_ERROR_ << "Merge: dynamic graph is empty";
32+ return false;
33+ }
34+ 
35+ auto bucket = BucketExistingEdges(dynamic);
36+ auto dynamicNodeIndex = BuildDynNodeIndex(dynamic);
37+ AppendStaticNodesAndEdges(dynamic, sp, bucket);
38+ WireStaticRoot(dynamic, sp, bucket);
39+ bool xrefValid = SpliceXRefEdges(dynamic, sp, dynamicNodeIndex, bucket);
40+ FlattenAndRenumber(dynamic, bucket);
41+ 
42+ if (!xrefValid) {
43+ return false;
44+ }
45+ 
46+ LOG_INFO_ << "Merge completed: nodes=" << dynamic.GetNodeCount() << " edges=" << dynamic.GetEdgeCount();
47+ if (duplicateNodeIds_ > 0 || xrefUnresolved_ > 0) {
48+ LOG_INFO_ << "Merge warnings: duplicateNodeIds=" << duplicateNodeIds_ << " xrefUnresolved=" << xrefUnresolved_;
49+ }
50+ return true;
51+}
52+ 
53+// ---- Phase 1 ----
54+ 
55+SnapshotMerger::EdgeBucket SnapshotMerger::BucketExistingEdges(RawHeap &dynamic)
56+{
57+ auto *targetNodes = dynamic.GetNodes();
58+ auto *targetEdges = dynamic.GetEdges();
59+ EdgeBucket bucket;
60+ size_t pos = 0;
61+ for (Node *node : *targetNodes) {
62+ auto &list = bucket[node];
63+ uint32_t cnt = node->edgeCount;
64+ for (uint32_t i = 0; i < cnt && pos < targetEdges->size(); ++i, ++pos) {
65+ list.push_back((*targetEdges)[pos]);
66+ }
67+ }
68+ return bucket;
69+}
70+ 
71+// ---- Phase 2 ----
72+ 
73+void SnapshotMerger::AppendStaticNodesAndEdges(RawHeap &dynamic, StaticRawheapTranslate &sp, EdgeBucket &bucket)
74+{
75+ auto *targetNodes = dynamic.GetNodes();
76+ auto *staNodes = sp.GetNodes();
77+ auto *staEdges = sp.GetEdges();
78+ 
79+ size_t pos = 0;
80+ for (Node *node : *staNodes) {
81+ // Remap the node's own type-name string id into the target pool.
82+ node->strId = RemapStrId(dynamic, sp, node->strId);
83+ targetNodes->push_back(node);
84+ 
85+ auto &list = bucket[node];
86+ uint32_t cnt = node->edgeCount;
87+ for (uint32_t i = 0; i < cnt && pos < staEdges->size(); ++i, ++pos) {
88+ Edge *edge = (*staEdges)[pos];
89+ if (EdgeUsesStringId(edge->type)) {
90+ edge->nameOrIndex = RemapStrId(dynamic, sp, edge->nameOrIndex);
91+ }
92+ list.push_back(edge);
93+ }
94+ }
95+ // Static edges/nodes are now owned by the target; detach from the static
96+ // parser so its destructor does not double-free them.
97+ staNodes->clear();
98+ staEdges->clear();
99+}
100+ 
101+// ---- Phase 3 ----
102+ 
103+void SnapshotMerger::WireStaticRoot(RawHeap &dynamic, StaticRawheapTranslate &sp, EdgeBucket &bucket)
104+{
105+ auto *targetNodes = dynamic.GetNodes();
106+ // The dynamic synthetic root is nodes_[0] (built by V1/V2 Translate).
107+ Node *syntheticRoot = (*targetNodes)[0];
108+ 
109+ Node *staticRoot = dynamic.CreateNode();
110+ dynamic.CreateRootNode(staticRoot, "StaticRoot", sp.GetRoots().size());
111+ // Note: CreateNode already pushed staticRoot into targetNodes
112+ // (dynamic.nodes_), so no second push_back is needed.
113+ 
114+ StringId subrootStrId = dynamic.InsertAndGetStringId("-subroot-");
115+ bucket[syntheticRoot].push_back(new Edge(staticRoot, subrootStrId, EdgeType::SHORTCUT));
116+ syntheticRoot->edgeCount++;
117+ 
118+ uint32_t index = 0;
119+ uint32_t skipped = 0;
120+ for (uint32_t nodeId : sp.GetRoots()) {
121+ Node *root = sp.FindNodeByNodeId(nodeId);
122+ if (root == nullptr) {
123+ skipped++;
124+ continue;
125+ }
126+ bucket[staticRoot].push_back(new Edge(root, index++, EdgeType::ELEMENT));
127+ staticRoot->edgeCount++;
128+ }
129+ if (skipped > 0) {
130+ LOG_INFO_ << "WireStaticRoot: " << skipped << " root nodeIds had no matching node";
131+ }
132+}
133+ 
134+// ---- Phase 4 ----
135+ 
136+bool SnapshotMerger::SpliceXRefEdges(RawHeap &dynamic, StaticRawheapTranslate &sp, const NodeIndex &dynamicNodeIndex,
137+ EdgeBucket &bucket)
138+{
139+ XRefEdgeStrings strs = {dynamic.InsertAndGetStringId("xref_dyn_sta"), dynamic.InsertAndGetStringId("xref_sta_dyn"),
140+ dynamic.InsertAndGetStringId("xref_bidir")};
141+ uint32_t resolved = 0;
142+ const auto &xrefs = sp.GetXRefs();
143+ for (const auto &x : xrefs) {
144+ // dynNodeId matches a dynamic node's nodeId (see class doc for rationale).
145+ auto it = dynamicNodeIndex.find(x.dynNodeId);
146+ Node *dynNode = (it != dynamicNodeIndex.end()) ? it->second : nullptr;
147+ Node *staNode = sp.FindNodeByNodeId(x.staNodeId);
148+ if (dynNode == nullptr || staNode == nullptr) {
149+ ++xrefUnresolved_;
150+ LOG_INFO_ << "SpliceXRefEdges: skipped dynNodeId=" << x.dynNodeId << " staNodeId=" << x.staNodeId
151+ << " dir=" << static_cast<int>(x.direction);
152+ continue;
153+ }
154+ EmitXRefEdge(x.direction, dynNode, staNode, strs, bucket);
155+ ++resolved;
156+ }
157+ return ValidateXRefResolution(xrefs, resolved);
158+}
159+ 
160+SnapshotMerger::NodeIndex SnapshotMerger::BuildDynNodeIndex(RawHeap &dynamic) const
161+{
162+ // Index the dynamic side by nodeId so dynNodeId resolves directly (see class
163+ // doc).
164+ std::unordered_map<uint64_t, Node *> dynNodeIndex;
165+ auto *dynNodes = dynamic.GetNodes();
166+ for (Node *node : *dynNodes) {
167+ if (node->nodeId != 0) {
168+ dynNodeIndex[node->nodeId] = node;
169+ }
170+ }
171+ return dynNodeIndex;
172+}
173+ 
174+void SnapshotMerger::EmitXRefEdge(uint8_t direction, Node *dynNode, Node *staNode, const XRefEdgeStrings &strs,
175+ EdgeBucket &bucket)
176+{
177+ switch (direction) {
178+ case XREF_DYN_TO_STA:
179+ bucket[dynNode].push_back(new Edge(staNode, strs.dynToSta, EdgeType::XREF));
180+ dynNode->edgeCount++;
181+ break;
182+ case XREF_STA_TO_DYN:
183+ bucket[staNode].push_back(new Edge(dynNode, strs.staToDyn, EdgeType::XREF));
184+ staNode->edgeCount++;
185+ break;
186+ case XREF_BIDIR:
187+ bucket[dynNode].push_back(new Edge(staNode, strs.bidir, EdgeType::XREF));
188+ dynNode->edgeCount++;
189+ bucket[staNode].push_back(new Edge(dynNode, strs.bidir, EdgeType::XREF));
190+ staNode->edgeCount++;
191+ break;
192+ default:
193+ break;
194+ }
195+}
196+ 
197+bool SnapshotMerger::ValidateXRefResolution(const std::vector<StaticRawheapTranslate::XRefRecord> &xrefs,
198+ uint32_t resolved) const
199+{
200+ if (xrefs.empty()) {
201+ return true;
202+ }
203+ if (resolved == 0) {
Petrov Igor
Petrov IgorPetrov Igor27 天前

This detects the all-unresolved XRef case but only logs it; Merge() still succeeds and serializes a graph with no cross-VM edges. The producer emits an XRef only after both endpoint IDs are non-zero, so a non-empty set where resolved == 0 indicates a mismatched or corrupt snapshot pair rather than an acceptable partial result. Please propagate this condition as a merge failure (while continuing to allow an empty XRef set and, if intended, partially resolved sets) and test a mismatched pair.

likedislike
yangxiaoshuai2022
yangxiaoshuai2022
23 天前 评论:
204+ LOG_ERROR_ << "SpliceXRefEdges: 0/" << xrefs.size()
205+ << " XRef records resolved - dynNodeId<->nodeId convention may not hold"
206+ << " (unresolved=" << xrefUnresolved_ << ")";
207+ return false;
208+ }
209+ if (xrefUnresolved_ > 0) {
210+ LOG_INFO_ << "SpliceXRefEdges: " << xrefUnresolved_ << "/" << xrefs.size() << " XRef records unresolved";
211+ }
212+ return true;
213+}
214+ 
215+// ---- Phase 5 ----
216+ 
217+void SnapshotMerger::FlattenAndRenumber(RawHeap &dynamic, EdgeBucket &bucket)
218+{
219+ auto *targetNodes = dynamic.GetNodes();
220+ auto *targetEdges = dynamic.GetEdges();
221+ 
222+ targetEdges->clear();
223+ for (Node *node : *targetNodes) {
224+ auto it = bucket.find(node);
225+ uint32_t cnt = (it == bucket.end()) ? 0 : static_cast<uint32_t>(it->second.size());
226+ node->edgeCount = cnt;
227+ if (it != bucket.end()) {
228+ for (Edge *edge : it->second) {
229+ targetEdges->push_back(edge);
230+ }
231+ }
232+ }
233+ for (uint32_t i = 0; i < targetNodes->size(); ++i) {
234+ (*targetNodes)[i]->index = i;
235+ }
236+ DetectDuplicateNodeIds(*targetNodes);
237+}
238+ 
239+void SnapshotMerger::DetectDuplicateNodeIds(std::vector<Node *> &nodes)
240+{
241+ // Duplicate-nodeId detection. The .heapsnapshot format addresses edges by
242+ // node index, not nodeId, so duplicates do not corrupt serialization - but
243+ // they break the XRef staNodeId<->nodeId convention and any tool that looks
244+ // nodes up by nodeId. Warn only; do not remap (preserves address semantics).
245+ std::unordered_set<uint64_t> seen;
246+ for (Node *node : nodes) {
247+ uint64_t id = node->nodeId;
248+ if (id == 0) {
249+ continue; // 0 is the synthetic root / placeholder, not a real id
250+ }
251+ if (!seen.insert(id).second) {
252+ duplicateNodeIds_++;
253+ LOG_ERROR_ << "FlattenAndRenumber: duplicate nodeId 0x" << std::hex << id << std::dec
254+ << " - XRef resolution by nodeId may be ambiguous";
255+ }
256+ }
257+ if (duplicateNodeIds_ > 0) {
258+ LOG_ERROR_ << "FlattenAndRenumber: " << duplicateNodeIds_ << " duplicate nodeIds detected";
259+ }
260+}
261+ 
262+// ---- String-id remapping helpers ----
263+ 
264+StringId SnapshotMerger::RemapStrId(RawHeap &target, StaticRawheapTranslate &sp, StringId staticStrId)
265+{
266+ // StringIds in the StringHashMap start from CUSTOM_STRID_START (3).
267+ // A value < 3 means the node/edge was never assigned a real string id
268+ // (e.g., a Node whose classAddr was not found in classMap_ during BuildGraph,
269+ // leaving strId at its default value). Treat it as empty string.
270+ if (staticStrId < StringHashMap::CUSTOM_STRID_START) {
271+ return target.InsertAndGetStringId("");
272+ }
273+ auto cached = strIdMap_.find(staticStrId);
274+ if (cached != strIdMap_.end()) {
275+ return cached->second;
276+ }
277+ auto *table = sp.GetStringTable();
278+ StringKey key = table->GetKeyByStringId(staticStrId);
279+ // GetKeyByStringId returns 0 for out-of-range ids (bounds-checked
280+ // internally).
281+ if (key == 0) {
282+ LOG_INFO_ << "RemapStrId: staticStrId=" << staticStrId << " out of range (capacity=" << table->GetCapacity()
283+ << "), falling back to empty string";
284+ return target.InsertAndGetStringId("");
285+ }
286+ std::string content = table->GetStringByKey(key);
287+ // Re-insert by content into the target (dynamic) pool - content-dedup.
288+ StringId newId = target.InsertAndGetStringId(content);
289+ strIdMap_[staticStrId] = newId;
290+ return newId;
291+}
292+ 
293+bool SnapshotMerger::EdgeUsesStringId(EdgeType type)
294+{
295+ return type == EdgeType::CONTEXT || type == EdgeType::PROPERTY || type == EdgeType::INTERNAL ||
296+ type == EdgeType::SHORTCUT || type == EdgeType::WEAK || type == EdgeType::HIDDEN || type == EdgeType::XREF;
297+}
298+ 
299+} // namespace rawheap_translate
Aecmascript/dfx/hprof/rawheap_translate/snapshot_merger.h+132-0
@@ -0,0 +1,132 @@
1+/*
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS of ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+#ifndef RAWHEAP_TRANSLATE_SNAPSHOT_MERGER_H
17+#define RAWHEAP_TRANSLATE_SNAPSHOT_MERGER_H
18+ 
19+#include "rawheap_translate.h"
20+#include "static_rawheap_translate.h"
21+#include <unordered_map>
22+#include <vector>
23+ 
24+namespace rawheap_translate {
25+ 
26+/**
27+ * @brief Merge a static-side StaticRawheapTranslate graph into a dynamic-side
28+ * RawHeap graph, producing one combined .heapsnapshot.
29+ *
30+ * The dynamic side is parsed+translated by the existing V1/V2 machinery
31+ * (dynamic parsing unchanged), which builds a complete graph including its own
32+ * synthetic root. The static side is parsed (object nodes + edges only, no
33+ * synthetic root) by StaticRawheapTranslate; the writer/reader pair and shared
34+ * wire format live in
35+ * runtime_core/static_core/runtime/tooling/hprof/static_dump.h,
36+ * rawheap_translate/common.h, and
37+ * runtime_core/static_core/plugins/ets/runtime/tooling/hprof/session/dump_format.h.
38+ *
39+ * String pools are merged by CONTENT, not by shared id: the two
40+ * virtual machines do not actually share a StringIdPool, so equal strings are
41+ * deduplicated and static string ids are remapped into the dynamic (target)
42+ * string table.
43+ *
44+ * XRef (cross-virtual-machine reference) records use explicit field semantics
45+ * instead of generic from/to addresses: each record carries
46+ * [dynNodeId(u32)][staNodeId(u32)][direction(u8)]. Both endpoints are 4-byte
47+ * nodeIds - the dump side resolves jsAddr->dynNodeId via the dynamic
48+ * participant's GetNodeId (mirroring etsAddr->staNodeId via ObjectIdMap) - so
49+ * the merger resolves both sides by nodeId->Node lookup. This eliminates the
50+ * need for a unified address index across both virtual machines.
51+ *
52+ * The .heapsnapshot format assigns edges to nodes sequentially (an object's
53+ * edges are the next `edgeCount` edges after the previous object's), so the
54+ * merged edge vector must be grouped by source node in node order. We rebuild
55+ * the edge vector: slice each side's edges by source node (using edgeCount),
56+ * bucket new edges (StaticRoot wiring + XRef) onto their source node, then
57+ * flatten in node order.
58+ *
59+ * Merge is a 5-phase process:
60+ * Phase 1 - Bucket existing dynamic edges by source node (using edgeCount).
61+ * Phase 2 - Append static nodes/edges with strId remapping, transfer
62+ * ownership. Phase 3 - Add a StaticRoot group node under the dynamic synthetic
63+ * root. Phase 4 - Splice XRef edges (cross-virtual-machine references from the
64+ * static file). Phase 5 - Flatten edges in node order; renumber node indices.
65+ */
66+class SnapshotMerger {
67+public:
68+ /**
69+ * @brief Merge `staticParser` into `dynamic` (in place).
70+ * @return false when the dynamic graph is empty or a non-empty XRef set
71+ * cannot resolve any record; true otherwise.
72+ */
73+ bool Merge(RawHeap &dynamic, StaticRawheapTranslate &staticParser);
74+ 
75+private:
76+ using EdgeBucket = std::unordered_map<Node *, std::vector<Edge *>>;
77+ using NodeIndex = std::unordered_map<uint64_t, Node *>;
78+ 
79+ // Phase 1: bucket existing dynamic edges by source node (slice by edgeCount).
80+ EdgeBucket BucketExistingEdges(RawHeap &dynamic);
81+ 
82+ // Phase 2: append static nodes + edges, remap strIds into target pool.
83+ // After this call, staticParser's node/edge vectors are empty (ownership
84+ // transferred to the dynamic graph).
85+ void AppendStaticNodesAndEdges(RawHeap &dynamic, StaticRawheapTranslate &sp, EdgeBucket &bucket);
86+ 
87+ // Phase 3: add a StaticRoot group node under the dynamic synthetic root.
88+ void WireStaticRoot(RawHeap &dynamic, StaticRawheapTranslate &sp, EdgeBucket &bucket);
89+ 
90+ // Phase 4: splice XRef edges. Both endpoints are nodeIds resolved by
91+ // nodeId->Node lookup (see class doc for the full rationale).
92+ bool SpliceXRefEdges(RawHeap &dynamic, StaticRawheapTranslate &sp, const NodeIndex &dynamicNodeIndex,
93+ EdgeBucket &bucket);
94+ 
95+ // Phase 4 helpers (kept next to SpliceXRefEdges so decl/impl order match).
96+ // The three XRef edge-name string ids, inserted into the target pool once.
97+ struct XRefEdgeStrings {
98+ StringId dynToSta;
99+ StringId staToDyn;
100+ StringId bidir;
101+ };
102+ // Build a dynNodeId->Node index over the dynamic side only (nodeId != 0).
103+ NodeIndex BuildDynNodeIndex(RawHeap &dynamic) const;
104+ // Emit one resolved XRef edge (or two, for bidirectional) into the bucket.
105+ void EmitXRefEdge(uint8_t direction, Node *dynNode, Node *staNode, const XRefEdgeStrings &strs, EdgeBucket &bucket);
106+ // Validate and log the resolve-rate summary. Empty XRef input and partial
107+ // resolution are valid; a non-empty set with zero resolved records is not.
108+ bool ValidateXRefResolution(const std::vector<StaticRawheapTranslate::XRefRecord> &xrefs, uint32_t resolved) const;
109+ 
110+ // Phase 5: flatten edges in node order; renumber node indices.
111+ void FlattenAndRenumber(RawHeap &dynamic, EdgeBucket &bucket);
112+ // Phase 5 helper: warn on duplicate nodeIds (breaks XRef nodeId lookup).
113+ void DetectDuplicateNodeIds(std::vector<Node *> &nodes);
114+ 
115+ // Remap a static-side StringId into the target (dynamic) string table,
116+ // inserting the string content if new. Cached in strIdMap_.
117+ StringId RemapStrId(RawHeap &target, StaticRawheapTranslate &sp, StringId staticStrId);
118+ 
119+ // ELEMENT edges carry a numeric index in nameOrIndex. All other supported
120+ // edge types, including INTERNAL edges such as "superClass", carry a string
121+ // id.
122+ static bool EdgeUsesStringId(EdgeType type);
123+ 
124+ std::unordered_map<uint32_t, uint32_t> strIdMap_;
125+ 
126+ // Diagnostic counters (detection + warning only, no remap).
127+ uint32_t duplicateNodeIds_ {0}; // nodeIds seen more than once in the merged graph
128+ uint32_t xrefUnresolved_ {0}; // XRef records that failed to resolve either side
129+};
130+ 
131+} // namespace rawheap_translate
132+#endif // RAWHEAP_TRANSLATE_SNAPSHOT_MERGER_H
Aecmascript/dfx/hprof/rawheap_translate/static_rawheap_translate.cpp+1462-0
@@ -0,0 +1,1462 @@
1+/*
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS of ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+#include "static_rawheap_translate.h"
17+ 
18+#include "securec.h"
19+#include <algorithm>
20+#include <array>
21+#include <cstring>
22+#include <iomanip>
23+#include <sstream>
24+#include <string_view>
25+ 
26+namespace rawheap_translate {
27+ 
28+namespace {
29+constexpr uint32_t MAX_RECORD_BODY_SIZE = 256U * 1024U * 1024U;
30+constexpr uint32_t MAX_RECORD_ITEM_COUNT = 64U * 1024U;
31+constexpr uint32_t MAX_STRING_DATA_SIZE = 64U * 1024U * 1024U;
32+constexpr uint32_t SKIP_BUFFER_SIZE = 4096U;
33+constexpr uint16_t UTF16_HIGH_SURROGATE_START = 0xD800U;
34+constexpr uint16_t UTF16_LOW_SURROGATE_END = 0xDFFFU;
35+constexpr uint16_t UTF8_ONE_BYTE_LIMIT = 0x80U;
36+constexpr uint16_t UTF8_TWO_BYTE_LIMIT = 0x800U;
37+constexpr size_t UNICODE_ESCAPE_PREFIX_SIZE = 2;
38+constexpr size_t UTF16_HEX_DIGIT_COUNT = 4;
39+constexpr uint32_t BITS_PER_HEX_DIGIT = 4;
40+constexpr uint16_t HEX_DIGIT_MASK = 0x0FU;
41+constexpr uint32_t UTF8_CONTINUATION_SHIFT = 6;
42+constexpr uint32_t UTF8_THREE_BYTE_SHIFT = 12;
43+constexpr uint16_t UTF8_CONTINUATION_MASK = 0x3FU;
44+constexpr uint16_t UTF8_TWO_BYTE_PREFIX = 0xC0U;
45+constexpr uint16_t UTF8_THREE_BYTE_PREFIX = 0xE0U;
46+constexpr uint16_t UTF8_CONTINUATION_PREFIX = 0x80U;
47+ 
48+constexpr size_t IDENTIFIER_SIZE_OFFSET = STATIC_VERSION_SIZE;
49+constexpr size_t TIMESTAMP_OFFSET = IDENTIFIER_SIZE_OFFSET + sizeof(uint32_t);
50+constexpr size_t LANGUAGE_OFFSET = TIMESTAMP_OFFSET + sizeof(uint64_t);
51+constexpr size_t HEADER_SIZE_OFFSET = LANGUAGE_OFFSET + sizeof(uint8_t);
52+constexpr size_t RECORD_COUNT_OFFSET = HEADER_SIZE_OFFSET + sizeof(uint32_t);
53+constexpr size_t FEATURE_FLAGS_OFFSET = RECORD_COUNT_OFFSET + sizeof(uint32_t);
54+ 
55+struct StaticSnapshotHeader {
56+ std::array<char, STATIC_VERSION_SIZE> version {};
57+ uint32_t identifierSize = 0;
58+ uint64_t timestamp = 0;
59+ uint8_t language = 0;
60+ uint32_t headerSize = 0;
61+ uint32_t recordCount = 0;
62+ uint32_t featureFlags = 0;
63+};
64+ 
65+bool ReadStaticSnapshotHeader(FileReader &file, StaticSnapshotHeader &header)
66+{
67+ std::array<char, STATIC_HEADER_SIZE> bytes {};
68+ if (!file.Seek(0) || !file.Read(bytes.data(), bytes.size())) {
69+ return false;
70+ }
71+ std::copy_n(bytes.data(), STATIC_VERSION_SIZE, header.version.data());
72+ header.identifierSize = ByteToU32(bytes.data() + IDENTIFIER_SIZE_OFFSET);
73+ header.timestamp = ByteToU64(bytes.data() + TIMESTAMP_OFFSET);
74+ header.language = static_cast<uint8_t>(bytes[LANGUAGE_OFFSET]);
75+ header.headerSize = ByteToU32(bytes.data() + HEADER_SIZE_OFFSET);
76+ header.recordCount = ByteToU32(bytes.data() + RECORD_COUNT_OFFSET);
77+ header.featureFlags = ByteToU32(bytes.data() + FEATURE_FLAGS_OFFSET);
78+ return true;
79+}
80+ 
81+bool ValidateStaticSnapshotHeader(const StaticSnapshotHeader &header, bool logError)
82+{
83+ auto versionEnd = std::find(header.version.begin(), header.version.end(), '\0');
84+ Version version;
85+ bool versionSupported = versionEnd != header.version.end() &&
86+ version.Parse(std::string(header.version.begin(), versionEnd)) &&
87+ version.GetMajor() == STATIC_SNAPSHOT_MAJOR_VERSION;
88+ if (!versionSupported) {
89+ if (logError) {
90+ LOG_ERROR_ << "unsupported static snapshot version";
91+ }
92+ return false;
93+ }
94+ if (header.identifierSize != STATIC_IDENTIFIER_SIZE) {
95+ if (logError) {
96+ LOG_ERROR_ << "identifierSize mismatch: expected " << STATIC_IDENTIFIER_SIZE << " got " <<
97+ header.identifierSize;
98+ }
99+ return false;
100+ }
101+ if (header.language != STATIC_LANGUAGE_STATIC && header.language != STATIC_LANGUAGE_HYBRID) {
102+ if (logError) {
103+ LOG_ERROR_ << "unsupported static snapshot language: " << static_cast<uint32_t>(header.language);
104+ }
105+ return false;
106+ }
107+ if (header.headerSize != STATIC_HEADER_SIZE) {
108+ if (logError) {
109+ LOG_ERROR_ << "headerSize mismatch: expected " << STATIC_HEADER_SIZE << " got " << header.headerSize;
110+ }
111+ return false;
112+ }
113+ if ((header.featureFlags & ~STATIC_SUPPORTED_FEATURE_FLAGS) != 0) {
114+ if (logError) {
115+ LOG_ERROR_ << "unsupported static snapshot feature flags: 0x" << std::hex << header.featureFlags <<
116+ std::dec;
117+ }
118+ return false;
119+ }
120+ return true;
121+}
122+ 
123+std::string EncodeEtsChar(uint16_t value)
124+{
125+ if (value == 0 || (value >= UTF16_HIGH_SURROGATE_START && value <= UTF16_LOW_SURROGATE_END)) {
126+ constexpr std::string_view HEX_DIGITS = "0123456789ABCDEF";
127+ std::string escaped = "\\u0000";
128+ for (size_t i = 0; i < UTF16_HEX_DIGIT_COUNT; ++i) {
129+ size_t shift = (UTF16_HEX_DIGIT_COUNT - i - 1) * BITS_PER_HEX_DIGIT;
130+ escaped[UNICODE_ESCAPE_PREFIX_SIZE + i] = HEX_DIGITS[(value >> shift) & HEX_DIGIT_MASK];
131+ }
132+ return escaped;
133+ }
134+ if (value < UTF8_ONE_BYTE_LIMIT) {
135+ return std::string(1, static_cast<char>(value));
136+ }
137+ if (value < UTF8_TWO_BYTE_LIMIT) {
138+ return {static_cast<char>(UTF8_TWO_BYTE_PREFIX | (value >> UTF8_CONTINUATION_SHIFT)),
139+ static_cast<char>(UTF8_CONTINUATION_PREFIX | (value & UTF8_CONTINUATION_MASK))};
140+ }
141+ return {static_cast<char>(UTF8_THREE_BYTE_PREFIX | (value >> UTF8_THREE_BYTE_SHIFT)),
142+ static_cast<char>(UTF8_CONTINUATION_PREFIX | ((value >> UTF8_CONTINUATION_SHIFT) & UTF8_CONTINUATION_MASK)),
143+ static_cast<char>(UTF8_CONTINUATION_PREFIX | (value & UTF8_CONTINUATION_MASK))};
144+}
145+ 
146+uint64_t ReadLittleEndianValue(const char *data, uint8_t byteSize)
147+{
148+ uint64_t value = 0;
149+ for (uint8_t i = 0; i < byteSize; ++i) {
150+ value |= static_cast<uint64_t>(static_cast<uint8_t>(data[i])) << (BITS_PER_BYTE * i);
151+ }
152+ return value;
153+}
154+ 
155+bool IsSupportedFieldValueType(uint8_t type)
156+{
157+ switch (static_cast<StaFieldType>(type)) {
158+ case StaFieldType::BOOLEAN:
159+ case StaFieldType::CHAR:
160+ case StaFieldType::FLOAT:
161+ case StaFieldType::DOUBLE:
162+ case StaFieldType::BYTE:
163+ case StaFieldType::SHORT:
164+ case StaFieldType::INT:
165+ case StaFieldType::LONG:
166+ case StaFieldType::OBJECT:
167+ case StaFieldType::ARRAY:
168+ case StaFieldType::TAGGED:
169+ case StaFieldType::WEAK_OBJECT:
170+ return true;
171+ case StaFieldType::UNKNOWN:
172+ return false;
173+ }
174+ return false;
175+}
176+} // namespace
177+ 
178+StaticRawheapTranslate::~StaticRawheapTranslate()
179+{
180+ // Nodes/edges/string table are owned and freed by the RawHeap base
181+ // destructor.
182+}
183+ 
184+// ---- Parse (Phase 1: Collect) ----
185+ 
186+bool StaticRawheapTranslate::Parse(FileReader &file, uint32_t rawheapFileSize)
187+{
188+ file_ = &file;
189+ parseOk_ = true;
190+ readingRecord_ = false;
191+ recordRemaining_ = 0;
192+ 
193+ if (!ParseHeader()) {
194+ LOG_ERROR_ << "failed to parse static snapshot header";
195+ return false;
196+ }
197+ 
198+ uint64_t fileSize = rawheapFileSize == 0 ? file.GetFileSize() : rawheapFileSize;
199+ uint64_t offset = header_.headerSize;
200+ if (offset > fileSize) {
201+ LOG_ERROR_ << "static snapshot header exceeds file size";
202+ return false;
203+ }
204+ 
205+ // Each record is parsed inside its declared body boundary. A collector may
206+ // neither consume bytes from the next record nor leave trailing body bytes.
207+ while (offset < fileSize) {
208+ if (!ParseRecord(offset, fileSize)) {
209+ return false;
210+ }
211+ }
212+ 
213+ // Two-file (merge) mode: build object nodes + edges only, no synthetic root.
214+ // Single-file mode: also build the synthetic-root framework (nodes[0..1]).
215+ BuildGraph(buildRootFramework_);
216+ 
217+ file_ = nullptr; // done with file reading
218+ return true;
219+}
220+ 
221+bool StaticRawheapTranslate::ParseRecord(uint64_t &offset, uint64_t fileSize)
222+{
223+ if (fileSize - offset < STATIC_RECORD_HDR_SIZE) {
224+ LOG_ERROR_ << "truncated static record header at offset " << offset;
225+ return false;
226+ }
227+ char hdr[STATIC_RECORD_HDR_SIZE];
228+ if (!file_->Read(hdr, STATIC_RECORD_HDR_SIZE)) {
229+ LOG_ERROR_ << "failed to read static record header at offset " << offset;
230+ return false;
231+ }
232+ offset += STATIC_RECORD_HDR_SIZE;
233+ const uint8_t tag = static_cast<uint8_t>(hdr[STATIC_RECORD_HDR_TAG_OFF]);
234+ const uint32_t length = ByteToU32(hdr + STATIC_RECORD_HDR_LENGTH_OFF);
235+ const uint32_t count = ByteToU32(hdr + STATIC_RECORD_HDR_COUNT_OFF);
236+ if (length > MAX_RECORD_BODY_SIZE) {
237+ LOG_ERROR_ << "record body length " << length << " exceeds safety limit " << MAX_RECORD_BODY_SIZE;
238+ return false;
239+ }
240+ if (length > fileSize - offset) {
241+ LOG_ERROR_ << "record body length " << length << " exceeds remaining file bytes " << fileSize - offset;
242+ return false;
243+ }
244+ if (count > length) {
245+ LOG_ERROR_ << "record item count " << count << " exceeds body length " << length;
246+ return false;
247+ }
248+ if (count > MAX_RECORD_ITEM_COUNT) {
249+ LOG_ERROR_ << "record item count " << count << " exceeds safety limit " << MAX_RECORD_ITEM_COUNT;
250+ return false;
251+ }
252+ 
253+ readingRecord_ = true;
254+ recordRemaining_ = length;
255+ if (!DispatchRecord(tag, length, count)) {
256+ LOG_ERROR_ << "failed to parse record tag=" << static_cast<int>(tag);
257+ return false;
258+ }
259+ if (!parseOk_) {
260+ LOG_ERROR_ << "read error during record collection";
261+ return false;
262+ }
263+ if (recordRemaining_ != 0) {
264+ LOG_ERROR_ << "record tag=" << static_cast<int>(tag) << " left " << recordRemaining_ <<
265+ " unconsumed body bytes";
266+ return false;
267+ }
268+ readingRecord_ = false;
269+ offset += length;
270+ return true;
271+}
272+ 
273+bool StaticRawheapTranslate::Translate()
274+{
275+ // Graph (including single-file synthetic root) is built during Parse().
276+ // Add the trailing primitive nodes the serializer expects.
277+ AddPrimitiveNodes();
278+ return true;
279+}
280+ 
281+Node *StaticRawheapTranslate::FindNodeByNodeId(uint32_t nodeId) const
282+{
283+ auto it = nodeIdToNode_.find(nodeId);
284+ return it == nodeIdToNode_.end() ? nullptr : it->second;
285+}
286+ 
287+bool StaticRawheapTranslate::ParseHeader()
288+{
289+ StaticSnapshotHeader header;
290+ if (!ReadStaticSnapshotHeader(*file_, header)) {
291+ LOG_ERROR_ << "failed to read static snapshot header";
292+ return false;
293+ }
294+ if (!ValidateStaticSnapshotHeader(header, true)) {
295+ return false;
296+ }
297+ 
298+ header_.identifierSize = header.identifierSize;
299+ header_.timestamp = header.timestamp;
300+ header_.language = header.language;
301+ header_.headerSize = header.headerSize;
302+ header_.recordCount = header.recordCount;
303+ header_.featureFlags = header.featureFlags;
304+ if (!file_->Seek(header_.headerSize)) {
305+ LOG_ERROR_ << "failed to seek past header";
306+ return false;
307+ }
308+ LOG_INFO_ << "static header: lang=" << static_cast<int>(header_.language) << " headerSize=" << header_.headerSize;
309+ return true;
310+}
311+ 
312+bool StaticRawheapTranslate::DispatchRecord(uint8_t tag, uint32_t length, uint32_t count)
313+{
314+ switch (tag) {
315+ case TAG_STRING_IN_UTF8:
316+ return CollectStringItems(length, count);
317+ case TAG_LOAD_CLASS:
318+ return CollectLoadClassItems(length, count);
319+ case TAG_STATIC_CLASS_DUMP:
320+ return CollectStaticClassDumpItems(length, count);
321+ case TAG_ROOT_RECORD:
322+ return CollectRootItems(length, count);
323+ case TAG_STATIC_INSTANCE_DUMP:
324+ return CollectInstanceItems(length, count);
325+ case TAG_STATIC_ARRAY_DUMP:
326+ return CollectArrayItems(length, count);
327+ case TAG_STATIC_STRING_DUMP:
328+ return CollectStaticStringDumpItems(length, count);
329+ case TAG_XREF_EDGE:
330+ return CollectXRefItems(length, count);
331+ case TAG_HEAP_SUMMARY:
332+ return CollectHeapSummary(length);
333+ default:
334+ LOG_INFO_ << "unknown static record tag=" << static_cast<int>(tag) << " length=" << length <<
335+ " count=" << count << ", skipping";
336+ return SkipBody(length);
337+ }
338+}
339+ 
340+bool StaticRawheapTranslate::SkipBody(uint32_t length)
341+{
342+ std::array<char, SKIP_BUFFER_SIZE> buffer {};
343+ uint32_t remaining = length;
344+ while (remaining > 0) {
345+ uint32_t chunk = std::min<uint32_t>(remaining, buffer.size());
346+ if (!ReadBytes(buffer.data(), chunk)) {
347+ return false;
348+ }
349+ remaining -= chunk;
350+ }
351+ return true;
352+}
353+ 
354+bool StaticRawheapTranslate::CollectStringItems(uint32_t length, uint32_t count)
355+{
356+ // Body contains count string items, each:
357+ // [stringId:u4][strLen:u4][utf8Data:strLen bytes]
358+ for (uint32_t i = 0; i < count; ++i) {
359+ uint32_t stringId = ReadU32();
360+ uint32_t strLen = ReadU32();
361+ if (!parseOk_) {
362+ return false;
363+ }
364+ if (strLen > MAX_STRING_DATA_SIZE) {
365+ LOG_ERROR_ << "string length " << strLen << " exceeds safety limit " << MAX_STRING_DATA_SIZE;
366+ return false;
367+ }
368+ if (strLen > recordRemaining_) {
369+ LOG_ERROR_ << "string length " << strLen << " exceeds remaining record bytes " << recordRemaining_;
370+ return false;
371+ }
372+ std::string str(strLen, '\0');
Petrov Igor
Petrov IgorPetrov Igor7月23日

The record's declared length is not enforced inside this collector. A record with length=8, count=1, and strLen=UINT32_MAX passes the outer checks and attempts to allocate about 4 GiB before any read can fail. Smaller oversized values can consume bytes from following records. CollectStaticStringDumpItems() has the same issue for valueLen. Please parse each record through a cursor bounded to exactly its body, validate every fixed/variable read against the remaining bytes before allocation, and require exact consumption.

likedislike
yangxiaoshuai2022
yangxiaoshuai2022
28 天前 评论:
373+ if (strLen > 0 && !ReadBytes(str.data(), strLen)) {
374+ return false;
375+ }
376+ stringTable_[stringId] = str;
377+ }
378+ return true;
379+}
380+ 
381+bool StaticRawheapTranslate::CollectLoadClassItems(uint32_t length, uint32_t count)
382+{
383+ for (uint32_t i = 0; i < count; ++i) {
384+ (void)ReadU32(); // classSerialNumber
385+ uint32_t classNodeId = ReadU32(); // classObjectId
386+ (void)ReadU32(); // stackTraceSerial
387+ uint32_t classNameId = ReadU32(); // classNameId
388+ (void)ReadU8(); // language
389+ (void)ReadU32(); // classFlags
390+ if (!parseOk_) {
391+ return false;
392+ }
393+ auto &info = classMap_[classNodeId];
394+ info.classNameId = classNameId;
395+ }
396+ return true;
397+}
398+ 
399+bool StaticRawheapTranslate::CollectStaticClassDumpItems(uint32_t length, uint32_t count)
400+{
401+ for (uint32_t i = 0; i < count; ++i) {
402+ uint32_t classNodeId = ReadU32(); // classObjectId
403+ (void)ReadU32(); // stackTraceSerial
404+ uint32_t superClassNodeId = ReadU32(); // superClassObjectId
405+ (void)ReadU32(); // classLoaderObjectId
406+ uint32_t instanceSize = ReadU32(); // instanceSize
407+ if (!parseOk_) {
408+ return false;
409+ }
410+ auto &info = classMap_[classNodeId];
411+ info.instanceSize = instanceSize;
412+ info.superClassNodeId = superClassNodeId;
413+ ReadFieldDescriptors(info.staticFields);
414+ ReadFieldDescriptors(info.instanceFields);
415+ ReadStaticValues(info.staticValues);
416+ ReadMethodNames(info.methodNameIds);
417+ if (!parseOk_) {
418+ return false;
419+ }
420+ }
421+ return true;
422+}
423+ 
424+void StaticRawheapTranslate::ReadFieldDescriptor(FieldDef &fd)
425+{
426+ fd.nameId = ReadU32();
427+ fd.type = ReadU8();
428+ fd.offset = ReadU32();
429+ fd.flags = ReadU16();
430+}
431+ 
432+void StaticRawheapTranslate::ReadFieldDescriptors(std::vector<FieldDef> &out)
433+{
434+ uint16_t cnt = ReadU16();
435+ out.reserve(cnt);
436+ for (uint16_t j = 0; j < cnt; ++j) {
437+ FieldDef fd;
438+ ReadFieldDescriptor(fd);
439+ out.push_back(fd);
440+ }
441+}
442+ 
443+void StaticRawheapTranslate::ReadStaticValues(std::vector<FieldValue> &out)
444+{
445+ // Static field values (parallel to staticFields, same order). Each entry
446+ // is [type:u1][value:variable], identical to INSTANCE_DUMP field values.
447+ uint16_t cnt = ReadU16();
448+ out.reserve(cnt);
449+ for (uint16_t j = 0; j < cnt; ++j) {
450+ FieldValue fv;
451+ fv.type = ReadU8();
452+ if (!IsSupportedFieldValueType(fv.type)) {
453+ LOG_ERROR_ << "unsupported static field value type " << static_cast<uint32_t>(fv.type);
454+ parseOk_ = false;
455+ return;
456+ }
457+ fv.value = ReadFieldValue(FieldSize(fv.type));
458+ out.push_back(fv);
459+ }
460+}
461+ 
462+void StaticRawheapTranslate::ReadMethodNames(std::vector<uint32_t> &out)
463+{
464+ // Declared method name ids (dump string-pool indices).
465+ uint16_t cnt = ReadU16();
466+ out.reserve(cnt);
467+ for (uint16_t j = 0; j < cnt; ++j) {
468+ out.push_back(ReadU32());
469+ }
470+}
471+ 
472+bool StaticRawheapTranslate::CollectRootItems(uint32_t length, uint32_t count)
473+{
474+ for (uint32_t i = 0; i < count; ++i) {
475+ (void)ReadU8(); // rootType
476+ uint32_t objectNodeId = ReadU32();
477+ if (!parseOk_) {
478+ return false;
479+ }
480+ roots_.push_back(objectNodeId);
481+ }
482+ return true;
483+}
484+ 
485+bool StaticRawheapTranslate::CollectInstanceItems(uint32_t length, uint32_t count)
486+{
487+ for (uint32_t i = 0; i < count; ++i) {
488+ InstanceRecord rec;
489+ rec.objectNodeId = ReadU32();
490+ rec.classNodeId = ReadU32();
491+ (void)ReadU32(); // stackTraceSerial
492+ rec.instanceSize = ReadU32();
493+ uint16_t fieldCount = ReadU16();
494+ if (!parseOk_) {
495+ return false;
496+ }
497+ 
498+ rec.values.reserve(fieldCount);
499+ for (uint16_t j = 0; j < fieldCount; ++j) {
500+ uint8_t fieldType = ReadU8();
501+ if (!IsSupportedFieldValueType(fieldType)) {
502+ LOG_ERROR_ << "unsupported instance field value type " << static_cast<uint32_t>(fieldType);
503+ return false;
504+ }
505+ uint8_t sz = FieldSize(fieldType);
506+ uint64_t val = ReadFieldValue(sz);
507+ rec.values.push_back({fieldType, val});
508+ }
509+ if (!parseOk_) {
510+ return false;
511+ }
512+ instances_.push_back(std::move(rec));
513+ }
514+ return true;
515+}
516+ 
517+bool StaticRawheapTranslate::CollectArrayItems(uint32_t length, uint32_t count)
518+{
519+ if (length == 0 || count == 0) {
520+ return true;
521+ }
522+ std::vector<char> bodyBuf(length);
523+ if (!ReadBytes(bodyBuf.data(), length)) {
524+ LOG_ERROR_ << "CollectArrayItems: failed to read body";
525+ return false;
526+ }
527+ char *body = bodyBuf.data();
528+ 
529+ std::vector<ArrayItemLayout> layouts(count);
530+ size_t totalKnownData = 0;
531+ size_t totalUnknownLength = 0;
532+ 
533+ if (!ScanArrayPrefixes(body, length, layouts, totalKnownData, totalUnknownLength)) {
534+ return false;
535+ }
536+ if (!DistributeUnknownData(layouts, count, totalKnownData, length, totalUnknownLength)) {
537+ return false;
538+ }
539+ if (!BuildArrayRecords(body, count, length, layouts)) {
540+ return false;
541+ }
542+ return true;
543+}
544+ 
545+bool StaticRawheapTranslate::ScanArrayPrefixes(char *body, uint32_t length,
546+ std::vector<ArrayItemLayout> &layouts, size_t &totalKnownData,
547+ size_t &totalUnknownLength)
548+{
549+ size_t pos = 0;
550+ for (size_t i = 0; i < layouts.size(); ++i) {
551+ if (pos + STATIC_ARRAY_PREFIX_BODY_SIZE > length) {
552+ LOG_ERROR_ << "CollectArrayItems: insufficient body for prefix at item " << i;
553+ return false;
554+ }
555+ ArrayItemLayout &lay = layouts[i];
556+ lay.prefixOff = pos;
557+ lay.arrayLength = ByteToU32(body + pos + STATIC_ARRAY_LENGTH_OFFSET);
558+ lay.elementType = static_cast<uint8_t>(body[pos + STATIC_ARRAY_ELEM_TYPE_OFFSET]);
559+ pos += STATIC_ARRAY_PREFIX_BODY_SIZE;
560+ 
561+ if (!ScanArrayDataSize(body, length, pos, lay, totalUnknownLength)) {
562+ return false;
563+ }
564+ if (lay.dataSizeKnown) {
565+ if (lay.dataSize > length - pos) {
566+ LOG_ERROR_ << "CollectArrayItems: item " << i << " data exceeds record body";
567+ return false;
568+ }
569+ totalKnownData += lay.dataSize;
570+ pos += lay.dataSize;
571+ }
572+ }
573+ return true;
574+}
575+ 
576+bool StaticRawheapTranslate::ScanArrayDataSize(const char *body, uint32_t length, size_t dataOffset,
577+ ArrayItemLayout &layout, size_t &totalUnknownLength)
578+{
579+ bool isReference = layout.elementType == static_cast<uint8_t>(StaFieldType::OBJECT) ||
580+ layout.elementType == static_cast<uint8_t>(StaFieldType::ARRAY) ||
581+ layout.elementType == static_cast<uint8_t>(StaFieldType::WEAK_OBJECT);
582+ if (layout.elementType == static_cast<uint8_t>(StaFieldType::TAGGED)) {
583+ size_t scanOffset = dataOffset;
584+ for (uint32_t index = 0; index < layout.arrayLength; ++index) {
585+ if (scanOffset >= length) {
586+ LOG_ERROR_ << "CollectArrayItems: tagged element type exceeds record body";
587+ return false;
588+ }
589+ uint8_t valueType = static_cast<uint8_t>(body[scanOffset++]);
590+ if (!IsSupportedFieldValueType(valueType)) {
591+ LOG_ERROR_ << "CollectArrayItems: unsupported tagged element type " <<
592+ static_cast<uint32_t>(valueType);
593+ return false;
594+ }
595+ uint8_t valueSize = FieldSize(valueType);
596+ if (valueSize > length - scanOffset) {
597+ LOG_ERROR_ << "CollectArrayItems: tagged element payload exceeds record body";
598+ return false;
599+ }
600+ scanOffset += valueSize;
601+ }
602+ layout.dataSize = scanOffset - dataOffset;
603+ layout.dataSizeKnown = true;
604+ return true;
605+ }
606+ 
607+ uint8_t elementSize = FieldSize(layout.elementType);
608+ if (isReference) {
609+ layout.dataSize = static_cast<size_t>(layout.arrayLength) * sizeof(uint32_t);
610+ layout.dataSizeKnown = true;
611+ } else if (elementSize > 0 && layout.arrayLength > 0) {
612+ layout.dataSize = static_cast<size_t>(layout.arrayLength) * elementSize;
613+ layout.dataSizeKnown = true;
614+ } else if (layout.arrayLength > 0) {
615+ totalUnknownLength += layout.arrayLength;
616+ } else {
617+ layout.dataSizeKnown = true;
618+ }
619+ return true;
620+}
621+ 
622+bool StaticRawheapTranslate::DistributeUnknownData(std::vector<ArrayItemLayout> &layouts, uint32_t count,
623+ size_t totalKnownData, uint32_t length, size_t totalUnknownLength)
624+{
625+ size_t prefixBytes = static_cast<size_t>(STATIC_ARRAY_PREFIX_BODY_SIZE) * count;
626+ if (prefixBytes > length || totalKnownData > length - prefixBytes) {
627+ LOG_ERROR_ << "CollectArrayItems: array layout exceeds record body";
628+ return false;
629+ }
630+ size_t totalUnknownData = length - prefixBytes - totalKnownData;
631+ for (uint32_t i = 0; i < count; ++i) {
632+ if (!layouts[i].dataSizeKnown && layouts[i].arrayLength > 0 && totalUnknownLength > 0) {
633+ layouts[i].dataSize = totalUnknownData * layouts[i].arrayLength / totalUnknownLength;
634+ layouts[i].dataSizeKnown = true;
635+ }
636+ }
637+ return true;
638+}
639+ 
640+bool StaticRawheapTranslate::BuildArrayRecords(char *body, uint32_t count, uint32_t bodyLength,
641+ const std::vector<ArrayItemLayout> &layouts)
642+{
643+ for (uint32_t i = 0; i < count; ++i) {
644+ ArrayRecord rec;
645+ if (!BuildArrayRecord(body, bodyLength, i, layouts[i], rec)) {
646+ return false;
647+ }
648+ arrays_.push_back(std::move(rec));
649+ }
650+ return true;
651+}
652+ 
653+bool StaticRawheapTranslate::BuildArrayRecord(char *body, uint32_t bodyLength, uint32_t itemIndex,
654+ const ArrayItemLayout &layout, ArrayRecord &record)
655+{
656+ size_t prefixOffset = layout.prefixOff;
657+ if (prefixOffset > bodyLength || bodyLength - prefixOffset < STATIC_ARRAY_PREFIX_BODY_SIZE) {
658+ LOG_ERROR_ << "CollectArrayItems: invalid prefix offset at item " << itemIndex;
659+ return false;
660+ }
661+ record.objectNodeId = ByteToU32(body + prefixOffset);
662+ record.classNodeId = ByteToU32(body + prefixOffset + STATIC_ARRAY_CLASS_OFFSET);
663+ record.instanceSize = ByteToU32(body + prefixOffset + STATIC_ARRAY_INSTSIZE_OFFSET);
664+ record.length = layout.arrayLength;
665+ record.elementType = layout.elementType;
666+ 
667+ size_t dataOffset = prefixOffset + STATIC_ARRAY_PREFIX_BODY_SIZE;
668+ if (layout.dataSize > bodyLength - dataOffset) {
669+ LOG_ERROR_ << "CollectArrayItems: invalid data range at item " << itemIndex;
670+ return false;
671+ }
672+ bool isReference = record.elementType == static_cast<uint8_t>(StaFieldType::OBJECT) ||
673+ record.elementType == static_cast<uint8_t>(StaFieldType::ARRAY) ||
674+ record.elementType == static_cast<uint8_t>(StaFieldType::WEAK_OBJECT);
675+ if (record.elementType == static_cast<uint8_t>(StaFieldType::TAGGED) && record.length > 0) {
676+ return ReadTaggedArrayValues(body, dataOffset, layout.dataSize, record);
677+ }
678+ if (isReference && record.length > 0) {
679+ record.elements.reserve(record.length);
680+ for (uint32_t index = 0; index < record.length; ++index) {
681+ record.elements.push_back(ByteToU32(body + dataOffset + index * sizeof(uint32_t)));
682+ }
683+ } else if (record.length > 0 && layout.dataSizeKnown && layout.dataSize > 0 &&
684+ FieldSize(record.elementType) > 0) {
685+ const char *data = body + dataOffset;
686+ record.primData.assign(data, data + layout.dataSize);
687+ }
688+ return true;
689+}
690+ 
691+bool StaticRawheapTranslate::ReadTaggedArrayValues(const char *body, size_t dataOffset, size_t dataSize,
692+ ArrayRecord &record)
693+{
694+ size_t valueOffset = dataOffset;
695+ size_t dataEnd = dataOffset + dataSize;
696+ record.taggedValues.reserve(record.length);
697+ for (uint32_t index = 0; index < record.length; ++index) {
698+ if (valueOffset >= dataEnd) {
699+ LOG_ERROR_ << "CollectArrayItems: tagged element type exceeds item data";
700+ return false;
701+ }
702+ uint8_t valueType = static_cast<uint8_t>(body[valueOffset++]);
703+ if (!IsSupportedFieldValueType(valueType)) {
704+ LOG_ERROR_ << "CollectArrayItems: unsupported tagged element type " << static_cast<uint32_t>(valueType);
705+ return false;
706+ }
707+ uint8_t valueSize = FieldSize(valueType);
708+ if (valueSize > dataEnd - valueOffset) {
709+ LOG_ERROR_ << "CollectArrayItems: tagged element payload exceeds item data";
710+ return false;
711+ }
712+ uint64_t value = ReadLittleEndianValue(body + valueOffset, valueSize);
713+ valueOffset += valueSize;
714+ record.taggedValues.push_back({valueType, value});
715+ }
716+ if (valueOffset != dataEnd) {
717+ LOG_ERROR_ << "CollectArrayItems: tagged array has trailing element data";
718+ return false;
719+ }
720+ return true;
721+}
722+ 
723+bool StaticRawheapTranslate::CollectStaticStringDumpItems(uint32_t length, uint32_t count)
724+{
725+ // Body per item:
726+ // [objId:u4][classObjId:u4][instSize:u4][valueLen:u4][valueBytes:valueLen]
727+ for (uint32_t i = 0; i < count; ++i) {
728+ StringInstanceRecord rec;
729+ rec.objectNodeId = ReadU32();
730+ rec.classNodeId = ReadU32();
731+ rec.instanceSize = ReadU32();
732+ uint32_t valueLen = ReadU32();
733+ if (!parseOk_) {
734+ return false;
735+ }
736+ if (valueLen > MAX_STRING_DATA_SIZE) {
737+ LOG_ERROR_ << "static string length " << valueLen << " exceeds safety limit " << MAX_STRING_DATA_SIZE;
738+ return false;
739+ }
740+ if (valueLen > recordRemaining_) {
741+ LOG_ERROR_ << "static string length " << valueLen << " exceeds remaining record bytes " << recordRemaining_;
742+ return false;
743+ }
744+ if (valueLen > 0) {
745+ std::string s(valueLen, '\0');
746+ if (!ReadBytes(s.data(), valueLen)) {
747+ return false;
748+ }
749+ rec.content = std::move(s);
750+ }
751+ stringInstances_.push_back(std::move(rec));
752+ }
753+ return true;
754+}
755+ 
756+bool StaticRawheapTranslate::CollectXRefItems(uint32_t length, uint32_t count)
757+{
758+ for (uint32_t i = 0; i < count; ++i) {
759+ XRefRecord xref;
760+ xref.dynNodeId = ReadU32(); // dynamic-side nodeId (4 bytes)
761+ xref.staNodeId = ReadU32();
762+ xref.direction = ReadU8();
763+ if (!parseOk_) {
764+ return false;
765+ }
766+ if (xref.direction != XREF_DYN_TO_STA && xref.direction != XREF_STA_TO_DYN && xref.direction != XREF_BIDIR) {
767+ LOG_ERROR_ << "unsupported XRef direction " << static_cast<uint32_t>(xref.direction);
768+ return false;
769+ }
770+ xrefs_.push_back(xref);
771+ }
772+ return true;
773+}
774+ 
775+bool StaticRawheapTranslate::CollectHeapSummary(uint32_t length)
776+{
777+ // Heap summary is informational only; not used for graph construction.
778+ return SkipBody(length);
779+}
780+ 
781+// ---- Build (Phase 2: graph construction from collected records) ----
782+ 
783+void StaticRawheapTranslate::BuildGraph(bool withRootFramework)
784+{
785+ Node *syntheticRoot = nullptr;
786+ Node *staticRoot = nullptr;
787+ if (withRootFramework) {
788+ syntheticRoot = CreateNode();
789+ staticRoot = CreateNode();
790+ }
791+ 
792+ CreateClassNodes();
793+ CreateInstanceNodes();
794+ CreateArrayNodes();
795+ CreateStringNodes();
796+ 
797+ // Mark roots. Preserve a node's semantic type (class/object/array) - root
798+ // status is conveyed by the StaticRoot->node ELEMENT edges, not by type.
799+ // Overwriting class nodes to ROOT would erase their "class" identity.
800+ for (uint32_t nodeId : roots_) {
801+ Node *node = GetOrCreateNode(nodeId);
802+ if (node->type != CLASS_NODETYPE && node->type != OBJECT_NODETYPE && node->type != ARRAY_NODETYPE &&
803+ node->type != STRING) {
804+ node->type = ROOT;
805+ }
806+ }
807+ 
808+ if (withRootFramework) {
809+ CreateRootEdges(syntheticRoot, staticRoot);
810+ }
811+ CreateClassEdges(); // class -> superClass + static fields (contiguous)
812+ CreateInstanceEdges(); // instance -> fields + instance -> class (PROPERTY)
813+ CreateArrayEdges();
814+ CreateStringEdges(); // string node -> hclass (std.core.String)
815+ // Edges were inserted in phase order; re-sort by source node index so the
816+ // flat vector satisfies the .heapsnapshot grouping contract (node i owns the
817+ // next edgeCount[i] edges). Every edge above carries edge->from for this.
818+ SortEdgesByFrom();
819+}
820+ 
821+std::vector<uint32_t> StaticRawheapTranslate::SortedClassNodeIds() const
822+{
823+ // Iterate classMap_ in deterministic order (sorted by nodeId) so that
824+ // the same input always produces the same output ordering.
825+ std::vector<uint32_t> ids;
826+ ids.reserve(classMap_.size());
827+ for (const auto &kv : classMap_) {
828+ ids.push_back(kv.first);
829+ }
830+ std::sort(ids.begin(), ids.end());
831+ return ids;
832+}
833+ 
834+void StaticRawheapTranslate::CreateClassNodes()
835+{
836+ for (uint32_t nodeId : SortedClassNodeIds()) {
837+ auto it = classMap_.find(nodeId);
838+ Node *node = GetOrCreateNode(nodeId);
Petrov Igor
Petrov IgorPetrov Igor27 天前

Class mirror nodes never receive their instance size. CreateClassNodes() sets only the name and type, and CreateInstanceNodes() skips the matching mirror record before node->size = rec.instanceSize. The resulting class nodes are serialized with self_size == 0, which undercounts retained memory and distorts heap analysis. Please preserve the CLASS semantics while assigning the mirror's recorded instance size before the early continue, and add a translated class-size assertion.

likedislike
yangxiaoshuai2022
yangxiaoshuai2022
23 天前 评论:
839+ node->strId = GetOrCreateStringId(it->second.classNameId);
840+ // These nodes come from TAG_STATIC_CLASS_DUMP / LOAD_CLASS records -
841+ // restore the CLASS node type rather than collapsing to DEFAULT.
842+ node->type = CLASS_NODETYPE;
843+ }
844+}
845+ 
846+void StaticRawheapTranslate::CreateInstanceNodes()
847+{
848+ for (auto &rec : instances_) {
849+ // A class mirror object is dumped both as a STATIC_CLASS_DUMP (keyed by
850+ // its classObjectId) and as a STATIC_INSTANCE_DUMP (an instance of the
851+ // metaclass std.core.Class). Skip the instance view so the class node
852+ // created by CreateClassNodes (name = the class name, type = CLASS) is
853+ // not overwritten with metaclass-instance data.
854+ if (classMap_.find(rec.objectNodeId) != classMap_.end()) {
855+ GetOrCreateNode(rec.objectNodeId)->size = rec.instanceSize;
856+ continue;
857+ }
858+ Node *node = GetOrCreateNode(rec.objectNodeId);
859+ node->size = rec.instanceSize;
860+ // These nodes come from TAG_STATIC_INSTANCE_DUMP records - restore
861+ // the OBJECT node type rather than leaving the default.
862+ node->type = OBJECT_NODETYPE;
863+ auto classIt = classMap_.find(rec.classNodeId);
864+ if (classIt != classMap_.end()) {
865+ node->strId = GetOrCreateStringId(classIt->second.classNameId);
866+ if (node->size == 0) {
867+ node->size = classIt->second.instanceSize;
868+ }
869+ }
870+ }
871+}
872+ 
873+void StaticRawheapTranslate::CreateArrayNodes()
874+{
875+ for (auto &rec : arrays_) {
876+ Node *node = GetOrCreateNode(rec.objectNodeId);
877+ node->size = rec.instanceSize;
878+ // These nodes come from TAG_STATIC_ARRAY_DUMP records - restore the
879+ // ARRAY node type rather than leaving the default.
880+ node->type = ARRAY_NODETYPE;
881+ auto classIt = classMap_.find(rec.classNodeId);
882+ if (classIt != classMap_.end()) {
883+ node->strId = GetOrCreateStringId(classIt->second.classNameId);
884+ }
885+ }
886+}
887+ 
888+void StaticRawheapTranslate::CreateStringNodes()
889+{
890+ // String objects arrive via TAG_STATIC_STRING_DUMP (not INSTANCE_DUMP), so
891+ // they are not in instances_ and would otherwise have no node at all.
892+ // Create a STRING-typed node named by the content - this is the only path
893+ // that makes the actual string value visible in the .heapsnapshot.
894+ for (auto &rec : stringInstances_) {
895+ Node *node = GetOrCreateNode(rec.objectNodeId);
896+ node->size = rec.instanceSize;
897+ node->type = STRING;
898+ node->strId = InsertAndGetStringId(rec.content);
899+ }
900+}
901+ 
902+void StaticRawheapTranslate::CreateRootEdges(Node *syntheticRoot, Node *staticRoot)
903+{
904+ syntheticRoot->nodeId = 1; // 1: root node id
905+ syntheticRoot->type = SYNTHETIC_NODETYPE;
906+ syntheticRoot->strId = InsertAndGetStringId("SyntheticRoot");
907+ syntheticRoot->edgeCount = 0;
908+ staticRoot->nodeId = 0;
909+ staticRoot->type = ROOT;
910+ staticRoot->strId = InsertAndGetStringId("StaticRoot[" + std::to_string(roots_.size()) + ']');
911+ staticRoot->size = VIRTUAL_NODE_SIZE;
912+ staticRoot->edgeCount = 0;
913+ 
914+ StringId subrootStrId = InsertAndGetStringId("-subroot-");
915+ InsertEdge(syntheticRoot, staticRoot, subrootStrId,
916+ EdgeType::SHORTCUT); // syntheticRoot -> StaticRoot
917+ syntheticRoot->edgeCount++;
918+ uint32_t index = 0;
919+ for (uint32_t nodeId : roots_) {
920+ Node *root = FindNodeByNodeId(nodeId);
921+ if (root == nullptr) {
922+ continue;
923+ }
924+ InsertEdge(staticRoot, root, index++, EdgeType::ELEMENT);
925+ staticRoot->edgeCount++;
926+ }
927+}
928+ 
929+void StaticRawheapTranslate::CreateClassEdges()
930+{
931+ // Emit a class node's superclass edge AND its static field edges in one pass
932+ // so a class's edges stay contiguous in the flat edge vector - required by
933+ // the .heapsnapshot grouping contract (node i owns the next edgeCount[i]
934+ // edges).
935+ for (uint32_t nodeId : SortedClassNodeIds()) {
936+ Node *classNode = FindNodeByNodeId(nodeId);
937+ if (classNode == nullptr) {
938+ continue;
939+ }
940+ const auto &info = classMap_[nodeId];
941+ EmitSuperClassEdge(classNode, info);
942+ EmitStaticFieldEdges(classNode, info);
943+ EmitMethodNameEdges(classNode, info);
944+ }
945+}
946+ 
947+void StaticRawheapTranslate::EmitSuperClassEdge(Node *classNode, const ClassInfo &info)
948+{
949+ // class -> superClass (INTERNAL, "superClass"). Without this edge every
950+ // class node except the root-reachable ones is orphaned; the superclass
951+ // chain is what makes the class subgraph connected.
952+ if (info.superClassNodeId == 0 || info.superClassNodeId == classNode->nodeId) {
953+ return; // no superclass / self-loop guard
954+ }
955+ InsertEdge(classNode, GetOrCreateNode(info.superClassNodeId), InsertAndGetStringId("superClass"),
956+ EdgeType::INTERNAL);
957+ classNode->edgeCount++;
958+}
959+ 
960+void StaticRawheapTranslate::EmitStaticFieldEdges(Node *classNode, const ClassInfo &info)
961+{
962+ // class -> static field value (PROPERTY, field name). staticValues parallels
963+ // staticFields (same order - see EmitInstanceFieldEdges).
964+ // OBJECT/ARRAY values are nodeIds into the heap; primitives get a
965+ // synthetic value node.
966+ size_t n = std::min(info.staticFields.size(), info.staticValues.size());
967+ for (size_t i = 0; i < n; ++i) {
968+ const auto &val = info.staticValues[i];
969+ StringId nameId = GetOrCreateStringId(info.staticFields[i].nameId);
970+ if (EmitFieldValueEdge(classNode, val, nameId)) {
971+ classNode->edgeCount++;
972+ }
973+ }
974+}
975+ 
976+bool StaticRawheapTranslate::EmitFieldValueEdge(Node *from, const FieldValue &value, StringId nameId)
977+{
978+ bool isStrongRef = value.type == static_cast<uint8_t>(StaFieldType::OBJECT) ||
979+ value.type == static_cast<uint8_t>(StaFieldType::ARRAY);
980+ bool isWeakRef = value.type == static_cast<uint8_t>(StaFieldType::WEAK_OBJECT);
981+ if ((isStrongRef || isWeakRef) && value.value == 0) {
982+ return false;
983+ }
984+ if (isStrongRef || isWeakRef) {
985+ EdgeType edgeType = isWeakRef ? EdgeType::WEAK : EdgeType::PROPERTY;
986+ InsertEdge(from, GetOrCreateNode(static_cast<uint32_t>(value.value)), nameId, edgeType);
987+ return true;
988+ }
989+ InsertEdge(from, GetOrCreateValueNode(value.type, value.value), nameId, EdgeType::PROPERTY);
990+ return true;
991+}
992+ 
993+void StaticRawheapTranslate::EmitMethodNameEdges(Node *classNode, const ClassInfo &info)
994+{
995+ // class -> method (PROPERTY, method name). The static dumper writes every
996+ // declared method's name into the dump string pool and records its id in
997+ // CLASS_DUMP's methodNameId[] (ReadMethodNames -> info.methodNameIds).
998+ // GetOrCreateStringId is the bridge that promotes a dump string-pool id
999+ // into the .heapsnapshot strings table - calling it here (on the edge name)
1000+ // is what makes method names appear in the output at all. Without this
1001+ // path the ids are read into ClassInfo but never referenced, so the
1002+ // strings never leave the binary. Each method becomes a synthetic
1003+ // "closure" node named after the method; the edge name is the method name.
1004+ for (uint32_t methodNameId : info.methodNameIds) {
1005+ StringId nameId = GetOrCreateStringId(methodNameId);
1006+ Node *methodNode = GetOrCreateMethodNode(nameId);
1007+ InsertEdge(classNode, methodNode, nameId, EdgeType::PROPERTY);
1008+ classNode->edgeCount++;
1009+ }
1010+}
1011+void StaticRawheapTranslate::CreateInstanceEdges()
1012+{
1013+ // Instance field edges (PROPERTY). Field values map to descriptors in order:
1014+ // first the static-field descriptors, then the instance-field descriptors.
1015+ for (auto &rec : instances_) {
1016+ Node *node = FindNodeByNodeId(rec.objectNodeId);
1017+ if (node == nullptr) {
1018+ continue;
1019+ }
1020+ // instance -> class edge ("hclass", PROPERTY). Mirrors the hidden-class
1021+ // edge and makes class nodes reachable from their instances.
1022+ if (rec.classNodeId != 0 && rec.classNodeId != rec.objectNodeId) {
1023+ InsertEdge(node, GetOrCreateNode(rec.classNodeId), InsertAndGetStringId("hclass"), EdgeType::DEFAULT);
1024+ node->edgeCount++;
1025+ }
1026+ auto classIt = classMap_.find(rec.classNodeId);
1027+ if (classIt == classMap_.end()) {
1028+ EmitFallbackFieldEdges(node, rec);
1029+ continue;
1030+ }
1031+ EmitInstanceFieldEdges(node, rec, classIt->second);
1032+ }
1033+}
1034+ 
1035+void StaticRawheapTranslate::EmitInstanceFieldEdges(Node *node, const InstanceRecord &rec, const ClassInfo &info)
1036+{
1037+ // INSTANCE_DUMP carries instance field values only. The values follow the
1038+ // instance-field descriptors serialized in CLASS_DUMP, including inherited
1039+ // fields, in the same order. Static field values are serialized separately
1040+ // in CLASS_DUMP and therefore are not part of rec.values.
1041+ // A class mirror's metaclass has an instance field literally named
1042+ // "superClass" pointing at the superclass mirror. EmitSuperClassEdge
1043+ // already emits that relationship as an [internal] superClass edge (from
1044+ // CLASS_DUMP.superClassId), so emitting it again here would create a
1045+ // duplicate [property] superClass edge. Skip it for class nodes only.
1046+ bool isClassNode = (node->type == CLASS_NODETYPE);
1047+ size_t idx = 0;
1048+ for (size_t i = 0; i < info.instanceFields.size() && idx < rec.values.size(); ++i, ++idx) {
1049+ const auto &val = rec.values[idx];
1050+ StringId nameId = GetOrCreateStringId(info.instanceFields[i].nameId);
1051+ if (isClassNode && GetString(info.instanceFields[i].nameId) == "superClass") {
1052+ continue; // deduplicated by EmitSuperClassEdge
1053+ }
1054+ if (EmitFieldValueEdge(node, val, nameId)) {
1055+ node->edgeCount++;
1056+ }
1057+ }
1058+}
1059+ 
1060+void StaticRawheapTranslate::EmitFallbackFieldEdges(Node *node, const InstanceRecord &rec)
1061+{
1062+ LOG_INFO_ << "CreateInstanceEdges: instance nodeId 0x" << std::hex << rec.objectNodeId << " classNodeId 0x" <<
1063+ rec.classNodeId << std::dec << " not found in classMap, using fallback type name";
1064+ node->strId = InsertAndGetStringId("Object");
1065+ for (auto &val : rec.values) {
1066+ bool isStrongRef = val.type == static_cast<uint8_t>(StaFieldType::OBJECT) ||
1067+ val.type == static_cast<uint8_t>(StaFieldType::ARRAY);
1068+ bool isWeakRef = val.type == static_cast<uint8_t>(StaFieldType::WEAK_OBJECT);
1069+ if (val.value == 0 || (!isStrongRef && !isWeakRef)) {
1070+ continue;
1071+ }
1072+ InsertEdge(node, GetOrCreateNode(static_cast<uint32_t>(val.value)), InsertAndGetStringId(""),
1073+ isWeakRef ? EdgeType::WEAK : EdgeType::PROPERTY);
1074+ node->edgeCount++;
1075+ }
1076+}
1077+ 
1078+// Boxed-wrapper class name for a primitive element type. ETS primitive arrays
1079+// store raw values (no boxed objects in the heap); the snapshot boxes each
1080+// element so array contents are visible, like the dynamic side.
1081+static const char *BoxedWrapperName(StaFieldType t)
1082+{
1083+ switch (t) {
1084+ case StaFieldType::BOOLEAN:
1085+ return "std.core.Boolean";
1086+ case StaFieldType::BYTE:
1087+ return "std.core.Byte";
1088+ case StaFieldType::CHAR:
1089+ return "std.core.Char";
1090+ case StaFieldType::SHORT:
1091+ return "std.core.Short";
1092+ case StaFieldType::INT:
1093+ return "std.core.Int";
1094+ case StaFieldType::LONG:
1095+ return "std.core.Long";
1096+ case StaFieldType::FLOAT:
1097+ return "std.core.Float";
1098+ case StaFieldType::DOUBLE:
1099+ return "std.core.Double";
1100+ default:
1101+ return "std.core.Object";
1102+ }
1103+}
1104+ 
1105+// Read one little-endian primitive of `esz` bytes from a captured byte buffer.
1106+// Returns 0 for unsupported widths (caller skips boxing when esz is 0).
1107+static uint64_t ReadPrimitiveFromBuffer(const char *data, uint32_t index, uint8_t esz)
1108+{
1109+ char *p = const_cast<char *>(data + static_cast<size_t>(index) * esz);
1110+ switch (esz) {
1111+ case sizeof(uint8_t):
1112+ return static_cast<uint8_t>(p[0]);
1113+ case sizeof(uint16_t):
1114+ return ByteToU16(p);
1115+ case sizeof(uint32_t):
1116+ return ByteToU32(p);
1117+ case sizeof(uint64_t):
1118+ return ByteToU64(p);
1119+ default:
1120+ return 0;
1121+ }
1122+}
1123+ 
1124+void StaticRawheapTranslate::CreateArrayEdges()
1125+{
1126+ // Each array owns a synthetic "buffer" sub-node carrying its ELEMENT edges:
1127+ // OBJECT/ARRAY elements point at heap nodes, primitives are boxed into
1128+ // std.core.<Type> wrappers. Name ids are shared across all arrays.
1129+ StringId bufferNameId = InsertAndGetStringId("buffer");
1130+ StringId valueNameId = InsertAndGetStringId("value");
1131+ for (auto &rec : arrays_) {
1132+ Node *node = FindNodeByNodeId(rec.objectNodeId);
1133+ if (node == nullptr) {
1134+ continue;
1135+ }
1136+ Node *buffer = CreateArrayBufferNode(node, bufferNameId);
1137+ bool isRef = (rec.elementType == static_cast<uint8_t>(StaFieldType::OBJECT) ||
1138+ rec.elementType == static_cast<uint8_t>(StaFieldType::ARRAY) ||
1139+ rec.elementType == static_cast<uint8_t>(StaFieldType::WEAK_OBJECT));
1140+ if (rec.elementType == static_cast<uint8_t>(StaFieldType::TAGGED)) {
1141+ EmitTaggedArrayElementEdges(buffer, rec);
1142+ } else if (isRef) {
1143+ EmitArrayRefElementEdges(buffer, rec);
1144+ } else {
1145+ EmitArrayBoxedPrimitiveEdges(buffer, rec, valueNameId);
1146+ }
1147+ }
1148+}
1149+ 
1150+Node *StaticRawheapTranslate::CreateArrayBufferNode(Node *arrayNode, StringId bufferNameId)
1151+{
1152+ // The buffer sub-node owns the array's ELEMENT edges; shares the array's
1153+ // class name (e.g. "int[]") to retain its identity.
1154+ uint32_t bufSynId = 0x40000000 + valueNodeCounter_++;
1155+ Node *buffer = GetOrCreateNode(bufSynId);
1156+ buffer->nodeId = bufSynId;
1157+ buffer->type = ARRAY_NODETYPE;
1158+ buffer->strId = arrayNode->strId;
1159+ buffer->size = 0;
1160+ InsertEdge(arrayNode, buffer, bufferNameId, EdgeType::PROPERTY);
1161+ arrayNode->edgeCount++;
1162+ return buffer;
1163+}
1164+ 
1165+void StaticRawheapTranslate::EmitArrayRefElementEdges(Node *buffer, const ArrayRecord &rec)
1166+{
1167+ // One ELEMENT edge per non-null nodeId; index advances over nulls so
1168+ // positions match the source array ordering.
1169+ uint32_t index = 0;
1170+ for (uint32_t elemNodeId : rec.elements) {
1171+ if (elemNodeId != 0) {
1172+ InsertEdge(buffer, GetOrCreateNode(elemNodeId), index, EdgeType::ELEMENT);
1173+ buffer->edgeCount++;
1174+ }
1175+ ++index;
1176+ }
1177+}
1178+ 
1179+void StaticRawheapTranslate::EmitTaggedArrayElementEdges(Node *buffer, const ArrayRecord &rec)
1180+{
1181+ uint32_t index = 0;
1182+ for (const auto &value : rec.taggedValues) {
1183+ bool isStrongRef = value.type == static_cast<uint8_t>(StaFieldType::OBJECT) ||
1184+ value.type == static_cast<uint8_t>(StaFieldType::ARRAY);
1185+ bool isWeakRef = value.type == static_cast<uint8_t>(StaFieldType::WEAK_OBJECT);
1186+ if ((isStrongRef || isWeakRef) && value.value == 0) {
1187+ ++index;
1188+ continue;
1189+ }
1190+ if (isStrongRef) {
1191+ InsertEdge(buffer, GetOrCreateNode(static_cast<uint32_t>(value.value)), index, EdgeType::ELEMENT);
1192+ } else if (isWeakRef) {
1193+ StringId nameId = InsertAndGetStringId(std::to_string(index));
1194+ InsertEdge(buffer, GetOrCreateNode(static_cast<uint32_t>(value.value)), nameId, EdgeType::WEAK);
1195+ } else {
1196+ InsertEdge(buffer, GetOrCreateValueNode(value.type, value.value), index, EdgeType::ELEMENT);
1197+ }
1198+ buffer->edgeCount++;
1199+ ++index;
1200+ }
1201+}
1202+ 
1203+void StaticRawheapTranslate::EmitArrayBoxedPrimitiveEdges(Node *buffer, const ArrayRecord &rec, StringId valueNameId)
1204+{
1205+ // Box each primitive element into a std.core.<Type> wrapper with a "value"
1206+ // edge to a synthetic number/string node. No-op when there is no payload.
1207+ uint8_t esz = FieldSize(rec.elementType);
1208+ if (esz == 0 || rec.length == 0 || rec.primData.size() < static_cast<size_t>(rec.length) * esz) {
1209+ return;
1210+ }
1211+ StringId wrapperNameId = InsertAndGetStringId(BoxedWrapperName(static_cast<StaFieldType>(rec.elementType)));
1212+ uint32_t index = 0;
1213+ for (uint32_t j = 0; j < rec.length; ++j) {
1214+ uint64_t val = ReadPrimitiveFromBuffer(rec.primData.data(), j, esz);
1215+ uint32_t wSynId = 0x40000000 + valueNodeCounter_++;
1216+ Node *wrapper = GetOrCreateNode(wSynId);
1217+ wrapper->nodeId = wSynId;
1218+ wrapper->type = OBJECT_NODETYPE;
1219+ wrapper->strId = wrapperNameId;
1220+ wrapper->size = 0;
1221+ Node *valueNode = GetOrCreateValueNode(rec.elementType, val);
1222+ InsertEdge(buffer, wrapper, index, EdgeType::ELEMENT);
1223+ buffer->edgeCount++;
1224+ InsertEdge(wrapper, valueNode, valueNameId, EdgeType::PROPERTY);
1225+ wrapper->edgeCount++;
1226+ ++index;
1227+ }
1228+}
1229+ 
1230+void StaticRawheapTranslate::CreateStringEdges()
1231+{
1232+ // string -> class ("hclass", PROPERTY). Mirrors CreateInstanceEdges'
1233+ // instance->class edge so string nodes are connected to the std.core.String
1234+ // class node (and through it to the rest of the graph). The string's value
1235+ // is already carried as the node name, so no further edges are needed.
1236+ for (auto &rec : stringInstances_) {
1237+ Node *node = FindNodeByNodeId(rec.objectNodeId);
1238+ if (node == nullptr) {
1239+ continue;
1240+ }
1241+ if (rec.classNodeId != 0 && rec.classNodeId != rec.objectNodeId) {
1242+ InsertEdge(node, GetOrCreateNode(rec.classNodeId), InsertAndGetStringId("hclass"), EdgeType::DEFAULT);
1243+ node->edgeCount++;
1244+ }
1245+ }
1246+}
1247+ 
1248+Node *StaticRawheapTranslate::GetOrCreateValueNode(uint8_t fieldType, uint64_t value)
1249+{
1250+ // Synthetic nodeId in a high range so it cannot collide with real heap
1251+ // addresses (which are even, low u32 nodeIds). The merger's duplicate-nodeId
1252+ // detection is the backstop if a collision ever occurs.
1253+ uint32_t synId = 0x40000000 + valueNodeCounter_++;
1254+ Node *node = GetOrCreateNode(synId);
1255+ node->nodeId = synId;
1256+ if (fieldType == static_cast<uint8_t>(StaFieldType::CHAR)) {
1257+ node->type = STRING;
1258+ } else if (fieldType == static_cast<uint8_t>(StaFieldType::TAGGED)) {
1259+ node->type = SYNTHETIC_NODETYPE;
1260+ } else {
1261+ node->type = HEAP_NUMBER;
1262+ }
1263+ node->strId = InsertAndGetStringId(MakeValueNodeName(fieldType, value));
1264+ node->size = 0;
1265+ return node;
1266+}
1267+ 
1268+Node *StaticRawheapTranslate::GetOrCreateMethodNode(StringId nameId)
1269+{
1270+ // Synthetic "closure" node representing a declared method, named after the
1271+ // method. nodeId lives in the same high synthetic range (0x40000000+) as
1272+ // value nodes so it cannot collide with real heap addresses. One node per
1273+ // method-name occurrence (one node per method-name occurrence, each function
1274+ // is its own node); the counter is shared with GetOrCreateValueNode so ids
1275+ // stay unique.
1276+ uint32_t synId = 0x40000000 + valueNodeCounter_++;
1277+ Node *node = GetOrCreateNode(synId);
1278+ node->nodeId = synId;
1279+ node->type = CLOSURE_NODETYPE;
1280+ node->strId = nameId;
1281+ node->size = 0;
1282+ return node;
1283+}
1284+ 
1285+std::string StaticRawheapTranslate::MakeValueNodeName(uint8_t fieldType, uint64_t value) const
1286+{
1287+ auto ft = static_cast<StaFieldType>(fieldType);
1288+ switch (ft) {
1289+ case StaFieldType::BOOLEAN:
1290+ return (value != 0) ? "true" : "false";
1291+ case StaFieldType::CHAR:
1292+ return EncodeEtsChar(static_cast<uint16_t>(value));
1293+ case StaFieldType::TAGGED:
1294+ return MakeTaggedValueNodeName(value);
1295+ case StaFieldType::BYTE:
1296+ return std::to_string(static_cast<int8_t>(value));
1297+ case StaFieldType::SHORT:
1298+ return std::to_string(static_cast<int16_t>(value));
1299+ case StaFieldType::INT:
1300+ return std::to_string(static_cast<int32_t>(value));
1301+ case StaFieldType::LONG:
1302+ return std::to_string(static_cast<int64_t>(value));
1303+ case StaFieldType::FLOAT: {
1304+ float f;
1305+ (void)memcpy_s(&f, sizeof(f), &value, sizeof(f));
1306+ return std::to_string(f);
1307+ }
1308+ case StaFieldType::DOUBLE: {
1309+ double d;
1310+ (void)memcpy_s(&d, sizeof(d), &value, sizeof(d));
1311+ return std::to_string(d);
1312+ }
1313+ default:
1314+ return std::to_string(value); // UNKNOWN / anything else: raw bits
1315+ }
1316+}
1317+ 
1318+std::string StaticRawheapTranslate::MakeTaggedValueNodeName(uint64_t value) const
1319+{
1320+ switch (value) {
1321+ case STATIC_TAGGED_HOLE:
1322+ return "hole";
1323+ case STATIC_TAGGED_NULL:
1324+ return "null";
1325+ case STATIC_TAGGED_FALSE:
1326+ return "false";
1327+ case STATIC_TAGGED_TRUE:
1328+ return "true";
1329+ case STATIC_TAGGED_UNDEFINED:
1330+ return "undefined";
1331+ case STATIC_TAGGED_EXCEPTION:
1332+ return "exception";
1333+ default: {
1334+ constexpr int taggedHexWidth = sizeof(uint64_t) * BITS_PER_BYTE / BITS_PER_HEX_DIGIT;
1335+ std::ostringstream stream;
1336+ stream << "0x" << std::uppercase << std::hex << std::setfill('0') << std::setw(taggedHexWidth) << value;
1337+ return stream.str();
1338+ }
1339+ }
1340+}
1341+ 
1342+Node *StaticRawheapTranslate::GetOrCreateNode(uint32_t nodeId)
1343+{
1344+ auto it = nodeIdToNode_.find(nodeId);
1345+ if (it != nodeIdToNode_.end()) {
1346+ return it->second;
1347+ }
1348+ Node *node = CreateNode();
1349+ node->nodeId = nodeId;
1350+ nodeIdToNode_.emplace(nodeId, node);
1351+ return node;
1352+}
1353+ 
1354+std::string StaticRawheapTranslate::GetString(uint32_t stringId) const
1355+{
1356+ auto it = stringTable_.find(stringId);
1357+ return it == stringTable_.end() ? std::string() : it->second;
1358+}
1359+ 
1360+StringId StaticRawheapTranslate::GetOrCreateStringId(uint32_t stringId)
1361+{
1362+ return InsertAndGetStringId(GetString(stringId));
1363+}
1364+ 
1365+// ---- Primitive readers (use internal file_ pointer) ----
1366+ 
1367+bool StaticRawheapTranslate::ReadBytes(char *buffer, uint32_t size)
1368+{
1369+ if (readingRecord_ && size > recordRemaining_) {
1370+ LOG_ERROR_ << "record read of " << size << " bytes exceeds remaining " << recordRemaining_;
1371+ parseOk_ = false;
1372+ return false;
1373+ }
1374+ if (!file_->Read(buffer, size)) {
1375+ parseOk_ = false;
1376+ return false;
1377+ }
1378+ if (readingRecord_) {
1379+ recordRemaining_ -= size;
1380+ }
1381+ return true;
1382+}
1383+ 
1384+uint8_t StaticRawheapTranslate::ReadU8()
1385+{
1386+ char buf[1] = {0};
1387+ if (!ReadBytes(buf, 1)) {
1388+ return 0;
1389+ }
1390+ return static_cast<uint8_t>(buf[0]);
1391+}
1392+ 
1393+uint16_t StaticRawheapTranslate::ReadU16()
1394+{
1395+ char buf[sizeof(uint16_t)] = {0};
1396+ if (!ReadBytes(buf, sizeof(uint16_t))) {
1397+ return 0;
1398+ }
1399+ return ByteToU16(buf);
1400+}
1401+ 
1402+uint32_t StaticRawheapTranslate::ReadU32()
1403+{
1404+ char buf[sizeof(uint32_t)] = {0};
1405+ if (!ReadBytes(buf, sizeof(uint32_t))) {
1406+ return 0;
1407+ }
1408+ return ByteToU32(buf);
1409+}
1410+ 
1411+uint64_t StaticRawheapTranslate::ReadU64()
1412+{
1413+ char buf[sizeof(uint64_t)] = {0};
1414+ if (!ReadBytes(buf, sizeof(uint64_t))) {
1415+ return 0;
1416+ }
1417+ return ByteToU64(buf);
1418+}
1419+ 
1420+uint64_t StaticRawheapTranslate::ReadFieldValue(uint8_t byteSize)
1421+{
1422+ if (byteSize == 0) {
1423+ return 0;
1424+ }
1425+ char buf[sizeof(uint64_t)] = {0}; // max field size is 8 bytes (LONG/DOUBLE)
1426+ if (!ReadBytes(buf, byteSize)) {
1427+ return 0;
1428+ }
1429+ uint64_t val = 0;
1430+ for (uint8_t b = 0; b < byteSize; ++b) {
1431+ val |= static_cast<uint64_t>(static_cast<uint8_t>(buf[b])) << (BITS_PER_BYTE * b);
1432+ }
1433+ return val;
1434+}
1435+ 
1436+uint8_t StaticRawheapTranslate::FieldSize(uint8_t fieldType)
1437+{
1438+ switch (fieldType) {
1439+ case static_cast<uint8_t>(StaFieldType::BOOLEAN):
1440+ case static_cast<uint8_t>(StaFieldType::BYTE):
1441+ return 1;
1442+ case static_cast<uint8_t>(StaFieldType::CHAR):
1443+ case static_cast<uint8_t>(StaFieldType::SHORT):
1444+ return sizeof(uint16_t);
1445+ case static_cast<uint8_t>(StaFieldType::INT):
1446+ case static_cast<uint8_t>(StaFieldType::FLOAT):
1447+ return sizeof(uint32_t);
1448+ case static_cast<uint8_t>(StaFieldType::LONG):
1449+ case static_cast<uint8_t>(StaFieldType::DOUBLE):
1450+ case static_cast<uint8_t>(StaFieldType::TAGGED):
1451+ return sizeof(uint64_t);
1452+ case static_cast<uint8_t>(StaFieldType::OBJECT):
1453+ case static_cast<uint8_t>(StaFieldType::ARRAY):
1454+ case static_cast<uint8_t>(StaFieldType::WEAK_OBJECT):
1455+ return sizeof(uint32_t); // nodeId (4 bytes) instead of address (8 bytes)
1456+ case static_cast<uint8_t>(StaFieldType::UNKNOWN):
1457+ default:
1458+ return 0;
1459+ }
1460+}
1461+ 
1462+} // namespace rawheap_translate
Aecmascript/dfx/hprof/rawheap_translate/static_rawheap_translate.h+289-0
@@ -0,0 +1,289 @@
1+/*
wanghuan2022wanghuan2022
wanghuan2022wanghuan20227月8日

static_rawheap_translate.cpp:1980 vs rawheap_translate.cpp:1092 — 版本检测策略不一致

ParseHeadermemcmp(version, STATIC_SNAPSHOT_VERSION, 8) 严格匹配 "3.0.0\0\0\0",而 IsStaticSnapshotFormatVersion::Parse + major >= 3 接受任何 3.x 版本。未来 3.1.0 文件会被 IsStaticSnapshotFormat 识别为静态格式但被 ParseHeader 拒绝,导致翻译失败。

建议:

  • ParseHeader 改用 Version::Parse + major == 3,与 IsStaticSnapshotFormat 保持一致
  • 或至少在 memcmp 失败时给出明确的版本不兼容提示
likedislike
yangxiaoshuai2022
yangxiaoshuai2022
7月28日 评论:
Petrov Igor
Petrov Igor
27 天前 评论:
yangxiaoshuai2022
yangxiaoshuai2022
23 天前 评论:
wanghuan2022wanghuan20227月8日

static_rawheap_translate.cpp:2199 — CollectArrayItems 无 length 上界检查,畸形文件可触发解析器 OOM

CollectArrayItems 直接用 std::vector<char> bodyBuf(length) 分配缓冲区,length 来自文件 record header,无上界检查。畸形文件的 length 字段可设为 GB 级值,导致解析器 OOM。

建议:

  • 添加上界检查:if (length > MAX_RECORD_BODY_SIZE) { LOG_ERROR; return false; }
  • MAX_RECORD_BODY_SIZE 可设为合理值(如 256MB)
likedislike
yangxiaoshuai2022
yangxiaoshuai2022
28 天前 评论:
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS of ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+#ifndef RAWHEAP_TRANSLATE_STATIC_RAWHEAP_TRANSLATE_H
17+#define RAWHEAP_TRANSLATE_STATIC_RAWHEAP_TRANSLATE_H
18+ 
19+#include "rawheap_translate.h"
20+#include <unordered_map>
21+#include <vector>
22+#include <cstdint>
23+ 
24+namespace rawheap_translate {
25+ 
26+/** @brief Per-item layout computed during CollectArrayItems Pass 1. */
27+struct ArrayItemLayout {
28+ size_t prefixOff = 0;
29+ uint32_t arrayLength = 0;
30+ uint8_t elementType = 0;
31+ size_t dataSize = 0;
32+ bool dataSizeKnown = false;
33+};
34+ 
35+/**
36+ * @brief Parser for the static binary snapshot format (ArkTS-Sta / ETS side).
37+ *
38+ * The static file is self-describing: it carries its own string pool
39+ * (STRING_IN_UTF8), class descriptors (LOAD_CLASS + STATIC_CLASS_DUMP) and
40+ * structured field values (STATIC_INSTANCE_DUMP, STATIC_ARRAY_DUMP), so unlike
41+ * V1/V2 it does not depend on an external metadata JSON.
42+ *
43+ * Counterpart writer: runtime_core/static_core/runtime/tooling/hprof/static_dump.h;
44+ * shared wire format in rawheap_translate/common.h and
45+ * runtime_core/static_core/plugins/ets/runtime/tooling/hprof/session/dump_format.h.
46+ *
47+ * Parsing is two-phase:
48+ * 1. Collect - read every RecordHeader + body, store raw records into
49+ * containers. No graph is built yet.
50+ * 2. Build - once all records (including class descriptors, which may
51+ * appear after the instances that reference them) are known,
52+ * walk instances / arrays / roots and create Node + Edge.
53+ *
54+ * Parse() does both phases. Translate() only runs in single-file mode: it
55+ * creates the synthetic root + StaticRoot group + primitive nodes. In two-file
56+ * (merge) mode the SnapshotMerger consumes the collected graph directly and
57+ * never calls Translate().
58+ *
59+ * recordCount in the file header is a summary metric, NOT the on-disk record
60+ * count - the main loop is EOF-driven (see the field-level note on Header::recordCount).
61+ */
62+class StaticRawheapTranslate : public RawHeap {
63+public:
64+ StaticRawheapTranslate() = default;
65+ ~StaticRawheapTranslate() override;
66+ 
67+ bool Parse(FileReader &file, uint32_t rawheapFileSize) override;
68+ bool Translate() override;
69+ 
70+ /**
71+ * @brief Enable the synthetic-root framework (single-file output mode).
72+ *
73+ * When set, Parse() prepends a SyntheticRoot + StaticRoot group to the
74+ * built graph so the result is a standalone .heapsnapshot. Leave it unset
75+ * (default) when the parser feeds a SnapshotMerger in two-file mode.
76+ */
77+ void EnableRootFramework() { buildRootFramework_ = true; }
78+ 
79+ /** Collected GC roots (object nodeIds), for the merger. */
80+ const std::vector<uint32_t> &GetRoots() const { return roots_; }
81+ 
82+ /** Collected XRef records, for the merger. */
83+ struct XRefRecord {
84+ uint32_t dynNodeId; // dynamic-side nodeId (4 bytes on disk)
85+ uint32_t staNodeId; // nodeId in static heap
86+ uint8_t direction;
87+ };
88+ const std::vector<XRefRecord> &GetXRefs() const { return xrefs_; }
89+ 
90+ /**
91+ * @brief Look up a Node by its nodeId.
92+ * Used by the merger to resolve XRef staNodeId values that point into
93+ * the static side. Returns nullptr if not found.
94+ */
95+ Node *FindNodeByNodeId(uint32_t nodeId) const;
96+ 
97+private:
98+ struct Header {
99+ uint32_t identifierSize = 0;
100+ uint64_t timestamp = 0;
101+ uint8_t language = 0;
102+ uint32_t headerSize = 0;
103+ uint32_t recordCount = 0; // summary metric only
104+ uint32_t featureFlags = 0;
105+ };
106+ 
107+ struct FieldDef {
108+ uint32_t nameId = 0;
109+ uint8_t type = 0;
110+ uint32_t offset = 0;
111+ uint16_t flags = 0;
112+ };
113+ 
114+ // A single field value read from STATIC_INSTANCE_DUMP or STATIC_CLASS_DUMP's
115+ // static-value section. For OBJECT/ARRAY, `value` holds a nodeId (uint32_t
116+ // cast to uint64_t).
117+ struct FieldValue {
118+ uint8_t type = 0;
119+ uint64_t value = 0;
120+ };
121+ 
122+ struct ClassInfo {
123+ uint32_t classNameId = 0;
124+ uint32_t instanceSize = 0;
125+ uint32_t superClassNodeId = 0; // superclass classObjectId (0 if none)
126+ std::vector<FieldDef> staticFields;
127+ std::vector<FieldValue> staticValues; // parallel to staticFields (same order)
128+ std::vector<uint32_t> methodNameIds; // declared method-name string-pool ids
129+ std::vector<FieldDef> instanceFields;
130+ };
131+ 
132+ struct InstanceRecord {
133+ uint32_t objectNodeId = 0;
134+ uint32_t classNodeId = 0;
135+ uint32_t instanceSize = 0;
136+ std::vector<FieldValue> values;
137+ };
138+ 
139+ struct ArrayRecord {
140+ uint32_t objectNodeId = 0;
141+ uint32_t classNodeId = 0;
142+ uint32_t instanceSize = 0;
143+ uint32_t length = 0;
144+ uint8_t elementType = 0;
145+ std::vector<uint32_t> elements; // populated only for OBJECT/ARRAY (nodeIds)
146+ // TAGGED arrays carry one runtime-typed FieldValue per element because
147+ // their payload widths vary (OBJECT=u32, TAGGED=u64, BOOLEAN=u8, ...).
148+ std::vector<FieldValue> taggedValues;
149+ // Raw LE element bytes for primitive arrays (length * FieldSize); empty for
150+ // OBJECT/ARRAY/TAGGED and unknown types. CreateArrayEdges boxes each into a wrapper.
151+ std::vector<char> primData;
152+ };
153+ 
154+ // A string object dumped via TAG_STATIC_STRING_DUMP. The UTF-8 content
155+ // becomes the node's name so string values are visible in the .heapsnapshot
156+ // (a plain INSTANCE_DUMP has no per-instance name field).
157+ struct StringInstanceRecord {
158+ uint32_t objectNodeId = 0;
159+ uint32_t classNodeId = 0;
160+ uint32_t instanceSize = 0;
161+ std::string content;
162+ };
163+ 
164+ Header header_;
165+ bool buildRootFramework_ {false};
166+ bool parseOk_ {true}; // set to false on any read failure
167+ bool readingRecord_ {false}; // enables record-body bounds in ReadBytes
168+ uint32_t recordRemaining_ {0};
169+ FileReader *file_ {nullptr}; // set during Parse(), used by primitive readers
170+ 
171+ std::unordered_map<uint32_t, std::string> stringTable_;
172+ std::unordered_map<uint32_t, ClassInfo> classMap_;
173+ std::vector<InstanceRecord> instances_;
174+ std::vector<ArrayRecord> arrays_;
175+ std::vector<StringInstanceRecord> stringInstances_;
176+ std::vector<uint32_t> roots_;
177+ std::vector<XRefRecord> xrefs_;
178+ std::unordered_map<uint32_t, Node *> nodeIdToNode_;
179+ uint32_t valueNodeCounter_ {0}; // synthetic nodeId generator for value nodes
180+ 
181+ // Phase 1: Collect (record reading).
182+ bool ParseHeader();
183+ bool ParseRecord(uint64_t &offset, uint64_t fileSize);
184+ bool DispatchRecord(uint8_t tag, uint32_t length, uint32_t count);
185+ bool SkipBody(uint32_t length);
186+ 
187+ // Record collectors (batched: each processes count items from the body).
188+ bool CollectStringItems(uint32_t length, uint32_t count);
189+ bool CollectLoadClassItems(uint32_t length, uint32_t count);
190+ bool CollectStaticClassDumpItems(uint32_t length, uint32_t count);
191+ // CollectStaticClassDumpItems helpers (one section each, single responsibility).
192+ void ReadFieldDescriptor(FieldDef &fd);
193+ void ReadFieldDescriptors(std::vector<FieldDef> &out);
194+ void ReadStaticValues(std::vector<FieldValue> &out);
195+ void ReadMethodNames(std::vector<uint32_t> &out);
196+ bool CollectRootItems(uint32_t length, uint32_t count);
197+ bool CollectInstanceItems(uint32_t length, uint32_t count);
198+ bool CollectArrayItems(uint32_t length, uint32_t count);
199+ bool CollectStaticStringDumpItems(uint32_t length, uint32_t count);
200+ bool ScanArrayPrefixes(char *body, uint32_t length, std::vector<struct ArrayItemLayout> &layouts,
201+ size_t &totalKnownData, size_t &totalUnknownLength);
202+ bool ScanArrayDataSize(const char *body, uint32_t length, size_t dataOffset,
203+ ArrayItemLayout &layout, size_t &totalUnknownLength);
204+ bool DistributeUnknownData(std::vector<struct ArrayItemLayout> &layouts,
205+ uint32_t count, size_t totalKnownData,
206+ uint32_t length, size_t totalUnknownLength);
207+ bool BuildArrayRecords(char *body, uint32_t count,
208+ uint32_t bodyLength, const std::vector<struct ArrayItemLayout> &layouts);
209+ bool BuildArrayRecord(char *body, uint32_t bodyLength, uint32_t itemIndex,
210+ const ArrayItemLayout &layout, ArrayRecord &record);
211+ bool ReadTaggedArrayValues(const char *body, size_t dataOffset, size_t dataSize, ArrayRecord &record);
212+ bool CollectXRefItems(uint32_t length, uint32_t count);
213+ bool CollectHeapSummary(uint32_t length);
214+ 
215+ // Phase 2: Build (graph construction from collected records).
216+ void BuildGraph(bool withRootFramework);
217+ // classMap_ keys in ascending nodeId order (deterministic output).
218+ std::vector<uint32_t> SortedClassNodeIds() const;
219+ void CreateClassNodes();
220+ void CreateInstanceNodes();
221+ void CreateArrayNodes();
222+ void CreateStringNodes(); // STRING-typed nodes from TAG_STATIC_STRING_DUMP
223+ void CreateRootEdges(Node *syntheticRoot, Node *staticRoot);
224+ // Emits a class node's superclass edge (INTERNAL "superClass") AND its static
225+ // field edges (PROPERTY) in one pass, so a class's edges stay contiguous in
226+ // the flat edge vector - required by the .heapsnapshot grouping contract
227+ // (node i's edges are the next edgeCount[i] edges). Splitting them across
228+ // phases would interleave other nodes' edges between them.
229+ void CreateClassEdges();
230+ // CreateClassEdges helpers.
231+ void EmitSuperClassEdge(Node *classNode, const ClassInfo &info);
232+ void EmitStaticFieldEdges(Node *classNode, const ClassInfo &info);
233+ bool EmitFieldValueEdge(Node *from, const FieldValue &value, StringId nameId);
234+ // Emit one PROPERTY edge per declared method name (class -> synthetic
235+ // "closure" node, edge name = method name). The static dumper records
236+ // method-name string ids in CLASS_DUMP's methodNameId[]; without this path
237+ // they would be read into ClassInfo::methodNameIds but never reach the
238+ // .heapsnapshot (no edge references them, so the strings are never promoted
239+ // into the strings table).
240+ void EmitMethodNameEdges(Node *classNode, const ClassInfo &info);
241+ void CreateInstanceEdges();
242+ void EmitInstanceFieldEdges(Node *node, const InstanceRecord &rec,
243+ const ClassInfo &info);
244+ void EmitFallbackFieldEdges(Node *node, const InstanceRecord &rec);
245+ void CreateArrayEdges();
246+ // CreateArrayEdges helpers.
247+ Node *CreateArrayBufferNode(Node *arrayNode, StringId bufferNameId);
248+ void EmitArrayRefElementEdges(Node *buffer, const ArrayRecord &rec);
249+ void EmitTaggedArrayElementEdges(Node *buffer, const ArrayRecord &rec);
250+ void EmitArrayBoxedPrimitiveEdges(Node *buffer, const ArrayRecord &rec,
251+ StringId valueNameId);
252+ // Emit the string node's hclass edge (string -> std.core.String class node),
253+ // mirroring the instance->class edge so string nodes are graph-reachable
254+ // and the String class node is connected to its instances.
255+ void CreateStringEdges();
256+ 
257+ // Emit (or reuse) a synthetic value node for a primitive field
258+ // value. Returns the target node for an edge. nodeId lives in a high
259+ // synthetic range (0x40000000+) to avoid colliding with real heap addrs.
260+ Node *GetOrCreateValueNode(uint8_t fieldType, uint64_t value);
261+ // Emit a synthetic "closure" node representing a declared method, named
262+ // after the method. One node per method-name occurrence (each function is
263+ // its own node). Shares the value-node synthetic-id range.
264+ Node *GetOrCreateMethodNode(StringId nameId);
265+ // Stringify a primitive field value for its synthetic value node's name.
266+ std::string MakeValueNodeName(uint8_t fieldType, uint64_t value) const;
267+ std::string MakeTaggedValueNodeName(uint64_t value) const;
268+ 
269+ Node *GetOrCreateNode(uint32_t nodeId);
270+ std::string GetString(uint32_t stringId) const;
271+ StringId GetOrCreateStringId(uint32_t stringId);
272+ 
273+ // Bounded reader for a record body. Header reads use the same helper with
274+ // readingRecord_ disabled.
275+ bool ReadBytes(char *buffer, uint32_t size);
276+ 
277+ // Primitive readers using ReadBytes (little-endian). On read failure, set
278+ // parseOk_ = false and return 0.
279+ uint8_t ReadU8();
280+ uint16_t ReadU16();
281+ uint32_t ReadU32();
282+ uint64_t ReadU64();
283+ // Read a field value of known byte size from file_ (little-endian pack).
284+ uint64_t ReadFieldValue(uint8_t byteSize);
285+ static uint8_t FieldSize(uint8_t fieldType);
286+};
287+ 
288+} // namespace rawheap_translate
289+#endif // RAWHEAP_TRANSLATE_STATIC_RAWHEAP_TRANSLATE_H
Mecmascript/dfx/hprof/rawheap_translate/string_hashmap.cpp+8-1
@@ -13,7 +13,7 @@
13 * limitations under the License.13 * limitations under the License.
14 */14 */
15 15 
16-#include "ecmascript/dfx/hprof/rawheap_translate/string_hashmap.h"16+#include "string_hashmap.h"
17 17 
18namespace rawheap_translate {18namespace rawheap_translate {
19std::string StringHashMap::GetStringByKey(StringKey key) const19std::string StringHashMap::GetStringByKey(StringKey key) const
@@ -27,6 +27,13 @@ std::string StringHashMap::GetStringByKey(StringKey key) const
27 27 
28StringKey StringHashMap::GetKeyByStringId(StringId stringId) const28StringKey StringHashMap::GetKeyByStringId(StringId stringId) const
29{29{
30+ // Bounds check: stringId must be >= CUSTOM_STRID_START and within the
31+ // inserted range. Out-of-range ids (e.g. a node whose strId was never
32+ // assigned) return 0, which GetStringByKey treats as "not found".
33+ if (stringId < CUSTOM_STRID_START ||
34+ static_cast<size_t>(stringId - CUSTOM_STRID_START) >= orderedKey_.size()) {
35+ return 0;
36+ }
30 return orderedKey_[stringId - CUSTOM_STRID_START]; // 3: index_ start from 337 return orderedKey_[stringId - CUSTOM_STRID_START]; // 3: index_ start from 3
31}38}
32 39 
Mecmascript/dfx/hprof/rawheap_translate/string_hashmap.h+3-3
@@ -16,8 +16,8 @@
16#ifndef RAWHEAP_TRANSLATE_STRING_HASHMAP_H16#ifndef RAWHEAP_TRANSLATE_STRING_HASHMAP_H
17#define RAWHEAP_TRANSLATE_STRING_HASHMAP_H17#define RAWHEAP_TRANSLATE_STRING_HASHMAP_H
18 18 
19-#include "ecmascript/dfx/hprof/rawheap_translate/common.h"19+#include "common.h"
20-#include "ecmascript/dfx/hprof/rawheap_translate/utils.h"20+#include "utils.h"
21 21 
22namespace rawheap_translate {22namespace rawheap_translate {
23// An Implementation for Native StringTable without Auto Mem-Management23// An Implementation for Native StringTable without Auto Mem-Management
@@ -45,7 +45,7 @@ public:
45 std::string GetStringByKey(StringKey key) const;45 std::string GetStringByKey(StringKey key) const;
46 StringKey GetKeyByStringId(StringId stringId) const;46 StringKey GetKeyByStringId(StringId stringId) const;
47 StringId InsertStrAndGetStringId(const std::string &cstrArg);47 StringId InsertStrAndGetStringId(const std::string &cstrArg);
48- size_t GetCapcity() const48+ size_t GetCapacity() const
49 {49 {
50 return orderedKey_.size();50 return orderedKey_.size();
51 }51 }
Mecmascript/dfx/hprof/rawheap_translate/utils.cpp+35-9
@@ -21,7 +21,8 @@
21#include <sys/stat.h>21#include <sys/stat.h>
22#include <ctime>22#include <ctime>
23#include <sstream>23#include <sstream>
24-#include "ecmascript/dfx/hprof/rawheap_translate/utils.h"24+#include "securec.h"
25+#include "utils.h"
25 26 
26namespace rawheap_translate {27namespace rawheap_translate {
27bool RealPath(const std::string &filename, std::string &realpath)28bool RealPath(const std::string &filename, std::string &realpath)
@@ -40,8 +41,9 @@ bool RealPath(const std::string &filename, std::string &realpath)
40 return false;41 return false;
41 }42 }
42 43 
43- char resolvedPath[PATH_MAX];44+ char resolvedPath[PATH_MAX] = {};
44- if (strcpy_s(resolvedPath, PATH_MAX, filename.c_str()) != 0) {45+ if (memcpy_s(resolvedPath, PATH_MAX, filename.c_str(), filename.size() + 1) != EOK) {
46+ LOG_ERROR_ << "memcpy_s failed!";
45 return false;47 return false;
46 }48 }
47 49 
@@ -89,6 +91,22 @@ bool GenerateDumpFileName(std::string &filename)
89 return true;91 return true;
90}92}
91 93 
94+bool GenerateOutputNameFromInput(const std::string &userOutput, std::string &output)
95+{
96+ if (userOutput.empty()) {
97+ // Not provided: use the timestamped hprof_<ts>.heapsnapshot name.
98+ return GenerateDumpFileName(output);
99+ }
100+ if (EndsWith(userOutput, ".heapsnapshot")) {
101+ // Correctly provided: use as-is.
102+ output = userOutput;
103+ return true;
104+ }
105+ // Provided but wrong extension: append ".heapsnapshot".
106+ output = userOutput + ".heapsnapshot";
107+ return true;
108+}
109+ 
92bool EndsWith(const std::string &str, const std::string &suffix)110bool EndsWith(const std::string &str, const std::string &suffix)
93{111{
94 if (str.length() < suffix.length()) {112 if (str.length() < suffix.length()) {
@@ -188,21 +206,29 @@ bool FileReader::Read(char *buf, uint32_t size)
188 LOG_ERROR_ << "file buf is nullptr!";206 LOG_ERROR_ << "file buf is nullptr!";
189 return false;207 return false;
190 }208 }
191- if (file_.read(buf, size).fail()) {209+ file_.read(buf, size);
210+ // eof+fail together means we requested more bytes than remain — a normal
211+ // EOF exit when parsing record-driven formats. Pure fail (no eof) is a
212+ // genuine I/O or format error worth logging.
213+ if (file_.eof() && file_.fail()) {
214+ file_.clear(); // reset both flags so subsequent Seek/Read works
215+ return false; // normal EOF
216+ }
217+ if (file_.fail()) {
192 LOG_ERROR_ << "read failed!";218 LOG_ERROR_ << "read failed!";
193 return false;219 return false;
194 }220 }
195 return true;221 return true;
196}222}
197 223 
198-bool FileReader::Seek(uint32_t offset)224+bool FileReader::Seek(uint64_t offset)
199{225{
200 if (!file_.is_open()) {226 if (!file_.is_open()) {
201 LOG_ERROR_ << "file not open!";227 LOG_ERROR_ << "file not open!";
202 return false;228 return false;
203 }229 }
204 file_.clear();230 file_.clear();
205- if (!file_.seekg(offset)) {231+ if (!file_.seekg(static_cast<std::streamoff>(offset))) {
206 LOG_ERROR_ << "set file offset failed, offset=" << offset;232 LOG_ERROR_ << "set file offset failed, offset=" << offset;
207 return false;233 return false;
208 }234 }
@@ -231,7 +257,7 @@ bool FileReader::ReadArray(std::vector<uint64_t> &array, uint32_t size)
231 return true;257 return true;
232}258}
233 259 
234-bool FileReader::CheckAndGetHeaderAt(uint32_t offset, uint32_t assertNum)260+bool FileReader::CheckAndGetHeaderAt(uint64_t offset, uint32_t assertNum)
235{261{
236 constexpr int HEADER_SIZE = sizeof(uint64_t) / sizeof(uint32_t);262 constexpr int HEADER_SIZE = sizeof(uint64_t) / sizeof(uint32_t);
237 std::vector<uint32_t> header(HEADER_SIZE);263 std::vector<uint32_t> header(HEADER_SIZE);
@@ -250,14 +276,14 @@ bool FileReader::CheckAndGetHeaderAt(uint32_t offset, uint32_t assertNum)
250 return true;276 return true;
251}277}
252 278 
253-uint32_t FileReader::GetFileSize(const std::string &path)279+uint64_t FileReader::GetFileSize(const std::string &path)
254{280{
255 if (path.empty()) {281 if (path.empty()) {
256 return 0;282 return 0;
257 }283 }
258 struct stat fileInfo;284 struct stat fileInfo;
259 if (stat(path.c_str(), &fileInfo) == 0) {285 if (stat(path.c_str(), &fileInfo) == 0) {
260- return static_cast<uint32_t>(fileInfo.st_size);286+ return static_cast<uint64_t>(fileInfo.st_size);
261 }287 }
262 return 0;288 return 0;
263}289}
Mecmascript/dfx/hprof/rawheap_translate/utils.h+16-6
@@ -25,8 +25,8 @@
25#include <unordered_set>25#include <unordered_set>
26#include <memory>26#include <memory>
27#include <string>27#include <string>
28-#include <securec.h>
29#include <sstream>28#include <sstream>
29+#include <cstring>
30 30 
31namespace rawheap_translate {31namespace rawheap_translate {
32#define LOG_INFO_ Logger(0) << std::left << std::setw(24) << __func__32#define LOG_INFO_ Logger(0) << std::left << std::setw(24) << __func__
@@ -42,6 +42,12 @@ bool RealPath(const std::string &filename, std::string &realpath);
42 42 
43bool GenerateDumpFileName(std::string &filename);43bool GenerateDumpFileName(std::string &filename);
44 44 
45+// Resolve the output .heapsnapshot path from a user-provided argument:
46+// - empty: fall back to the timestamped hprof_<ts>.heapsnapshot name
47+// - already ends in ".heapsnapshot": use as-is
48+// - otherwise: append ".heapsnapshot" to the provided name
49+bool GenerateOutputNameFromInput(const std::string &userOutput, std::string &output);
50+ 
45bool EndsWith(const std::string &str, const std::string &suffix);51bool EndsWith(const std::string &str, const std::string &suffix);
46 52 
47bool IsLittleEndian();53bool IsLittleEndian();
@@ -94,10 +100,10 @@ public:
94 100 
95 bool Initialize(const std::string &path);101 bool Initialize(const std::string &path);
96 bool Read(char *buf, uint32_t size);102 bool Read(char *buf, uint32_t size);
97- bool Seek(uint32_t offset);103+ bool Seek(uint64_t offset);
98 bool ReadArray(std::vector<uint32_t> &array, uint32_t size);104 bool ReadArray(std::vector<uint32_t> &array, uint32_t size);
99 bool ReadArray(std::vector<uint64_t> &array, uint32_t size);105 bool ReadArray(std::vector<uint64_t> &array, uint32_t size);
100- bool CheckAndGetHeaderAt(uint32_t offset, uint32_t assertNum);106+ bool CheckAndGetHeaderAt(uint64_t offset, uint32_t assertNum);
101 107 
102 uint32_t GetHeaderLeft()108 uint32_t GetHeaderLeft()
103 {109 {
@@ -109,18 +115,18 @@ public:
109 return right_;115 return right_;
110 }116 }
111 117 
112- uint32_t GetFileSize()118+ uint64_t GetFileSize()
113 {119 {
114 return fileSize_;120 return fileSize_;
115 }121 }
116 122 
117- static uint32_t GetFileSize(const std::string &path);123+ static uint64_t GetFileSize(const std::string &path);
118 124 
119private:125private:
120 std::ifstream file_;126 std::ifstream file_;
121 uint32_t left_ {0};127 uint32_t left_ {0};
122 uint32_t right_ {0};128 uint32_t right_ {0};
123- uint32_t fileSize_ {0};129+ uint64_t fileSize_ {0};
124};130};
125 131 
126class Version {132class Version {
@@ -153,6 +159,10 @@ public:
153 return std::to_string(major_) + '.' + std::to_string(minor_) + '.' + std::to_string(build_);159 return std::to_string(major_) + '.' + std::to_string(minor_) + '.' + std::to_string(build_);
154 }160 }
155 161 
162+ int GetMajor() const { return major_; }
163+ int GetMinor() const { return minor_; }
164+ int GetBuild() const { return build_; }
165+ 
156private:166private:
157 int major_ {0};167 int major_ {0};
158 int minor_ {0};168 int minor_ {0};
Mecmascript/dfx/hprof/tests/BUILD.gn+36-6
@@ -73,12 +73,6 @@ host_unittest_action("HeapDumpTest") {
73 module_out_path = module_output_path73 module_out_path = module_output_path
74 74 
75 sources = [75 sources = [
76- # test file
77- "../rawheap_translate/metadata_parse.cpp",
78- "../rawheap_translate/rawheap_translate.cpp",
79- "../rawheap_translate/serializer.cpp",
80- "../rawheap_translate/string_hashmap.cpp",
81- "../rawheap_translate/utils.cpp",
82 "heap_dump_test.cpp",76 "heap_dump_test.cpp",
83 ]77 ]
84 78 
@@ -86,6 +80,7 @@ host_unittest_action("HeapDumpTest") {
86 80 
87 deps = [81 deps = [
88 ":gen_metadata_for_test",82 ":gen_metadata_for_test",
83+ "../rawheap_translate:rawheap_translate_static",
89 "../../../../:libark_jsruntime_test",84 "../../../../:libark_jsruntime_test",
90 ]85 ]
91 86 
@@ -271,9 +266,12 @@ host_unittest_action("RawHeapTranslateTest") {
271 # test file266 # test file
272 "../rawheap_translate/metadata_parse.cpp",267 "../rawheap_translate/metadata_parse.cpp",
273 "../rawheap_translate/rawheap_translate.cpp",268 "../rawheap_translate/rawheap_translate.cpp",
269+ "../rawheap_translate/snapshot_merger.cpp",
270+ "../rawheap_translate/static_rawheap_translate.cpp",
274 "../rawheap_translate/serializer.cpp",271 "../rawheap_translate/serializer.cpp",
275 "../rawheap_translate/string_hashmap.cpp",272 "../rawheap_translate/string_hashmap.cpp",
276 "../rawheap_translate/utils.cpp",273 "../rawheap_translate/utils.cpp",
274+ "../rawheap_translate/main.cpp",
277 "rawheap_translate_test.cpp",275 "rawheap_translate_test.cpp",
278 ]276 ]
279 277 
@@ -281,6 +279,36 @@ host_unittest_action("RawHeapTranslateTest") {
281 279 
282 deps = [ "$js_root:libark_jsruntime_test" ]280 deps = [ "$js_root:libark_jsruntime_test" ]
283 281 
282+ cflags_cc = [ "-std=c++17", "-DRAWHEAP_TRANSLATOR_UNITTEST" ]
283+ 
284+ external_deps = [
285+ "bounds_checking_function:libsec_static",
286+ "cJSON:cjson_static",
287+ "icu:shared_icui18n",
288+ "icu:shared_icuuc",
289+ ]
290+ 
291+ # hiviewdfx libraries
292+ external_deps += hiviewdfx_ext_deps
293+ deps += hiviewdfx_deps
294+}
295+ 
296+host_unittest_action("RawHeapStaticSnapshotTest") {
297+ module_out_path = module_output_path
298+ 
299+ sources = [
300+ "rawheap_static_snapshot_test.cpp",
301+ ]
302+ 
303+ configs = [ "$js_root:ecma_test_config" ]
304+ 
305+ # Uses rawheap_translate_static for the translate logic. The static lib
306+ # does not depend on arkplatform (common.h carries its own format constants).
307+ deps = [
308+ "../rawheap_translate:rawheap_translate_static",
309+ "$js_root:libark_jsruntime_test",
310+ ]
311+ 
284 cflags_cc = [ "-std=c++17" ]312 cflags_cc = [ "-std=c++17" ]
285 313 
286 external_deps = [314 external_deps = [
@@ -483,6 +511,7 @@ group("unittest") {
483 ":HeapTrackerSecondTest",511 ":HeapTrackerSecondTest",
484 ":HeapTrackerThirdTest",512 ":HeapTrackerThirdTest",
485 ":JSMetadataTest",513 ":JSMetadataTest",
514+ ":RawHeapStaticSnapshotTest",
486 ":RawHeapTranslateTest",515 ":RawHeapTranslateTest",
487 ":HybridHeapSnapshotTest",516 ":HybridHeapSnapshotTest",
488 ]517 ]
@@ -514,6 +543,7 @@ group("host_unittest") {
514 ":HeapTrackerThirdTestAction",543 ":HeapTrackerThirdTestAction",
515 ":HybridHeapSnapshotTestAction",544 ":HybridHeapSnapshotTestAction",
516 ":JSMetadataTestAction",545 ":JSMetadataTestAction",
546+ ":RawHeapStaticSnapshotTestAction",
517 ":RawHeapTranslateTestAction",547 ":RawHeapTranslateTestAction",
518 ]548 ]
519 if (is_mac) {549 if (is_mac) {
Mecmascript/dfx/hprof/tests/heap_dump_test.cpp+94-2
@@ -12,6 +12,7 @@
12 * See the License for the specific language governing permissions and12 * See the License for the specific language governing permissions and
13 * limitations under the License.13 * limitations under the License.
14 */14 */
15+#include <algorithm>
15#include <fcntl.h>16#include <fcntl.h>
16#include <regex>17#include <regex>
17#include <sstream>18#include <sstream>
@@ -22,6 +23,7 @@
22#include "ecmascript/dfx/hprof/heap_snapshot.h"23#include "ecmascript/dfx/hprof/heap_snapshot.h"
23#include "ecmascript/dfx/hprof/heap_profiler.h"24#include "ecmascript/dfx/hprof/heap_profiler.h"
24#include "ecmascript/dfx/hprof/heap_root_visitor.h"25#include "ecmascript/dfx/hprof/heap_root_visitor.h"
26+#include "ecmascript/dfx/hprof/rawheap_dump.h"
25#include "ecmascript/dfx/hprof/rawheap_translate/rawheap_translate.h"27#include "ecmascript/dfx/hprof/rawheap_translate/rawheap_translate.h"
26#include "ecmascript/dfx/hprof/heap_marker.h"28#include "ecmascript/dfx/hprof/heap_marker.h"
27#include "ecmascript/global_env.h"29#include "ecmascript/global_env.h"
@@ -66,7 +68,7 @@ public:
66 {68 {
67 TestHelper::CreateEcmaVMWithScope(ecmaVm_, thread_, scope_);69 TestHelper::CreateEcmaVMWithScope(ecmaVm_, thread_, scope_);
68 ecmaVm_->SetEnableForceGC(false);70 ecmaVm_->SetEnableForceGC(false);
69- HeapProfiler::ResetOOMDump();71+ HeapProfilerInterface::ResetOOMDump();
70 }72 }
71 73 
72 void TearDown() override74 void TearDown() override
@@ -169,7 +171,13 @@ public:
169 continue;171 continue;
170 }172 }
171 173 
172- if (translate.hashSet_.find(hash) == translate.hashSet_.end()) {174+ std::string expected = "Int:" + std::to_string(hash);
175+ bool found = std::any_of(translate.primitiveNodes_.begin(), translate.primitiveNodes_.end(),
176+ [&translate, &expected](const rawheap_translate::Node *node) {
177+ auto key = translate.strTable_->GetKeyByStringId(node->strId);
178+ return translate.strTable_->GetStringByKey(key) == expected;
179+ });
180+ if (!found) {
173 std::cout << "CheckHashInRawheap, missed object hash in rawheap." << std::endl;181 std::cout << "CheckHashInRawheap, missed object hash in rawheap." << std::endl;
174 return false;182 return false;
175 }183 }
@@ -872,6 +880,62 @@ private:
872 EcmaVM *instance {nullptr};880 EcmaVM *instance {nullptr};
873};881};
874 882 
883+class NodeIdTestStream : public Stream {
884+public:
885+ void EndOfStream() override {}
886+ 
887+ int GetSize() override
888+ {
889+ constexpr int chunkSize = 64;
890+ return chunkSize;
891+ }
892+ 
893+ bool WriteChunk([[maybe_unused]] char *data, [[maybe_unused]] int32_t size) override
894+ {
895+ return true;
896+ }
897+ 
898+ bool WriteBinBlock([[maybe_unused]] char *data, [[maybe_unused]] int32_t size) override
899+ {
900+ return true;
901+ }
902+ 
903+ bool Good() override
904+ {
905+ return true;
906+ }
907+ 
908+ void UpdateHeapStats([[maybe_unused]] HeapStat *data, [[maybe_unused]] int32_t count) override {}
909+ 
910+ void UpdateLastSeenObjectId([[maybe_unused]] int32_t lastSeenObjectId,
911+ [[maybe_unused]] int64_t timeStampUs) override
912+ {
913+ }
914+};
915+ 
916+class NodeIdTestRawHeapDump final : public RawHeapDump {
917+public:
918+ NodeIdTestRawHeapDump(const EcmaVM *vm, Stream *stream, EntryIdMap *entryIdMap,
919+ const DumpSnapShotOption &dumpOption)
920+ : RawHeapDump(vm, stream, nullptr, entryIdMap, dumpOption)
921+ {
922+ }
923+ 
924+ NodeId GenerateId(JSTaggedType addr)
925+ {
926+ return GenerateNodeId(addr);
927+ }
928+ 
929+private:
930+ void DumpRootTable() override {}
931+ void DumpStringTable() override {}
932+ void DumpObjectTable() override {}
933+ void DumpObjectMemory() override {}
934+ void UpdateStringTable([[maybe_unused]] JSTaggedType addr, [[maybe_unused]] StringId strId) override {}
935+ void CollectRootAddrByType([[maybe_unused]] const CSet<JSTaggedType> &rootSet) override {}
936+ void WriteGlobalRefHeapObjAddr([[maybe_unused]] JSTaggedType addr) override {}
937+};
938+ 
875class MockHeapProfiler : public HeapProfilerInterface {939class MockHeapProfiler : public HeapProfilerInterface {
876public:940public:
877 NO_MOVE_SEMANTIC(MockHeapProfiler);941 NO_MOVE_SEMANTIC(MockHeapProfiler);
@@ -975,6 +1039,12 @@ private:
975 Callback &cb_;1039 Callback &cb_;
976};1040};
977 1041 
1042+HWTEST_F_L0(HeapDumpTest, TestOOMDumpCanOnlyStartOnce)
1043+{
1044+ EXPECT_TRUE(HeapProfilerInterface::TryStartOOMDump());
1045+ EXPECT_FALSE(HeapProfilerInterface::TryStartOOMDump());
1046+}
1047+ 
978HWTEST_F_L0(HeapDumpTest, TestAllocationEvent)1048HWTEST_F_L0(HeapDumpTest, TestAllocationEvent)
979{1049{
980 const std::string abcFileName = HPROF_TEST_ABC_FILES_DIR"heapdump.abc";1050 const std::string abcFileName = HPROF_TEST_ABC_FILES_DIR"heapdump.abc";
@@ -1541,6 +1611,28 @@ void CreateObjectsForBinaryDump(JSThread *thread, ObjectFactory *factory, HeapDu
1541 CREATE_ARRAY_AND_ADD_REFS(factory, JSSharedArrayBuffer, 10, refs)1611 CREATE_ARRAY_AND_ADD_REFS(factory, JSSharedArrayBuffer, 10, refs)
1542}1612}
1543 1613 
1614+HWTEST_F_L0(HeapDumpTest, TestHybridOOMRegistersNodeIdForXRef)
1615+{
1616+ constexpr JSTaggedType objectAddress = 0x1000;
1617+ NodeIdTestStream stream;
1618+ 
1619+ DumpSnapShotOption legacyOption;
1620+ legacyOption.isDumpOOM = true;
1621+ EntryIdMap legacyIdMap;
1622+ NodeIdTestRawHeapDump legacyDump(ecmaVm_, &stream, &legacyIdMap, legacyOption);
1623+ EXPECT_NE(legacyDump.GenerateId(objectAddress), 0U);
1624+ EXPECT_EQ(legacyIdMap.FindNodeId(objectAddress), 0U);
1625+ 
1626+ DumpSnapShotOption hybridOption;
1627+ hybridOption.isDumpOOM = true;
1628+ hybridOption.isForHybridXRef = true;
1629+ EntryIdMap hybridIdMap;
1630+ NodeIdTestRawHeapDump hybridDump(ecmaVm_, &stream, &hybridIdMap, hybridOption);
1631+ NodeId nodeId = hybridDump.GenerateId(objectAddress);
1632+ EXPECT_NE(nodeId, 0U);
1633+ EXPECT_EQ(hybridIdMap.FindNodeId(objectAddress), static_cast<uint32_t>(nodeId));
1634+}
1635+ 
1544HWTEST_F_L0(HeapDumpTest, TestHeapDumpBinaryDumpV0)1636HWTEST_F_L0(HeapDumpTest, TestHeapDumpBinaryDumpV0)
1545{1637{
1546 ObjectFactory *factory = ecmaVm_->GetFactory();1638 ObjectFactory *factory = ecmaVm_->GetFactory();
Mecmascript/dfx/hprof/tests/heap_tracker_second_test.cpp+43-3
@@ -13,9 +13,15 @@
13 * limitations under the License.13 * limitations under the License.
14 */14 */
15 15 
16+#include <cerrno>
17+#include <cstdlib>
16#include <cstdio>18#include <cstdio>
17-#include <fstream>
18#include <fcntl.h>19#include <fcntl.h>
20+#include <fstream>
21+#if defined(__linux__)
22+#include <sys/wait.h>
23+#include <unistd.h>
24+#endif
19 25 
20#include "ecmascript/dfx/hprof/heap_profiler_interface.h"26#include "ecmascript/dfx/hprof/heap_profiler_interface.h"
21#include "ecmascript/dfx/hprof/heap_profiler.h"27#include "ecmascript/dfx/hprof/heap_profiler.h"
@@ -257,11 +263,45 @@ HWTEST_F_L0(HeapTrackerTest, GenDumpFileName_004)
257 HeapProfilerInterface::Destroy(instance);263 HeapProfilerInterface::Destroy(instance);
258}264}
259 265 
266+#if defined(__linux__)
260HWTEST_F_L0(HeapTrackerTest, FileDescriptorStreamEndOfStream)267HWTEST_F_L0(HeapTrackerTest, FileDescriptorStreamEndOfStream)
261{268{
262- int fd = 3;269+ int pipeFds[2] = {-1, -1};
270+ ASSERT_EQ(pipe(pipeFds), 0);
271+ int fd = pipeFds[1];
263 FileDescriptorStream fileStream(fd);272 FileDescriptorStream fileStream(fd);
264 EXPECT_TRUE(fileStream.Good());273 EXPECT_TRUE(fileStream.Good());
265 fileStream.EndOfStream();274 fileStream.EndOfStream();
275+ errno = 0;
276+ EXPECT_EQ(fcntl(fd, F_GETFD), -1);
277+ EXPECT_EQ(errno, EBADF);
278+ close(pipeFds[0]);
266}279}
267-} // namespace panda::test280+ 
281+HWTEST_F_L0(HeapTrackerTest, FileDescriptorStreamClosesFdZero)
282+{
283+ pid_t childPid = fork();
284+ ASSERT_GE(childPid, 0);
285+ if (childPid == 0) {
286+ close(STDIN_FILENO);
287+ int fd = open("/dev/null", O_WRONLY);
288+ if (fd != STDIN_FILENO) {
289+ _exit(EXIT_FAILURE);
290+ }
291+ FileDescriptorStream fileStream(fd);
292+ if (!fileStream.Good()) {
293+ _exit(EXIT_FAILURE);
294+ }
295+ fileStream.EndOfStream();
296+ errno = 0;
297+ bool fdClosed = fcntl(fd, F_GETFD) == -1 && errno == EBADF;
298+ _exit(fdClosed ? EXIT_SUCCESS : EXIT_FAILURE);
299+ }
300+ 
301+ int status = 0;
302+ ASSERT_EQ(waitpid(childPid, &status, 0), childPid);
303+ ASSERT_TRUE(WIFEXITED(status));
304+ EXPECT_EQ(WEXITSTATUS(status), EXIT_SUCCESS);
305+}
306+#endif
307+} // namespace panda::test
Mecmascript/dfx/hprof/tests/hybrid_heap_snapshot_test.cpp+271-9
@@ -13,6 +13,9 @@
13 * limitations under the License.13 * limitations under the License.
14 */14 */
15 15 
16+#include "ecmascript/checkpoint/thread_state_transition.h"
17+#include "ecmascript/dfx/hprof/dynamic_dump.h"
18+#include "ecmascript/dfx/hprof/heap_dump_session.h"
16#include "ecmascript/dfx/hprof/hybrid/hybrid_heap_profiler.h"19#include "ecmascript/dfx/hprof/hybrid/hybrid_heap_profiler.h"
17#include "ecmascript/dfx/hprof/hybrid/hybrid_heap_snapshot.h"20#include "ecmascript/dfx/hprof/hybrid/hybrid_heap_snapshot.h"
18#include "ecmascript/dfx/hprof/heap_snapshot_json_serializer.h"21#include "ecmascript/dfx/hprof/heap_snapshot_json_serializer.h"
@@ -22,7 +25,14 @@
22#include "ecmascript/global_env.h"25#include "ecmascript/global_env.h"
23#include "ecmascript/object_factory-inl.h"26#include "ecmascript/object_factory-inl.h"
24#include "ecmascript/tests/test_helper.h"27#include "ecmascript/tests/test_helper.h"
28+#include "profiler/heap_dump.h"
25 29 
30+#include <atomic>
31+#include <condition_variable>
32+#include <functional>
33+#include <memory>
34+#include <mutex>
35+#include <thread>
26#include <unordered_map>36#include <unordered_map>
27#include <unistd.h>37#include <unistd.h>
28 38 
@@ -30,6 +40,10 @@ using namespace panda::ecmascript;
30 40 
31namespace panda::test {41namespace panda::test {
32 42 
43+using common::dump::DumpExecutionMode;
44+using common::dump::DumpRequest;
45+using common::dump::DumpScope;
46+ 
33// ============================================================================47// ============================================================================
34// MockSTSVMInterface — implements all Hybrid Heapdump virtual methods48// MockSTSVMInterface — implements all Hybrid Heapdump virtual methods
35// ============================================================================49// ============================================================================
@@ -39,13 +53,17 @@ public:
39 std::vector<arkplatform::NodeInfo> roots_;53 std::vector<arkplatform::NodeInfo> roots_;
40 std::map<uint64_t, arkplatform::NodeInfo> nodeInfoMap_;54 std::map<uint64_t, arkplatform::NodeInfo> nodeInfoMap_;
41 std::map<uint64_t, std::vector<arkplatform::EdgeInfo>> edgeMap_;55 std::map<uint64_t, std::vector<arkplatform::EdgeInfo>> edgeMap_;
42- std::unordered_map<uint64_t, uint64_t> jsToEts_;56+ XRefMap jsToEts_;
43- std::unordered_map<uint64_t, uint64_t> etsToJs_;57+ XRefMap etsToJs_;
44 bool isAttached_ = true;58 bool isAttached_ = true;
45 bool xgcTriggered_ = false;59 bool xgcTriggered_ = false;
60+ bool xgcResult_ = true;
46 bool etsGCed_ = false;61 bool etsGCed_ = false;
47 bool etsSuspended_ = false;62 bool etsSuspended_ = false;
48 bool etsWasSuspended_ = false;63 bool etsWasSuspended_ = false;
64+ bool dumpRequested_ = false;
65+ bool dynamicDumpRequested_ = false;
66+ bool staticDumpRequested_ = false;
49 67 
50 void MarkFromObject(void *obj) override68 void MarkFromObject(void *obj) override
51 {69 {
@@ -105,7 +123,7 @@ public:
105 bool TriggerXGCAndWait() override123 bool TriggerXGCAndWait() override
106 {124 {
107 xgcTriggered_ = true;125 xgcTriggered_ = true;
108- return true;126+ return xgcResult_;
109 }127 }
110 128 
111 void EtsForceFullGC() override129 void EtsForceFullGC() override
@@ -162,14 +180,22 @@ public:
162 }180 }
163 }181 }
164 182 
165- void GetXRefMaps(uintptr_t ecmaVM, std::unordered_map<uint64_t, uint64_t> &jsToEts,183+ void GetXRefMaps(uintptr_t ecmaVM, XRefMap &jsToEts, XRefMap &etsToJs) override
166- std::unordered_map<uint64_t, uint64_t> &etsToJs) override
167 {184 {
168 (void)ecmaVM;185 (void)ecmaVM;
169 jsToEts = jsToEts_;186 jsToEts = jsToEts_;
170 etsToJs = etsToJs_;187 etsToJs = etsToJs_;
171 }188 }
172 189 
190+ bool ExecuteHeapDump(const DumpRequest &, arkplatform::EcmaVMInterface *ecmaInterface,
191+ bool dumpStaticHeap) override
192+ {
193+ dumpRequested_ = true;
194+ dynamicDumpRequested_ = ecmaInterface != nullptr;
195+ staticDumpRequested_ = dumpStaticHeap;
196+ return true;
197+ }
198+ 
173 bool IsCurrentThreadAttached() override199 bool IsCurrentThreadAttached() override
174 {200 {
175 return isAttached_;201 return isAttached_;
@@ -292,6 +318,12 @@ public:
292 {318 {
293 profiler.FillIdMap(vm, dumpOption);319 profiler.FillIdMap(vm, dumpOption);
294 }320 }
321+ 
322+ static bool BinaryDump(HybridHeapProfiler &profiler, EcmaVM *vm,
323+ DumpSnapShotOption &dumpOption)
324+ {
325+ return profiler.BinaryDump(vm, dumpOption);
326+ }
295};327};
296} // namespace panda::ecmascript328} // namespace panda::ecmascript
297 329 
@@ -418,6 +450,32 @@ static void AssertOutputContains(const std::string &output, const std::string &f
418 ASSERT_NE(output.find(field), std::string::npos) << "serialized snapshot should contain " << field;450 ASSERT_NE(output.find(field), std::string::npos) << "serialized snapshot should contain " << field;
419}451}
420 452 
453+static void WaitForHeapDumpSession(std::atomic<JSThread *> &contender, std::atomic<bool> &ready,
454+ std::atomic<bool> &start, std::atomic<bool> &acquired)
455+{
456+ RuntimeOption workerOption;
457+ workerOption.SetIsWorker();
458+ EcmaVM *workerVm = JSNApi::CreateJSVM(workerOption);
459+ if (workerVm == nullptr) {
460+ ready.store(true, std::memory_order_release);
461+ return;
462+ }
463+ 
464+ JSThread *workerThread = workerVm->GetJSThread();
465+ workerThread->ManagedCodeBegin();
466+ contender.store(workerThread, std::memory_order_release);
467+ ready.store(true, std::memory_order_release);
468+ while (!start.load(std::memory_order_acquire)) {
469+ std::this_thread::yield();
470+ }
471+ {
472+ HeapDumpSession waitingSession;
473+ acquired.store(true, std::memory_order_release);
474+ }
475+ workerThread->ManagedCodeEnd();
476+ JSNApi::DestroyJSVM(workerVm);
477+}
478+ 
421// ============================================================================479// ============================================================================
422// Fixtures480// Fixtures
423// ============================================================================481// ============================================================================
@@ -429,6 +487,7 @@ public:
429 void SetUp() override487 void SetUp() override
430 {488 {
431 TestHelper::CreateEcmaVMWithScope(ecmaVm_, thread_, scope_);489 TestHelper::CreateEcmaVMWithScope(ecmaVm_, thread_, scope_);
490+ JSNApi::InitHybridVMEnv(ecmaVm_);
432 ecmaVm_->SetEnableForceGC(false);491 ecmaVm_->SetEnableForceGC(false);
433 }492 }
434 493 
@@ -484,10 +543,123 @@ HWTEST_F_L0(PureFunctionTest, NewEnumAndStructValues)
484 ASSERT_NE(EdgeType::XREF, EdgeType::PROPERTY);543 ASSERT_NE(EdgeType::XREF, EdgeType::PROPERTY);
485}544}
486 545 
546+HWTEST_F_L0(PureFunctionTest, HeapDumpSessionsAreSerialized)
547+{
548+ constexpr size_t threadCount = 4;
549+ constexpr size_t iterations = 20;
550+ std::atomic<size_t> activeSessions {0};
551+ std::atomic<bool> concurrentSessions {false};
552+ std::vector<std::thread> workers;
553+ 
554+ for (size_t i = 0; i < threadCount; ++i) {
555+ workers.emplace_back([&activeSessions, &concurrentSessions]() {
556+ for (size_t j = 0; j < iterations; ++j) {
557+ HeapDumpSession session;
558+ if (activeSessions.fetch_add(1) != 0) {
559+ concurrentSessions.store(true);
560+ }
561+ std::this_thread::yield();
562+ activeSessions.fetch_sub(1);
563+ }
564+ });
565+ }
566+ 
567+ for (auto &worker : workers) {
568+ worker.join();
569+ }
570+ EXPECT_FALSE(concurrentSessions.load());
571+}
572+ 
487// ============================================================================573// ============================================================================
488// Profiler basic tests574// Profiler basic tests
489// ============================================================================575// ============================================================================
490 576 
577+HWTEST_F_L0(HybridHeapSnapshotTest, DynamicProcessDumpCanSuspendFromExternalThread)
578+{
579+ DumpRequest request;
580+ request.policy.executionMode = DumpExecutionMode::IN_PROCESS;
581+ request.policy.scope = DumpScope::PROCESS;
582+ request.policy.triggerGC = false;
583+ 
584+ std::mutex mutex;
585+ std::condition_variable condition;
586+ bool prepared = false;
587+ bool release = false;
588+ 
589+ ThreadSuspensionScope suspensionScope(thread_);
590+ std::thread worker([this, &request, &mutex, &prepared, &condition, &release]() {
591+ DynamicDump dumper(ecmaVm_, request);
592+ dumper.PrepareSession();
593+ {
594+ std::lock_guard<std::mutex> lock(mutex);
595+ prepared = true;
596+ }
597+ condition.notify_one();
598+ 
599+ std::unique_lock<std::mutex> lock(mutex);
600+ condition.wait(lock, [&release]() {
601+ return release;
602+ });
603+ });
604+ 
605+ {
606+ std::unique_lock<std::mutex> lock(mutex);
607+ condition.wait(lock, [&prepared]() {
608+ return prepared;
609+ });
610+ EXPECT_TRUE(thread_->HasSuspendRequest());
611+ release = true;
612+ }
613+ condition.notify_one();
614+ worker.join();
615+}
616+ 
617+HWTEST_F_L0(HybridHeapSnapshotTest, HeapDumpSessionWaitDoesNotBlockSharedGC)
618+{
619+ if (g_isEnableCMCGC) {
620+ GTEST_SKIP() << "JSThread state inspection is unavailable with CMC GC";
621+ }
622+ 
623+ auto owner = std::make_unique<HeapDumpSession>();
624+ std::atomic<JSThread *> contender {nullptr};
625+ std::atomic<bool> ready {false};
626+ std::atomic<bool> start {false};
627+ std::atomic<bool> acquired {false};
628+ 
629+ std::thread worker(WaitForHeapDumpSession, std::ref(contender), std::ref(ready),
630+ std::ref(start), std::ref(acquired));
631+ 
632+ while (!ready.load(std::memory_order_acquire)) {
633+ std::this_thread::yield();
634+ }
635+ JSThread *workerThread = contender.load(std::memory_order_acquire);
636+ if (workerThread == nullptr) {
637+ owner.reset();
638+ worker.join();
639+ FAIL() << "Cannot create worker VM";
640+ return;
641+ }
642+ 
643+ start.store(true, std::memory_order_release);
644+ constexpr size_t waitAttempts = 100000;
645+ bool enteredWaitState = false;
646+ for (size_t i = 0; i < waitAttempts; ++i) {
647+ if (workerThread->GetState() == ThreadState::WAIT) {
648+ enteredWaitState = true;
649+ break;
650+ }
651+ std::this_thread::yield();
652+ }
653+ EXPECT_TRUE(enteredWaitState);
654+ if (enteredWaitState) {
655+ SharedHeap::GetInstance()->CollectGarbage<TriggerGCType::SHARED_GC, GCReason::OTHER>(thread_);
656+ }
657+ 
658+ owner.reset();
659+ worker.join();
660+ EXPECT_TRUE(acquired.load(std::memory_order_acquire));
661+}
662+ 
491HWTEST_F_L0(HybridHeapSnapshotTest, ProfilerBasicBehavior)663HWTEST_F_L0(HybridHeapSnapshotTest, ProfilerBasicBehavior)
492{664{
493 HybridHeapProfiler profiler(ecmaVm_);665 HybridHeapProfiler profiler(ecmaVm_);
@@ -846,7 +1018,7 @@ HWTEST_F_L0(HybridHeapSnapshotTest, DumpWithXRef_VerifyXRefEdges)
846 JSTaggedValue globalObj = ecmaVm_->GetGlobalEnv()->GetGlobalObject();1018 JSTaggedValue globalObj = ecmaVm_->GetGlobalEnv()->GetGlobalObject();
847 ASSERT_TRUE(globalObj.IsHeapObject());1019 ASSERT_TRUE(globalObj.IsHeapObject());
848 uint64_t jsAddr = static_cast<uint64_t>(globalObj.GetRawData());1020 uint64_t jsAddr = static_cast<uint64_t>(globalObj.GetRawData());
849- mockSts->jsToEts_[jsAddr] = 0x1000;1021+ mockSts->jsToEts_.emplace(jsAddr, 0x1000);
850 1022 
851 EntryIdMap entryIdMap;1023 EntryIdMap entryIdMap;
852 StringHashMap stringTable(ecmaVm_);1024 StringHashMap stringTable(ecmaVm_);
@@ -868,7 +1040,7 @@ HWTEST_F_L0(HybridHeapSnapshotTest, DumpWithXRef_StaticToDynamic)
868 JSTaggedValue globalObj = ecmaVm_->GetGlobalEnv()->GetGlobalObject();1040 JSTaggedValue globalObj = ecmaVm_->GetGlobalEnv()->GetGlobalObject();
869 ASSERT_TRUE(globalObj.IsHeapObject());1041 ASSERT_TRUE(globalObj.IsHeapObject());
870 uint64_t jsAddr = static_cast<uint64_t>(globalObj.GetRawData());1042 uint64_t jsAddr = static_cast<uint64_t>(globalObj.GetRawData());
871- mockSts->etsToJs_[0x1000] = jsAddr;1043+ mockSts->etsToJs_.emplace(0x1000, jsAddr);
872 1044 
873 EntryIdMap entryIdMap;1045 EntryIdMap entryIdMap;
874 StringHashMap stringTable(ecmaVm_);1046 StringHashMap stringTable(ecmaVm_);
@@ -893,8 +1065,8 @@ HWTEST_F_L0(HybridHeapSnapshotTest, DumpWithXRef_BidirectionalMapping)
893 1065 
894 ASSERT_TRUE(globalObj.IsHeapObject());1066 ASSERT_TRUE(globalObj.IsHeapObject());
895 uint64_t jsAddr = static_cast<uint64_t>(globalObj.GetRawData());1067 uint64_t jsAddr = static_cast<uint64_t>(globalObj.GetRawData());
896- mockSts->jsToEts_[jsAddr] = 0x1000;1068+ mockSts->jsToEts_.emplace(jsAddr, 0x1000);
897- mockSts->etsToJs_[0x1000] = jsAddr;1069+ mockSts->etsToJs_.emplace(0x1000, jsAddr);
898 1070 
899 EntryIdMap entryIdMap;1071 EntryIdMap entryIdMap;
900 StringHashMap stringTable(ecmaVm_);1072 StringHashMap stringTable(ecmaVm_);
@@ -1363,4 +1535,94 @@ HWTEST_F_L0(HybridHeapSnapshotTest, Serialization_ValidateJSONStructure)
1363 AssertOutputContains(output, "\"Obj");1535 AssertOutputContains(output, "\"Obj");
1364}1536}
1365 1537 
1538+// ============================================================================
1539+// BinaryDump tests — verify that requests are routed with the selected runtime
1540+// participants. Byte-level output is covered by the ETS hprof tests.
1541+// ============================================================================
1542+ 
1543+HWTEST_F_L0(HybridHeapSnapshotTest, BinaryDump_NoSTSInterface_ReturnsFalse)
1544+{
1545+ HybridHeapProfiler profiler(ecmaVm_);
1546+ // No STS interface set → should return false
1547+ ASSERT_FALSE(profiler.HasSTSInterface());
1548+ 
1549+ DumpSnapShotOption dumpOption;
1550+ dumpOption.dumpFormat = DumpFormat::BINARY;
1551+ ASSERT_FALSE(HybridHeapProfilerTestHelper::BinaryDump(profiler, ecmaVm_, dumpOption));
1552+}
1553+ 
1554+HWTEST_F_L0(HybridHeapSnapshotTest, BinaryDump_DynamicOnly_SetsFlagsCorrectly)
1555+{
1556+ HybridHeapProfiler profiler(ecmaVm_);
1557+ auto mockSts = std::make_unique<MockSTSVMInterface>();
1558+ mockSts->isAttached_ = false; // STS not attached → only dynamic
1559+ HybridHeapProfilerTestHelper::SetSTSInterface(profiler, mockSts.get());
1560+ 
1561+ DumpSnapShotOption dumpOption;
1562+ dumpOption.dumpFormat = DumpFormat::BINARY;
1563+ dumpOption.isFullGC = false;
1564+ dumpOption.isSync = true;
1565+ 
1566+ // BinaryDump sets dumpDynamicHeap=true (vm!=null), dumpStaticHeap=false (not attached)
1567+ ASSERT_TRUE(HybridHeapProfilerTestHelper::BinaryDump(profiler, ecmaVm_, dumpOption));
1568+ ASSERT_TRUE(mockSts->dumpRequested_);
1569+ ASSERT_TRUE(mockSts->dynamicDumpRequested_);
1570+ ASSERT_FALSE(mockSts->staticDumpRequested_);
1571+}
1572+ 
1573+HWTEST_F_L0(HybridHeapSnapshotTest, BinaryDump_NeitherHeapAvailable_ReturnsFalse)
1574+{
1575+ HybridHeapProfiler profiler(ecmaVm_);
1576+ auto mockSts = std::make_unique<MockSTSVMInterface>();
1577+ mockSts->isAttached_ = false;
1578+ HybridHeapProfilerTestHelper::SetSTSInterface(profiler, mockSts.get());
1579+ 
1580+ // Pass vm=nullptr → dynamic heap not available, STS not attached → static not available
1581+ DumpSnapShotOption dumpOption;
1582+ dumpOption.dumpFormat = DumpFormat::BINARY;
1583+ ASSERT_FALSE(HybridHeapProfilerTestHelper::BinaryDump(profiler, nullptr, dumpOption));
1584+ ASSERT_FALSE(mockSts->dumpRequested_);
1585+}
1586+ 
1587+HWTEST_F_L0(HybridHeapSnapshotTest, BinaryDump_StaticOnly_SetsFlagsCorrectly)
1588+{
1589+ HybridHeapProfiler profiler(ecmaVm_);
1590+ auto mockSts = std::make_unique<MockSTSVMInterface>();
1591+ mockSts->isAttached_ = true;
1592+ HybridHeapProfilerTestHelper::SetSTSInterface(profiler, mockSts.get());
1593+ 
1594+ // vm=nullptr, STS attached → static-only
1595+ DumpSnapShotOption dumpOption;
1596+ dumpOption.dumpFormat = DumpFormat::BINARY;
1597+ dumpOption.isFullGC = false;
1598+ dumpOption.isSync = true;
1599+ 
1600+ ASSERT_TRUE(HybridHeapProfilerTestHelper::BinaryDump(profiler, nullptr, dumpOption));
1601+ ASSERT_FALSE(dumpOption.dumpDynamicHeap) << "vm=nullptr should mean dynamic=false";
1602+ ASSERT_TRUE(dumpOption.dumpStaticHeap) << "STS attached should mean static=true";
1603+ ASSERT_TRUE(mockSts->dumpRequested_);
1604+ ASSERT_FALSE(mockSts->dynamicDumpRequested_);
1605+ ASSERT_TRUE(mockSts->staticDumpRequested_);
1606+}
1607+ 
1608+HWTEST_F_L0(HybridHeapSnapshotTest, BinaryDump_HybridFlags)
1609+{
1610+ HybridHeapProfiler profiler(ecmaVm_);
1611+ auto mockSts = std::make_unique<MockSTSVMInterface>();
1612+ mockSts->isAttached_ = true;
1613+ HybridHeapProfilerTestHelper::SetSTSInterface(profiler, mockSts.get());
1614+ 
1615+ DumpSnapShotOption dumpOption;
1616+ dumpOption.dumpFormat = DumpFormat::BINARY;
1617+ dumpOption.isFullGC = false;
1618+ dumpOption.isSync = true;
1619+ 
1620+ ASSERT_TRUE(HybridHeapProfilerTestHelper::BinaryDump(profiler, ecmaVm_, dumpOption));
1621+ ASSERT_TRUE(dumpOption.dumpDynamicHeap) << "vm!=nullptr should mean dynamic=true";
1622+ ASSERT_TRUE(dumpOption.dumpStaticHeap) << "STS attached should mean static=true";
1623+ ASSERT_TRUE(mockSts->dumpRequested_);
1624+ ASSERT_TRUE(mockSts->dynamicDumpRequested_);
1625+ ASSERT_TRUE(mockSts->staticDumpRequested_);
1626+}
1627+ 
1366} // namespace panda::test1628} // namespace panda::test
Aecmascript/dfx/hprof/tests/rawheap_static_snapshot_test.cpp+1998-0
@@ -0,0 +1,1998 @@
1+/*
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS of ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+#include "ecmascript/dfx/hprof/rawheap_translate/common.h"
17+#include "ecmascript/dfx/hprof/rawheap_translate/rawheap_translate.h"
18+#include "ecmascript/dfx/hprof/rawheap_translate/snapshot_merger.h"
19+#include "ecmascript/dfx/hprof/rawheap_translate/static_rawheap_translate.h"
20+#include "ecmascript/dfx/hprof/rawheap_translate/utils.h"
21+#include "ecmascript/tests/test_helper.h"
22+ 
23+#include "securec.h"
24+#include <algorithm>
25+#include <array>
26+#include <cstring>
27+#include <fstream>
28+#include <limits>
29+#include <random>
30+#include <utility>
31+#include <vector>
32+ 
33+using namespace panda::ecmascript;
34+using namespace rawheap_translate;
35+ 
36+namespace panda::test {
37+ 
38+// Test timestamp value used in header serialization.
39+static constexpr uint64_t TEST_TIMESTAMP_VALUE = 1000000;
40+ 
41+// Parameter struct for WriteStaticArrayDumpRecord (reduces function parameter
42+// count). Object identifiers are u32 nodeIds (even numbers >= 2) in the new
43+// wire format.
44+struct ArrayDumpParams {
45+ uint32_t objectId = 0;
46+ uint32_t classObjectId = 0;
47+ uint32_t instanceSize = 0;
48+ uint32_t arrayLength = 0;
49+ uint8_t elementType = 0;
50+ // For OBJECT/ARRAY element types, each element is a u32 nodeId.
51+ std::vector<uint32_t> elements = {};
52+ // Primitive elements are encoded at their FieldSize width. Missing values
53+ // are zero-filled so existing tests can omit this vector.
54+ std::vector<uint64_t> primitiveValues = {};
55+ // TAGGED arrays are heterogeneous; each pair is runtime type + payload.
56+ std::vector<std::pair<uint8_t, uint64_t>> taggedValues = {};
57+};
58+ 
59+// ============================================================================
60+// Static binary snapshot test data builder (writes the canonical wire format)
61+// ============================================================================
62+ 
63+class StaticSnapshotDataBuilder {
64+public:
65+ StaticSnapshotDataBuilder() = default;
66+ 
67+ struct HeaderParams {
68+ std::array<char, STATIC_VERSION_SIZE> version = {'3', '.', '0', '.', '0', '\0', '\0', '\0'};
69+ uint32_t identifierSize = STATIC_IDENTIFIER_SIZE;
70+ uint8_t language = STATIC_LANGUAGE_STATIC;
71+ uint32_t headerSize = STATIC_HEADER_SIZE;
72+ uint32_t featureFlags = STATIC_SUPPORTED_FEATURE_FLAGS;
73+ };
74+ 
75+ // File header (33 bytes): version(8) + identifierSize(4) + timestamp(8)
76+ // + language(u8) + headerSize(u32) + recordCount(u32) + featureFlags(u32).
77+ // recordCount is a summary metric the parser ignores - it loops to EOF.
78+ void WriteHeader(uint8_t language = 1)
79+ {
80+ HeaderParams params;
81+ params.language = language;
82+ WriteHeader(params);
83+ }
84+ 
85+ void WriteHeader(const HeaderParams &params)
86+ {
87+ data_.insert(data_.end(), params.version.begin(), params.version.end());
88+ WriteU32(params.identifierSize);
89+ WriteU64(TEST_TIMESTAMP_VALUE);
90+ WriteU8(params.language);
91+ WriteU32(params.headerSize);
92+ WriteU32(0); // recordCount (summary metric, unused by parser)
93+ WriteU32(params.featureFlags);
94+ }
95+ 
96+ void WriteStringRecord(uint32_t stringId, const std::string &str)
97+ {
98+ BeginRecord(TAG_STRING_IN_UTF8);
99+ std::vector<uint8_t> body;
100+ AppendU32(body, stringId);
101+ AppendU32(body, static_cast<uint32_t>(str.size()));
102+ body.insert(body.end(), reinterpret_cast<const uint8_t *>(str.data()),
103+ reinterpret_cast<const uint8_t *>(str.data()) + str.size());
104+ EndRecord(body);
105+ }
106+ 
107+ // TAG_LOAD_CLASS body (21 bytes): classSerial(u32) + classObjectId(u32)
108+ // + stackTraceSerial(u32) + classNameId(u32) + language(u8) +
109+ // classFlags(u32).
110+ void WriteLoadClassRecord(uint32_t classObjectId, uint32_t classNameId)
111+ {
112+ BeginRecord(TAG_LOAD_CLASS);
113+ std::vector<uint8_t> body;
114+ AppendU32(body, 0); // classSerialNumber
115+ AppendU32(body, classObjectId);
116+ AppendU32(body, 0); // stackTraceSerial
117+ AppendU32(body, classNameId);
118+ AppendU8(body, 1); // language = STATIC
119+ AppendU32(body, 0); // classFlags
120+ EndRecord(body);
121+ }
122+ 
123+ // STATIC_CLASS_DUMP body: classObjectId(u32) + stackTraceSerial(u32)
124+ // + superClassObjectId(u32) + classLoaderObjectId(u32) + instanceSize(u32)
125+ // + staticFieldCount(u16) + N field descriptors (11 bytes each)
126+ // + instanceFieldCount(u16) + N descriptors
127+ // + staticValueCount(u16) + 0 values
128+ // + methodCount(u16) + 0 method name ids.
129+ void WriteStaticClassDumpRecord(uint32_t classObjectId, uint32_t instanceSize,
130+ const std::vector<uint32_t> &instanceFieldNames)
131+ {
132+ BeginRecord(TAG_STATIC_CLASS_DUMP);
133+ std::vector<uint8_t> body;
134+ AppendU32(body, classObjectId);
135+ AppendU32(body, 0); // stackTraceSerial
136+ AppendU32(body, 0); // superClassObjectId
137+ AppendU32(body, 0); // classLoaderObjectId
138+ AppendU32(body, instanceSize);
139+ AppendU16(body, 0); // staticFieldCount
140+ AppendU16(body, static_cast<uint16_t>(instanceFieldNames.size())); // instanceFieldCount
141+ for (uint32_t nameId : instanceFieldNames) {
142+ AppendU32(body, nameId); // nameId
143+ AppendU8(body,
144+ static_cast<uint8_t>(StaFieldType::OBJECT)); // type = OBJECT
145+ AppendU32(body, 0); // offset
146+ AppendU16(body, 0); // flags
147+ }
148+ AppendU16(body, 0); // staticValueCount
149+ AppendU16(body, 0); // methodCount
150+ EndRecord(body);
151+ }
152+ 
153+ // Variant of WriteStaticClassDumpRecord that also emits static field
154+ // descriptors. Used to verify that static field descriptors do NOT consume
155+ // instance field values during instance edge emission (EmitInstanceFieldEdges
156+ // must only iterate instance field descriptors, since INSTANCE_DUMP carries
157+ // instance field values only).
158+ void WriteStaticClassDumpRecordWithFields(uint32_t classObjectId, uint32_t instanceSize,
159+ const std::vector<uint32_t> &staticFieldNames,
160+ const std::vector<uint32_t> &instanceFieldNames)
161+ {
162+ BeginRecord(TAG_STATIC_CLASS_DUMP);
163+ std::vector<uint8_t> body;
164+ AppendU32(body, classObjectId);
165+ AppendU32(body, 0); // stackTraceSerial
166+ AppendU32(body, 0); // superClassObjectId
167+ AppendU32(body, 0); // classLoaderObjectId
168+ AppendU32(body, instanceSize);
169+ AppendU16(body, static_cast<uint16_t>(staticFieldNames.size())); // staticFieldCount
170+ for (uint32_t nameId : staticFieldNames) {
171+ AppendU32(body, nameId); // nameId
172+ AppendU8(body,
173+ static_cast<uint8_t>(StaFieldType::OBJECT)); // type = OBJECT
174+ AppendU32(body, 0); // offset
175+ AppendU16(body, 0); // flags (IS_STATIC bit unused by reader)
176+ }
177+ AppendU16(body, static_cast<uint16_t>(instanceFieldNames.size())); // instanceFieldCount
178+ for (uint32_t nameId : instanceFieldNames) {
179+ AppendU32(body, nameId); // nameId
180+ AppendU8(body,
181+ static_cast<uint8_t>(StaFieldType::OBJECT)); // type = OBJECT
182+ AppendU32(body, 0); // offset
183+ AppendU16(body, 0); // flags
184+ }
185+ AppendU16(body, 0); // staticValueCount
186+ AppendU16(body, 0); // methodCount
187+ EndRecord(body);
188+ }
189+ 
190+ // Full STATIC_CLASS_DUMP: superClassId + static field descriptors with types
191+ // + parallel static values + instance field descriptors (OBJECT) + method
192+ // name ids. Used to exercise the variable tail and the edge emission
193+ // (superClass, static-field, primitive value nodes) it drives.
194+ struct FieldDesc {
195+ uint32_t nameId;
196+ uint8_t type;
197+ };
198+ struct FieldValue {
199+ uint8_t type;
200+ uint64_t value;
201+ };
202+ // Parameter struct for WriteStaticClassDumpRecordFull (reduces function
203+ // parameter count): the variable tail of a STATIC_CLASS_DUMP record is made
204+ // of four parallel sequences (static field descriptors, static values,
205+ // instance field names, method name ids) that are written together.
206+ struct StaticClassDumpRecordFields {
207+ std::vector<FieldDesc> staticFields;
208+ std::vector<FieldValue> staticValues;
209+ std::vector<uint32_t> instanceFieldNames;
210+ std::vector<uint32_t> methodNameIds;
211+ };
212+ void WriteStaticClassDumpRecordFull(uint32_t classObjectId, uint32_t superClassId, uint32_t instanceSize,
213+ const StaticClassDumpRecordFields &fields)
214+ {
215+ const auto &staticFields = fields.staticFields;
216+ const auto &staticValues = fields.staticValues;
217+ const auto &instanceFieldNames = fields.instanceFieldNames;
218+ const auto &methodNameIds = fields.methodNameIds;
219+ BeginRecord(TAG_STATIC_CLASS_DUMP);
220+ std::vector<uint8_t> body;
221+ AppendU32(body, classObjectId);
222+ AppendU32(body, 0); // stackTraceSerial
223+ AppendU32(body, superClassId); // superClassObjectId
224+ AppendU32(body, 0); // classLoaderObjectId
225+ AppendU32(body, instanceSize);
226+ AppendU16(body,
227+ static_cast<uint16_t>(staticFields.size())); // staticFieldCount
228+ for (const auto &fd : staticFields) {
229+ AppendU32(body, fd.nameId);
230+ AppendU8(body, fd.type);
231+ AppendU32(body, 0); // offset
232+ AppendU16(body, 0); // flags
233+ }
234+ AppendU16(body, static_cast<uint16_t>(instanceFieldNames.size())); // instanceFieldCount
235+ for (uint32_t nameId : instanceFieldNames) {
236+ AppendU32(body, nameId);
237+ AppendU8(body, static_cast<uint8_t>(StaFieldType::OBJECT));
238+ AppendU32(body, 0);
239+ AppendU16(body, 0);
240+ }
241+ // Static field values (parallel to staticFields).
242+ AppendU16(body,
243+ static_cast<uint16_t>(staticValues.size())); // staticValueCount
244+ for (const auto &fv : staticValues) {
245+ AppendFieldValue(body, fv.type, fv.value);
246+ }
247+ // Method name ids.
248+ AppendU16(body, static_cast<uint16_t>(methodNameIds.size())); // methodCount
249+ for (uint32_t mid : methodNameIds) {
250+ AppendU32(body, mid);
251+ }
252+ EndRecord(body);
253+ }
254+ // + classNodeId(u32) + stackTraceSerial(u32) + instanceSize(u32)
255+ // + fieldCount(u16)) + per field: fieldType(u8) + value(FieldSize(type)
256+ // bytes). This builder only ever emits OBJECT fields, so each value is a u32
257+ // nodeId.
258+ void WriteStaticInstanceDumpRecord(uint32_t objectId, uint32_t classObjectId, uint32_t instanceSize,
259+ const std::vector<uint32_t> &fieldValues)
260+ {
261+ BeginRecord(TAG_STATIC_INSTANCE_DUMP);
262+ std::vector<uint8_t> body;
263+ AppendU32(body, objectId);
264+ AppendU32(body, classObjectId);
265+ AppendU32(body, 0); // stackTraceSerial
266+ AppendU32(body, instanceSize);
267+ AppendU16(body, static_cast<uint16_t>(fieldValues.size()));
268+ for (uint32_t v : fieldValues) {
269+ AppendU8(body,
270+ static_cast<uint8_t>(StaFieldType::OBJECT)); // type = OBJECT
271+ AppendU32(body, v); // OBJECT value: u32 nodeId (4 bytes)
272+ }
273+ EndRecord(body);
274+ }
275+ 
276+ void WriteStaticArrayDumpRecord(const ArrayDumpParams &params)
277+ {
278+ BeginRecord(TAG_STATIC_ARRAY_DUMP);
279+ std::vector<uint8_t> body;
280+ AppendArrayDumpItem(body, params);
281+ EndRecord(body);
282+ }
283+ 
284+ void WriteStaticArrayDumpRecordBatch(const std::vector<ArrayDumpParams> &items)
285+ {
286+ BeginRecord(TAG_STATIC_ARRAY_DUMP);
287+ std::vector<uint8_t> body;
288+ for (const auto &params : items) {
289+ AppendArrayDumpItem(body, params);
290+ }
291+ EndRecord(body, static_cast<uint32_t>(items.size()));
292+ }
293+ 
294+ // TAG_ROOT_RECORD body (5 bytes): rootType(u8) + objectNodeId(u32).
295+ void WriteRootRecord(uint32_t objectId)
296+ {
297+ BeginRecord(TAG_ROOT_RECORD);
298+ std::vector<uint8_t> body;
299+ AppendU8(body, ROOT_TYPE_STATIC_OBJECT); // rootType = STATIC_OBJECT
300+ AppendU32(body, objectId);
301+ EndRecord(body);
302+ }
303+ 
304+ // TAG_XREF_EDGE body (9 bytes): dynNodeId(u32) + staNodeId(u32) +
305+ // direction(u8). Both endpoints are 4-byte nodeIds (symmetric): the dynamic
306+ // side is the nodeId the dump resolved from the JS heap address via
307+ // GetNodeId.
308+ void WriteXRefEdgeRecord(uint32_t dynNodeId, uint32_t staNodeId, uint8_t direction)
309+ {
310+ BeginRecord(TAG_XREF_EDGE);
311+ std::vector<uint8_t> body;
312+ AppendU32(body, dynNodeId);
313+ AppendU32(body, staNodeId);
314+ AppendU8(body, direction);
315+ EndRecord(body);
316+ }
317+ 
318+ void WriteHeapSummaryRecord()
319+ {
320+ BeginRecord(TAG_HEAP_SUMMARY);
321+ std::vector<uint8_t> body;
322+ AppendU64(body, 0);
323+ AppendU64(body, 0);
324+ AppendU64(body, 0);
325+ AppendU64(body, 0);
326+ AppendU64(body, 0);
327+ AppendU64(body, 0);
328+ AppendU64(body, 0);
329+ EndRecord(body);
330+ }
331+ 
332+ // Write an unknown-tag record (for the skip test).
333+ void WriteUnknownRecord(uint8_t tag, const std::vector<uint8_t> &payload)
334+ {
335+ BeginRecord(tag);
336+ EndRecord(payload);
337+ }
338+ 
339+ void WriteRawRecord(uint8_t tag, uint32_t declaredLength, uint32_t count, const std::vector<uint8_t> &body)
340+ {
341+ BeginRecord(tag);
342+ (void)memcpy_s(data_.data() + lengthOffset_, data_.size() - lengthOffset_, &declaredLength,
343+ sizeof(declaredLength));
344+ (void)memcpy_s(data_.data() + countOffset_, data_.size() - countOffset_, &count, sizeof(count));
345+ data_.insert(data_.end(), body.begin(), body.end());
346+ }
347+ 
348+ // Write data to a temp file with mkstemp for unique naming.
349+ // Uses a random suffix instead of `this` pointer to avoid collisions
350+ // and improve portability.
351+ std::string WriteToTempFile(const std::string &tag) const
352+ {
353+ std::random_device rd;
354+ std::string path = "static_snap_test_" + tag + "_" + std::to_string(rd());
355+ std::ofstream ofs(path, std::ios::binary);
356+ ofs.write(reinterpret_cast<const char *>(data_.data()), data_.size());
357+ ofs.close();
358+ return path;
359+ }
360+ 
361+private:
362+ std::vector<uint8_t> data_;
363+ size_t lengthOffset_ = 0;
364+ size_t countOffset_ = 0;
365+ 
366+ static void AppendArrayDumpItem(std::vector<uint8_t> &body, const ArrayDumpParams &params)
367+ {
368+ // Prefix (21 bytes): objectId(u32) + classObjectId(u32) +
369+ // stackTraceSerial(u32)
370+ // + instanceSize(u32) + arrayLength(u32) + elementType(u8).
371+ AppendU32(body, params.objectId);
372+ AppendU32(body, params.classObjectId);
373+ AppendU32(body, 0); // stackTraceSerial
374+ AppendU32(body, params.instanceSize);
375+ AppendU32(body, params.arrayLength);
376+ AppendU8(body, params.elementType);
377+ bool isRef = (params.elementType == static_cast<uint8_t>(StaFieldType::OBJECT) ||
378+ params.elementType == static_cast<uint8_t>(StaFieldType::ARRAY) ||
379+ params.elementType == static_cast<uint8_t>(StaFieldType::WEAK_OBJECT));
380+ if (isRef) {
381+ // OBJECT/ARRAY elements: each element is a u32 nodeId.
382+ for (uint32_t e : params.elements) {
383+ AppendU32(body, e);
384+ }
385+ } else if (params.elementType == static_cast<uint8_t>(StaFieldType::TAGGED)) {
386+ for (const auto &[type, value] : params.taggedValues) {
387+ AppendFieldValue(body, type, value);
388+ }
389+ } else {
390+ // Non-OBJECT arrays: the parser's ScanArrayPrefixes reads arrayLength
391+ // from the prefix and expects arrayLength * FieldSize(elementType)
392+ // bytes of element data after the prefix (via DistributeUnknownData).
393+ uint8_t esz = FieldSize(params.elementType);
394+ for (uint32_t i = 0; i < params.arrayLength; ++i) {
395+ uint64_t value = i < params.primitiveValues.size() ? params.primitiveValues[i] : 0;
396+ switch (esz) {
397+ case sizeof(uint8_t):
398+ AppendU8(body, static_cast<uint8_t>(value));
399+ break;
400+ case sizeof(uint16_t):
401+ AppendU16(body, static_cast<uint16_t>(value));
402+ break;
403+ case sizeof(uint32_t):
404+ AppendU32(body, static_cast<uint32_t>(value));
405+ break;
406+ case sizeof(uint64_t):
407+ AppendU64(body, value);
408+ break;
409+ default:
410+ break;
411+ }
412+ }
413+ }
414+ }
415+ 
416+ void WriteU8(uint8_t v)
417+ {
418+ data_.push_back(v);
419+ }
420+ void WriteU32(uint32_t v)
421+ {
422+ uint8_t b[sizeof(uint32_t)];
423+ (void)memcpy_s(b, sizeof(uint32_t), &v, sizeof(uint32_t));
424+ data_.insert(data_.end(), b, b + sizeof(uint32_t));
425+ }
426+ void WriteU64(uint64_t v)
427+ {
428+ uint8_t b[sizeof(uint64_t)];
429+ (void)memcpy_s(b, sizeof(uint64_t), &v, sizeof(uint64_t));
430+ data_.insert(data_.end(), b, b + sizeof(uint64_t));
431+ }
432+ 
433+ // Record header: 17 bytes (tag:1 + time:8 + length:4 + count:4)
434+ void BeginRecord(uint8_t tag)
435+ {
436+ data_.push_back(tag);
437+ uint64_t time = 0;
438+ uint8_t timeBuf[sizeof(uint64_t)];
439+ (void)memcpy_s(timeBuf, sizeof(uint64_t), &time, sizeof(uint64_t));
440+ data_.insert(data_.end(), timeBuf, timeBuf + sizeof(uint64_t));
441+ lengthOffset_ = data_.size();
442+ uint8_t lenBuf[sizeof(uint32_t)] = {0, 0, 0, 0};
443+ data_.insert(data_.end(), lenBuf, lenBuf + sizeof(uint32_t));
444+ countOffset_ = data_.size();
445+ uint8_t countBuf[sizeof(uint32_t)] = {0, 0, 0, 0};
446+ data_.insert(data_.end(), countBuf, countBuf + sizeof(uint32_t));
447+ }
448+ 
449+ void EndRecord(const std::vector<uint8_t> &body, uint32_t count = 1)
450+ {
451+ uint32_t bodyLen = static_cast<uint32_t>(body.size());
452+ (void)memcpy_s(data_.data() + lengthOffset_, data_.size() - lengthOffset_, &bodyLen, sizeof(uint32_t));
453+ (void)memcpy_s(data_.data() + countOffset_, data_.size() - countOffset_, &count, sizeof(uint32_t));
454+ data_.insert(data_.end(), body.begin(), body.end());
455+ }
456+ 
457+ static void AppendU8(std::vector<uint8_t> &v, uint8_t val)
458+ {
459+ v.push_back(val);
460+ }
461+ static void AppendU16(std::vector<uint8_t> &v, uint16_t val)
462+ {
463+ uint8_t b[sizeof(uint16_t)];
464+ (void)memcpy_s(b, sizeof(uint16_t), &val, sizeof(uint16_t));
465+ v.insert(v.end(), b, b + sizeof(uint16_t));
466+ }
467+ static void AppendU32(std::vector<uint8_t> &v, uint32_t val)
468+ {
469+ uint8_t b[sizeof(uint32_t)];
470+ (void)memcpy_s(b, sizeof(uint32_t), &val, sizeof(uint32_t));
471+ v.insert(v.end(), b, b + sizeof(uint32_t));
472+ }
473+ static void AppendU64(std::vector<uint8_t> &v, uint64_t val)
474+ {
475+ uint8_t b[sizeof(uint64_t)];
476+ (void)memcpy_s(b, sizeof(uint64_t), &val, sizeof(uint64_t));
477+ v.insert(v.end(), b, b + sizeof(uint64_t));
478+ }
479+ 
480+ // Append a field value [type:u1][value:FieldSize(type) bytes LE]. Mirrors
481+ // the writer's WriteFieldValue encoding.
482+ static void AppendFieldValue(std::vector<uint8_t> &v, uint8_t type, uint64_t value)
483+ {
484+ AppendU8(v, type);
485+ switch (static_cast<StaFieldType>(type)) {
486+ case StaFieldType::BOOLEAN:
487+ case StaFieldType::BYTE:
488+ AppendU8(v, static_cast<uint8_t>(value));
489+ break;
490+ case StaFieldType::CHAR:
491+ case StaFieldType::SHORT:
492+ AppendU16(v, static_cast<uint16_t>(value));
493+ break;
494+ case StaFieldType::INT:
495+ case StaFieldType::FLOAT:
496+ AppendU32(v, static_cast<uint32_t>(value));
497+ break;
498+ case StaFieldType::LONG:
499+ case StaFieldType::DOUBLE:
500+ case StaFieldType::TAGGED:
501+ AppendU64(v, value);
502+ break;
503+ case StaFieldType::OBJECT:
504+ case StaFieldType::ARRAY:
505+ case StaFieldType::WEAK_OBJECT:
506+ AppendU32(v, static_cast<uint32_t>(value)); // nodeId
507+ break;
508+ case StaFieldType::UNKNOWN:
509+ default:
510+ break; // type byte only
511+ }
512+ }
513+ 
514+ // Mirror of StaticRawheapTranslate::FieldSize - byte size of a field value
515+ // for the given StaFieldType. OBJECT/ARRAY are u32 nodeIds (4 bytes).
516+ static uint8_t FieldSize(uint8_t fieldType)
517+ {
518+ switch (fieldType) {
519+ case static_cast<uint8_t>(StaFieldType::BOOLEAN):
520+ case static_cast<uint8_t>(StaFieldType::BYTE):
521+ return 1;
522+ case static_cast<uint8_t>(StaFieldType::CHAR):
523+ case static_cast<uint8_t>(StaFieldType::SHORT):
524+ return sizeof(uint16_t);
525+ case static_cast<uint8_t>(StaFieldType::INT):
526+ case static_cast<uint8_t>(StaFieldType::FLOAT):
527+ return sizeof(uint32_t);
528+ case static_cast<uint8_t>(StaFieldType::LONG):
529+ case static_cast<uint8_t>(StaFieldType::DOUBLE):
530+ case static_cast<uint8_t>(StaFieldType::TAGGED):
531+ return sizeof(uint64_t);
532+ case static_cast<uint8_t>(StaFieldType::OBJECT):
533+ case static_cast<uint8_t>(StaFieldType::ARRAY):
534+ case static_cast<uint8_t>(StaFieldType::WEAK_OBJECT):
535+ return sizeof(uint32_t); // nodeId (4 bytes)
536+ default:
537+ return 0;
538+ }
539+ }
540+};
541+ 
542+// ============================================================================
543+// Test fixture
544+// ============================================================================
545+ 
546+class RawHeapStaticSnapshotTest : public testing::Test {
547+public:
548+ static void SetUpTestCase() {}
549+ static void TearDownTestCase() {}
550+ 
551+ void TearDown() override
552+ {
553+ for (const auto &path : tempFiles_) {
554+ std::remove(path.c_str());
555+ }
556+ tempFiles_.clear();
557+ }
558+ 
559+ // Parse a builder's output as a single-file static snapshot and return a
560+ // fully-translated parser (synthetic root framework enabled).
561+ bool ParseSingle(const StaticSnapshotDataBuilder &builder, const std::string &tag, StaticRawheapTranslate &parser)
562+ {
563+ std::string path = builder.WriteToTempFile(tag);
564+ tempFiles_.push_back(path);
565+ FileReader file;
566+ if (!file.Initialize(path)) {
567+ return false;
568+ }
569+ parser.EnableRootFramework();
570+ if (!parser.Parse(file, file.GetFileSize())) {
571+ return false;
572+ }
573+ return parser.Translate();
574+ }
575+ 
576+ // Parse without the synthetic root (two-file/merge mode shape).
577+ bool ParseForMerge(const StaticSnapshotDataBuilder &builder, const std::string &tag, StaticRawheapTranslate &parser)
578+ {
579+ std::string path = builder.WriteToTempFile(tag);
580+ tempFiles_.push_back(path);
581+ FileReader file;
582+ if (!file.Initialize(path)) {
583+ return false;
584+ }
585+ return parser.Parse(file, file.GetFileSize());
586+ }
587+ 
588+ void ExpectUnsupportedHeader(const StaticSnapshotDataBuilder::HeaderParams &params, const std::string &tag)
589+ {
590+ StaticSnapshotDataBuilder builder;
591+ builder.WriteHeader(params);
592+ std::string path = builder.WriteToTempFile(tag);
593+ tempFiles_.push_back(path);
594+ 
595+ FileReader probeFile;
596+ ASSERT_TRUE(probeFile.Initialize(path));
597+ EXPECT_TRUE(RawHeap::IsStaticSnapshotFormat(probeFile));
598+ 
599+ FileReader parseFile;
600+ ASSERT_TRUE(parseFile.Initialize(path));
601+ StaticRawheapTranslate parser;
602+ EXPECT_FALSE(parser.Parse(parseFile, parseFile.GetFileSize()));
603+ }
604+ 
605+ static std::string ResolveString(const StringHashMap *strings, StringId id)
606+ {
607+ if (id < StringHashMap::CUSTOM_STRID_START) {
608+ return std::string();
609+ }
610+ return strings->GetStringByKey(strings->GetKeyByStringId(id));
611+ }
612+ 
613+ static rawheap_translate::Node *FindArrayBuffer(StaticRawheapTranslate &parser,
614+ const rawheap_translate::Node *arrayNode)
615+ {
616+ for (rawheap_translate::Edge *edge : *parser.GetEdges()) {
617+ if (edge->from == arrayNode && edge->to != nullptr) {
618+ return edge->to;
619+ }
620+ }
621+ return nullptr;
622+ }
623+ 
624+ static void VerifyTaggedArrayEdges(StaticRawheapTranslate &parser, const rawheap_translate::Node *buffer,
625+ uint32_t weakNodeId, size_t expectedElementCount)
626+ {
627+ auto *strings = parser.GetStringTable();
628+ size_t elementCount = 0;
629+ size_t weakCount = 0;
630+ std::vector<std::string> valueNames;
631+ for (rawheap_translate::Edge *edge : *parser.GetEdges()) {
632+ if (edge->from != buffer || edge->to == nullptr) {
633+ continue;
634+ }
635+ if (edge->type == EdgeType::ELEMENT) {
636+ ++elementCount;
637+ } else if (edge->type == EdgeType::WEAK) {
638+ ++weakCount;
639+ EXPECT_EQ(edge->to->nodeId, weakNodeId);
640+ }
641+ valueNames.push_back(ResolveString(strings, edge->to->strId));
642+ }
643+ EXPECT_EQ(elementCount, expectedElementCount);
644+ EXPECT_EQ(weakCount, 1U);
645+ EXPECT_NE(std::find(valueNames.begin(), valueNames.end(), "-7"), valueNames.end());
646+ EXPECT_NE(std::find(valueNames.begin(), valueNames.end(), "true"), valueNames.end());
647+ EXPECT_NE(std::find(valueNames.begin(), valueNames.end(), "null"), valueNames.end());
648+ EXPECT_NE(std::find(valueNames.begin(), valueNames.end(), "undefined"), valueNames.end());
649+ EXPECT_NE(std::find(valueNames.begin(), valueNames.end(), "hole"), valueNames.end());
650+ EXPECT_NE(std::find(valueNames.begin(), valueNames.end(), "exception"), valueNames.end());
651+ EXPECT_NE(std::find(valueNames.begin(), valueNames.end(), "false"), valueNames.end());
652+ EXPECT_NE(std::find(valueNames.begin(), valueNames.end(), "0xFEDCBA9876543210"), valueNames.end());
653+ }
654+ 
655+ std::vector<std::string> tempFiles_;
656+};
657+ 
658+// ============================================================================
659+// Header / file-level tests
660+// ============================================================================
661+ 
662+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseHeader_ValidStatic)
663+{
664+ StaticSnapshotDataBuilder builder;
665+ builder.WriteHeader(1); // STATIC
666+ auto path = builder.WriteToTempFile("hdr");
667+ tempFiles_.push_back(path);
668+ 
669+ FileReader file;
670+ ASSERT_TRUE(file.Initialize(path));
671+ ASSERT_TRUE(RawHeap::IsStaticSnapshotFormat(file));
672+}
673+ 
674+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseHeader_ValidHybrid)
675+{
676+ StaticSnapshotDataBuilder builder;
677+ builder.WriteHeader(STATIC_LANGUAGE_HYBRID);
678+ auto path = builder.WriteToTempFile("hdr_hybrid");
679+ tempFiles_.push_back(path);
680+ 
681+ FileReader file;
682+ ASSERT_TRUE(file.Initialize(path));
683+ ASSERT_TRUE(RawHeap::IsStaticSnapshotFormat(file));
684+ StaticRawheapTranslate parser;
685+ ASSERT_TRUE(parser.Parse(file, file.GetFileSize()));
686+}
687+ 
688+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseHeader_V3MinorVersionSupported)
689+{
690+ StaticSnapshotDataBuilder::HeaderParams params;
691+ params.version = {'3', '.', '1', '.', '0', '\0', '\0', '\0'};
692+ StaticSnapshotDataBuilder builder;
693+ builder.WriteHeader(params);
694+ auto path = builder.WriteToTempFile("hdr_v3_minor");
695+ tempFiles_.push_back(path);
696+ 
697+ FileReader file;
698+ ASSERT_TRUE(file.Initialize(path));
699+ EXPECT_TRUE(RawHeap::IsStaticSnapshotFormat(file));
700+ StaticRawheapTranslate parser;
701+ EXPECT_TRUE(parser.Parse(file, file.GetFileSize()));
702+}
703+ 
704+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseHeader_UnsupportedMajorVersionFails)
705+{
706+ StaticSnapshotDataBuilder::HeaderParams params;
707+ params.version = {'4', '.', '0', '.', '0', '\0', '\0', '\0'};
708+ StaticSnapshotDataBuilder builder;
709+ builder.WriteHeader(params);
710+ auto path = builder.WriteToTempFile("hdr_major_version");
711+ tempFiles_.push_back(path);
712+ 
713+ FileReader probeFile;
714+ ASSERT_TRUE(probeFile.Initialize(path));
715+ EXPECT_FALSE(RawHeap::IsStaticSnapshotFormat(probeFile));
716+ 
717+ FileReader parseFile;
718+ ASSERT_TRUE(parseFile.Initialize(path));
719+ StaticRawheapTranslate parser;
720+ EXPECT_FALSE(parser.Parse(parseFile, parseFile.GetFileSize()));
721+}
722+ 
723+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseHeader_UnsupportedIdentifierSize_Fails)
724+{
725+ StaticSnapshotDataBuilder::HeaderParams params;
726+ params.identifierSize = static_cast<uint32_t>(sizeof(uint64_t));
727+ ExpectUnsupportedHeader(params, "hdr_identifier");
728+}
729+ 
730+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseHeader_UnsupportedLanguage_Fails)
731+{
732+ StaticSnapshotDataBuilder::HeaderParams params;
733+ params.language = 0;
734+ ExpectUnsupportedHeader(params, "hdr_language");
735+}
736+ 
737+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseHeader_UnsupportedSize_Fails)
738+{
739+ StaticSnapshotDataBuilder::HeaderParams params;
740+ params.headerSize = STATIC_HEADER_SIZE + 1;
741+ ExpectUnsupportedHeader(params, "hdr_size");
742+}
743+ 
744+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseHeader_UnknownFeatureFlags_Fails)
745+{
746+ StaticSnapshotDataBuilder::HeaderParams params;
747+ params.featureFlags = 1;
748+ ExpectUnsupportedHeader(params, "hdr_flags");
749+}
750+ 
751+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseHeader_InvalidMagic_Fails)
752+{
753+ std::vector<uint8_t> bad(STATIC_HEADER_SIZE, 0);
754+ std::random_device rd;
755+ std::string path = "static_snap_test_badmagic_" + std::to_string(rd());
756+ std::ofstream ofs(path, std::ios::binary);
757+ ofs.write(reinterpret_cast<const char *>(bad.data()), bad.size());
758+ ofs.close();
759+ tempFiles_.push_back(path);
760+ 
761+ FileReader file;
762+ ASSERT_TRUE(file.Initialize(path));
763+ StaticRawheapTranslate parser;
764+ ASSERT_FALSE(parser.Parse(file, file.GetFileSize()));
765+}
766+ 
767+HWTEST_F_L0(RawHeapStaticSnapshotTest, Parse_EmptyFile_Fails)
768+{
769+ std::random_device rd;
770+ std::string path = "static_snap_test_empty_" + std::to_string(rd());
771+ std::ofstream ofs(path, std::ios::binary);
772+ ofs.close();
773+ tempFiles_.push_back(path);
774+ FileReader file;
775+ if (!file.Initialize(path)) {
776+ return; // empty file may not initialize
777+ }
778+ StaticRawheapTranslate parser;
779+ ASSERT_FALSE(parser.Parse(file, file.GetFileSize()));
780+}
781+ 
782+HWTEST_F_L0(RawHeapStaticSnapshotTest, Parse_UnknownTag_Skipped)
783+{
784+ StaticSnapshotDataBuilder builder;
785+ builder.WriteHeader(1);
786+ builder.WriteUnknownRecord(0x99, {0xAA, 0xBB, 0xCC, 0xDD});
787+ builder.WriteHeapSummaryRecord();
788+ auto path = builder.WriteToTempFile("unk");
789+ tempFiles_.push_back(path);
790+ 
791+ FileReader file;
792+ ASSERT_TRUE(file.Initialize(path));
793+ StaticRawheapTranslate parser;
794+ ASSERT_TRUE(parser.Parse(file, file.GetFileSize()));
795+}
796+ 
797+// An unknown tag (0xFF, the PARTIAL_MARKER chunk boundary) must be skipped
798+// without processing. Consecutive unknown tags are also skipped. After
799+// skipping 0xFF and 0xAA the parser must still consume the subsequent STRING
800+// and LoadClass records; the LoadClass references stringId 10 ("AfterUnknown")
801+// so BuildGraph promotes it into the StringHashMap, and resolving the class
802+// node's name then proves the post-skip records were consumed correctly.
803+HWTEST_F_L0(RawHeapStaticSnapshotTest, Parse_UnknownTag_FF_AndConsecutive_Skipped)
804+{
805+ StaticSnapshotDataBuilder builder;
806+ builder.WriteHeader(1);
807+ // Write 0xFF (PARTIAL_MARKER) - should be skipped
808+ builder.WriteUnknownRecord(TAG_PARTIAL_MARKER, {0x01, 0x02, 0x03, 0x04});
809+ // Write another unknown tag in sequence
810+ builder.WriteUnknownRecord(0xAA, {0x11, 0x22});
811+ // Write a valid STRING record after unknowns, then a LoadClass referencing
812+ // it, so the parser must continue past the skipped unknowns.
813+ builder.WriteStringRecord(10, "AfterUnknown");
814+ builder.WriteLoadClassRecord(2, 10); // class nodeId 2 -> "AfterUnknown"
815+ builder.WriteHeapSummaryRecord();
816+ 
817+ StaticRawheapTranslate parser;
818+ ASSERT_TRUE(ParseSingle(builder, "unkff", parser));
819+ 
820+ // The class node created from the LoadClass record must carry the name
821+ // "AfterUnknown" - this is only possible if the parser skipped the unknown
822+ // tags and then correctly parsed the STRING + LoadClass records that follow.
823+ rawheap_translate::Node *cls = parser.FindNodeByNodeId(2);
824+ ASSERT_NE(cls, nullptr) << "LoadClass record after unknown tags was not parsed";
825+ auto *tab = parser.GetStringTable();
826+ ASSERT_NE(tab, nullptr);
827+ auto resolveStr = [tab](StringId id) -> std::string {
828+ if (id < StringHashMap::CUSTOM_STRID_START) {
829+ return std::string();
830+ }
831+ return tab->GetStringByKey(tab->GetKeyByStringId(id));
832+ };
833+ EXPECT_EQ(resolveStr(cls->strId), "AfterUnknown")
834+ << "STRING record after unknown tags should be parsed and referenced";
835+}
836+ 
837+HWTEST_F_L0(RawHeapStaticSnapshotTest, Parse_StringCannotReadPastDeclaredRecordBody)
838+{
839+ StaticSnapshotDataBuilder builder;
840+ builder.WriteHeader(1);
841+ std::vector<uint8_t> body(sizeof(uint32_t) * 2);
842+ uint32_t stringId = 10;
843+ uint32_t stringLength = 4;
844+ (void)memcpy_s(body.data(), body.size(), &stringId, sizeof(stringId));
845+ (void)memcpy_s(body.data() + sizeof(uint32_t), body.size() - sizeof(uint32_t), &stringLength, sizeof(stringLength));
846+ // The record declares only the two u32 fields. A parser must reject the
847+ // missing string payload instead of consuming bytes from the next record.
848+ builder.WriteRawRecord(TAG_STRING_IN_UTF8, static_cast<uint32_t>(body.size()), 1, body);
849+ builder.WriteHeapSummaryRecord();
850+ 
851+ StaticRawheapTranslate parser;
852+ EXPECT_FALSE(ParseForMerge(builder, "bounded_string", parser));
853+}
854+ 
855+HWTEST_F_L0(RawHeapStaticSnapshotTest, Parse_RejectsTrailingRecordBytes)
856+{
857+ StaticSnapshotDataBuilder builder;
858+ builder.WriteHeader(1);
859+ std::vector<uint8_t> body = {ROOT_TYPE_STATIC_OBJECT, 2, 0, 0, 0, 0xFF};
860+ builder.WriteRawRecord(TAG_ROOT_RECORD, static_cast<uint32_t>(body.size()), 1, body);
861+ 
862+ StaticRawheapTranslate parser;
863+ EXPECT_FALSE(ParseForMerge(builder, "trailing_record_bytes", parser));
864+}
865+ 
866+HWTEST_F_L0(RawHeapStaticSnapshotTest, Parse_RejectsArrayPayloadOutsideRecord)
867+{
868+ StaticSnapshotDataBuilder builder;
869+ builder.WriteHeader(1);
870+ std::vector<uint8_t> body(STATIC_ARRAY_PREFIX_BODY_SIZE, 0);
871+ uint32_t arrayLength = std::numeric_limits<uint32_t>::max();
872+ (void)memcpy_s(body.data() + STATIC_ARRAY_LENGTH_OFFSET, body.size() - STATIC_ARRAY_LENGTH_OFFSET, &arrayLength,
873+ sizeof(arrayLength));
874+ body[STATIC_ARRAY_ELEM_TYPE_OFFSET] = static_cast<uint8_t>(StaFieldType::INT);
875+ builder.WriteRawRecord(TAG_STATIC_ARRAY_DUMP, static_cast<uint32_t>(body.size()), 1, body);
876+ 
877+ StaticRawheapTranslate parser;
878+ EXPECT_FALSE(ParseForMerge(builder, "bounded_array", parser));
879+}
880+ 
881+// ============================================================================
882+// Static record parsing tests
883+// ============================================================================
884+ 
885+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseLoadClassAndStaticClassDump)
886+{
887+ StaticSnapshotDataBuilder builder;
888+ builder.WriteHeader(1);
889+ builder.WriteStringRecord(10, "TestClass");
890+ builder.WriteLoadClassRecord(2, 10); // 0x1000 -> nodeId 2
891+ // instanceFieldNames: empty (no instance fields in class dump)
892+ builder.WriteStaticClassDumpRecord(2, 32, {});
893+ builder.WriteHeapSummaryRecord();
894+ 
895+ StaticRawheapTranslate parser;
896+ ASSERT_TRUE(ParseSingle(builder, "cls", parser));
897+ ASSERT_GT(parser.GetNodeCount(), 0u);
898+ ASSERT_NE(parser.FindNodeByNodeId(2), nullptr); // 0x1000 -> nodeId 2
899+}
900+ 
901+HWTEST_F_L0(RawHeapStaticSnapshotTest, ClassMirrorPreservesInstanceSize)
902+{
903+ StaticSnapshotDataBuilder builder;
904+ builder.WriteHeader();
905+ builder.WriteStringRecord(10, "TestClass");
906+ builder.WriteStringRecord(11, "std.core.Class");
907+ builder.WriteLoadClassRecord(2, 10);
908+ builder.WriteLoadClassRecord(4, 11);
909+ builder.WriteStaticClassDumpRecord(2, 16, {});
910+ builder.WriteStaticClassDumpRecord(4, 24, {});
911+ // The class mirror is also an instance of std.core.Class. Its actual
912+ // object size comes from this instance record, not the class descriptor.
913+ builder.WriteStaticInstanceDumpRecord(2, 4, 48, {});
914+ builder.WriteHeapSummaryRecord();
915+ 
916+ StaticRawheapTranslate parser;
917+ ASSERT_TRUE(ParseSingle(builder, "class_mirror_size", parser));
918+ rawheap_translate::Node *classMirror = parser.FindNodeByNodeId(2);
919+ ASSERT_NE(classMirror, nullptr);
920+ EXPECT_EQ(classMirror->type, CLASS_NODETYPE);
921+ EXPECT_EQ(classMirror->size, 48U);
922+}
923+ 
924+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseStaticInstanceDump_WithObjectField)
925+{
926+ StaticSnapshotDataBuilder builder;
927+ builder.WriteHeader(1);
928+ builder.WriteStringRecord(10, "TestClassName");
929+ builder.WriteStringRecord(11, "fieldA");
930+ builder.WriteLoadClassRecord(2, 10); // 0x1000 -> nodeId 2
931+ // instanceFieldNames: {11} = "fieldA"
932+ builder.WriteStaticClassDumpRecord(2, 24, {11});
933+ // fieldValues: {6} = object reference value (0x3000 -> nodeId 6)
934+ builder.WriteStaticInstanceDumpRecord(4, 2, 24, {6}); // 0x2000->4, 0x1000->2
935+ builder.WriteHeapSummaryRecord();
936+ 
937+ StaticRawheapTranslate parser;
938+ ASSERT_TRUE(ParseSingle(builder, "inst", parser));
939+ // class node + instance node + field-target node + synthetic framework
940+ ASSERT_GT(parser.GetNodeCount(), 3u);
941+ ASSERT_NE(parser.FindNodeByNodeId(4), nullptr); // 0x2000 -> nodeId 4
942+ ASSERT_NE(parser.FindNodeByNodeId(6), nullptr); // 0x3000 -> nodeId 6
943+}
944+ 
945+// Guards the EmitInstanceFieldEdges fix: INSTANCE_DUMP carries instance field
946+// values only (the writer's WriteNormalInstance iterates
947+// cls->GetInstanceFields() alone). The reader must emit PROPERTY edges for
948+// those values labeled with the INSTANCE field names - not the static field
949+// names, and never empty. If the reader iterates static field descriptors
950+// against rec.values (the old bug), the first instance references get
951+// mislabeled with static field names and the tail is dropped; if the classMap_
952+// join misses, EmitFallbackFieldEdges writes empty names. This test catches
953+// both regressions.
954+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseStaticInstanceDump_InstanceFieldNamesCorrect)
955+{
956+ StaticSnapshotDataBuilder builder;
957+ builder.WriteHeader(1);
958+ builder.WriteStringRecord(10, "MyClass");
959+ builder.WriteStringRecord(20, "staticField"); // static field name (must NOT appear on edges)
960+ builder.WriteStringRecord(21, "instA"); // instance field name
961+ builder.WriteStringRecord(22, "instB"); // instance field name
962+ builder.WriteLoadClassRecord(2, 10); // class nodeId 2 -> "MyClass"
963+ // 1 static field + 2 instance fields. classObjectId (2) matches the
964+ // instance's classNodeId below so classMap_ join succeeds.
965+ builder.WriteStaticClassDumpRecordWithFields(2, 24, {20}, {21, 22});
966+ // Instance nodeId 4, class nodeId 2, two OBJECT field values -> 6 and 8.
967+ builder.WriteStaticInstanceDumpRecord(4, 2, 24, {6, 8});
968+ builder.WriteHeapSummaryRecord();
969+ 
970+ StaticRawheapTranslate parser;
971+ ASSERT_TRUE(ParseSingle(builder, "instfn", parser));
972+ 
973+ // Instance node type-name must resolve to the class name - proves the
974+ // classMap_ join (rec.classNodeId -> classMap_) succeeded.
975+ rawheap_translate::Node *inst = parser.FindNodeByNodeId(4);
976+ ASSERT_NE(inst, nullptr);
977+ auto *tab = parser.GetStringTable();
978+ auto resolveStr = [tab](StringId id) -> std::string {
979+ if (id < StringHashMap::CUSTOM_STRID_START) {
980+ return std::string();
981+ }
982+ StringKey key = tab->GetKeyByStringId(id);
983+ return tab->GetStringByKey(key);
984+ };
985+ EXPECT_EQ(resolveStr(inst->strId), "MyClass");
986+ 
987+ // Collect every PROPERTY edge name in the graph. The instance now emits
988+ // its field edges (instA, instB) PLUS an instance->class "hclass" edge
989+ // (EdgeType::DEFAULT == PROPERTY). Root framework uses ELEMENT/SHORTCUT;
990+ // primitive nodes carry no edges. So the set must be {instA, instB, hclass}.
991+ std::vector<std::string> propNames;
992+ for (rawheap_translate::Edge *e : *parser.GetEdges()) {
993+ if (e->type == EdgeType::PROPERTY) {
994+ propNames.push_back(resolveStr(e->nameOrIndex));
995+ }
996+ }
997+ ASSERT_EQ(propNames.size(), 3u);
998+ EXPECT_NE(std::find(propNames.begin(), propNames.end(), "instA"), propNames.end());
999+ EXPECT_NE(std::find(propNames.begin(), propNames.end(), "instB"), propNames.end());
1000+ EXPECT_NE(std::find(propNames.begin(), propNames.end(), "hclass"), propNames.end());
1001+ EXPECT_EQ(std::find(propNames.begin(), propNames.end(), "staticField"), propNames.end());
1002+ for (const auto &n : propNames) {
1003+ EXPECT_FALSE(n.empty()) << "instance field edge has empty name (fallback path hit)";
1004+ }
1005+}
1006+ 
1007+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseStaticArrayDump_ObjectElements)
1008+{
1009+ StaticSnapshotDataBuilder builder;
1010+ builder.WriteHeader(1);
1011+ builder.WriteStringRecord(10, "Object[]");
1012+ builder.WriteLoadClassRecord(2, 10); // 0x1000 -> nodeId 2
1013+ // elements: {8, 10} = two object element references (0x4000->8, 0x5000->10)
1014+ ArrayDumpParams arrParams;
1015+ arrParams.objectId = 6; // 0x3000 -> nodeId 6
1016+ arrParams.classObjectId = 2; // 0x1000 -> nodeId 2
1017+ arrParams.instanceSize = 32;
1018+ arrParams.arrayLength = 2;
1019+ arrParams.elementType = static_cast<uint8_t>(StaFieldType::OBJECT);
1020+ arrParams.elements = {8, 10};
1021+ builder.WriteStaticArrayDumpRecord(arrParams);
1022+ builder.WriteHeapSummaryRecord();
1023+ 
1024+ StaticRawheapTranslate parser;
1025+ ASSERT_TRUE(ParseSingle(builder, "arr", parser));
1026+ ASSERT_NE(parser.FindNodeByNodeId(8), nullptr); // 0x4000 -> nodeId 8
1027+ ASSERT_NE(parser.FindNodeByNodeId(10), nullptr); // 0x5000 -> nodeId 10
1028+}
1029+ 
1030+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseStaticArrayDump_NonObjectElements)
1031+{
1032+ // Non-OBJECT element arrays carry arrayLength * FieldSize(type) bytes of
1033+ // element data after the prefix; the parser captures those bytes and boxes
1034+ // each element as a std.core.<Type> wrapper (CreateArrayEdges). This test
1035+ // only asserts the array node itself exists; element boxing is exercised by
1036+ // the integration tests (HeapsnapshotArrayElementsMatchEts).
1037+ StaticSnapshotDataBuilder builder;
1038+ builder.WriteHeader(1);
1039+ builder.WriteStringRecord(10, "int[]");
1040+ builder.WriteLoadClassRecord(2, 10); // 0x1000 -> nodeId 2
1041+ ArrayDumpParams intArrParams;
1042+ intArrParams.objectId = 6; // 0x3000 -> nodeId 6
1043+ intArrParams.classObjectId = 2; // 0x1000 -> nodeId 2
1044+ intArrParams.instanceSize = 32;
1045+ intArrParams.arrayLength = 4; // 4 INT elements -> 4 * 4 = 16 zero bytes
1046+ intArrParams.elementType = static_cast<uint8_t>(StaFieldType::INT);
1047+ builder.WriteStaticArrayDumpRecord(intArrParams);
1048+ builder.WriteHeapSummaryRecord();
1049+ 
1050+ StaticRawheapTranslate parser;
1051+ ASSERT_TRUE(ParseSingle(builder, "iarr", parser));
1052+ ASSERT_NE(parser.FindNodeByNodeId(6), nullptr); // 0x3000 -> nodeId 6
1053+}
1054+ 
1055+// ============================================================================
1056+// Merge tests - static graph spliced into a synthetic dynamic graph
1057+// ============================================================================
1058+ 
1059+HWTEST_F_L0(RawHeapStaticSnapshotTest, Merge_TwoFileTranslate_Succeeds)
1060+{
1061+ // Full two-file path: dynamic file (V1/V2 rawheap) + static file -> one
1062+ // heapsnapshot. The dynamic side is exercised through the real
1063+ // TranslateRawheap entry point; here we only assert the static file parses
1064+ // into a mergeable shape and the two-file TranslateRawheap runs without
1065+ // aborting on a static file that lacks a real dynamic counterpart (it will
1066+ // return false for an invalid dynamic file, which we treat as the
1067+ // routing-success signal).
1068+ StaticSnapshotDataBuilder builder;
1069+ builder.WriteHeader(1);
1070+ builder.WriteStringRecord(10, "Sta");
1071+ builder.WriteStringRecord(11, "f");
1072+ builder.WriteLoadClassRecord(2, 10); // 0x1000 -> nodeId 2
1073+ // instanceFieldNames: {11} = "f"
1074+ builder.WriteStaticClassDumpRecord(2, 16, {11});
1075+ // fieldValues: {6} = object reference value (0x3000 -> nodeId 6)
1076+ builder.WriteStaticInstanceDumpRecord(4, 2, 16, {6}); // 0x2000->4, 0x1000->2
1077+ builder.WriteRootRecord(4); // 0x2000 -> nodeId 4
1078+ // XRef: static side (0x2000 -> nodeId 4) goes into staNodeId; the dynamic
1079+ // side is a dynNodeId (resolved from the JS address at dump time; here an
1080+ // arbitrary value, since this test only asserts xref routing, not
1081+ // resolution).
1082+ builder.WriteXRefEdgeRecord(0xDDDD0000U, 4, XREF_STA_TO_DYN);
1083+ builder.WriteHeapSummaryRecord();
1084+ 
1085+ StaticRawheapTranslate staticParser;
1086+ ASSERT_TRUE(ParseForMerge(builder, "merge", staticParser));
1087+ ASSERT_EQ(staticParser.GetXRefs().size(), 1u);
1088+ ASSERT_EQ(staticParser.GetRoots().size(), 1u);
1089+ // The static graph was built (object nodes only, no synthetic root).
1090+ ASSERT_GT(staticParser.GetNodeCount(), 0u);
1091+}
1092+ 
1093+// Issue 5: the merger must RESOLVE xref records into actual xref edges when the
1094+// dynNodeId matches a dynamic node's nodeId. The merger resolves by indexing
1095+// dynamic nodes by nodeId and looking up x.dynNodeId - this test exercises that
1096+// resolution path (which Merge_TwoFileTranslate_Succeeds above does not, since
1097+// its dynamic side is absent). Builds a minimal dynamic graph by hand (the
1098+// merger only uses RawHeap base-class methods, so a V1 with a null MetaParser
1099+// is sufficient) + a static parser carrying one STA_TO_DYN xref, runs Merge,
1100+// and asserts the merged graph contains the xref edge.
1101+HWTEST_F_L0(RawHeapStaticSnapshotTest, Merge_XRefEdge_Resolved)
1102+{
1103+ // Static side: class node nodeId=8 ("StaCls"), root=8, one xref record
1104+ // (dynNodeId=0xDDDD0000, staNodeId=8, direction=STA_TO_DYN).
1105+ constexpr uint32_t staClassId = 8;
1106+ constexpr uint32_t dynNodeId = 0xDDDD0000U;
1107+ StaticSnapshotDataBuilder builder;
1108+ builder.WriteHeader(1);
1109+ builder.WriteStringRecord(10, "StaCls");
1110+ builder.WriteLoadClassRecord(staClassId, 10);
1111+ builder.WriteStaticClassDumpRecord(staClassId, 16, {});
1112+ builder.WriteRootRecord(staClassId);
1113+ builder.WriteXRefEdgeRecord(dynNodeId, staClassId, XREF_STA_TO_DYN);
1114+ builder.WriteHeapSummaryRecord();
1115+ 
1116+ StaticRawheapTranslate staticParser;
1117+ ASSERT_TRUE(ParseForMerge(builder, "xrefmerge", staticParser));
1118+ ASSERT_EQ(staticParser.GetXRefs().size(), 1u);
1119+ rawheap_translate::Node *staNode = staticParser.FindNodeByNodeId(staClassId);
1120+ ASSERT_NE(staNode, nullptr);
1121+ 
1122+ // Dynamic side: a synthetic root (nodes_[0], nodeId=1) + one object node
1123+ // whose nodeId == the xref dynNodeId. The dump side resolves
1124+ // jsAddr->dynNodeId via the dynamic participant's GetNodeId (mirroring
1125+ // etsAddr->staNodeId); the merger indexes dynamic nodes by nodeId and looks
1126+ // up x.dynNodeId, so the dynNodeId<->nodeId match is what makes the xref
1127+ // resolve. Here we set the dynamic node's nodeId directly to the value the
1128+ // dump would have written. (Node/Edge qualified - panda::ecmascript::Node is
1129+ // also in scope via the using-directives.)
1130+ RawHeapTranslateV1 dynamic(nullptr);
1131+ rawheap_translate::Node *synRoot = dynamic.CreateNode();
1132+ synRoot->nodeId = 1;
1133+ synRoot->type = SYNTHETIC_NODETYPE;
1134+ synRoot->strId = dynamic.InsertAndGetStringId("SyntheticRoot");
1135+ synRoot->edgeCount = 0;
1136+ rawheap_translate::Node *dynNode = dynamic.CreateNode();
1137+ dynNode->nodeId = dynNodeId;
1138+ dynNode->type = OBJECT_NODETYPE;
1139+ dynNode->strId = dynamic.InsertAndGetStringId("DynObj");
1140+ dynNode->edgeCount = 0;
1141+ 
1142+ SnapshotMerger merger;
1143+ ASSERT_TRUE(merger.Merge(dynamic, staticParser));
1144+ 
1145+ // After merge, the merged graph must contain exactly one xref edge from
1146+ // the static class node to the dynamic object node (STA_TO_DYN direction
1147+ // emits the edge on the static side, pointing at the dynamic node).
1148+ int xrefEdges = 0;
1149+ for (rawheap_translate::Edge *edge : *dynamic.GetEdges()) {
1150+ if (edge->type == EdgeType::XREF) {
1151+ EXPECT_EQ(edge->to, dynNode) << "xref edge should point at the dynamic node";
1152+ ++xrefEdges;
1153+ }
1154+ }
1155+ EXPECT_EQ(xrefEdges, 1) << "merger should emit exactly one resolved xref edge";
1156+}
1157+ 
1158+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseXRef_InvalidDirectionFails)
1159+{
1160+ constexpr uint8_t invalidDirection = 0xFFU;
1161+ constexpr uint32_t dynamicNodeId = 2;
1162+ constexpr uint32_t staticNodeId = 4;
1163+ StaticSnapshotDataBuilder builder;
1164+ builder.WriteHeader();
1165+ builder.WriteXRefEdgeRecord(dynamicNodeId, staticNodeId, invalidDirection);
1166+ 
1167+ StaticRawheapTranslate parser;
1168+ EXPECT_FALSE(ParseForMerge(builder, "xref_invalid_direction", parser));
1169+}
1170+ 
1171+HWTEST_F_L0(RawHeapStaticSnapshotTest, Merge_InternalEdgeNameUsesDynamicStringTable)
1172+{
1173+ constexpr uint32_t baseClassId = 2;
1174+ constexpr uint32_t derivedClassId = 4;
1175+ constexpr uint32_t baseClassNameId = 10;
1176+ constexpr uint32_t derivedClassNameId = 11;
1177+ constexpr uint32_t classInstanceSize = 16;
1178+ constexpr uint32_t syntheticRootId = 1;
1179+ StaticSnapshotDataBuilder builder;
1180+ builder.WriteHeader();
1181+ builder.WriteStringRecord(baseClassNameId, "Base");
1182+ builder.WriteStringRecord(derivedClassNameId, "Derived");
1183+ builder.WriteLoadClassRecord(baseClassId, baseClassNameId);
1184+ builder.WriteLoadClassRecord(derivedClassId, derivedClassNameId);
1185+ builder.WriteStaticClassDumpRecordFull(baseClassId, 0, classInstanceSize,
1186+ StaticSnapshotDataBuilder::StaticClassDumpRecordFields {});
1187+ builder.WriteStaticClassDumpRecordFull(derivedClassId, baseClassId, classInstanceSize,
1188+ StaticSnapshotDataBuilder::StaticClassDumpRecordFields {});
1189+ builder.WriteHeapSummaryRecord();
1190+ 
1191+ StaticRawheapTranslate staticParser;
1192+ ASSERT_TRUE(ParseForMerge(builder, "internal_edge_string", staticParser));
1193+ rawheap_translate::Node *derivedClass = staticParser.FindNodeByNodeId(derivedClassId);
1194+ ASSERT_NE(derivedClass, nullptr);
1195+ 
1196+ RawHeapTranslateV1 dynamic(nullptr);
1197+ rawheap_translate::Node *root = dynamic.CreateNode();
1198+ root->nodeId = syntheticRootId;
1199+ root->type = SYNTHETIC_NODETYPE;
1200+ root->strId = dynamic.InsertAndGetStringId("SyntheticRoot");
1201+ (void)dynamic.InsertAndGetStringId("dynamic-one");
1202+ (void)dynamic.InsertAndGetStringId("dynamic-two");
1203+ 
1204+ SnapshotMerger merger;
1205+ ASSERT_TRUE(merger.Merge(dynamic, staticParser));
1206+ auto *strings = dynamic.GetStringTable();
1207+ bool foundSuperClass = false;
1208+ for (rawheap_translate::Edge *edge : *dynamic.GetEdges()) {
1209+ if (edge->from != derivedClass || edge->type != EdgeType::INTERNAL) {
1210+ continue;
1211+ }
1212+ auto key = strings->GetKeyByStringId(edge->nameOrIndex);
1213+ foundSuperClass = strings->GetStringByKey(key) == "superClass";
1214+ }
1215+ EXPECT_TRUE(foundSuperClass);
1216+}
1217+ 
1218+HWTEST_F_L0(RawHeapStaticSnapshotTest, Merge_NonEmptyFullyUnresolvedXRefs_Fails)
1219+{
1220+ constexpr uint32_t staClassId = 8;
1221+ StaticSnapshotDataBuilder builder;
1222+ builder.WriteHeader();
1223+ builder.WriteStringRecord(10, "StaCls");
1224+ builder.WriteLoadClassRecord(staClassId, 10);
1225+ builder.WriteStaticClassDumpRecord(staClassId, 16, {});
1226+ builder.WriteXRefEdgeRecord(0xDEADU, staClassId, XREF_STA_TO_DYN);
1227+ builder.WriteHeapSummaryRecord();
1228+ 
1229+ StaticRawheapTranslate staticParser;
1230+ ASSERT_TRUE(ParseForMerge(builder, "xref_unresolved", staticParser));
1231+ RawHeapTranslateV1 dynamic(nullptr);
1232+ rawheap_translate::Node *root = dynamic.CreateNode();
1233+ root->nodeId = 1;
1234+ root->type = SYNTHETIC_NODETYPE;
1235+ root->strId = dynamic.InsertAndGetStringId("SyntheticRoot");
1236+ 
1237+ SnapshotMerger merger;
1238+ EXPECT_FALSE(merger.Merge(dynamic, staticParser));
1239+}
1240+ 
1241+HWTEST_F_L0(RawHeapStaticSnapshotTest, Merge_EmptyXRefs_Succeeds)
1242+{
1243+ StaticSnapshotDataBuilder builder;
1244+ builder.WriteHeader();
1245+ builder.WriteStringRecord(10, "StaCls");
1246+ builder.WriteLoadClassRecord(8, 10);
1247+ builder.WriteStaticClassDumpRecord(8, 16, {});
1248+ builder.WriteHeapSummaryRecord();
1249+ 
1250+ StaticRawheapTranslate staticParser;
1251+ ASSERT_TRUE(ParseForMerge(builder, "xref_empty", staticParser));
1252+ RawHeapTranslateV1 dynamic(nullptr);
1253+ rawheap_translate::Node *root = dynamic.CreateNode();
1254+ root->nodeId = 1;
1255+ root->type = SYNTHETIC_NODETYPE;
1256+ root->strId = dynamic.InsertAndGetStringId("SyntheticRoot");
1257+ 
1258+ SnapshotMerger merger;
1259+ EXPECT_TRUE(merger.Merge(dynamic, staticParser));
1260+}
1261+ 
1262+HWTEST_F_L0(RawHeapStaticSnapshotTest, Merge_PartiallyResolvedXRefs_Succeeds)
1263+{
1264+ constexpr uint32_t staClassId = 8;
1265+ constexpr uint32_t dynNodeId = 0xDDDD0000U;
1266+ StaticSnapshotDataBuilder builder;
1267+ builder.WriteHeader();
1268+ builder.WriteStringRecord(10, "StaCls");
1269+ builder.WriteLoadClassRecord(staClassId, 10);
1270+ builder.WriteStaticClassDumpRecord(staClassId, 16, {});
1271+ builder.WriteXRefEdgeRecord(dynNodeId, staClassId, XREF_STA_TO_DYN);
1272+ builder.WriteXRefEdgeRecord(0xDEADU, staClassId, XREF_STA_TO_DYN);
1273+ builder.WriteHeapSummaryRecord();
1274+ 
1275+ StaticRawheapTranslate staticParser;
1276+ ASSERT_TRUE(ParseForMerge(builder, "xref_partial", staticParser));
1277+ RawHeapTranslateV1 dynamic(nullptr);
1278+ rawheap_translate::Node *root = dynamic.CreateNode();
1279+ root->nodeId = 1;
1280+ root->type = SYNTHETIC_NODETYPE;
1281+ root->strId = dynamic.InsertAndGetStringId("SyntheticRoot");
1282+ rawheap_translate::Node *dynNode = dynamic.CreateNode();
1283+ dynNode->nodeId = dynNodeId;
1284+ dynNode->type = OBJECT_NODETYPE;
1285+ dynNode->strId = dynamic.InsertAndGetStringId("DynObj");
1286+ 
1287+ SnapshotMerger merger;
1288+ EXPECT_TRUE(merger.Merge(dynamic, staticParser));
1289+ size_t xrefCount = 0;
1290+ for (rawheap_translate::Edge *edge : *dynamic.GetEdges()) {
1291+ if (edge->type == EdgeType::XREF) {
1292+ ++xrefCount;
1293+ }
1294+ }
1295+ EXPECT_EQ(xrefCount, 1U);
1296+}
1297+ 
1298+// A dynamic-to-static cross-VM reference (XRef direction DYN_TO_STA) is
1299+// resolved by the merger into a static-bound edge.
1300+// When xref direction is DYN_TO_STA, the edge points at the static node.
1301+// NOTE: EmitXRefEdge uses 3-arg Edge(to, idx, type) constructor, so edge->from
1302+// is always nullptr. We only verify edge->to and edge count.
1303+HWTEST_F_L0(RawHeapStaticSnapshotTest, Merge_XRefEdge_DynToSta)
1304+{
1305+ constexpr uint32_t staClassId = 8;
1306+ constexpr uint32_t dynNodeId = 0xDDDD0000U;
1307+ StaticSnapshotDataBuilder builder;
1308+ builder.WriteHeader(1);
1309+ builder.WriteStringRecord(10, "StaCls");
1310+ builder.WriteLoadClassRecord(staClassId, 10);
1311+ builder.WriteStaticClassDumpRecord(staClassId, 16, {});
1312+ builder.WriteRootRecord(staClassId);
1313+ // DYN_TO_STA: dynamic node references static node
1314+ builder.WriteXRefEdgeRecord(dynNodeId, staClassId, XREF_DYN_TO_STA);
1315+ builder.WriteHeapSummaryRecord();
1316+ 
1317+ StaticRawheapTranslate staticParser;
1318+ ASSERT_TRUE(ParseForMerge(builder, "xref_dyn2sta", staticParser));
1319+ ASSERT_EQ(staticParser.GetXRefs().size(), 1u);
1320+ rawheap_translate::Node *staNode = staticParser.FindNodeByNodeId(staClassId);
1321+ ASSERT_NE(staNode, nullptr);
1322+ 
1323+ RawHeapTranslateV1 dynamic(nullptr);
1324+ rawheap_translate::Node *synRoot = dynamic.CreateNode();
1325+ synRoot->nodeId = 1;
1326+ synRoot->type = SYNTHETIC_NODETYPE;
1327+ synRoot->strId = dynamic.InsertAndGetStringId("SyntheticRoot");
1328+ synRoot->edgeCount = 0;
1329+ rawheap_translate::Node *dynNode = dynamic.CreateNode();
1330+ dynNode->nodeId = dynNodeId;
1331+ dynNode->type = OBJECT_NODETYPE;
1332+ dynNode->strId = dynamic.InsertAndGetStringId("DynObj");
1333+ dynNode->edgeCount = 0;
1334+ 
1335+ SnapshotMerger merger;
1336+ ASSERT_TRUE(merger.Merge(dynamic, staticParser));
1337+ 
1338+ // DYN_TO_STA: expect 1 XREF edge pointing at static node
1339+ int xrefToStatic = 0;
1340+ for (rawheap_translate::Edge *edge : *dynamic.GetEdges()) {
1341+ if (edge->type == EdgeType::XREF) {
1342+ if (edge->to == staNode) {
1343+ xrefToStatic++;
1344+ }
1345+ }
1346+ }
1347+ EXPECT_EQ(xrefToStatic, 1) << "DYN_TO_STA should emit 1 XREF edge to static node";
1348+}
1349+ 
1350+// A bidirectional XRef (BIDIR) is expanded by the merger into two edges: one
1351+// to static and one to dynamic.
1352+// NOTE: EmitXRefEdge uses 3-arg Edge(to, idx, type) constructor, so edge->from
1353+// is always nullptr. We only verify edge->to targets and edge count.
1354+HWTEST_F_L0(RawHeapStaticSnapshotTest, Merge_XRefEdge_Bidirectional)
1355+{
1356+ constexpr uint32_t staClassId = 8;
1357+ constexpr uint32_t dynNodeId = 0xDDDD0000U;
1358+ StaticSnapshotDataBuilder builder;
1359+ builder.WriteHeader(1);
1360+ builder.WriteStringRecord(10, "StaCls");
1361+ builder.WriteLoadClassRecord(staClassId, 10);
1362+ builder.WriteStaticClassDumpRecord(staClassId, 16, {});
1363+ builder.WriteRootRecord(staClassId);
1364+ // BIDIR: both virtual machines share the object state - emit edges on both
1365+ // sides
1366+ builder.WriteXRefEdgeRecord(dynNodeId, staClassId, XREF_BIDIR);
1367+ builder.WriteHeapSummaryRecord();
1368+ 
1369+ StaticRawheapTranslate staticParser;
1370+ ASSERT_TRUE(ParseForMerge(builder, "xref_bidir", staticParser));
1371+ ASSERT_EQ(staticParser.GetXRefs().size(), 1u);
1372+ rawheap_translate::Node *staNode = staticParser.FindNodeByNodeId(staClassId);
1373+ ASSERT_NE(staNode, nullptr);
1374+ 
1375+ RawHeapTranslateV1 dynamic(nullptr);
1376+ rawheap_translate::Node *synRoot = dynamic.CreateNode();
1377+ synRoot->nodeId = 1;
1378+ synRoot->type = SYNTHETIC_NODETYPE;
1379+ synRoot->strId = dynamic.InsertAndGetStringId("SyntheticRoot");
1380+ synRoot->edgeCount = 0;
1381+ rawheap_translate::Node *dynNode = dynamic.CreateNode();
1382+ dynNode->nodeId = dynNodeId;
1383+ dynNode->type = OBJECT_NODETYPE;
1384+ dynNode->strId = dynamic.InsertAndGetStringId("DynObj");
1385+ dynNode->edgeCount = 0;
1386+ 
1387+ SnapshotMerger merger;
1388+ ASSERT_TRUE(merger.Merge(dynamic, staticParser));
1389+ 
1390+ // BIDIR: expect 2 XRef edges - one to static node, one to dynamic node
1391+ int xrefToStatic = 0;
1392+ int xrefToDynamic = 0;
1393+ for (rawheap_translate::Edge *edge : *dynamic.GetEdges()) {
1394+ if (edge->type == EdgeType::XREF) {
1395+ if (edge->to == staNode) {
1396+ xrefToStatic++;
1397+ }
1398+ if (edge->to == dynNode) {
1399+ xrefToDynamic++;
1400+ }
1401+ }
1402+ }
1403+ // BIDIR: expect 2 XRef edges - exactly one to static node and one to dynamic
1404+ // node. Asserting each side ==1 (not just sum==2) catches a regression where
1405+ // both edges are emitted in the same direction.
1406+ EXPECT_EQ(xrefToStatic, 1) << "BIDIR should emit one XRef edge to static node";
1407+ EXPECT_EQ(xrefToDynamic, 1) << "BIDIR should emit one XRef edge to dynamic node";
1408+}
1409+ 
1410+HWTEST_F_L0(RawHeapStaticSnapshotTest, Serialize_SyntheticSnapshot_ValidJSON)
1411+{
1412+ // End-to-end single-file: parse + translate + serialize produces a file whose
1413+ // edge counts are self-consistent (sum(node.edgeCount) == edge_count).
1414+ StaticSnapshotDataBuilder builder;
1415+ builder.WriteHeader(1);
1416+ builder.WriteStringRecord(10, "C");
1417+ builder.WriteStringRecord(11, "fld");
1418+ builder.WriteLoadClassRecord(2, 10); // 0x1000 -> nodeId 2
1419+ // instanceFieldNames: {11} = "fld"
1420+ builder.WriteStaticClassDumpRecord(2, 16, {11});
1421+ // fieldValues: {6} = object reference value (0x3000 -> nodeId 6)
1422+ builder.WriteStaticInstanceDumpRecord(4, 2, 16, {6}); // 0x2000->4, 0x1000->2
1423+ builder.WriteRootRecord(4); // 0x2000 -> nodeId 4
1424+ builder.WriteHeapSummaryRecord();
1425+ 
1426+ std::string inPath = builder.WriteToTempFile("ser");
1427+ tempFiles_.push_back(inPath);
1428+ std::string outPath = inPath + ".heapsnapshot";
1429+ tempFiles_.push_back(outPath);
1430+ 
1431+ ASSERT_TRUE(RawHeap::TranslateRawheap(inPath, outPath));
1432+ std::ifstream ifs(outPath);
1433+ ASSERT_TRUE(ifs.good());
1434+ std::string content((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
1435+ // The output must declare a node_count and edge_count, and be valid JSON-ish.
1436+ ASSERT_NE(content.find("\"nodes\":["), std::string::npos);
1437+ ASSERT_NE(content.find("\"edges\":["), std::string::npos);
1438+}
1439+ 
1440+// SuperClass edges: class -> superClass (INTERNAL, "superClass"). With the
1441+// superClass chain Leaf(4)->Mid(3)->Base(2), the graph must contain an INTERNAL
1442+// "superClass" edge targeting Base (from Mid) and one targeting Mid (from
1443+// Leaf).
1444+HWTEST_F_L0(RawHeapStaticSnapshotTest, CreateSuperClassEdges_ChainBuilt)
1445+{
1446+ StaticSnapshotDataBuilder builder;
1447+ builder.WriteHeader(1);
1448+ builder.WriteStringRecord(10, "Base");
1449+ builder.WriteStringRecord(11, "Mid");
1450+ builder.WriteStringRecord(12, "Leaf");
1451+ builder.WriteLoadClassRecord(2, 10); // Base
1452+ builder.WriteLoadClassRecord(3, 11); // Mid
1453+ builder.WriteLoadClassRecord(4, 12); // Leaf
1454+ builder.WriteStaticClassDumpRecordFull(2, 0, 16, {}); // Base: no super
1455+ builder.WriteStaticClassDumpRecordFull(3, 2, 16, {}); // Mid -> Base
1456+ builder.WriteStaticClassDumpRecordFull(4, 3, 16, {}); // Leaf -> Mid
1457+ builder.WriteHeapSummaryRecord();
1458+ 
1459+ StaticRawheapTranslate parser;
1460+ ASSERT_TRUE(ParseSingle(builder, "supercls", parser));
1461+ 
1462+ auto *tab = parser.GetStringTable();
1463+ auto resolveStr = [tab](StringId id) -> std::string {
1464+ if (id < StringHashMap::CUSTOM_STRID_START) {
1465+ return std::string();
1466+ }
1467+ return tab->GetStringByKey(tab->GetKeyByStringId(id));
1468+ };
1469+ 
1470+ uint32_t edgesToBase = 0;
1471+ uint32_t edgesToMid = 0;
1472+ for (rawheap_translate::Edge *e : *parser.GetEdges()) {
1473+ if (e->type != EdgeType::INTERNAL) {
1474+ continue;
1475+ }
1476+ if (resolveStr(e->nameOrIndex) != "superClass") {
1477+ continue;
1478+ }
1479+ uint32_t toId = static_cast<uint32_t>(e->to->nodeId);
1480+ if (toId == 2) { // -> Base (from Mid)
1481+ edgesToBase++;
1482+ }
1483+ if (toId == 3) { // -> Mid (from Leaf)
1484+ edgesToMid++;
1485+ }
1486+ }
1487+ EXPECT_EQ(edgesToBase, 1u) << "Mid->Base superClass edge missing";
1488+ EXPECT_EQ(edgesToMid, 1u) << "Leaf->Mid superClass edge missing";
1489+}
1490+ 
1491+// Static field edges + primitive value node: a static INT field "counter"
1492+// with value 2 must produce a PROPERTY edge named "counter" whose target is a
1493+// HEAP_NUMBER node whose name stringifies to "2".
1494+HWTEST_F_L0(RawHeapStaticSnapshotTest, CreateStaticFieldEdges_PrimitiveValueNode)
1495+{
1496+ StaticSnapshotDataBuilder builder;
1497+ builder.WriteHeader(1);
1498+ builder.WriteStringRecord(10, "Base");
1499+ builder.WriteStringRecord(20, "counter");
1500+ builder.WriteLoadClassRecord(2, 10);
1501+ builder.WriteStaticClassDumpRecordFull(2, 0, 16,
1502+ StaticSnapshotDataBuilder::StaticClassDumpRecordFields {
1503+ {{20, static_cast<uint8_t>(StaFieldType::INT)}}, // staticFields
1504+ {{static_cast<uint8_t>(StaFieldType::INT), 2}}, // staticValues
1505+ {}, // instanceFieldNames
1506+ {}}); // methodNameIds
1507+ builder.WriteHeapSummaryRecord();
1508+ 
1509+ StaticRawheapTranslate parser;
1510+ ASSERT_TRUE(ParseSingle(builder, "staval", parser));
1511+ 
1512+ auto *tab = parser.GetStringTable();
1513+ auto resolveStr = [tab](StringId id) -> std::string {
1514+ if (id < StringHashMap::CUSTOM_STRID_START) {
1515+ return std::string();
1516+ }
1517+ return tab->GetStringByKey(tab->GetKeyByStringId(id));
1518+ };
1519+ 
1520+ bool found = false;
1521+ for (rawheap_translate::Edge *e : *parser.GetEdges()) {
1522+ if (e->type != EdgeType::PROPERTY) {
1523+ continue;
1524+ }
1525+ if (resolveStr(e->nameOrIndex) != "counter") {
1526+ continue;
1527+ }
1528+ ASSERT_NE(e->to, nullptr);
1529+ EXPECT_EQ(e->to->type, HEAP_NUMBER) << "counter value node should be HEAP_NUMBER";
1530+ EXPECT_EQ(resolveStr(e->to->strId), "2") << "counter value node name should stringify the value";
1531+ found = true;
1532+ }
1533+ EXPECT_TRUE(found) << "static field 'counter' PROPERTY edge + value node not found";
1534+}
1535+ 
1536+HWTEST_F_L0(RawHeapStaticSnapshotTest, StaticCharFieldsUseUtf8OrUnicodeEscape)
1537+{
1538+ StaticSnapshotDataBuilder builder;
1539+ builder.WriteHeader();
1540+ builder.WriteStringRecord(10, "CharFields");
1541+ builder.WriteStringRecord(20, "ascii");
1542+ builder.WriteStringRecord(21, "cjk");
1543+ builder.WriteStringRecord(22, "nul");
1544+ builder.WriteStringRecord(23, "surrogate");
1545+ builder.WriteLoadClassRecord(2, 10);
1546+ const uint8_t charType = static_cast<uint8_t>(StaFieldType::CHAR);
1547+ builder.WriteStaticClassDumpRecordFull(
1548+ 2, 0, 16,
1549+ StaticSnapshotDataBuilder::StaticClassDumpRecordFields {
1550+ {{20, charType}, {21, charType}, {22, charType}, {23, charType}},
1551+ {{charType, 0x0041U}, {charType, 0x4E2DU}, {charType, 0x0000U}, {charType, 0xD800U}},
1552+ {},
1553+ {}});
1554+ builder.WriteHeapSummaryRecord();
1555+ 
1556+ StaticRawheapTranslate parser;
1557+ ASSERT_TRUE(ParseSingle(builder, "char_fields", parser));
1558+ auto *strings = parser.GetStringTable();
1559+ auto resolve = [strings](StringId id) -> std::string {
1560+ if (id < StringHashMap::CUSTOM_STRID_START) {
1561+ return std::string();
1562+ }
1563+ return strings->GetStringByKey(strings->GetKeyByStringId(id));
1564+ };
1565+ std::vector<std::string> values;
1566+ for (rawheap_translate::Edge *edge : *parser.GetEdges()) {
1567+ if (edge->from != nullptr && edge->from->nodeId == 2 && edge->to != nullptr && edge->to->type == STRING) {
1568+ values.push_back(resolve(edge->to->strId));
1569+ }
1570+ }
1571+ EXPECT_NE(std::find(values.begin(), values.end(), "A"), values.end());
1572+ EXPECT_NE(std::find(values.begin(), values.end(), "\xE4\xB8\xAD"), values.end());
1573+ EXPECT_NE(std::find(values.begin(), values.end(), "\\u0000"), values.end());
1574+ EXPECT_NE(std::find(values.begin(), values.end(), "\\uD800"), values.end());
1575+}
1576+ 
1577+HWTEST_F_L0(RawHeapStaticSnapshotTest, TaggedFieldsPreserveRuntimeValueKinds)
1578+{
1579+ constexpr uint32_t classNodeId = 2;
1580+ constexpr uint32_t strongNodeId = 6;
1581+ constexpr uint32_t weakNodeId = 8;
1582+ StaticSnapshotDataBuilder builder;
1583+ builder.WriteHeader();
1584+ builder.WriteStringRecord(10, "TaggedFields");
1585+ builder.WriteStringRecord(20, "strong");
1586+ builder.WriteStringRecord(21, "weak");
1587+ builder.WriteStringRecord(22, "undefined");
1588+ builder.WriteStringRecord(23, "unknown");
1589+ builder.WriteLoadClassRecord(classNodeId, 10);
1590+ const uint8_t taggedType = static_cast<uint8_t>(StaFieldType::TAGGED);
1591+ builder.WriteStaticClassDumpRecordFull(classNodeId, 0, 16,
1592+ StaticSnapshotDataBuilder::StaticClassDumpRecordFields {
1593+ {{20, taggedType}, {21, taggedType}, {22, taggedType}, {23, taggedType}},
1594+ {{static_cast<uint8_t>(StaFieldType::OBJECT), strongNodeId},
1595+ {static_cast<uint8_t>(StaFieldType::WEAK_OBJECT), weakNodeId},
1596+ {taggedType, STATIC_TAGGED_UNDEFINED},
1597+ {taggedType, 0x123456789ABCDEF0ULL}},
1598+ {},
1599+ {}});
1600+ builder.WriteHeapSummaryRecord();
1601+ 
1602+ StaticRawheapTranslate parser;
1603+ ASSERT_TRUE(ParseSingle(builder, "tagged_fields", parser));
1604+ rawheap_translate::Node *classNode = parser.FindNodeByNodeId(classNodeId);
1605+ ASSERT_NE(classNode, nullptr);
1606+ auto *strings = parser.GetStringTable();
1607+ 
1608+ bool foundStrong = false;
1609+ bool foundWeak = false;
1610+ bool foundUndefined = false;
1611+ bool foundUnknown = false;
1612+ for (rawheap_translate::Edge *edge : *parser.GetEdges()) {
1613+ if (edge->from != classNode || edge->to == nullptr) {
1614+ continue;
1615+ }
1616+ std::string name = ResolveString(strings, edge->nameOrIndex);
1617+ foundStrong |= name == "strong" && edge->type == EdgeType::PROPERTY && edge->to->nodeId == strongNodeId;
1618+ foundWeak |= name == "weak" && edge->type == EdgeType::WEAK && edge->to->nodeId == weakNodeId;
1619+ foundUndefined |=
1620+ name == "undefined" && ResolveString(strings, edge->to->strId) == "undefined" &&
1621+ edge->to->type == SYNTHETIC_NODETYPE;
1622+ foundUnknown |= name == "unknown" && ResolveString(strings, edge->to->strId) == "0x123456789ABCDEF0" &&
1623+ edge->to->type == SYNTHETIC_NODETYPE;
1624+ }
1625+ EXPECT_TRUE(foundStrong);
1626+ EXPECT_TRUE(foundWeak);
1627+ EXPECT_TRUE(foundUndefined);
1628+ EXPECT_TRUE(foundUnknown);
1629+}
1630+ 
1631+// ============================================================================
1632+// EmitMethodNameEdges turns each method-name id into a PROPERTY edge.
1633+// Exercises it by writing a non-empty methodNameIds map.
1634+// The class node should emit PROPERTY edges named "foo"/"bar" targeting
1635+// closure/method nodes (CLOSURE_NODETYPE). This path was previously only
1636+// tested with empty methodNameIds, missing the core method-name edge logic.
1637+// ============================================================================
1638+ 
1639+HWTEST_F_L0(RawHeapStaticSnapshotTest, EmitMethodNameEdges_NonEmptyMethodNames)
1640+{
1641+ StaticSnapshotDataBuilder builder;
1642+ builder.WriteHeader(1);
1643+ builder.WriteStringRecord(10, "MyClass");
1644+ builder.WriteStringRecord(20, "foo"); // method name id 20 -> "foo"
1645+ builder.WriteStringRecord(21, "bar"); // method name id 21 -> "bar"
1646+ builder.WriteLoadClassRecord(2, 10); // class nodeId 2 -> "MyClass"
1647+ 
1648+ // Write a class with 2 method name ids (20, 21). The parser should emit
1649+ // 2 PROPERTY edges from the class node, named "foo" and "bar", targeting
1650+ // synthetic closure nodes (CLOSURE_NODETYPE).
1651+ builder.WriteStaticClassDumpRecordFull(2, 0, 16,
1652+ StaticSnapshotDataBuilder::StaticClassDumpRecordFields {
1653+ {},
1654+ {},
1655+ {}, // no static fields, no static values, no instance fields
1656+ {20, 21}}); // methodNameIds: {20, 21} -> "foo", "bar"
1657+ builder.WriteRootRecord(2); // mark class as root so it appears in graph
1658+ builder.WriteHeapSummaryRecord();
1659+ 
1660+ StaticRawheapTranslate parser;
1661+ ASSERT_TRUE(ParseSingle(builder, "methodnames", parser));
1662+ 
1663+ auto *tab = parser.GetStringTable();
1664+ auto resolveStr = [tab](StringId id) -> std::string {
1665+ if (id < StringHashMap::CUSTOM_STRID_START) {
1666+ return std::string();
1667+ }
1668+ return tab->GetStringByKey(tab->GetKeyByStringId(id));
1669+ };
1670+ 
1671+ // Find the class node
1672+ rawheap_translate::Node *classNode = parser.FindNodeByNodeId(2);
1673+ ASSERT_NE(classNode, nullptr);
1674+ EXPECT_EQ(classNode->type, CLASS_NODETYPE);
1675+ 
1676+ // Collect PROPERTY edges from the class node that correspond to method names.
1677+ // EmitMethodNameEdges creates edges named by the method name string,
1678+ // targeting closure nodes (CLOSURE_NODETYPE).
1679+ std::vector<std::string> methodEdgeNames;
1680+ int closureEdgeCount = 0;
1681+ for (rawheap_translate::Edge *e : *parser.GetEdges()) {
1682+ if (e->type != EdgeType::PROPERTY) {
1683+ continue;
1684+ }
1685+ // The method-name edges originate from the class node
1686+ if (e->from == classNode) {
1687+ std::string edgeName = resolveStr(e->nameOrIndex);
1688+ methodEdgeNames.push_back(edgeName);
1689+ // Verify target is a closure node
1690+ if (e->to != nullptr && e->to->type == CLOSURE_NODETYPE) {
1691+ closureEdgeCount++;
1692+ }
1693+ }
1694+ }
1695+ 
1696+ // Assert 2 method-name edges emitted
1697+ ASSERT_EQ(methodEdgeNames.size(), 2u) << "class should have 2 method-name PROPERTY edges";
1698+ EXPECT_NE(std::find(methodEdgeNames.begin(), methodEdgeNames.end(), "foo"), methodEdgeNames.end())
1699+ << "method edge 'foo' not found";
1700+ EXPECT_NE(std::find(methodEdgeNames.begin(), methodEdgeNames.end(), "bar"), methodEdgeNames.end())
1701+ << "method edge 'bar' not found";
1702+ EXPECT_EQ(closureEdgeCount, 2) << "both method edges should target CLOSURE nodes";
1703+}
1704+ 
1705+// ============================================================================
1706+// ReadFieldValue on a single OBJECT field (u32 nodeId, 4 bytes). The class
1707+// declares 5 field descriptors but the instance body carries only 1 OBJECT
1708+// value, so only the OBJECT (4-byte) read path is exercised here. For
1709+// mixed-type coverage (BOOLEAN/CHAR/INT/LONG/FLOAT/DOUBLE sizes) see the
1710+// array-element tests.
1711+// ============================================================================
1712+ 
1713+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseStaticInstanceDump_SingleObjectField)
1714+{
1715+ // Class declares 5 instance field descriptors, but the instance body below
1716+ // writes only 1 OBJECT value. This tests the OBJECT (4-byte nodeId) read
1717+ // path in ReadFieldValue; the class's field descriptors are used for edge
1718+ // names, not for value parsing (instance body's fieldCount determines that).
1719+ StaticSnapshotDataBuilder builder;
1720+ builder.WriteHeader(1);
1721+ builder.WriteStringRecord(10, "FiveFieldsClass");
1722+ builder.WriteStringRecord(11, "boolField");
1723+ builder.WriteStringRecord(12, "charField");
1724+ builder.WriteStringRecord(13, "intField");
1725+ builder.WriteStringRecord(14, "longField");
1726+ builder.WriteStringRecord(15, "objField");
1727+ builder.WriteLoadClassRecord(2, 10); // 0x1000 -> nodeId 2
1728+ // Class declares 5 fields (BOOLEAN/CHAR/INT/LONG/OBJECT) for edge names
1729+ builder.WriteStaticClassDumpRecord(2, 32, {11, 12, 13, 14, 15});
1730+ // Instance carries only 1 OBJECT field value (fieldCount in body = 1)
1731+ builder.WriteStaticInstanceDumpRecord(4, 2, 32, {10}); // 0x2000->4, 0x1000->2
1732+ builder.WriteHeapSummaryRecord();
1733+ 
1734+ StaticRawheapTranslate parser;
1735+ ASSERT_TRUE(ParseSingle(builder, "singleobj", parser));
1736+ // Verify the class and instance were parsed
1737+ ASSERT_NE(parser.FindNodeByNodeId(2), nullptr); // 0x1000 -> nodeId 2
1738+ ASSERT_NE(parser.FindNodeByNodeId(4), nullptr); // 0x2000 -> nodeId 4
1739+ ASSERT_GT(parser.GetNodeCount(), 3u);
1740+}
1741+ 
1742+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseStaticArrayDump_BooleanElement)
1743+{
1744+ // BOOLEAN arrays: elementType=BOOLEAN, FieldSize=1B per element
1745+ // Exercises ReadFieldValue with byteSize=1. 4 elements -> 4 zero bytes.
1746+ StaticSnapshotDataBuilder builder;
1747+ builder.WriteHeader(1);
1748+ builder.WriteStringRecord(10, "boolean[]");
1749+ builder.WriteLoadClassRecord(2, 10); // 0x1000 -> nodeId 2
1750+ ArrayDumpParams arrParams;
1751+ arrParams.objectId = 4; // 0x2000 -> nodeId 4
1752+ arrParams.classObjectId = 2; // 0x1000 -> nodeId 2
1753+ arrParams.instanceSize = 24;
1754+ arrParams.arrayLength = 4; // 4 * 1 = 4 zero bytes
1755+ arrParams.elementType = static_cast<uint8_t>(StaFieldType::BOOLEAN);
1756+ builder.WriteStaticArrayDumpRecord(arrParams);
1757+ builder.WriteHeapSummaryRecord();
1758+ 
1759+ StaticRawheapTranslate parser;
1760+ ASSERT_TRUE(ParseSingle(builder, "boolarr", parser));
1761+ ASSERT_NE(parser.FindNodeByNodeId(4), nullptr); // 0x2000 -> nodeId 4
1762+}
1763+ 
1764+HWTEST_F_L0(RawHeapStaticSnapshotTest, StaticCharArrayUsesUtf8OrUnicodeEscape)
1765+{
1766+ StaticSnapshotDataBuilder builder;
1767+ builder.WriteHeader();
1768+ builder.WriteStringRecord(10, "char[]");
1769+ builder.WriteLoadClassRecord(2, 10);
1770+ ArrayDumpParams params;
1771+ params.objectId = 4;
1772+ params.classObjectId = 2;
1773+ params.instanceSize = 32;
1774+ params.arrayLength = 4;
1775+ params.elementType = static_cast<uint8_t>(StaFieldType::CHAR);
1776+ params.primitiveValues = {0x0041U, 0x4E2DU, 0x0000U, 0xDFFFU};
1777+ builder.WriteStaticArrayDumpRecord(params);
1778+ builder.WriteHeapSummaryRecord();
1779+ 
1780+ StaticRawheapTranslate parser;
1781+ ASSERT_TRUE(ParseSingle(builder, "char_array", parser));
1782+ auto *strings = parser.GetStringTable();
1783+ auto resolve = [strings](StringId id) -> std::string {
1784+ if (id < StringHashMap::CUSTOM_STRID_START) {
1785+ return std::string();
1786+ }
1787+ return strings->GetStringByKey(strings->GetKeyByStringId(id));
1788+ };
1789+ std::vector<std::string> values;
1790+ for (rawheap_translate::Node *node : *parser.GetNodes()) {
1791+ if (node->type == STRING) {
1792+ values.push_back(resolve(node->strId));
1793+ }
1794+ }
1795+ EXPECT_NE(std::find(values.begin(), values.end(), "A"), values.end());
1796+ EXPECT_NE(std::find(values.begin(), values.end(), "\xE4\xB8\xAD"), values.end());
1797+ EXPECT_NE(std::find(values.begin(), values.end(), "\\u0000"), values.end());
1798+ EXPECT_NE(std::find(values.begin(), values.end(), "\\uDFFF"), values.end());
1799+}
1800+ 
1801+HWTEST_F_L0(RawHeapStaticSnapshotTest, TaggedArrayEmitsDirectValueAndWeakEdges)
1802+{
1803+ constexpr uint32_t arrayNodeId = 4;
1804+ constexpr uint32_t strongNodeId = 6;
1805+ constexpr uint32_t weakNodeId = 8;
1806+ StaticSnapshotDataBuilder builder;
1807+ builder.WriteHeader();
1808+ builder.WriteStringRecord(10, "TaggedValue[]");
1809+ builder.WriteLoadClassRecord(2, 10);
1810+ ArrayDumpParams params;
1811+ params.objectId = arrayNodeId;
1812+ params.classObjectId = 2;
1813+ params.instanceSize = 64;
1814+ params.arrayLength = 10;
1815+ params.elementType = static_cast<uint8_t>(StaFieldType::TAGGED);
1816+ params.taggedValues = {
1817+ {static_cast<uint8_t>(StaFieldType::OBJECT), strongNodeId},
1818+ {static_cast<uint8_t>(StaFieldType::WEAK_OBJECT), weakNodeId},
1819+ {static_cast<uint8_t>(StaFieldType::INT), 0xFFFFFFF9U},
1820+ {static_cast<uint8_t>(StaFieldType::BOOLEAN), 1},
1821+ {static_cast<uint8_t>(StaFieldType::TAGGED), STATIC_TAGGED_NULL},
1822+ {static_cast<uint8_t>(StaFieldType::TAGGED), STATIC_TAGGED_UNDEFINED},
1823+ {static_cast<uint8_t>(StaFieldType::TAGGED), STATIC_TAGGED_HOLE},
1824+ {static_cast<uint8_t>(StaFieldType::TAGGED), STATIC_TAGGED_EXCEPTION},
1825+ {static_cast<uint8_t>(StaFieldType::TAGGED), STATIC_TAGGED_FALSE},
1826+ {static_cast<uint8_t>(StaFieldType::TAGGED), 0xFEDCBA9876543210ULL},
1827+ };
1828+ builder.WriteStaticArrayDumpRecord(params);
1829+ builder.WriteHeapSummaryRecord();
1830+ 
1831+ StaticRawheapTranslate parser;
1832+ ASSERT_TRUE(ParseSingle(builder, "tagged_array", parser));
1833+ rawheap_translate::Node *arrayNode = parser.FindNodeByNodeId(arrayNodeId);
1834+ ASSERT_NE(arrayNode, nullptr);
1835+ rawheap_translate::Node *buffer = FindArrayBuffer(parser, arrayNode);
1836+ ASSERT_NE(buffer, nullptr);
1837+ VerifyTaggedArrayEdges(parser, buffer, weakNodeId, params.arrayLength - 1U);
1838+}
1839+ 
1840+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseTaggedArrayBatchLocatesEveryItemPrefix)
1841+{
1842+ StaticSnapshotDataBuilder builder;
1843+ builder.WriteHeader();
1844+ builder.WriteStringRecord(10, "TaggedValue[]");
1845+ builder.WriteLoadClassRecord(2, 10);
1846+ ArrayDumpParams first;
1847+ first.objectId = 4;
1848+ first.classObjectId = 2;
1849+ first.instanceSize = 32;
1850+ first.arrayLength = 2;
1851+ first.elementType = static_cast<uint8_t>(StaFieldType::TAGGED);
1852+ first.taggedValues = {{static_cast<uint8_t>(StaFieldType::INT), 7},
1853+ {static_cast<uint8_t>(StaFieldType::TAGGED), STATIC_TAGGED_NULL}};
1854+ ArrayDumpParams second;
1855+ second.objectId = 6;
1856+ second.classObjectId = 2;
1857+ second.instanceSize = 24;
1858+ second.arrayLength = 1;
1859+ second.elementType = static_cast<uint8_t>(StaFieldType::TAGGED);
1860+ second.taggedValues = {{static_cast<uint8_t>(StaFieldType::TAGGED), STATIC_TAGGED_UNDEFINED}};
1861+ builder.WriteStaticArrayDumpRecordBatch({first, second});
1862+ builder.WriteHeapSummaryRecord();
1863+ 
1864+ StaticRawheapTranslate parser;
1865+ ASSERT_TRUE(ParseSingle(builder, "tagged_array_batch", parser));
1866+ EXPECT_NE(parser.FindNodeByNodeId(first.objectId), nullptr);
1867+ EXPECT_NE(parser.FindNodeByNodeId(second.objectId), nullptr);
1868+}
1869+ 
1870+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseTaggedArrayRejectsUnknownRuntimeType)
1871+{
1872+ StaticSnapshotDataBuilder builder;
1873+ builder.WriteHeader();
1874+ ArrayDumpParams params;
1875+ params.objectId = 4;
1876+ params.arrayLength = 1;
1877+ params.elementType = static_cast<uint8_t>(StaFieldType::TAGGED);
1878+ params.taggedValues = {{0xFFU, 0}};
1879+ builder.WriteStaticArrayDumpRecord(params);
1880+ 
1881+ StaticRawheapTranslate parser;
1882+ EXPECT_FALSE(ParseForMerge(builder, "tagged_array_bad_type", parser));
1883+}
1884+ 
1885+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseStaticArrayDump_IntElement)
1886+{
1887+ // INT arrays: elementType=INT, FieldSize=4B per element
1888+ // Exercises ReadFieldValue with byteSize=4. 8 elements -> 8 * 4 = 32 zero
1889+ // bytes.
1890+ StaticSnapshotDataBuilder builder;
1891+ builder.WriteHeader(1);
1892+ builder.WriteStringRecord(10, "int[]");
1893+ builder.WriteLoadClassRecord(2, 10); // 0x1000 -> nodeId 2
1894+ ArrayDumpParams arrParams;
1895+ arrParams.objectId = 4; // 0x2000 -> nodeId 4
1896+ arrParams.classObjectId = 2; // 0x1000 -> nodeId 2
1897+ arrParams.instanceSize = 32;
1898+ arrParams.arrayLength = 8; // 8 * 4 = 32 zero bytes
1899+ arrParams.elementType = static_cast<uint8_t>(StaFieldType::INT);
1900+ builder.WriteStaticArrayDumpRecord(arrParams);
1901+ builder.WriteHeapSummaryRecord();
1902+ 
1903+ StaticRawheapTranslate parser;
1904+ ASSERT_TRUE(ParseSingle(builder, "intarr", parser));
1905+ ASSERT_NE(parser.FindNodeByNodeId(4), nullptr); // 0x2000 -> nodeId 4
1906+}
1907+ 
1908+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseStaticArrayDump_DoubleElement)
1909+{
1910+ // DOUBLE arrays: elementType=DOUBLE, FieldSize=8B per element
1911+ // Exercises ReadFieldValue with byteSize=8. 3 elements -> 3 * 8 = 24 zero
1912+ // bytes.
1913+ StaticSnapshotDataBuilder builder;
1914+ builder.WriteHeader(1);
1915+ builder.WriteStringRecord(10, "double[]");
1916+ builder.WriteLoadClassRecord(2, 10); // 0x1000 -> nodeId 2
1917+ ArrayDumpParams arrParams;
1918+ arrParams.objectId = 4; // 0x2000 -> nodeId 4
1919+ arrParams.classObjectId = 2; // 0x1000 -> nodeId 2
1920+ arrParams.instanceSize = 48;
1921+ arrParams.arrayLength = 3; // 3 * 8 = 24 zero bytes
1922+ arrParams.elementType = static_cast<uint8_t>(StaFieldType::DOUBLE);
1923+ builder.WriteStaticArrayDumpRecord(arrParams);
1924+ builder.WriteHeapSummaryRecord();
1925+ 
1926+ StaticRawheapTranslate parser;
1927+ ASSERT_TRUE(ParseSingle(builder, "dblarr", parser));
1928+ ASSERT_NE(parser.FindNodeByNodeId(4), nullptr); // 0x2000 -> nodeId 4
1929+}
1930+ 
1931+// ============================================================================
1932+// Two-phase array parsing: ScanArrayPrefixes partitions known vs unknown
1933+// element sizes, then DistributeUnknownData assigns the leftover bytes.
1934+// Exercises it by constructing arrays with mixed known/unknown element sizes.
1935+// ============================================================================
1936+ 
1937+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseStaticArrayDump_MixedKnownAndUnknownTypes)
1938+{
1939+ // Two arrays in one record: one with known OBJECT type, one with SHORT type
1940+ // This exercises ScanArrayPrefixes to partition known vs unknown data,
1941+ // and DistributeUnknownData to allocate the remaining body bytes
1942+ StaticSnapshotDataBuilder builder;
1943+ builder.WriteHeader(1);
1944+ builder.WriteStringRecord(10, "Object[]");
1945+ builder.WriteStringRecord(11, "short[]");
1946+ builder.WriteLoadClassRecord(2, 10); // 0x1000 -> nodeId 2
1947+ builder.WriteLoadClassRecord(12, 11); // 0x1100 -> nodeId 12
1948+ 
1949+ // OBJECT array: 2 elements, each 4 bytes (u32 nodeId) -> 8 bytes known data
1950+ ArrayDumpParams objArr;
1951+ objArr.objectId = 4; // 0x2000 -> nodeId 4
1952+ objArr.classObjectId = 2; // 0x1000 -> nodeId 2
1953+ objArr.instanceSize = 32;
1954+ objArr.arrayLength = 2;
1955+ objArr.elementType = static_cast<uint8_t>(StaFieldType::OBJECT);
1956+ objArr.elements = {8, 10}; // 0x4000->8, 0x5000->10
1957+ builder.WriteStaticArrayDumpRecord(objArr);
1958+ 
1959+ // SHORT array: 3 elements, each 2 bytes -> 6 bytes element data
1960+ ArrayDumpParams shortArr;
1961+ shortArr.objectId = 14; // 0x2100 -> nodeId 14
1962+ shortArr.classObjectId = 12; // 0x1100 -> nodeId 12
1963+ shortArr.instanceSize = 24;
1964+ shortArr.arrayLength = 3; // 3 * 2 = 6 zero bytes
1965+ shortArr.elementType = static_cast<uint8_t>(StaFieldType::SHORT);
1966+ builder.WriteStaticArrayDumpRecord(shortArr);
1967+ 
1968+ builder.WriteHeapSummaryRecord();
1969+ 
1970+ StaticRawheapTranslate parser;
1971+ ASSERT_TRUE(ParseSingle(builder, "mixarr", parser));
1972+ ASSERT_NE(parser.FindNodeByNodeId(8), nullptr); // 0x4000 -> nodeId 8
1973+ ASSERT_NE(parser.FindNodeByNodeId(10), nullptr); // 0x5000 -> nodeId 10
1974+}
1975+ 
1976+HWTEST_F_L0(RawHeapStaticSnapshotTest, ParseStaticArrayDump_EmptyArray)
1977+{
1978+ // Empty array (arrayLength=0): ScanArrayPrefixes should handle gracefully
1979+ // No element data to distribute - dataSizeKnown=true, dataSize=0
1980+ StaticSnapshotDataBuilder builder;
1981+ builder.WriteHeader(1);
1982+ builder.WriteStringRecord(10, "Object[]");
1983+ builder.WriteLoadClassRecord(2, 10); // 0x1000 -> nodeId 2
1984+ ArrayDumpParams arrParams;
1985+ arrParams.objectId = 4; // 0x2000 -> nodeId 4
1986+ arrParams.classObjectId = 2; // 0x1000 -> nodeId 2
1987+ arrParams.instanceSize = 24;
1988+ arrParams.arrayLength = 0;
1989+ arrParams.elementType = static_cast<uint8_t>(StaFieldType::OBJECT);
1990+ builder.WriteStaticArrayDumpRecord(arrParams);
1991+ builder.WriteHeapSummaryRecord();
1992+ 
1993+ StaticRawheapTranslate parser;
1994+ ASSERT_TRUE(ParseSingle(builder, "emptyarr", parser));
1995+ ASSERT_NE(parser.FindNodeByNodeId(4), nullptr); // 0x2000 -> nodeId 4
1996+}
1997+ 
1998+} // namespace panda::test
Mecmascript/dfx/hprof/tests/rawheap_translate_test.cpp+239-0
@@ -16,8 +16,21 @@
16#include "ecmascript/tests/test_helper.h"16#include "ecmascript/tests/test_helper.h"
17#include "ecmascript/dfx/hprof/rawheap_translate/metadata_parse.h"17#include "ecmascript/dfx/hprof/rawheap_translate/metadata_parse.h"
18#include "ecmascript/dfx/hprof/rawheap_translate/rawheap_translate.h"18#include "ecmascript/dfx/hprof/rawheap_translate/rawheap_translate.h"
19+#include "ecmascript/dfx/hprof/rawheap_translate/static_rawheap_translate.h"
19#include "ecmascript/dfx/hprof/rawheap_translate/utils.h"20#include "ecmascript/dfx/hprof/rawheap_translate/utils.h"
20 21 
22+#include <cstdlib>
23+ 
24+// Forward declarations for the rawheap_translator CLI entry functions
25+// (defined in main.cpp, compiled into this test binary via the BUILD.gn
26+// sources list).
27+namespace rawheap_translate {
28+bool ParseArgsSingle(const int argc, const char **argv, std::string &input, std::string &output);
29+bool ParseArgsTwoFile(const int argc, const char **argv, std::string &dynamicInput,
30+ std::string &staticInput, std::string &output);
31+int Main(const int argc, const char **argv);
32+} // namespace rawheap_translate
33+ 
21using namespace panda::ecmascript;34using namespace panda::ecmascript;
22 35 
23namespace panda::test {36namespace panda::test {
@@ -1331,4 +1344,230 @@ HWTEST_F_L0(RawHeapTranslateTest, ParseRawheapWithoutGlobalHandleObjectData)
1331 ASSERT_NO_FATAL_FAILURE(ParseV2NoGlobalHandleObject("sentinel", sentinel));1344 ASSERT_NO_FATAL_FAILURE(ParseV2NoGlobalHandleObject("sentinel", sentinel));
1332}1345}
1333 1346 
1347+// ============================================================================
1348+// rawheap_translator CLI entry points: ParseArgsSingle / ParseArgsTwoFile.
1349+// main.cpp inspects the input format, selects a parser, then runs either the
1350+// merge path or the single-file path through these entry functions.
1351+// ============================================================================
1352+ 
1353+HWTEST_F_L0(RawHeapTranslateTest, CLI_ParseArgsSingle_Basic)
1354+{
1355+ // Single-file mode: 1 .rawheap path, no explicit output
1356+ const char *argv[] = {"rawheap_translator", "/tmp/test.rawheap"};
1357+ int argc = 2;
1358+ std::string input, output;
1359+ bool result = rawheap_translate::ParseArgsSingle(argc, argv, input, output);
1360+ ASSERT_TRUE(result);
1361+ ASSERT_EQ(input, "/tmp/test.rawheap");
1362+ // No output provided: fall back to the timestamped hprof_<ts>.heapsnapshot.
1363+ ASSERT_FALSE(output.empty());
1364+ ASSERT_TRUE(rawheap_translate::EndsWith(output, ".heapsnapshot"));
1365+ ASSERT_EQ(output.rfind("hprof_", 0), 0U);
1366+}
1367+ 
1368+HWTEST_F_L0(RawHeapTranslateTest, CLI_ParseArgsSingle_WithExplicitOutput)
1369+{
1370+ // Single-file mode with a correct explicit output path (argv[2])
1371+ const char *argv[] = {"rawheap_translator", "/tmp/test.rawheap", "/tmp/out.heapsnapshot"};
1372+ int argc = 3;
1373+ std::string input, output;
1374+ bool result = rawheap_translate::ParseArgsSingle(argc, argv, input, output);
1375+ ASSERT_TRUE(result);
1376+ ASSERT_EQ(input, "/tmp/test.rawheap");
1377+ ASSERT_EQ(output, "/tmp/out.heapsnapshot");
1378+}
1379+ 
1380+HWTEST_F_L0(RawHeapTranslateTest, CLI_ParseArgsSingle_WrongOutputExtension)
1381+{
1382+ // Single-file mode, output given with a wrong extension: append ".heapsnapshot"
1383+ const char *argv[] = {"rawheap_translator", "/tmp/test.rawheap", "/tmp/out.txt"};
1384+ int argc = 3;
1385+ std::string input, output;
1386+ bool result = rawheap_translate::ParseArgsSingle(argc, argv, input, output);
1387+ ASSERT_TRUE(result);
1388+ ASSERT_EQ(input, "/tmp/test.rawheap");
1389+ ASSERT_EQ(output, "/tmp/out.txt.heapsnapshot");
1390+}
1391+ 
1392+HWTEST_F_L0(RawHeapTranslateTest, CLI_ParseArgsTwoFile_Basic)
1393+{
1394+ // Two-file mode: dynamic + static .rawheap paths, no explicit output
1395+ const char *argv[] = {"rawheap_translator", "/tmp/dynamic.rawheap", "/tmp/static.rawheap"};
1396+ int argc = 3;
1397+ std::string dynamicInput, staticInput, output;
1398+ bool result = rawheap_translate::ParseArgsTwoFile(argc, argv, dynamicInput, staticInput, output);
1399+ ASSERT_TRUE(result);
1400+ ASSERT_EQ(dynamicInput, "/tmp/dynamic.rawheap");
1401+ ASSERT_EQ(staticInput, "/tmp/static.rawheap");
1402+ // No output provided: fall back to the timestamped hprof_<ts>.heapsnapshot.
1403+ ASSERT_TRUE(rawheap_translate::EndsWith(output, ".heapsnapshot"));
1404+ ASSERT_EQ(output.rfind("hprof_", 0), 0U);
1405+}
1406+ 
1407+HWTEST_F_L0(RawHeapTranslateTest, CLI_ParseArgsTwoFile_WithExplicitOutput)
1408+{
1409+ // Two-file mode with explicit output path
1410+ const char *argv[] = {"rawheap_translator", "/tmp/dynamic.rawheap", "/tmp/static.rawheap",
1411+ "/tmp/merged.heapsnapshot"};
1412+ int argc = 4;
1413+ std::string dynamicInput, staticInput, output;
1414+ bool result = rawheap_translate::ParseArgsTwoFile(argc, argv, dynamicInput, staticInput, output);
1415+ ASSERT_TRUE(result);
1416+ ASSERT_EQ(dynamicInput, "/tmp/dynamic.rawheap");
1417+ ASSERT_EQ(staticInput, "/tmp/static.rawheap");
1418+ ASSERT_EQ(output, "/tmp/merged.heapsnapshot");
1419+}
1420+ 
1421+HWTEST_F_L0(RawHeapTranslateTest, CLI_ParseArgsTwoFile_InvalidExtension)
1422+{
1423+ // Two-file mode: one file without .rawheap extension → should fail
1424+ const char *argv[] = {"rawheap_translator", "/tmp/dynamic.heapsnapshot", "/tmp/static.rawheap"};
1425+ int argc = 3;
1426+ std::string dynamicInput, staticInput, output;
1427+ bool result = rawheap_translate::ParseArgsTwoFile(argc, argv, dynamicInput, staticInput, output);
1428+ ASSERT_FALSE(result);
1429+}
1430+ 
1431+HWTEST_F_L0(RawHeapTranslateTest, CLI_ParseArgsTwoFile_InsufficientArgs)
1432+{
1433+ // Only 1 argument → two-file mode should fail
1434+ const char *argv[] = {"rawheap_translator", "/tmp/test.rawheap"};
1435+ int argc = 2;
1436+ std::string dynamicInput, staticInput, output;
1437+ bool result = rawheap_translate::ParseArgsTwoFile(argc, argv, dynamicInput, staticInput, output);
1438+ ASSERT_FALSE(result);
1439+}
1440+ 
1441+HWTEST_F_L0(RawHeapTranslateTest, CLI_Main_VersionFlag)
1442+{
1443+ // --version should return 0 without doing any translation
1444+ const char *argv[] = {"rawheap_translator", "--version"};
1445+ int argc = 2;
1446+ int result = rawheap_translate::Main(argc, argv);
1447+ ASSERT_EQ(result, 0);
1448+}
1449+ 
1450+HWTEST_F_L0(RawHeapTranslateTest, CLI_Main_HelpFlag)
1451+{
1452+ // --help should return 0 without doing any translation
1453+ const char *argv[] = {"rawheap_translator", "--help"};
1454+ int argc = 2;
1455+ int result = rawheap_translate::Main(argc, argv);
1456+ ASSERT_EQ(result, 0);
1457+}
1458+ 
1459+HWTEST_F_L0(RawHeapTranslateTest, CLI_Main_NoArgs)
1460+{
1461+ // No arguments → should return 0 (print help message)
1462+ const char *argv[] = {"rawheap_translator"};
1463+ int argc = 1;
1464+ int result = rawheap_translate::Main(argc, argv);
1465+ ASSERT_EQ(result, 0);
1466+}
1467+ 
1468+HWTEST_F_L0(RawHeapTranslateTest, CLI_Main_InvalidInput)
1469+{
1470+ // Non-.rawheap argument → should return 0 (print help/error)
1471+ const char *argv[] = {"rawheap_translator", "/tmp/something.txt"};
1472+ int argc = 2;
1473+ int result = rawheap_translate::Main(argc, argv);
1474+ ASSERT_EQ(result, 0);
1475+}
1476+ 
1477+HWTEST_F_L0(RawHeapTranslateTest, CLI_Main_TwoFileTranslationFailure)
1478+{
1479+ const char *dynamicPath = "/tmp/nonexistent_dynamic.rawheap";
1480+ const char *staticPath = "/tmp/nonexistent_static.rawheap";
1481+ const char *outputPath = "/tmp/nonexistent_output.heapsnapshot";
1482+ std::remove(dynamicPath);
1483+ std::remove(staticPath);
1484+ std::remove(outputPath);
1485+ const char *argv[] = {"rawheap_translator", dynamicPath, staticPath, outputPath};
1486+ 
1487+ EXPECT_EQ(rawheap_translate::Main(4, argv), EXIT_FAILURE);
1488+}
1489+ 
1490+// ============================================================================
1491+// TranslateRawheap error paths: malformed input and merge-failure scenarios
1492+// must return false rather than crashing.
1493+// ============================================================================
1494+ 
1495+HWTEST_F_L0(RawHeapTranslateTest, TranslateRawheap_BadMagic_ReturnsFalse)
1496+{
1497+ // Write a file with wrong magic/version - parser should reject and return false
1498+ std::string badPath = "/tmp/bad_magic_test.rawheap";
1499+ std::ofstream ofs(badPath, std::ios::binary);
1500+ // Write 8 zero bytes instead of valid version string
1501+ for (int i = 0; i < 33; i++) { // minimum header size
1502+ ofs.put(0x00);
1503+ }
1504+ ofs.close();
1505+ 
1506+ std::string outPath = badPath + ".heapsnapshot";
1507+ bool result = rawheap_translate::RawHeap::TranslateRawheap(badPath, outPath);
1508+ EXPECT_FALSE(result) << "TranslateRawheap should return false for bad magic";
1509+ 
1510+ std::remove(badPath.c_str());
1511+ std::remove(outPath.c_str());
1512+}
1513+ 
1514+HWTEST_F_L0(RawHeapTranslateTest, TranslateRawheap_TruncatedFile_ReturnsFalse)
1515+{
1516+ // Write a truncated file - header starts but body is incomplete
1517+ std::string truncPath = "/tmp/truncated_test.rawheap";
1518+ std::ofstream ofs(truncPath, std::ios::binary);
1519+ // Write version string "3.0.0\0\0\0" (valid static header start)
1520+ ofs.write("3.0.0\0\0\0", 8);
1521+ // Write partial identifierSize (2 bytes instead of 4)
1522+ ofs.put(0x04);
1523+ ofs.put(0x00);
1524+ ofs.close(); // truncated before header is complete
1525+ 
1526+ std::string outPath = truncPath + ".heapsnapshot";
1527+ bool result = rawheap_translate::RawHeap::TranslateRawheap(truncPath, outPath);
1528+ EXPECT_FALSE(result) << "TranslateRawheap should return false for truncated file";
1529+ 
1530+ std::remove(truncPath.c_str());
1531+ std::remove(outPath.c_str());
1532+}
1533+ 
1534+HWTEST_F_L0(RawHeapTranslateTest, TranslateRawheap_TwoFileMergeFails_ReturnsFalse)
1535+{
1536+ // Two-file merge with invalid dynamic file should return false
1537+ // Write a valid-looking static file and an invalid dynamic file
1538+ std::string staticPath = "/tmp/static_merge_test.rawheap";
1539+ std::string dynamicPath = "/tmp/dynamic_merge_test.rawheap";
1540+ std::string outPath = "/tmp/merge_output.heapsnapshot";
1541+ 
1542+ // Static file: minimal valid header
1543+ std::ofstream staticOfs(staticPath, std::ios::binary);
1544+ staticOfs.write("3.0.0\0\0\0", 8); // version
1545+ uint32_t idSize = 4;
1546+ staticOfs.write(reinterpret_cast<const char*>(&idSize), 4);
1547+ uint64_t timestamp = 1000000;
1548+ staticOfs.write(reinterpret_cast<const char*>(&timestamp), 8);
1549+ uint8_t language = 1; // STATIC
1550+ staticOfs.put(language);
1551+ uint32_t headerSize = 33;
1552+ staticOfs.write(reinterpret_cast<const char*>(&headerSize), 4);
1553+ uint32_t recordCount = 0;
1554+ staticOfs.write(reinterpret_cast<const char*>(&recordCount), 4);
1555+ uint32_t flags = 0;
1556+ staticOfs.write(reinterpret_cast<const char*>(&flags), 4);
1557+ staticOfs.close();
1558+ 
1559+ // Dynamic file: garbage (not V1/V2 format)
1560+ std::ofstream dynamicOfs(dynamicPath, std::ios::binary);
1561+ dynamicOfs.write("GARBAGE", 7);
1562+ dynamicOfs.close();
1563+ 
1564+ bool result = rawheap_translate::RawHeap::TranslateRawheap(dynamicPath, staticPath, outPath);
1565+ EXPECT_FALSE(result) << "Two-file merge with invalid dynamic should return false";
1566+ 
1567+ std::remove(staticPath.c_str());
1568+ std::remove(dynamicPath.c_str());
1569+ std::remove(outPath.c_str());
1570+}
1571+ 
1572+// ============================================================================
1334} // namespace panda::test1573} // namespace panda::test
Mecmascript/mem/heap.cpp+135-38
@@ -56,6 +56,14 @@
56#include "syspara/parameter.h"56#include "syspara/parameter.h"
57#endif57#endif
58 58 
59+#ifdef PANDA_JS_ETS_HYBRID_MODE
60+#include <unistd.h>
61+ 
62+#include "ecmascript/cross_vm/cross_vm_operator.h"
63+#include "ecmascript/dfx/hprof/hybrid/hybrid_heap_profiler.h"
64+#include "libpandabase/utils/time.h"
65+#endif
66+ 
59#if defined(ECMASCRIPT_SUPPORT_SNAPSHOT) && defined(PANDA_TARGET_OHOS) && defined(ENABLE_HISYSEVENT)67#if defined(ECMASCRIPT_SUPPORT_SNAPSHOT) && defined(PANDA_TARGET_OHOS) && defined(ENABLE_HISYSEVENT)
60#include "parameters.h"68#include "parameters.h"
61#include "hisysevent.h"69#include "hisysevent.h"
@@ -72,6 +80,54 @@ static bool g_futVersion = OHOS::system::GetIntParameter("const.product.dfx.fans
72 80 
73namespace panda::ecmascript {81namespace panda::ecmascript {
74RWLock BaseHeap::gcExclusiveRWLock_;82RWLock BaseHeap::gcExclusiveRWLock_;
83+ 
84+#ifdef PANDA_JS_ETS_HYBRID_MODE
85+using common::dump::DumpExecutionMode;
86+using common::dump::DumpReason;
87+using common::dump::DumpRequest;
88+using common::dump::DumpScope;
89+ 
90+/**
91+ * @brief Execute a hybrid OOM binary dump.
92+ *
93+ * The caller selects this path only for a hybrid runtime. Once selected, the
94+ * OOM is fully handled here and must not fall back to the legacy dynamic-only
95+ * dump, even if one participant fails.
96+ */
97+static void DumpHybridHeapSnapshotBeforeOOM(EcmaVM *vm, DumpReason reason, bool isProcDump,
98+ const std::string &spaceType, const std::string &heapType)
99+{
100+ LOG_ECMA(INFO) << "[HybDump][Dyn] OOM dump begin: scope=" << (isProcDump ? "process" : "vm");
101+ DumpRequest request;
102+ request.reason = reason;
103+ request.oom.spaceType = spaceType;
104+ request.oom.heapType = heapType;
105+ request.policy.triggerGC = false;
106+ request.policy.scope = isProcDump ? DumpScope::PROCESS : DumpScope::VM;
107+ request.policy.executionMode = DumpExecutionMode::IN_PROCESS;
108+ request.identity = {static_cast<int32_t>(getpid()), static_cast<int32_t>(JSThread::GetCurrentThreadId()),
109+ panda::time::GetCurrentTimeInMillis(true)};
110+ auto *crossVMOperator = vm == nullptr ? nullptr : vm->GetCrossVMOperator();
111+ auto *ecmaInterface = crossVMOperator == nullptr ? nullptr : crossVMOperator->GetEcmaVMInterface();
112+ if (ecmaInterface == nullptr) {
113+ LOG_ECMA(ERROR) << "[HybDump][Dyn] OOM dump failed: dynamic interface unavailable";
114+ return;
115+ }
116+ auto *stsInterface = Runtime::GetInstance()->GetSTSVMInterface();
117+ if (stsInterface == nullptr) {
118+ LOG_ECMA(ERROR) << "[HybDump][Dyn] OOM dump failed: static interface unavailable";
119+ return;
120+ }
121+ bool dumpStaticHeap = stsInterface->IsCurrentThreadAttached();
122+ if (!dumpStaticHeap) {
123+ LOG_ECMA(INFO) << "[HybDump][Dyn] Static dump skipped: current thread is not attached to static VM";
124+ }
125+ if (!stsInterface->ExecuteHeapDump(request, ecmaInterface, dumpStaticHeap)) {
126+ LOG_ECMA(ERROR) << "[HybDump][Dyn] OOM dump failed";
127+ }
128+}
129+#endif // PANDA_JS_ETS_HYBRID_MODE
130+ 
75SharedHeap *SharedHeap::instance_ = nullptr;131SharedHeap *SharedHeap::instance_ = nullptr;
76 132 
77void SharedHeap::CreateNewInstance()133void SharedHeap::CreateNewInstance()
@@ -1029,6 +1085,10 @@ void SharedHeap::DumpHeapSnapshotBeforeOOM([[maybe_unused]] JSThread *thread,
1029 [[maybe_unused]] const std::string &heapType)1085 [[maybe_unused]] const std::string &heapType)
1030{1086{
1031#if defined(ECMASCRIPT_SUPPORT_SNAPSHOT) && defined(ENABLE_DUMP_IN_FAULTLOG)1087#if defined(ECMASCRIPT_SUPPORT_SNAPSHOT) && defined(ENABLE_DUMP_IN_FAULTLOG)
1088+ if (!HeapProfilerInterface::TryStartOOMDump()) {
1089+ LOG_ECMA(INFO) << "SharedHeap::DumpHeapSnapshotBeforeOOM, OOM dump already triggered.";
1090+ return;
1091+ }
1032 AppFreezeFilterCallback appfreezeCallback = Runtime::GetInstance()->GetAppFreezeFilterCallback();1092 AppFreezeFilterCallback appfreezeCallback = Runtime::GetInstance()->GetAppFreezeFilterCallback();
1033 std::string eventConfig;1093 std::string eventConfig;
1034 bool shouldDump = (appfreezeCallback == nullptr || appfreezeCallback(getprocpid(), true, eventConfig));1094 bool shouldDump = (appfreezeCallback == nullptr || appfreezeCallback(getprocpid(), true, eventConfig));
@@ -1047,6 +1107,16 @@ void SharedHeap::DumpHeapSnapshotBeforeOOM([[maybe_unused]] JSThread *thread,
1047#endif1107#endif
1048#if defined(ECMASCRIPT_SUPPORT_SNAPSHOT)1108#if defined(ECMASCRIPT_SUPPORT_SNAPSHOT)
1049#if defined(ENABLE_DUMP_IN_FAULTLOG)1109#if defined(ENABLE_DUMP_IN_FAULTLOG)
1110+#ifdef PANDA_JS_ETS_HYBRID_MODE
1111+ auto *runtime = Runtime::GetInstance();
1112+ if (runtime->IsHybridVm()) {
1113+ const auto dumpReason = source == SharedHeapOOMSource::SHARED_GC ? DumpReason::DYNAMIC_SHARED_GC_OOM
1114+ : DumpReason::DYNAMIC_SHARED_OOM;
1115+ DumpHybridHeapSnapshotBeforeOOM(vm, dumpReason, runtime->IsEnableProcDumpInSharedOOM(), spaceType,
1116+ heapType);
1117+ return;
1118+ }
1119+#endif // PANDA_JS_ETS_HYBRID_MODE
1050 HeapProfilerInterface *heapProfile = nullptr;1120 HeapProfilerInterface *heapProfile = nullptr;
1051 if (source == SharedHeapOOMSource::SHARED_GC) {1121 if (source == SharedHeapOOMSource::SHARED_GC) {
1052#ifndef NDEBUG1122#ifndef NDEBUG
@@ -2236,6 +2306,10 @@ void Heap::DumpHeapSnapshotBeforeOOM(bool isProcDump, [[maybe_unused]] const std
2236 [[maybe_unused]] const std::string &heapType)2306 [[maybe_unused]] const std::string &heapType)
2237{2307{
2238#if defined(ECMASCRIPT_SUPPORT_SNAPSHOT) && defined(ENABLE_DUMP_IN_FAULTLOG)2308#if defined(ECMASCRIPT_SUPPORT_SNAPSHOT) && defined(ENABLE_DUMP_IN_FAULTLOG)
2309+ if (!HeapProfilerInterface::TryStartOOMDump()) {
2310+ LOG_ECMA(INFO) << "Heap::DumpHeapSnapshotBeforeOOM, OOM dump already triggered.";
2311+ return;
2312+ }
2239 AppFreezeFilterCallback appfreezeCallback = Runtime::GetInstance()->GetAppFreezeFilterCallback();2313 AppFreezeFilterCallback appfreezeCallback = Runtime::GetInstance()->GetAppFreezeFilterCallback();
2240 std::string eventConfig;2314 std::string eventConfig;
2241 bool shouldDump = (appfreezeCallback == nullptr || appfreezeCallback(getprocpid(), true, eventConfig));2315 bool shouldDump = (appfreezeCallback == nullptr || appfreezeCallback(getprocpid(), true, eventConfig));
@@ -2253,6 +2327,14 @@ void Heap::DumpHeapSnapshotBeforeOOM(bool isProcDump, [[maybe_unused]] const std
2253#endif2327#endif
2254#if defined(ECMASCRIPT_SUPPORT_SNAPSHOT)2328#if defined(ECMASCRIPT_SUPPORT_SNAPSHOT)
2255#if defined(ENABLE_DUMP_IN_FAULTLOG)2329#if defined(ENABLE_DUMP_IN_FAULTLOG)
2330+#ifdef PANDA_JS_ETS_HYBRID_MODE
2331+ if (Runtime::GetInstance()->IsHybridVm()) {
2332+ DumpHybridHeapSnapshotBeforeOOM(GetEcmaVM(), DumpReason::DYNAMIC_LOCAL_OOM, isProcDump, spaceType,
2333+ heapType);
2334+ hasOOMDump_ = true;
2335+ return;
2336+ }
2337+#endif // PANDA_JS_ETS_HYBRID_MODE
2256 if (ecmaVm_->GetHeapProfile() != nullptr) {2338 if (ecmaVm_->GetHeapProfile() != nullptr) {
2257 LOG_ECMA(ERROR) << "Heap::DumpHeapSnapshotBeforeOOM, HeapProfile is nullptr";2339 LOG_ECMA(ERROR) << "Heap::DumpHeapSnapshotBeforeOOM, HeapProfile is nullptr";
2258 return;2340 return;
@@ -3553,47 +3635,62 @@ void Heap::SetJsDumpThresholds(size_t thresholds) const
3553void Heap::ThresholdReachedDump()3635void Heap::ThresholdReachedDump()
3554{3636{
3555 size_t limitSize = GetHeapLimitSize();3637 size_t limitSize = GetHeapLimitSize();
3556- if (!limitSize) {3638+ if (limitSize == 0) {
3557- LOG_GC(INFO) << "ThresholdReachedDump limitSize is invaild";3639+ LOG_GC(INFO) << "ThresholdReachedDump limit size is invalid";
3558 return;3640 return;
3559 }3641 }
3560- size_t nowPrecent = GetHeapObjectSize() * DEC_TO_INT / limitSize;3642+ 
3561- if (g_debugLeak || (nowPrecent >= g_threshold && (g_lastHeapDumpTime == 0 ||3643+ size_t currentPercent = GetHeapObjectSize() * DEC_TO_INT / limitSize;
3562- GetCurrentTickMillseconds() - g_lastHeapDumpTime > HEAP_DUMP_REPORT_INTERVAL))) {3644+ uint64_t currentTime = GetCurrentTickMillseconds();
3563- size_t liveObjectSize = GetLiveObjectSize();3645+ bool reportIntervalElapsed =
3564- size_t nowPrecentRecheck = liveObjectSize * DEC_TO_INT / limitSize;3646+ g_lastHeapDumpTime == 0 || currentTime - g_lastHeapDumpTime > HEAP_DUMP_REPORT_INTERVAL;
3565- LOG_GC(INFO) << "ThresholdReachedDump nowPrecentCheck is " << nowPrecentRecheck;3647+ if (!g_debugLeak && (currentPercent < g_threshold || !reportIntervalElapsed)) {
3566- if (nowPrecentRecheck < g_threshold) {3648+ return;
3567- return;3649+ }
3568- }3650+ 
3569- g_lastHeapDumpTime = GetCurrentTickMillseconds();3651+ size_t liveObjectSize = GetLiveObjectSize();
3570- base::BlockHookScope blockScope;3652+ size_t liveObjectPercent = liveObjectSize * DEC_TO_INT / limitSize;
3571- HeapProfilerInterface *heapProfile = HeapProfilerInterface::GetInstance(ecmaVm_);3653+ LOG_GC(INFO) << "ThresholdReachedDump live object percent is " << liveObjectPercent;
3572- AppFreezeFilterCallback appfreezeCallback = Runtime::GetInstance()->GetAppFreezeFilterCallback();3654+ if (liveObjectPercent < g_threshold) {
3573- std::string eventConfig;3655+ return;
3574- bool shouldDump = (appfreezeCallback == nullptr || appfreezeCallback(getprocpid(), true, eventConfig));3656+ }
3575- GetEcmaGCKeyStats()->SendSysEventBeforeDump("thresholdReachedDump",3657+ 
3576- GetHeapLimitSize(), GetLiveObjectSize(), eventConfig,3658+ g_lastHeapDumpTime = currentTime;
3577- "", 0, LOCAL_HEAP_STR);3659+ base::BlockHookScope blockScope;
3578- if (shouldDump) {3660+ AppFreezeFilterCallback appfreezeCallback = Runtime::GetInstance()->GetAppFreezeFilterCallback();
3579- LOG_ECMA(INFO) << "ThresholdReachedDump and avoid freeze success.";3661+ std::string eventConfig;
3580- } else {3662+ bool shouldDump = appfreezeCallback == nullptr || appfreezeCallback(getprocpid(), true, eventConfig);
3581- LOG_ECMA(WARN) << "ThresholdReachedDump but avoid freeze failed.";3663+ GetEcmaGCKeyStats()->SendSysEventBeforeDump("thresholdReachedDump", GetHeapLimitSize(), liveObjectSize,
3582- return;3664+ eventConfig, "", 0, LOCAL_HEAP_STR);
3583- }3665+ if (!shouldDump) {
3584- DumpSnapShotOption dumpOption;3666+ LOG_ECMA(WARN) << "ThresholdReachedDump but avoid freeze failed.";
3585- dumpOption.dumpFormat = DumpFormat::BINARY;3667+ return;
3586- dumpOption.isVmMode = true;3668+ }
3587- dumpOption.isPrivate = false;3669+ LOG_ECMA(INFO) << "ThresholdReachedDump and avoid freeze success.";
3588- dumpOption.captureNumericValue = false;3670+ 
3589- dumpOption.isFullGC = false;3671+ DumpSnapShotOption dumpOption;
3590- dumpOption.isSimplify = true;3672+ dumpOption.dumpFormat = DumpFormat::BINARY;
3591- dumpOption.isSync = false;3673+ dumpOption.isVmMode = true;
3592- dumpOption.isBeforeFill = false;3674+ dumpOption.isPrivate = false;
3593- heapProfile->DumpHeapSnapshotForOOM(dumpOption);3675+ dumpOption.captureNumericValue = false;
3594- hasOOMDump_ = false;3676+ dumpOption.isFullGC = false;
3595- HeapProfilerInterface::Destroy(ecmaVm_);3677+ dumpOption.isSimplify = true;
3678+ dumpOption.isSync = false;
3679+ dumpOption.isBeforeFill = false;
3680+#ifdef PANDA_JS_ETS_HYBRID_MODE
3681+ if (Runtime::GetInstance()->IsHybridVm()) {
3682+ auto *hybridProfiler = HybridHeapProfiler::GetInstance();
3683+ if (hybridProfiler == nullptr || !hybridProfiler->BinaryDump(ecmaVm_, dumpOption)) {
3684+ LOG_ECMA(ERROR) << "[HybDump][Dyn] Threshold dump failed";
3596 }3685 }
3686+ hasOOMDump_ = false;
3687+ return;
3688+ }
3689+#endif
3690+ HeapProfilerInterface *heapProfile = HeapProfilerInterface::GetInstance(ecmaVm_);
3691+ heapProfile->DumpHeapSnapshotForOOM(dumpOption);
3692+ hasOOMDump_ = false;
3693+ HeapProfilerInterface::Destroy(ecmaVm_);
3597}3694}
3598#endif3695#endif
3599 3696 
Mecmascript/napi/dfx_jsnapi.cpp+20-8
@@ -84,6 +84,13 @@ void DFXJSNApi::DumpHeapSnapshot([[maybe_unused]] const EcmaVM *vm, [[maybe_unus
84 const std::function<void(uint8_t)> &callback)84 const std::function<void(uint8_t)> &callback)
85{85{
86#if defined(ECMASCRIPT_SUPPORT_SNAPSHOT)86#if defined(ECMASCRIPT_SUPPORT_SNAPSHOT)
87+ if (dumpOption.languageEnv != LanguageEnv::DYNAMIC && dumpOption.dumpFormat == DumpFormat::BINARY) {
88+ LOG_ECMA(ERROR) << "DumpHeapSnapshot: hybrid binary dump does not support an external path";
89+ if (callback) {
90+ callback(static_cast<uint8_t>(ecmascript::DumpHeapSnapshotStatus::FORK_FAILED));
91+ }
92+ return;
93+ }
87 FileStream stream(path);94 FileStream stream(path);
88 if (dumpOption.languageEnv == LanguageEnv::DYNAMIC) {95 if (dumpOption.languageEnv == LanguageEnv::DYNAMIC) {
89 DumpHeapSnapshot(vm, &stream, dumpOption, nullptr, callback);96 DumpHeapSnapshot(vm, &stream, dumpOption, nullptr, callback);
@@ -291,8 +298,8 @@ bool DFXJSNApi::PerformHybridHeapDump([[maybe_unused]] const EcmaVM *vm,
291 return false;298 return false;
292 }299 }
293 if (dumpOption.dumpFormat == DumpFormat::BINARY) {300 if (dumpOption.dumpFormat == DumpFormat::BINARY) {
294- LOG_ECMA(ERROR) << "PerformHybridHeapDump: hybrid binary dump is not supported yet";301+ return hybridProfiler->BinaryDump(const_cast<EcmaVM *>(vm),
295- return false;302+ const_cast<DumpSnapShotOption &>(dumpOption));
296 }303 }
297 return hybridProfiler->Dump(const_cast<EcmaVM *>(vm), nullptr,304 return hybridProfiler->Dump(const_cast<EcmaVM *>(vm), nullptr,
298 const_cast<DumpSnapShotOption &>(dumpOption));305 const_cast<DumpSnapShotOption &>(dumpOption));
@@ -328,12 +335,15 @@ void DFXJSNApi::ScheduleHybridHeapDump([[maybe_unused]] const EcmaVM *vm,
328 uint32_t mainTid = vm->GetTid();335 uint32_t mainTid = vm->GetTid();
329 336 
330 if (tid == 0) {337 if (tid == 0) {
331- // Dump all: main -> hybrid, workers -> dynamic338+ // VM scope: main -> hybrid, workers -> dynamic. Process scope already
339+ // includes every dynamic VM and the single static heap in one dump.
332 ScheduleHybridDumpOnLoop(vm, dumpOption, mainTid);340 ScheduleHybridDumpOnLoop(vm, dumpOption, mainTid);
333- DumpSnapShotOption dynamicOption = dumpOption;341+ if (!dumpOption.isProcDump) {
334- const_cast<EcmaVM *>(vm)->EnumerateWorkerVm([&](const EcmaVM *workerVm) {342+ DumpSnapShotOption dynamicOption = dumpOption;
335- DumpHeapSnapshotWithVm(workerVm, dynamicOption, workerVm->GetTid());343+ const_cast<EcmaVM *>(vm)->EnumerateWorkerVm([&](const EcmaVM *workerVm) {
336- });344+ DumpHeapSnapshotWithVm(workerVm, dynamicOption, workerVm->GetTid());
345+ });
346+ }
337 } else if (tid == mainTid) {347 } else if (tid == mainTid) {
338 // Dump main thread: always hybrid348 // Dump main thread: always hybrid
339 ScheduleHybridDumpOnLoop(vm, dumpOption, mainTid);349 ScheduleHybridDumpOnLoop(vm, dumpOption, mainTid);
@@ -377,7 +387,9 @@ void DFXJSNApi::ScheduleHybridDumpOnLoop([[maybe_unused]] const EcmaVM *vm,
377 int ret = uv_queue_work(loop, work, [](uv_work_t *) {},387 int ret = uv_queue_work(loop, work, [](uv_work_t *) {},
378 [](uv_work_t *work, int32_t) {388 [](uv_work_t *work, int32_t) {
379 struct DumpForSnapShotStruct *dump = static_cast<struct DumpForSnapShotStruct *>(work->data);389 struct DumpForSnapShotStruct *dump = static_cast<struct DumpForSnapShotStruct *>(work->data);
380- // tid matches this VM hybrid/dynamic dump; tid mismatch → static-only dump390+ // A matching VM identifies the dynamic participant. Whether the
391+ // current worker also has a static participant can only be decided
392+ // here, on that worker's event-loop thread.
381 const EcmaVM *targetVm = (dump->vm->GetTid() == dump->tid) ? dump->vm : nullptr;393 const EcmaVM *targetVm = (dump->vm->GetTid() == dump->tid) ? dump->vm : nullptr;
382 PerformHybridHeapDump(targetVm, dump->dumpOption);394 PerformHybridHeapDump(targetVm, dump->dumpOption);
383 delete dump;395 delete dump;
Mecmascript/napi/include/dfx_jsnapi.h+1-1
@@ -229,4 +229,4 @@ public:
229 Local<StringRef> moduleName, const std::string &failureInfo);229 Local<StringRef> moduleName, const std::string &failureInfo);
230};230};
231}231}
232-#endif232+#endif
Mecmascript/napi/test/dfx_jsnapi_tests.cpp+66-0
@@ -30,6 +30,8 @@
30#include <chrono>30#include <chrono>
31#include <csignal>31#include <csignal>
32#include <cstdio>32#include <cstdio>
33+#include <fstream>
34+#include <iterator>
33#include <thread>35#include <thread>
34#include <vector>36#include <vector>
35 37 
@@ -1581,6 +1583,8 @@ HWTEST_F_L0(DFXJSNApiTests, PerformHybridHeapDump_DumpOption)
1581 1583 
1582HWTEST_F_L0(DFXJSNApiTests, PerformHybridHeapDump_WithStream)1584HWTEST_F_L0(DFXJSNApiTests, PerformHybridHeapDump_WithStream)
1583{1585{
1586+ // Without an attached STS runtime, the stream path fails for any format.
1587+ // BINARY serialization bytes are covered in arkplatform tests.
1584 const std::string filePath = "DFXJSNApiTests_perform_stream.heapsnapshot";1588 const std::string filePath = "DFXJSNApiTests_perform_stream.heapsnapshot";
1585 ASSERT_TRUE(CreateEmptyFile(filePath));1589 ASSERT_TRUE(CreateEmptyFile(filePath));
1586 ecmascript::FileStream stream(filePath);1590 ecmascript::FileStream stream(filePath);
@@ -1921,4 +1925,66 @@ HWTEST_F_L0(DFXJSNApiTests, GetHandleNodeIdMap_Perf_100k)
1921#endif1925#endif
1922}1926}
1923 1927 
1928+HWTEST_F_L0(DFXJSNApiTests, PerformHybridHeapDump_BinaryFormat_ReturnsFalseWithoutSTS)
1929+{
1930+ // When DumpFormat::BINARY and no STS runtime, PerformHybridHeapDump
1931+ // should route to HybridHeapProfiler::BinaryDump which returns false.
1932+ DumpSnapShotOption dumpOption;
1933+ dumpOption.dumpFormat = ecmascript::DumpFormat::BINARY;
1934+ bool result = DFXJSNApi::PerformHybridHeapDump(vm_, dumpOption);
1935+ EXPECT_FALSE(result) << "BINARY format hybrid dump should fail without an attached STS runtime";
1936+}
1937+ 
1938+HWTEST_F_L0(DFXJSNApiTests, PerformHybridHeapDump_BinaryFormatWithStream_ReturnsFalse)
1939+{
1940+ // When DumpFormat::BINARY with stream, should return false
1941+ // (hybrid binary dump with external stream is not supported yet)
1942+ const std::string filePath = "DFXJSNApiTests_binary_stream.heapsnapshot";
1943+ ASSERT_TRUE(CreateEmptyFile(filePath));
1944+ ecmascript::FileStream stream(filePath);
1945+ 
1946+ DumpSnapShotOption dumpOption;
1947+ dumpOption.dumpFormat = ecmascript::DumpFormat::BINARY;
1948+ bool result = DFXJSNApi::PerformHybridHeapDump(vm_, &stream, dumpOption);
1949+ EXPECT_FALSE(result) << "BINARY format hybrid dump with stream should return false";
1950+ EXPECT_TRUE(IsEmptyFile(filePath));
1951+ std::remove(filePath.c_str());
1952+}
1953+ 
1954+HWTEST_F_L0(DFXJSNApiTests, DumpHeapSnapshot_HybridBinaryPathPreservesExistingFile)
1955+{
1956+ const std::string filePath = "DFXJSNApiTests_binary_path.rawheap";
1957+ const std::string originalContent = "existing heap dump";
1958+ {
1959+ std::ofstream output(filePath, std::ios::binary | std::ios::trunc);
1960+ ASSERT_TRUE(output.is_open());
1961+ output.write(originalContent.data(), originalContent.size());
1962+ }
1963+ 
1964+ DumpSnapShotOption dumpOption;
1965+ dumpOption.dumpFormat = ecmascript::DumpFormat::BINARY;
1966+ dumpOption.languageEnv = ecmascript::LanguageEnv::HYBRID;
1967+ bool callbackCalled = false;
1968+ uint8_t callbackStatus = 0;
1969+ DFXJSNApi::DumpHeapSnapshot(vm_, filePath, dumpOption, [&](uint8_t status) {
1970+ callbackCalled = true;
1971+ callbackStatus = status;
1972+ });
1973+ 
1974+ std::ifstream input(filePath, std::ios::binary);
1975+ std::string actualContent((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
1976+ EXPECT_EQ(actualContent, originalContent);
1977+ EXPECT_TRUE(callbackCalled);
1978+ EXPECT_EQ(callbackStatus, static_cast<uint8_t>(ecmascript::DumpHeapSnapshotStatus::FORK_FAILED));
1979+ std::remove(filePath.c_str());
1980+}
1981+ 
1982+HWTEST_F_L0(DFXJSNApiTests, PerformHybridHeapDump_JSONFormat_StillFailsWithoutSTS)
1983+{
1984+ // JSON format should still fail without STS (same as before)
1985+ DumpSnapShotOption dumpOption;
1986+ dumpOption.dumpFormat = ecmascript::DumpFormat::JSON;
1987+ bool result = DFXJSNApi::PerformHybridHeapDump(vm_, dumpOption);
1988+ EXPECT_FALSE(result) << "JSON format hybrid dump should still fail without STS runtime";
1989+}
1924} // namespace panda::test1990} // namespace panda::test
Mtest/resource/js_runtime/ohos_test.xml+5-0
@@ -1971,6 +1971,11 @@
1971 <option name="push" value="arkcompiler/ets_runtime/libark_jsruntime_test.so -> /data/test" src="out"/>1971 <option name="push" value="arkcompiler/ets_runtime/libark_jsruntime_test.so -> /data/test" src="out"/>
1972 </preparer>1972 </preparer>
1973 </target>1973 </target>
1974+ <target name="RawHeapStaticSnapshotTest">
1975+ <preparer>
1976+ <option name="push" value="arkcompiler/ets_runtime/libark_jsruntime_test.so -> /data/test" src="out"/>
1977+ </preparer>
1978+ </target>
1974 <target name="LocalHandleLeakDetectTest">1979 <target name="LocalHandleLeakDetectTest">
1975 <preparer>1980 <preparer>
1976 <option name="push" value="arkcompiler/ets_runtime/libark_jsruntime_test.so -> /data/test" src="out"/>1981 <option name="push" value="arkcompiler/ets_runtime/libark_jsruntime_test.so -> /data/test" src="out"/>