From 7c343eaaa75b4dbb28bcde31a4fddc9d8dcad50b Mon Sep 17 00:00:00 2001
From: Rosen Matev <rosen.matev@cern.ch>
Date: Fri, 6 Jan 2023 23:03:49 +0100
Subject: [PATCH] Add algorithm to programmatically enable/disable perf

---
 GaudiProfiling/CMakeLists.txt                 | 12 +++
 .../src/component/perf/PerfProfile.cpp        | 78 +++++++++++++++++++
 .../tests/pytest/test_perf_profile.py         | 51 ++++++++++++
 3 files changed, 141 insertions(+)
 create mode 100644 GaudiProfiling/src/component/perf/PerfProfile.cpp
 create mode 100644 GaudiProfiling/tests/pytest/test_perf_profile.py

diff --git a/GaudiProfiling/CMakeLists.txt b/GaudiProfiling/CMakeLists.txt
index a3da42da5e..8f39c939d4 100644
--- a/GaudiProfiling/CMakeLists.txt
+++ b/GaudiProfiling/CMakeLists.txt
@@ -111,6 +111,16 @@ gaudi_add_module(GaudiJemalloc
                  LINK GaudiKernel
                       GaudiAlgLib)
 
+#-----------------------------------
+# Linux perf
+#-----------------------------------
+# TODO: The PerfProfile algorithm should only be compiled for Linux
+#       (and consequently the test should only run if PerfProfile is there).
+gaudi_add_module(GaudiPerf
+                 SOURCES src/component/perf/PerfProfile.cpp
+                 LINK GaudiKernel
+                      GaudiAlgLib)
+
 # Special handling of unresolved symbols in Jemmalloc.
 # The profilers need to have libjemalloc.so pre-loaded to
 # work, so it's better if the symbols stay undefined in case somebody tries to
@@ -122,6 +132,8 @@ if(GAUDI_USE_JEMALLOC)
            COMMAND run ${CMAKE_COMMAND} -E env LD_PRELOAD=$<TARGET_PROPERTY:jemalloc::jemalloc,LOCATION> gaudirun.py)
 endif()
 
+gaudi_add_tests(pytest)
+
 # Install python modules
 gaudi_install(PYTHON)
 # Install other scripts
diff --git a/GaudiProfiling/src/component/perf/PerfProfile.cpp b/GaudiProfiling/src/component/perf/PerfProfile.cpp
new file mode 100644
index 0000000000..a3914e49e9
--- /dev/null
+++ b/GaudiProfiling/src/component/perf/PerfProfile.cpp
@@ -0,0 +1,78 @@
+/***********************************************************************************\
+* (c) Copyright 1998-2023 CERN for the benefit of the LHCb and ATLAS collaborations *
+*                                                                                   *
+* This software is distributed under the terms of the Apache version 2 licence,     *
+* copied verbatim in the file "LICENSE".                                            *
+*                                                                                   *
+* In applying this licence, CERN does not waive the privileges and immunities       *
+* granted to it by virtue of its status as an Intergovernmental Organization        *
+* or submit itself to any jurisdiction.                                             *
+\***********************************************************************************/
+#include "GaudiAlg/Consumer.h"
+#include <fcntl.h>
+#include <string_view>
+#include <sys/stat.h>
+
+/** Algorithm to enable/disable profiling with Linux perf at given events.
+ *
+ *  Needs at least perf 5.9. To control perf record, start it as
+ *
+ *    perf record -D -1 --control fifo:GaudiPerfProfile.fifo ... gaudirun.py ...
+ *
+ *  The path to the control fifo (GaudiPerfProfile.fifo) is configurable
+ *  with the FIFOPath property. The fifo must be created before running perf.
+ *
+ */
+struct PerfProfile final : Gaudi::Functional::Consumer<void()> {
+  using Consumer::Consumer;
+
+  StatusCode initialize() override {
+    return Consumer::initialize().andThen( [this]() {
+      m_fifo = ::open( m_fifoPath.value().c_str(), O_WRONLY | O_NONBLOCK );
+      if ( m_fifo == -1 ) {
+        fatal() << "open(\"" << m_fifoPath.value() << "\"): " << ::strerror( errno ) << endmsg;
+        return StatusCode::FAILURE;
+      }
+      return StatusCode::SUCCESS;
+    } );
+  }
+
+  StatusCode finalize() override {
+    if ( m_fifo != -1 ) { ::close( m_fifo ); }
+    return Consumer::finalize();
+  }
+
+  void operator()() const override {
+    // We could use EventContext::evt(), however it is not always valid, as is the case with EvtSel="NONE". Instead, use
+    // an atomic counter.
+    auto eventNumber = m_eventNumber++;
+    if ( eventNumber == m_nStartFromEvent.value() ) {
+      warning() << "Starting perf profile at event " << eventNumber << endmsg;
+      fifo_write( "enable\n" );
+    }
+
+    if ( m_nStopAtEvent > 0 && eventNumber == m_nStopAtEvent.value() ) {
+      warning() << "Stopping perf profile at event " << eventNumber << endmsg;
+      fifo_write( "disable\n" );
+    }
+  }
+
+private:
+  void fifo_write( std::string_view s ) const {
+    if ( ::write( m_fifo, s.data(), s.size() ) < ssize_t( s.size() ) ) {
+      error() << "Write of \"" << s << "\" to FIFO failed: " << ::strerror( errno ) << endmsg;
+    }
+  }
+
+  mutable std::atomic<long unsigned> m_eventNumber = 0;
+  int                                m_fifo        = -1;
+
+  Gaudi::Property<std::string>   m_fifoPath{ this, "FIFOPath", "GaudiPerfProfile.fifo", "Path to perf control FIFO." };
+  Gaudi::Property<unsigned long> m_nStartFromEvent{ this, "StartFromEventN", 1,
+                                                    "After what event we start profiling." };
+  Gaudi::Property<unsigned long> m_nStopAtEvent{
+      this, "StopAtEventN", 0,
+      "After what event we stop profiling. If 0 than we also profile finalization stage. Default = 0." };
+};
+
+DECLARE_COMPONENT( PerfProfile )
diff --git a/GaudiProfiling/tests/pytest/test_perf_profile.py b/GaudiProfiling/tests/pytest/test_perf_profile.py
new file mode 100644
index 0000000000..ae7b5b2827
--- /dev/null
+++ b/GaudiProfiling/tests/pytest/test_perf_profile.py
@@ -0,0 +1,51 @@
+#####################################################################################
+# (c) Copyright 2022-2023 CERN for the benefit of the LHCb and ATLAS collaborations #
+#                                                                                   #
+# This software is distributed under the terms of the Apache version 2 licence,     #
+# copied verbatim in the file "LICENSE".                                            #
+#                                                                                   #
+# In applying this licence, CERN does not waive the privileges and immunities       #
+# granted to it by virtue of its status as an Intergovernmental Organization        #
+# or submit itself to any jurisdiction.                                             #
+#####################################################################################
+import os
+from threading import Thread
+
+from GaudiTests import run_gaudi
+
+
+def config():
+    from Configurables import ApplicationMgr, PerfProfile
+
+    prof = PerfProfile(
+        FIFOPath="control.fifo",
+        StartFromEventN=3,
+        StopAtEventN=8,
+    )
+    ApplicationMgr(
+        EvtSel="NONE",
+        EvtMax=10,
+        TopAlg=[prof],
+    )
+
+
+def test(tmp_path):
+    """Emulate perf record --control"""
+    fifo = tmp_path / "control.fifo"
+    fifo_lines = []
+    os.mkfifo(fifo)
+
+    def reader():
+        # open blocks until FIFO is opened for writing by PerfProfile::initialize(), so run it in a thread
+        with open(fifo) as f:
+            for line in f:
+                fifo_lines.append(line)
+
+    t = Thread(target=reader)
+    t.start()
+
+    run_gaudi(f"{__file__}:config", check=True, cwd=tmp_path)
+
+    t.join()
+
+    assert fifo_lines == ["enable\n", "disable\n"]
-- 
GitLab