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();