diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt
index a1f3590d..69cacb0a 100644
--- a/src/libs/utils/CMakeLists.txt
+++ b/src/libs/utils/CMakeLists.txt
@@ -10,6 +10,7 @@ add_library(lmsutils SHARED
impl/Logger.cpp
impl/NetAddress.cpp
impl/Path.cpp
+ impl/Profiler.cpp
impl/Random.cpp
impl/RecursiveSharedMutex.cpp
impl/StreamLogger.cpp
@@ -42,3 +43,7 @@ install(TARGETS lmsutils DESTINATION lib)
if(BUILD_TESTING)
add_subdirectory(test)
endif()
+
+if (BUILD_BENCHMARKS)
+ add_subdirectory(bench)
+endif()
diff --git a/src/libs/utils/bench/CMakeLists.txt b/src/libs/utils/bench/CMakeLists.txt
new file mode 100644
index 00000000..8f1361c1
--- /dev/null
+++ b/src/libs/utils/bench/CMakeLists.txt
@@ -0,0 +1,9 @@
+
+add_executable(bench-utils
+ ProfilerBench.cpp
+ )
+
+target_link_libraries(bench-utils PRIVATE
+ lmsutils
+ benchmark
+ )
diff --git a/src/libs/utils/bench/ProfilerBench.cpp b/src/libs/utils/bench/ProfilerBench.cpp
new file mode 100644
index 00000000..d8bde857
--- /dev/null
+++ b/src/libs/utils/bench/ProfilerBench.cpp
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include
+#include
+#include
+
+#include "utils/ILogger.hpp"
+#include "utils/IProfiler.hpp"
+#include "utils/StreamLogger.hpp"
+
+
+// Profiler is meant to built/destroyed once
+Service logger{ std::make_unique(std::cout, StreamLogger::allSeverities) };
+Service profiler{ profiling::createProfiler(::profiling::Level::Overview) };
+
+static void BM_Profiler_Overview(benchmark::State& state)
+{
+ for (auto _ : state)
+ {
+ LMS_SCOPED_PROFILE_OVERVIEW("Cat", "Test");
+ }
+}
+
+static void BM_Profiler_Detailed(benchmark::State& state)
+{
+ for (auto _ : state)
+ {
+ // Should do nothing
+ LMS_SCOPED_PROFILE_DETAILED("Cat", "Test");
+ }
+}
+
+BENCHMARK(BM_Profiler_Overview)->Threads(1)->Threads(std::thread::hardware_concurrency());
+BENCHMARK(BM_Profiler_Detailed)->Threads(1)->Threads(std::thread::hardware_concurrency());
+
+BENCHMARK_MAIN();
\ No newline at end of file
diff --git a/src/libs/utils/impl/Profiler.cpp b/src/libs/utils/impl/Profiler.cpp
new file mode 100644
index 00000000..b432108f
--- /dev/null
+++ b/src/libs/utils/impl/Profiler.cpp
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2024 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "Profiler.hpp"
+
+#include
+#include
+#include
+#include "utils/Exception.hpp"
+#include "utils/ILogger.hpp"
+
+namespace profiling
+{
+ namespace
+ {
+ class CurrentThreadUnregisterer
+ {
+ public:
+ CurrentThreadUnregisterer(Profiler* profiler) : _profiler{ profiler } {}
+ ~CurrentThreadUnregisterer()
+ {
+ if (_profiler)
+ _profiler->onThreadPreDestroy();
+ }
+
+ private:
+ CurrentThreadUnregisterer(const CurrentThreadUnregisterer&) = delete;
+ CurrentThreadUnregisterer& operator=(const CurrentThreadUnregisterer&) = delete;
+
+ Profiler* _profiler;
+ };
+ }
+
+ thread_local Profiler::Buffer* Profiler::_currentBuffer{};
+
+ std::unique_ptr createProfiler(Level minLevel, std::size_t bufferSizeInMbytes)
+ {
+ return std::make_unique(minLevel, bufferSizeInMbytes);
+ }
+
+ Profiler::Profiler(Level minLevel, std::size_t bufferSizeinMBytes)
+ : _minLevel{ minLevel }
+ , _start{ clock::now() }
+ , _creatorThreadId{ std::this_thread::get_id() }
+ , _buffers((bufferSizeinMBytes * 1024 * 1024) / BufferSize)
+ {
+ if (bufferSizeinMBytes < MinBufferSizeInMBytes)
+ throw LmsException{ "Profiler must be configured with at least " + std::to_string(MinBufferSizeInMBytes) + " MBytes" };
+
+ for (Buffer& buffer : _buffers)
+ _freeBuffers.push_back(&buffer);
+
+ LMS_LOG(UTILS, INFO, "Profiler: using " << _buffers.size() << " buffers. Buffer size = " << std::to_string(BufferSize));
+ }
+
+ bool Profiler::isLevelActive(Level level) const
+ {
+ return static_cast>(level) <= static_cast>(_minLevel);
+ }
+
+ void Profiler::write(const CompleteEvent& event)
+ {
+ if (!_currentBuffer)
+ _currentBuffer = acquireBuffer();
+
+ _currentBuffer->durationEvents[_currentBuffer->currentDurationIndex] = event;
+
+ // update the index after writing the event, in case another thread wants to dump
+ if (++_currentBuffer->currentDurationIndex == _currentBuffer->durationEvents.size())
+ {
+ releaseBuffer(_currentBuffer);
+ _currentBuffer = nullptr;
+ }
+ }
+
+ void Profiler::onThreadPreDestroy()
+ {
+ if (_currentBuffer)
+ releaseBuffer(_currentBuffer);
+ }
+
+ Profiler::Buffer* Profiler::acquireBuffer()
+ {
+ // We consider the creator thread will survive the profiler (thus we don't want to release anything on thread destruction)
+ static thread_local CurrentThreadUnregisterer currentThreadUnregister{ _creatorThreadId == std::this_thread::get_id() ? nullptr : this };
+
+ std::scoped_lock lock{ _mutex };
+ assert(!_freeBuffers.empty());
+
+ Profiler::Buffer* buffer{ _freeBuffers.front() };
+ _freeBuffers.pop_front();
+
+ // Empty new buffer only now (we want to keep history on released buffers since we dump them)
+ buffer->currentDurationIndex = 0;
+ return buffer;
+ }
+
+ void Profiler::releaseBuffer(Buffer* buffer)
+ {
+ assert(buffer);
+
+ std::scoped_lock lock{ _mutex };
+ _freeBuffers.push_back(buffer);
+ }
+
+ void Profiler::dumpCurrentBuffer(std::ostream& os)
+ {
+ os << "{" << std::endl;
+ os << "\t\"traceEvents\": [" << std::endl;
+
+ // we allow threads to fill in their current block while dumping
+ {
+ os << "\t\t{ ";
+ os << "\"name\" : \"thread_name\", ";
+ os << "\"pid\" : 1, ";
+ os << "\"tid\" : " << _creatorThreadId << ", ";
+ os << "\"ph\" : \"M\", ";
+ os << "\"args\" : { \"name\" : \"MainThread\" }";
+ os << " }";
+
+ std::scoped_lock lock{ _mutex };
+
+ for (Buffer& buffer : _buffers)
+ {
+ // Looks like tracing viewer is not pleased when nested event start at the same timestamp
+ for (std::size_t i{}; i < buffer.currentDurationIndex; ++i)
+ {
+ using clockMicro = std::chrono::duration;
+ const CompleteEvent& event{ buffer.durationEvents[i] };
+
+ os << "," << std::endl;
+ os << "\t\t{ ";
+ os << "\"name\" : \"" << event.name.c_str() << "\", ";
+ os << "\"cat\" : \"" << event.category.c_str() << "\", ";
+ os << "\"pid\": 1, ";
+ os << "\"tid\" : " << event.threadId << ", ";
+ os << "\"ts\" : " << std::fixed << std::setprecision(3) << std::chrono::duration_cast(event.start - _start).count() << ", ";
+ os << "\"dur\" : " << std::fixed << std::setprecision(3) << std::chrono::duration_cast(event.duration).count() << ", ";
+ os << "\"ph\" : \"X\"";
+ os << " }";
+ }
+ }
+ }
+
+ os << std::endl;
+ os << "\t]," << std::endl;
+ os << "\t\"meta_cpu_count\" : " << std::thread::hardware_concurrency() << std::endl;
+ os << "}" << std::endl;
+ }
+}
\ No newline at end of file
diff --git a/src/libs/utils/impl/Profiler.hpp b/src/libs/utils/impl/Profiler.hpp
new file mode 100644
index 00000000..e9668068
--- /dev/null
+++ b/src/libs/utils/impl/Profiler.hpp
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include "utils/IProfiler.hpp"
+
+namespace profiling
+{
+ class Profiler : public IProfiler
+ {
+ public:
+ Profiler(Level minLevel, std::size_t bufferSizeinMBytes);
+
+ void onThreadPreDestroy();
+
+ private:
+ bool isLevelActive(Level level) const override;
+ void write(const CompleteEvent& event) override;
+ void dumpCurrentBuffer(std::ostream& os) override;
+
+ static constexpr std::size_t BufferSize{ 32 * 1024 };
+
+ struct alignas(64) Buffer
+ {
+ static constexpr std::size_t CompleteEventCount{ BufferSize / sizeof(CompleteEvent) };
+
+ std::array durationEvents;
+ std::atomic currentDurationIndex{};
+ };
+
+ Buffer* acquireBuffer();
+ void releaseBuffer(Buffer* buffer);
+
+ const Level _minLevel;
+ const clock::time_point _start;
+ const std::thread::id _creatorThreadId;
+
+ std::vector _buffers; // allocated once during construction
+
+ std::mutex _mutex;
+ std::deque _freeBuffers;
+
+ static thread_local Buffer* _currentBuffer;
+ };
+}
\ No newline at end of file
diff --git a/src/libs/utils/include/utils/IProfiler.hpp b/src/libs/utils/include/utils/IProfiler.hpp
new file mode 100644
index 00000000..3ce806a1
--- /dev/null
+++ b/src/libs/utils/include/utils/IProfiler.hpp
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "LiteralString.hpp"
+#include "Service.hpp"
+
+#define LMS_ENABLE_PROFILING 1
+
+#define LMS_CONCAT_IMPL(x, y) x##y
+#define LMS_CONCAT(x, y) LMS_CONCAT_IMPL(x, y)
+
+#if LMS_ENABLE_PROFILING
+#define LMS_SCOPED_PROFILE(CATEGORY, LEVEL, NAME) ::profiling::ScopedEvent LMS_CONCAT(scopedEvent_, __LINE__){ CATEGORY, LEVEL, NAME }
+
+#else
+#define LMS_SCOPED_PROFILE(CATEGORY, LEVEL, NAME) (void)0
+#endif
+
+#define LMS_SCOPED_PROFILE_OVERVIEW(CATEGORY, NAME) LMS_SCOPED_PROFILE(CATEGORY, ::profiling::Level::Overview, NAME)
+#define LMS_SCOPED_PROFILE_DETAILED(CATEGORY, NAME) LMS_SCOPED_PROFILE(CATEGORY, ::profiling::Level::Detailed, NAME)
+
+namespace profiling
+{
+ using clock = std::chrono::steady_clock;
+
+ enum class Level
+ {
+ Overview,
+ Detailed,
+ };
+
+ class IProfiler
+ {
+ public:
+ struct CompleteEvent
+ {
+ clock::time_point start;
+ clock::duration duration;
+ std::thread::id threadId;
+ LiteralString name;
+ LiteralString category;
+ };
+
+ virtual ~IProfiler() = default;
+
+ virtual bool isLevelActive(Level level) const = 0;
+ virtual void write(const CompleteEvent& entry) = 0;
+ virtual void dumpCurrentBuffer(std::ostream& os) = 0;
+ };
+
+ static constexpr std::size_t MinBufferSizeInMBytes = 16;
+ std::unique_ptr createProfiler(Level minLevel = Level::Overview, std::size_t bufferSizeInMbytes = MinBufferSizeInMBytes);
+
+ class ScopedEvent
+ {
+ public:
+ ScopedEvent(LiteralString category, Level level, LiteralString name, IProfiler* profiler = Service::get())
+ {
+ if (profiler && profiler->isLevelActive(level))
+ {
+ _profiler = profiler;
+
+ _event.start = clock::now();
+ _event.threadId = std::this_thread::get_id();
+ _event.name = name;
+ _event.category = category;
+ }
+ else
+ {
+ _profiler = nullptr;
+ }
+ }
+
+ ~ScopedEvent()
+ {
+ if (_profiler)
+ {
+ _event.duration = clock::now() - _event.start;
+ _profiler->write(_event);
+ }
+ }
+
+ private:
+ ScopedEvent(const ScopedEvent&) = delete;
+ ScopedEvent& operator=(const ScopedEvent&) = delete;
+
+ IProfiler* _profiler;
+ IProfiler::CompleteEvent _event;
+ };
+}
\ No newline at end of file
diff --git a/src/libs/utils/test/CMakeLists.txt b/src/libs/utils/test/CMakeLists.txt
index f6de0dbd..b8762ad8 100644
--- a/src/libs/utils/test/CMakeLists.txt
+++ b/src/libs/utils/test/CMakeLists.txt
@@ -4,6 +4,7 @@ add_executable(test-utils
EnumSet.cpp
LiteralString.cpp
Path.cpp
+ Profiler.cpp
RecursiveSharedMutex.cpp
String.cpp
Utils.cpp
diff --git a/src/libs/utils/test/Profiler.cpp b/src/libs/utils/test/Profiler.cpp
new file mode 100644
index 00000000..45e02eed
--- /dev/null
+++ b/src/libs/utils/test/Profiler.cpp
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include
+#include
+#include
+
+#include "utils/IProfiler.hpp"
+
+namespace profiling::tests
+{
+ // not much can be tested with this implementation
+ TEST(Profiler, MultipleThreads)
+ {
+ auto profiler{ createProfiler(Level::Overview) };
+
+ std::vector threads;
+ for (std::size_t i{}; i < 16; ++i)
+ {
+ threads.emplace_back([&]
+ {
+ ScopedEvent loggedEvent{ "MyCategory", Level::Overview, "MyEventLogged", profiler.get() };
+ ScopedEvent notLoggedEvent{ "MyCategory", Level::Detailed, "MyEventNotLogged", profiler.get() };
+ });
+ }
+
+ for (std::thread& t : threads)
+ t.join();
+
+ std::ostringstream oss;
+ profiler->dumpCurrentBuffer(oss);
+
+ EXPECT_NE(oss.str().find("MyEventLogged"), std::string::npos);
+ EXPECT_EQ(oss.str().find("MyEventNotLogged"), std::string::npos);
+ }
+}
\ No newline at end of file