18
18
import time
19
19
import urllib .parse
20
20
import collections
21
+ import shlex
22
+ import sys
21
23
22
24
from .authproxy import JSONRPCException
23
25
from .util import (
@@ -59,7 +61,13 @@ class TestNode():
59
61
To make things easier for the test writer, any unrecognised messages will
60
62
be dispatched to the RPC connection."""
61
63
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
+
63
71
self .index = i
64
72
self .datadir = datadir
65
73
self .stdout_dir = os .path .join (self .datadir , "stdout" )
@@ -87,6 +95,7 @@ def __init__(self, i, datadir, *, rpchost, timewait, bitcoind, bitcoin_cli, mock
87
95
88
96
self .cli = TestNodeCLI (bitcoin_cli , self .datadir )
89
97
self .use_cli = use_cli
98
+ self .start_perf = start_perf
90
99
91
100
self .running = False
92
101
self .process = None
@@ -95,6 +104,8 @@ def __init__(self, i, datadir, *, rpchost, timewait, bitcoind, bitcoin_cli, mock
95
104
self .url = None
96
105
self .log = logging .getLogger ('TestFramework.node%d' % i )
97
106
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 = {}
98
109
99
110
self .p2ps = []
100
111
@@ -186,6 +197,9 @@ def start(self, extra_args=None, *, stdout=None, stderr=None, **kwargs):
186
197
self .running = True
187
198
self .log .debug ("bitcoind started, waiting for RPC to come up" )
188
199
200
+ if self .start_perf :
201
+ self ._start_perf ()
202
+
189
203
def wait_for_rpc_connection (self ):
190
204
"""Sets up an RPC connection to the bitcoind process. Returns False if unable to connect."""
191
205
# Poll at a rate of four times per second
@@ -238,6 +252,10 @@ def stop_node(self, expected_stderr='', wait=0):
238
252
except http .client .CannotSendRequest :
239
253
self .log .exception ("Unable to stop node." )
240
254
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
+
241
259
# Check that stderr is as expected
242
260
self .stderr .seek (0 )
243
261
stderr = self .stderr .read ().decode ('utf-8' ).strip ()
@@ -317,6 +335,84 @@ def assert_memory_usage_stable(self, *, increase_allowed=0.03):
317
335
increase_allowed * 100 , before_memory_usage , after_memory_usage ,
318
336
perc_increase_memory_usage * 100 ))
319
337
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
+
320
416
def assert_start_raises_init_error (self , extra_args = None , expected_msg = None , match = ErrorMatch .FULL_TEXT , * args , ** kwargs ):
321
417
"""Attempt to start the node and expect it to raise an error.
322
418
0 commit comments