diff --git a/python/.gitignore b/python/.gitignore new file mode 100644 index 0000000..5ead72c --- /dev/null +++ b/python/.gitignore @@ -0,0 +1,7 @@ +*.pyc +build +dist +*.egg-info/ +venv +__pycache__ +.coverage diff --git a/python/MANIFEST.in b/python/MANIFEST.in new file mode 100644 index 0000000..3d19431 --- /dev/null +++ b/python/MANIFEST.in @@ -0,0 +1,11 @@ +include README.md +include MANIFEST.in +include pyproject.toml +include setup.py +recursive-include ittapi *.py +recursive-include ittapi.native *.cpp *.hpp +recursive-include ../include *.h +recursive-include ../LICENSES * +recursive-include ../src * +include ../README.md +include ../SECURITY.md \ No newline at end of file diff --git a/python/README.md b/python/README.md new file mode 100644 index 0000000..db8f23b --- /dev/null +++ b/python/README.md @@ -0,0 +1,106 @@ +# ittapi + +ittapi is a Python binding to Intel Instrumentation and Tracing Technology (ITT) API. It provides a convenient way +to mark up the Python code for further performance analysis using performance analyzers from Intel like Intel VTune +or others. + +ittapi supports following ITT APIs: + - Collection Control API + - Domain API + - Event API + - Id API + - String Handle API + - Task API + - Thread Naming API + +## Usage + +The main goal of the project is to provide the ability to instrument a Python code using ITT API in the Pythonic way. +ittapi provides wrappers that simplify markup of Python code. + +```python +import ittapi + +@ittapi.task +def workload(): + pass + +workload() +``` + +`ittapi.task` can be used as a decorator. In this case, the name of a callable object (`workload` function in this +example) will be used as a name of the task and the task will be attributed to a default domain named 'ittapi'. +If you want to change the default name and/or other parameters for the task (e.g. task domain), you can pass +them as arguments to `ittapi.task`: + +```python +import ittapi + +@ittapi.task('My Task', domain='My Task Domain') +def workload(): + pass + +workload() +``` + +Also, `ittapi.task` returns the object that can be used as a context manager: + +```python +import ittapi + +with ittapi.task(): + # some code here... + pass +``` + +If the task name is not specified, the `ittapi.task` uses call site information (filename and line number) to give +the name to the task. A custom name for the task and other task parameters can be specified via arguments +for `ittapi.task` in the same way as for the decorator form. + +## Installation + +[TODO] intel-ittapi package is available on PyPi and can be installed in the usual way for the supported configurations: + +[TODO] pip install intel-ittapi + +## Build + +The native part of ittapi module is written using C++20 standard, therefore you need a compiler that supports this +standard, for example GCC-10 for Linux and Visual Studio 2022 for Windows. + +### Ubuntu 22.04 + +1. Install the compiler and Python utilities to build module: + + sudo apt install gcc g++ python3-pip + +2. Clone the repository: + + git clone https://github.com/intel/ittapi.git + +3. Build and install ittapi: + + cd python + pip install . + +### Windows 10/11 + +1. Install [Python 3.8+](https://www.python.org/downloads/) together with pip utility. + +2. Install [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/). + Make sure that "Desktop development with C++" workload is selected. + +3. Clone the repository + + git clone https://github.com/intel/ittapi.git + +4. Build and install ittapi + + cd python + pip install . + +## References + + - [Instrumentation and Tracing Technology APIs](https://www.intel.com/content/www/us/en/docs/vtune-profiler/user-guide/2023-0/instrumentation-and-tracing-technology-apis.html) + - [Intel® VTune™ Profiler User Guide - Task Analysis](https://www.intel.com/content/www/us/en/docs/vtune-profiler/user-guide/2023-0/task-analysis.html) + - [Intel® Graphics Performance Analyzers User Guide - Instrumentation and Tracing Technology API Support](https://www.intel.com/content/www/us/en/docs/gpa/user-guide/2022-4/instrumentation-and-tracing-technology-apis.html) diff --git a/python/ittapi.native/collection_control.cpp b/python/ittapi.native/collection_control.cpp new file mode 100644 index 0000000..965ece4 --- /dev/null +++ b/python/ittapi.native/collection_control.cpp @@ -0,0 +1,33 @@ +#include "collection_control.hpp" + +#include + + +namespace ittapi +{ + +PyObject* pause(PyObject* self, PyObject* Py_UNUSED(args)) +{ + Py_BEGIN_ALLOW_THREADS; + __itt_pause(); + Py_END_ALLOW_THREADS; + Py_RETURN_NONE; +} + +PyObject* resume(PyObject* self, PyObject* Py_UNUSED(args)) +{ + Py_BEGIN_ALLOW_THREADS; + __itt_resume(); + Py_END_ALLOW_THREADS; + Py_RETURN_NONE; +} + +PyObject* detach(PyObject* self, PyObject* Py_UNUSED(args)) +{ + Py_BEGIN_ALLOW_THREADS; + __itt_detach(); + Py_END_ALLOW_THREADS; + Py_RETURN_NONE; +} + +} // namespace ittapi \ No newline at end of file diff --git a/python/ittapi.native/collection_control.hpp b/python/ittapi.native/collection_control.hpp new file mode 100644 index 0000000..e607f32 --- /dev/null +++ b/python/ittapi.native/collection_control.hpp @@ -0,0 +1,14 @@ +#pragma once + +#define PY_SSIZE_T_CLEAN +#include + + +namespace ittapi +{ + +PyObject* pause(PyObject* self, PyObject* args); +PyObject* resume(PyObject* self, PyObject* args); +PyObject* detach(PyObject* self, PyObject* args); + +} // namespace ittapi \ No newline at end of file diff --git a/python/ittapi.native/domain.cpp b/python/ittapi.native/domain.cpp new file mode 100644 index 0000000..2295e04 --- /dev/null +++ b/python/ittapi.native/domain.cpp @@ -0,0 +1,235 @@ +#include "domain.hpp" + +#include + +#include "string_handle.hpp" +#include "extensions/string.hpp" + + +namespace ittapi +{ + +template +T* domain_cast(Domain* self); + +template<> +PyObject* domain_cast(Domain* self) +{ + return reinterpret_cast(self); +} + +static PyObject* domain_new(PyTypeObject* type, PyObject* args, PyObject* kwargs); +static void domain_dealloc(PyObject* self); + +static PyObject* domain_repr(PyObject* self); +static PyObject* domain_str(PyObject* self); + +static PyMemberDef domain_attrs[] = +{ + {"name", T_OBJECT, offsetof(Domain, name), READONLY, "a domain name"}, + {nullptr}, +}; + +PyTypeObject DomainType = +{ + .ob_base = PyVarObject_HEAD_INIT(nullptr, 0) + .tp_name = "ittapi.native.Domain", + .tp_basicsize = sizeof(Domain), + .tp_itemsize = 0, + + /* Methods to implement standard operations */ + .tp_dealloc = domain_dealloc, + .tp_vectorcall_offset = 0, + .tp_getattr = nullptr, + .tp_setattr = nullptr, + .tp_as_async = nullptr, + .tp_repr = domain_repr, + + /* Method suites for standard classes */ + .tp_as_number = nullptr, + .tp_as_sequence = nullptr, + .tp_as_mapping = nullptr, + + /* More standard operations (here for binary compatibility) */ + .tp_hash = nullptr, + .tp_call = nullptr, + .tp_str = domain_str, + .tp_getattro = nullptr, + .tp_setattro = nullptr, + + /* Functions to access object as input/output buffer */ + .tp_as_buffer = nullptr, + + /* Flags to define presence of optional/expanded features */ + .tp_flags = Py_TPFLAGS_DEFAULT, + + /* Documentation string */ + .tp_doc = "A class that represents a ITT domain.", + + /* Assigned meaning in release 2.0 call function for all accessible objects */ + .tp_traverse = nullptr, + + /* Delete references to contained objects */ + .tp_clear = nullptr, + + /* Assigned meaning in release 2.1 rich comparisons */ + .tp_richcompare = nullptr, + + /* weak reference enabler */ + .tp_weaklistoffset = 0, + + /* Iterators */ + .tp_iter = nullptr, + .tp_iternext = nullptr, + + /* Attribute descriptor and subclassing stuff */ + .tp_methods = nullptr, + .tp_members = domain_attrs, + .tp_getset = nullptr, + + /* Strong reference on a heap type, borrowed reference on a static type */ + .tp_base = nullptr, + .tp_dict = nullptr, + .tp_descr_get = nullptr, + .tp_descr_set = nullptr, + .tp_dictoffset = 0, + .tp_init = nullptr, + .tp_alloc = nullptr, + .tp_new = domain_new, + + /* Low-level free-memory routine */ + .tp_free = nullptr, + + /* For PyObject_IS_GC */ + .tp_is_gc = nullptr, + .tp_bases = nullptr, + + /* method resolution order */ + .tp_mro = nullptr, + .tp_cache = nullptr, + .tp_subclasses = nullptr, + .tp_weaklist = nullptr, + .tp_del = nullptr, + + /* Type attribute cache version tag. Added in version 2.6 */ + .tp_version_tag = 0, + + .tp_finalize = nullptr, + .tp_vectorcall = nullptr, +}; + +static PyObject* domain_new(PyTypeObject* type, PyObject* args, PyObject* kwargs) +{ + Domain* self = domain_obj(type->tp_alloc(type, 0)); + if (self == nullptr) + { + return nullptr; + } + + char name_key[] = { "name" }; + char* kwlist[] = { name_key, nullptr }; + + PyObject* name = nullptr; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O", kwlist, &name)) + { + return nullptr; + } + + if (name == nullptr || name == Py_None) + { + self->name = PyUnicode_FromString("ittapi"); + } + else if (PyUnicode_Check(name)) + { + self->name = pyext::new_ref(name); + } + else if (Py_TYPE(name) == &StringHandleType) + { + self->name = pyext::new_ref(string_handle_obj(name)->str); + } + else + { + Py_DecRef(domain_cast(self)); + + PyErr_SetString(PyExc_TypeError, "The passed domain name is not a valid instance of str or StringHandle."); + return nullptr; + } + + pyext::string name_str = pyext::string::from_unicode(self->name); + if (name_str.c_str() == nullptr) + { + Py_DecRef(domain_cast(self)); + return nullptr; + } + +#if defined(_WIN32) + self->handle = __itt_domain_createW(name_str.c_str()); +#else + self->handle = __itt_domain_create(name_str.c_str()); +#endif + + return domain_cast(self); +} + +static void domain_dealloc(PyObject* self) +{ + if (self == nullptr) + { + return; + } + + Domain* obj = domain_obj(self); + Py_XDECREF(obj->name); +} + +static PyObject* domain_repr(PyObject* self) +{ + Domain* obj = domain_check(self); + if (obj == nullptr) + { + return nullptr; + } + + if (obj->name == nullptr) + { + PyErr_SetString(PyExc_AttributeError, "The name attribute has not been initialized."); + return nullptr; + } + + return PyUnicode_FromFormat("%s('%U')", DomainType.tp_name, obj->name); +} + +static PyObject* domain_str(PyObject* self) +{ + Domain* obj = domain_check(self); + if (obj == nullptr) + { + return nullptr; + } + + if (obj->name == nullptr) + { + PyErr_SetString(PyExc_AttributeError, "The name attribute has not been initialized."); + return nullptr; + } + + return pyext::new_ref(obj->name); +} + +Domain* domain_check(PyObject* self) +{ + if (self == nullptr || Py_TYPE(self) != &DomainType) + { + PyErr_SetString(PyExc_TypeError, "The passed domain is not a valid instance of Domain type."); + return nullptr; + } + + return domain_obj(self); +} + +int exec_domain(PyObject* module) +{ + return pyext::add_type(module, &DomainType); +} + +} // namespace ittapi diff --git a/python/ittapi.native/domain.hpp b/python/ittapi.native/domain.hpp new file mode 100644 index 0000000..a56b269 --- /dev/null +++ b/python/ittapi.native/domain.hpp @@ -0,0 +1,34 @@ +#pragma once + +#define PY_SSIZE_T_CLEAN +#include + +#include + +#include "extensions/python.hpp" + + +namespace ittapi +{ + +struct Domain +{ + PyObject_HEAD + PyObject* name; + __itt_domain* handle; +}; + +extern PyTypeObject DomainType; + +inline Domain* domain_obj(PyObject* self); +Domain* domain_check(PyObject* self); +int exec_domain(PyObject* module); + + +/* Implementation of inline functions */ +Domain* domain_obj(PyObject* self) +{ + return pyext::pyobject_cast(self); +} + +} // namespace ittapi diff --git a/python/ittapi.native/event.cpp b/python/ittapi.native/event.cpp new file mode 100644 index 0000000..eadc99f --- /dev/null +++ b/python/ittapi.native/event.cpp @@ -0,0 +1,263 @@ +#include "event.hpp" + +#include + +#include "string_handle.hpp" +#include "extensions/string.hpp" + + +namespace ittapi +{ + +template +T* event_cast(Event* self); + +template<> +PyObject* event_cast(Event* self) +{ + return reinterpret_cast(self); +} + +static PyObject* event_new(PyTypeObject* type, PyObject* args, PyObject* kwargs); +static void event_dealloc(PyObject* self); + +static PyObject* event_repr(PyObject* self); +static PyObject* event_str(PyObject* self); + +static PyObject* event_begin(PyObject* self, PyObject* args); +static PyObject* event_end(PyObject* self, PyObject* args); + +static PyMemberDef event_attrs[] = +{ + {"name", T_OBJECT_EX, offsetof(Event, name), READONLY, "a name of the event"}, + {nullptr}, +}; + +static PyMethodDef event_methods[] = +{ + {"begin", event_begin, METH_NOARGS, "Marks the beginning of the event."}, + {"end", event_end, METH_NOARGS, "Marks the end of the event."}, + {nullptr}, +}; + +PyTypeObject EventType = +{ + .ob_base = PyVarObject_HEAD_INIT(nullptr, 0) + .tp_name = "ittapi.native.Event", + .tp_basicsize = sizeof(Event), + .tp_itemsize = 0, + + /* Methods to implement standard operations */ + .tp_dealloc = event_dealloc, + .tp_vectorcall_offset = 0, + .tp_getattr = nullptr, + .tp_setattr = nullptr, + .tp_as_async = nullptr, + .tp_repr = event_repr, + + /* Method suites for standard classes */ + .tp_as_number = nullptr, + .tp_as_sequence = nullptr, + .tp_as_mapping = nullptr, + + /* More standard operations (here for binary compatibility) */ + .tp_hash = nullptr, + .tp_call = nullptr, + .tp_str = event_str, + .tp_getattro = nullptr, + .tp_setattro = nullptr, + + /* Functions to access object as input/output buffer */ + .tp_as_buffer = nullptr, + + /* Flags to define presence of optional/expanded features */ + .tp_flags = Py_TPFLAGS_DEFAULT, + + /* Documentation string */ + .tp_doc = "A class that represents a ITT event.", + + /* Assigned meaning in release 2.0 call function for all accessible objects */ + .tp_traverse = nullptr, + + /* Delete references to contained objects */ + .tp_clear = nullptr, + + /* Assigned meaning in release 2.1 rich comparisons */ + .tp_richcompare = nullptr, + + /* weak reference enabler */ + .tp_weaklistoffset = 0, + + /* Iterators */ + .tp_iter = nullptr, + .tp_iternext = nullptr, + + /* Attribute descriptor and subclassing stuff */ + .tp_methods = event_methods, + .tp_members = event_attrs, + .tp_getset = nullptr, + + /* Strong reference on a heap type, borrowed reference on a static type */ + .tp_base = nullptr, + .tp_dict = nullptr, + .tp_descr_get = nullptr, + .tp_descr_set = nullptr, + .tp_dictoffset = 0, + .tp_init = nullptr, + .tp_alloc = nullptr, + .tp_new = event_new, + + /* Low-level free-memory routine */ + .tp_free = nullptr, + + /* For PyObject_IS_GC */ + .tp_is_gc = nullptr, + .tp_bases = nullptr, + + /* method resolution order */ + .tp_mro = nullptr, + .tp_cache = nullptr, + .tp_subclasses = nullptr, + .tp_weaklist = nullptr, + .tp_del = nullptr, + + /* Type attribute cache version tag. Added in version 2.6 */ + .tp_version_tag = 0, + + .tp_finalize = nullptr, + .tp_vectorcall = nullptr, +}; + +static PyObject* event_new(PyTypeObject* type, PyObject* args, PyObject* kwargs) +{ + Event* self = event_obj(type->tp_alloc(type, 0)); + + if (self == nullptr) + { + return nullptr; + } + + char name_key[] = { "name" }; + char* kwlist[] = { name_key, nullptr }; + + PyObject* name = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &name)) + { + return nullptr; + } + + if (name && PyUnicode_Check(name)) + { + self->name = pyext::new_ref(name); + } + else if (name && Py_TYPE(name) == &StringHandleType) + { + self->name = pyext::new_ref(string_handle_obj(name)->str); + } + else + { + Py_DecRef(event_cast(self)); + + PyErr_SetString(PyExc_TypeError, "The passed event name is not a valid instance of str or StringHandle."); + return nullptr; + } + + pyext::string name_str = pyext::string::from_unicode(self->name); + if (name_str.c_str() == nullptr) + { + Py_DecRef(event_cast(self)); + return nullptr; + } + +#if defined(_WIN32) + self->event = __itt_event_createW(name_str.c_str(), static_cast(name_str.length())); +#else + self->event = __itt_event_create(name_str.c_str(), static_cast(name_str.length())); +#endif + + return event_cast(self); +} + +static void event_dealloc(PyObject* self) +{ + Event* obj = event_obj(self); + if (obj == nullptr) + { + return; + } + + Py_XDECREF(obj->name); +} + +static PyObject* event_repr(PyObject* self) +{ + Event* obj = event_check(self); + if (obj == nullptr) + { + return nullptr; + } + + return PyUnicode_FromFormat("%s('%U')", EventType.tp_name, obj->name); +} + +static PyObject* event_str(PyObject* self) +{ + Event* obj = event_check(self); + if (obj == nullptr) + { + return nullptr; + } + + if (obj->name == nullptr) + { + PyErr_SetString(PyExc_AttributeError, "The name attribute has not been initialized."); + return nullptr; + } + + return pyext::new_ref(obj->name); +} + +static PyObject* event_begin(PyObject* self, PyObject* args) +{ + Event* obj = event_check(self); + if (obj == nullptr) + { + return nullptr; + } + + __itt_event_start(obj->event); + + Py_RETURN_NONE; +} + +static PyObject* event_end(PyObject* self, PyObject* args) +{ + Event* obj = event_check(self); + if (obj == nullptr) + { + return nullptr; + } + + __itt_event_end(obj->event); + + Py_RETURN_NONE; +} + +Event* event_check(PyObject* self) +{ + if (self == nullptr || Py_TYPE(self) != &EventType) + { + PyErr_SetString(PyExc_TypeError, "The passed event is not a valid instance of Event type."); + return nullptr; + } + + return event_obj(self); +} + +int exec_event(PyObject* module) +{ + return pyext::add_type(module, &EventType); +} + +} // namespace ittapi diff --git a/python/ittapi.native/event.hpp b/python/ittapi.native/event.hpp new file mode 100644 index 0000000..aa87674 --- /dev/null +++ b/python/ittapi.native/event.hpp @@ -0,0 +1,34 @@ +#pragma once + +#define PY_SSIZE_T_CLEAN +#include + +#include + +#include "extensions/python.hpp" + + +namespace ittapi +{ + +struct Event +{ + PyObject_HEAD + PyObject* name; + __itt_event event; +}; + +extern PyTypeObject EventType; + +inline Event* event_obj(PyObject* self); +Event* event_check(PyObject* self); +int exec_event(PyObject* module); + + +/* Implementation of inline functions */ +Event* event_obj(PyObject* self) +{ + return pyext::pyobject_cast(self); +} + +} // namespace ittapi diff --git a/python/ittapi.native/extensions/python.cpp b/python/ittapi.native/extensions/python.cpp new file mode 100644 index 0000000..a63aded --- /dev/null +++ b/python/ittapi.native/extensions/python.cpp @@ -0,0 +1,23 @@ +#include "python.hpp" + + +namespace ittapi +{ +namespace pyext +{ + +int add_type(PyObject* module, PyTypeObject* type) +{ + if (PyType_Ready(type) < 0) + { + return -1; + } + + const char* name = _PyType_Name(type); + + Py_INCREF(type); + return PyModule_AddObject(module, name, _PyObject_CAST(type)); +} + +} // namespace pyext +} // namespace ittapi \ No newline at end of file diff --git a/python/ittapi.native/extensions/python.hpp b/python/ittapi.native/extensions/python.hpp new file mode 100644 index 0000000..32f21ab --- /dev/null +++ b/python/ittapi.native/extensions/python.hpp @@ -0,0 +1,38 @@ +#pragma once + +#define PY_SSIZE_T_CLEAN +#include + + +namespace ittapi +{ +namespace pyext +{ + +template +T* pyobject_cast(PyObject* self) +{ + return reinterpret_cast(self); +} + +inline PyObject* new_ref(PyObject* obj); +inline PyObject* xnew_ref(PyObject* obj); + +int add_type(PyObject* module, PyTypeObject* type); + + +/* Implementation of inline functions */ +PyObject* new_ref(PyObject* obj) +{ + Py_INCREF(obj); + return obj; +} + +PyObject* xnew_ref(PyObject* obj) +{ + Py_XINCREF(obj); + return obj; +} + +} // namespace pyext +} // namespace ittapi \ No newline at end of file diff --git a/python/ittapi.native/extensions/string.cpp b/python/ittapi.native/extensions/string.cpp new file mode 100644 index 0000000..56a756c --- /dev/null +++ b/python/ittapi.native/extensions/string.cpp @@ -0,0 +1,36 @@ +#include "string.hpp" + + +namespace ittapi +{ +namespace pyext +{ + +string::~string() +{ + if (m_is_owner) + { + PyMem_Free(const_cast(m_str)); + } +} + +string string::from_unicode(PyObject* str) +{ + if (!PyUnicode_Check(str)) + { + return string(nullptr, false); + } + +#if defined(_WIN32) + pointer str_ptr = PyUnicode_AsWideCharString(str, nullptr); + const bool is_owner = true; +#else + const_pointer str_ptr = PyUnicode_AsUTF8(str); + const bool is_owner = false; +#endif + + return string(str_ptr, is_owner); +} + +} // namespace pyext +} // namespace ittapi diff --git a/python/ittapi.native/extensions/string.hpp b/python/ittapi.native/extensions/string.hpp new file mode 100644 index 0000000..e94d4f1 --- /dev/null +++ b/python/ittapi.native/extensions/string.hpp @@ -0,0 +1,66 @@ +#pragma once + +#define PY_SSIZE_T_CLEAN +#include + +#if defined(_WIN32) +#include +#else +#include +#endif + + +namespace ittapi +{ +namespace pyext +{ + +class string +{ +public: +#if defined(_WIN32) + using value_type = wchar_t; +#else + using value_type = char; +#endif + + using reference = value_type&; + using const_reference = const value_type&; + using pointer = value_type*; + using const_pointer = const value_type*; + + ~string(); + + inline const_pointer c_str() const; + inline std::size_t length() const; + + static string from_unicode(PyObject* str); + +private: + inline string(const_pointer str, bool take_ownership); + + const_pointer m_str; + bool m_is_owner; +}; + +string::string(const_pointer str, bool take_ownership) + : m_str(str) + , m_is_owner(take_ownership) +{} + +string::const_pointer string::c_str() const +{ + return m_str; +} + +std::size_t string::length() const +{ +#if defined(_WIN32) + return std::wcslen(c_str()); +#else + return std::strlen(c_str()); +#endif +} + +} // namespace pyext +} // namespace ittapi \ No newline at end of file diff --git a/python/ittapi.native/id.cpp b/python/ittapi.native/id.cpp new file mode 100644 index 0000000..fbf9920 --- /dev/null +++ b/python/ittapi.native/id.cpp @@ -0,0 +1,208 @@ +#include "id.hpp" + +#include + +#include "domain.hpp" +#include "extensions/string.hpp" + + +namespace ittapi +{ + +template +T* id_cast(Id* self); + +template<> +PyObject* id_cast(Id* self) +{ + return reinterpret_cast(self); +} + +static PyObject* id_new(PyTypeObject* type, PyObject* args, PyObject* kwargs); +static void id_dealloc(PyObject* self); + +static PyObject* id_repr(PyObject* self); +static PyObject* id_str(PyObject* self); + +static PyMemberDef id_attrs[] = +{ + {"domain", T_OBJECT_EX, offsetof(Id, domain), READONLY, "a domain that controls the creation and destruction of the identifier"}, + {nullptr}, +}; + +PyTypeObject IdType = +{ + .ob_base = PyVarObject_HEAD_INIT(nullptr, 0) + .tp_name = "ittapi.native.Id", + .tp_basicsize = sizeof(Id), + .tp_itemsize = 0, + + /* Methods to implement standard operations */ + .tp_dealloc = id_dealloc, + .tp_vectorcall_offset = 0, + .tp_getattr = nullptr, + .tp_setattr = nullptr, + .tp_as_async = nullptr, + .tp_repr = id_repr, + + /* Method suites for standard classes */ + .tp_as_number = nullptr, + .tp_as_sequence = nullptr, + .tp_as_mapping = nullptr, + + /* More standard operations (here for binary compatibility) */ + .tp_hash = nullptr, + .tp_call = nullptr, + .tp_str = id_str, + .tp_getattro = nullptr, + .tp_setattro = nullptr, + + /* Functions to access object as input/output buffer */ + .tp_as_buffer = nullptr, + + /* Flags to define presence of optional/expanded features */ + .tp_flags = Py_TPFLAGS_DEFAULT, + + /* Documentation string */ + .tp_doc = "A class that represents a ITT id.", + + /* Assigned meaning in release 2.0 call function for all accessible objects */ + .tp_traverse = nullptr, + + /* Delete references to contained objects */ + .tp_clear = nullptr, + + /* Assigned meaning in release 2.1 rich comparisons */ + .tp_richcompare = nullptr, + + /* weak reference enabler */ + .tp_weaklistoffset = 0, + + /* Iterators */ + .tp_iter = nullptr, + .tp_iternext = nullptr, + + /* Attribute descriptor and subclassing stuff */ + .tp_methods = nullptr, + .tp_members = id_attrs, + .tp_getset = nullptr, + + /* Strong reference on a heap type, borrowed reference on a static type */ + .tp_base = nullptr, + .tp_dict = nullptr, + .tp_descr_get = nullptr, + .tp_descr_set = nullptr, + .tp_dictoffset = 0, + .tp_init = nullptr, + .tp_alloc = nullptr, + .tp_new = id_new, + + /* Low-level free-memory routine */ + .tp_free = nullptr, + + /* For PyObject_IS_GC */ + .tp_is_gc = nullptr, + .tp_bases = nullptr, + + /* method resolution order */ + .tp_mro = nullptr, + .tp_cache = nullptr, + .tp_subclasses = nullptr, + .tp_weaklist = nullptr, + .tp_del = nullptr, + + /* Type attribute cache version tag. Added in version 2.6 */ + .tp_version_tag = 0, + + .tp_finalize = nullptr, + .tp_vectorcall = nullptr, +}; + +static PyObject* id_new(PyTypeObject* type, PyObject* args, PyObject* kwargs) +{ + Id* self = id_obj(type->tp_alloc(type, 0)); + + if (self == nullptr) + { + return nullptr; + } + + char domain_key[] = { "domain" }; + char* kwlist[] = { domain_key, nullptr }; + + PyObject* domain = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &domain)) + { + return nullptr; + } + + Domain* domain_obj = domain_check(domain); + if (domain_obj == nullptr) + { + return nullptr; + } + + self->domain = pyext::new_ref(domain); + self->id = __itt_id_make(self, 0); + + __itt_id_create(domain_obj->handle, self->id); + + return id_cast(self); +} + +static void id_dealloc(PyObject* self) +{ + if (self == nullptr) + { + return; + } + + Id* obj = id_obj(self); + if (obj->domain) + { + __itt_id_destroy(domain_obj(obj->domain)->handle, obj->id); + } + + Py_XDECREF(obj->domain); +} + +static PyObject* id_repr(PyObject* self) +{ + Id* obj = id_check(self); + if (obj == nullptr) + { + return nullptr; + } + + return PyUnicode_FromFormat("%s(%llu, %llu)", IdType.tp_name, obj->id.d1, obj->id.d2); +} + +static PyObject* id_str(PyObject* self) +{ + Id* obj = id_check(self); + if (obj == nullptr) + { + return nullptr; + } + + return PyUnicode_FromFormat("(%llu, %llu)", obj->id.d1, obj->id.d2); +} + +Id* id_check(PyObject* self) +{ + if (self == nullptr || Py_TYPE(self) != &IdType) + { + PyErr_SetString(PyExc_TypeError, "The passed id is not a valid instance of Id type."); + return nullptr; + } + + return id_obj(self); +} + +int exec_id(PyObject* module) +{ + return pyext::add_type(module, &IdType); +} + +} // namespace ittapi diff --git a/python/ittapi.native/id.hpp b/python/ittapi.native/id.hpp new file mode 100644 index 0000000..50b0b24 --- /dev/null +++ b/python/ittapi.native/id.hpp @@ -0,0 +1,34 @@ +#pragma once + +#define PY_SSIZE_T_CLEAN +#include + +#include + +#include "extensions/python.hpp" + + +namespace ittapi +{ + +struct Id +{ + PyObject_HEAD + PyObject* domain; + __itt_id id; +}; + +extern PyTypeObject IdType; + +inline Id* id_obj(PyObject* self); +Id* id_check(PyObject* self); +int exec_id(PyObject* module); + + +/* Implementation of inline functions */ +Id* id_obj(PyObject* self) +{ + return pyext::pyobject_cast(self); +} + +} // namespace ittapi diff --git a/python/ittapi.native/ittapi.cpp b/python/ittapi.native/ittapi.cpp new file mode 100644 index 0000000..db8777c --- /dev/null +++ b/python/ittapi.native/ittapi.cpp @@ -0,0 +1,82 @@ +#define PY_SSIZE_T_CLEAN +#include + +#include "collection_control.hpp" +#include "domain.hpp" +#include "event.hpp" +#include "id.hpp" +#include "string_handle.hpp" +#include "task.hpp" +#include "thread_naming.hpp" + + +namespace ittapi +{ + +/** + Initialize ittapi. + May be called multiple times, so avoid using static state. + */ +static int exec_ittapi_module(PyObject* module) +{ + static PyMethodDef ittapi_functions[] = + { + /* Collection Control API */ + {"pause", pause, METH_NOARGS, "Pause data collection."}, + {"resume", resume, METH_NOARGS, "Resume data collection."}, + {"detach", detach, METH_NOARGS, "Detach data collection."}, + /* Thread Naming API */ + {"thread_set_name", thread_set_name, METH_O, "Sets a name for current thread."}, + /* Task API */ + {"task_begin", task_begin, METH_VARARGS, "Marks the beginning of a task."}, + {"task_end", task_end, METH_VARARGS, "Marks the end of a task."}, + {"task_begin_overlapped", task_begin_overlapped, METH_VARARGS, "Marks the beginning of an overlapped task."}, + {"task_end_overlapped", task_end_overlapped, METH_VARARGS, "Marks the end of an overlapped task."}, + /* marks end of array */ + { nullptr }, + }; + + PyModule_AddFunctions(module, ittapi_functions); + + PyModule_AddStringConstant(module, "__author__", "Egor Suldin"); + PyModule_AddStringConstant(module, "__version__", "1.1.0"); + PyModule_AddIntConstant(module, "year", 2024); + + return 0; +} + +static void destroy_ittapi_module(void*) +{ + __itt_release_resources(); +} + +PyMODINIT_FUNC PyInit_native() +{ + PyDoc_STRVAR(ittapi_doc, "The ittapi module."); + + static PyModuleDef_Slot ittapi_slots[] = + { + { Py_mod_exec, reinterpret_cast(exec_ittapi_module) }, + { Py_mod_exec, reinterpret_cast(exec_domain) }, + { Py_mod_exec, reinterpret_cast(exec_event) }, + { Py_mod_exec, reinterpret_cast(exec_id) }, + { Py_mod_exec, reinterpret_cast(exec_string_handle) }, + { 0, nullptr } + }; + + static PyModuleDef ittapi_def = { + PyModuleDef_HEAD_INIT, + "ittapi", + ittapi_doc, + 0, /* m_size */ + nullptr, /* m_methods */ + ittapi_slots, + nullptr, /* m_traverse */ + nullptr, /* m_clear */ + destroy_ittapi_module, /* m_free */ + }; + + return PyModuleDef_Init(&ittapi_def); +} + +} // namespace ittapi diff --git a/python/ittapi.native/string_handle.cpp b/python/ittapi.native/string_handle.cpp new file mode 100644 index 0000000..5bebdd4 --- /dev/null +++ b/python/ittapi.native/string_handle.cpp @@ -0,0 +1,228 @@ +#include "string_handle.hpp" + +#include + +#include "extensions/string.hpp" + + +namespace ittapi +{ + +template +T* string_handle_cast(StringHandle* self); + +template<> +PyObject* string_handle_cast(StringHandle* self) +{ + return reinterpret_cast(self); +} + +static PyObject* string_handle_new(PyTypeObject* type, PyObject* args, PyObject* kwargs); +static void string_handle_dealloc(PyObject* self); + +static PyObject* string_handle_repr(PyObject* self); +static PyObject* string_handle_str(PyObject* self); + +static PyMemberDef string_handle_attrs[] = +{ + {"_str", T_OBJECT, offsetof(StringHandle, str), READONLY, "a string for which the handle has been created"}, + {nullptr}, +}; + +PyTypeObject StringHandleType = +{ + .ob_base = PyVarObject_HEAD_INIT(nullptr, 0) + .tp_name = "ittapi.native.StringHandle", + .tp_basicsize = sizeof(StringHandle), + .tp_itemsize = 0, + + /* Methods to implement standard operations */ + .tp_dealloc = string_handle_dealloc, + .tp_vectorcall_offset = 0, + .tp_getattr = nullptr, + .tp_setattr = nullptr, + .tp_as_async = nullptr, + .tp_repr = string_handle_repr, + + /* Method suites for standard classes */ + .tp_as_number = nullptr, + .tp_as_sequence = nullptr, + .tp_as_mapping = nullptr, + + /* More standard operations (here for binary compatibility) */ + .tp_hash = nullptr, + .tp_call = nullptr, + .tp_str = string_handle_str, + .tp_getattro = nullptr, + .tp_setattro = nullptr, + + /* Functions to access object as input/output buffer */ + .tp_as_buffer = nullptr, + + /* Flags to define presence of optional/expanded features */ + .tp_flags = Py_TPFLAGS_DEFAULT, + + /* Documentation string */ + .tp_doc = "A class that represents a ITT string handle.", + + /* Assigned meaning in release 2.0 call function for all accessible objects */ + .tp_traverse = nullptr, + + /* Delete references to contained objects */ + .tp_clear = nullptr, + + /* Assigned meaning in release 2.1 rich comparisons */ + .tp_richcompare = nullptr, + + /* weak reference enabler */ + .tp_weaklistoffset = 0, + + /* Iterators */ + .tp_iter = nullptr, + .tp_iternext = nullptr, + + /* Attribute descriptor and subclassing stuff */ + .tp_methods = nullptr, + .tp_members = string_handle_attrs, + .tp_getset = nullptr, + + /* Strong reference on a heap type, borrowed reference on a static type */ + .tp_base = nullptr, + .tp_dict = nullptr, + .tp_descr_get = nullptr, + .tp_descr_set = nullptr, + .tp_dictoffset = 0, + .tp_init = nullptr, + .tp_alloc = nullptr, + .tp_new = string_handle_new, + + /* Low-level free-memory routine */ + .tp_free = nullptr, + + /* For PyObject_IS_GC */ + .tp_is_gc = nullptr, + .tp_bases = nullptr, + + /* method resolution order */ + .tp_mro = nullptr, + .tp_cache = nullptr, + .tp_subclasses = nullptr, + .tp_weaklist = nullptr, + .tp_del = nullptr, + + /* Type attribute cache version tag. Added in version 2.6 */ + .tp_version_tag = 0, + + .tp_finalize = nullptr, + .tp_vectorcall = nullptr, +}; + +static PyObject* string_handle_new(PyTypeObject* type, PyObject* args, PyObject* kwargs) +{ + StringHandle* self = string_handle_obj(type->tp_alloc(type, 0)); + + if (self == nullptr) + { + return nullptr; + } + + char str_key[] = { "str" }; + char* kwlist[] = { str_key, nullptr }; + + PyObject* str = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &str)) + { + return nullptr; + } + + if (str && PyUnicode_Check(str)) + { + self->str = pyext::new_ref(str); + } + else + { + Py_DecRef(string_handle_cast(self)); + + PyErr_SetString(PyExc_TypeError, "The passed string to create string handle is not a valid instance of str."); + return nullptr; + } + + pyext::string str_wrapper = pyext::string::from_unicode(self->str); + if (str_wrapper.c_str() == nullptr) + { + Py_DecRef(string_handle_cast(self)); + return nullptr; + } + +#if defined(_WIN32) + self->handle = __itt_string_handle_createW(str_wrapper.c_str()); +#else + self->handle = __itt_string_handle_create(str_wrapper.c_str()); +#endif + + return string_handle_cast(self); +} + +static void string_handle_dealloc(PyObject* self) +{ + if (self == nullptr) + { + return; + } + + StringHandle* obj = string_handle_obj(self); + Py_XDECREF(obj->str); +} + +static PyObject* string_handle_repr(PyObject* self) +{ + StringHandle* obj = string_handle_check(self); + if (obj == nullptr) + { + return nullptr; + } + + if (obj->str == nullptr) + { + PyErr_SetString(PyExc_AttributeError, "The str attribute has not been initialized."); + return nullptr; + } + + return PyUnicode_FromFormat("%s('%U')", StringHandleType.tp_name, obj->str); +} + +static PyObject* string_handle_str(PyObject* self) +{ + StringHandle* obj = string_handle_check(self); + if (obj == nullptr) + { + return nullptr; + } + + if (obj->str == nullptr) + { + PyErr_SetString(PyExc_AttributeError, "The str attribute has not been initialized."); + return nullptr; + } + + return pyext::new_ref(obj->str); +} + +StringHandle* string_handle_check(PyObject* self) +{ + if (self == nullptr || Py_TYPE(self) != &StringHandleType) + { + PyErr_SetString(PyExc_TypeError, "The passed string handle is not a valid instance of StringHandle."); + return nullptr; + } + + return string_handle_obj(self); +} + +int exec_string_handle(PyObject* module) +{ + return pyext::add_type(module, &StringHandleType); +} + +} // namespace ittapi diff --git a/python/ittapi.native/string_handle.hpp b/python/ittapi.native/string_handle.hpp new file mode 100644 index 0000000..9717516 --- /dev/null +++ b/python/ittapi.native/string_handle.hpp @@ -0,0 +1,34 @@ +#pragma once + +#define PY_SSIZE_T_CLEAN +#include + +#include + +#include "extensions/python.hpp" + + +namespace ittapi +{ + +struct StringHandle +{ + PyObject_HEAD + PyObject* str; + __itt_string_handle* handle; +}; + +extern PyTypeObject StringHandleType; + +inline StringHandle* string_handle_obj(PyObject* self); +StringHandle* string_handle_check(PyObject* self); +int exec_string_handle(PyObject* module); + + +/* Implementation of inline functions */ +StringHandle* string_handle_obj(PyObject* self) +{ + return pyext::pyobject_cast(self); +} + +} // namespace ittapi \ No newline at end of file diff --git a/python/ittapi.native/task.cpp b/python/ittapi.native/task.cpp new file mode 100644 index 0000000..41c8c3d --- /dev/null +++ b/python/ittapi.native/task.cpp @@ -0,0 +1,164 @@ +#include "task.hpp" + +#include + +#include "domain.hpp" +#include "id.hpp" +#include "string_handle.hpp" + +namespace ittapi +{ + +PyObject* task_begin(PyObject* self, PyObject* args) +{ + PyObject* domain = nullptr; + PyObject* name_string_handle = nullptr; + PyObject* task_id = nullptr; + PyObject* parent_id = nullptr; + + if (!PyArg_ParseTuple(args, "OO|OO", &domain, &name_string_handle, &task_id, &parent_id)) + { + return nullptr; + } + + Domain* domain_obj = domain_check(domain); + if (domain_obj == nullptr) + { + return nullptr; + } + + StringHandle* name_string_handle_obj = string_handle_check(name_string_handle); + if (name_string_handle_obj == nullptr) + { + return nullptr; + } + + __itt_id id = __itt_null; + if (task_id && task_id != Py_None) + { + Id* task_id_obj = id_check(task_id); + if (task_id_obj == nullptr) + { + return nullptr; + } + + id = task_id_obj->id; + } + + __itt_id p_id = __itt_null; + if (parent_id && parent_id != Py_None) + { + Id* parent_id_obj = id_check(parent_id); + if (parent_id_obj == nullptr) + { + return nullptr; + } + + p_id = parent_id_obj->id; + } + + __itt_task_begin(domain_obj->handle, id, p_id, name_string_handle_obj->handle); + + Py_RETURN_NONE; +} + +PyObject* task_end(PyObject* self, PyObject* args) +{ + + PyObject* domain = nullptr; + + if (!PyArg_ParseTuple(args, "O", &domain)) + { + return nullptr; + } + + Domain* domain_obj = domain_check(domain); + if (domain_obj == nullptr) + { + return nullptr; + } + + __itt_task_end(domain_obj->handle); + + Py_RETURN_NONE; +} + +PyObject* task_begin_overlapped(PyObject* self, PyObject* args) +{ + PyObject* domain = nullptr; + PyObject* name_string_handle = nullptr; + PyObject* task_id = nullptr; + PyObject* parent_id = nullptr; + + if (!PyArg_ParseTuple(args, "OOO|O", &domain, &name_string_handle, &task_id, &parent_id)) + { + return nullptr; + } + + Domain* domain_obj = domain_check(domain); + if (domain_obj == nullptr) + { + return nullptr; + } + + StringHandle* name_string_handle_obj = string_handle_check(name_string_handle); + if (name_string_handle_obj == nullptr) + { + return nullptr; + } + + Id* task_id_obj = id_check(task_id); + if (task_id_obj == nullptr) + { + return nullptr; + } + + __itt_id p_id = __itt_null; + if (parent_id && parent_id != Py_None) + { + Id* parent_id_obj = id_check(parent_id); + if (parent_id_obj == nullptr) + { + return nullptr; + } + + p_id = parent_id_obj->id; + } + + __itt_task_begin_overlapped(domain_obj->handle, + task_id_obj->id, + p_id, + name_string_handle_obj->handle); + + Py_RETURN_NONE; +} + +PyObject* task_end_overlapped(PyObject* self, PyObject* args) +{ + + PyObject* domain = nullptr; + PyObject* task_id = nullptr; + + if (!PyArg_ParseTuple(args, "OO", &domain, &task_id)) + { + return nullptr; + } + + Domain* domain_obj = domain_check(domain); + if (domain_obj == nullptr) + { + return nullptr; + } + + Id* task_id_obj = id_check(task_id); + if (task_id_obj == nullptr) + { + return nullptr; + } + + __itt_task_end_overlapped(domain_obj->handle, task_id_obj->id); + + Py_RETURN_NONE; +} + +} // namespace ittapi \ No newline at end of file diff --git a/python/ittapi.native/task.hpp b/python/ittapi.native/task.hpp new file mode 100644 index 0000000..0ef4066 --- /dev/null +++ b/python/ittapi.native/task.hpp @@ -0,0 +1,15 @@ +#pragma once + +#define PY_SSIZE_T_CLEAN +#include + + +namespace ittapi +{ + +PyObject* task_begin(PyObject* self, PyObject* args); +PyObject* task_end(PyObject* self, PyObject* args); +PyObject* task_begin_overlapped(PyObject* self, PyObject* args); +PyObject* task_end_overlapped(PyObject* self, PyObject* args); + +} // namespace ittapi diff --git a/python/ittapi.native/thread_naming.cpp b/python/ittapi.native/thread_naming.cpp new file mode 100644 index 0000000..5c13fc2 --- /dev/null +++ b/python/ittapi.native/thread_naming.cpp @@ -0,0 +1,45 @@ +#include "collection_control.hpp" + +#include + +#include "string_handle.hpp" + + +namespace ittapi +{ + +PyObject* thread_set_name(PyObject* self, PyObject* name) +{ + if (Py_TYPE(name) == &StringHandleType) + { + name = string_handle_obj(name)->str; + } + else if (!PyUnicode_Check(name)) + { + PyErr_SetString(PyExc_TypeError, "The passed thread name is not a valid instance of str or StringHandle."); + return nullptr; + } + +#if defined(_WIN32) + wchar_t* name_wstr = PyUnicode_AsWideCharString(name, nullptr); + if (name_wstr == nullptr) + { + return nullptr; + } + + __itt_thread_set_nameW(name_wstr); + PyMem_Free(name_wstr); +#else + const char* name_str = PyUnicode_AsUTF8(name); + if (name_str == nullptr) + { + return nullptr; + } + + __itt_thread_set_name(name_str); +#endif + + Py_RETURN_NONE; +} + +} // namespace ittapi \ No newline at end of file diff --git a/python/ittapi.native/thread_naming.hpp b/python/ittapi.native/thread_naming.hpp new file mode 100644 index 0000000..fb2d3d3 --- /dev/null +++ b/python/ittapi.native/thread_naming.hpp @@ -0,0 +1,12 @@ +#pragma once + +#define PY_SSIZE_T_CLEAN +#include + + +namespace ittapi +{ + +PyObject* thread_set_name(PyObject* self, PyObject* args); + +} // namespace ittapi \ No newline at end of file diff --git a/python/ittapi/__init__.py b/python/ittapi/__init__.py new file mode 100644 index 0000000..654b08d --- /dev/null +++ b/python/ittapi/__init__.py @@ -0,0 +1,16 @@ +""" +Python binding to Intel Instrumentation and Tracing Technology (ITT) API. + +This module provides a convenient way to mark up the Python code for further performance analysis using performance +analyzers from Intel like Intel VTune or others. +""" + +from ittapi.native import Domain, Id, StringHandle +from ittapi.native import task_begin, task_end, task_begin_overlapped, task_end_overlapped +from .collection_control import detach, pause, resume, active_region, paused_region, ActiveRegion, PausedRegion +from .event import event, Event +from .domain import domain +from .id import id +from .string_handle import string_handle +from .task import NestedTask, OverlappedTask, task, nested_task, overlapped_task +from .thread_naming import thread_set_name diff --git a/python/ittapi/collection_control.py b/python/ittapi/collection_control.py new file mode 100644 index 0000000..1c1f998 --- /dev/null +++ b/python/ittapi/collection_control.py @@ -0,0 +1,169 @@ +""" +collection_control.py - Python module wrapper for ITT Collection Control API +""" +from ittapi.native import detach as _detach, pause as _pause, resume as _resume + +from .region import _Region + + +class _CollectionRegion(_Region): + """ + An abstract base class that provides common functionality for subclasses that represent paused/resumed collection + regions. + """ + def __init__(self, func=None, activator=None): + """ + Creates a collection region. + :param func: a callable object that represents the collection region, e.g. function. + :param activator: a callable object that determines if the region is active or not. The callable object should + not take any arguments and should return True if the region is active, otherwise should return + False. If the region is active, _CollectionRegion performs _begin()/_end() calls to inform + subclasses about the start and end of the region. Otherwise, it does nothing. + """ + super().__init__(func) + self.activator = activator + self.__is_paired_call_needed = False + + def _begin(self): + raise NotImplementedError() + + def _end(self): + raise NotImplementedError() + + def begin(self): + """Marks the beginning of a collection region.""" + if callable(self.activator): + activator_state = self.activator() + self.__is_paired_call_needed = activator_state + + if activator_state: + self._begin() + else: + self.__is_paired_call_needed = True + self._begin() + + def end(self): + """Marks the end of a collection region.""" + if self.__is_paired_call_needed: + self._end() + + +def detach() -> None: + """Detach collection of profiling data.""" + _detach() + + +def pause() -> None: + """Pause collection of profiling data.""" + _pause() + + +def resume() -> None: + """Resume collection of profiling data.""" + _resume() + + +class ManualCollectionRegionActivator: + """ + A class that provides ability to activate/deactivate paused/resumed regions. + """ + INACTIVE = 0 + ACTIVE = 1 + + def __init__(self, state=ACTIVE): + """ + Creates an activator. + :param state: sets the initial state of activator. + """ + self._state = state + + def __call__(self): + return self._state == self.ACTIVE + + def activate(self): + """Activates the region.""" + self._state = self.ACTIVE + + def deactivate(self): + """Deactivates the region.""" + self._state = self.INACTIVE + + +class ActiveRegion(_CollectionRegion): + """ + A class that represents resumed collection region. + + It allows to collect profiling only for this region. The collection of profiling data have to be run in + Start Paused mode. + """ + def __init__(self, func=None, activator=ManualCollectionRegionActivator()): + """ + Creates an instance of the class that represents resumed collection region. + :param func: a callable object that represents the collection region, e.g. function. + :param activator: a callable object that determines if the region is active or not. The callable object should + not take any arguments and should return True if the region is active, otherwise should return + False. If the region is active, a call of begin() method of the instance will resume + the collection of profiling data and a call of end() method will pause the collection again. + Otherwise, these calls do nothing. + """ + super().__init__(func, activator) + + def _begin(self): + resume() + + def _end(self): + pause() + + +def active_region(func=None, activator=ManualCollectionRegionActivator()): + """ + Creates a resumed collection region with the given arguments. + :param func: a callable object that represents the collection region, e.g. function. + :param activator: a callable object that determines if the region is active or not. The callable object should + not take any arguments and should return True if the region is active, otherwise should return + False. If the region is active, a call of begin() method of the instance will resume + the collection of profiling data and a call of end() method will pause the collection again. + Otherwise, these calls do nothing. + :return: an instance of ActiveRegion. + """ + return ActiveRegion(func, activator) + + +class PausedRegion(_CollectionRegion): + """ + A class that represents paused collection region. + + An instance of this class allows to disable the collection of profiling data for the code region that is not + interested. + """ + def __init__(self, func=None, activator=ManualCollectionRegionActivator()): + """ + Creates an instance of the class that represents paused collection region. + :param func: a callable object that represents the collection region, e.g. function. + :param activator: a callable object that determines if the region is active or not. The callable object should + not take any arguments and should return True if the region is active, otherwise should return + False. If the region is active, a call of begin() method for the instance will pause + the collection of profiling data and a call of end() method will resume the collection again. + Otherwise, these calls do nothing. + """ + super().__init__(func, activator) + + def _begin(self): + pause() + + def _end(self): + resume() + + +def paused_region(func=None, activator=ManualCollectionRegionActivator()): + """ + Creates a paused collection region with the given arguments. + :param func: a callable object that represents the collection region, e.g. function. + :param activator: a callable object that determines if the region is active or not. The callable object should + not take any arguments and should return True if the region is active, otherwise should return + False. If the region is active, a call of begin() method for the instance will pause + the collection of profiling data and a call of end() method will resume the collection again. + Otherwise, these calls do nothing. + :return: an instance of PausedRegion. + """ + return PausedRegion(func, activator) diff --git a/python/ittapi/domain.py b/python/ittapi/domain.py new file mode 100644 index 0000000..28dac12 --- /dev/null +++ b/python/ittapi/domain.py @@ -0,0 +1,14 @@ +""" +domain.py - Python module wrapper for ITT Domain API +""" +from ittapi.native import Domain as _Domain + + +def domain(name=None): + """ + Creates a domain with the given name. + :param name: a name of the domain + :return: a handle to the created domain with given name if the `name` is not None, + otherwise, returns handle to default domain. + """ + return _Domain(name) diff --git a/python/ittapi/event.py b/python/ittapi/event.py new file mode 100644 index 0000000..37a7c79 --- /dev/null +++ b/python/ittapi/event.py @@ -0,0 +1,46 @@ +""" +event.py - Python module wrapper for ITT Event API +""" +from functools import partial as _partial + +from ittapi.native import Event as _Event + +from .region import _CallSite, _NamedRegion + + +class Event(_NamedRegion): + """ + A class that represents Event. + """ + def __init__(self, region=None): + """ + Creates the instance of the class that represents ITT Event. + :param region: a name of the event or a callable object (e.g. function) to wrap. If the callable object is + passed the name of this object is used as a name for the event. + """ + self._event = None + + super().__init__(region, _partial(Event.__deferred_event_creation, self)) + + def __deferred_event_creation(self, name) -> None: + """Performs deferred creation of native Event.""" + self._event = _Event(name) + + def begin(self) -> None: + """Marks the beginning of an event region.""" + self._event.begin() + + def end(self) -> None: + """Marks the end of an event region.""" + self._event.end() + + +def event(region=None) -> Event: + """ + Creates an Event instance. + :param region: a name of the event or a callable object (e.g. function) to wrap. If the callable object is + passed the name of this object is used as a name for the event. + :return: an Event instance + """ + region = _CallSite(_CallSite.CallerFrame) if region is None else region + return Event(region) diff --git a/python/ittapi/id.py b/python/ittapi/id.py new file mode 100644 index 0000000..4c98d84 --- /dev/null +++ b/python/ittapi/id.py @@ -0,0 +1,13 @@ +""" +id.py - Python module wrapper for ITT ID API +""" +from ittapi.native import Id as _Id + + +def id(domain): + """ + Creates a unique identifier. + :param domain: a domain that controls the creation of the identifier + :return: an instance of the identifier + """ + return _Id(domain) diff --git a/python/ittapi/region.py b/python/ittapi/region.py new file mode 100644 index 0000000..46aee21 --- /dev/null +++ b/python/ittapi/region.py @@ -0,0 +1,240 @@ +""" +region.py - Python module wrapper for code region +""" +from functools import partial as _partial, wraps as _wraps +from inspect import stack as _stack +from os.path import basename as _basename + +from .string_handle import string_handle as _string_handle + + +class _Region: + """ + An abstract base class that provides common functionality to wrap a code region. + + The subclasses itself and instances of the subclasses can be used as a context manager or as a decorator + to automatically track the execution of code region and callable objects (e.g. function). + """ + def __init__(self, func=None, deferred_wrap_callback=None) -> None: + """ + Creates the instance of class that represents a traced code region. + :param func: a callable object to wrap. If it is None, a wrapper creation will be deferred and can be done + using __call__() function for the object. + :param deferred_wrap_callback: a callable object that will be called when wrapper is created if func is None. + """ + self.__function = func + self.__deferred_wrap_callback_function = deferred_wrap_callback + + if self.__function is None: + self.__call_target = self.__wrap + elif callable(self.__function): + self.__call_target = self.__get_wrapper(self.__function) + _wraps(self.__function, updated=())(self) + else: + raise TypeError('func must be a callable object or None.') + + def __get__(self, obj, objtype): + return _wraps(self)(self.__get_wrapper(self.__function, obj)) + + def __enter__(self) -> None: + self.begin() + + def __exit__(self, *args) -> None: + self.end() + + def __call__(self, *args, **kwargs): + return self.__call_target(*args, **kwargs) + + def begin(self) -> None: + """Marks the beginning of a code region.""" + raise NotImplementedError() + + def end(self) -> None: + """Marks the end of a code region.""" + raise NotImplementedError() + + def __wrap(self, func): + """ + Wraps a callable object. + :param func: a callable object to wrap + :return: a wrapper to trace the execution of the callable object + """ + if callable(func): + self.__function = func + else: + raise TypeError('Callable object is expected as a first argument.') + + self.__call_wrap_callback() + + return _wraps(self.__function)(self.__get_wrapper(self.__function)) + + def __call_wrap_callback(self): + """ + Call a callback for wrapper creation. + """ + if callable(self.__deferred_wrap_callback_function): + self.__deferred_wrap_callback_function(self.__function) + + def __get_wrapper(self, func, obj=None): + """ + Returns a pure wrapper for a callable object. + :param func: the callable object to wrap + :param obj: an object to which the callable object is bound + :return: the wrapper to trace the execution of the callable object + """ + if not callable(func): + raise TypeError('Callable object is expected to be passed.') + + def _function_wrapper(*args, **kwargs): + """ + A wrapper to trace the execution of a callable object + :param args: positional arguments of the callable object + :param kwargs: keyword arguments of the callable object + :return: result of a call of the callable object + """ + self.begin() + + try: + func_result = func(*args, **kwargs) + finally: + self.end() + + return func_result + + def _method_wrapper(*args, **kwargs): + """ + A wrapper to trace the execution of a class method + :param args: positional arguments of the class method + :param kwargs: keyword arguments of the class method + :return: result of a call of the class method + """ + self.begin() + + try: + func_result = func(obj, *args, **kwargs) + finally: + self.end() + + return func_result + + return _function_wrapper if obj is None or isinstance(func, staticmethod) else _method_wrapper + + +class _CallSite: + """ + A class that represents a call site for a callable object. + """ + CallerFrame = 1 + + def __init__(self, frame_number: int) -> None: + """ + Creates a call site. + :param frame_number: relative frame number that should be used to extract the information about the call site + """ + caller = _stack()[frame_number+1] + self._filename = _basename(caller.filename) + self._lineno = caller.lineno + + def filename(self): + """Returns filename for the call site.""" + return self._filename + + def lineno(self): + """Returns line number for the call site.""" + return self._lineno + + +class _NamedRegion(_Region): + """ + An abstract base class that represents a named code region. + """ + def __init__(self, func=None, name_creation_callback=None) -> None: + """ + Creates the instance of class that represents a named code region. + :param func: a name of the code region, a call site for the code region or a callable object (e.g. function) to + wrap. + If the call site object is passed, it is used as the initial choice for the region name. The name + that was derived based on call site object can be replaced with the name of the callable object if + it is passed in the future. + If the callable object is passed the name of this object is used as a name for the code region. + """ + super().__init__(self.__get_function(func), _partial(_NamedRegion.__deferred_wrap_callback, self)) + + self._name = self.__get_name(func) + self.__name_creation_callback = name_creation_callback + self.__is_final_name_determined = False + self.__is_custom_name_specified = isinstance(func, str) + + final_name_can_be_determined_now = not (func is None or isinstance(func, _CallSite)) + if final_name_can_be_determined_now: + self.__original_begin_func = None + self.__determine_final_name() + else: + self.__original_begin_func = self.begin + self.begin = self.__determine_final_name + + def __str__(self) -> str: + return self._name + + def __repr__(self) -> str: + return f"{self.__class__.__name__}('{self._name}')" + + def begin(self) -> None: + """Marks the beginning of a code region.""" + raise NotImplementedError() + + def end(self) -> None: + """Marks the end of a code region.""" + raise NotImplementedError() + + def name(self): + """Return the name of the code region.""" + return self._name + + def __determine_final_name(self): + """Determines a final name of a code region""" + self.__is_final_name_determined = True + + if callable(self.__name_creation_callback): + self.__name_creation_callback(self._name) + + if self.__original_begin_func is not None: + self.begin = self.__original_begin_func + self.begin() + + def __deferred_wrap_callback(self, func): + """Determines a final name of a code region if it has not been done before""" + if not self.__is_final_name_determined: + self._name = self.__get_name(func) + self.__determine_final_name() + elif not self.__is_custom_name_specified: + raise RuntimeError('A custom name for a code region must be specified before' + ' _NamedRegion.__call__() can be called more than once.') + + @staticmethod + def __get_function(func): + """Returns the argument if it is callable, otherwise returns None""" + return func if callable(func) else None + + @staticmethod + def __get_name(func): + """Returns appropriate code region name""" + if func is None: + return None + + if isinstance(func, str): + return _string_handle(func) + + if isinstance(func, _CallSite): + return _string_handle(f'{func.filename()}:{func.lineno()}') + + if hasattr(func, '__qualname__'): + return _string_handle(func.__qualname__) + + if hasattr(func, '__name__'): + return _string_handle(func.__name__) + + if hasattr(func, '__class__'): + return _string_handle(f'{func.__class__.__name__}.__call__') + + raise ValueError('Cannot get the name for the code region.') diff --git a/python/ittapi/string_handle.py b/python/ittapi/string_handle.py new file mode 100644 index 0000000..4d67d52 --- /dev/null +++ b/python/ittapi/string_handle.py @@ -0,0 +1,13 @@ +""" +string_handle.py - Python module wrapper for ITT String Handle API +""" +from ittapi.native import StringHandle as _StringHandle + + +def string_handle(string: str): + """ + Creates a handle for a string. + :param string: a string + :return: the handle to the given string + """ + return _StringHandle(string) diff --git a/python/ittapi/task.py b/python/ittapi/task.py new file mode 100644 index 0000000..8e75ad8 --- /dev/null +++ b/python/ittapi/task.py @@ -0,0 +1,147 @@ +""" +task.py - Python module wrapper for ITT Task API +""" +from ittapi.native import task_begin as _task_begin, task_end as _task_end +from ittapi.native import task_begin_overlapped as _task_begin_overlapped, task_end_overlapped as _task_end_overlapped + +from .domain import domain as _domain +from .id import id as _id +from .region import _CallSite, _NamedRegion + + +class _Task(_NamedRegion): + """ + An abstract base class that provides common functionality for subtypes that represent ITT Tasks. + """ + def __init__(self, task=None, domain=None, id=None, parent=None) -> None: + """ + Creates the instance of the class that represents ITT task. + :param task: a name of the task or a callable object (e.g. function) to wrap. If the callable object is passed + the name of this object is used as a name for the task. + :param domain: a task domain + :param id: a task id + :param parent: a parent task or an id of the parent + """ + super().__init__(task) + + self._domain = self.__get_task_domain(domain) + self._id = self.__get_task_id(id, self._domain) + self._parent_id = self.__get_parent_id(parent) + + def __str__(self) -> str: + return (f"{{ name: '{str(self._name)}', domain: '{str(self._domain)}'," + f" id: {str(self._id)}, parent_id: {str(self._parent_id)} }}") + + def __repr__(self) -> str: + return (f'{self.__class__.__name__}({repr(self._name)}, {repr(self._domain)},' + f' {repr(self._id)}, {repr(self._parent_id)})') + + def domain(self): + """Returns the domain of the task.""" + return self._domain + + def id(self): + """Returns the id of the task.""" + return self._id + + def parent_id(self): + """Returns the parent id for the task.""" + return self._parent_id + + def begin(self) -> None: + """Marks the beginning of a task.""" + raise NotImplementedError() + + def end(self) -> None: + """Marks the end of a task.""" + raise NotImplementedError() + + @staticmethod + def __get_task_domain(original_domain): + """Returns task domain""" + if original_domain is None or isinstance(original_domain, str): + return _domain(original_domain) + + return original_domain + + @staticmethod + def __get_task_id(original_id, domain): + """Returns task id for specified domain""" + return _id(domain) if original_id is None else original_id + + @staticmethod + def __get_parent_id(original_parent): + """Returns parent id""" + return original_parent.id() if isinstance(original_parent, task.__class__) else original_parent + + +class NestedTask(_Task): + """ + A class that represents nested tasks. + + Nested tasks implicitly support a concept of embedded execution. This means that the call end() finalizes the + most recent begin() call of the same or another nested task. + """ + def begin(self) -> None: + """Marks the beginning of a task.""" + _task_begin(self._domain, self._name, self._id, self._parent_id) + + def end(self) -> None: + """Marks the end of a task.""" + _task_end(self._domain) + + +def nested_task(task=None, domain=None, id=None, parent=None): + """ + Creates a nested task instance with the given arguments. + :param task: a name of the task or a callable object + :param domain: a task domain + :param id: a task id + :param parent: a parent task or an id of the parent + :return: an instance of NestedTask + """ + task = _CallSite(_CallSite.CallerFrame) if task is None else task + return NestedTask(task, domain, id, parent) + + +class OverlappedTask(_Task): + """ + A class that represents overlapped tasks. + + Execution regions of overlapped tasks may intersect. + """ + def begin(self) -> None: + """Marks the beginning of a task.""" + _task_begin_overlapped(self._domain, self._name, self._id, self._parent_id) + + def end(self) -> None: + """Marks the end of a task.""" + _task_end_overlapped(self._domain, self._id) + + +def overlapped_task(task=None, domain=None, id=None, parent=None): + """ + Creates an overlapped task instance with the given arguments. + :param task: a name of the task or a callable object + :param domain: a task domain + :param id: a task id + :param parent: a parent task or an id of the parent + :return: an instance of OverlappedTask + """ + task = _CallSite(_CallSite.CallerFrame) if task is None else task + return OverlappedTask(task, domain, id, parent) + + +def task(task=None, domain=None, id=None, parent=None, overlapped=False): + """ + Creates a task instance with the given arguments. + :param task: a name of the task or a callable object + :param domain: a task domain + :param id: a task id + :param parent: a parent task or an id of the parent + :param overlapped: determines if the created task should be an instance of OverlappedTask class + or NestedTask class + :return: a task instance + """ + task = _CallSite(_CallSite.CallerFrame) if task is None else task + return OverlappedTask(task, domain, id, parent) if overlapped else NestedTask(task, domain, id, parent) diff --git a/python/ittapi/thread_naming.py b/python/ittapi/thread_naming.py new file mode 100644 index 0000000..745b0ee --- /dev/null +++ b/python/ittapi/thread_naming.py @@ -0,0 +1,12 @@ +""" +thread_naming.py - Python module wrapper for ITT Thread Naming API +""" +from ittapi.native import thread_set_name as _thread_set_name + + +def thread_set_name(name: str): + """ + Sets a thread name of calling thread. + :param name: the thread name + """ + _thread_set_name(name) diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000..2171cad --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "ittapi" +version = "1.1.0" +authors = [ + { name="Egor Suldin"}, +] +description = "Python bindings to Intel Instrumentation and Tracing Technology (ITT) API." +readme = "README.md" +requires-python = ">=3.8" +classifiers = [ + "License :: OSI Approved :: BSD License", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +[project.urls] +"Homepage" = "https://github.com/intel/ittapi" +"Bug Tracker" = "https://github.com/intel/ittapi/issues" + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" diff --git a/python/samples/collection_control_sample.py b/python/samples/collection_control_sample.py new file mode 100644 index 0000000..3b1fde2 --- /dev/null +++ b/python/samples/collection_control_sample.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +import ittapi + +# pylint: disable=C0411 +from argparse import ArgumentParser +from vtune_tool import run_vtune_hotspot_collection +from workload import workload + + +def run_sample(): + @ittapi.active_region + @ittapi.task + def run_workload(): + workload() + + run_workload() + + for i in range(4): + with ittapi.active_region(activator=lambda: i % 2): # pylint: disable=W0640 + with ittapi.task(f'for loop iteration {i}'): + workload() + + ittapi.collection_control.resume() + + with ittapi.task('resumed region'): + workload() + + with ittapi.paused_region(): + with ittapi.task('paused region'): + workload() + + +# pylint: disable=R0801 +if __name__ == '__main__': + parser = ArgumentParser( + description='The sample that demonstrates the use of wrappers for the Collection Control API.' + ) + parser.add_argument('--run-sample', + help='Runs code that uses wrappers for Collection Control API.', + action='store_true') + args = parser.parse_args() + + if args.run_sample: + run_sample() + else: + run_vtune_hotspot_collection(['python', __file__, '--run-sample'], + ['-start-paused']) diff --git a/python/samples/event_sample.py b/python/samples/event_sample.py new file mode 100644 index 0000000..28ec9fd --- /dev/null +++ b/python/samples/event_sample.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +import ittapi + +# pylint: disable=C0411 +from argparse import ArgumentParser +from vtune_tool import run_vtune_hotspot_collection +from workload import workload + + +def run_sample(): + # ittapi.event can be used as decorator + @ittapi.event + def my_function_1(): + workload() + + # the list of arguments can be empty + @ittapi.event() + def my_function_2(): + workload() + + # or you can specify the name of the event and other parameters + @ittapi.event('my function 3') + def my_function_3(): + workload() + + @ittapi.event + @ittapi.event('my function 4') + def my_function_4(): + workload() + + # also, ittapi.event can be used as a context manager + with ittapi.event(): + workload() + # in this form you also can specify the name, the domain and other parameters in the same way + with ittapi.event('my event'): + workload() + + my_function_1() + my_function_2() + my_function_3() + my_function_4() + + # example for overlapped events + overlapped_event_1 = ittapi.event('overlapped event 1') + overlapped_event_1.begin() + workload() + overlapped_event_2 = ittapi.event('overlapped event 2') + overlapped_event_2.begin() + workload() + overlapped_event_1.end() + workload() + overlapped_event_2.end() + + # example with callable object + class CallableClass: + def __call__(self, *args, **kwargs): # pylint: disable=W0621 + workload() + + callable_object = ittapi.event(CallableClass()) + callable_object() + + +# pylint: disable=R0801 +if __name__ == '__main__': + parser = ArgumentParser(description='The sample that demonstrates the use of wrappers for the Event API.') + parser.add_argument('--run-sample', + help='Runs code that uses wrappers for the Event API.', + action='store_true') + args = parser.parse_args() + + if args.run_sample: + run_sample() + else: + run_vtune_hotspot_collection(['python', __file__, '--run-sample']) diff --git a/python/samples/task_sample.py b/python/samples/task_sample.py new file mode 100644 index 0000000..fc56aab --- /dev/null +++ b/python/samples/task_sample.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +import ittapi + +# pylint: disable=C0411 +from argparse import ArgumentParser +from vtune_tool import run_vtune_hotspot_collection +from workload import workload + + +def run_sample(): + # ittapi.task can be used as decorator + @ittapi.task + def my_function_1(): + workload() + + # the list of arguments can be empty + @ittapi.task() + def my_function_2(): + workload() + + # or you can specify the name of the task and other parameters + @ittapi.task('my function 3') + def my_function_3(): + workload() + + # like domain + @ittapi.task(domain='my domain') + def my_function_4(): + workload() + + @ittapi.task + @ittapi.task('my function 5') + def my_function_5(): + workload() + + # also, ittapi.task can be used as a context manager + with ittapi.task(): + workload() + # in this form you also can specify the name, the domain and other parameters in the same way + with ittapi.task('my task', 'my domain'): + workload() + + my_function_1() + my_function_2() + my_function_3() + my_function_4() + my_function_5() + + # example for overlapped tasks + overlapped_task_1 = ittapi.task('overlapped task 1', overlapped=True) + overlapped_task_1.begin() + workload() + overlapped_task_2 = ittapi.task('overlapped task 2', overlapped=True) + overlapped_task_2.begin() + workload() + overlapped_task_1.end() + workload() + overlapped_task_2.end() + + # example with callable object + class CallableClass: + def __call__(self, *args, **kwargs): # pylint: disable=W0621 + workload() + + callable_object = ittapi.task(CallableClass()) + callable_object() + + +# pylint: disable=R0801 +if __name__ == '__main__': + parser = ArgumentParser(description='The sample that demonstrates the use of wrappers for the Task API.') + parser.add_argument('--run-sample', + help='Runs code that uses wrappers for the Task API.', + action='store_true') + args = parser.parse_args() + + if args.run_sample: + run_sample() + else: + run_vtune_hotspot_collection(['python', __file__, '--run-sample']) diff --git a/python/samples/thread_naming_sample.py b/python/samples/thread_naming_sample.py new file mode 100644 index 0000000..eb0c896 --- /dev/null +++ b/python/samples/thread_naming_sample.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +import ittapi + +# pylint: disable=C0411 +from argparse import ArgumentParser +from vtune_tool import run_vtune_hotspot_collection +from workload import workload +from threading import Thread + + +def run_sample(): + @ittapi.task + def run_workload(): + workload() + + def thread_func(name: str): + ittapi.thread_set_name(name) + run_workload() + + threads = [Thread(target=thread_func, args=(f'Thread for iteration {i}',)) for i in range(4)] + [thread.start() for thread in threads] # pylint: disable=W0106 + [thread.join() for thread in threads] # pylint: disable=W0106 + + +# pylint: disable=R0801 +if __name__ == '__main__': + parser = ArgumentParser( + description='The sample that demonstrates the use of wrappers for the Thread Naming API.' + ) + parser.add_argument('--run-sample', + help='Runs code that uses wrappers for Thread Naming API.', + action='store_true') + args = parser.parse_args() + + if args.run_sample: + run_sample() + else: + run_vtune_hotspot_collection(['python', __file__, '--run-sample']) diff --git a/python/samples/vtune_tool.py b/python/samples/vtune_tool.py new file mode 100644 index 0000000..e1c91ff --- /dev/null +++ b/python/samples/vtune_tool.py @@ -0,0 +1,55 @@ +from os import environ, path +from sys import platform +from subprocess import run + + +class VTuneTool: + _ONEAPI_ROOT_ENV = 'ONEAPI_ROOT' + _VTUNE_PROFILER_DIR_ENV = 'VTUNE_PROFILER_DIR' + + def __init__(self): + if environ.get(self._VTUNE_PROFILER_DIR_ENV, None): + self.path = path.join(environ[self._VTUNE_PROFILER_DIR_ENV]) + elif environ.get(self._ONEAPI_ROOT_ENV, None): + self.path = path.join(environ[self._ONEAPI_ROOT_ENV], 'vtune', 'latest') + else: + self.path = None + + if not self.path or not path.exists(self.path): + if platform == 'win32': + amplxe_vars_script_command = '\\amplxe-vars.bat' + export_command = 'set VTUNE_PROFILER_DIR=' + else: + amplxe_vars_script_command = '/amplxe-vars.sh' + export_command = 'export VTUNE_PROFILER_DIR=' + + raise ValueError(f'VTune Profiler installation directory is not found.\n' + f'Use {amplxe_vars_script_command} to prepare the environment or specify VTune Profiler' + f' installation directory using VTUNE_PROFILER_DIR environment variable:\n' + f'{export_command}') + self._tool_path = path.join(self.path, 'bin64', 'vtune.exe' if platform == 'win32' else 'vtune') + + def run_hotspot_collection(self, app_args, additional_collection_args=None): + collection_args = [self._tool_path, '-collect', 'hotspots', '-knob', 'enable-characterization-insights=false'] + + if isinstance(additional_collection_args, list): + collection_args.extend(additional_collection_args) + elif isinstance(additional_collection_args, str): + collection_args.extend(additional_collection_args.split(' ')) + elif additional_collection_args is not None: + raise TypeError('additional_collection_args argument must be a list or str') + + collection_args.append('--') + + if isinstance(app_args, list): + collection_args.extend(app_args) + elif isinstance(app_args, str): + collection_args.extend(app_args.split(' ')) + else: + raise TypeError('app_args argument must be a list or str') + + return run(collection_args, check=True) + + +def run_vtune_hotspot_collection(app_args, additional_collection_args=None): + return VTuneTool().run_hotspot_collection(app_args, additional_collection_args) diff --git a/python/samples/workload.py b/python/samples/workload.py new file mode 100644 index 0000000..48bbbd9 --- /dev/null +++ b/python/samples/workload.py @@ -0,0 +1,8 @@ +from math import sin, cos + + +def workload(): + v = 1.0 + for _ in range(500000): + v += sin(v) * cos(v) + return v diff --git a/python/setup.py b/python/setup.py new file mode 100644 index 0000000..d984fe4 --- /dev/null +++ b/python/setup.py @@ -0,0 +1,129 @@ +""" +setup.py - Python module to install ittapi package +""" +import os +import sys + +from setuptools import setup, Extension +from setuptools.command.build_ext import build_ext +from subprocess import run # pylint: disable=C0411 + + +def get_environment_flag(name): + """ + Get an environment variable and check its value. + :param name: the environment variable name + :return: None if the environment variable is not set. False if the environment variable is set and has one of the + following values: `''`, `0`, `n`, `no` or `false`. Otherwise, return True. + """ + flag_value = os.environ.get(name, None) + return (flag_value and flag_value.lower() not in ('0', 'n', 'no', 'false')) if flag_value is not None else None + + +IS_64_ARCHITECTURE = sys.maxsize > 2 ** 32 + +# Check if custom location for ITT API source code is specified +ITT_DEFAULT_DIR = '../' +itt_dir = os.environ.get('ITTAPI_ITT_API_SOURCE_DIR', None) +itt_dir = itt_dir if itt_dir else ITT_DEFAULT_DIR + +assert os.path.exists(itt_dir), 'The specified directory with ITT API source code does not exist.' +assert itt_dir != ITT_DEFAULT_DIR or len(os.listdir(itt_dir)), \ + (f'The specified directory with ITT API source code ({itt_dir}) is empty.\n' + f'Please make sure you provide a valid path.') + +# Check if IPT support is requested +build_itt_with_ipt_support = get_environment_flag('ITTAPI_BUILD_WITH_ITT_API_IPT_SUPPORT') +build_itt_with_ipt_support = build_itt_with_ipt_support if build_itt_with_ipt_support is not None else True + +itt_source = [os.path.join(itt_dir, 'src', 'ittnotify', 'ittnotify_static.c')] +itt_include_dirs = [os.path.join(itt_dir, 'include')] +itt_license_files = [] +if itt_dir == ITT_DEFAULT_DIR: + itt_license_files = [os.path.join(itt_dir, 'LICENSES', 'BSD-3-Clause.txt'), + os.path.join(itt_dir, 'LICENSES', 'GPL-2.0-only.txt')] + +if build_itt_with_ipt_support: + itt_compiler_flags = ['-DITT_API_IPT_SUPPORT'] + if sys.platform == 'win32': + ITT_PTMARK_SOURCE = 'ittptmark64.asm' if IS_64_ARCHITECTURE else 'ittptmark32.asm' + else: + ITT_PTMARK_SOURCE = 'ittptmark64.S' if IS_64_ARCHITECTURE else 'ittptmark32.S' + itt_extra_objects = [os.path.join(itt_dir, 'src', 'ittnotify', ITT_PTMARK_SOURCE)] +else: + itt_compiler_flags = [] + itt_extra_objects = [] + +ittapi_license_files = [] +ittapi_native_sources = ['ittapi.native/extensions/python.cpp', + 'ittapi.native/extensions/string.cpp', + 'ittapi.native/collection_control.cpp', + 'ittapi.native/domain.cpp', + 'ittapi.native/event.cpp', + 'ittapi.native/id.cpp', + 'ittapi.native/string_handle.cpp', + 'ittapi.native/task.cpp', + 'ittapi.native/thread_naming.cpp', + 'ittapi.native/ittapi.cpp'] + +ittapi_native_compiler_args = ['/std:c++20' if sys.platform == 'win32' else '-std=c++20'] +if build_itt_with_ipt_support: + ittapi_native_compiler_args.append('-DITTAPI_BUILD_WITH_ITT_API_IPT_SUPPORT=1') + +ittapi_native = Extension('ittapi.native', + sources=itt_source + ittapi_native_sources, + include_dirs=itt_include_dirs, + extra_compile_args=itt_compiler_flags + ittapi_native_compiler_args, + extra_objects=itt_extra_objects) + + +class NativeBuildExtension(build_ext): # pylint: disable=R0903 + """ + A class that implements the build extension to compile ittapi.native module. + """ + def build_extension(self, ext) -> None: + """ + Build native extension module + :param ext: the extension to build + """ + if ext.name == 'ittapi.native' and self.compiler.compiler_type == 'msvc': + # Setup asm tool + as_tool = 'ml64.exe' if IS_64_ARCHITECTURE else 'ml.exe' + as_ext = '.asm' + + if hasattr(self.compiler, 'initialized') and hasattr(self.compiler, 'initialize'): + if not self.compiler.initialized: + self.compiler.initialize() + + as_path = os.path.dirname(self.compiler.cc) if hasattr(self.compiler, 'cc') else '' + + # Extract asm files from extra objects + # pylint: disable=W0106 + asm_files = [filename for filename in ext.extra_objects if filename.lower().endswith(as_ext)] + [ext.extra_objects.remove(filename) for filename in asm_files] + + # Create temp directories + [os.makedirs(os.path.join(self.build_temp, os.path.dirname(filename)), exist_ok=True) + for filename in asm_files] + + # Generate target names + src_dir = os.path.dirname(__file__) + obj_asm_pairs = [(os.path.join(self.build_temp, os.path.splitext(filename)[0]) + '.obj', + os.path.join(src_dir, filename)) for filename in asm_files] + # Compile + [run([os.path.join(as_path, as_tool), '/Fo', obj_file, '/c', asm_file], check=True) + for obj_file, asm_file in obj_asm_pairs] + + [ext.extra_objects.append(obj_file) for obj_file, _ in obj_asm_pairs] + + build_ext.build_extension(self, ext) + + +setup(name='ittapi', + version='1.1.0', + description='ITT API bindings for Python', + packages=['ittapi'], + ext_modules=[ittapi_native], + license_files=ittapi_license_files + itt_license_files, + cmdclass={'build_ext': NativeBuildExtension} if build_itt_with_ipt_support else {}, + zip_safe=False) diff --git a/python/utest/ittapi_native_mock/__init__.py b/python/utest/ittapi_native_mock/__init__.py new file mode 100644 index 0000000..a7130ab --- /dev/null +++ b/python/utest/ittapi_native_mock/__init__.py @@ -0,0 +1,8 @@ +from sys import modules + +from .patch import patch +from .ittapi_native_mock import ITTAPI_NATIVE_MODULE_NAME +from .ittapi_native_mock import IttapiNativeMock + + +modules[ITTAPI_NATIVE_MODULE_NAME] = IttapiNativeMock() diff --git a/python/utest/ittapi_native_mock/ittapi_native_mock.py b/python/utest/ittapi_native_mock/ittapi_native_mock.py new file mode 100644 index 0000000..ad36107 --- /dev/null +++ b/python/utest/ittapi_native_mock/ittapi_native_mock.py @@ -0,0 +1,29 @@ +from types import ModuleType as _ModuleType +from unittest.mock import MagicMock as _MagicMock + +ITTAPI_NATIVE_MODULE_NAME = 'ittapi.native' + + +class IttapiNativeMock(_ModuleType): + def __init__(self): + super().__init__(ITTAPI_NATIVE_MODULE_NAME) + self.attrs = { + 'detach': _MagicMock(), + 'pause': _MagicMock(), + 'resume': _MagicMock(), + 'task_begin': _MagicMock(), + 'task_end': _MagicMock(), + 'task_begin_overlapped': _MagicMock(), + 'task_end_overlapped': _MagicMock(), + 'thread_set_name': _MagicMock(), + 'Domain': _MagicMock(), + 'Event': _MagicMock(), + 'Id': _MagicMock(), + 'StringHandle': _MagicMock(), + } + + def __getattr__(self, item): + return self.attrs.get(item) + + def attributes(self): + return self.attrs diff --git a/python/utest/ittapi_native_mock/patch.py b/python/utest/ittapi_native_mock/patch.py new file mode 100644 index 0000000..12396c3 --- /dev/null +++ b/python/utest/ittapi_native_mock/patch.py @@ -0,0 +1,41 @@ +from functools import wraps as _wraps +from sys import modules as _modules + +from .ittapi_native_mock import ITTAPI_NATIVE_MODULE_NAME + + +class _IttapiNativeAttributeMock: + def __init__(self, name): + self._name = name + + def __enter__(self): + return self.__reset_attribute_mock() + + def __exit__(self, *args): + self.__reset_attribute_mock() + + def __native_module_attribute(self): + return _modules[ITTAPI_NATIVE_MODULE_NAME].attributes().get(self._name) + + def __reset_attribute_mock(self): + attr = self.__native_module_attribute() + attr.reset_mock() + attr.side_effect = None + return attr + + +class _IttapiNativePatch: + def __init__(self, attr_name): + self._attr_name = attr_name + + def __call__(self, func): + def wrapper(*args, **kwargs): + with _IttapiNativeAttributeMock(self._attr_name) as attr_mock: + new_args = args + (attr_mock,) + return func(*new_args, **kwargs) + + return _wraps(func)(wrapper) + + +def patch(attr_name): + return _IttapiNativePatch(attr_name) diff --git a/python/utest/test_collection_control.py b/python/utest/test_collection_control.py new file mode 100644 index 0000000..ee0907d --- /dev/null +++ b/python/utest/test_collection_control.py @@ -0,0 +1,149 @@ +from unittest import main as unittest_main, TestCase + +from ittapi_native_mock import patch as ittapi_native_patch +import ittapi + + +class DirectCollectionControlTests(TestCase): + @ittapi_native_patch('detach') + def test_detach_call(self, detach_mock): + ittapi.collection_control.detach() + detach_mock.assert_called_once() + + @ittapi_native_patch('pause') + def test_pause_call(self, pause_mock): + ittapi.collection_control.pause() + pause_mock.assert_called_once() + + @ittapi_native_patch('resume') + def test_resume_call(self, resume_mock): + ittapi.collection_control.resume() + resume_mock.assert_called_once() + + +class ActiveRegionTests(TestCase): + @ittapi_native_patch('pause') + @ittapi_native_patch('resume') + def test_active_region_as_decorator(self, pause_mock, resume_mock): + @ittapi.active_region + def my_function(): + return 42 + + self.assertEqual(my_function(), 42) + resume_mock.assert_called_once() + pause_mock.assert_called_once() + + @ittapi_native_patch('pause') + @ittapi_native_patch('resume') + def test_active_region_as_context_manager(self, pause_mock, resume_mock): + with ittapi.active_region(): + pass + + resume_mock.assert_called_once() + pause_mock.assert_called_once() + + @ittapi_native_patch('pause') + @ittapi_native_patch('resume') + def test_active_region_with_manual_activation(self, pause_mock, resume_mock): + region = ittapi.active_region() + + region.activator.deactivate() + with region: + pass + + resume_mock.assert_not_called() + pause_mock.assert_not_called() + + region.activator.activate() + with region: + pass + + resume_mock.assert_called_once() + pause_mock.assert_called_once() + + @ittapi_native_patch('pause') + @ittapi_native_patch('resume') + def test_active_region_with_custom_activator(self, pause_mock, resume_mock): + for i in range(4): + with ittapi.active_region(activator=lambda: i % 2): # pylint: disable=W0640 + pass + + self.assertEqual(resume_mock.call_count, 2) + self.assertEqual(pause_mock.call_count, 2) + + @ittapi_native_patch('pause') + @ittapi_native_patch('resume') + def test_active_region_as_decorator_without_activator(self, pause_mock, resume_mock): + @ittapi.active_region(activator=None) + def my_function(): + return 42 + + self.assertEqual(my_function(), 42) + resume_mock.assert_called_once() + pause_mock.assert_called_once() + + +class PausedRegionTests(TestCase): + @ittapi_native_patch('pause') + @ittapi_native_patch('resume') + def test_paused_region_as_decorator(self, pause_mock, resume_mock): + @ittapi.paused_region + def my_function(): + return 42 + + self.assertEqual(my_function(), 42) + resume_mock.assert_called_once() + pause_mock.assert_called_once() + + @ittapi_native_patch('pause') + @ittapi_native_patch('resume') + def test_paused_region_as_context_manager(self, pause_mock, resume_mock): + with ittapi.paused_region(): + pass + + resume_mock.assert_called_once() + pause_mock.assert_called_once() + + @ittapi_native_patch('pause') + @ittapi_native_patch('resume') + def test_paused_region_with_manual_activation(self, pause_mock, resume_mock): + region = ittapi.paused_region() + + region.activator.deactivate() + with region: + pass + + resume_mock.assert_not_called() + pause_mock.assert_not_called() + + region.activator.activate() + with region: + pass + + resume_mock.assert_called_once() + pause_mock.assert_called_once() + + @ittapi_native_patch('pause') + @ittapi_native_patch('resume') + def test_paused_region_with_custom_activator(self, pause_mock, resume_mock): + for i in range(4): + with ittapi.paused_region(activator=lambda: i % 2): # pylint: disable=W0640 + pass + + self.assertEqual(resume_mock.call_count, 2) + self.assertEqual(pause_mock.call_count, 2) + + @ittapi_native_patch('pause') + @ittapi_native_patch('resume') + def test_paused_region_as_decorator_without_activator(self, pause_mock, resume_mock): + @ittapi.paused_region(activator=None) + def my_function(): + return 42 + + self.assertEqual(my_function(), 42) + resume_mock.assert_called_once() + pause_mock.assert_called_once() + + +if __name__ == '__main__': + unittest_main() # pragma: no cover diff --git a/python/utest/test_domain.py b/python/utest/test_domain.py new file mode 100644 index 0000000..7e04beb --- /dev/null +++ b/python/utest/test_domain.py @@ -0,0 +1,21 @@ +from unittest import main as unittest_main, TestCase + +from ittapi_native_mock import patch as ittapi_native_patch +import ittapi + + +class DomainTests(TestCase): + @ittapi_native_patch('Domain') + def test_domain_call_without_arguments(self, domain_mock): + ittapi.domain() + domain_mock.assert_called_once_with(None) + + @ittapi_native_patch('Domain') + def test_domain_call_with_name(self, domain_mock): + name = 'my domain' + ittapi.domain(name) + domain_mock.assert_called_once_with(name) + + +if __name__ == '__main__': + unittest_main() # pragma: no cover diff --git a/python/utest/test_event.py b/python/utest/test_event.py new file mode 100644 index 0000000..9f45827 --- /dev/null +++ b/python/utest/test_event.py @@ -0,0 +1,423 @@ +from inspect import stack +from os.path import basename +from sys import version_info +from unittest import main as unittest_main, TestCase +from unittest.mock import call + +from ittapi_native_mock import patch as ittapi_native_patch +import ittapi + + +class EventCreationTests(TestCase): + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_creation_with_default_constructor(self, event_mock, string_handle_mock): + string_handle_mock.side_effect = lambda x: x + + event = ittapi.event() + caller = stack()[0] + expected_name = f'{basename(caller.filename)}:{caller.lineno-1}' + + event_mock.assert_not_called() + + event.begin() + + event_mock.assert_called_once_with(expected_name) + self.assertEqual(event.name(), expected_name) + + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_creation_as_decorator_for_function(self, event_mock, string_handle_mock): + string_handle_mock.side_effect = lambda x: x + + @ittapi.event + def my_function(): + pass # pragma: no cover + + event_mock.assert_called_once_with(my_function.__qualname__) + + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_creation_as_decorator_with_empty_arguments_for_function(self, event_mock, string_handle_mock): + string_handle_mock.side_effect = lambda x: x + + @ittapi.event() + def my_function(): + pass # pragma: no cover + + event_mock.assert_called_with(my_function.__qualname__) + + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_creation_as_decorator_with_name_for_function(self, event_mock, string_handle_mock): + string_handle_mock.side_effect = lambda x: x + + @ittapi.event('my function') + def my_function(): + pass # pragma: no cover + + event_mock.assert_called_once_with('my function') + + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_creation_as_decorator_with_empty_args_and_name_for_function(self, event_mock, string_handle_mock): + string_handle_mock.side_effect = lambda x: x + + @ittapi.event + @ittapi.event('my function') + def my_function(): + pass # pragma: no cover + + expected_calls = [call('my function'), + call(my_function.__qualname__)] + event_mock.assert_has_calls(expected_calls) + + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_creation_with_default_constructor_as_context_manager(self, event_mock, string_handle_mock): + string_handle_mock.side_effect = lambda x: x + + caller = stack()[0] + with ittapi.event(): + pass + + event_mock.assert_called_once_with(f'{basename(caller.filename)}:{caller.lineno+1}') + + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_creation_with_name_and_domain_as_context_manager(self, event_mock, string_handle_mock): + string_handle_mock.side_effect = lambda x: x + + with ittapi.event('my event'): + pass + + event_mock.assert_called_once_with('my event') + + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_creation_for_callable_object(self, event_mock, string_handle_mock): + string_handle_mock.side_effect = lambda x: x + + class CallableClass: + def __call__(self, *args, **kwargs): + pass # pragma: no cover + + event = ittapi.event(CallableClass()) + + expected_name = f'{CallableClass.__name__}.__call__' + event_mock.assert_called_once_with(expected_name) + + self.assertEqual(event.name(), expected_name) + + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_unnamed_event_creation_for_callable_object(self, event_mock, string_handle_mock): + string_handle_mock.side_effect = lambda x: x + + class CallableClass: + def __call__(self, *args, **kwargs): + pass # pragma: no cover + + event = ittapi.event() + event(CallableClass()) + + expected_name = f'{CallableClass.__name__}.__call__' + event_mock.assert_called_once_with(expected_name) + self.assertEqual(event.name(), expected_name) + + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_creation_for_method(self, event_mock, string_handle_mock): + string_handle_mock.side_effect = lambda x: x + + class MyClass: + @ittapi.event + def my_method(self): + pass # pragma: no cover + + event_mock.assert_called_once_with(f'{MyClass.my_method.__qualname__}') + + +class EventPropertiesTest(TestCase): + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_properties(self, event_mock, string_handle_mock): + string_handle_mock.side_effect = lambda x: x + + class CallableClass: + def __call__(self, *args, **kwargs): + pass # pragma: no cover + + event = ittapi.event(CallableClass()) + + expected_name = f'{CallableClass.__name__}.__call__' + event_mock.assert_called_once_with(expected_name) + + self.assertEqual(event.name(), expected_name) + + self.assertEqual(str(event), expected_name) + self.assertEqual(repr(event), f'{event.__class__.__name__}(\'{expected_name}\')') + + +class EventExecutionTests(TestCase): + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_for_function(self, event_mock, string_handle_mock): + string_handle_mock.return_value = 'string_handle' + + @ittapi.event + def my_function(): + return 42 + + string_handle_mock.assert_called_once_with(my_function.__qualname__) + event_mock.assert_called_once_with(string_handle_mock.return_value) + + self.assertEqual(my_function(), 42) + + expected_calls = [call().begin(), + call().end()] + event_mock.assert_has_calls(expected_calls) + + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_nested_events_for_function(self, event_mock, string_handle_mock): + string_handle_mock.side_effect = lambda x: x + + @ittapi.event + @ittapi.event('my function') + def my_function(): + return 42 + + expected_calls = [call('my function'), + call(my_function.__qualname__)] + string_handle_mock.assert_has_calls(expected_calls) + event_mock.assert_has_calls(expected_calls) + + self.assertEqual(my_function(), 42) + + expected_calls = [call().begin(), + call().begin(), + call().end(), + call().end()] + event_mock.assert_has_calls(expected_calls) + + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_as_context_manager(self, event_mock, string_handle_mock): + string_handle_mock.side_effect = lambda x: x + + region_name = 'my region' + with ittapi.event(region_name): + pass + + string_handle_mock.assert_called_once_with(region_name) + event_mock.assert_called_once_with(region_name) + + expected_calls = [call().begin(), + call().end()] + event_mock.assert_has_calls(expected_calls) + + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_for_callable_object(self, event_mock, string_handle_mock): + string_handle_mock.return_value = 'string_handle' + + class CallableClass: + def __call__(self, *args, **kwargs): + return 42 + + callable_object = ittapi.event(CallableClass()) + string_handle_mock.assert_called_once_with(f'{CallableClass.__name__}.__call__') + event_mock.assert_called_once_with(string_handle_mock.return_value) + + self.assertEqual(callable_object(), 42) + + expected_calls = [call().begin(), + call().end()] + event_mock.assert_has_calls(expected_calls) + + def test_event_for_multiple_callable_objects(self): + class CallableClass: + def __call__(self, *args, **kwargs): + pass # pragma: no cover + + event = ittapi.event() + event(CallableClass()) + + with self.assertRaises(RuntimeError) as context: + event(CallableClass()) + + self.assertEqual(str(context.exception), 'A custom name for a code region must be specified before' + ' _NamedRegion.__call__() can be called more than once.') + + def test_event_for_noncallable_object(self): + with self.assertRaises(TypeError) as context: + ittapi.event()(42) + + self.assertEqual(str(context.exception), 'Callable object is expected as a first argument.') + + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_for_method(self, event_mock, string_handle_mock): + string_handle_mock.side_effect = lambda x: x + + class MyClass: + @ittapi.event + def my_method(self): + return 42 + + string_handle_mock.assert_called_once_with(f'{MyClass.my_method.__qualname__}') + event_mock.assert_called_once_with(f'{MyClass.my_method.__qualname__}') + + my_object = MyClass() + self.assertEqual(my_object.my_method(), 42) + + expected_calls = [call().begin(), + call().end()] + event_mock.assert_has_calls(expected_calls) + + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_for_class_method(self, event_mock, string_handle_mock): + string_handle_mock.side_effect = lambda x: x + + class MyClass: + @classmethod + @ittapi.event + def my_class_method(cls): + return 42 + + string_handle_mock.assert_called_once_with(f'{MyClass.my_class_method.__qualname__}') + event_mock.assert_called_once_with(f'{MyClass.my_class_method.__qualname__}') + + self.assertEqual(MyClass.my_class_method(), 42) + + expected_calls = [call().begin(), + call().end()] + event_mock.assert_has_calls(expected_calls) + + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_for_static_method(self, event_mock, string_handle_mock): + string_handle_mock.side_effect = lambda x: x + + class MyClass: + @staticmethod + @ittapi.event + def my_static_method(): + return 42 + + string_handle_mock.assert_called_once_with(f'{MyClass.my_static_method.__qualname__}') + event_mock.assert_called_once_with(f'{MyClass.my_static_method.__qualname__}') + + self.assertEqual(MyClass.my_static_method(), 42) + self.assertEqual(MyClass().my_static_method(), 42) + + expected_calls = [call().begin(), + call().end(), + call().begin(), + call().end()] + event_mock.assert_has_calls(expected_calls) + + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_for_static_method_with_wrong_order_of_decorators(self, event_mock, string_handle_mock): + string_handle_mock.side_effect = lambda x: x + + class MyClass: + @ittapi.event + @staticmethod + def my_static_method(): + return 42 # pragma: no cover + + if version_info >= (3, 10): + string_handle_mock.assert_called_once_with(f'{MyClass.my_static_method.__qualname__}') + event_mock.assert_called_once_with(f'{MyClass.my_static_method.__qualname__}') + + self.assertEqual(MyClass().my_static_method(), 42) + self.assertEqual(MyClass.my_static_method(), 42) + + expected_calls = [call().begin(), + call().end(), + call().begin(), + call().end()] + event_mock.assert_has_calls(expected_calls) + else: + # @staticmethod decorator returns a descriptor which is not callable before Python 3.10 + # therefore, it cannot be traced. @staticmethod have to be always above ittapi decorators for Python 3.9 or + # older. otherwise, the exception is thrown. + with self.assertRaises(TypeError) as context: + MyClass().my_static_method() + + self.assertEqual(str(context.exception), 'Callable object is expected to be passed.') + + def test_event_for_class_method_with_wrong_order_of_decorators(self): + # @classmethod decorator returns a descriptor and the descriptor is not callable object, + # therefore, it cannot be traced. @classmethod have to be always above ittapi decorators, + # otherwise, the exception is thrown. + class MyClass: + @ittapi.event + @classmethod + def my_class_method(cls): + return 42 # pragma: no cover + + with self.assertRaises(TypeError) as context: + MyClass().my_class_method() + + self.assertEqual(str(context.exception), 'Callable object is expected to be passed.') + + with self.assertRaises(TypeError) as context: + MyClass.my_class_method() + + self.assertEqual(str(context.exception), 'Callable object is expected to be passed.') + + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_for_function_raised_exception(self, event_mock, string_handle_mock): + string_handle_mock.return_value = 'string_handle' + + exception_msg = 'ValueError exception from my_function' + + @ittapi.event + def my_function(): + raise ValueError(exception_msg) + + string_handle_mock.assert_called_once_with(my_function.__qualname__) + event_mock.assert_called_once_with(string_handle_mock.return_value) + + with self.assertRaises(ValueError) as context: + my_function() + + self.assertEqual(str(context.exception), exception_msg) + + expected_calls = [call().begin(), + call().end()] + event_mock.assert_has_calls(expected_calls) + + @ittapi_native_patch('Event') + @ittapi_native_patch('StringHandle') + def test_event_for_method_raised_exception(self, event_mock, string_handle_mock): + string_handle_mock.side_effect = lambda x: x + + exception_msg = 'ValueError exception from my_method' + + class MyClass: + @ittapi.event + def my_method(self): + raise ValueError(exception_msg) + + string_handle_mock.assert_called_once_with(f'{MyClass.my_method.__qualname__}') + event_mock.assert_called_once_with(f'{MyClass.my_method.__qualname__}') + + with self.assertRaises(ValueError) as context: + MyClass().my_method() + + self.assertEqual(str(context.exception), exception_msg) + + expected_calls = [call().begin(), + call().end()] + event_mock.assert_has_calls(expected_calls) + + +if __name__ == '__main__': + unittest_main() # pragma: no cover diff --git a/python/utest/test_id.py b/python/utest/test_id.py new file mode 100644 index 0000000..8835d49 --- /dev/null +++ b/python/utest/test_id.py @@ -0,0 +1,16 @@ +from unittest import main as unittest_main, TestCase + +from ittapi_native_mock import patch as ittapi_native_patch +import ittapi + + +class IdTests(TestCase): + @ittapi_native_patch('Id') + def test_id_call(self, id_mock): + domain = 'my domain' + ittapi.id(domain) + id_mock.assert_called_once_with(domain) + + +if __name__ == '__main__': + unittest_main() # pragma: no cover diff --git a/python/utest/test_string_handle.py b/python/utest/test_string_handle.py new file mode 100644 index 0000000..910b60e --- /dev/null +++ b/python/utest/test_string_handle.py @@ -0,0 +1,16 @@ +from unittest import main as unittest_main, TestCase + +from ittapi_native_mock import patch as ittapi_native_patch +import ittapi + + +class StringHandleTests(TestCase): + @ittapi_native_patch('StringHandle') + def test_string_handle_call(self, string_handle_mock): + s = 'my string' + ittapi.string_handle(s) + string_handle_mock.assert_called_once_with(s) + + +if __name__ == '__main__': + unittest_main() # pragma: no cover diff --git a/python/utest/test_task.py b/python/utest/test_task.py new file mode 100644 index 0000000..18b8ec3 --- /dev/null +++ b/python/utest/test_task.py @@ -0,0 +1,581 @@ +from inspect import stack +from os.path import basename +from sys import version_info +from unittest import main as unittest_main, TestCase +from unittest.mock import call + +from ittapi_native_mock import patch as ittapi_native_patch +import ittapi + + +class TaskCreationTests(TestCase): + @ittapi_native_patch('Domain') + @ittapi_native_patch('StringHandle') + @ittapi_native_patch('Id') + def test_task_creation_with_default_constructor(self, domain_mock, string_handle_mock, id_mock): + domain_mock.return_value = 'ittapi' + string_handle_mock.side_effect = lambda x: x + id_mock.return_value = 1 + + task = ittapi.task() + caller = stack()[0] + expected_name = f'{basename(caller.filename)}:{caller.lineno-1}' + + string_handle_mock.assert_called_once_with(expected_name) + domain_mock.assert_called_once_with(None) + + self.assertEqual(task.name(), expected_name) + self.assertEqual(task.domain(), domain_mock.return_value) + self.assertEqual(task.id(), id_mock.return_value) + self.assertIsNone(task.parent_id()) + + @ittapi_native_patch('StringHandle') + def test_task_creation_as_decorator_for_function(self, string_handle_mock): + @ittapi.task + def my_function(): + pass # pragma: no cover + + string_handle_mock.assert_called_once_with(my_function.__qualname__) + + @ittapi_native_patch('StringHandle') + def test_task_creation_as_decorator_with_empty_arguments_for_function(self, string_handle_mock): + @ittapi.task() + def my_function(): + pass # pragma: no cover + + string_handle_mock.assert_called_with(my_function.__qualname__) + + @ittapi_native_patch('StringHandle') + def test_task_creation_as_decorator_with_name_for_function(self, string_handle_mock): + @ittapi.task('my function') + def my_function(): + pass # pragma: no cover + + string_handle_mock.assert_called_once_with('my function') + + @ittapi_native_patch('Domain') + def test_task_creation_as_decorator_with_domain_for_function(self, domain_mock): + @ittapi.task(domain='my domain') + def my_function(): + pass # pragma: no cover + + domain_mock.assert_called_once_with('my domain') + + @ittapi_native_patch('StringHandle') + def test_task_creation_as_decorator_with_empty_args_and_name_for_function(self, string_handle_mock): + @ittapi.task + @ittapi.task('my function') + def my_function(): + pass # pragma: no cover + + expected_calls = [call('my function'), + call(my_function.__qualname__)] + string_handle_mock.assert_has_calls(expected_calls) + + @ittapi_native_patch('Domain') + @ittapi_native_patch('StringHandle') + def test_task_creation_with_default_constructor_as_context_manager(self, domain_mock, string_handle_mock): + caller = stack()[0] + with ittapi.task(): + pass + + string_handle_mock.assert_called_once_with(f'{basename(caller.filename)}:{caller.lineno+1}') + domain_mock.assert_called_once_with(None) + + @ittapi_native_patch('Domain') + @ittapi_native_patch('StringHandle') + def test_task_creation_with_name_and_domain_as_context_manager(self, domain_mock, string_handle_mock): + with ittapi.task('my task', 'my domain'): + pass + + string_handle_mock.assert_called_once_with('my task') + domain_mock.assert_called_once_with('my domain') + + @ittapi_native_patch('Domain') + @ittapi_native_patch('Id') + @ittapi_native_patch('StringHandle') + def test_task_creation_for_callable_object(self, domain_mock, id_mock, string_handle_mock): + domain_mock.return_value = 'domain' + string_handle_mock.side_effect = lambda x: x + id_mock.return_value = 1 + + class CallableClass: + def __call__(self, *args, **kwargs): + pass # pragma: no cover + + task = ittapi.task(CallableClass()) + + expected_name = f'{CallableClass.__name__}.__call__' + string_handle_mock.assert_called_once_with(expected_name) + + self.assertEqual(task.name(), expected_name) + self.assertEqual(task.domain(), domain_mock.return_value) + self.assertEqual(task.id(), id_mock.return_value) + self.assertIsNone(task.parent_id()) + + @ittapi_native_patch('Domain') + @ittapi_native_patch('Id') + @ittapi_native_patch('StringHandle') + def test_unnamed_task_creation_for_callable_object(self, domain_mock, id_mock, string_handle_mock): + domain_mock.return_value = 'domain' + string_handle_mock.side_effect = lambda x: x + id_mock.return_value = 1 + + class CallableClass: + def __call__(self, *args, **kwargs): + pass # pragma: no cover + + caller = stack()[0] + task = ittapi.task() + task(CallableClass()) + + expected_name = f'{CallableClass.__name__}.__call__' + expected_calls = [ + call(f'{basename(caller.filename)}:{caller.lineno+1}'), + call(expected_name) + ] + string_handle_mock.assert_has_calls(expected_calls) + + self.assertEqual(task.name(), expected_name) + self.assertEqual(task.domain(), domain_mock.return_value) + self.assertEqual(task.id(), id_mock.return_value) + self.assertIsNone(task.parent_id()) + + @ittapi_native_patch('StringHandle') + def test_task_creation_for_method(self, string_handle_mock): + class MyClass: + @ittapi.task + def my_method(self): + pass # pragma: no cover + + string_handle_mock.assert_called_once_with(f'{MyClass.my_method.__qualname__}') + + +class TaskPropertiesTest(TestCase): + @ittapi_native_patch('Domain') + @ittapi_native_patch('Id') + @ittapi_native_patch('StringHandle') + def test_task_properties(self, domain_mock, id_mock, string_handle_mock): + domain_mock.side_effect = lambda x: x + string_handle_mock.side_effect = lambda x: x + id_mock.return_value = lambda x: x + + class CallableClass: + def __call__(self, *args, **kwargs): + pass # pragma: no cover + + domain_name = 'my domain' + task_id = 2 + parent_id = 1 + task = ittapi.task(CallableClass(), domain=domain_name, id=task_id, parent=parent_id) + + expected_name = f'{CallableClass.__name__}.__call__' + string_handle_mock.assert_called_once_with(expected_name) + + self.assertEqual(task.name(), expected_name) + self.assertEqual(task.domain(), domain_name) + self.assertEqual(task.id(), task_id) + self.assertEqual(task.parent_id(), parent_id) + + self.assertEqual(str(task), f"{{ name: '{str(expected_name)}', domain: '{str(domain_name)}'," + f" id: {str(task_id)}, parent_id: {str(parent_id)} }}") + + self.assertEqual(repr(task), f'{task.__class__.__name__}({repr(expected_name)}, {repr(domain_name)},' + f' {repr(task_id)}, {repr(parent_id)})') + + +class TaskExecutionTests(TestCase): + @ittapi_native_patch('Domain') + @ittapi_native_patch('Id') + @ittapi_native_patch('StringHandle') + @ittapi_native_patch('task_begin') + @ittapi_native_patch('task_end') + def test_task_for_function(self, domain_mock, id_mock, string_handle_mock, task_begin_mock, task_end_mock): + domain_mock.return_value = 'domain_handle' + string_handle_mock.return_value = 'string_handle' + id_mock.return_value = 'id_handle' + + @ittapi.task + def my_function(): + return 42 + + domain_mock.assert_called_once_with(None) + string_handle_mock.assert_called_once_with(my_function.__qualname__) + id_mock.assert_called_once_with(domain_mock.return_value) + + self.assertEqual(my_function(), 42) + + task_begin_mock.assert_called_once_with(domain_mock.return_value, string_handle_mock.return_value, + id_mock.return_value, None) + task_end_mock.assert_called_once_with(domain_mock.return_value) + + @ittapi_native_patch('Domain') + @ittapi_native_patch('Id') + @ittapi_native_patch('StringHandle') + @ittapi_native_patch('task_begin') + @ittapi_native_patch('task_end') + def test_nested_tasks_for_function(self, domain_mock, id_mock, string_handle_mock, task_begin_mock, task_end_mock): + domain_mock.return_value = 'domain_handle' + string_handle_mock.side_effect = lambda x: x + id_mock.return_value = 'id_handle' + + @ittapi.task + @ittapi.task('my function') + def my_function(): + return 42 + + expected_calls = [call('my function'), + call(my_function.__qualname__)] + string_handle_mock.assert_has_calls(expected_calls) + + self.assertEqual(my_function(), 42) + + expected_calls = [call(domain_mock.return_value, my_function.__qualname__, id_mock.return_value, None), + call(domain_mock.return_value, 'my function', id_mock.return_value, None)] + task_begin_mock.assert_has_calls(expected_calls) + + expected_calls = [call(domain_mock.return_value), + call(domain_mock.return_value)] + task_end_mock.assert_has_calls(expected_calls) + + @ittapi_native_patch('Domain') + @ittapi_native_patch('Id') + @ittapi_native_patch('StringHandle') + @ittapi_native_patch('task_begin') + @ittapi_native_patch('task_end') + def test_task_as_context_manager(self, domain_mock, id_mock, string_handle_mock, task_begin_mock, task_end_mock): + domain_mock.return_value = 'domain_handle' + string_handle_mock.side_effect = lambda x: x + id_mock.return_value = 'id_handle' + + region_name = 'my region' + with ittapi.task(region_name): + pass + + domain_mock.assert_called_once_with(None) + string_handle_mock.assert_called_once_with(region_name) + id_mock.assert_called_once_with(domain_mock.return_value) + + task_begin_mock.assert_called_once_with(domain_mock.return_value, region_name, id_mock.return_value, None) + task_end_mock.assert_called_once_with(domain_mock.return_value) + + @ittapi_native_patch('Domain') + @ittapi_native_patch('Id') + @ittapi_native_patch('StringHandle') + @ittapi_native_patch('task_begin') + @ittapi_native_patch('task_end') + def test_task_for_callable_object(self, domain_mock, id_mock, string_handle_mock, task_begin_mock, task_end_mock): + domain_mock.return_value = 'domain_handle' + string_handle_mock.return_value = 'string_handle' + id_mock.return_value = 'id_handle' + + class CallableClass: + def __call__(self, *args, **kwargs): + return 42 + + callable_object = ittapi.task(CallableClass()) + string_handle_mock.assert_called_once_with(f'{CallableClass.__name__}.__call__') + + self.assertEqual(callable_object(), 42) + + task_begin_mock.assert_called_once_with(domain_mock.return_value, string_handle_mock.return_value, + id_mock.return_value, None) + task_end_mock.assert_called_once_with(domain_mock.return_value) + + def test_task_for_multiple_callable_objects(self): + class CallableClass: + def __call__(self, *args, **kwargs): + pass + + task = ittapi.task() + task(CallableClass()) + + with self.assertRaises(RuntimeError) as context: + task(CallableClass()) + + self.assertEqual(str(context.exception), 'A custom name for a code region must be specified before' + ' _NamedRegion.__call__() can be called more than once.') + + def test_task_for_noncallable_object(self): + with self.assertRaises(TypeError) as context: + ittapi.task()(42) + + self.assertEqual(str(context.exception), 'Callable object is expected as a first argument.') + + @ittapi_native_patch('Domain') + @ittapi_native_patch('Id') + @ittapi_native_patch('StringHandle') + @ittapi_native_patch('task_begin') + @ittapi_native_patch('task_end') + def test_task_for_method(self, domain_mock, id_mock, string_handle_mock, task_begin_mock, task_end_mock): + domain_mock.return_value = 'domain_handle' + string_handle_mock.side_effect = lambda x: x + id_mock.return_value = 'id_handle' + + class MyClass: + @ittapi.task + def my_method(self): + return 42 + + string_handle_mock.assert_called_once_with(f'{MyClass.my_method.__qualname__}') + + my_object = MyClass() + self.assertEqual(my_object.my_method(), 42) + + task_begin_mock.assert_called_once_with(domain_mock.return_value, f'{MyClass.my_method.__qualname__}', + id_mock.return_value, None) + task_end_mock.assert_called_once_with(domain_mock.return_value) + + @ittapi_native_patch('Domain') + @ittapi_native_patch('Id') + @ittapi_native_patch('StringHandle') + @ittapi_native_patch('task_begin') + @ittapi_native_patch('task_end') + def test_task_for_class_method(self, domain_mock, id_mock, string_handle_mock, task_begin_mock, task_end_mock): + domain_mock.return_value = 'domain_handle' + string_handle_mock.side_effect = lambda x: x + id_mock.return_value = 'id_handle' + + class MyClass: + @classmethod + @ittapi.task + def my_class_method(cls): + return 42 + + string_handle_mock.assert_called_once_with(f'{MyClass.my_class_method.__qualname__}') + + self.assertEqual(MyClass.my_class_method(), 42) + + task_begin_mock.assert_called_once_with(domain_mock.return_value, f'{MyClass.my_class_method.__qualname__}', + id_mock.return_value, None) + task_end_mock.assert_called_once_with(domain_mock.return_value) + + @ittapi_native_patch('Domain') + @ittapi_native_patch('Id') + @ittapi_native_patch('StringHandle') + @ittapi_native_patch('task_begin') + @ittapi_native_patch('task_end') + def test_task_for_static_method(self, domain_mock, id_mock, string_handle_mock, task_begin_mock, task_end_mock): + domain_mock.return_value = 'domain_handle' + string_handle_mock.side_effect = lambda x: x + id_mock.return_value = 'id_handle' + + class MyClass: + @staticmethod + @ittapi.task + def my_static_method(): + return 42 + + string_handle_mock.assert_called_once_with(f'{MyClass.my_static_method.__qualname__}') + + self.assertEqual(MyClass.my_static_method(), 42) + + task_begin_mock.assert_called_once_with(domain_mock.return_value, f'{MyClass.my_static_method.__qualname__}', + id_mock.return_value, None) + task_end_mock.assert_called_once_with(domain_mock.return_value) + + task_begin_mock.reset_mock() + task_end_mock.reset_mock() + + self.assertEqual(MyClass().my_static_method(), 42) + + task_begin_mock.assert_called_once_with(domain_mock.return_value, f'{MyClass.my_static_method.__qualname__}', + id_mock.return_value, None) + task_end_mock.assert_called_once_with(domain_mock.return_value) + + @ittapi_native_patch('Domain') + @ittapi_native_patch('Id') + @ittapi_native_patch('StringHandle') + @ittapi_native_patch('task_begin') + @ittapi_native_patch('task_end') + def test_task_for_static_method_with_wrong_order_of_decorators(self, domain_mock, id_mock, string_handle_mock, + task_begin_mock, task_end_mock): + domain_mock.return_value = 'domain_handle' + string_handle_mock.side_effect = lambda x: x + id_mock.return_value = 'id_handle' + + class MyClass: + @ittapi.task + @staticmethod + def my_static_method(): + return 42 # pragma: no cover + + if version_info >= (3, 10): + string_handle_mock.assert_called_once_with(f'{MyClass.my_static_method.__qualname__}') + + self.assertEqual(MyClass().my_static_method(), 42) + + task_begin_mock.assert_called_once_with(domain_mock.return_value, + f'{MyClass.my_static_method.__qualname__}', + id_mock.return_value, None) + task_end_mock.assert_called_once_with(domain_mock.return_value) + + task_begin_mock.reset_mock() + task_end_mock.reset_mock() + + self.assertEqual(MyClass.my_static_method(), 42) + + task_begin_mock.assert_called_once_with(domain_mock.return_value, + f'{MyClass.my_static_method.__qualname__}', + id_mock.return_value, None) + task_end_mock.assert_called_once_with(domain_mock.return_value) + else: + # @staticmethod decorator returns a descriptor which is not callable before Python 3.10 + # therefore, it cannot be traced. @staticmethod have to be always above ittapi decorators for Python 3.9 or + # older. otherwise, the exception is thrown. + with self.assertRaises(TypeError) as context: + MyClass().my_static_method() + + self.assertEqual(str(context.exception), 'Callable object is expected to be passed.') + + def test_task_for_class_method_with_wrong_order_of_decorators(self): + # @classmethod decorator returns a descriptor and the descriptor is not callable object, + # therefore, it cannot be traced. @classmethod have to be always above ittapi decorators, + # otherwise, the exception is thrown. + class MyClass: + @ittapi.task + @classmethod + def my_class_method(cls): + return 42 # pragma: no cover + + with self.assertRaises(TypeError) as context: + MyClass().my_class_method() + + self.assertEqual(str(context.exception), 'Callable object is expected to be passed.') + + with self.assertRaises(TypeError) as context: + MyClass.my_class_method() + + self.assertEqual(str(context.exception), 'Callable object is expected to be passed.') + + @ittapi_native_patch('Domain') + @ittapi_native_patch('Id') + @ittapi_native_patch('StringHandle') + @ittapi_native_patch('task_begin') + @ittapi_native_patch('task_end') + def test_task_for_function_raised_exception(self, domain_mock, id_mock, string_handle_mock, + task_begin_mock, task_end_mock): + domain_mock.return_value = 'domain_handle' + string_handle_mock.return_value = 'string_handle' + id_mock.return_value = 'id_handle' + + exception_msg = 'ValueError exception from my_function' + + @ittapi.task + def my_function(): + raise ValueError(exception_msg) + + domain_mock.assert_called_once_with(None) + string_handle_mock.assert_called_once_with(my_function.__qualname__) + id_mock.assert_called_once_with(domain_mock.return_value) + + with self.assertRaises(ValueError) as context: + my_function() + + self.assertEqual(str(context.exception), exception_msg) + + task_begin_mock.assert_called_once_with(domain_mock.return_value, string_handle_mock.return_value, + id_mock.return_value, None) + task_end_mock.assert_called_once_with(domain_mock.return_value) + + @ittapi_native_patch('Domain') + @ittapi_native_patch('Id') + @ittapi_native_patch('StringHandle') + @ittapi_native_patch('task_begin') + @ittapi_native_patch('task_end') + def test_task_for_method_raised_exception(self, domain_mock, id_mock, string_handle_mock, + task_begin_mock, task_end_mock): + domain_mock.return_value = 'domain_handle' + string_handle_mock.side_effect = lambda x: x + id_mock.return_value = 'id_handle' + + exception_msg = 'ValueError exception from my_method' + + class MyClass: + @ittapi.task + def my_method(self): + raise ValueError(exception_msg) + + string_handle_mock.assert_called_once_with(f'{MyClass.my_method.__qualname__}') + + with self.assertRaises(ValueError) as context: + MyClass().my_method() + + self.assertEqual(str(context.exception), exception_msg) + + task_begin_mock.assert_called_once_with(domain_mock.return_value, f'{MyClass.my_method.__qualname__}', + id_mock.return_value, None) + task_end_mock.assert_called_once_with(domain_mock.return_value) + + @ittapi_native_patch('Domain') + @ittapi_native_patch('Id') + @ittapi_native_patch('StringHandle') + @ittapi_native_patch('task_begin_overlapped') + @ittapi_native_patch('task_end_overlapped') + def test_overlapped_tasks(self, domain_mock, id_mock, string_handle_mock, + task_begin_overlapped_mock, task_end_overlapped_mock): + domain_mock.return_value = 'domain_handle' + string_handle_mock.side_effect = lambda x: x + + id_value = 0 + + def id_generator(*args, **kwargs): # pylint: disable=W0613 + nonlocal id_value + id_value += 1 + return id_value + + id_mock.side_effect = id_generator + + overlapped_task_1_name = 'overlapped task 1' + overlapped_task_2_name = 'overlapped task 2' + + overlapped_task_1 = ittapi.task(overlapped_task_1_name, overlapped=True) + overlapped_task_1.begin() + + overlapped_task_2 = ittapi.task(overlapped_task_2_name, overlapped=True) + overlapped_task_2.begin() + + overlapped_task_1.end() + overlapped_task_2.end() + + expected_calls = [ + call(overlapped_task_1_name), + call(overlapped_task_2_name) + ] + string_handle_mock.assert_has_calls(expected_calls) + + expected_calls = [ + call(domain_mock.return_value, overlapped_task_1_name, 1, None), + call(domain_mock.return_value, overlapped_task_2_name, 2, None) + ] + task_begin_overlapped_mock.assert_has_calls(expected_calls) + + expected_calls = [ + call(domain_mock.return_value, 1), + call(domain_mock.return_value, 2) + ] + task_end_overlapped_mock.assert_has_calls(expected_calls) + + +class NestedTaskCreationTests(TestCase): + @ittapi_native_patch('Domain') + @ittapi_native_patch('StringHandle') + def test_task_creation_with_default_constructor(self, domain_mock, string_handle_mock): + ittapi.nested_task() + caller = stack()[0] + string_handle_mock.assert_called_once_with(f'{basename(caller.filename)}:{caller.lineno-1}') + domain_mock.assert_called_once_with(None) + + +class OverlappedTaskCreationTests(TestCase): + @ittapi_native_patch('Domain') + @ittapi_native_patch('StringHandle') + def test_task_creation_with_default_constructor(self, domain_mock, string_handle_mock): + ittapi.overlapped_task() + caller = stack()[0] + string_handle_mock.assert_called_once_with(f'{basename(caller.filename)}:{caller.lineno-1}') + domain_mock.assert_called_once_with(None) + + +if __name__ == '__main__': + unittest_main() # pragma: no cover diff --git a/python/utest/test_thread_naming.py b/python/utest/test_thread_naming.py new file mode 100644 index 0000000..81d3777 --- /dev/null +++ b/python/utest/test_thread_naming.py @@ -0,0 +1,16 @@ +from unittest import main as unittest_main, TestCase + +from ittapi_native_mock import patch as ittapi_native_patch +import ittapi + + +class ThreadNamingTests(TestCase): + @ittapi_native_patch('thread_set_name') + def test_string_handle_call(self, thread_set_name_mock): + name = 'my thread' + ittapi.thread_set_name(name) + thread_set_name_mock.assert_called_once_with(name) + + +if __name__ == '__main__': + unittest_main() # pragma: no cover