mirror of
https://github.com/MoreTore/openpilot.git
synced 2026-09-30 03:13:53 +08:00
Vendored
-1
@@ -1 +0,0 @@
|
||||
../pyflame/flame.sh
|
||||
Vendored
-9
@@ -1,9 +0,0 @@
|
||||
https://github.com/brendangregg/FlameGraph @ v1.0
|
||||
|
||||
phone:
|
||||
git clone git@github.com:uber/pyflame.git
|
||||
cd pyflame
|
||||
git apply < ../fix-aarch64.patch
|
||||
./autogen.sh
|
||||
./configure
|
||||
make
|
||||
Vendored
-339
@@ -1,339 +0,0 @@
|
||||
diff --git a/.travis.yml b/.travis.yml
|
||||
index 9cc00ec..8a2d6c7 100644
|
||||
--- a/.travis.yml
|
||||
+++ b/.travis.yml
|
||||
@@ -12,6 +12,7 @@ env:
|
||||
- PYVERSION=python3.4
|
||||
- PYVERSION=python3.5
|
||||
- PYVERSION=python3.6
|
||||
+ - PYVERSION=python3.7
|
||||
|
||||
addons:
|
||||
apt:
|
||||
diff --git a/configure.ac b/configure.ac
|
||||
index 0858fc3..8827b3a 100644
|
||||
--- a/configure.ac
|
||||
+++ b/configure.ac
|
||||
@@ -23,7 +23,7 @@ AS_IF([test x"$host_cpu" = xx86_64],
|
||||
],
|
||||
[AC_MSG_NOTICE([Threading support will be disabled (only works on x86-64)])
|
||||
AC_DEFINE([ENABLE_THREADS], [0], [Threads are enabled.])
|
||||
- AC_DEFINE([USE_ELF64], [0], [Expect 64-bit ELF symbols.])
|
||||
+ AC_DEFINE([USE_ELF64], [1], [Expect 64-bit ELF symbols.])
|
||||
])
|
||||
|
||||
AC_DEFINE_UNQUOTED([HOST_CPU], ["$host_cpu"], [CPU of target architecture.])
|
||||
@@ -111,7 +111,12 @@ PKG_CHECK_MODULES([PY36], [python-3.6], [enable_py36="yes"], [AC_MSG_WARN([Build
|
||||
AM_CONDITIONAL([ENABLE_PY36], [test x"$enable_py36" = xyes])
|
||||
AM_COND_IF([ENABLE_PY36], [AC_DEFINE([ENABLE_PY36], [1], [Python 3.6 will be enabled])])
|
||||
|
||||
-AS_IF([test x"$enable_py26" = xyes -o x"$enable_py34" = xyes -o x"$enable_py36" = xyes],
|
||||
+enable_py37=no
|
||||
+PKG_CHECK_MODULES([PY37], [python-3.7], [enable_py37="yes"], [AC_MSG_WARN([Building without Python 3.7 support])])
|
||||
+AM_CONDITIONAL([ENABLE_PY37], [test x"$enable_py37" = xyes])
|
||||
+AM_COND_IF([ENABLE_PY37], [AC_DEFINE([ENABLE_PY37], [1], [Python 3.7 will be enabled])])
|
||||
+
|
||||
+AS_IF([test x"$enable_py26" = xyes -o x"$enable_py34" = xyes -o x"$enable_py36" = xyes -o x"$enable_py37" = xyes],
|
||||
[AC_MSG_NOTICE([Found at least one copy of Python.h])],
|
||||
[AC_MSG_ERROR([Failed to find a supported Python.h])]
|
||||
)
|
||||
@@ -127,7 +132,8 @@ echo
|
||||
echo " with threads = $enable_threads"
|
||||
echo " with Python 2.6/7 = $enable_py26"
|
||||
echo " with Python 3.4/5 = $enable_py34"
|
||||
-echo " with Python 3.6+ = $enable_py36"
|
||||
+echo " with Python 3.6 = $enable_py36"
|
||||
+echo " with Python 3.7+ = $enable_py37"
|
||||
echo
|
||||
echo " CXX = $CXX"
|
||||
echo " CXXFLAGS = $CXXFLAGS"
|
||||
diff --git a/docs/faq.rst b/docs/faq.rst
|
||||
index 6fe8d1e..da6cec8 100644
|
||||
--- a/docs/faq.rst
|
||||
+++ b/docs/faq.rst
|
||||
@@ -7,7 +7,7 @@ What Python Versions Are Supported?
|
||||
Python 2 is tested with Python 2.6 and 2.7. Earlier versions of Python 2 are
|
||||
likely to work as well, but have not been tested.
|
||||
|
||||
-Python 3 is tested with Python 3.4, 3.5, and 3.6. Python 3.6 introduces a new
|
||||
+Python 3 is tested with Python 3.4, 3.5, 3.6 and 3.7 Python 3.6 introduces a new
|
||||
ABI for the ``PyCodeObject`` type, so Pyflame only supports the Python 3
|
||||
versions that header files were available for when Pyflame was compiled.
|
||||
|
||||
diff --git a/docs/man.md b/docs/man.md
|
||||
index 1aa8d25..538a764 100644
|
||||
--- a/docs/man.md
|
||||
+++ b/docs/man.md
|
||||
@@ -87,7 +87,7 @@ The following options are less commonly used.
|
||||
cases when profiling embedded Python builds (e.g. uWSGI), and only if
|
||||
pyflame doesn't automatically detect the correct ABI. *VERSION* should be a
|
||||
two digit integer consisting of the Python major and minor version, e.g. 27
|
||||
- for Python 2.7 or 36 for Python 3.6.
|
||||
+ for Python 2.7, 36 for Python 3.6 or 37 for Python 3.7.
|
||||
|
||||
**--flamechart**
|
||||
: Print the timestamp for each stack. This is useful for generating "flame
|
||||
diff --git a/docs/pyflame.man b/docs/pyflame.man
|
||||
index 971a1de..7fdeb85 100644
|
||||
--- a/docs/pyflame.man
|
||||
+++ b/docs/pyflame.man
|
||||
@@ -119,7 +119,7 @@ uWSGI), and only if pyflame doesn\[aq]t automatically detect the correct
|
||||
ABI.
|
||||
\f[I]VERSION\f[] should be a two digit integer consisting of the Python
|
||||
major and minor version, e.g.
|
||||
-27 for Python 2.7 or 36 for Python 3.6.
|
||||
+27 for Python 2.7, 36 for Python 3.6 or 37 for Python 3.7.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
diff --git a/src/Makefile.am b/src/Makefile.am
|
||||
index 11d8ab6..4a62bdb 100644
|
||||
--- a/src/Makefile.am
|
||||
+++ b/src/Makefile.am
|
||||
@@ -2,11 +2,11 @@
|
||||
#
|
||||
# The way this code is structured is the code that know about Python interpreter
|
||||
# internals is in frob.cc. This file isn't compiled directly, instead there are
|
||||
-# files frob{26,34,36}.cc that define preprocessor macros to compile frob.cc for
|
||||
+# files frob{26,34,36,37}.cc that define preprocessor macros to compile frob.cc for
|
||||
# different Python ABIs.
|
||||
#
|
||||
# The libtool magic here makes it so that frob26.cc is compiled with python2
|
||||
-# flags, and frob3{4,6}.cc are compiled with python3.4/3.6 flags.
|
||||
+# flags, and frob3{4,6,7}.cc are compiled with python3.4/3.6/3.7 flags.
|
||||
|
||||
bin_PROGRAMS = pyflame
|
||||
pyflame_SOURCES = aslr.cc frame.cc thread.cc namespace.cc posix.cc prober.cc ptrace.cc pyflame.cc pyfrob.cc symbol.cc
|
||||
@@ -34,3 +34,11 @@ libfrob36_la_CXXFLAGS = $(PY36_CFLAGS)
|
||||
noinst_LTLIBRARIES += libfrob36.la
|
||||
pyflame_LDADD += libfrob36.la
|
||||
endif
|
||||
+
|
||||
+if ENABLE_PY37
|
||||
+libfrob37_la_SOURCES = frob37.cc offset37.c
|
||||
+libfrob37_la_CFLAGS = $(PY37_CFLAGS)
|
||||
+libfrob37_la_CXXFLAGS = $(PY37_CFLAGS)
|
||||
+noinst_LTLIBRARIES += libfrob37.la
|
||||
+pyflame_LDADD += libfrob37.la
|
||||
+endif
|
||||
diff --git a/src/frob.cc b/src/frob.cc
|
||||
index 6fcff39..c1c5b30 100644
|
||||
--- a/src/frob.cc
|
||||
+++ b/src/frob.cc
|
||||
@@ -17,6 +17,7 @@
|
||||
// information.
|
||||
|
||||
#include <Python.h>
|
||||
+
|
||||
#include <frameobject.h>
|
||||
|
||||
#if PYFLAME_PY_VERSION >= 34
|
||||
@@ -89,6 +90,22 @@ unsigned long ByteData(unsigned long addr) {
|
||||
return addr + offsetof(PyBytesObject, ob_sval);
|
||||
}
|
||||
|
||||
+#elif PYFLAME_PY_VERSION == 37
|
||||
+namespace py37 {
|
||||
+std::string StringDataPython3(pid_t pid, unsigned long addr);
|
||||
+
|
||||
+unsigned long StringSize(unsigned long addr) {
|
||||
+ return addr + offsetof(PyVarObject, ob_size);
|
||||
+}
|
||||
+
|
||||
+std::string StringData(pid_t pid, unsigned long addr) {
|
||||
+ return StringDataPython3(pid, addr);
|
||||
+}
|
||||
+
|
||||
+unsigned long ByteData(unsigned long addr) {
|
||||
+ return addr + offsetof(PyBytesObject, ob_sval);
|
||||
+}
|
||||
+
|
||||
#else
|
||||
static_assert(false, "uh oh, bad PYFLAME_PY_VERSION");
|
||||
#endif
|
||||
diff --git a/src/prober.cc b/src/prober.cc
|
||||
index a691c83..c653357 100644
|
||||
--- a/src/prober.cc
|
||||
+++ b/src/prober.cc
|
||||
@@ -64,7 +64,7 @@ static const char usage_str[] =
|
||||
" -x, --exclude-idle Exclude idle time from statistics\n"
|
||||
"\n"
|
||||
"Advanced Options:\n"
|
||||
- " --abi Force a particular Python ABI (26, 34, 36)\n"
|
||||
+ " --abi Force a particular Python ABI (26, 34, 36, 37)\n"
|
||||
" --flamechart Include timestamps for generating Chrome "
|
||||
"\"flamecharts\"\n");
|
||||
|
||||
@@ -79,6 +79,9 @@ static const int build_abis[] = {
|
||||
#ifdef ENABLE_PY36
|
||||
36,
|
||||
#endif
|
||||
+#ifdef ENABLE_PY37
|
||||
+ 37,
|
||||
+#endif
|
||||
};
|
||||
|
||||
static_assert(sizeof(build_abis) > 0, "No Python ABIs detected!");
|
||||
@@ -221,6 +224,9 @@ int Prober::ParseOpts(int argc, char **argv) {
|
||||
case 36:
|
||||
abi_ = PyABI::Py36;
|
||||
break;
|
||||
+ case 37:
|
||||
+ abi_ = PyABI::Py37;
|
||||
+ break;
|
||||
default:
|
||||
std::cerr << "Unknown or unsupported ABI version: " << abi_version
|
||||
<< "\n";
|
||||
diff --git a/src/ptrace.cc b/src/ptrace.cc
|
||||
index 610fabb..85483ad 100644
|
||||
--- a/src/ptrace.cc
|
||||
+++ b/src/ptrace.cc
|
||||
@@ -122,6 +122,7 @@ void PtraceInterrupt(pid_t pid) {
|
||||
DoWait(pid);
|
||||
}
|
||||
|
||||
+#if 0
|
||||
user_regs_struct PtraceGetRegs(pid_t pid) {
|
||||
user_regs_struct regs;
|
||||
if (ptrace(PTRACE_GETREGS, pid, 0, ®s)) {
|
||||
@@ -139,6 +140,7 @@ void PtraceSetRegs(pid_t pid, user_regs_struct regs) {
|
||||
throw PtraceException(ss.str());
|
||||
}
|
||||
}
|
||||
+#endif
|
||||
|
||||
void PtracePoke(pid_t pid, unsigned long addr, long data) {
|
||||
if (ptrace(PTRACE_POKEDATA, pid, addr, (void *)data)) {
|
||||
diff --git a/src/pyfrob.cc b/src/pyfrob.cc
|
||||
index 7c0588d..b280d96 100644
|
||||
--- a/src/pyfrob.cc
|
||||
+++ b/src/pyfrob.cc
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include "./pyfrob.h"
|
||||
|
||||
+#include <cstddef>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
@@ -25,6 +26,8 @@
|
||||
#include "./ptrace.h"
|
||||
#include "./symbol.h"
|
||||
|
||||
+extern size_t PYFLAME_PYTHON37_OFFSET;
|
||||
+
|
||||
#define FROB_FUNCS \
|
||||
std::vector<Thread> GetThreads(pid_t pid, PyAddresses addr, \
|
||||
bool enable_threads);
|
||||
@@ -126,11 +129,20 @@ FROB_FUNCS
|
||||
}
|
||||
#endif
|
||||
|
||||
+#ifdef ENABLE_PY37
|
||||
+namespace py37 {
|
||||
+FROB_FUNCS
|
||||
+extern size_t tstate_offset;
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
// Fill the addrs_ member
|
||||
int PyFrob::set_addrs_(PyABI *abi) {
|
||||
Namespace ns(pid_);
|
||||
try {
|
||||
addrs_ = Addrs(pid_, &ns, abi);
|
||||
+ if (*abi >= PyABI::Py37)
|
||||
+ addrs_.tstate_addr += PYFLAME_PYTHON37_OFFSET;
|
||||
} catch (const SymbolException &exc) {
|
||||
return 1;
|
||||
}
|
||||
@@ -172,6 +184,11 @@ int PyFrob::DetectABI(PyABI abi) {
|
||||
case PyABI::Py36:
|
||||
get_threads_ = py36::GetThreads;
|
||||
break;
|
||||
+#endif
|
||||
+#ifdef ENABLE_PY37
|
||||
+ case PyABI::Py37:
|
||||
+ get_threads_ = py37::GetThreads;
|
||||
+ break;
|
||||
#endif
|
||||
default:
|
||||
std::ostringstream os;
|
||||
@@ -198,4 +215,5 @@ std::string PyFrob::Status() const {
|
||||
std::vector<Thread> PyFrob::GetThreads(void) const {
|
||||
return get_threads_(pid_, addrs_, enable_threads_);
|
||||
}
|
||||
+
|
||||
} // namespace pyflame
|
||||
diff --git a/src/symbol.cc b/src/symbol.cc
|
||||
index 39c3e81..3a6123c 100644
|
||||
--- a/src/symbol.cc
|
||||
+++ b/src/symbol.cc
|
||||
@@ -137,31 +137,43 @@ PyABI ELF::WalkTable(int sym, int str, PyAddresses *addrs) {
|
||||
reinterpret_cast<const sym_t *>(p() + s->sh_offset + i * s->sh_entsize);
|
||||
const char *name =
|
||||
reinterpret_cast<const char *>(p() + d->sh_offset + sym->st_name);
|
||||
- if (!addrs->tstate_addr && strcmp(name, "_PyThreadState_Current") == 0) {
|
||||
- addrs->tstate_addr = static_cast<unsigned long>(sym->st_value);
|
||||
- } else if (!addrs->interp_head_addr && strcmp(name, "interp_head") == 0) {
|
||||
- addrs->interp_head_addr = static_cast<unsigned long>(sym->st_value);
|
||||
- } else if (!addrs->interp_head_addr &&
|
||||
- strcmp(name, "PyInterpreterState_Head") == 0) {
|
||||
- addrs->interp_head_fn_addr = static_cast<unsigned long>(sym->st_value);
|
||||
- } else if (!have_abi) {
|
||||
+ if (!addrs->tstate_addr) {
|
||||
+ if (strcmp(name, "_PyThreadState_Current") == 0) {
|
||||
+ addrs->tstate_addr = static_cast<unsigned long>(sym->st_value);
|
||||
+ } else if (strcmp(name, "_PyRuntime") == 0) {
|
||||
+ addrs->tstate_addr = static_cast<unsigned long>(sym->st_value);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if (!addrs->interp_head_addr) {
|
||||
+ if (strcmp(name, "interp_head") == 0) {
|
||||
+ addrs->interp_head_addr = static_cast<unsigned long>(sym->st_value);
|
||||
+ } else if (strcmp(name, "PyInterpreterState_Head") == 0) {
|
||||
+ addrs->interp_head_fn_addr = static_cast<unsigned long>(sym->st_value);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if (!have_abi) {
|
||||
if (strcmp(name, "PyString_Type") == 0) {
|
||||
// If we find PyString_Type, this is some kind of Python 2.
|
||||
have_abi = true;
|
||||
abi = PyABI::Py26;
|
||||
- } else if (strcmp(name, "PyBytes_Type") == 0) {
|
||||
+ } else if (strcmp(name, "PyBytes_Type") == 0 && abi < PyABI::Py34) {
|
||||
// If we find PyBytes_Type, it's Python 3. Continue looping though, in
|
||||
- // case we see a Python 3.6 symbol.
|
||||
+ // case we see a Python 3.6+ symbol.
|
||||
abi = PyABI::Py34;
|
||||
} else if (strcmp(name, "_PyEval_RequestCodeExtraIndex") == 0 ||
|
||||
strcmp(name, "_PyCode_GetExtra") == 0 ||
|
||||
strcmp(name, "_PyCode_SetExtra") == 0) {
|
||||
// Symbols added for Python 3.6, see:
|
||||
// https://www.python.org/dev/peps/pep-0523/
|
||||
- have_abi = true;
|
||||
abi = PyABI::Py36;
|
||||
+ } else if (strcmp(name, "_PyRuntime") == 0) {
|
||||
+ have_abi = true;
|
||||
+ abi = PyABI::Py37;
|
||||
}
|
||||
}
|
||||
+
|
||||
}
|
||||
return abi;
|
||||
}
|
||||
diff --git a/src/symbol.h b/src/symbol.h
|
||||
index bb92b9a..818424d 100644
|
||||
--- a/src/symbol.h
|
||||
+++ b/src/symbol.h
|
||||
@@ -53,7 +53,8 @@ enum class PyABI {
|
||||
Unknown = 0, // Unknown Python ABI
|
||||
Py26 = 26, // ABI for Python 2.6/2.7
|
||||
Py34 = 34, // ABI for Python 3.4/3.5
|
||||
- Py36 = 36 // ABI for Python 3.6
|
||||
+ Py36 = 36, // ABI for Python 3.6
|
||||
+ Py37 = 37 // ABI for Python 3.7
|
||||
};
|
||||
|
||||
// Symbols
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
sudo $HOME/one/external/pyflame/pyflame -s 5 -o /tmp/perf$1.txt -p $1 &&
|
||||
$HOME/one/external/pyflame/flamegraph.pl /tmp/perf$1.txt > /tmp/perf$1.svg &&
|
||||
google-chrome /tmp/perf$1.svg
|
||||
Vendored
-1125
File diff suppressed because it is too large
Load Diff
Vendored
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Vendored
-8
@@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd "$( dirname "${BASH_SOURCE[0]}" )"
|
||||
|
||||
ssh "$1" '$HOME/one/external/simpleperf/bin/android/arm64/simpleperf record --call-graph fp -a --duration 10 -o /tmp/perf.data'
|
||||
scp "$1":/tmp/perf.data /tmp/perf.data
|
||||
python2 report_html.py -i /tmp/perf.data -o /tmp/report.html
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
git clone https://android.googlesource.com/platform/prebuilts/simpleperf
|
||||
git reset --hard 311a9d2cd27841498fc90a0b26a755deb47e7ebd
|
||||
cp -r report_html.* simpleperf_report_lib.py utils.py inferno lib ~/one/external/simpleperf/
|
||||
-136
@@ -1,136 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2016 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
|
||||
class CallSite:
|
||||
|
||||
def __init__(self, method, dso):
|
||||
self.method = method
|
||||
self.dso = dso
|
||||
|
||||
|
||||
class Thread:
|
||||
|
||||
def __init__(self, tid, pid):
|
||||
self.tid = tid
|
||||
self.pid = pid
|
||||
self.name = ""
|
||||
self.samples = []
|
||||
self.flamegraph = FlameGraphCallSite("root", "", 0)
|
||||
self.num_samples = 0
|
||||
self.num_events = 0
|
||||
|
||||
def add_callchain(self, callchain, symbol, sample):
|
||||
self.name = sample.thread_comm
|
||||
self.num_samples += 1
|
||||
self.num_events += sample.period
|
||||
chain = []
|
||||
for j in range(callchain.nr):
|
||||
entry = callchain.entries[callchain.nr - j - 1]
|
||||
if entry.ip == 0:
|
||||
continue
|
||||
chain.append(CallSite(entry.symbol.symbol_name, entry.symbol.dso_name))
|
||||
|
||||
chain.append(CallSite(symbol.symbol_name, symbol.dso_name))
|
||||
self.flamegraph.add_callchain(chain, sample.period)
|
||||
|
||||
|
||||
class Process:
|
||||
|
||||
def __init__(self, name, pid):
|
||||
self.name = name
|
||||
self.pid = pid
|
||||
self.threads = {}
|
||||
self.cmd = ""
|
||||
self.props = {}
|
||||
# num_samples is the count of samples recorded in the profiling file.
|
||||
self.num_samples = 0
|
||||
# num_events is the count of events contained in all samples. Each sample contains a
|
||||
# count of events happened since last sample. If we use cpu-cycles event, the count
|
||||
# shows how many cpu-cycles have happened during recording.
|
||||
self.num_events = 0
|
||||
|
||||
def get_thread(self, tid, pid):
|
||||
thread = self.threads.get(tid)
|
||||
if thread is None:
|
||||
thread = self.threads[tid] = Thread(tid, pid)
|
||||
return thread
|
||||
|
||||
def add_sample(self, sample, symbol, callchain):
|
||||
thread = self.get_thread(sample.tid, sample.pid)
|
||||
thread.add_callchain(callchain, symbol, sample)
|
||||
self.num_samples += 1
|
||||
# sample.period is the count of events happened since last sample.
|
||||
self.num_events += sample.period
|
||||
|
||||
|
||||
class FlameGraphCallSite:
|
||||
|
||||
callsite_counter = 0
|
||||
@classmethod
|
||||
def _get_next_callsite_id(cls):
|
||||
cls.callsite_counter += 1
|
||||
return cls.callsite_counter
|
||||
|
||||
def __init__(self, method, dso, id):
|
||||
# map from (dso, method) to FlameGraphCallSite. Used to speed up add_callchain().
|
||||
self.child_dict = {}
|
||||
self.children = []
|
||||
self.method = method
|
||||
self.dso = dso
|
||||
self.num_events = 0
|
||||
self.offset = 0 # Offset allows position nodes in different branches.
|
||||
self.id = id
|
||||
|
||||
def weight(self):
|
||||
return float(self.num_events)
|
||||
|
||||
def add_callchain(self, chain, num_events):
|
||||
self.num_events += num_events
|
||||
current = self
|
||||
for callsite in chain:
|
||||
current = current._get_child(callsite)
|
||||
current.num_events += num_events
|
||||
|
||||
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())
|
||||
return child
|
||||
|
||||
def trim_callchain(self, min_num_events):
|
||||
""" Remove call sites with num_events < min_num_events in the subtree.
|
||||
Remaining children are collected in a list.
|
||||
"""
|
||||
for key in self.child_dict:
|
||||
child = self.child_dict[key]
|
||||
if child.num_events >= min_num_events:
|
||||
child.trim_callchain(min_num_events)
|
||||
self.children.append(child)
|
||||
# Relese child_dict since it will not be used.
|
||||
self.child_dict = None
|
||||
|
||||
def get_max_depth(self):
|
||||
return max([c.get_max_depth() for c in self.children]) + 1 if self.children else 1
|
||||
|
||||
def generate_offset(self, start_offset):
|
||||
self.offset = start_offset
|
||||
child_offset = start_offset
|
||||
for child in self.children:
|
||||
child_offset = child.generate_offset(child_offset)
|
||||
return self.offset + self.num_events
|
||||
-1
File diff suppressed because one or more lines are too long
-333
@@ -1,333 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2016 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
Inferno is a tool to generate flamegraphs for android programs. It was originally written
|
||||
to profile surfaceflinger (Android compositor) but it can be used for other C++ program.
|
||||
It uses simpleperf to collect data. Programs have to be compiled with frame pointers which
|
||||
excludes ART based programs for the time being.
|
||||
|
||||
Here is how it works:
|
||||
|
||||
1/ Data collection is started via simpleperf and pulled locally as "perf.data".
|
||||
2/ The raw format is parsed, callstacks are merged to form a flamegraph data structure.
|
||||
3/ The data structure is used to generate a SVG embedded into an HTML page.
|
||||
4/ Javascript is injected to allow flamegraph navigation, search, coloring model.
|
||||
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
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 *
|
||||
|
||||
|
||||
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"]
|
||||
if args.app:
|
||||
app_profiler_args += ["-p", args.app]
|
||||
elif args.native_program:
|
||||
app_profiler_args += ["-np", args.native_program]
|
||||
else:
|
||||
log_exit("Please set profiling target with -p or -np option.")
|
||||
if args.compile_java_code:
|
||||
app_profiler_args.append("--compile_java_code")
|
||||
if args.disable_adb_root:
|
||||
app_profiler_args.append("--disable_adb_root")
|
||||
record_arg_str = ""
|
||||
if args.dwarf_unwinding:
|
||||
record_arg_str += "-g "
|
||||
else:
|
||||
record_arg_str += "--call-graph fp "
|
||||
if args.events:
|
||||
tokens = args.events.split()
|
||||
if len(tokens) == 2:
|
||||
num_events = tokens[0]
|
||||
event_name = tokens[1]
|
||||
record_arg_str += "-c %s -e %s " % (num_events, event_name)
|
||||
else:
|
||||
log_exit("Event format string of -e option cann't be recognized.")
|
||||
log_info("Using event sampling (-c %s -e %s)." % (num_events, event_name))
|
||||
else:
|
||||
record_arg_str += "-f %d " % args.sample_frequency
|
||||
log_info("Using frequency sampling (-f %d)." % args.sample_frequency)
|
||||
record_arg_str += "--duration %d " % args.capture_duration
|
||||
app_profiler_args += ["-r", record_arg_str]
|
||||
returncode = subprocess.call(app_profiler_args)
|
||||
return returncode == 0
|
||||
|
||||
|
||||
def parse_samples(process, args, sample_filter_fn):
|
||||
"""Read samples from record file.
|
||||
process: Process object
|
||||
args: arguments
|
||||
sample_filter_fn: if not None, is used to modify and filter samples.
|
||||
It returns false for samples should be filtered out.
|
||||
"""
|
||||
|
||||
record_file = args.record_file
|
||||
symfs_dir = args.symfs
|
||||
kallsyms_file = args.kallsyms
|
||||
|
||||
lib = ReportLib()
|
||||
|
||||
lib.ShowIpForUnknownSymbol()
|
||||
if symfs_dir:
|
||||
lib.SetSymfs(symfs_dir)
|
||||
if record_file:
|
||||
lib.SetRecordFile(record_file)
|
||||
if kallsyms_file:
|
||||
lib.SetKallsymsFile(kallsyms_file)
|
||||
if args.show_art_frames:
|
||||
lib.ShowArtFrames(True)
|
||||
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]
|
||||
if lib.MetaInfo().get('trace_offcpu') == 'true':
|
||||
process.props['trace_offcpu'] = True
|
||||
if args.one_flamegraph:
|
||||
log_exit("It doesn't make sense to report with --one-flamegraph for perf.data " +
|
||||
"recorded with --trace-offcpu.""")
|
||||
else:
|
||||
process.props['trace_offcpu'] = False
|
||||
|
||||
while True:
|
||||
sample = lib.GetNextSample()
|
||||
if sample is None:
|
||||
lib.Close()
|
||||
break
|
||||
symbol = lib.GetSymbolOfCurrentSample()
|
||||
callchain = lib.GetCallChainOfCurrentSample()
|
||||
if sample_filter_fn and not sample_filter_fn(sample, symbol, callchain):
|
||||
continue
|
||||
process.add_sample(sample, symbol, callchain)
|
||||
|
||||
if process.pid == 0:
|
||||
main_threads = [thread for thread in process.threads.values() if thread.tid == thread.pid]
|
||||
if main_threads:
|
||||
process.name = main_threads[0].name
|
||||
process.pid = main_threads[0].pid
|
||||
|
||||
for thread in process.threads.values():
|
||||
min_event_count = thread.num_events * args.min_callchain_percentage * 0.01
|
||||
thread.flamegraph.trim_callchain(min_event_count)
|
||||
|
||||
log_info("Parsed %s callchains." % process.num_samples)
|
||||
|
||||
|
||||
def get_local_asset_content(local_path):
|
||||
"""
|
||||
Retrieves local package text content
|
||||
:param local_path: str, filename of local asset
|
||||
:return: str, the content of local_path
|
||||
"""
|
||||
with open(os.path.join(os.path.dirname(__file__), local_path), 'r') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def output_report(process, args):
|
||||
"""
|
||||
Generates a HTML report representing the result of simpleperf sampling as flamegraph
|
||||
:param process: Process object
|
||||
:return: str, absolute path to the file
|
||||
"""
|
||||
f = open(args.report_path, 'w')
|
||||
filepath = os.path.realpath(f.name)
|
||||
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 ""))
|
||||
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>')
|
||||
f.write('<img height="180" alt = "Embedded Image" src ="data')
|
||||
f.write(get_local_asset_content("inferno.b64"))
|
||||
f.write('"/>')
|
||||
process_entry = ("Process : %s (%d)<br/>" % (process.name, process.pid)) if process.pid else ""
|
||||
if process.props['trace_offcpu']:
|
||||
event_entry = 'Total time: %s<br/>' % get_proper_scaled_time_string(process.num_events)
|
||||
else:
|
||||
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 ""
|
||||
f.write("""<div style='display:inline-block;'>
|
||||
<font size='8'>
|
||||
Inferno Flamegraph Report%s</font><br/><br/>
|
||||
%s
|
||||
Date : %s<br/>
|
||||
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))
|
||||
if 'ro.product.model' in process.props:
|
||||
f.write(
|
||||
"Machine : %s (%s) by %s<br/>" %
|
||||
(process.props["ro.product.model"],
|
||||
process.props["ro.product.name"],
|
||||
process.props["ro.product.manufacturer"]))
|
||||
if process.cmd:
|
||||
f.write("Capture : %s<br/><br/>" % process.cmd)
|
||||
f.write("</div>")
|
||||
f.write("""<br/><br/>
|
||||
<div>Navigate with WASD, zoom in with SPACE, zoom out with BACKSPACE.</div>""")
|
||||
f.write("<script>%s</script>" % get_local_asset_content("script.js"))
|
||||
if not args.embedded_flamegraph:
|
||||
f.write("<script>document.addEventListener('DOMContentLoaded', flamegraphInit);</script>")
|
||||
|
||||
# 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)
|
||||
|
||||
f.write("</div>")
|
||||
if not args.embedded_flamegraph:
|
||||
f.write("</body></html")
|
||||
f.close()
|
||||
return "file://" + filepath
|
||||
|
||||
|
||||
def generate_threads_offsets(process):
|
||||
for thread in process.threads.values():
|
||||
thread.flamegraph.generate_offset(0)
|
||||
|
||||
|
||||
def collect_machine_info(process):
|
||||
adb = AdbHelper()
|
||||
process.props = {}
|
||||
process.props['ro.product.model'] = adb.get_property('ro.product.model')
|
||||
process.props['ro.product.name'] = adb.get_property('ro.product.name')
|
||||
process.props['ro.product.manufacturer'] = adb.get_property('ro.product.manufacturer')
|
||||
|
||||
|
||||
def main():
|
||||
# Allow deep callchain with length >1000.
|
||||
sys.setrecursionlimit(1500)
|
||||
parser = argparse.ArgumentParser(description="""Report samples in perf.data. Default option
|
||||
is: "-np surfaceflinger -f 6000 -t 10".""")
|
||||
record_group = parser.add_argument_group('Record options')
|
||||
record_group.add_argument('-du', '--dwarf_unwinding', action='store_true', help="""Perform
|
||||
unwinding using dwarf instead of fp.""")
|
||||
record_group.add_argument('-e', '--events', default="", help="""Sample based on event
|
||||
occurences instead of frequency. Format expected is
|
||||
"event_counts event_name". e.g: "10000 cpu-cyles". A few examples
|
||||
of event_name: cpu-cycles, cache-references, cache-misses,
|
||||
branch-instructions, branch-misses""")
|
||||
record_group.add_argument('-f', '--sample_frequency', type=int, default=6000, help="""Sample
|
||||
frequency""")
|
||||
record_group.add_argument('--compile_java_code', action='store_true',
|
||||
help="""On Android N and Android O, we need to compile Java code
|
||||
into native instructions to profile Java code. Android O
|
||||
also needs wrap.sh in the apk to use the native
|
||||
instructions.""")
|
||||
record_group.add_argument('-np', '--native_program', default="surfaceflinger", help="""Profile
|
||||
a native program. The program should be running on the device.
|
||||
Like -np surfaceflinger.""")
|
||||
record_group.add_argument('-p', '--app', help="""Profile an Android app, given the package
|
||||
name. Like -p com.example.android.myapp.""")
|
||||
record_group.add_argument('--record_file', default='perf.data', help='Default is perf.data.')
|
||||
record_group.add_argument('-sc', '--skip_collection', action='store_true', help="""Skip data
|
||||
collection""")
|
||||
record_group.add_argument('-t', '--capture_duration', type=int, default=10, help="""Capture
|
||||
duration in seconds.""")
|
||||
|
||||
report_group = parser.add_argument_group('Report options')
|
||||
report_group.add_argument('-c', '--color', default='hot', choices=['hot', 'dso', 'legacy'],
|
||||
help="""Color theme: hot=percentage of samples, dso=callsite DSO
|
||||
name, legacy=brendan style""")
|
||||
report_group.add_argument('--embedded_flamegraph', action='store_true', help="""Generate
|
||||
embedded flamegraph.""")
|
||||
report_group.add_argument('--kallsyms', help='Set the path to find kernel symbols.')
|
||||
report_group.add_argument('--min_callchain_percentage', default=0.01, type=float, help="""
|
||||
Set min percentage of callchains shown in the report.
|
||||
It is used to limit nodes shown in the flamegraph. For example,
|
||||
when set to 0.01, only callchains taking >= 0.01%% of the event
|
||||
count of the owner thread are collected in the report.""")
|
||||
report_group.add_argument('--no_browser', action='store_true', help="""Don't open report
|
||||
in browser.""")
|
||||
report_group.add_argument('-o', '--report_path', default='report.html', help="""Set report
|
||||
path.""")
|
||||
report_group.add_argument('--one-flamegraph', action='store_true', help="""Generate one
|
||||
flamegraph instead of one for each thread.""")
|
||||
report_group.add_argument('--symfs', help="""Set the path to find binaries with symbols and
|
||||
debug info.""")
|
||||
report_group.add_argument('--title', help='Show a title in the report.')
|
||||
report_group.add_argument('--show_art_frames', action='store_true',
|
||||
help='Show frames of internal methods in the ART Java interpreter.')
|
||||
|
||||
debug_group = parser.add_argument_group('Debug options')
|
||||
debug_group.add_argument('--disable_adb_root', action='store_true', help="""Force adb to run
|
||||
in non root mode.""")
|
||||
args = parser.parse_args()
|
||||
process = Process("", 0)
|
||||
|
||||
if not args.skip_collection:
|
||||
process.name = args.app or args.native_program
|
||||
log_info("Starting data collection stage for process '%s'." % process.name)
|
||||
if not collect_data(args):
|
||||
log_exit("Unable to collect data.")
|
||||
result, output = AdbHelper().run_and_return_output(['shell', 'pidof', process.name])
|
||||
if result:
|
||||
try:
|
||||
process.pid = int(output)
|
||||
except:
|
||||
process.pid = 0
|
||||
collect_machine_info(process)
|
||||
else:
|
||||
args.capture_duration = 0
|
||||
|
||||
sample_filter_fn = None
|
||||
if args.one_flamegraph:
|
||||
def filter_fn(sample, symbol, callchain):
|
||||
sample.pid = sample.tid = process.pid
|
||||
return True
|
||||
sample_filter_fn = filter_fn
|
||||
if not args.title:
|
||||
args.title = ''
|
||||
args.title += '(One Flamegraph)'
|
||||
|
||||
parse_samples(process, args, sample_filter_fn)
|
||||
generate_threads_offsets(process)
|
||||
report_path = output_report(process, args)
|
||||
if not args.no_browser:
|
||||
open_report_in_browser(report_path)
|
||||
|
||||
log_info("Flamegraph generated at '%s'." % report_path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
-274
@@ -1,274 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
function flamegraphInit() {
|
||||
let flamegraph = document.getElementById('flamegraph_id');
|
||||
let svgs = flamegraph.getElementsByTagName('svg');
|
||||
for (let i = 0; i < svgs.length; ++i) {
|
||||
createZoomHistoryStack(svgs[i]);
|
||||
adjust_text_size(svgs[i]);
|
||||
}
|
||||
|
||||
function throttle(callback) {
|
||||
let running = false;
|
||||
return function() {
|
||||
if (!running) {
|
||||
running = true;
|
||||
window.requestAnimationFrame(function () {
|
||||
callback();
|
||||
running = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
window.addEventListener('resize', throttle(function() {
|
||||
let flamegraph = document.getElementById('flamegraph_id');
|
||||
let svgs = flamegraph.getElementsByTagName('svg');
|
||||
for (let i = 0; i < svgs.length; ++i) {
|
||||
adjust_text_size(svgs[i]);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Create a stack add the root svg element in it.
|
||||
function createZoomHistoryStack(svgElement) {
|
||||
svgElement.zoomStack = [svgElement.getElementById(svgElement.attributes['rootid'].value)];
|
||||
}
|
||||
|
||||
function adjust_node_text_size(x, svgWidth) {
|
||||
let title = x.getElementsByTagName('title')[0];
|
||||
let text = x.getElementsByTagName('text')[0];
|
||||
let rect = x.getElementsByTagName('rect')[0];
|
||||
|
||||
let width = parseFloat(rect.attributes['width'].value) * svgWidth * 0.01;
|
||||
|
||||
// Don't even bother trying to find a best fit. The area is too small.
|
||||
if (width < 28) {
|
||||
text.textContent = '';
|
||||
return;
|
||||
}
|
||||
// Remove dso and #samples which are here only for mouseover purposes.
|
||||
let methodName = title.textContent.split(' | ')[0];
|
||||
|
||||
let numCharacters;
|
||||
for (numCharacters = methodName.length; numCharacters > 4; numCharacters--) {
|
||||
// Avoid reflow by using hard-coded estimate instead of
|
||||
// text.getSubStringLength(0, numCharacters).
|
||||
if (numCharacters * 7.5 <= width) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (numCharacters == methodName.length) {
|
||||
text.textContent = methodName;
|
||||
return;
|
||||
}
|
||||
|
||||
text.textContent = methodName.substring(0, numCharacters-2) + '..';
|
||||
}
|
||||
|
||||
function adjust_text_size(svgElement) {
|
||||
let svgWidth = window.innerWidth;
|
||||
let x = svgElement.getElementsByTagName('g');
|
||||
for (let i = 0; i < x.length; i++) {
|
||||
adjust_node_text_size(x[i], svgWidth);
|
||||
}
|
||||
}
|
||||
|
||||
function zoom(e) {
|
||||
let svgElement = e.ownerSVGElement;
|
||||
let zoomStack = svgElement.zoomStack;
|
||||
zoomStack.push(e);
|
||||
displaySVGElement(svgElement);
|
||||
select(e);
|
||||
|
||||
// Show zoom out button.
|
||||
svgElement.getElementById('zoom_rect').style.display = 'block';
|
||||
svgElement.getElementById('zoom_text').style.display = 'block';
|
||||
}
|
||||
|
||||
function displaySVGElement(svgElement) {
|
||||
let zoomStack = svgElement.zoomStack;
|
||||
let e = zoomStack[zoomStack.length - 1];
|
||||
let clicked_rect = e.getElementsByTagName('rect')[0];
|
||||
let clicked_origin_x;
|
||||
let clicked_origin_y = clicked_rect.attributes['oy'].value;
|
||||
let clicked_origin_width;
|
||||
|
||||
if (zoomStack.length == 1) {
|
||||
// Show all nodes when zoomStack only contains the root node.
|
||||
// This is needed to show flamegraph containing more than one node at the root level.
|
||||
clicked_origin_x = 0;
|
||||
clicked_origin_width = 100;
|
||||
} else {
|
||||
clicked_origin_x = clicked_rect.attributes['ox'].value;
|
||||
clicked_origin_width = clicked_rect.attributes['owidth'].value;
|
||||
}
|
||||
|
||||
|
||||
let svgBox = svgElement.getBoundingClientRect();
|
||||
let svgBoxHeight = svgBox.height;
|
||||
let svgBoxWidth = 100;
|
||||
let scaleFactor = svgBoxWidth / clicked_origin_width;
|
||||
|
||||
let callsites = svgElement.getElementsByTagName('g');
|
||||
for (let i = 0; i < callsites.length; i++) {
|
||||
let text = callsites[i].getElementsByTagName('text')[0];
|
||||
let rect = callsites[i].getElementsByTagName('rect')[0];
|
||||
|
||||
let rect_o_x = parseFloat(rect.attributes['ox'].value);
|
||||
let rect_o_y = parseFloat(rect.attributes['oy'].value);
|
||||
|
||||
// Avoid multiple forced reflow by hiding nodes.
|
||||
if (rect_o_y > clicked_origin_y) {
|
||||
rect.style.display = 'none';
|
||||
text.style.display = 'none';
|
||||
continue;
|
||||
}
|
||||
rect.style.display = 'block';
|
||||
text.style.display = 'block';
|
||||
|
||||
let newrec_x = rect.attributes['x'].value = (rect_o_x - clicked_origin_x) * scaleFactor +
|
||||
'%';
|
||||
let newrec_y = rect.attributes['y'].value = rect_o_y + (svgBoxHeight - clicked_origin_y
|
||||
- 17 - 2);
|
||||
|
||||
text.attributes['y'].value = newrec_y + 12;
|
||||
text.attributes['x'].value = newrec_x;
|
||||
|
||||
rect.attributes['width'].value = (rect.attributes['owidth'].value * scaleFactor) + '%';
|
||||
}
|
||||
|
||||
adjust_text_size(svgElement);
|
||||
}
|
||||
|
||||
function unzoom(e) {
|
||||
let svgOwner = e.ownerSVGElement;
|
||||
let stack = svgOwner.zoomStack;
|
||||
|
||||
// Unhighlight whatever was selected.
|
||||
if (selected) {
|
||||
selected.classList.remove('s');
|
||||
}
|
||||
|
||||
// Stack management: Never remove the last element which is the flamegraph root.
|
||||
if (stack.length > 1) {
|
||||
let previouslySelected = stack.pop();
|
||||
select(previouslySelected);
|
||||
}
|
||||
|
||||
// Hide zoom out button.
|
||||
if (stack.length == 1) {
|
||||
svgOwner.getElementById('zoom_rect').style.display = 'none';
|
||||
svgOwner.getElementById('zoom_text').style.display = 'none';
|
||||
}
|
||||
|
||||
displaySVGElement(svgOwner);
|
||||
}
|
||||
|
||||
function search(e) {
|
||||
let term = prompt('Search for:', '');
|
||||
let callsites = e.ownerSVGElement.getElementsByTagName('g');
|
||||
|
||||
if (!term) {
|
||||
for (let i = 0; i < callsites.length; i++) {
|
||||
let rect = callsites[i].getElementsByTagName('rect')[0];
|
||||
rect.attributes['fill'].value = rect.attributes['ofill'].value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < callsites.length; i++) {
|
||||
let title = callsites[i].getElementsByTagName('title')[0];
|
||||
let rect = callsites[i].getElementsByTagName('rect')[0];
|
||||
if (title.textContent.indexOf(term) != -1) {
|
||||
rect.attributes['fill'].value = 'rgb(230,100,230)';
|
||||
} else {
|
||||
rect.attributes['fill'].value = rect.attributes['ofill'].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let selected;
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!selected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let nav = selected.attributes['nav'].value.split(',');
|
||||
let navigation_index;
|
||||
switch (e.keyCode) {
|
||||
// case 38: // ARROW UP
|
||||
case 87: navigation_index = 0; break; // W
|
||||
|
||||
// case 32 : // ARROW LEFT
|
||||
case 65: navigation_index = 1; break; // A
|
||||
|
||||
// case 43: // ARROW DOWN
|
||||
case 68: navigation_index = 3; break; // S
|
||||
|
||||
// case 39: // ARROW RIGHT
|
||||
case 83: navigation_index = 2; break; // D
|
||||
|
||||
case 32: zoom(selected); return false; // SPACE
|
||||
|
||||
case 8: // BACKSPACE
|
||||
unzoom(selected); return false;
|
||||
default: return true;
|
||||
}
|
||||
|
||||
if (nav[navigation_index] == '0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let target_element = selected.ownerSVGElement.getElementById(nav[navigation_index]);
|
||||
select(target_element);
|
||||
return false;
|
||||
});
|
||||
|
||||
function select(e) {
|
||||
if (selected) {
|
||||
selected.classList.remove('s');
|
||||
}
|
||||
selected = e;
|
||||
selected.classList.add('s');
|
||||
|
||||
// Update info bar
|
||||
let titleElement = selected.getElementsByTagName('title')[0];
|
||||
let text = titleElement.textContent;
|
||||
|
||||
// Parse title
|
||||
let method_and_info = text.split(' | ');
|
||||
let methodName = method_and_info[0];
|
||||
let info = method_and_info[1];
|
||||
|
||||
// Parse info
|
||||
// '/system/lib64/libhwbinder.so (4 events: 0.28%)'
|
||||
let regexp = /(.*) \((.*)\)/g;
|
||||
let match = regexp.exec(info);
|
||||
if (match.length > 2) {
|
||||
let percentage = match[2];
|
||||
// Write percentage
|
||||
let percentageTextElement = selected.ownerSVGElement.getElementById('percent_text');
|
||||
percentageTextElement.textContent = percentage;
|
||||
// console.log("'" + percentage + "'")
|
||||
}
|
||||
|
||||
// Set fields
|
||||
let barTextElement = selected.ownerSVGElement.getElementById('info_text');
|
||||
barTextElement.textContent = methodName;
|
||||
}
|
||||
-204
@@ -1,204 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2016 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import sys
|
||||
|
||||
SVG_NODE_HEIGHT = 17
|
||||
FONT_SIZE = 12
|
||||
|
||||
UNZOOM_NODE_ORIGIN_X = 10
|
||||
UNZOOM_NODE_WIDTH = 80
|
||||
INFO_NODE_ORIGIN_X = 120
|
||||
INFO_NODE_WIDTH = 800
|
||||
PERCENT_NODE_ORIGIN_X = 930
|
||||
PERCENT_NODE_WIDTH = 250
|
||||
SEARCH_NODE_ORIGIN_X = 1190
|
||||
SEARCH_NODE_WIDTH = 80
|
||||
RECT_TEXT_PADDING = 10
|
||||
|
||||
|
||||
def hash_to_float(string):
|
||||
return hash(string) / float(sys.maxsize)
|
||||
|
||||
|
||||
def getLegacyColor(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):
|
||||
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):
|
||||
r = 245 + 10 * (1 - callsite.weight() / total_weight)
|
||||
g = 110 + 105 * (1 - callsite.weight() / total_weight)
|
||||
b = 100
|
||||
return (r, g, b)
|
||||
|
||||
def get_proper_scaled_time_string(value):
|
||||
if value >= 1e9:
|
||||
return '%.3f s' % (value / 1e9)
|
||||
if value >= 1e6:
|
||||
return '%.3f ms' % (value / 1e6)
|
||||
if value >= 1e3:
|
||||
return '%.3f us' % (value / 1e3)
|
||||
return '%.0f ns' % value
|
||||
|
||||
def createSVGNode(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
|
||||
|
||||
method = callsite.method.replace(">", ">").replace("<", "<")
|
||||
if width <= 0:
|
||||
return
|
||||
|
||||
if color_scheme == "dso":
|
||||
r, g, b = getDSOColor(callsite.dso)
|
||||
elif color_scheme == "legacy":
|
||||
r, g, b = getLegacyColor(method)
|
||||
else:
|
||||
r, g, b = getHeatColor(callsite, total_weight)
|
||||
|
||||
r_border, g_border, b_border = [max(0, color - 50) for color in [r, g, b]]
|
||||
|
||||
if process.props['trace_offcpu']:
|
||||
weight_str = get_proper_scaled_time_string(callsite.weight())
|
||||
else:
|
||||
weight_str = "{:,}".format(int(callsite.weight())) + ' events'
|
||||
|
||||
f.write(
|
||||
"""<g id=%d class="n" onclick="zoom(this);" onmouseenter="select(this);" nav="%s">
|
||||
<title>%s | %s (%s: %3.2f%%)</title>
|
||||
<rect x="%f%%" y="%f" ox="%f" oy="%f" width="%f%%" owidth="%f" height="15.0"
|
||||
ofill="rgb(%d,%d,%d)" fill="rgb(%d,%d,%d)" style="stroke:rgb(%d,%d,%d)"/>
|
||||
<text x="%f%%" y="%f" font-size="%d" font-family="Monospace"></text>
|
||||
</g>""" %
|
||||
(callsite.id,
|
||||
','.join(str(x) for x in nav),
|
||||
method,
|
||||
callsite.dso,
|
||||
weight_str,
|
||||
callsite.weight() / total_weight * 100,
|
||||
x,
|
||||
y,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
width,
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
r_border,
|
||||
g_border,
|
||||
b_border,
|
||||
x,
|
||||
y + 12,
|
||||
FONT_SIZE))
|
||||
|
||||
|
||||
def renderSVGNodes(process, flamegraph, depth, f, total_weight, height, color_scheme):
|
||||
for i, child in enumerate(flamegraph.children):
|
||||
# Prebuild navigation target for wasd
|
||||
|
||||
if i == 0:
|
||||
left_index = 0
|
||||
else:
|
||||
left_index = flamegraph.children[i - 1].id
|
||||
|
||||
if i == len(flamegraph.children) - 1:
|
||||
right_index = 0
|
||||
else:
|
||||
right_index = flamegraph.children[i + 1].id
|
||||
|
||||
up_index = max(child.children, key=lambda x: x.weight()).id if child.children else 0
|
||||
|
||||
# up, left, down, right
|
||||
nav = [up_index, left_index, flamegraph.id, right_index]
|
||||
|
||||
createSVGNode(process, child, depth, f, total_weight, height, color_scheme, nav)
|
||||
# Recurse down
|
||||
renderSVGNodes(process, child, depth + 1, f, total_weight, height, color_scheme)
|
||||
|
||||
|
||||
def renderSearchNode(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)""/>
|
||||
<text id="search_text" class="t" x="%d" y="30" onclick="search(this);">Search</text>
|
||||
""" % (SEARCH_NODE_ORIGIN_X, SEARCH_NODE_WIDTH, SEARCH_NODE_ORIGIN_X + RECT_TEXT_PADDING))
|
||||
|
||||
|
||||
def renderUnzoomNode(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"
|
||||
fill="rgb(255,255,255)"/>
|
||||
<text id="zoom_text" style="display:none;" class="t" x="%d" y="30"
|
||||
onclick="unzoom(this);">Zoom out</text>
|
||||
""" % (UNZOOM_NODE_ORIGIN_X, UNZOOM_NODE_WIDTH, UNZOOM_NODE_ORIGIN_X + RECT_TEXT_PADDING))
|
||||
|
||||
|
||||
def renderInfoNode(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)"/>
|
||||
</clipPath>
|
||||
<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)"/>
|
||||
<text clip-path="url(#info_clip_path)" id="info_text" x="%d" y="30"></text>
|
||||
""" % (INFO_NODE_ORIGIN_X, INFO_NODE_WIDTH, INFO_NODE_ORIGIN_X, INFO_NODE_WIDTH,
|
||||
INFO_NODE_ORIGIN_X + RECT_TEXT_PADDING))
|
||||
|
||||
|
||||
def renderPercentNode(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)"/>
|
||||
<text id="percent_text" text-anchor="end" x="%d" y="30">100.00%%</text>
|
||||
""" % (PERCENT_NODE_ORIGIN_X, PERCENT_NODE_WIDTH,
|
||||
PERCENT_NODE_ORIGIN_X + PERCENT_NODE_WIDTH - RECT_TEXT_PADDING))
|
||||
|
||||
|
||||
def renderSVG(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)
|
||||
f.write("""<svg xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"
|
||||
width="100%%" height="100%%" style="border: 1px solid black;"
|
||||
rootid="%d">
|
||||
""" % (flamegraph.children[0].id))
|
||||
f.write("""<defs > <linearGradient id="background_gradiant" y1="0" y2="1" x1="0" x2="0" >
|
||||
<stop stop-color="#eeeeee" offset="5%" /> <stop stop-color="#efefb1" offset="90%" />
|
||||
</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)
|
||||
f.write("</svg></div><br/>\n\n")
|
||||
Vendored
-1339
File diff suppressed because it is too large
Load Diff
Vendored
-930
@@ -1,930 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (C) 2017 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from simpleperf_report_lib import ReportLib
|
||||
from utils import *
|
||||
|
||||
|
||||
class HtmlWriter(object):
|
||||
|
||||
def __init__(self, output_path):
|
||||
self.fh = open(output_path, 'w')
|
||||
self.tag_stack = []
|
||||
|
||||
def close(self):
|
||||
self.fh.close()
|
||||
|
||||
def open_tag(self, tag, **attrs):
|
||||
attr_str = ''
|
||||
for key in attrs:
|
||||
attr_str += ' %s="%s"' % (key, attrs[key])
|
||||
self.fh.write('<%s%s>' % (tag, attr_str))
|
||||
self.tag_stack.append(tag)
|
||||
return self
|
||||
|
||||
def close_tag(self, tag=None):
|
||||
if tag:
|
||||
assert tag == self.tag_stack[-1]
|
||||
self.fh.write('</%s>\n' % self.tag_stack.pop())
|
||||
|
||||
def add(self, text):
|
||||
self.fh.write(text)
|
||||
return self
|
||||
|
||||
def add_file(self, file_path):
|
||||
file_path = os.path.join(get_script_dir(), file_path)
|
||||
with open(file_path, 'r') as f:
|
||||
self.add(f.read())
|
||||
return self
|
||||
|
||||
def modify_text_for_html(text):
|
||||
return text.replace('>', '>').replace('<', '<')
|
||||
|
||||
class EventScope(object):
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.processes = {} # map from pid to ProcessScope
|
||||
self.sample_count = 0
|
||||
self.event_count = 0
|
||||
|
||||
def get_process(self, pid):
|
||||
process = self.processes.get(pid)
|
||||
if not process:
|
||||
process = self.processes[pid] = ProcessScope(pid)
|
||||
return process
|
||||
|
||||
def get_sample_info(self, gen_addr_hit_map):
|
||||
result = {}
|
||||
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()]
|
||||
return result
|
||||
|
||||
|
||||
class ProcessScope(object):
|
||||
|
||||
def __init__(self, pid):
|
||||
self.pid = pid
|
||||
self.name = ''
|
||||
self.event_count = 0
|
||||
self.threads = {} # map from tid to ThreadScope
|
||||
|
||||
def get_thread(self, tid, thread_name):
|
||||
thread = self.threads.get(tid)
|
||||
if not thread:
|
||||
thread = self.threads[tid] = ThreadScope(tid)
|
||||
thread.name = thread_name
|
||||
if self.pid == tid:
|
||||
self.name = thread_name
|
||||
return thread
|
||||
|
||||
def get_sample_info(self, gen_addr_hit_map):
|
||||
result = {}
|
||||
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()]
|
||||
return result
|
||||
|
||||
|
||||
class ThreadScope(object):
|
||||
|
||||
def __init__(self, tid):
|
||||
self.tid = tid
|
||||
self.name = ''
|
||||
self.event_count = 0
|
||||
self.libs = {} # map from lib_id to LibScope
|
||||
|
||||
def add_callstack(self, event_count, callstack, build_addr_hit_map):
|
||||
""" 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]
|
||||
# When a callstack contains recursive function, only add for each function once.
|
||||
if func_id in hit_func_ids:
|
||||
continue
|
||||
hit_func_ids.add(func_id)
|
||||
|
||||
lib = self.libs.get(lib_id)
|
||||
if not lib:
|
||||
lib = self.libs[lib_id] = LibScope(lib_id)
|
||||
function = lib.get_function(func_id)
|
||||
if i == 0:
|
||||
lib.event_count += event_count
|
||||
function.sample_count += 1
|
||||
function.add_reverse_callchain(callstack, i + 1, len(callstack), event_count)
|
||||
|
||||
if build_addr_hit_map:
|
||||
function.build_addr_hit_map(addr, event_count if i == 0 else 0, event_count)
|
||||
|
||||
hit_func_ids.clear()
|
||||
for i in range(len(callstack) - 1, -1, -1):
|
||||
lib_id, func_id, _ = callstack[i]
|
||||
# When a callstack contains recursive function, only add for each function once.
|
||||
if func_id in hit_func_ids:
|
||||
continue
|
||||
hit_func_ids.add(func_id)
|
||||
lib = self.libs.get(lib_id)
|
||||
lib.get_function(func_id).add_callchain(callstack, i - 1, -1, event_count)
|
||||
|
||||
def get_sample_info(self, gen_addr_hit_map):
|
||||
result = {}
|
||||
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()]
|
||||
return result
|
||||
|
||||
|
||||
class LibScope(object):
|
||||
|
||||
def __init__(self, lib_id):
|
||||
self.lib_id = lib_id
|
||||
self.event_count = 0
|
||||
self.functions = {} # map from func_id to FunctionScope.
|
||||
|
||||
def get_function(self, func_id):
|
||||
function = self.functions.get(func_id)
|
||||
if not function:
|
||||
function = self.functions[func_id] = FunctionScope(func_id)
|
||||
return function
|
||||
|
||||
def gen_sample_info(self, gen_addr_hit_map):
|
||||
result = {}
|
||||
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()]
|
||||
return result
|
||||
|
||||
|
||||
class FunctionScope(object):
|
||||
|
||||
def __init__(self, func_id):
|
||||
self.sample_count = 0
|
||||
self.call_graph = CallNode(func_id)
|
||||
self.reverse_call_graph = CallNode(func_id)
|
||||
self.addr_hit_map = None # map from addr to [event_count, subtree_event_count].
|
||||
# map from (source_file_id, line) to [event_count, subtree_event_count].
|
||||
self.line_hit_map = None
|
||||
|
||||
def add_callchain(self, callchain, start, end, event_count):
|
||||
node = self.call_graph
|
||||
for i in range(start, end, -1):
|
||||
node = node.get_child(callchain[i][1])
|
||||
node.event_count += event_count
|
||||
|
||||
def add_reverse_callchain(self, callchain, start, end, event_count):
|
||||
node = self.reverse_call_graph
|
||||
for i in range(start, end):
|
||||
node = node.get_child(callchain[i][1])
|
||||
node.event_count += event_count
|
||||
|
||||
def build_addr_hit_map(self, addr, event_count, subtree_event_count):
|
||||
if self.addr_hit_map is None:
|
||||
self.addr_hit_map = {}
|
||||
count_info = self.addr_hit_map.get(addr)
|
||||
if count_info is None:
|
||||
self.addr_hit_map[addr] = [event_count, subtree_event_count]
|
||||
else:
|
||||
count_info[0] += event_count
|
||||
count_info[1] += subtree_event_count
|
||||
|
||||
def build_line_hit_map(self, source_file_id, line, event_count, subtree_event_count):
|
||||
if self.line_hit_map is None:
|
||||
self.line_hit_map = {}
|
||||
key = (source_file_id, line)
|
||||
count_info = self.line_hit_map.get(key)
|
||||
if count_info is None:
|
||||
self.line_hit_map[key] = [event_count, subtree_event_count]
|
||||
else:
|
||||
count_info[0] += event_count
|
||||
count_info[1] += subtree_event_count
|
||||
|
||||
def update_subtree_event_count(self):
|
||||
a = self.call_graph.update_subtree_event_count()
|
||||
b = self.reverse_call_graph.update_subtree_event_count()
|
||||
return max(a, b)
|
||||
|
||||
def limit_callchain_percent(self, min_callchain_percent, hit_func_ids):
|
||||
min_limit = min_callchain_percent * 0.01 * self.call_graph.subtree_event_count
|
||||
self.call_graph.cut_edge(min_limit, hit_func_ids)
|
||||
self.reverse_call_graph.cut_edge(min_limit, hit_func_ids)
|
||||
|
||||
def gen_sample_info(self, gen_addr_hit_map):
|
||||
result = {}
|
||||
result['c'] = self.sample_count
|
||||
result['g'] = self.call_graph.gen_sample_info()
|
||||
result['rg'] = self.reverse_call_graph.gen_sample_info()
|
||||
if self.line_hit_map:
|
||||
items = []
|
||||
for key in self.line_hit_map:
|
||||
count_info = self.line_hit_map[key]
|
||||
item = {'f': key[0], 'l': key[1], 'e': count_info[0], 's': count_info[1]}
|
||||
items.append(item)
|
||||
result['s'] = items
|
||||
if gen_addr_hit_map and self.addr_hit_map:
|
||||
items = []
|
||||
for addr in sorted(self.addr_hit_map):
|
||||
count_info = self.addr_hit_map[addr]
|
||||
items.append({'a': addr, 'e': count_info[0], 's': count_info[1]})
|
||||
result['a'] = items
|
||||
return result
|
||||
|
||||
|
||||
class CallNode(object):
|
||||
|
||||
def __init__(self, func_id):
|
||||
self.event_count = 0
|
||||
self.subtree_event_count = 0
|
||||
self.func_id = func_id
|
||||
self.children = {} # map from func_id to CallNode
|
||||
|
||||
def get_child(self, func_id):
|
||||
child = self.children.get(func_id)
|
||||
if not child:
|
||||
child = self.children[func_id] = CallNode(func_id)
|
||||
return child
|
||||
|
||||
def update_subtree_event_count(self):
|
||||
self.subtree_event_count = self.event_count
|
||||
for child in self.children.values():
|
||||
self.subtree_event_count += child.update_subtree_event_count()
|
||||
return self.subtree_event_count
|
||||
|
||||
def cut_edge(self, min_limit, hit_func_ids):
|
||||
hit_func_ids.add(self.func_id)
|
||||
to_del_children = []
|
||||
for key in self.children:
|
||||
child = self.children[key]
|
||||
if child.subtree_event_count < min_limit:
|
||||
to_del_children.append(key)
|
||||
else:
|
||||
child.cut_edge(min_limit, hit_func_ids)
|
||||
for key in to_del_children:
|
||||
del self.children[key]
|
||||
|
||||
def gen_sample_info(self):
|
||||
result = {}
|
||||
result['e'] = self.event_count
|
||||
result['s'] = self.subtree_event_count
|
||||
result['f'] = self.func_id
|
||||
result['c'] = [child.gen_sample_info() for child in self.children.values()]
|
||||
return result
|
||||
|
||||
|
||||
class LibSet(object):
|
||||
""" Collection of shared libraries used in perf.data. """
|
||||
def __init__(self):
|
||||
self.lib_name_to_id = {}
|
||||
self.lib_id_to_name = []
|
||||
|
||||
def get_lib_id(self, lib_name):
|
||||
lib_id = self.lib_name_to_id.get(lib_name)
|
||||
if lib_id is None:
|
||||
lib_id = len(self.lib_id_to_name)
|
||||
self.lib_name_to_id[lib_name] = lib_id
|
||||
self.lib_id_to_name.append(lib_name)
|
||||
return lib_id
|
||||
|
||||
def get_lib_name(self, lib_id):
|
||||
return self.lib_id_to_name[lib_id]
|
||||
|
||||
|
||||
class Function(object):
|
||||
""" Represent a function in a shared library. """
|
||||
def __init__(self, lib_id, func_name, func_id, start_addr, addr_len):
|
||||
self.lib_id = lib_id
|
||||
self.func_name = func_name
|
||||
self.func_id = func_id
|
||||
self.start_addr = start_addr
|
||||
self.addr_len = addr_len
|
||||
self.source_info = None
|
||||
self.disassembly = None
|
||||
|
||||
|
||||
class FunctionSet(object):
|
||||
""" Collection of functions used in perf.data. """
|
||||
def __init__(self):
|
||||
self.name_to_func = {}
|
||||
self.id_to_func = {}
|
||||
|
||||
def get_func_id(self, lib_id, symbol):
|
||||
key = (lib_id, symbol.symbol_name)
|
||||
function = self.name_to_func.get(key)
|
||||
if function is None:
|
||||
func_id = len(self.id_to_func)
|
||||
function = Function(lib_id, symbol.symbol_name, func_id, symbol.symbol_addr,
|
||||
symbol.symbol_len)
|
||||
self.name_to_func[key] = function
|
||||
self.id_to_func[func_id] = function
|
||||
return function.func_id
|
||||
|
||||
def trim_functions(self, left_func_ids):
|
||||
""" Remove functions excepts those in left_func_ids. """
|
||||
for function in self.name_to_func.values():
|
||||
if function.func_id not in left_func_ids:
|
||||
del self.id_to_func[function.func_id]
|
||||
# name_to_func will not be used.
|
||||
self.name_to_func = None
|
||||
|
||||
|
||||
class SourceFile(object):
|
||||
""" A source file containing source code hit by samples. """
|
||||
def __init__(self, file_id, abstract_path):
|
||||
self.file_id = file_id
|
||||
self.abstract_path = abstract_path # path reported by addr2line
|
||||
self.real_path = None # file path in the file system
|
||||
self.requested_lines = set()
|
||||
self.line_to_code = {} # map from line to code in that line.
|
||||
|
||||
def request_lines(self, start_line, end_line):
|
||||
self.requested_lines |= set(range(start_line, end_line + 1))
|
||||
|
||||
def add_source_code(self, real_path):
|
||||
self.real_path = real_path
|
||||
with open(real_path, 'r') as f:
|
||||
source_code = f.readlines()
|
||||
max_line = len(source_code)
|
||||
for line in self.requested_lines:
|
||||
if line > 0 and line <= max_line:
|
||||
self.line_to_code[line] = source_code[line - 1]
|
||||
# requested_lines is no longer used.
|
||||
self.requested_lines = None
|
||||
|
||||
|
||||
class SourceFileSet(object):
|
||||
""" Collection of source files. """
|
||||
def __init__(self):
|
||||
self.path_to_source_files = {} # map from file path to SourceFile.
|
||||
|
||||
def get_source_file(self, file_path):
|
||||
source_file = self.path_to_source_files.get(file_path)
|
||||
if source_file is None:
|
||||
source_file = SourceFile(len(self.path_to_source_files), file_path)
|
||||
self.path_to_source_files[file_path] = source_file
|
||||
return source_file
|
||||
|
||||
def load_source_code(self, source_dirs):
|
||||
file_searcher = SourceFileSearcher(source_dirs)
|
||||
for source_file in self.path_to_source_files.values():
|
||||
real_path = file_searcher.get_real_path(source_file.abstract_path)
|
||||
if real_path:
|
||||
source_file.add_source_code(real_path)
|
||||
|
||||
|
||||
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.
|
||||
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:
|
||||
1. Collect all file paths under the provided source_dirs. The suffix of a
|
||||
source file should contain one of below:
|
||||
h: for C/C++ header files.
|
||||
c: for C/C++ source files.
|
||||
java: for Java source files.
|
||||
kt: for Kotlin source files.
|
||||
2. Given an abstract_path reported by addr2line, select the best real path
|
||||
as below:
|
||||
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.
|
||||
"""
|
||||
def __init__(self, source_dirs):
|
||||
# Map from filename to a list of reversed directory path containing filename.
|
||||
self.filename_to_rparents = {}
|
||||
self._collect_paths(source_dirs)
|
||||
|
||||
def _collect_paths(self, source_dirs):
|
||||
for source_dir in source_dirs:
|
||||
for parent, _, file_names in os.walk(source_dir):
|
||||
rparent = None
|
||||
for file_name in file_names:
|
||||
if self.is_source_filename(file_name):
|
||||
rparents = self.filename_to_rparents.get(file_name)
|
||||
if rparents is None:
|
||||
rparents = self.filename_to_rparents[file_name] = []
|
||||
if rparent is None:
|
||||
rparent = parent[::-1]
|
||||
rparents.append(rparent)
|
||||
|
||||
def get_real_path(self, abstract_path):
|
||||
abstract_path = abstract_path.replace('/', os.sep)
|
||||
abstract_parent, file_name = os.path.split(abstract_path)
|
||||
abstract_rparent = abstract_parent[::-1]
|
||||
real_rparents = self.filename_to_rparents.get(file_name)
|
||||
if real_rparents is None:
|
||||
return None
|
||||
best_matched_rparent = None
|
||||
best_common_length = -1
|
||||
for real_rparent in real_rparents:
|
||||
length = len(os.path.commonprefix((real_rparent, abstract_rparent)))
|
||||
if length > best_common_length:
|
||||
best_common_length = length
|
||||
best_matched_rparent = real_rparent
|
||||
if best_matched_rparent is None:
|
||||
return None
|
||||
return os.path.join(best_matched_rparent[::-1], file_name)
|
||||
|
||||
|
||||
class RecordData(object):
|
||||
|
||||
"""RecordData reads perf.data, and generates data used by report.js in json format.
|
||||
All generated items are listed as below:
|
||||
1. recordTime: string
|
||||
2. machineType: string
|
||||
3. androidVersion: string
|
||||
4. recordCmdline: string
|
||||
5. totalSamples: int
|
||||
6. processNames: map from pid to processName.
|
||||
7. threadNames: map from tid to threadName.
|
||||
8. libList: an array of libNames, indexed by libId.
|
||||
9. functionMap: map from functionId to funcData.
|
||||
funcData = {
|
||||
l: libId
|
||||
f: functionName
|
||||
s: [sourceFileId, startLine, endLine] [optional]
|
||||
d: [(disassembly, addr)] [optional]
|
||||
}
|
||||
|
||||
10. sampleInfo = [eventInfo]
|
||||
eventInfo = {
|
||||
eventName
|
||||
eventCount
|
||||
processes: [processInfo]
|
||||
}
|
||||
processInfo = {
|
||||
pid
|
||||
eventCount
|
||||
threads: [threadInfo]
|
||||
}
|
||||
threadInfo = {
|
||||
tid
|
||||
eventCount
|
||||
libs: [libInfo],
|
||||
}
|
||||
libInfo = {
|
||||
libId,
|
||||
eventCount,
|
||||
functions: [funcInfo]
|
||||
}
|
||||
funcInfo = {
|
||||
c: sampleCount
|
||||
g: callGraph
|
||||
rg: reverseCallgraph
|
||||
s: [sourceCodeInfo] [optional]
|
||||
a: [addrInfo] (sorted by addrInfo.addr) [optional]
|
||||
}
|
||||
callGraph and reverseCallGraph are both of type CallNode.
|
||||
callGraph shows how a function calls other functions.
|
||||
reverseCallGraph shows how a function is called by other functions.
|
||||
CallNode {
|
||||
e: selfEventCount
|
||||
s: subTreeEventCount
|
||||
f: functionId
|
||||
c: [CallNode] # children
|
||||
}
|
||||
|
||||
sourceCodeInfo {
|
||||
f: sourceFileId
|
||||
l: line
|
||||
e: eventCount
|
||||
s: subtreeEventCount
|
||||
}
|
||||
|
||||
addrInfo {
|
||||
a: addr
|
||||
e: eventCount
|
||||
s: subtreeEventCount
|
||||
}
|
||||
|
||||
11. sourceFiles: an array of sourceFile, indexed by sourceFileId.
|
||||
sourceFile {
|
||||
path
|
||||
code: # a map from line to code for that line.
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, binary_cache_path, ndk_path, build_addr_hit_map):
|
||||
self.binary_cache_path = binary_cache_path
|
||||
self.ndk_path = ndk_path
|
||||
self.build_addr_hit_map = build_addr_hit_map
|
||||
self.meta_info = None
|
||||
self.cmdline = None
|
||||
self.arch = None
|
||||
self.events = {}
|
||||
self.libs = LibSet()
|
||||
self.functions = FunctionSet()
|
||||
self.total_samples = 0
|
||||
self.source_files = SourceFileSet()
|
||||
self.gen_addr_hit_map_in_record_info = False
|
||||
|
||||
def load_record_file(self, record_file, show_art_frames):
|
||||
lib = ReportLib()
|
||||
lib.SetRecordFile(record_file)
|
||||
# If not showing ip for unknown symbols, the percent of the unknown symbol may be
|
||||
# accumulated to very big, and ranks first in the sample table.
|
||||
lib.ShowIpForUnknownSymbol()
|
||||
if show_art_frames:
|
||||
lib.ShowArtFrames()
|
||||
if self.binary_cache_path:
|
||||
lib.SetSymfs(self.binary_cache_path)
|
||||
self.meta_info = lib.MetaInfo()
|
||||
self.cmdline = lib.GetRecordCmd()
|
||||
self.arch = lib.GetArch()
|
||||
while True:
|
||||
raw_sample = lib.GetNextSample()
|
||||
if not raw_sample:
|
||||
lib.Close()
|
||||
break
|
||||
raw_event = lib.GetEventOfCurrentSample()
|
||||
symbol = lib.GetSymbolOfCurrentSample()
|
||||
callchain = lib.GetCallChainOfCurrentSample()
|
||||
event = self._get_event(raw_event.name)
|
||||
self.total_samples += 1
|
||||
event.sample_count += 1
|
||||
event.event_count += raw_sample.period
|
||||
process = event.get_process(raw_sample.pid)
|
||||
process.event_count += raw_sample.period
|
||||
thread = process.get_thread(raw_sample.tid, raw_sample.thread_comm)
|
||||
thread.event_count += raw_sample.period
|
||||
|
||||
lib_id = self.libs.get_lib_id(symbol.dso_name)
|
||||
func_id = self.functions.get_func_id(lib_id, symbol)
|
||||
callstack = [(lib_id, func_id, symbol.vaddr_in_file)]
|
||||
for i in range(callchain.nr):
|
||||
symbol = callchain.entries[i].symbol
|
||||
lib_id = self.libs.get_lib_id(symbol.dso_name)
|
||||
func_id = self.functions.get_func_id(lib_id, symbol)
|
||||
callstack.append((lib_id, func_id, symbol.vaddr_in_file))
|
||||
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()
|
||||
|
||||
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]
|
||||
self.functions.trim_functions(hit_func_ids)
|
||||
|
||||
def _get_event(self, event_name):
|
||||
if event_name not in self.events:
|
||||
self.events[event_name] = EventScope(event_name)
|
||||
return self.events[event_name]
|
||||
|
||||
def add_source_code(self, source_dirs):
|
||||
""" Collect source code information:
|
||||
1. Find line ranges for each function in FunctionSet.
|
||||
2. Find line for each addr in FunctionScope.addr_hit_map.
|
||||
3. Collect needed source code in SourceFileSet.
|
||||
"""
|
||||
addr2line = Addr2Nearestline(self.ndk_path, self.binary_cache_path)
|
||||
# Request line range for each function.
|
||||
for function in self.functions.id_to_func.values():
|
||||
if function.func_name == 'unknown':
|
||||
continue
|
||||
lib_name = self.libs.get_lib_name(function.lib_id)
|
||||
addr2line.add_addr(lib_name, function.start_addr, function.start_addr)
|
||||
addr2line.add_addr(lib_name, function.start_addr,
|
||||
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)
|
||||
addr2line.convert_addrs_to_lines()
|
||||
|
||||
# Set line range for each function.
|
||||
for function in self.functions.id_to_func.values():
|
||||
if function.func_name == 'unknown':
|
||||
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)
|
||||
if not start_source or not end_source:
|
||||
continue
|
||||
start_file_path, start_line = start_source[-1]
|
||||
end_file_path, end_line = end_source[-1]
|
||||
if start_file_path != end_file_path or start_line > end_line:
|
||||
continue
|
||||
source_file = self.source_files.get_source_file(start_file_path)
|
||||
source_file.request_lines(start_line, end_line)
|
||||
function.source_info = (source_file.file_id, start_line, end_line)
|
||||
|
||||
# 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])
|
||||
|
||||
# Collect needed source code in SourceFileSet.
|
||||
self.source_files.load_source_code(source_dirs)
|
||||
|
||||
def add_disassembly(self):
|
||||
""" Collect disassembly information:
|
||||
1. Use objdump to collect disassembly for each function in FunctionSet.
|
||||
2. Set flag to dump addr_hit_map when generating record info.
|
||||
"""
|
||||
objdump = Objdump(self.ndk_path, self.binary_cache_path)
|
||||
for function in self.functions.id_to_func.values():
|
||||
if function.func_name == 'unknown':
|
||||
continue
|
||||
lib_name = self.libs.get_lib_name(function.lib_id)
|
||||
code = objdump.disassemble_code(lib_name, function.start_addr, function.addr_len)
|
||||
function.disassembly = code
|
||||
|
||||
self.gen_addr_hit_map_in_record_info = True
|
||||
|
||||
def gen_record_info(self):
|
||||
record_info = {}
|
||||
timestamp = self.meta_info.get('timestamp')
|
||||
if timestamp:
|
||||
t = datetime.datetime.fromtimestamp(int(timestamp))
|
||||
else:
|
||||
t = datetime.datetime.now()
|
||||
record_info['recordTime'] = t.strftime('%Y-%m-%d (%A) %H:%M:%S')
|
||||
|
||||
product_props = self.meta_info.get('product_props')
|
||||
machine_type = self.arch
|
||||
if product_props:
|
||||
manufacturer, model, name = product_props.split(':')
|
||||
machine_type = '%s (%s) by %s, arch %s' % (model, name, manufacturer, self.arch)
|
||||
record_info['machineType'] = machine_type
|
||||
record_info['androidVersion'] = self.meta_info.get('android_version', '')
|
||||
record_info['recordCmdline'] = self.cmdline
|
||||
record_info['totalSamples'] = self.total_samples
|
||||
record_info['processNames'] = self._gen_process_names()
|
||||
record_info['threadNames'] = self._gen_thread_names()
|
||||
record_info['libList'] = self._gen_lib_list()
|
||||
record_info['functionMap'] = self._gen_function_map()
|
||||
record_info['sampleInfo'] = self._gen_sample_info()
|
||||
record_info['sourceFiles'] = self._gen_source_files()
|
||||
return record_info
|
||||
|
||||
def _gen_process_names(self):
|
||||
process_names = {}
|
||||
for event in self.events.values():
|
||||
for process in event.processes.values():
|
||||
process_names[process.pid] = process.name
|
||||
return process_names
|
||||
|
||||
def _gen_thread_names(self):
|
||||
thread_names = {}
|
||||
for event in self.events.values():
|
||||
for process in event.processes.values():
|
||||
for thread in process.threads.values():
|
||||
thread_names[thread.tid] = thread.name
|
||||
return thread_names
|
||||
|
||||
def _gen_lib_list(self):
|
||||
return [modify_text_for_html(x) for x in self.libs.lib_id_to_name]
|
||||
|
||||
def _gen_function_map(self):
|
||||
func_map = {}
|
||||
for func_id in sorted(self.functions.id_to_func):
|
||||
function = self.functions.id_to_func[func_id]
|
||||
func_data = {}
|
||||
func_data['l'] = function.lib_id
|
||||
func_data['f'] = modify_text_for_html(function.func_name)
|
||||
if function.source_info:
|
||||
func_data['s'] = function.source_info
|
||||
if function.disassembly:
|
||||
disassembly_list = []
|
||||
for code, addr in function.disassembly:
|
||||
disassembly_list.append([modify_text_for_html(code), addr])
|
||||
func_data['d'] = disassembly_list
|
||||
func_map[func_id] = func_data
|
||||
return func_map
|
||||
|
||||
def _gen_sample_info(self):
|
||||
return [event.get_sample_info(self.gen_addr_hit_map_in_record_info)
|
||||
for event in self.events.values()]
|
||||
|
||||
def _gen_source_files(self):
|
||||
source_files = sorted(self.source_files.path_to_source_files.values(),
|
||||
key=lambda x: x.file_id)
|
||||
file_list = []
|
||||
for source_file in source_files:
|
||||
file_data = {}
|
||||
if not source_file.real_path:
|
||||
file_data['path'] = ''
|
||||
file_data['code'] = {}
|
||||
else:
|
||||
file_data['path'] = source_file.real_path
|
||||
code_map = {}
|
||||
for line in source_file.line_to_code:
|
||||
code_map[line] = modify_text_for_html(source_file.line_to_code[line])
|
||||
file_data['code'] = code_map
|
||||
file_list.append(file_data)
|
||||
return file_list
|
||||
|
||||
|
||||
class ReportGenerator(object):
|
||||
|
||||
def __init__(self, html_path):
|
||||
self.hw = HtmlWriter(html_path)
|
||||
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()
|
||||
|
||||
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()
|
||||
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://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()
|
||||
self.hw.open_tag('style', type='text/css').add("""
|
||||
.colForLine { width: 50px; }
|
||||
.colForCount { width: 100px; }
|
||||
.tableCell { font-size: 17px; }
|
||||
.boldTableCell { font-weight: bold; font-size: 17px; }
|
||||
""").close_tag()
|
||||
self.hw.close_tag('head')
|
||||
self.hw.open_tag('body')
|
||||
self.record_info = {}
|
||||
|
||||
def write_content_div(self):
|
||||
self.hw.open_tag('div', id='report_content').close_tag()
|
||||
|
||||
def write_record_data(self, record_data):
|
||||
self.hw.open_tag('script', id='record_data', type='application/json')
|
||||
self.hw.add(json.dumps(record_data))
|
||||
self.hw.close_tag()
|
||||
|
||||
def write_flamegraph(self, flamegraph):
|
||||
self.hw.add(flamegraph)
|
||||
|
||||
def write_script(self):
|
||||
self.hw.open_tag('script').add_file('report_html.js').close_tag()
|
||||
|
||||
def finish(self):
|
||||
self.hw.close_tag('body')
|
||||
self.hw.close_tag('html')
|
||||
self.hw.close()
|
||||
|
||||
|
||||
def gen_flamegraph(record_file, show_art_frames):
|
||||
fd, flamegraph_path = tempfile.mkstemp()
|
||||
os.close(fd)
|
||||
inferno_script_path = os.path.join(get_script_dir(), 'inferno', 'inferno.py')
|
||||
args = [sys.executable, inferno_script_path, '-sc', '-o', flamegraph_path,
|
||||
'--record_file', record_file, '--embedded_flamegraph', '--no_browser']
|
||||
if show_art_frames:
|
||||
args.append('--show_art_frames')
|
||||
subprocess.check_call(args)
|
||||
with open(flamegraph_path, 'r') as fh:
|
||||
data = fh.read()
|
||||
remove(flamegraph_path)
|
||||
return data
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='report profiling data')
|
||||
parser.add_argument('-i', '--record_file', nargs='+', default=['perf.data'], help="""
|
||||
Set profiling data file to report. Default is perf.data.""")
|
||||
parser.add_argument('-o', '--report_path', default='report.html', help="""
|
||||
Set output html file. Default is report.html.""")
|
||||
parser.add_argument('--min_func_percent', default=0.01, type=float, help="""
|
||||
Set min percentage of functions shown in the report.
|
||||
For example, when set to 0.01, only functions taking >= 0.01%% of total
|
||||
event count are collected in the report. Default is 0.01.""")
|
||||
parser.add_argument('--min_callchain_percent', default=0.01, type=float, help="""
|
||||
Set min percentage of callchains shown in the report.
|
||||
It is used to limit nodes shown in the function flamegraph. For example,
|
||||
when set to 0.01, only callchains taking >= 0.01%% of the event count of
|
||||
the starting function are collected in the report. Default is 0.01.""")
|
||||
parser.add_argument('--add_source_code', action='store_true', help='Add source code.')
|
||||
parser.add_argument('--source_dirs', nargs='+', help='Source code directories.')
|
||||
parser.add_argument('--add_disassembly', action='store_true', help='Add disassembled code.')
|
||||
parser.add_argument('--ndk_path', nargs=1, help='Find tools in the ndk path.')
|
||||
parser.add_argument('--no_browser', action='store_true', help="Don't open report in browser.")
|
||||
parser.add_argument('--show_art_frames', action='store_true',
|
||||
help='Show frames of internal methods in the ART Java interpreter.')
|
||||
args = parser.parse_args()
|
||||
|
||||
# 1. Process args.
|
||||
binary_cache_path = 'binary_cache'
|
||||
if not os.path.isdir(binary_cache_path):
|
||||
if args.add_source_code or args.add_disassembly:
|
||||
log_exit("""binary_cache/ doesn't exist. Can't add source code or disassembled code
|
||||
without collected binaries. Please run binary_cache_builder.py to
|
||||
collect binaries for current profiling data, or run app_profiler.py
|
||||
without -nb option.""")
|
||||
binary_cache_path = None
|
||||
|
||||
if args.add_source_code and not args.source_dirs:
|
||||
log_exit('--source_dirs is needed to add source code.')
|
||||
build_addr_hit_map = args.add_source_code or args.add_disassembly
|
||||
ndk_path = None if not args.ndk_path else args.ndk_path[0]
|
||||
|
||||
# 2. Produce record data.
|
||||
record_data = RecordData(binary_cache_path, ndk_path, build_addr_hit_map)
|
||||
for record_file in args.record_file:
|
||||
record_data.load_record_file(record_file, args.show_art_frames)
|
||||
record_data.limit_percents(args.min_func_percent, args.min_callchain_percent)
|
||||
if args.add_source_code:
|
||||
record_data.add_source_code(args.source_dirs)
|
||||
if args.add_disassembly:
|
||||
record_data.add_disassembly()
|
||||
|
||||
# 3. Generate report html.
|
||||
report_generator = ReportGenerator(args.report_path)
|
||||
report_generator.write_content_div()
|
||||
report_generator.write_record_data(record_data.gen_record_info())
|
||||
report_generator.write_script()
|
||||
# TODO: support multiple perf.data in flamegraph.
|
||||
if len(args.record_file) > 1:
|
||||
log_warning('flamegraph will only be shown for %s' % args.record_file[0])
|
||||
flamegraph = gen_flamegraph(args.record_file[0], args.show_art_frames)
|
||||
report_generator.write_flamegraph(flamegraph)
|
||||
report_generator.finish()
|
||||
|
||||
if not args.no_browser:
|
||||
open_report_in_browser(args.report_path)
|
||||
log_info("Report generated at '%s'." % args.report_path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
-370
@@ -1,370 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (C) 2016 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""simpleperf_report_lib.py: a python wrapper of libsimpleperf_report.so.
|
||||
Used to access samples in perf.data.
|
||||
|
||||
"""
|
||||
|
||||
import ctypes as ct
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from utils import *
|
||||
|
||||
|
||||
def _get_native_lib():
|
||||
return get_host_binary_path('libsimpleperf_report.so')
|
||||
|
||||
|
||||
def _is_null(p):
|
||||
if p:
|
||||
return False
|
||||
return ct.cast(p, ct.c_void_p).value is None
|
||||
|
||||
|
||||
def _char_pt(s):
|
||||
return str_to_bytes(s)
|
||||
|
||||
|
||||
def _char_pt_to_str(char_pt):
|
||||
return bytes_to_str(char_pt)
|
||||
|
||||
|
||||
class SampleStruct(ct.Structure):
|
||||
""" Instance of a sample in perf.data.
|
||||
ip: the program counter of the thread generating the sample.
|
||||
pid: process id (or thread group id) of the thread generating the sample.
|
||||
tid: thread id.
|
||||
thread_comm: thread name.
|
||||
time: time at which the sample was generated. The value is in nanoseconds.
|
||||
The clock is decided by the --clockid option in `simpleperf record`.
|
||||
in_kernel: whether the instruction is in kernel space or user space.
|
||||
cpu: the cpu generating the sample.
|
||||
period: count of events have happened since last sample. For example, if we use
|
||||
-e cpu-cycles, it means how many cpu-cycles have happened.
|
||||
If we use -e cpu-clock, it means how many nanoseconds have passed.
|
||||
"""
|
||||
_fields_ = [('ip', ct.c_uint64),
|
||||
('pid', ct.c_uint32),
|
||||
('tid', ct.c_uint32),
|
||||
('thread_comm', ct.c_char_p),
|
||||
('time', ct.c_uint64),
|
||||
('in_kernel', ct.c_uint32),
|
||||
('cpu', ct.c_uint32),
|
||||
('period', ct.c_uint64)]
|
||||
|
||||
|
||||
class EventStruct(ct.Structure):
|
||||
""" Name of the event. """
|
||||
_fields_ = [('name', ct.c_char_p)]
|
||||
|
||||
|
||||
class MappingStruct(ct.Structure):
|
||||
""" A mapping area in the monitored threads, like the content in /proc/<pid>/maps.
|
||||
start: start addr in memory.
|
||||
end: end addr in memory.
|
||||
pgoff: offset in the mapped shared library.
|
||||
"""
|
||||
_fields_ = [('start', ct.c_uint64),
|
||||
('end', ct.c_uint64),
|
||||
('pgoff', ct.c_uint64)]
|
||||
|
||||
|
||||
class SymbolStruct(ct.Structure):
|
||||
""" Symbol info of the instruction hit by a sample or a callchain entry of a sample.
|
||||
dso_name: path of the shared library containing the instruction.
|
||||
vaddr_in_file: virtual address of the instruction in the shared library.
|
||||
symbol_name: name of the function containing the instruction.
|
||||
symbol_addr: start addr of the function containing the instruction.
|
||||
symbol_len: length of the function in the shared library.
|
||||
mapping: the mapping area hit by the instruction.
|
||||
"""
|
||||
_fields_ = [('dso_name', ct.c_char_p),
|
||||
('vaddr_in_file', ct.c_uint64),
|
||||
('symbol_name', ct.c_char_p),
|
||||
('symbol_addr', ct.c_uint64),
|
||||
('symbol_len', ct.c_uint64),
|
||||
('mapping', ct.POINTER(MappingStruct))]
|
||||
|
||||
|
||||
class CallChainEntryStructure(ct.Structure):
|
||||
""" A callchain entry of a sample.
|
||||
ip: the address of the instruction of the callchain entry.
|
||||
symbol: symbol info of the callchain entry.
|
||||
"""
|
||||
_fields_ = [('ip', ct.c_uint64),
|
||||
('symbol', SymbolStruct)]
|
||||
|
||||
|
||||
class CallChainStructure(ct.Structure):
|
||||
""" Callchain info of a sample.
|
||||
nr: number of entries in the callchain.
|
||||
entries: a pointer to an array of CallChainEntryStructure.
|
||||
|
||||
For example, if a sample is generated when a thread is running function C
|
||||
with callchain function A -> function B -> function C.
|
||||
Then nr = 2, and entries = [function B, function A].
|
||||
"""
|
||||
_fields_ = [('nr', ct.c_uint32),
|
||||
('entries', ct.POINTER(CallChainEntryStructure))]
|
||||
|
||||
|
||||
class FeatureSectionStructure(ct.Structure):
|
||||
""" A feature section in perf.data to store information like record cmd, device arch, etc.
|
||||
data: a pointer to a buffer storing the section data.
|
||||
data_size: data size in bytes.
|
||||
"""
|
||||
_fields_ = [('data', ct.POINTER(ct.c_char)),
|
||||
('data_size', ct.c_uint32)]
|
||||
|
||||
|
||||
# convert char_p to str for python3.
|
||||
class SampleStructUsingStr(object):
|
||||
def __init__(self, sample):
|
||||
self.ip = sample.ip
|
||||
self.pid = sample.pid
|
||||
self.tid = sample.tid
|
||||
self.thread_comm = _char_pt_to_str(sample.thread_comm)
|
||||
self.time = sample.time
|
||||
self.in_kernel = sample.in_kernel
|
||||
self.cpu = sample.cpu
|
||||
self.period = sample.period
|
||||
|
||||
|
||||
class EventStructUsingStr(object):
|
||||
def __init__(self, event):
|
||||
self.name = _char_pt_to_str(event.name)
|
||||
|
||||
|
||||
class SymbolStructUsingStr(object):
|
||||
def __init__(self, symbol):
|
||||
self.dso_name = _char_pt_to_str(symbol.dso_name)
|
||||
self.vaddr_in_file = symbol.vaddr_in_file
|
||||
self.symbol_name = _char_pt_to_str(symbol.symbol_name)
|
||||
self.symbol_addr = symbol.symbol_addr
|
||||
self.mapping = symbol.mapping
|
||||
|
||||
|
||||
class CallChainEntryStructureUsingStr(object):
|
||||
def __init__(self, entry):
|
||||
self.ip = entry.ip
|
||||
self.symbol = SymbolStructUsingStr(entry.symbol)
|
||||
|
||||
|
||||
class CallChainStructureUsingStr(object):
|
||||
def __init__(self, callchain):
|
||||
self.nr = callchain.nr
|
||||
self.entries = []
|
||||
for i in range(self.nr):
|
||||
self.entries.append(CallChainEntryStructureUsingStr(callchain.entries[i]))
|
||||
|
||||
|
||||
class ReportLibStructure(ct.Structure):
|
||||
_fields_ = []
|
||||
|
||||
|
||||
class ReportLib(object):
|
||||
|
||||
def __init__(self, native_lib_path=None):
|
||||
if native_lib_path is None:
|
||||
native_lib_path = _get_native_lib()
|
||||
|
||||
self._load_dependent_lib()
|
||||
self._lib = ct.CDLL(native_lib_path)
|
||||
self._CreateReportLibFunc = self._lib.CreateReportLib
|
||||
self._CreateReportLibFunc.restype = ct.POINTER(ReportLibStructure)
|
||||
self._DestroyReportLibFunc = self._lib.DestroyReportLib
|
||||
self._SetLogSeverityFunc = self._lib.SetLogSeverity
|
||||
self._SetSymfsFunc = self._lib.SetSymfs
|
||||
self._SetRecordFileFunc = self._lib.SetRecordFile
|
||||
self._SetKallsymsFileFunc = self._lib.SetKallsymsFile
|
||||
self._ShowIpForUnknownSymbolFunc = self._lib.ShowIpForUnknownSymbol
|
||||
self._ShowArtFramesFunc = self._lib.ShowArtFrames
|
||||
self._GetNextSampleFunc = self._lib.GetNextSample
|
||||
self._GetNextSampleFunc.restype = ct.POINTER(SampleStruct)
|
||||
self._GetEventOfCurrentSampleFunc = self._lib.GetEventOfCurrentSample
|
||||
self._GetEventOfCurrentSampleFunc.restype = ct.POINTER(EventStruct)
|
||||
self._GetSymbolOfCurrentSampleFunc = self._lib.GetSymbolOfCurrentSample
|
||||
self._GetSymbolOfCurrentSampleFunc.restype = ct.POINTER(SymbolStruct)
|
||||
self._GetCallChainOfCurrentSampleFunc = self._lib.GetCallChainOfCurrentSample
|
||||
self._GetCallChainOfCurrentSampleFunc.restype = ct.POINTER(
|
||||
CallChainStructure)
|
||||
self._GetBuildIdForPathFunc = self._lib.GetBuildIdForPath
|
||||
self._GetBuildIdForPathFunc.restype = ct.c_char_p
|
||||
self._GetFeatureSection = self._lib.GetFeatureSection
|
||||
self._GetFeatureSection.restype = ct.POINTER(FeatureSectionStructure)
|
||||
self._instance = self._CreateReportLibFunc()
|
||||
assert not _is_null(self._instance)
|
||||
|
||||
self.convert_to_str = (sys.version_info >= (3, 0))
|
||||
self.meta_info = None
|
||||
self.current_sample = None
|
||||
self.record_cmd = None
|
||||
|
||||
def _load_dependent_lib(self):
|
||||
# As the windows dll is built with mingw we need to load 'libwinpthread-1.dll'.
|
||||
if is_windows():
|
||||
self._libwinpthread = ct.CDLL(get_host_binary_path('libwinpthread-1.dll'))
|
||||
|
||||
def Close(self):
|
||||
if self._instance is None:
|
||||
return
|
||||
self._DestroyReportLibFunc(self._instance)
|
||||
self._instance = None
|
||||
|
||||
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')
|
||||
|
||||
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')
|
||||
|
||||
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')
|
||||
|
||||
def ShowIpForUnknownSymbol(self):
|
||||
self._ShowIpForUnknownSymbolFunc(self.getInstance())
|
||||
|
||||
def ShowArtFrames(self, show=True):
|
||||
""" Show frames of internal methods of the Java interpreter. """
|
||||
self._ShowArtFramesFunc(self.getInstance(), show)
|
||||
|
||||
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')
|
||||
|
||||
def GetNextSample(self):
|
||||
psample = self._GetNextSampleFunc(self.getInstance())
|
||||
if _is_null(psample):
|
||||
self.current_sample = None
|
||||
else:
|
||||
sample = psample[0]
|
||||
self.current_sample = SampleStructUsingStr(sample) if self.convert_to_str else sample
|
||||
return self.current_sample
|
||||
|
||||
def GetCurrentSample(self):
|
||||
return self.current_sample
|
||||
|
||||
def GetEventOfCurrentSample(self):
|
||||
event = self._GetEventOfCurrentSampleFunc(self.getInstance())
|
||||
assert not _is_null(event)
|
||||
if self.convert_to_str:
|
||||
return EventStructUsingStr(event[0])
|
||||
return event[0]
|
||||
|
||||
def GetSymbolOfCurrentSample(self):
|
||||
symbol = self._GetSymbolOfCurrentSampleFunc(self.getInstance())
|
||||
assert not _is_null(symbol)
|
||||
if self.convert_to_str:
|
||||
return SymbolStructUsingStr(symbol[0])
|
||||
return symbol[0]
|
||||
|
||||
def GetCallChainOfCurrentSample(self):
|
||||
callchain = self._GetCallChainOfCurrentSampleFunc(self.getInstance())
|
||||
assert not _is_null(callchain)
|
||||
if self.convert_to_str:
|
||||
return CallChainStructureUsingStr(callchain[0])
|
||||
return callchain[0]
|
||||
|
||||
def GetBuildIdForPath(self, path):
|
||||
build_id = self._GetBuildIdForPathFunc(self.getInstance(), _char_pt(path))
|
||||
assert not _is_null(build_id)
|
||||
return _char_pt_to_str(build_id)
|
||||
|
||||
def GetRecordCmd(self):
|
||||
if self.record_cmd is not None:
|
||||
return self.record_cmd
|
||||
self.record_cmd = ''
|
||||
feature_data = self._GetFeatureSection(self.getInstance(), _char_pt('cmdline'))
|
||||
if not _is_null(feature_data):
|
||||
void_p = ct.cast(feature_data[0].data, ct.c_void_p)
|
||||
arg_count = ct.cast(void_p, ct.POINTER(ct.c_uint32)).contents.value
|
||||
void_p.value += 4
|
||||
args = []
|
||||
for _ in range(arg_count):
|
||||
str_len = ct.cast(void_p, ct.POINTER(ct.c_uint32)).contents.value
|
||||
void_p.value += 4
|
||||
char_p = ct.cast(void_p, ct.POINTER(ct.c_char))
|
||||
current_str = ''
|
||||
for j in range(str_len):
|
||||
c = bytes_to_str(char_p[j])
|
||||
if c != '\0':
|
||||
current_str += c
|
||||
if ' ' in current_str:
|
||||
current_str = '"' + current_str + '"'
|
||||
args.append(current_str)
|
||||
void_p.value += str_len
|
||||
self.record_cmd = ' '.join(args)
|
||||
return self.record_cmd
|
||||
|
||||
def _GetFeatureString(self, feature_name):
|
||||
feature_data = self._GetFeatureSection(self.getInstance(), _char_pt(feature_name))
|
||||
result = ''
|
||||
if not _is_null(feature_data):
|
||||
void_p = ct.cast(feature_data[0].data, ct.c_void_p)
|
||||
str_len = ct.cast(void_p, ct.POINTER(ct.c_uint32)).contents.value
|
||||
void_p.value += 4
|
||||
char_p = ct.cast(void_p, ct.POINTER(ct.c_char))
|
||||
for i in range(str_len):
|
||||
c = bytes_to_str(char_p[i])
|
||||
if c == '\0':
|
||||
break
|
||||
result += c
|
||||
return result
|
||||
|
||||
def GetArch(self):
|
||||
return self._GetFeatureString('arch')
|
||||
|
||||
def MetaInfo(self):
|
||||
""" Return a string to string map stored in meta_info section in perf.data.
|
||||
It is used to pass some short meta information.
|
||||
"""
|
||||
if self.meta_info is None:
|
||||
self.meta_info = {}
|
||||
feature_data = self._GetFeatureSection(self.getInstance(), _char_pt('meta_info'))
|
||||
if not _is_null(feature_data):
|
||||
str_list = []
|
||||
data = feature_data[0].data
|
||||
data_size = feature_data[0].data_size
|
||||
current_str = ''
|
||||
for i in range(data_size):
|
||||
c = bytes_to_str(data[i])
|
||||
if c != '\0':
|
||||
current_str += c
|
||||
else:
|
||||
str_list.append(current_str)
|
||||
current_str = ''
|
||||
for i in range(0, len(str_list), 2):
|
||||
self.meta_info[str_list[i]] = str_list[i + 1]
|
||||
return self.meta_info
|
||||
|
||||
def getInstance(self):
|
||||
if self._instance is None:
|
||||
raise Exception('Instance is Closed')
|
||||
return self._instance
|
||||
|
||||
def _check(self, cond, failmsg):
|
||||
if not cond:
|
||||
raise Exception(failmsg)
|
||||
Vendored
-677
@@ -1,677 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (C) 2016 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""utils.py: export utility functions.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
def get_script_dir():
|
||||
return os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
def is_windows():
|
||||
return sys.platform == 'win32' or sys.platform == 'cygwin'
|
||||
|
||||
def is_darwin():
|
||||
return sys.platform == 'darwin'
|
||||
|
||||
def get_platform():
|
||||
if is_windows():
|
||||
return 'windows'
|
||||
if is_darwin():
|
||||
return 'darwin'
|
||||
return 'linux'
|
||||
|
||||
def is_python3():
|
||||
return sys.version_info >= (3, 0)
|
||||
|
||||
|
||||
def log_debug(msg):
|
||||
logging.debug(msg)
|
||||
|
||||
|
||||
def log_info(msg):
|
||||
logging.info(msg)
|
||||
|
||||
|
||||
def log_warning(msg):
|
||||
logging.warning(msg)
|
||||
|
||||
|
||||
def log_fatal(msg):
|
||||
raise Exception(msg)
|
||||
|
||||
def log_exit(msg):
|
||||
sys.exit(msg)
|
||||
|
||||
def disable_debug_log():
|
||||
logging.getLogger().setLevel(logging.WARN)
|
||||
|
||||
def str_to_bytes(str):
|
||||
if not is_python3():
|
||||
return str
|
||||
# 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')
|
||||
|
||||
def bytes_to_str(bytes):
|
||||
if not is_python3():
|
||||
return bytes
|
||||
return bytes.decode('utf-8')
|
||||
|
||||
def get_target_binary_path(arch, binary_name):
|
||||
if arch == 'aarch64':
|
||||
arch = 'arm64'
|
||||
arch_dir = os.path.join(get_script_dir(), "bin", "android", arch)
|
||||
if not os.path.isdir(arch_dir):
|
||||
log_fatal("can't find arch directory: %s" % arch_dir)
|
||||
binary_path = os.path.join(arch_dir, binary_name)
|
||||
if not os.path.isfile(binary_path):
|
||||
log_fatal("can't find binary: %s" % binary_path)
|
||||
return binary_path
|
||||
|
||||
|
||||
def get_host_binary_path(binary_name):
|
||||
dir = 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')
|
||||
elif sys.platform == 'darwin': # OSX
|
||||
if binary_name.endswith('.so'):
|
||||
binary_name = binary_name[0:-3] + '.dylib'
|
||||
dir = os.path.join(dir, '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)
|
||||
if not os.path.isfile(binary_path):
|
||||
log_fatal("can't find binary: %s" % binary_path)
|
||||
return binary_path
|
||||
|
||||
|
||||
def is_executable_available(executable, option='--help'):
|
||||
""" Run an executable to see if it exists. """
|
||||
try:
|
||||
subproc = subprocess.Popen([executable, option], stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
subproc.communicate()
|
||||
return subproc.returncode == 0
|
||||
except:
|
||||
return False
|
||||
|
||||
DEFAULT_NDK_PATH = {
|
||||
'darwin': 'Library/Android/sdk/ndk-bundle',
|
||||
'linux': 'Android/Sdk/ndk-bundle',
|
||||
'windows': 'AppData/Local/Android/sdk/ndk-bundle',
|
||||
}
|
||||
|
||||
EXPECTED_TOOLS = {
|
||||
'adb': {
|
||||
'is_binutils': False,
|
||||
'test_option': 'version',
|
||||
'path_in_ndk': '../platform-tools/adb',
|
||||
},
|
||||
'readelf': {
|
||||
'is_binutils': True,
|
||||
'accept_tool_without_arch': True,
|
||||
},
|
||||
'addr2line': {
|
||||
'is_binutils': True,
|
||||
'accept_tool_without_arch': True
|
||||
},
|
||||
'objdump': {
|
||||
'is_binutils': True,
|
||||
},
|
||||
}
|
||||
|
||||
def _get_binutils_path_in_ndk(toolname, arch, platform):
|
||||
if not arch:
|
||||
arch = 'arm64'
|
||||
if arch == 'arm64':
|
||||
name = 'aarch64-linux-android-' + toolname
|
||||
path = 'toolchains/aarch64-linux-android-4.9/prebuilt/%s-x86_64/bin/%s' % (platform, name)
|
||||
elif arch == 'arm':
|
||||
name = 'arm-linux-androideabi-' + toolname
|
||||
path = 'toolchains/arm-linux-androideabi-4.9/prebuilt/%s-x86_64/bin/%s' % (platform, name)
|
||||
elif arch == 'x86_64':
|
||||
name = 'x86_64-linux-android-' + toolname
|
||||
path = 'toolchains/x86_64-4.9/prebuilt/%s-x86_64/bin/%s' % (platform, name)
|
||||
elif arch == 'x86':
|
||||
name = 'i686-linux-android-' + toolname
|
||||
path = 'toolchains/x86-4.9/prebuilt/%s-x86_64/bin/%s' % (platform, name)
|
||||
else:
|
||||
log_fatal('unexpected arch %s' % arch)
|
||||
return (name, path)
|
||||
|
||||
def find_tool_path(toolname, ndk_path=None, arch=None):
|
||||
if toolname not in EXPECTED_TOOLS:
|
||||
return None
|
||||
tool_info = EXPECTED_TOOLS[toolname]
|
||||
is_binutils = tool_info['is_binutils']
|
||||
test_option = tool_info.get('test_option', '--help')
|
||||
platform = get_platform()
|
||||
if is_binutils:
|
||||
toolname_with_arch, path_in_ndk = _get_binutils_path_in_ndk(toolname, arch, platform)
|
||||
else:
|
||||
toolname_with_arch = toolname
|
||||
path_in_ndk = tool_info['path_in_ndk']
|
||||
path_in_ndk = path_in_ndk.replace('/', os.sep)
|
||||
|
||||
# 1. Find tool in the given ndk path.
|
||||
if ndk_path:
|
||||
path = os.path.join(ndk_path, path_in_ndk)
|
||||
if is_executable_available(path, test_option):
|
||||
return path
|
||||
|
||||
# 2. Find tool in the ndk directory containing simpleperf scripts.
|
||||
path = os.path.join('..', path_in_ndk)
|
||||
if is_executable_available(path, test_option):
|
||||
return path
|
||||
|
||||
# 3. Find tool in the default ndk installation path.
|
||||
home = os.environ.get('HOMEPATH') if is_windows() else os.environ.get('HOME')
|
||||
if home:
|
||||
default_ndk_path = os.path.join(home, DEFAULT_NDK_PATH[platform].replace('/', os.sep))
|
||||
path = os.path.join(default_ndk_path, path_in_ndk)
|
||||
if is_executable_available(path, test_option):
|
||||
return path
|
||||
|
||||
# 4. Find tool in $PATH.
|
||||
if is_executable_available(toolname_with_arch, test_option):
|
||||
return toolname_with_arch
|
||||
|
||||
# 5. Find tool without arch in $PATH.
|
||||
if is_binutils and tool_info.get('accept_tool_without_arch'):
|
||||
if is_executable_available(toolname, test_option):
|
||||
return toolname
|
||||
return None
|
||||
|
||||
|
||||
class AdbHelper(object):
|
||||
def __init__(self, enable_switch_to_root=True):
|
||||
adb_path = find_tool_path('adb')
|
||||
if not adb_path:
|
||||
log_exit("Can't find adb in PATH environment.")
|
||||
self.adb_path = adb_path
|
||||
self.enable_switch_to_root = enable_switch_to_root
|
||||
|
||||
def run(self, adb_args):
|
||||
return self.run_and_return_output(adb_args)[0]
|
||||
|
||||
def run_and_return_output(self, adb_args, stdout_file=None, log_output=True):
|
||||
adb_args = [self.adb_path] + adb_args
|
||||
log_debug('run adb cmd: %s' % adb_args)
|
||||
if stdout_file:
|
||||
with open(stdout_file, 'wb') as stdout_fh:
|
||||
returncode = subprocess.call(adb_args, stdout=stdout_fh)
|
||||
stdoutdata = ''
|
||||
else:
|
||||
subproc = subprocess.Popen(adb_args, stdout=subprocess.PIPE)
|
||||
(stdoutdata, _) = subproc.communicate()
|
||||
returncode = subproc.returncode
|
||||
result = (returncode == 0)
|
||||
if stdoutdata and adb_args[1] != 'push' and adb_args[1] != 'pull':
|
||||
stdoutdata = bytes_to_str(stdoutdata)
|
||||
if log_output:
|
||||
log_debug(stdoutdata)
|
||||
log_debug('run adb cmd: %s [result %s]' % (adb_args, result))
|
||||
return (result, stdoutdata)
|
||||
|
||||
def check_run(self, adb_args):
|
||||
self.check_run_and_return_output(adb_args)
|
||||
|
||||
def check_run_and_return_output(self, adb_args, stdout_file=None, log_output=True):
|
||||
result, stdoutdata = self.run_and_return_output(adb_args, stdout_file, log_output)
|
||||
if not result:
|
||||
log_exit('run "adb %s" failed' % adb_args)
|
||||
return stdoutdata
|
||||
|
||||
def _unroot(self):
|
||||
result, stdoutdata = self.run_and_return_output(['shell', 'whoami'])
|
||||
if not result:
|
||||
return
|
||||
if 'root' not in stdoutdata:
|
||||
return
|
||||
log_info('unroot adb')
|
||||
self.run(['unroot'])
|
||||
self.run(['wait-for-device'])
|
||||
time.sleep(1)
|
||||
|
||||
def switch_to_root(self):
|
||||
if not self.enable_switch_to_root:
|
||||
self._unroot()
|
||||
return False
|
||||
result, stdoutdata = self.run_and_return_output(['shell', 'whoami'])
|
||||
if not result:
|
||||
return False
|
||||
if 'root' in stdoutdata:
|
||||
return True
|
||||
build_type = self.get_property('ro.build.type')
|
||||
if build_type == 'user':
|
||||
return False
|
||||
self.run(['root'])
|
||||
time.sleep(1)
|
||||
self.run(['wait-for-device'])
|
||||
result, stdoutdata = self.run_and_return_output(['shell', 'whoami'])
|
||||
return result and 'root' in stdoutdata
|
||||
|
||||
def get_property(self, name):
|
||||
result, stdoutdata = self.run_and_return_output(['shell', 'getprop', name])
|
||||
return stdoutdata if result else None
|
||||
|
||||
def set_property(self, name, value):
|
||||
return self.run(['shell', 'setprop', name, value])
|
||||
|
||||
def get_device_arch(self):
|
||||
output = self.check_run_and_return_output(['shell', 'uname', '-m'])
|
||||
if 'aarch64' in output:
|
||||
return 'arm64'
|
||||
if 'arm' in output:
|
||||
return 'arm'
|
||||
if 'x86_64' in output:
|
||||
return 'x86_64'
|
||||
if '86' in output:
|
||||
return 'x86'
|
||||
log_fatal('unsupported architecture: %s' % output.strip())
|
||||
|
||||
def get_android_version(self):
|
||||
build_version = self.get_property('ro.build.version.release')
|
||||
android_version = 0
|
||||
if build_version:
|
||||
if not build_version[0].isdigit():
|
||||
c = build_version[0].upper()
|
||||
if c.isupper() and c >= 'L':
|
||||
android_version = ord(c) - ord('L') + 5
|
||||
else:
|
||||
strs = build_version.split('.')
|
||||
if strs:
|
||||
android_version = int(strs[0])
|
||||
return android_version
|
||||
|
||||
|
||||
def flatten_arg_list(arg_list):
|
||||
res = []
|
||||
if arg_list:
|
||||
for items in arg_list:
|
||||
res += items
|
||||
return res
|
||||
|
||||
|
||||
def remove(dir_or_file):
|
||||
if os.path.isfile(dir_or_file):
|
||||
os.remove(dir_or_file)
|
||||
elif os.path.isdir(dir_or_file):
|
||||
shutil.rmtree(dir_or_file, ignore_errors=True)
|
||||
|
||||
|
||||
def open_report_in_browser(report_path):
|
||||
if is_darwin():
|
||||
# On darwin 10.12.6, webbrowser can't open browser, so try `open` cmd first.
|
||||
try:
|
||||
subprocess.check_call(['open', report_path])
|
||||
return
|
||||
except:
|
||||
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.open(report_path, new=0, autoraise=True)
|
||||
except:
|
||||
# webbrowser.get() doesn't work well on darwin/windows.
|
||||
webbrowser.open_new_tab(report_path)
|
||||
|
||||
|
||||
def find_real_dso_path(dso_path_in_record_file, binary_cache_path):
|
||||
""" Given the path of a shared library in perf.data, find its real path in the file system. """
|
||||
if dso_path_in_record_file[0] != '/' or dso_path_in_record_file == '//anon':
|
||||
return None
|
||||
if binary_cache_path:
|
||||
tmp_path = os.path.join(binary_cache_path, dso_path_in_record_file[1:])
|
||||
if os.path.isfile(tmp_path):
|
||||
return tmp_path
|
||||
if os.path.isfile(dso_path_in_record_file):
|
||||
return dso_path_in_record_file
|
||||
return None
|
||||
|
||||
|
||||
class Addr2Nearestline(object):
|
||||
""" Use addr2line to convert (dso_path, func_addr, addr) to (source_file, line) pairs.
|
||||
For instructions generated by C++ compilers without a matching statement in source code
|
||||
(like stack corruption check, switch optimization, etc.), addr2line can't generate
|
||||
line information. However, we want to assign the instruction to the nearest line before
|
||||
the instruction (just like objdump -dl). So we use below strategy:
|
||||
Instead of finding the exact line of the instruction in an address, we find the nearest
|
||||
line to the instruction in an address. If an address doesn't have a line info, we find
|
||||
the line info of address - 1. If still no line info, then use address - 2, address - 3,
|
||||
etc.
|
||||
|
||||
The implementation steps are as below:
|
||||
1. Collect all (dso_path, func_addr, addr) requests before converting. This saves the
|
||||
times to call addr2line.
|
||||
2. Convert addrs to (source_file, line) pairs for each dso_path as below:
|
||||
2.1 Check if the dso_path has .debug_line. If not, omit its conversion.
|
||||
2.2 Get arch of the dso_path, and decide the addr_step for it. addr_step is the step we
|
||||
change addr each time. For example, since instructions of arm64 are all 4 bytes long,
|
||||
addr_step for arm64 can be 4.
|
||||
2.3 Use addr2line to find line info for each addr in the dso_path.
|
||||
2.4 For each addr without line info, use addr2line to find line info for
|
||||
range(addr - addr_step, addr - addr_step * 4 - 1, -addr_step).
|
||||
2.5 For each addr without line info, use addr2line to find line info for
|
||||
range(addr - addr_step * 5, addr - addr_step * 128 - 1, -addr_step).
|
||||
(128 is a guess number. A nested switch statement in
|
||||
system/core/demangle/Demangler.cpp has >300 bytes without line info in arm64.)
|
||||
"""
|
||||
class Dso(object):
|
||||
""" Info of a dynamic shared library.
|
||||
addrs: a map from address to Addr object in this dso.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.addrs = {}
|
||||
|
||||
class Addr(object):
|
||||
""" Info of an addr request.
|
||||
func_addr: start_addr of the function containing addr.
|
||||
source_lines: a list of [file_id, line_number] for addr.
|
||||
source_lines[:-1] are all for inlined functions.
|
||||
"""
|
||||
def __init__(self, func_addr):
|
||||
self.func_addr = func_addr
|
||||
self.source_lines = None
|
||||
|
||||
def __init__(self, ndk_path, binary_cache_path):
|
||||
self.addr2line_path = find_tool_path('addr2line', ndk_path)
|
||||
if not self.addr2line_path:
|
||||
log_exit("Can't find addr2line. Please set ndk path with --ndk-path option.")
|
||||
self.readelf = ReadElf(ndk_path)
|
||||
self.dso_map = {} # map from dso_path to Dso.
|
||||
self.binary_cache_path = binary_cache_path
|
||||
# Saving file names for each addr takes a lot of memory. So we store file ids in Addr,
|
||||
# and provide data structures connecting file id and file name here.
|
||||
self.file_name_to_id = {}
|
||||
self.file_id_to_name = []
|
||||
|
||||
def add_addr(self, dso_path, func_addr, addr):
|
||||
dso = self.dso_map.get(dso_path)
|
||||
if dso is None:
|
||||
dso = self.dso_map[dso_path] = self.Dso()
|
||||
if addr not in dso.addrs:
|
||||
dso.addrs[addr] = self.Addr(func_addr)
|
||||
|
||||
def convert_addrs_to_lines(self):
|
||||
for dso_path in self.dso_map:
|
||||
self._convert_addrs_in_one_dso(dso_path, self.dso_map[dso_path])
|
||||
|
||||
def _convert_addrs_in_one_dso(self, dso_path, dso):
|
||||
real_path = find_real_dso_path(dso_path, self.binary_cache_path)
|
||||
if not real_path:
|
||||
if dso_path not in ['//anon', 'unknown', '[kernel.kallsyms]']:
|
||||
log_debug("Can't find dso %s" % dso_path)
|
||||
return
|
||||
|
||||
if not self._check_debug_line_section(real_path):
|
||||
log_debug("file %s doesn't contain .debug_line section." % real_path)
|
||||
return
|
||||
|
||||
addr_step = self._get_addr_step(real_path)
|
||||
self._collect_line_info(dso, real_path, [0])
|
||||
self._collect_line_info(dso, real_path, range(-addr_step, -addr_step * 4 - 1, -addr_step))
|
||||
self._collect_line_info(dso, real_path,
|
||||
range(-addr_step * 5, -addr_step * 128 - 1, -addr_step))
|
||||
|
||||
def _check_debug_line_section(self, real_path):
|
||||
return '.debug_line' in self.readelf.get_sections(real_path)
|
||||
|
||||
def _get_addr_step(self, real_path):
|
||||
arch = self.readelf.get_arch(real_path)
|
||||
if arch == 'arm64':
|
||||
return 4
|
||||
if arch == 'arm':
|
||||
return 2
|
||||
return 1
|
||||
|
||||
def _collect_line_info(self, dso, real_path, addr_shifts):
|
||||
""" Use addr2line to get line info in a dso, with given addr shifts. """
|
||||
# 1. Collect addrs to send to addr2line.
|
||||
addr_set = set()
|
||||
for addr in dso.addrs:
|
||||
addr_obj = dso.addrs[addr]
|
||||
if addr_obj.source_lines: # already has source line, no need to search.
|
||||
continue
|
||||
for shift in addr_shifts:
|
||||
# The addr after shift shouldn't change to another function.
|
||||
shifted_addr = max(addr + shift, addr_obj.func_addr)
|
||||
addr_set.add(shifted_addr)
|
||||
if shifted_addr == addr_obj.func_addr:
|
||||
break
|
||||
if not addr_set:
|
||||
return
|
||||
addr_request = '\n'.join(['%x' % addr for addr in sorted(addr_set)])
|
||||
|
||||
# 2. Use addr2line to collect line info.
|
||||
try:
|
||||
subproc = subprocess.Popen([self.addr2line_path, '-ai', '-e', real_path],
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
(stdoutdata, _) = subproc.communicate(str_to_bytes(addr_request))
|
||||
stdoutdata = bytes_to_str(stdoutdata)
|
||||
except:
|
||||
return
|
||||
addr_map = {}
|
||||
cur_line_list = None
|
||||
for line in stdoutdata.strip().split('\n'):
|
||||
if line[:2] == '0x':
|
||||
# a new address
|
||||
cur_line_list = addr_map[int(line, 16)] = []
|
||||
else:
|
||||
# a file:line.
|
||||
if cur_line_list is None:
|
||||
continue
|
||||
# Handle lines like "C:\Users\...\file:32".
|
||||
items = line.rsplit(':', 1)
|
||||
if len(items) != 2:
|
||||
continue
|
||||
if '?' in line:
|
||||
# if ? in line, it doesn't have a valid line info.
|
||||
# An addr can have a list of (file, line), when the addr belongs to an inlined
|
||||
# function. Sometimes only part of the list has ? mark. In this case, we think
|
||||
# the line info is valid if the first line doesn't have ? mark.
|
||||
if not cur_line_list:
|
||||
cur_line_list = None
|
||||
continue
|
||||
(file_path, line_number) = items
|
||||
line_number = line_number.split()[0] # Remove comments after line number
|
||||
try:
|
||||
line_number = int(line_number)
|
||||
except ValueError:
|
||||
continue
|
||||
file_id = self._get_file_id(file_path)
|
||||
cur_line_list.append((file_id, line_number))
|
||||
|
||||
# 3. Fill line info in dso.addrs.
|
||||
for addr in dso.addrs:
|
||||
addr_obj = dso.addrs[addr]
|
||||
if addr_obj.source_lines:
|
||||
continue
|
||||
for shift in addr_shifts:
|
||||
shifted_addr = max(addr + shift, addr_obj.func_addr)
|
||||
lines = addr_map.get(shifted_addr)
|
||||
if lines:
|
||||
addr_obj.source_lines = lines
|
||||
break
|
||||
if shifted_addr == addr_obj.func_addr:
|
||||
break
|
||||
|
||||
def _get_file_id(self, file_path):
|
||||
file_id = self.file_name_to_id.get(file_path)
|
||||
if file_id is None:
|
||||
file_id = self.file_name_to_id[file_path] = len(self.file_id_to_name)
|
||||
self.file_id_to_name.append(file_path)
|
||||
return file_id
|
||||
|
||||
def get_dso(self, dso_path):
|
||||
return self.dso_map.get(dso_path)
|
||||
|
||||
def get_addr_source(self, dso, addr):
|
||||
source = dso.addrs[addr].source_lines
|
||||
if source is None:
|
||||
return None
|
||||
return [(self.file_id_to_name[file_id], line) for (file_id, line) in source]
|
||||
|
||||
|
||||
class Objdump(object):
|
||||
""" A wrapper of objdump to disassemble code. """
|
||||
def __init__(self, ndk_path, binary_cache_path):
|
||||
self.ndk_path = ndk_path
|
||||
self.binary_cache_path = binary_cache_path
|
||||
self.readelf = ReadElf(ndk_path)
|
||||
self.objdump_paths = {}
|
||||
|
||||
def disassemble_code(self, dso_path, start_addr, addr_len):
|
||||
""" Disassemble [start_addr, start_addr + addr_len] of dso_path.
|
||||
Return a list of pair (disassemble_code_line, addr).
|
||||
"""
|
||||
# 1. Find real path.
|
||||
real_path = find_real_dso_path(dso_path, self.binary_cache_path)
|
||||
if real_path is None:
|
||||
return None
|
||||
|
||||
# 2. Get path of objdump.
|
||||
arch = self.readelf.get_arch(real_path)
|
||||
if arch == 'unknown':
|
||||
return None
|
||||
objdump_path = self.objdump_paths.get(arch)
|
||||
if not objdump_path:
|
||||
objdump_path = find_tool_path('objdump', self.ndk_path, arch)
|
||||
if not objdump_path:
|
||||
log_exit("Can't find objdump. Please set ndk path with --ndk_path option.")
|
||||
self.objdump_paths[arch] = objdump_path
|
||||
|
||||
# 3. Run objdump.
|
||||
args = [objdump_path, '-dlC', '--no-show-raw-insn',
|
||||
'--start-address=0x%x' % start_addr,
|
||||
'--stop-address=0x%x' % (start_addr + addr_len),
|
||||
real_path]
|
||||
try:
|
||||
subproc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
(stdoutdata, _) = subproc.communicate()
|
||||
stdoutdata = bytes_to_str(stdoutdata)
|
||||
except:
|
||||
return None
|
||||
|
||||
if not stdoutdata:
|
||||
return None
|
||||
result = []
|
||||
for line in stdoutdata.split('\n'):
|
||||
line = line.rstrip() # Remove '\r' on Windows.
|
||||
items = line.split(':', 1)
|
||||
try:
|
||||
addr = int(items[0], 16)
|
||||
except ValueError:
|
||||
addr = 0
|
||||
result.append((line, addr))
|
||||
return result
|
||||
|
||||
|
||||
class ReadElf(object):
|
||||
""" A wrapper of readelf. """
|
||||
def __init__(self, ndk_path):
|
||||
self.readelf_path = find_tool_path('readelf', ndk_path)
|
||||
if not self.readelf_path:
|
||||
log_exit("Can't find readelf. Please set ndk path with --ndk_path option.")
|
||||
|
||||
def get_arch(self, elf_file_path):
|
||||
""" Get arch of an elf file. """
|
||||
try:
|
||||
output = subprocess.check_output([self.readelf_path, '-h', elf_file_path])
|
||||
if output.find('AArch64') != -1:
|
||||
return 'arm64'
|
||||
if output.find('ARM') != -1:
|
||||
return 'arm'
|
||||
if output.find('X86-64') != -1:
|
||||
return 'x86_64'
|
||||
if output.find('80386') != -1:
|
||||
return 'x86'
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
return 'unknown'
|
||||
|
||||
def get_build_id(self, elf_file_path):
|
||||
""" Get build id of an elf file. """
|
||||
try:
|
||||
output = subprocess.check_output([self.readelf_path, '-n', elf_file_path])
|
||||
output = bytes_to_str(output)
|
||||
result = re.search(r'Build ID:\s*(\S+)', output)
|
||||
if result:
|
||||
build_id = result.group(1)
|
||||
if len(build_id) < 40:
|
||||
build_id += '0' * (40 - len(build_id))
|
||||
else:
|
||||
build_id = build_id[:40]
|
||||
build_id = '0x' + build_id
|
||||
return build_id
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
return ""
|
||||
|
||||
def get_sections(self, elf_file_path):
|
||||
""" Get sections of an elf file. """
|
||||
section_names = []
|
||||
try:
|
||||
output = subprocess.check_output([self.readelf_path, '-SW', elf_file_path])
|
||||
output = bytes_to_str(output)
|
||||
for line in output.split('\n'):
|
||||
# Parse line like:" [ 1] .note.android.ident NOTE 0000000000400190 ...".
|
||||
result = re.search(r'^\s+\[\s*\d+\]\s(.+?)\s', line)
|
||||
if result:
|
||||
section_name = result.group(1).strip()
|
||||
if section_name:
|
||||
section_names.append(section_name)
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
return section_names
|
||||
|
||||
def extant_dir(arg):
|
||||
"""ArgumentParser type that only accepts extant directories.
|
||||
|
||||
Args:
|
||||
arg: The string argument given on the command line.
|
||||
Returns: The argument as a realpath.
|
||||
Raises:
|
||||
argparse.ArgumentTypeError: The given path isn't a directory.
|
||||
"""
|
||||
path = os.path.realpath(arg)
|
||||
if not os.path.isdir(path):
|
||||
import argparse
|
||||
raise argparse.ArgumentTypeError('{} is not a directory.'.format(path))
|
||||
return path
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
Reference in New Issue
Block a user