Add support for thread names to out of process heap profiling.
This CL adds a new memlog stack mode: native-with-thread-names. When enabled,
the thread name will be inserted as the first frame of each stack.
Other minor changes:
* Fixed minor bug in the parsing logic for ProfilingTestDriver.
* Fixed a JSON exporter issue [node and string ids should begin at 1, not 0.
chrome://tracing UI ignores nodes with id 0.]
* Add TLS destructor for allocator shim.
* base::android::AttachCurrentThread() now preserves thread name.
Change-Id: I8c9d82084d6439e663f94e563068c987d1cf3b23
Bug: 758739
Reviewed-on: https://chromium-review.googlesource.com/877085
Reviewed-by: Maria Khomenko <[email protected]>
Reviewed-by: Richard Coles <[email protected]>
Reviewed-by: Dmitry Skiba <[email protected]>
Reviewed-by: Daniel Cheng <[email protected]>
Reviewed-by: Albert J. Wong <[email protected]>
Commit-Queue: Erik Chen <[email protected]>
Cr-Commit-Position: refs/heads/master@{#531992}
diff --git a/base/android/jni_android.cc b/base/android/jni_android.cc
index 71bd2d3..d4e8d9e 100644
--- a/base/android/jni_android.cc
+++ b/base/android/jni_android.cc
@@ -5,6 +5,7 @@
#include "base/android/jni_android.h"
#include <stddef.h>
+#include <sys/prctl.h>
#include <map>
@@ -38,9 +39,26 @@
JNIEnv* AttachCurrentThread() {
DCHECK(g_jvm);
- JNIEnv* env = NULL;
- jint ret = g_jvm->AttachCurrentThread(&env, NULL);
- DCHECK_EQ(JNI_OK, ret);
+ JNIEnv* env = nullptr;
+ jint ret = g_jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_2);
+ if (ret == JNI_EDETACHED || !env) {
+ JavaVMAttachArgs args;
+ args.version = JNI_VERSION_1_2;
+ args.group = nullptr;
+
+ // 16 is the maximum size for thread names on Android.
+ char thread_name[16];
+ int err = prctl(PR_GET_NAME, thread_name);
+ if (err < 0) {
+ DPLOG(ERROR) << "prctl(PR_GET_NAME)";
+ args.name = nullptr;
+ } else {
+ args.name = thread_name;
+ }
+
+ ret = g_jvm->AttachCurrentThread(&env, &args);
+ DCHECK_EQ(JNI_OK, ret);
+ }
return env;
}
diff --git a/base/threading/thread_id_name_manager.cc b/base/threading/thread_id_name_manager.cc
index 74a42c7d..ebcc3ce 100644
--- a/base/threading/thread_id_name_manager.cc
+++ b/base/threading/thread_id_name_manager.cc
@@ -47,6 +47,11 @@
name_to_interned_name_[kDefaultName];
}
+void ThreadIdNameManager::InstallSetNameCallback(SetNameCallback callback) {
+ AutoLock locked(lock_);
+ set_name_callback_ = std::move(callback);
+}
+
void ThreadIdNameManager::SetName(PlatformThreadId id,
const std::string& name) {
std::string* leaked_str = nullptr;
@@ -63,6 +68,10 @@
ThreadIdToHandleMap::iterator id_to_handle_iter =
thread_id_to_handle_.find(id);
+ if (set_name_callback_) {
+ set_name_callback_.Run(leaked_str->c_str());
+ }
+
// The main thread of a process will not be created as a Thread object which
// means there is no PlatformThreadHandler registered.
if (id_to_handle_iter == thread_id_to_handle_.end()) {
diff --git a/base/threading/thread_id_name_manager.h b/base/threading/thread_id_name_manager.h
index f469b60..d0717b09 100644
--- a/base/threading/thread_id_name_manager.h
+++ b/base/threading/thread_id_name_manager.h
@@ -9,6 +9,7 @@
#include <string>
#include "base/base_export.h"
+#include "base/callback.h"
#include "base/macros.h"
#include "base/synchronization/lock.h"
#include "base/threading/platform_thread.h"
@@ -27,6 +28,12 @@
// Register the mapping between a thread |id| and |handle|.
void RegisterThread(PlatformThreadHandle::Handle handle, PlatformThreadId id);
+ // The callback is called on the thread, immediately after the name is set.
+ // |name| is a pointer to a C string that is guaranteed to remain valid for
+ // the duration of the process.
+ using SetNameCallback = base::RepeatingCallback<void(const char* name)>;
+ void InstallSetNameCallback(SetNameCallback callback);
+
// Set the name for the given id.
void SetName(PlatformThreadId id, const std::string& name);
@@ -60,6 +67,8 @@
std::string* main_process_name_;
PlatformThreadId main_process_id_;
+ SetNameCallback set_name_callback_;
+
DISALLOW_COPY_AND_ASSIGN(ThreadIdNameManager);
};
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/profiling_host/ProfilingProcessHostAndroidTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/profiling_host/ProfilingProcessHostAndroidTest.java
index 32f9ff1b..0baacdf 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/profiling_host/ProfilingProcessHostAndroidTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/profiling_host/ProfilingProcessHostAndroidTest.java
@@ -37,24 +37,25 @@
@Test
@MediumTest
- @CommandLineFlags.Add({"memlog=browser"})
+ @CommandLineFlags.Add({"memlog=browser", "memlog-stack-mode=native-include-thread-names"})
public void testModeBrowser() throws Exception {
TestAndroidShim profilingProcessHost = new TestAndroidShim();
- Assert.assertTrue(profilingProcessHost.runTestForMode("browser", false, false));
+ Assert.assertTrue(profilingProcessHost.runTestForMode(
+ "browser", false, "native-include-thread-names"));
}
@Test
@MediumTest
public void testModeBrowserDynamic() throws Exception {
TestAndroidShim profilingProcessHost = new TestAndroidShim();
- Assert.assertTrue(profilingProcessHost.runTestForMode("browser", true, false));
+ Assert.assertTrue(profilingProcessHost.runTestForMode("browser", true, "native"));
}
@Test
@MediumTest
public void testModeBrowserDynamicPseudo() throws Exception {
TestAndroidShim profilingProcessHost = new TestAndroidShim();
- Assert.assertTrue(profilingProcessHost.runTestForMode("browser", true, true));
+ Assert.assertTrue(profilingProcessHost.runTestForMode("browser", true, "pseudo"));
}
// Non-browser processes must be profiled with a command line flag, since
@@ -67,7 +68,7 @@
@DisabledTest
public void testModeRendererPseudo() throws Exception {
TestAndroidShim profilingProcessHost = new TestAndroidShim();
- Assert.assertTrue(profilingProcessHost.runTestForMode("all-renderers", false, true));
+ Assert.assertTrue(profilingProcessHost.runTestForMode("all-renderers", false, "pseudo"));
}
@Test
@@ -75,6 +76,6 @@
@CommandLineFlags.Add({"memlog=gpu", "memlog-stack-mode=pseudo"})
public void testModeGpuPseudo() throws Exception {
TestAndroidShim profilingProcessHost = new TestAndroidShim();
- Assert.assertTrue(profilingProcessHost.runTestForMode("gpu", false, true));
+ Assert.assertTrue(profilingProcessHost.runTestForMode("gpu", false, "native"));
}
}
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/profiling_host/TestAndroidShim.java b/chrome/android/javatests/src/org/chromium/chrome/browser/profiling_host/TestAndroidShim.java
index 392d98c..7a94161 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/profiling_host/TestAndroidShim.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/profiling_host/TestAndroidShim.java
@@ -23,9 +23,9 @@
* rather than native stacks.
*/
public boolean runTestForMode(
- String mode, boolean dynamicallyStartProfiling, boolean pseudoStacks) {
+ String mode, boolean dynamicallyStartProfiling, String stackMode) {
return nativeRunTestForMode(
- mNativeTestAndroidShim, mode, dynamicallyStartProfiling, pseudoStacks);
+ mNativeTestAndroidShim, mode, dynamicallyStartProfiling, stackMode);
}
/**
@@ -43,5 +43,5 @@
private native long nativeInit();
private native void nativeDestroy(long nativeTestAndroidShim);
private native boolean nativeRunTestForMode(long nativeTestAndroidShim, String mode,
- boolean dynamicallyStartProfiling, boolean pseudoStacks);
+ boolean dynamicallyStartProfiling, String stackMode);
}
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index bfa0c62f..5ed7048 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -991,6 +991,9 @@
{flags_ui::kGenericExperimentChoiceDisabled, "", ""},
{flag_descriptions::kOOPHPStackModeNative, switches::kMemlogStackMode,
switches::kMemlogStackModeNative},
+ {flag_descriptions::kOOPHPStackModeNativeWithThreadNames,
+ switches::kMemlogStackMode,
+ switches::kMemlogStackModeNativeWithThreadNames},
{flag_descriptions::kOOPHPStackModePseudo, switches::kMemlogStackMode,
switches::kMemlogStackModePseudo},
{flag_descriptions::kOOPHPStackModeMixed, switches::kMemlogStackMode,
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index 7fb909f..ea92168 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -532,11 +532,13 @@
const char kOOPHPStackModeDescription[] =
"By default, memlog heap dumps record native stacks, which requires a "
"post-processing step to symbolize. Requires a custom build with frame "
- "pointers to work on Android. It's also possible to record a pseudo stack "
- "using trace events as identifiers. It's also possible to do a mix of "
- "both.";
+ "pointers to work on Android. Native with thread names will add the thread "
+ "name as the first frame of each native stack. It's also possible to "
+ "record a pseudo stack using trace events as identifiers. It's also "
+ "possible to do a mix of both.";
const char kOOPHPStackModeMixed[] = "Mixed";
const char kOOPHPStackModeNative[] = "Native";
+const char kOOPHPStackModeNativeWithThreadNames[] = "Native with thread names";
const char kOOPHPStackModePseudo[] = "Trace events";
const char kEnablePictureInPictureName[] = "Enable picture in picture.";
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 30c7af5..9209b4ad 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -339,6 +339,7 @@
extern const char kOOPHPStackModeDescription[];
extern const char kOOPHPStackModeMixed[];
extern const char kOOPHPStackModeNative[];
+extern const char kOOPHPStackModeNativeWithThreadNames[];
extern const char kOOPHPStackModePseudo[];
extern const char kEnablePictureInPictureName[];
diff --git a/chrome/browser/profiling_host/memlog_browsertest.cc b/chrome/browser/profiling_host/memlog_browsertest.cc
index 623ea3f..7deb542 100644
--- a/chrome/browser/profiling_host/memlog_browsertest.cc
+++ b/chrome/browser/profiling_host/memlog_browsertest.cc
@@ -54,7 +54,13 @@
if (GetParam().stack_mode == mojom::StackMode::PSEUDO) {
command_line->AppendSwitchASCII(switches::kMemlogStackMode,
switches::kMemlogStackModePseudo);
- } else if (GetParam().stack_mode == mojom::StackMode::NATIVE) {
+ } else if (GetParam().stack_mode ==
+ mojom::StackMode::NATIVE_WITH_THREAD_NAMES) {
+ command_line->AppendSwitchASCII(
+ switches::kMemlogStackMode,
+ switches::kMemlogStackModeNativeWithThreadNames);
+ } else if (GetParam().stack_mode ==
+ mojom::StackMode::NATIVE_WITHOUT_THREAD_NAMES) {
command_line->AppendSwitchASCII(switches::kMemlogStackMode,
switches::kMemlogStackModeNative);
} else if (GetParam().stack_mode == mojom::StackMode::MIXED) {
@@ -97,7 +103,7 @@
std::vector<mojom::StackMode> stack_modes;
stack_modes.push_back(mojom::StackMode::MIXED);
- stack_modes.push_back(mojom::StackMode::NATIVE);
+ stack_modes.push_back(mojom::StackMode::NATIVE_WITHOUT_THREAD_NAMES);
stack_modes.push_back(mojom::StackMode::PSEUDO);
for (const auto& mode : dynamic_start_modes) {
@@ -117,6 +123,10 @@
params.push_back({mode, stack_mode, true});
}
}
+
+ // Test thread names for native profiling.
+ params.push_back({ProfilingProcessHost::Mode::kBrowser,
+ mojom::StackMode::NATIVE_WITH_THREAD_NAMES, false});
return params;
}
diff --git a/chrome/browser/profiling_host/profiling_process_host.cc b/chrome/browser/profiling_host/profiling_process_host.cc
index 3b2fdb4..e21ab4263 100644
--- a/chrome/browser/profiling_host/profiling_process_host.cc
+++ b/chrome/browser/profiling_host/profiling_process_host.cc
@@ -431,20 +431,30 @@
kOOPHeapProfilingFeature, kOOPHeapProfilingFeatureStackMode);
}
- if (stack_mode == switches::kMemlogStackModeNative)
- return profiling::mojom::StackMode::NATIVE;
- if (stack_mode == switches::kMemlogStackModePseudo)
+ return ConvertStringToStackMode(stack_mode);
+}
+
+// static
+mojom::StackMode ProfilingProcessHost::ConvertStringToStackMode(
+ const std::string& input) {
+ if (input == switches::kMemlogStackModeNative)
+ return profiling::mojom::StackMode::NATIVE_WITHOUT_THREAD_NAMES;
+ if (input == switches::kMemlogStackModeNativeWithThreadNames)
+ return profiling::mojom::StackMode::NATIVE_WITH_THREAD_NAMES;
+ if (input == switches::kMemlogStackModePseudo)
return profiling::mojom::StackMode::PSEUDO;
- if (stack_mode == switches::kMemlogStackModeMixed)
+ if (input == switches::kMemlogStackModeMixed)
return profiling::mojom::StackMode::MIXED;
- return profiling::mojom::StackMode::NATIVE;
+ DLOG(ERROR) << "Unsupported value: \"" << input << "\" passed to --"
+ << switches::kMemlogStackMode;
+ return profiling::mojom::StackMode::NATIVE_WITHOUT_THREAD_NAMES;
}
// static
ProfilingProcessHost* ProfilingProcessHost::Start(
content::ServiceManagerConnection* connection,
Mode mode,
- profiling::mojom::StackMode stack_mode) {
+ mojom::StackMode stack_mode) {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
CHECK(!has_started_);
has_started_ = true;
diff --git a/chrome/browser/profiling_host/profiling_process_host.h b/chrome/browser/profiling_host/profiling_process_host.h
index d33d801..08e0fc6 100644
--- a/chrome/browser/profiling_host/profiling_process_host.h
+++ b/chrome/browser/profiling_host/profiling_process_host.h
@@ -100,14 +100,15 @@
// Returns the mode specified by the command line or via about://flags.
static Mode GetModeForStartup();
static Mode ConvertStringToMode(const std::string& input);
- static profiling::mojom::StackMode GetStackModeForStartup();
+ static mojom::StackMode GetStackModeForStartup();
+ static mojom::StackMode ConvertStringToStackMode(const std::string& input);
bool ShouldProfileNonRendererProcessType(int process_type);
// Launches the profiling process and returns a pointer to it.
static ProfilingProcessHost* Start(
content::ServiceManagerConnection* connection,
Mode mode,
- profiling::mojom::StackMode stack_mode);
+ mojom::StackMode stack_mode);
// Returns true if Start() has been called.
static bool has_started() { return has_started_; }
diff --git a/chrome/browser/profiling_host/profiling_test_driver.cc b/chrome/browser/profiling_host/profiling_test_driver.cc
index 730141c..d0954fad 100644
--- a/chrome/browser/profiling_host/profiling_test_driver.cc
+++ b/chrome/browser/profiling_host/profiling_test_driver.cc
@@ -12,6 +12,7 @@
#include "base/process/process_handle.h"
#include "base/run_loop.h"
#include "base/task_scheduler/post_task.h"
+#include "base/threading/platform_thread.h"
#include "base/trace_event/heap_profiler_event_filter.h"
#include "base/trace_event/trace_config_memory_test_util.h"
#include "base/values.h"
@@ -30,6 +31,7 @@
const char kMallocEvent[] = "kMallocEvent";
const char kPAEvent[] = "kPAEvent";
const char kVariadicEvent[] = "kVariadicEvent";
+const char kThreadName[] = "kThreadName";
// Make some specific allocations in Browser to do a deeper test of the
// allocation tracking.
@@ -124,9 +126,11 @@
return nullptr;
}
+constexpr uint64_t kNullParent = std::numeric_limits<int>::max();
struct Node {
- uint64_t name_id;
+ int name_id;
std::string name;
+ int parent_id = kNullParent;
};
using NodeMap = std::unordered_map<uint64_t, Node>;
@@ -143,6 +147,47 @@
Node node;
node.name_id = name_sid->GetInt();
+
+ const base::Value* parent_id = node_value.FindKey("parent");
+ if (parent_id) {
+ node.parent_id = parent_id->GetInt();
+ }
+
+ (*output)[id->GetInt()] = node;
+ }
+
+ base::Value* strings = heaps_v2->FindPath({"maps", "strings"});
+ for (const base::Value& string_value : strings->GetList()) {
+ const base::Value* id = string_value.FindKey("id");
+ const base::Value* string = string_value.FindKey("string");
+ if (!id || !string) {
+ LOG(ERROR) << "String struct missing id or string field";
+ return false;
+ }
+ for (auto& pair : *output) {
+ if (pair.second.name_id == id->GetInt()) {
+ pair.second.name = string->GetString();
+ break;
+ }
+ }
+ }
+
+ return true;
+}
+
+// Parses maps.types and maps.strings. Returns |true| on success.
+bool ParseTypes(base::Value* heaps_v2, NodeMap* output) {
+ base::Value* types = heaps_v2->FindPath({"maps", "types"});
+ for (const base::Value& type_value : types->GetList()) {
+ const base::Value* id = type_value.FindKey("id");
+ const base::Value* name_sid = type_value.FindKey("name_sid");
+ if (!id || !name_sid) {
+ LOG(ERROR) << "Node missing id or name_sid field";
+ return false;
+ }
+
+ Node node;
+ node.name_id = name_sid->GetInt();
(*output)[id->GetInt()] = node;
}
@@ -155,7 +200,7 @@
return false;
}
for (auto& pair : *output) {
- if (pair.second.name_id == static_cast<uint64_t>(id->GetInt())) {
+ if (pair.second.name_id == id->GetInt()) {
pair.second.name = string->GetString();
break;
}
@@ -171,7 +216,8 @@
int expected_alloc_count,
const char* allocator_name,
const char* type_name,
- const std::string& frame_name) {
+ const std::string& frame_name,
+ const std::string& thread_name) {
base::Value* sizes =
heaps_v2->FindPath({"allocators", allocator_name, "sizes"});
if (!sizes) {
@@ -268,25 +314,21 @@
// Find the type, if an expectation was passed in.
if (type_name) {
- bool found = false;
- int type = types_list[browser_alloc_index].GetInt();
- base::Value* strings = heaps_v2->FindPath({"maps", "strings"});
- for (const base::Value& dict : strings->GetList()) {
- // Each dict has the format {"id":1,"string":"kPartitionAllocTypeName"}
- int id = dict.FindKey("id")->GetInt();
- if (id == type) {
- found = true;
- std::string name = dict.FindKey("string")->GetString();
- if (name != type_name) {
- LOG(ERROR) << "actual name: " << name
- << " expected name: " << type_name;
- return false;
- }
- break;
- }
+ NodeMap node_map;
+ if (!ParseTypes(heaps_v2, &node_map)) {
+ LOG(ERROR) << "Failed to parse type and string structs";
+ return false;
}
- if (!found) {
- LOG(ERROR) << "Failed to find type name string: " << type_name;
+
+ int type = types_list[browser_alloc_index].GetInt();
+ auto it = node_map.find(type);
+ if (it == node_map.end()) {
+ LOG(ERROR) << "Failed to look up type.";
+ return false;
+ }
+ if (it->second.name != type_name) {
+ LOG(ERROR) << "actual name: " << it->second.name
+ << " expected name: " << type_name;
return false;
}
}
@@ -303,7 +345,7 @@
auto it = node_map.find(node_id);
if (it == node_map.end()) {
- LOG(ERROR) << "Failed to find root for node with id: " << node_id;
+ LOG(ERROR) << "Failed to find frame for node with id: " << node_id;
return false;
}
@@ -314,6 +356,34 @@
}
}
+ // Check that the thread [top frame] has the right name.
+ if (!thread_name.empty()) {
+ NodeMap node_map;
+ if (!ParseNodes(heaps_v2, &node_map)) {
+ LOG(ERROR) << "Failed to parse node and string structs";
+ return false;
+ }
+
+ int node_id = nodes_list[browser_alloc_index].GetInt();
+ auto it = node_map.find(node_id);
+ while (true) {
+ if (it == node_map.end() || it->second.parent_id == kNullParent)
+ break;
+ it = node_map.find(it->second.parent_id);
+ }
+
+ if (it == node_map.end()) {
+ LOG(ERROR) << "Failed to find root for node with id: " << node_id;
+ return false;
+ }
+
+ if (it->second.name != thread_name) {
+ LOG(ERROR) << "Wrong name: " << it->second.name
+ << " for thread with expected name: " << thread_name;
+ return false;
+ }
+ }
+
return true;
}
@@ -457,6 +527,8 @@
void ProfilingTestDriver::MakeTestAllocations() {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
+ base::PlatformThread::SetName(kThreadName);
+
leaks_.reserve(2 * kMallocAllocCount + 1 + kPartitionAllocSize);
{
@@ -561,10 +633,12 @@
should_validate_dumps = false;
#endif
+ std::string thread_name = ShouldIncludeNativeThreadNames() ? kThreadName : "";
+
if (should_validate_dumps) {
result = ValidateDump(heaps_v2, kMallocAllocSize * kMallocAllocCount,
kMallocAllocCount, "malloc", nullptr,
- HasPseudoFrames() ? kMallocEvent : "");
+ HasPseudoFrames() ? kMallocEvent : "", thread_name);
if (!result) {
LOG(ERROR) << "Failed to validate malloc fixed allocations";
return false;
@@ -572,7 +646,7 @@
result = ValidateDump(heaps_v2, total_variadic_allocations_,
kVariadicAllocCount, "malloc", nullptr,
- HasPseudoFrames() ? kVariadicEvent : "");
+ HasPseudoFrames() ? kVariadicEvent : "", thread_name);
if (!result) {
LOG(ERROR) << "Failed to validate malloc variadic allocations";
return false;
@@ -584,10 +658,10 @@
// only one place that uses partition alloc in the browser process [this
// test], the count is still valid. This should still be made more robust by
// fixing backtrace. https://crbug.com/786450.
- result =
- ValidateDump(heaps_v2, kPartitionAllocSize * kPartitionAllocCount,
- kPartitionAllocCount, "partition_alloc",
- kPartitionAllocTypeName, HasPseudoFrames() ? kPAEvent : "");
+ result = ValidateDump(heaps_v2, kPartitionAllocSize * kPartitionAllocCount,
+ kPartitionAllocCount, "partition_alloc",
+ kPartitionAllocTypeName,
+ HasPseudoFrames() ? kPAEvent : "", thread_name);
if (!result) {
LOG(ERROR) << "Failed to validate PA allocations";
return false;
@@ -657,8 +731,13 @@
options_.mode == ProfilingProcessHost::Mode::kAllRenderers;
}
+bool ProfilingTestDriver::ShouldIncludeNativeThreadNames() {
+ return options_.stack_mode == mojom::StackMode::NATIVE_WITH_THREAD_NAMES;
+}
+
bool ProfilingTestDriver::HasPseudoFrames() {
- return options_.stack_mode != profiling::mojom::StackMode::NATIVE;
+ return options_.stack_mode == mojom::StackMode::PSEUDO ||
+ options_.stack_mode == mojom::StackMode::MIXED;
}
void ProfilingTestDriver::WaitForProfilingToStartForAllRenderersUIThread() {
diff --git a/chrome/browser/profiling_host/profiling_test_driver.h b/chrome/browser/profiling_host/profiling_test_driver.h
index 7a082e2a..8f960b7 100644
--- a/chrome/browser/profiling_host/profiling_test_driver.h
+++ b/chrome/browser/profiling_host/profiling_test_driver.h
@@ -47,9 +47,7 @@
profiling::mojom::StackMode stack_mode;
// Whether the caller has already started profiling with the given mode.
- // TODO(erikchen): Implement and test the case where this member is false.
- // Starting profiling is an asynchronous operation, so this requires adding
- // some more plumbing. https://crbug.com/753218.
+ // When false, the test driver is responsible for starting profiling.
bool profiling_already_started;
};
@@ -93,6 +91,7 @@
bool ShouldProfileBrowser();
bool ShouldProfileRenderer();
+ bool ShouldIncludeNativeThreadNames();
bool HasPseudoFrames();
void WaitForProfilingToStartForAllRenderersUIThread();
diff --git a/chrome/browser/profiling_host/test_android_shim.cc b/chrome/browser/profiling_host/test_android_shim.cc
index f1375e9..73b044d8 100644
--- a/chrome/browser/profiling_host/test_android_shim.cc
+++ b/chrome/browser/profiling_host/test_android_shim.cc
@@ -31,13 +31,14 @@
const base::android::JavaParamRef<jobject>& obj,
const base::android::JavaParamRef<jstring>& mode,
jboolean dynamically_start_profiling,
- jboolean pseudo_stacks) {
+ const base::android::JavaParamRef<jstring>& stack_mode) {
profiling::ProfilingTestDriver driver;
profiling::ProfilingTestDriver::Options options;
options.mode = profiling::ProfilingProcessHost::ConvertStringToMode(
base::android::ConvertJavaStringToUTF8(mode));
- options.stack_mode = pseudo_stacks ? profiling::mojom::StackMode::PSEUDO
- : profiling::mojom::StackMode::NATIVE;
+ options.stack_mode =
+ profiling::ProfilingProcessHost::ConvertStringToStackMode(
+ base::android::ConvertJavaStringToUTF8(stack_mode));
options.profiling_already_started = !dynamically_start_profiling;
return driver.RunTest(options);
}
diff --git a/chrome/browser/profiling_host/test_android_shim.h b/chrome/browser/profiling_host/test_android_shim.h
index c6d8345..8f96e16c 100644
--- a/chrome/browser/profiling_host/test_android_shim.h
+++ b/chrome/browser/profiling_host/test_android_shim.h
@@ -17,11 +17,12 @@
TestAndroidShim(JNIEnv* env, jobject obj);
void Destroy(JNIEnv* env, const base::android::JavaParamRef<jobject>& obj);
- jboolean RunTestForMode(JNIEnv* env,
- const base::android::JavaParamRef<jobject>& obj,
- const base::android::JavaParamRef<jstring>& mode,
- jboolean dynamically_start_profiling,
- jboolean pseudo_stacks);
+ jboolean RunTestForMode(
+ JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj,
+ const base::android::JavaParamRef<jstring>& mode,
+ jboolean dynamically_start_profiling,
+ const base::android::JavaParamRef<jstring>& stack_mode);
private:
~TestAndroidShim();
diff --git a/chrome/common/chrome_switches.cc b/chrome/common/chrome_switches.cc
index 4644fde..efbd4ae 100644
--- a/chrome/common/chrome_switches.cc
+++ b/chrome/common/chrome_switches.cc
@@ -482,6 +482,7 @@
const char kMemlogStackMode[] = "memlog-stack-mode";
const char kMemlogStackModeMixed[] = "mixed";
const char kMemlogStackModeNative[] = "native";
+const char kMemlogStackModeNativeWithThreadNames[] = "native-with-thread-names";
const char kMemlogStackModePseudo[] = "pseudo";
// Allows setting a different destination ID for connection-monitoring GCM
diff --git a/chrome/common/chrome_switches.h b/chrome/common/chrome_switches.h
index 5adf77db..feb239f 100644
--- a/chrome/common/chrome_switches.h
+++ b/chrome/common/chrome_switches.h
@@ -159,6 +159,7 @@
extern const char kMemlogStackMode[];
extern const char kMemlogStackModeMixed[];
extern const char kMemlogStackModeNative[];
+extern const char kMemlogStackModeNativeWithThreadNames[];
extern const char kMemlogStackModePseudo[];
extern const char kMonitoringDestinationID[];
extern const char kNetLogCaptureMode[];
diff --git a/chrome/common/profiling/memlog_allocator_shim.cc b/chrome/common/profiling/memlog_allocator_shim.cc
index 57e92ab2..fd91937 100644
--- a/chrome/common/profiling/memlog_allocator_shim.cc
+++ b/chrome/common/profiling/memlog_allocator_shim.cc
@@ -14,6 +14,7 @@
#include "base/lazy_instance.h"
#include "base/numerics/safe_conversions.h"
#include "base/synchronization/lock.h"
+#include "base/threading/thread_id_name_manager.h"
#include "base/threading/thread_local.h"
#include "base/trace_event/heap_profiler_allocation_context_tracker.h"
#include "base/trace_event/heap_profiler_allocation_register.h"
@@ -26,6 +27,10 @@
#include <limits.h>
#endif
+#if defined(OS_LINUX) || defined(OS_ANDROID)
+#include <sys/prctl.h>
+#endif
+
using base::trace_event::AllocationContext;
using base::trace_event::AllocationContextTracker;
using CaptureMode = base::trace_event::AllocationContextTracker::CaptureMode;
@@ -42,6 +47,9 @@
MemlogSenderPipe* g_sender_pipe = nullptr;
+// In NATIVE stack mode, whether to insert stack names into the backtraces.
+bool g_include_thread_names = false;
+
// Prime since this is used like a hash table. Numbers of this magnitude seemed
// to provide sufficient parallelism to avoid lock overhead in ad-hoc testing.
constexpr int kNumSendBuffers = 17;
@@ -62,12 +70,85 @@
base::LazyInstance<base::ThreadLocalBoolean>::Leaky g_prevent_reentrancy =
LAZY_INSTANCE_INITIALIZER;
-// If we are using pseudo stacks, we need to inform the profiling service of the
-// address to string mapping. To avoid a global lock, we keep a thread-local
-// unordered_set of every address that has been sent from the thread in
-// question.
-base::LazyInstance<base::ThreadLocalPointer<std::unordered_set<const void*>>>::
- Leaky g_sent_strings = LAZY_INSTANCE_INITIALIZER;
+// The allocator shim needs to retain some additional state for each thread.
+struct ShimState {
+ // The pointer must be valid for the lifetime of the process.
+ const char* thread_name = nullptr;
+
+ // If we are using pseudo stacks, we need to inform the profiling service of
+ // the address to string mapping. To avoid a global lock, we keep a
+ // thread-local unordered_set of every address that has been sent from the
+ // thread in question.
+ std::unordered_set<const void*> sent_strings;
+};
+
+// This function is added to the TLS slot to clean up the instance when the
+// thread exits.
+void DestructShimState(void* shim_state) {
+ delete static_cast<ShimState*>(shim_state);
+}
+
+base::ThreadLocalStorage::StaticSlot g_tls_shim_state = TLS_INITIALIZER;
+
+// We don't need to worry about re-entrancy because g_prevent_reentrancy
+// already guards against that.
+ShimState* GetShimState() {
+ ShimState* state = static_cast<ShimState*>(g_tls_shim_state.Get());
+
+ if (!state) {
+ state = new ShimState();
+ g_tls_shim_state.Set(state);
+ }
+
+ return state;
+}
+
+// Set the thread name, which is a pointer to a leaked string, to ensure
+// validity forever.
+void SetCurrentThreadName(const char* name) {
+ GetShimState()->thread_name = name;
+}
+
+// Cannot call ThreadIdNameManager::GetName because it holds a lock and causes
+// deadlock when lock is already held by ThreadIdNameManager before the current
+// allocation. Gets the thread name from kernel if available or returns a string
+// with id. This function intentionally leaks the allocated strings since they
+// are used to tag allocations even after the thread dies.
+const char* GetAndLeakThreadName() {
+ // prctl requires 16 bytes, snprintf requires 19, pthread_getname_np requires
+ // 64 on macOS, see PlatformThread::SetName in platform_thread_mac.mm.
+ constexpr size_t kBufferLen = 64;
+ char name[kBufferLen];
+#if defined(OS_LINUX) || defined(OS_ANDROID)
+ // If the thread name is not set, try to get it from prctl. Thread name might
+ // not be set in cases where the thread started before heap profiling was
+ // enabled.
+ int err = prctl(PR_GET_NAME, name);
+ if (!err) {
+ return strdup(name);
+ }
+#elif defined(OS_MACOSX)
+ int err = pthread_getname_np(pthread_self(), name, kBufferLen);
+ if (err == 0 && name[0] != '\0') {
+ return strdup(name);
+ }
+#endif // defined(OS_LINUX) || defined(OS_ANDROID)
+
+ // Use tid if we don't have a thread name.
+ snprintf(name, sizeof(name), "Thread %lu",
+ static_cast<unsigned long>(base::PlatformThread::CurrentId()));
+ return strdup(name);
+}
+
+// Returns the thread name, looking it up if necessary.
+const char* GetOrSetThreadName() {
+ const char* thread_name = GetShimState()->thread_name;
+ if (UNLIKELY(!thread_name)) {
+ thread_name = GetAndLeakThreadName();
+ GetShimState()->thread_name = thread_name;
+ }
+ return thread_name;
+}
class SendBuffer {
public:
@@ -298,6 +379,34 @@
AddInstructionPointer(frames[i]);
}
+ void AddCString(const char* c_string) {
+ // Using a TLS cache of sent_strings avoids lock contention on malloc, which
+ // would kill performance.
+ std::unordered_set<const void*>* sent_strings =
+ &GetShimState()->sent_strings;
+
+ if (sent_strings->find(c_string) == sent_strings->end()) {
+ // No point in allowing arbitrarily long c-strings, which might cause pipe
+ // max length issues. Pick a reasonable length like 255.
+ static const size_t kMaxCStringLen = 255;
+
+ // length does not include the null terminator.
+ size_t length = strnlen(c_string, kMaxCStringLen);
+
+ char message[sizeof(StringMappingPacket) + kMaxCStringLen];
+ StringMappingPacket* string_mapping_packet =
+ new (&message) StringMappingPacket();
+ string_mapping_packet->address = reinterpret_cast<uint64_t>(c_string);
+ string_mapping_packet->string_len = length;
+ memcpy(message + sizeof(StringMappingPacket), c_string, length);
+ DoSend(address_, message, sizeof(StringMappingPacket) + length,
+ send_buffers_);
+ sent_strings->insert(c_string);
+ }
+
+ AddInstructionPointer(c_string);
+ }
+
size_t count() { return count_; }
private:
@@ -307,38 +416,7 @@
return;
}
- // Using a TLS cache of sent_strings avoids lock contention on malloc, which
- // would kill performance.
- std::unordered_set<const void*>* sent_strings =
- g_sent_strings.Pointer()->Get();
-
- if (sent_strings == nullptr) {
- sent_strings = new std::unordered_set<const void*>;
- g_sent_strings.Pointer()->Set(sent_strings);
- }
-
- if (sent_strings->find(frame.value) == sent_strings->end()) {
- // No point in allowing arbitrarily long c-strings, which might cause pipe
- // max length issues. Pick a reasonable length like 255.
- static const size_t kMaxCStringLen = 255;
- const char* null_terminated_cstring =
- static_cast<const char*>(frame.value);
- // length does not include the null terminator.
- size_t length = strnlen(null_terminated_cstring, kMaxCStringLen);
-
- char message[sizeof(StringMappingPacket) + kMaxCStringLen];
- StringMappingPacket* string_mapping_packet =
- new (&message) StringMappingPacket();
- string_mapping_packet->address = reinterpret_cast<uint64_t>(frame.value);
- string_mapping_packet->string_len = length;
- memcpy(message + sizeof(StringMappingPacket), null_terminated_cstring,
- length);
- DoSend(address_, message, sizeof(StringMappingPacket) + length,
- send_buffers_);
- sent_strings->insert(frame.value);
- }
-
- AddInstructionPointer(frame.value);
+ AddCString(static_cast<const char*>(frame.value));
}
void AddInstructionPointer(const void* value) {
@@ -366,6 +444,8 @@
void InitTLSSlot() {
ignore_result(g_prevent_reentrancy.Pointer()->Get());
+ if (!g_tls_shim_state.initialized())
+ g_tls_shim_state.Initialize(DestructShimState);
}
// In order for pseudo stacks to work, trace event filtering must be enabled.
@@ -395,6 +475,12 @@
// Must be done before hooking any functions that make stack traces.
base::debug::EnableInProcessStackDumping();
+ if (stack_mode == mojom::StackMode::NATIVE_WITH_THREAD_NAMES) {
+ g_include_thread_names = true;
+ base::ThreadIdNameManager::GetInstance()->InstallSetNameCallback(
+ base::BindRepeating(&SetCurrentThreadName));
+ }
+
switch (stack_mode) {
case mojom::StackMode::PSEUDO:
EnableTraceEventFiltering();
@@ -404,7 +490,8 @@
EnableTraceEventFiltering();
AllocationContextTracker::SetCaptureMode(CaptureMode::MIXED_STACK);
break;
- case mojom::StackMode::NATIVE:
+ case mojom::StackMode::NATIVE_WITH_THREAD_NAMES:
+ case mojom::StackMode::NATIVE_WITHOUT_THREAD_NAMES:
AllocationContextTracker::SetCaptureMode(CaptureMode::DISABLED);
break;
}
@@ -464,19 +551,24 @@
void SerializeFramesFromBacktrace(FrameSerializer* serializer) {
#if BUILDFLAG(CAN_UNWIND_WITH_FRAME_POINTERS)
- const void* frames[kMaxStackEntries];
+ const void* frames[kMaxStackEntries - 1];
size_t frame_count = base::debug::TraceStackFramePointers(
- frames, kMaxStackEntries,
+ frames, kMaxStackEntries - 1,
1); // exclude this function from the trace.
#else // BUILDFLAG(CAN_UNWIND_WITH_FRAME_POINTERS)
// Fall-back to capturing the stack with base::debug::StackTrace,
// which is likely slower, but more reliable.
- base::debug::StackTrace stack_trace(kMaxStackEntries);
+ base::debug::StackTrace stack_trace(kMaxStackEntries - 1);
size_t frame_count = 0u;
const void* const* frames = stack_trace.Addresses(&frame_count);
#endif // BUILDFLAG(CAN_UNWIND_WITH_FRAME_POINTERS)
serializer->AddAllInstructionPointers(frame_count, frames);
+
+ if (g_include_thread_names) {
+ const char* thread_name = GetOrSetThreadName();
+ serializer->AddCString(thread_name);
+ }
}
void AllocatorShimLogAlloc(AllocatorType type,
diff --git a/chrome/common/profiling/memlog_allocator_shim.h b/chrome/common/profiling/memlog_allocator_shim.h
index 9f83d5c..683aee47 100644
--- a/chrome/common/profiling/memlog_allocator_shim.h
+++ b/chrome/common/profiling/memlog_allocator_shim.h
@@ -17,7 +17,9 @@
void InitTLSSlot();
// Begin profiling all allocations in the process. Send the results to
-// |sender_pipe|.
+// |sender_pipe|. |stack_mode| describes the type of stack to record for each
+// allocation. |include_thread_names| describes whether to insert thread names
+// into NATIVE stacks.
void InitAllocatorShim(MemlogSenderPipe* sender_pipe,
mojom::StackMode stack_mode);
diff --git a/chrome/common/profiling/profiling_client.mojom b/chrome/common/profiling/profiling_client.mojom
index 6aac963..555a9b2 100644
--- a/chrome/common/profiling/profiling_client.mojom
+++ b/chrome/common/profiling/profiling_client.mojom
@@ -6,8 +6,12 @@
// The data that is recorded for an allocation stack.
enum StackMode {
+ // Instruction addresses from unwinding the stack. Includes the thread name as
+ // the first frame.
+ NATIVE_WITH_THREAD_NAMES,
+
// Instruction addresses from unwinding the stack.
- NATIVE,
+ NATIVE_WITHOUT_THREAD_NAMES,
// Uses trace events as identifiers.
PSEUDO,
diff --git a/chrome/profiling/json_exporter.cc b/chrome/profiling/json_exporter.cc
index c3a7077..905f4f75 100644
--- a/chrome/profiling/json_exporter.cc
+++ b/chrome/profiling/json_exporter.cc
@@ -168,7 +168,8 @@
// Inserts or retrieves the ID for a string in the string table.
size_t AddOrGetString(const std::string& str, StringTable* string_table) {
- auto result = string_table->emplace(str, string_table->size());
+ // The lowest ID should be 1. The chrome://tracing UI doesn't handle ID 0.
+ auto result = string_table->emplace(str, string_table->size() + 1);
// "result.first" is an iterator into the map.
return result.first->second;
}
@@ -202,8 +203,9 @@
size_t AddOrGetBacktraceNode(BacktraceNode node,
BacktraceTable* backtrace_table) {
+ // The lowest ID should be 1. The chrome://tracing UI doesn't handle ID 0.
auto result =
- backtrace_table->emplace(std::move(node), backtrace_table->size());
+ backtrace_table->emplace(std::move(node), backtrace_table->size() + 1);
// "result.first" is an iterator into the map.
return result.first->second;
}