Skip to content

Commit 5029e94

Browse files
author
MarcoFalke
committed
Merge #14519: tests: add utility to easily profile node performance with perf
13782b8 docs: add perf section to developer docs (James O'Beirne) 58180b5 tests: add utility to easily profile node performance with perf (James O'Beirne) Pull request description: Adds a context manager to easily (and selectively) profile node performance during functional test execution using `perf`. While writing some tests, I encountered some odd bitcoind slowness. I wrote up a utility (`TestNode.profile_with_perf`) that generates performance diagnostics for a node by running `perf` during the execution of a particular region of test code. `perf` usage is detailed in the excellent (and sadly unmerged) bitcoin/bitcoin#12649; all due props to @eklitzke. ### Example ```python with node.profile_with_perf("large-msgs"): for i in range(200): node.p2p.send_message(some_large_msg) node.p2p.sync_with_ping() ``` This generates a perf data file in the test node's datadir (`/tmp/testtxmpod0y/node0/node-0-TestName-large-msgs.perf.data`). Running `perf report` generates nice output about where the node spent most of its time while running that part of the test: ```bash $ perf report -i /tmp/testtxmpod0y/node0/node-0-TestName-large-msgs.perf.data --stdio \ | c++filt \ | less # To display the perf.data header info, please use --header/--header-only options. # # # Total Lost Samples: 0 # # Samples: 135 of event 'cycles:pp' # Event count (approx.): 1458205679493582 # # Children Self Command Shared Object Symbol # ........ ........ ............... ................... ........................................................................................................................................................................................................................................................................ # 70.14% 0.00% bitcoin-net bitcoind [.] CNode::ReceiveMsgBytes(char const*, unsigned int, bool&) | ---CNode::ReceiveMsgBytes(char const*, unsigned int, bool&) 70.14% 0.00% bitcoin-net bitcoind [.] CNetMessage::readData(char const*, unsigned int) | ---CNetMessage::readData(char const*, unsigned int) CNode::ReceiveMsgBytes(char const*, unsigned int, bool&) 35.52% 0.00% bitcoin-net bitcoind [.] std::vector<char, zero_after_free_allocator<char> >::_M_fill_insert(__gnu_cxx::__normal_iterator<char*, std::vector<char, zero_after_free_allocator<char> > >, unsigned long, char const&) | ---std::vector<char, zero_after_free_allocator<char> >::_M_fill_insert(__gnu_cxx::__normal_iterator<char*, std::vector<char, zero_after_free_allocator<char> > >, unsigned long, char const&) CNetMessage::readData(char const*, unsigned int) CNode::ReceiveMsgBytes(char const*, unsigned int, bool&) ... ``` Tree-SHA512: 9ac4ceaa88818d5eca00994e8e3c8ad42ae019550d6583972a0a4f7b0c4f61032e3d0c476b4ae58756bc5eb8f8015a19a7fc26c095bd588f31d49a37ed0c6b3e
2 parents baf125b + 13782b8 commit 5029e94

File tree

6 files changed

+211
-4
lines changed

6 files changed

+211
-4
lines changed

doc/developer-notes.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Developer Notes
1717
- [DEBUG_LOCKORDER](#debug_lockorder)
1818
- [Valgrind suppressions file](#valgrind-suppressions-file)
1919
- [Compiling for test coverage](#compiling-for-test-coverage)
20+
- [Performance profiling with perf](#performance-profiling-with-perf)
2021
- [Locking/mutex usage notes](#lockingmutex-usage-notes)
2122
- [Threads](#threads)
2223
- [Ignoring IDE/editor files](#ignoring-ideeditor-files)
@@ -265,6 +266,51 @@ make cov
265266
# A coverage report will now be accessible at `./test_bitcoin.coverage/index.html`.
266267
```
267268

269+
### Performance profiling with perf
270+
271+
Profiling is a good way to get a precise idea of where time is being spent in
272+
code. One tool for doing profiling on Linux platforms is called
273+
[`perf`](http://www.brendangregg.com/perf.html), and has been integrated into
274+
the functional test framework. Perf can observe a running process and sample
275+
(at some frequency) where its execution is.
276+
277+
Perf installation is contingent on which kernel version you're running; see
278+
[this StackExchange
279+
thread](https://askubuntu.com/questions/50145/how-to-install-perf-monitoring-tool)
280+
for specific instructions.
281+
282+
Certain kernel parameters may need to be set for perf to be able to inspect the
283+
running process' stack.
284+
285+
```sh
286+
$ sudo sysctl -w kernel.perf_event_paranoid=-1
287+
$ sudo sysctl -w kernel.kptr_restrict=0
288+
```
289+
290+
Make sure you [understand the security
291+
trade-offs](https://lwn.net/Articles/420403/) of setting these kernel
292+
parameters.
293+
294+
To profile a running bitcoind process for 60 seconds, you could use an
295+
invocation of `perf record` like this:
296+
297+
```sh
298+
$ perf record \
299+
-g --call-graph dwarf --per-thread -F 140 \
300+
-p `pgrep bitcoind` -- sleep 60
301+
```
302+
303+
You could then analyze the results by running
304+
305+
```sh
306+
perf report --stdio | c++filt | less
307+
```
308+
309+
or using a graphical tool like [Hotspot](https://github.com/KDAB/hotspot).
310+
311+
See the functional test documentation for how to invoke perf within tests.
312+
313+
268314
**Sanitizers**
269315

270316
Bitcoin Core can be compiled with various "sanitizers" enabled, which add

test/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,26 @@ gdb /home/example/bitcoind <pid>
178178
Note: gdb attach step may require ptrace_scope to be modified, or `sudo` preceding the `gdb`.
179179
See this link for considerations: https://www.kernel.org/doc/Documentation/security/Yama.txt
180180

181+
##### Profiling
182+
183+
An easy way to profile node performance during functional tests is provided
184+
for Linux platforms using `perf`.
185+
186+
Perf will sample the running node and will generate profile data in the node's
187+
datadir. The profile data can then be presented using `perf report` or a graphical
188+
tool like [hotspot](https://github.com/KDAB/hotspot).
189+
190+
To generate a profile during test suite runs, use the `--perf` flag.
191+
192+
To see render the output to text, run
193+
194+
```sh
195+
perf report -i /path/to/datadir/send-big-msgs.perf.data.xxxx --stdio | c++filt | less
196+
```
197+
198+
For ways to generate more granular profiles, see the README in
199+
[test/functional](/test/functional).
200+
181201
### Util tests
182202

183203
Util tests can be run locally by running `test/util/bitcoin-util-test.py`.

test/functional/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,36 @@ Helpers for script.py
122122

123123
#### [test_framework/blocktools.py](test_framework/blocktools.py)
124124
Helper functions for creating blocks and transactions.
125+
126+
### Benchmarking with perf
127+
128+
An easy way to profile node performance during functional tests is provided
129+
for Linux platforms using `perf`.
130+
131+
Perf will sample the running node and will generate profile data in the node's
132+
datadir. The profile data can then be presented using `perf report` or a graphical
133+
tool like [hotspot](https://github.com/KDAB/hotspot).
134+
135+
There are two ways of invoking perf: one is to use the `--perf` flag when
136+
running tests, which will profile each node during the entire test run: perf
137+
begins to profile when the node starts and ends when it shuts down. The other
138+
way is the use the `profile_with_perf` context manager, e.g.
139+
140+
```python
141+
with node.profile_with_perf("send-big-msgs"):
142+
# Perform activity on the node you're interested in profiling, e.g.:
143+
for _ in range(10000):
144+
node.p2p.send_message(some_large_message)
145+
```
146+
147+
To see useful textual output, run
148+
149+
```sh
150+
perf report -i /path/to/datadir/send-big-msgs.perf.data.xxxx --stdio | c++filt | less
151+
```
152+
153+
#### See also:
154+
155+
- [Installing perf](https://askubuntu.com/q/50145)
156+
- [Perf examples](http://www.brendangregg.com/perf.html)
157+
- [Hotspot](https://github.com/KDAB/hotspot): a GUI for perf output analysis

test/functional/test_framework/test_framework.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ def main(self):
128128
help="Attach a python debugger if test fails")
129129
parser.add_argument("--usecli", dest="usecli", default=False, action="store_true",
130130
help="use bitcoin-cli instead of RPC for all commands")
131+
parser.add_argument("--perf", dest="perf", default=False, action="store_true",
132+
help="profile running nodes with perf for the duration of the test")
131133
self.add_options(parser)
132134
self.options = parser.parse_args()
133135

@@ -202,11 +204,20 @@ def main(self):
202204
node.cleanup_on_exit = False
203205
self.log.info("Note: bitcoinds were not stopped and may still be running")
204206

205-
if not self.options.nocleanup and not self.options.noshutdown and success != TestStatus.FAILED:
207+
should_clean_up = (
208+
not self.options.nocleanup and
209+
not self.options.noshutdown and
210+
success != TestStatus.FAILED and
211+
not self.options.perf
212+
)
213+
if should_clean_up:
206214
self.log.info("Cleaning up {} on exit".format(self.options.tmpdir))
207215
cleanup_tree_on_exit = True
216+
elif self.options.perf:
217+
self.log.warning("Not cleaning up dir {} due to perf data".format(self.options.tmpdir))
218+
cleanup_tree_on_exit = False
208219
else:
209-
self.log.warning("Not cleaning up dir %s" % self.options.tmpdir)
220+
self.log.warning("Not cleaning up dir {}".format(self.options.tmpdir))
210221
cleanup_tree_on_exit = False
211222

212223
if success == TestStatus.PASSED:
@@ -310,6 +321,7 @@ def add_nodes(self, num_nodes, extra_args=None, *, rpchost=None, binary=None):
310321
extra_conf=extra_confs[i],
311322
extra_args=extra_args[i],
312323
use_cli=self.options.usecli,
324+
start_perf=self.options.perf,
313325
))
314326

315327
def start_node(self, i, *args, **kwargs):

test/functional/test_framework/test_node.py

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import time
1919
import urllib.parse
2020
import collections
21+
import shlex
22+
import sys
2123

2224
from .authproxy import JSONRPCException
2325
from .util import (
@@ -59,7 +61,13 @@ class TestNode():
5961
To make things easier for the test writer, any unrecognised messages will
6062
be dispatched to the RPC connection."""
6163

62-
def __init__(self, i, datadir, *, rpchost, timewait, bitcoind, bitcoin_cli, mocktime, coverage_dir, extra_conf=None, extra_args=None, use_cli=False):
64+
def __init__(self, i, datadir, *, rpchost, timewait, bitcoind, bitcoin_cli, mocktime, coverage_dir, extra_conf=None, extra_args=None, use_cli=False, start_perf=False):
65+
"""
66+
Kwargs:
67+
start_perf (bool): If True, begin profiling the node with `perf` as soon as
68+
the node starts.
69+
"""
70+
6371
self.index = i
6472
self.datadir = datadir
6573
self.stdout_dir = os.path.join(self.datadir, "stdout")
@@ -87,6 +95,7 @@ def __init__(self, i, datadir, *, rpchost, timewait, bitcoind, bitcoin_cli, mock
8795

8896
self.cli = TestNodeCLI(bitcoin_cli, self.datadir)
8997
self.use_cli = use_cli
98+
self.start_perf = start_perf
9099

91100
self.running = False
92101
self.process = None
@@ -95,6 +104,8 @@ def __init__(self, i, datadir, *, rpchost, timewait, bitcoind, bitcoin_cli, mock
95104
self.url = None
96105
self.log = logging.getLogger('TestFramework.node%d' % i)
97106
self.cleanup_on_exit = True # Whether to kill the node when this object goes away
107+
# Cache perf subprocesses here by their data output filename.
108+
self.perf_subprocesses = {}
98109

99110
self.p2ps = []
100111

@@ -186,6 +197,9 @@ def start(self, extra_args=None, *, stdout=None, stderr=None, **kwargs):
186197
self.running = True
187198
self.log.debug("bitcoind started, waiting for RPC to come up")
188199

200+
if self.start_perf:
201+
self._start_perf()
202+
189203
def wait_for_rpc_connection(self):
190204
"""Sets up an RPC connection to the bitcoind process. Returns False if unable to connect."""
191205
# Poll at a rate of four times per second
@@ -238,6 +252,10 @@ def stop_node(self, expected_stderr='', wait=0):
238252
except http.client.CannotSendRequest:
239253
self.log.exception("Unable to stop node.")
240254

255+
# If there are any running perf processes, stop them.
256+
for profile_name in tuple(self.perf_subprocesses.keys()):
257+
self._stop_perf(profile_name)
258+
241259
# Check that stderr is as expected
242260
self.stderr.seek(0)
243261
stderr = self.stderr.read().decode('utf-8').strip()
@@ -317,6 +335,84 @@ def assert_memory_usage_stable(self, *, increase_allowed=0.03):
317335
increase_allowed * 100, before_memory_usage, after_memory_usage,
318336
perc_increase_memory_usage * 100))
319337

338+
@contextlib.contextmanager
339+
def profile_with_perf(self, profile_name):
340+
"""
341+
Context manager that allows easy profiling of node activity using `perf`.
342+
343+
See `test/functional/README.md` for details on perf usage.
344+
345+
Args:
346+
profile_name (str): This string will be appended to the
347+
profile data filename generated by perf.
348+
"""
349+
subp = self._start_perf(profile_name)
350+
351+
yield
352+
353+
if subp:
354+
self._stop_perf(profile_name)
355+
356+
def _start_perf(self, profile_name=None):
357+
"""Start a perf process to profile this node.
358+
359+
Returns the subprocess running perf."""
360+
subp = None
361+
362+
def test_success(cmd):
363+
return subprocess.call(
364+
# shell=True required for pipe use below
365+
cmd, shell=True,
366+
stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) == 0
367+
368+
if not sys.platform.startswith('linux'):
369+
self.log.warning("Can't profile with perf; only availabe on Linux platforms")
370+
return None
371+
372+
if not test_success('which perf'):
373+
self.log.warning("Can't profile with perf; must install perf-tools")
374+
return None
375+
376+
if not test_success('readelf -S {} | grep .debug_str'.format(shlex.quote(self.binary))):
377+
self.log.warning(
378+
"perf output won't be very useful without debug symbols compiled into bitcoind")
379+
380+
output_path = tempfile.NamedTemporaryFile(
381+
dir=self.datadir,
382+
prefix="{}.perf.data.".format(profile_name or 'test'),
383+
delete=False,
384+
).name
385+
386+
cmd = [
387+
'perf', 'record',
388+
'-g', # Record the callgraph.
389+
'--call-graph', 'dwarf', # Compatibility for gcc's --fomit-frame-pointer.
390+
'-F', '101', # Sampling frequency in Hz.
391+
'-p', str(self.process.pid),
392+
'-o', output_path,
393+
]
394+
subp = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
395+
self.perf_subprocesses[profile_name] = subp
396+
397+
return subp
398+
399+
def _stop_perf(self, profile_name):
400+
"""Stop (and pop) a perf subprocess."""
401+
subp = self.perf_subprocesses.pop(profile_name)
402+
output_path = subp.args[subp.args.index('-o') + 1]
403+
404+
subp.terminate()
405+
subp.wait(timeout=10)
406+
407+
stderr = subp.stderr.read().decode()
408+
if 'Consider tweaking /proc/sys/kernel/perf_event_paranoid' in stderr:
409+
self.log.warning(
410+
"perf couldn't collect data! Try "
411+
"'sudo sysctl -w kernel.perf_event_paranoid=-1'")
412+
else:
413+
report_cmd = "perf report -i {}".format(output_path)
414+
self.log.info("See perf output by running '{}'".format(report_cmd))
415+
320416
def assert_start_raises_init_error(self, extra_args=None, expected_msg=None, match=ErrorMatch.FULL_TEXT, *args, **kwargs):
321417
"""Attempt to start the node and expect it to raise an error.
322418

test/lint/lint-python-dead-code.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ fi
1515

1616
vulture \
1717
--min-confidence 60 \
18-
--ignore-names "argtypes,connection_lost,connection_made,converter,data_received,daemon,errcheck,get_ecdh_key,get_privkey,is_compressed,is_fullyvalid,msg_generic,on_*,optionxform,restype,set_privkey" \
18+
--ignore-names "argtypes,connection_lost,connection_made,converter,data_received,daemon,errcheck,get_ecdh_key,get_privkey,is_compressed,is_fullyvalid,msg_generic,on_*,optionxform,restype,set_privkey,profile_with_perf" \
1919
$(git ls-files -- "*.py" ":(exclude)contrib/" ":(exclude)test/functional/data/invalid_txs.py")

0 commit comments

Comments
 (0)