| // Copyright 2025 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "stack_sampler.h" |
| |
| #include <lib/syslog/cpp/macros.h> |
| #include <lib/trace/event.h> |
| #include <lib/zx/clock.h> |
| #include <lib/zx/result.h> |
| #include <lib/zx/suspend_token.h> |
| #include <zircon/errors.h> |
| #include <zircon/syscalls-next.h> |
| #include <zircon/syscalls.h> |
| #include <zircon/syscalls/debug.h> |
| #include <zircon/syscalls/object.h> |
| |
| #include <charconv> |
| #include <cstdint> |
| #include <cstring> |
| #include <vector> |
| |
| #include <src/lib/unwinder/fuchsia.h> |
| #include <src/lib/unwinder/registers.h> |
| #include <src/lib/unwinder/unwind.h> |
| |
| namespace profiler { |
| |
| constexpr size_t kStackCaptureSize = 4096ul * 4; // 16 kib |
| |
| #if defined(__aarch64__) |
| // CPSR/SPSR M[4]: set when a thread is executing in AArch32 mode. |
| constexpr uint64_t kCpsrArch32Mask = uint64_t{1} << 4; |
| #endif |
| |
| template <typename T> |
| bool IsAarch32(const T& state) { |
| #ifdef __aarch64__ |
| return state.cpsr & kCpsrArch32Mask; |
| #else |
| return false; |
| #endif |
| } |
| |
| zx::result<> StackSampler::Start(size_t buffer_size_mb) { |
| TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__); |
| |
| // TODO(https://fxbug.dev/468419097): Deduplicate watcher code. |
| zx::result<> res = targets_.ForEachProcess([this](std::span<const zx_koid_t> job_path, |
| const ProcessTarget& p) -> zx::result<> { |
| TRACE_DURATION("cpu_profiler", "StackSampler::Start/ForEachProcess"); |
| std::vector<zx_koid_t> saved_path{job_path.begin(), job_path.end()}; |
| (void)RefreshMappings(p); |
| |
| auto process_watcher = std::make_unique<ProcessWatcher>( |
| p.handle.borrow(), |
| [saved_path, this](zx_koid_t pid, zx_koid_t tid, zx::thread t) { |
| AddThread(saved_path, pid, tid, std::move(t)); |
| }, |
| [saved_path, this](zx_koid_t pid, zx_koid_t tid) { RemoveThread(saved_path, pid, tid); }); |
| |
| auto [it, emplaced] = process_watchers_.emplace(p.pid, std::move(process_watcher)); |
| if (emplaced) { |
| if (zx::result watch_result = it->second->Watch(dispatcher_); watch_result.is_error()) { |
| FX_PLOGS(WARNING, watch_result.status_value()) << "Failed to watch process: " << p.pid; |
| } |
| } |
| return zx::ok(); |
| }); |
| |
| if (res.is_error()) { |
| return res; |
| } |
| |
| if (auto res = targets_.ForEachJob([this](const JobTarget& target) { |
| if (zx::result res = WatchTarget(target); res.is_error()) { |
| FX_PLOGS(WARNING, res.status_value()) |
| << "Failed to watch job [" << target.job_id << "] and its children"; |
| } |
| return zx::ok(); |
| }); |
| res.is_error()) { |
| return res; |
| } |
| |
| return zx::make_result(sample_task_.Post(dispatcher_)); |
| } |
| |
| zx::result<> StackSampler::Stop() { |
| TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__); |
| return zx::make_result(sample_task_.Cancel()); |
| } |
| |
| void StackSampler::AddThread(std::vector<zx_koid_t> job_path, zx_koid_t pid, zx_koid_t tid, |
| zx::thread t) { |
| TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__); |
| std::string thread_name = profiler::GetThreadName(t); |
| zx::result res = targets_.AddThread( |
| job_path, pid, |
| ThreadTarget{.handle = std::move(t), .tid = tid, .name = std::move(thread_name)}); |
| if (res.is_error()) { |
| FX_PLOGS(ERROR, res.status_value()) << "Failed to add thread to session: " << tid; |
| } else { |
| zx::result<ProcessTarget*> process_target = targets_.GetProcess(job_path, pid); |
| if (process_target.is_ok() && process_target.value() != nullptr) { |
| (void)RefreshMappings(*process_target.value()); |
| } |
| } |
| } |
| |
| void StackSampler::PopulateRestrictedStateAddrs(const ProcessTarget& target) { |
| constexpr char kRestrictedStateVmoPrefix[] = "restricted_state_vmo:"; |
| constexpr size_t kPrefixLen = sizeof(kRestrictedStateVmoPrefix) - 1; |
| |
| for (const zx_info_maps& map : target.cached_mappings) { |
| if (strncmp(map.name, kRestrictedStateVmoPrefix, kPrefixLen) == 0) { |
| const char* tid_str = map.name + kPrefixLen; |
| zx_koid_t tid; |
| auto result = std::from_chars(tid_str, map.name + sizeof(map.name), tid); |
| if (result.ec != std::errc()) { |
| continue; |
| } |
| auto it = target.threads.find(tid); |
| if (it != target.threads.end()) { |
| it->second.restricted_state_addr = map.base; |
| } |
| } |
| } |
| } |
| |
| zx::result<> StackSampler::RefreshMappings(const ProcessTarget& target) { |
| size_t actual = 0; |
| size_t avail = 0; |
| zx_status_t status = target.handle.get_info(ZX_INFO_PROCESS_MAPS, nullptr, 0, &actual, &avail); |
| if (status != ZX_OK) { |
| FX_PLOGS(ERROR, status) << "Process has exited. Failed to get mappings for process: " |
| << target.pid; |
| target.cached_mappings.clear(); |
| return zx::error(status); |
| } |
| target.cached_mappings.resize(avail); |
| status = target.handle.get_info(ZX_INFO_PROCESS_MAPS, target.cached_mappings.data(), |
| target.cached_mappings.size() * sizeof(zx_info_maps_t), &actual, |
| &avail); |
| if (status != ZX_OK) { |
| FX_PLOGS(ERROR, status) << "Process has exited. Failed to get mappings for process: " |
| << target.pid; |
| target.cached_mappings.clear(); |
| return zx::error(status); |
| } |
| if (actual < avail) { |
| target.cached_mappings.resize(actual); |
| } |
| PopulateRestrictedStateAddrs(target); |
| return zx::ok(); |
| } |
| |
| uint64_t StackSampler::GetRestrictedSP(const zx_restricted_state_t& restricted_state) { |
| #if defined(__aarch64__) |
| // On targets running in aarch32 mode, the stack pointer is stored in x13 rather than sp. |
| if (IsAarch32(restricted_state)) { |
| return restricted_state.x[13]; |
| } |
| return restricted_state.sp; |
| #elif defined(__x86_64__) |
| return restricted_state.rsp; |
| #elif defined(__riscv) |
| return restricted_state.sp; |
| #else |
| #error "unsupported architecture" |
| #endif |
| } |
| |
| void StackSampler::CollectSamples(async_dispatcher_t* dispatcher, async::TaskBase* task, |
| zx_status_t status) { |
| zx::ticks start_time = zx::ticks::now(); |
| TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__); |
| if (status != ZX_OK) { |
| return; |
| } |
| |
| (void)targets_.ForEachProcess([this](std::span<const zx_koid_t>, |
| const ProcessTarget& target) -> zx::result<> { |
| TRACE_DURATION("cpu_profiler", "StackSampler::CollectSamples/ForEachProcess"); |
| |
| for (const auto& [tid, thread] : target.threads) { |
| zx_info_thread_t thread_info; |
| zx_status_t status = thread.handle.get_info(ZX_INFO_THREAD, &thread_info, sizeof(thread_info), |
| nullptr, nullptr); |
| if (status != ZX_OK) { |
| FX_PLOGS(ERROR, status) << "unable to get thread info for thread " << thread.handle.get() |
| << ", skipping"; |
| continue; // Skip this thread. |
| } |
| |
| // Skip threads that are not actively running or blocked |
| if (thread_info.state != ZX_THREAD_STATE_RUNNING) { |
| continue; |
| } |
| |
| // Suspend thread |
| zx::suspend_token suspend_token; |
| status = thread.handle.suspend(&suspend_token); |
| if (status != ZX_OK) { |
| // skip terminited thread |
| continue; |
| } |
| |
| // IPI Profiling Target States and Sampling Strategy: |
| // |
| // 1. Userspace: IPI executes immediately. (Action: SAMPLE) |
| // 2. Kernel (Interruptible): IPI executes immediately. (Action: SAMPLE) |
| // 3. Kernel (Uninterruptible): IPI execution delayed until interrupts are |
| // re-enabled. Captured to reflect heavy kernel workloads. (Action: SAMPLE) |
| // 4. Context Switch / Blocking: Thread is yielding the CPU. We only sample |
| // if we catch the exact state entering the block. Otherwise, we skip |
| // to avoid capturing off-CPU behavior. (Action: CONDITIONAL SKIP) |
| // |
| // Poll until the thread handles the suspend request. |
| // We yield briefly if it is still running, and break immediately if it blocks. |
| // The timeout is kept short (2ms) to avoid blocking the dispatcher for too long. |
| zx::time max_loop_dur = zx::clock::get_monotonic() + zx::msec(2); |
| bool thread_suspended = false; |
| |
| while (zx::clock::get_monotonic() < max_loop_dur) { |
| zx_info_thread_t info; |
| status = thread.handle.get_info(ZX_INFO_THREAD, &info, sizeof(info), nullptr, nullptr); |
| if (status != ZX_OK) { |
| break; |
| } |
| if (info.state == ZX_THREAD_STATE_SUSPENDED) { |
| thread_suspended = true; |
| break; |
| } |
| if (info.state == ZX_THREAD_STATE_RUNNING) { |
| // Wait for the thread to suspend, or time out after 100µs. |
| // This is edge-triggered, so it returns immediately if the signal is asserted. |
| zx_signals_t observed; |
| status = thread.handle.wait_one(ZX_THREAD_SUSPENDED, zx::deadline_after(zx::usec(100)), |
| &observed); |
| if (status == ZX_OK) { |
| thread_suspended = true; |
| break; |
| } |
| } else { |
| // Thread blocked before we could sample it, skip |
| break; |
| } |
| } |
| |
| zx::ticks tick_suspended = zx::ticks::now(); |
| |
| if (!thread_suspended) { |
| continue; |
| } |
| |
| // Get registers |
| zx_thread_state_general_regs_t regs; |
| if (thread.handle.read_state(ZX_THREAD_STATE_GENERAL_REGS, ®s, sizeof(regs)) != ZX_OK) { |
| continue; |
| } |
| |
| unwinder::Registers registers = unwinder::FromFuchsiaRegisters(regs); |
| |
| uint64_t sp = 0; |
| #ifdef __aarch64__ |
| // For a thread executing in AArch32 mode (CPSR M[4] set), the stack |
| // pointer is R13, reported in r[13]; the sp field holds SP_EL0, which |
| // AArch32 execution never updates and is therefore stale. |
| if (IsAarch32(regs)) { |
| sp = regs.r[13]; |
| } else |
| #endif |
| { |
| if (registers.GetSP(sp).has_err()) { |
| continue; |
| } |
| } |
| |
| BufferedStackMemory stack_memory(target.handle.borrow(), {}, target.cached_mappings); |
| stack_memory.CaptureStack(sp, kStackCaptureSize); |
| |
| if (thread.restricted_state_addr.has_value()) { |
| uint64_t restricted_base = thread.restricted_state_addr.value(); |
| |
| // Capture the memory containing the restricted state struct |
| if (stack_memory.CaptureStack(restricted_base, sizeof(zx_restricted_state_t)) == ZX_OK) { |
| zx_restricted_state_t restricted_state; |
| // Read from local buffer |
| if (!stack_memory.ReadBytes(restricted_base, sizeof(restricted_state), &restricted_state) |
| .has_err()) { |
| sp = GetRestrictedSP(restricted_state); |
| stack_memory.CaptureStack(sp, kStackCaptureSize); |
| } else { |
| thread.restricted_state_addr.reset(); |
| } |
| } else { |
| thread.restricted_state_addr.reset(); |
| } |
| } |
| suspend_token.reset(); |
| zx::ticks tick_resume = zx::ticks::now(); |
| |
| // Group registers and memory chunks into a single chunk |
| zx::ticks sample_time = zx::ticks::now(); |
| if (sample_cb_) { |
| std::vector<uint8_t> sample_memory; |
| |
| // Append registers headers |
| uint64_t regs_size = sizeof(regs); |
| sample_memory.insert(sample_memory.end(), reinterpret_cast<uint8_t*>(®s_size), |
| reinterpret_cast<uint8_t*>(®s_size) + sizeof(regs_size)); |
| sample_memory.insert(sample_memory.end(), reinterpret_cast<uint8_t*>(®s), |
| reinterpret_cast<uint8_t*>(®s) + sizeof(regs)); |
| |
| // Append all memory chunks |
| for (const auto& chunk : stack_memory.GetChunks()) { |
| uint64_t base = chunk.base; |
| uint64_t size = chunk.data.size(); |
| sample_memory.insert(sample_memory.end(), reinterpret_cast<uint8_t*>(&base), |
| reinterpret_cast<uint8_t*>(&base) + sizeof(base)); |
| sample_memory.insert(sample_memory.end(), reinterpret_cast<uint8_t*>(&size), |
| reinterpret_cast<uint8_t*>(&size) + sizeof(size)); |
| sample_memory.insert(sample_memory.end(), chunk.data.begin(), chunk.data.end()); |
| } |
| |
| sample_cb_({.pid = target.pid, |
| .tid = tid, |
| .stack = {}, |
| .timestamp = sample_time, |
| .stack_memory = std::move(sample_memory)}); |
| } |
| |
| inspecting_durations_.push_back(tick_resume - tick_suspended); |
| } |
| return zx::ok(); |
| }); |
| |
| zx::time end_time = zx::clock::get_monotonic(); |
| zx::duration total_dur = end_time - zx::time(start_time.get()); |
| FX_LOGS(DEBUG) << "CollectSamples pass finished in " << total_dur.to_msecs() << " ms"; |
| |
| zx::duration period = zx::msec(10); |
| if (!sample_specs_.empty() && sample_specs_[0].period().has_value()) { |
| period = zx::nsec(sample_specs_[0].period().value()); |
| } |
| sample_task_.PostDelayed(dispatcher_, period); |
| } |
| } // namespace profiler |