// Copyright 2019 The Chromium 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 "base/android/reached_code_profiler.h" #include #include #include #include #include #include "base/android/library_loader/anchor_functions.h" #include "base/android/orderfile/orderfile_buildflags.h" #include "base/android/reached_addresses_bitset.h" #include "base/base_switches.h" #include "base/bind.h" #include "base/command_line.h" #include "base/feature_list.h" #include "base/files/file_path.h" #include "base/files/file_util.h" #include "base/files/important_file_writer.h" #include "base/linux_util.h" #include "base/logging.h" #include "base/no_destructor.h" #include "base/optional.h" #include "base/path_service.h" #include "base/scoped_generic.h" #include "base/single_thread_task_runner.h" #include "base/strings/string_piece.h" #include "base/strings/stringprintf.h" #include "base/synchronization/lock.h" #include "base/threading/thread.h" #include "base/timer/timer.h" #if !BUILDFLAG(SUPPORTS_CODE_ORDERING) #error Code ordering support is required for the reached code profiler. #endif namespace base { namespace android { namespace { #if !defined(NDEBUG) || defined(COMPONENT_BUILD) // Always disabled for debug builds to avoid hitting a limit of signal // interrupts that can get delivered into a single HANDLE_EINTR. Also // debugging experience would be bad if there are a lot of signals flying // around. // Always disabled for component builds because in this case the code is not // organized in one contiguous region which is required for the reached code // profiler. constexpr const bool kConfigurationSupported = false; #else constexpr const bool kConfigurationSupported = true; #endif constexpr const char kDumpToFileFlag[] = "reached-code-profiler-dump-to-file"; constexpr uint64_t kIterationsBeforeSkipping = 50; constexpr uint64_t kIterationsBetweenUpdates = 100; constexpr int kProfilerSignal = SIGURG; constexpr base::TimeDelta kSamplingInterval = base::TimeDelta::FromMilliseconds(10); constexpr base::TimeDelta kDumpInterval = base::TimeDelta::FromSeconds(30); void HandleSignal(int signal, siginfo_t* info, void* context) { if (signal != kProfilerSignal) return; ucontext_t* ucontext = reinterpret_cast(context); uint32_t address = ucontext->uc_mcontext.arm_pc; ReachedAddressesBitset::GetTextBitset()->RecordAddress(address); } struct ScopedTimerCloseTraits { static base::Optional InvalidValue() { return base::nullopt; } static void Free(base::Optional x) { timer_delete(*x); } }; // RAII object holding an interval timer. using ScopedTimer = base::ScopedGeneric, ScopedTimerCloseTraits>; void DumpToFile(const base::FilePath& path, scoped_refptr task_runner) { DCHECK(task_runner->BelongsToCurrentThread()); auto dir_path = path.DirName(); if (!base::DirectoryExists(dir_path) && !base::CreateDirectory(dir_path)) { PLOG(ERROR) << "Could not create " << dir_path; return; } std::vector reached_offsets = ReachedAddressesBitset::GetTextBitset()->GetReachedOffsets(); base::StringPiece contents( reinterpret_cast(reached_offsets.data()), reached_offsets.size()); if (!base::ImportantFileWriter::WriteFileAtomically(path, contents, "ReachedDump")) { LOG(ERROR) << "Could not write reached dump into " << path; } task_runner->PostDelayedTask( FROM_HERE, base::BindOnce(&DumpToFile, path, task_runner), kDumpInterval); } class ReachedCodeProfiler { public: static ReachedCodeProfiler* GetInstance() { static base::NoDestructor instance; return instance.get(); } // Starts to periodically send |kProfilerSignal| to all threads. void Start(LibraryProcessType library_process_type) { if (is_enabled_) return; // Set |kProfilerSignal| signal handler. // TODO(crbug.com/916263): consider restoring |old_handler| after the // profiler gets stopped. struct sigaction old_handler; struct sigaction sa; sigemptyset(&sa.sa_mask); sa.sa_sigaction = &HandleSignal; sa.sa_flags = SA_RESTART | SA_SIGINFO; int ret = sigaction(kProfilerSignal, &sa, &old_handler); if (ret) { PLOG(ERROR) << "Error setting signal handler. The reached code profiler " "is disabled"; return; } // Create a new interval timer. struct sigevent sevp; memset(&sevp, 0, sizeof(sevp)); sevp.sigev_notify = SIGEV_THREAD; sevp.sigev_notify_function = &OnTimerNotify; timer_t timerid; ret = timer_create(CLOCK_PROCESS_CPUTIME_ID, &sevp, &timerid); if (ret) { PLOG(ERROR) << "timer_create() failed. The reached code profiler is disabled"; return; } timer_.reset(timerid); // Start the interval timer. struct itimerspec its; memset(&its, 0, sizeof(its)); its.it_interval.tv_nsec = kSamplingInterval.InNanoseconds(); its.it_value = its.it_interval; ret = timer_settime(timerid, 0, &its, nullptr); if (ret) { PLOG(ERROR) << "timer_settime() failed. The reached code profiler is disabled"; return; } if (library_process_type == PROCESS_BROWSER) StartDumpingReachedCode(); is_enabled_ = true; } // Stops profiling. void Stop() { timer_.reset(); dumping_thread_.reset(); is_enabled_ = false; } // Returns whether the profiler is currently enabled. bool IsEnabled() { return is_enabled_; } private: ReachedCodeProfiler() : current_pid_(getpid()), iteration_number_(0), is_enabled_(false) {} static void OnTimerNotify(sigval_t ignored) { ReachedCodeProfiler::GetInstance()->SendSignalToAllThreads(); } void SendSignalToAllThreads() { // This code should be thread-safe. base::AutoLock scoped_lock(lock_); ++iteration_number_; if (iteration_number_ <= kIterationsBeforeSkipping || iteration_number_ % kIterationsBetweenUpdates == 0) { tids_.clear(); if (!base::GetThreadsForProcess(current_pid_, &tids_)) { LOG(WARNING) << "Failed to get a list of threads for process " << current_pid_; return; } } pid_t current_tid = gettid(); for (pid_t tid : tids_) { if (tid != current_tid) tgkill(current_pid_, tid, kProfilerSignal); } } void StartDumpingReachedCode() { const base::CommandLine* cmdline = base::CommandLine::ForCurrentProcess(); if (!cmdline->HasSwitch(kDumpToFileFlag)) return; base::FilePath dir_path(cmdline->GetSwitchValueASCII(kDumpToFileFlag)); if (dir_path.empty()) { if (!base::PathService::Get(base::DIR_CACHE, &dir_path)) { LOG(WARNING) << "Failed to get cache dir path."; return; } } auto file_path = dir_path.Append(base::StringPrintf("reached-code-%d.txt", getpid())); dumping_thread_ = std::make_unique("ReachedCodeProfilerDumpingThread"); base::Thread::Options options; options.priority = base::ThreadPriority::BACKGROUND; dumping_thread_->StartWithOptions(options); dumping_thread_->task_runner()->PostDelayedTask( FROM_HERE, base::BindOnce(&DumpToFile, file_path, dumping_thread_->task_runner()), kDumpInterval); } base::Lock lock_; std::vector tids_; const pid_t current_pid_; uint64_t iteration_number_; ScopedTimer timer_; std::unique_ptr dumping_thread_; bool is_enabled_; friend class NoDestructor; DISALLOW_COPY_AND_ASSIGN(ReachedCodeProfiler); }; bool ShouldEnableReachedCodeProfiler() { if (!kConfigurationSupported) return false; const base::CommandLine* cmdline = base::CommandLine::ForCurrentProcess(); return cmdline->HasSwitch(switches::kEnableReachedCodeProfiler); } } // namespace void InitReachedCodeProfilerAtStartup(LibraryProcessType library_process_type) { // The profiler shouldn't be run as part of webview. CHECK(library_process_type == PROCESS_BROWSER || library_process_type == PROCESS_CHILD); if (!ShouldEnableReachedCodeProfiler()) return; ReachedCodeProfiler::GetInstance()->Start(library_process_type); } bool IsReachedCodeProfilerEnabled() { return ReachedCodeProfiler::GetInstance()->IsEnabled(); } bool IsReachedCodeProfilerSupported() { return kConfigurationSupported; } } // namespace android } // namespace base