Skip to content

Commit 1257a12

Browse files
committed
Add and test instrumentation when using threading.Thread
1 parent cb89d13 commit 1257a12

8 files changed

Lines changed: 121 additions & 4 deletions

File tree

scorep/_instrumenters/scorep_cProfile.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,13 @@ class ScorepCProfile(scorep_bindings.CInstrumenter, ScorepInstrumenter):
66
def __init__(self, enable_instrumenter):
77
scorep_bindings.CInstrumenter.__init__(self, tracingOrProfiling=False)
88
ScorepInstrumenter.__init__(self, enable_instrumenter)
9+
10+
def _enable_instrumenter(self):
11+
if self._threading:
12+
self._threading.setprofile(self)
13+
super()._enable_instrumenter()
14+
15+
def _disable_instrumenter(self):
16+
super()._disable_instrumenter()
17+
if self._threading:
18+
self._threading.setprofile(None)

scorep/_instrumenters/scorep_cTrace.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,13 @@ class ScorepCTrace(scorep_bindings.CInstrumenter, ScorepInstrumenter):
66
def __init__(self, enable_instrumenter):
77
scorep_bindings.CInstrumenter.__init__(self, tracingOrProfiling=True)
88
ScorepInstrumenter.__init__(self, enable_instrumenter)
9+
10+
def _enable_instrumenter(self):
11+
if self._threading:
12+
self._threading.settrace(self)
13+
super()._enable_instrumenter()
14+
15+
def _disable_instrumenter(self):
16+
super()._disable_instrumenter()
17+
if self._threading:
18+
self._threading.settrace(None)

scorep/_instrumenters/scorep_instrumenter.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ def __init__(self, enable_instrumenter=True):
1515
"""
1616
self._tracer_registered = False
1717
self._enabled = enable_instrumenter
18+
# TODO: Support other threading libs, e.g. greenlet?
19+
try:
20+
import threading
21+
self._threading = threading
22+
except ImportError:
23+
self._threading = None
1824

1925
@abc.abstractmethod
2026
def _enable_instrumenter(self):

src/classes.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ extern "C"
5555
self->disable_instrumenter();
5656
Py_RETURN_NONE;
5757
}
58+
59+
static PyObject* CInstrumenter_call(scorepy::CInstrumenter* self, PyObject* args,
60+
PyObject* kwds)
61+
{
62+
static const char* kwlist[] = { "frame", "event", "arg", nullptr };
63+
64+
PyFrameObject* frame;
65+
const char* event;
66+
PyObject* arg;
67+
68+
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!sO", const_cast<char**>(kwlist),
69+
&PyFrame_Type, &frame, &event, &arg))
70+
return nullptr;
71+
return (*self)(*frame, event, arg);
72+
}
5873
}
5974

6075
namespace scorepy
@@ -84,6 +99,7 @@ PyTypeObject& getCInstrumenterType()
8499
type.tp_new = call_object_new;
85100
type.tp_init = scorepy::castToPyFunc(CInstrumenter_init);
86101
type.tp_methods = methods;
102+
type.tp_call = scorepy::castToPyFunc(CInstrumenter_call);
87103
type.tp_getset = getseters;
88104
type.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
89105
type.tp_doc = "Class for the C instrumenter interface of Score-P";

src/scorepy/cInstrumenter.cpp

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#include "cInstrumenter.hpp"
22
#include "events.hpp"
33
#include "pythonHelpers.hpp"
4+
#include <algorithm>
5+
#include <array>
46
#include <string>
57

68
namespace scorepy
@@ -16,10 +18,6 @@ static const std::string& make_region_name(const char* moduleName, const char* n
1618

1719
void CInstrumenter::enable_instrumenter()
1820
{
19-
// TODO: Known issue: `sys.getprofile()` returns the user object (2nd arg)
20-
// So `sys.setprofile(sys.getprofile())` will not round-trip as it will try to call the
21-
// 2nd arg. If it is nullptr (here) it means it will be disabled completely
22-
// See https://nedbatchelder.com/text/trace-function.html for details
2321
const auto callback = [](PyObject* obj, PyFrameObject* frame, int what, PyObject* arg) -> int {
2422
return fromPyObject(obj)->onEvent(*frame, what, arg) ? 0 : -1;
2523
};
@@ -41,6 +39,34 @@ void CInstrumenter::disable_instrumenter()
4139
PyEval_SetProfile(nullptr, nullptr);
4240
}
4341

42+
/// Mapping of PyTrace_* to it's string representations
43+
/// List taken from CPythons sysmodule.c
44+
static const std::array<std::string, 8> whatStrings = { "call", "exception", "line",
45+
"return", "c_call", "c_exception",
46+
"c_return", "opcode" };
47+
48+
// Required because: `sys.getprofile()` returns the user object (2nd arg to PyEval_SetTrace)
49+
// So `sys.setprofile(sys.getprofile())` will not round-trip as it will try to call the
50+
// 2nd arg through pythons dispatch function. Hence make the object callable.
51+
// See https://nedbatchelder.com/text/trace-function.html for details
52+
PyObject* CInstrumenter::operator()(PyFrameObject& frame, const char* what, PyObject* arg)
53+
{
54+
const auto itWhat = std::find(whatStrings.begin(), whatStrings.end(), what);
55+
const int iWhat = itWhat == whatStrings.end() ? -1 : std::distance(whatStrings.begin(), itWhat);
56+
// To speed up further event processing install this class directly as the handler
57+
// But we might be inside a `sys.settrace` call where the user wanted to set another function
58+
// which would then be overwritten here. Hence use the CALL event which avoids the problem
59+
if (iWhat == PyTrace_CALL)
60+
enable_instrumenter();
61+
if (onEvent(frame, iWhat, arg))
62+
{
63+
Py_INCREF(toPyObject());
64+
return toPyObject();
65+
}
66+
else
67+
return nullptr;
68+
}
69+
4470
bool CInstrumenter::onEvent(PyFrameObject& frame, int what, PyObject*)
4571
{
4672
switch (what)

src/scorepy/cInstrumenter.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ struct CInstrumenter
1717
void enable_instrumenter();
1818
void disable_instrumenter();
1919

20+
/// Callback for when this object is called directly
21+
PyObject* operator()(PyFrameObject& frame, const char* what, PyObject* arg);
22+
2023
/// These casts are valid as long as `PyObject_HEAD` is the first entry in this struct
2124
PyObject* toPyObject()
2225
{

test/cases/use_threads.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import random
2+
import threading
3+
import time
4+
import instrumentation2
5+
6+
7+
def worker(id, func):
8+
print("Thread %s started" % id)
9+
# Use a random delay to add non-determinism to the output
10+
time.sleep(random.uniform(0.01, 0.9))
11+
func()
12+
13+
14+
def foo():
15+
print("hello world")
16+
t1 = threading.Thread(target=worker, args=(0, instrumentation2.bar))
17+
t2 = threading.Thread(target=worker, args=(1, instrumentation2.baz))
18+
t1.start()
19+
t2.start()
20+
21+
22+
foo()

test/test_scorep.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,3 +294,27 @@ def test_instrumentation_ctracing(scorep_env, instrumenter):
294294
for func in ('__main__:foo', 'instrumentation2:bar', 'instrumentation2:baz'):
295295
for event in ('ENTER', 'LEAVE'):
296296
assert re.search('%s[ ]*[0-9 ]*[0-9 ]*Region: "%s"' % (event, func), std_out)
297+
298+
299+
@pytest.mark.parametrize('instrumenter', ['profile', 'trace', 'cProfile', 'cTrace'])
300+
def test_threads(scorep_env, instrumenter):
301+
if instrumenter[0] == 'c' and sys.version_info.major < 3:
302+
pytest.skip("C extension class only implemented for Python3")
303+
304+
trace_path = scorep_env["SCOREP_EXPERIMENT_DIRECTORY"] + "/traces.otf2"
305+
306+
std_out, std_err = call_with_scorep("cases/use_threads.py",
307+
["--nocompiler", "--instrumenter-type=" + instrumenter],
308+
env=scorep_env)
309+
310+
# assert std_err == "" TODO: Readd when issue #87 is resolved
311+
assert re.search(("hello world\n" +
312+
"(Thread 0 started\nThread 1 started\n|Thread 1 started\nThread 0 started\n)" +
313+
"(baz\nbar\n|bar\nbaz\n)"), std_out)
314+
315+
std_out, std_err = call(["otf2-print", trace_path])
316+
317+
assert std_err == ""
318+
for func in ('__main__:foo', 'instrumentation2:bar', 'instrumentation2:baz'):
319+
for event in ('ENTER', 'LEAVE'):
320+
assert re.search('%s[ ]*[0-9 ]*[0-9 ]*Region: "%s"' % (event, func), std_out)

0 commit comments

Comments
 (0)