diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt
index 8341084c..5665725c 100644
--- a/src/libs/subsonic/CMakeLists.txt
+++ b/src/libs/subsonic/CMakeLists.txt
@@ -58,3 +58,6 @@ target_link_libraries(lmssubsonic PUBLIC
install(TARGETS lmssubsonic DESTINATION ${CMAKE_INSTALL_LIBDIR})
+if (BUILD_BENCHMARKS)
+ add_subdirectory(bench)
+endif()
\ No newline at end of file
diff --git a/src/libs/subsonic/bench/CMakeLists.txt b/src/libs/subsonic/bench/CMakeLists.txt
new file mode 100644
index 00000000..a16798c4
--- /dev/null
+++ b/src/libs/subsonic/bench/CMakeLists.txt
@@ -0,0 +1,14 @@
+add_executable(bench-subsonic
+ SubsonicBench.cpp
+ )
+
+target_include_directories(bench-subsonic PRIVATE
+ ../impl
+ )
+
+target_link_libraries(bench-subsonic PRIVATE
+ lmscore
+ lmsdatabase
+ lmssubsonic
+ benchmark
+ )
\ No newline at end of file
diff --git a/src/libs/subsonic/bench/SubsonicBench.cpp b/src/libs/subsonic/bench/SubsonicBench.cpp
new file mode 100644
index 00000000..123bc39a
--- /dev/null
+++ b/src/libs/subsonic/bench/SubsonicBench.cpp
@@ -0,0 +1,79 @@
+/*
+ * 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 "SubsonicResponse.hpp"
+
+namespace lms::api::subsonic::benchs
+{
+ namespace
+ {
+ Response generateFakeResponse()
+ {
+ Response response{ Response::createOkResponse(defaultServerProtocolVersion) };
+
+ Response::Node& node{ response.createNode("MyNode") };
+ node.setAttribute("Attr1", "value1");
+ node.setAttribute("Attr2", "value2");
+
+ for (std::size_t i{}; i < 100; ++i)
+ {
+ Response::Node& childNode{ node.createArrayChild("MyArrayChild") };
+ childNode.setAttribute("Attr42", i);
+
+ node.addArrayValue("MyArray1", "value1");
+ node.addArrayValue("MyArray1", "value2");
+ for (std::size_t j{}; j < i; ++j)
+ node.addArrayValue("MyArray2", j);
+ }
+
+ return response;
+ }
+ }
+
+ static void BM_SubsonicResponse_generate(benchmark::State& state)
+ {
+ for (auto _ : state)
+ {
+ Response response{ generateFakeResponse() };
+ benchmark::DoNotOptimize(response);
+ TLSMonotonicMemoryResource::getInstance().reset();
+ }
+ }
+
+ template
+ static void BM_SubsonicResponse_serialize(benchmark::State& state)
+ {
+ const Response response{ generateFakeResponse() };
+
+ for (auto _ : state)
+ {
+ std::ostringstream oss; // TODO find something more optimized
+ response.write(oss, responseFormat);
+ }
+ }
+
+ BENCHMARK(BM_SubsonicResponse_generate)->Threads(1)->Threads(std::thread::hardware_concurrency());
+ BENCHMARK(BM_SubsonicResponse_serialize);
+ BENCHMARK(BM_SubsonicResponse_serialize);
+}
+
+BENCHMARK_MAIN();
\ No newline at end of file
diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp
index e709a96b..b7330368 100644
--- a/src/libs/subsonic/impl/SubsonicResource.cpp
+++ b/src/libs/subsonic/impl/SubsonicResource.cpp
@@ -159,7 +159,7 @@ namespace lms::api::subsonic
CheckImplementedFunc checkFunc{};
};
- static const std::unordered_map requestEntryPoints
+ const std::unordered_map requestEntryPoints
{
// System
{"/ping", {handlePingRequest}},
@@ -266,13 +266,26 @@ namespace lms::api::subsonic
};
using MediaRetrievalHandlerFunc = std::function;
- static std::unordered_map mediaRetrievalHandlers
+ const std::unordered_map mediaRetrievalHandlers
{
// Media retrieval
{"/download", handleDownload},
{"/stream", handleStream},
{"/getCoverArt", handleGetCoverArt},
};
+
+ struct TLSMonotonicMemoryResourceCleaner
+ {
+ TLSMonotonicMemoryResourceCleaner() = default;
+ ~TLSMonotonicMemoryResourceCleaner()
+ {
+ TLSMonotonicMemoryResource::getInstance().reset();
+ }
+
+ private:
+ TLSMonotonicMemoryResourceCleaner(const TLSMonotonicMemoryResourceCleaner&) = delete;
+ TLSMonotonicMemoryResourceCleaner& operator=(const TLSMonotonicMemoryResourceCleaner&) = delete;
+ };
}
SubsonicResource::SubsonicResource(db::Db& db)
@@ -287,7 +300,8 @@ namespace lms::api::subsonic
{
static std::atomic curRequestId{};
- const std::size_t requestId{ curRequestId++ };
+ const std::size_t requestId{ curRequestId++ };
+ TLSMonotonicMemoryResourceCleaner memoryResourceCleaner;
LMS_LOG(API_SUBSONIC, DEBUG, "Handling request " << requestId << " '" << request.pathInfo() << "', continuation = " << (request.continuation() ? "true" : "false") << ", params = " << parameterMapToDebugString(request.getParameterMap()));
diff --git a/src/libs/subsonic/impl/SubsonicResponse.cpp b/src/libs/subsonic/impl/SubsonicResponse.cpp
index ea1cc831..f82f7305 100644
--- a/src/libs/subsonic/impl/SubsonicResponse.cpp
+++ b/src/libs/subsonic/impl/SubsonicResponse.cpp
@@ -44,7 +44,7 @@ namespace lms::api::subsonic
void Response::Node::setValue(std::string_view value)
{
assert(_children.empty() && _childrenArrays.empty() && _childrenValues.empty());
- _value = std::string{ value };
+ _value = string{ value };
}
void Response::Node::setValue(long long value)
@@ -55,13 +55,13 @@ namespace lms::api::subsonic
void Response::Node::setAttribute(Key key, std::string_view value)
{
- _attributes[key] = std::string{ value };
+ _attributes[key] = string{ value };
}
void Response::Node::addChild(Key key, Node&& node)
{
assert(!_value);
- assert(_children.find(key) == std::cend(_children));
+ assert(!_children.contains(key));
_children[key] = std::move(node);
}
@@ -69,29 +69,29 @@ namespace lms::api::subsonic
{
assert(!_value);
assert(_children.find(key) == std::cend(_children));
- _childrenArrays.emplace(key, std::vector{});
+ _childrenArrays.emplace(key, vector{});
}
void Response::Node::addArrayChild(Key key, Node&& node)
{
assert(!_value);
- assert(_children.find(key) == std::cend(_children));
+ assert(!_children.contains(key));
_childrenArrays[key].emplace_back(std::move(node));
}
void Response::Node::createEmptyArrayValue(Key key)
{
assert(!_value);
- assert(_children.find(key) == std::cend(_children));
+ assert(!_children.contains(key));
_childrenValues.emplace(key, ValuesType{});
}
void Response::Node::addArrayValue(Key key, std::string_view value)
{
assert(!_value);
- assert(_children.find(key) == std::cend(_children));
+ assert(!_children.contains(key));
auto& values{ _childrenValues[key] };
- values.push_back(std::string{ value });
+ values.emplace_back(string{ value });
assert(std::all_of(std::cbegin(values) + 1, std::cend(values), [&](const ValueType& value) {return value.index() == values.front().index();}));
}
@@ -99,7 +99,7 @@ namespace lms::api::subsonic
{
assert(!_value);
auto& values{ _childrenValues[key] };
- values.push_back(value);
+ values.emplace_back(value);
assert(std::all_of(std::cbegin(values) + 1, std::cend(values), [&](const ValueType& value) {return value.index() == values.front().index();}));
}
@@ -112,7 +112,7 @@ namespace lms::api::subsonic
Response::Node& Response::Node::createArrayChild(Key key)
{
assert(!_value);
- assert(_children.find(key) == std::cend(_children));
+ assert(!_children.contains(key));
_childrenArrays[key].emplace_back();
return _childrenArrays[key].back();
}
@@ -192,8 +192,8 @@ namespace lms::api::subsonic
for (const auto& [key, value] : node._attributes)
{
- if (std::holds_alternative(value))
- res.put("." + std::string{ key.str() }, std::get(value));
+ if (std::holds_alternative(value))
+ res.put("." + std::string{ key.str() }, std::get(value));
else if (std::holds_alternative(value))
res.put("." + std::string{ key.str() }, std::get(value));
else if (std::holds_alternative(value))
@@ -240,7 +240,7 @@ namespace lms::api::subsonic
return res;
};
- boost::property_tree::ptree root{ nodeToPropertyTree(_root) };
+ const boost::property_tree::ptree root{ nodeToPropertyTree(_root) };
boost::property_tree::write_xml(os, root);
}
@@ -337,9 +337,9 @@ namespace lms::api::subsonic
void Response::JsonSerializer::serializeValue(std::ostream& os, const Node::ValueType& value)
{
- if (std::holds_alternative(value))
+ if (std::holds_alternative(value))
{
- serializeEscapedString(os, std::get(value));
+ serializeEscapedString(os, std::get(value));
}
else if (std::holds_alternative(value))
{
diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp
index cd80fe1c..c1ecc59a 100644
--- a/src/libs/subsonic/impl/SubsonicResponse.hpp
+++ b/src/libs/subsonic/impl/SubsonicResponse.hpp
@@ -27,6 +27,7 @@
#include "core/LiteralString.hpp"
#include "RequestContext.hpp"
+#include "SubsonicResponseAllocator.hpp"
namespace lms::api::subsonic
{
@@ -240,14 +241,23 @@ namespace lms::api::subsonic
void setVersionAttribute(ProtocolVersion version);
friend class Response;
- using ValueType = std::variant;
- std::map _attributes;
- std::optional _value;
- std::map _children;
- std::map> _childrenArrays;
- using ValuesType = std::vector;
- std::map _childrenValues;
+ template
+ using map = std::map< Key, Value, std::less, ResponseAllocator>>;
+
+ template
+ using vector = std::vector>;
+
+ using string = std::basic_string, ResponseAllocator>;
+
+ using ValueType = std::variant;
+ map _attributes;
+ std::optional _value;
+ map _children;
+ map> _childrenArrays;
+
+ using ValuesType = vector;
+ map _childrenValues;
};
static Response createOkResponse(ProtocolVersion protocolVersion);
@@ -272,7 +282,7 @@ namespace lms::api::subsonic
{
public:
void serializeNode(std::ostream& os, const Node& node);
- void serializeValue(std::ostream& os, const Node::ValueType& node);
+ void serializeValue(std::ostream& os, const Node::ValueType& value);
void serializeEscapedString(std::ostream&, std::string_view str);
};
diff --git a/src/libs/subsonic/impl/SubsonicResponseAllocator.hpp b/src/libs/subsonic/impl/SubsonicResponseAllocator.hpp
new file mode 100644
index 00000000..b8195a08
--- /dev/null
+++ b/src/libs/subsonic/impl/SubsonicResponseAllocator.hpp
@@ -0,0 +1,75 @@
+/*
+ * 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 "TLSMonotonicMemoryResource.hpp"
+
+namespace lms::api::subsonic
+{
+ // Stateless allocator that uses a shared MemoryResource
+ template
+ class Allocator
+ {
+ public:
+ using value_type = T;
+ using pointer = T*;
+ using const_pointer = const T*;
+ using reference = T&;
+ using const_reference = const T&;
+ using size_type = std::size_t;
+ using difference_type = std::ptrdiff_t;
+
+ constexpr Allocator() noexcept = default;
+
+ template
+ constexpr Allocator(const Allocator&) noexcept {}
+
+ template
+ struct rebind
+ {
+ using other = Allocator;
+ };
+
+ [[nodiscard]] pointer allocate(size_type n)
+ {
+ return reinterpret_cast(MemoryResource::getInstance().allocate(n * sizeof(T), alignof(T)));
+ }
+
+ // Deallocate memory pointed to by p
+ void deallocate(pointer p, std::size_t) noexcept
+ {
+ MemoryResource::getInstance().deallocate(reinterpret_cast(p));
+ }
+ };
+
+ template
+ bool operator==(const Allocator &, const Allocator &)
+ {
+ return true;
+ }
+
+ template
+ bool operator!=(const Allocator &, const Allocator &)
+ {
+ return false;
+ }
+
+ template
+ using ResponseAllocator = Allocator;
+}
\ No newline at end of file
diff --git a/src/libs/subsonic/impl/TLSMonotonicMemoryResource.hpp b/src/libs/subsonic/impl/TLSMonotonicMemoryResource.hpp
new file mode 100644
index 00000000..07e03639
--- /dev/null
+++ b/src/libs/subsonic/impl/TLSMonotonicMemoryResource.hpp
@@ -0,0 +1,103 @@
+/*
+ * 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
+
+namespace lms::api::subsonic
+{
+ class TLSMonotonicMemoryResource
+ {
+ public:
+ static TLSMonotonicMemoryResource& getInstance()
+ {
+ static thread_local TLSMonotonicMemoryResource instance;
+ return instance;
+ }
+
+ TLSMonotonicMemoryResource()
+ {
+ allocateNewBlock();
+ }
+
+ [[nodiscard]] std::byte* allocate(std::size_t byteCount, std::size_t alignment)
+ {
+ std::byte* currentAddrAligned{ computeAlignedAddr(_currentAddr, alignment) };
+
+ if (currentAddrAligned + byteCount > &_currentBlock->back() + 1)
+ {
+ allocateNewBlock();
+ currentAddrAligned = computeAlignedAddr(_currentAddr, alignment);
+ }
+
+ // Requested too many bytes for blockSize!
+ if (currentAddrAligned + byteCount > &_currentBlock->back() + 1)
+ throw std::bad_alloc{};
+
+ assert(currentAddrAligned >= &_currentBlock->front());
+
+ std::byte* res{ currentAddrAligned };
+ _currentAddr = currentAddrAligned + byteCount;
+
+ return res;
+ }
+
+ void deallocate(std::byte*)
+ {
+ // nothing to do!
+ }
+
+ void reset()
+ {
+ // always keep at least one block
+ if (_blocks.size() > 1)
+ _blocks.erase(std::next(std::cbegin(_blocks), 1), std::cend(_blocks));
+
+ _currentBlock = &_blocks.front();
+ _currentAddr = _currentBlock->data();
+ }
+
+ private:
+ void allocateNewBlock()
+ {
+ _currentBlock = &_blocks.emplace_back();
+ _currentAddr = _currentBlock->data();
+ }
+
+ static std::byte* computeAlignedAddr(std::byte* addr, std::size_t alignment) noexcept
+ {
+ const std::uintptr_t addrValue{ reinterpret_cast(addr) };
+
+ const std::uintptr_t mask{ alignment - 1 };
+ const std::uintptr_t adjustment{ (alignment - (addrValue & mask)) & mask };
+ return reinterpret_cast(addr + adjustment);
+ }
+
+ static constexpr std::size_t blockSize{ static_cast(1 * 1024 * 1024) };
+ using BlockType = std::array;
+
+ std::list _blocks;
+ BlockType* _currentBlock{};
+ std::byte* _currentAddr{};
+ };
+}
\ No newline at end of file