Merge "Perfprofd: Pep8 the python scripts"
diff --git a/perfprofd/scripts/perf_proto_stack_sqlite_flame.py b/perfprofd/scripts/perf_proto_stack_sqlite_flame.py
index 62b2ca9..3eb2379 100644
--- a/perfprofd/scripts/perf_proto_stack_sqlite_flame.py
+++ b/perfprofd/scripts/perf_proto_stack_sqlite_flame.py
@@ -206,7 +206,7 @@
return None
def print_svg(self, filename, depth):
- from svg_renderer import renderSVG
+ from svg_renderer import render_svg
self.root.svgrenderer_compat(self.dsos, self.syms)
self.root.generate_offset(0)
f = open(filename, 'w')
@@ -222,7 +222,7 @@
def __init__(self):
self.props = {'trace_offcpu': False}
fake_process = FakeProcess()
- renderSVG(fake_process, self.root, f, 'hot')
+ render_svg(fake_process, self.root, f, 'hot')
f.write('''
</div>
diff --git a/simpleperf/IOEventLoop.cpp b/simpleperf/IOEventLoop.cpp
index 01f2acd..92d3afe 100644
--- a/simpleperf/IOEventLoop.cpp
+++ b/simpleperf/IOEventLoop.cpp
@@ -24,11 +24,13 @@
struct IOEvent {
IOEventLoop* loop;
event* e;
+ timeval timeout;
std::function<bool()> callback;
bool enabled;
IOEvent(IOEventLoop* loop, const std::function<bool()>& callback)
- : loop(loop), e(nullptr), callback(callback), enabled(false) {}
+ : loop(loop), e(nullptr), timeout({}), callback(callback), enabled(false) {
+ }
~IOEvent() {
if (e != nullptr) {
@@ -138,9 +140,8 @@
return true;
}
-bool IOEventLoop::AddPeriodicEvent(timeval duration,
- const std::function<bool()>& callback) {
- return AddEvent(-1, EV_PERSIST, &duration, callback) != nullptr;
+IOEventRef IOEventLoop::AddPeriodicEvent(timeval duration, const std::function<bool()>& callback) {
+ return AddEvent(-1, EV_PERSIST, &duration, callback);
}
IOEventRef IOEventLoop::AddEvent(int fd_or_sig, short events, timeval* timeout,
@@ -158,6 +159,9 @@
LOG(ERROR) << "event_add() failed";
return nullptr;
}
+ if (timeout != nullptr) {
+ e->timeout = *timeout;
+ }
e->enabled = true;
events_.push_back(std::move(e));
return events_.back().get();
@@ -200,7 +204,9 @@
bool IOEventLoop::EnableEvent(IOEventRef ref) {
if (!ref->enabled) {
- if (event_add(ref->e, nullptr) != 0) {
+ timeval* timeout = (ref->timeout.tv_sec != 0 || ref->timeout.tv_usec != 0) ?
+ &ref->timeout : nullptr;
+ if (event_add(ref->e, timeout) != 0) {
LOG(ERROR) << "event_add() failed";
return false;
}
diff --git a/simpleperf/IOEventLoop.h b/simpleperf/IOEventLoop.h
index 4a84197..3a15d05 100644
--- a/simpleperf/IOEventLoop.h
+++ b/simpleperf/IOEventLoop.h
@@ -57,8 +57,7 @@
// Register a periodic Event, so [callback] is called periodically every
// [duration].
- bool AddPeriodicEvent(timeval duration,
- const std::function<bool()>& callback);
+ IOEventRef AddPeriodicEvent(timeval duration, const std::function<bool()>& callback);
// Run a loop polling for Events. It only exits when ExitLoop() is called
// in a callback function of registered Events.
diff --git a/simpleperf/IOEventLoop_test.cpp b/simpleperf/IOEventLoop_test.cpp
index cf231ba..2a3fa96 100644
--- a/simpleperf/IOEventLoop_test.cpp
+++ b/simpleperf/IOEventLoop_test.cpp
@@ -216,6 +216,32 @@
close(fd[1]);
}
+TEST(IOEventLoop, disable_enable_periodic_event) {
+ timeval tv;
+ tv.tv_sec = 0;
+ tv.tv_usec = 200000;
+ IOEventLoop loop;
+ IOEventRef wait_ref = loop.AddPeriodicEvent(tv, [&]() { return loop.ExitLoop(); });
+ ASSERT_TRUE(wait_ref != nullptr);
+ ASSERT_TRUE(loop.DisableEvent(wait_ref));
+
+ tv.tv_sec = 0;
+ tv.tv_usec = 100000;
+ size_t periodic_count = 0;
+ IOEventRef ref = loop.AddPeriodicEvent(tv, [&]() {
+ if (!loop.DisableEvent(ref)) {
+ return false;
+ }
+ periodic_count++;
+ if (periodic_count < 2u) {
+ return loop.EnableEvent(ref);
+ }
+ return loop.EnableEvent(wait_ref);
+ });
+ ASSERT_TRUE(loop.RunLoop());
+ ASSERT_EQ(2u, periodic_count);
+}
+
TEST(IOEventLoop, exit_before_loop) {
IOEventLoop loop;
ASSERT_TRUE(loop.ExitLoop());
diff --git a/simpleperf/JITDebugReader.cpp b/simpleperf/JITDebugReader.cpp
index 23f4ccd..7377799 100644
--- a/simpleperf/JITDebugReader.cpp
+++ b/simpleperf/JITDebugReader.cpp
@@ -36,14 +36,18 @@
namespace simpleperf {
-// To avoid too long time reading the jit/dex linked list, set an uplimit of entries read from the
-// linked list.
-static constexpr size_t MAX_LINKLED_LIST_LENGTH = 1024u;
-
// If the size of a symfile is larger than EXPECTED_MAX_SYMFILE_SIZE, we don't want to read it
// remotely.
static constexpr size_t MAX_JIT_SYMFILE_SIZE = 1024 * 1024u;
+// It takes about 30us-130us on Pixel (depending on the cpu frequency) to check if the descriptors
+// have been updated (most time spent in process_vm_preadv). We want to know if the JIT debug info
+// changed as soon as possible, while not wasting too much time checking for updates. So use a
+// period of 100 ms.
+// In system wide profiling, we may need to check JIT debug info changes for many processes, to
+// avoid spending all time checking, wait 100 ms between any two checks.
+static constexpr size_t kUpdateJITDebugInfoIntervalInMs = 100;
+
// Match the format of JITDescriptor in art/runtime/jit/debugger_itnerface.cc.
template <typename ADDRT>
struct JITDescriptor {
@@ -114,27 +118,117 @@
#endif
static_assert(sizeof(JITCodeEntry64) == 40, "");
-JITDebugReader::JITDebugReader(pid_t pid, bool keep_symfiles)
- : pid_(pid),
- keep_symfiles_(keep_symfiles),
- initialized_(false) {
- TryInit();
+bool JITDebugReader::RegisterSymFileCallback(IOEventLoop* loop,
+ const symfile_callback_t& callback) {
+ symfile_callback_ = callback;
+ read_event_ = loop->AddPeriodicEvent(SecondToTimeval(kUpdateJITDebugInfoIntervalInMs / 1000.0),
+ [this]() { return ReadAllProcesses(); });
+ return (read_event_ != nullptr && IOEventLoop::DisableEvent(read_event_));
}
-void JITDebugReader::ReadUpdate(std::vector<JITSymFile>* new_jit_symfiles,
- std::vector<DexSymFile>* new_dex_symfiles) {
- if (!TryInit()) {
+bool JITDebugReader::MonitorProcess(pid_t pid) {
+ if (processes_.find(pid) == processes_.end()) {
+ processes_[pid].pid = pid;
+ LOG(DEBUG) << "Start monitoring process " << pid;
+ if (processes_.size() == 1u) {
+ if (!IOEventLoop::EnableEvent(read_event_)) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+static bool IsArtLib(const std::string& filename) {
+ return android::base::EndsWith(filename, "libart.so") ||
+ android::base::EndsWith(filename, "libartd.so");
+}
+
+bool JITDebugReader::UpdateRecord(const Record* record) {
+ if (record->type() == PERF_RECORD_MMAP) {
+ auto r = static_cast<const MmapRecord*>(record);
+ if (IsArtLib(r->filename)) {
+ pids_with_art_lib_.emplace(r->data->pid, false);
+ }
+ } else if (record->type() == PERF_RECORD_MMAP2) {
+ auto r = static_cast<const Mmap2Record*>(record);
+ if (IsArtLib(r->filename)) {
+ pids_with_art_lib_.emplace(r->data->pid, false);
+ }
+ } else if (record->type() == PERF_RECORD_FORK) {
+ auto r = static_cast<const ForkRecord*>(record);
+ if (r->data->pid != r->data->ppid &&
+ pids_with_art_lib_.find(r->data->ppid) != pids_with_art_lib_.end()) {
+ pids_with_art_lib_.emplace(r->data->pid, false);
+ }
+ } else if (record->type() == PERF_RECORD_SAMPLE) {
+ auto r = static_cast<const SampleRecord*>(record);
+ auto it = pids_with_art_lib_.find(r->tid_data.pid);
+ if (it != pids_with_art_lib_.end() && !it->second) {
+ it->second = true;
+ if (!MonitorProcess(r->tid_data.pid)) {
+ return false;
+ }
+ return ReadProcess(r->tid_data.pid);
+ }
+ }
+ return true;
+}
+
+bool JITDebugReader::ReadAllProcesses() {
+ if (!IOEventLoop::DisableEvent(read_event_)) {
+ return false;
+ }
+ std::vector<JITSymFile> jit_symfiles;
+ std::vector<DexSymFile> dex_symfiles;
+ for (auto it = processes_.begin(); it != processes_.end();) {
+ Process& process = it->second;
+ ReadProcess(process, &jit_symfiles, &dex_symfiles);
+ if (process.died) {
+ LOG(DEBUG) << "Stop monitoring process " << process.pid;
+ it = processes_.erase(it);
+ } else {
+ ++it;
+ }
+ }
+ if (!jit_symfiles.empty() || !dex_symfiles.empty()) {
+ if (!symfile_callback_(jit_symfiles, dex_symfiles, true)) {
+ return false;
+ }
+ }
+ if (!processes_.empty()) {
+ return IOEventLoop::EnableEvent(read_event_);
+ }
+ return true;
+}
+
+bool JITDebugReader::ReadProcess(pid_t pid) {
+ auto it = processes_.find(pid);
+ if (it != processes_.end()) {
+ std::vector<JITSymFile> jit_symfiles;
+ std::vector<DexSymFile> dex_symfiles;
+ ReadProcess(it->second, &jit_symfiles, &dex_symfiles);
+ if (!jit_symfiles.empty() || !dex_symfiles.empty()) {
+ return symfile_callback_(jit_symfiles, dex_symfiles, false);
+ }
+ }
+ return true;
+}
+
+void JITDebugReader::ReadProcess(Process& process, std::vector<JITSymFile>* jit_symfiles,
+ std::vector<DexSymFile>* dex_symfiles) {
+ if (process.died || (!process.initialized && !InitializeProcess(process))) {
return;
}
// 1. Read descriptors.
Descriptor jit_descriptor;
Descriptor dex_descriptor;
- if (!ReadDescriptors(&jit_descriptor, &dex_descriptor)) {
+ if (!ReadDescriptors(process, &jit_descriptor, &dex_descriptor)) {
return;
}
// 2. Return if descriptors are not changed.
- if (jit_descriptor.action_seqlock == last_jit_descriptor_.action_seqlock &&
- dex_descriptor.action_seqlock == last_dex_descriptor_.action_seqlock) {
+ if (jit_descriptor.action_seqlock == process.last_jit_descriptor.action_seqlock &&
+ dex_descriptor.action_seqlock == process.last_dex_descriptor.action_seqlock) {
return;
}
@@ -142,7 +236,7 @@
auto check_descriptor = [&](Descriptor& descriptor, bool is_jit) {
Descriptor tmp_jit_descriptor;
Descriptor tmp_dex_descriptor;
- if (!ReadDescriptors(&tmp_jit_descriptor, &tmp_dex_descriptor)) {
+ if (!ReadDescriptors(process, &tmp_jit_descriptor, &tmp_dex_descriptor)) {
return false;
}
if (is_jit) {
@@ -155,82 +249,93 @@
bool is_jit) {
bool has_update = new_descriptor.action_seqlock != old_descriptor.action_seqlock &&
(new_descriptor.action_seqlock & 1) == 0;
+ LOG(DEBUG) << (is_jit ? "JIT" : "Dex") << " symfiles of pid " << process.pid
+ << ": old seqlock " << old_descriptor.action_seqlock
+ << ", new seqlock " << new_descriptor.action_seqlock;
if (!has_update) {
return false;
}
std::vector<CodeEntry> new_entries;
- if (!ReadNewCodeEntries(new_descriptor, old_descriptor.action_timestamp, &new_entries)) {
+ // Adding or removing one code entry will make two increments of action_seqlock. So we should
+ // not read more than (seqlock_diff / 2) new entries.
+ uint32_t read_entry_limit = (new_descriptor.action_seqlock - old_descriptor.action_seqlock) / 2;
+ if (!ReadNewCodeEntries(process, new_descriptor, old_descriptor.action_timestamp,
+ read_entry_limit, &new_entries)) {
return false;
}
// Check if the descriptor was changed while we were reading new entries.
if (!check_descriptor(new_descriptor, is_jit)) {
return false;
}
+ LOG(DEBUG) << (is_jit ? "JIT" : "Dex") << " symfiles of pid " << process.pid
+ << ": read " << new_entries.size() << " new entries";
if (new_entries.empty()) {
return true;
}
if (is_jit) {
- ReadJITSymFiles(new_entries, new_jit_symfiles);
+ ReadJITSymFiles(process, new_entries, jit_symfiles);
} else {
- ReadDexSymFiles(new_entries, new_dex_symfiles);
- }
- // Check if the descriptor was changed while we were reading symfiles.
- if (!check_descriptor(new_descriptor, is_jit)) {
- if (is_jit) {
- new_jit_symfiles->clear();
- } else {
- new_dex_symfiles->clear();
- }
- return false;
+ ReadDexSymFiles(process, new_entries, dex_symfiles);
}
return true;
};
- if (read_new_symfiles(jit_descriptor, last_jit_descriptor_, true)) {
- last_jit_descriptor_ = jit_descriptor;
+ if (read_new_symfiles(jit_descriptor, process.last_jit_descriptor, true)) {
+ process.last_jit_descriptor = jit_descriptor;
}
- if (read_new_symfiles(dex_descriptor, last_dex_descriptor_, false)) {
- last_dex_descriptor_ = dex_descriptor;
+ if (read_new_symfiles(dex_descriptor, process.last_dex_descriptor, false)) {
+ process.last_dex_descriptor = dex_descriptor;
}
}
-bool JITDebugReader::TryInit() {
- if (initialized_) {
- return true;
- }
+bool JITDebugReader::InitializeProcess(Process& process) {
// 1. Read map file to find the location of libart.so.
std::vector<ThreadMmap> thread_mmaps;
- if (!GetThreadMmapsInProcess(pid_, &thread_mmaps)) {
+ if (!GetThreadMmapsInProcess(process.pid, &thread_mmaps)) {
+ process.died = true;
return false;
}
std::string art_lib_path;
+ uint64_t min_vaddr_in_memory;
for (auto& map : thread_mmaps) {
- if (android::base::EndsWith(map.name, "libart.so")) {
+ if (map.executable && IsArtLib(map.name)) {
art_lib_path = map.name;
+ min_vaddr_in_memory = map.start_addr;
break;
}
}
if (art_lib_path.empty()) {
return false;
}
- is_64bit_ = art_lib_path.find("lib64") != std::string::npos;
+ process.is_64bit = art_lib_path.find("lib64") != std::string::npos;
// 2. Read libart.so to find the addresses of __jit_debug_descriptor and __dex_debug_descriptor.
+ const DescriptorsLocation* location = GetDescriptorsLocation(art_lib_path, process.is_64bit);
+ if (location == nullptr) {
+ return false;
+ }
+ process.descriptors_addr = location->relative_addr + min_vaddr_in_memory;
+ process.descriptors_size = location->size;
+ process.jit_descriptor_offset = location->jit_descriptor_offset;
+ process.dex_descriptor_offset = location->dex_descriptor_offset;
+ process.initialized = true;
+ return true;
+}
+
+const JITDebugReader::DescriptorsLocation* JITDebugReader::GetDescriptorsLocation(
+ const std::string& art_lib_path, bool is_64bit) {
+ auto it = descriptors_location_cache_.find(art_lib_path);
+ if (it != descriptors_location_cache_.end()) {
+ return it->second.relative_addr == 0u ? nullptr : &it->second;
+ }
+ DescriptorsLocation& location = descriptors_location_cache_[art_lib_path];
+
+ // Read libart.so to find the addresses of __jit_debug_descriptor and __dex_debug_descriptor.
uint64_t min_vaddr_in_file;
ElfStatus status = ReadMinExecutableVirtualAddressFromElfFile(art_lib_path, BuildId(),
- &min_vaddr_in_file);
+ &min_vaddr_in_file);
if (status != ElfStatus::NO_ERROR) {
LOG(ERROR) << "ReadMinExecutableVirtualAddress failed, status = " << status;
- return false;
- }
- uint64_t min_vaddr_in_memory = 0u;
- for (auto& map : thread_mmaps) {
- if (map.executable && map.name == art_lib_path) {
- min_vaddr_in_memory = map.start_addr;
- break;
- }
- }
- if (min_vaddr_in_memory == 0u) {
- return false;
+ return nullptr;
}
const char* jit_str = "__jit_debug_descriptor";
const char* dex_str = "__dex_debug_descriptor";
@@ -239,68 +344,74 @@
auto callback = [&](const ElfFileSymbol& symbol) {
if (symbol.name == jit_str) {
- jit_addr = symbol.vaddr - min_vaddr_in_file + min_vaddr_in_memory;
+ jit_addr = symbol.vaddr - min_vaddr_in_file;
} else if (symbol.name == dex_str) {
- dex_addr = symbol.vaddr - min_vaddr_in_file + min_vaddr_in_memory;
+ dex_addr = symbol.vaddr - min_vaddr_in_file;
}
};
if (ParseDynamicSymbolsFromElfFile(art_lib_path, callback) != ElfStatus::NO_ERROR) {
- return false;
+ return nullptr;
}
if (jit_addr == 0u || dex_addr == 0u) {
- return false;
+ return nullptr;
}
- descriptors_addr_ = std::min(jit_addr, dex_addr);
- descriptors_size_ = std::max(jit_addr, dex_addr) +
- (is_64bit_ ? sizeof(JITDescriptor64) : sizeof(JITDescriptor32)) - descriptors_addr_;
- if (descriptors_size_ >= 4096u) {
- PLOG(WARNING) << "The descriptors_size is unexpected large: " << descriptors_size_;
+ location.relative_addr = std::min(jit_addr, dex_addr);
+ location.size = std::max(jit_addr, dex_addr) +
+ (is_64bit ? sizeof(JITDescriptor64) : sizeof(JITDescriptor32)) - location.relative_addr;
+ if (location.size >= 4096u) {
+ PLOG(WARNING) << "The descriptors_size is unexpected large: " << location.size;
}
- descriptors_buf_.resize(descriptors_size_);
- jit_descriptor_offset_ = jit_addr - descriptors_addr_;
- dex_descriptor_offset_ = dex_addr - descriptors_addr_;
- initialized_ = true;
- return true;
+ if (descriptors_buf_.size() < location.size) {
+ descriptors_buf_.resize(location.size);
+ }
+ location.jit_descriptor_offset = jit_addr - location.relative_addr;
+ location.dex_descriptor_offset = dex_addr - location.relative_addr;
+ return &location;
}
-bool JITDebugReader::ReadRemoteMem(uint64_t remote_addr, uint64_t size, void* data) {
+bool JITDebugReader::ReadRemoteMem(Process& process, uint64_t remote_addr, uint64_t size,
+ void* data) {
iovec local_iov;
local_iov.iov_base = data;
local_iov.iov_len = size;
iovec remote_iov;
remote_iov.iov_base = reinterpret_cast<void*>(static_cast<uintptr_t>(remote_addr));
remote_iov.iov_len = size;
- ssize_t result = process_vm_readv(pid_, &local_iov, 1, &remote_iov, 1, 0);
+ ssize_t result = process_vm_readv(process.pid, &local_iov, 1, &remote_iov, 1, 0);
if (static_cast<size_t>(result) != size) {
- PLOG(DEBUG) << "ReadRemoteMem(" << " pid " << pid_ << ", addr " << std::hex
+ PLOG(DEBUG) << "ReadRemoteMem(" << " pid " << process.pid << ", addr " << std::hex
<< remote_addr << ", size " << size << ") failed";
+ process.died = true;
return false;
}
return true;
}
-bool JITDebugReader::ReadDescriptors(Descriptor* jit_descriptor, Descriptor* dex_descriptor) {
- if (!ReadRemoteMem(descriptors_addr_, descriptors_size_, descriptors_buf_.data())) {
+bool JITDebugReader::ReadDescriptors(Process& process, Descriptor* jit_descriptor,
+ Descriptor* dex_descriptor) {
+ if (!ReadRemoteMem(process, process.descriptors_addr, process.descriptors_size,
+ descriptors_buf_.data())) {
return false;
}
- return LoadDescriptor(descriptors_buf_.data() + jit_descriptor_offset_, jit_descriptor) &&
- LoadDescriptor(descriptors_buf_.data() + dex_descriptor_offset_, dex_descriptor);
+ return LoadDescriptor(process.is_64bit, &descriptors_buf_[process.jit_descriptor_offset],
+ jit_descriptor) &&
+ LoadDescriptor(process.is_64bit, &descriptors_buf_[process.dex_descriptor_offset],
+ dex_descriptor);
}
-bool JITDebugReader::LoadDescriptor(const char* data, Descriptor* descriptor) {
- if (is_64bit_) {
- return LoadDescriptorImpl<JITDescriptor64>(data, descriptor);
+bool JITDebugReader::LoadDescriptor(bool is_64bit, const char* data, Descriptor* descriptor) {
+ if (is_64bit) {
+ return LoadDescriptorImpl<JITDescriptor64, JITCodeEntry64>(data, descriptor);
}
- return LoadDescriptorImpl<JITDescriptor32>(data, descriptor);
+ return LoadDescriptorImpl<JITDescriptor32, JITCodeEntry32>(data, descriptor);
}
-template <typename DescriptorT>
+template <typename DescriptorT, typename CodeEntryT>
bool JITDebugReader::LoadDescriptorImpl(const char* data, Descriptor* descriptor) {
DescriptorT raw_descriptor;
MoveFromBinaryFormat(raw_descriptor, data);
if (!raw_descriptor.Valid() || sizeof(raw_descriptor) != raw_descriptor.sizeof_descriptor ||
- (is_64bit_ ? sizeof(JITCodeEntry64) : sizeof(JITCodeEntry32)) != raw_descriptor.sizeof_entry
- ) {
+ sizeof(CodeEntryT) != raw_descriptor.sizeof_entry) {
return false;
}
descriptor->action_seqlock = raw_descriptor.action_seqlock;
@@ -312,33 +423,32 @@
// Read new code entries with timestamp > last_action_timestamp.
// Since we don't stop the app process while reading code entries, it is possible we are reading
// broken data. So return false once we detect that the data is broken.
-bool JITDebugReader::ReadNewCodeEntries(const Descriptor& descriptor,
- uint64_t last_action_timestamp,
+bool JITDebugReader::ReadNewCodeEntries(Process& process, const Descriptor& descriptor,
+ uint64_t last_action_timestamp, uint32_t read_entry_limit,
std::vector<CodeEntry>* new_code_entries) {
- if (is_64bit_) {
- return ReadNewCodeEntriesImpl<JITDescriptor64, JITCodeEntry64>(descriptor,
- last_action_timestamp,
- new_code_entries);
+ if (process.is_64bit) {
+ return ReadNewCodeEntriesImpl<JITDescriptor64, JITCodeEntry64>(
+ process, descriptor, last_action_timestamp, read_entry_limit, new_code_entries);
}
- return ReadNewCodeEntriesImpl<JITDescriptor32, JITCodeEntry32>(descriptor,
- last_action_timestamp,
- new_code_entries);
+ return ReadNewCodeEntriesImpl<JITDescriptor32, JITCodeEntry32>(
+ process, descriptor, last_action_timestamp, read_entry_limit, new_code_entries);
}
template <typename DescriptorT, typename CodeEntryT>
-bool JITDebugReader::ReadNewCodeEntriesImpl(const Descriptor& descriptor,
+bool JITDebugReader::ReadNewCodeEntriesImpl(Process& process, const Descriptor& descriptor,
uint64_t last_action_timestamp,
+ uint32_t read_entry_limit,
std::vector<CodeEntry>* new_code_entries) {
uint64_t current_entry_addr = descriptor.first_entry_addr;
uint64_t prev_entry_addr = 0u;
std::unordered_set<uint64_t> entry_addr_set;
- for (size_t i = 0u; i < MAX_LINKLED_LIST_LENGTH && current_entry_addr != 0u; ++i) {
+ for (size_t i = 0u; i < read_entry_limit && current_entry_addr != 0u; ++i) {
if (entry_addr_set.find(current_entry_addr) != entry_addr_set.end()) {
// We enter a loop, which means a broken linked list.
return false;
}
CodeEntryT entry;
- if (!ReadRemoteMem(current_entry_addr, sizeof(entry), &entry)) {
+ if (!ReadRemoteMem(process, current_entry_addr, sizeof(entry), &entry)) {
return false;
}
if (entry.prev_addr != prev_entry_addr || !entry.Valid()) {
@@ -362,7 +472,7 @@
return true;
}
-void JITDebugReader::ReadJITSymFiles(const std::vector<CodeEntry>& jit_entries,
+void JITDebugReader::ReadJITSymFiles(Process& process, const std::vector<CodeEntry>& jit_entries,
std::vector<JITSymFile>* jit_symfiles) {
std::vector<char> data;
for (auto& jit_entry : jit_entries) {
@@ -372,7 +482,7 @@
if (data.size() < jit_entry.symfile_size) {
data.resize(jit_entry.symfile_size);
}
- if (!ReadRemoteMem(jit_entry.symfile_addr, jit_entry.symfile_size, data.data())) {
+ if (!ReadRemoteMem(process, jit_entry.symfile_addr, jit_entry.symfile_size, data.data())) {
continue;
}
if (!IsValidElfFileMagic(data.data(), jit_entry.symfile_size)) {
@@ -398,18 +508,15 @@
if (keep_symfiles_) {
tmp_file->DoNotRemove();
}
- JITSymFile symfile;
- symfile.addr = min_addr;
- symfile.len = max_addr - min_addr;
- symfile.file_path = tmp_file->path;
- jit_symfiles->push_back(symfile);
+ jit_symfiles->emplace_back(process.pid, min_addr, max_addr - min_addr, tmp_file->path);
}
}
-void JITDebugReader::ReadDexSymFiles(const std::vector<CodeEntry>& dex_entries,
+void JITDebugReader::ReadDexSymFiles(Process& process, const std::vector<CodeEntry>& dex_entries,
std::vector<DexSymFile>* dex_symfiles) {
std::vector<ThreadMmap> thread_mmaps;
- if (!GetThreadMmapsInProcess(pid_, &thread_mmaps)) {
+ if (!GetThreadMmapsInProcess(process.pid, &thread_mmaps)) {
+ process.died = true;
return;
}
auto comp = [](const ThreadMmap& map, uint64_t addr) {
diff --git a/simpleperf/JITDebugReader.h b/simpleperf/JITDebugReader.h
index dbf5b76..8561461 100644
--- a/simpleperf/JITDebugReader.h
+++ b/simpleperf/JITDebugReader.h
@@ -22,18 +22,27 @@
#include <functional>
#include <memory>
#include <stack>
+#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <android-base/logging.h>
#include <android-base/test_utils.h>
+#include "IOEventLoop.h"
+#include "record.h"
+
namespace simpleperf {
struct JITSymFile {
+ pid_t pid; // The process having the JITed code
uint64_t addr; // The start addr of the JITed code
uint64_t len; // The length of the JITed code
std::string file_path; // The path of a temporary ELF file storing debug info of the JITed code
+
+ JITSymFile() {}
+ JITSymFile(pid_t pid, uint64_t addr, uint64_t len, const std::string& file_path)
+ : pid(pid), addr(addr), len(len), file_path(file_path) {}
};
struct DexSymFile {
@@ -45,16 +54,23 @@
: dex_file_offset(dex_file_offset), file_path(file_path) {}
};
+// JITDebugReader reads debug info of JIT code and dex files of processes using ART. The
+// corresponding debug interface in ART is at art/runtime/jit/debugger_interface.cc.
class JITDebugReader {
public:
- JITDebugReader(pid_t pid, bool keep_symfiles);
+ JITDebugReader(bool keep_symfiles) : keep_symfiles_(keep_symfiles) {}
- pid_t Pid() const {
- return pid_;
- }
+ typedef std::function<bool(const std::vector<JITSymFile>&, const std::vector<DexSymFile>&, bool)>
+ symfile_callback_t;
+ bool RegisterSymFileCallback(IOEventLoop* loop, const symfile_callback_t& callback);
- void ReadUpdate(std::vector<JITSymFile>* new_jit_symfiles,
- std::vector<DexSymFile>* new_dex_symfiles);
+ // There are two ways to select which processes to monitor. One is using MonitorProcess(), the
+ // other is finding all processes having libart.so using records.
+ bool MonitorProcess(pid_t pid);
+ bool UpdateRecord(const Record* record);
+
+ // Read new debug info from all monitored processes.
+ bool ReadAllProcesses();
private:
@@ -73,42 +89,69 @@
uint64_t timestamp; // CLOCK_MONOTONIC time of last action
};
- bool TryInit();
- bool ReadRemoteMem(uint64_t remote_addr, uint64_t size, void* data);
- bool ReadDescriptors(Descriptor* jit_descriptor, Descriptor* dex_descriptor);
- bool LoadDescriptor(const char* data, Descriptor* descriptor);
- template <typename DescriptorT>
+ struct Process {
+ pid_t pid = -1;
+ bool initialized = false;
+ bool died = false;
+ bool is_64bit = false;
+ // The jit descriptor and dex descriptor can be read in one process_vm_readv() call.
+ uint64_t descriptors_addr = 0;
+ uint64_t descriptors_size = 0;
+ // offset relative to descriptors_addr
+ uint64_t jit_descriptor_offset = 0;
+ // offset relative to descriptors_addr
+ uint64_t dex_descriptor_offset = 0;
+
+ // The state we know about the remote jit debug descriptor.
+ Descriptor last_jit_descriptor;
+ // The state we know about the remote dex debug descriptor.
+ Descriptor last_dex_descriptor;
+ };
+
+ // The location of descriptors in libart.so.
+ struct DescriptorsLocation {
+ uint64_t relative_addr = 0;
+ uint64_t size = 0;
+ uint64_t jit_descriptor_offset = 0;
+ uint64_t dex_descriptor_offset = 0;
+ };
+
+ bool ReadProcess(pid_t pid);
+ void ReadProcess(Process& process, std::vector<JITSymFile>* jit_symfiles,
+ std::vector<DexSymFile>* dex_symfiles);
+ bool InitializeProcess(Process& process);
+ const DescriptorsLocation* GetDescriptorsLocation(const std::string& art_lib_path,
+ bool is_64bit);
+ bool ReadRemoteMem(Process& process, uint64_t remote_addr, uint64_t size, void* data);
+ bool ReadDescriptors(Process& process, Descriptor* jit_descriptor, Descriptor* dex_descriptor);
+ bool LoadDescriptor(bool is_64bit, const char* data, Descriptor* descriptor);
+ template <typename DescriptorT, typename CodeEntryT>
bool LoadDescriptorImpl(const char* data, Descriptor* descriptor);
- bool ReadNewCodeEntries(const Descriptor& descriptor, uint64_t last_action_timestamp,
+ bool ReadNewCodeEntries(Process& process, const Descriptor& descriptor,
+ uint64_t last_action_timestamp, uint32_t read_entry_limit,
std::vector<CodeEntry>* new_code_entries);
template <typename DescriptorT, typename CodeEntryT>
- bool ReadNewCodeEntriesImpl(const Descriptor& descriptor, uint64_t last_action_timestamp,
+ bool ReadNewCodeEntriesImpl(Process& process, const Descriptor& descriptor,
+ uint64_t last_action_timestamp, uint32_t read_entry_limit,
std::vector<CodeEntry>* new_code_entries);
- void ReadJITSymFiles(const std::vector<CodeEntry>& jit_entries,
+ void ReadJITSymFiles(Process& process, const std::vector<CodeEntry>& jit_entries,
std::vector<JITSymFile>* jit_symfiles);
- void ReadDexSymFiles(const std::vector<CodeEntry>& dex_entries,
+ void ReadDexSymFiles(Process& process, const std::vector<CodeEntry>& dex_entries,
std::vector<DexSymFile>* dex_symfiles);
- pid_t pid_;
- bool keep_symfiles_;
- bool initialized_;
- bool is_64bit_;
+ bool keep_symfiles_ = false;
+ IOEventRef read_event_ = nullptr;
+ symfile_callback_t symfile_callback_;
- // The jit descriptor and dex descriptor can be read in one process_vm_readv() call.
- uint64_t descriptors_addr_;
- uint64_t descriptors_size_;
+ // Keys are pids of processes having libart.so, values show whether a process has been monitored.
+ std::unordered_map<pid_t, bool> pids_with_art_lib_;
+
+ // All monitored processes
+ std::unordered_map<pid_t, Process> processes_;
+ std::unordered_map<std::string, DescriptorsLocation> descriptors_location_cache_;
std::vector<char> descriptors_buf_;
- // offset relative to descriptors_addr
- uint64_t jit_descriptor_offset_;
- // offset relative to descriptors_addr
- uint64_t dex_descriptor_offset_;
-
- // The state we know about the remote jit debug descriptor.
- Descriptor last_jit_descriptor_;
- // The state we know about the remote dex debug descriptor.
- Descriptor last_dex_descriptor_;
};
} //namespace simpleperf
diff --git a/simpleperf/cmd_record.cpp b/simpleperf/cmd_record.cpp
index 98cbcb4..5664f0b 100644
--- a/simpleperf/cmd_record.cpp
+++ b/simpleperf/cmd_record.cpp
@@ -259,7 +259,8 @@
bool SaveRecordForPostUnwinding(Record* record);
bool SaveRecordAfterUnwinding(Record* record);
bool SaveRecordWithoutUnwinding(Record* record);
- bool UpdateJITDebugInfo();
+ bool ProcessJITDebugInfo(const std::vector<JITSymFile>& jit_symfiles,
+ const std::vector<DexSymFile>& dex_symfiles, bool sync_kernel_records);
void UpdateRecordForEmbeddedPath(Record* record);
bool UnwindRecord(SampleRecord& r);
@@ -416,21 +417,12 @@
need_to_check_targets = true;
}
// Profiling JITed/interpreted Java code is supported starting from Android P.
- if (!system_wide_collection_ && !app_package_name_.empty() &&
- GetAndroidVersion() >= kAndroidVersionP) {
- pid_t app_pid = 0;
- // TODO: support a JITDebugReader for each app process?
- if (!event_selection_set_.GetMonitoredProcesses().empty()) {
- app_pid = *event_selection_set_.GetMonitoredProcesses().begin();
- } else if (!event_selection_set_.GetMonitoredThreads().empty()) {
- app_pid = *event_selection_set_.GetMonitoredThreads().begin();
- }
- CHECK_NE(app_pid, 0);
+ if (GetAndroidVersion() >= kAndroidVersionP) {
// JIT symfiles are stored in temporary files, and are deleted after recording. But if
// `-g --no-unwind` option is used, we want to keep symfiles to support unwinding in
// the debug-unwind cmd.
bool keep_symfiles = dwarf_callchain_sampling_ && !unwind_dwarf_callchain_;
- jit_debug_reader_.reset(new JITDebugReader(app_pid, keep_symfiles));
+ jit_debug_reader_.reset(new JITDebugReader(keep_symfiles));
// To profile java code, need to dump maps containing vdex files, which are not executable.
event_selection_set_.SetRecordNotExecutableMaps(true);
}
@@ -478,18 +470,29 @@
}
}
if (jit_debug_reader_) {
- // Update JIT info at the beginning of recording.
- if (!UpdateJITDebugInfo()) {
+ auto callback = [this](const std::vector<JITSymFile>& jit_symfiles,
+ const std::vector<DexSymFile>& dex_symfiles, bool sync_kernel_records) {
+ return ProcessJITDebugInfo(jit_symfiles, dex_symfiles, sync_kernel_records);
+ };
+ if (!jit_debug_reader_->RegisterSymFileCallback(loop, callback)) {
return false;
}
- // It takes about 30us-130us on Pixel (depending on the cpu frequency) to check update when
- // no update happens (most time spent in process_vm_preadv). We want to know the JIT debug
- // info change as soon as possible, while not wasting too much time checking updates. So use
- // a period of 100 ms.
- const double kUpdateJITDebugInfoPeriodInSecond = 0.1;
- if (!loop->AddPeriodicEvent(SecondToTimeval(kUpdateJITDebugInfoPeriodInSecond),
- [&]() { return UpdateJITDebugInfo(); })) {
- return false;
+ if (!app_package_name_.empty()) {
+ std::set<pid_t> pids = event_selection_set_.GetMonitoredProcesses();
+ for (pid_t tid : event_selection_set_.GetMonitoredThreads()) {
+ pid_t pid;
+ if (GetProcessForThread(tid, &pid)) {
+ pids.insert(pid);
+ }
+ }
+ for (pid_t pid : pids) {
+ if (!jit_debug_reader_->MonitorProcess(pid)) {
+ return false;
+ }
+ }
+ if (!jit_debug_reader_->ReadAllProcesses()) {
+ return false;
+ }
}
}
return true;
@@ -1048,7 +1051,10 @@
return event_selection_set_.GetIOEventLoop()->ExitLoop();
}
}
- last_record_timestamp_ = record->Timestamp();
+ if (jit_debug_reader_ && !jit_debug_reader_->UpdateRecord(record)) {
+ return false;
+ }
+ last_record_timestamp_ = std::max(last_record_timestamp_, record->Timestamp());
if (unwind_dwarf_callchain_) {
if (post_unwind_) {
return SaveRecordForPostUnwinding(record);
@@ -1139,16 +1145,12 @@
return record_file_writer_->WriteRecord(*record);
}
-bool RecordCommand::UpdateJITDebugInfo() {
- std::vector<JITSymFile> jit_symfiles;
- std::vector<DexSymFile> dex_symfiles;
- jit_debug_reader_->ReadUpdate(&jit_symfiles, &dex_symfiles);
- if (jit_symfiles.empty() && dex_symfiles.empty()) {
- return true;
- }
+bool RecordCommand::ProcessJITDebugInfo(const std::vector<JITSymFile>& jit_symfiles,
+ const std::vector<DexSymFile>& dex_symfiles,
+ bool sync_kernel_records) {
EventAttrWithId attr_id = event_selection_set_.GetEventAttrWithId()[0];
for (auto& symfile : jit_symfiles) {
- Mmap2Record record(*attr_id.attr, false, jit_debug_reader_->Pid(), jit_debug_reader_->Pid(),
+ Mmap2Record record(*attr_id.attr, false, symfile.pid, symfile.pid,
symfile.addr, symfile.len, 0, map_flags::PROT_JIT_SYMFILE_MAP,
symfile.file_path, attr_id.ids[0], last_record_timestamp_);
if (!ProcessRecord(&record)) {
@@ -1162,7 +1164,7 @@
// generated after them. So process existing samples each time generating new JIT maps. We prefer
// to process samples after processing JIT maps. Because some of the samples may hit the new JIT
// maps, and we want to report them properly.
- if (!event_selection_set_.ReadMmapEventData()) {
+ if (sync_kernel_records && !event_selection_set_.ReadMmapEventData()) {
return false;
}
return true;
@@ -1175,6 +1177,13 @@
return;
}
std::string filename = r.filename;
+ bool name_changed = false;
+ // Some vdex files in map files are marked with deleted flag, but they exist in the file system.
+ // It may be because a new file is used to replace the old one, but still worth to try.
+ if (android::base::EndsWith(filename, " (deleted)")) {
+ filename.resize(filename.size() - 10);
+ name_changed = true;
+ }
if (r.data->pgoff != 0) {
// For the case of a shared library "foobar.so" embedded
// inside an APK, we rewrite the original MMAP from
@@ -1198,8 +1207,12 @@
std::string zip_path;
std::string entry_name;
if (ParseExtractedInMemoryPath(filename, &zip_path, &entry_name)) {
+ filename = GetUrlInApk(zip_path, entry_name);
+ name_changed = true;
+ }
+ if (name_changed) {
auto data = *r.data;
- r.SetDataAndFilename(data, GetUrlInApk(zip_path, entry_name));
+ r.SetDataAndFilename(data, filename);
}
}
diff --git a/simpleperf/dso.cpp b/simpleperf/dso.cpp
index 223866d..a9c86f4 100644
--- a/simpleperf/dso.cpp
+++ b/simpleperf/dso.cpp
@@ -333,7 +333,12 @@
: Dso(DSO_DEX_FILE, path, debug_file_path) {}
void AddDexFileOffset(uint64_t dex_file_offset) override {
- dex_file_offsets_.push_back(dex_file_offset);
+ auto it = std::lower_bound(dex_file_offsets_.begin(), dex_file_offsets_.end(),
+ dex_file_offset);
+ if (it != dex_file_offsets_.end() && *it == dex_file_offset) {
+ return;
+ }
+ dex_file_offsets_.insert(it, dex_file_offset);
}
const std::vector<uint64_t>* DexFileOffsets() override {
diff --git a/simpleperf/dso_test.cpp b/simpleperf/dso_test.cpp
index 75d4c85..a1d005a 100644
--- a/simpleperf/dso_test.cpp
+++ b/simpleperf/dso_test.cpp
@@ -92,6 +92,15 @@
#endif // defined(__linux__)
}
+TEST(dso, dex_file_offsets) {
+ std::unique_ptr<Dso> dso = Dso::CreateDso(DSO_DEX_FILE, "");
+ ASSERT_TRUE(dso);
+ for (uint64_t offset : {0x3, 0x1, 0x5, 0x4, 0x2, 0x4, 0x3}) {
+ dso->AddDexFileOffset(offset);
+ }
+ ASSERT_EQ(*dso->DexFileOffsets(), std::vector<uint64_t>({0x1, 0x2, 0x3, 0x4, 0x5}));
+}
+
TEST(dso, embedded_elf) {
const std::string file_path = GetUrlInApk(GetTestData(APK_FILE), NATIVELIB_IN_APK);
std::unique_ptr<Dso> dso = Dso::CreateDso(DSO_ELF_FILE, file_path);
diff --git a/simpleperf/scripts/annotate.py b/simpleperf/scripts/annotate.py
index 066e14e..30cfce1 100644
--- a/simpleperf/scripts/annotate.py
+++ b/simpleperf/scripts/annotate.py
@@ -24,14 +24,15 @@
import os.path
import shutil
import subprocess
-import sys
-from simpleperf_report_lib import *
-from utils import *
+from simpleperf_report_lib import ReportLib
+from utils import log_info, log_warning, log_exit, log_fatal
+from utils import bytes_to_str, extant_dir, find_tool_path, flatten_arg_list, is_windows
+from utils import ReadElf, str_to_bytes
class SourceLine(object):
- def __init__(self, file, function, line):
- self.file = file
+ def __init__(self, file_id, function, line):
+ self.file = file_id
self.function = function
self.line = line
@@ -56,31 +57,30 @@
"""collect information of how to map [dso_name,vaddr] to [source_file:line].
"""
def __init__(self, ndk_path, symfs_dir=None):
- self.dso_dict = dict()
+ self.dso_dict = {}
self.addr2line_path = find_tool_path('addr2line', ndk_path)
if self.addr2line_path is None:
log_exit("Can't find addr2line. Please set ndk path with --ndk-path option.")
self.readelf = ReadElf(ndk_path)
self.symfs_dir = symfs_dir
+ # store a list of source files
+ self.file_list = []
+ # map from file to id with file_list[id] == file
+ self.file_dict = {}
def add_addr(self, dso_name, addr):
dso = self.dso_dict.get(dso_name)
if dso is None:
- self.dso_dict[dso_name] = dso = dict()
+ self.dso_dict[dso_name] = dso = {}
if addr not in dso:
dso[addr] = None
-
def convert_addrs_to_lines(self):
- # store a list of source files
- self.file_list = []
- # map from file to id with file_list[id] == file
- self.file_dict = {}
self.file_list.append('')
self.file_dict[''] = 0
- for dso_name in self.dso_dict.keys():
+ for dso_name in self.dso_dict:
self._convert_addrs_to_lines(dso_name, self.dso_dict[dso_name])
self._combine_source_files()
@@ -121,30 +121,30 @@
items = stdoutdata[out_pos].rsplit(':', 1)
if len(items) != 2:
continue
- (file, line) = items
+ (file_path, line) = items
line = line.split()[0] # Remove comments after line number
out_pos += 1
- if '?' in file:
- file = 0
+ if '?' in file_path:
+ file_id = 0
else:
- file = self._get_file_id(file)
+ file_id = self._get_file_id(file_path)
if '?' in line:
line = 0
else:
line = int(line)
- source_lines.append(SourceLine(file, function, line))
+ source_lines.append(SourceLine(file_id, function, line))
dso[addrs[addr_pos]] = source_lines
addr_pos += 1
assert addr_pos == len(addrs)
- def _get_file_id(self, file):
- id = self.file_dict.get(file)
- if id is None:
- id = len(self.file_list)
- self.file_list.append(file)
- self.file_dict[file] = id
- return id
+ def _get_file_id(self, file_path):
+ file_id = self.file_dict.get(file_path)
+ if file_id is None:
+ file_id = len(self.file_list)
+ self.file_list.append(file_path)
+ self.file_dict[file_path] = file_id
+ return file_id
def _combine_source_files(self):
"""It is possible that addr2line gives us different names for the same
@@ -155,29 +155,29 @@
source files with no conflicts in path.
"""
# Collect files having the same filename.
- filename_dict = dict()
- for file in self.file_list:
- index = max(file.rfind('/'), file.rfind(os.sep))
- filename = file[index+1:]
+ filename_dict = {}
+ for file_path in self.file_list:
+ index = max(file_path.rfind('/'), file_path.rfind(os.sep))
+ filename = file_path[index+1:]
entry = filename_dict.get(filename)
if entry is None:
filename_dict[filename] = entry = []
- entry.append(file)
+ entry.append(file_path)
# Combine files having the same filename and having no conflicts in path.
- for filename in filename_dict.keys():
+ for filename in filename_dict:
files = filename_dict[filename]
if len(files) == 1:
continue
- for file in files:
- to_file = file
+ for file_path in files:
+ to_file = file_path
# Test if we can merge files[i] with another file having longer
# path.
for f in files:
- if len(f) > len(to_file) and f.find(file) != -1:
+ if len(f) > len(to_file) and f.find(file_path) != -1:
to_file = f
- if to_file != file:
- from_id = self.file_dict[file]
+ if to_file != file_path:
+ from_id = self.file_dict[file_path]
to_id = self.file_dict[to_file]
self.file_list[from_id] = self.file_list[to_id]
@@ -239,8 +239,8 @@
class FilePeriod(object):
"""Period for each source file"""
- def __init__(self, file):
- self.file = file
+ def __init__(self, file_id):
+ self.file = file_id
self.period = Period()
# Period for each line in the file.
self.line_dict = {}
@@ -283,12 +283,6 @@
kallsyms = 'binary_cache/kallsyms'
if not os.path.isfile(kallsyms):
kallsyms = None
- source_dirs = config['source_dirs']
- for dir in source_dirs:
- if not os.path.isdir(dir):
- log_exit('[source_dirs] "%s" is not a dir' % dir)
- if not config['source_dirs']:
- log_exit('Please set source directories.')
# init member variables
self.config = config
@@ -312,6 +306,10 @@
os.makedirs(output_dir)
self.addr2line = Addr2Line(self.config['ndk_path'], symfs_dir)
+ self.period = 0
+ self.dso_periods = {}
+ self.file_periods = {}
+ self.source_file_dict = {}
def annotate(self):
@@ -380,9 +378,6 @@
"""read perf.data, collect Period for all types:
binaries, source files, functions, lines.
"""
- self.period = 0
- self.dso_periods = dict()
- self.file_periods = dict()
for perf_data in self.config['perf_data_list']:
lib = ReportLib()
lib.SetRecordFile(perf_data)
@@ -397,49 +392,52 @@
break
if not self._filter_sample(sample):
continue
- symbols = []
- symbols.append(lib.GetSymbolOfCurrentSample())
- callchain = lib.GetCallChainOfCurrentSample()
- for i in range(callchain.nr):
- symbols.append(callchain.entries[i].symbol)
- # Each sample has a callchain, but its period is only used once
- # to add period for each function/source_line/source_file/binary.
- # For example, if more than one entry in the callchain hits a
- # function, the event count of that function is only increased once.
- # Otherwise, we may get periods > 100%.
- is_sample_used = False
- used_dso_dict = dict()
- used_file_dict = dict()
- used_function_dict = dict()
- used_line_dict = dict()
- period = Period(sample.period, sample.period)
- for i in range(len(symbols)):
- symbol = symbols[i]
- if i == 1:
- period = Period(0, sample.period)
- if not self._filter_symbol(symbol):
- continue
- is_sample_used = True
- # Add period to dso.
- self._add_dso_period(symbol.dso_name, period, used_dso_dict)
- # Add period to source file.
- sources = self.addr2line.get_sources(symbol.dso_name, symbol.vaddr_in_file)
- for source in sources:
- if source.file:
- self._add_file_period(source, period, used_file_dict)
- # Add period to line.
- if source.line:
- self._add_line_period(source, period, used_line_dict)
- # Add period to function.
- sources = self.addr2line.get_sources(symbol.dso_name, symbol.symbol_addr)
- for source in sources:
- if source.file:
- self._add_file_period(source, period, used_file_dict)
- if source.function:
- self._add_function_period(source, period, used_function_dict)
+ self._generate_periods_for_sample(lib, sample)
- if is_sample_used:
- self.period += sample.period
+
+ def _generate_periods_for_sample(self, lib, sample):
+ symbols = []
+ symbols.append(lib.GetSymbolOfCurrentSample())
+ callchain = lib.GetCallChainOfCurrentSample()
+ for i in range(callchain.nr):
+ symbols.append(callchain.entries[i].symbol)
+ # Each sample has a callchain, but its period is only used once
+ # to add period for each function/source_line/source_file/binary.
+ # For example, if more than one entry in the callchain hits a
+ # function, the event count of that function is only increased once.
+ # Otherwise, we may get periods > 100%.
+ is_sample_used = False
+ used_dso_dict = {}
+ used_file_dict = {}
+ used_function_dict = {}
+ used_line_dict = {}
+ period = Period(sample.period, sample.period)
+ for j, symbol in enumerate(symbols):
+ if j == 1:
+ period = Period(0, sample.period)
+ if not self._filter_symbol(symbol):
+ continue
+ is_sample_used = True
+ # Add period to dso.
+ self._add_dso_period(symbol.dso_name, period, used_dso_dict)
+ # Add period to source file.
+ sources = self.addr2line.get_sources(symbol.dso_name, symbol.vaddr_in_file)
+ for source in sources:
+ if source.file:
+ self._add_file_period(source, period, used_file_dict)
+ # Add period to line.
+ if source.line:
+ self._add_line_period(source, period, used_line_dict)
+ # Add period to function.
+ sources = self.addr2line.get_sources(symbol.dso_name, symbol.symbol_addr)
+ for source in sources:
+ if source.file:
+ self._add_file_period(source, period, used_file_dict)
+ if source.function:
+ self._add_function_period(source, period, used_function_dict)
+
+ if is_sample_used:
+ self.period += sample.period
def _add_dso_period(self, dso_name, period, used_dso_dict):
@@ -521,20 +519,19 @@
def _collect_source_files(self):
- self.source_file_dict = dict()
source_file_suffix = ['h', 'c', 'cpp', 'cc', 'java', 'kt']
for source_dir in self.config['source_dirs']:
for root, _, files in os.walk(source_dir):
- for file in files:
- if file[file.rfind('.')+1:] in source_file_suffix:
- entry = self.source_file_dict.get(file)
+ for filename in files:
+ if filename[filename.rfind('.')+1:] in source_file_suffix:
+ entry = self.source_file_dict.get(filename)
if entry is None:
- entry = self.source_file_dict[file] = []
- entry.append(os.path.join(root, file))
+ entry = self.source_file_dict[filename] = []
+ entry.append(os.path.join(root, filename))
- def _find_source_file(self, file):
- filename = file[file.rfind(os.sep)+1:]
+ def _find_source_file(self, file_path):
+ filename = file_path[file_path.rfind(os.sep)+1:]
source_files = self.source_file_dict.get(filename)
if source_files is None:
return None
@@ -542,7 +539,7 @@
best_path = None
best_suffix_len = 0
for path in source_files:
- suffix_len = len(os.path.commonprefix((path[::-1], file[::-1])))
+ suffix_len = len(os.path.commonprefix((path[::-1], file_path[::-1])))
if suffix_len > best_suffix_len:
best_suffix_len = suffix_len
best_path = path
@@ -550,7 +547,7 @@
elif suffix_len == best_suffix_len:
best_path_count += 1
if best_path_count > 1:
- log_warning('multiple source for %s, select %s' % (file, best_path))
+ log_warning('multiple source for %s, select %s' % (file_path, best_path))
return best_path
@@ -560,7 +557,7 @@
2. Annotate c++ source files.
"""
dest_dir = self.config['annotate_dest_dir']
- for key in self.file_periods.keys():
+ for key in self.file_periods:
is_java = False
if key.startswith('$JAVA_SRC_ROOT/'):
path = key[len('$JAVA_SRC_ROOT/'):]
@@ -601,7 +598,7 @@
with open(from_path, 'r') as rf:
lines = rf.readlines()
- annotates = dict()
+ annotates = {}
for line in file_period.line_dict.keys():
annotates[line] = self._get_percentage_str(file_period.line_dict[line], True)
for func_name in file_period.function_dict.keys():
@@ -613,7 +610,7 @@
annotates[1] = '[file] ' + self._get_percentage_str(file_period.period, True)
max_annotate_cols = 0
- for key in annotates.keys():
+ for key in annotates:
max_annotate_cols = max(max_annotate_cols, len(annotates[key]))
empty_annotate = ' ' * (max_annotate_cols + 6)
@@ -636,22 +633,22 @@
wf.write(lines[line-1])
def main():
- parser = argparse.ArgumentParser(description=
-"""Annotate source files based on profiling data. It reads line information from
-binary_cache generated by app_profiler.py or binary_cache_builder.py, and
-generate annotated source files in annotated_files directory.""")
- parser.add_argument('-i', '--perf_data_list', nargs='+', action='append', help=
-"""The paths of profiling data. Default is perf.data.""")
- parser.add_argument('-s', '--source_dirs', nargs='+', action='append', help=
-"""Directories to find source files.""")
- parser.add_argument('--comm', nargs='+', action='append', help=
-"""Use samples only in threads with selected names.""")
- parser.add_argument('--pid', nargs='+', action='append', help=
-"""Use samples only in processes with selected process ids.""")
- parser.add_argument('--tid', nargs='+', action='append', help=
-"""Use samples only in threads with selected thread ids.""")
- parser.add_argument('--dso', nargs='+', action='append', help=
-"""Use samples only in selected binaries.""")
+ parser = argparse.ArgumentParser(description="""
+ Annotate source files based on profiling data. It reads line information from binary_cache
+ generated by app_profiler.py or binary_cache_builder.py, and generate annotated source
+ files in annotated_files directory.""")
+ parser.add_argument('-i', '--perf_data_list', nargs='+', action='append', help="""
+ The paths of profiling data. Default is perf.data.""")
+ parser.add_argument('-s', '--source_dirs', type=extant_dir, nargs='+', action='append', help="""
+ Directories to find source files.""")
+ parser.add_argument('--comm', nargs='+', action='append', help="""
+ Use samples only in threads with selected names.""")
+ parser.add_argument('--pid', nargs='+', action='append', help="""
+ Use samples only in processes with selected process ids.""")
+ parser.add_argument('--tid', nargs='+', action='append', help="""
+ Use samples only in threads with selected thread ids.""")
+ parser.add_argument('--dso', nargs='+', action='append', help="""
+ Use samples only in selected binaries.""")
parser.add_argument('--ndk_path', type=extant_dir, help='Set the path of a ndk release.')
args = parser.parse_args()
diff --git a/simpleperf/scripts/binary_cache_builder.py b/simpleperf/scripts/binary_cache_builder.py
index deb312a..b5a53da 100644
--- a/simpleperf/scripts/binary_cache_builder.py
+++ b/simpleperf/scripts/binary_cache_builder.py
@@ -23,14 +23,10 @@
import argparse
import os
import os.path
-import re
import shutil
-import subprocess
-import sys
-import time
-from simpleperf_report_lib import *
-from utils import *
+from simpleperf_report_lib import ReportLib
+from utils import AdbHelper, flatten_arg_list, log_info, log_warning, log_exit, ReadElf
class BinaryCacheBuilder(object):
@@ -53,6 +49,7 @@
self.binary_cache_dir = 'binary_cache'
if not os.path.isdir(self.binary_cache_dir):
os.makedirs(self.binary_cache_dir)
+ self.binaries = {}
def build_binary_cache(self):
@@ -65,7 +62,7 @@
def _collect_used_binaries(self):
"""read perf.data, collect all used binaries and their build id (if available)."""
# A dict mapping from binary name to build_id
- binaries = dict()
+ binaries = {}
lib = ReportLib()
lib.SetRecordFile(self.perf_data_path)
lib.SetLogSeverity('error')
@@ -99,7 +96,7 @@
# and same build_id.
# Map from filename to binary paths.
- filename_dict = dict()
+ filename_dict = {}
for binary in self.binaries:
index = binary.rfind('/')
filename = binary[index+1:]
@@ -111,17 +108,19 @@
# Walk through all files in symfs_dirs, and copy matching files to build_cache.
for symfs_dir in self.symfs_dirs:
for root, _, files in os.walk(symfs_dir):
- for file in files:
- paths = filename_dict.get(file)
- if paths is not None:
- build_id = self._read_build_id(os.path.join(root, file))
- if not build_id:
- continue
- for binary in paths:
- expected_build_id = self.binaries.get(binary)
- if expected_build_id == build_id:
- self._copy_to_binary_cache(os.path.join(root, file),
- expected_build_id, binary)
+ for filename in files:
+ paths = filename_dict.get(filename)
+ if not paths:
+ continue
+ build_id = self._read_build_id(os.path.join(root, filename))
+ if not build_id:
+ continue
+ for binary in paths:
+ expected_build_id = self.binaries.get(binary)
+ if expected_build_id == build_id:
+ self._copy_to_binary_cache(os.path.join(root, filename),
+ expected_build_id, binary)
+ break
def _copy_to_binary_cache(self, from_path, expected_build_id, target_file):
@@ -129,10 +128,8 @@
target_file = target_file[1:]
target_file = target_file.replace('/', os.sep)
target_file = os.path.join(self.binary_cache_dir, target_file)
- if (os.path.isfile(target_file) and self._read_build_id(target_file) == expected_build_id
- and self._file_has_symbol_table(target_file)):
- # The existing file in binary_cache can provide more information, so no
- # need to copy.
+ if not self._need_to_copy(target_file, expected_build_id):
+ # The existing file in binary_cache can provide more information, so no need to copy.
return
target_dir = os.path.dirname(target_file)
if not os.path.isdir(target_dir):
@@ -141,6 +138,16 @@
shutil.copy(from_path, target_file)
+ def _need_to_copy(self, target_file, expected_build_id):
+ if not os.path.isfile(target_file):
+ return True
+ if self._read_build_id(target_file) != expected_build_id:
+ return True
+ if not self._file_has_symbol_table(target_file):
+ return True
+ return False
+
+
def _pull_binaries_from_device(self):
"""pull binaries needed in perf.data to binary_cache."""
for binary in self.binaries:
@@ -176,14 +183,14 @@
log_info('use current file in binary_cache: %s' % binary_cache_file)
- def _read_build_id(self, file):
+ def _read_build_id(self, file_path):
"""read build id of a binary on host."""
- return self.readelf.get_build_id(file)
+ return self.readelf.get_build_id(file_path)
- def _file_has_symbol_table(self, file):
+ def _file_has_symbol_table(self, file_path):
"""Test if an elf file has symbol table section."""
- return '.symtab' in self.readelf.get_sections(file)
+ return '.symtab' in self.readelf.get_sections(file_path)
def _pull_file_from_device(self, device_path, host_path):
@@ -193,7 +200,7 @@
# Instead, we can first copy the file to /data/local/tmp, then pull it.
filename = device_path[device_path.rfind('/')+1:]
if (self.adb.run(['shell', 'cp', device_path, '/data/local/tmp']) and
- self.adb.run(['pull', '/data/local/tmp/' + filename, host_path])):
+ self.adb.run(['pull', '/data/local/tmp/' + filename, host_path])):
self.adb.run(['shell', 'rm', '/data/local/tmp/' + filename])
return True
log_warning('failed to pull %s from device' % device_path)
@@ -201,24 +208,23 @@
def _pull_kernel_symbols(self):
- file = os.path.join(self.binary_cache_dir, 'kallsyms')
- if os.path.isfile(file):
- os.remove(file)
+ file_path = os.path.join(self.binary_cache_dir, 'kallsyms')
+ if os.path.isfile(file_path):
+ os.remove(file_path)
if self.adb.switch_to_root():
self.adb.run(['shell', '"echo 0 >/proc/sys/kernel/kptr_restrict"'])
- self.adb.run(['pull', '/proc/kallsyms', file])
+ self.adb.run(['pull', '/proc/kallsyms', file_path])
def main():
- parser = argparse.ArgumentParser(description=
-"""Pull binaries needed by perf.data from device to binary_cache directory.""")
- parser.add_argument('-i', '--perf_data_path', default='perf.data', help=
-"""The path of profiling data.""")
- parser.add_argument('-lib', '--native_lib_dir', nargs='+', help=
-"""Path to find debug version of native shared libraries used in the app.""",
- action='append')
- parser.add_argument('--disable_adb_root', action='store_true', help=
-"""Force adb to run in non root mode.""")
+ parser = argparse.ArgumentParser(description="""
+ Pull binaries needed by perf.data from device to binary_cache directory.""")
+ parser.add_argument('-i', '--perf_data_path', default='perf.data', help="""
+ The path of profiling data.""")
+ parser.add_argument('-lib', '--native_lib_dir', nargs='+', help="""
+ Path to find debug version of native shared libraries used in the app.""", action='append')
+ parser.add_argument('--disable_adb_root', action='store_true', help="""
+ Force adb to run in non root mode.""")
parser.add_argument('--ndk_path', nargs=1, help='Find tools in the ndk path.')
args = parser.parse_args()
config = {}
@@ -232,4 +238,4 @@
if __name__ == '__main__':
- main()
\ No newline at end of file
+ main()
diff --git a/simpleperf/scripts/debug_unwind_reporter.py b/simpleperf/scripts/debug_unwind_reporter.py
index 56b5176..83faa6c 100644
--- a/simpleperf/scripts/debug_unwind_reporter.py
+++ b/simpleperf/scripts/debug_unwind_reporter.py
@@ -44,7 +44,8 @@
import re
import subprocess
-from utils import *
+from utils import log_exit, log_fatal
+from utils import get_host_binary_path
class MapEntry(object):
@@ -122,7 +123,7 @@
class UnwindingMemConsumption(object):
- def __init___(self):
+ def __init__(self):
self.before_unwinding = None
self.after_unwinding = None
@@ -341,13 +342,14 @@
elif items[0] == 'callchain:':
in_callchain = True
elif in_callchain:
- # "dalvik-jit-code-cache (deleted)[+346c] (/dev/ashmem/dalvik-jit-code-cache (deleted)[+346c])"
+ # "dalvik-jit-code-cache (deleted)[+346c] (/dev/ashmem/dalvik-jit-code-cache
+ # (deleted)[+346c])"
if re.search(r'\)\[\+\w+\]\)$', line):
break_pos = line.rfind('(', 0, line.rfind('('))
else:
break_pos = line.rfind('(')
if break_pos > 0:
- m = re.match('(.+)\[\+(\w+)\]\)', line[break_pos + 1:])
+ m = re.match(r'(.+)\[\+(\w+)\]\)', line[break_pos + 1:])
if m:
function_names.append(line[:break_pos].strip())
filenames.append(m.group(1))
diff --git a/simpleperf/scripts/inferno/data_types.py b/simpleperf/scripts/inferno/data_types.py
index 17af700..deb9f51 100644
--- a/simpleperf/scripts/inferno/data_types.py
+++ b/simpleperf/scripts/inferno/data_types.py
@@ -15,14 +15,14 @@
#
-class CallSite:
+class CallSite(object):
def __init__(self, method, dso):
self.method = method
self.dso = dso
-class Thread:
+class Thread(object):
def __init__(self, tid, pid):
self.tid = tid
@@ -48,7 +48,7 @@
self.flamegraph.add_callchain(chain, sample.period)
-class Process:
+class Process(object):
def __init__(self, name, pid):
self.name = name
@@ -77,7 +77,7 @@
self.num_events += sample.period
-class FlameGraphCallSite:
+class FlameGraphCallSite(object):
callsite_counter = 0
@classmethod
@@ -85,7 +85,7 @@
cls.callsite_counter += 1
return cls.callsite_counter
- def __init__(self, method, dso, id):
+ def __init__(self, method, dso, callsite_id):
# map from (dso, method) to FlameGraphCallSite. Used to speed up add_callchain().
self.child_dict = {}
self.children = []
@@ -93,7 +93,7 @@
self.dso = dso
self.num_events = 0
self.offset = 0 # Offset allows position nodes in different branches.
- self.id = id
+ self.id = callsite_id
def weight(self):
return float(self.num_events)
@@ -102,15 +102,15 @@
self.num_events += num_events
current = self
for callsite in chain:
- current = current._get_child(callsite)
+ current = current.get_child(callsite)
current.num_events += num_events
- def _get_child(self, callsite):
+ def get_child(self, callsite):
key = (callsite.dso, callsite.method)
child = self.child_dict.get(key)
if child is None:
child = self.child_dict[key] = FlameGraphCallSite(callsite.method, callsite.dso,
- self._get_next_callsite_id())
+ self._get_next_callsite_id())
return child
def trim_callchain(self, min_num_events):
diff --git a/simpleperf/scripts/inferno/inferno.py b/simpleperf/scripts/inferno/inferno.py
index e0035d8..517b742 100644
--- a/simpleperf/scripts/inferno/inferno.py
+++ b/simpleperf/scripts/inferno/inferno.py
@@ -35,18 +35,19 @@
import subprocess
import sys
-scripts_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
-sys.path.append(scripts_path)
+# pylint: disable=wrong-import-position
+SCRIPTS_PATH = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+sys.path.append(SCRIPTS_PATH)
from simpleperf_report_lib import ReportLib
from utils import log_exit, log_info, AdbHelper, open_report_in_browser
-from data_types import *
-from svg_renderer import *
+from data_types import Process
+from svg_renderer import get_proper_scaled_time_string, render_svg
def collect_data(args):
""" Run app_profiler.py to generate record file. """
- app_profiler_args = [sys.executable, os.path.join(scripts_path, "app_profiler.py"), "-nb"]
+ app_profiler_args = [sys.executable, os.path.join(SCRIPTS_PATH, "app_profiler.py"), "-nb"]
if args.app:
app_profiler_args += ["-p", args.app]
elif args.native_program:
@@ -106,10 +107,10 @@
process.cmd = lib.GetRecordCmd()
product_props = lib.MetaInfo().get("product_props")
if product_props:
- tuple = product_props.split(':')
- process.props['ro.product.manufacturer'] = tuple[0]
- process.props['ro.product.model'] = tuple[1]
- process.props['ro.product.name'] = tuple[2]
+ manufacturer, model, name = product_props.split(':')
+ process.props['ro.product.manufacturer'] = manufacturer
+ process.props['ro.product.model'] = model
+ process.props['ro.product.name'] = name
if lib.MetaInfo().get('trace_offcpu') == 'true':
process.props['trace_offcpu'] = True
if args.one_flamegraph:
@@ -163,7 +164,7 @@
if not args.embedded_flamegraph:
f.write("<html><body>")
f.write("<div id='flamegraph_id' style='font-family: Monospace; %s'>" % (
- "display: none;" if args.embedded_flamegraph else ""))
+ "display: none;" if args.embedded_flamegraph else ""))
f.write("""<style type="text/css"> .s { stroke:black; stroke-width:0.5; cursor:pointer;}
</style>""")
f.write('<style type="text/css"> .t:hover { cursor:pointer; } </style>')
@@ -177,7 +178,7 @@
event_entry = 'Event count: %s<br/>' % ("{:,}".format(process.num_events))
# TODO: collect capture duration info from perf.data.
duration_entry = ("Duration: %s seconds<br/>" % args.capture_duration
- ) if args.capture_duration else ""
+ ) if args.capture_duration else ""
f.write("""<div style='display:inline-block;'>
<font size='8'>
Inferno Flamegraph Report%s</font><br/><br/>
@@ -186,14 +187,13 @@
Threads : %d <br/>
Samples : %d<br/>
%s
- %s""" % (
- (': ' + args.title) if args.title else '',
- process_entry,
- datetime.datetime.now().strftime("%Y-%m-%d (%A) %H:%M:%S"),
- len(process.threads),
- process.num_samples,
- event_entry,
- duration_entry))
+ %s""" % ((': ' + args.title) if args.title else '',
+ process_entry,
+ datetime.datetime.now().strftime("%Y-%m-%d (%A) %H:%M:%S"),
+ len(process.threads),
+ process.num_samples,
+ event_entry,
+ duration_entry))
if 'ro.product.model' in process.props:
f.write(
"Machine : %s (%s) by %s<br/>" %
@@ -212,8 +212,8 @@
# Sort threads by the event count in a thread.
for thread in sorted(process.threads.values(), key=lambda x: x.num_events, reverse=True):
f.write("<br/><br/><b>Thread %d (%s) (%d samples):</b><br/>\n\n\n\n" % (
- thread.tid, thread.name, thread.num_samples))
- renderSVG(process, thread.flamegraph, f, args.color)
+ thread.tid, thread.name, thread.num_samples))
+ render_svg(process, thread.flamegraph, f, args.color)
f.write("</div>")
if not args.embedded_flamegraph:
@@ -224,7 +224,7 @@
def generate_threads_offsets(process):
for thread in process.threads.values():
- thread.flamegraph.generate_offset(0)
+ thread.flamegraph.generate_offset(0)
def collect_machine_info(process):
@@ -305,7 +305,7 @@
if result:
try:
process.pid = int(output)
- except:
+ except ValueError:
process.pid = 0
collect_machine_info(process)
else:
@@ -313,7 +313,7 @@
sample_filter_fn = None
if args.one_flamegraph:
- def filter_fn(sample, symbol, callchain):
+ def filter_fn(sample, _symbol, _callchain):
sample.pid = sample.tid = process.pid
return True
sample_filter_fn = filter_fn
diff --git a/simpleperf/scripts/inferno/svg_renderer.py b/simpleperf/scripts/inferno/svg_renderer.py
index fd0096d..0627bc6 100644
--- a/simpleperf/scripts/inferno/svg_renderer.py
+++ b/simpleperf/scripts/inferno/svg_renderer.py
@@ -34,21 +34,21 @@
return hash(string) / float(sys.maxsize)
-def getLegacyColor(method):
+def get_legacy_color(method):
r = 175 + int(50 * hash_to_float(reversed(method)))
g = 60 + int(180 * hash_to_float(method))
b = 60 + int(55 * hash_to_float(reversed(method)))
return (r, g, b)
-def getDSOColor(method):
+def get_dso_color(method):
r = 170 + int(80 * hash_to_float(reversed(method)))
g = 180 + int(70 * hash_to_float((method)))
b = 170 + int(80 * hash_to_float(reversed(method)))
return (r, g, b)
-def getHeatColor(callsite, total_weight):
+def get_heat_color(callsite, total_weight):
r = 245 + 10 * (1 - callsite.weight() / total_weight)
g = 110 + 105 * (1 - callsite.weight() / total_weight)
b = 100
@@ -63,7 +63,7 @@
return '%.3f us' % (value / 1e3)
return '%.0f ns' % value
-def createSVGNode(process, callsite, depth, f, total_weight, height, color_scheme, nav):
+def create_svg_node(process, callsite, depth, f, total_weight, height, color_scheme, nav):
x = float(callsite.offset) / total_weight * 100
y = height - (depth + 1) * SVG_NODE_HEIGHT
width = callsite.weight() / total_weight * 100
@@ -73,11 +73,11 @@
return
if color_scheme == "dso":
- r, g, b = getDSOColor(callsite.dso)
+ r, g, b = get_dso_color(callsite.dso)
elif color_scheme == "legacy":
- r, g, b = getLegacyColor(method)
+ r, g, b = get_legacy_color(method)
else:
- r, g, b = getHeatColor(callsite, total_weight)
+ r, g, b = get_heat_color(callsite, total_weight)
r_border, g_border, b_border = [max(0, color - 50) for color in [r, g, b]]
@@ -119,7 +119,7 @@
FONT_SIZE))
-def renderSVGNodes(process, flamegraph, depth, f, total_weight, height, color_scheme):
+def render_svg_nodes(process, flamegraph, depth, f, total_weight, height, color_scheme):
for i, child in enumerate(flamegraph.children):
# Prebuild navigation target for wasd
@@ -138,12 +138,12 @@
# up, left, down, right
nav = [up_index, left_index, flamegraph.id, right_index]
- createSVGNode(process, child, depth, f, total_weight, height, color_scheme, nav)
+ create_svg_node(process, child, depth, f, total_weight, height, color_scheme, nav)
# Recurse down
- renderSVGNodes(process, child, depth + 1, f, total_weight, height, color_scheme)
+ render_svg_nodes(process, child, depth + 1, f, total_weight, height, color_scheme)
-def renderSearchNode(f):
+def render_search_node(f):
f.write(
"""<rect id="search_rect" style="stroke:rgb(0,0,0);" onclick="search(this);" class="t"
rx="10" ry="10" x="%d" y="10" width="%d" height="30" fill="rgb(255,255,255)""/>
@@ -151,7 +151,7 @@
""" % (SEARCH_NODE_ORIGIN_X, SEARCH_NODE_WIDTH, SEARCH_NODE_ORIGIN_X + RECT_TEXT_PADDING))
-def renderUnzoomNode(f):
+def render_unzoom_node(f):
f.write(
"""<rect id="zoom_rect" style="display:none;stroke:rgb(0,0,0);" class="t"
onclick="unzoom(this);" rx="10" ry="10" x="%d" y="10" width="%d" height="30"
@@ -161,7 +161,7 @@
""" % (UNZOOM_NODE_ORIGIN_X, UNZOOM_NODE_WIDTH, UNZOOM_NODE_ORIGIN_X + RECT_TEXT_PADDING))
-def renderInfoNode(f):
+def render_info_node(f):
f.write(
"""<clipPath id="info_clip_path"> <rect id="info_rect" style="stroke:rgb(0,0,0);"
rx="10" ry="10" x="%d" y="10" width="%d" height="30" fill="rgb(255,255,255)"/>
@@ -173,7 +173,7 @@
INFO_NODE_ORIGIN_X + RECT_TEXT_PADDING))
-def renderPercentNode(f):
+def render_percent_node(f):
f.write(
"""<rect id="percent_rect" style="stroke:rgb(0,0,0);"
rx="10" ry="10" x="%d" y="10" width="%d" height="30" fill="rgb(255,255,255)"/>
@@ -182,7 +182,7 @@
PERCENT_NODE_ORIGIN_X + PERCENT_NODE_WIDTH - RECT_TEXT_PADDING))
-def renderSVG(process, flamegraph, f, color_scheme):
+def render_svg(process, flamegraph, f, color_scheme):
height = (flamegraph.get_max_depth() + 2) * SVG_NODE_HEIGHT
f.write("""<div class="flamegraph_block" style="width:100%%; height:%dpx;">
""" % height)
@@ -196,9 +196,9 @@
</linearGradient> </defs>""")
f.write("""<rect x="0.0" y="0" width="100%" height="100%" fill="url(#background_gradiant)" />
""")
- renderSVGNodes(process, flamegraph, 0, f, flamegraph.weight(), height, color_scheme)
- renderSearchNode(f)
- renderUnzoomNode(f)
- renderInfoNode(f)
- renderPercentNode(f)
+ render_svg_nodes(process, flamegraph, 0, f, flamegraph.weight(), height, color_scheme)
+ render_search_node(f)
+ render_unzoom_node(f)
+ render_info_node(f)
+ render_percent_node(f)
f.write("</svg></div><br/>\n\n")
diff --git a/simpleperf/scripts/pprof_proto_generator.py b/simpleperf/scripts/pprof_proto_generator.py
index fcca090..19f5257 100644
--- a/simpleperf/scripts/pprof_proto_generator.py
+++ b/simpleperf/scripts/pprof_proto_generator.py
@@ -28,22 +28,16 @@
import argparse
import os
import os.path
-import re
-import shutil
-import sys
-import time
from annotate import Addr2Line
-from simpleperf_report_lib import *
-from utils import *
-
+from simpleperf_report_lib import ReportLib
+from utils import log_info, log_exit
+from utils import extant_dir, find_tool_path, flatten_arg_list
try:
- import google.protobuf
-except:
+ import profile_pb2
+except ImportError:
log_exit('google.protobuf module is missing. Please install it first.')
-import profile_pb2
-
def load_pprof_profile(filename):
profile = profile_pb2.Profile()
with open(filename, "rb") as f:
@@ -165,15 +159,8 @@
print('%sfilename: %s' % (space, self.string(function.filename)))
print('%sstart_line: %d' % (space, function.start_line))
- def show_label(self, label, space=''):
- print('%sLabel(%s =', space, self.string(label.key), end='')
- if label.HasField('str'):
- print('%s)' % self.get_string(label.str))
- else:
- print('%d)' % label.num)
-
- def string(self, id):
- return self.string_table[id]
+ def string(self, string_id):
+ return self.string_table[string_id]
class Sample(object):
@@ -185,13 +172,12 @@
def add_location_id(self, location_id):
self.location_ids.append(location_id)
- def add_value(self, id, value):
- self.values[id] = self.values.get(id, 0) + value
+ def add_value(self, sample_type_id, value):
+ self.values[sample_type_id] = self.values.get(sample_type_id, 0) + value
def add_values(self, values):
- for id in values.keys():
- value = values[id]
- self.add_value(id, value)
+ for sample_type_id, value in values.items():
+ self.add_value(sample_type_id, value)
@property
def key(self):
@@ -254,6 +240,7 @@
return (self.name_id, self.dso_name_id)
+# pylint: disable=no-member
class PprofProfileGenerator(object):
def __init__(self, config):
@@ -280,8 +267,6 @@
else:
self.tid_filter = None
self.dso_filter = set(config['dso_filters']) if config.get('dso_filters') else None
-
- def gen(self):
self.profile = profile_pb2.Profile()
self.profile.string_table.append('')
self.string_table = {}
@@ -295,6 +280,7 @@
self.function_map = {}
self.function_list = []
+ def gen(self):
# 1. Process all samples in perf.data, aggregate samples.
while True:
report_sample = self.lib.GetNextSample()
@@ -356,33 +342,33 @@
return True
return False
- def get_string_id(self, str):
- if len(str) == 0:
+ def get_string_id(self, str_value):
+ if not str_value:
return 0
- id = self.string_table.get(str)
- if id is not None:
- return id
- id = len(self.string_table) + 1
- self.string_table[str] = id
- self.profile.string_table.append(str)
- return id
+ str_id = self.string_table.get(str_value)
+ if str_id is not None:
+ return str_id
+ str_id = len(self.string_table) + 1
+ self.string_table[str_value] = str_id
+ self.profile.string_table.append(str_value)
+ return str_id
- def get_string(self, string_id):
- return self.profile.string_table[string_id]
+ def get_string(self, str_id):
+ return self.profile.string_table[str_id]
def get_sample_type_id(self, name):
- id = self.sample_types.get(name)
- if id is not None:
- return id
- id = len(self.profile.sample_type)
+ sample_type_id = self.sample_types.get(name)
+ if sample_type_id is not None:
+ return sample_type_id
+ sample_type_id = len(self.profile.sample_type)
sample_type = self.profile.sample_type.add()
sample_type.type = self.get_string_id('event_' + name + '_samples')
sample_type.unit = self.get_string_id('count')
sample_type = self.profile.sample_type.add()
sample_type.type = self.get_string_id('event_' + name + '_count')
sample_type.unit = self.get_string_id('count')
- self.sample_types[name] = id
- return id
+ self.sample_types[name] = sample_type_id
+ return sample_type_id
def get_location_id(self, ip, symbol):
mapping_id = self.get_mapping_id(symbol.mapping[0], symbol.dso_name)
@@ -485,7 +471,7 @@
if source_id == 0:
# Clear default line info
location.lines = []
- location.lines.append(self.add_line(source, dso_name, function_id))
+ location.lines.append(self.add_line(source, function_id))
source_id += 1
for function in self.function_list:
@@ -498,7 +484,7 @@
if source.line:
function.start_line = source.line
- def add_line(self, source, dso_name, function_id):
+ def add_line(self, source, function_id):
line = Line()
function = self.get_function(function_id)
function.source_filename_id = self.get_string_id(source.file)
@@ -511,8 +497,8 @@
profile_sample.location_id.extend(sample.location_ids)
sample_type_count = len(self.sample_types) * 2
values = [0] * sample_type_count
- for id in sample.values.keys():
- values[id] = sample.values[id]
+ for sample_type_id in sample.values:
+ values[sample_type_id] = sample.values[sample_type_id]
profile_sample.value.extend(values)
def gen_profile_mapping(self, mapping):
@@ -554,18 +540,18 @@
def main():
parser = argparse.ArgumentParser(description='Generate pprof profile data in pprof.profile.')
parser.add_argument('--show', nargs='?', action='append', help='print existing pprof.profile.')
- parser.add_argument('-i', '--perf_data_path', default='perf.data', help=
-"""The path of profiling data.""")
- parser.add_argument('-o', '--output_file', default='pprof.profile', help=
-"""The path of generated pprof profile data.""")
- parser.add_argument('--comm', nargs='+', action='append', help=
-"""Use samples only in threads with selected names.""")
- parser.add_argument('--pid', nargs='+', action='append', help=
-"""Use samples only in processes with selected process ids.""")
- parser.add_argument('--tid', nargs='+', action='append', help=
-"""Use samples only in threads with selected thread ids.""")
- parser.add_argument('--dso', nargs='+', action='append', help=
-"""Use samples only in selected binaries.""")
+ parser.add_argument('-i', '--perf_data_path', default='perf.data', help="""
+ The path of profiling data.""")
+ parser.add_argument('-o', '--output_file', default='pprof.profile', help="""
+ The path of generated pprof profile data.""")
+ parser.add_argument('--comm', nargs='+', action='append', help="""
+ Use samples only in threads with selected names.""")
+ parser.add_argument('--pid', nargs='+', action='append', help="""
+ Use samples only in processes with selected process ids.""")
+ parser.add_argument('--tid', nargs='+', action='append', help="""
+ Use samples only in threads with selected thread ids.""")
+ parser.add_argument('--dso', nargs='+', action='append', help="""
+ Use samples only in selected binaries.""")
parser.add_argument('--ndk_path', type=extant_dir, help='Set the path of a ndk release.')
args = parser.parse_args()
diff --git a/simpleperf/scripts/pylintrc b/simpleperf/scripts/pylintrc
index aeb022e..2801acb 100644
--- a/simpleperf/scripts/pylintrc
+++ b/simpleperf/scripts/pylintrc
@@ -7,3 +7,6 @@
function-rgx=[a-z_][a-z0-9_]{2,50}$
method-rgx=[a-z_][a-z0-9_]{2,50}$
variable-rgx=[a-z_][a-z0-9_]{0,30}$
+attr-rgx=[a-z_][a-z0-9_]{0,30}$
+argument-rgx=[a-z_][a-z0-9_]{0,30}$
+max-module-lines=5000
\ No newline at end of file
diff --git a/simpleperf/scripts/report_html.py b/simpleperf/scripts/report_html.py
index b52a8d1..73ea166 100644
--- a/simpleperf/scripts/report_html.py
+++ b/simpleperf/scripts/report_html.py
@@ -24,7 +24,8 @@
import tempfile
from simpleperf_report_lib import ReportLib
-from utils import *
+from utils import log_info, log_warning, log_exit
+from utils import Addr2Nearestline, get_script_dir, Objdump, open_report_in_browser, remove
class HtmlWriter(object):
@@ -81,9 +82,16 @@
result['eventName'] = self.name
result['eventCount'] = self.event_count
result['processes'] = [process.get_sample_info(gen_addr_hit_map)
- for process in self.processes.values()]
+ for process in self.processes.values()]
return result
+ @property
+ def libraries(self):
+ for process in self.processes.values():
+ for thread in process.threads.values():
+ for lib in thread.libs.values():
+ yield lib
+
class ProcessScope(object):
@@ -107,7 +115,7 @@
result['pid'] = self.pid
result['eventCount'] = self.event_count
result['threads'] = [thread.get_sample_info(gen_addr_hit_map)
- for thread in self.threads.values()]
+ for thread in self.threads.values()]
return result
@@ -123,8 +131,7 @@
""" callstack is a list of tuple (lib_id, func_id, addr).
For each i > 0, callstack[i] calls callstack[i-1]."""
hit_func_ids = set()
- for i in range(len(callstack)):
- lib_id, func_id, addr = callstack[i]
+ for i, (lib_id, func_id, addr) in enumerate(callstack):
# When a callstack contains recursive function, only add for each function once.
if func_id in hit_func_ids:
continue
@@ -157,7 +164,7 @@
result['tid'] = self.tid
result['eventCount'] = self.event_count
result['libs'] = [lib.gen_sample_info(gen_addr_hit_map)
- for lib in self.libs.values()]
+ for lib in self.libs.values()]
return result
@@ -179,7 +186,7 @@
result['libId'] = self.lib_id
result['eventCount'] = self.event_count
result['functions'] = [func.gen_sample_info(gen_addr_hit_map)
- for func in self.functions.values()]
+ for func in self.functions.values()]
return result
@@ -399,17 +406,7 @@
class SourceFileSearcher(object):
-
- SOURCE_FILE_EXTS = {'.h', '.hh', '.H', '.hxx', '.hpp', '.h++',
- '.c', '.cc', '.C', '.cxx', '.cpp', '.c++',
- '.java', '.kt'}
-
- @classmethod
- def is_source_filename(cls, filename):
- ext = os.path.splitext(filename)[1]
- return ext in cls.SOURCE_FILE_EXTS
-
- """" Find source file paths in the file system.
+ """ Find source file paths in the file system.
The file paths reported by addr2line are the paths stored in debug sections
of shared libraries. And we need to convert them to file paths in the file
system. It is done in below steps:
@@ -424,6 +421,16 @@
2.1 Find all real paths with the same file name as the abstract path.
2.2 Select the real path having the longest common suffix with the abstract path.
"""
+
+ SOURCE_FILE_EXTS = {'.h', '.hh', '.H', '.hxx', '.hpp', '.h++',
+ '.c', '.cc', '.C', '.cxx', '.cpp', '.c++',
+ '.java', '.kt'}
+
+ @classmethod
+ def is_source_filename(cls, filename):
+ ext = os.path.splitext(filename)[1]
+ return ext in cls.SOURCE_FILE_EXTS
+
def __init__(self, source_dirs):
# Map from filename to a list of reversed directory path containing filename.
self.filename_to_rparents = {}
@@ -594,30 +601,25 @@
thread.add_callstack(raw_sample.period, callstack, self.build_addr_hit_map)
for event in self.events.values():
- for process in event.processes.values():
- for thread in process.threads.values():
- for lib in thread.libs.values():
- for func_id in lib.functions:
- function = lib.functions[func_id]
- function.update_subtree_event_count()
+ for lib in event.libraries:
+ for func_id in lib.functions:
+ function = lib.functions[func_id]
+ function.update_subtree_event_count()
def limit_percents(self, min_func_percent, min_callchain_percent):
hit_func_ids = set()
for event in self.events.values():
min_limit = event.event_count * min_func_percent * 0.01
- for process in event.processes.values():
- for thread in process.threads.values():
- for lib in thread.libs.values():
- to_del_func_ids = []
- for func_id in lib.functions:
- function = lib.functions[func_id]
- if function.call_graph.subtree_event_count < min_limit:
- to_del_func_ids.append(func_id)
- else:
- function.limit_callchain_percent(min_callchain_percent,
- hit_func_ids)
- for func_id in to_del_func_ids:
- del lib.functions[func_id]
+ for lib in event.libraries:
+ to_del_func_ids = []
+ for func_id in lib.functions:
+ function = lib.functions[func_id]
+ if function.call_graph.subtree_event_count < min_limit:
+ to_del_func_ids.append(func_id)
+ else:
+ function.limit_callchain_percent(min_callchain_percent, hit_func_ids)
+ for func_id in to_del_func_ids:
+ del lib.functions[func_id]
self.functions.trim_functions(hit_func_ids)
def _get_event(self, event_name):
@@ -642,15 +644,12 @@
function.start_addr + function.addr_len - 1)
# Request line for each addr in FunctionScope.addr_hit_map.
for event in self.events.values():
- for process in event.processes.values():
- for thread in process.threads.values():
- for lib in thread.libs.values():
- lib_name = self.libs.get_lib_name(lib.lib_id)
- for function in lib.functions.values():
- func_addr = self.functions.id_to_func[
- function.call_graph.func_id].start_addr
- for addr in function.addr_hit_map:
- addr2line.add_addr(lib_name, func_addr, addr)
+ for lib in event.libraries:
+ lib_name = self.libs.get_lib_name(lib.lib_id)
+ for function in lib.functions.values():
+ func_addr = self.functions.id_to_func[function.call_graph.func_id].start_addr
+ for addr in function.addr_hit_map:
+ addr2line.add_addr(lib_name, func_addr, addr)
addr2line.convert_addrs_to_lines()
# Set line range for each function.
@@ -659,8 +658,7 @@
continue
dso = addr2line.get_dso(self.libs.get_lib_name(function.lib_id))
start_source = addr2line.get_addr_source(dso, function.start_addr)
- end_source = addr2line.get_addr_source(dso,
- function.start_addr + function.addr_len - 1)
+ end_source = addr2line.get_addr_source(dso, function.start_addr + function.addr_len - 1)
if not start_source or not end_source:
continue
start_file_path, start_line = start_source[-1]
@@ -673,22 +671,20 @@
# Build FunctionScope.line_hit_map.
for event in self.events.values():
- for process in event.processes.values():
- for thread in process.threads.values():
- for lib in thread.libs.values():
- dso = addr2line.get_dso(self.libs.get_lib_name(lib.lib_id))
- for function in lib.functions.values():
- for addr in function.addr_hit_map:
- source = addr2line.get_addr_source(dso, addr)
- if not source:
- continue
- for file_path, line in source:
- source_file = self.source_files.get_source_file(file_path)
- # Show [line - 5, line + 5] of the line hit by a sample.
- source_file.request_lines(line - 5, line + 5)
- count_info = function.addr_hit_map[addr]
- function.build_line_hit_map(source_file.file_id, line,
- count_info[0], count_info[1])
+ for lib in event.libraries:
+ dso = addr2line.get_dso(self.libs.get_lib_name(lib.lib_id))
+ for function in lib.functions.values():
+ for addr in function.addr_hit_map:
+ source = addr2line.get_addr_source(dso, addr)
+ if not source:
+ continue
+ for file_path, line in source:
+ source_file = self.source_files.get_source_file(file_path)
+ # Show [line - 5, line + 5] of the line hit by a sample.
+ source_file.request_lines(line - 5, line + 5)
+ count_info = function.addr_hit_map[addr]
+ function.build_line_hit_map(source_file.file_id, line, count_info[0],
+ count_info[1])
# Collect needed source code in SourceFileSet.
self.source_files.load_source_code(source_dirs)
@@ -771,7 +767,7 @@
def _gen_sample_info(self):
return [event.get_sample_info(self.gen_addr_hit_map_in_record_info)
- for event in self.events.values()]
+ for event in self.events.values()]
def _gen_source_files(self):
source_files = sorted(self.source_files.path_to_source_files.values(),
@@ -799,22 +795,23 @@
self.hw.open_tag('html')
self.hw.open_tag('head')
self.hw.open_tag('link', rel='stylesheet', type='text/css',
- href='https://code.jquery.com/ui/1.12.0/themes/smoothness/jquery-ui.css'
- ).close_tag()
+ href='https://code.jquery.com/ui/1.12.0/themes/smoothness/jquery-ui.css'
+ ).close_tag()
self.hw.open_tag('link', rel='stylesheet', type='text/css',
- href='https://cdn.datatables.net/1.10.16/css/jquery.dataTables.min.css'
- ).close_tag()
+ href='https://cdn.datatables.net/1.10.16/css/jquery.dataTables.min.css'
+ ).close_tag()
self.hw.open_tag('script', src='https://www.gstatic.com/charts/loader.js').close_tag()
self.hw.open_tag('script').add(
"google.charts.load('current', {'packages': ['corechart', 'table']});").close_tag()
self.hw.open_tag('script', src='https://code.jquery.com/jquery-3.2.1.js').close_tag()
- self.hw.open_tag('script', src='https://code.jquery.com/ui/1.12.1/jquery-ui.js'
- ).close_tag()
+ self.hw.open_tag('script', src='https://code.jquery.com/ui/1.12.1/jquery-ui.js').close_tag()
self.hw.open_tag('script',
- src='https://cdn.datatables.net/1.10.16/js/jquery.dataTables.min.js').close_tag()
+ src='https://cdn.datatables.net/1.10.16/js/jquery.dataTables.min.js'
+ ).close_tag()
self.hw.open_tag('script',
- src='https://cdn.datatables.net/1.10.16/js/dataTables.jqueryui.min.js').close_tag()
+ src='https://cdn.datatables.net/1.10.16/js/dataTables.jqueryui.min.js'
+ ).close_tag()
self.hw.open_tag('style', type='text/css').add("""
.colForLine { width: 50px; }
.colForCount { width: 100px; }
diff --git a/simpleperf/scripts/report_sample.py b/simpleperf/scripts/report_sample.py
index 45e884a..32a5621 100644
--- a/simpleperf/scripts/report_sample.py
+++ b/simpleperf/scripts/report_sample.py
@@ -20,8 +20,7 @@
from __future__ import print_function
import argparse
-import sys
-from simpleperf_report_lib import *
+from simpleperf_report_lib import ReportLib
def report_sample(record_file, symfs_dir, kallsyms_file=None):
@@ -57,7 +56,7 @@
print('')
-if __name__ == '__main__':
+def main():
parser = argparse.ArgumentParser(description='Report samples in perf.data.')
parser.add_argument('--symfs',
help='Set the path to find binaries with symbols and debug info.')
@@ -66,3 +65,7 @@
help='Default is perf.data.')
args = parser.parse_args()
report_sample(args.record_file, args.symfs, args.kallsyms)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/simpleperf/scripts/run_simpleperf_on_device.py b/simpleperf/scripts/run_simpleperf_on_device.py
index 37155bc..9732d6d 100644
--- a/simpleperf/scripts/run_simpleperf_on_device.py
+++ b/simpleperf/scripts/run_simpleperf_on_device.py
@@ -21,7 +21,7 @@
"""
import subprocess
import sys
-from utils import *
+from utils import AdbHelper, disable_debug_log, get_target_binary_path
def main():
disable_debug_log()
@@ -34,4 +34,4 @@
sys.exit(subprocess.call([adb.adb_path, 'shell', shell_cmd]))
if __name__ == '__main__':
- main()
\ No newline at end of file
+ main()
diff --git a/simpleperf/scripts/simpleperf_report_lib.py b/simpleperf/scripts/simpleperf_report_lib.py
index 2eff83e..f31a4e9 100644
--- a/simpleperf/scripts/simpleperf_report_lib.py
+++ b/simpleperf/scripts/simpleperf_report_lib.py
@@ -21,11 +21,8 @@
"""
import ctypes as ct
-import os
-import subprocess
import sys
-import unittest
-from utils import *
+from utils import bytes_to_str, get_host_binary_path, is_windows, str_to_bytes
def _get_native_lib():
@@ -45,6 +42,9 @@
def _char_pt_to_str(char_pt):
return bytes_to_str(char_pt)
+def _check(cond, failmsg):
+ if not cond:
+ raise RuntimeError(failmsg)
class SampleStruct(ct.Structure):
""" Instance of a sample in perf.data.
@@ -179,6 +179,7 @@
_fields_ = []
+# pylint: disable=invalid-name
class ReportLib(object):
def __init__(self, native_lib_path=None):
@@ -231,17 +232,17 @@
def SetLogSeverity(self, log_level='info'):
""" Set log severity of native lib, can be verbose,debug,info,error,fatal."""
cond = self._SetLogSeverityFunc(self.getInstance(), _char_pt(log_level))
- self._check(cond, 'Failed to set log level')
+ _check(cond, 'Failed to set log level')
def SetSymfs(self, symfs_dir):
""" Set directory used to find symbols."""
cond = self._SetSymfsFunc(self.getInstance(), _char_pt(symfs_dir))
- self._check(cond, 'Failed to set symbols directory')
+ _check(cond, 'Failed to set symbols directory')
def SetRecordFile(self, record_file):
""" Set the path of record file, like perf.data."""
cond = self._SetRecordFileFunc(self.getInstance(), _char_pt(record_file))
- self._check(cond, 'Failed to set record file')
+ _check(cond, 'Failed to set record file')
def ShowIpForUnknownSymbol(self):
self._ShowIpForUnknownSymbolFunc(self.getInstance())
@@ -253,7 +254,7 @@
def SetKallsymsFile(self, kallsym_file):
""" Set the file path to a copy of the /proc/kallsyms file (for off device decoding) """
cond = self._SetKallsymsFileFunc(self.getInstance(), _char_pt(kallsym_file))
- self._check(cond, 'Failed to set kallsyms file')
+ _check(cond, 'Failed to set kallsyms file')
def GetNextSample(self):
psample = self._GetNextSampleFunc(self.getInstance())
@@ -364,7 +365,3 @@
if self._instance is None:
raise Exception('Instance is Closed')
return self._instance
-
- def _check(self, cond, failmsg):
- if not cond:
- raise Exception(failmsg)
diff --git a/simpleperf/scripts/test.py b/simpleperf/scripts/test.py
index 1b80964..7642b2e 100644
--- a/simpleperf/scripts/test.py
+++ b/simpleperf/scripts/test.py
@@ -34,42 +34,41 @@
Test using both `adb root` and `adb unroot`.
"""
-
+from __future__ import print_function
import os
import re
import shutil
import signal
+import subprocess
import sys
-import tempfile
import time
+import types
import unittest
from app_profiler import NativeLibDownloader
from simpleperf_report_lib import ReportLib
-from utils import *
+from utils import log_info, log_fatal
+from utils import AdbHelper, Addr2Nearestline, get_script_dir, is_windows, Objdump, ReadElf, remove
-has_google_protobuf = True
try:
+ # pylint: disable=unused-import
import google.protobuf
-except:
- has_google_protobuf = False
+ HAS_GOOGLE_PROTOBUF = True
+except ImportError:
+ HAS_GOOGLE_PROTOBUF = False
-inferno_script = os.path.join(get_script_dir(), "inferno.bat" if is_windows() else "./inferno.sh")
+INFERNO_SCRIPT = os.path.join(get_script_dir(), "inferno.bat" if is_windows() else "./inferno.sh")
-support_trace_offcpu = None
+def get_device_features():
+ adb = AdbHelper()
+ adb.check_run_and_return_output(['push',
+ 'bin/android/%s/simpleperf' % adb.get_device_arch(),
+ '/data/local/tmp'])
+ adb.check_run_and_return_output(['shell', 'chmod', 'a+x', '/data/local/tmp/simpleperf'])
+ return adb.check_run_and_return_output(['shell', '/data/local/tmp/simpleperf', 'list',
+ '--show-features'])
-def is_trace_offcpu_supported():
- global support_trace_offcpu
- if support_trace_offcpu is None:
- adb = AdbHelper()
- adb.check_run_and_return_output(['push',
- 'bin/android/%s/simpleperf' % adb.get_device_arch(),
- "/data/local/tmp"])
- adb.check_run_and_return_output(['shell', 'chmod', 'a+x', '/data/local/tmp/simpleperf'])
- output = adb.check_run_and_return_output(['shell', '/data/local/tmp/simpleperf', 'list',
- '--show-features'])
- support_trace_offcpu = 'trace-offcpu' in output
- return support_trace_offcpu
+SUPPORT_TRACE_OFFCPU = 'trace-offcpu' in get_device_features()
def build_testdata():
""" Collect testdata from ../testdata and ../demo. """
@@ -77,7 +76,7 @@
from_demo_path = os.path.join('..', 'demo')
from_script_testdata_path = 'script_testdata'
if (not os.path.isdir(from_testdata_path) or not os.path.isdir(from_demo_path) or
- not from_script_testdata_path):
+ not from_script_testdata_path):
return
copy_testdata_list = ['perf_with_symbols.data', 'perf_with_trace_offcpu.data',
'perf_with_tracepoint_event.data', 'perf_with_interpreter_frames.data']
@@ -106,11 +105,12 @@
subproc = subprocess.Popen(args, stdout=subprocess.PIPE, shell=use_shell)
(output_data, _) = subproc.communicate()
returncode = subproc.returncode
- except:
+ except OSError:
returncode = None
self.assertEqual(returncode, 0, msg="failed to run cmd: %s" % args)
if return_output:
return output_data
+ return ''
class TestExampleBase(TestBase):
@@ -145,7 +145,7 @@
cls.use_compiled_java_code = android_version <= 8
def setUp(self):
- if self.id().find('TraceOffCpu') != -1 and not is_trace_offcpu_supported():
+ if self.id().find('TraceOffCpu') != -1 and not SUPPORT_TRACE_OFFCPU:
self.skipTest('trace-offcpu is not supported on device')
cls = self.__class__
if not cls.has_perf_data_for_report:
@@ -193,28 +193,28 @@
if not self.adb_root:
args.append("--disable_adb_root")
self.run_cmd(args)
- self.check_exist(file="perf.data")
+ self.check_exist(filename="perf.data")
if build_binary_cache:
- self.check_exist(dir="binary_cache")
+ self.check_exist(dirname="binary_cache")
- def check_exist(self, file=None, dir=None):
- if file:
- self.assertTrue(os.path.isfile(file), file)
- if dir:
- self.assertTrue(os.path.isdir(dir), dir)
+ def check_exist(self, filename=None, dirname=None):
+ if filename:
+ self.assertTrue(os.path.isfile(filename), filename)
+ if dirname:
+ self.assertTrue(os.path.isdir(dirname), dirname)
- def check_file_under_dir(self, dir, file):
- self.check_exist(dir=dir)
- for _, _, files in os.walk(dir):
+ def check_file_under_dir(self, dirname, filename):
+ self.check_exist(dirname=dirname)
+ for _, _, files in os.walk(dirname):
for f in files:
- if f == file:
+ if f == filename:
return
- self.fail("Failed to call check_file_under_dir(dir=%s, file=%s)" % (dir, file))
+ self.fail("Failed to call check_file_under_dir(dir=%s, file=%s)" % (dirname, filename))
- def check_strings_in_file(self, file, strings):
- self.check_exist(file=file)
- with open(file, 'r') as fh:
+ def check_strings_in_file(self, filename, strings):
+ self.check_exist(filename=filename)
+ with open(filename, 'r') as fh:
self.check_strings_in_content(fh.read(), strings)
def check_strings_in_content(self, content, strings):
@@ -226,17 +226,15 @@
This function checks for each entry, if the line containing [name]
has at least required accumulated_period and period.
"""
- self.check_exist(file=summary_file)
+ self.check_exist(filename=summary_file)
with open(summary_file, 'r') as fh:
summary = fh.read()
fulfilled = [False for x in check_entries]
- if not hasattr(self, "summary_check_re"):
- self.summary_check_re = re.compile(r'accumulated_period:\s*([\d.]+)%.*period:\s*([\d.]+)%')
+ summary_check_re = re.compile(r'accumulated_period:\s*([\d.]+)%.*period:\s*([\d.]+)%')
for line in summary.split('\n'):
- for i in range(len(check_entries)):
- (name, need_acc_period, need_period) = check_entries[i]
+ for i, (name, need_acc_period, need_period) in enumerate(check_entries):
if not fulfilled[i] and name in line:
- m = self.summary_check_re.search(line)
+ m = summary_check_re.search(line)
if m:
acc_period = float(m.group(1))
period = float(m.group(2))
@@ -244,9 +242,9 @@
fulfilled[i] = True
self.assertEqual(len(fulfilled), sum([int(x) for x in fulfilled]), fulfilled)
- def check_inferno_report_html(self, check_entries, file="report.html"):
- self.check_exist(file=file)
- with open(file, 'r') as fh:
+ def check_inferno_report_html(self, check_entries, filename="report.html"):
+ self.check_exist(filename=filename)
+ with open(filename, 'r') as fh:
data = fh.read()
fulfilled = [False for _ in check_entries]
for line in data.split('\n'):
@@ -258,7 +256,7 @@
if m and float(m.group(1)) >= entry[1]:
fulfilled[i] = True
break
- self.assertEqual(fulfilled, [True for x in check_entries])
+ self.assertEqual(fulfilled, [True for _ in check_entries])
def common_test_app_profiler(self):
self.run_cmd(["app_profiler.py", "-h"])
@@ -269,7 +267,7 @@
if not self.adb_root:
args.append("--disable_adb_root")
self.run_cmd(args)
- self.check_exist(dir="binary_cache")
+ self.check_exist(dirname="binary_cache")
remove("binary_cache")
self.run_app_profiler(build_binary_cache=True)
self.run_app_profiler()
@@ -280,13 +278,13 @@
self.run_cmd(["report.py"])
self.run_cmd(["report.py", "-i", "perf.data"])
self.run_cmd(["report.py", "-g"])
- self.run_cmd(["report.py", "--self-kill-for-testing", "-g", "--gui"])
+ self.run_cmd(["report.py", "--self-kill-for-testing", "-g", "--gui"])
def common_test_annotate(self):
self.run_cmd(["annotate.py", "-h"])
remove("annotated_files")
self.run_cmd(["annotate.py", "-s", self.example_path])
- self.check_exist(dir="annotated_files")
+ self.check_exist(dirname="annotated_files")
def common_test_report_sample(self, check_strings):
self.run_cmd(["report_sample.py", "-h"])
@@ -296,37 +294,36 @@
def common_test_pprof_proto_generator(self, check_strings_with_lines,
check_strings_without_lines):
- if not has_google_protobuf:
+ if not HAS_GOOGLE_PROTOBUF:
log_info('Skip test for pprof_proto_generator because google.protobuf is missing')
return
self.run_cmd(["pprof_proto_generator.py", "-h"])
self.run_cmd(["pprof_proto_generator.py"])
remove("pprof.profile")
self.run_cmd(["pprof_proto_generator.py", "-i", "perf.data", "-o", "pprof.profile"])
- self.check_exist(file="pprof.profile")
+ self.check_exist(filename="pprof.profile")
self.run_cmd(["pprof_proto_generator.py", "--show"])
output = self.run_cmd(["pprof_proto_generator.py", "--show", "pprof.profile"],
return_output=True)
- self.check_strings_in_content(output, check_strings_with_lines +
- ["has_line_numbers: True"])
+ self.check_strings_in_content(output, check_strings_with_lines + ["has_line_numbers: True"])
remove("binary_cache")
self.run_cmd(["pprof_proto_generator.py"])
output = self.run_cmd(["pprof_proto_generator.py", "--show", "pprof.profile"],
return_output=True)
self.check_strings_in_content(output, check_strings_without_lines +
- ["has_line_numbers: False"])
+ ["has_line_numbers: False"])
def common_test_inferno(self):
- self.run_cmd([inferno_script, "-h"])
+ self.run_cmd([INFERNO_SCRIPT, "-h"])
remove("perf.data")
append_args = [] if self.adb_root else ["--disable_adb_root"]
- self.run_cmd([inferno_script, "-p", self.package_name, "-t", "3"] + append_args)
- self.check_exist(file="perf.data")
- self.run_cmd([inferno_script, "-p", self.package_name, "-f", "1000", "-du", "-t", "1"] +
+ self.run_cmd([INFERNO_SCRIPT, "-p", self.package_name, "-t", "3"] + append_args)
+ self.check_exist(filename="perf.data")
+ self.run_cmd([INFERNO_SCRIPT, "-p", self.package_name, "-f", "1000", "-du", "-t", "1"] +
append_args)
- self.run_cmd([inferno_script, "-p", self.package_name, "-e", "100000 cpu-cycles",
+ self.run_cmd([INFERNO_SCRIPT, "-p", self.package_name, "-e", "100000 cpu-cycles",
"-t", "1"] + append_args)
- self.run_cmd([inferno_script, "-sc"])
+ self.run_cmd([INFERNO_SCRIPT, "-sc"])
def common_test_report_html(self):
self.run_cmd(['report_html.py', '-h'])
@@ -353,9 +350,9 @@
def test_app_profiler_profile_from_launch(self):
self.run_app_profiler(start_activity=True, build_binary_cache=False)
self.run_cmd(["report.py", "-g", "-o", "report.txt"])
- self.check_strings_in_file("report.txt",
- ["com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run()",
- "__start_thread"])
+ self.check_strings_in_file("report.txt", [
+ "com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run",
+ "__start_thread"])
def test_app_profiler_multiprocesses(self):
self.adb.check_run(['shell', 'am', 'force-stop', self.package_name])
@@ -396,20 +393,20 @@
def test_report(self):
self.common_test_report()
self.run_cmd(["report.py", "-g", "-o", "report.txt"])
- self.check_strings_in_file("report.txt",
- ["com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run()",
- "__start_thread"])
+ self.check_strings_in_file("report.txt", [
+ "com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run",
+ "__start_thread"])
def test_profile_with_process_id(self):
self.adb.check_run(['shell', 'am', 'start', '-n', self.package_name + '/.MainActivity'])
time.sleep(1)
- pid = self.adb.check_run_and_return_output(['shell', 'pidof',
- 'com.example.simpleperf.simpleperfexamplepurejava']).strip()
+ pid = self.adb.check_run_and_return_output([
+ 'shell', 'pidof', 'com.example.simpleperf.simpleperfexamplepurejava']).strip()
self.run_app_profiler(start_activity=False, record_arg='-g --duration 10 -p ' + pid)
self.run_cmd(["report.py", "-g", "-o", "report.txt"])
- self.check_strings_in_file("report.txt",
- ["com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run()",
- "__start_thread"])
+ self.check_strings_in_file("report.txt", [
+ "com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run",
+ "__start_thread"])
def test_annotate(self):
self.common_test_annotate()
@@ -418,15 +415,15 @@
return
self.check_file_under_dir("annotated_files", "MainActivity.java")
summary_file = os.path.join("annotated_files", "summary")
- self.check_annotation_summary(summary_file,
- [("MainActivity.java", 80, 80),
- ("run", 80, 0),
- ("callFunction", 0, 0),
- ("line 23", 80, 0)])
+ self.check_annotation_summary(summary_file, [
+ ("MainActivity.java", 80, 80),
+ ("run", 80, 0),
+ ("callFunction", 0, 0),
+ ("line 23", 80, 0)])
def test_report_sample(self):
self.common_test_report_sample(
- ["com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run()",
+ ["com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run",
"__start_thread"])
def test_pprof_proto_generator(self):
@@ -437,18 +434,18 @@
"run"]
self.common_test_pprof_proto_generator(
check_strings_with_lines=check_strings_with_lines,
- check_strings_without_lines=
- ["com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run()"])
+ check_strings_without_lines=[
+ "com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run"])
def test_inferno(self):
self.common_test_inferno()
self.run_app_profiler()
- self.run_cmd([inferno_script, "-sc"])
+ self.run_cmd([INFERNO_SCRIPT, "-sc"])
self.check_inferno_report_html(
- [('com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run()', 80)])
- self.run_cmd([inferno_script, "-sc", "-o", "report2.html"])
+ [('com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run', 80)])
+ self.run_cmd([INFERNO_SCRIPT, "-sc", "-o", "report2.html"])
self.check_inferno_report_html(
- [('com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run()', 80)],
+ [('com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run', 80)],
"report2.html")
remove("report2.html")
@@ -460,8 +457,8 @@
os.chdir(test_dir)
self.run_cmd(['python', os.path.join(saved_dir, 'app_profiler.py'),
'--app', self.package_name, '-r', '-e task-clock:u -g --duration 3'])
- self.check_exist(file="perf.data")
- self.run_cmd([inferno_script, "-sc"])
+ self.check_exist(filename="perf.data")
+ self.run_cmd([INFERNO_SCRIPT, "-sc"])
os.chdir(saved_dir)
remove(test_dir)
@@ -475,7 +472,7 @@
self.adb.check_run(['kill-server'])
time.sleep(3)
self.run_cmd(['run_simpleperf_without_usb_connection.py', 'stop'])
- self.check_exist(file="perf.data")
+ self.check_exist(filename="perf.data")
self.run_cmd(["report.py", "-g", "-o", "report.txt"])
@@ -501,25 +498,25 @@
def test_smoke(self):
self.run_app_profiler(record_arg="-g -f 1000 --duration 10 -e cpu-cycles:u --trace-offcpu")
self.run_cmd(["report.py", "-g", "-o", "report.txt"])
- self.check_strings_in_file("report.txt",
- ["com.example.simpleperf.simpleperfexamplepurejava.SleepActivity$1.run",
- "com.example.simpleperf.simpleperfexamplepurejava.SleepActivity$1.RunFunction",
- "com.example.simpleperf.simpleperfexamplepurejava.SleepActivity$1.SleepFunction"
- ])
+ self.check_strings_in_file("report.txt", [
+ "com.example.simpleperf.simpleperfexamplepurejava.SleepActivity$1.run",
+ "com.example.simpleperf.simpleperfexamplepurejava.SleepActivity$1.RunFunction",
+ "com.example.simpleperf.simpleperfexamplepurejava.SleepActivity$1.SleepFunction"
+ ])
remove("annotated_files")
self.run_cmd(["annotate.py", "-s", self.example_path])
- self.check_exist(dir="annotated_files")
+ self.check_exist(dirname="annotated_files")
if self.use_compiled_java_code:
self.check_file_under_dir("annotated_files", "SleepActivity.java")
summary_file = os.path.join("annotated_files", "summary")
- self.check_annotation_summary(summary_file,
- [("SleepActivity.java", 80, 20),
+ self.check_annotation_summary(summary_file, [
+ ("SleepActivity.java", 80, 20),
("run", 80, 0),
("RunFunction", 20, 20),
("SleepFunction", 20, 0),
("line 24", 20, 0),
("line 32", 20, 0)])
- self.run_cmd([inferno_script, "-sc"])
+ self.run_cmd([INFERNO_SCRIPT, "-sc"])
self.check_inferno_report_html(
[('com.example.simpleperf.simpleperfexamplepurejava.SleepActivity$1.run', 80),
('com.example.simpleperf.simpleperfexamplepurejava.SleepActivity$1.RunFunction',
@@ -541,25 +538,21 @@
def test_app_profiler_profile_from_launch(self):
self.run_app_profiler(start_activity=True, build_binary_cache=False)
self.run_cmd(["report.py", "-g", "-o", "report.txt"])
- self.check_strings_in_file("report.txt",
- ["BusyLoopThread",
- "__start_thread"])
+ self.check_strings_in_file("report.txt", ["BusyLoopThread", "__start_thread"])
def test_report(self):
self.common_test_report()
self.run_cmd(["report.py", "-g", "-o", "report.txt"])
- self.check_strings_in_file("report.txt",
- ["BusyLoopThread",
- "__start_thread"])
+ self.check_strings_in_file("report.txt", ["BusyLoopThread", "__start_thread"])
def test_annotate(self):
self.common_test_annotate()
self.check_file_under_dir("annotated_files", "native-lib.cpp")
summary_file = os.path.join("annotated_files", "summary")
- self.check_annotation_summary(summary_file,
- [("native-lib.cpp", 20, 0),
- ("BusyLoopThread", 20, 0),
- ("line 46", 20, 0)])
+ self.check_annotation_summary(summary_file, [
+ ("native-lib.cpp", 20, 0),
+ ("BusyLoopThread", 20, 0),
+ ("line 46", 20, 0)])
def test_report_sample(self):
self.common_test_report_sample(
@@ -568,16 +561,13 @@
def test_pprof_proto_generator(self):
self.common_test_pprof_proto_generator(
- check_strings_with_lines=
- ["native-lib.cpp",
- "BusyLoopThread"],
- check_strings_without_lines=
- ["BusyLoopThread"])
+ check_strings_with_lines=["native-lib.cpp", "BusyLoopThread"],
+ check_strings_without_lines=["BusyLoopThread"])
def test_inferno(self):
self.common_test_inferno()
self.run_app_profiler()
- self.run_cmd([inferno_script, "-sc"])
+ self.run_cmd([INFERNO_SCRIPT, "-sc"])
self.check_inferno_report_html([('BusyLoopThread', 20)])
def test_report_html(self):
@@ -606,23 +596,23 @@
def test_smoke(self):
self.run_app_profiler(record_arg="-g -f 1000 --duration 10 -e cpu-cycles:u --trace-offcpu")
self.run_cmd(["report.py", "-g", "--comms", "SleepThread", "-o", "report.txt"])
- self.check_strings_in_file("report.txt",
- ["SleepThread(void*)",
- "RunFunction()",
- "SleepFunction(unsigned long long)"])
+ self.check_strings_in_file("report.txt", [
+ "SleepThread(void*)",
+ "RunFunction()",
+ "SleepFunction(unsigned long long)"])
remove("annotated_files")
self.run_cmd(["annotate.py", "-s", self.example_path, "--comm", "SleepThread"])
- self.check_exist(dir="annotated_files")
+ self.check_exist(dirname="annotated_files")
self.check_file_under_dir("annotated_files", "native-lib.cpp")
summary_file = os.path.join("annotated_files", "summary")
- self.check_annotation_summary(summary_file,
- [("native-lib.cpp", 80, 20),
- ("SleepThread", 80, 0),
- ("RunFunction", 20, 20),
- ("SleepFunction", 20, 0),
- ("line 73", 20, 0),
- ("line 83", 20, 0)])
- self.run_cmd([inferno_script, "-sc"])
+ self.check_annotation_summary(summary_file, [
+ ("native-lib.cpp", 80, 20),
+ ("SleepThread", 80, 0),
+ ("RunFunction", 20, 20),
+ ("SleepFunction", 20, 0),
+ ("line 73", 20, 0),
+ ("line 83", 20, 0)])
+ self.run_cmd([INFERNO_SCRIPT, "-sc"])
self.check_inferno_report_html([('SleepThread', 80),
('RunFunction', 20),
('SleepFunction', 20)])
@@ -638,28 +628,26 @@
def test_smoke(self):
self.run_app_profiler()
self.run_cmd(["report.py", "-g", "--comms", "BusyThread", "-o", "report.txt"])
- self.check_strings_in_file("report.txt",
- ["void com.example.simpleperf.simpleperfexamplewithnative.MixActivity$1.run()",
- "int com.example.simpleperf.simpleperfexamplewithnative.MixActivity.callFunction(int)",
- "Java_com_example_simpleperf_simpleperfexamplewithnative_MixActivity_callFunction"])
+ self.check_strings_in_file("report.txt", [
+ "com.example.simpleperf.simpleperfexamplewithnative.MixActivity$1.run",
+ "com.example.simpleperf.simpleperfexamplewithnative.MixActivity.callFunction",
+ "Java_com_example_simpleperf_simpleperfexamplewithnative_MixActivity_callFunction"])
remove("annotated_files")
self.run_cmd(["annotate.py", "-s", self.example_path, "--comm", "BusyThread"])
- self.check_exist(dir="annotated_files")
+ self.check_exist(dirname="annotated_files")
self.check_file_under_dir("annotated_files", "native-lib.cpp")
summary_file = os.path.join("annotated_files", "summary")
- self.check_annotation_summary(summary_file,
- [("native-lib.cpp", 5, 0),
- ("line 40", 5, 0)])
+ self.check_annotation_summary(summary_file, [("native-lib.cpp", 5, 0), ("line 40", 5, 0)])
if self.use_compiled_java_code:
self.check_file_under_dir("annotated_files", "MixActivity.java")
- self.check_annotation_summary(summary_file,
- [("MixActivity.java", 80, 0),
- ("run", 80, 0),
- ("line 26", 20, 0),
- ("native-lib.cpp", 5, 0),
- ("line 40", 5, 0)])
+ self.check_annotation_summary(summary_file, [
+ ("MixActivity.java", 80, 0),
+ ("run", 80, 0),
+ ("line 26", 20, 0),
+ ("native-lib.cpp", 5, 0),
+ ("line 40", 5, 0)])
- self.run_cmd([inferno_script, "-sc"])
+ self.run_cmd([INFERNO_SCRIPT, "-sc"])
class TestExampleWithNativeForceArm(TestExampleWithNative):
@@ -703,16 +691,16 @@
def test_app_profiler_profile_from_launch(self):
self.run_app_profiler(start_activity=True, build_binary_cache=False)
self.run_cmd(["report.py", "-g", "-o", "report.txt"])
- self.check_strings_in_file("report.txt",
- ["com.example.simpleperf.simpleperfexampleofkotlin.MainActivity$createBusyThread$1.run()",
- "__start_thread"])
+ self.check_strings_in_file("report.txt", [
+ "com.example.simpleperf.simpleperfexampleofkotlin.MainActivity$createBusyThread$1." +
+ "run", "__start_thread"])
def test_report(self):
self.common_test_report()
self.run_cmd(["report.py", "-g", "-o", "report.txt"])
- self.check_strings_in_file("report.txt",
- ["com.example.simpleperf.simpleperfexampleofkotlin.MainActivity$createBusyThread$1.run()",
- "__start_thread"])
+ self.check_strings_in_file("report.txt", [
+ "com.example.simpleperf.simpleperfexampleofkotlin.MainActivity$createBusyThread$1." +
+ "run", "__start_thread"])
def test_annotate(self):
if not self.use_compiled_java_code:
@@ -720,17 +708,17 @@
self.common_test_annotate()
self.check_file_under_dir("annotated_files", "MainActivity.kt")
summary_file = os.path.join("annotated_files", "summary")
- self.check_annotation_summary(summary_file,
- [("MainActivity.kt", 80, 80),
- ("run", 80, 0),
- ("callFunction", 0, 0),
- ("line 19", 80, 0),
- ("line 25", 0, 0)])
+ self.check_annotation_summary(summary_file, [
+ ("MainActivity.kt", 80, 80),
+ ("run", 80, 0),
+ ("callFunction", 0, 0),
+ ("line 19", 80, 0),
+ ("line 25", 0, 0)])
def test_report_sample(self):
- self.common_test_report_sample(
- ["com.example.simpleperf.simpleperfexampleofkotlin.MainActivity$createBusyThread$1.run()",
- "__start_thread"])
+ self.common_test_report_sample([
+ "com.example.simpleperf.simpleperfexampleofkotlin.MainActivity$createBusyThread$1." +
+ "run", "__start_thread"])
def test_pprof_proto_generator(self):
check_strings_with_lines = []
@@ -740,16 +728,15 @@
"run"]
self.common_test_pprof_proto_generator(
check_strings_with_lines=check_strings_with_lines,
- check_strings_without_lines=
- ["com.example.simpleperf.simpleperfexampleofkotlin.MainActivity$createBusyThread$1.run()"])
+ check_strings_without_lines=["com.example.simpleperf.simpleperfexampleofkotlin." +
+ "MainActivity$createBusyThread$1.run"])
def test_inferno(self):
self.common_test_inferno()
self.run_app_profiler()
- self.run_cmd([inferno_script, "-sc"])
- self.check_inferno_report_html(
- [('com.example.simpleperf.simpleperfexampleofkotlin.MainActivity$createBusyThread$1.run()',
- 80)])
+ self.run_cmd([INFERNO_SCRIPT, "-sc"])
+ self.check_inferno_report_html([('com.example.simpleperf.simpleperfexampleofkotlin.' +
+ 'MainActivity$createBusyThread$1.run', 80)])
def test_report_html(self):
self.common_test_report_html()
@@ -777,33 +764,32 @@
def test_smoke(self):
self.run_app_profiler(record_arg="-g -f 1000 --duration 10 -e cpu-cycles:u --trace-offcpu")
self.run_cmd(["report.py", "-g", "-o", "report.txt"])
- self.check_strings_in_file("report.txt",
- ["com.example.simpleperf.simpleperfexampleofkotlin.SleepActivity$createRunSleepThread$1.run",
- "com.example.simpleperf.simpleperfexampleofkotlin.SleepActivity$createRunSleepThread$1.RunFunction",
- "com.example.simpleperf.simpleperfexampleofkotlin.SleepActivity$createRunSleepThread$1.SleepFunction"
- ])
+ function_prefix = "com.example.simpleperf.simpleperfexampleofkotlin." + \
+ "SleepActivity$createRunSleepThread$1."
+ self.check_strings_in_file("report.txt", [
+ function_prefix + "run",
+ function_prefix + "RunFunction",
+ function_prefix + "SleepFunction"
+ ])
if self.use_compiled_java_code:
remove("annotated_files")
self.run_cmd(["annotate.py", "-s", self.example_path])
- self.check_exist(dir="annotated_files")
+ self.check_exist(dirname="annotated_files")
self.check_file_under_dir("annotated_files", "SleepActivity.kt")
summary_file = os.path.join("annotated_files", "summary")
- self.check_annotation_summary(summary_file,
- [("SleepActivity.kt", 80, 20),
- ("run", 80, 0),
- ("RunFunction", 20, 20),
- ("SleepFunction", 20, 0),
- ("line 24", 20, 0),
- ("line 32", 20, 0)])
+ self.check_annotation_summary(summary_file, [
+ ("SleepActivity.kt", 80, 20),
+ ("run", 80, 0),
+ ("RunFunction", 20, 20),
+ ("SleepFunction", 20, 0),
+ ("line 24", 20, 0),
+ ("line 32", 20, 0)])
- self.run_cmd([inferno_script, "-sc"])
- self.check_inferno_report_html(
- [('com.example.simpleperf.simpleperfexampleofkotlin.SleepActivity$createRunSleepThread$1.run',
- 80),
- ('com.example.simpleperf.simpleperfexampleofkotlin.SleepActivity$createRunSleepThread$1.RunFunction',
- 20),
- ('com.example.simpleperf.simpleperfexampleofkotlin.SleepActivity$createRunSleepThread$1.SleepFunction',
- 20)])
+ self.run_cmd([INFERNO_SCRIPT, "-sc"])
+ self.check_inferno_report_html([
+ (function_prefix + 'run', 80),
+ (function_prefix + 'RunFunction', 20),
+ (function_prefix + 'SleepFunction', 20)])
class TestProfilingCmd(TestBase):
@@ -819,8 +805,8 @@
if adb.switch_to_root():
self.run_cmd(["app_profiler.py", "-np", "surfaceflinger"])
self.run_cmd(["report.py", "-g", "-o", "report.txt"])
- self.run_cmd([inferno_script, "-sc"])
- self.run_cmd([inferno_script, "-np", "surfaceflinger"])
+ self.run_cmd([INFERNO_SCRIPT, "-sc"])
+ self.run_cmd([INFERNO_SCRIPT, "-np", "surfaceflinger"])
class TestReportLib(unittest.TestCase):
@@ -838,7 +824,6 @@
def test_symbol(self):
found_func2 = False
while self.report_lib.GetNextSample():
- sample = self.report_lib.GetCurrentSample()
symbol = self.report_lib.GetSymbolOfCurrentSample()
if symbol.symbol_name == 'func2(int, int)':
found_func2 = True
@@ -883,7 +868,8 @@
def test_record_cmd(self):
self.report_lib.SetRecordFile(os.path.join('testdata', 'perf_with_trace_offcpu.data'))
self.assertEqual(self.report_lib.GetRecordCmd(),
- "/data/local/tmp/simpleperf record --trace-offcpu --duration 2 -g ./simpleperf_runtest_run_and_sleep64")
+ "/data/local/tmp/simpleperf record --trace-offcpu --duration 2 -g " +
+ "./simpleperf_runtest_run_and_sleep64")
def test_offcpu(self):
self.report_lib.SetRecordFile(os.path.join('testdata', 'perf_with_trace_offcpu.data'))
@@ -1010,8 +996,7 @@
actual_source = addr2line.get_addr_source(dso, test_addr['addr'])
self.assertTrue(actual_source is not None)
self.assertEqual(len(actual_source), len(expected_source))
- for i in range(len(expected_source)):
- actual_file_path, actual_line = actual_source[i]
+ for i, (actual_file_path, actual_line) in enumerate(expected_source):
self.assertEqual(actual_file_path, expected_source[i][0])
self.assertEqual(actual_line, expected_source[i][1])
@@ -1066,28 +1051,28 @@
def test_readelf(self):
test_map = {
- '/simpleperf_runtest_two_functions_arm64': {
- 'arch': 'arm64',
- 'build_id': '0xe8ecb3916d989dbdc068345c30f0c24300000000',
- 'sections': ['.interp', '.note.android.ident', '.note.gnu.build-id', '.dynsym',
- '.dynstr', '.gnu.hash', '.gnu.version', '.gnu.version_r', '.rela.dyn',
- '.rela.plt', '.plt', '.text', '.rodata', '.eh_frame', '.eh_frame_hdr',
- '.preinit_array', '.init_array', '.fini_array', '.dynamic', '.got',
- '.got.plt', '.data', '.bss', '.comment', '.debug_str', '.debug_loc',
- '.debug_abbrev', '.debug_info', '.debug_ranges', '.debug_macinfo',
- '.debug_pubnames', '.debug_pubtypes', '.debug_line',
- '.note.gnu.gold-version', '.symtab', '.strtab', '.shstrtab'],
- },
- '/simpleperf_runtest_two_functions_arm': {
- 'arch': 'arm',
- 'build_id': '0x718f5b36c4148ee1bd3f51af89ed2be600000000',
- },
- '/simpleperf_runtest_two_functions_x86_64': {
- 'arch': 'x86_64',
- },
- '/simpleperf_runtest_two_functions_x86': {
- 'arch': 'x86',
- }
+ '/simpleperf_runtest_two_functions_arm64': {
+ 'arch': 'arm64',
+ 'build_id': '0xe8ecb3916d989dbdc068345c30f0c24300000000',
+ 'sections': ['.interp', '.note.android.ident', '.note.gnu.build-id', '.dynsym',
+ '.dynstr', '.gnu.hash', '.gnu.version', '.gnu.version_r', '.rela.dyn',
+ '.rela.plt', '.plt', '.text', '.rodata', '.eh_frame', '.eh_frame_hdr',
+ '.preinit_array', '.init_array', '.fini_array', '.dynamic', '.got',
+ '.got.plt', '.data', '.bss', '.comment', '.debug_str', '.debug_loc',
+ '.debug_abbrev', '.debug_info', '.debug_ranges', '.debug_macinfo',
+ '.debug_pubnames', '.debug_pubtypes', '.debug_line',
+ '.note.gnu.gold-version', '.symtab', '.strtab', '.shstrtab'],
+ },
+ '/simpleperf_runtest_two_functions_arm': {
+ 'arch': 'arm',
+ 'build_id': '0x718f5b36c4148ee1bd3f51af89ed2be600000000',
+ },
+ '/simpleperf_runtest_two_functions_x86_64': {
+ 'arch': 'x86_64',
+ },
+ '/simpleperf_runtest_two_functions_x86': {
+ 'arch': 'x86',
+ }
}
readelf = ReadElf(None)
for dso_path in test_map:
@@ -1105,10 +1090,14 @@
class TestNativeLibDownloader(unittest.TestCase):
def test_smoke(self):
- self.adb = AdbHelper()
+ adb = AdbHelper()
+
+ def is_lib_on_device(path):
+ return adb.run(['shell', 'ls', path])
+
# Sync all native libs on device.
- self.adb.run(['shell', 'rm', '-rf', '/data/local/tmp/native_libs'])
- downloader = NativeLibDownloader(None, 'arm64', self.adb)
+ adb.run(['shell', 'rm', '-rf', '/data/local/tmp/native_libs'])
+ downloader = NativeLibDownloader(None, 'arm64', adb)
downloader.collect_native_libs_on_host(os.path.join(
'testdata', 'SimpleperfExampleWithNative', 'app', 'build', 'intermediates', 'cmake',
'profiling'))
@@ -1127,36 +1116,46 @@
downloader.sync_natives_libs_on_device()
downloader.collect_native_libs_on_device()
self.assertEqual(len(downloader.device_build_id_map), sync_count)
- for i in range(len(lib_list)):
- build_id = lib_list[i][0]
- name = lib_list[i][1].name
+ for i, item in enumerate(lib_list):
+ build_id = item[0]
+ name = item[1].name
if i < sync_count:
self.assertTrue(build_id in downloader.device_build_id_map)
self.assertEqual(name, downloader.device_build_id_map[build_id])
- self.assertTrue(self._is_lib_on_device(downloader.dir_on_device + name))
+ self.assertTrue(is_lib_on_device(downloader.dir_on_device + name))
else:
self.assertTrue(build_id not in downloader.device_build_id_map)
- self.assertFalse(self._is_lib_on_device(downloader.dir_on_device + name))
+ self.assertFalse(is_lib_on_device(downloader.dir_on_device + name))
if sync_count == 1:
- self.adb.run(['pull', '/data/local/tmp/native_libs/build_id_list',
- 'build_id_list'])
+ adb.run(['pull', '/data/local/tmp/native_libs/build_id_list', 'build_id_list'])
with open('build_id_list', 'rb') as fh:
self.assertEqual(fh.read(), '{}={}\n'.format(lib_list[0][0],
lib_list[0][1].name))
remove('build_id_list')
- self.adb.run(['shell', 'rm', '-rf', '/data/local/tmp/native_libs'])
+ adb.run(['shell', 'rm', '-rf', '/data/local/tmp/native_libs'])
- def _is_lib_on_device(self, path):
- return self.adb.run(['shell', 'ls', path])
+
+def list_tests():
+ tests = []
+ for name, value in globals().items():
+ if isinstance(value, type) and issubclass(value, unittest.TestCase):
+ test_methods = [x for x, y in value.__dict__.items()
+ if isinstance(y, types.FunctionType) and x.startswith('test')]
+ for method in test_methods:
+ tests.append(name + '.' + method)
+ print(' '.join(sorted(tests)))
def main():
+ if len(sys.argv) == 2 and sys.argv[1] == '--list-tests':
+ list_tests()
+ return
os.chdir(get_script_dir())
build_testdata()
if AdbHelper().get_android_version() < 7:
log_info("Skip tests on Android version < N.")
sys.exit(0)
- unittest.main(failfast=True)
+ unittest.main(failfast=True, verbosity=2)
if __name__ == '__main__':
main()
diff --git a/simpleperf/scripts/update.py b/simpleperf/scripts/update.py
index 53ac74f..65281aa 100644
--- a/simpleperf/scripts/update.py
+++ b/simpleperf/scripts/update.py
@@ -34,7 +34,7 @@
self.need_strip = need_strip
-install_list = [
+INSTALL_LIST = [
# simpleperf on device
InstallEntry('sdk_arm64-sdk', 'simpleperf', 'android/arm64/simpleperf'),
InstallEntry('sdk_arm64-sdk', 'simpleperf32', 'android/arm/simpleperf'),
@@ -51,18 +51,24 @@
InstallEntry('sdk', 'simpleperf32.exe', 'windows/x86/simpleperf.exe', True),
# libsimpleperf_report.so on host
- InstallEntry('sdk_arm64-sdk', 'libsimpleperf_report.so', 'linux/x86_64/libsimpleperf_report.so', True),
- InstallEntry('sdk_arm64-sdk', 'libsimpleperf_report32.so', 'linux/x86/libsimpleperf_report.so', True),
- InstallEntry('sdk_mac', 'libsimpleperf_report.dylib', 'darwin/x86_64/libsimpleperf_report.dylib'),
+ InstallEntry('sdk_arm64-sdk', 'libsimpleperf_report.so', 'linux/x86_64/libsimpleperf_report.so',
+ True),
+ InstallEntry('sdk_arm64-sdk', 'libsimpleperf_report32.so', 'linux/x86/libsimpleperf_report.so',
+ True),
+ InstallEntry('sdk_mac', 'libsimpleperf_report.dylib',
+ 'darwin/x86_64/libsimpleperf_report.dylib'),
InstallEntry('sdk_mac', 'libsimpleperf_report32.so', 'darwin/x86/libsimpleperf_report.dylib'),
- InstallEntry('sdk', 'libsimpleperf_report.dll', 'windows/x86_64/libsimpleperf_report.dll', True),
+ InstallEntry('sdk', 'libsimpleperf_report.dll', 'windows/x86_64/libsimpleperf_report.dll',
+ True),
InstallEntry('sdk', 'libsimpleperf_report32.dll', 'windows/x86/libsimpleperf_report.dll', True),
# libwinpthread-1.dll on windows host
- InstallEntry('local:../../../../prebuilts/gcc/linux-x86/host/x86_64-w64-mingw32-4.8/x86_64-w64-mingw32/bin/libwinpthread-1.dll',
- 'libwinpthread-1.dll', 'windows/x86_64/libwinpthread-1.dll', False),
- InstallEntry('local:../../../../prebuilts/gcc/linux-x86/host/x86_64-w64-mingw32-4.8/x86_64-w64-mingw32/lib32/libwinpthread-1.dll',
- 'libwinpthread-1_32.dll', 'windows/x86/libwinpthread-1.dll', False),
+ InstallEntry('local:../../../../prebuilts/gcc/linux-x86/host/x86_64-w64-mingw32-4.8' +
+ '/x86_64-w64-mingw32/bin/libwinpthread-1.dll', 'libwinpthread-1.dll',
+ 'windows/x86_64/libwinpthread-1.dll', False),
+ InstallEntry('local:../../../../prebuilts/gcc/linux-x86/host/x86_64-w64-mingw32-4.8' +
+ '/x86_64-w64-mingw32/lib32/libwinpthread-1.dll', 'libwinpthread-1_32.dll',
+ 'windows/x86/libwinpthread-1.dll', False),
]
@@ -123,7 +129,7 @@
def install_new_release(branch, build, install_dir):
"""Installs the new release."""
- for entry in install_list:
+ for entry in INSTALL_LIST:
install_entry(branch, build, install_dir, entry)
@@ -139,9 +145,9 @@
os.chmod(name, exe_stat.st_mode | stat.S_IEXEC)
if need_strip:
check_call(['strip', name])
- dir = os.path.dirname(install_path)
- if not os.path.isdir(dir):
- os.makedirs(dir)
+ dirname = os.path.dirname(install_path)
+ if not os.path.isdir(dirname):
+ os.makedirs(dirname)
shutil.move(name, install_path)
@@ -185,4 +191,4 @@
if __name__ == '__main__':
- main()
\ No newline at end of file
+ main()
diff --git a/simpleperf/scripts/utils.py b/simpleperf/scripts/utils.py
index b224ce8..e14436f 100644
--- a/simpleperf/scripts/utils.py
+++ b/simpleperf/scripts/utils.py
@@ -69,17 +69,17 @@
def disable_debug_log():
logging.getLogger().setLevel(logging.WARN)
-def str_to_bytes(str):
+def str_to_bytes(str_value):
if not is_python3():
- return str
+ return str_value
# In python 3, str are wide strings whereas the C api expects 8 bit strings,
# hence we have to convert. For now using utf-8 as the encoding.
- return str.encode('utf-8')
+ return str_value.encode('utf-8')
-def bytes_to_str(bytes):
+def bytes_to_str(bytes_value):
if not is_python3():
- return bytes
- return bytes.decode('utf-8')
+ return bytes_value
+ return bytes_value.decode('utf-8')
def get_target_binary_path(arch, binary_name):
if arch == 'aarch64':
@@ -94,21 +94,21 @@
def get_host_binary_path(binary_name):
- dir = os.path.join(get_script_dir(), 'bin')
+ dirname = os.path.join(get_script_dir(), 'bin')
if is_windows():
if binary_name.endswith('.so'):
binary_name = binary_name[0:-3] + '.dll'
elif '.' not in binary_name:
binary_name += '.exe'
- dir = os.path.join(dir, 'windows')
+ dirname = os.path.join(dirname, 'windows')
elif sys.platform == 'darwin': # OSX
if binary_name.endswith('.so'):
binary_name = binary_name[0:-3] + '.dylib'
- dir = os.path.join(dir, 'darwin')
+ dirname = os.path.join(dirname, 'darwin')
else:
- dir = os.path.join(dir, 'linux')
- dir = os.path.join(dir, 'x86_64' if sys.maxsize > 2 ** 32 else 'x86')
- binary_path = os.path.join(dir, binary_name)
+ dirname = os.path.join(dirname, 'linux')
+ dirname = os.path.join(dirname, 'x86_64' if sys.maxsize > 2 ** 32 else 'x86')
+ binary_path = os.path.join(dirname, binary_name)
if not os.path.isfile(binary_path):
log_fatal("can't find binary: %s" % binary_path)
return binary_path
@@ -121,7 +121,7 @@
stderr=subprocess.PIPE)
subproc.communicate()
return subproc.returncode == 0
- except:
+ except OSError:
return False
DEFAULT_NDK_PATH = {
@@ -304,6 +304,7 @@
if '86' in output:
return 'x86'
log_fatal('unsupported architecture: %s' % output.strip())
+ return ''
def get_android_version(self):
@@ -342,18 +343,14 @@
try:
subprocess.check_call(['open', report_path])
return
- except:
+ except subprocess.CalledProcessError:
pass
import webbrowser
try:
# Try to open the report with Chrome
- browser_key = ''
- for key, _ in webbrowser._browsers.items():
- if 'chrome' in key:
- browser_key = key
- browser = webbrowser.get(browser_key)
+ browser = webbrowser.get('google-chrome')
browser.open(report_path, new=0, autoraise=True)
- except:
+ except webbrowser.Error:
# webbrowser.get() doesn't work well on darwin/windows.
webbrowser.open_new_tab(report_path)
@@ -490,7 +487,7 @@
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
(stdoutdata, _) = subproc.communicate(str_to_bytes(addr_request))
stdoutdata = bytes_to_str(stdoutdata)
- except:
+ except OSError:
return
addr_map = {}
cur_line_list = None
@@ -591,7 +588,7 @@
subproc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
(stdoutdata, _) = subproc.communicate()
stdoutdata = bytes_to_str(stdoutdata)
- except:
+ except OSError:
return None
if not stdoutdata: