Skip to content

Commit e65258f

Browse files
committed
Implement CInstrumenter
This is the equivalent of ScorepTrace/Profile but implemented in C/C++ TODO: Threading support
1 parent 9b05409 commit e65258f

14 files changed

Lines changed: 389 additions & 1 deletion

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ find_package(Python REQUIRED COMPONENTS Interpreter Development)
1313
Python_add_library(scorep_bindings
1414
src/methods.cpp src/scorep_bindings.cpp src/scorepy/events.cpp
1515
)
16+
if(Python_VERSION_MAJOR GREATER_EQUAL 3)
17+
target_sources(scorep_bindings PRIVATE src/classes.cpp src/scorepy/cInstrumenter.cpp src/scorepy/pythonHelpers.cpp)
18+
endif()
1619
target_link_libraries(scorep_bindings PRIVATE Scorep::Plugin)
1720
target_compile_features(scorep_bindings PRIVATE cxx_std_11)
1821
target_compile_definitions(scorep_bindings PRIVATE PY_SSIZE_T_CLEAN)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from scorep._instrumenters.scorep_instrumenter import ScorepInstrumenter
2+
from scorep import scorep_bindings
3+
4+
5+
class ScorepCProfile(scorep_bindings.CInstrumenter, ScorepInstrumenter):
6+
def __init__(self, enable_instrumenter):
7+
scorep_bindings.CInstrumenter.__init__(self, tracingOrProfiling=False)
8+
ScorepInstrumenter.__init__(self, enable_instrumenter)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from scorep._instrumenters.scorep_instrumenter import ScorepInstrumenter
2+
from scorep import scorep_bindings
3+
4+
5+
class ScorepCTrace(scorep_bindings.CInstrumenter, ScorepInstrumenter):
6+
def __init__(self, enable_instrumenter):
7+
scorep_bindings.CInstrumenter.__init__(self, tracingOrProfiling=True)
8+
ScorepInstrumenter.__init__(self, enable_instrumenter)

scorep/instrumenter.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ def get_instrumenter(enable_instrumenter=False,
2424
elif instrumenter_type == "dummy":
2525
from scorep._instrumenters.dummy import ScorepDummy
2626
global_instrumenter = ScorepDummy(enable_instrumenter)
27+
elif instrumenter_type == "cTrace":
28+
from scorep._instrumenters.scorep_cTrace import ScorepCTrace
29+
global_instrumenter = ScorepCTrace(enable_instrumenter)
30+
elif instrumenter_type == "cProfile":
31+
from scorep._instrumenters.scorep_cProfile import ScorepCProfile
32+
global_instrumenter = ScorepCProfile(enable_instrumenter)
2733
else:
2834
raise RuntimeError('instrumenter_type "{}" unkown'.format(instrumenter_type))
2935

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import sys
23
from distutils.core import setup, Extension
34
import scorep.helper
45

@@ -24,6 +25,8 @@
2425
src_folder = os.path.abspath('src')
2526
include += [src_folder]
2627
sources = ['src/methods.cpp', 'src/scorep_bindings.cpp', 'src/scorepy/events.cpp']
28+
if sys.version_info.major >= 3:
29+
sources.extend(['src/classes.cpp', 'src/scorepy/cInstrumenter.cpp', 'src/scorepy/pythonHelpers.cpp'])
2730

2831
cmodules.append(Extension('scorep.scorep_bindings',
2932
include_dirs=include,

src/classes.cpp

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#include "classes.hpp"
2+
#include "scorepy/cInstrumenter.hpp"
3+
#include "scorepy/pythonHelpers.hpp"
4+
#include <Python.h>
5+
#include <type_traits>
6+
7+
static_assert(std::is_trivial<scorepy::CInstrumenter>::value,
8+
"Must be trivial or object creation by Python is UB");
9+
static_assert(std::is_standard_layout<scorepy::CInstrumenter>::value,
10+
"Must be standard layout or object creation by Python is UB");
11+
12+
extern "C"
13+
{
14+
15+
/// tp_new implementation that calls object.__new__ with not args to allow ABC classes to work
16+
static PyObject* call_object_new(PyTypeObject* type, PyObject*, PyObject*)
17+
{
18+
scorepy::PyRefObject empty_tuple(PyTuple_New(0), scorepy::adopt_object);
19+
if (!empty_tuple)
20+
return nullptr;
21+
scorepy::PyRefObject empty_dict(PyDict_New(), scorepy::adopt_object);
22+
if (!empty_dict)
23+
return nullptr;
24+
return PyBaseObject_Type.tp_new(type, empty_tuple, empty_dict);
25+
}
26+
27+
static int CInstrumenter_init(scorepy::CInstrumenter* self, PyObject* args, PyObject* kwds)
28+
{
29+
static const char* kwlist[] = { "tracingOrProfiling", nullptr };
30+
int tracingOrProfiling;
31+
32+
if (!PyArg_ParseTupleAndKeywords(args, kwds, "p", const_cast<char**>(kwlist),
33+
&tracingOrProfiling))
34+
return -1;
35+
36+
self->init(tracingOrProfiling != 0);
37+
return 0;
38+
}
39+
40+
static PyObject* CInstrumenter_get_tracingOrProfiling(scorepy::CInstrumenter* self, void*)
41+
{
42+
scorepy::PyRefObject result(self->tracingOrProfiling ? Py_True : Py_False,
43+
scorepy::retain_object);
44+
return result;
45+
}
46+
47+
static PyObject* CInstrumenter_enable_instrumenter(scorepy::CInstrumenter* self, PyObject*)
48+
{
49+
self->enable_instrumenter();
50+
Py_RETURN_NONE;
51+
}
52+
53+
static PyObject* CInstrumenter_disable_instrumenter(scorepy::CInstrumenter* self, PyObject*)
54+
{
55+
self->disable_instrumenter();
56+
Py_RETURN_NONE;
57+
}
58+
}
59+
60+
namespace scorepy
61+
{
62+
63+
PyTypeObject& getCInstrumenterType()
64+
{
65+
static PyMethodDef methods[] = {
66+
{ "_enable_instrumenter", reinterpret_cast<PyCFunction>(CInstrumenter_enable_instrumenter),
67+
METH_NOARGS, "Enable the instrumenter" },
68+
{ "_disable_instrumenter",
69+
reinterpret_cast<PyCFunction>(CInstrumenter_disable_instrumenter), METH_NOARGS,
70+
"Disable the instrumenter" },
71+
{ nullptr } /* Sentinel */
72+
};
73+
static PyGetSetDef getseters[] = {
74+
{ "tracingOrProfiling", reinterpret_cast<getter>(CInstrumenter_get_tracingOrProfiling),
75+
nullptr, "Return whether the trace (True) or profile (False) instrumentation is used",
76+
nullptr },
77+
{ nullptr } /* Sentinel */
78+
};
79+
// Sets the first few fields explicitely and remaining ones to zero
80+
static PyTypeObject type = {
81+
PyVarObject_HEAD_INIT(nullptr, 0) /* header */
82+
"scorep.scorep_bindings.CInstrumenter", /* tp_name */
83+
sizeof(CInstrumenter), /* tp_basicsize */
84+
};
85+
type.tp_new = call_object_new;
86+
type.tp_init = reinterpret_cast<initproc>(CInstrumenter_init);
87+
type.tp_methods = methods;
88+
type.tp_getset = getseters;
89+
type.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
90+
type.tp_doc = "Class for the C instrumenter interface of Score-P";
91+
return type;
92+
}
93+
94+
} // namespace scorepy

src/classes.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#pragma once
2+
3+
#include <Python.h>
4+
5+
namespace scorepy
6+
{
7+
/// Return the type info to define for the python module
8+
PyTypeObject& getCInstrumenterType();
9+
} // namespace scorepy

src/scorep.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#pragma once
2+
3+
#include <Python.h>
4+
5+
namespace scorepy
6+
{
7+
/// Return the methods to define for the python module
8+
PyMethodDef* getMethodTable();
9+
} // namespace scorepy

src/scorep_bindings.cpp

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#include "classes.hpp"
12
#include "methods.hpp"
23
#include <Python.h>
34

@@ -16,6 +17,22 @@ static struct PyModuleDef scorepmodule = { PyModuleDef_HEAD_INIT,
1617
scorepy::getMethodTable() };
1718
PyMODINIT_FUNC PyInit_scorep_bindings(void)
1819
{
19-
return PyModule_Create(&scorepmodule);
20+
auto* ctracerType = &scorepy::getCInstrumenterType();
21+
if (PyType_Ready(ctracerType) < 0)
22+
return nullptr;
23+
24+
auto* m = PyModule_Create(&scorepmodule);
25+
if (!m)
26+
return nullptr;
27+
28+
Py_INCREF(ctracerType);
29+
if (PyModule_AddObject(m, "CInstrumenter", (PyObject*)ctracerType) < 0)
30+
{
31+
Py_DECREF(ctracerType);
32+
Py_DECREF(m);
33+
return nullptr;
34+
}
35+
36+
return m;
2037
}
2138
#endif /*python 3*/

src/scorepy/cInstrumenter.cpp

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#include "cInstrumenter.hpp"
2+
#include "events.hpp"
3+
#include "pythonHelpers.hpp"
4+
#include <string>
5+
6+
namespace scorepy
7+
{
8+
static const std::string& make_region_name(const char* moduleName, const char* name)
9+
{
10+
static std::string region;
11+
region = moduleName;
12+
region += ":";
13+
region += name;
14+
return region;
15+
}
16+
17+
void CInstrumenter::enable_instrumenter()
18+
{
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
23+
const auto callback = [](PyObject* obj, PyFrameObject* frame, int what, PyObject* arg) -> int {
24+
return fromPyObject(obj)->onEvent(*frame, what, arg) ? 0 : -1;
25+
};
26+
if (tracingOrProfiling)
27+
{
28+
PyEval_SetTrace(callback, toPyObject());
29+
}
30+
else
31+
{
32+
PyEval_SetProfile(callback, toPyObject());
33+
}
34+
}
35+
36+
void CInstrumenter::disable_instrumenter()
37+
{
38+
if (tracingOrProfiling)
39+
PyEval_SetTrace(nullptr, nullptr);
40+
else
41+
PyEval_SetProfile(nullptr, nullptr);
42+
}
43+
44+
bool CInstrumenter::onEvent(PyFrameObject& frame, int what, PyObject*)
45+
{
46+
switch (what)
47+
{
48+
case PyTrace_CALL:
49+
{
50+
const PyCodeObject& code = *frame.f_code;
51+
const char* name = PyUnicode_AsUTF8(code.co_name);
52+
const char* moduleName = get_module_name(frame);
53+
assert(name);
54+
assert(moduleName);
55+
// TODO: Use string_view/CString comparison?
56+
if (std::string(name) != "_unsetprofile" && std::string(moduleName, 0, 6) != "scorep")
57+
{
58+
const int lineNumber = code.co_firstlineno;
59+
const auto& regionName = make_region_name(moduleName, name);
60+
const auto fileName = get_file_name(frame);
61+
region_begin(regionName, moduleName, fileName, lineNumber);
62+
}
63+
break;
64+
}
65+
case PyTrace_RETURN:
66+
{
67+
const PyCodeObject& code = *frame.f_code;
68+
const char* name = PyUnicode_AsUTF8(code.co_name);
69+
const char* moduleName = get_module_name(frame);
70+
assert(name);
71+
assert(moduleName);
72+
// TODO: Use string_view/CString comparison?
73+
if (std::string(name) != "_unsetprofile" && std::string(moduleName, 0, 6) != "scorep")
74+
{
75+
const auto& regionName = make_region_name(moduleName, name);
76+
region_end(regionName);
77+
}
78+
break;
79+
}
80+
}
81+
return true;
82+
}
83+
84+
} // namespace scorepy

0 commit comments

Comments
 (0)