| @@ -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 | ] |
| @@ -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": { |
| @@ -19,6 +19,9 @@ | |||
| 19 | 19 | ||
| 20 | 20 | ||
| 21 | 21 | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + | ||
| 22 | 25 | ||
| 23 | 26 | ||
| 24 | 27 | ||
| @@ -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::ecmascript | 255 | + |
| 256 | +std::unique_ptr<AbstractDumper> CrossVMOperator::EcmaVMInterfaceImpl::CreateHeapDumper(const DumpRequest &request) | ||
| 257 | +{ | ||
| 258 | + | ||
| 259 | + return DynamicDump::Create(vm_, request); | ||
| 260 | + | ||
| 261 | + (void)request; | ||
| 262 | + return nullptr; | ||
| 263 | + | ||
| 264 | +} | ||
| 265 | +} // namespace panda::ecmascript | ||
| @@ -23,9 +23,13 @@ | |||
| 23 | 23 | ||
| 24 | 24 | ||
| 25 | 25 | ||
| 26 | + | ||
| 26 | 27 | ||
| 27 | namespace panda::ecmascript { | 28 | namespace panda::ecmascript { |
| 28 | 29 | ||
| 30 | +using common::dump::AbstractDumper; | ||
| 31 | +using common::dump::DumpRequest; | ||
| 32 | + | ||
| 29 | using JSTaggedType = uint64_t; | 33 | using JSTaggedType = uint64_t; |
| 30 | class EcmaVM; | 34 | class 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::ecmascript | 92 | } // namespace panda::ecmascript |
| 88 | 93 | ||
| 89 | -#endif // ETS_RUNTIME_ECMASCRIPT_CROSS_VM_CROSS_VM_OPERATOR_H | 94 | +#endif // ETS_RUNTIME_ECMASCRIPT_CROSS_VM_CROSS_VM_OPERATOR_H |
| @@ -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 | ||
| @@ -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 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + | ||
| 25 | + | ||
| 26 | + | ||
| 27 | + | ||
| 28 | + | ||
| 29 | + | ||
| 30 | + | ||
| 31 | + | ||
| 32 | + | ||
| 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 | + | ||
| 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 | + | ||
| 295 | + int fd = -1; | ||
| 296 | + | ||
| 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 | ||
| @@ -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 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + | ||
| 25 | + | ||
| 26 | + | ||
| 27 | + | ||
| 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 | + | ||
| @@ -106,7 +106,7 @@ void FileDescriptorStream::EndOfStream() | |||
| 106 | 106 | ||
| 107 | bool FileDescriptorStream::Good() | 107 | bool FileDescriptorStream::Good() |
| 108 | { | 108 | { |
| 109 | - return fd_ > 0; | 109 | + return fd_ >= 0; |
| 110 | } | 110 | } |
| 111 | 111 | ||
| 112 | // Writes the chunk of data into the stream | 112 | // Writes the chunk of data into the stream |
| @@ -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 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 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 | ||
| @@ -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 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 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 | + | ||
| @@ -43,8 +43,6 @@ | |||
| 43 | 43 | ||
| 44 | namespace panda::ecmascript { | 44 | namespace panda::ecmascript { |
| 45 | 45 | ||
| 46 | -bool HeapProfiler::oomDumpActive_ = false; | ||
| 47 | - | ||
| 48 | std::pair<bool, NodeId> EntryIdMap::FindId(JSTaggedType addr) | 46 | std::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 | 185 | ||
| 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 | ||
| 223 | void HeapProfiler::DumpHeapSnapshotFromSharedGCForOOM(Stream *stream, const DumpSnapShotOption &dumpOption) | 221 | void 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 oom | 547 | // 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 | 958 | ||
| 984 | void HeapProfiler::DumpHandleLeakRecords() | 959 | void HeapProfiler::DumpHandleLeakRecords() |
| 985 | { | 960 | { |
| @@ -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() override | 142 | 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 | ||
| 170 | private: | 181 | private: |
| 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 heap | 183 | * trigger full gc to make sure no unreachable objects in heap |
| 174 | */ | 184 | */ |
| @@ -20,6 +20,8 @@ | |||
| 20 | 20 | ||
| 21 | namespace panda::ecmascript { | 21 | namespace panda::ecmascript { |
| 22 | 22 | ||
| 23 | +std::atomic<bool> HeapProfilerInterface::oomDumpActive_ {false}; | ||
| 24 | + | ||
| 23 | HeapProfilerInterface *HeapProfilerInterface::GetInstance(EcmaVM *vm) | 25 | HeapProfilerInterface *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 | + | ||
| 43 | void HeapProfilerInterface::DumpHeapSnapshotForCMCOOM(void *thread) | 56 | void HeapProfilerInterface::DumpHeapSnapshotForCMCOOM(void *thread) |
| 44 | { | 57 | { |
| 45 | 58 | ||
| 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(); |
| @@ -16,6 +16,7 @@ | |||
| 16 | 16 | ||
| 17 | 17 | ||
| 18 | 18 | ||
| 19 | + | ||
| 19 | 20 | ||
| 20 | 21 | ||
| 21 | 22 | ||
| @@ -46,6 +47,7 @@ struct DumpSnapShotOption { | |||
| 46 | bool isClearNodeIdCache = false; // whether clear node id map cache after dump | 47 | bool isClearNodeIdCache = false; // whether clear node id map cache after dump |
| 47 | bool dumpDynamicHeap = false; // whether to dump dynamic (ArkTS) heap | 48 | bool dumpDynamicHeap = false; // whether to dump dynamic (ArkTS) heap |
| 48 | bool dumpStaticHeap = false; // whether to dump static heap | 49 | 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 dump | 51 | std::string spaceType = ""; // space type for raw heap dump |
| 50 | std::string heapType = ""; // heap type: local heap or shared heap | 52 | std::string heapType = ""; // heap type: local heap or shared heap |
| 51 | LanguageEnv languageEnv = LanguageEnv::DYNAMIC; // language environment selection | 53 | 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::ecmascript | 114 | } // namespace panda::ecmascript |
| 99 | 115 | ||
| @@ -14,12 +14,14 @@ | |||
| 14 | */ | 14 | */ |
| 15 | 15 | ||
| 16 | 16 | ||
| 17 | + | ||
| 17 | 18 | ||
| 18 | 19 | ||
| 19 | 20 | ||
| 20 | 21 | ||
| 21 | 22 | ||
| 22 | 23 | ||
| 24 | + | ||
| 23 | 25 | ||
| 24 | 26 | ||
| 25 | 27 | ||
| @@ -30,11 +32,17 @@ | |||
| 30 | 32 | ||
| 31 | 33 | ||
| 32 | 34 | ||
| 35 | + | ||
| 33 | 36 | ||
| 34 | 37 | ||
| 35 | 38 | ||
| 36 | 39 | ||
| 37 | namespace panda::ecmascript { | 40 | namespace panda::ecmascript { |
| 41 | + | ||
| 42 | +using common::dump::DumpExecutionMode; | ||
| 43 | +using common::dump::DumpRequest; | ||
| 44 | +using common::dump::DumpScope; | ||
| 45 | + | ||
| 38 | HybridHeapProfiler *HybridHeapProfiler::GetInstance() | 46 | HybridHeapProfiler *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 | ||
| 134 | static void HybridWaitProcess(pid_t pid) | 142 | static 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 | 280 | ||
| 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 | + | ||
| 275 | bool HybridHeapProfiler::SetAppFreezeFilter() | 319 | bool HybridHeapProfiler::SetAppFreezeFilter() |
| 276 | { | 320 | { |
| 277 | AppFreezeFilterCallback callback = Runtime::GetInstance()->GetAppFreezeFilterCallback(); | 321 | AppFreezeFilterCallback callback = Runtime::GetInstance()->GetAppFreezeFilterCallback(); |
| @@ -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_; |
| @@ -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::ecmascript | 86 | } // namespace panda::ecmascript |
| @@ -150,22 +150,33 @@ void ObjectMarker::MarkRootObjects() | |||
| 150 | 150 | ||
| 151 | RawHeapDump::RawHeapDump(const EcmaVM *vm, Stream *stream, HeapSnapshot *snapshot, | 151 | RawHeapDump::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 | ||
| 159 | RawHeapDump::~RawHeapDump() | 159 | RawHeapDump::~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 | ||
| 229 | void RawHeapDump::IterateMarkedObjects(const std::function<void(JSTaggedType)> &visitor) | 243 | void RawHeapDump::IterateMarkedObjects(const std::function<void(JSTaggedType)> &visitor) |
| @@ -293,8 +307,8 @@ void RawHeapDump::DumpVersion(const std::string &version) | |||
| 293 | 307 | ||
| 294 | void RawHeapDump::DumpMetadataFields() | 308 | void RawHeapDump::DumpMetadataFields() |
| 295 | { | 309 | { |
| 296 | - DumpStringField(dumpOption_->spaceType, 32, "space type"); // 32: space type size | 310 | + DumpStringField(dumpOption_.spaceType, 32, "space type"); // 32: space type size |
| 297 | - DumpStringField(dumpOption_->heapType, 16, "heap type"); // 16: heap type size | 311 | + DumpStringField(dumpOption_.heapType, 16, "heap type"); // 16: heap type size |
| 298 | DumpStringField("dynamic", 8, "vm type"); // 8: vm type size | 312 | DumpStringField("dynamic", 8, "vm type"); // 8: vm type size |
| 299 | } | 313 | } |
| 300 | 314 | ||
| @@ -326,8 +340,10 @@ void RawHeapDump::DumpSectionIndex() | |||
| 326 | */ | 340 | */ |
| 327 | NodeId RawHeapDump::GenerateNodeId(JSTaggedType addr) | 341 | NodeId 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_t | 352 | 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 | + | ||
| 339 | void RawHeapDump::WriteChunk(char *data, size_t size) | 366 | void 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 | }); |
| @@ -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 EcmaModuleFilename | 211 | // 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_; |
| @@ -13,23 +13,42 @@ | |||
| 13 | 13 | ||
| 14 | import("//arkcompiler/ets_runtime/js_runtime_config.gni") | 14 | import("//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" |
| @@ -16,6 +16,7 @@ | |||
| 16 | 16 | ||
| 17 | 17 | ||
| 18 | 18 | ||
| 19 | + | ||
| 19 | 20 | ||
| 20 | 21 | ||
| 21 | 22 | ||
| @@ -27,11 +28,23 @@ using StringKey = size_t; | |||
| 27 | using StringId = uint32_t; | 28 | using StringId = uint32_t; |
| 28 | 29 | ||
| 29 | static constexpr NodeType DEFAULT_NODETYPE = 8; // 8: means default node type | 30 | static constexpr NodeType DEFAULT_NODETYPE = 8; // 8: means default node type |
| 31 | +static constexpr NodeType SYNTHETIC_NODETYPE = 9; // 9: means SYNTHETIC node type | ||
| 30 | static constexpr NodeType FRAMEWORK_NODETYPE = 14; | 32 | static constexpr NodeType FRAMEWORK_NODETYPE = 14; |
| 31 | static constexpr NodeType ROOT = 15; | 33 | static constexpr NodeType ROOT = 15; |
| 32 | static constexpr NodeType HEAP_NUMBER = 7; | 34 | static constexpr NodeType HEAP_NUMBER = 7; |
| 33 | static constexpr NodeType STRING = 2; | 35 | static 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 | ||
| 36 | static constexpr int VIRTUAL_NODE_SIZE = 1; // The virtual node size is fixed at 1 | 49 | static 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 | ||
| 114 | static constexpr uint8_t ZERO_VALUE = 0x02U; // 0000 0010 | 133 | static 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). |
| 127 | constexpr uint32_t GLOBAL_REF_TRACK_OFF_MARK = 0xFFFFFFFFU; | 146 | constexpr 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_translate | 257 | } // namespace rawheap_translate |
| 129 | 258 | ||
| @@ -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 | + | ||
| 20 | + | ||
| 21 | + | ||
| 19 | 22 | ||
| 20 | namespace rawheap_translate { | 23 | namespace 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 | + | ||
| 21 | std::string RAWHEAP_TRANSLATE_HELPER = | 36 | std::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::endl | 75 | + 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 | ||
| 74 | int Main(const int argc, const char **argv) | 83 | int 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_translate | 122 | } // namespace rawheap_translate |
| 123 | + | ||
| 124 | + | ||
| 87 | int main(int argc, const char **argv) | 125 | int 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 | + | ||
| @@ -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 | 17 | ||
| 18 | 18 | ||
| 19 | namespace rawheap_translate { | 19 | namespace rawheap_translate { |
| @@ -17,8 +17,8 @@ | |||
| 17 | 17 | ||
| 18 | 18 | ||
| 19 | 19 | ||
| 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 | ||
| 23 | namespace rawheap_translate { | 23 | namespace rawheap_translate { |
| 24 | class MetaParser { | 24 | class MetaParser { |
| @@ -13,11 +13,16 @@ | |||
| 13 | * limitations under the License. | 13 | * limitations under the License. |
| 14 | */ | 14 | */ |
| 15 | 15 | ||
| 16 | + | ||
| 16 | 17 | ||
| 18 | + | ||
| 17 | 19 | ||
| 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 | + | ||
| 24 | + | ||
| 25 | + | ||
| 21 | 26 | ||
| 22 | namespace rawheap_translate { | 27 | namespace rawheap_translate { |
| 23 | RawHeap::~RawHeap() | 28 | RawHeap::~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 | ||
| 38 | bool RawHeap::TranslateRawheap(const std::string &inputPath, const std::string &outputPath) | 39 | bool 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); |
| 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 | + | ||
| 221 | StringId RawHeap::InsertAndGetStringId(const std::string &str) | 384 | StringId 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 | - | ||
| 249 | - hashSet_.insert(hash); | ||
| 250 | - | ||
| 251 | } | 410 | } |
| 252 | 411 | ||
| 253 | void RawHeap::AddPrimitiveNodes() | 412 | void RawHeap::AddPrimitiveNodes() |
| @@ -271,7 +430,7 @@ void RawHeap::CreateRootNode(Node *root, const std::string &name, size_t count) | |||
| 271 | void RawHeap::CreateMetadataNode(Node *metadataNode) | 430 | void RawHeap::CreateMetadataNode(Node *metadataNode) |
| 272 | { | 431 | { |
| 273 | metadataNode->nodeId = 0; | 432 | metadataNode->nodeId = 0; |
| 274 | - metadataNode->type = 8; // 8 is native nodetype | 433 | + 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 node | 955 | syntheticRoot->nodeId = 1; // 1: means root node |
| 797 | - syntheticRoot->type = 9; // 9: means SYNTHETIC node type | 956 | + 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 | |||
| 1661 | void RawHeapTranslateV2::AddSyntheticRootNode(std::vector<uint32_t> &roots) | 1820 | void RawHeapTranslateV2::AddSyntheticRootNode(std::vector<uint32_t> &roots) |
| 1662 | { | 1821 | { |
| 1663 | syntheticRoot_->nodeId = 1; // 1: means root node | 1822 | syntheticRoot_->nodeId = 1; // 1: means root node |
| 1664 | - syntheticRoot_->type = 9; // 9: means SYNTHETIC node type | 1823 | + 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-"); |
| @@ -16,9 +16,11 @@ | |||
| 16 | 16 | ||
| 17 | 17 | ||
| 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 | + | ||
| 23 | + | ||
| 22 | 24 | ||
| 23 | namespace panda::test { | 25 | namespace panda::test { |
| 24 | class HeapDumpTestHelper; | 26 | class HeapDumpTestHelper; |
| @@ -27,6 +29,8 @@ class RawHeapTranslateV2TestHelper; | |||
| 27 | }; | 29 | }; |
| 28 | 30 | ||
| 29 | namespace rawheap_translate { | 31 | namespace rawheap_translate { |
| 32 | +class SnapshotMerger; | ||
| 33 | +class StaticRawheapTranslate; | ||
| 30 | class RawHeap { | 34 | class RawHeap { |
| 31 | public: | 35 | public: |
| 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 | ||
| 58 | protected: | 86 | protected: |
| 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> §ion); | 106 | static bool ReadSectionInfo(FileReader &file, uint32_t offset, std::vector<uint32_t> §ion); |
| 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_; |
中
建议:
![]() ![]() | |||
| 83 | uint32_t nodeIndex_ {0}; | 133 | uint32_t nodeIndex_ {0}; |
| 84 | 134 | ||
| 85 | - | ||
| 86 | - std::unordered_set<uint32_t> hashSet_ {}; | ||
| 87 | - | ||
| 88 | - | ||
| 89 | friend class panda::test::HeapDumpTestHelper; | 135 | friend class panda::test::HeapDumpTestHelper; |
| 136 | + friend class SnapshotMerger; | ||
| 90 | }; | 137 | }; |
| 91 | 138 | ||
| 92 | class RawHeapTranslateV1 : public RawHeap { | 139 | class 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_translate | 296 | } // namespace rawheap_translate |
| 250 | -#endif // RAWHEAP_TRANSLATE_H | 297 | +#endif // RAWHEAP_TRANSLATE_H |
| @@ -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 | + | ||
| 17 | 18 | ||
| 18 | namespace rawheap_translate { | 19 | namespace rawheap_translate { |
| 19 | bool StreamWriter::Initialize(const std::string &filePath) | 20 | bool 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 3 | 199 | // 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 unicode | 251 | + // 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); |
| @@ -17,8 +17,8 @@ | |||
| 17 | 17 | ||
| 18 | 18 | ||
| 19 | 19 | ||
| 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 | ||
| 24 | namespace rawheap_translate { | 24 | namespace rawheap_translate { |
| @@ -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 | + | ||
| 17 | + | ||
| 18 | + | ||
| 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) { | ||
This detects the all-unresolved XRef case but only logs it; ![]() ![]() | |||
| 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 | ||
| @@ -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 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 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 | + | ||
| @@ -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 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + | ||
| 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'); | ||
The record's declared ![]() ![]() | |||
| 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); | ||
Class mirror nodes never receive their instance size. ![]() ![]() | |||
| 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 | ||
| @@ -0,0 +1,289 @@ | |||
| 1 | +/* | ||
中
建议:
![]() ![]() yangxiaoshuai2022 7月28日 评论: 7月28日 评论: Petrov Igor 27 天前 评论: 27 天前 评论: 中
建议:
![]() ![]() | |||
| 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 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 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 | + | ||
| @@ -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 | ||
| 18 | namespace rawheap_translate { | 18 | namespace rawheap_translate { |
| 19 | std::string StringHashMap::GetStringByKey(StringKey key) const | 19 | std::string StringHashMap::GetStringByKey(StringKey key) const |
| @@ -27,6 +27,13 @@ std::string StringHashMap::GetStringByKey(StringKey key) const | |||
| 27 | 27 | ||
| 28 | StringKey StringHashMap::GetKeyByStringId(StringId stringId) const | 28 | StringKey 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 3 | 37 | return orderedKey_[stringId - CUSTOM_STRID_START]; // 3: index_ start from 3 |
| 31 | } | 38 | } |
| 32 | 39 | ||
| @@ -16,8 +16,8 @@ | |||
| 16 | 16 | ||
| 17 | 17 | ||
| 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 | ||
| 22 | namespace rawheap_translate { | 22 | namespace rawheap_translate { |
| 23 | // An Implementation for Native StringTable without Auto Mem-Management | 23 | // 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() const | 48 | + size_t GetCapacity() const |
| 49 | { | 49 | { |
| 50 | return orderedKey_.size(); | 50 | return orderedKey_.size(); |
| 51 | } | 51 | } |
| @@ -21,7 +21,8 @@ | |||
| 21 | 21 | ||
| 22 | 22 | ||
| 23 | 23 | ||
| 24 | -#include "ecmascript/dfx/hprof/rawheap_translate/utils.h" | 24 | +#include "securec.h" |
| 25 | + | ||
| 25 | 26 | ||
| 26 | namespace rawheap_translate { | 27 | namespace rawheap_translate { |
| 27 | bool RealPath(const std::string &filename, std::string &realpath) | 28 | bool 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 | + | ||
| 92 | bool EndsWith(const std::string &str, const std::string &suffix) | 110 | bool 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 | } |
| @@ -25,8 +25,8 @@ | |||
| 25 | 25 | ||
| 26 | 26 | ||
| 27 | 27 | ||
| 28 | - | ||
| 29 | 28 | ||
| 29 | + | ||
| 30 | 30 | ||
| 31 | namespace rawheap_translate { | 31 | namespace rawheap_translate { |
| 32 | 32 | ||
| @@ -42,6 +42,12 @@ bool RealPath(const std::string &filename, std::string &realpath); | |||
| 42 | 42 | ||
| 43 | bool GenerateDumpFileName(std::string &filename); | 43 | bool 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 | + | ||
| 45 | bool EndsWith(const std::string &str, const std::string &suffix); | 51 | bool EndsWith(const std::string &str, const std::string &suffix); |
| 46 | 52 | ||
| 47 | bool IsLittleEndian(); | 53 | bool 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 | ||
| 119 | private: | 125 | private: |
| 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 | ||
| 126 | class Version { | 132 | class 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 | + | ||
| 156 | private: | 166 | private: |
| 157 | int major_ {0}; | 167 | int major_ {0}; |
| 158 | int minor_ {0}; | 168 | int minor_ {0}; |
| @@ -73,12 +73,6 @@ host_unittest_action("HeapDumpTest") { | |||
| 73 | module_out_path = module_output_path | 73 | 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 file | 266 | # 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) { |
| @@ -12,6 +12,7 @@ | |||
| 12 | * See the License for the specific language governing permissions and | 12 | * See the License for the specific language governing permissions and |
| 13 | * limitations under the License. | 13 | * limitations under the License. |
| 14 | */ | 14 | */ |
| 15 | + | ||
| 15 | 16 | ||
| 16 | 17 | ||
| 17 | 18 | ||
| @@ -22,6 +23,7 @@ | |||
| 22 | 23 | ||
| 23 | 24 | ||
| 24 | 25 | ||
| 26 | + | ||
| 25 | 27 | ||
| 26 | 28 | ||
| 27 | 29 | ||
| @@ -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() override | 74 | 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 | + | ||
| 875 | class MockHeapProfiler : public HeapProfilerInterface { | 939 | class MockHeapProfiler : public HeapProfilerInterface { |
| 876 | public: | 940 | public: |
| 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 | + | ||
| 978 | HWTEST_F_L0(HeapDumpTest, TestAllocationEvent) | 1048 | HWTEST_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 | + | ||
| 1544 | HWTEST_F_L0(HeapDumpTest, TestHeapDumpBinaryDumpV0) | 1636 | HWTEST_F_L0(HeapDumpTest, TestHeapDumpBinaryDumpV0) |
| 1545 | { | 1637 | { |
| 1546 | ObjectFactory *factory = ecmaVm_->GetFactory(); | 1638 | ObjectFactory *factory = ecmaVm_->GetFactory(); |
| @@ -13,9 +13,15 @@ | |||
| 13 | * limitations under the License. | 13 | * limitations under the License. |
| 14 | */ | 14 | */ |
| 15 | 15 | ||
| 16 | + | ||
| 17 | + | ||
| 16 | 18 | ||
| 17 | - | ||
| 18 | 19 | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + | ||
| 19 | 25 | ||
| 20 | 26 | ||
| 21 | 27 | ||
| @@ -257,11 +263,45 @@ HWTEST_F_L0(HeapTrackerTest, GenDumpFileName_004) | |||
| 257 | HeapProfilerInterface::Destroy(instance); | 263 | HeapProfilerInterface::Destroy(instance); |
| 258 | } | 264 | } |
| 259 | 265 | ||
| 266 | + | ||
| 260 | HWTEST_F_L0(HeapTrackerTest, FileDescriptorStreamEndOfStream) | 267 | HWTEST_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::test | 280 | + |
| 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 | + | ||
| 307 | +} // namespace panda::test | ||
| @@ -13,6 +13,9 @@ | |||
| 13 | * limitations under the License. | 13 | * limitations under the License. |
| 14 | */ | 14 | */ |
| 15 | 15 | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 16 | 19 | ||
| 17 | 20 | ||
| 18 | 21 | ||
| @@ -22,7 +25,14 @@ | |||
| 22 | 25 | ||
| 23 | 26 | ||
| 24 | 27 | ||
| 28 | + | ||
| 25 | 29 | ||
| 30 | + | ||
| 31 | + | ||
| 32 | + | ||
| 33 | + | ||
| 34 | + | ||
| 35 | + | ||
| 26 | 36 | ||
| 27 | 37 | ||
| 28 | 38 | ||
| @@ -30,6 +40,10 @@ using namespace panda::ecmascript; | |||
| 30 | 40 | ||
| 31 | namespace panda::test { | 41 | namespace 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 methods | 48 | // 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) override | 68 | void MarkFromObject(void *obj) override |
| 51 | { | 69 | { |
| @@ -105,7 +123,7 @@ public: | |||
| 105 | bool TriggerXGCAndWait() override | 123 | bool TriggerXGCAndWait() override |
| 106 | { | 124 | { |
| 107 | xgcTriggered_ = true; | 125 | xgcTriggered_ = true; |
| 108 | - return true; | 126 | + return xgcResult_; |
| 109 | } | 127 | } |
| 110 | 128 | ||
| 111 | void EtsForceFullGC() override | 129 | 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() override | 199 | 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::ecmascript | 328 | } // 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 | // Fixtures | 480 | // Fixtures |
| 423 | // ============================================================================ | 481 | // ============================================================================ |
| @@ -429,6 +487,7 @@ public: | |||
| 429 | void SetUp() override | 487 | 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 tests | 574 | // 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 | + | ||
| 491 | HWTEST_F_L0(HybridHeapSnapshotTest, ProfilerBasicBehavior) | 663 | HWTEST_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::test | 1628 | } // namespace panda::test |
| @@ -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 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + | ||
| 25 | + | ||
| 26 | + | ||
| 27 | + | ||
| 28 | + | ||
| 29 | + | ||
| 30 | + | ||
| 31 | + | ||
| 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 ¶ms) | ||
| 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 ¶ms) | ||
| 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 ¶ms : 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 ¶ms) | ||
| 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 ¶ms, 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 | ||
| @@ -16,8 +16,21 @@ | |||
| 16 | 16 | ||
| 17 | 17 | ||
| 18 | 18 | ||
| 19 | + | ||
| 19 | 20 | ||
| 20 | 21 | ||
| 22 | + | ||
| 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 | + | ||
| 21 | using namespace panda::ecmascript; | 34 | using namespace panda::ecmascript; |
| 22 | 35 | ||
| 23 | namespace panda::test { | 36 | namespace 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*>(×tamp), 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::test | 1573 | } // namespace panda::test |
| @@ -56,6 +56,14 @@ | |||
| 56 | 56 | ||
| 57 | 57 | ||
| 58 | 58 | ||
| 59 | + | ||
| 60 | + | ||
| 61 | + | ||
| 62 | + | ||
| 63 | + | ||
| 64 | + | ||
| 65 | + | ||
| 66 | + | ||
| 59 | 67 | ||
| 60 | 68 | ||
| 61 | 69 | ||
| @@ -72,6 +80,54 @@ static bool g_futVersion = OHOS::system::GetIntParameter("const.product.dfx.fans | |||
| 72 | 80 | ||
| 73 | namespace panda::ecmascript { | 81 | namespace panda::ecmascript { |
| 74 | RWLock BaseHeap::gcExclusiveRWLock_; | 82 | RWLock BaseHeap::gcExclusiveRWLock_; |
| 83 | + | ||
| 84 | + | ||
| 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 | + | ||
| 130 | + | ||
| 75 | SharedHeap *SharedHeap::instance_ = nullptr; | 131 | SharedHeap *SharedHeap::instance_ = nullptr; |
| 76 | 132 | ||
| 77 | void SharedHeap::CreateNewInstance() | 133 | void 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 | 1087 | ||
| 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 | 1107 | ||
| 1048 | 1108 | ||
| 1049 | 1109 | ||
| 1110 | + | ||
| 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 | + | ||
| 1050 | HeapProfilerInterface *heapProfile = nullptr; | 1120 | HeapProfilerInterface *heapProfile = nullptr; |
| 1051 | if (source == SharedHeapOOMSource::SHARED_GC) { | 1121 | if (source == SharedHeapOOMSource::SHARED_GC) { |
| 1052 | 1122 | ||
| @@ -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 | 2308 | ||
| 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 | 2327 | ||
| 2254 | 2328 | ||
| 2255 | 2329 | ||
| 2330 | + | ||
| 2331 | + if (Runtime::GetInstance()->IsHybridVm()) { | ||
| 2332 | + DumpHybridHeapSnapshotBeforeOOM(GetEcmaVM(), DumpReason::DYNAMIC_LOCAL_OOM, isProcDump, spaceType, | ||
| 2333 | + heapType); | ||
| 2334 | + hasOOMDump_ = true; | ||
| 2335 | + return; | ||
| 2336 | + } | ||
| 2337 | + | ||
| 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 | |||
| 3553 | void Heap::ThresholdReachedDump() | 3635 | void 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 | + | ||
| 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 | + | ||
| 3690 | + HeapProfilerInterface *heapProfile = HeapProfilerInterface::GetInstance(ecmaVm_); | ||
| 3691 | + heapProfile->DumpHeapSnapshotForOOM(dumpOption); | ||
| 3692 | + hasOOMDump_ = false; | ||
| 3693 | + HeapProfilerInterface::Destroy(ecmaVm_); | ||
| 3597 | } | 3694 | } |
| 3598 | 3695 | ||
| 3599 | 3696 | ||
| @@ -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 | 86 | ||
| 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 -> dynamic | 338 | + // 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 hybrid | 348 | // 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 dump | 390 | + // 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; |
| @@ -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 | -#endif | 232 | +#endif |
| @@ -30,6 +30,8 @@ | |||
| 30 | 30 | ||
| 31 | 31 | ||
| 32 | 32 | ||
| 33 | + | ||
| 34 | + | ||
| 33 | 35 | ||
| 34 | 36 | ||
| 35 | 37 | ||
| @@ -1581,6 +1583,8 @@ HWTEST_F_L0(DFXJSNApiTests, PerformHybridHeapDump_DumpOption) | |||
| 1581 | 1583 | ||
| 1582 | HWTEST_F_L0(DFXJSNApiTests, PerformHybridHeapDump_WithStream) | 1584 | HWTEST_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 | 1925 | ||
| 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::test | 1990 | } // namespace panda::test |
| @@ -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"/> |


低
rawheap_translate.cpp:108— 日志单位不一致TranslateDynamicRawheap日志写cost << std::to_string(duration) << 's',但duration是milliseconds计数(std::chrono::duration_cast<std::chrono::milliseconds>),单位标为 's'(秒)是错误的。建议:
cost << duration << "ms"