From 6cbd155ad974d1d7c90a2e7060776db88d15d67f Mon Sep 17 00:00:00 2001
From: Robin De Schepper <robin.deschepper93@gmail.com>
Date: Thu, 7 Oct 2021 13:40:15 +0200
Subject: [PATCH] Expose profiler to Python (#1688)

Adds the `profiler_initialize(ctx)` and `profiler_summary()` functions to the Python module and the `profiling` key to `arbor.config()`. closes #1685.
---
 .gitignore                         |   4 +-
 doc/install/build_install.rst      |  23 ++++++-
 doc/python/profiler.rst            |  16 +++++
 python/config.cpp                  |   5 ++
 python/profiler.cpp                |  13 ++++
 python/test/unit/runner.py         |   3 +
 python/test/unit/test_profiling.py | 107 +++++++++++++++++++++++++++++
 7 files changed, 168 insertions(+), 3 deletions(-)
 create mode 100644 python/test/unit/test_profiling.py

diff --git a/.gitignore b/.gitignore
index cb17f702..f2c26f5f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,9 @@
 # intermediate python files
 *.pyc
 
+# python dev env files
+.python-version
+
 # graphviz files generated by executables
 *.dot
 
@@ -83,4 +86,3 @@ dist
 
 # generated image files by Python examples
 python/example/*.svg
-
diff --git a/doc/install/build_install.rst b/doc/install/build_install.rst
index 8fcf077b..95a75fa4 100644
--- a/doc/install/build_install.rst
+++ b/doc/install/build_install.rst
@@ -285,6 +285,12 @@ CMake parameters and flags, follow links to the more detailed descriptions below
 
         cmake -DARB_VECTORIZE=ON -DCMAKE_INSTALL_PREFIX=/opt/arbor
 
+.. topic:: `Release <buildtarget_>`_ mode with profiling enabled
+
+    .. code-block:: bash
+
+        cmake -DARB_WITH_PROFILING=ON
+
 .. _buildtarget:
 
 Build target
@@ -459,7 +465,7 @@ use ``ARB_PYTHON_LIB_PATH`` to specify the location where the Python module is t
     The location of libraries under a prefix in only guaranteed to be standard for Python's global library location.
     Therefore, correct installation of the Python package to any other location using ``CMAKE_INSTALL_PREFIX``,
     such as user directory (e.g. `~/.local`), a Python or Conda virtual environment, may result in installation to a wrong path.
-    
+
     ``python3 -m site --user-site`` (for user installations) or a path from ``python3 -c 'import site; print(site.getsitepackages())'``
     (for virtual environment installation) can be used in combination with ``ARB_PYTHON_LIB_PATH``.
 
@@ -551,6 +557,20 @@ component ``neuroml``. The corresponding CMake library target is ``arbor::arbori
    # ...
    target_link_libraries(myapp arbor::arborio)
 
+.. install-profiling:
+
+Profiling
+---------
+
+Arbor has built in profiling that can report the time spent in each step during
+the simulation that can be toggled with the ``-DARB_WITH_PROFILING`` CMake option:
+
+.. code-block:: bash
+
+  cmake .. -DARB_WITH_PROFILING=ON
+
+By default ``ARB_WITH_PROFILING=OFF``.
+
 
 .. _install:
 
@@ -871,4 +891,3 @@ need to be `updated <install-downloading_>`_.
         git submodule update
     Or download submodules recursively when checking out:
         git clone --recurse-submodules https://github.com/arbor-sim/arbor.git
-
diff --git a/doc/python/profiler.rst b/doc/python/profiler.rst
index 9df7a77b..6940682c 100644
--- a/doc/python/profiler.rst
+++ b/doc/python/profiler.rst
@@ -5,6 +5,22 @@
 Profiler
 ========
 
+If Arbor is built with :ref:`profiling support <install-profiling>` the profiler
+can be initialized after the context is created and a summary is available after
+the simulation has concluded:
+
+.. code-block:: python
+
+  arbor.profiler_initialize(context)
+  simulation.run(tfinal)
+  summary = arbor.profiler_summary()
+  print(summary)
+
+
+
+Meter manager
+=============
+
 Arbor's python module :py:mod:`arbor` has a :class:`meter_manager` for measuring time (and if applicable memory) consumptions of regions of interest in the python code.
 
 Users manually instrument the regions to measure.
diff --git a/python/config.cpp b/python/config.cpp
index 69188a7a..3de7f86d 100644
--- a/python/config.cpp
+++ b/python/config.cpp
@@ -33,6 +33,11 @@ pybind11::dict config() {
     dict[pybind11::str("vectorize")] = pybind11::bool_(true);
 #else
     dict[pybind11::str("vectorize")] = pybind11::bool_(false);
+#endif
+#ifdef ARB_PROFILE_ENABLED
+    dict[pybind11::str("profiling")] = pybind11::bool_(true);
+#else
+    dict[pybind11::str("profiling")] = pybind11::bool_(false);
 #endif
     dict[pybind11::str("version")] = pybind11::str(ARB_VERSION);
     dict[pybind11::str("source")]  = pybind11::str(ARB_SOURCE_ID);
diff --git a/python/profiler.cpp b/python/profiler.cpp
index e08b4402..31fe6b06 100644
--- a/python/profiler.cpp
+++ b/python/profiler.cpp
@@ -4,6 +4,8 @@
 #include <pybind11/stl.h>
 
 #include <arbor/profile/meter_manager.hpp>
+#include <arbor/profile/profiler.hpp>
+#include <arbor/version.hpp>
 
 #include "context.hpp"
 #include "strprintf.hpp"
@@ -52,6 +54,17 @@ void register_profiler(pybind11::module& m) {
             "manager"_a, "context"_a)
         .def("__str__",  [](arb::profile::meter_report& r){return util::pprintf("{}", r);})
         .def("__repr__", [](arb::profile::meter_report& r){return "<arbor.meter_report>";});
+
+#ifdef ARB_PROFILE_ENABLED
+    m.def("profiler_initialize", [](context_shim& ctx) {
+        arb::profile::profiler_initialize(ctx.context);
+    });
+    m.def("profiler_summary", [](){
+        std::stringstream stream;
+        stream << arb::profile::profiler_summary();
+        return stream.str();
+    });
+#endif
 }
 
 } // namespace pyarb
diff --git a/python/test/unit/runner.py b/python/test/unit/runner.py
index 3b12c402..75550dee 100644
--- a/python/test/unit/runner.py
+++ b/python/test/unit/runner.py
@@ -18,6 +18,7 @@ try:
     import test_event_generators
     import test_identifiers
     import test_morphology
+    import test_profiling
     import test_schedules
     import test_spikes
     import test_tests
@@ -32,6 +33,7 @@ except ModuleNotFoundError:
     from test.unit import test_event_generators
     from test.unit import test_identifiers
     from test.unit import test_morphology
+    from test.unit import test_profiling
     from test.unit import test_schedules
     from test.unit import test_spikes
     # add more if needed
@@ -45,6 +47,7 @@ test_modules = [\
     test_event_generators,\
     test_identifiers,\
     test_morphology,\
+    test_profiling,\
     test_schedules,\
     test_spikes,\
 ] # add more if needed
diff --git a/python/test/unit/test_profiling.py b/python/test/unit/test_profiling.py
new file mode 100644
index 00000000..bd0a99b5
--- /dev/null
+++ b/python/test/unit/test_profiling.py
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-
+#
+# test_schedules.py
+
+import unittest
+
+import arbor as arb
+import functools
+
+# to be able to run .py file from child directory
+import sys, os
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
+
+try:
+    import options
+except ModuleNotFoundError:
+    from test import options
+
+"""
+all tests for profiling
+"""
+
+def lazy_skipIf(condition, reason):
+    """
+    Postpone skip evaluation until test is ran by evaluating callable `condition`
+    """
+    def inner_decorator(f):
+        @functools.wraps(f)
+        def wrapped(*args, **kwargs):
+            if condition():
+                raise unittest.SkipTest(reason)
+            else:
+                return f(*args, **kwargs)
+
+        return wrapped
+
+    return inner_decorator
+
+class a_recipe(arb.recipe):
+    def __init__(self):
+        arb.recipe.__init__(self)
+        self.props = arb.neuron_cable_properties()
+        self.trains = [
+                [0.8, 2, 2.1, 3],
+                [0.4, 2, 2.2, 3.1, 4.5],
+                [0.2, 2, 2.8, 3]]
+
+    def num_cells(self):
+        return 3
+
+    def cell_kind(self, gid):
+        return arb.cell_kind.spike_source
+
+    def connections_on(self, gid):
+        return []
+
+    def event_generators(self, gid):
+        return []
+
+    def global_properties(self, kind):
+        return self.the_props
+
+    def probes(self, gid):
+        return []
+
+    def cell_description(self, gid):
+        return arb.spike_source_cell("src", arb.explicit_schedule(self.trains[gid]))
+
+def skipWithoutSupport():
+    return not bool(arb.config().get("profiling", False))
+
+class TestProfiling(unittest.TestCase):
+    def test_support(self):
+        self.assertTrue("profiling" in arb.config(), 'profiling key not in config')
+        profiling_support = arb.config()["profiling"]
+        self.assertEqual(bool, type(profiling_support), 'profiling flag should be bool')
+        if profiling_support:
+            self.assertTrue(hasattr(arb, "profiler_initialize"), 'missing profiling interface with profiling support')
+            self.assertTrue(hasattr(arb, "profiler_summary"), 'missing profiling interface with profiling support')
+        else:
+            self.assertFalse(hasattr(arb, "profiler_initialize"), 'profiling interface without profiling support')
+            self.assertFalse(hasattr(arb, "profiler_summary"), 'profiling interface without profiling support')
+
+    @lazy_skipIf(skipWithoutSupport, "run test only with profiling support")
+    def test_summary(self):
+        context = arb.context()
+        arb.profiler_initialize(context)
+        recipe = a_recipe()
+        dd = arb.partition_load_balance(recipe, context)
+        arb.simulation(recipe, dd, context).run(1)
+        summary = arb.profiler_summary()
+        self.assertEqual(str, type(summary), 'profiler summary must be str')
+        self.assertTrue(summary, 'empty summary')
+
+def suite():
+    # specify class and test functions in tuple (here: all tests starting with 'test' from classes RegularSchedule, ExplicitSchedule and PoissonSchedule
+    suite = unittest.TestSuite()
+    suite.addTests(unittest.makeSuite(TestProfiling, ('test')))
+    return suite
+
+def run():
+    v = options.parse_arguments().verbosity
+    runner = unittest.TextTestRunner(verbosity = v)
+    runner.run(suite())
+
+if __name__ == "__main__":
+    run()
-- 
GitLab