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