blob: 879f0ae85f85bcec74ca9c777d417168db5df4e2 [file] [log] [blame]
// 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