diff --git a/base/BUILD.gn b/base/BUILD.gn index 7c20ce6733e09..4048816d7a03e 100644 --- a/base/BUILD.gn +++ b/base/BUILD.gn @@ -1313,6 +1313,8 @@ jumbo_component("base") { "android/path_service_android.cc", "android/path_utils.cc", "android/path_utils.h", + "android/reached_code_profiler.cc", + "android/reached_code_profiler.h", "android/record_histogram.cc", "android/record_user_action.cc", "android/scoped_hardware_buffer_handle.cc", @@ -1382,6 +1384,13 @@ jumbo_component("base") { ] } + if (current_cpu != "arm") { + # The reached code profiler is only supported on Android with 32-bit arm + # arch. + sources -= [ "android/reached_code_profiler.cc" ] + sources += [ "android/reached_code_profiler_stub.cc" ] + } + # This is actually a linker script, but it can be added to the link in the # same way as a library. libs += [ "android/library_loader/anchor_functions.lds" ] diff --git a/base/android/reached_code_profiler.cc b/base/android/reached_code_profiler.cc new file mode 100644 index 0000000000000..879f0ae85f85b --- /dev/null +++ b/base/android/reached_code_profiler.cc @@ -0,0 +1,326 @@ +// 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 <signal.h> +#include <sys/time.h> +#include <ucontext.h> +#include <unistd.h> + +#include <atomic> + +#include "base/android/library_loader/anchor_functions.h" +#include "base/android/orderfile/orderfile_buildflags.h" +#include "base/command_line.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 { + +constexpr const char kDumpToFileFlag[] = "reached-code-profiler-dump-to-file"; + +// Enough for 1 << 29 bytes of code, 512MB. +constexpr size_t kBitfieldSize = 1 << 20; +constexpr size_t kBitsPerElement = 4 * 32; + +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); + +std::atomic<uint32_t> g_reached[kBitfieldSize]; +std::atomic<std::atomic<uint32_t>*> g_enabled_and_reached(g_reached); + +size_t NumberOfReachableElements() { + return (kEndOfText - kStartOfText) / kBitsPerElement + 1; +} + +void RecordAddress(uint32_t address) { + auto* reached = g_enabled_and_reached.load(std::memory_order_relaxed); + if (!reached) + return; + + // Stopped in libc, third-party, or Java code. + if (address < kStartOfText || address > kEndOfText) + return; + + size_t offset = address - kStartOfText; + static_assert(sizeof(int) == 4, + "Collection and processing code assumes that sizeof(int) == 4"); + size_t offset_index = offset / 4; + + // Atomically set the corresponding bit in the array. + std::atomic<uint32_t>* element = reached + (offset_index / 32); + // First, a racy check. This saves a CAS if the bit is already set, and + // allows the cache line to remain shared acoss CPUs in this case. + uint32_t value = element->load(std::memory_order_relaxed); + uint32_t mask = 1 << (offset_index % 32); + if (value & mask) + return; + element->fetch_or(mask, std::memory_order_relaxed); +} + +void HandleSignal(int signal, siginfo_t* info, void* context) { + if (signal != kProfilerSignal) + return; + + ucontext_t* ucontext = reinterpret_cast<ucontext_t*>(context); + uint32_t address = ucontext->uc_mcontext.arm_pc; + RecordAddress(address); +} + +struct ScopedTimerCloseTraits { + static base::Optional<timer_t> InvalidValue() { return base::nullopt; } + + static void Free(base::Optional<timer_t> x) { timer_delete(*x); } +}; + +// RAII object holding an interval timer. +using ScopedTimer = + base::ScopedGeneric<base::Optional<timer_t>, ScopedTimerCloseTraits>; + +std::vector<uint8_t> SnapshotReachedCodeBitset() { + std::vector<uint8_t> buf; + size_t elements = NumberOfReachableElements(); + buf.resize(elements * sizeof(uint32_t)); + // Copy the reached array into a buffer with atomic loads with the explicit + // memory ordering flag. In practice this is likely not necessary because: + // a) integrity of the data across individual elements of |g_reached| is not + // maintained anyway + // b) write(2) will not take the data in smaller chunks than 4 bytes + // c) it would be bizarre for mojo initialization code to cause the compiler + // to spill stuff into the array.. + // Anyway .. come to the Safe Side, we have CPUs to spin. + for (size_t i = 0; i < elements; i++) { + uint32_t word = g_reached[i].load(std::memory_order_relaxed); + for (int j = 0; j < 4; j++) { + buf[4 * i + j] = static_cast<uint8_t>((word >> (j * 8)) & 0xFF); + } + } + return buf; +} + +void DumpToFile(const base::FilePath& path, + scoped_refptr<base::SingleThreadTaskRunner> task_runner) { + CHECK(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<uint8_t> buf = SnapshotReachedCodeBitset(); + base::StringPiece contents(reinterpret_cast<const char*>(buf.data()), + buf.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<ReachedCodeProfiler> 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<base::Thread>("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<pid_t> tids_; + const pid_t current_pid_; + uint64_t iteration_number_; + ScopedTimer timer_; + std::unique_ptr<base::Thread> dumping_thread_; + + bool is_enabled_; + + friend class NoDestructor<ReachedCodeProfiler>; + + DISALLOW_COPY_AND_ASSIGN(ReachedCodeProfiler); +}; + +bool ShouldEnableReachedCodeProfiler() { +#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. + return false; +#else + // TODO(crbug.com/916263): this should be set up according to the finch + // experiment. + return false; +#endif +} + +} // 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(); +} + +} // namespace android +} // namespace base diff --git a/base/android/reached_code_profiler.h b/base/android/reached_code_profiler.h new file mode 100644 index 0000000000000..8d18e09fa38b5 --- /dev/null +++ b/base/android/reached_code_profiler.h @@ -0,0 +1,30 @@ +// 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. + +#ifndef BASE_ANDROID_REACHED_CODE_PROFILER_H_ +#define BASE_ANDROID_REACHED_CODE_PROFILER_H_ + +#include "base/android/library_loader/library_loader_hooks.h" +#include "base/base_export.h" + +namespace base { +namespace android { + +// Initializes and starts the reached code profiler for |library_process_type|. +// Reached symbols are not recorded before calling this function, so it has to +// be called as early in startup as possible. This has to be called before the +// process creates any thread. +// TODO(crbug.com/916263): Currently, the reached code profiler must be +// initialized before the tracing profiler. If we want to start it at later +// point, we need to check that the tracing profiler isn't initialized first. +BASE_EXPORT void InitReachedCodeProfilerAtStartup( + LibraryProcessType library_process_type); + +// Returns whether the reached code profiler is enabled. +BASE_EXPORT bool IsReachedCodeProfilerEnabled(); + +} // namespace android +} // namespace base + +#endif // BASE_ANDROID_REACHED_CODE_PROFILER_H_ diff --git a/base/android/reached_code_profiler_stub.cc b/base/android/reached_code_profiler_stub.cc new file mode 100644 index 0000000000000..56f0fb002af31 --- /dev/null +++ b/base/android/reached_code_profiler_stub.cc @@ -0,0 +1,18 @@ +// 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" + +namespace base { +namespace android { + +void InitReachedCodeProfilerAtStartup(LibraryProcessType library_process_type) { +} + +bool IsReachedCodeProfilerEnabled() { + return false; +} + +} // namespace android +} // namespace base diff --git a/base/linux_util.cc b/base/linux_util.cc index ddf848eeb701f..caf471a39e15c 100644 --- a/base/linux_util.cc +++ b/base/linux_util.cc @@ -14,12 +14,13 @@ #include <unistd.h> #include <memory> -#include <vector> #include "base/command_line.h" +#include "base/files/dir_reader_posix.h" #include "base/files/file_util.h" #include "base/memory/singleton.h" #include "base/process/launch.h" +#include "base/strings/safe_sprintf.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_split.h" #include "base/strings/string_tokenizer.h" @@ -75,28 +76,6 @@ class LinuxDistroHelper { }; #endif // if defined(OS_LINUX) -bool GetTasksForProcess(pid_t pid, std::vector<pid_t>* tids) { - char buf[256]; - snprintf(buf, sizeof(buf), "/proc/%d/task", pid); - - DIR* task = opendir(buf); - if (!task) { - DLOG(WARNING) << "Cannot open " << buf; - return false; - } - - struct dirent* dent; - while ((dent = readdir(task))) { - char* endptr; - const unsigned long int tid_ul = strtoul(dent->d_name, &endptr, 10); - if (tid_ul == ULONG_MAX || *endptr) - continue; - tids->push_back(tid_ul); - } - closedir(task); - return true; -} - } // namespace namespace base { @@ -155,13 +134,35 @@ void SetLinuxDistro(const std::string& distro) { strlcpy(g_linux_distro, trimmed_distro.c_str(), kDistroSize); } +bool GetThreadsForProcess(pid_t pid, std::vector<pid_t>* tids) { + // 25 > strlen("/proc//task") + strlen(std::to_string(INT_MAX)) + 1 = 22 + char buf[25]; + base::strings::SafeSPrintf(buf, "/proc/%d/task", pid); + DirReaderPosix dir_reader(buf); + + if (!dir_reader.IsValid()) { + DLOG(WARNING) << "Cannot open " << buf; + return false; + } + + while (dir_reader.Next()) { + char* endptr; + const unsigned long int tid_ul = strtoul(dir_reader.name(), &endptr, 10); + if (tid_ul == ULONG_MAX || *endptr) + continue; + tids->push_back(tid_ul); + } + + return true; +} + pid_t FindThreadIDWithSyscall(pid_t pid, const std::string& expected_data, bool* syscall_supported) { if (syscall_supported != nullptr) *syscall_supported = false; std::vector<pid_t> tids; - if (!GetTasksForProcess(pid, &tids)) + if (!GetThreadsForProcess(pid, &tids)) return -1; std::unique_ptr<char[]> syscall_data(new char[expected_data.length()]); @@ -191,7 +192,7 @@ pid_t FindThreadID(pid_t pid, pid_t ns_tid, bool* ns_pid_supported) { *ns_pid_supported = false; std::vector<pid_t> tids; - if (!GetTasksForProcess(pid, &tids)) + if (!GetThreadsForProcess(pid, &tids)) return -1; for (pid_t tid : tids) { diff --git a/base/linux_util.h b/base/linux_util.h index 272e06b7d8cb5..568b4e86eacf4 100644 --- a/base/linux_util.h +++ b/base/linux_util.h @@ -9,6 +9,7 @@ #include <sys/types.h> #include <string> +#include <vector> #include "base/base_export.h" @@ -24,6 +25,10 @@ BASE_EXPORT std::string GetLinuxDistro(); // Set the Linux Distro string. BASE_EXPORT void SetLinuxDistro(const std::string& distro); +// For a given process |pid|, get a list of all its threads. On success, returns +// true and appends the list of threads to |tids|. Otherwise, returns false. +BASE_EXPORT bool GetThreadsForProcess(pid_t pid, std::vector<pid_t>* tids); + // For a given process |pid|, look through all its threads and find the first // thread with /proc/[pid]/task/[thread_id]/syscall whose first N bytes matches // |expected_data|, where N is the length of |expected_data|. diff --git a/components/tracing/common/tracing_sampler_profiler.cc b/components/tracing/common/tracing_sampler_profiler.cc index f8c79fee999d2..9c9e4bd7e9cb2 100644 --- a/components/tracing/common/tracing_sampler_profiler.cc +++ b/components/tracing/common/tracing_sampler_profiler.cc @@ -18,6 +18,10 @@ #include "build/build_config.h" #include "build/buildflag.h" +#if defined(OS_ANDROID) +#include "base/android/reached_code_profiler.h" +#endif + #if defined(OS_ANDROID) && BUILDFLAG(CAN_UNWIND_WITH_CFI_TABLE) && \ defined(OFFICIAL_BUILD) #include <dlfcn.h> @@ -178,6 +182,13 @@ void TracingSamplerProfiler::OnTraceLogEnabled() { if (!enabled) return; +#if defined(OS_ANDROID) + // The sampler profiler would conflict with the reached code profiler if they + // run at the same time because they use the same signal to suspend threads. + if (base::android::IsReachedCodeProfilerEnabled()) + return; +#endif + base::StackSamplingProfiler::SamplingParams params; params.samples_per_profile = std::numeric_limits<int>::max(); params.sampling_interval = base::TimeDelta::FromMilliseconds(50); diff --git a/content/app/android/library_loader_hooks.cc b/content/app/android/library_loader_hooks.cc index 386c79c6d4483..1620ebe22a168 100644 --- a/content/app/android/library_loader_hooks.cc +++ b/content/app/android/library_loader_hooks.cc @@ -4,6 +4,7 @@ #include "content/app/android/library_loader_hooks.h" +#include "base/android/reached_code_profiler.h" #include "base/logging.h" #include "base/trace_event/trace_event.h" #include "components/tracing/common/trace_startup.h" @@ -14,6 +15,8 @@ namespace content { bool LibraryLoaded(JNIEnv* env, jclass clazz, base::android::LibraryProcessType library_process_type) { + base::android::InitReachedCodeProfilerAtStartup(library_process_type); + // Enable startup tracing asap to avoid early TRACE_EVENT calls being ignored. tracing::EnableStartupTracingIfNeeded();