From cd8f96f21ee0e45d7b7833929adb29676776b11e Mon Sep 17 00:00:00 2001 From: Douglas Adler Date: Fri, 1 May 2026 14:52:43 -0500 Subject: [PATCH 01/11] Add local VM runtime patches --- cmake/wpeframeworkcom-config.cmake | 51 ++++++ cmake/wpeframeworkcore-config.cmake | 51 ++++++ media/client/main/source/ClientController.cpp | 1 + .../gstplayer/source/GstGenericPlayer.cpp | 153 +++++++++++++++++- media/server/main/interface/IMainThread.h | 2 + 5 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 cmake/wpeframeworkcom-config.cmake create mode 100644 cmake/wpeframeworkcore-config.cmake diff --git a/cmake/wpeframeworkcom-config.cmake b/cmake/wpeframeworkcom-config.cmake new file mode 100644 index 000000000..a83d16276 --- /dev/null +++ b/cmake/wpeframeworkcom-config.cmake @@ -0,0 +1,51 @@ +# +# If not stated otherwise in this file or this component's LICENSE file the +# following copyright and licenses apply: +# +# Copyright 2024 Sky UK +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +set(WPEFRAMEWORK_COM_VERSION 1.0.0) + + +####### Expanded from @PACKAGE_INIT@ by configure_package_config_file() ####### +####### Any changes to this file will be overwritten by the next CMake run #### +####### The input file was wpeframeworkcom-config.cmake.in ######## + +get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../../rialto-build/install" ABSOLUTE) + +macro(set_and_check _var _file) + set(${_var} "${_file}") + if(NOT EXISTS "${_file}") + message(FATAL_ERROR "File or directory ${_file} referenced by variable ${_var} does not exist !") + endif() +endmacro() + +macro(check_required_components _NAME) + foreach(comp ${${_NAME}_FIND_COMPONENTS}) + if(NOT ${_NAME}_${comp}_FOUND) + if(${_NAME}_FIND_REQUIRED_${comp}) + set(${_NAME}_FOUND FALSE) + endif() + endif() + endforeach() +endmacro() + +#################################################################################### + +set_and_check(WPEFRAMEWORK_COM_INCLUDE_DIRS "/home/dadler/development/rialto/rialto/stubs/wpeframework-com/third-party/Source/") + + +check_required_components(WPEFrameworkCOM) diff --git a/cmake/wpeframeworkcore-config.cmake b/cmake/wpeframeworkcore-config.cmake new file mode 100644 index 000000000..c20847e4e --- /dev/null +++ b/cmake/wpeframeworkcore-config.cmake @@ -0,0 +1,51 @@ +# +# If not stated otherwise in this file or this component's LICENSE file the +# following copyright and licenses apply: +# +# Copyright 2024 Sky UK +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +set(WPEFRAMEWORK_CORE_VERSION 1.0.0) + + +####### Expanded from @PACKAGE_INIT@ by configure_package_config_file() ####### +####### Any changes to this file will be overwritten by the next CMake run #### +####### The input file was wpeframeworkcore-config.cmake.in ######## + +get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../../rialto-build/install" ABSOLUTE) + +macro(set_and_check _var _file) + set(${_var} "${_file}") + if(NOT EXISTS "${_file}") + message(FATAL_ERROR "File or directory ${_file} referenced by variable ${_var} does not exist !") + endif() +endmacro() + +macro(check_required_components _NAME) + foreach(comp ${${_NAME}_FIND_COMPONENTS}) + if(NOT ${_NAME}_${comp}_FOUND) + if(${_NAME}_FIND_REQUIRED_${comp}) + set(${_NAME}_FOUND FALSE) + endif() + endif() + endforeach() +endmacro() + +#################################################################################### + +set_and_check(WPEFRAMEWORK_CORE_INCLUDE_DIRS "/home/dadler/development/rialto/rialto/stubs/wpeframework-core/third-party/Source/") + + +check_required_components(WPEFrameworkCore) diff --git a/media/client/main/source/ClientController.cpp b/media/client/main/source/ClientController.cpp index 5f9fe6b6f..1ef44c490 100644 --- a/media/client/main/source/ClientController.cpp +++ b/media/client/main/source/ClientController.cpp @@ -20,6 +20,7 @@ #include "ClientController.h" #include "RialtoClientLogging.h" #include "SharedMemoryHandle.h" +#include #include #include #include diff --git a/media/server/gstplayer/source/GstGenericPlayer.cpp b/media/server/gstplayer/source/GstGenericPlayer.cpp index a374ffb0e..ddfabea84 100644 --- a/media/server/gstplayer/source/GstGenericPlayer.cpp +++ b/media/server/gstplayer/source/GstGenericPlayer.cpp @@ -19,9 +19,13 @@ #include #include +#include +#include #include #include +#include #include +#include #include #include "FlushWatcher.h" @@ -49,6 +53,122 @@ namespace constexpr std::chrono::milliseconds kPositionReportTimerMs{250}; constexpr std::chrono::seconds kSubtitleClockResyncInterval{10}; +std::optional getIntEnv(const char *envName) +{ + const char *value = std::getenv(envName); + if (!value || value[0] == '\0') + { + return std::nullopt; + } + + char *endPtr{nullptr}; + const long parsedValue{std::strtol(value, &endPtr, 10)}; + if (endPtr == value || *endPtr != '\0' || parsedValue < std::numeric_limits::min() || + parsedValue > std::numeric_limits::max()) + { + RIALTO_SERVER_LOG_WARN("Ignoring invalid integer value '%s' from %s", value, envName); + return std::nullopt; + } + + return static_cast(parsedValue); +} + +std::optional getDefaultVideoGeometryFromEnvironment() +{ + const char *rectangleValue = std::getenv("RIALTO_VIDEO_WINDOW_RECTANGLE"); + if (rectangleValue && rectangleValue[0] != '\0') + { + int x{}, y{}, width{}, height{}; + if (std::sscanf(rectangleValue, "%d,%d,%d,%d", &x, &y, &width, &height) == 4) + { + if (width > 0 && height > 0) + { + return firebolt::rialto::server::Rectangle{x, y, width, height}; + } + + RIALTO_SERVER_LOG_WARN("Ignoring invalid %s value '%s' because width/height must be positive", + "RIALTO_VIDEO_WINDOW_RECTANGLE", rectangleValue); + return std::nullopt; + } + + RIALTO_SERVER_LOG_WARN("Ignoring invalid %s value '%s', expected x,y,width,height", + "RIALTO_VIDEO_WINDOW_RECTANGLE", rectangleValue); + return std::nullopt; + } + + const std::optional x = getIntEnv("RIALTO_VIDEO_WINDOW_X"); + const std::optional y = getIntEnv("RIALTO_VIDEO_WINDOW_Y"); + const std::optional width = getIntEnv("RIALTO_VIDEO_WINDOW_WIDTH"); + const std::optional height = getIntEnv("RIALTO_VIDEO_WINDOW_HEIGHT"); + + if (!x && !y && !width && !height) + { + return std::nullopt; + } + + if (!x || !y || !width || !height) + { + RIALTO_SERVER_LOG_WARN("Ignoring incomplete video geometry environment. Set all of %s, %s, %s and %s", + "RIALTO_VIDEO_WINDOW_X", "RIALTO_VIDEO_WINDOW_Y", "RIALTO_VIDEO_WINDOW_WIDTH", + "RIALTO_VIDEO_WINDOW_HEIGHT"); + return std::nullopt; + } + + if (*width <= 0 || *height <= 0) + { + RIALTO_SERVER_LOG_WARN("Ignoring invalid video geometry from environment because width/height must be positive"); + return std::nullopt; + } + + return firebolt::rialto::server::Rectangle{*x, *y, *width, *height}; +} + +bool setRenderRectangleProperty(const std::shared_ptr &gstWrapper, + const std::shared_ptr &glibWrapper, + GstElement *videoSink, const firebolt::rialto::server::Rectangle &rectangle) +{ + GValue renderRectangle = G_VALUE_INIT; + glibWrapper->gValueInit(&renderRectangle, GST_TYPE_ARRAY); + + auto appendCoordinate = [&](int coordinate) { + GValue value = G_VALUE_INIT; + glibWrapper->gValueInit(&value, G_TYPE_INT); + g_value_set_int(&value, coordinate); + gstWrapper->gstValueArrayAppendValue(&renderRectangle, &value); + glibWrapper->gValueUnset(&value); + }; + + appendCoordinate(rectangle.x); + appendCoordinate(rectangle.y); + appendCoordinate(rectangle.width); + appendCoordinate(rectangle.height); + + g_object_set_property(G_OBJECT(videoSink), "render-rectangle", &renderRectangle); + glibWrapper->gValueUnset(&renderRectangle); + return true; +} + +void applyPlaybinSinkOverride(const std::shared_ptr &gstWrapper, + const std::shared_ptr &glibWrapper, + GstElement *pipeline, const char *envName, const char *propertyName) +{ + const char *sinkFactoryName = std::getenv(envName); + if (!sinkFactoryName || sinkFactoryName[0] == '\0') + { + return; + } + + GstElement *sink = gstWrapper->gstElementFactoryMake(sinkFactoryName, sinkFactoryName); + if (!sink) + { + RIALTO_SERVER_LOG_ERROR("Failed to create '%s' from %s", sinkFactoryName, envName); + return; + } + + glibWrapper->gObjectSet(pipeline, propertyName, sink, nullptr); + RIALTO_SERVER_LOG_INFO("Overrode playbin %s with %s from %s", propertyName, sinkFactoryName, envName); +} + bool operator==(const firebolt::rialto::server::SegmentData &lhs, const firebolt::rialto::server::SegmentData &rhs) { return (lhs.position == rhs.position) && (lhs.resetTime == rhs.resetTime) && (lhs.appliedRate == rhs.appliedRate) && @@ -268,6 +388,18 @@ void GstGenericPlayer::initMsePipeline() { // Make playbin m_context.pipeline = m_gstWrapper->gstElementFactoryMake("playbin", "media_pipeline"); + + if (const auto defaultGeometry = getDefaultVideoGeometryFromEnvironment()) + { + m_context.pendingGeometry = *defaultGeometry; + RIALTO_SERVER_LOG_MIL("Loaded default video geometry from environment: x=%d y=%d width=%d height=%d", + defaultGeometry->x, defaultGeometry->y, defaultGeometry->width, + defaultGeometry->height); + } + + applyPlaybinSinkOverride(m_gstWrapper, m_glibWrapper, m_context.pipeline, "RIALTO_PLAYBIN_AUDIO_SINK", "audio-sink"); + applyPlaybinSinkOverride(m_gstWrapper, m_glibWrapper, m_context.pipeline, "RIALTO_PLAYBIN_VIDEO_SINK", "video-sink"); + // Set pipeline flags setPlaybinFlags(true); @@ -1986,18 +2118,31 @@ bool GstGenericPlayer::setVideoSinkRectangle() GstElement *videoSink{getSink(MediaSourceType::VIDEO)}; if (videoSink) { + const Rectangle pendingGeometry = m_context.pendingGeometry; if (m_glibWrapper->gObjectClassFindProperty(G_OBJECT_GET_CLASS(videoSink), "rectangle")) { - std::string rect = - std::to_string(m_context.pendingGeometry.x) + ',' + std::to_string(m_context.pendingGeometry.y) + ',' + - std::to_string(m_context.pendingGeometry.width) + ',' + std::to_string(m_context.pendingGeometry.height); + std::string rect = std::to_string(pendingGeometry.x) + ',' + std::to_string(pendingGeometry.y) + ',' + + std::to_string(pendingGeometry.width) + ',' + + std::to_string(pendingGeometry.height); m_glibWrapper->gObjectSet(videoSink, "rectangle", rect.c_str(), nullptr); + result = true; + } + else if (m_glibWrapper->gObjectClassFindProperty(G_OBJECT_GET_CLASS(videoSink), "render-rectangle")) + { + result = setRenderRectangleProperty(m_gstWrapper, m_glibWrapper, videoSink, pendingGeometry); + } + + if (result) + { + RIALTO_SERVER_LOG_MIL("Applied video geometry x=%d y=%d width=%d height=%d to sink '%s'", + pendingGeometry.x, pendingGeometry.y, pendingGeometry.width, + pendingGeometry.height, GST_ELEMENT_NAME(videoSink)); m_context.pendingGeometry.clear(); result = true; } else { - RIALTO_SERVER_LOG_ERROR("Failed to set the video rectangle"); + RIALTO_SERVER_LOG_ERROR("Failed to set video geometry on sink '%s'", GST_ELEMENT_NAME(videoSink)); } m_gstWrapper->gstObjectUnref(videoSink); } diff --git a/media/server/main/interface/IMainThread.h b/media/server/main/interface/IMainThread.h index 3a920a67a..fa391bfc0 100644 --- a/media/server/main/interface/IMainThread.h +++ b/media/server/main/interface/IMainThread.h @@ -20,6 +20,8 @@ #ifndef FIREBOLT_RIALTO_SERVER_I_MAIN_THREAD_H_ #define FIREBOLT_RIALTO_SERVER_I_MAIN_THREAD_H_ +#include "IMainThread.h" +#include #include #include #include From 68aaf434212c4afa66684d5d9378561f59710b82 Mon Sep 17 00:00:00 2001 From: Douglas Adler Date: Mon, 4 May 2026 10:38:28 -0500 Subject: [PATCH 02/11] Add private metrics IPC sampling Introduce a private metrics module that lets ready clients receive connected and periodic sample requests, report process CPU metrics back to the server, and log combined client/server CPU usage. Wire the module into client and server IPC setup and update client controller tests/mocks for the new dependency. --- media/client/ipc/CMakeLists.txt | 1 + media/client/ipc/include/PrivateMetricsIpc.h | 66 ++++ .../client/ipc/interface/IPrivateMetricsIpc.h | 73 ++++ .../ipc/proto/privatemetricsmodule.proto | 1 + media/client/ipc/source/PrivateMetricsIpc.cpp | 209 +++++++++++ media/client/main/include/ClientController.h | 32 +- media/client/main/source/ClientController.cpp | 70 +++- media/server/ipc/CMakeLists.txt | 1 + .../include/IPrivateMetricsModuleService.h | 59 ++++ .../ipc/include/PrivateMetricsModuleService.h | 102 ++++++ .../ipc/include/SessionManagementServer.h | 3 + .../ipc/proto/privatemetricsmodule.proto | 1 + media/server/ipc/source/IpcFactory.cpp | 2 + .../source/PrivateMetricsModuleService.cpp | 326 ++++++++++++++++++ .../ipc/source/SessionManagementServer.cpp | 5 + proto/CMakeLists.txt | 5 +- proto/privatemetricsmodule.proto | 66 ++++ .../main/clientController/CreateTest.cpp | 18 +- .../clientController/MemoryManagementTest.cpp | 14 +- .../mocks/ipc/PrivateMetricsIpcFactoryMock.h | 40 +++ .../client/mocks/ipc/PrivateMetricsIpcMock.h | 41 +++ 21 files changed, 1124 insertions(+), 11 deletions(-) create mode 100644 media/client/ipc/include/PrivateMetricsIpc.h create mode 100644 media/client/ipc/interface/IPrivateMetricsIpc.h create mode 100644 media/client/ipc/proto/privatemetricsmodule.proto create mode 100644 media/client/ipc/source/PrivateMetricsIpc.cpp create mode 100644 media/server/ipc/include/IPrivateMetricsModuleService.h create mode 100644 media/server/ipc/include/PrivateMetricsModuleService.h create mode 100644 media/server/ipc/proto/privatemetricsmodule.proto create mode 100644 media/server/ipc/source/PrivateMetricsModuleService.cpp create mode 100644 proto/privatemetricsmodule.proto create mode 100644 tests/unittests/media/client/mocks/ipc/PrivateMetricsIpcFactoryMock.h create mode 100644 tests/unittests/media/client/mocks/ipc/PrivateMetricsIpcMock.h diff --git a/media/client/ipc/CMakeLists.txt b/media/client/ipc/CMakeLists.txt index 97d09750e..f41408958 100644 --- a/media/client/ipc/CMakeLists.txt +++ b/media/client/ipc/CMakeLists.txt @@ -29,6 +29,7 @@ add_library ( source/MediaPipelineIpc.cpp source/MediaPipelineCapabilitiesIpc.cpp source/ControlIpc.cpp + source/PrivateMetricsIpc.cpp source/MediaKeysIpc.cpp source/MediaKeysCapabilitiesIpc.cpp source/RialtoCommonIpc.cpp diff --git a/media/client/ipc/include/PrivateMetricsIpc.h b/media/client/ipc/include/PrivateMetricsIpc.h new file mode 100644 index 000000000..dbe67e1fb --- /dev/null +++ b/media/client/ipc/include/PrivateMetricsIpc.h @@ -0,0 +1,66 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_CLIENT_PRIVATE_METRICS_IPC_H_ +#define FIREBOLT_RIALTO_CLIENT_PRIVATE_METRICS_IPC_H_ + +#include "IPrivateMetricsIpc.h" +#include "IEventThread.h" +#include "IpcModule.h" +#include "privatemetricsmodule.pb.h" +#include + +namespace firebolt::rialto::client +{ +class PrivateMetricsIpcFactory : public IPrivateMetricsIpcFactory +{ +public: + PrivateMetricsIpcFactory() = default; + ~PrivateMetricsIpcFactory() override = default; + + std::shared_ptr createPrivateMetricsIpc(IPrivateMetricsIpcClient *client) override; + + static std::shared_ptr createFactory(); +}; + +class PrivateMetricsIpc : public IPrivateMetricsIpc, public IpcModule +{ +public: + PrivateMetricsIpc(IPrivateMetricsIpcClient *client, IIpcClient &ipcClient, + const std::shared_ptr &eventThreadFactory); + ~PrivateMetricsIpc() override; + + bool reportClientMetrics(std::uint64_t sampleId, std::uint32_t reason, const std::string &appName, + std::uint32_t processId, std::uint64_t monotonicTimeMs, std::uint64_t epochTimeMs, + std::uint64_t processCpuTimeMs) override; + +private: + bool notifyClientReady(); + bool createRpcStubs(const std::shared_ptr &ipcChannel) override; + bool subscribeToEvents(const std::shared_ptr &ipcChannel) override; + void onMetricsSampleRequested(const std::shared_ptr &event); + +private: + IPrivateMetricsIpcClient *m_privateMetricsIpcClient; + std::unique_ptr m_eventThread; + std::shared_ptr<::firebolt::rialto::PrivateMetricsModule_Stub> m_privateMetricsStub; +}; +} // namespace firebolt::rialto::client + +#endif // FIREBOLT_RIALTO_CLIENT_PRIVATE_METRICS_IPC_H_ diff --git a/media/client/ipc/interface/IPrivateMetricsIpc.h b/media/client/ipc/interface/IPrivateMetricsIpc.h new file mode 100644 index 000000000..253f2d262 --- /dev/null +++ b/media/client/ipc/interface/IPrivateMetricsIpc.h @@ -0,0 +1,73 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_CLIENT_I_PRIVATE_METRICS_IPC_H_ +#define FIREBOLT_RIALTO_CLIENT_I_PRIVATE_METRICS_IPC_H_ + +#include +#include +#include + +namespace firebolt::rialto::client +{ +class IPrivateMetricsIpc; + +class IPrivateMetricsIpcClient +{ +public: + IPrivateMetricsIpcClient() = default; + virtual ~IPrivateMetricsIpcClient() = default; + + IPrivateMetricsIpcClient(const IPrivateMetricsIpcClient &) = delete; + IPrivateMetricsIpcClient &operator=(const IPrivateMetricsIpcClient &) = delete; + IPrivateMetricsIpcClient(IPrivateMetricsIpcClient &&) = delete; + IPrivateMetricsIpcClient &operator=(IPrivateMetricsIpcClient &&) = delete; + + virtual void reportClientMetrics(std::uint64_t sampleId, std::uint32_t reason) = 0; +}; + +class IPrivateMetricsIpcFactory +{ +public: + IPrivateMetricsIpcFactory() = default; + virtual ~IPrivateMetricsIpcFactory() = default; + + static std::shared_ptr createFactory(); + + virtual std::shared_ptr createPrivateMetricsIpc(IPrivateMetricsIpcClient *client) = 0; +}; + +class IPrivateMetricsIpc +{ +public: + IPrivateMetricsIpc() = default; + virtual ~IPrivateMetricsIpc() = default; + + IPrivateMetricsIpc(const IPrivateMetricsIpc &) = delete; + IPrivateMetricsIpc &operator=(const IPrivateMetricsIpc &) = delete; + IPrivateMetricsIpc(IPrivateMetricsIpc &&) = delete; + IPrivateMetricsIpc &operator=(IPrivateMetricsIpc &&) = delete; + + virtual bool reportClientMetrics(std::uint64_t sampleId, std::uint32_t reason, const std::string &appName, + std::uint32_t processId, std::uint64_t monotonicTimeMs, + std::uint64_t epochTimeMs, std::uint64_t processCpuTimeMs) = 0; +}; +} // namespace firebolt::rialto::client + +#endif // FIREBOLT_RIALTO_CLIENT_I_PRIVATE_METRICS_IPC_H_ diff --git a/media/client/ipc/proto/privatemetricsmodule.proto b/media/client/ipc/proto/privatemetricsmodule.proto new file mode 100644 index 000000000..cdef9ba7d --- /dev/null +++ b/media/client/ipc/proto/privatemetricsmodule.proto @@ -0,0 +1 @@ +../../../../proto/privatemetricsmodule.proto diff --git a/media/client/ipc/source/PrivateMetricsIpc.cpp b/media/client/ipc/source/PrivateMetricsIpc.cpp new file mode 100644 index 000000000..a7435dd18 --- /dev/null +++ b/media/client/ipc/source/PrivateMetricsIpc.cpp @@ -0,0 +1,209 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "PrivateMetricsIpc.h" +#include "IpcClient.h" +#include "RialtoClientLogging.h" +#include +#include + +namespace +{ +const char *sampleReasonToString(::firebolt::rialto::MetricsSampleReason reason) +{ + switch (reason) + { + case firebolt::rialto::METRICS_SAMPLE_REASON_CONNECTED: + return "CONNECTED"; + case firebolt::rialto::METRICS_SAMPLE_REASON_PERIODIC: + return "PERIODIC"; + case firebolt::rialto::METRICS_SAMPLE_REASON_UNKNOWN: + default: + return "UNKNOWN"; + } +} +} // namespace + +namespace firebolt::rialto::client +{ +std::shared_ptr IPrivateMetricsIpcFactory::createFactory() +{ + return PrivateMetricsIpcFactory::createFactory(); +} + +std::shared_ptr PrivateMetricsIpcFactory::createFactory() +{ + std::shared_ptr factory; + + try + { + factory = std::make_shared(); + } + catch (const std::exception &e) + { + RIALTO_CLIENT_LOG_ERROR("Failed to create the rialto private metrics ipc factory, reason: %s", e.what()); + } + + return factory; +} + +std::shared_ptr PrivateMetricsIpcFactory::createPrivateMetricsIpc(IPrivateMetricsIpcClient *client) +{ + auto &ipcClient{IIpcClientAccessor::instance().getIpcClient()}; + return std::make_shared(client, ipcClient, + firebolt::rialto::common::IEventThreadFactory::createFactory()); +} + +PrivateMetricsIpc::PrivateMetricsIpc(IPrivateMetricsIpcClient *client, IIpcClient &ipcClient, + const std::shared_ptr &eventThreadFactory) + : IpcModule(ipcClient), m_privateMetricsIpcClient{client}, + m_eventThread(eventThreadFactory->createEventThread("rialto-metrics-events")) +{ + RIALTO_CLIENT_LOG_MIL("Initialising private metrics IPC, pid=%d", getpid()); + if (!attachChannel()) + { + throw std::runtime_error("Failed attach to the ipc channel"); + } + if (!notifyClientReady()) + { + throw std::runtime_error("Failed to notify private metrics readiness"); + } +} + +PrivateMetricsIpc::~PrivateMetricsIpc() +{ + RIALTO_CLIENT_LOG_MIL("Terminating private metrics IPC, pid=%d", getpid()); + detachChannel(); + m_eventThread.reset(); +} + +bool PrivateMetricsIpc::reportClientMetrics(std::uint64_t sampleId, std::uint32_t reason, const std::string &appName, + std::uint32_t processId, std::uint64_t monotonicTimeMs, + std::uint64_t epochTimeMs, std::uint64_t processCpuTimeMs) +{ + if (!reattachChannelIfRequired()) + { + RIALTO_CLIENT_LOG_ERROR("Reattachment of the ipc channel failed, ipc disconnected"); + return false; + } + + firebolt::rialto::ReportClientMetricsRequest request; + auto metrics{request.mutable_metrics()}; + metrics->set_sample_id(sampleId); + metrics->set_reason(static_cast(reason)); + metrics->set_app_name(appName); + metrics->set_process_id(processId); + metrics->set_monotonic_time_ms(monotonicTimeMs); + metrics->set_epoch_time_ms(epochTimeMs); + metrics->set_process_cpu_time_ms(processCpuTimeMs); + + RIALTO_CLIENT_LOG_MIL("Reporting metrics sample=%" PRIu64 ", reason=%s, app='%s', pid=%u, cpu_ms=%" PRIu64, + sampleId, + sampleReasonToString(static_cast(reason)), + appName.c_str(), processId, processCpuTimeMs); + + firebolt::rialto::ReportClientMetricsResponse response; + auto ipcController = m_ipc.createRpcController(); + auto blockingClosure = m_ipc.createBlockingClosure(); + m_privateMetricsStub->reportClientMetrics(ipcController.get(), &request, &response, blockingClosure.get()); + + blockingClosure->wait(); + + if (ipcController->Failed()) + { + RIALTO_CLIENT_LOG_ERROR("failed to report client metrics due to '%s'", ipcController->ErrorText().c_str()); + return false; + } + + RIALTO_CLIENT_LOG_INFO("Reported metrics sample=%" PRIu64 ", reason=%s", sampleId, + sampleReasonToString(static_cast(reason))); + + return true; +} + +bool PrivateMetricsIpc::notifyClientReady() +{ + if (!reattachChannelIfRequired()) + { + RIALTO_CLIENT_LOG_ERROR("Reattachment of the ipc channel failed, ipc disconnected"); + return false; + } + + RIALTO_CLIENT_LOG_MIL("Notifying server that private metrics IPC is ready, pid=%d", getpid()); + + firebolt::rialto::NotifyClientReadyRequest request; + firebolt::rialto::NotifyClientReadyResponse response; + auto ipcController = m_ipc.createRpcController(); + auto blockingClosure = m_ipc.createBlockingClosure(); + m_privateMetricsStub->notifyClientReady(ipcController.get(), &request, &response, blockingClosure.get()); + + blockingClosure->wait(); + + if (ipcController->Failed()) + { + RIALTO_CLIENT_LOG_ERROR("failed to notify private metrics readiness due to '%s'", + ipcController->ErrorText().c_str()); + return false; + } + + RIALTO_CLIENT_LOG_MIL("Server acknowledged private metrics IPC readiness, pid=%d", getpid()); + + return true; +} + +bool PrivateMetricsIpc::createRpcStubs(const std::shared_ptr &ipcChannel) +{ + m_privateMetricsStub = std::make_shared<::firebolt::rialto::PrivateMetricsModule_Stub>(ipcChannel.get()); + return static_cast(m_privateMetricsStub); +} + +bool PrivateMetricsIpc::subscribeToEvents(const std::shared_ptr &ipcChannel) +{ + if (!ipcChannel) + { + return false; + } + + int eventTag = ipcChannel->subscribe( + [this](const std::shared_ptr &event) + { m_eventThread->add(&PrivateMetricsIpc::onMetricsSampleRequested, this, event); }); + if (eventTag < 0) + { + return false; + } + m_eventTags.push_back(eventTag); + + RIALTO_CLIENT_LOG_MIL("Subscribed to private metrics sample requests, pid=%d, event_tag=%d", getpid(), eventTag); + + return true; +} + +void PrivateMetricsIpc::onMetricsSampleRequested( + const std::shared_ptr &event) +{ + if (!m_privateMetricsIpcClient) + { + RIALTO_CLIENT_LOG_WARN("No private metrics client registered"); + return; + } + RIALTO_CLIENT_LOG_MIL("Received metrics sample request sample=%" PRIu64 ", reason=%s, pid=%d", event->sample_id(), + sampleReasonToString(event->reason()), getpid()); + m_privateMetricsIpcClient->reportClientMetrics(event->sample_id(), event->reason()); +} +} // namespace firebolt::rialto::client diff --git a/media/client/main/include/ClientController.h b/media/client/main/include/ClientController.h index c209b3e3f..6946c1744 100644 --- a/media/client/main/include/ClientController.h +++ b/media/client/main/include/ClientController.h @@ -28,6 +28,7 @@ #include "IClientController.h" #include "IControlClient.h" #include "IControlIpc.h" +#include "IPrivateMetricsIpc.h" namespace firebolt::rialto::client { @@ -38,10 +39,11 @@ class ClientControllerAccessor : public IClientControllerAccessor IClientController &getClientController() const override; }; -class ClientController : public IClientController, public IControlClient +class ClientController : public IClientController, public IControlClient, public IPrivateMetricsIpcClient { public: - explicit ClientController(const std::shared_ptr &ControlIpcFactory); + explicit ClientController(const std::shared_ptr &ControlIpcFactory, + const std::shared_ptr &privateMetricsIpcFactory); ~ClientController() override; std::shared_ptr getSharedMemoryHandle() override; @@ -50,6 +52,7 @@ class ClientController : public IClientController, public IControlClient private: void notifyApplicationState(ApplicationState state) override; + void reportClientMetrics(std::uint64_t sampleId, std::uint32_t reason) override; /** * @brief Initalised the shared memory for media playback. @@ -80,6 +83,26 @@ class ClientController : public IClientController, public IControlClient */ void changeStateAndNotifyClients(ApplicationState state); + /** + * @brief Gets the monotonic timestamp in milliseconds. + */ + std::uint64_t getMonotonicTimeMs() const; + + /** + * @brief Gets the epoch timestamp in milliseconds. + */ + std::uint64_t getEpochTimeMs() const; + + /** + * @brief Gets accumulated process CPU time in milliseconds. + */ + std::uint64_t getProcessCpuTimeMs() const; + + /** + * @brief Gets the process name used for metrics reporting. + */ + std::string getProcessName() const; + private: /** * @brief Mutex protection for class attributes. @@ -106,6 +129,11 @@ class ClientController : public IClientController, public IControlClient */ std::shared_ptr m_controlIpc; + /** + * @brief The rialto private metrics ipc instance. + */ + std::shared_ptr m_privateMetricsIpc; + /** * @brief List of clients to notify. */ diff --git a/media/client/main/source/ClientController.cpp b/media/client/main/source/ClientController.cpp index 1ef44c490..a5c19509b 100644 --- a/media/client/main/source/ClientController.cpp +++ b/media/client/main/source/ClientController.cpp @@ -21,10 +21,13 @@ #include "RialtoClientLogging.h" #include "SharedMemoryHandle.h" #include +#include #include +#include #include #include #include +#include #include #include @@ -46,11 +49,13 @@ IClientControllerAccessor &IClientControllerAccessor::instance() IClientController &ClientControllerAccessor::getClientController() const { - static ClientController ClientController{IControlIpcFactory::createFactory()}; + static ClientController ClientController{IControlIpcFactory::createFactory(), + IPrivateMetricsIpcFactory::createFactory()}; return ClientController; } -ClientController::ClientController(const std::shared_ptr &ControlIpcFactory) +ClientController::ClientController(const std::shared_ptr &ControlIpcFactory, + const std::shared_ptr &privateMetricsIpcFactory) : m_currentState{ApplicationState::UNKNOWN}, m_registrationRequired{true} { RIALTO_CLIENT_LOG_DEBUG("entry:"); @@ -79,6 +84,12 @@ ClientController::ClientController(const std::shared_ptr &Co { throw std::runtime_error("Failed to create the ControlIpc object"); } + + m_privateMetricsIpc = privateMetricsIpcFactory->createPrivateMetricsIpc(this); + if (nullptr == m_privateMetricsIpc) + { + throw std::runtime_error("Failed to create the PrivateMetricsIpc object"); + } } ClientController::~ClientController() @@ -280,4 +291,59 @@ void ClientController::changeStateAndNotifyClients(ApplicationState state) client->notifyApplicationState(state); } } + +void ClientController::reportClientMetrics(std::uint64_t sampleId, std::uint32_t reason) +{ + if (!m_privateMetricsIpc->reportClientMetrics(sampleId, reason, getProcessName(), static_cast(getpid()), + getMonotonicTimeMs(), getEpochTimeMs(), getProcessCpuTimeMs())) + { + RIALTO_CLIENT_LOG_WARN("Failed to report client process metrics"); + } +} + +std::uint64_t ClientController::getMonotonicTimeMs() const +{ + using std::chrono::duration_cast; + using std::chrono::milliseconds; + using std::chrono::steady_clock; + + return static_cast(duration_cast(steady_clock::now().time_since_epoch()).count()); +} + +std::uint64_t ClientController::getEpochTimeMs() const +{ + using std::chrono::duration_cast; + using std::chrono::milliseconds; + using std::chrono::system_clock; + + return static_cast(duration_cast(system_clock::now().time_since_epoch()).count()); +} + +std::uint64_t ClientController::getProcessCpuTimeMs() const +{ + struct tms processTimes + {}; + const clock_t kCurrentTicks{times(&processTimes)}; + const long kTicksPerSecond{sysconf(_SC_CLK_TCK)}; + if ((static_cast(-1) == kCurrentTicks) || (kTicksPerSecond <= 0)) + { + RIALTO_CLIENT_LOG_WARN("Failed to sample client process CPU usage"); + return 0; + } + + const auto kProcessTicks{processTimes.tms_utime + processTimes.tms_stime}; + return static_cast((static_cast(kProcessTicks) * 1000.0) / + static_cast(kTicksPerSecond)); +} + +std::string ClientController::getProcessName() const +{ + std::ifstream comm{"/proc/self/comm"}; + std::string processName; + if (std::getline(comm, processName) && !processName.empty()) + { + return processName; + } + return "unknown"; +} } // namespace firebolt::rialto::client diff --git a/media/server/ipc/CMakeLists.txt b/media/server/ipc/CMakeLists.txt index c233a4dcc..3ed0c879a 100644 --- a/media/server/ipc/CMakeLists.txt +++ b/media/server/ipc/CMakeLists.txt @@ -41,6 +41,7 @@ add_library ( source/MediaKeysCapabilitiesModuleService.cpp source/ControlClientServerInternal.cpp source/ControlModuleService.cpp + source/PrivateMetricsModuleService.cpp source/ServerManagerModuleService.cpp source/SessionManagementServer.cpp source/SetLogLevelsService.cpp diff --git a/media/server/ipc/include/IPrivateMetricsModuleService.h b/media/server/ipc/include/IPrivateMetricsModuleService.h new file mode 100644 index 000000000..07cca10fd --- /dev/null +++ b/media/server/ipc/include/IPrivateMetricsModuleService.h @@ -0,0 +1,59 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_IPC_I_PRIVATE_METRICS_MODULE_SERVICE_H_ +#define FIREBOLT_RIALTO_SERVER_IPC_I_PRIVATE_METRICS_MODULE_SERVICE_H_ + +#include "privatemetricsmodule.pb.h" +#include +#include + +namespace firebolt::rialto::server::ipc +{ +class IPrivateMetricsModuleService; + +class IPrivateMetricsModuleServiceFactory +{ +public: + IPrivateMetricsModuleServiceFactory() = default; + virtual ~IPrivateMetricsModuleServiceFactory() = default; + + static std::shared_ptr createFactory(); + + virtual std::shared_ptr create() const = 0; +}; + +class IPrivateMetricsModuleService : public ::firebolt::rialto::PrivateMetricsModule, + public std::enable_shared_from_this +{ +public: + IPrivateMetricsModuleService() = default; + virtual ~IPrivateMetricsModuleService() = default; + + IPrivateMetricsModuleService(const IPrivateMetricsModuleService &) = delete; + IPrivateMetricsModuleService(IPrivateMetricsModuleService &&) = delete; + IPrivateMetricsModuleService &operator=(const IPrivateMetricsModuleService &) = delete; + IPrivateMetricsModuleService &operator=(IPrivateMetricsModuleService &&) = delete; + + virtual void clientConnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) = 0; + virtual void clientDisconnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) = 0; +}; +} // namespace firebolt::rialto::server::ipc + +#endif // FIREBOLT_RIALTO_SERVER_IPC_I_PRIVATE_METRICS_MODULE_SERVICE_H_ diff --git a/media/server/ipc/include/PrivateMetricsModuleService.h b/media/server/ipc/include/PrivateMetricsModuleService.h new file mode 100644 index 000000000..303d27a6c --- /dev/null +++ b/media/server/ipc/include/PrivateMetricsModuleService.h @@ -0,0 +1,102 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_IPC_PRIVATE_METRICS_MODULE_SERVICE_H_ +#define FIREBOLT_RIALTO_SERVER_IPC_PRIVATE_METRICS_MODULE_SERVICE_H_ + +#include "IPrivateMetricsModuleService.h" +#include +#include +#include +#include +#include +#include +#include + +namespace firebolt::rialto::server::ipc +{ +class PrivateMetricsModuleServiceFactory : public IPrivateMetricsModuleServiceFactory +{ +public: + PrivateMetricsModuleServiceFactory() = default; + ~PrivateMetricsModuleServiceFactory() override = default; + + std::shared_ptr create() const override; +}; + +class PrivateMetricsModuleService : public IPrivateMetricsModuleService +{ +public: + PrivateMetricsModuleService(); + ~PrivateMetricsModuleService() override; + + void clientConnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) override; + void clientDisconnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) override; + + void reportClientMetrics(::google::protobuf::RpcController *controller, + const ::firebolt::rialto::ReportClientMetricsRequest *request, + ::firebolt::rialto::ReportClientMetricsResponse *response, + ::google::protobuf::Closure *done) override; + void notifyClientReady(::google::protobuf::RpcController *controller, + const ::firebolt::rialto::NotifyClientReadyRequest *request, + ::firebolt::rialto::NotifyClientReadyResponse *response, + ::google::protobuf::Closure *done) override; + +private: + struct ProcessMetricsSample + { + std::uint64_t monotonicTimeMs; + std::uint64_t epochTimeMs; + std::uint64_t processCpuTimeMs; + }; + + struct MetricsSamplePair + { + ::firebolt::rialto::ClientProcessMetrics clientMetrics; + ProcessMetricsSample serverMetrics; + }; + + struct ClientMetricsState + { + bool isReady{false}; + std::optional latestMetrics; + }; + + void runMetricsSampler(); + void requestMetricsSample(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient, + ::firebolt::rialto::MetricsSampleReason reason); + ProcessMetricsSample getProcessMetricsSample() const; + void logMetrics(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient, + const ::firebolt::rialto::ClientProcessMetrics &clientMetrics, + const ProcessMetricsSample &serverMetrics); + const char *sampleReasonToString(::firebolt::rialto::MetricsSampleReason reason) const; + double calculateCpuPercentage(std::uint64_t currentCpuTimeMs, std::uint64_t previousCpuTimeMs, + std::uint64_t currentMonotonicTimeMs, std::uint64_t previousMonotonicTimeMs) const; + +private: + std::atomic m_isRunning; + std::atomic m_nextSampleId; + std::thread m_metricsThread; + std::condition_variable m_wakeup; + std::mutex m_mutex; + std::map, ClientMetricsState> m_clients; +}; +} // namespace firebolt::rialto::server::ipc + +#endif // FIREBOLT_RIALTO_SERVER_IPC_PRIVATE_METRICS_MODULE_SERVICE_H_ diff --git a/media/server/ipc/include/SessionManagementServer.h b/media/server/ipc/include/SessionManagementServer.h index 19b74357f..e56858b5d 100644 --- a/media/server/ipc/include/SessionManagementServer.h +++ b/media/server/ipc/include/SessionManagementServer.h @@ -28,6 +28,7 @@ #include "IMediaPipelineCapabilitiesModuleService.h" #include "IMediaPipelineModuleService.h" #include "IPlaybackService.h" +#include "IPrivateMetricsModuleService.h" #include "ISessionManagementServer.h" #include "IWebAudioPlayerModuleService.h" #include "SetLogLevelsService.h" @@ -50,6 +51,7 @@ class SessionManagementServer : public ISessionManagementServer const std::shared_ptr &mediaKeysModuleFactory, const std::shared_ptr &mediaKeysCapabilitiesModuleFactory, const std::shared_ptr &webAudioPlayerModuleFactory, + const std::shared_ptr &privateMetricsModuleFactory, const std::shared_ptr &controlModuleFactory, service::IPlaybackService &playbackService, service::ICdmService &cdmService, service::IControlService &controlService); @@ -81,6 +83,7 @@ class SessionManagementServer : public ISessionManagementServer std::shared_ptr m_mediaKeysModule; std::shared_ptr m_mediaKeysCapabilitiesModule; std::shared_ptr m_webAudioPlayerModule; + std::shared_ptr m_privateMetricsModule; std::shared_ptr m_controlModule; SetLogLevelsService m_setLogLevelsService; }; diff --git a/media/server/ipc/proto/privatemetricsmodule.proto b/media/server/ipc/proto/privatemetricsmodule.proto new file mode 100644 index 000000000..cdef9ba7d --- /dev/null +++ b/media/server/ipc/proto/privatemetricsmodule.proto @@ -0,0 +1 @@ +../../../../proto/privatemetricsmodule.proto diff --git a/media/server/ipc/source/IpcFactory.cpp b/media/server/ipc/source/IpcFactory.cpp index 0c470c570..c178c7c67 100644 --- a/media/server/ipc/source/IpcFactory.cpp +++ b/media/server/ipc/source/IpcFactory.cpp @@ -25,6 +25,7 @@ #include "IMediaKeysModuleService.h" #include "IMediaPipelineCapabilitiesModuleService.h" #include "IMediaPipelineModuleService.h" +#include "IPrivateMetricsModuleService.h" #include "IServerManagerModuleServiceFactory.h" #include "IWebAudioPlayerModuleService.h" #include "SessionManagementServer.h" @@ -51,6 +52,7 @@ IpcFactory::createSessionManagementServer(service::IPlaybackService &playbackSer firebolt::rialto::server::ipc::IMediaKeysModuleServiceFactory::createFactory(), firebolt::rialto::server::ipc::IMediaKeysCapabilitiesModuleServiceFactory::createFactory(), firebolt::rialto::server::ipc::IWebAudioPlayerModuleServiceFactory::createFactory(), + firebolt::rialto::server::ipc::IPrivateMetricsModuleServiceFactory::createFactory(), firebolt::rialto::server::ipc::IControlModuleServiceFactory::createFactory(), playbackService, cdmService, controlService); } diff --git a/media/server/ipc/source/PrivateMetricsModuleService.cpp b/media/server/ipc/source/PrivateMetricsModuleService.cpp new file mode 100644 index 000000000..5cda04105 --- /dev/null +++ b/media/server/ipc/source/PrivateMetricsModuleService.cpp @@ -0,0 +1,326 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "PrivateMetricsModuleService.h" +#include "RialtoServerLogging.h" +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +constexpr std::chrono::seconds kMetricsInterval{15}; +} // namespace + +namespace firebolt::rialto::server::ipc +{ +std::shared_ptr IPrivateMetricsModuleServiceFactory::createFactory() +{ + std::shared_ptr factory; + + try + { + factory = std::make_shared(); + } + catch (const std::exception &e) + { + RIALTO_SERVER_LOG_ERROR("Failed to create the rialto private metrics module service factory, reason: %s", + e.what()); + } + + return factory; +} + +std::shared_ptr PrivateMetricsModuleServiceFactory::create() const +{ + std::shared_ptr privateMetricsModule; + + try + { + privateMetricsModule = std::make_shared(); + } + catch (const std::exception &e) + { + RIALTO_SERVER_LOG_ERROR("Failed to create the rialto private metrics module service, reason: %s", e.what()); + } + + return privateMetricsModule; +} + +PrivateMetricsModuleService::PrivateMetricsModuleService() : m_isRunning{true}, m_nextSampleId{1} +{ + m_metricsThread = std::thread(&PrivateMetricsModuleService::runMetricsSampler, this); +} + +PrivateMetricsModuleService::~PrivateMetricsModuleService() +{ + m_isRunning.store(false); + m_wakeup.notify_all(); + if (m_metricsThread.joinable()) + { + m_metricsThread.join(); + } +} + +void PrivateMetricsModuleService::clientConnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) +{ + RIALTO_SERVER_LOG_INFO("Client connected to private metrics module"); + { + std::lock_guard lock{m_mutex}; + m_clients.emplace(ipcClient, ClientMetricsState{}); + } + ipcClient->exportService(shared_from_this()); +} + +void PrivateMetricsModuleService::clientDisconnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) +{ + RIALTO_SERVER_LOG_INFO("Client disconnected from private metrics module"); + std::lock_guard lock{m_mutex}; + m_clients.erase(ipcClient); +} + +void PrivateMetricsModuleService::notifyClientReady(::google::protobuf::RpcController *controller, + const ::firebolt::rialto::NotifyClientReadyRequest *request, + ::firebolt::rialto::NotifyClientReadyResponse *response, + ::google::protobuf::Closure *done) +{ + RIALTO_SERVER_LOG_DEBUG("entry:"); + auto ipcController = dynamic_cast(controller); + if (!ipcController) + { + RIALTO_SERVER_LOG_ERROR("ipc library provided incompatible controller object"); + controller->SetFailed("ipc library provided incompatible controller object"); + done->Run(); + return; + } + + auto ipcClient{ipcController->getClient()}; + { + std::lock_guard lock{m_mutex}; + auto clientIter{m_clients.find(ipcClient)}; + if (m_clients.end() == clientIter) + { + RIALTO_SERVER_LOG_WARN("Ignoring private metrics ready notification from unknown client"); + done->Run(); + return; + } + clientIter->second.isReady = true; + } + + RIALTO_SERVER_LOG_MIL("Client ready for private metrics samples"); + done->Run(); + requestMetricsSample(ipcClient, firebolt::rialto::METRICS_SAMPLE_REASON_CONNECTED); +} + +void PrivateMetricsModuleService::reportClientMetrics(::google::protobuf::RpcController *controller, + const ::firebolt::rialto::ReportClientMetricsRequest *request, + ::firebolt::rialto::ReportClientMetricsResponse *response, + ::google::protobuf::Closure *done) +{ + RIALTO_SERVER_LOG_DEBUG("entry:"); + auto ipcController = dynamic_cast(controller); + if (!ipcController) + { + RIALTO_SERVER_LOG_ERROR("ipc library provided incompatible controller object"); + controller->SetFailed("ipc library provided incompatible controller object"); + done->Run(); + return; + } + if (!request->has_metrics()) + { + RIALTO_SERVER_LOG_ERROR("reportClientMetrics request missing metrics"); + controller->SetFailed("Missing metrics"); + done->Run(); + return; + } + + const auto &metrics{request->metrics()}; + const auto kServerMetrics{getProcessMetricsSample()}; + auto ipcClient{ipcController->getClient()}; + logMetrics(ipcClient, metrics, kServerMetrics); + { + std::lock_guard lock{m_mutex}; + auto &clientState{m_clients[ipcClient]}; + clientState.isReady = true; + clientState.latestMetrics = MetricsSamplePair{metrics, kServerMetrics}; + } + + done->Run(); +} + +void PrivateMetricsModuleService::runMetricsSampler() +{ + while (m_isRunning.load()) + { + std::vector> clients; + { + std::unique_lock lock{m_mutex}; + m_wakeup.wait_for(lock, kMetricsInterval, [this]() { return !m_isRunning.load(); }); + if (!m_isRunning.load()) + { + break; + } + for (const auto &client : m_clients) + { + if (client.second.isReady) + { + clients.push_back(client.first); + } + } + } + + for (const auto &client : clients) + { + requestMetricsSample(client, firebolt::rialto::METRICS_SAMPLE_REASON_PERIODIC); + } + } +} + +void PrivateMetricsModuleService::requestMetricsSample( + const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient, + ::firebolt::rialto::MetricsSampleReason reason) +{ + if (!ipcClient || !ipcClient->isConnected()) + { + return; + } + + auto event{std::make_shared()}; + const auto kSampleId{m_nextSampleId.fetch_add(1)}; + event->set_sample_id(kSampleId); + event->set_reason(reason); + + RIALTO_SERVER_LOG_MIL("Requesting metrics sample=%" PRIu64 ", reason=%s", kSampleId, sampleReasonToString(reason)); + + if (!ipcClient->sendEvent(event)) + { + RIALTO_SERVER_LOG_WARN("Failed to request client metrics sample=%" PRIu64 ", reason=%s", kSampleId, + sampleReasonToString(reason)); + } +} + +PrivateMetricsModuleService::ProcessMetricsSample PrivateMetricsModuleService::getProcessMetricsSample() const +{ + using std::chrono::duration_cast; + using std::chrono::milliseconds; + using std::chrono::steady_clock; + using std::chrono::system_clock; + + struct tms processTimes + {}; + const clock_t kCurrentTicks{times(&processTimes)}; + const long kTicksPerSecond{sysconf(_SC_CLK_TCK)}; + std::uint64_t processCpuTimeMs{0}; + if ((static_cast(-1) != kCurrentTicks) && (kTicksPerSecond > 0)) + { + const auto kProcessTicks{processTimes.tms_utime + processTimes.tms_stime}; + processCpuTimeMs = static_cast((static_cast(kProcessTicks) * 1000.0) / + static_cast(kTicksPerSecond)); + } + else + { + RIALTO_SERVER_LOG_WARN("Failed to sample server process CPU usage"); + } + + return ProcessMetricsSample{ + static_cast(duration_cast(steady_clock::now().time_since_epoch()).count()), + static_cast(duration_cast(system_clock::now().time_since_epoch()).count()), + processCpuTimeMs}; +} + +void PrivateMetricsModuleService::logMetrics(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient, + const ::firebolt::rialto::ClientProcessMetrics &clientMetrics, + const ProcessMetricsSample &serverMetrics) +{ + std::optional previousSample; + { + std::lock_guard lock{m_mutex}; + const auto kClientIter{m_clients.find(ipcClient)}; + if ((m_clients.end() != kClientIter) && kClientIter->second.latestMetrics.has_value()) + { + previousSample = kClientIter->second.latestMetrics; + } + } + + if (!previousSample.has_value() || !previousSample->clientMetrics.has_process_cpu_time_ms()) + { + RIALTO_SERVER_LOG_MIL("Metrics baseline: sample=%" PRIu64 ", reason=%s, app='%s', client_pid=%u, " + "client_cpu_ms=%" PRIu64 ", server_cpu_ms=%" PRIu64, + clientMetrics.sample_id(), sampleReasonToString(clientMetrics.reason()), + clientMetrics.app_name().c_str(), clientMetrics.process_id(), + clientMetrics.process_cpu_time_ms(), serverMetrics.processCpuTimeMs); + return; + } + + const auto &previousClientMetrics{previousSample->clientMetrics}; + const auto &previousServerMetrics{previousSample->serverMetrics}; + const double kClientCpuPercentage{calculateCpuPercentage( + clientMetrics.process_cpu_time_ms(), previousClientMetrics.process_cpu_time_ms(), + clientMetrics.monotonic_time_ms(), previousClientMetrics.monotonic_time_ms())}; + const double kServerCpuPercentage{calculateCpuPercentage(serverMetrics.processCpuTimeMs, + previousServerMetrics.processCpuTimeMs, + serverMetrics.monotonicTimeMs, + previousServerMetrics.monotonicTimeMs)}; + const double kCombinedCpuPercentage{calculateCpuPercentage( + clientMetrics.process_cpu_time_ms() + serverMetrics.processCpuTimeMs, + previousClientMetrics.process_cpu_time_ms() + previousServerMetrics.processCpuTimeMs, + serverMetrics.monotonicTimeMs, previousServerMetrics.monotonicTimeMs)}; + + RIALTO_SERVER_LOG_MIL("Metrics sample=%" PRIu64 ", reason=%s, app='%s', client_pid=%u, client_cpu=%.2f%%, " + "server_cpu=%.2f%%, combined_cpu=%.2f%%, client_cpu_ms=%" PRIu64 ", " + "server_cpu_ms=%" PRIu64, + clientMetrics.sample_id(), sampleReasonToString(clientMetrics.reason()), + clientMetrics.app_name().c_str(), clientMetrics.process_id(), kClientCpuPercentage, + kServerCpuPercentage, kCombinedCpuPercentage, clientMetrics.process_cpu_time_ms(), + serverMetrics.processCpuTimeMs); +} + +const char *PrivateMetricsModuleService::sampleReasonToString(::firebolt::rialto::MetricsSampleReason reason) const +{ + switch (reason) + { + case firebolt::rialto::METRICS_SAMPLE_REASON_CONNECTED: + return "CONNECTED"; + case firebolt::rialto::METRICS_SAMPLE_REASON_PERIODIC: + return "PERIODIC"; + case firebolt::rialto::METRICS_SAMPLE_REASON_UNKNOWN: + default: + return "UNKNOWN"; + } +} + +double PrivateMetricsModuleService::calculateCpuPercentage(std::uint64_t currentCpuTimeMs, + std::uint64_t previousCpuTimeMs, + std::uint64_t currentMonotonicTimeMs, + std::uint64_t previousMonotonicTimeMs) const +{ + if ((currentCpuTimeMs < previousCpuTimeMs) || (currentMonotonicTimeMs <= previousMonotonicTimeMs)) + { + return 0.0; + } + + return (static_cast(currentCpuTimeMs - previousCpuTimeMs) / + static_cast(currentMonotonicTimeMs - previousMonotonicTimeMs)) * + 100.0; +} +} // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/source/SessionManagementServer.cpp b/media/server/ipc/source/SessionManagementServer.cpp index ab9954a92..16a37bcd9 100644 --- a/media/server/ipc/source/SessionManagementServer.cpp +++ b/media/server/ipc/source/SessionManagementServer.cpp @@ -22,6 +22,7 @@ #include "IMediaKeysCapabilitiesModuleService.h" #include "IMediaKeysModuleService.h" #include "IMediaPipelineModuleService.h" +#include "IPrivateMetricsModuleService.h" #include "IWebAudioPlayerModuleService.h" #include "LinuxUtils.h" #include "RialtoServerLogging.h" @@ -46,6 +47,7 @@ SessionManagementServer::SessionManagementServer( const std::shared_ptr &mediaKeysModuleFactory, const std::shared_ptr &mediaKeysCapabilitiesModuleFactory, const std::shared_ptr &webAudioPlayerModuleFactory, + const std::shared_ptr &privateMetricsModuleFactory, const std::shared_ptr &controlModuleFactory, service::IPlaybackService &playbackService, service::ICdmService &cdmService, service::IControlService &controlService) : m_isRunning{false}, @@ -55,6 +57,7 @@ SessionManagementServer::SessionManagementServer( m_mediaKeysModule{mediaKeysModuleFactory->create(cdmService)}, m_mediaKeysCapabilitiesModule{mediaKeysCapabilitiesModuleFactory->create(cdmService)}, m_webAudioPlayerModule{webAudioPlayerModuleFactory->create(playbackService.getWebAudioPlayerService())}, + m_privateMetricsModule{privateMetricsModuleFactory->create()}, m_controlModule{controlModuleFactory->create(playbackService, controlService)} { m_ipcServer = ipcFactory->create(); @@ -170,6 +173,7 @@ void SessionManagementServer::onClientConnected(const std::shared_ptr<::firebolt m_mediaKeysModule->clientConnected(client); m_mediaKeysCapabilitiesModule->clientConnected(client); m_webAudioPlayerModule->clientConnected(client); + m_privateMetricsModule->clientConnected(client); m_setLogLevelsService.clientConnected(client); } @@ -182,6 +186,7 @@ void SessionManagementServer::onClientDisconnected(const std::shared_ptr<::fireb m_mediaPipelineCapabilitiesModule->clientDisconnected(client); m_mediaPipelineModule->clientDisconnected(client); m_webAudioPlayerModule->clientDisconnected(client); + m_privateMetricsModule->clientDisconnected(client); m_controlModule->clientDisconnected(client); } } // namespace firebolt::rialto::server::ipc diff --git a/proto/CMakeLists.txt b/proto/CMakeLists.txt index d701fb4d2..26018547f 100644 --- a/proto/CMakeLists.txt +++ b/proto/CMakeLists.txt @@ -21,8 +21,8 @@ include( FindProtobuf ) set( Protobuf_IMPORT_DIRS "${CMAKE_SYSROOT}/usr/include" "${CMAKE_CURRENT_LIST_DIR}/../ipc/common/proto" ) protobuf_generate_cpp( PROTO_SRCS PROTO_HEADERS rialtocommon.proto mediapipelinemodule.proto mediapipelinecapabilitiesmodule.proto - mediakeysmodule.proto mediakeyscapabilitiesmodule.proto controlmodule.proto webaudioplayermodule.proto rialtoipc.proto - rialtoipc-transport.proto metadata.proto servermanagermodule.proto) + mediakeysmodule.proto mediakeyscapabilitiesmodule.proto controlmodule.proto privatemetricsmodule.proto + webaudioplayermodule.proto rialtoipc.proto rialtoipc-transport.proto metadata.proto servermanagermodule.proto) # Find includes in corresponding build directories set( CMAKE_INCLUDE_CURRENT_DIR ON ) @@ -90,4 +90,3 @@ if( ENABLE_PROTO_OPTIMIZATION ) DESTINATION ${CMAKE_INSTALL_LIBDIR} ) endif() - diff --git a/proto/privatemetricsmodule.proto b/proto/privatemetricsmodule.proto new file mode 100644 index 000000000..a38e5a4f5 --- /dev/null +++ b/proto/privatemetricsmodule.proto @@ -0,0 +1,66 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto2"; + +package firebolt.rialto; + +option cc_generic_services = true; + +enum MetricsSampleReason { + METRICS_SAMPLE_REASON_UNKNOWN = 0; + METRICS_SAMPLE_REASON_CONNECTED = 1; + METRICS_SAMPLE_REASON_PERIODIC = 2; +} + +message ClientProcessMetrics { + optional uint64 sample_id = 1; + optional MetricsSampleReason reason = 2 [default = METRICS_SAMPLE_REASON_UNKNOWN]; + optional string app_name = 3; + optional uint32 process_id = 4; + optional uint64 monotonic_time_ms = 5; + optional uint64 epoch_time_ms = 6; + optional uint64 process_cpu_time_ms = 7; +} + +message ReportClientMetricsRequest { + optional ClientProcessMetrics metrics = 1; +} + +message ReportClientMetricsResponse { +} + +message NotifyClientReadyRequest { +} + +message NotifyClientReadyResponse { +} + +message MetricsSampleRequestEvent { + optional uint64 sample_id = 1; + optional MetricsSampleReason reason = 2 [default = METRICS_SAMPLE_REASON_UNKNOWN]; +} + +service PrivateMetricsModule { + rpc notifyClientReady(NotifyClientReadyRequest) returns (NotifyClientReadyResponse) { + } + + rpc reportClientMetrics(ReportClientMetricsRequest) returns (ReportClientMetricsResponse) { + } +} diff --git a/tests/unittests/media/client/main/clientController/CreateTest.cpp b/tests/unittests/media/client/main/clientController/CreateTest.cpp index be9a61329..42f5cb430 100644 --- a/tests/unittests/media/client/main/clientController/CreateTest.cpp +++ b/tests/unittests/media/client/main/clientController/CreateTest.cpp @@ -20,12 +20,15 @@ #include "ClientController.h" #include "ControlIpcFactoryMock.h" #include "ControlIpcMock.h" +#include "PrivateMetricsIpcFactoryMock.h" +#include "PrivateMetricsIpcMock.h" #include using namespace firebolt::rialto; using namespace firebolt::rialto::client; using ::testing::_; +using ::testing::NiceMock; using ::testing::Return; using ::testing::StrictMock; @@ -34,10 +37,14 @@ class ClientControllerCreateTest : public ::testing::Test protected: std::shared_ptr> m_controlIpcFactoryMock; std::shared_ptr> m_controlIpcMock; + std::shared_ptr> m_privateMetricsIpcFactoryMock; + std::shared_ptr> m_privateMetricsIpcMock; ClientControllerCreateTest() : m_controlIpcFactoryMock{std::make_shared>()}, - m_controlIpcMock{std::make_shared>()} + m_controlIpcMock{std::make_shared>()}, + m_privateMetricsIpcFactoryMock{std::make_shared>()}, + m_privateMetricsIpcMock{std::make_shared>()} { } @@ -45,6 +52,8 @@ class ClientControllerCreateTest : public ::testing::Test { m_controlIpcMock.reset(); m_controlIpcFactoryMock.reset(); + m_privateMetricsIpcMock.reset(); + m_privateMetricsIpcFactoryMock.reset(); } }; @@ -54,8 +63,10 @@ TEST_F(ClientControllerCreateTest, CreateDestroy) // Create EXPECT_CALL(*m_controlIpcFactoryMock, createControlIpc(_)).WillOnce(Return(m_controlIpcMock)); + EXPECT_CALL(*m_privateMetricsIpcFactoryMock, createPrivateMetricsIpc(_)).WillOnce(Return(m_privateMetricsIpcMock)); - EXPECT_NO_THROW(controller = std::make_unique(m_controlIpcFactoryMock)); + EXPECT_NO_THROW(controller = + std::make_unique(m_controlIpcFactoryMock, m_privateMetricsIpcFactoryMock)); // Destroy controller.reset(); @@ -67,6 +78,7 @@ TEST_F(ClientControllerCreateTest, CreateControlIpcFailure) EXPECT_CALL(*m_controlIpcFactoryMock, createControlIpc(_)).WillOnce(Return(nullptr)); - EXPECT_THROW(controller = std::make_unique(m_controlIpcFactoryMock), std::runtime_error); + EXPECT_THROW(controller = std::make_unique(m_controlIpcFactoryMock, m_privateMetricsIpcFactoryMock), + std::runtime_error); EXPECT_EQ(controller, nullptr); } diff --git a/tests/unittests/media/client/main/clientController/MemoryManagementTest.cpp b/tests/unittests/media/client/main/clientController/MemoryManagementTest.cpp index 1e52746c4..6899ad80a 100644 --- a/tests/unittests/media/client/main/clientController/MemoryManagementTest.cpp +++ b/tests/unittests/media/client/main/clientController/MemoryManagementTest.cpp @@ -27,12 +27,15 @@ #include "ControlClientMock.h" #include "ControlIpcFactoryMock.h" #include "ControlIpcMock.h" +#include "PrivateMetricsIpcFactoryMock.h" +#include "PrivateMetricsIpcMock.h" using namespace firebolt::rialto; using namespace firebolt::rialto::client; using ::testing::_; using ::testing::DoAll; +using ::testing::NiceMock; using ::testing::Return; using ::testing::SetArgReferee; using ::testing::StrictMock; @@ -45,19 +48,26 @@ class ClientControllerMemoryManagementTest : public ::testing::Test std::shared_ptr> m_controlIpcFactoryMock; std::shared_ptr> m_controlIpcMock; + std::shared_ptr> m_privateMetricsIpcFactoryMock; + std::shared_ptr> m_privateMetricsIpcMock; std::shared_ptr> m_controlClientMock; std::unique_ptr m_sut; ClientControllerMemoryManagementTest() : m_controlIpcFactoryMock{std::make_shared>()}, m_controlIpcMock{std::make_shared>()}, + m_privateMetricsIpcFactoryMock{std::make_shared>()}, + m_privateMetricsIpcMock{std::make_shared>()}, m_controlClientMock{std::make_shared>()} { // Create a valid file descriptor m_fd = memfd_create("memfdfile", 0); EXPECT_CALL(*m_controlIpcFactoryMock, createControlIpc(_)).WillOnce(Return(m_controlIpcMock)); - EXPECT_NO_THROW(m_sut = std::make_unique(m_controlIpcFactoryMock)); + EXPECT_CALL(*m_privateMetricsIpcFactoryMock, createPrivateMetricsIpc(_)) + .WillOnce(Return(m_privateMetricsIpcMock)); + EXPECT_NO_THROW(m_sut = + std::make_unique(m_controlIpcFactoryMock, m_privateMetricsIpcFactoryMock)); } ~ClientControllerMemoryManagementTest() @@ -66,6 +76,8 @@ class ClientControllerMemoryManagementTest : public ::testing::Test m_controlIpcMock.reset(); m_controlIpcFactoryMock.reset(); + m_privateMetricsIpcMock.reset(); + m_privateMetricsIpcFactoryMock.reset(); close(m_fd); } diff --git a/tests/unittests/media/client/mocks/ipc/PrivateMetricsIpcFactoryMock.h b/tests/unittests/media/client/mocks/ipc/PrivateMetricsIpcFactoryMock.h new file mode 100644 index 000000000..4c028ac3b --- /dev/null +++ b/tests/unittests/media/client/mocks/ipc/PrivateMetricsIpcFactoryMock.h @@ -0,0 +1,40 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_CLIENT_PRIVATE_METRICS_IPC_FACTORY_MOCK_H_ +#define FIREBOLT_RIALTO_CLIENT_PRIVATE_METRICS_IPC_FACTORY_MOCK_H_ + +#include "IPrivateMetricsIpc.h" +#include +#include + +namespace firebolt::rialto::client +{ +class PrivateMetricsIpcFactoryMock : public IPrivateMetricsIpcFactory +{ +public: + PrivateMetricsIpcFactoryMock() = default; + virtual ~PrivateMetricsIpcFactoryMock() = default; + + MOCK_METHOD(std::shared_ptr, createPrivateMetricsIpc, (IPrivateMetricsIpcClient * client), + (override)); +}; +} // namespace firebolt::rialto::client + +#endif // FIREBOLT_RIALTO_CLIENT_PRIVATE_METRICS_IPC_FACTORY_MOCK_H_ diff --git a/tests/unittests/media/client/mocks/ipc/PrivateMetricsIpcMock.h b/tests/unittests/media/client/mocks/ipc/PrivateMetricsIpcMock.h new file mode 100644 index 000000000..b27eb93e3 --- /dev/null +++ b/tests/unittests/media/client/mocks/ipc/PrivateMetricsIpcMock.h @@ -0,0 +1,41 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_CLIENT_PRIVATE_METRICS_IPC_MOCK_H_ +#define FIREBOLT_RIALTO_CLIENT_PRIVATE_METRICS_IPC_MOCK_H_ + +#include "IPrivateMetricsIpc.h" +#include + +namespace firebolt::rialto::client +{ +class PrivateMetricsIpcMock : public IPrivateMetricsIpc +{ +public: + PrivateMetricsIpcMock() = default; + virtual ~PrivateMetricsIpcMock() = default; + + MOCK_METHOD(bool, reportClientMetrics, + (std::uint64_t sampleId, std::uint32_t reason, const std::string &appName, std::uint32_t processId, + std::uint64_t monotonicTimeMs, std::uint64_t epochTimeMs, std::uint64_t processCpuTimeMs), + (override)); +}; +} // namespace firebolt::rialto::client + +#endif // FIREBOLT_RIALTO_CLIENT_PRIVATE_METRICS_IPC_MOCK_H_ From 1d5f63488184cabe230b72fb296b66edc4c2c0fc Mon Sep 17 00:00:00 2001 From: Douglas Adler Date: Mon, 1 Jun 2026 11:58:56 -0500 Subject: [PATCH 03/11] Add metrics aggregation with state-aware reporting, thresholds, and pluggable output - Add process memory (VmRSS) to client metrics reporting - Add cgroup memory (v2 with v1 fallback) to server metrics - Implement Welford's algorithm for online mean/variance (MetricsAccumulator) - Add StateMetricsAggregator: accumulates per-state min/max/mean/stddev - Track playback state transitions per session, emit aggregated report on state change (e.g. PLAYING->END_OF_STREAM reports CPU/memory stats) - Track application state transitions (RUNNING/INACTIVE), emit report - Add IMetricsReporter interface with LogMetricsReporter and CompositeMetricsReporter for pluggable output destinations - Add MetricsThresholdChecker with configurable warning/critical levels and 2-sample debounce to avoid alert storms - Only aggregate and check thresholds on PERIODIC samples (not STATE_TRANSITION boundary samples which have unreliable CPU data) - Minimum 100ms elapsed time for CPU percentage calculation to prevent division-by-tiny-delta artifacts - Wire playback state notifications through MediaPipelineClient - Wire application state notifications through SessionManagementServer - Add METRICS_SAMPLE_REASON_STATE_TRANSITION to proto enum - Update mocks for new interface methods --- docs/MetricsDesign.md | 379 ++++++++++++++++++ media/client/ipc/include/PrivateMetricsIpc.h | 2 +- .../client/ipc/interface/IPrivateMetricsIpc.h | 3 +- media/client/ipc/source/PrivateMetricsIpc.cpp | 9 +- media/client/main/include/ClientController.h | 5 + media/client/main/source/ClientController.cpp | 23 +- media/server/ipc/CMakeLists.txt | 3 + .../ipc/include/CompositeMetricsReporter.h | 49 +++ .../ipc/include/IMediaPipelineModuleService.h | 6 + media/server/ipc/include/IMetricsReporter.h | 100 +++++ .../include/IPrivateMetricsModuleService.h | 5 + media/server/ipc/include/LogMetricsReporter.h | 42 ++ .../server/ipc/include/MediaPipelineClient.h | 7 +- .../ipc/include/MediaPipelineModuleService.h | 2 + media/server/ipc/include/MetricsAccumulator.h | 102 +++++ .../ipc/include/MetricsThresholdChecker.h | 93 +++++ .../ipc/include/PrivateMetricsModuleService.h | 33 ++ .../ipc/include/SessionManagementServer.h | 1 + .../ipc/include/StateMetricsAggregator.h | 131 ++++++ .../ipc/interface/ISessionManagementServer.h | 2 + .../ipc/source/CompositeMetricsReporter.cpp | 55 +++ .../server/ipc/source/LogMetricsReporter.cpp | 67 ++++ .../server/ipc/source/MediaPipelineClient.cpp | 17 +- .../ipc/source/MediaPipelineModuleService.cpp | 9 +- .../ipc/source/MetricsThresholdChecker.cpp | 102 +++++ .../source/PrivateMetricsModuleService.cpp | 337 +++++++++++++++- .../ipc/source/SessionManagementServer.cpp | 9 + .../service/source/SessionServerManager.cpp | 4 + proto/privatemetricsmodule.proto | 2 + .../client/mocks/ipc/PrivateMetricsIpcMock.h | 3 +- .../ipc/MediaPipelineModuleServiceMock.h | 2 + .../mocks/ipc/SessionManagementServerMock.h | 1 + 32 files changed, 1578 insertions(+), 27 deletions(-) create mode 100644 docs/MetricsDesign.md create mode 100644 media/server/ipc/include/CompositeMetricsReporter.h create mode 100644 media/server/ipc/include/IMetricsReporter.h create mode 100644 media/server/ipc/include/LogMetricsReporter.h create mode 100644 media/server/ipc/include/MetricsAccumulator.h create mode 100644 media/server/ipc/include/MetricsThresholdChecker.h create mode 100644 media/server/ipc/include/StateMetricsAggregator.h create mode 100644 media/server/ipc/source/CompositeMetricsReporter.cpp create mode 100644 media/server/ipc/source/LogMetricsReporter.cpp create mode 100644 media/server/ipc/source/MetricsThresholdChecker.cpp diff --git a/docs/MetricsDesign.md b/docs/MetricsDesign.md new file mode 100644 index 000000000..efdf73417 --- /dev/null +++ b/docs/MetricsDesign.md @@ -0,0 +1,379 @@ +# Metrics Gathering System Design + +## Overview + +The Rialto metrics system provides CPU and memory usage monitoring for both client and server processes, with state-aware aggregation, configurable thresholds, and pluggable output. + +The system is built on top of the `PrivateMetricsModule` — a dedicated IPC service channel between the Rialto client library and the Rialto server that is separate from the media pipeline control path. It is "private" in the sense that it is an internal implementation detail not exposed to application developers. + +## Why "Private" Metrics + +Rialto uses a client-server architecture where the client library (`libRialtoClient.so`) runs inside the application process and communicates with a separate `rialto-server` process via protobuf-over-Unix-socket IPC. The metrics system needs data from *both* processes: + +- **Client process**: CPU time and memory of the application hosting the media pipeline +- **Server process**: CPU time, memory, and cgroup resource limits of the renderer + +Since these are different processes, the server cannot simply read `/proc/self/...` to get client data — it must ask the client to report it. The `PrivateMetricsModule` provides this request/response channel. + +## PrivateMetrics IPC Protocol + +### Proto Definition (`privatemetricsmodule.proto`) + +```protobuf +enum MetricsSampleReason { + METRICS_SAMPLE_REASON_UNKNOWN = 0; + METRICS_SAMPLE_REASON_CONNECTED = 1; + METRICS_SAMPLE_REASON_PERIODIC = 2; + METRICS_SAMPLE_REASON_STATE_TRANSITION = 3; +} + +message ClientProcessMetrics { + optional uint64 sample_id = 1; + optional MetricsSampleReason reason = 2; + optional string app_name = 3; + optional uint32 process_id = 4; + optional uint64 monotonic_time_ms = 5; + optional uint64 epoch_time_ms = 6; + optional uint64 process_cpu_time_ms = 7; + optional uint64 process_memory_kb = 8; +} + +message MetricsSampleRequestEvent { + optional uint64 sample_id = 1; + optional MetricsSampleReason reason = 2; +} + +service PrivateMetricsModule { + rpc notifyClientReady(NotifyClientReadyRequest) returns (NotifyClientReadyResponse); + rpc reportClientMetrics(ReportClientMetricsRequest) returns (ReportClientMetricsResponse); +} +``` + +### Communication Pattern + +The protocol uses a **server-initiated push** model: + +```mermaid +sequenceDiagram + participant Client as Client (ClientController) + participant Server as Server (PrivateMetricsModuleService) + + Note over Client,Server: Client connects via IPC socket + Server->>Client: exportService(PrivateMetricsModule) + Client->>Server: notifyClientReady() + Server->>Client: MetricsSampleRequestEvent(id=1, reason=CONNECTED) + Client->>Server: reportClientMetrics(ClientProcessMetrics) + Note over Server: Stores baseline (no CPU% yet) + + loop Every 15 seconds + Server->>Client: MetricsSampleRequestEvent(id=N, reason=PERIODIC) + Client->>Server: reportClientMetrics(ClientProcessMetrics) + Note over Server: Compute CPU%, feed aggregators, check thresholds + end + + Note over Server: Playback state changes + Server->>Client: MetricsSampleRequestEvent(id=N, reason=STATE_TRANSITION) + Client->>Server: reportClientMetrics(ClientProcessMetrics) +``` + +Key design points: +- The **server drives timing** — the client never spontaneously reports; it only responds to requests +- The `sample_id` field correlates requests with responses and provides ordering +- The `reason` field is echoed back by the client so the server knows how to handle the response +- The service is exported per-client on connection, allowing multi-client support + +### Client Side (`ClientController` + `PrivateMetricsIpc`) + +When the client library initializes (via `ClientController`), it: +1. Creates a `PrivateMetricsIpc` that subscribes to `MetricsSampleRequestEvent` +2. Calls `notifyClientReady()` to signal the server it can accept sample requests +3. On each `MetricsSampleRequestEvent`, gathers: + - `monotonic_time_ms`: `CLOCK_MONOTONIC` in milliseconds + - `epoch_time_ms`: wall-clock time (for log correlation) + - `process_cpu_time_ms`: `CLOCK_PROCESS_CPUTIME_ID` (total user+system CPU) + - `process_memory_kb`: VmRSS from `/proc/self/status` + - `app_name`: from `/proc/self/comm` + - `process_id`: `getpid()` +4. Sends the data back via `reportClientMetrics()` + +### Server Side (`PrivateMetricsModuleService`) + +The server maintains per-client state: + +```cpp +struct ClientMetricsState { + bool isReady{false}; + std::optional latestMetrics; // previous sample for delta computation +}; +``` + +On receiving `reportClientMetrics`: +1. Takes its own `ProcessMetricsSample` (server CPU, memory, cgroup) +2. If a previous sample exists, computes CPU percentages from time deltas +3. Passes the paired client+server data through the reporting/aggregation pipeline +4. Stores the sample as `latestMetrics` for next delta computation + +### Threading Model + +``` +┌─────────────────────────────────────────────────────────────┐ +│ m_metricsThread (sampler) │ +│ - Sleeps 15s via condition_variable │ +│ - Wakes and sends MetricsSampleRequestEvent to clients │ +│ - Can be woken early by m_wakeup.notify_all() on stop │ +└─────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────┐ +│ IPC event loop thread │ +│ - Receives reportClientMetrics RPC calls │ +│ - Calls logMetrics() → reporter → aggregator → threshold │ +│ - Receives notifyPlaybackState events │ +│ - Calls notifyPlaybackStateChanged() │ +└─────────────────────────────────────────────────────────────┘ +``` + +The `m_mutex` protects shared state (`m_clients`, `m_sessionStates`, `m_globalAggregator`) accessed from both threads. + +### Lifecycle + +1. **Server start**: `SessionManagementServer` creates `PrivateMetricsModuleService` via factory +2. **Client connects**: `clientConnected()` → registers client, exports service +3. **Client ready**: `notifyClientReady()` → marks client ready, requests initial sample +4. **Periodic collection**: sampler thread fires every 15s +5. **State changes**: `MediaPipelineClient` and `SessionServerManager` notify as states transition +6. **Client disconnects**: `clientDisconnected()` → removes client state +7. **Server stop**: destructor sets `m_isRunning=false`, wakes sampler thread, joins it + +## System Architecture + +```mermaid +graph TD + subgraph Client Process + APP[Application] --> RCL[libRialtoClient.so] + RCL --> CC[ClientController] + CC -->|VmRSS, CPU time| PI[PrivateMetricsIpc] + PI -->|protobuf RPC| IPC((Unix Socket IPC)) + end + + subgraph Server Process + IPC --> SMS2[SessionManagementServer] + SMS2 --> PMS[PrivateMetricsModuleService] + PMS -->|samples /proc, cgroup| OS[OS Interfaces] + PMS --> AGG[StateMetricsAggregator] + PMS --> THR[MetricsThresholdChecker] + PMS --> REP[IMetricsReporter] + + MPC[MediaPipelineClient] -->|notifyPlaybackStateChanged| PMS + SMS[SessionServerManager] -->|notifyApplicationStateChanged| SMS2 + SMS2 --> PMS + + REP --> LOG[LogMetricsReporter] + REP --> COMP[CompositeMetricsReporter] + COMP --> LOG + COMP --> REMOTE[Future: RemoteTelemetryReporter] + end +``` + +## Components + +### Data Collection + +| Component | Role | +|-----------|------| +| `ClientController` | Reads client VmRSS from `/proc/self/status` and CPU time via `clock_gettime(CLOCK_PROCESS_CPUTIME_ID)` | +| `PrivateMetricsModuleService` | Reads server CPU time via `times()`, server VmRSS from `/proc/self/status`, cgroup memory from `/sys/fs/cgroup/.../memory.current` | +| `privatemetricsmodule.proto` | Defines `ClientProcessMetrics` message with `process_memory_kb` field and `MetricsSampleReason` enum | + +### Sampling + +- **Periodic**: Every 15 seconds, the server requests a sample from connected clients +- **On connection**: Baseline sample taken immediately (no CPU % computed) +- **State transitions**: Immediate sample requested for clean state boundaries (reported but not fed into aggregators due to unreliable CPU data from tiny time deltas) + +### CPU Percentage Calculation + +``` +CPU% = (cpu_time_delta_ms / wall_time_delta_ms) × 100 +``` + +- Computed independently for client, server, and combined (client+server) +- Multi-core systems can exceed 100% (acceptable) +- **Minimum elapsed time**: 100ms threshold; returns 0% if wall-clock delta is too small to prevent division artifacts + +### Cgroup Memory + +Resolved dynamically from `/proc/self/cgroup`: +1. Parse cgroup v2 line (`0::`) +2. Read `/sys/fs/cgroup//memory.current` and `memory.max` +3. Fall back to cgroup v1 paths if v2 unavailable +4. Value of "max" (unlimited) → reported as 0 + +### State-Aware Aggregation + +#### MetricsAccumulator (Welford's Algorithm) + +Header-only implementation providing O(1) memory online computation of: +- Count, min, max, mean, standard deviation + +#### StateMetricsAggregator + +Wraps 6 `MetricsAccumulator` instances (one per metric dimension): +- Client CPU %, Server CPU %, Combined CPU % +- Client memory KB, Server memory KB, Cgroup memory KB + +Lifecycle: +1. `begin(stateName, startTimeMs)` — reset and start accumulating +2. `addSample(MetricsSample)` — feed each periodic sample +3. `finalize(endTimeMs)` → `StateMetricsReport` with duration and all stats + +#### Per-Session Tracking + +Each media pipeline session has a `SessionMetricsState` containing: +- Current `PlaybackState` +- A `StateMetricsAggregator` + +On playback state change: +1. Finalize the old state's aggregator → emit report +2. Begin a new accumulation period for the new state +3. On terminal states (STOPPED, END_OF_STREAM, FAILURE) — remove session + +#### Global Tracking + +A single `StateMetricsAggregator` tracks the RUNNING application state period. +On transition from RUNNING → INACTIVE, the report is emitted. + +### Threshold Checking + +`MetricsThresholdChecker` evaluates each PERIODIC sample against configured limits: + +| Metric | Warning | Critical | +|--------|---------|----------| +| Client CPU % | 80 | 95 | +| Server CPU % | 80 | 95 | +| Combined CPU % | 150 | 190 | +| Client memory KB | 512,000 | 768,000 | +| Server memory KB | 512,000 | 768,000 | +| Cgroup memory % | 80 | 95 | + +**Debounce**: An alert fires once when exceeded. It can fire again only after the metric drops below the threshold for 2 consecutive samples. + +### Output (IMetricsReporter) + +Abstract interface with three report types: + +| Method | When | +|--------|------| +| `reportPeriodicSample` | Every sampling interval | +| `reportStateTransition` | On playback/application state change | +| `reportThresholdExceeded` | When a metric breaches a threshold | + +Implementations: +- **LogMetricsReporter** — writes to Rialto server log (default) +- **CompositeMetricsReporter** — fans out to multiple reporters (for adding remote telemetry) + +## Data Flow + +``` +┌─ Every 15s ─────────────────────────────────────────────────────────────┐ +│ │ +│ Timer fires → requestMetricsSample(PERIODIC) to all ready clients │ +│ Client responds with ClientProcessMetrics (CPU time, memory, etc.) │ +│ Server takes its own sample (CPU, memory, cgroup) │ +│ logMetrics(): │ +│ 1. Compute CPU percentages from deltas │ +│ 2. Report via IMetricsReporter::reportPeriodicSample │ +│ 3. Feed sample into all active session aggregators │ +│ 4. Feed sample into global aggregator (if RUNNING) │ +│ 5. Check thresholds │ +│ │ +└──────────────────────────────────────────────────────────────────────────┘ + +┌─ On State Change ───────────────────────────────────────────────────────┐ +│ │ +│ MediaPipelineClient::notifyPlaybackState(newState) │ +│ → notifyPlaybackStateChanged(sessionId, oldState, newState) │ +│ → Finalize old state aggregator │ +│ → IMetricsReporter::reportStateTransition (aggregated stats) │ +│ → Begin new state aggregator │ +│ → Request STATE_TRANSITION sample (for log visibility only) │ +│ │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +## Example Output + +### Periodic Sample +``` +Metrics sample=5, reason=PERIODIC, app='python3', client_pid=11708, + client_cpu=0.47%, server_cpu=23.13%, combined_cpu=23.60%, + client_cpu_ms=440, server_cpu_ms=4250, + client_mem_kb=59648, server_mem_kb=216684, cgroup_mem_kb=209016/0 +``` + +### State Transition Report +``` +Metrics state report [session=0] state='PLAYING', duration_ms=30607, samples=2, + client_cpu={min=0.47, max=10.11, mean=5.29, stddev=6.81}%, + server_cpu={min=23.13, max=26.40, mean=24.77, stddev=2.31}%, + combined_cpu={min=23.60, max=36.52, mean=30.06, stddev=9.13}%, + client_mem_kb={min=59520, max=59648, mean=59584}, + server_mem_kb={min=214704, max=216684, mean=215694}, + cgroup_mem_kb={min=206332, max=209016, mean=207674} +``` + +### Threshold Alert +``` +Metrics threshold WARNING: server_cpu=88.24 exceeds 80.00 +``` + +## Extension Points + +1. **Remote telemetry**: Implement `IMetricsReporter` and add to `CompositeMetricsReporter` +2. **Custom thresholds**: Pass a different `MetricsThresholdConfig` to the constructor +3. **JSON config loading**: Add a loader that reads `MetricsThresholdConfig` from a file +4. **Per-session threshold tuning**: Different limits for different pipeline types +5. **QoS metrics**: Separate data path for dropped frames / buffer underruns + +## File Inventory + +### Client Side + +| File | Purpose | +|------|---------| +| `media/client/ipc/interface/IPrivateMetricsIpc.h` | Client-side metrics IPC interface | +| `media/client/ipc/include/PrivateMetricsIpc.h` | Client-side IPC implementation header | +| `media/client/ipc/source/PrivateMetricsIpc.cpp` | Subscribes to sample requests, gathers and sends metrics | +| `media/client/main/include/ClientController.h` | Client initialization, owns PrivateMetricsIpc | +| `media/client/main/source/ClientController.cpp` | Reads VmRSS, reports metrics on request | + +### Server Side + +| File | Purpose | +|------|---------| +| `media/server/ipc/include/IPrivateMetricsModuleService.h` | Server metrics service interface | +| `media/server/ipc/include/PrivateMetricsModuleService.h` | Concrete service with sampler thread and aggregators | +| `media/server/ipc/source/PrivateMetricsModuleService.cpp` | Core sampling, aggregation, wiring | +| `media/server/ipc/source/MediaPipelineClient.cpp` | Playback state hook | +| `media/server/ipc/source/MediaPipelineModuleService.cpp` | Passes metrics service to pipeline clients | +| `media/server/ipc/source/SessionManagementServer.cpp` | Application state hook, owns metrics service | +| `media/server/service/source/SessionServerManager.cpp` | Triggers app state notifications | + +### Metrics Framework + +| File | Purpose | +|------|---------| +| `media/server/ipc/include/MetricsAccumulator.h` | Welford's online mean/variance | +| `media/server/ipc/include/StateMetricsAggregator.h` | Per-state multi-metric accumulation | +| `media/server/ipc/include/IMetricsReporter.h` | Reporter interface + report structs | +| `media/server/ipc/include/LogMetricsReporter.h` | Log-based reporter | +| `media/server/ipc/include/CompositeMetricsReporter.h` | Multi-reporter fanout | +| `media/server/ipc/include/MetricsThresholdChecker.h` | Threshold config + checker | +| `media/server/ipc/source/LogMetricsReporter.cpp` | Reporter implementation | +| `media/server/ipc/source/CompositeMetricsReporter.cpp` | Fanout implementation | +| `media/server/ipc/source/MetricsThresholdChecker.cpp` | Threshold checking logic | + +### Protocol + +| File | Purpose | +|------|---------| +| `proto/privatemetricsmodule.proto` | IPC message and service definitions | diff --git a/media/client/ipc/include/PrivateMetricsIpc.h b/media/client/ipc/include/PrivateMetricsIpc.h index dbe67e1fb..be10f066d 100644 --- a/media/client/ipc/include/PrivateMetricsIpc.h +++ b/media/client/ipc/include/PrivateMetricsIpc.h @@ -48,7 +48,7 @@ class PrivateMetricsIpc : public IPrivateMetricsIpc, public IpcModule bool reportClientMetrics(std::uint64_t sampleId, std::uint32_t reason, const std::string &appName, std::uint32_t processId, std::uint64_t monotonicTimeMs, std::uint64_t epochTimeMs, - std::uint64_t processCpuTimeMs) override; + std::uint64_t processCpuTimeMs, std::uint64_t processMemoryKb) override; private: bool notifyClientReady(); diff --git a/media/client/ipc/interface/IPrivateMetricsIpc.h b/media/client/ipc/interface/IPrivateMetricsIpc.h index 253f2d262..a23dffae7 100644 --- a/media/client/ipc/interface/IPrivateMetricsIpc.h +++ b/media/client/ipc/interface/IPrivateMetricsIpc.h @@ -66,7 +66,8 @@ class IPrivateMetricsIpc virtual bool reportClientMetrics(std::uint64_t sampleId, std::uint32_t reason, const std::string &appName, std::uint32_t processId, std::uint64_t monotonicTimeMs, - std::uint64_t epochTimeMs, std::uint64_t processCpuTimeMs) = 0; + std::uint64_t epochTimeMs, std::uint64_t processCpuTimeMs, + std::uint64_t processMemoryKb) = 0; }; } // namespace firebolt::rialto::client diff --git a/media/client/ipc/source/PrivateMetricsIpc.cpp b/media/client/ipc/source/PrivateMetricsIpc.cpp index a7435dd18..ffd49ac71 100644 --- a/media/client/ipc/source/PrivateMetricsIpc.cpp +++ b/media/client/ipc/source/PrivateMetricsIpc.cpp @@ -95,7 +95,8 @@ PrivateMetricsIpc::~PrivateMetricsIpc() bool PrivateMetricsIpc::reportClientMetrics(std::uint64_t sampleId, std::uint32_t reason, const std::string &appName, std::uint32_t processId, std::uint64_t monotonicTimeMs, - std::uint64_t epochTimeMs, std::uint64_t processCpuTimeMs) + std::uint64_t epochTimeMs, std::uint64_t processCpuTimeMs, + std::uint64_t processMemoryKb) { if (!reattachChannelIfRequired()) { @@ -112,11 +113,13 @@ bool PrivateMetricsIpc::reportClientMetrics(std::uint64_t sampleId, std::uint32_ metrics->set_monotonic_time_ms(monotonicTimeMs); metrics->set_epoch_time_ms(epochTimeMs); metrics->set_process_cpu_time_ms(processCpuTimeMs); + metrics->set_process_memory_kb(processMemoryKb); - RIALTO_CLIENT_LOG_MIL("Reporting metrics sample=%" PRIu64 ", reason=%s, app='%s', pid=%u, cpu_ms=%" PRIu64, + RIALTO_CLIENT_LOG_MIL("Reporting metrics sample=%" PRIu64 ", reason=%s, app='%s', pid=%u, cpu_ms=%" PRIu64 + ", mem_kb=%" PRIu64, sampleId, sampleReasonToString(static_cast(reason)), - appName.c_str(), processId, processCpuTimeMs); + appName.c_str(), processId, processCpuTimeMs, processMemoryKb); firebolt::rialto::ReportClientMetricsResponse response; auto ipcController = m_ipc.createRpcController(); diff --git a/media/client/main/include/ClientController.h b/media/client/main/include/ClientController.h index 6946c1744..af522b917 100644 --- a/media/client/main/include/ClientController.h +++ b/media/client/main/include/ClientController.h @@ -98,6 +98,11 @@ class ClientController : public IClientController, public IControlClient, public */ std::uint64_t getProcessCpuTimeMs() const; + /** + * @brief Gets process RSS memory usage in kilobytes. + */ + std::uint64_t getProcessMemoryKb() const; + /** * @brief Gets the process name used for metrics reporting. */ diff --git a/media/client/main/source/ClientController.cpp b/media/client/main/source/ClientController.cpp index a5c19509b..f84923c02 100644 --- a/media/client/main/source/ClientController.cpp +++ b/media/client/main/source/ClientController.cpp @@ -22,6 +22,7 @@ #include "SharedMemoryHandle.h" #include #include +#include #include #include #include @@ -295,7 +296,8 @@ void ClientController::changeStateAndNotifyClients(ApplicationState state) void ClientController::reportClientMetrics(std::uint64_t sampleId, std::uint32_t reason) { if (!m_privateMetricsIpc->reportClientMetrics(sampleId, reason, getProcessName(), static_cast(getpid()), - getMonotonicTimeMs(), getEpochTimeMs(), getProcessCpuTimeMs())) + getMonotonicTimeMs(), getEpochTimeMs(), getProcessCpuTimeMs(), + getProcessMemoryKb())) { RIALTO_CLIENT_LOG_WARN("Failed to report client process metrics"); } @@ -346,4 +348,23 @@ std::string ClientController::getProcessName() const } return "unknown"; } + +std::uint64_t ClientController::getProcessMemoryKb() const +{ + std::ifstream status{"/proc/self/status"}; + std::string line; + while (std::getline(status, line)) + { + if (line.rfind("VmRSS:", 0) == 0) + { + std::uint64_t memKb{0}; + if (std::sscanf(line.c_str(), "VmRSS: %" SCNu64, &memKb) == 1) + { + return memKb; + } + } + } + RIALTO_CLIENT_LOG_WARN("Failed to sample client process memory usage"); + return 0; +} } // namespace firebolt::rialto::client diff --git a/media/server/ipc/CMakeLists.txt b/media/server/ipc/CMakeLists.txt index 3ed0c879a..15bd28844 100644 --- a/media/server/ipc/CMakeLists.txt +++ b/media/server/ipc/CMakeLists.txt @@ -42,6 +42,9 @@ add_library ( source/ControlClientServerInternal.cpp source/ControlModuleService.cpp source/PrivateMetricsModuleService.cpp + source/LogMetricsReporter.cpp + source/CompositeMetricsReporter.cpp + source/MetricsThresholdChecker.cpp source/ServerManagerModuleService.cpp source/SessionManagementServer.cpp source/SetLogLevelsService.cpp diff --git a/media/server/ipc/include/CompositeMetricsReporter.h b/media/server/ipc/include/CompositeMetricsReporter.h new file mode 100644 index 000000000..c08471517 --- /dev/null +++ b/media/server/ipc/include/CompositeMetricsReporter.h @@ -0,0 +1,49 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_IPC_COMPOSITE_METRICS_REPORTER_H_ +#define FIREBOLT_RIALTO_SERVER_IPC_COMPOSITE_METRICS_REPORTER_H_ + +#include "IMetricsReporter.h" +#include +#include + +namespace firebolt::rialto::server::ipc +{ +/** + * @brief Fans out metrics to multiple reporters (log + remote telemetry, etc.) + */ +class CompositeMetricsReporter : public IMetricsReporter +{ +public: + CompositeMetricsReporter() = default; + ~CompositeMetricsReporter() override = default; + + void addReporter(std::unique_ptr reporter); + + void reportPeriodicSample(const PeriodicMetricsReport &report) override; + void reportStateTransition(const StateTransitionReport &report) override; + void reportThresholdExceeded(const ThresholdAlert &alert) override; + +private: + std::vector> m_reporters; +}; +} // namespace firebolt::rialto::server::ipc + +#endif // FIREBOLT_RIALTO_SERVER_IPC_COMPOSITE_METRICS_REPORTER_H_ diff --git a/media/server/ipc/include/IMediaPipelineModuleService.h b/media/server/ipc/include/IMediaPipelineModuleService.h index 877d3912b..7caf1c9ac 100644 --- a/media/server/ipc/include/IMediaPipelineModuleService.h +++ b/media/server/ipc/include/IMediaPipelineModuleService.h @@ -28,6 +28,7 @@ namespace firebolt::rialto::server::ipc { class IMediaPipelineModuleService; +class IPrivateMetricsModuleService; /** * @brief IMediaPipelineModuleService factory class, returns a concrete implementation of IMediaPipelineModuleService @@ -82,6 +83,11 @@ class IMediaPipelineModuleService : public ::firebolt::rialto::MediaPipelineModu * @param[in] ipcClient : The ipc client to disconnect to. */ virtual void clientDisconnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) = 0; + + /** + * @brief Set the metrics service for state transition notifications. + */ + virtual void setMetricsService(const std::shared_ptr &metricsService) = 0; }; } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/include/IMetricsReporter.h b/media/server/ipc/include/IMetricsReporter.h new file mode 100644 index 000000000..0e6d9cd97 --- /dev/null +++ b/media/server/ipc/include/IMetricsReporter.h @@ -0,0 +1,100 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_IPC_I_METRICS_REPORTER_H_ +#define FIREBOLT_RIALTO_SERVER_IPC_I_METRICS_REPORTER_H_ + +#include "StateMetricsAggregator.h" +#include +#include +#include + +namespace firebolt::rialto::server::ipc +{ +/** + * @brief Periodic sample data reported each sampling interval. + */ +struct PeriodicMetricsReport +{ + std::uint64_t sampleId{0}; + std::string reason; + std::string appName; + std::uint32_t clientPid{0}; + double clientCpuPercent{0.0}; + double serverCpuPercent{0.0}; + double combinedCpuPercent{0.0}; + std::uint64_t clientCpuTimeMs{0}; + std::uint64_t serverCpuTimeMs{0}; + std::uint64_t clientMemoryKb{0}; + std::uint64_t serverMemoryKb{0}; + std::uint64_t cgroupMemoryUsageKb{0}; + std::uint64_t cgroupMemoryLimitKb{0}; +}; + +/** + * @brief Report emitted when a state period ends (playback state or application state). + */ +struct StateTransitionReport +{ + std::string context; // e.g. "session=1" or "global" + StateMetricsReport metrics; +}; + +/** + * @brief Severity level for threshold alerts. + */ +enum class ThresholdSeverity +{ + WARNING, + CRITICAL +}; + +/** + * @brief Alert emitted when a metric exceeds a configured threshold. + */ +struct ThresholdAlert +{ + std::string metricName; + double currentValue{0.0}; + double thresholdValue{0.0}; + ThresholdSeverity severity{ThresholdSeverity::WARNING}; +}; + +/** + * @brief Abstract interface for metrics output. + * Implementations can log, push to remote telemetry, or both. + */ +class IMetricsReporter +{ +public: + IMetricsReporter() = default; + virtual ~IMetricsReporter() = default; + + IMetricsReporter(const IMetricsReporter &) = delete; + IMetricsReporter &operator=(const IMetricsReporter &) = delete; + IMetricsReporter(IMetricsReporter &&) = delete; + IMetricsReporter &operator=(IMetricsReporter &&) = delete; + + virtual void reportPeriodicSample(const PeriodicMetricsReport &report) = 0; + virtual void reportStateTransition(const StateTransitionReport &report) = 0; + virtual void reportThresholdExceeded(const ThresholdAlert &alert) = 0; +}; +} // namespace firebolt::rialto::server::ipc + +#endif // FIREBOLT_RIALTO_SERVER_IPC_I_METRICS_REPORTER_H_ diff --git a/media/server/ipc/include/IPrivateMetricsModuleService.h b/media/server/ipc/include/IPrivateMetricsModuleService.h index 07cca10fd..ae97d8658 100644 --- a/media/server/ipc/include/IPrivateMetricsModuleService.h +++ b/media/server/ipc/include/IPrivateMetricsModuleService.h @@ -20,6 +20,8 @@ #ifndef FIREBOLT_RIALTO_SERVER_IPC_I_PRIVATE_METRICS_MODULE_SERVICE_H_ #define FIREBOLT_RIALTO_SERVER_IPC_I_PRIVATE_METRICS_MODULE_SERVICE_H_ +#include "ControlCommon.h" +#include "MediaCommon.h" #include "privatemetricsmodule.pb.h" #include #include @@ -53,6 +55,9 @@ class IPrivateMetricsModuleService : public ::firebolt::rialto::PrivateMetricsMo virtual void clientConnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) = 0; virtual void clientDisconnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) = 0; + + virtual void notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) = 0; + virtual void notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) = 0; }; } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/include/LogMetricsReporter.h b/media/server/ipc/include/LogMetricsReporter.h new file mode 100644 index 000000000..0a423c742 --- /dev/null +++ b/media/server/ipc/include/LogMetricsReporter.h @@ -0,0 +1,42 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_IPC_LOG_METRICS_REPORTER_H_ +#define FIREBOLT_RIALTO_SERVER_IPC_LOG_METRICS_REPORTER_H_ + +#include "IMetricsReporter.h" + +namespace firebolt::rialto::server::ipc +{ +/** + * @brief Outputs metrics to the Rialto log system (default reporter). + */ +class LogMetricsReporter : public IMetricsReporter +{ +public: + LogMetricsReporter() = default; + ~LogMetricsReporter() override = default; + + void reportPeriodicSample(const PeriodicMetricsReport &report) override; + void reportStateTransition(const StateTransitionReport &report) override; + void reportThresholdExceeded(const ThresholdAlert &alert) override; +}; +} // namespace firebolt::rialto::server::ipc + +#endif // FIREBOLT_RIALTO_SERVER_IPC_LOG_METRICS_REPORTER_H_ diff --git a/media/server/ipc/include/MediaPipelineClient.h b/media/server/ipc/include/MediaPipelineClient.h index 685f2d325..ac45db7a0 100644 --- a/media/server/ipc/include/MediaPipelineClient.h +++ b/media/server/ipc/include/MediaPipelineClient.h @@ -27,10 +27,13 @@ namespace firebolt::rialto::server::ipc { +class IPrivateMetricsModuleService; + class MediaPipelineClient : public IMediaPipelineClient { public: - MediaPipelineClient(int sessionId, const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient); + MediaPipelineClient(int sessionId, const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient, + IPrivateMetricsModuleService *metricsService = nullptr); ~MediaPipelineClient() override; void notifyDuration(int64_t duration) override; @@ -53,6 +56,8 @@ class MediaPipelineClient : public IMediaPipelineClient private: int m_sessionId; std::shared_ptr<::firebolt::rialto::ipc::IClient> m_ipcClient; + IPrivateMetricsModuleService *m_metricsService; + PlaybackState m_currentPlaybackState{PlaybackState::UNKNOWN}; // It is possible for a needData to be sent while a source is been attached, // this causes an issue in client side as they recieve a needData from a source diff --git a/media/server/ipc/include/MediaPipelineModuleService.h b/media/server/ipc/include/MediaPipelineModuleService.h index 91787419a..6e30c7f4e 100644 --- a/media/server/ipc/include/MediaPipelineModuleService.h +++ b/media/server/ipc/include/MediaPipelineModuleService.h @@ -46,6 +46,7 @@ class MediaPipelineModuleService : public IMediaPipelineModuleService void clientConnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) override; void clientDisconnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) override; + void setMetricsService(const std::shared_ptr &metricsService) override; void createSession(::google::protobuf::RpcController *controller, const ::firebolt::rialto::CreateSessionRequest *request, @@ -172,6 +173,7 @@ class MediaPipelineModuleService : public IMediaPipelineModuleService private: service::IMediaPipelineService &m_mediaPipelineService; + std::shared_ptr m_metricsService; std::map, std::set> m_clientSessions; }; } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/include/MetricsAccumulator.h b/media/server/ipc/include/MetricsAccumulator.h new file mode 100644 index 000000000..be54d3ca4 --- /dev/null +++ b/media/server/ipc/include/MetricsAccumulator.h @@ -0,0 +1,102 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_IPC_METRICS_ACCUMULATOR_H_ +#define FIREBOLT_RIALTO_SERVER_IPC_METRICS_ACCUMULATOR_H_ + +#include +#include +#include + +namespace firebolt::rialto::server::ipc +{ +struct MetricsStatistics +{ + double min{0.0}; + double max{0.0}; + double mean{0.0}; + double stddev{0.0}; + std::uint64_t count{0}; +}; + +/** + * @brief Numerically stable running statistics using Welford's online algorithm. + * Computes min, max, mean, and standard deviation in O(1) memory. + */ +class MetricsAccumulator +{ +public: + MetricsAccumulator() = default; + ~MetricsAccumulator() = default; + + void addSample(double value) + { + ++m_count; + if (value < m_min) + { + m_min = value; + } + if (value > m_max) + { + m_max = value; + } + + // Welford's online algorithm + const double kDelta{value - m_mean}; + m_mean += kDelta / static_cast(m_count); + const double kDelta2{value - m_mean}; + m_m2 += kDelta * kDelta2; + } + + void reset() + { + m_count = 0; + m_min = std::numeric_limits::max(); + m_max = std::numeric_limits::lowest(); + m_mean = 0.0; + m_m2 = 0.0; + } + + MetricsStatistics getStats() const + { + MetricsStatistics stats; + stats.count = m_count; + if (m_count == 0) + { + return stats; + } + stats.min = m_min; + stats.max = m_max; + stats.mean = m_mean; + stats.stddev = (m_count > 1) ? std::sqrt(m_m2 / static_cast(m_count - 1)) : 0.0; + return stats; + } + + std::uint64_t getCount() const { return m_count; } + +private: + std::uint64_t m_count{0}; + double m_min{std::numeric_limits::max()}; + double m_max{std::numeric_limits::lowest()}; + double m_mean{0.0}; + double m_m2{0.0}; +}; +} // namespace firebolt::rialto::server::ipc + +#endif // FIREBOLT_RIALTO_SERVER_IPC_METRICS_ACCUMULATOR_H_ diff --git a/media/server/ipc/include/MetricsThresholdChecker.h b/media/server/ipc/include/MetricsThresholdChecker.h new file mode 100644 index 000000000..b909b1c6f --- /dev/null +++ b/media/server/ipc/include/MetricsThresholdChecker.h @@ -0,0 +1,93 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_IPC_METRICS_THRESHOLD_CHECKER_H_ +#define FIREBOLT_RIALTO_SERVER_IPC_METRICS_THRESHOLD_CHECKER_H_ + +#include "IMetricsReporter.h" +#include +#include +#include + +namespace firebolt::rialto::server::ipc +{ +/** + * @brief Configuration for a single metric threshold. + */ +struct MetricsThreshold +{ + std::string metricName; + double warningLevel{0.0}; + double criticalLevel{0.0}; +}; + +/** + * @brief Complete threshold configuration. + */ +struct MetricsThresholdConfig +{ + MetricsThreshold clientCpu{"client_cpu", 80.0, 95.0}; + MetricsThreshold serverCpu{"server_cpu", 80.0, 95.0}; + MetricsThreshold combinedCpu{"combined_cpu", 150.0, 190.0}; + MetricsThreshold clientMemoryKb{"client_mem_kb", 512000.0, 768000.0}; + MetricsThreshold serverMemoryKb{"server_mem_kb", 512000.0, 768000.0}; + MetricsThreshold cgroupMemoryPercent{"cgroup_mem_pct", 80.0, 95.0}; +}; + +/** + * @brief Checks metric samples against configured thresholds with debounce. + * + * An alert fires when a metric exceeds the threshold. + * The alert resets (can fire again) only after the metric drops below the threshold + * for at least kDebounceSamples consecutive samples. + */ +class MetricsThresholdChecker +{ +public: + explicit MetricsThresholdChecker(MetricsThresholdConfig config, IMetricsReporter *reporter); + ~MetricsThresholdChecker() = default; + + void checkSample(double clientCpu, double serverCpu, double combinedCpu, std::uint64_t clientMemKb, + std::uint64_t serverMemKb, std::uint64_t cgroupUsageKb, std::uint64_t cgroupLimitKb); + +private: + static constexpr int kDebounceSamples{2}; + + struct ThresholdState + { + bool warningFired{false}; + bool criticalFired{false}; + int belowWarningCount{0}; + int belowCriticalCount{0}; + }; + + void checkMetric(const MetricsThreshold &threshold, double value, ThresholdState &state); + + MetricsThresholdConfig m_config; + IMetricsReporter *m_reporter; // non-owning + ThresholdState m_clientCpuState; + ThresholdState m_serverCpuState; + ThresholdState m_combinedCpuState; + ThresholdState m_clientMemState; + ThresholdState m_serverMemState; + ThresholdState m_cgroupMemState; +}; +} // namespace firebolt::rialto::server::ipc + +#endif // FIREBOLT_RIALTO_SERVER_IPC_METRICS_THRESHOLD_CHECKER_H_ diff --git a/media/server/ipc/include/PrivateMetricsModuleService.h b/media/server/ipc/include/PrivateMetricsModuleService.h index 303d27a6c..dc80c12cb 100644 --- a/media/server/ipc/include/PrivateMetricsModuleService.h +++ b/media/server/ipc/include/PrivateMetricsModuleService.h @@ -20,13 +20,18 @@ #ifndef FIREBOLT_RIALTO_SERVER_IPC_PRIVATE_METRICS_MODULE_SERVICE_H_ #define FIREBOLT_RIALTO_SERVER_IPC_PRIVATE_METRICS_MODULE_SERVICE_H_ +#include "IMetricsReporter.h" #include "IPrivateMetricsModuleService.h" +#include "MetricsThresholdChecker.h" +#include "StateMetricsAggregator.h" #include #include #include #include +#include #include #include +#include #include namespace firebolt::rialto::server::ipc @@ -49,6 +54,9 @@ class PrivateMetricsModuleService : public IPrivateMetricsModuleService void clientConnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) override; void clientDisconnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) override; + void notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) override; + void notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) override; + void reportClientMetrics(::google::protobuf::RpcController *controller, const ::firebolt::rialto::ReportClientMetricsRequest *request, ::firebolt::rialto::ReportClientMetricsResponse *response, @@ -64,6 +72,9 @@ class PrivateMetricsModuleService : public IPrivateMetricsModuleService std::uint64_t monotonicTimeMs; std::uint64_t epochTimeMs; std::uint64_t processCpuTimeMs; + std::uint64_t processMemoryKb; + std::uint64_t cgroupMemoryUsageKb; + std::uint64_t cgroupMemoryLimitKb; }; struct MetricsSamplePair @@ -78,6 +89,12 @@ class PrivateMetricsModuleService : public IPrivateMetricsModuleService std::optional latestMetrics; }; + struct SessionMetricsState + { + PlaybackState currentPlaybackState{PlaybackState::UNKNOWN}; + StateMetricsAggregator aggregator; + }; + void runMetricsSampler(); void requestMetricsSample(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient, ::firebolt::rialto::MetricsSampleReason reason); @@ -85,7 +102,10 @@ class PrivateMetricsModuleService : public IPrivateMetricsModuleService void logMetrics(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient, const ::firebolt::rialto::ClientProcessMetrics &clientMetrics, const ProcessMetricsSample &serverMetrics); + void logStateReport(const StateMetricsReport &report, const std::string &context); const char *sampleReasonToString(::firebolt::rialto::MetricsSampleReason reason) const; + static const char *playbackStateToString(PlaybackState state); + static const char *applicationStateToString(ApplicationState state); double calculateCpuPercentage(std::uint64_t currentCpuTimeMs, std::uint64_t previousCpuTimeMs, std::uint64_t currentMonotonicTimeMs, std::uint64_t previousMonotonicTimeMs) const; @@ -96,6 +116,19 @@ class PrivateMetricsModuleService : public IPrivateMetricsModuleService std::condition_variable m_wakeup; std::mutex m_mutex; std::map, ClientMetricsState> m_clients; + + // Per-session state tracking (sessionId -> session state) + std::map m_sessionStates; + + // Global aggregator (active across all sessions while RUNNING) + StateMetricsAggregator m_globalAggregator; + ApplicationState m_currentApplicationState{ApplicationState::UNKNOWN}; + + // Pluggable metrics reporter (log, telemetry, or composite) + std::unique_ptr m_reporter; + + // Threshold checker + MetricsThresholdChecker m_thresholdChecker; }; } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/include/SessionManagementServer.h b/media/server/ipc/include/SessionManagementServer.h index e56858b5d..9c227c390 100644 --- a/media/server/ipc/include/SessionManagementServer.h +++ b/media/server/ipc/include/SessionManagementServer.h @@ -68,6 +68,7 @@ class SessionManagementServer : public ISessionManagementServer void stop() override; void setLogLevels(RIALTO_DEBUG_LEVEL defaultLogLevels, RIALTO_DEBUG_LEVEL clientLogLevels, RIALTO_DEBUG_LEVEL ipcLogLevels, RIALTO_DEBUG_LEVEL commonLogLevels) override; + void notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) override; private: void onClientConnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &client); diff --git a/media/server/ipc/include/StateMetricsAggregator.h b/media/server/ipc/include/StateMetricsAggregator.h new file mode 100644 index 000000000..cffb79007 --- /dev/null +++ b/media/server/ipc/include/StateMetricsAggregator.h @@ -0,0 +1,131 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_IPC_STATE_METRICS_AGGREGATOR_H_ +#define FIREBOLT_RIALTO_SERVER_IPC_STATE_METRICS_AGGREGATOR_H_ + +#include "MetricsAccumulator.h" +#include +#include + +namespace firebolt::rialto::server::ipc +{ +/** + * @brief A single metrics sample to be fed into the aggregator. + */ +struct MetricsSample +{ + double clientCpuPercent{0.0}; + double serverCpuPercent{0.0}; + double combinedCpuPercent{0.0}; + std::uint64_t clientMemoryKb{0}; + std::uint64_t serverMemoryKb{0}; + std::uint64_t cgroupMemoryUsageKb{0}; + std::uint64_t cgroupMemoryLimitKb{0}; +}; + +/** + * @brief Aggregated statistics report produced when a state is finalized. + */ +struct StateMetricsReport +{ + std::string stateName; + std::uint64_t durationMs{0}; + MetricsStatistics clientCpu; + MetricsStatistics serverCpu; + MetricsStatistics combinedCpu; + MetricsStatistics clientMemoryKb; + MetricsStatistics serverMemoryKb; + MetricsStatistics cgroupMemoryUsageKb; + MetricsStatistics cgroupMemoryLimitKb; +}; + +/** + * @brief Accumulates metrics samples for a single state period and produces a + * statistical report on finalization. + */ +class StateMetricsAggregator +{ +public: + StateMetricsAggregator() = default; + ~StateMetricsAggregator() = default; + + void begin(const std::string &stateName, std::uint64_t monotonicTimeMs) + { + reset(); + m_stateName = stateName; + m_startTimeMs = monotonicTimeMs; + } + + void addSample(const MetricsSample &sample) + { + m_clientCpu.addSample(sample.clientCpuPercent); + m_serverCpu.addSample(sample.serverCpuPercent); + m_combinedCpu.addSample(sample.combinedCpuPercent); + m_clientMemory.addSample(static_cast(sample.clientMemoryKb)); + m_serverMemory.addSample(static_cast(sample.serverMemoryKb)); + m_cgroupUsage.addSample(static_cast(sample.cgroupMemoryUsageKb)); + m_cgroupLimit.addSample(static_cast(sample.cgroupMemoryLimitKb)); + } + + StateMetricsReport finalize(std::uint64_t monotonicTimeMs) const + { + StateMetricsReport report; + report.stateName = m_stateName; + report.durationMs = (monotonicTimeMs > m_startTimeMs) ? (monotonicTimeMs - m_startTimeMs) : 0; + report.clientCpu = m_clientCpu.getStats(); + report.serverCpu = m_serverCpu.getStats(); + report.combinedCpu = m_combinedCpu.getStats(); + report.clientMemoryKb = m_clientMemory.getStats(); + report.serverMemoryKb = m_serverMemory.getStats(); + report.cgroupMemoryUsageKb = m_cgroupUsage.getStats(); + report.cgroupMemoryLimitKb = m_cgroupLimit.getStats(); + return report; + } + + void reset() + { + m_stateName.clear(); + m_startTimeMs = 0; + m_clientCpu.reset(); + m_serverCpu.reset(); + m_combinedCpu.reset(); + m_clientMemory.reset(); + m_serverMemory.reset(); + m_cgroupUsage.reset(); + m_cgroupLimit.reset(); + } + + bool hasData() const { return m_clientCpu.getCount() > 0; } + const std::string &getStateName() const { return m_stateName; } + +private: + std::string m_stateName; + std::uint64_t m_startTimeMs{0}; + MetricsAccumulator m_clientCpu; + MetricsAccumulator m_serverCpu; + MetricsAccumulator m_combinedCpu; + MetricsAccumulator m_clientMemory; + MetricsAccumulator m_serverMemory; + MetricsAccumulator m_cgroupUsage; + MetricsAccumulator m_cgroupLimit; +}; +} // namespace firebolt::rialto::server::ipc + +#endif // FIREBOLT_RIALTO_SERVER_IPC_STATE_METRICS_AGGREGATOR_H_ diff --git a/media/server/ipc/interface/ISessionManagementServer.h b/media/server/ipc/interface/ISessionManagementServer.h index a5591874f..a9842eff7 100644 --- a/media/server/ipc/interface/ISessionManagementServer.h +++ b/media/server/ipc/interface/ISessionManagementServer.h @@ -20,6 +20,7 @@ #ifndef FIREBOLT_RIALTO_SERVER_IPC_I_SESSION_MANAGEMENT_SERVER_H_ #define FIREBOLT_RIALTO_SERVER_IPC_I_SESSION_MANAGEMENT_SERVER_H_ +#include "ControlCommon.h" #include "RialtoServerLogging.h" #include #include @@ -44,6 +45,7 @@ class ISessionManagementServer virtual void stop() = 0; virtual void setLogLevels(RIALTO_DEBUG_LEVEL defaultLogLevels, RIALTO_DEBUG_LEVEL clientLogLevels, RIALTO_DEBUG_LEVEL ipcLogLevels, RIALTO_DEBUG_LEVEL commonLogLevels) = 0; + virtual void notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) = 0; }; } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/source/CompositeMetricsReporter.cpp b/media/server/ipc/source/CompositeMetricsReporter.cpp new file mode 100644 index 000000000..a8e22d5ee --- /dev/null +++ b/media/server/ipc/source/CompositeMetricsReporter.cpp @@ -0,0 +1,55 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "CompositeMetricsReporter.h" + +namespace firebolt::rialto::server::ipc +{ +void CompositeMetricsReporter::addReporter(std::unique_ptr reporter) +{ + if (reporter) + { + m_reporters.push_back(std::move(reporter)); + } +} + +void CompositeMetricsReporter::reportPeriodicSample(const PeriodicMetricsReport &report) +{ + for (auto &reporter : m_reporters) + { + reporter->reportPeriodicSample(report); + } +} + +void CompositeMetricsReporter::reportStateTransition(const StateTransitionReport &report) +{ + for (auto &reporter : m_reporters) + { + reporter->reportStateTransition(report); + } +} + +void CompositeMetricsReporter::reportThresholdExceeded(const ThresholdAlert &alert) +{ + for (auto &reporter : m_reporters) + { + reporter->reportThresholdExceeded(alert); + } +} +} // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/source/LogMetricsReporter.cpp b/media/server/ipc/source/LogMetricsReporter.cpp new file mode 100644 index 000000000..f5dad20b1 --- /dev/null +++ b/media/server/ipc/source/LogMetricsReporter.cpp @@ -0,0 +1,67 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "LogMetricsReporter.h" +#include "RialtoServerLogging.h" +#include + +namespace firebolt::rialto::server::ipc +{ +void LogMetricsReporter::reportPeriodicSample(const PeriodicMetricsReport &report) +{ + // Don't report the periodic samples as milestone logs, + // as they are expected to be emitted frequently and + // may not indicate a significant event on their own. + // Instead, log them at INFO level. + RIALTO_SERVER_LOG_INFO("Metrics sample=%" PRIu64 ", reason=%s, app='%s', client_pid=%u, client_cpu=%.2f%%, " + "server_cpu=%.2f%%, combined_cpu=%.2f%%, client_cpu_ms=%" PRIu64 ", " + "server_cpu_ms=%" PRIu64 ", client_mem_kb=%" PRIu64 ", server_mem_kb=%" PRIu64 ", " + "cgroup_mem_kb=%" PRIu64 "/%" PRIu64, + report.sampleId, report.reason.c_str(), report.appName.c_str(), report.clientPid, + report.clientCpuPercent, report.serverCpuPercent, report.combinedCpuPercent, + report.clientCpuTimeMs, report.serverCpuTimeMs, report.clientMemoryKb, + report.serverMemoryKb, report.cgroupMemoryUsageKb, report.cgroupMemoryLimitKb); +} + +void LogMetricsReporter::reportStateTransition(const StateTransitionReport &report) +{ + const auto &r{report.metrics}; + RIALTO_SERVER_LOG_MIL("Metrics state report [%s] state='%s', duration_ms=%" PRIu64 ", samples=%" PRIu64 ", " + "client_cpu={min=%.2f, max=%.2f, mean=%.2f, stddev=%.2f}%%, " + "server_cpu={min=%.2f, max=%.2f, mean=%.2f, stddev=%.2f}%%, " + "combined_cpu={min=%.2f, max=%.2f, mean=%.2f, stddev=%.2f}%%, " + "client_mem_kb={min=%.0f, max=%.0f, mean=%.0f}, " + "server_mem_kb={min=%.0f, max=%.0f, mean=%.0f}, " + "cgroup_mem_kb={min=%.0f, max=%.0f, mean=%.0f}", + report.context.c_str(), r.stateName.c_str(), r.durationMs, r.clientCpu.count, + r.clientCpu.min, r.clientCpu.max, r.clientCpu.mean, r.clientCpu.stddev, + r.serverCpu.min, r.serverCpu.max, r.serverCpu.mean, r.serverCpu.stddev, + r.combinedCpu.min, r.combinedCpu.max, r.combinedCpu.mean, r.combinedCpu.stddev, + r.clientMemoryKb.min, r.clientMemoryKb.max, r.clientMemoryKb.mean, + r.serverMemoryKb.min, r.serverMemoryKb.max, r.serverMemoryKb.mean, + r.cgroupMemoryUsageKb.min, r.cgroupMemoryUsageKb.max, r.cgroupMemoryUsageKb.mean); +} + +void LogMetricsReporter::reportThresholdExceeded(const ThresholdAlert &alert) +{ + const char *severity = (alert.severity == ThresholdSeverity::CRITICAL) ? "CRITICAL" : "WARNING"; + RIALTO_SERVER_LOG_WARN("Metrics threshold %s: %s=%.2f exceeds %.2f", severity, alert.metricName.c_str(), + alert.currentValue, alert.thresholdValue); +} +} // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/source/MediaPipelineClient.cpp b/media/server/ipc/source/MediaPipelineClient.cpp index a020d8ac6..175f568b3 100644 --- a/media/server/ipc/source/MediaPipelineClient.cpp +++ b/media/server/ipc/source/MediaPipelineClient.cpp @@ -18,6 +18,7 @@ */ #include "MediaPipelineClient.h" +#include "IPrivateMetricsModuleService.h" #include "RialtoServerLogging.h" #include "mediapipelinemodule.pb.h" #include @@ -137,8 +138,9 @@ firebolt::rialto::PlaybackErrorEvent_PlaybackError convertPlaybackError(const fi namespace firebolt::rialto::server::ipc { -MediaPipelineClient::MediaPipelineClient(int sessionId, const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) - : m_sessionId{sessionId}, m_ipcClient{ipcClient} +MediaPipelineClient::MediaPipelineClient(int sessionId, const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient, + IPrivateMetricsModuleService *metricsService) + : m_sessionId{sessionId}, m_ipcClient{ipcClient}, m_metricsService{metricsService} { } @@ -180,6 +182,17 @@ void MediaPipelineClient::notifyPlaybackState(PlaybackState state) { RIALTO_SERVER_LOG_DEBUG("Sending PlaybackStateChangeEvent..."); + if (m_metricsService) + { + PlaybackState oldState = m_currentPlaybackState; + m_currentPlaybackState = state; + m_metricsService->notifyPlaybackStateChanged(m_sessionId, oldState, state); + } + else + { + m_currentPlaybackState = state; + } + auto event = std::make_shared(); event->set_session_id(m_sessionId); event->set_state(convertPlaybackState(state)); diff --git a/media/server/ipc/source/MediaPipelineModuleService.cpp b/media/server/ipc/source/MediaPipelineModuleService.cpp index 1b2d29a12..7a475b18c 100644 --- a/media/server/ipc/source/MediaPipelineModuleService.cpp +++ b/media/server/ipc/source/MediaPipelineModuleService.cpp @@ -19,6 +19,7 @@ #include "MediaPipelineModuleService.h" #include "IMediaPipelineService.h" +#include "IPrivateMetricsModuleService.h" #include "MediaPipelineClient.h" #include "RialtoCommonModule.h" #include "RialtoServerLogging.h" @@ -297,6 +298,11 @@ MediaPipelineModuleService::MediaPipelineModuleService(service::IMediaPipelineSe MediaPipelineModuleService::~MediaPipelineModuleService() {} +void MediaPipelineModuleService::setMetricsService(const std::shared_ptr &metricsService) +{ + m_metricsService = metricsService; +} + void MediaPipelineModuleService::clientConnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) { RIALTO_SERVER_LOG_INFO("Client Connected!"); @@ -344,7 +350,8 @@ void MediaPipelineModuleService::createSession(::google::protobuf::RpcController int sessionId = generateSessionId(); bool sessionCreated = m_mediaPipelineService.createSession(sessionId, - std::make_shared(sessionId, ipcController->getClient()), + std::make_shared(sessionId, ipcController->getClient(), + m_metricsService.get()), request->max_width(), request->max_height()); if (sessionCreated) { diff --git a/media/server/ipc/source/MetricsThresholdChecker.cpp b/media/server/ipc/source/MetricsThresholdChecker.cpp new file mode 100644 index 000000000..892bd9c31 --- /dev/null +++ b/media/server/ipc/source/MetricsThresholdChecker.cpp @@ -0,0 +1,102 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MetricsThresholdChecker.h" + +namespace firebolt::rialto::server::ipc +{ +MetricsThresholdChecker::MetricsThresholdChecker(MetricsThresholdConfig config, IMetricsReporter *reporter) + : m_config{std::move(config)}, m_reporter{reporter} +{ +} + +void MetricsThresholdChecker::checkSample(double clientCpu, double serverCpu, double combinedCpu, + std::uint64_t clientMemKb, std::uint64_t serverMemKb, + std::uint64_t cgroupUsageKb, std::uint64_t cgroupLimitKb) +{ + if (!m_reporter) + { + return; + } + + checkMetric(m_config.clientCpu, clientCpu, m_clientCpuState); + checkMetric(m_config.serverCpu, serverCpu, m_serverCpuState); + checkMetric(m_config.combinedCpu, combinedCpu, m_combinedCpuState); + checkMetric(m_config.clientMemoryKb, static_cast(clientMemKb), m_clientMemState); + checkMetric(m_config.serverMemoryKb, static_cast(serverMemKb), m_serverMemState); + + // Cgroup memory as percentage of limit + if (cgroupLimitKb > 0) + { + const double cgroupPct{(static_cast(cgroupUsageKb) / static_cast(cgroupLimitKb)) * 100.0}; + checkMetric(m_config.cgroupMemoryPercent, cgroupPct, m_cgroupMemState); + } +} + +void MetricsThresholdChecker::checkMetric(const MetricsThreshold &threshold, double value, ThresholdState &state) +{ + // Critical check + if (value >= threshold.criticalLevel) + { + state.belowCriticalCount = 0; + if (!state.criticalFired) + { + state.criticalFired = true; + ThresholdAlert alert; + alert.metricName = threshold.metricName; + alert.currentValue = value; + alert.thresholdValue = threshold.criticalLevel; + alert.severity = ThresholdSeverity::CRITICAL; + m_reporter->reportThresholdExceeded(alert); + } + } + else + { + ++state.belowCriticalCount; + if (state.belowCriticalCount >= kDebounceSamples) + { + state.criticalFired = false; + } + } + + // Warning check + if (value >= threshold.warningLevel) + { + state.belowWarningCount = 0; + if (!state.warningFired) + { + state.warningFired = true; + ThresholdAlert alert; + alert.metricName = threshold.metricName; + alert.currentValue = value; + alert.thresholdValue = threshold.warningLevel; + alert.severity = ThresholdSeverity::WARNING; + m_reporter->reportThresholdExceeded(alert); + } + } + else + { + ++state.belowWarningCount; + if (state.belowWarningCount >= kDebounceSamples) + { + state.warningFired = false; + } + } +} +} // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/source/PrivateMetricsModuleService.cpp b/media/server/ipc/source/PrivateMetricsModuleService.cpp index 5cda04105..ea02eab91 100644 --- a/media/server/ipc/source/PrivateMetricsModuleService.cpp +++ b/media/server/ipc/source/PrivateMetricsModuleService.cpp @@ -18,11 +18,14 @@ */ #include "PrivateMetricsModuleService.h" +#include "LogMetricsReporter.h" #include "RialtoServerLogging.h" #include #include #include +#include #include +#include #include #include #include @@ -67,7 +70,9 @@ std::shared_ptr PrivateMetricsModuleServiceFactory return privateMetricsModule; } -PrivateMetricsModuleService::PrivateMetricsModuleService() : m_isRunning{true}, m_nextSampleId{1} +PrivateMetricsModuleService::PrivateMetricsModuleService() + : m_isRunning{true}, m_nextSampleId{1}, m_reporter{std::make_unique()}, + m_thresholdChecker{MetricsThresholdConfig{}, m_reporter.get()} { m_metricsThread = std::thread(&PrivateMetricsModuleService::runMetricsSampler, this); } @@ -226,8 +231,7 @@ PrivateMetricsModuleService::ProcessMetricsSample PrivateMetricsModuleService::g using std::chrono::steady_clock; using std::chrono::system_clock; - struct tms processTimes - {}; + struct tms processTimes{}; const clock_t kCurrentTicks{times(&processTimes)}; const long kTicksPerSecond{sysconf(_SC_CLK_TCK)}; std::uint64_t processCpuTimeMs{0}; @@ -242,10 +246,101 @@ PrivateMetricsModuleService::ProcessMetricsSample PrivateMetricsModuleService::g RIALTO_SERVER_LOG_WARN("Failed to sample server process CPU usage"); } + std::uint64_t processMemoryKb{0}; + { + std::ifstream status{"/proc/self/status"}; + std::string line; + while (std::getline(status, line)) + { + if (line.rfind("VmRSS:", 0) == 0) + { + if (std::sscanf(line.c_str(), "VmRSS: %" SCNu64, &processMemoryKb) != 1) + { + RIALTO_SERVER_LOG_WARN("Failed to parse server process memory usage"); + } + break; + } + } + } + + std::uint64_t cgroupMemoryUsageKb{0}; + std::uint64_t cgroupMemoryLimitKb{0}; + { + auto readFileValue = [](const std::string &path) -> std::uint64_t + { + std::ifstream file{path}; + if (!file.is_open()) + { + return 0; + } + std::string content; + if (!std::getline(file, content) || content.empty() || content == "max") + { + return 0; + } + std::uint64_t value{0}; + if (std::sscanf(content.c_str(), "%" SCNu64, &value) == 1) + { + return value; + } + return 0; + }; + + // Resolve the process's cgroup path from /proc/self/cgroup + // cgroup v2 format: "0::" + auto getCgroupBasePath = [&readFileValue]() -> std::string + { + std::ifstream cgroupFile{"/proc/self/cgroup"}; + if (!cgroupFile.is_open()) + { + return {}; + } + std::string line; + while (std::getline(cgroupFile, line)) + { + // cgroup v2 line starts with "0::" + if (line.rfind("0::", 0) == 0) + { + std::string relativePath{line.substr(3)}; + if (!relativePath.empty() && relativePath != "/") + { + return "/sys/fs/cgroup" + relativePath; + } + return "/sys/fs/cgroup"; + } + } + return {}; + }; + + std::uint64_t usageBytes{0}; + std::uint64_t limitBytes{0}; + + // cgroup v2: read from process's own cgroup path + std::string cgroupBase{getCgroupBasePath()}; + if (!cgroupBase.empty()) + { + usageBytes = readFileValue(cgroupBase + "/memory.current"); + limitBytes = readFileValue(cgroupBase + "/memory.max"); + } + + if (usageBytes == 0) + { + // cgroup v1 fallback + usageBytes = readFileValue("/sys/fs/cgroup/memory/memory.usage_in_bytes"); + limitBytes = readFileValue("/sys/fs/cgroup/memory/memory.limit_in_bytes"); + } + + cgroupMemoryUsageKb = usageBytes / 1024; + cgroupMemoryLimitKb = limitBytes / 1024; + } + return ProcessMetricsSample{ static_cast(duration_cast(steady_clock::now().time_since_epoch()).count()), static_cast(duration_cast(system_clock::now().time_since_epoch()).count()), - processCpuTimeMs}; + processCpuTimeMs, + processMemoryKb, + cgroupMemoryUsageKb, + cgroupMemoryLimitKb}; } void PrivateMetricsModuleService::logMetrics(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient, @@ -265,10 +360,14 @@ void PrivateMetricsModuleService::logMetrics(const std::shared_ptr<::firebolt::r if (!previousSample.has_value() || !previousSample->clientMetrics.has_process_cpu_time_ms()) { RIALTO_SERVER_LOG_MIL("Metrics baseline: sample=%" PRIu64 ", reason=%s, app='%s', client_pid=%u, " - "client_cpu_ms=%" PRIu64 ", server_cpu_ms=%" PRIu64, + "client_cpu_ms=%" PRIu64 ", server_cpu_ms=%" PRIu64 ", " + "client_mem_kb=%" PRIu64 ", server_mem_kb=%" PRIu64 ", " + "cgroup_mem_kb=%" PRIu64 "/%" PRIu64, clientMetrics.sample_id(), sampleReasonToString(clientMetrics.reason()), clientMetrics.app_name().c_str(), clientMetrics.process_id(), - clientMetrics.process_cpu_time_ms(), serverMetrics.processCpuTimeMs); + clientMetrics.process_cpu_time_ms(), serverMetrics.processCpuTimeMs, + clientMetrics.process_memory_kb(), serverMetrics.processMemoryKb, + serverMetrics.cgroupMemoryUsageKb, serverMetrics.cgroupMemoryLimitKb); return; } @@ -286,13 +385,63 @@ void PrivateMetricsModuleService::logMetrics(const std::shared_ptr<::firebolt::r previousClientMetrics.process_cpu_time_ms() + previousServerMetrics.processCpuTimeMs, serverMetrics.monotonicTimeMs, previousServerMetrics.monotonicTimeMs)}; - RIALTO_SERVER_LOG_MIL("Metrics sample=%" PRIu64 ", reason=%s, app='%s', client_pid=%u, client_cpu=%.2f%%, " - "server_cpu=%.2f%%, combined_cpu=%.2f%%, client_cpu_ms=%" PRIu64 ", " - "server_cpu_ms=%" PRIu64, - clientMetrics.sample_id(), sampleReasonToString(clientMetrics.reason()), - clientMetrics.app_name().c_str(), clientMetrics.process_id(), kClientCpuPercentage, - kServerCpuPercentage, kCombinedCpuPercentage, clientMetrics.process_cpu_time_ms(), - serverMetrics.processCpuTimeMs); + // Report via pluggable reporter + if (m_reporter) + { + PeriodicMetricsReport periodicReport; + periodicReport.sampleId = clientMetrics.sample_id(); + periodicReport.reason = sampleReasonToString(clientMetrics.reason()); + periodicReport.appName = clientMetrics.app_name(); + periodicReport.clientPid = clientMetrics.process_id(); + periodicReport.clientCpuPercent = kClientCpuPercentage; + periodicReport.serverCpuPercent = kServerCpuPercentage; + periodicReport.combinedCpuPercent = kCombinedCpuPercentage; + periodicReport.clientCpuTimeMs = clientMetrics.process_cpu_time_ms(); + periodicReport.serverCpuTimeMs = serverMetrics.processCpuTimeMs; + periodicReport.clientMemoryKb = clientMetrics.process_memory_kb(); + periodicReport.serverMemoryKb = serverMetrics.processMemoryKb; + periodicReport.cgroupMemoryUsageKb = serverMetrics.cgroupMemoryUsageKb; + periodicReport.cgroupMemoryLimitKb = serverMetrics.cgroupMemoryLimitKb; + m_reporter->reportPeriodicSample(periodicReport); + } + + // Only feed PERIODIC samples into aggregators — STATE_TRANSITION samples have + // unreliable CPU percentages due to tiny time deltas between rapid samples. + if (clientMetrics.reason() == firebolt::rialto::METRICS_SAMPLE_REASON_PERIODIC) + { + MetricsSample sample; + sample.clientCpuPercent = kClientCpuPercentage; + sample.serverCpuPercent = kServerCpuPercentage; + sample.combinedCpuPercent = kCombinedCpuPercentage; + sample.clientMemoryKb = clientMetrics.process_memory_kb(); + sample.serverMemoryKb = serverMetrics.processMemoryKb; + sample.cgroupMemoryUsageKb = serverMetrics.cgroupMemoryUsageKb; + sample.cgroupMemoryLimitKb = serverMetrics.cgroupMemoryLimitKb; + + { + std::lock_guard lock{m_mutex}; + + // Feed into per-session aggregators + for (auto &[sessionId, sessionState] : m_sessionStates) + { + sessionState.aggregator.addSample(sample); + } + + // Feed into global aggregator + if (m_currentApplicationState == ApplicationState::RUNNING) + { + m_globalAggregator.addSample(sample); + } + } + } + + // Check thresholds (only for PERIODIC samples with reliable CPU data) + if (clientMetrics.reason() == firebolt::rialto::METRICS_SAMPLE_REASON_PERIODIC) + { + m_thresholdChecker.checkSample(kClientCpuPercentage, kServerCpuPercentage, kCombinedCpuPercentage, + clientMetrics.process_memory_kb(), serverMetrics.processMemoryKb, + serverMetrics.cgroupMemoryUsageKb, serverMetrics.cgroupMemoryLimitKb); + } } const char *PrivateMetricsModuleService::sampleReasonToString(::firebolt::rialto::MetricsSampleReason reason) const @@ -303,24 +452,180 @@ const char *PrivateMetricsModuleService::sampleReasonToString(::firebolt::rialto return "CONNECTED"; case firebolt::rialto::METRICS_SAMPLE_REASON_PERIODIC: return "PERIODIC"; + case firebolt::rialto::METRICS_SAMPLE_REASON_STATE_TRANSITION: + return "STATE_TRANSITION"; case firebolt::rialto::METRICS_SAMPLE_REASON_UNKNOWN: default: return "UNKNOWN"; } } +const char *PrivateMetricsModuleService::playbackStateToString(PlaybackState state) +{ + switch (state) + { + case PlaybackState::IDLE: + return "IDLE"; + case PlaybackState::PLAYING: + return "PLAYING"; + case PlaybackState::PAUSED: + return "PAUSED"; + case PlaybackState::SEEKING: + return "SEEKING"; + case PlaybackState::SEEK_DONE: + return "SEEK_DONE"; + case PlaybackState::STOPPED: + return "STOPPED"; + case PlaybackState::END_OF_STREAM: + return "END_OF_STREAM"; + case PlaybackState::FAILURE: + return "FAILURE"; + case PlaybackState::UNKNOWN: + default: + return "UNKNOWN"; + } +} + +const char *PrivateMetricsModuleService::applicationStateToString(ApplicationState state) +{ + switch (state) + { + case ApplicationState::RUNNING: + return "RUNNING"; + case ApplicationState::INACTIVE: + return "INACTIVE"; + case ApplicationState::UNKNOWN: + default: + return "UNKNOWN"; + } +} + +void PrivateMetricsModuleService::notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, + PlaybackState newState) +{ + RIALTO_SERVER_LOG_MIL("Metrics: PlaybackState changed session=%d, %s -> %s", sessionId, + playbackStateToString(oldState), playbackStateToString(newState)); + + using std::chrono::duration_cast; + using std::chrono::milliseconds; + using std::chrono::steady_clock; + const auto kNowMs{ + static_cast(duration_cast(steady_clock::now().time_since_epoch()).count())}; + + std::lock_guard lock{m_mutex}; + auto sessionIter{m_sessionStates.find(sessionId)}; + if (m_sessionStates.end() == sessionIter) + { + // First state notification for this session — create entry + SessionMetricsState sessionState; + sessionState.currentPlaybackState = newState; + sessionState.aggregator.begin(playbackStateToString(newState), kNowMs); + m_sessionStates.emplace(sessionId, std::move(sessionState)); + return; + } + + auto &sessionState{sessionIter->second}; + + // Finalize old state and emit report + if (sessionState.aggregator.hasData()) + { + auto report{sessionState.aggregator.finalize(kNowMs)}; + logStateReport(report, "session=" + std::to_string(sessionId)); + } + + if (newState == PlaybackState::STOPPED || newState == PlaybackState::END_OF_STREAM || + newState == PlaybackState::FAILURE) + { + // Terminal state — remove session tracking + m_sessionStates.erase(sessionIter); + } + else + { + // Begin accumulating for new state + sessionState.currentPlaybackState = newState; + sessionState.aggregator.begin(playbackStateToString(newState), kNowMs); + } + + // Request immediate sample for clean boundary + for (const auto &client : m_clients) + { + if (client.second.isReady) + { + requestMetricsSample(client.first, firebolt::rialto::METRICS_SAMPLE_REASON_STATE_TRANSITION); + } + } +} + +void PrivateMetricsModuleService::notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) +{ + RIALTO_SERVER_LOG_MIL("Metrics: ApplicationState changed %s -> %s", applicationStateToString(oldState), + applicationStateToString(newState)); + + using std::chrono::duration_cast; + using std::chrono::milliseconds; + using std::chrono::steady_clock; + const auto kNowMs{ + static_cast(duration_cast(steady_clock::now().time_since_epoch()).count())}; + + std::lock_guard lock{m_mutex}; + m_currentApplicationState = newState; + + if (oldState == ApplicationState::RUNNING && newState != ApplicationState::RUNNING) + { + // Leaving RUNNING — finalize global aggregator + if (m_globalAggregator.hasData()) + { + auto report{m_globalAggregator.finalize(kNowMs)}; + logStateReport(report, "global"); + } + m_globalAggregator.reset(); + } + + if (newState == ApplicationState::RUNNING && oldState != ApplicationState::RUNNING) + { + // Entering RUNNING — start fresh global accumulation + m_globalAggregator.begin(applicationStateToString(newState), kNowMs); + } + + // Request immediate sample for clean boundary + for (const auto &client : m_clients) + { + if (client.second.isReady) + { + requestMetricsSample(client.first, firebolt::rialto::METRICS_SAMPLE_REASON_STATE_TRANSITION); + } + } +} + +void PrivateMetricsModuleService::logStateReport(const StateMetricsReport &report, const std::string &context) +{ + if (m_reporter) + { + StateTransitionReport transitionReport; + transitionReport.context = context; + transitionReport.metrics = report; + m_reporter->reportStateTransition(transitionReport); + } +} + double PrivateMetricsModuleService::calculateCpuPercentage(std::uint64_t currentCpuTimeMs, std::uint64_t previousCpuTimeMs, std::uint64_t currentMonotonicTimeMs, std::uint64_t previousMonotonicTimeMs) const { + constexpr std::uint64_t kMinElapsedMs{100}; if ((currentCpuTimeMs < previousCpuTimeMs) || (currentMonotonicTimeMs <= previousMonotonicTimeMs)) { return 0.0; } - return (static_cast(currentCpuTimeMs - previousCpuTimeMs) / - static_cast(currentMonotonicTimeMs - previousMonotonicTimeMs)) * - 100.0; + const auto kElapsedMs{currentMonotonicTimeMs - previousMonotonicTimeMs}; + if (kElapsedMs < kMinElapsedMs) + { + // Time delta too small for meaningful CPU percentage + return 0.0; + } + + return (static_cast(currentCpuTimeMs - previousCpuTimeMs) / static_cast(kElapsedMs)) * 100.0; } } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/source/SessionManagementServer.cpp b/media/server/ipc/source/SessionManagementServer.cpp index 16a37bcd9..8fe584437 100644 --- a/media/server/ipc/source/SessionManagementServer.cpp +++ b/media/server/ipc/source/SessionManagementServer.cpp @@ -61,6 +61,7 @@ SessionManagementServer::SessionManagementServer( m_controlModule{controlModuleFactory->create(playbackService, controlService)} { m_ipcServer = ipcFactory->create(); + m_mediaPipelineModule->setMetricsService(m_privateMetricsModule); } SessionManagementServer::~SessionManagementServer() @@ -164,6 +165,14 @@ void SessionManagementServer::setLogLevels(RIALTO_DEBUG_LEVEL defaultLogLevels, m_setLogLevelsService.setLogLevels(defaultLogLevels, clientLogLevels, ipcLogLevels, commonLogLevels); } +void SessionManagementServer::notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) +{ + if (m_privateMetricsModule) + { + m_privateMetricsModule->notifyApplicationStateChanged(oldState, newState); + } +} + void SessionManagementServer::onClientConnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &client) { RIALTO_SERVER_LOG_MIL("Client app connected"); diff --git a/media/server/service/source/SessionServerManager.cpp b/media/server/service/source/SessionServerManager.cpp index b63a7752f..66bdbb40e 100644 --- a/media/server/service/source/SessionServerManager.cpp +++ b/media/server/service/source/SessionServerManager.cpp @@ -210,7 +210,9 @@ bool SessionServerManager::switchToActive() } if (m_applicationManagementServer->sendStateChangedEvent(common::SessionServerState::ACTIVE)) { + ApplicationState oldState = ApplicationState::INACTIVE; // switching from inactive/uninitialized to active m_controlService.setApplicationState(ApplicationState::RUNNING); + m_sessionManagementServer->notifyApplicationStateChanged(oldState, ApplicationState::RUNNING); m_currentState.store(common::SessionServerState::ACTIVE); RIALTO_SERVER_LOG_MIL("RialtoServer state is ACTIVE now"); return true; @@ -231,7 +233,9 @@ bool SessionServerManager::switchToInactive() m_cdmService.switchToInactive(); if (m_applicationManagementServer->sendStateChangedEvent(common::SessionServerState::INACTIVE)) { + ApplicationState oldState = ApplicationState::RUNNING; // switching from active to inactive m_controlService.setApplicationState(ApplicationState::INACTIVE); + m_sessionManagementServer->notifyApplicationStateChanged(oldState, ApplicationState::INACTIVE); m_currentState.store(common::SessionServerState::INACTIVE); RIALTO_SERVER_LOG_MIL("RialtoServer state is INACTIVE now"); return true; diff --git a/proto/privatemetricsmodule.proto b/proto/privatemetricsmodule.proto index a38e5a4f5..1f96137ad 100644 --- a/proto/privatemetricsmodule.proto +++ b/proto/privatemetricsmodule.proto @@ -27,6 +27,7 @@ enum MetricsSampleReason { METRICS_SAMPLE_REASON_UNKNOWN = 0; METRICS_SAMPLE_REASON_CONNECTED = 1; METRICS_SAMPLE_REASON_PERIODIC = 2; + METRICS_SAMPLE_REASON_STATE_TRANSITION = 3; } message ClientProcessMetrics { @@ -37,6 +38,7 @@ message ClientProcessMetrics { optional uint64 monotonic_time_ms = 5; optional uint64 epoch_time_ms = 6; optional uint64 process_cpu_time_ms = 7; + optional uint64 process_memory_kb = 8; } message ReportClientMetricsRequest { diff --git a/tests/unittests/media/client/mocks/ipc/PrivateMetricsIpcMock.h b/tests/unittests/media/client/mocks/ipc/PrivateMetricsIpcMock.h index b27eb93e3..a420f7aca 100644 --- a/tests/unittests/media/client/mocks/ipc/PrivateMetricsIpcMock.h +++ b/tests/unittests/media/client/mocks/ipc/PrivateMetricsIpcMock.h @@ -33,7 +33,8 @@ class PrivateMetricsIpcMock : public IPrivateMetricsIpc MOCK_METHOD(bool, reportClientMetrics, (std::uint64_t sampleId, std::uint32_t reason, const std::string &appName, std::uint32_t processId, - std::uint64_t monotonicTimeMs, std::uint64_t epochTimeMs, std::uint64_t processCpuTimeMs), + std::uint64_t monotonicTimeMs, std::uint64_t epochTimeMs, std::uint64_t processCpuTimeMs, + std::uint64_t processMemoryKb), (override)); }; } // namespace firebolt::rialto::client diff --git a/tests/unittests/media/server/mocks/ipc/MediaPipelineModuleServiceMock.h b/tests/unittests/media/server/mocks/ipc/MediaPipelineModuleServiceMock.h index 3ffcca89b..5bcca7b47 100644 --- a/tests/unittests/media/server/mocks/ipc/MediaPipelineModuleServiceMock.h +++ b/tests/unittests/media/server/mocks/ipc/MediaPipelineModuleServiceMock.h @@ -35,6 +35,8 @@ class MediaPipelineModuleServiceMock : public IMediaPipelineModuleService MOCK_METHOD(void, clientConnected, (const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient), (override)); MOCK_METHOD(void, clientDisconnected, (const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient), (override)); + MOCK_METHOD(void, setMetricsService, (const std::shared_ptr &metricsService), + (override)); MOCK_METHOD(void, createSession, (::google::protobuf::RpcController * controller, const ::firebolt::rialto::CreateSessionRequest *request, ::firebolt::rialto::CreateSessionResponse *response, ::google::protobuf::Closure *done), diff --git a/tests/unittests/media/server/mocks/ipc/SessionManagementServerMock.h b/tests/unittests/media/server/mocks/ipc/SessionManagementServerMock.h index 4d197a388..eda77efa0 100644 --- a/tests/unittests/media/server/mocks/ipc/SessionManagementServerMock.h +++ b/tests/unittests/media/server/mocks/ipc/SessionManagementServerMock.h @@ -41,6 +41,7 @@ class SessionManagementServerMock : public ISessionManagementServer (RIALTO_DEBUG_LEVEL defaultLogLevels, RIALTO_DEBUG_LEVEL clientLogLevels, RIALTO_DEBUG_LEVEL ipcLogLevels, RIALTO_DEBUG_LEVEL commonLogLevels), (override)); + MOCK_METHOD(void, notifyApplicationStateChanged, (ApplicationState oldState, ApplicationState newState), (override)); }; } // namespace firebolt::rialto::server::ipc From a22225a81d1465817369687e9e79b71596cbc901 Mon Sep 17 00:00:00 2001 From: Douglas Adler Date: Wed, 1 Jul 2026 13:42:37 -0500 Subject: [PATCH 04/11] Improve metrics to meet rialto design Signed-off-by: Douglas Adler --- docs/MetricsDesign.md | 216 ++++--- docs/ServerManagerDesign.html | 245 ++++++++ media/server/ipc/CMakeLists.txt | 3 - .../include/IPrivateMetricsModuleService.h | 4 +- .../ipc/include/PrivateMetricsModuleService.h | 87 +-- .../ipc/include/SessionManagementServer.h | 3 +- media/server/ipc/interface/IIpcFactory.h | 4 +- media/server/ipc/interface/IpcFactory.h | 3 +- media/server/ipc/source/IpcFactory.cpp | 5 +- .../source/PrivateMetricsModuleService.cpp | 566 +++--------------- .../ipc/source/SessionManagementServer.cpp | 5 +- media/server/main/CMakeLists.txt | 4 + .../include/CompositeMetricsReporter.h | 10 +- .../{ipc => main}/include/IMetricsReporter.h | 12 +- .../include/LogMetricsReporter.h | 10 +- .../include/MetricsAccumulator.h | 10 +- media/server/main/include/MetricsCollector.h | 114 ++++ .../include/MetricsThresholdChecker.h | 10 +- .../include/StateMetricsAggregator.h | 10 +- .../server/main/interface/IMetricsCollector.h | 114 ++++ .../main/interface/IMetricsCollectorClient.h | 64 ++ .../source/CompositeMetricsReporter.cpp | 4 +- .../source/LogMetricsReporter.cpp | 31 +- media/server/main/source/MetricsCollector.cpp | 479 +++++++++++++++ .../source/MetricsThresholdChecker.cpp | 4 +- media/server/service/CMakeLists.txt | 1 + .../service/include/IPrivateMetricsService.h | 87 +++ .../service/source/PrivateMetricsService.cpp | 95 +++ .../service/source/PrivateMetricsService.h | 50 ++ .../service/source/SessionServerManager.cpp | 7 +- .../service/source/SessionServerManager.h | 2 + .../media/server/mocks/ipc/IpcFactoryMock.h | 2 +- .../SessionServerManagerTestsFixture.cpp | 2 +- 33 files changed, 1587 insertions(+), 676 deletions(-) create mode 100644 docs/ServerManagerDesign.html rename media/server/{ipc => main}/include/CompositeMetricsReporter.h (83%) rename media/server/{ipc => main}/include/IMetricsReporter.h (89%) rename media/server/{ipc => main}/include/LogMetricsReporter.h (81%) rename media/server/{ipc => main}/include/MetricsAccumulator.h (90%) create mode 100644 media/server/main/include/MetricsCollector.h rename media/server/{ipc => main}/include/MetricsThresholdChecker.h (90%) rename media/server/{ipc => main}/include/StateMetricsAggregator.h (93%) create mode 100644 media/server/main/interface/IMetricsCollector.h create mode 100644 media/server/main/interface/IMetricsCollectorClient.h rename media/server/{ipc => main}/source/CompositeMetricsReporter.cpp (94%) rename media/server/{ipc => main}/source/LogMetricsReporter.cpp (62%) create mode 100644 media/server/main/source/MetricsCollector.cpp rename media/server/{ipc => main}/source/MetricsThresholdChecker.cpp (97%) create mode 100644 media/server/service/include/IPrivateMetricsService.h create mode 100644 media/server/service/source/PrivateMetricsService.cpp create mode 100644 media/server/service/source/PrivateMetricsService.h diff --git a/docs/MetricsDesign.md b/docs/MetricsDesign.md index efdf73417..aaad296b9 100644 --- a/docs/MetricsDesign.md +++ b/docs/MetricsDesign.md @@ -56,30 +56,44 @@ The protocol uses a **server-initiated push** model: ```mermaid sequenceDiagram participant Client as Client (ClientController) - participant Server as Server (PrivateMetricsModuleService) - - Note over Client,Server: Client connects via IPC socket - Server->>Client: exportService(PrivateMetricsModule) - Client->>Server: notifyClientReady() - Server->>Client: MetricsSampleRequestEvent(id=1, reason=CONNECTED) - Client->>Server: reportClientMetrics(ClientProcessMetrics) - Note over Server: Stores baseline (no CPU% yet) - - loop Every 15 seconds - Server->>Client: MetricsSampleRequestEvent(id=N, reason=PERIODIC) - Client->>Server: reportClientMetrics(ClientProcessMetrics) - Note over Server: Compute CPU%, feed aggregators, check thresholds + participant IPC as PrivateMetricsModuleService (ipc) + participant Svc as PrivateMetricsService (service) + participant Main as MetricsCollector (main) + + Note over Client,Main: Client connects via IPC socket + IPC->>Client: exportService(PrivateMetricsModule) + Client->>IPC: notifyClientReady() + IPC->>Svc: clientReady(clientId, ipcClient) + Svc->>Main: creates MetricsCollector(clientId) + Main->>IPC: requestSample(clientId, CONNECTED) + IPC->>Client: MetricsSampleRequestEvent(id=1, reason=CONNECTED) + Client->>IPC: reportClientMetrics(clientId, metrics) + IPC->>Svc: reportMetrics(clientId, metrics) + Svc->>Main: processMetrics(metrics) + Note over Main: Stores baseline (no CPU% yet) + + loop Every 15 seconds (ITimer periodic) + Main->>IPC: requestSample(clientId, PERIODIC) + IPC->>Client: MetricsSampleRequestEvent(id=N, reason=PERIODIC) + Client->>IPC: reportClientMetrics(clientId, metrics) + IPC->>Svc: reportMetrics(clientId, metrics) + Svc->>Main: processMetrics(metrics) + Note over Main: Compute CPU%, feed aggregators, check thresholds end - Note over Server: Playback state changes - Server->>Client: MetricsSampleRequestEvent(id=N, reason=STATE_TRANSITION) - Client->>Server: reportClientMetrics(ClientProcessMetrics) + Note over Main: Playback state changes + Main->>IPC: requestSample(clientId, STATE_TRANSITION) + IPC->>Client: MetricsSampleRequestEvent(id=N, reason=STATE_TRANSITION) + Client->>IPC: reportClientMetrics(clientId, metrics) + IPC->>Svc: reportMetrics(clientId, metrics) + Svc->>Main: processMetrics(metrics) ``` Key design points: - The **server drives timing** — the client never spontaneously reports; it only responds to requests - The `sample_id` field correlates requests with responses and provides ordering - The `reason` field is echoed back by the client so the server knows how to handle the response +- The `client_id` maps each client to its per-client `MetricsCollector` in `server/main` - The service is exported per-client on connection, allowing multi-client support ### Client Side (`ClientController` + `PrivateMetricsIpc`) @@ -96,53 +110,100 @@ When the client library initializes (via `ClientController`), it: - `process_id`: `getpid()` 4. Sends the data back via `reportClientMetrics()` -### Server Side (`PrivateMetricsModuleService`) +### Server-Side Architecture -The server maintains per-client state: +The server follows Rialto's standard three-layer architecture (ipc → service → main): -```cpp -struct ClientMetricsState { - bool isReady{false}; - std::optional latestMetrics; // previous sample for delta computation -}; +``` +┌──────────────────────────────────────────────────────────────┐ +│ server/ipc (PrivateMetricsModuleService) │ +│ - Receives protobuf RPC calls │ +│ - Sends MetricsSampleRequestEvent to IPC clients │ +│ - Generates unique client IDs on notifyClientReady │ +│ - Implements IMetricsCollectorClient (callback from main) │ +│ - Delegates ALL business logic to service layer │ +└────────────────────────┬─────────────────────────────────────┘ + │ +┌────────────────────────▼─────────────────────────────────────┐ +│ server/service (IPrivateMetricsService) │ +│ - Routes calls between ipc and main │ +│ - Maps client IDs to MetricsCollector instances │ +│ - Creates/destroys MetricsCollector instances │ +└────────────────────────┬─────────────────────────────────────┘ + │ +┌────────────────────────▼─────────────────────────────────────┐ +│ server/main (MetricsCollector) │ +│ - Owns ITimer (periodic, 15s) │ +│ - Owns MetricsAccumulator, StateMetricsAggregator │ +│ - Owns MetricsThresholdChecker │ +│ - Owns IMetricsReporter │ +│ - Computes CPU%, feeds aggregators, checks thresholds │ +│ - Receives playback/application state notifications │ +│ - Samples /proc, cgroup for server-side metrics │ +└──────────────────────────────────────────────────────────────┘ ``` -On receiving `reportClientMetrics`: -1. Takes its own `ProcessMetricsSample` (server CPU, memory, cgroup) -2. If a previous sample exists, computes CPU percentages from time deltas -3. Passes the paired client+server data through the reporting/aggregation pipeline -4. Stores the sample as `latestMetrics` for next delta computation +#### IPC Layer (`server/ipc`) -### Threading Model +`PrivateMetricsModuleService`: +- Receives `notifyClientReady` → generates unique client ID → calls service layer → returns ID in response +- Receives `reportClientMetrics` → extracts client ID and metrics → delegates to service layer +- Implements `IMetricsCollectorClient` so `MetricsCollector` can request samples back through IPC +- Maintains mapping of client IDs to IPC client connections +- **No business logic** — no CPU calculation, no aggregation, no thresholds -``` -┌─────────────────────────────────────────────────────────────┐ -│ m_metricsThread (sampler) │ -│ - Sleeps 15s via condition_variable │ -│ - Wakes and sends MetricsSampleRequestEvent to clients │ -│ - Can be woken early by m_wakeup.notify_all() on stop │ -└─────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────┐ -│ IPC event loop thread │ -│ - Receives reportClientMetrics RPC calls │ -│ - Calls logMetrics() → reporter → aggregator → threshold │ -│ - Receives notifyPlaybackState events │ -│ - Calls notifyPlaybackStateChanged() │ -└─────────────────────────────────────────────────────────────┘ +#### Service Layer (`server/service`) + +`IPrivateMetricsService` / `PrivateMetricsService`: +- `clientReady(clientId, client)` → creates `MetricsCollector` in main layer +- `clientDisconnected(clientId)` → destroys `MetricsCollector` +- `reportMetrics(clientId, metrics)` → finds collector, calls `processMetrics()` +- `notifyPlaybackStateChanged(sessionId, oldState, newState)` → routes to all collectors +- `notifyApplicationStateChanged(oldState, newState)` → routes to all collectors + +#### Main Layer (`server/main`) + +`MetricsCollector` (one per connected client): +- Created by `PrivateMetricsService` when client is ready +- Constructor creates `ITimer` (periodic, 15s) — timer callback requests sample via `IMetricsCollectorClient` +- `processMetrics()` — CPU calculation, aggregator feeding, threshold checking, reporting +- `notifyPlaybackStateChanged()` — finalize/begin state aggregators +- `notifyApplicationStateChanged()` — finalize/begin global aggregator +- Destructor cancels timer (ITimer destructor handles this automatically) + +### Timer Model + +Uses the existing `ITimer` framework (`common/interface/ITimer.h`): + +```cpp +// In MetricsCollector constructor: +m_timer = m_timerFactory->createTimer( + std::chrono::seconds{15}, + [this]() { onTimerFired(); }, + TimerType::PERIODIC +); + +// Timer callback: +void MetricsCollector::onTimerFired() +{ + m_client->requestMetricsSample(m_clientId, m_nextSampleId++, MetricsSampleReason::PERIODIC); +} ``` -The `m_mutex` protects shared state (`m_clients`, `m_sessionStates`, `m_globalAggregator`) accessed from both threads. +- `ITimer` manages its own thread internally +- `cancel()` or destructor stops the timer cleanly +- No need for `std::thread`, `std::condition_variable`, or `std::atomic m_isRunning` ### Lifecycle -1. **Server start**: `SessionManagementServer` creates `PrivateMetricsModuleService` via factory -2. **Client connects**: `clientConnected()` → registers client, exports service -3. **Client ready**: `notifyClientReady()` → marks client ready, requests initial sample -4. **Periodic collection**: sampler thread fires every 15s -5. **State changes**: `MediaPipelineClient` and `SessionServerManager` notify as states transition -6. **Client disconnects**: `clientDisconnected()` → removes client state -7. **Server stop**: destructor sets `m_isRunning=false`, wakes sampler thread, joins it +1. **Server start**: `SessionManagementServer` creates `PrivateMetricsModuleService` (ipc) + `PrivateMetricsService` (service) +2. **Client connects**: `clientConnected()` → registers IPC client, exports service +3. **Client ready**: `notifyClientReady()` → generates client ID → service creates `MetricsCollector` → timer starts → requests initial sample +4. **Periodic collection**: `ITimer` fires every 15s → `MetricsCollector::onTimerFired()` → requests sample via `IMetricsCollectorClient` +5. **Client responds**: `reportClientMetrics()` → IPC extracts data → service routes → `MetricsCollector::processMetrics()` +6. **State changes**: `MediaPipelineClient` notifies playback state → routed through service → `MetricsCollector::notifyPlaybackStateChanged()` +7. **Client disconnects**: `clientDisconnected()` → service destroys `MetricsCollector` (timer auto-cancelled in destructor) +8. **Server stop**: service destructor destroys all collectors ## System Architecture @@ -156,16 +217,20 @@ graph TD end subgraph Server Process - IPC --> SMS2[SessionManagementServer] - SMS2 --> PMS[PrivateMetricsModuleService] - PMS -->|samples /proc, cgroup| OS[OS Interfaces] - PMS --> AGG[StateMetricsAggregator] - PMS --> THR[MetricsThresholdChecker] - PMS --> REP[IMetricsReporter] + IPC --> PMS[PrivateMetricsModuleService
server/ipc] + PMS -->|IMetricsCollectorClient| MC + + PMS --> PSVC[PrivateMetricsService
server/service] + PSVC --> MC[MetricsCollector
server/main] + + MC -->|ITimer periodic| TIMER[ITimer 15s] + MC -->|samples /proc, cgroup| OS[OS Interfaces] + MC --> AGG[StateMetricsAggregator] + MC --> THR[MetricsThresholdChecker] + MC --> REP[IMetricsReporter] MPC[MediaPipelineClient] -->|notifyPlaybackStateChanged| PMS - SMS[SessionServerManager] -->|notifyApplicationStateChanged| SMS2 - SMS2 --> PMS + SMS[SessionServerManager] -->|notifyApplicationStateChanged| PMS REP --> LOG[LogMetricsReporter] REP --> COMP[CompositeMetricsReporter] @@ -350,9 +415,16 @@ Metrics threshold WARNING: server_cpu=88.24 exceeds 80.00 | File | Purpose | |------|---------| -| `media/server/ipc/include/IPrivateMetricsModuleService.h` | Server metrics service interface | -| `media/server/ipc/include/PrivateMetricsModuleService.h` | Concrete service with sampler thread and aggregators | -| `media/server/ipc/source/PrivateMetricsModuleService.cpp` | Core sampling, aggregation, wiring | +| `media/server/ipc/include/IPrivateMetricsModuleService.h` | Thin IPC service interface | +| `media/server/ipc/include/PrivateMetricsModuleService.h` | IPC service: RPC handlers + event sending only | +| `media/server/ipc/source/PrivateMetricsModuleService.cpp` | IPC service implementation | +| `media/server/service/include/IPrivateMetricsService.h` | Service-layer interface (routing) | +| `media/server/service/source/PrivateMetricsService.h` | Service implementation header | +| `media/server/service/source/PrivateMetricsService.cpp` | Service: maps clientId to MetricsCollector | +| `media/server/main/interface/IMetricsCollector.h` | MetricsCollector interface | +| `media/server/main/interface/IMetricsCollectorClient.h` | Callback: main→ipc for sending events | +| `media/server/main/include/MetricsCollector.h` | Business logic header | +| `media/server/main/source/MetricsCollector.cpp` | Business logic: CPU calc, aggregation, thresholds | | `media/server/ipc/source/MediaPipelineClient.cpp` | Playback state hook | | `media/server/ipc/source/MediaPipelineModuleService.cpp` | Passes metrics service to pipeline clients | | `media/server/ipc/source/SessionManagementServer.cpp` | Application state hook, owns metrics service | @@ -362,15 +434,15 @@ Metrics threshold WARNING: server_cpu=88.24 exceeds 80.00 | File | Purpose | |------|---------| -| `media/server/ipc/include/MetricsAccumulator.h` | Welford's online mean/variance | -| `media/server/ipc/include/StateMetricsAggregator.h` | Per-state multi-metric accumulation | -| `media/server/ipc/include/IMetricsReporter.h` | Reporter interface + report structs | -| `media/server/ipc/include/LogMetricsReporter.h` | Log-based reporter | -| `media/server/ipc/include/CompositeMetricsReporter.h` | Multi-reporter fanout | -| `media/server/ipc/include/MetricsThresholdChecker.h` | Threshold config + checker | -| `media/server/ipc/source/LogMetricsReporter.cpp` | Reporter implementation | -| `media/server/ipc/source/CompositeMetricsReporter.cpp` | Fanout implementation | -| `media/server/ipc/source/MetricsThresholdChecker.cpp` | Threshold checking logic | +| `media/server/main/include/MetricsAccumulator.h` | Welford's online mean/variance | +| `media/server/main/include/StateMetricsAggregator.h` | Per-state multi-metric accumulation | +| `media/server/main/include/IMetricsReporter.h` | Reporter interface + report structs | +| `media/server/main/include/LogMetricsReporter.h` | Log-based reporter | +| `media/server/main/include/CompositeMetricsReporter.h` | Multi-reporter fanout | +| `media/server/main/include/MetricsThresholdChecker.h` | Threshold config + checker | +| `media/server/main/source/LogMetricsReporter.cpp` | Reporter implementation | +| `media/server/main/source/CompositeMetricsReporter.cpp` | Fanout implementation | +| `media/server/main/source/MetricsThresholdChecker.cpp` | Threshold checking logic | ### Protocol diff --git a/docs/ServerManagerDesign.html b/docs/ServerManagerDesign.html new file mode 100644 index 000000000..a7ea2a500 --- /dev/null +++ b/docs/ServerManagerDesign.html @@ -0,0 +1,245 @@ + + + + + + RialtoServerManager ↔ RialtoServer Design + + + + +

RialtoServerManager ↔ RialtoServer Interface Design

+ +

Architecture Overview

+ +

RialtoServerManager is a library linked into the platform's app-management process. +It owns the lifecycle of one or more RialtoServer (RialtoSessionServer) child processes — one per application. +Each RialtoServer manages media playback for a single app.

+ +
+┌─────────────────────┐ +│ App Management │ +│ (Platform) │ +│ ┌───────────────┐ │ ┌─────────────────────┐ +│ │ ServerManager │──┼─────────│ RialtoServer │ +│ │ (library) │ │ Control │ (App 1 process) │ +│ │ │◄─┼─────────│ │ +│ │ │ │ Events └──────────┬──────────┘ +│ │ │ │ │ Session IPC +│ │ │──┼──────┐ ┌──────────▼──────────┐ +│ │ │ │ │ │ Client App 1 │ +│ └───────────────┘ │ │ └─────────────────────┘ +└─────────────────────┘ │ + │ ┌─────────────────────┐ + └──│ RialtoServer │ + Control │ (App 2 process) │ + ┌──│ │ + │ └──────────┬──────────┘ + │ │ Session IPC + │ ┌──────────▼──────────┐ + │ │ Client App 2 │ + │ └─────────────────────┘ + │ +
+ +

Two Distinct IPC Channels Per Server

+ + + + + + + + + + + + + + + + + + + + +
ChannelPurposeTransportCreated By
Control ChannelManager ↔ Server lifecycle commandssocketpair(AF_UNIX, SOCK_SEQPACKET), FD passed as argv[1]ServerManager at spawn
Session ChannelClient App ↔ Server media operationsNamed Unix domain socket (e.g. /tmp/rialto-N)Server after setConfiguration
+ +

Control Channel Protocol

+ +

Protobuf RPC over the control socketpair. The wire format is asymmetric:

+
    +
  • MessageToServer contains only MethodCall (Manager → Server)
  • +
  • MessageFromServer contains only Reply | Error | Event (Server → Manager)
  • +
+ +

Manager → Server (RPC Calls)

+ + + + + + +
RPCPurpose
setConfigurationInitial setup: socket name/FD, permissions, state, resources, log levels, app name
setStateRequest state transition (ACTIVE / INACTIVE / NOT_RUNNING)
setLogLevelsUpdate log levels across components
pingHealthcheck probe
+ +

Server → Manager (Events only)

+ + + + +
EventPurpose
StateChangedEventNotify manager of state transitions
AckEventHealthcheck acknowledgement (with success/failure flag)
+ +

Server States

+ +
+ ┌──────────────┐ + spawn │ UNINITIALIZED│ + ┌──────────────►│ │ + │ └──────┬───────┘ + │ │ setConfiguration + │ ┌──────▼───────┐ + │ ┌───►│ ACTIVE │◄───┐ + │ │ └──────┬───────┘ │ + │ setState│ │setState │setState + │ │ ┌──────▼───────┐ │ + │ └────│ INACTIVE │────┘ + │ └──────┬───────┘ + │ │ setState(NOT_RUNNING) +┌───┴──────────┐ ┌──────▼───────┐ +│ NOT_RUNNING │◄───│ │ +└──────────────┘ └──────────────┘ + + Any state ──── healthcheck failure ────► ERROR ──── restart ────► UNINITIALIZED +
+ +

Session Lifecycle

+
    +
  1. Platform calls initiateApplication(appId, ACTIVE, appConfig)
  2. +
  3. Manager picks a preloaded child or spawns a new one via vfork + execve
  4. +
  5. Child reads control socket FD from argv[1], starts ApplicationManagementServer, emits UNINITIALIZED
  6. +
  7. Manager receives UNINITIALIZED, sends SetConfigurationRequest
  8. +
  9. Server creates the app-facing named socket, starts media services, transitions to requested state
  10. +
  11. Server sends StateChangedEvent; Manager forwards to IStateObserver
  12. +
  13. Client app connects to the named socket for media playback
  14. +
  15. Periodic pingAckEvent healthchecks run
  16. +
  17. On NOT_RUNNING: server tears down, manager cleans up
  18. +
  19. On healthcheck failure: manager marks ERROR, kills child, restarts with preserved config
  20. +
+ +

Key Interfaces

+ + + + + + + + + + +
InterfaceSideRole
IServerManagerServiceManager (public API)External API for platform to manage apps
IStateObserverManager (callback)Notifies platform of state changes
IControllerManager (internal)Dispatches RPCs to per-server Clients
ISessionServerAppManagerManager (internal)Orchestrates lifecycle, healthchecks, restart
ISessionServerManagerServer (service layer)Server's internal lifecycle manager
IApplicationManagementServerServer (IPC layer)Control channel endpoint; sends events back
ServerManagerModuleServiceServer (IPC layer)Protobuf RPC handler for incoming commands
+ +
+ +

Solution Options: Adding Server → Manager Data Requests

+ +

The current IPC framework does not support server-initiated RPC on the control socket. +Below are three options for enabling the Server to request data from the Manager.

+ +
+

Option 1: Event + Correlation ID Small

+

Approach: Use the existing event mechanism with a request/response pattern.

+
    +
  • Server sends a new event: DataRequestEvent{id, request_type, params}
  • +
  • Manager receives it, fetches the data, sends it back via a new RPC: provideData(id, payload)
  • +
+

Changes (~5–10 files):

+
    +
  • Add 1 new event message + 1 new RPC to servermanagermodule.proto
  • +
  • Manager-side: subscribe to new event in Client, add new RPC call
  • +
  • Server-side: new sendEvent in ApplicationManagementServer, new handler in ServerManagerModuleService
  • +
+

Pros: No IPC framework changes. Follows existing AckEvent precedent.

+

Cons: Asynchronous only. Requires correlation ID management. Slightly awkward request/response semantics.

+
+ +
+

Option 2: Second Reverse Socket Medium

+

Approach: Manager runs an IpcServer on a known socket. Server creates an IpcClient to it after configuration.

+
    +
  • Manager exports a new service (e.g. ServerManagerDataModule)
  • +
  • Server uses a _Stub to make synchronous RPC calls to the manager
  • +
+

Changes (~15–20 files):

+
    +
  • New proto service definition for manager-provided data
  • +
  • Manager-side: new IpcServer instance, export service, handle incoming RPCs
  • +
  • Server-side: new IpcClient/IChannel in SessionServerManager, use a Stub for calls
  • +
  • New socket path management (passed in SetConfigurationRequest)
  • +
+

Pros: Uses IPC libraries as designed. Synchronous request/response. Clean separation of concerns.

+

Cons: Extra socket per server. More FD management. More boilerplate setup.

+
+ +
+

Option 3: Symmetric IPC Framework Large

+

Approach: Extend the core IPC transport to support bidirectional RPC on a single socket.

+
    +
  • Modify rialtoipc-transport.proto to allow MethodCall in both directions
  • +
  • Add exportService() to client-side IChannel
  • +
  • Add CallMethod()/stub support to server-side IClient
  • +
+

Changes (30+ files across ipc/, serverManager/, media/server/):

+
    +
  • Transport protocol redesign
  • +
  • Reply tracking and dispatch on both sides
  • +
  • Threading/reentrancy review (deadlock risk with mutual blocking calls)
  • +
  • Refactor all existing consumers
  • +
+

Pros: Cleanest long-term architecture. Single socket. Full bidirectional RPC.

+

Cons: High effort. Risk of deadlocks. Touches core infrastructure used by all components.

+
+ +
+

Recommendation

+

Option 1 is the pragmatic choice for infrequent, async data requests. It requires no structural + changes and follows the existing ping/AckEvent precedent.

+

Option 2 is the right choice if you need synchronous request/response semantics or expect the + Server→Manager data API to grow over time. It stays within the framework's design intent with moderate effort.

+
+ + + diff --git a/media/server/ipc/CMakeLists.txt b/media/server/ipc/CMakeLists.txt index 15bd28844..3ed0c879a 100644 --- a/media/server/ipc/CMakeLists.txt +++ b/media/server/ipc/CMakeLists.txt @@ -42,9 +42,6 @@ add_library ( source/ControlClientServerInternal.cpp source/ControlModuleService.cpp source/PrivateMetricsModuleService.cpp - source/LogMetricsReporter.cpp - source/CompositeMetricsReporter.cpp - source/MetricsThresholdChecker.cpp source/ServerManagerModuleService.cpp source/SessionManagementServer.cpp source/SetLogLevelsService.cpp diff --git a/media/server/ipc/include/IPrivateMetricsModuleService.h b/media/server/ipc/include/IPrivateMetricsModuleService.h index ae97d8658..774f368d3 100644 --- a/media/server/ipc/include/IPrivateMetricsModuleService.h +++ b/media/server/ipc/include/IPrivateMetricsModuleService.h @@ -21,6 +21,7 @@ #define FIREBOLT_RIALTO_SERVER_IPC_I_PRIVATE_METRICS_MODULE_SERVICE_H_ #include "ControlCommon.h" +#include "IPrivateMetricsService.h" #include "MediaCommon.h" #include "privatemetricsmodule.pb.h" #include @@ -38,7 +39,8 @@ class IPrivateMetricsModuleServiceFactory static std::shared_ptr createFactory(); - virtual std::shared_ptr create() const = 0; + virtual std::shared_ptr + create(service::IPrivateMetricsService &metricsService) const = 0; }; class IPrivateMetricsModuleService : public ::firebolt::rialto::PrivateMetricsModule, diff --git a/media/server/ipc/include/PrivateMetricsModuleService.h b/media/server/ipc/include/PrivateMetricsModuleService.h index dc80c12cb..65f2a49d9 100644 --- a/media/server/ipc/include/PrivateMetricsModuleService.h +++ b/media/server/ipc/include/PrivateMetricsModuleService.h @@ -20,19 +20,14 @@ #ifndef FIREBOLT_RIALTO_SERVER_IPC_PRIVATE_METRICS_MODULE_SERVICE_H_ #define FIREBOLT_RIALTO_SERVER_IPC_PRIVATE_METRICS_MODULE_SERVICE_H_ -#include "IMetricsReporter.h" +#include "IMetricsCollectorClient.h" #include "IPrivateMetricsModuleService.h" -#include "MetricsThresholdChecker.h" -#include "StateMetricsAggregator.h" +#include "IPrivateMetricsService.h" #include -#include #include #include #include #include -#include -#include -#include namespace firebolt::rialto::server::ipc { @@ -42,21 +37,25 @@ class PrivateMetricsModuleServiceFactory : public IPrivateMetricsModuleServiceFa PrivateMetricsModuleServiceFactory() = default; ~PrivateMetricsModuleServiceFactory() override = default; - std::shared_ptr create() const override; + std::shared_ptr + create(service::IPrivateMetricsService &metricsService) const override; }; -class PrivateMetricsModuleService : public IPrivateMetricsModuleService +class PrivateMetricsModuleService : public IPrivateMetricsModuleService, + public firebolt::rialto::server::IMetricsCollectorClient { public: - PrivateMetricsModuleService(); + explicit PrivateMetricsModuleService(service::IPrivateMetricsService &metricsService); ~PrivateMetricsModuleService() override; + // IPrivateMetricsModuleService void clientConnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) override; void clientDisconnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) override; void notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) override; void notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) override; + // PrivateMetricsModule RPC handlers void reportClientMetrics(::google::protobuf::RpcController *controller, const ::firebolt::rialto::ReportClientMetricsRequest *request, ::firebolt::rialto::ReportClientMetricsResponse *response, @@ -66,69 +65,19 @@ class PrivateMetricsModuleService : public IPrivateMetricsModuleService ::firebolt::rialto::NotifyClientReadyResponse *response, ::google::protobuf::Closure *done) override; -private: - struct ProcessMetricsSample - { - std::uint64_t monotonicTimeMs; - std::uint64_t epochTimeMs; - std::uint64_t processCpuTimeMs; - std::uint64_t processMemoryKb; - std::uint64_t cgroupMemoryUsageKb; - std::uint64_t cgroupMemoryLimitKb; - }; - - struct MetricsSamplePair - { - ::firebolt::rialto::ClientProcessMetrics clientMetrics; - ProcessMetricsSample serverMetrics; - }; - - struct ClientMetricsState - { - bool isReady{false}; - std::optional latestMetrics; - }; - - struct SessionMetricsState - { - PlaybackState currentPlaybackState{PlaybackState::UNKNOWN}; - StateMetricsAggregator aggregator; - }; - - void runMetricsSampler(); - void requestMetricsSample(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient, - ::firebolt::rialto::MetricsSampleReason reason); - ProcessMetricsSample getProcessMetricsSample() const; - void logMetrics(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient, - const ::firebolt::rialto::ClientProcessMetrics &clientMetrics, - const ProcessMetricsSample &serverMetrics); - void logStateReport(const StateMetricsReport &report, const std::string &context); - const char *sampleReasonToString(::firebolt::rialto::MetricsSampleReason reason) const; - static const char *playbackStateToString(PlaybackState state); - static const char *applicationStateToString(ApplicationState state); - double calculateCpuPercentage(std::uint64_t currentCpuTimeMs, std::uint64_t previousCpuTimeMs, - std::uint64_t currentMonotonicTimeMs, std::uint64_t previousMonotonicTimeMs) const; + // IMetricsCollectorClient + void requestMetricsSample(int clientId, std::uint64_t sampleId, + firebolt::rialto::server::MetricsSampleReason reason) override; private: - std::atomic m_isRunning; - std::atomic m_nextSampleId; - std::thread m_metricsThread; - std::condition_variable m_wakeup; + service::IPrivateMetricsService &m_metricsService; + std::atomic m_nextClientId{1}; std::mutex m_mutex; - std::map, ClientMetricsState> m_clients; - - // Per-session state tracking (sessionId -> session state) - std::map m_sessionStates; - - // Global aggregator (active across all sessions while RUNNING) - StateMetricsAggregator m_globalAggregator; - ApplicationState m_currentApplicationState{ApplicationState::UNKNOWN}; - - // Pluggable metrics reporter (log, telemetry, or composite) - std::unique_ptr m_reporter; - // Threshold checker - MetricsThresholdChecker m_thresholdChecker; + // Map IPC client pointer → client ID + std::map, int> m_clientIds; + // Map client ID → IPC client pointer (for sending events back) + std::map> m_ipcClients; }; } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/include/SessionManagementServer.h b/media/server/ipc/include/SessionManagementServer.h index 9c227c390..00709b2fa 100644 --- a/media/server/ipc/include/SessionManagementServer.h +++ b/media/server/ipc/include/SessionManagementServer.h @@ -29,6 +29,7 @@ #include "IMediaPipelineModuleService.h" #include "IPlaybackService.h" #include "IPrivateMetricsModuleService.h" +#include "IPrivateMetricsService.h" #include "ISessionManagementServer.h" #include "IWebAudioPlayerModuleService.h" #include "SetLogLevelsService.h" @@ -54,7 +55,7 @@ class SessionManagementServer : public ISessionManagementServer const std::shared_ptr &privateMetricsModuleFactory, const std::shared_ptr &controlModuleFactory, service::IPlaybackService &playbackService, service::ICdmService &cdmService, - service::IControlService &controlService); + service::IControlService &controlService, service::IPrivateMetricsService &metricsService); ~SessionManagementServer() override; SessionManagementServer(const SessionManagementServer &) = delete; SessionManagementServer(SessionManagementServer &&) = delete; diff --git a/media/server/ipc/interface/IIpcFactory.h b/media/server/ipc/interface/IIpcFactory.h index 668473761..c5370de38 100644 --- a/media/server/ipc/interface/IIpcFactory.h +++ b/media/server/ipc/interface/IIpcFactory.h @@ -24,6 +24,7 @@ #include "ICdmService.h" #include "IControlService.h" #include "IPlaybackService.h" +#include "IPrivateMetricsService.h" #include "ISessionManagementServer.h" #include "ISessionServerManager.h" #include @@ -40,7 +41,8 @@ class IIpcFactory createApplicationManagementServer(service::ISessionServerManager &sessionServerManager) const = 0; virtual std::unique_ptr createSessionManagementServer(service::IPlaybackService &playbackService, service::ICdmService &cdmService, - service::IControlService &controlService) const = 0; + service::IControlService &controlService, + service::IPrivateMetricsService &metricsService) const = 0; }; } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/interface/IpcFactory.h b/media/server/ipc/interface/IpcFactory.h index bb5d9e166..4fafd5ce2 100644 --- a/media/server/ipc/interface/IpcFactory.h +++ b/media/server/ipc/interface/IpcFactory.h @@ -39,7 +39,8 @@ class IpcFactory : public IIpcFactory createApplicationManagementServer(service::ISessionServerManager &sessionServerManager) const override; std::unique_ptr createSessionManagementServer(service::IPlaybackService &playbackService, service::ICdmService &cdmService, - service::IControlService &controlService) const override; + service::IControlService &controlService, + service::IPrivateMetricsService &metricsService) const override; }; } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/source/IpcFactory.cpp b/media/server/ipc/source/IpcFactory.cpp index c178c7c67..20d9ca389 100644 --- a/media/server/ipc/source/IpcFactory.cpp +++ b/media/server/ipc/source/IpcFactory.cpp @@ -43,7 +43,8 @@ IpcFactory::createApplicationManagementServer(service::ISessionServerManager &se std::unique_ptr IpcFactory::createSessionManagementServer(service::IPlaybackService &playbackService, service::ICdmService &cdmService, - service::IControlService &controlService) const + service::IControlService &controlService, + service::IPrivateMetricsService &metricsService) const { return std::make_unique< SessionManagementServer>(firebolt::rialto::ipc::IServerFactory::createFactory(), @@ -54,6 +55,6 @@ IpcFactory::createSessionManagementServer(service::IPlaybackService &playbackSer firebolt::rialto::server::ipc::IWebAudioPlayerModuleServiceFactory::createFactory(), firebolt::rialto::server::ipc::IPrivateMetricsModuleServiceFactory::createFactory(), firebolt::rialto::server::ipc::IControlModuleServiceFactory::createFactory(), - playbackService, cdmService, controlService); + playbackService, cdmService, controlService, metricsService); } } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/source/PrivateMetricsModuleService.cpp b/media/server/ipc/source/PrivateMetricsModuleService.cpp index ea02eab91..b9e21b2d2 100644 --- a/media/server/ipc/source/PrivateMetricsModuleService.cpp +++ b/media/server/ipc/source/PrivateMetricsModuleService.cpp @@ -18,22 +18,9 @@ */ #include "PrivateMetricsModuleService.h" -#include "LogMetricsReporter.h" #include "RialtoServerLogging.h" #include -#include #include -#include -#include -#include -#include -#include -#include - -namespace -{ -constexpr std::chrono::seconds kMetricsInterval{15}; -} // namespace namespace firebolt::rialto::server::ipc { @@ -54,13 +41,14 @@ std::shared_ptr IPrivateMetricsModuleServic return factory; } -std::shared_ptr PrivateMetricsModuleServiceFactory::create() const +std::shared_ptr +PrivateMetricsModuleServiceFactory::create(service::IPrivateMetricsService &metricsService) const { std::shared_ptr privateMetricsModule; try { - privateMetricsModule = std::make_shared(); + privateMetricsModule = std::make_shared(metricsService); } catch (const std::exception &e) { @@ -70,29 +58,19 @@ std::shared_ptr PrivateMetricsModuleServiceFactory return privateMetricsModule; } -PrivateMetricsModuleService::PrivateMetricsModuleService() - : m_isRunning{true}, m_nextSampleId{1}, m_reporter{std::make_unique()}, - m_thresholdChecker{MetricsThresholdConfig{}, m_reporter.get()} +PrivateMetricsModuleService::PrivateMetricsModuleService(service::IPrivateMetricsService &metricsService) + : m_metricsService{metricsService} { - m_metricsThread = std::thread(&PrivateMetricsModuleService::runMetricsSampler, this); } -PrivateMetricsModuleService::~PrivateMetricsModuleService() -{ - m_isRunning.store(false); - m_wakeup.notify_all(); - if (m_metricsThread.joinable()) - { - m_metricsThread.join(); - } -} +PrivateMetricsModuleService::~PrivateMetricsModuleService() = default; void PrivateMetricsModuleService::clientConnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) { RIALTO_SERVER_LOG_INFO("Client connected to private metrics module"); { std::lock_guard lock{m_mutex}; - m_clients.emplace(ipcClient, ClientMetricsState{}); + // Don't assign a clientId yet — wait for notifyClientReady } ipcClient->exportService(shared_from_this()); } @@ -100,8 +78,21 @@ void PrivateMetricsModuleService::clientConnected(const std::shared_ptr<::firebo void PrivateMetricsModuleService::clientDisconnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) { RIALTO_SERVER_LOG_INFO("Client disconnected from private metrics module"); - std::lock_guard lock{m_mutex}; - m_clients.erase(ipcClient); + int clientId{0}; + { + std::lock_guard lock{m_mutex}; + auto iter = m_clientIds.find(ipcClient); + if (iter != m_clientIds.end()) + { + clientId = iter->second; + m_clientIds.erase(iter); + m_ipcClients.erase(clientId); + } + } + if (clientId != 0) + { + m_metricsService.clientDisconnected(clientId); + } } void PrivateMetricsModuleService::notifyClientReady(::google::protobuf::RpcController *controller, @@ -120,27 +111,27 @@ void PrivateMetricsModuleService::notifyClientReady(::google::protobuf::RpcContr } auto ipcClient{ipcController->getClient()}; + const int kClientId{m_nextClientId.fetch_add(1)}; { std::lock_guard lock{m_mutex}; - auto clientIter{m_clients.find(ipcClient)}; - if (m_clients.end() == clientIter) - { - RIALTO_SERVER_LOG_WARN("Ignoring private metrics ready notification from unknown client"); - done->Run(); - return; - } - clientIter->second.isReady = true; + m_clientIds[ipcClient] = kClientId; + m_ipcClients[kClientId] = ipcClient; } - RIALTO_SERVER_LOG_MIL("Client ready for private metrics samples"); + RIALTO_SERVER_LOG_MIL("Client ready for private metrics samples, assigned clientId=%d", kClientId); done->Run(); - requestMetricsSample(ipcClient, firebolt::rialto::METRICS_SAMPLE_REASON_CONNECTED); + + // Create a shared_ptr to this as IMetricsCollectorClient, aliasing with shared_from_this() + // so the IPC layer stays alive as long as the MetricsCollector holds a reference. + auto self = shared_from_this(); + std::shared_ptr clientInterface( + self, static_cast(this)); + m_metricsService.clientReady(kClientId, clientInterface); } -void PrivateMetricsModuleService::reportClientMetrics(::google::protobuf::RpcController *controller, - const ::firebolt::rialto::ReportClientMetricsRequest *request, - ::firebolt::rialto::ReportClientMetricsResponse *response, - ::google::protobuf::Closure *done) +void PrivateMetricsModuleService::reportClientMetrics( + ::google::protobuf::RpcController *controller, const ::firebolt::rialto::ReportClientMetricsRequest *request, + ::firebolt::rialto::ReportClientMetricsResponse *response, ::google::protobuf::Closure *done) { RIALTO_SERVER_LOG_DEBUG("entry:"); auto ipcController = dynamic_cast(controller); @@ -159,473 +150,112 @@ void PrivateMetricsModuleService::reportClientMetrics(::google::protobuf::RpcCon return; } - const auto &metrics{request->metrics()}; - const auto kServerMetrics{getProcessMetricsSample()}; auto ipcClient{ipcController->getClient()}; - logMetrics(ipcClient, metrics, kServerMetrics); - { - std::lock_guard lock{m_mutex}; - auto &clientState{m_clients[ipcClient]}; - clientState.isReady = true; - clientState.latestMetrics = MetricsSamplePair{metrics, kServerMetrics}; - } - - done->Run(); -} - -void PrivateMetricsModuleService::runMetricsSampler() -{ - while (m_isRunning.load()) - { - std::vector> clients; - { - std::unique_lock lock{m_mutex}; - m_wakeup.wait_for(lock, kMetricsInterval, [this]() { return !m_isRunning.load(); }); - if (!m_isRunning.load()) - { - break; - } - for (const auto &client : m_clients) - { - if (client.second.isReady) - { - clients.push_back(client.first); - } - } - } - - for (const auto &client : clients) - { - requestMetricsSample(client, firebolt::rialto::METRICS_SAMPLE_REASON_PERIODIC); - } - } -} - -void PrivateMetricsModuleService::requestMetricsSample( - const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient, - ::firebolt::rialto::MetricsSampleReason reason) -{ - if (!ipcClient || !ipcClient->isConnected()) - { - return; - } - - auto event{std::make_shared()}; - const auto kSampleId{m_nextSampleId.fetch_add(1)}; - event->set_sample_id(kSampleId); - event->set_reason(reason); - - RIALTO_SERVER_LOG_MIL("Requesting metrics sample=%" PRIu64 ", reason=%s", kSampleId, sampleReasonToString(reason)); - - if (!ipcClient->sendEvent(event)) - { - RIALTO_SERVER_LOG_WARN("Failed to request client metrics sample=%" PRIu64 ", reason=%s", kSampleId, - sampleReasonToString(reason)); - } -} - -PrivateMetricsModuleService::ProcessMetricsSample PrivateMetricsModuleService::getProcessMetricsSample() const -{ - using std::chrono::duration_cast; - using std::chrono::milliseconds; - using std::chrono::steady_clock; - using std::chrono::system_clock; - - struct tms processTimes{}; - const clock_t kCurrentTicks{times(&processTimes)}; - const long kTicksPerSecond{sysconf(_SC_CLK_TCK)}; - std::uint64_t processCpuTimeMs{0}; - if ((static_cast(-1) != kCurrentTicks) && (kTicksPerSecond > 0)) - { - const auto kProcessTicks{processTimes.tms_utime + processTimes.tms_stime}; - processCpuTimeMs = static_cast((static_cast(kProcessTicks) * 1000.0) / - static_cast(kTicksPerSecond)); - } - else - { - RIALTO_SERVER_LOG_WARN("Failed to sample server process CPU usage"); - } - - std::uint64_t processMemoryKb{0}; - { - std::ifstream status{"/proc/self/status"}; - std::string line; - while (std::getline(status, line)) - { - if (line.rfind("VmRSS:", 0) == 0) - { - if (std::sscanf(line.c_str(), "VmRSS: %" SCNu64, &processMemoryKb) != 1) - { - RIALTO_SERVER_LOG_WARN("Failed to parse server process memory usage"); - } - break; - } - } - } - - std::uint64_t cgroupMemoryUsageKb{0}; - std::uint64_t cgroupMemoryLimitKb{0}; - { - auto readFileValue = [](const std::string &path) -> std::uint64_t - { - std::ifstream file{path}; - if (!file.is_open()) - { - return 0; - } - std::string content; - if (!std::getline(file, content) || content.empty() || content == "max") - { - return 0; - } - std::uint64_t value{0}; - if (std::sscanf(content.c_str(), "%" SCNu64, &value) == 1) - { - return value; - } - return 0; - }; - - // Resolve the process's cgroup path from /proc/self/cgroup - // cgroup v2 format: "0::" - auto getCgroupBasePath = [&readFileValue]() -> std::string - { - std::ifstream cgroupFile{"/proc/self/cgroup"}; - if (!cgroupFile.is_open()) - { - return {}; - } - std::string line; - while (std::getline(cgroupFile, line)) - { - // cgroup v2 line starts with "0::" - if (line.rfind("0::", 0) == 0) - { - std::string relativePath{line.substr(3)}; - if (!relativePath.empty() && relativePath != "/") - { - return "/sys/fs/cgroup" + relativePath; - } - return "/sys/fs/cgroup"; - } - } - return {}; - }; - - std::uint64_t usageBytes{0}; - std::uint64_t limitBytes{0}; - - // cgroup v2: read from process's own cgroup path - std::string cgroupBase{getCgroupBasePath()}; - if (!cgroupBase.empty()) - { - usageBytes = readFileValue(cgroupBase + "/memory.current"); - limitBytes = readFileValue(cgroupBase + "/memory.max"); - } - - if (usageBytes == 0) - { - // cgroup v1 fallback - usageBytes = readFileValue("/sys/fs/cgroup/memory/memory.usage_in_bytes"); - limitBytes = readFileValue("/sys/fs/cgroup/memory/memory.limit_in_bytes"); - } - - cgroupMemoryUsageKb = usageBytes / 1024; - cgroupMemoryLimitKb = limitBytes / 1024; - } - - return ProcessMetricsSample{ - static_cast(duration_cast(steady_clock::now().time_since_epoch()).count()), - static_cast(duration_cast(system_clock::now().time_since_epoch()).count()), - processCpuTimeMs, - processMemoryKb, - cgroupMemoryUsageKb, - cgroupMemoryLimitKb}; -} - -void PrivateMetricsModuleService::logMetrics(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient, - const ::firebolt::rialto::ClientProcessMetrics &clientMetrics, - const ProcessMetricsSample &serverMetrics) -{ - std::optional previousSample; + int clientId{0}; { std::lock_guard lock{m_mutex}; - const auto kClientIter{m_clients.find(ipcClient)}; - if ((m_clients.end() != kClientIter) && kClientIter->second.latestMetrics.has_value()) + auto iter = m_clientIds.find(ipcClient); + if (iter != m_clientIds.end()) { - previousSample = kClientIter->second.latestMetrics; + clientId = iter->second; } } - if (!previousSample.has_value() || !previousSample->clientMetrics.has_process_cpu_time_ms()) + if (clientId == 0) { - RIALTO_SERVER_LOG_MIL("Metrics baseline: sample=%" PRIu64 ", reason=%s, app='%s', client_pid=%u, " - "client_cpu_ms=%" PRIu64 ", server_cpu_ms=%" PRIu64 ", " - "client_mem_kb=%" PRIu64 ", server_mem_kb=%" PRIu64 ", " - "cgroup_mem_kb=%" PRIu64 "/%" PRIu64, - clientMetrics.sample_id(), sampleReasonToString(clientMetrics.reason()), - clientMetrics.app_name().c_str(), clientMetrics.process_id(), - clientMetrics.process_cpu_time_ms(), serverMetrics.processCpuTimeMs, - clientMetrics.process_memory_kb(), serverMetrics.processMemoryKb, - serverMetrics.cgroupMemoryUsageKb, serverMetrics.cgroupMemoryLimitKb); + RIALTO_SERVER_LOG_WARN("reportClientMetrics from unknown client"); + done->Run(); return; } - const auto &previousClientMetrics{previousSample->clientMetrics}; - const auto &previousServerMetrics{previousSample->serverMetrics}; - const double kClientCpuPercentage{calculateCpuPercentage( - clientMetrics.process_cpu_time_ms(), previousClientMetrics.process_cpu_time_ms(), - clientMetrics.monotonic_time_ms(), previousClientMetrics.monotonic_time_ms())}; - const double kServerCpuPercentage{calculateCpuPercentage(serverMetrics.processCpuTimeMs, - previousServerMetrics.processCpuTimeMs, - serverMetrics.monotonicTimeMs, - previousServerMetrics.monotonicTimeMs)}; - const double kCombinedCpuPercentage{calculateCpuPercentage( - clientMetrics.process_cpu_time_ms() + serverMetrics.processCpuTimeMs, - previousClientMetrics.process_cpu_time_ms() + previousServerMetrics.processCpuTimeMs, - serverMetrics.monotonicTimeMs, previousServerMetrics.monotonicTimeMs)}; - - // Report via pluggable reporter - if (m_reporter) - { - PeriodicMetricsReport periodicReport; - periodicReport.sampleId = clientMetrics.sample_id(); - periodicReport.reason = sampleReasonToString(clientMetrics.reason()); - periodicReport.appName = clientMetrics.app_name(); - periodicReport.clientPid = clientMetrics.process_id(); - periodicReport.clientCpuPercent = kClientCpuPercentage; - periodicReport.serverCpuPercent = kServerCpuPercentage; - periodicReport.combinedCpuPercent = kCombinedCpuPercentage; - periodicReport.clientCpuTimeMs = clientMetrics.process_cpu_time_ms(); - periodicReport.serverCpuTimeMs = serverMetrics.processCpuTimeMs; - periodicReport.clientMemoryKb = clientMetrics.process_memory_kb(); - periodicReport.serverMemoryKb = serverMetrics.processMemoryKb; - periodicReport.cgroupMemoryUsageKb = serverMetrics.cgroupMemoryUsageKb; - periodicReport.cgroupMemoryLimitKb = serverMetrics.cgroupMemoryLimitKb; - m_reporter->reportPeriodicSample(periodicReport); - } + const auto &protoMetrics{request->metrics()}; + firebolt::rialto::server::ClientMetricsData metrics; + metrics.sampleId = protoMetrics.sample_id(); + metrics.appName = protoMetrics.app_name(); + metrics.processId = protoMetrics.process_id(); + metrics.monotonicTimeMs = protoMetrics.monotonic_time_ms(); + metrics.epochTimeMs = protoMetrics.epoch_time_ms(); + metrics.processCpuTimeMs = protoMetrics.process_cpu_time_ms(); + metrics.processMemoryKb = protoMetrics.process_memory_kb(); - // Only feed PERIODIC samples into aggregators — STATE_TRANSITION samples have - // unreliable CPU percentages due to tiny time deltas between rapid samples. - if (clientMetrics.reason() == firebolt::rialto::METRICS_SAMPLE_REASON_PERIODIC) - { - MetricsSample sample; - sample.clientCpuPercent = kClientCpuPercentage; - sample.serverCpuPercent = kServerCpuPercentage; - sample.combinedCpuPercent = kCombinedCpuPercentage; - sample.clientMemoryKb = clientMetrics.process_memory_kb(); - sample.serverMemoryKb = serverMetrics.processMemoryKb; - sample.cgroupMemoryUsageKb = serverMetrics.cgroupMemoryUsageKb; - sample.cgroupMemoryLimitKb = serverMetrics.cgroupMemoryLimitKb; - - { - std::lock_guard lock{m_mutex}; - - // Feed into per-session aggregators - for (auto &[sessionId, sessionState] : m_sessionStates) - { - sessionState.aggregator.addSample(sample); - } - - // Feed into global aggregator - if (m_currentApplicationState == ApplicationState::RUNNING) - { - m_globalAggregator.addSample(sample); - } - } - } - - // Check thresholds (only for PERIODIC samples with reliable CPU data) - if (clientMetrics.reason() == firebolt::rialto::METRICS_SAMPLE_REASON_PERIODIC) - { - m_thresholdChecker.checkSample(kClientCpuPercentage, kServerCpuPercentage, kCombinedCpuPercentage, - clientMetrics.process_memory_kb(), serverMetrics.processMemoryKb, - serverMetrics.cgroupMemoryUsageKb, serverMetrics.cgroupMemoryLimitKb); - } -} - -const char *PrivateMetricsModuleService::sampleReasonToString(::firebolt::rialto::MetricsSampleReason reason) const -{ - switch (reason) + // Convert proto reason to our enum + switch (protoMetrics.reason()) { case firebolt::rialto::METRICS_SAMPLE_REASON_CONNECTED: - return "CONNECTED"; + metrics.reason = firebolt::rialto::server::MetricsSampleReason::CONNECTED; + break; case firebolt::rialto::METRICS_SAMPLE_REASON_PERIODIC: - return "PERIODIC"; + metrics.reason = firebolt::rialto::server::MetricsSampleReason::PERIODIC; + break; case firebolt::rialto::METRICS_SAMPLE_REASON_STATE_TRANSITION: - return "STATE_TRANSITION"; - case firebolt::rialto::METRICS_SAMPLE_REASON_UNKNOWN: + metrics.reason = firebolt::rialto::server::MetricsSampleReason::STATE_TRANSITION; + break; default: - return "UNKNOWN"; + metrics.reason = firebolt::rialto::server::MetricsSampleReason::UNKNOWN; + break; } -} -const char *PrivateMetricsModuleService::playbackStateToString(PlaybackState state) -{ - switch (state) - { - case PlaybackState::IDLE: - return "IDLE"; - case PlaybackState::PLAYING: - return "PLAYING"; - case PlaybackState::PAUSED: - return "PAUSED"; - case PlaybackState::SEEKING: - return "SEEKING"; - case PlaybackState::SEEK_DONE: - return "SEEK_DONE"; - case PlaybackState::STOPPED: - return "STOPPED"; - case PlaybackState::END_OF_STREAM: - return "END_OF_STREAM"; - case PlaybackState::FAILURE: - return "FAILURE"; - case PlaybackState::UNKNOWN: - default: - return "UNKNOWN"; - } -} + done->Run(); -const char *PrivateMetricsModuleService::applicationStateToString(ApplicationState state) -{ - switch (state) - { - case ApplicationState::RUNNING: - return "RUNNING"; - case ApplicationState::INACTIVE: - return "INACTIVE"; - case ApplicationState::UNKNOWN: - default: - return "UNKNOWN"; - } + m_metricsService.reportMetrics(clientId, metrics); } void PrivateMetricsModuleService::notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) { - RIALTO_SERVER_LOG_MIL("Metrics: PlaybackState changed session=%d, %s -> %s", sessionId, - playbackStateToString(oldState), playbackStateToString(newState)); - - using std::chrono::duration_cast; - using std::chrono::milliseconds; - using std::chrono::steady_clock; - const auto kNowMs{ - static_cast(duration_cast(steady_clock::now().time_since_epoch()).count())}; - - std::lock_guard lock{m_mutex}; - auto sessionIter{m_sessionStates.find(sessionId)}; - if (m_sessionStates.end() == sessionIter) - { - // First state notification for this session — create entry - SessionMetricsState sessionState; - sessionState.currentPlaybackState = newState; - sessionState.aggregator.begin(playbackStateToString(newState), kNowMs); - m_sessionStates.emplace(sessionId, std::move(sessionState)); - return; - } - - auto &sessionState{sessionIter->second}; - - // Finalize old state and emit report - if (sessionState.aggregator.hasData()) - { - auto report{sessionState.aggregator.finalize(kNowMs)}; - logStateReport(report, "session=" + std::to_string(sessionId)); - } - - if (newState == PlaybackState::STOPPED || newState == PlaybackState::END_OF_STREAM || - newState == PlaybackState::FAILURE) - { - // Terminal state — remove session tracking - m_sessionStates.erase(sessionIter); - } - else - { - // Begin accumulating for new state - sessionState.currentPlaybackState = newState; - sessionState.aggregator.begin(playbackStateToString(newState), kNowMs); - } - - // Request immediate sample for clean boundary - for (const auto &client : m_clients) - { - if (client.second.isReady) - { - requestMetricsSample(client.first, firebolt::rialto::METRICS_SAMPLE_REASON_STATE_TRANSITION); - } - } + m_metricsService.notifyPlaybackStateChanged(sessionId, oldState, newState); } void PrivateMetricsModuleService::notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) { - RIALTO_SERVER_LOG_MIL("Metrics: ApplicationState changed %s -> %s", applicationStateToString(oldState), - applicationStateToString(newState)); - - using std::chrono::duration_cast; - using std::chrono::milliseconds; - using std::chrono::steady_clock; - const auto kNowMs{ - static_cast(duration_cast(steady_clock::now().time_since_epoch()).count())}; - - std::lock_guard lock{m_mutex}; - m_currentApplicationState = newState; + m_metricsService.notifyApplicationStateChanged(oldState, newState); +} - if (oldState == ApplicationState::RUNNING && newState != ApplicationState::RUNNING) +void PrivateMetricsModuleService::requestMetricsSample(int clientId, std::uint64_t sampleId, + firebolt::rialto::server::MetricsSampleReason reason) +{ + std::shared_ptr<::firebolt::rialto::ipc::IClient> ipcClient; { - // Leaving RUNNING — finalize global aggregator - if (m_globalAggregator.hasData()) + std::lock_guard lock{m_mutex}; + auto iter = m_ipcClients.find(clientId); + if (iter == m_ipcClients.end()) { - auto report{m_globalAggregator.finalize(kNowMs)}; - logStateReport(report, "global"); + return; } - m_globalAggregator.reset(); + ipcClient = iter->second; } - if (newState == ApplicationState::RUNNING && oldState != ApplicationState::RUNNING) + if (!ipcClient || !ipcClient->isConnected()) { - // Entering RUNNING — start fresh global accumulation - m_globalAggregator.begin(applicationStateToString(newState), kNowMs); + return; } - // Request immediate sample for clean boundary - for (const auto &client : m_clients) - { - if (client.second.isReady) - { - requestMetricsSample(client.first, firebolt::rialto::METRICS_SAMPLE_REASON_STATE_TRANSITION); - } - } -} + auto event{std::make_shared()}; + event->set_sample_id(sampleId); -void PrivateMetricsModuleService::logStateReport(const StateMetricsReport &report, const std::string &context) -{ - if (m_reporter) + // Convert our enum to proto enum + switch (reason) { - StateTransitionReport transitionReport; - transitionReport.context = context; - transitionReport.metrics = report; - m_reporter->reportStateTransition(transitionReport); + case firebolt::rialto::server::MetricsSampleReason::CONNECTED: + event->set_reason(firebolt::rialto::METRICS_SAMPLE_REASON_CONNECTED); + break; + case firebolt::rialto::server::MetricsSampleReason::PERIODIC: + event->set_reason(firebolt::rialto::METRICS_SAMPLE_REASON_PERIODIC); + break; + case firebolt::rialto::server::MetricsSampleReason::STATE_TRANSITION: + event->set_reason(firebolt::rialto::METRICS_SAMPLE_REASON_STATE_TRANSITION); + break; + default: + event->set_reason(firebolt::rialto::METRICS_SAMPLE_REASON_UNKNOWN); + break; } -} -double PrivateMetricsModuleService::calculateCpuPercentage(std::uint64_t currentCpuTimeMs, - std::uint64_t previousCpuTimeMs, - std::uint64_t currentMonotonicTimeMs, - std::uint64_t previousMonotonicTimeMs) const -{ - constexpr std::uint64_t kMinElapsedMs{100}; - if ((currentCpuTimeMs < previousCpuTimeMs) || (currentMonotonicTimeMs <= previousMonotonicTimeMs)) - { - return 0.0; - } + RIALTO_SERVER_LOG_DEBUG("Requesting metrics sample=%" PRIu64 " from client %d", sampleId, clientId); - const auto kElapsedMs{currentMonotonicTimeMs - previousMonotonicTimeMs}; - if (kElapsedMs < kMinElapsedMs) + if (!ipcClient->sendEvent(event)) { - // Time delta too small for meaningful CPU percentage - return 0.0; + RIALTO_SERVER_LOG_WARN("Failed to request client metrics sample=%" PRIu64 " from client %d", sampleId, + clientId); } - - return (static_cast(currentCpuTimeMs - previousCpuTimeMs) / static_cast(kElapsedMs)) * 100.0; } } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/source/SessionManagementServer.cpp b/media/server/ipc/source/SessionManagementServer.cpp index 8fe584437..05e17bd67 100644 --- a/media/server/ipc/source/SessionManagementServer.cpp +++ b/media/server/ipc/source/SessionManagementServer.cpp @@ -49,7 +49,8 @@ SessionManagementServer::SessionManagementServer( const std::shared_ptr &webAudioPlayerModuleFactory, const std::shared_ptr &privateMetricsModuleFactory, const std::shared_ptr &controlModuleFactory, service::IPlaybackService &playbackService, - service::ICdmService &cdmService, service::IControlService &controlService) + service::ICdmService &cdmService, service::IControlService &controlService, + service::IPrivateMetricsService &metricsService) : m_isRunning{false}, m_mediaPipelineModule{mediaPipelineModuleFactory->create(playbackService.getMediaPipelineService())}, m_mediaPipelineCapabilitiesModule{ @@ -57,7 +58,7 @@ SessionManagementServer::SessionManagementServer( m_mediaKeysModule{mediaKeysModuleFactory->create(cdmService)}, m_mediaKeysCapabilitiesModule{mediaKeysCapabilitiesModuleFactory->create(cdmService)}, m_webAudioPlayerModule{webAudioPlayerModuleFactory->create(playbackService.getWebAudioPlayerService())}, - m_privateMetricsModule{privateMetricsModuleFactory->create()}, + m_privateMetricsModule{privateMetricsModuleFactory->create(metricsService)}, m_controlModule{controlModuleFactory->create(playbackService, controlService)} { m_ipcServer = ipcFactory->create(); diff --git a/media/server/main/CMakeLists.txt b/media/server/main/CMakeLists.txt index bf6b23dd7..113691a2b 100644 --- a/media/server/main/CMakeLists.txt +++ b/media/server/main/CMakeLists.txt @@ -55,6 +55,10 @@ add_library( source/TextTrackAccessor.cpp source/TextTrackSession.cpp source/NeedDataDelayCalculator.cpp + source/MetricsCollector.cpp + source/LogMetricsReporter.cpp + source/CompositeMetricsReporter.cpp + source/MetricsThresholdChecker.cpp ) target_include_directories( diff --git a/media/server/ipc/include/CompositeMetricsReporter.h b/media/server/main/include/CompositeMetricsReporter.h similarity index 83% rename from media/server/ipc/include/CompositeMetricsReporter.h rename to media/server/main/include/CompositeMetricsReporter.h index c08471517..b0aef77e1 100644 --- a/media/server/ipc/include/CompositeMetricsReporter.h +++ b/media/server/main/include/CompositeMetricsReporter.h @@ -17,14 +17,14 @@ * limitations under the License. */ -#ifndef FIREBOLT_RIALTO_SERVER_IPC_COMPOSITE_METRICS_REPORTER_H_ -#define FIREBOLT_RIALTO_SERVER_IPC_COMPOSITE_METRICS_REPORTER_H_ +#ifndef FIREBOLT_RIALTO_SERVER_COMPOSITE_METRICS_REPORTER_H_ +#define FIREBOLT_RIALTO_SERVER_COMPOSITE_METRICS_REPORTER_H_ #include "IMetricsReporter.h" #include #include -namespace firebolt::rialto::server::ipc +namespace firebolt::rialto::server { /** * @brief Fans out metrics to multiple reporters (log + remote telemetry, etc.) @@ -44,6 +44,6 @@ class CompositeMetricsReporter : public IMetricsReporter private: std::vector> m_reporters; }; -} // namespace firebolt::rialto::server::ipc +} // namespace firebolt::rialto::server -#endif // FIREBOLT_RIALTO_SERVER_IPC_COMPOSITE_METRICS_REPORTER_H_ +#endif // FIREBOLT_RIALTO_SERVER_COMPOSITE_METRICS_REPORTER_H_ diff --git a/media/server/ipc/include/IMetricsReporter.h b/media/server/main/include/IMetricsReporter.h similarity index 89% rename from media/server/ipc/include/IMetricsReporter.h rename to media/server/main/include/IMetricsReporter.h index 0e6d9cd97..00db04016 100644 --- a/media/server/ipc/include/IMetricsReporter.h +++ b/media/server/main/include/IMetricsReporter.h @@ -17,15 +17,15 @@ * limitations under the License. */ -#ifndef FIREBOLT_RIALTO_SERVER_IPC_I_METRICS_REPORTER_H_ -#define FIREBOLT_RIALTO_SERVER_IPC_I_METRICS_REPORTER_H_ +#ifndef FIREBOLT_RIALTO_SERVER_I_METRICS_REPORTER_H_ +#define FIREBOLT_RIALTO_SERVER_I_METRICS_REPORTER_H_ #include "StateMetricsAggregator.h" #include #include #include -namespace firebolt::rialto::server::ipc +namespace firebolt::rialto::server { /** * @brief Periodic sample data reported each sampling interval. @@ -52,7 +52,7 @@ struct PeriodicMetricsReport */ struct StateTransitionReport { - std::string context; // e.g. "session=1" or "global" + std::string context; // e.g. "session=1" or "global" StateMetricsReport metrics; }; @@ -95,6 +95,6 @@ class IMetricsReporter virtual void reportStateTransition(const StateTransitionReport &report) = 0; virtual void reportThresholdExceeded(const ThresholdAlert &alert) = 0; }; -} // namespace firebolt::rialto::server::ipc +} // namespace firebolt::rialto::server -#endif // FIREBOLT_RIALTO_SERVER_IPC_I_METRICS_REPORTER_H_ +#endif // FIREBOLT_RIALTO_SERVER_I_METRICS_REPORTER_H_ diff --git a/media/server/ipc/include/LogMetricsReporter.h b/media/server/main/include/LogMetricsReporter.h similarity index 81% rename from media/server/ipc/include/LogMetricsReporter.h rename to media/server/main/include/LogMetricsReporter.h index 0a423c742..ff87d559e 100644 --- a/media/server/ipc/include/LogMetricsReporter.h +++ b/media/server/main/include/LogMetricsReporter.h @@ -17,12 +17,12 @@ * limitations under the License. */ -#ifndef FIREBOLT_RIALTO_SERVER_IPC_LOG_METRICS_REPORTER_H_ -#define FIREBOLT_RIALTO_SERVER_IPC_LOG_METRICS_REPORTER_H_ +#ifndef FIREBOLT_RIALTO_SERVER_LOG_METRICS_REPORTER_H_ +#define FIREBOLT_RIALTO_SERVER_LOG_METRICS_REPORTER_H_ #include "IMetricsReporter.h" -namespace firebolt::rialto::server::ipc +namespace firebolt::rialto::server { /** * @brief Outputs metrics to the Rialto log system (default reporter). @@ -37,6 +37,6 @@ class LogMetricsReporter : public IMetricsReporter void reportStateTransition(const StateTransitionReport &report) override; void reportThresholdExceeded(const ThresholdAlert &alert) override; }; -} // namespace firebolt::rialto::server::ipc +} // namespace firebolt::rialto::server -#endif // FIREBOLT_RIALTO_SERVER_IPC_LOG_METRICS_REPORTER_H_ +#endif // FIREBOLT_RIALTO_SERVER_LOG_METRICS_REPORTER_H_ diff --git a/media/server/ipc/include/MetricsAccumulator.h b/media/server/main/include/MetricsAccumulator.h similarity index 90% rename from media/server/ipc/include/MetricsAccumulator.h rename to media/server/main/include/MetricsAccumulator.h index be54d3ca4..7a53243c9 100644 --- a/media/server/ipc/include/MetricsAccumulator.h +++ b/media/server/main/include/MetricsAccumulator.h @@ -17,14 +17,14 @@ * limitations under the License. */ -#ifndef FIREBOLT_RIALTO_SERVER_IPC_METRICS_ACCUMULATOR_H_ -#define FIREBOLT_RIALTO_SERVER_IPC_METRICS_ACCUMULATOR_H_ +#ifndef FIREBOLT_RIALTO_SERVER_METRICS_ACCUMULATOR_H_ +#define FIREBOLT_RIALTO_SERVER_METRICS_ACCUMULATOR_H_ #include #include #include -namespace firebolt::rialto::server::ipc +namespace firebolt::rialto::server { struct MetricsStatistics { @@ -97,6 +97,6 @@ class MetricsAccumulator double m_mean{0.0}; double m_m2{0.0}; }; -} // namespace firebolt::rialto::server::ipc +} // namespace firebolt::rialto::server -#endif // FIREBOLT_RIALTO_SERVER_IPC_METRICS_ACCUMULATOR_H_ +#endif // FIREBOLT_RIALTO_SERVER_METRICS_ACCUMULATOR_H_ diff --git a/media/server/main/include/MetricsCollector.h b/media/server/main/include/MetricsCollector.h new file mode 100644 index 000000000..ffcb77a4a --- /dev/null +++ b/media/server/main/include/MetricsCollector.h @@ -0,0 +1,114 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_METRICS_COLLECTOR_H_ +#define FIREBOLT_RIALTO_SERVER_METRICS_COLLECTOR_H_ + +#include "IMetricsCollector.h" +#include "IMetricsReporter.h" +#include "ITimer.h" +#include "MetricsThresholdChecker.h" +#include "StateMetricsAggregator.h" +#include +#include +#include +#include +#include +#include + +namespace firebolt::rialto::server +{ +class MetricsCollectorFactory : public IMetricsCollectorFactory +{ +public: + MetricsCollectorFactory() = default; + ~MetricsCollectorFactory() override = default; + + std::unique_ptr create(int clientId, + const std::shared_ptr &client) override; +}; + +class MetricsCollector : public IMetricsCollector +{ +public: + MetricsCollector(int clientId, const std::shared_ptr &client, + const std::shared_ptr &timerFactory); + ~MetricsCollector() override; + + void processMetrics(const ClientMetricsData &metrics) override; + void notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) override; + void notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) override; + +private: + struct ProcessMetricsSample + { + std::uint64_t monotonicTimeMs{0}; + std::uint64_t epochTimeMs{0}; + std::uint64_t processCpuTimeMs{0}; + std::uint64_t processMemoryKb{0}; + std::uint64_t cgroupMemoryUsageKb{0}; + std::uint64_t cgroupMemoryLimitKb{0}; + }; + + struct PreviousSample + { + std::uint64_t clientMonotonicTimeMs{0}; + std::uint64_t clientCpuTimeMs{0}; + std::uint64_t clientMemoryKb{0}; + ProcessMetricsSample serverMetrics; + }; + + struct SessionMetricsState + { + PlaybackState currentPlaybackState{PlaybackState::UNKNOWN}; + StateMetricsAggregator aggregator; + }; + + void onTimerFired(); + ProcessMetricsSample getServerMetrics() const; + double calculateCpuPercentage(std::uint64_t currentCpuTimeMs, std::uint64_t previousCpuTimeMs, + std::uint64_t currentMonotonicTimeMs, std::uint64_t previousMonotonicTimeMs) const; + static const char *sampleReasonToString(MetricsSampleReason reason); + static const char *playbackStateToString(PlaybackState state); + static const char *applicationStateToString(ApplicationState state); + + const int m_clientId; + std::shared_ptr m_client; + std::unique_ptr m_timer; + std::uint64_t m_nextSampleId{1}; + + std::mutex m_mutex; + std::optional m_previousSample; + + // Per-session state tracking (sessionId -> session state) + std::map m_sessionStates; + + // Global aggregator (active across all sessions while RUNNING) + StateMetricsAggregator m_globalAggregator; + ApplicationState m_currentApplicationState{ApplicationState::UNKNOWN}; + + // Pluggable metrics reporter (log, telemetry, or composite) + std::unique_ptr m_reporter; + + // Threshold checker + MetricsThresholdChecker m_thresholdChecker; +}; +} // namespace firebolt::rialto::server + +#endif // FIREBOLT_RIALTO_SERVER_METRICS_COLLECTOR_H_ diff --git a/media/server/ipc/include/MetricsThresholdChecker.h b/media/server/main/include/MetricsThresholdChecker.h similarity index 90% rename from media/server/ipc/include/MetricsThresholdChecker.h rename to media/server/main/include/MetricsThresholdChecker.h index b909b1c6f..af4e0c575 100644 --- a/media/server/ipc/include/MetricsThresholdChecker.h +++ b/media/server/main/include/MetricsThresholdChecker.h @@ -17,15 +17,15 @@ * limitations under the License. */ -#ifndef FIREBOLT_RIALTO_SERVER_IPC_METRICS_THRESHOLD_CHECKER_H_ -#define FIREBOLT_RIALTO_SERVER_IPC_METRICS_THRESHOLD_CHECKER_H_ +#ifndef FIREBOLT_RIALTO_SERVER_METRICS_THRESHOLD_CHECKER_H_ +#define FIREBOLT_RIALTO_SERVER_METRICS_THRESHOLD_CHECKER_H_ #include "IMetricsReporter.h" #include #include #include -namespace firebolt::rialto::server::ipc +namespace firebolt::rialto::server { /** * @brief Configuration for a single metric threshold. @@ -88,6 +88,6 @@ class MetricsThresholdChecker ThresholdState m_serverMemState; ThresholdState m_cgroupMemState; }; -} // namespace firebolt::rialto::server::ipc +} // namespace firebolt::rialto::server -#endif // FIREBOLT_RIALTO_SERVER_IPC_METRICS_THRESHOLD_CHECKER_H_ +#endif // FIREBOLT_RIALTO_SERVER_METRICS_THRESHOLD_CHECKER_H_ diff --git a/media/server/ipc/include/StateMetricsAggregator.h b/media/server/main/include/StateMetricsAggregator.h similarity index 93% rename from media/server/ipc/include/StateMetricsAggregator.h rename to media/server/main/include/StateMetricsAggregator.h index cffb79007..adf2279c0 100644 --- a/media/server/ipc/include/StateMetricsAggregator.h +++ b/media/server/main/include/StateMetricsAggregator.h @@ -17,14 +17,14 @@ * limitations under the License. */ -#ifndef FIREBOLT_RIALTO_SERVER_IPC_STATE_METRICS_AGGREGATOR_H_ -#define FIREBOLT_RIALTO_SERVER_IPC_STATE_METRICS_AGGREGATOR_H_ +#ifndef FIREBOLT_RIALTO_SERVER_STATE_METRICS_AGGREGATOR_H_ +#define FIREBOLT_RIALTO_SERVER_STATE_METRICS_AGGREGATOR_H_ #include "MetricsAccumulator.h" #include #include -namespace firebolt::rialto::server::ipc +namespace firebolt::rialto::server { /** * @brief A single metrics sample to be fed into the aggregator. @@ -126,6 +126,6 @@ class StateMetricsAggregator MetricsAccumulator m_cgroupUsage; MetricsAccumulator m_cgroupLimit; }; -} // namespace firebolt::rialto::server::ipc +} // namespace firebolt::rialto::server -#endif // FIREBOLT_RIALTO_SERVER_IPC_STATE_METRICS_AGGREGATOR_H_ +#endif // FIREBOLT_RIALTO_SERVER_STATE_METRICS_AGGREGATOR_H_ diff --git a/media/server/main/interface/IMetricsCollector.h b/media/server/main/interface/IMetricsCollector.h new file mode 100644 index 000000000..60ab1b3ed --- /dev/null +++ b/media/server/main/interface/IMetricsCollector.h @@ -0,0 +1,114 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_I_METRICS_COLLECTOR_H_ +#define FIREBOLT_RIALTO_SERVER_I_METRICS_COLLECTOR_H_ + +#include "ControlCommon.h" +#include "IMetricsCollectorClient.h" +#include "MediaCommon.h" +#include +#include +#include + +namespace firebolt::rialto::server +{ +/** + * @brief Client-reported metrics data (mirrors proto ClientProcessMetrics). + */ +struct ClientMetricsData +{ + std::uint64_t sampleId{0}; + MetricsSampleReason reason{MetricsSampleReason::UNKNOWN}; + std::string appName; + std::uint32_t processId{0}; + std::uint64_t monotonicTimeMs{0}; + std::uint64_t epochTimeMs{0}; + std::uint64_t processCpuTimeMs{0}; + std::uint64_t processMemoryKb{0}; +}; + +class IMetricsCollector; + +/** + * @brief Factory for creating MetricsCollector instances. + */ +class IMetricsCollectorFactory +{ +public: + IMetricsCollectorFactory() = default; + virtual ~IMetricsCollectorFactory() = default; + + static std::shared_ptr createFactory(); + + /** + * @brief Create a new MetricsCollector for a connected client. + * + * @param clientId Unique client identifier. + * @param client Callback interface for requesting samples from the client. + * + * @return The new MetricsCollector instance, or nullptr on failure. + */ + virtual std::unique_ptr create(int clientId, + const std::shared_ptr &client) = 0; +}; + +/** + * @brief Collects, aggregates, and reports metrics for a single connected client. + * + * Each instance owns an ITimer (periodic) that drives sampling, and holds + * the aggregation and threshold-checking framework classes. + */ +class IMetricsCollector +{ +public: + IMetricsCollector() = default; + virtual ~IMetricsCollector() = default; + + IMetricsCollector(const IMetricsCollector &) = delete; + IMetricsCollector(IMetricsCollector &&) = delete; + IMetricsCollector &operator=(const IMetricsCollector &) = delete; + IMetricsCollector &operator=(IMetricsCollector &&) = delete; + + /** + * @brief Process a metrics report received from the client. + * + * Computes CPU percentages from deltas, feeds aggregators, checks thresholds. + * + * @param metrics The client-reported metrics data. + */ + virtual void processMetrics(const ClientMetricsData &metrics) = 0; + + /** + * @brief Notify that a media pipeline's playback state has changed. + * + * Finalizes the old state's aggregator and begins a new one. + */ + virtual void notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) = 0; + + /** + * @brief Notify that the application state has changed (RUNNING/INACTIVE). + * + * Finalizes the RUNNING aggregator on transition to INACTIVE. + */ + virtual void notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) = 0; +}; +} // namespace firebolt::rialto::server + +#endif // FIREBOLT_RIALTO_SERVER_I_METRICS_COLLECTOR_H_ diff --git a/media/server/main/interface/IMetricsCollectorClient.h b/media/server/main/interface/IMetricsCollectorClient.h new file mode 100644 index 000000000..5754b03c0 --- /dev/null +++ b/media/server/main/interface/IMetricsCollectorClient.h @@ -0,0 +1,64 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_I_METRICS_COLLECTOR_CLIENT_H_ +#define FIREBOLT_RIALTO_SERVER_I_METRICS_COLLECTOR_CLIENT_H_ + +#include + +namespace firebolt::rialto::server +{ +/** + * @brief Reason for a metrics sample request, mirroring the proto enum. + */ +enum class MetricsSampleReason +{ + UNKNOWN, + CONNECTED, + PERIODIC, + STATE_TRANSITION +}; + +/** + * @brief Callback interface used by MetricsCollector (server/main) to send + * sample requests back through the IPC layer to the client. + */ +class IMetricsCollectorClient +{ +public: + IMetricsCollectorClient() = default; + virtual ~IMetricsCollectorClient() = default; + + IMetricsCollectorClient(const IMetricsCollectorClient &) = delete; + IMetricsCollectorClient(IMetricsCollectorClient &&) = delete; + IMetricsCollectorClient &operator=(const IMetricsCollectorClient &) = delete; + IMetricsCollectorClient &operator=(IMetricsCollectorClient &&) = delete; + + /** + * @brief Request that the client send a metrics sample. + * + * @param clientId The client to request from. + * @param sampleId Unique sample identifier for correlation. + * @param reason Why the sample is being requested. + */ + virtual void requestMetricsSample(int clientId, std::uint64_t sampleId, MetricsSampleReason reason) = 0; +}; +} // namespace firebolt::rialto::server + +#endif // FIREBOLT_RIALTO_SERVER_I_METRICS_COLLECTOR_CLIENT_H_ diff --git a/media/server/ipc/source/CompositeMetricsReporter.cpp b/media/server/main/source/CompositeMetricsReporter.cpp similarity index 94% rename from media/server/ipc/source/CompositeMetricsReporter.cpp rename to media/server/main/source/CompositeMetricsReporter.cpp index a8e22d5ee..20181707b 100644 --- a/media/server/ipc/source/CompositeMetricsReporter.cpp +++ b/media/server/main/source/CompositeMetricsReporter.cpp @@ -19,7 +19,7 @@ #include "CompositeMetricsReporter.h" -namespace firebolt::rialto::server::ipc +namespace firebolt::rialto::server { void CompositeMetricsReporter::addReporter(std::unique_ptr reporter) { @@ -52,4 +52,4 @@ void CompositeMetricsReporter::reportThresholdExceeded(const ThresholdAlert &ale reporter->reportThresholdExceeded(alert); } } -} // namespace firebolt::rialto::server::ipc +} // namespace firebolt::rialto::server diff --git a/media/server/ipc/source/LogMetricsReporter.cpp b/media/server/main/source/LogMetricsReporter.cpp similarity index 62% rename from media/server/ipc/source/LogMetricsReporter.cpp rename to media/server/main/source/LogMetricsReporter.cpp index f5dad20b1..60d6c687f 100644 --- a/media/server/ipc/source/LogMetricsReporter.cpp +++ b/media/server/main/source/LogMetricsReporter.cpp @@ -21,22 +21,18 @@ #include "RialtoServerLogging.h" #include -namespace firebolt::rialto::server::ipc +namespace firebolt::rialto::server { void LogMetricsReporter::reportPeriodicSample(const PeriodicMetricsReport &report) { - // Don't report the periodic samples as milestone logs, - // as they are expected to be emitted frequently and - // may not indicate a significant event on their own. - // Instead, log them at INFO level. RIALTO_SERVER_LOG_INFO("Metrics sample=%" PRIu64 ", reason=%s, app='%s', client_pid=%u, client_cpu=%.2f%%, " - "server_cpu=%.2f%%, combined_cpu=%.2f%%, client_cpu_ms=%" PRIu64 ", " - "server_cpu_ms=%" PRIu64 ", client_mem_kb=%" PRIu64 ", server_mem_kb=%" PRIu64 ", " - "cgroup_mem_kb=%" PRIu64 "/%" PRIu64, - report.sampleId, report.reason.c_str(), report.appName.c_str(), report.clientPid, - report.clientCpuPercent, report.serverCpuPercent, report.combinedCpuPercent, - report.clientCpuTimeMs, report.serverCpuTimeMs, report.clientMemoryKb, - report.serverMemoryKb, report.cgroupMemoryUsageKb, report.cgroupMemoryLimitKb); + "server_cpu=%.2f%%, combined_cpu=%.2f%%, client_cpu_ms=%" PRIu64 ", " + "server_cpu_ms=%" PRIu64 ", client_mem_kb=%" PRIu64 ", server_mem_kb=%" PRIu64 ", " + "cgroup_mem_kb=%" PRIu64 "/%" PRIu64, + report.sampleId, report.reason.c_str(), report.appName.c_str(), report.clientPid, + report.clientCpuPercent, report.serverCpuPercent, report.combinedCpuPercent, + report.clientCpuTimeMs, report.serverCpuTimeMs, report.clientMemoryKb, + report.serverMemoryKb, report.cgroupMemoryUsageKb, report.cgroupMemoryLimitKb); } void LogMetricsReporter::reportStateTransition(const StateTransitionReport &report) @@ -50,11 +46,10 @@ void LogMetricsReporter::reportStateTransition(const StateTransitionReport &repo "server_mem_kb={min=%.0f, max=%.0f, mean=%.0f}, " "cgroup_mem_kb={min=%.0f, max=%.0f, mean=%.0f}", report.context.c_str(), r.stateName.c_str(), r.durationMs, r.clientCpu.count, - r.clientCpu.min, r.clientCpu.max, r.clientCpu.mean, r.clientCpu.stddev, - r.serverCpu.min, r.serverCpu.max, r.serverCpu.mean, r.serverCpu.stddev, - r.combinedCpu.min, r.combinedCpu.max, r.combinedCpu.mean, r.combinedCpu.stddev, - r.clientMemoryKb.min, r.clientMemoryKb.max, r.clientMemoryKb.mean, - r.serverMemoryKb.min, r.serverMemoryKb.max, r.serverMemoryKb.mean, + r.clientCpu.min, r.clientCpu.max, r.clientCpu.mean, r.clientCpu.stddev, r.serverCpu.min, + r.serverCpu.max, r.serverCpu.mean, r.serverCpu.stddev, r.combinedCpu.min, r.combinedCpu.max, + r.combinedCpu.mean, r.combinedCpu.stddev, r.clientMemoryKb.min, r.clientMemoryKb.max, + r.clientMemoryKb.mean, r.serverMemoryKb.min, r.serverMemoryKb.max, r.serverMemoryKb.mean, r.cgroupMemoryUsageKb.min, r.cgroupMemoryUsageKb.max, r.cgroupMemoryUsageKb.mean); } @@ -64,4 +59,4 @@ void LogMetricsReporter::reportThresholdExceeded(const ThresholdAlert &alert) RIALTO_SERVER_LOG_WARN("Metrics threshold %s: %s=%.2f exceeds %.2f", severity, alert.metricName.c_str(), alert.currentValue, alert.thresholdValue); } -} // namespace firebolt::rialto::server::ipc +} // namespace firebolt::rialto::server diff --git a/media/server/main/source/MetricsCollector.cpp b/media/server/main/source/MetricsCollector.cpp new file mode 100644 index 000000000..b0faaf10a --- /dev/null +++ b/media/server/main/source/MetricsCollector.cpp @@ -0,0 +1,479 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MetricsCollector.h" +#include "LogMetricsReporter.h" +#include "RialtoServerLogging.h" +#include +#include +#include +#include +#include +#include + +namespace +{ +constexpr std::chrono::seconds kMetricsInterval{15}; +constexpr std::uint64_t kMinElapsedMs{100}; +} // namespace + +namespace firebolt::rialto::server +{ +std::shared_ptr IMetricsCollectorFactory::createFactory() +{ + std::shared_ptr factory; + try + { + factory = std::make_shared(); + } + catch (const std::exception &e) + { + RIALTO_SERVER_LOG_ERROR("Failed to create MetricsCollectorFactory, reason: %s", e.what()); + } + return factory; +} + +std::unique_ptr +MetricsCollectorFactory::create(int clientId, const std::shared_ptr &client) +{ + std::unique_ptr collector; + try + { + auto timerFactory = firebolt::rialto::common::ITimerFactory::getFactory(); + collector = std::make_unique(clientId, client, timerFactory); + } + catch (const std::exception &e) + { + RIALTO_SERVER_LOG_ERROR("Failed to create MetricsCollector for client %d, reason: %s", clientId, e.what()); + } + return collector; +} + +MetricsCollector::MetricsCollector(int clientId, const std::shared_ptr &client, + const std::shared_ptr &timerFactory) + : m_clientId{clientId}, m_client{client}, m_reporter{std::make_unique()}, + m_thresholdChecker{MetricsThresholdConfig{}, m_reporter.get()} +{ + m_timer = timerFactory->createTimer(kMetricsInterval, [this]() { onTimerFired(); }, + firebolt::rialto::common::TimerType::PERIODIC); + + // Request initial baseline sample + m_client->requestMetricsSample(m_clientId, m_nextSampleId++, MetricsSampleReason::CONNECTED); +} + +MetricsCollector::~MetricsCollector() +{ + if (m_timer) + { + m_timer->cancel(); + } +} + +void MetricsCollector::onTimerFired() +{ + m_client->requestMetricsSample(m_clientId, m_nextSampleId++, MetricsSampleReason::PERIODIC); +} + +void MetricsCollector::processMetrics(const ClientMetricsData &metrics) +{ + const auto kServerMetrics{getServerMetrics()}; + + std::optional previous; + { + std::lock_guard lock{m_mutex}; + previous = m_previousSample; + } + + if (!previous.has_value()) + { + // Baseline sample — store and return + RIALTO_SERVER_LOG_MIL("Metrics baseline: sample=%" PRIu64 ", reason=%s, app='%s', client_pid=%u, " + "client_cpu_ms=%" PRIu64 ", server_cpu_ms=%" PRIu64 ", " + "client_mem_kb=%" PRIu64 ", server_mem_kb=%" PRIu64 ", " + "cgroup_mem_kb=%" PRIu64 "/%" PRIu64, + metrics.sampleId, sampleReasonToString(metrics.reason), metrics.appName.c_str(), + metrics.processId, metrics.processCpuTimeMs, kServerMetrics.processCpuTimeMs, + metrics.processMemoryKb, kServerMetrics.processMemoryKb, + kServerMetrics.cgroupMemoryUsageKb, kServerMetrics.cgroupMemoryLimitKb); + + std::lock_guard lock{m_mutex}; + m_previousSample = PreviousSample{metrics.monotonicTimeMs, metrics.processCpuTimeMs, metrics.processMemoryKb, + kServerMetrics}; + return; + } + + const auto &prev{previous.value()}; + const double kClientCpuPercentage{ + calculateCpuPercentage(metrics.processCpuTimeMs, prev.clientCpuTimeMs, metrics.monotonicTimeMs, + prev.clientMonotonicTimeMs)}; + const double kServerCpuPercentage{calculateCpuPercentage(kServerMetrics.processCpuTimeMs, + prev.serverMetrics.processCpuTimeMs, + kServerMetrics.monotonicTimeMs, + prev.serverMetrics.monotonicTimeMs)}; + const double kCombinedCpuPercentage{ + calculateCpuPercentage(metrics.processCpuTimeMs + kServerMetrics.processCpuTimeMs, + prev.clientCpuTimeMs + prev.serverMetrics.processCpuTimeMs, kServerMetrics.monotonicTimeMs, + prev.serverMetrics.monotonicTimeMs)}; + + // Report via pluggable reporter + if (m_reporter) + { + PeriodicMetricsReport periodicReport; + periodicReport.sampleId = metrics.sampleId; + periodicReport.reason = sampleReasonToString(metrics.reason); + periodicReport.appName = metrics.appName; + periodicReport.clientPid = metrics.processId; + periodicReport.clientCpuPercent = kClientCpuPercentage; + periodicReport.serverCpuPercent = kServerCpuPercentage; + periodicReport.combinedCpuPercent = kCombinedCpuPercentage; + periodicReport.clientCpuTimeMs = metrics.processCpuTimeMs; + periodicReport.serverCpuTimeMs = kServerMetrics.processCpuTimeMs; + periodicReport.clientMemoryKb = metrics.processMemoryKb; + periodicReport.serverMemoryKb = kServerMetrics.processMemoryKb; + periodicReport.cgroupMemoryUsageKb = kServerMetrics.cgroupMemoryUsageKb; + periodicReport.cgroupMemoryLimitKb = kServerMetrics.cgroupMemoryLimitKb; + m_reporter->reportPeriodicSample(periodicReport); + } + + // Only feed PERIODIC samples into aggregators — STATE_TRANSITION samples have + // unreliable CPU percentages due to tiny time deltas between rapid samples. + if (metrics.reason == MetricsSampleReason::PERIODIC) + { + MetricsSample sample; + sample.clientCpuPercent = kClientCpuPercentage; + sample.serverCpuPercent = kServerCpuPercentage; + sample.combinedCpuPercent = kCombinedCpuPercentage; + sample.clientMemoryKb = metrics.processMemoryKb; + sample.serverMemoryKb = kServerMetrics.processMemoryKb; + sample.cgroupMemoryUsageKb = kServerMetrics.cgroupMemoryUsageKb; + sample.cgroupMemoryLimitKb = kServerMetrics.cgroupMemoryLimitKb; + + { + std::lock_guard lock{m_mutex}; + + // Feed into per-session aggregators + for (auto &[sessionId, sessionState] : m_sessionStates) + { + sessionState.aggregator.addSample(sample); + } + + // Feed into global aggregator + if (m_currentApplicationState == ApplicationState::RUNNING) + { + m_globalAggregator.addSample(sample); + } + } + + // Check thresholds + m_thresholdChecker.checkSample(kClientCpuPercentage, kServerCpuPercentage, kCombinedCpuPercentage, + metrics.processMemoryKb, kServerMetrics.processMemoryKb, + kServerMetrics.cgroupMemoryUsageKb, kServerMetrics.cgroupMemoryLimitKb); + } + + // Update previous sample + { + std::lock_guard lock{m_mutex}; + m_previousSample = + PreviousSample{metrics.monotonicTimeMs, metrics.processCpuTimeMs, metrics.processMemoryKb, kServerMetrics}; + } +} + +void MetricsCollector::notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) +{ + RIALTO_SERVER_LOG_MIL("Metrics: PlaybackState changed session=%d, %s -> %s", sessionId, + playbackStateToString(oldState), playbackStateToString(newState)); + + using std::chrono::duration_cast; + using std::chrono::milliseconds; + using std::chrono::steady_clock; + const auto kNowMs{ + static_cast(duration_cast(steady_clock::now().time_since_epoch()).count())}; + + std::lock_guard lock{m_mutex}; + auto sessionIter{m_sessionStates.find(sessionId)}; + if (m_sessionStates.end() == sessionIter) + { + // First state notification for this session — create entry + SessionMetricsState sessionState; + sessionState.currentPlaybackState = newState; + sessionState.aggregator.begin(playbackStateToString(newState), kNowMs); + m_sessionStates.emplace(sessionId, std::move(sessionState)); + return; + } + + auto &sessionState{sessionIter->second}; + + // Finalize old state and emit report + if (sessionState.aggregator.hasData() && m_reporter) + { + auto report{sessionState.aggregator.finalize(kNowMs)}; + StateTransitionReport transitionReport; + transitionReport.context = "session=" + std::to_string(sessionId); + transitionReport.metrics = report; + m_reporter->reportStateTransition(transitionReport); + } + + if (newState == PlaybackState::STOPPED || newState == PlaybackState::END_OF_STREAM || + newState == PlaybackState::FAILURE) + { + // Terminal state — remove session tracking + m_sessionStates.erase(sessionIter); + } + else + { + // Begin accumulating for new state + sessionState.currentPlaybackState = newState; + sessionState.aggregator.begin(playbackStateToString(newState), kNowMs); + } + + // Request immediate sample for clean boundary + m_client->requestMetricsSample(m_clientId, m_nextSampleId++, MetricsSampleReason::STATE_TRANSITION); +} + +void MetricsCollector::notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) +{ + RIALTO_SERVER_LOG_MIL("Metrics: ApplicationState changed %s -> %s", applicationStateToString(oldState), + applicationStateToString(newState)); + + using std::chrono::duration_cast; + using std::chrono::milliseconds; + using std::chrono::steady_clock; + const auto kNowMs{ + static_cast(duration_cast(steady_clock::now().time_since_epoch()).count())}; + + std::lock_guard lock{m_mutex}; + m_currentApplicationState = newState; + + if (oldState == ApplicationState::RUNNING && newState != ApplicationState::RUNNING) + { + // Leaving RUNNING — finalize global aggregator + if (m_globalAggregator.hasData() && m_reporter) + { + auto report{m_globalAggregator.finalize(kNowMs)}; + StateTransitionReport transitionReport; + transitionReport.context = "global"; + transitionReport.metrics = report; + m_reporter->reportStateTransition(transitionReport); + } + m_globalAggregator.reset(); + } + + if (newState == ApplicationState::RUNNING && oldState != ApplicationState::RUNNING) + { + // Entering RUNNING — start fresh global accumulation + m_globalAggregator.begin(applicationStateToString(newState), kNowMs); + } + + // Request immediate sample for clean boundary + m_client->requestMetricsSample(m_clientId, m_nextSampleId++, MetricsSampleReason::STATE_TRANSITION); +} + +MetricsCollector::ProcessMetricsSample MetricsCollector::getServerMetrics() const +{ + using std::chrono::duration_cast; + using std::chrono::milliseconds; + using std::chrono::steady_clock; + using std::chrono::system_clock; + + struct tms processTimes + { + }; + const clock_t kCurrentTicks{times(&processTimes)}; + const long kTicksPerSecond{sysconf(_SC_CLK_TCK)}; + std::uint64_t processCpuTimeMs{0}; + if ((static_cast(-1) != kCurrentTicks) && (kTicksPerSecond > 0)) + { + const auto kProcessTicks{processTimes.tms_utime + processTimes.tms_stime}; + processCpuTimeMs = static_cast((static_cast(kProcessTicks) * 1000.0) / + static_cast(kTicksPerSecond)); + } + else + { + RIALTO_SERVER_LOG_WARN("Failed to sample server process CPU usage"); + } + + std::uint64_t processMemoryKb{0}; + { + std::ifstream status{"/proc/self/status"}; + std::string line; + while (std::getline(status, line)) + { + if (line.rfind("VmRSS:", 0) == 0) + { + if (std::sscanf(line.c_str(), "VmRSS: %" SCNu64, &processMemoryKb) != 1) + { + RIALTO_SERVER_LOG_WARN("Failed to parse server process memory usage"); + } + break; + } + } + } + + std::uint64_t cgroupMemoryUsageKb{0}; + std::uint64_t cgroupMemoryLimitKb{0}; + { + auto readFileValue = [](const std::string &path) -> std::uint64_t + { + std::ifstream file{path}; + if (!file.is_open()) + { + return 0; + } + std::string content; + if (!std::getline(file, content) || content.empty() || content == "max") + { + return 0; + } + std::uint64_t value{0}; + if (std::sscanf(content.c_str(), "%" SCNu64, &value) == 1) + { + return value; + } + return 0; + }; + + // Resolve the process's cgroup path from /proc/self/cgroup + // cgroup v2 format: "0::" + auto getCgroupBasePath = []() -> std::string + { + std::ifstream cgroupFile{"/proc/self/cgroup"}; + if (!cgroupFile.is_open()) + { + return {}; + } + std::string line; + while (std::getline(cgroupFile, line)) + { + // cgroup v2 line starts with "0::" + if (line.rfind("0::", 0) == 0) + { + std::string relativePath{line.substr(3)}; + if (!relativePath.empty() && relativePath != "/") + { + return "/sys/fs/cgroup" + relativePath; + } + return "/sys/fs/cgroup"; + } + } + return {}; + }; + + std::uint64_t usageBytes{0}; + std::uint64_t limitBytes{0}; + + // cgroup v2: read from process's own cgroup path + std::string cgroupBase{getCgroupBasePath()}; + if (!cgroupBase.empty()) + { + usageBytes = readFileValue(cgroupBase + "/memory.current"); + limitBytes = readFileValue(cgroupBase + "/memory.max"); + } + + if (usageBytes == 0) + { + // cgroup v1 fallback + usageBytes = readFileValue("/sys/fs/cgroup/memory/memory.usage_in_bytes"); + limitBytes = readFileValue("/sys/fs/cgroup/memory/memory.limit_in_bytes"); + } + + cgroupMemoryUsageKb = usageBytes / 1024; + cgroupMemoryLimitKb = limitBytes / 1024; + } + + return ProcessMetricsSample{ + static_cast(duration_cast(steady_clock::now().time_since_epoch()).count()), + static_cast(duration_cast(system_clock::now().time_since_epoch()).count()), + processCpuTimeMs, processMemoryKb, cgroupMemoryUsageKb, cgroupMemoryLimitKb}; +} + +double MetricsCollector::calculateCpuPercentage(std::uint64_t currentCpuTimeMs, std::uint64_t previousCpuTimeMs, + std::uint64_t currentMonotonicTimeMs, + std::uint64_t previousMonotonicTimeMs) const +{ + if ((currentCpuTimeMs < previousCpuTimeMs) || (currentMonotonicTimeMs <= previousMonotonicTimeMs)) + { + return 0.0; + } + + const auto kElapsedMs{currentMonotonicTimeMs - previousMonotonicTimeMs}; + if (kElapsedMs < kMinElapsedMs) + { + // Time delta too small for meaningful CPU percentage + return 0.0; + } + + return (static_cast(currentCpuTimeMs - previousCpuTimeMs) / static_cast(kElapsedMs)) * 100.0; +} + +const char *MetricsCollector::sampleReasonToString(MetricsSampleReason reason) +{ + switch (reason) + { + case MetricsSampleReason::CONNECTED: + return "CONNECTED"; + case MetricsSampleReason::PERIODIC: + return "PERIODIC"; + case MetricsSampleReason::STATE_TRANSITION: + return "STATE_TRANSITION"; + case MetricsSampleReason::UNKNOWN: + default: + return "UNKNOWN"; + } +} + +const char *MetricsCollector::playbackStateToString(PlaybackState state) +{ + switch (state) + { + case PlaybackState::IDLE: + return "IDLE"; + case PlaybackState::PLAYING: + return "PLAYING"; + case PlaybackState::PAUSED: + return "PAUSED"; + case PlaybackState::SEEKING: + return "SEEKING"; + case PlaybackState::SEEK_DONE: + return "SEEK_DONE"; + case PlaybackState::STOPPED: + return "STOPPED"; + case PlaybackState::END_OF_STREAM: + return "END_OF_STREAM"; + case PlaybackState::FAILURE: + return "FAILURE"; + case PlaybackState::UNKNOWN: + default: + return "UNKNOWN"; + } +} + +const char *MetricsCollector::applicationStateToString(ApplicationState state) +{ + switch (state) + { + case ApplicationState::RUNNING: + return "RUNNING"; + case ApplicationState::INACTIVE: + return "INACTIVE"; + case ApplicationState::UNKNOWN: + default: + return "UNKNOWN"; + } +} +} // namespace firebolt::rialto::server diff --git a/media/server/ipc/source/MetricsThresholdChecker.cpp b/media/server/main/source/MetricsThresholdChecker.cpp similarity index 97% rename from media/server/ipc/source/MetricsThresholdChecker.cpp rename to media/server/main/source/MetricsThresholdChecker.cpp index 892bd9c31..bdfc237f5 100644 --- a/media/server/ipc/source/MetricsThresholdChecker.cpp +++ b/media/server/main/source/MetricsThresholdChecker.cpp @@ -19,7 +19,7 @@ #include "MetricsThresholdChecker.h" -namespace firebolt::rialto::server::ipc +namespace firebolt::rialto::server { MetricsThresholdChecker::MetricsThresholdChecker(MetricsThresholdConfig config, IMetricsReporter *reporter) : m_config{std::move(config)}, m_reporter{reporter} @@ -99,4 +99,4 @@ void MetricsThresholdChecker::checkMetric(const MetricsThreshold &threshold, dou } } } -} // namespace firebolt::rialto::server::ipc +} // namespace firebolt::rialto::server diff --git a/media/server/service/CMakeLists.txt b/media/server/service/CMakeLists.txt index 0ec9d559d..8969c8172 100644 --- a/media/server/service/CMakeLists.txt +++ b/media/server/service/CMakeLists.txt @@ -36,6 +36,7 @@ add_library ( source/SessionServerManager.cpp source/MediaPipelineService.cpp source/WebAudioPlayerService.cpp + source/PrivateMetricsService.cpp ) set_target_properties ( RialtoServerService diff --git a/media/server/service/include/IPrivateMetricsService.h b/media/server/service/include/IPrivateMetricsService.h new file mode 100644 index 000000000..fdefe6ea4 --- /dev/null +++ b/media/server/service/include/IPrivateMetricsService.h @@ -0,0 +1,87 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_SERVICE_I_PRIVATE_METRICS_SERVICE_H_ +#define FIREBOLT_RIALTO_SERVER_SERVICE_I_PRIVATE_METRICS_SERVICE_H_ + +#include "ControlCommon.h" +#include "IMetricsCollector.h" +#include "IMetricsCollectorClient.h" +#include "MediaCommon.h" +#include + +namespace firebolt::rialto::server::service +{ +class IPrivateMetricsService +{ +public: + IPrivateMetricsService() = default; + virtual ~IPrivateMetricsService() = default; + + IPrivateMetricsService(const IPrivateMetricsService &) = delete; + IPrivateMetricsService(IPrivateMetricsService &&) = delete; + IPrivateMetricsService &operator=(const IPrivateMetricsService &) = delete; + IPrivateMetricsService &operator=(IPrivateMetricsService &&) = delete; + + /** + * @brief A client has signalled readiness for metrics collection. + * + * Creates a MetricsCollector instance for this client. + * + * @param clientId Unique client identifier. + * @param client Callback interface for requesting samples from the client. + */ + virtual void clientReady(int clientId, const std::shared_ptr &client) = 0; + + /** + * @brief A client has disconnected. + * + * Destroys the MetricsCollector instance associated with this client. + * + * @param clientId The client that disconnected. + */ + virtual void clientDisconnected(int clientId) = 0; + + /** + * @brief Process metrics data received from a client. + * + * Routes the data to the appropriate MetricsCollector. + * + * @param clientId The reporting client. + * @param metrics The client-reported metrics data. + */ + virtual void reportMetrics(int clientId, const firebolt::rialto::server::ClientMetricsData &metrics) = 0; + + /** + * @brief Notify that a media pipeline's playback state has changed. + * + * Routes to all active MetricsCollector instances. + */ + virtual void notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) = 0; + + /** + * @brief Notify that the application state has changed (RUNNING/INACTIVE). + * + * Routes to all active MetricsCollector instances. + */ + virtual void notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) = 0; +}; +} // namespace firebolt::rialto::server::service + +#endif // FIREBOLT_RIALTO_SERVER_SERVICE_I_PRIVATE_METRICS_SERVICE_H_ diff --git a/media/server/service/source/PrivateMetricsService.cpp b/media/server/service/source/PrivateMetricsService.cpp new file mode 100644 index 000000000..a1ed8a92f --- /dev/null +++ b/media/server/service/source/PrivateMetricsService.cpp @@ -0,0 +1,95 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "PrivateMetricsService.h" +#include "RialtoServerLogging.h" + +namespace firebolt::rialto::server::service +{ +PrivateMetricsService::PrivateMetricsService( + std::shared_ptr collectorFactory) + : m_collectorFactory{std::move(collectorFactory)} +{ +} + +PrivateMetricsService::~PrivateMetricsService() +{ + std::lock_guard lock{m_mutex}; + m_collectors.clear(); +} + +void PrivateMetricsService::clientReady(int clientId, + const std::shared_ptr &client) +{ + std::lock_guard lock{m_mutex}; + auto collector = m_collectorFactory->create(clientId, client); + if (collector) + { + m_collectors.emplace(clientId, std::move(collector)); + RIALTO_SERVER_LOG_INFO("MetricsCollector created for client %d", clientId); + } + else + { + RIALTO_SERVER_LOG_ERROR("Failed to create MetricsCollector for client %d", clientId); + } +} + +void PrivateMetricsService::clientDisconnected(int clientId) +{ + std::lock_guard lock{m_mutex}; + auto iter = m_collectors.find(clientId); + if (iter != m_collectors.end()) + { + m_collectors.erase(iter); + RIALTO_SERVER_LOG_INFO("MetricsCollector destroyed for client %d", clientId); + } +} + +void PrivateMetricsService::reportMetrics(int clientId, const firebolt::rialto::server::ClientMetricsData &metrics) +{ + std::lock_guard lock{m_mutex}; + auto iter = m_collectors.find(clientId); + if (iter != m_collectors.end()) + { + iter->second->processMetrics(metrics); + } + else + { + RIALTO_SERVER_LOG_WARN("reportMetrics for unknown client %d", clientId); + } +} + +void PrivateMetricsService::notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) +{ + std::lock_guard lock{m_mutex}; + for (auto &[clientId, collector] : m_collectors) + { + collector->notifyPlaybackStateChanged(sessionId, oldState, newState); + } +} + +void PrivateMetricsService::notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) +{ + std::lock_guard lock{m_mutex}; + for (auto &[clientId, collector] : m_collectors) + { + collector->notifyApplicationStateChanged(oldState, newState); + } +} +} // namespace firebolt::rialto::server::service diff --git a/media/server/service/source/PrivateMetricsService.h b/media/server/service/source/PrivateMetricsService.h new file mode 100644 index 000000000..a8900c773 --- /dev/null +++ b/media/server/service/source/PrivateMetricsService.h @@ -0,0 +1,50 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_SERVICE_PRIVATE_METRICS_SERVICE_H_ +#define FIREBOLT_RIALTO_SERVER_SERVICE_PRIVATE_METRICS_SERVICE_H_ + +#include "IMetricsCollector.h" +#include "IPrivateMetricsService.h" +#include +#include +#include + +namespace firebolt::rialto::server::service +{ +class PrivateMetricsService : public IPrivateMetricsService +{ +public: + explicit PrivateMetricsService(std::shared_ptr collectorFactory); + ~PrivateMetricsService() override; + + void clientReady(int clientId, const std::shared_ptr &client) override; + void clientDisconnected(int clientId) override; + void reportMetrics(int clientId, const firebolt::rialto::server::ClientMetricsData &metrics) override; + void notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) override; + void notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) override; + +private: + std::shared_ptr m_collectorFactory; + std::mutex m_mutex; + std::map> m_collectors; +}; +} // namespace firebolt::rialto::server::service + +#endif // FIREBOLT_RIALTO_SERVER_SERVICE_PRIVATE_METRICS_SERVICE_H_ diff --git a/media/server/service/source/SessionServerManager.cpp b/media/server/service/source/SessionServerManager.cpp index 66bdbb40e..ceefe9f92 100644 --- a/media/server/service/source/SessionServerManager.cpp +++ b/media/server/service/source/SessionServerManager.cpp @@ -20,7 +20,9 @@ #include "SessionServerManager.h" #include "IApplicationManagementServer.h" #include "IIpcFactory.h" +#include "IMetricsCollector.h" #include "ISessionManagementServer.h" +#include "PrivateMetricsService.h" #include "RialtoServerLogging.h" #include @@ -42,8 +44,11 @@ SessionServerManager::SessionServerManager(const ipc::IIpcFactory &ipcFactory, I std::unique_ptr &&heartbeatProcedureFactory) : m_playbackService{playbackService}, m_cdmService{cdmService}, m_controlService{controlService}, m_heartbeatProcedureFactory{std::move(heartbeatProcedureFactory)}, + m_privateMetricsService{std::make_unique( + firebolt::rialto::server::IMetricsCollectorFactory::createFactory())}, m_applicationManagementServer{ipcFactory.createApplicationManagementServer(*this)}, - m_sessionManagementServer{ipcFactory.createSessionManagementServer(playbackService, cdmService, controlService)}, + m_sessionManagementServer{ + ipcFactory.createSessionManagementServer(playbackService, cdmService, controlService, *m_privateMetricsService)}, m_isServiceRunning{true}, m_currentState{common::SessionServerState::UNINITIALIZED} { RIALTO_SERVER_LOG_INFO("Starting Rialto Server Service"); diff --git a/media/server/service/source/SessionServerManager.h b/media/server/service/source/SessionServerManager.h index 29f28871a..3b5689202 100644 --- a/media/server/service/source/SessionServerManager.h +++ b/media/server/service/source/SessionServerManager.h @@ -26,6 +26,7 @@ #include "IHeartbeatProcedure.h" #include "IIpcFactory.h" #include "IPlaybackService.h" +#include "IPrivateMetricsService.h" #include "ISessionManagementServer.h" #include "ISessionServerManager.h" #include @@ -72,6 +73,7 @@ class SessionServerManager : public ISessionServerManager ICdmService &m_cdmService; IControlService &m_controlService; std::unique_ptr m_heartbeatProcedureFactory; + std::unique_ptr m_privateMetricsService; std::unique_ptr m_applicationManagementServer; std::unique_ptr m_sessionManagementServer; std::mutex m_serviceMutex; diff --git a/tests/unittests/media/server/mocks/ipc/IpcFactoryMock.h b/tests/unittests/media/server/mocks/ipc/IpcFactoryMock.h index 71ff39420..877e4d375 100644 --- a/tests/unittests/media/server/mocks/ipc/IpcFactoryMock.h +++ b/tests/unittests/media/server/mocks/ipc/IpcFactoryMock.h @@ -35,7 +35,7 @@ class IpcFactoryMock : public IIpcFactory (service::ISessionServerManager & sessionServerManager), (const, override)); MOCK_METHOD(std::unique_ptr, createSessionManagementServer, (service::IPlaybackService & playbackService, service::ICdmService &cdmService, - service::IControlService &controlService), + service::IControlService &controlService, service::IPrivateMetricsService &metricsService), (const, override)); }; } // namespace firebolt::rialto::server::ipc diff --git a/tests/unittests/media/server/service/sessionServerManager/SessionServerManagerTestsFixture.cpp b/tests/unittests/media/server/service/sessionServerManager/SessionServerManagerTestsFixture.cpp index 14b8e0342..53199dcb7 100644 --- a/tests/unittests/media/server/service/sessionServerManager/SessionServerManagerTestsFixture.cpp +++ b/tests/unittests/media/server/service/sessionServerManager/SessionServerManagerTestsFixture.cpp @@ -67,7 +67,7 @@ SessionServerManagerTests::SessionServerManagerTests() { EXPECT_CALL(m_ipcFactoryMock, createApplicationManagementServer(_)) .WillOnce(Return(ByMove(std::move(m_applicationManagementServer)))); - EXPECT_CALL(m_ipcFactoryMock, createSessionManagementServer(_, _, _)) + EXPECT_CALL(m_ipcFactoryMock, createSessionManagementServer(_, _, _, _)) .WillOnce(Return(ByMove(std::move(m_sessionManagementServer)))); m_sut = std::make_unique(m_ipcFactoryMock, m_playbackServiceMock, m_cdmServiceMock, m_controlServiceMock, std::move(m_heartbeatProcedureFactory)); From e2692b224ceef0e75307c98b859f95684610fa8d Mon Sep 17 00:00:00 2001 From: Douglas Adler Date: Thu, 2 Jul 2026 13:41:00 -0500 Subject: [PATCH 05/11] metrics: add INACTIVE memory snapshot and shm accounting On transition to INACTIVE, record a server-side memory snapshot after pipelines and shared memory have been freed. This fires unconditionally (before the manager ACK) so it is captured even if the IPC socket breaks. The snapshot reads /proc/self/smaps_rollup and logs: server_mem_kb - VmRSS total cgroup_mem_kb - cgroup memory usage anon_kb - anonymous (= private_dirty_kb) private_dirty_kb - truly committed RAM the OS cannot reclaim private_clean_kb - file-backed, OS-reclaimable shared_clean_kb - loaded .so libs, OS-reclaimable Also call malloc_trim(0) in PlaybackService::switchToInactive() to return heap fragmentation to the OS after pipelines are torn down. Add shm_mem_kb (Pss_Shmem from smaps_rollup) to each periodic sample so the memfd-backed shared transport buffer is visible during playback. Promote reportPeriodicSample log level from INFO to MIL so samples appear in production logs. --- media/server/main/include/IMetricsReporter.h | 1 + media/server/main/include/MetricsCollector.h | 1 + .../server/main/source/LogMetricsReporter.cpp | 16 +-- media/server/main/source/MetricsCollector.cpp | 19 ++- .../server/service/source/PlaybackService.cpp | 4 + .../service/source/PrivateMetricsService.cpp | 108 ++++++++++++++++++ .../service/source/SessionServerManager.cpp | 5 +- 7 files changed, 143 insertions(+), 11 deletions(-) diff --git a/media/server/main/include/IMetricsReporter.h b/media/server/main/include/IMetricsReporter.h index 00db04016..707c07005 100644 --- a/media/server/main/include/IMetricsReporter.h +++ b/media/server/main/include/IMetricsReporter.h @@ -45,6 +45,7 @@ struct PeriodicMetricsReport std::uint64_t serverMemoryKb{0}; std::uint64_t cgroupMemoryUsageKb{0}; std::uint64_t cgroupMemoryLimitKb{0}; + std::uint64_t shmMemoryKb{0}; }; /** diff --git a/media/server/main/include/MetricsCollector.h b/media/server/main/include/MetricsCollector.h index ffcb77a4a..563fa6d87 100644 --- a/media/server/main/include/MetricsCollector.h +++ b/media/server/main/include/MetricsCollector.h @@ -64,6 +64,7 @@ class MetricsCollector : public IMetricsCollector std::uint64_t processMemoryKb{0}; std::uint64_t cgroupMemoryUsageKb{0}; std::uint64_t cgroupMemoryLimitKb{0}; + std::uint64_t shmMemoryKb{0}; }; struct PreviousSample diff --git a/media/server/main/source/LogMetricsReporter.cpp b/media/server/main/source/LogMetricsReporter.cpp index 60d6c687f..6d9dbfbcb 100644 --- a/media/server/main/source/LogMetricsReporter.cpp +++ b/media/server/main/source/LogMetricsReporter.cpp @@ -25,14 +25,14 @@ namespace firebolt::rialto::server { void LogMetricsReporter::reportPeriodicSample(const PeriodicMetricsReport &report) { - RIALTO_SERVER_LOG_INFO("Metrics sample=%" PRIu64 ", reason=%s, app='%s', client_pid=%u, client_cpu=%.2f%%, " - "server_cpu=%.2f%%, combined_cpu=%.2f%%, client_cpu_ms=%" PRIu64 ", " - "server_cpu_ms=%" PRIu64 ", client_mem_kb=%" PRIu64 ", server_mem_kb=%" PRIu64 ", " - "cgroup_mem_kb=%" PRIu64 "/%" PRIu64, - report.sampleId, report.reason.c_str(), report.appName.c_str(), report.clientPid, - report.clientCpuPercent, report.serverCpuPercent, report.combinedCpuPercent, - report.clientCpuTimeMs, report.serverCpuTimeMs, report.clientMemoryKb, - report.serverMemoryKb, report.cgroupMemoryUsageKb, report.cgroupMemoryLimitKb); + RIALTO_SERVER_LOG_MIL("Metrics sample=%" PRIu64 ", reason=%s, app='%s', client_pid=%u, client_cpu=%.2f%%, " + "server_cpu=%.2f%%, combined_cpu=%.2f%%, client_cpu_ms=%" PRIu64 ", " + "server_cpu_ms=%" PRIu64 ", client_mem_kb=%" PRIu64 ", server_mem_kb=%" PRIu64 ", " + "shm_mem_kb=%" PRIu64 ", cgroup_mem_kb=%" PRIu64 "/%" PRIu64, + report.sampleId, report.reason.c_str(), report.appName.c_str(), report.clientPid, + report.clientCpuPercent, report.serverCpuPercent, report.combinedCpuPercent, + report.clientCpuTimeMs, report.serverCpuTimeMs, report.clientMemoryKb, + report.serverMemoryKb, report.shmMemoryKb, report.cgroupMemoryUsageKb, report.cgroupMemoryLimitKb); } void LogMetricsReporter::reportStateTransition(const StateTransitionReport &report) diff --git a/media/server/main/source/MetricsCollector.cpp b/media/server/main/source/MetricsCollector.cpp index b0faaf10a..c55a30f28 100644 --- a/media/server/main/source/MetricsCollector.cpp +++ b/media/server/main/source/MetricsCollector.cpp @@ -87,6 +87,8 @@ MetricsCollector::~MetricsCollector() void MetricsCollector::onTimerFired() { + RIALTO_SERVER_LOG_MIL("Metrics: periodic timer fired for client %d, requesting sample=%" PRIu64, m_clientId, + m_nextSampleId); m_client->requestMetricsSample(m_clientId, m_nextSampleId++, MetricsSampleReason::PERIODIC); } @@ -148,6 +150,7 @@ void MetricsCollector::processMetrics(const ClientMetricsData &metrics) periodicReport.serverMemoryKb = kServerMetrics.processMemoryKb; periodicReport.cgroupMemoryUsageKb = kServerMetrics.cgroupMemoryUsageKb; periodicReport.cgroupMemoryLimitKb = kServerMetrics.cgroupMemoryLimitKb; + periodicReport.shmMemoryKb = kServerMetrics.shmMemoryKb; m_reporter->reportPeriodicSample(periodicReport); } @@ -396,10 +399,24 @@ MetricsCollector::ProcessMetricsSample MetricsCollector::getServerMetrics() cons cgroupMemoryLimitKb = limitBytes / 1024; } + std::uint64_t shmMemoryKb{0}; + { + std::ifstream smaps{"/proc/self/smaps_rollup"}; + std::string sline; + while (std::getline(smaps, sline)) + { + if (sline.rfind("Pss_Shmem:", 0) == 0) + { + std::sscanf(sline.c_str(), "Pss_Shmem: %" SCNu64, &shmMemoryKb); + break; + } + } + } + return ProcessMetricsSample{ static_cast(duration_cast(steady_clock::now().time_since_epoch()).count()), static_cast(duration_cast(system_clock::now().time_since_epoch()).count()), - processCpuTimeMs, processMemoryKb, cgroupMemoryUsageKb, cgroupMemoryLimitKb}; + processCpuTimeMs, processMemoryKb, cgroupMemoryUsageKb, cgroupMemoryLimitKb, shmMemoryKb}; } double MetricsCollector::calculateCpuPercentage(std::uint64_t currentCpuTimeMs, std::uint64_t previousCpuTimeMs, diff --git a/media/server/service/source/PlaybackService.cpp b/media/server/service/source/PlaybackService.cpp index 8a3b06ff0..ac8794985 100644 --- a/media/server/service/source/PlaybackService.cpp +++ b/media/server/service/source/PlaybackService.cpp @@ -23,6 +23,7 @@ #include "RialtoServerLogging.h" #include #include +#include #include #include #include @@ -72,6 +73,9 @@ void PlaybackService::switchToInactive() m_mediaPipelineService->clearMediaPipelines(); m_webAudioPlayerService->clearWebAudioPlayers(); m_shmBuffer.reset(); + // Return freed heap pages to the OS now that pipelines and shared memory + // have been released, so the process has a low memory footprint while idle. + ::malloc_trim(0); } void PlaybackService::setMaxPlaybacks(int maxPlaybacks) diff --git a/media/server/service/source/PrivateMetricsService.cpp b/media/server/service/source/PrivateMetricsService.cpp index a1ed8a92f..88096565b 100644 --- a/media/server/service/source/PrivateMetricsService.cpp +++ b/media/server/service/source/PrivateMetricsService.cpp @@ -19,6 +19,9 @@ #include "PrivateMetricsService.h" #include "RialtoServerLogging.h" +#include +#include +#include namespace firebolt::rialto::server::service { @@ -91,5 +94,110 @@ void PrivateMetricsService::notifyApplicationStateChanged(ApplicationState oldSt { collector->notifyApplicationStateChanged(oldState, newState); } + + // When transitioning to INACTIVE, record a server-side memory snapshot. + // At this point, pipelines and shared memory have already been freed but + // no client may be connected to supply a full sample — so we read the + // server's own memory directly. + if (newState == ApplicationState::INACTIVE) + { + std::uint64_t serverMemoryKb{0}; + { + std::ifstream status{"/proc/self/status"}; + std::string line; + while (std::getline(status, line)) + { + if (line.rfind("VmRSS:", 0) == 0) + { + std::sscanf(line.c_str(), "VmRSS: %" SCNu64, &serverMemoryKb); + break; + } + } + } + + std::uint64_t cgroupMemoryUsageKb{0}; + { + auto readFileValue = [](const std::string &path) -> std::uint64_t + { + std::ifstream file{path}; + if (!file.is_open()) + { + return 0; + } + std::string content; + if (!std::getline(file, content) || content.empty() || content == "max") + { + return 0; + } + std::uint64_t value{0}; + if (std::sscanf(content.c_str(), "%" SCNu64, &value) == 1) + { + return value; + } + return 0; + }; + + // Resolve the process's cgroup path from /proc/self/cgroup + std::ifstream cgroupFile{"/proc/self/cgroup"}; + std::string cgroupBase; + if (cgroupFile.is_open()) + { + std::string line; + while (std::getline(cgroupFile, line)) + { + if (line.rfind("0::", 0) == 0) + { + std::string relativePath{line.substr(3)}; + if (!relativePath.empty() && relativePath != "/") + { + cgroupBase = "/sys/fs/cgroup" + relativePath; + } + else + { + cgroupBase = "/sys/fs/cgroup"; + } + break; + } + } + } + + std::uint64_t usageBytes{0}; + if (!cgroupBase.empty()) + { + usageBytes = readFileValue(cgroupBase + "/memory.current"); + } + if (usageBytes == 0) + { + usageBytes = readFileValue("/sys/fs/cgroup/memory/memory.usage_in_bytes"); + } + cgroupMemoryUsageKb = usageBytes / 1024; + } + + // Read smaps_rollup to split private-dirty heap from file-backed libs. + std::uint64_t anonKb{0}, sharedCleanKb{0}, privateCleanKb{0}, privateDirtyKb{0}; + { + std::ifstream smaps{"/proc/self/smaps_rollup"}; + std::string sline; + while (std::getline(smaps, sline)) + { + if (sline.rfind("Anonymous:", 0) == 0) + std::sscanf(sline.c_str(), "Anonymous: %" SCNu64, &anonKb); + else if (sline.rfind("Shared_Clean:", 0) == 0) + std::sscanf(sline.c_str(), "Shared_Clean: %" SCNu64, &sharedCleanKb); + else if (sline.rfind("Private_Clean:", 0) == 0) + std::sscanf(sline.c_str(), "Private_Clean: %" SCNu64, &privateCleanKb); + else if (sline.rfind("Private_Dirty:", 0) == 0) + std::sscanf(sline.c_str(), "Private_Dirty: %" SCNu64, &privateDirtyKb); + } + } + RIALTO_SERVER_LOG_MIL("Metrics: INACTIVE memory snapshot — server_mem_kb=%" PRIu64 + ", cgroup_mem_kb=%" PRIu64 + ", anon_kb=%" PRIu64 + ", private_dirty_kb=%" PRIu64 + ", private_clean_kb=%" PRIu64 + ", shared_clean_kb=%" PRIu64, + serverMemoryKb, cgroupMemoryUsageKb, + anonKb, privateDirtyKb, privateCleanKb, sharedCleanKb); + } } } // namespace firebolt::rialto::server::service diff --git a/media/server/service/source/SessionServerManager.cpp b/media/server/service/source/SessionServerManager.cpp index ceefe9f92..921bcadb6 100644 --- a/media/server/service/source/SessionServerManager.cpp +++ b/media/server/service/source/SessionServerManager.cpp @@ -236,11 +236,12 @@ bool SessionServerManager::switchToInactive() } m_playbackService.switchToInactive(); m_cdmService.switchToInactive(); + // Record INACTIVE memory snapshot immediately after resource teardown, + // before the manager ACK — ensures we capture it even if the socket breaks. + m_sessionManagementServer->notifyApplicationStateChanged(ApplicationState::RUNNING, ApplicationState::INACTIVE); if (m_applicationManagementServer->sendStateChangedEvent(common::SessionServerState::INACTIVE)) { - ApplicationState oldState = ApplicationState::RUNNING; // switching from active to inactive m_controlService.setApplicationState(ApplicationState::INACTIVE); - m_sessionManagementServer->notifyApplicationStateChanged(oldState, ApplicationState::INACTIVE); m_currentState.store(common::SessionServerState::INACTIVE); RIALTO_SERVER_LOG_MIL("RialtoServer state is INACTIVE now"); return true; From f75dd80f776a9429c2f48068daee96f6e00cf7ec Mon Sep 17 00:00:00 2001 From: Douglas Adler Date: Wed, 1 Jul 2026 13:42:37 -0500 Subject: [PATCH 06/11] Improve metrics to meet rialto design Signed-off-by: Douglas Adler --- media/server/main/include/MetricsCollector.h | 114 +++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/media/server/main/include/MetricsCollector.h b/media/server/main/include/MetricsCollector.h index 563fa6d87..dde7443b8 100644 --- a/media/server/main/include/MetricsCollector.h +++ b/media/server/main/include/MetricsCollector.h @@ -113,3 +113,117 @@ class MetricsCollector : public IMetricsCollector } // namespace firebolt::rialto::server #endif // FIREBOLT_RIALTO_SERVER_METRICS_COLLECTOR_H_ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_METRICS_COLLECTOR_H_ +#define FIREBOLT_RIALTO_SERVER_METRICS_COLLECTOR_H_ + +#include "IMetricsCollector.h" +#include "IMetricsReporter.h" +#include "ITimer.h" +#include "MetricsThresholdChecker.h" +#include "StateMetricsAggregator.h" +#include +#include +#include +#include +#include +#include + +namespace firebolt::rialto::server +{ +class MetricsCollectorFactory : public IMetricsCollectorFactory +{ +public: + MetricsCollectorFactory() = default; + ~MetricsCollectorFactory() override = default; + + std::unique_ptr create(int clientId, + const std::shared_ptr &client) override; +}; + +class MetricsCollector : public IMetricsCollector +{ +public: + MetricsCollector(int clientId, const std::shared_ptr &client, + const std::shared_ptr &timerFactory); + ~MetricsCollector() override; + + void processMetrics(const ClientMetricsData &metrics) override; + void notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) override; + void notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) override; + +private: + struct ProcessMetricsSample + { + std::uint64_t monotonicTimeMs{0}; + std::uint64_t epochTimeMs{0}; + std::uint64_t processCpuTimeMs{0}; + std::uint64_t processMemoryKb{0}; + std::uint64_t cgroupMemoryUsageKb{0}; + std::uint64_t cgroupMemoryLimitKb{0}; + }; + + struct PreviousSample + { + std::uint64_t clientMonotonicTimeMs{0}; + std::uint64_t clientCpuTimeMs{0}; + std::uint64_t clientMemoryKb{0}; + ProcessMetricsSample serverMetrics; + }; + + struct SessionMetricsState + { + PlaybackState currentPlaybackState{PlaybackState::UNKNOWN}; + StateMetricsAggregator aggregator; + }; + + void onTimerFired(); + ProcessMetricsSample getServerMetrics() const; + double calculateCpuPercentage(std::uint64_t currentCpuTimeMs, std::uint64_t previousCpuTimeMs, + std::uint64_t currentMonotonicTimeMs, std::uint64_t previousMonotonicTimeMs) const; + static const char *sampleReasonToString(MetricsSampleReason reason); + static const char *playbackStateToString(PlaybackState state); + static const char *applicationStateToString(ApplicationState state); + + const int m_clientId; + std::shared_ptr m_client; + std::unique_ptr m_timer; + std::uint64_t m_nextSampleId{1}; + + std::mutex m_mutex; + std::optional m_previousSample; + + // Per-session state tracking (sessionId -> session state) + std::map m_sessionStates; + + // Global aggregator (active across all sessions while RUNNING) + StateMetricsAggregator m_globalAggregator; + ApplicationState m_currentApplicationState{ApplicationState::UNKNOWN}; + + // Pluggable metrics reporter (log, telemetry, or composite) + std::unique_ptr m_reporter; + + // Threshold checker + MetricsThresholdChecker m_thresholdChecker; +}; +} // namespace firebolt::rialto::server + +#endif // FIREBOLT_RIALTO_SERVER_METRICS_COLLECTOR_H_ From 75a7333951c5a6d056845001df74c04adc2e58d5 Mon Sep 17 00:00:00 2001 From: Douglas Adler Date: Mon, 3 Aug 2026 15:34:56 -0500 Subject: [PATCH 07/11] Refine private metrics integration and reporting Replace the copied private metrics proto files with symbolic links and remove the duplicated MetricsCollector header content. Move private metrics ownership into PlaybackService so MediaPipeline and WebAudio can share playback-state reporting without passing a raw service pointer through IPC. Keep API-provided video geometry authoritative and use environment geometry only as a fallback. Retain the 15-second sampling interval while limiting active routine logs to 10-minute intervals. Report significant changes early, suppress stable inactive samples, and handle nonresponsive clients. Add unit coverage for IPC, collectors, reporters, services, WebAudio, video geometry, and metrics ownership. Signed-off-by: Douglas Adler --- docs/metrics/RialtoMetricsReport.md | 289 ++++++++++++++++++ .../ipc/proto/privatemetricsmodule.proto | 2 +- media/client/ipc/source/PrivateMetricsIpc.cpp | 20 +- media/client/main/source/ClientController.cpp | 2 +- .../gstplayer/include/GenericPlayerContext.h | 12 + .../gstplayer/source/GstGenericPlayer.cpp | 5 +- .../source/tasks/generic/SetupElement.cpp | 5 + .../ipc/include/IMediaPipelineModuleService.h | 6 - .../include/IPrivateMetricsModuleService.h | 1 - .../server/ipc/include/MediaPipelineClient.h | 7 +- .../ipc/include/MediaPipelineModuleService.h | 2 - .../ipc/include/PrivateMetricsModuleService.h | 1 - .../ipc/include/SessionManagementServer.h | 3 +- media/server/ipc/interface/IIpcFactory.h | 4 +- media/server/ipc/interface/IpcFactory.h | 3 +- .../ipc/proto/privatemetricsmodule.proto | 2 +- media/server/ipc/source/IpcFactory.cpp | 5 +- .../server/ipc/source/MediaPipelineClient.cpp | 18 +- .../ipc/source/MediaPipelineModuleService.cpp | 8 +- .../source/PrivateMetricsModuleService.cpp | 10 +- .../ipc/source/SessionManagementServer.cpp | 6 +- media/server/main/include/IMetricsReporter.h | 3 + .../server/main/include/LogMetricsReporter.h | 9 + media/server/main/include/MetricsCollector.h | 136 +-------- media/server/main/interface/IMainThread.h | 1 - .../server/main/interface/IMetricsCollector.h | 14 +- .../server/main/source/LogMetricsReporter.cpp | 65 ++++ media/server/main/source/MetricsCollector.cpp | 135 ++++++-- media/server/service/CMakeLists.txt | 2 + .../server/service/include/IPlaybackService.h | 2 + .../service/include/IPrivateMetricsService.h | 12 +- .../source/MediaPipelineMetricsClient.cpp | 72 +++++ .../source/MediaPipelineMetricsClient.h | 61 ++++ .../service/source/MediaPipelineService.cpp | 9 +- .../service/source/MediaPipelineService.h | 4 +- .../server/service/source/PlaybackService.cpp | 13 +- media/server/service/source/PlaybackService.h | 3 + .../service/source/PrivateMetricsService.cpp | 13 +- .../service/source/PrivateMetricsService.h | 3 + .../service/source/SessionServerManager.cpp | 7 +- .../service/source/SessionServerManager.h | 2 - .../source/WebAudioPlayerMetricsClient.cpp | 37 +++ .../source/WebAudioPlayerMetricsClient.h | 46 +++ .../service/source/WebAudioPlayerService.cpp | 12 +- .../service/source/WebAudioPlayerService.h | 5 +- .../unittests/media/client/ipc/CMakeLists.txt | 3 + .../PrivateMetricsIpcTests.cpp | 126 ++++++++ .../main/clientController/CreateTest.cpp | 22 ++ .../GstGenericPlayerPrivateTest.cpp | 1 + .../common/GenericTasksTestsBase.cpp | 47 +++ .../common/GenericTasksTestsBase.h | 9 + .../tasksTests/SetupElementTest.cpp | 24 ++ .../unittests/media/server/ipc/CMakeLists.txt | 3 + .../PrivateMetricsModuleServiceTests.cpp | 110 +++++++ .../SessionManagementServerTestsFixture.cpp | 12 + .../SessionManagementServerTestsFixture.h | 4 + .../media/server/main/CMakeLists.txt | 4 + .../main/metrics/LogMetricsReporterTests.cpp | 136 +++++++++ .../main/metrics/MetricsCollectorTests.cpp | 132 ++++++++ .../main/metrics/MetricsHelpersTests.cpp | 134 ++++++++ .../media/server/mocks/ipc/IpcFactoryMock.h | 2 +- .../ipc/MediaPipelineModuleServiceMock.h | 2 - .../ipc/PrivateMetricsModuleServiceMock.h | 57 ++++ .../mocks/main/MetricsCollectorClientMock.h | 36 +++ .../server/mocks/main/MetricsCollectorMock.h | 50 +++ .../server/mocks/main/MetricsReporterMock.h | 37 +++ .../mocks/service/PlaybackServiceMock.h | 1 + .../mocks/service/PrivateMetricsServiceMock.h | 46 +++ .../media/server/service/CMakeLists.txt | 3 + .../MediaPipelineServiceTestsFixture.cpp | 14 +- .../MediaPipelineServiceTestsFixture.h | 2 + .../service/metrics/MetricsClientsTests.cpp | 106 +++++++ .../metrics/PrivateMetricsServiceTests.cpp | 101 ++++++ .../playbackService/PlaybackServiceTests.cpp | 6 + .../PlaybackServiceTestsFixture.cpp | 5 + .../PlaybackServiceTestsFixture.h | 1 + .../SessionServerManagerTestsFixture.cpp | 16 +- .../WebAudioPlayerServiceTestsFixture.cpp | 3 +- .../WebAudioPlayerServiceTestsFixture.h | 2 + 79 files changed, 2086 insertions(+), 248 deletions(-) create mode 100644 docs/metrics/RialtoMetricsReport.md mode change 100644 => 120000 media/client/ipc/proto/privatemetricsmodule.proto mode change 100644 => 120000 media/server/ipc/proto/privatemetricsmodule.proto create mode 100644 media/server/service/source/MediaPipelineMetricsClient.cpp create mode 100644 media/server/service/source/MediaPipelineMetricsClient.h create mode 100644 media/server/service/source/WebAudioPlayerMetricsClient.cpp create mode 100644 media/server/service/source/WebAudioPlayerMetricsClient.h create mode 100644 tests/unittests/media/client/ipc/privateMetricsIpc/PrivateMetricsIpcTests.cpp create mode 100644 tests/unittests/media/server/ipc/privateMetricsModuleService/PrivateMetricsModuleServiceTests.cpp create mode 100644 tests/unittests/media/server/main/metrics/LogMetricsReporterTests.cpp create mode 100644 tests/unittests/media/server/main/metrics/MetricsCollectorTests.cpp create mode 100644 tests/unittests/media/server/main/metrics/MetricsHelpersTests.cpp create mode 100644 tests/unittests/media/server/mocks/ipc/PrivateMetricsModuleServiceMock.h create mode 100644 tests/unittests/media/server/mocks/main/MetricsCollectorClientMock.h create mode 100644 tests/unittests/media/server/mocks/main/MetricsCollectorMock.h create mode 100644 tests/unittests/media/server/mocks/main/MetricsReporterMock.h create mode 100644 tests/unittests/media/server/mocks/service/PrivateMetricsServiceMock.h create mode 100644 tests/unittests/media/server/service/metrics/MetricsClientsTests.cpp create mode 100644 tests/unittests/media/server/service/metrics/PrivateMetricsServiceTests.cpp diff --git a/docs/metrics/RialtoMetricsReport.md b/docs/metrics/RialtoMetricsReport.md new file mode 100644 index 000000000..63e31400c --- /dev/null +++ b/docs/metrics/RialtoMetricsReport.md @@ -0,0 +1,289 @@ +# Rialto Server Metrics — Findings Report + +**Date:** 2026-07-08 +**Branch:** `cpu-metrics-updated` +**Platform data:** SkyCobalt production device (2026-07-08) + +--- + +## 1. Overview + +The Rialto metrics system collects CPU and memory usage data from both the client application +and the Rialto server process during media playback. Samples are taken periodically (every 15 s) +and also on every playback state transition (IDLE→PAUSED→PLAYING etc.) and application state +change (RUNNING→INACTIVE). + +The system is implemented as a three-layer pipeline: + +``` +Client process Server process +────────────── ────────────── +MetricsSampleCollector ──IPC──► PrivateMetricsModuleService + (reads /proc/self) └─► MetricsCollector + ├─ CPU delta calculation + ├─ State aggregation + └─ LogMetricsReporter → server log +``` + +--- + +## 2. Log Message Reference + +### 2.1 Baseline (on client connect) + +``` +Metrics baseline: sample=1, reason=CONNECTED, app='SkyCobalt', client_pid=18, + client_cpu_ms=1690, server_cpu_ms=80, + client_mem_kb=91660, server_mem_kb=10404, + cgroup_mem_kb=1825324/9007199254740988 +``` + +Records initial CPU and memory at the moment the client registers with the metrics +service. All subsequent CPU percentages are deltas relative to the *previous* sample. + +--- + +### 2.2 Periodic / State-Transition Sample + +``` +Metrics sample=N, reason=, app='SkyCobalt', + client_pid=18, + client_cpu=18.44%, ← % of one CPU core used by client since last sample + server_cpu=18.71%, ← % of one CPU core used by server since last sample + combined_cpu=37.15%, ← sum of above (> 100% possible on multi-core) + client_cpu_ms=107030, ← cumulative client CPU time since connect (ms) + server_cpu_ms=82910, ← cumulative server CPU time since connect (ms) + client_mem_kb=137076, ← client VmRSS (resident set size) + server_mem_kb=19312, ← server VmRSS + shm_mem_kb=4096, ← server's Pss_Shmem: proportional share of the + memfd-backed shared transport buffer + cgroup_mem_kb=2057416/0 ← cgroup memory usage / limit (0 = unlimited) +``` + +**Key fields explained:** + +| Field | Source | What it measures | +|-------|--------|-----------------| +| `client_cpu` | `/proc//stat` delta | CPU load of the app process | +| `server_cpu` | `/proc/self/stat` delta | CPU load of the Rialto server | +| `combined_cpu` | sum | Total CPU cost of the playback stack | +| `client_mem_kb` | `/proc//status` VmRSS | All RAM mapped by the app (shared libs included) | +| `server_mem_kb` | `/proc/self/status` VmRSS | All RAM mapped by the server | +| `shm_mem_kb` | `/proc/self/smaps_rollup` Pss_Shmem | The shared memory transport buffer allocated for the pipeline (4 MB per session) | +| `cgroup_mem_kb` | cgroup `memory.current` | Total memory usage of the entire cgroup (all processes) | + +--- + +### 2.3 State Aggregation Report + +Emitted whenever a playback state ends (e.g. PLAYING→PAUSED). Summarises all samples +collected during that state period. + +``` +Metrics state report [session=2] state='PLAYING', duration_ms=413382, samples=28, + client_cpu={min=14.07, max=34.36, mean=18.44, stddev=4.66}%, + server_cpu={min=16.06, max=28.82, mean=18.71, stddev=2.38}%, + combined_cpu={min=31.69, max=63.21, mean=37.15, stddev=6.73}%, + client_mem_kb={min=154264, max=188152, mean=181857}, + server_mem_kb={min=26932, max=34192, mean=33132}, + cgroup_mem_kb={min=1887156, max=2093852, mean=2021011} +``` + +--- + +### 2.4 INACTIVE Memory Snapshot + +Emitted immediately after `switchToInactive()` frees all pipelines and shared memory, +but before the process receives any new client connection. This gives the true +post-teardown memory footprint. + +``` +Metrics: INACTIVE memory snapshot — + server_mem_kb=19016, ← VmRSS after teardown + cgroup_mem_kb=2084812, + anon_kb=6852, ← anonymous pages (= private_dirty_kb on this platform) + private_dirty_kb=6852, ← TRUE committed RAM — OS cannot reclaim this + private_clean_kb=0, ← file-backed pages not yet written (OS-reclaimable) + shared_clean_kb=13096 ← loaded .so libraries (OS-reclaimable under pressure) +``` + +#### How the snapshot is collected + +The snapshot fires inside `PrivateMetricsService::notifyApplicationStateChanged()` when +`newState == INACTIVE`. The call sequence is: + +``` +SessionServerManager::switchToInactive() + └─► PlaybackService::switchToInactive() + ├─ destroys GStreamer pipeline (m_mainThread, decoders, sinks) + ├─ resets shared memory buffer (m_shmBuffer.reset()) + └─ ::malloc_trim(0) ← returns heap fragmentation to OS + └─► notifyApplicationStateChanged(INACTIVE) ← snapshot fires here + ├─ reads /proc/self/status → server_mem_kb (VmRSS) + ├─ reads cgroup memory.current → cgroup_mem_kb + └─ reads /proc/self/smaps_rollup → anon_kb, private_dirty_kb, + private_clean_kb, shared_clean_kb + └─► sendStateChangedEvent() ← manager ACK (after snapshot) +``` + +The snapshot is deliberately taken **before** the manager ACK so that it survives even +if the IPC socket is closed by the session manager. + +#### `/proc/self/smaps_rollup` fields + +`/proc/self/smaps_rollup` is a kernel file that aggregates the `smaps` entries for all +virtual memory areas (VMAs) of the process into a single summary. The fields used are: + +| smaps_rollup field | Log field | Kernel meaning | +|--------------------|-----------|---------------| +| `Anonymous` | `anon_kb` | Pages with no file backing — heap, stacks, `mmap(MAP_ANONYMOUS)` | +| `Private_Dirty` | `private_dirty_kb` | Private pages that have been written; the OS **cannot** reclaim these | +| `Private_Clean` | `private_clean_kb` | Private file-backed pages not yet written (COW pages); reclaimable | +| `Shared_Clean` | `shared_clean_kb` | Shared file-backed pages mapped read-only (`.so` libraries); reclaimable | + +On a typical embedded Linux system `Anonymous ≈ Private_Dirty` because every anonymous +page written becomes private-dirty immediately. This is confirmed in the production data +where both values are 6,852 KB. + +#### Relationship to VmRSS + +`VmRSS` (the `server_mem_kb` field) is the total resident set size — every physical +page currently mapped by the process. The smaps categories partition it: + +$$\text{VmRSS} \approx \text{private\_dirty} + \text{private\_clean} + \text{shared\_clean} + \text{shared\_dirty} + \text{other}$$ + +From the production snapshot: + +$$19{,}016\ \text{KB} \approx 6{,}852 + 0 + 13{,}096 + \sim1{,}068\ \text{KB (rounding + shared\_dirty)}$$ + +This confirms that virtually all of VmRSS is accounted for by the three reported +categories plus a small residual. + +#### Memory category breakdown + +| Category | Reclaimable? | Typical contents | +|----------|-------------|-----------------| +| `private_dirty_kb` | **No** | Heap allocations, thread stacks, GStreamer type registry | +| `private_clean_kb` | Yes | File-backed mappings not yet written (COW pages from `.so` loads) | +| `shared_clean_kb` | Yes | Loaded shared libraries (`.so` files) mapped read-only | + +The key figure for platform memory planning is `private_dirty_kb` — this is the memory +the OS is **obligated** to keep in RAM. Everything else can be silently paged out under +memory pressure. + +--- + +## 3. Production Session Analysis — SkyCobalt (2026-07-08) + +### 3.1 Session Timeline + +``` +20:21:45 Client connected (app launched, no playback) +20:22:48 Session 1 created: UNKNOWN → IDLE → PAUSED → PLAYING +20:23:06 Session 1 PAUSED (channel change), Session 2 starts immediately +20:23:07 Session 2: IDLE → PAUSED → PLAYING +20:30:00 Session 2 PAUSED (end of playback, ~6m 53s) +20:30:07 Server → INACTIVE (app backgrounded) +20:30:15 Periodic monitoring continues (app still connected, server idle) +``` + +### 3.2 Memory Profile + +| Phase | server_mem_kb | private_dirty_kb | shm_mem_kb | Notes | +|-------|-------------|-----------------|-----------|-------| +| Idle (no pipeline) | 10,404 | — | 0 | Server baseline after connect | +| Pipeline loading | 24,692 | — | 4,096 | GStreamer elements initialised | +| Steady-state playback | 33,948 | — | 4,096 | Stable from ~sample 17 onwards | +| **INACTIVE snapshot** | **19,016** | **6,852** | **0** | After malloc_trim + pipeline teardown | +| Post-INACTIVE idle | 19,312 | — | 0 | Stable | + +**Interpretation:** + +- The **4 MB `shm_mem_kb`** is the `memfd`-backed shared transport buffer used to pass + compressed media frames between client and server. It is allocated when the pipeline + becomes active and freed on `switchToInactive()`. + +- **`server_mem_kb` during playback: ~34 MB** (from ~10 MB baseline). The ~24 MB growth + covers GStreamer pipeline elements, decoder state, and the shared memory mapping. + +- **After INACTIVE, VmRSS drops to ~19 MB** — a 44% reduction from peak playback. + +- **`private_dirty_kb` = 6,852 KB (~6.7 MB)** is the true committed RAM cost of an + idle/backgrounded server instance. The remaining ~12 MB of VmRSS is `shared_clean` + (.so files) that the OS can page out under memory pressure. + + The smaps breakdown for this snapshot: + + | Category | KB | % of VmRSS | Notes | + |----------|----|-----------|-------| + | `private_dirty` | 6,852 | 36% | Heap + stacks; cannot be reclaimed | + | `private_clean` | 0 | 0% | All COW pages already promoted to dirty | + | `shared_clean` | 13,096 | 69% | Loaded `.so` libraries; OS-reclaimable | + | Residual / shared_dirty | ~1,068 | ~6% | Rounding + any shared writable mappings | + | **VmRSS total** | **19,016** | **100%** | | + + The `private_clean` value of 0 is notable — it means every file-backed page that was + mapped on this platform had already been written (promoted to dirty) before teardown. + This can differ on platforms where library pages remain clean for longer. + +### 3.3 CPU Profile During Playback (Session 2, 28 samples over 6m 53s) + +| Metric | Min | Mean | Max | Stddev | +|--------|-----|------|-----|--------| +| Client CPU | 14.1% | 18.4% | 34.4% | 4.7% | +| Server CPU | 16.1% | 18.7% | 28.8% | 2.4% | +| Combined CPU | 31.7% | 37.2% | 63.2% | 6.7% | + +- Server CPU is **remarkably stable** (stddev 2.4%) — the server's workload is + predictable and bounded by the media pipeline decode/demux loop. +- Client CPU is more variable (stddev 4.7%) — likely driven by UI rendering, JS + execution, and adaptive bitrate logic in SkyCobalt. +- Combined mean of **~37% of one CPU core** is the steady-state cost of a single + active playback session. + +### 3.4 Channel Change Behaviour + +At 20:23:06, session 1 was paused and session 2 started within ~500 ms — a clean +channel change pattern. The state report for session 1 shows `duration_ms=16841` +(17 seconds) with only 1 sample, which is expected for such a short-lived playing state. On Cobalt this is probably the leader ad to the content + +### 3.5 Post-INACTIVE Server Behaviour + +After going INACTIVE, the server CPU drops to **~0.07%** — effectively zero. The +`malloc_trim(0)` call we added returns heap fragmentation to the OS immediately after +pipeline teardown, contributing to the reduced VmRSS. + +The cgroup memory also gradually decreases after INACTIVE — from ~2,090 MB down to +~1,944 MB over the following 5 minutes — as the OS pages out `shared_clean` library +mappings from all processes in the cgroup. + +--- + +## 4. Multi-Instance Memory Estimate + +For platform memory planning with multiple backgrounded app instances: + +| Instances | Committed RAM (private_dirty × N) | VmRSS (worst case, no reclaim) | +|-----------|----------------------------------|-------------------------------| +| 1 | ~7 MB | ~19 MB | +| 3 | ~21 MB | ~57 MB | +| 5 | **~34 MB** | ~95 MB | + +The committed RAM figure (~34 MB for 5 instances) is the **hard floor** — memory that +cannot be reclaimed regardless of pressure. The VmRSS figure (~95 MB) is the upper +bound assuming no library pages have been paged out. + +In practice, with memory pressure the shared_clean pages (~13 MB per instance) will be +reclaimed first, bringing 5 instances closer to the committed floor of ~34 MB. + +--- + +## 5. Summary of Changes Implemented + +| Change | File | Purpose | +|--------|------|---------| +| INACTIVE memory snapshot | `PrivateMetricsService.cpp` | Record VmRSS + smaps breakdown after teardown | +| `malloc_trim(0)` on INACTIVE | `PlaybackService.cpp` | Return heap fragmentation to OS | +| Fix snapshot ordering | `SessionServerManager.cpp` | Fire snapshot before manager ACK (survives socket failure) | +| `shm_mem_kb` in periodic samples | `MetricsCollector.cpp`, `LogMetricsReporter.cpp` | Account for shared transport buffer | +| Promote sample log to MIL | `LogMetricsReporter.cpp` | Visible in production logs | diff --git a/media/client/ipc/proto/privatemetricsmodule.proto b/media/client/ipc/proto/privatemetricsmodule.proto deleted file mode 100644 index cdef9ba7d..000000000 --- a/media/client/ipc/proto/privatemetricsmodule.proto +++ /dev/null @@ -1 +0,0 @@ -../../../../proto/privatemetricsmodule.proto diff --git a/media/client/ipc/proto/privatemetricsmodule.proto b/media/client/ipc/proto/privatemetricsmodule.proto new file mode 120000 index 000000000..31c78e1ad --- /dev/null +++ b/media/client/ipc/proto/privatemetricsmodule.proto @@ -0,0 +1 @@ +../../../../proto/privatemetricsmodule.proto \ No newline at end of file diff --git a/media/client/ipc/source/PrivateMetricsIpc.cpp b/media/client/ipc/source/PrivateMetricsIpc.cpp index ffd49ac71..283e78e9a 100644 --- a/media/client/ipc/source/PrivateMetricsIpc.cpp +++ b/media/client/ipc/source/PrivateMetricsIpc.cpp @@ -115,11 +115,11 @@ bool PrivateMetricsIpc::reportClientMetrics(std::uint64_t sampleId, std::uint32_ metrics->set_process_cpu_time_ms(processCpuTimeMs); metrics->set_process_memory_kb(processMemoryKb); - RIALTO_CLIENT_LOG_MIL("Reporting metrics sample=%" PRIu64 ", reason=%s, app='%s', pid=%u, cpu_ms=%" PRIu64 - ", mem_kb=%" PRIu64, - sampleId, - sampleReasonToString(static_cast(reason)), - appName.c_str(), processId, processCpuTimeMs, processMemoryKb); + RIALTO_CLIENT_LOG_DEBUG("Reporting metrics sample=%" PRIu64 ", reason=%s, app='%s', pid=%u, cpu_ms=%" PRIu64 + ", mem_kb=%" PRIu64, + sampleId, + sampleReasonToString(static_cast(reason)), + appName.c_str(), processId, processCpuTimeMs, processMemoryKb); firebolt::rialto::ReportClientMetricsResponse response; auto ipcController = m_ipc.createRpcController(); @@ -130,12 +130,12 @@ bool PrivateMetricsIpc::reportClientMetrics(std::uint64_t sampleId, std::uint32_ if (ipcController->Failed()) { - RIALTO_CLIENT_LOG_ERROR("failed to report client metrics due to '%s'", ipcController->ErrorText().c_str()); + RIALTO_CLIENT_LOG_DEBUG("Failed to report client metrics due to '%s'", ipcController->ErrorText().c_str()); return false; } - RIALTO_CLIENT_LOG_INFO("Reported metrics sample=%" PRIu64 ", reason=%s", sampleId, - sampleReasonToString(static_cast(reason))); + RIALTO_CLIENT_LOG_DEBUG("Reported metrics sample=%" PRIu64 ", reason=%s", sampleId, + sampleReasonToString(static_cast(reason))); return true; } @@ -205,8 +205,8 @@ void PrivateMetricsIpc::onMetricsSampleRequested( RIALTO_CLIENT_LOG_WARN("No private metrics client registered"); return; } - RIALTO_CLIENT_LOG_MIL("Received metrics sample request sample=%" PRIu64 ", reason=%s, pid=%d", event->sample_id(), - sampleReasonToString(event->reason()), getpid()); + RIALTO_CLIENT_LOG_DEBUG("Received metrics sample request sample=%" PRIu64 ", reason=%s, pid=%d", + event->sample_id(), sampleReasonToString(event->reason()), getpid()); m_privateMetricsIpcClient->reportClientMetrics(event->sample_id(), event->reason()); } } // namespace firebolt::rialto::client diff --git a/media/client/main/source/ClientController.cpp b/media/client/main/source/ClientController.cpp index f84923c02..0cd4b942d 100644 --- a/media/client/main/source/ClientController.cpp +++ b/media/client/main/source/ClientController.cpp @@ -299,7 +299,7 @@ void ClientController::reportClientMetrics(std::uint64_t sampleId, std::uint32_t getMonotonicTimeMs(), getEpochTimeMs(), getProcessCpuTimeMs(), getProcessMemoryKb())) { - RIALTO_CLIENT_LOG_WARN("Failed to report client process metrics"); + RIALTO_CLIENT_LOG_DEBUG("Failed to report client process metrics"); } } diff --git a/media/server/gstplayer/include/GenericPlayerContext.h b/media/server/gstplayer/include/GenericPlayerContext.h index 17cf6f741..6e00358ab 100644 --- a/media/server/gstplayer/include/GenericPlayerContext.h +++ b/media/server/gstplayer/include/GenericPlayerContext.h @@ -27,10 +27,12 @@ #include "ITimer.h" #include "MediaCommon.h" #include +#include #include #include #include #include +#include #include #include @@ -141,6 +143,16 @@ struct GenericPlayerContext */ Rectangle pendingGeometry; + /** + * @brief Fallback video geometry used only when setVideoWindow() was not called. + */ + Rectangle defaultVideoGeometry; + + /** + * @brief True once geometry has been supplied through setVideoWindow(). + */ + std::atomic_bool videoGeometrySetByApi{false}; + /** * @brief Current playback rate */ diff --git a/media/server/gstplayer/source/GstGenericPlayer.cpp b/media/server/gstplayer/source/GstGenericPlayer.cpp index ddfabea84..2e71d1728 100644 --- a/media/server/gstplayer/source/GstGenericPlayer.cpp +++ b/media/server/gstplayer/source/GstGenericPlayer.cpp @@ -391,8 +391,8 @@ void GstGenericPlayer::initMsePipeline() if (const auto defaultGeometry = getDefaultVideoGeometryFromEnvironment()) { - m_context.pendingGeometry = *defaultGeometry; - RIALTO_SERVER_LOG_MIL("Loaded default video geometry from environment: x=%d y=%d width=%d height=%d", + m_context.defaultVideoGeometry = *defaultGeometry; + RIALTO_SERVER_LOG_INFO("Loaded fallback video geometry from environment: x=%d y=%d width=%d height=%d", defaultGeometry->x, defaultGeometry->y, defaultGeometry->width, defaultGeometry->height); } @@ -2097,6 +2097,7 @@ int64_t GstGenericPlayer::getPosition(GstElement *element) void GstGenericPlayer::setVideoGeometry(int x, int y, int width, int height) { + m_context.videoGeometrySetByApi.store(true); if (m_workerThread) { m_workerThread->enqueueTask( diff --git a/media/server/gstplayer/source/tasks/generic/SetupElement.cpp b/media/server/gstplayer/source/tasks/generic/SetupElement.cpp index 1da193120..45fb3f7ae 100644 --- a/media/server/gstplayer/source/tasks/generic/SetupElement.cpp +++ b/media/server/gstplayer/source/tasks/generic/SetupElement.cpp @@ -370,6 +370,11 @@ void SetupElement::execute() const { m_player.setVideoSinkRectangle(); } + else if (!m_context.videoGeometrySetByApi.load() && !m_context.defaultVideoGeometry.empty()) + { + m_context.pendingGeometry = m_context.defaultVideoGeometry; + m_player.setVideoSinkRectangle(); + } if (m_context.pendingImmediateOutputForVideo.has_value()) { m_player.setImmediateOutput(); diff --git a/media/server/ipc/include/IMediaPipelineModuleService.h b/media/server/ipc/include/IMediaPipelineModuleService.h index 7caf1c9ac..a26f4091d 100644 --- a/media/server/ipc/include/IMediaPipelineModuleService.h +++ b/media/server/ipc/include/IMediaPipelineModuleService.h @@ -28,8 +28,6 @@ namespace firebolt::rialto::server::ipc { class IMediaPipelineModuleService; -class IPrivateMetricsModuleService; - /** * @brief IMediaPipelineModuleService factory class, returns a concrete implementation of IMediaPipelineModuleService */ @@ -84,10 +82,6 @@ class IMediaPipelineModuleService : public ::firebolt::rialto::MediaPipelineModu */ virtual void clientDisconnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) = 0; - /** - * @brief Set the metrics service for state transition notifications. - */ - virtual void setMetricsService(const std::shared_ptr &metricsService) = 0; }; } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/include/IPrivateMetricsModuleService.h b/media/server/ipc/include/IPrivateMetricsModuleService.h index 774f368d3..5ea0a75c7 100644 --- a/media/server/ipc/include/IPrivateMetricsModuleService.h +++ b/media/server/ipc/include/IPrivateMetricsModuleService.h @@ -58,7 +58,6 @@ class IPrivateMetricsModuleService : public ::firebolt::rialto::PrivateMetricsMo virtual void clientConnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) = 0; virtual void clientDisconnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) = 0; - virtual void notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) = 0; virtual void notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) = 0; }; } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/include/MediaPipelineClient.h b/media/server/ipc/include/MediaPipelineClient.h index ac45db7a0..685f2d325 100644 --- a/media/server/ipc/include/MediaPipelineClient.h +++ b/media/server/ipc/include/MediaPipelineClient.h @@ -27,13 +27,10 @@ namespace firebolt::rialto::server::ipc { -class IPrivateMetricsModuleService; - class MediaPipelineClient : public IMediaPipelineClient { public: - MediaPipelineClient(int sessionId, const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient, - IPrivateMetricsModuleService *metricsService = nullptr); + MediaPipelineClient(int sessionId, const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient); ~MediaPipelineClient() override; void notifyDuration(int64_t duration) override; @@ -56,8 +53,6 @@ class MediaPipelineClient : public IMediaPipelineClient private: int m_sessionId; std::shared_ptr<::firebolt::rialto::ipc::IClient> m_ipcClient; - IPrivateMetricsModuleService *m_metricsService; - PlaybackState m_currentPlaybackState{PlaybackState::UNKNOWN}; // It is possible for a needData to be sent while a source is been attached, // this causes an issue in client side as they recieve a needData from a source diff --git a/media/server/ipc/include/MediaPipelineModuleService.h b/media/server/ipc/include/MediaPipelineModuleService.h index 6e30c7f4e..91787419a 100644 --- a/media/server/ipc/include/MediaPipelineModuleService.h +++ b/media/server/ipc/include/MediaPipelineModuleService.h @@ -46,7 +46,6 @@ class MediaPipelineModuleService : public IMediaPipelineModuleService void clientConnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) override; void clientDisconnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) override; - void setMetricsService(const std::shared_ptr &metricsService) override; void createSession(::google::protobuf::RpcController *controller, const ::firebolt::rialto::CreateSessionRequest *request, @@ -173,7 +172,6 @@ class MediaPipelineModuleService : public IMediaPipelineModuleService private: service::IMediaPipelineService &m_mediaPipelineService; - std::shared_ptr m_metricsService; std::map, std::set> m_clientSessions; }; } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/include/PrivateMetricsModuleService.h b/media/server/ipc/include/PrivateMetricsModuleService.h index 65f2a49d9..5684f7297 100644 --- a/media/server/ipc/include/PrivateMetricsModuleService.h +++ b/media/server/ipc/include/PrivateMetricsModuleService.h @@ -52,7 +52,6 @@ class PrivateMetricsModuleService : public IPrivateMetricsModuleService, void clientConnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) override; void clientDisconnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) override; - void notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) override; void notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) override; // PrivateMetricsModule RPC handlers diff --git a/media/server/ipc/include/SessionManagementServer.h b/media/server/ipc/include/SessionManagementServer.h index 00709b2fa..9c227c390 100644 --- a/media/server/ipc/include/SessionManagementServer.h +++ b/media/server/ipc/include/SessionManagementServer.h @@ -29,7 +29,6 @@ #include "IMediaPipelineModuleService.h" #include "IPlaybackService.h" #include "IPrivateMetricsModuleService.h" -#include "IPrivateMetricsService.h" #include "ISessionManagementServer.h" #include "IWebAudioPlayerModuleService.h" #include "SetLogLevelsService.h" @@ -55,7 +54,7 @@ class SessionManagementServer : public ISessionManagementServer const std::shared_ptr &privateMetricsModuleFactory, const std::shared_ptr &controlModuleFactory, service::IPlaybackService &playbackService, service::ICdmService &cdmService, - service::IControlService &controlService, service::IPrivateMetricsService &metricsService); + service::IControlService &controlService); ~SessionManagementServer() override; SessionManagementServer(const SessionManagementServer &) = delete; SessionManagementServer(SessionManagementServer &&) = delete; diff --git a/media/server/ipc/interface/IIpcFactory.h b/media/server/ipc/interface/IIpcFactory.h index c5370de38..668473761 100644 --- a/media/server/ipc/interface/IIpcFactory.h +++ b/media/server/ipc/interface/IIpcFactory.h @@ -24,7 +24,6 @@ #include "ICdmService.h" #include "IControlService.h" #include "IPlaybackService.h" -#include "IPrivateMetricsService.h" #include "ISessionManagementServer.h" #include "ISessionServerManager.h" #include @@ -41,8 +40,7 @@ class IIpcFactory createApplicationManagementServer(service::ISessionServerManager &sessionServerManager) const = 0; virtual std::unique_ptr createSessionManagementServer(service::IPlaybackService &playbackService, service::ICdmService &cdmService, - service::IControlService &controlService, - service::IPrivateMetricsService &metricsService) const = 0; + service::IControlService &controlService) const = 0; }; } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/interface/IpcFactory.h b/media/server/ipc/interface/IpcFactory.h index 4fafd5ce2..bb5d9e166 100644 --- a/media/server/ipc/interface/IpcFactory.h +++ b/media/server/ipc/interface/IpcFactory.h @@ -39,8 +39,7 @@ class IpcFactory : public IIpcFactory createApplicationManagementServer(service::ISessionServerManager &sessionServerManager) const override; std::unique_ptr createSessionManagementServer(service::IPlaybackService &playbackService, service::ICdmService &cdmService, - service::IControlService &controlService, - service::IPrivateMetricsService &metricsService) const override; + service::IControlService &controlService) const override; }; } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/proto/privatemetricsmodule.proto b/media/server/ipc/proto/privatemetricsmodule.proto deleted file mode 100644 index cdef9ba7d..000000000 --- a/media/server/ipc/proto/privatemetricsmodule.proto +++ /dev/null @@ -1 +0,0 @@ -../../../../proto/privatemetricsmodule.proto diff --git a/media/server/ipc/proto/privatemetricsmodule.proto b/media/server/ipc/proto/privatemetricsmodule.proto new file mode 120000 index 000000000..31c78e1ad --- /dev/null +++ b/media/server/ipc/proto/privatemetricsmodule.proto @@ -0,0 +1 @@ +../../../../proto/privatemetricsmodule.proto \ No newline at end of file diff --git a/media/server/ipc/source/IpcFactory.cpp b/media/server/ipc/source/IpcFactory.cpp index 20d9ca389..c178c7c67 100644 --- a/media/server/ipc/source/IpcFactory.cpp +++ b/media/server/ipc/source/IpcFactory.cpp @@ -43,8 +43,7 @@ IpcFactory::createApplicationManagementServer(service::ISessionServerManager &se std::unique_ptr IpcFactory::createSessionManagementServer(service::IPlaybackService &playbackService, service::ICdmService &cdmService, - service::IControlService &controlService, - service::IPrivateMetricsService &metricsService) const + service::IControlService &controlService) const { return std::make_unique< SessionManagementServer>(firebolt::rialto::ipc::IServerFactory::createFactory(), @@ -55,6 +54,6 @@ IpcFactory::createSessionManagementServer(service::IPlaybackService &playbackSer firebolt::rialto::server::ipc::IWebAudioPlayerModuleServiceFactory::createFactory(), firebolt::rialto::server::ipc::IPrivateMetricsModuleServiceFactory::createFactory(), firebolt::rialto::server::ipc::IControlModuleServiceFactory::createFactory(), - playbackService, cdmService, controlService, metricsService); + playbackService, cdmService, controlService); } } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/source/MediaPipelineClient.cpp b/media/server/ipc/source/MediaPipelineClient.cpp index 175f568b3..da48e5a4b 100644 --- a/media/server/ipc/source/MediaPipelineClient.cpp +++ b/media/server/ipc/source/MediaPipelineClient.cpp @@ -18,7 +18,6 @@ */ #include "MediaPipelineClient.h" -#include "IPrivateMetricsModuleService.h" #include "RialtoServerLogging.h" #include "mediapipelinemodule.pb.h" #include @@ -138,9 +137,9 @@ firebolt::rialto::PlaybackErrorEvent_PlaybackError convertPlaybackError(const fi namespace firebolt::rialto::server::ipc { -MediaPipelineClient::MediaPipelineClient(int sessionId, const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient, - IPrivateMetricsModuleService *metricsService) - : m_sessionId{sessionId}, m_ipcClient{ipcClient}, m_metricsService{metricsService} +MediaPipelineClient::MediaPipelineClient(int sessionId, + const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) + : m_sessionId{sessionId}, m_ipcClient{ipcClient} { } @@ -182,17 +181,6 @@ void MediaPipelineClient::notifyPlaybackState(PlaybackState state) { RIALTO_SERVER_LOG_DEBUG("Sending PlaybackStateChangeEvent..."); - if (m_metricsService) - { - PlaybackState oldState = m_currentPlaybackState; - m_currentPlaybackState = state; - m_metricsService->notifyPlaybackStateChanged(m_sessionId, oldState, state); - } - else - { - m_currentPlaybackState = state; - } - auto event = std::make_shared(); event->set_session_id(m_sessionId); event->set_state(convertPlaybackState(state)); diff --git a/media/server/ipc/source/MediaPipelineModuleService.cpp b/media/server/ipc/source/MediaPipelineModuleService.cpp index 7a475b18c..5b6cca472 100644 --- a/media/server/ipc/source/MediaPipelineModuleService.cpp +++ b/media/server/ipc/source/MediaPipelineModuleService.cpp @@ -298,11 +298,6 @@ MediaPipelineModuleService::MediaPipelineModuleService(service::IMediaPipelineSe MediaPipelineModuleService::~MediaPipelineModuleService() {} -void MediaPipelineModuleService::setMetricsService(const std::shared_ptr &metricsService) -{ - m_metricsService = metricsService; -} - void MediaPipelineModuleService::clientConnected(const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) { RIALTO_SERVER_LOG_INFO("Client Connected!"); @@ -350,8 +345,7 @@ void MediaPipelineModuleService::createSession(::google::protobuf::RpcController int sessionId = generateSessionId(); bool sessionCreated = m_mediaPipelineService.createSession(sessionId, - std::make_shared(sessionId, ipcController->getClient(), - m_metricsService.get()), + std::make_shared(sessionId, ipcController->getClient()), request->max_width(), request->max_height()); if (sessionCreated) { diff --git a/media/server/ipc/source/PrivateMetricsModuleService.cpp b/media/server/ipc/source/PrivateMetricsModuleService.cpp index b9e21b2d2..4770768e2 100644 --- a/media/server/ipc/source/PrivateMetricsModuleService.cpp +++ b/media/server/ipc/source/PrivateMetricsModuleService.cpp @@ -200,12 +200,6 @@ void PrivateMetricsModuleService::reportClientMetrics( m_metricsService.reportMetrics(clientId, metrics); } -void PrivateMetricsModuleService::notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, - PlaybackState newState) -{ - m_metricsService.notifyPlaybackStateChanged(sessionId, oldState, newState); -} - void PrivateMetricsModuleService::notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) { m_metricsService.notifyApplicationStateChanged(oldState, newState); @@ -254,8 +248,8 @@ void PrivateMetricsModuleService::requestMetricsSample(int clientId, std::uint64 if (!ipcClient->sendEvent(event)) { - RIALTO_SERVER_LOG_WARN("Failed to request client metrics sample=%" PRIu64 " from client %d", sampleId, - clientId); + RIALTO_SERVER_LOG_DEBUG("Failed to request client metrics sample=%" PRIu64 " from client %d", sampleId, + clientId); } } } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/source/SessionManagementServer.cpp b/media/server/ipc/source/SessionManagementServer.cpp index 05e17bd67..44f91e926 100644 --- a/media/server/ipc/source/SessionManagementServer.cpp +++ b/media/server/ipc/source/SessionManagementServer.cpp @@ -49,8 +49,7 @@ SessionManagementServer::SessionManagementServer( const std::shared_ptr &webAudioPlayerModuleFactory, const std::shared_ptr &privateMetricsModuleFactory, const std::shared_ptr &controlModuleFactory, service::IPlaybackService &playbackService, - service::ICdmService &cdmService, service::IControlService &controlService, - service::IPrivateMetricsService &metricsService) + service::ICdmService &cdmService, service::IControlService &controlService) : m_isRunning{false}, m_mediaPipelineModule{mediaPipelineModuleFactory->create(playbackService.getMediaPipelineService())}, m_mediaPipelineCapabilitiesModule{ @@ -58,11 +57,10 @@ SessionManagementServer::SessionManagementServer( m_mediaKeysModule{mediaKeysModuleFactory->create(cdmService)}, m_mediaKeysCapabilitiesModule{mediaKeysCapabilitiesModuleFactory->create(cdmService)}, m_webAudioPlayerModule{webAudioPlayerModuleFactory->create(playbackService.getWebAudioPlayerService())}, - m_privateMetricsModule{privateMetricsModuleFactory->create(metricsService)}, + m_privateMetricsModule{privateMetricsModuleFactory->create(playbackService.getPrivateMetricsService())}, m_controlModule{controlModuleFactory->create(playbackService, controlService)} { m_ipcServer = ipcFactory->create(); - m_mediaPipelineModule->setMetricsService(m_privateMetricsModule); } SessionManagementServer::~SessionManagementServer() diff --git a/media/server/main/include/IMetricsReporter.h b/media/server/main/include/IMetricsReporter.h index 707c07005..56914cda1 100644 --- a/media/server/main/include/IMetricsReporter.h +++ b/media/server/main/include/IMetricsReporter.h @@ -20,6 +20,7 @@ #ifndef FIREBOLT_RIALTO_SERVER_I_METRICS_REPORTER_H_ #define FIREBOLT_RIALTO_SERVER_I_METRICS_REPORTER_H_ +#include "ControlCommon.h" #include "StateMetricsAggregator.h" #include #include @@ -33,7 +34,9 @@ namespace firebolt::rialto::server struct PeriodicMetricsReport { std::uint64_t sampleId{0}; + std::uint64_t monotonicTimeMs{0}; std::string reason; + ApplicationState applicationState{ApplicationState::UNKNOWN}; std::string appName; std::uint32_t clientPid{0}; double clientCpuPercent{0.0}; diff --git a/media/server/main/include/LogMetricsReporter.h b/media/server/main/include/LogMetricsReporter.h index ff87d559e..017286e59 100644 --- a/media/server/main/include/LogMetricsReporter.h +++ b/media/server/main/include/LogMetricsReporter.h @@ -21,6 +21,8 @@ #define FIREBOLT_RIALTO_SERVER_LOG_METRICS_REPORTER_H_ #include "IMetricsReporter.h" +#include +#include namespace firebolt::rialto::server { @@ -36,6 +38,13 @@ class LogMetricsReporter : public IMetricsReporter void reportPeriodicSample(const PeriodicMetricsReport &report) override; void reportStateTransition(const StateTransitionReport &report) override; void reportThresholdExceeded(const ThresholdAlert &alert) override; + +private: + bool shouldReportPeriodicSample(const PeriodicMetricsReport &report); + static bool changedSignificantly(double current, double previous, double absoluteFloor); + + std::mutex m_mutex; + std::optional m_lastReportedSample; }; } // namespace firebolt::rialto::server diff --git a/media/server/main/include/MetricsCollector.h b/media/server/main/include/MetricsCollector.h index dde7443b8..6f03b6075 100644 --- a/media/server/main/include/MetricsCollector.h +++ b/media/server/main/include/MetricsCollector.h @@ -40,19 +40,23 @@ class MetricsCollectorFactory : public IMetricsCollectorFactory MetricsCollectorFactory() = default; ~MetricsCollectorFactory() override = default; - std::unique_ptr create(int clientId, - const std::shared_ptr &client) override; + std::unique_ptr + create(int clientId, const std::shared_ptr &client, + ApplicationState initialApplicationState) override; }; class MetricsCollector : public IMetricsCollector { public: MetricsCollector(int clientId, const std::shared_ptr &client, - const std::shared_ptr &timerFactory); + const std::shared_ptr &timerFactory, + ApplicationState initialApplicationState = ApplicationState::UNKNOWN); ~MetricsCollector() override; void processMetrics(const ClientMetricsData &metrics) override; void notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) override; + void notifyWebAudioPlayerStateChanged(int handle, WebAudioPlayerState oldState, + WebAudioPlayerState newState) override; void notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) override; private: @@ -77,7 +81,7 @@ class MetricsCollector : public IMetricsCollector struct SessionMetricsState { - PlaybackState currentPlaybackState{PlaybackState::UNKNOWN}; + std::string currentState; StateMetricsAggregator aggregator; }; @@ -87,132 +91,24 @@ class MetricsCollector : public IMetricsCollector std::uint64_t currentMonotonicTimeMs, std::uint64_t previousMonotonicTimeMs) const; static const char *sampleReasonToString(MetricsSampleReason reason); static const char *playbackStateToString(PlaybackState state); + static const char *webAudioPlayerStateToString(WebAudioPlayerState state); static const char *applicationStateToString(ApplicationState state); + void notifyPlayerStateChanged(const std::string &context, const char *oldState, const char *newState, + bool terminalState); const int m_clientId; std::shared_ptr m_client; std::unique_ptr m_timer; std::uint64_t m_nextSampleId{1}; + std::optional m_pendingPeriodicSampleId; + unsigned int m_pendingPeriodicTimerCount{0}; + bool m_clientResponsive{true}; std::mutex m_mutex; std::optional m_previousSample; - // Per-session state tracking (sessionId -> session state) - std::map m_sessionStates; - - // Global aggregator (active across all sessions while RUNNING) - StateMetricsAggregator m_globalAggregator; - ApplicationState m_currentApplicationState{ApplicationState::UNKNOWN}; - - // Pluggable metrics reporter (log, telemetry, or composite) - std::unique_ptr m_reporter; - - // Threshold checker - MetricsThresholdChecker m_thresholdChecker; -}; -} // namespace firebolt::rialto::server - -#endif // FIREBOLT_RIALTO_SERVER_METRICS_COLLECTOR_H_ -/* - * If not stated otherwise in this file or this component's LICENSE file the - * following copyright and licenses apply: - * - * Copyright 2026 Sky UK - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef FIREBOLT_RIALTO_SERVER_METRICS_COLLECTOR_H_ -#define FIREBOLT_RIALTO_SERVER_METRICS_COLLECTOR_H_ - -#include "IMetricsCollector.h" -#include "IMetricsReporter.h" -#include "ITimer.h" -#include "MetricsThresholdChecker.h" -#include "StateMetricsAggregator.h" -#include -#include -#include -#include -#include -#include - -namespace firebolt::rialto::server -{ -class MetricsCollectorFactory : public IMetricsCollectorFactory -{ -public: - MetricsCollectorFactory() = default; - ~MetricsCollectorFactory() override = default; - - std::unique_ptr create(int clientId, - const std::shared_ptr &client) override; -}; - -class MetricsCollector : public IMetricsCollector -{ -public: - MetricsCollector(int clientId, const std::shared_ptr &client, - const std::shared_ptr &timerFactory); - ~MetricsCollector() override; - - void processMetrics(const ClientMetricsData &metrics) override; - void notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) override; - void notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) override; - -private: - struct ProcessMetricsSample - { - std::uint64_t monotonicTimeMs{0}; - std::uint64_t epochTimeMs{0}; - std::uint64_t processCpuTimeMs{0}; - std::uint64_t processMemoryKb{0}; - std::uint64_t cgroupMemoryUsageKb{0}; - std::uint64_t cgroupMemoryLimitKb{0}; - }; - - struct PreviousSample - { - std::uint64_t clientMonotonicTimeMs{0}; - std::uint64_t clientCpuTimeMs{0}; - std::uint64_t clientMemoryKb{0}; - ProcessMetricsSample serverMetrics; - }; - - struct SessionMetricsState - { - PlaybackState currentPlaybackState{PlaybackState::UNKNOWN}; - StateMetricsAggregator aggregator; - }; - - void onTimerFired(); - ProcessMetricsSample getServerMetrics() const; - double calculateCpuPercentage(std::uint64_t currentCpuTimeMs, std::uint64_t previousCpuTimeMs, - std::uint64_t currentMonotonicTimeMs, std::uint64_t previousMonotonicTimeMs) const; - static const char *sampleReasonToString(MetricsSampleReason reason); - static const char *playbackStateToString(PlaybackState state); - static const char *applicationStateToString(ApplicationState state); - - const int m_clientId; - std::shared_ptr m_client; - std::unique_ptr m_timer; - std::uint64_t m_nextSampleId{1}; - - std::mutex m_mutex; - std::optional m_previousSample; - - // Per-session state tracking (sessionId -> session state) - std::map m_sessionStates; + // Per-player state tracking (typed context -> state) + std::map m_sessionStates; // Global aggregator (active across all sessions while RUNNING) StateMetricsAggregator m_globalAggregator; diff --git a/media/server/main/interface/IMainThread.h b/media/server/main/interface/IMainThread.h index fa391bfc0..f8bd50e2e 100644 --- a/media/server/main/interface/IMainThread.h +++ b/media/server/main/interface/IMainThread.h @@ -20,7 +20,6 @@ #ifndef FIREBOLT_RIALTO_SERVER_I_MAIN_THREAD_H_ #define FIREBOLT_RIALTO_SERVER_I_MAIN_THREAD_H_ -#include "IMainThread.h" #include #include #include diff --git a/media/server/main/interface/IMetricsCollector.h b/media/server/main/interface/IMetricsCollector.h index 60ab1b3ed..5e8088239 100644 --- a/media/server/main/interface/IMetricsCollector.h +++ b/media/server/main/interface/IMetricsCollector.h @@ -62,11 +62,13 @@ class IMetricsCollectorFactory * * @param clientId Unique client identifier. * @param client Callback interface for requesting samples from the client. + * @param initialApplicationState Application state when the client connected. * * @return The new MetricsCollector instance, or nullptr on failure. */ - virtual std::unique_ptr create(int clientId, - const std::shared_ptr &client) = 0; + virtual std::unique_ptr + create(int clientId, const std::shared_ptr &client, + ApplicationState initialApplicationState) = 0; }; /** @@ -102,6 +104,14 @@ class IMetricsCollector */ virtual void notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) = 0; + /** + * @brief Notify that a WebAudio player's state has changed. + * + * Finalizes the old state's aggregator and begins a new one. + */ + virtual void notifyWebAudioPlayerStateChanged(int handle, WebAudioPlayerState oldState, + WebAudioPlayerState newState) = 0; + /** * @brief Notify that the application state has changed (RUNNING/INACTIVE). * diff --git a/media/server/main/source/LogMetricsReporter.cpp b/media/server/main/source/LogMetricsReporter.cpp index 6d9dbfbcb..282d1c247 100644 --- a/media/server/main/source/LogMetricsReporter.cpp +++ b/media/server/main/source/LogMetricsReporter.cpp @@ -19,12 +19,27 @@ #include "LogMetricsReporter.h" #include "RialtoServerLogging.h" +#include +#include #include +namespace +{ +constexpr std::uint64_t kActiveReportIntervalMs{10 * 60 * 1000}; +constexpr double kRelativeChangeTolerance{0.10}; +constexpr double kCpuAbsoluteFloor{1.0}; +constexpr double kMemoryAbsoluteFloorKb{1024.0}; +} // namespace + namespace firebolt::rialto::server { void LogMetricsReporter::reportPeriodicSample(const PeriodicMetricsReport &report) { + if (!shouldReportPeriodicSample(report)) + { + return; + } + RIALTO_SERVER_LOG_MIL("Metrics sample=%" PRIu64 ", reason=%s, app='%s', client_pid=%u, client_cpu=%.2f%%, " "server_cpu=%.2f%%, combined_cpu=%.2f%%, client_cpu_ms=%" PRIu64 ", " "server_cpu_ms=%" PRIu64 ", client_mem_kb=%" PRIu64 ", server_mem_kb=%" PRIu64 ", " @@ -35,6 +50,56 @@ void LogMetricsReporter::reportPeriodicSample(const PeriodicMetricsReport &repor report.serverMemoryKb, report.shmMemoryKb, report.cgroupMemoryUsageKb, report.cgroupMemoryLimitKb); } +bool LogMetricsReporter::shouldReportPeriodicSample(const PeriodicMetricsReport &report) +{ + std::lock_guard lock{m_mutex}; + + if (report.reason != "PERIODIC") + { + return false; + } + + if (!m_lastReportedSample) + { + m_lastReportedSample = report; + return true; + } + + const auto &previous{*m_lastReportedSample}; + const bool stateChanged{report.applicationState != previous.applicationState}; + const bool metricsChanged{ + changedSignificantly(report.clientCpuPercent, previous.clientCpuPercent, kCpuAbsoluteFloor) || + changedSignificantly(report.serverCpuPercent, previous.serverCpuPercent, kCpuAbsoluteFloor) || + changedSignificantly(report.combinedCpuPercent, previous.combinedCpuPercent, kCpuAbsoluteFloor) || + changedSignificantly(static_cast(report.clientMemoryKb), + static_cast(previous.clientMemoryKb), kMemoryAbsoluteFloorKb) || + changedSignificantly(static_cast(report.serverMemoryKb), + static_cast(previous.serverMemoryKb), kMemoryAbsoluteFloorKb) || + changedSignificantly(static_cast(report.cgroupMemoryUsageKb), + static_cast(previous.cgroupMemoryUsageKb), kMemoryAbsoluteFloorKb) || + changedSignificantly(static_cast(report.cgroupMemoryLimitKb), + static_cast(previous.cgroupMemoryLimitKb), kMemoryAbsoluteFloorKb) || + changedSignificantly(static_cast(report.shmMemoryKb), static_cast(previous.shmMemoryKb), + kMemoryAbsoluteFloorKb)}; + const bool activeIntervalElapsed{report.applicationState == ApplicationState::RUNNING && + report.monotonicTimeMs >= previous.monotonicTimeMs && + report.monotonicTimeMs - previous.monotonicTimeMs >= kActiveReportIntervalMs}; + + if (stateChanged || metricsChanged || activeIntervalElapsed) + { + m_lastReportedSample = report; + return true; + } + + return false; +} + +bool LogMetricsReporter::changedSignificantly(double current, double previous, double absoluteFloor) +{ + const double threshold{std::max(std::abs(previous) * kRelativeChangeTolerance, absoluteFloor)}; + return std::abs(current - previous) >= threshold; +} + void LogMetricsReporter::reportStateTransition(const StateTransitionReport &report) { const auto &r{report.metrics}; diff --git a/media/server/main/source/MetricsCollector.cpp b/media/server/main/source/MetricsCollector.cpp index c55a30f28..c19838d03 100644 --- a/media/server/main/source/MetricsCollector.cpp +++ b/media/server/main/source/MetricsCollector.cpp @@ -31,6 +31,7 @@ namespace { constexpr std::chrono::seconds kMetricsInterval{15}; constexpr std::uint64_t kMinElapsedMs{100}; +constexpr unsigned int kResponseTimeoutTimerCount{2}; } // namespace namespace firebolt::rialto::server @@ -50,13 +51,14 @@ std::shared_ptr IMetricsCollectorFactory::createFactor } std::unique_ptr -MetricsCollectorFactory::create(int clientId, const std::shared_ptr &client) +MetricsCollectorFactory::create(int clientId, const std::shared_ptr &client, + ApplicationState initialApplicationState) { std::unique_ptr collector; try { auto timerFactory = firebolt::rialto::common::ITimerFactory::getFactory(); - collector = std::make_unique(clientId, client, timerFactory); + collector = std::make_unique(clientId, client, timerFactory, initialApplicationState); } catch (const std::exception &e) { @@ -66,10 +68,22 @@ MetricsCollectorFactory::create(int clientId, const std::shared_ptr &client, - const std::shared_ptr &timerFactory) - : m_clientId{clientId}, m_client{client}, m_reporter{std::make_unique()}, + const std::shared_ptr &timerFactory, + ApplicationState initialApplicationState) + : m_clientId{clientId}, m_client{client}, m_currentApplicationState{initialApplicationState}, + m_reporter{std::make_unique()}, m_thresholdChecker{MetricsThresholdConfig{}, m_reporter.get()} { + if (m_currentApplicationState == ApplicationState::RUNNING) + { + using std::chrono::duration_cast; + using std::chrono::milliseconds; + using std::chrono::steady_clock; + const auto kNowMs{ + static_cast(duration_cast(steady_clock::now().time_since_epoch()).count())}; + m_globalAggregator.begin(applicationStateToString(m_currentApplicationState), kNowMs); + } + m_timer = timerFactory->createTimer(kMetricsInterval, [this]() { onTimerFired(); }, firebolt::rialto::common::TimerType::PERIODIC); @@ -87,9 +101,32 @@ MetricsCollector::~MetricsCollector() void MetricsCollector::onTimerFired() { - RIALTO_SERVER_LOG_MIL("Metrics: periodic timer fired for client %d, requesting sample=%" PRIu64, m_clientId, - m_nextSampleId); - m_client->requestMetricsSample(m_clientId, m_nextSampleId++, MetricsSampleReason::PERIODIC); + std::uint64_t sampleId{0}; + bool becameUnresponsive{false}; + { + std::lock_guard lock{m_mutex}; + if (m_pendingPeriodicSampleId && ++m_pendingPeriodicTimerCount < kResponseTimeoutTimerCount) + { + return; + } + + if (m_pendingPeriodicSampleId && m_clientResponsive) + { + m_clientResponsive = false; + becameUnresponsive = true; + } + + sampleId = m_nextSampleId++; + m_pendingPeriodicSampleId = sampleId; + m_pendingPeriodicTimerCount = 0; + } + + if (becameUnresponsive) + { + RIALTO_SERVER_LOG_WARN("Metrics client %d is not responding to sample requests", m_clientId); + } + RIALTO_SERVER_LOG_DEBUG("Requesting periodic metrics sample=%" PRIu64 " from client %d", sampleId, m_clientId); + m_client->requestMetricsSample(m_clientId, sampleId, MetricsSampleReason::PERIODIC); } void MetricsCollector::processMetrics(const ClientMetricsData &metrics) @@ -97,9 +134,30 @@ void MetricsCollector::processMetrics(const ClientMetricsData &metrics) const auto kServerMetrics{getServerMetrics()}; std::optional previous; + ApplicationState applicationState{ApplicationState::UNKNOWN}; + bool becameResponsive{false}; { std::lock_guard lock{m_mutex}; previous = m_previousSample; + applicationState = m_currentApplicationState; + if (metrics.reason == MetricsSampleReason::PERIODIC) + { + if (m_pendingPeriodicSampleId && metrics.sampleId == *m_pendingPeriodicSampleId) + { + m_pendingPeriodicSampleId.reset(); + m_pendingPeriodicTimerCount = 0; + if (!m_clientResponsive) + { + m_clientResponsive = true; + becameResponsive = true; + } + } + } + } + + if (becameResponsive) + { + RIALTO_SERVER_LOG_INFO("Metrics client %d is responding again", m_clientId); } if (!previous.has_value()) @@ -138,7 +196,9 @@ void MetricsCollector::processMetrics(const ClientMetricsData &metrics) { PeriodicMetricsReport periodicReport; periodicReport.sampleId = metrics.sampleId; + periodicReport.monotonicTimeMs = kServerMetrics.monotonicTimeMs; periodicReport.reason = sampleReasonToString(metrics.reason); + periodicReport.applicationState = applicationState; periodicReport.appName = metrics.appName; periodicReport.clientPid = metrics.processId; periodicReport.clientCpuPercent = kClientCpuPercentage; @@ -171,8 +231,9 @@ void MetricsCollector::processMetrics(const ClientMetricsData &metrics) std::lock_guard lock{m_mutex}; // Feed into per-session aggregators - for (auto &[sessionId, sessionState] : m_sessionStates) + for (auto &[unusedContext, sessionState] : m_sessionStates) { + (void)unusedContext; sessionState.aggregator.addSample(sample); } @@ -199,8 +260,25 @@ void MetricsCollector::processMetrics(const ClientMetricsData &metrics) void MetricsCollector::notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) { - RIALTO_SERVER_LOG_MIL("Metrics: PlaybackState changed session=%d, %s -> %s", sessionId, - playbackStateToString(oldState), playbackStateToString(newState)); + notifyPlayerStateChanged("media-pipeline=" + std::to_string(sessionId), playbackStateToString(oldState), + playbackStateToString(newState), + newState == PlaybackState::STOPPED || newState == PlaybackState::END_OF_STREAM || + newState == PlaybackState::FAILURE); +} + +void MetricsCollector::notifyWebAudioPlayerStateChanged(int handle, WebAudioPlayerState oldState, + WebAudioPlayerState newState) +{ + notifyPlayerStateChanged("web-audio=" + std::to_string(handle), webAudioPlayerStateToString(oldState), + webAudioPlayerStateToString(newState), + newState == WebAudioPlayerState::END_OF_STREAM || + newState == WebAudioPlayerState::FAILURE); +} + +void MetricsCollector::notifyPlayerStateChanged(const std::string &context, const char *oldState, const char *newState, + bool terminalState) +{ + RIALTO_SERVER_LOG_MIL("Metrics: PlaybackState changed %s, %s -> %s", context.c_str(), oldState, newState); using std::chrono::duration_cast; using std::chrono::milliseconds; @@ -209,14 +287,14 @@ void MetricsCollector::notifyPlaybackStateChanged(int sessionId, PlaybackState o static_cast(duration_cast(steady_clock::now().time_since_epoch()).count())}; std::lock_guard lock{m_mutex}; - auto sessionIter{m_sessionStates.find(sessionId)}; + auto sessionIter{m_sessionStates.find(context)}; if (m_sessionStates.end() == sessionIter) { // First state notification for this session — create entry SessionMetricsState sessionState; - sessionState.currentPlaybackState = newState; - sessionState.aggregator.begin(playbackStateToString(newState), kNowMs); - m_sessionStates.emplace(sessionId, std::move(sessionState)); + sessionState.currentState = newState; + sessionState.aggregator.begin(newState, kNowMs); + m_sessionStates.emplace(context, std::move(sessionState)); return; } @@ -227,13 +305,12 @@ void MetricsCollector::notifyPlaybackStateChanged(int sessionId, PlaybackState o { auto report{sessionState.aggregator.finalize(kNowMs)}; StateTransitionReport transitionReport; - transitionReport.context = "session=" + std::to_string(sessionId); + transitionReport.context = context; transitionReport.metrics = report; m_reporter->reportStateTransition(transitionReport); } - if (newState == PlaybackState::STOPPED || newState == PlaybackState::END_OF_STREAM || - newState == PlaybackState::FAILURE) + if (terminalState) { // Terminal state — remove session tracking m_sessionStates.erase(sessionIter); @@ -241,8 +318,8 @@ void MetricsCollector::notifyPlaybackStateChanged(int sessionId, PlaybackState o else { // Begin accumulating for new state - sessionState.currentPlaybackState = newState; - sessionState.aggregator.begin(playbackStateToString(newState), kNowMs); + sessionState.currentState = newState; + sessionState.aggregator.begin(newState, kNowMs); } // Request immediate sample for clean boundary @@ -480,6 +557,26 @@ const char *MetricsCollector::playbackStateToString(PlaybackState state) } } +const char *MetricsCollector::webAudioPlayerStateToString(WebAudioPlayerState state) +{ + switch (state) + { + case WebAudioPlayerState::IDLE: + return "IDLE"; + case WebAudioPlayerState::PLAYING: + return "PLAYING"; + case WebAudioPlayerState::PAUSED: + return "PAUSED"; + case WebAudioPlayerState::END_OF_STREAM: + return "END_OF_STREAM"; + case WebAudioPlayerState::FAILURE: + return "FAILURE"; + case WebAudioPlayerState::UNKNOWN: + default: + return "UNKNOWN"; + } +} + const char *MetricsCollector::applicationStateToString(ApplicationState state) { switch (state) diff --git a/media/server/service/CMakeLists.txt b/media/server/service/CMakeLists.txt index 8969c8172..9737fd11b 100644 --- a/media/server/service/CMakeLists.txt +++ b/media/server/service/CMakeLists.txt @@ -35,7 +35,9 @@ add_library ( source/ControlService.cpp source/SessionServerManager.cpp source/MediaPipelineService.cpp + source/MediaPipelineMetricsClient.cpp source/WebAudioPlayerService.cpp + source/WebAudioPlayerMetricsClient.cpp source/PrivateMetricsService.cpp ) set_target_properties ( diff --git a/media/server/service/include/IPlaybackService.h b/media/server/service/include/IPlaybackService.h index e3e8860ed..10fec053d 100644 --- a/media/server/service/include/IPlaybackService.h +++ b/media/server/service/include/IPlaybackService.h @@ -22,6 +22,7 @@ #include "IMediaPipelineService.h" #include "ISharedMemoryBuffer.h" +#include "IPrivateMetricsService.h" #include "IWebAudioPlayerService.h" #include "MediaCommon.h" #include @@ -56,6 +57,7 @@ class IPlaybackService virtual std::shared_ptr getShmBuffer() const = 0; virtual IMediaPipelineService &getMediaPipelineService() const = 0; virtual IWebAudioPlayerService &getWebAudioPlayerService() const = 0; + virtual IPrivateMetricsService &getPrivateMetricsService() const = 0; virtual void ping(const std::shared_ptr &heartbeatProcedure) const = 0; }; } // namespace firebolt::rialto::server::service diff --git a/media/server/service/include/IPrivateMetricsService.h b/media/server/service/include/IPrivateMetricsService.h index fdefe6ea4..e3c9f1d50 100644 --- a/media/server/service/include/IPrivateMetricsService.h +++ b/media/server/service/include/IPrivateMetricsService.h @@ -47,7 +47,9 @@ class IPrivateMetricsService * @param clientId Unique client identifier. * @param client Callback interface for requesting samples from the client. */ - virtual void clientReady(int clientId, const std::shared_ptr &client) = 0; + virtual void + clientReady(int clientId, + const std::shared_ptr &client) = 0; /** * @brief A client has disconnected. @@ -75,6 +77,14 @@ class IPrivateMetricsService */ virtual void notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) = 0; + /** + * @brief Notify that a WebAudio player's state has changed. + * + * Routes to all active MetricsCollector instances. + */ + virtual void notifyWebAudioPlayerStateChanged(int handle, WebAudioPlayerState oldState, + WebAudioPlayerState newState) = 0; + /** * @brief Notify that the application state has changed (RUNNING/INACTIVE). * diff --git a/media/server/service/source/MediaPipelineMetricsClient.cpp b/media/server/service/source/MediaPipelineMetricsClient.cpp new file mode 100644 index 000000000..98e6c06cb --- /dev/null +++ b/media/server/service/source/MediaPipelineMetricsClient.cpp @@ -0,0 +1,72 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MediaPipelineMetricsClient.h" + +namespace firebolt::rialto::server::service +{ +MediaPipelineMetricsClient::MediaPipelineMetricsClient(int sessionId, const std::shared_ptr &client, + IPrivateMetricsService &metricsService) + : m_sessionId{sessionId}, m_client{client}, m_metricsService{metricsService} +{ +} + +void MediaPipelineMetricsClient::notifyDuration(int64_t duration) { m_client->notifyDuration(duration); } +void MediaPipelineMetricsClient::notifyPosition(int64_t position) { m_client->notifyPosition(position); } +void MediaPipelineMetricsClient::notifyNativeSize(uint32_t width, uint32_t height, double aspect) +{ + m_client->notifyNativeSize(width, height, aspect); +} +void MediaPipelineMetricsClient::notifyNetworkState(NetworkState state) { m_client->notifyNetworkState(state); } +void MediaPipelineMetricsClient::notifyPlaybackState(PlaybackState state) +{ + m_metricsService.notifyPlaybackStateChanged(m_sessionId, m_currentPlaybackState, state); + m_currentPlaybackState = state; + m_client->notifyPlaybackState(state); +} +void MediaPipelineMetricsClient::notifyVideoData(bool hasData) { m_client->notifyVideoData(hasData); } +void MediaPipelineMetricsClient::notifyAudioData(bool hasData) { m_client->notifyAudioData(hasData); } +void MediaPipelineMetricsClient::notifyNeedMediaData(int32_t sourceId, size_t frameCount, uint32_t needDataRequestId, + const std::shared_ptr &shmInfo) +{ + m_client->notifyNeedMediaData(sourceId, frameCount, needDataRequestId, shmInfo); +} +void MediaPipelineMetricsClient::notifyCancelNeedMediaData(int32_t sourceId) +{ + m_client->notifyCancelNeedMediaData(sourceId); +} +void MediaPipelineMetricsClient::notifyQos(int32_t sourceId, const QosInfo &qosInfo) +{ + m_client->notifyQos(sourceId, qosInfo); +} +void MediaPipelineMetricsClient::notifyBufferUnderflow(int32_t sourceId) { m_client->notifyBufferUnderflow(sourceId); } +void MediaPipelineMetricsClient::notifyFirstFrameReceived(int32_t sourceId) +{ + m_client->notifyFirstFrameReceived(sourceId); +} +void MediaPipelineMetricsClient::notifyPlaybackError(int32_t sourceId, PlaybackError error) +{ + m_client->notifyPlaybackError(sourceId, error); +} +void MediaPipelineMetricsClient::notifySourceFlushed(int32_t sourceId) { m_client->notifySourceFlushed(sourceId); } +void MediaPipelineMetricsClient::notifyPlaybackInfo(const PlaybackInfo &playbackInfo) +{ + m_client->notifyPlaybackInfo(playbackInfo); +} +} // namespace firebolt::rialto::server::service diff --git a/media/server/service/source/MediaPipelineMetricsClient.h b/media/server/service/source/MediaPipelineMetricsClient.h new file mode 100644 index 000000000..4575c62e0 --- /dev/null +++ b/media/server/service/source/MediaPipelineMetricsClient.h @@ -0,0 +1,61 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_SERVICE_MEDIA_PIPELINE_METRICS_CLIENT_H_ +#define FIREBOLT_RIALTO_SERVER_SERVICE_MEDIA_PIPELINE_METRICS_CLIENT_H_ + +#include "IMediaPipelineClient.h" +#include "IPrivateMetricsService.h" +#include + +namespace firebolt::rialto::server::service +{ +class MediaPipelineMetricsClient : public IMediaPipelineClient +{ +public: + MediaPipelineMetricsClient(int sessionId, const std::shared_ptr &client, + IPrivateMetricsService &metricsService); + ~MediaPipelineMetricsClient() override = default; + + void notifyDuration(int64_t duration) override; + void notifyPosition(int64_t position) override; + void notifyNativeSize(uint32_t width, uint32_t height, double aspect) override; + void notifyNetworkState(NetworkState state) override; + void notifyPlaybackState(PlaybackState state) override; + void notifyVideoData(bool hasData) override; + void notifyAudioData(bool hasData) override; + void notifyNeedMediaData(int32_t sourceId, size_t frameCount, uint32_t needDataRequestId, + const std::shared_ptr &shmInfo) override; + void notifyCancelNeedMediaData(int32_t sourceId) override; + void notifyQos(int32_t sourceId, const QosInfo &qosInfo) override; + void notifyBufferUnderflow(int32_t sourceId) override; + void notifyFirstFrameReceived(int32_t sourceId) override; + void notifyPlaybackError(int32_t sourceId, PlaybackError error) override; + void notifySourceFlushed(int32_t sourceId) override; + void notifyPlaybackInfo(const PlaybackInfo &playbackInfo) override; + +private: + int m_sessionId; + std::shared_ptr m_client; + IPrivateMetricsService &m_metricsService; + PlaybackState m_currentPlaybackState{PlaybackState::UNKNOWN}; +}; +} // namespace firebolt::rialto::server::service + +#endif // FIREBOLT_RIALTO_SERVER_SERVICE_MEDIA_PIPELINE_METRICS_CLIENT_H_ diff --git a/media/server/service/source/MediaPipelineService.cpp b/media/server/service/source/MediaPipelineService.cpp index 717063564..184051f84 100644 --- a/media/server/service/source/MediaPipelineService.cpp +++ b/media/server/service/source/MediaPipelineService.cpp @@ -19,6 +19,7 @@ #include "MediaPipelineService.h" #include "IMediaPipelineServerInternal.h" +#include "MediaPipelineMetricsClient.h" #include "RialtoServerLogging.h" #include #include @@ -31,10 +32,10 @@ namespace firebolt::rialto::server::service MediaPipelineService::MediaPipelineService( IPlaybackService &playbackService, std::shared_ptr &&mediaPipelineFactory, std::shared_ptr &&mediaPipelineCapabilitiesFactory, - IDecryptionService &decryptionService) + IDecryptionService &decryptionService, IPrivateMetricsService &metricsService) : m_playbackService{playbackService}, m_mediaPipelineFactory{std::move(mediaPipelineFactory)}, m_mediaPipelineCapabilities{mediaPipelineCapabilitiesFactory->createMediaPipelineCapabilities()}, - m_decryptionService{decryptionService} + m_decryptionService{decryptionService}, m_metricsService{metricsService} { if (!m_mediaPipelineCapabilities) { @@ -80,7 +81,9 @@ bool MediaPipelineService::createSession(int sessionId, const std::shared_ptrcreateMediaPipelineServerInternal(mediaPipelineClient, + m_mediaPipelineFactory->createMediaPipelineServerInternal( + std::make_shared(sessionId, mediaPipelineClient, + m_metricsService), VideoRequirements{maxWidth, maxHeight}, sessionId, shmBuffer, m_decryptionService))); diff --git a/media/server/service/source/MediaPipelineService.h b/media/server/service/source/MediaPipelineService.h index 7e777c777..ef8e77542 100644 --- a/media/server/service/source/MediaPipelineService.h +++ b/media/server/service/source/MediaPipelineService.h @@ -25,6 +25,7 @@ #include "IMediaPipelineServerInternal.h" #include "IMediaPipelineService.h" #include "IPlaybackService.h" +#include "IPrivateMetricsService.h" #include "ISharedMemoryBuffer.h" #include #include @@ -45,7 +46,7 @@ class MediaPipelineService : public IMediaPipelineService MediaPipelineService(IPlaybackService &playbackService, std::shared_ptr &&mediaPipelineFactory, std::shared_ptr &&mediaPipelineCapabilitiesFactory, - IDecryptionService &decryptionService); + IDecryptionService &decryptionService, IPrivateMetricsService &metricsService); ~MediaPipelineService() override; MediaPipelineService(const MediaPipelineService &) = delete; MediaPipelineService(MediaPipelineService &&) = delete; @@ -113,6 +114,7 @@ class MediaPipelineService : public IMediaPipelineService std::shared_ptr m_mediaPipelineFactory; std::shared_ptr m_mediaPipelineCapabilities; IDecryptionService &m_decryptionService; + IPrivateMetricsService &m_metricsService; std::map> m_mediaPipelines; std::mutex m_mediaPipelineMutex; }; diff --git a/media/server/service/source/PlaybackService.cpp b/media/server/service/source/PlaybackService.cpp index ac8794985..90f74984c 100644 --- a/media/server/service/source/PlaybackService.cpp +++ b/media/server/service/source/PlaybackService.cpp @@ -18,6 +18,8 @@ */ #include "PlaybackService.h" +#include "IMetricsCollector.h" +#include "PrivateMetricsService.h" #include "IMediaPipelineServerInternal.h" #include "IWebAudioPlayerServerInternal.h" #include "RialtoServerLogging.h" @@ -36,10 +38,12 @@ PlaybackService::PlaybackService(std::shared_ptr &&shmBufferFactory, IDecryptionService &decryptionService) : m_shmBufferFactory{std::move(shmBufferFactory)}, m_isActive{false}, m_maxPlaybacks{0}, m_maxWebAudioPlayers{0}, + m_privateMetricsService{std::make_unique(IMetricsCollectorFactory::createFactory())}, m_mediaPipelineService{std::make_unique(*this, std::move(mediaPipelineFactory), std::move(mediaPipelineCapabilitiesFactory), - decryptionService)}, - m_webAudioPlayerService{std::make_unique(*this, std::move(webAudioPlayerFactory))} + decryptionService, *m_privateMetricsService)}, + m_webAudioPlayerService{ + std::make_unique(*this, std::move(webAudioPlayerFactory), *m_privateMetricsService)} { RIALTO_SERVER_LOG_DEBUG("PlaybackService is constructed"); } @@ -151,6 +155,11 @@ IWebAudioPlayerService &PlaybackService::getWebAudioPlayerService() const return *m_webAudioPlayerService; } +IPrivateMetricsService &PlaybackService::getPrivateMetricsService() const +{ + return *m_privateMetricsService; +} + void PlaybackService::ping(const std::shared_ptr &heartbeatProcedure) const { m_mediaPipelineService->ping(heartbeatProcedure); diff --git a/media/server/service/source/PlaybackService.h b/media/server/service/source/PlaybackService.h index abe226186..0564e563c 100644 --- a/media/server/service/source/PlaybackService.h +++ b/media/server/service/source/PlaybackService.h @@ -24,6 +24,7 @@ #include "IHeartbeatProcedure.h" #include "IMediaPipelineCapabilities.h" #include "IMediaPipelineServerInternal.h" +#include "IPrivateMetricsService.h" #include "IPlaybackService.h" #include "ISharedMemoryBuffer.h" #include "IWebAudioPlayerServerInternal.h" @@ -70,6 +71,7 @@ class PlaybackService : public IPlaybackService std::shared_ptr getShmBuffer() const override; IMediaPipelineService &getMediaPipelineService() const override; IWebAudioPlayerService &getWebAudioPlayerService() const override; + IPrivateMetricsService &getPrivateMetricsService() const override; void ping(const std::shared_ptr &heartbeatProcedure) const override; private: @@ -78,6 +80,7 @@ class PlaybackService : public IPlaybackService std::atomic m_maxPlaybacks; std::atomic m_maxWebAudioPlayers; std::shared_ptr m_shmBuffer; + std::unique_ptr m_privateMetricsService; std::unique_ptr m_mediaPipelineService; std::unique_ptr m_webAudioPlayerService; }; diff --git a/media/server/service/source/PrivateMetricsService.cpp b/media/server/service/source/PrivateMetricsService.cpp index 88096565b..fb5643286 100644 --- a/media/server/service/source/PrivateMetricsService.cpp +++ b/media/server/service/source/PrivateMetricsService.cpp @@ -41,7 +41,7 @@ void PrivateMetricsService::clientReady(int clientId, const std::shared_ptr &client) { std::lock_guard lock{m_mutex}; - auto collector = m_collectorFactory->create(clientId, client); + auto collector = m_collectorFactory->create(clientId, client, m_currentApplicationState); if (collector) { m_collectors.emplace(clientId, std::move(collector)); @@ -87,9 +87,20 @@ void PrivateMetricsService::notifyPlaybackStateChanged(int sessionId, PlaybackSt } } +void PrivateMetricsService::notifyWebAudioPlayerStateChanged(int handle, WebAudioPlayerState oldState, + WebAudioPlayerState newState) +{ + std::lock_guard lock{m_mutex}; + for (auto &[clientId, collector] : m_collectors) + { + collector->notifyWebAudioPlayerStateChanged(handle, oldState, newState); + } +} + void PrivateMetricsService::notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) { std::lock_guard lock{m_mutex}; + m_currentApplicationState = newState; for (auto &[clientId, collector] : m_collectors) { collector->notifyApplicationStateChanged(oldState, newState); diff --git a/media/server/service/source/PrivateMetricsService.h b/media/server/service/source/PrivateMetricsService.h index a8900c773..7349951d9 100644 --- a/media/server/service/source/PrivateMetricsService.h +++ b/media/server/service/source/PrivateMetricsService.h @@ -38,12 +38,15 @@ class PrivateMetricsService : public IPrivateMetricsService void clientDisconnected(int clientId) override; void reportMetrics(int clientId, const firebolt::rialto::server::ClientMetricsData &metrics) override; void notifyPlaybackStateChanged(int sessionId, PlaybackState oldState, PlaybackState newState) override; + void notifyWebAudioPlayerStateChanged(int handle, WebAudioPlayerState oldState, + WebAudioPlayerState newState) override; void notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) override; private: std::shared_ptr m_collectorFactory; std::mutex m_mutex; std::map> m_collectors; + ApplicationState m_currentApplicationState{ApplicationState::UNKNOWN}; }; } // namespace firebolt::rialto::server::service diff --git a/media/server/service/source/SessionServerManager.cpp b/media/server/service/source/SessionServerManager.cpp index 921bcadb6..a97298ddc 100644 --- a/media/server/service/source/SessionServerManager.cpp +++ b/media/server/service/source/SessionServerManager.cpp @@ -20,9 +20,7 @@ #include "SessionServerManager.h" #include "IApplicationManagementServer.h" #include "IIpcFactory.h" -#include "IMetricsCollector.h" #include "ISessionManagementServer.h" -#include "PrivateMetricsService.h" #include "RialtoServerLogging.h" #include @@ -44,11 +42,8 @@ SessionServerManager::SessionServerManager(const ipc::IIpcFactory &ipcFactory, I std::unique_ptr &&heartbeatProcedureFactory) : m_playbackService{playbackService}, m_cdmService{cdmService}, m_controlService{controlService}, m_heartbeatProcedureFactory{std::move(heartbeatProcedureFactory)}, - m_privateMetricsService{std::make_unique( - firebolt::rialto::server::IMetricsCollectorFactory::createFactory())}, m_applicationManagementServer{ipcFactory.createApplicationManagementServer(*this)}, - m_sessionManagementServer{ - ipcFactory.createSessionManagementServer(playbackService, cdmService, controlService, *m_privateMetricsService)}, + m_sessionManagementServer{ipcFactory.createSessionManagementServer(playbackService, cdmService, controlService)}, m_isServiceRunning{true}, m_currentState{common::SessionServerState::UNINITIALIZED} { RIALTO_SERVER_LOG_INFO("Starting Rialto Server Service"); diff --git a/media/server/service/source/SessionServerManager.h b/media/server/service/source/SessionServerManager.h index 3b5689202..29f28871a 100644 --- a/media/server/service/source/SessionServerManager.h +++ b/media/server/service/source/SessionServerManager.h @@ -26,7 +26,6 @@ #include "IHeartbeatProcedure.h" #include "IIpcFactory.h" #include "IPlaybackService.h" -#include "IPrivateMetricsService.h" #include "ISessionManagementServer.h" #include "ISessionServerManager.h" #include @@ -73,7 +72,6 @@ class SessionServerManager : public ISessionServerManager ICdmService &m_cdmService; IControlService &m_controlService; std::unique_ptr m_heartbeatProcedureFactory; - std::unique_ptr m_privateMetricsService; std::unique_ptr m_applicationManagementServer; std::unique_ptr m_sessionManagementServer; std::mutex m_serviceMutex; diff --git a/media/server/service/source/WebAudioPlayerMetricsClient.cpp b/media/server/service/source/WebAudioPlayerMetricsClient.cpp new file mode 100644 index 000000000..1e24032b5 --- /dev/null +++ b/media/server/service/source/WebAudioPlayerMetricsClient.cpp @@ -0,0 +1,37 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "WebAudioPlayerMetricsClient.h" + +namespace firebolt::rialto::server::service +{ +WebAudioPlayerMetricsClient::WebAudioPlayerMetricsClient(int handle, + const std::shared_ptr &client, + IPrivateMetricsService &metricsService) + : m_handle{handle}, m_client{client}, m_metricsService{metricsService} +{ +} + +void WebAudioPlayerMetricsClient::notifyState(WebAudioPlayerState state) +{ + m_metricsService.notifyWebAudioPlayerStateChanged(m_handle, m_currentState, state); + m_currentState = state; + m_client->notifyState(state); +} +} // namespace firebolt::rialto::server::service diff --git a/media/server/service/source/WebAudioPlayerMetricsClient.h b/media/server/service/source/WebAudioPlayerMetricsClient.h new file mode 100644 index 000000000..7e5989378 --- /dev/null +++ b/media/server/service/source/WebAudioPlayerMetricsClient.h @@ -0,0 +1,46 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_SERVICE_WEB_AUDIO_PLAYER_METRICS_CLIENT_H_ +#define FIREBOLT_RIALTO_SERVER_SERVICE_WEB_AUDIO_PLAYER_METRICS_CLIENT_H_ + +#include "IPrivateMetricsService.h" +#include "IWebAudioPlayerClient.h" +#include + +namespace firebolt::rialto::server::service +{ +class WebAudioPlayerMetricsClient : public IWebAudioPlayerClient +{ +public: + WebAudioPlayerMetricsClient(int handle, const std::shared_ptr &client, + IPrivateMetricsService &metricsService); + ~WebAudioPlayerMetricsClient() override = default; + + void notifyState(WebAudioPlayerState state) override; + +private: + int m_handle; + std::shared_ptr m_client; + IPrivateMetricsService &m_metricsService; + WebAudioPlayerState m_currentState{WebAudioPlayerState::UNKNOWN}; +}; +} // namespace firebolt::rialto::server::service + +#endif // FIREBOLT_RIALTO_SERVER_SERVICE_WEB_AUDIO_PLAYER_METRICS_CLIENT_H_ diff --git a/media/server/service/source/WebAudioPlayerService.cpp b/media/server/service/source/WebAudioPlayerService.cpp index b3831e63e..b35a7f9ef 100644 --- a/media/server/service/source/WebAudioPlayerService.cpp +++ b/media/server/service/source/WebAudioPlayerService.cpp @@ -24,6 +24,7 @@ #include "IWebAudioPlayer.h" #include "IWebAudioPlayerServerInternal.h" #include "RialtoServerLogging.h" +#include "WebAudioPlayerMetricsClient.h" #include #include #include @@ -33,8 +34,10 @@ namespace firebolt::rialto::server::service { WebAudioPlayerService::WebAudioPlayerService(IPlaybackService &playbackService, - std::shared_ptr &&webAudioPlayerFactory) - : m_playbackService{playbackService}, m_webAudioPlayerFactory{std::move(webAudioPlayerFactory)} + std::shared_ptr &&webAudioPlayerFactory, + IPrivateMetricsService &metricsService) + : m_playbackService{playbackService}, m_webAudioPlayerFactory{std::move(webAudioPlayerFactory)}, + m_metricsService{metricsService} { RIALTO_SERVER_LOG_DEBUG("WebAudioPlayerService is constructed"); } @@ -78,7 +81,10 @@ bool WebAudioPlayerService::createWebAudioPlayer(int handle, m_webAudioPlayers.emplace( std::make_pair(handle, m_webAudioPlayerFactory - ->createWebAudioPlayerServerInternal(webAudioPlayerClient, audioMimeType, + ->createWebAudioPlayerServerInternal( + std::make_shared(handle, webAudioPlayerClient, + m_metricsService), + audioMimeType, priority, config, shmBuffer, handle, IMainThreadFactory::createFactory(), IGstWebAudioPlayerFactory::getFactory(), diff --git a/media/server/service/source/WebAudioPlayerService.h b/media/server/service/source/WebAudioPlayerService.h index 806a86518..191ca39e6 100644 --- a/media/server/service/source/WebAudioPlayerService.h +++ b/media/server/service/source/WebAudioPlayerService.h @@ -21,6 +21,7 @@ #define FIREBOLT_RIALTO_SERVER_SERVICE_WEB_AUDIO_PLAYER_SERVICE_H_ #include "IPlaybackService.h" +#include "IPrivateMetricsService.h" #include "IWebAudioPlayerServerInternal.h" #include "IWebAudioPlayerService.h" #include @@ -40,7 +41,8 @@ class WebAudioPlayerService : public IWebAudioPlayerService { public: WebAudioPlayerService(IPlaybackService &playbackService, - std::shared_ptr &&webAudioPlayerFactory); + std::shared_ptr &&webAudioPlayerFactory, + IPrivateMetricsService &metricsService); ~WebAudioPlayerService() override; WebAudioPlayerService(const WebAudioPlayerService &) = delete; WebAudioPlayerService(WebAudioPlayerService &&) = delete; @@ -68,6 +70,7 @@ class WebAudioPlayerService : public IWebAudioPlayerService private: IPlaybackService &m_playbackService; std::shared_ptr m_webAudioPlayerFactory; + IPrivateMetricsService &m_metricsService; std::map> m_webAudioPlayers; std::mutex m_webAudioPlayerMutex; }; diff --git a/tests/unittests/media/client/ipc/CMakeLists.txt b/tests/unittests/media/client/ipc/CMakeLists.txt index 8c4835390..ba964df7b 100644 --- a/tests/unittests/media/client/ipc/CMakeLists.txt +++ b/tests/unittests/media/client/ipc/CMakeLists.txt @@ -117,6 +117,9 @@ add_gtests ( webAudioPlayerIpc/WriteBufferTest.cpp webAudioPlayerIpc/GetDeviceInfoTest.cpp webAudioPlayerIpc/GetBufferAvailable.cpp + + # PrivateMetricsIpc tests + privateMetricsIpc/PrivateMetricsIpcTests.cpp ) target_include_directories( diff --git a/tests/unittests/media/client/ipc/privateMetricsIpc/PrivateMetricsIpcTests.cpp b/tests/unittests/media/client/ipc/privateMetricsIpc/PrivateMetricsIpcTests.cpp new file mode 100644 index 000000000..175562f50 --- /dev/null +++ b/tests/unittests/media/client/ipc/privateMetricsIpc/PrivateMetricsIpcTests.cpp @@ -0,0 +1,126 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "EventThreadFactoryMock.h" +#include "EventThreadMock.h" +#include "IpcModuleBase.h" +#include "PrivateMetricsIpc.h" +#include + +using namespace firebolt::rialto; +using namespace firebolt::rialto::client; +using namespace firebolt::rialto::common; +using testing::_; +using testing::ByMove; +using testing::Invoke; +using testing::Return; +using testing::StrictMock; +using testing::WithArgs; + +class PrivateMetricsIpcClientMock : public IPrivateMetricsIpcClient +{ +public: + MOCK_METHOD(void, reportClientMetrics, (std::uint64_t sampleId, std::uint32_t reason), (override)); +}; + +class PrivateMetricsIpcTests : public IpcModuleBase, public testing::Test +{ +protected: + void createIpc() + { + expectInitIpc(); + EXPECT_CALL(*m_eventThreadFactory, createEventThread("rialto-metrics-events")) + .WillOnce(Return(ByMove(std::move(m_eventThread)))); + EXPECT_CALL(*m_channelMock, subscribeImpl("firebolt.rialto.MetricsSampleRequestEvent", _, _)) + .WillOnce(Invoke( + [this](const std::string &, const google::protobuf::Descriptor *, + std::function &)> &&handler) + { + m_eventCallback = std::move(handler); + return kEventTag; + })); + expectIpcApiCallSuccess(); + EXPECT_CALL(*m_channelMock, + CallMethod(methodMatcher("notifyClientReady"), m_controllerMock.get(), _, _, + m_blockingClosureMock.get())); + m_sut = std::make_unique(&m_client, *m_ipcClientMock, m_eventThreadFactory); + } + + void destroyIpc() + { + EXPECT_CALL(*m_channelMock, unsubscribe(kEventTag)).WillOnce(Return(true)); + m_sut.reset(); + } + + static constexpr int kEventTag{6}; + StrictMock m_client; + std::shared_ptr> m_eventThreadFactory{ + std::make_shared>()}; + std::unique_ptr> m_eventThread{std::make_unique>()}; + StrictMock *m_eventThreadMock{m_eventThread.get()}; + std::function &)> m_eventCallback; + std::unique_ptr m_sut; +}; + +TEST_F(PrivateMetricsIpcTests, reportsMetricsAndForwardsSampleEvent) +{ + createIpc(); + expectIpcApiCallSuccess(); + EXPECT_CALL(*m_channelMock, + CallMethod(methodMatcher("reportClientMetrics"), m_controllerMock.get(), _, _, + m_blockingClosureMock.get())) + .WillOnce(WithArgs<2>(Invoke( + [](const google::protobuf::Message *request) + { + const auto *report{dynamic_cast(request)}; + ASSERT_NE(report, nullptr); + EXPECT_EQ(report->metrics().sample_id(), 12); + EXPECT_EQ(report->metrics().reason(), METRICS_SAMPLE_REASON_PERIODIC); + EXPECT_EQ(report->metrics().app_name(), "app"); + EXPECT_EQ(report->metrics().process_id(), 42); + EXPECT_EQ(report->metrics().monotonic_time_ms(), 100); + EXPECT_EQ(report->metrics().epoch_time_ms(), 200); + EXPECT_EQ(report->metrics().process_cpu_time_ms(), 300); + EXPECT_EQ(report->metrics().process_memory_kb(), 400); + }))); + EXPECT_TRUE(m_sut->reportClientMetrics(12, METRICS_SAMPLE_REASON_PERIODIC, "app", 42, 100, 200, 300, 400)); + + auto event{std::make_shared()}; + event->set_sample_id(13); + event->set_reason(METRICS_SAMPLE_REASON_STATE_TRANSITION); + std::function eventTask; + EXPECT_CALL(*m_eventThreadMock, addImpl(_)) + .WillOnce(Invoke([&eventTask](std::function &&task) { eventTask = std::move(task); })); + m_eventCallback(event); + ASSERT_TRUE(static_cast(eventTask)); + EXPECT_CALL(m_client, reportClientMetrics(13, METRICS_SAMPLE_REASON_STATE_TRANSITION)); + eventTask(); + destroyIpc(); +} + +TEST_F(PrivateMetricsIpcTests, reportsRpcFailure) +{ + createIpc(); + expectIpcApiCallFailure(); + EXPECT_CALL(*m_channelMock, + CallMethod(methodMatcher("reportClientMetrics"), m_controllerMock.get(), _, _, + m_blockingClosureMock.get())); + EXPECT_FALSE(m_sut->reportClientMetrics(1, METRICS_SAMPLE_REASON_CONNECTED, "", 0, 0, 0, 0, 0)); + destroyIpc(); +} diff --git a/tests/unittests/media/client/main/clientController/CreateTest.cpp b/tests/unittests/media/client/main/clientController/CreateTest.cpp index 42f5cb430..5044d10bc 100644 --- a/tests/unittests/media/client/main/clientController/CreateTest.cpp +++ b/tests/unittests/media/client/main/clientController/CreateTest.cpp @@ -82,3 +82,25 @@ TEST_F(ClientControllerCreateTest, CreateControlIpcFailure) std::runtime_error); EXPECT_EQ(controller, nullptr); } + +TEST_F(ClientControllerCreateTest, CreatePrivateMetricsIpcFailure) +{ + std::unique_ptr controller; + EXPECT_CALL(*m_controlIpcFactoryMock, createControlIpc(_)).WillOnce(Return(m_controlIpcMock)); + EXPECT_CALL(*m_privateMetricsIpcFactoryMock, createPrivateMetricsIpc(_)).WillOnce(Return(nullptr)); + + EXPECT_THROW(controller = + std::make_unique(m_controlIpcFactoryMock, m_privateMetricsIpcFactoryMock), + std::runtime_error); + EXPECT_EQ(controller, nullptr); +} + +TEST_F(ClientControllerCreateTest, ReportsClientMetrics) +{ + EXPECT_CALL(*m_controlIpcFactoryMock, createControlIpc(_)).WillOnce(Return(m_controlIpcMock)); + EXPECT_CALL(*m_privateMetricsIpcFactoryMock, createPrivateMetricsIpc(_)).WillOnce(Return(m_privateMetricsIpcMock)); + auto controller{std::make_unique(m_controlIpcFactoryMock, m_privateMetricsIpcFactoryMock)}; + + EXPECT_CALL(*m_privateMetricsIpcMock, reportClientMetrics(12, 2, _, _, _, _, _, _)).WillOnce(Return(true)); + static_cast(*controller).reportClientMetrics(12, 2); +} diff --git a/tests/unittests/media/server/gstplayer/genericPlayer/GstGenericPlayerPrivateTest.cpp b/tests/unittests/media/server/gstplayer/genericPlayer/GstGenericPlayerPrivateTest.cpp index 43056feff..c3fac35ca 100644 --- a/tests/unittests/media/server/gstplayer/genericPlayer/GstGenericPlayerPrivateTest.cpp +++ b/tests/unittests/media/server/gstplayer/genericPlayer/GstGenericPlayerPrivateTest.cpp @@ -434,6 +434,7 @@ TEST_F(GstGenericPlayerPrivateTest, shouldNotSetVideoRectangleWhenVideoSinkDoesN { expectGetAVSink(kVideoSinkStr, m_realElement); EXPECT_CALL(*m_glibWrapperMock, gObjectClassFindProperty(_, StrEq("rectangle"))).WillOnce(Return(nullptr)); + EXPECT_CALL(*m_glibWrapperMock, gObjectClassFindProperty(_, StrEq("render-rectangle"))).WillOnce(Return(nullptr)); EXPECT_CALL(*m_gstWrapperMock, gstObjectUnref(m_realElement)); EXPECT_FALSE(m_sut->setVideoSinkRectangle()); } diff --git a/tests/unittests/media/server/gstplayer/genericPlayer/common/GenericTasksTestsBase.cpp b/tests/unittests/media/server/gstplayer/genericPlayer/common/GenericTasksTestsBase.cpp index 0814f8227..c57544565 100644 --- a/tests/unittests/media/server/gstplayer/genericPlayer/common/GenericTasksTestsBase.cpp +++ b/tests/unittests/media/server/gstplayer/genericPlayer/common/GenericTasksTestsBase.cpp @@ -760,6 +760,53 @@ void GenericTasksTestsBase::shouldSetupVideoElementWithPendingGeometry() expectSetupVideoSinkElement(); } +void GenericTasksTestsBase::shouldSetupVideoElementWithFallbackGeometry() +{ + testContext->m_context.defaultVideoGeometry = kRectangle; + EXPECT_CALL(*testContext->m_glibWrapper, gTypeName(G_OBJECT_TYPE(testContext->m_element))) + .WillOnce(Return(kElementTypeName.c_str())); + EXPECT_CALL(*testContext->m_glibWrapper, gStrHasPrefix(_, StrEq("amlhalasink"))).WillOnce(Return(FALSE)); + EXPECT_CALL(*testContext->m_glibWrapper, gStrHasPrefix(_, StrEq("brcmaudiosink"))).WillOnce(Return(FALSE)); + EXPECT_CALL(*testContext->m_glibWrapper, gStrHasPrefix(_, StrEq("rialtotexttracksink"))).WillOnce(Return(FALSE)); + EXPECT_CALL(*testContext->m_gstWrapper, gstIsBaseParse(_)).WillOnce(Return(FALSE)); + EXPECT_CALL(testContext->m_gstPlayer, setVideoSinkRectangle()); + expectSetupVideoSinkElement(); +} + +void GenericTasksTestsBase::shouldSetupVideoElementWithApiGeometryAndFallback() +{ + constexpr Rectangle kApiGeometry{5, 6, 7, 8}; + testContext->m_context.defaultVideoGeometry = kRectangle; + testContext->m_context.pendingGeometry = kApiGeometry; + testContext->m_context.videoGeometrySetByApi.store(true); + EXPECT_CALL(*testContext->m_glibWrapper, gTypeName(G_OBJECT_TYPE(testContext->m_element))) + .WillOnce(Return(kElementTypeName.c_str())); + EXPECT_CALL(*testContext->m_glibWrapper, gStrHasPrefix(_, StrEq("amlhalasink"))).WillOnce(Return(FALSE)); + EXPECT_CALL(*testContext->m_glibWrapper, gStrHasPrefix(_, StrEq("brcmaudiosink"))).WillOnce(Return(FALSE)); + EXPECT_CALL(*testContext->m_glibWrapper, gStrHasPrefix(_, StrEq("rialtotexttracksink"))).WillOnce(Return(FALSE)); + EXPECT_CALL(*testContext->m_gstWrapper, gstIsBaseParse(_)).WillOnce(Return(FALSE)); + EXPECT_CALL(testContext->m_gstPlayer, setVideoSinkRectangle()); + expectSetupVideoSinkElement(); +} + +void GenericTasksTestsBase::shouldSetupVideoElementWithoutFallbackAfterApiCall() +{ + testContext->m_context.defaultVideoGeometry = kRectangle; + testContext->m_context.videoGeometrySetByApi.store(true); + EXPECT_CALL(*testContext->m_glibWrapper, gTypeName(G_OBJECT_TYPE(testContext->m_element))) + .WillOnce(Return(kElementTypeName.c_str())); + EXPECT_CALL(*testContext->m_glibWrapper, gStrHasPrefix(_, StrEq("amlhalasink"))).WillOnce(Return(FALSE)); + EXPECT_CALL(*testContext->m_glibWrapper, gStrHasPrefix(_, StrEq("brcmaudiosink"))).WillOnce(Return(FALSE)); + EXPECT_CALL(*testContext->m_glibWrapper, gStrHasPrefix(_, StrEq("rialtotexttracksink"))).WillOnce(Return(FALSE)); + EXPECT_CALL(*testContext->m_gstWrapper, gstIsBaseParse(_)).WillOnce(Return(FALSE)); + expectSetupVideoSinkElement(); +} + +void GenericTasksTestsBase::checkPendingGeometry(const Rectangle &geometry) +{ + EXPECT_EQ(testContext->m_context.pendingGeometry, geometry); +} + void GenericTasksTestsBase::shouldSetupVideoElementWithPendingImmediateOutput() { testContext->m_context.pendingImmediateOutputForVideo = true; diff --git a/tests/unittests/media/server/gstplayer/genericPlayer/common/GenericTasksTestsBase.h b/tests/unittests/media/server/gstplayer/genericPlayer/common/GenericTasksTestsBase.h index 97dc9dfcf..c97bc7315 100644 --- a/tests/unittests/media/server/gstplayer/genericPlayer/common/GenericTasksTestsBase.h +++ b/tests/unittests/media/server/gstplayer/genericPlayer/common/GenericTasksTestsBase.h @@ -43,6 +43,11 @@ using ::testing::SetArgPointee; using ::testing::StrEq; using ::testing::StrictMock; +namespace firebolt::rialto::server +{ +struct Rectangle; +} + /** * @brief GenericTasksTest Base class * @@ -83,6 +88,10 @@ class GenericTasksTestsBase : public ::testing::Test void shouldSetupVideoDecoderElementOnly(); void shouldSetupVideoDecoderElementWithFirstVideoFrameCallback(); void shouldSetupVideoElementWithPendingGeometry(); + void shouldSetupVideoElementWithFallbackGeometry(); + void shouldSetupVideoElementWithApiGeometryAndFallback(); + void shouldSetupVideoElementWithoutFallbackAfterApiCall(); + void checkPendingGeometry(const firebolt::rialto::server::Rectangle &geometry); void shouldSetupVideoElementWithPendingImmediateOutput(); void shouldSetupAudioSinkElementWithPendingLowLatency(); void shouldSetupAudioSinkElementWithPendingSync(); diff --git a/tests/unittests/media/server/gstplayer/genericPlayer/tasksTests/SetupElementTest.cpp b/tests/unittests/media/server/gstplayer/genericPlayer/tasksTests/SetupElementTest.cpp index fa0d75007..88bd135f7 100644 --- a/tests/unittests/media/server/gstplayer/genericPlayer/tasksTests/SetupElementTest.cpp +++ b/tests/unittests/media/server/gstplayer/genericPlayer/tasksTests/SetupElementTest.cpp @@ -18,6 +18,7 @@ */ #include "GenericTasksTestsBase.h" +#include "GenericPlayerContext.h" class SetupElementTest : public GenericTasksTestsBase { @@ -35,6 +36,29 @@ TEST_F(SetupElementTest, shouldSetupVideoElementWithPendingGeometry) triggerSetupElement(); } +TEST_F(SetupElementTest, shouldUseEnvironmentGeometryAsFallback) +{ + const firebolt::rialto::server::Rectangle fallbackGeometry{1, 2, 3, 4}; + shouldSetupVideoElementWithFallbackGeometry(); + triggerSetupElement(); + checkPendingGeometry(fallbackGeometry); +} + +TEST_F(SetupElementTest, shouldKeepApiGeometryAuthoritativeOverEnvironmentFallback) +{ + const firebolt::rialto::server::Rectangle apiGeometry{5, 6, 7, 8}; + shouldSetupVideoElementWithApiGeometryAndFallback(); + triggerSetupElement(); + checkPendingGeometry(apiGeometry); +} + +TEST_F(SetupElementTest, shouldNotApplyEnvironmentFallbackAfterApiGeometryWasCleared) +{ + shouldSetupVideoElementWithoutFallbackAfterApiCall(); + triggerSetupElement(); + checkPendingGeometry({}); +} + TEST_F(SetupElementTest, shouldSetupVideoElementWithPendingImmediateOutput) { shouldSetupVideoElementWithPendingImmediateOutput(); diff --git a/tests/unittests/media/server/ipc/CMakeLists.txt b/tests/unittests/media/server/ipc/CMakeLists.txt index ac1d8856c..e8f2bfbe5 100644 --- a/tests/unittests/media/server/ipc/CMakeLists.txt +++ b/tests/unittests/media/server/ipc/CMakeLists.txt @@ -65,6 +65,9 @@ add_gtests ( # WebAudioPlayerModuleService unittests webAudioPlayerModuleService/WebAudioPlayerModuleServiceTestsFixture.cpp webAudioPlayerModuleService/WebAudioPlayerModuleServiceTests.cpp + + # PrivateMetricsModuleService unittests + privateMetricsModuleService/PrivateMetricsModuleServiceTests.cpp ) target_include_directories( diff --git a/tests/unittests/media/server/ipc/privateMetricsModuleService/PrivateMetricsModuleServiceTests.cpp b/tests/unittests/media/server/ipc/privateMetricsModuleService/PrivateMetricsModuleServiceTests.cpp new file mode 100644 index 000000000..7c76195e7 --- /dev/null +++ b/tests/unittests/media/server/ipc/privateMetricsModuleService/PrivateMetricsModuleServiceTests.cpp @@ -0,0 +1,110 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ClientMock.h" +#include "ClosureMock.h" +#include "IpcControllerMock.h" +#include "PrivateMetricsModuleService.h" +#include "PrivateMetricsServiceMock.h" +#include + +using namespace firebolt::rialto; +using namespace firebolt::rialto::server; +using namespace firebolt::rialto::server::ipc; +using namespace firebolt::rialto::server::service; +using testing::_; +using testing::Invoke; +using testing::Return; +using testing::SaveArg; +using testing::StrictMock; + +MATCHER_P(MetricsSampleRequestMatcher, expectedReason, "") +{ + auto event{std::dynamic_pointer_cast(arg)}; + return event && event->sample_id() == 12 && event->reason() == expectedReason; +} + +TEST(PrivateMetricsModuleServiceTests, handlesClientLifecycleReportsAndSampleRequests) +{ + StrictMock metricsService; + auto client{std::make_shared>()}; + StrictMock controller; + StrictMock closure; + auto sut{std::make_shared(metricsService)}; + + EXPECT_CALL(*client, exportService(_)); + sut->clientConnected(client); + + std::shared_ptr collectorClient; + EXPECT_CALL(controller, getClient()).WillOnce(Return(client)); + EXPECT_CALL(closure, Run()); + EXPECT_CALL(metricsService, clientReady(1, _)).WillOnce(SaveArg<1>(&collectorClient)); + NotifyClientReadyRequest readyRequest; + NotifyClientReadyResponse readyResponse; + sut->notifyClientReady(&controller, &readyRequest, &readyResponse, &closure); + ASSERT_NE(collectorClient, nullptr); + + EXPECT_CALL(*client, isConnected()).WillOnce(Return(true)); + EXPECT_CALL(*client, sendEvent(MetricsSampleRequestMatcher(METRICS_SAMPLE_REASON_PERIODIC))) + .WillOnce(Return(true)); + collectorClient->requestMetricsSample(1, 12, firebolt::rialto::server::MetricsSampleReason::PERIODIC); + + ReportClientMetricsRequest reportRequest; + auto *protoMetrics{reportRequest.mutable_metrics()}; + protoMetrics->set_sample_id(12); + protoMetrics->set_reason(METRICS_SAMPLE_REASON_PERIODIC); + protoMetrics->set_app_name("test-app"); + protoMetrics->set_process_id(42); + protoMetrics->set_monotonic_time_ms(100); + protoMetrics->set_epoch_time_ms(200); + protoMetrics->set_process_cpu_time_ms(300); + protoMetrics->set_process_memory_kb(400); + ReportClientMetricsResponse reportResponse; + EXPECT_CALL(controller, getClient()).WillOnce(Return(client)); + EXPECT_CALL(closure, Run()); + EXPECT_CALL(metricsService, reportMetrics(1, _)) + .WillOnce(Invoke( + [](int, const ClientMetricsData &metrics) + { + EXPECT_EQ(metrics.sampleId, 12); + EXPECT_EQ(metrics.reason, firebolt::rialto::server::MetricsSampleReason::PERIODIC); + EXPECT_EQ(metrics.appName, "test-app"); + EXPECT_EQ(metrics.processId, 42); + EXPECT_EQ(metrics.monotonicTimeMs, 100); + EXPECT_EQ(metrics.epochTimeMs, 200); + EXPECT_EQ(metrics.processCpuTimeMs, 300); + EXPECT_EQ(metrics.processMemoryKb, 400); + })); + sut->reportClientMetrics(&controller, &reportRequest, &reportResponse, &closure); + + EXPECT_CALL(metricsService, + notifyApplicationStateChanged(ApplicationState::RUNNING, ApplicationState::INACTIVE)); + sut->notifyApplicationStateChanged(ApplicationState::RUNNING, ApplicationState::INACTIVE); + + EXPECT_CALL(metricsService, clientDisconnected(1)); + sut->clientDisconnected(client); +} + +TEST(PrivateMetricsModuleServiceTests, factoryCreatesService) +{ + StrictMock metricsService; + PrivateMetricsModuleServiceFactory factory; + EXPECT_NE(factory.create(metricsService), nullptr); + EXPECT_NE(IPrivateMetricsModuleServiceFactory::createFactory(), nullptr); +} diff --git a/tests/unittests/media/server/ipc/sessionManagementServer/SessionManagementServerTestsFixture.cpp b/tests/unittests/media/server/ipc/sessionManagementServer/SessionManagementServerTestsFixture.cpp index 261453d0e..7f42f5725 100644 --- a/tests/unittests/media/server/ipc/sessionManagementServer/SessionManagementServerTestsFixture.cpp +++ b/tests/unittests/media/server/ipc/sessionManagementServer/SessionManagementServerTestsFixture.cpp @@ -80,6 +80,8 @@ SessionManagementServerTests::SessionManagementServerTests() std::make_shared>()}, m_webAudioPlayerModuleMock{ std::make_shared>()}, + m_privateMetricsModuleMock{ + std::make_shared>()}, m_controlModuleMock{std::make_shared>()} { std::shared_ptr> serverFactoryMock = @@ -113,6 +115,11 @@ SessionManagementServerTests::SessionManagementServerTests() std::make_shared>(); EXPECT_CALL(*webAudioPlayerModuleFactoryMock, create(_)).WillOnce(Return(m_webAudioPlayerModuleMock)); EXPECT_CALL(m_playbackServiceMock, getWebAudioPlayerService()).WillOnce(ReturnRef(m_webAudioPlayerServiceMock)); + std::shared_ptr> + privateMetricsModuleFactoryMock = + std::make_shared>(); + EXPECT_CALL(m_playbackServiceMock, getPrivateMetricsService()).WillOnce(ReturnRef(m_privateMetricsServiceMock)); + EXPECT_CALL(*privateMetricsModuleFactoryMock, create(_)).WillOnce(Return(m_privateMetricsModuleMock)); std::shared_ptr> controlModuleFactoryMock = std::make_shared>(); EXPECT_CALL(*controlModuleFactoryMock, create(_, _)).WillOnce(Return(m_controlModuleMock)); @@ -124,6 +131,7 @@ SessionManagementServerTests::SessionManagementServerTests() mediaKeysModuleFactoryMock, mediaKeysCapabilitiesModuleFactoryMock, webAudioPlayerModuleFactoryMock, + privateMetricsModuleFactoryMock, controlModuleFactoryMock, m_playbackServiceMock, m_cdmServiceMock, m_controlServiceMock); @@ -177,6 +185,8 @@ void SessionManagementServerTests::clientWillConnect() clientConnected(std::dynamic_pointer_cast<::firebolt::rialto::ipc::IClient>(m_clientMock))); EXPECT_CALL(*m_webAudioPlayerModuleMock, clientConnected(std::dynamic_pointer_cast<::firebolt::rialto::ipc::IClient>(m_clientMock))); + EXPECT_CALL(*m_privateMetricsModuleMock, + clientConnected(std::dynamic_pointer_cast<::firebolt::rialto::ipc::IClient>(m_clientMock))); EXPECT_CALL(*m_controlModuleMock, clientConnected(std::dynamic_pointer_cast<::firebolt::rialto::ipc::IClient>(m_clientMock))); } @@ -193,6 +203,8 @@ void SessionManagementServerTests::clientWillDisconnect() clientDisconnected(std::dynamic_pointer_cast<::firebolt::rialto::ipc::IClient>(m_clientMock))); EXPECT_CALL(*m_webAudioPlayerModuleMock, clientDisconnected(std::dynamic_pointer_cast<::firebolt::rialto::ipc::IClient>(m_clientMock))); + EXPECT_CALL(*m_privateMetricsModuleMock, + clientDisconnected(std::dynamic_pointer_cast<::firebolt::rialto::ipc::IClient>(m_clientMock))); EXPECT_CALL(*m_controlModuleMock, clientDisconnected(std::dynamic_pointer_cast<::firebolt::rialto::ipc::IClient>(m_clientMock))); } diff --git a/tests/unittests/media/server/ipc/sessionManagementServer/SessionManagementServerTestsFixture.h b/tests/unittests/media/server/ipc/sessionManagementServer/SessionManagementServerTestsFixture.h index 357b45c15..cfa3f0221 100644 --- a/tests/unittests/media/server/ipc/sessionManagementServer/SessionManagementServerTestsFixture.h +++ b/tests/unittests/media/server/ipc/sessionManagementServer/SessionManagementServerTestsFixture.h @@ -32,6 +32,8 @@ #include "MediaPipelineModuleServiceMock.h" #include "MediaPipelineServiceMock.h" #include "PlaybackServiceMock.h" +#include "PrivateMetricsModuleServiceMock.h" +#include "PrivateMetricsServiceMock.h" #include "WebAudioPlayerModuleServiceMock.h" #include "WebAudioPlayerServiceMock.h" #include @@ -69,6 +71,7 @@ class SessionManagementServerTests : public testing::Test StrictMock m_playbackServiceMock; StrictMock m_mediaPipelineServiceMock; StrictMock m_webAudioPlayerServiceMock; + StrictMock m_privateMetricsServiceMock; StrictMock m_cdmServiceMock; StrictMock m_controlServiceMock; std::shared_ptr> m_serverMock; @@ -79,6 +82,7 @@ class SessionManagementServerTests : public testing::Test std::shared_ptr> m_mediaKeysCapabilitiesModuleMock; std::shared_ptr> m_webAudioPlayerModuleMock; + std::shared_ptr> m_privateMetricsModuleMock; std::shared_ptr> m_controlModuleMock; std::unique_ptr m_sut; diff --git a/tests/unittests/media/server/main/CMakeLists.txt b/tests/unittests/media/server/main/CMakeLists.txt index 646d2ff4d..9eadccca6 100644 --- a/tests/unittests/media/server/main/CMakeLists.txt +++ b/tests/unittests/media/server/main/CMakeLists.txt @@ -102,6 +102,10 @@ add_gtests ( mainThread/MainThreadTest.cpp + metrics/LogMetricsReporterTests.cpp + metrics/MetricsCollectorTests.cpp + metrics/MetricsHelpersTests.cpp + textTrackAccessor/TextTrackAccessorTest.cpp textTrackSession/TextTrackSessionTest.cpp diff --git a/tests/unittests/media/server/main/metrics/LogMetricsReporterTests.cpp b/tests/unittests/media/server/main/metrics/LogMetricsReporterTests.cpp new file mode 100644 index 000000000..24734363a --- /dev/null +++ b/tests/unittests/media/server/main/metrics/LogMetricsReporterTests.cpp @@ -0,0 +1,136 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "LogMetricsReporter.h" +#include "RialtoLogging.h" +#include + +using namespace firebolt::rialto; +using namespace firebolt::rialto::logging; +using namespace firebolt::rialto::server; + +namespace +{ +std::size_t g_logCount{0}; + +void countLog(RIALTO_DEBUG_LEVEL, const char *, int, const char *, const char *, std::size_t) +{ + ++g_logCount; +} + +PeriodicMetricsReport makeReport(ApplicationState state, std::uint64_t timeMs) +{ + PeriodicMetricsReport report; + report.sampleId = timeMs; + report.monotonicTimeMs = timeMs; + report.reason = "PERIODIC"; + report.applicationState = state; + report.clientCpuPercent = 20.0; + report.serverCpuPercent = 10.0; + report.combinedCpuPercent = 30.0; + report.clientMemoryKb = 100000; + report.serverMemoryKb = 200000; + report.cgroupMemoryUsageKb = 300000; + report.cgroupMemoryLimitKb = 400000; + report.shmMemoryKb = 50000; + return report; +} +} // namespace + +class LogMetricsReporterTests : public testing::Test +{ +protected: + void SetUp() override + { + g_logCount = 0; + m_previousLevels = getLogLevels(RIALTO_COMPONENT_SERVER); + ASSERT_EQ(setLogHandler(RIALTO_COMPONENT_SERVER, countLog, true), RIALTO_LOGGING_STATUS_OK); + } + + void TearDown() override + { + EXPECT_EQ(setLogHandler(RIALTO_COMPONENT_SERVER, nullptr, false), RIALTO_LOGGING_STATUS_OK); + EXPECT_EQ(setLogLevels(RIALTO_COMPONENT_SERVER, m_previousLevels), RIALTO_LOGGING_STATUS_OK); + } + + RIALTO_DEBUG_LEVEL m_previousLevels{RIALTO_DEBUG_LEVEL_DEFAULT}; +}; + +TEST_F(LogMetricsReporterTests, inactiveSamplesOnlyLogAfterSignificantGaugeChange) +{ + LogMetricsReporter sut; + auto report{makeReport(ApplicationState::INACTIVE, 0)}; + sut.reportPeriodicSample(report); + EXPECT_EQ(g_logCount, 1); + + report.monotonicTimeMs = 10 * 60 * 1000; + report.clientCpuTimeMs = 1000000; + report.serverCpuTimeMs = 2000000; + report.clientCpuPercent = 21.9; + report.clientMemoryKb = 109000; + sut.reportPeriodicSample(report); + EXPECT_EQ(g_logCount, 1); + + report.clientCpuPercent = 22.0; + sut.reportPeriodicSample(report); + EXPECT_EQ(g_logCount, 2); +} + +TEST_F(LogMetricsReporterTests, activeSamplesLogAtTenMinutesOrAfterSignificantChange) +{ + LogMetricsReporter sut; + auto report{makeReport(ApplicationState::RUNNING, 100)}; + sut.reportPeriodicSample(report); + EXPECT_EQ(g_logCount, 1); + + report.monotonicTimeMs += 10 * 60 * 1000 - 1; + sut.reportPeriodicSample(report); + EXPECT_EQ(g_logCount, 1); + + report.monotonicTimeMs += 1; + sut.reportPeriodicSample(report); + EXPECT_EQ(g_logCount, 2); + + report.monotonicTimeMs += 1; + report.serverMemoryKb += 20000; + sut.reportPeriodicSample(report); + EXPECT_EQ(g_logCount, 3); +} + +TEST_F(LogMetricsReporterTests, applicationStateChangesLogImmediatelyButNonPeriodicSamplesAreSuppressed) +{ + LogMetricsReporter sut; + auto report{makeReport(ApplicationState::INACTIVE, 100)}; + sut.reportPeriodicSample(report); + + report.monotonicTimeMs = 101; + report.applicationState = ApplicationState::RUNNING; + sut.reportPeriodicSample(report); + report.reason = "STATE_TRANSITION"; + sut.reportPeriodicSample(report); + EXPECT_EQ(g_logCount, 2); +} + +TEST_F(LogMetricsReporterTests, transitionAndThresholdReportsAreLogged) +{ + LogMetricsReporter sut; + sut.reportStateTransition(StateTransitionReport{}); + sut.reportThresholdExceeded(ThresholdAlert{}); + EXPECT_EQ(g_logCount, 2); +} diff --git a/tests/unittests/media/server/main/metrics/MetricsCollectorTests.cpp b/tests/unittests/media/server/main/metrics/MetricsCollectorTests.cpp new file mode 100644 index 000000000..52d86904f --- /dev/null +++ b/tests/unittests/media/server/main/metrics/MetricsCollectorTests.cpp @@ -0,0 +1,132 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MetricsCollector.h" +#include "MetricsCollectorClientMock.h" +#include "TimerFactoryMock.h" +#include "TimerMock.h" +#include + +using namespace firebolt::rialto; +using namespace firebolt::rialto::server; +using testing::_; +using testing::ByMove; +using testing::Invoke; +using testing::Return; +using testing::StrictMock; + +class MetricsCollectorTests : public testing::Test +{ +protected: + void createCollector(ApplicationState initialApplicationState = ApplicationState::UNKNOWN) + { + auto timer{std::make_unique>()}; + m_timer = timer.get(); + EXPECT_CALL(*m_timerFactory, createTimer(std::chrono::milliseconds{15000}, _, common::TimerType::PERIODIC)) + .WillOnce(Invoke( + [this, &timer](const std::chrono::milliseconds &, const std::function &callback, + common::TimerType) + { + m_timerCallback = callback; + return std::move(timer); + })); + EXPECT_CALL(*m_client, requestMetricsSample(kClientId, 1, MetricsSampleReason::CONNECTED)); + m_sut = std::make_unique(kClientId, m_client, m_timerFactory, initialApplicationState); + } + + void destroyCollector() + { + EXPECT_CALL(*m_timer, cancel()); + m_sut.reset(); + } + + static constexpr int kClientId{3}; + std::shared_ptr> m_client{ + std::make_shared>()}; + std::shared_ptr> m_timerFactory{ + std::make_shared>()}; + StrictMock *m_timer{nullptr}; + std::function m_timerCallback; + std::unique_ptr m_sut; +}; + +TEST_F(MetricsCollectorTests, doesNotQueueRequestsWhileClientIsUnresponsive) +{ + createCollector(); + EXPECT_CALL(*m_client, requestMetricsSample(kClientId, 2, MetricsSampleReason::PERIODIC)); + m_timerCallback(); + m_timerCallback(); + EXPECT_CALL(*m_client, requestMetricsSample(kClientId, 3, MetricsSampleReason::PERIODIC)); + m_timerCallback(); + + ClientMetricsData response; + response.sampleId = 3; + response.reason = MetricsSampleReason::PERIODIC; + response.monotonicTimeMs = 1000; + response.processCpuTimeMs = 100; + response.processMemoryKb = 1000; + m_sut->processMetrics(response); + + EXPECT_CALL(*m_client, requestMetricsSample(kClientId, 4, MetricsSampleReason::PERIODIC)); + m_timerCallback(); + destroyCollector(); +} + +TEST_F(MetricsCollectorTests, startsWithCurrentApplicationStateWithoutRequestingAnotherSample) +{ + createCollector(ApplicationState::RUNNING); + destroyCollector(); +} + +TEST_F(MetricsCollectorTests, processesSamplesAndStateBoundaries) +{ + createCollector(); + ClientMetricsData baseline; + baseline.sampleId = 1; + baseline.reason = MetricsSampleReason::CONNECTED; + baseline.monotonicTimeMs = 1000; + baseline.processCpuTimeMs = 100; + baseline.processMemoryKb = 1000; + m_sut->processMetrics(baseline); + + EXPECT_CALL(*m_client, requestMetricsSample(kClientId, 2, MetricsSampleReason::STATE_TRANSITION)); + m_sut->notifyApplicationStateChanged(ApplicationState::UNKNOWN, ApplicationState::RUNNING); + m_sut->notifyPlaybackStateChanged(10, PlaybackState::UNKNOWN, PlaybackState::PLAYING); + EXPECT_CALL(*m_client, requestMetricsSample(kClientId, 3, MetricsSampleReason::STATE_TRANSITION)); + m_sut->notifyPlaybackStateChanged(10, PlaybackState::PLAYING, PlaybackState::PAUSED); + m_sut->notifyWebAudioPlayerStateChanged(11, WebAudioPlayerState::UNKNOWN, WebAudioPlayerState::PLAYING); + EXPECT_CALL(*m_client, requestMetricsSample(kClientId, 4, MetricsSampleReason::STATE_TRANSITION)); + m_sut->notifyWebAudioPlayerStateChanged(11, WebAudioPlayerState::PLAYING, WebAudioPlayerState::PAUSED); + + ClientMetricsData periodic{baseline}; + periodic.sampleId = 5; + periodic.reason = MetricsSampleReason::PERIODIC; + periodic.monotonicTimeMs = 2000; + periodic.processCpuTimeMs = 200; + periodic.processMemoryKb = 1100; + m_sut->processMetrics(periodic); + + EXPECT_CALL(*m_client, requestMetricsSample(kClientId, 5, MetricsSampleReason::STATE_TRANSITION)); + m_sut->notifyApplicationStateChanged(ApplicationState::RUNNING, ApplicationState::INACTIVE); + EXPECT_CALL(*m_client, requestMetricsSample(kClientId, 6, MetricsSampleReason::STATE_TRANSITION)); + m_sut->notifyPlaybackStateChanged(10, PlaybackState::PAUSED, PlaybackState::STOPPED); + EXPECT_CALL(*m_client, requestMetricsSample(kClientId, 7, MetricsSampleReason::STATE_TRANSITION)); + m_sut->notifyWebAudioPlayerStateChanged(11, WebAudioPlayerState::PAUSED, WebAudioPlayerState::END_OF_STREAM); + destroyCollector(); +} diff --git a/tests/unittests/media/server/main/metrics/MetricsHelpersTests.cpp b/tests/unittests/media/server/main/metrics/MetricsHelpersTests.cpp new file mode 100644 index 000000000..d73ef20ef --- /dev/null +++ b/tests/unittests/media/server/main/metrics/MetricsHelpersTests.cpp @@ -0,0 +1,134 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "CompositeMetricsReporter.h" +#include "MetricsAccumulator.h" +#include "MetricsReporterMock.h" +#include "MetricsThresholdChecker.h" +#include "StateMetricsAggregator.h" +#include + +using namespace firebolt::rialto::server; +using testing::_; +using testing::ByMove; +using testing::Invoke; +using testing::StrictMock; + +TEST(MetricsAccumulatorTests, calculatesStatisticsAndResets) +{ + MetricsAccumulator sut; + EXPECT_EQ(sut.getStats().count, 0); + + sut.addSample(1.0); + sut.addSample(2.0); + sut.addSample(3.0); + const auto stats{sut.getStats()}; + EXPECT_EQ(stats.count, 3); + EXPECT_DOUBLE_EQ(stats.min, 1.0); + EXPECT_DOUBLE_EQ(stats.max, 3.0); + EXPECT_DOUBLE_EQ(stats.mean, 2.0); + EXPECT_DOUBLE_EQ(stats.stddev, 1.0); + + sut.reset(); + EXPECT_EQ(sut.getCount(), 0); +} + +TEST(StateMetricsAggregatorTests, finalizesAllMetricsForState) +{ + StateMetricsAggregator sut; + sut.begin("PLAYING", 100); + sut.addSample(MetricsSample{1.0, 2.0, 3.0, 4, 5, 6, 7}); + sut.addSample(MetricsSample{3.0, 4.0, 5.0, 6, 7, 8, 9}); + + const auto report{sut.finalize(250)}; + EXPECT_TRUE(sut.hasData()); + EXPECT_EQ(sut.getStateName(), "PLAYING"); + EXPECT_EQ(report.stateName, "PLAYING"); + EXPECT_EQ(report.durationMs, 150); + EXPECT_DOUBLE_EQ(report.clientCpu.mean, 2.0); + EXPECT_DOUBLE_EQ(report.serverCpu.mean, 3.0); + EXPECT_DOUBLE_EQ(report.combinedCpu.mean, 4.0); + EXPECT_DOUBLE_EQ(report.clientMemoryKb.mean, 5.0); + EXPECT_DOUBLE_EQ(report.serverMemoryKb.mean, 6.0); + EXPECT_DOUBLE_EQ(report.cgroupMemoryUsageKb.mean, 7.0); + EXPECT_DOUBLE_EQ(report.cgroupMemoryLimitKb.mean, 8.0); + + sut.reset(); + EXPECT_FALSE(sut.hasData()); + EXPECT_EQ(sut.finalize(10).durationMs, 10); +} + +TEST(CompositeMetricsReporterTests, forwardsEveryReportAndIgnoresNullReporter) +{ + CompositeMetricsReporter sut; + auto first{std::make_unique>()}; + auto second{std::make_unique>()}; + auto *firstMock{first.get()}; + auto *secondMock{second.get()}; + sut.addReporter(nullptr); + sut.addReporter(std::move(first)); + sut.addReporter(std::move(second)); + + PeriodicMetricsReport periodic; + StateTransitionReport transition; + ThresholdAlert alert; + EXPECT_CALL(*firstMock, reportPeriodicSample(testing::Ref(periodic))); + EXPECT_CALL(*secondMock, reportPeriodicSample(testing::Ref(periodic))); + sut.reportPeriodicSample(periodic); + EXPECT_CALL(*firstMock, reportStateTransition(testing::Ref(transition))); + EXPECT_CALL(*secondMock, reportStateTransition(testing::Ref(transition))); + sut.reportStateTransition(transition); + EXPECT_CALL(*firstMock, reportThresholdExceeded(testing::Ref(alert))); + EXPECT_CALL(*secondMock, reportThresholdExceeded(testing::Ref(alert))); + sut.reportThresholdExceeded(alert); +} + +TEST(MetricsThresholdCheckerTests, reportsOnceAndRearmsAfterTwoLowerSamples) +{ + StrictMock reporter; + MetricsThresholdConfig config; + MetricsThresholdChecker sut{config, &reporter}; + + EXPECT_CALL(reporter, reportThresholdExceeded(_)) + .Times(2) + .WillRepeatedly(Invoke([](const ThresholdAlert &alert) + { EXPECT_EQ(alert.metricName, "client_cpu"); })); + sut.checkSample(96.0, 0.0, 0.0, 0, 0, 0, 0); + sut.checkSample(96.0, 0.0, 0.0, 0, 0, 0, 0); + + sut.checkSample(0.0, 0.0, 0.0, 0, 0, 0, 0); + sut.checkSample(0.0, 0.0, 0.0, 0, 0, 0, 0); + EXPECT_CALL(reporter, reportThresholdExceeded(_)).Times(2); + sut.checkSample(96.0, 0.0, 0.0, 0, 0, 0, 0); +} + +TEST(MetricsThresholdCheckerTests, reportsConfiguredMetricsIncludingCgroupPercentage) +{ + StrictMock reporter; + MetricsThresholdChecker sut{MetricsThresholdConfig{}, &reporter}; + + EXPECT_CALL(reporter, reportThresholdExceeded(_)).Times(6); + sut.checkSample(81.0, 81.0, 151.0, 512000, 512000, 81, 100); +} + +TEST(MetricsThresholdCheckerTests, acceptsNullReporter) +{ + MetricsThresholdChecker sut{MetricsThresholdConfig{}, nullptr}; + sut.checkSample(100.0, 100.0, 200.0, 1000000, 1000000, 100, 100); +} diff --git a/tests/unittests/media/server/mocks/ipc/IpcFactoryMock.h b/tests/unittests/media/server/mocks/ipc/IpcFactoryMock.h index 877e4d375..71ff39420 100644 --- a/tests/unittests/media/server/mocks/ipc/IpcFactoryMock.h +++ b/tests/unittests/media/server/mocks/ipc/IpcFactoryMock.h @@ -35,7 +35,7 @@ class IpcFactoryMock : public IIpcFactory (service::ISessionServerManager & sessionServerManager), (const, override)); MOCK_METHOD(std::unique_ptr, createSessionManagementServer, (service::IPlaybackService & playbackService, service::ICdmService &cdmService, - service::IControlService &controlService, service::IPrivateMetricsService &metricsService), + service::IControlService &controlService), (const, override)); }; } // namespace firebolt::rialto::server::ipc diff --git a/tests/unittests/media/server/mocks/ipc/MediaPipelineModuleServiceMock.h b/tests/unittests/media/server/mocks/ipc/MediaPipelineModuleServiceMock.h index 5bcca7b47..3ffcca89b 100644 --- a/tests/unittests/media/server/mocks/ipc/MediaPipelineModuleServiceMock.h +++ b/tests/unittests/media/server/mocks/ipc/MediaPipelineModuleServiceMock.h @@ -35,8 +35,6 @@ class MediaPipelineModuleServiceMock : public IMediaPipelineModuleService MOCK_METHOD(void, clientConnected, (const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient), (override)); MOCK_METHOD(void, clientDisconnected, (const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient), (override)); - MOCK_METHOD(void, setMetricsService, (const std::shared_ptr &metricsService), - (override)); MOCK_METHOD(void, createSession, (::google::protobuf::RpcController * controller, const ::firebolt::rialto::CreateSessionRequest *request, ::firebolt::rialto::CreateSessionResponse *response, ::google::protobuf::Closure *done), diff --git a/tests/unittests/media/server/mocks/ipc/PrivateMetricsModuleServiceMock.h b/tests/unittests/media/server/mocks/ipc/PrivateMetricsModuleServiceMock.h new file mode 100644 index 000000000..152724996 --- /dev/null +++ b/tests/unittests/media/server/mocks/ipc/PrivateMetricsModuleServiceMock.h @@ -0,0 +1,57 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_IPC_PRIVATE_METRICS_MODULE_SERVICE_MOCK_H_ +#define FIREBOLT_RIALTO_SERVER_IPC_PRIVATE_METRICS_MODULE_SERVICE_MOCK_H_ + +#include "IPrivateMetricsModuleService.h" +#include + +namespace firebolt::rialto::server::ipc +{ +class PrivateMetricsModuleServiceMock : public IPrivateMetricsModuleService +{ +public: + MOCK_METHOD(void, clientConnected, + (const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient), (override)); + MOCK_METHOD(void, clientDisconnected, + (const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient), (override)); + MOCK_METHOD(void, notifyApplicationStateChanged, + (ApplicationState oldState, ApplicationState newState), (override)); + MOCK_METHOD(void, reportClientMetrics, + (::google::protobuf::RpcController * controller, + const ::firebolt::rialto::ReportClientMetricsRequest *request, + ::firebolt::rialto::ReportClientMetricsResponse *response, ::google::protobuf::Closure *done), + (override)); + MOCK_METHOD(void, notifyClientReady, + (::google::protobuf::RpcController * controller, + const ::firebolt::rialto::NotifyClientReadyRequest *request, + ::firebolt::rialto::NotifyClientReadyResponse *response, ::google::protobuf::Closure *done), + (override)); +}; + +class PrivateMetricsModuleServiceFactoryMock : public IPrivateMetricsModuleServiceFactory +{ +public: + MOCK_METHOD(std::shared_ptr, create, + (service::IPrivateMetricsService & metricsService), (const, override)); +}; +} // namespace firebolt::rialto::server::ipc + +#endif // FIREBOLT_RIALTO_SERVER_IPC_PRIVATE_METRICS_MODULE_SERVICE_MOCK_H_ diff --git a/tests/unittests/media/server/mocks/main/MetricsCollectorClientMock.h b/tests/unittests/media/server/mocks/main/MetricsCollectorClientMock.h new file mode 100644 index 000000000..764c5f99e --- /dev/null +++ b/tests/unittests/media/server/mocks/main/MetricsCollectorClientMock.h @@ -0,0 +1,36 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_METRICS_COLLECTOR_CLIENT_MOCK_H_ +#define FIREBOLT_RIALTO_SERVER_METRICS_COLLECTOR_CLIENT_MOCK_H_ + +#include "IMetricsCollectorClient.h" +#include + +namespace firebolt::rialto::server +{ +class MetricsCollectorClientMock : public IMetricsCollectorClient +{ +public: + MOCK_METHOD(void, requestMetricsSample, + (int clientId, std::uint64_t sampleId, MetricsSampleReason reason), (override)); +}; +} // namespace firebolt::rialto::server + +#endif // FIREBOLT_RIALTO_SERVER_METRICS_COLLECTOR_CLIENT_MOCK_H_ diff --git a/tests/unittests/media/server/mocks/main/MetricsCollectorMock.h b/tests/unittests/media/server/mocks/main/MetricsCollectorMock.h new file mode 100644 index 000000000..779e40020 --- /dev/null +++ b/tests/unittests/media/server/mocks/main/MetricsCollectorMock.h @@ -0,0 +1,50 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_METRICS_COLLECTOR_MOCK_H_ +#define FIREBOLT_RIALTO_SERVER_METRICS_COLLECTOR_MOCK_H_ + +#include "IMetricsCollector.h" +#include + +namespace firebolt::rialto::server +{ +class MetricsCollectorMock : public IMetricsCollector +{ +public: + MOCK_METHOD(void, processMetrics, (const ClientMetricsData &metrics), (override)); + MOCK_METHOD(void, notifyPlaybackStateChanged, + (int sessionId, PlaybackState oldState, PlaybackState newState), (override)); + MOCK_METHOD(void, notifyWebAudioPlayerStateChanged, + (int handle, WebAudioPlayerState oldState, WebAudioPlayerState newState), (override)); + MOCK_METHOD(void, notifyApplicationStateChanged, + (ApplicationState oldState, ApplicationState newState), (override)); +}; + +class MetricsCollectorFactoryMock : public IMetricsCollectorFactory +{ +public: + MOCK_METHOD(std::unique_ptr, create, + (int clientId, const std::shared_ptr &client, + ApplicationState initialApplicationState), + (override)); +}; +} // namespace firebolt::rialto::server + +#endif // FIREBOLT_RIALTO_SERVER_METRICS_COLLECTOR_MOCK_H_ diff --git a/tests/unittests/media/server/mocks/main/MetricsReporterMock.h b/tests/unittests/media/server/mocks/main/MetricsReporterMock.h new file mode 100644 index 000000000..146366040 --- /dev/null +++ b/tests/unittests/media/server/mocks/main/MetricsReporterMock.h @@ -0,0 +1,37 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_METRICS_REPORTER_MOCK_H_ +#define FIREBOLT_RIALTO_SERVER_METRICS_REPORTER_MOCK_H_ + +#include "IMetricsReporter.h" +#include + +namespace firebolt::rialto::server +{ +class MetricsReporterMock : public IMetricsReporter +{ +public: + MOCK_METHOD(void, reportPeriodicSample, (const PeriodicMetricsReport &report), (override)); + MOCK_METHOD(void, reportStateTransition, (const StateTransitionReport &report), (override)); + MOCK_METHOD(void, reportThresholdExceeded, (const ThresholdAlert &alert), (override)); +}; +} // namespace firebolt::rialto::server + +#endif // FIREBOLT_RIALTO_SERVER_METRICS_REPORTER_MOCK_H_ diff --git a/tests/unittests/media/server/mocks/service/PlaybackServiceMock.h b/tests/unittests/media/server/mocks/service/PlaybackServiceMock.h index 86d96aa05..fcf9eaedc 100644 --- a/tests/unittests/media/server/mocks/service/PlaybackServiceMock.h +++ b/tests/unittests/media/server/mocks/service/PlaybackServiceMock.h @@ -44,6 +44,7 @@ class PlaybackServiceMock : public IPlaybackService MOCK_METHOD(std::shared_ptr, getShmBuffer, (), (const, override)); MOCK_METHOD(IMediaPipelineService &, getMediaPipelineService, (), (const, override)); MOCK_METHOD(IWebAudioPlayerService &, getWebAudioPlayerService, (), (const, override)); + MOCK_METHOD(IPrivateMetricsService &, getPrivateMetricsService, (), (const, override)); MOCK_METHOD(void, ping, (const std::shared_ptr &heartbeatProcedure), (const, override)); }; } // namespace firebolt::rialto::server::service diff --git a/tests/unittests/media/server/mocks/service/PrivateMetricsServiceMock.h b/tests/unittests/media/server/mocks/service/PrivateMetricsServiceMock.h new file mode 100644 index 000000000..173ba2360 --- /dev/null +++ b/tests/unittests/media/server/mocks/service/PrivateMetricsServiceMock.h @@ -0,0 +1,46 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBOLT_RIALTO_SERVER_SERVICE_PRIVATE_METRICS_SERVICE_MOCK_H_ +#define FIREBOLT_RIALTO_SERVER_SERVICE_PRIVATE_METRICS_SERVICE_MOCK_H_ + +#include "IPrivateMetricsService.h" +#include + +namespace firebolt::rialto::server::service +{ +class PrivateMetricsServiceMock : public IPrivateMetricsService +{ +public: + MOCK_METHOD(void, clientReady, + (int clientId, const std::shared_ptr &client), + (override)); + MOCK_METHOD(void, clientDisconnected, (int clientId), (override)); + MOCK_METHOD(void, reportMetrics, + (int clientId, const firebolt::rialto::server::ClientMetricsData &metrics), (override)); + MOCK_METHOD(void, notifyPlaybackStateChanged, + (int sessionId, PlaybackState oldState, PlaybackState newState), (override)); + MOCK_METHOD(void, notifyWebAudioPlayerStateChanged, + (int handle, WebAudioPlayerState oldState, WebAudioPlayerState newState), (override)); + MOCK_METHOD(void, notifyApplicationStateChanged, + (ApplicationState oldState, ApplicationState newState), (override)); +}; +} // namespace firebolt::rialto::server::service + +#endif // FIREBOLT_RIALTO_SERVER_SERVICE_PRIVATE_METRICS_SERVICE_MOCK_H_ diff --git a/tests/unittests/media/server/service/CMakeLists.txt b/tests/unittests/media/server/service/CMakeLists.txt index e2fab6d94..6e453306e 100644 --- a/tests/unittests/media/server/service/CMakeLists.txt +++ b/tests/unittests/media/server/service/CMakeLists.txt @@ -38,6 +38,9 @@ add_gtests ( controlService/ControlServiceTestsFixture.cpp controlService/ControlServiceTests.cpp + + metrics/MetricsClientsTests.cpp + metrics/PrivateMetricsServiceTests.cpp ) target_include_directories( diff --git a/tests/unittests/media/server/service/mediaPipelineService/MediaPipelineServiceTestsFixture.cpp b/tests/unittests/media/server/service/mediaPipelineService/MediaPipelineServiceTestsFixture.cpp index 0c43860d9..e52e292f9 100644 --- a/tests/unittests/media/server/service/mediaPipelineService/MediaPipelineServiceTestsFixture.cpp +++ b/tests/unittests/media/server/service/mediaPipelineService/MediaPipelineServiceTestsFixture.cpp @@ -579,9 +579,10 @@ void MediaPipelineServiceTests::createMediaPipelineShouldSuccess() .WillOnce(Return(ByMove(std::move(m_mediaPipelineCapabilities)))); m_sut = std::make_unique(m_playbackServiceMock, - m_mediaPipelineFactoryMock, - m_mediaPipelineCapabilitiesFactoryMock, - m_decryptionServiceMock); + m_mediaPipelineFactoryMock, + m_mediaPipelineCapabilitiesFactoryMock, + m_decryptionServiceMock, + m_metricsServiceMock); } void MediaPipelineServiceTests::createMediaPipelineShouldFailWhenMediaPipelineCapabilitiesFactoryReturnsNullptr() @@ -590,9 +591,10 @@ void MediaPipelineServiceTests::createMediaPipelineShouldFailWhenMediaPipelineCa .WillOnce(Return(ByMove(std::unique_ptr()))); EXPECT_THROW(m_sut = std::make_unique(m_playbackServiceMock, - m_mediaPipelineFactoryMock, - m_mediaPipelineCapabilitiesFactoryMock, - m_decryptionServiceMock), + m_mediaPipelineFactoryMock, + m_mediaPipelineCapabilitiesFactoryMock, + m_decryptionServiceMock, + m_metricsServiceMock), std::runtime_error); } diff --git a/tests/unittests/media/server/service/mediaPipelineService/MediaPipelineServiceTestsFixture.h b/tests/unittests/media/server/service/mediaPipelineService/MediaPipelineServiceTestsFixture.h index d76ca55a3..4cc1dff88 100644 --- a/tests/unittests/media/server/service/mediaPipelineService/MediaPipelineServiceTestsFixture.h +++ b/tests/unittests/media/server/service/mediaPipelineService/MediaPipelineServiceTestsFixture.h @@ -28,6 +28,7 @@ #include "MediaPipelineServerInternalMock.h" #include "MediaPipelineService.h" #include "PlaybackServiceMock.h" +#include "PrivateMetricsServiceMock.h" #include "SharedMemoryBufferMock.h" #include #include @@ -238,6 +239,7 @@ class MediaPipelineServiceTests : public testing::Test StrictMock &m_mediaPipelineMock; StrictMock m_decryptionServiceMock; StrictMock m_playbackServiceMock; + StrictMock m_metricsServiceMock; std::shared_ptr> m_heartbeatProcedureMock; std::unique_ptr m_sut; }; diff --git a/tests/unittests/media/server/service/metrics/MetricsClientsTests.cpp b/tests/unittests/media/server/service/metrics/MetricsClientsTests.cpp new file mode 100644 index 000000000..33d7c71cc --- /dev/null +++ b/tests/unittests/media/server/service/metrics/MetricsClientsTests.cpp @@ -0,0 +1,106 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MediaPipelineClientMock.h" +#include "MediaPipelineMetricsClient.h" +#include "PrivateMetricsServiceMock.h" +#include "WebAudioPlayerClientMock.h" +#include "WebAudioPlayerMetricsClient.h" +#include + +using namespace firebolt::rialto; +using namespace firebolt::rialto::server::service; +using testing::_; +using testing::Ref; +using testing::StrictMock; + +TEST(MetricsClientsTests, mediaPipelineStateIsReportedAndForwarded) +{ + constexpr int kSessionId{7}; + auto client{std::make_shared>()}; + StrictMock metricsService; + MediaPipelineMetricsClient sut{kSessionId, client, metricsService}; + + EXPECT_CALL(metricsService, + notifyPlaybackStateChanged(kSessionId, PlaybackState::UNKNOWN, PlaybackState::PLAYING)); + EXPECT_CALL(*client, notifyPlaybackState(PlaybackState::PLAYING)); + sut.notifyPlaybackState(PlaybackState::PLAYING); + + EXPECT_CALL(metricsService, + notifyPlaybackStateChanged(kSessionId, PlaybackState::PLAYING, PlaybackState::PAUSED)); + EXPECT_CALL(*client, notifyPlaybackState(PlaybackState::PAUSED)); + sut.notifyPlaybackState(PlaybackState::PAUSED); +} + +TEST(MetricsClientsTests, mediaPipelineCallbacksAreForwarded) +{ + auto client{std::make_shared>()}; + StrictMock metricsService; + MediaPipelineMetricsClient sut{1, client, metricsService}; + auto shmInfo{std::make_shared(MediaPlayerShmInfo{1, 2, 3, 4})}; + const QosInfo qosInfo{5, 6}; + const PlaybackInfo playbackInfo{7, 0.5}; + + EXPECT_CALL(*client, notifyDuration(10)); + sut.notifyDuration(10); + EXPECT_CALL(*client, notifyPosition(11)); + sut.notifyPosition(11); + EXPECT_CALL(*client, notifyNativeSize(1920, 1080, 1.5)); + sut.notifyNativeSize(1920, 1080, 1.5); + EXPECT_CALL(*client, notifyNetworkState(NetworkState::IDLE)); + sut.notifyNetworkState(NetworkState::IDLE); + EXPECT_CALL(*client, notifyVideoData(true)); + sut.notifyVideoData(true); + EXPECT_CALL(*client, notifyAudioData(false)); + sut.notifyAudioData(false); + EXPECT_CALL(*client, notifyNeedMediaData(2, 3, 4, shmInfo)); + sut.notifyNeedMediaData(2, 3, 4, shmInfo); + EXPECT_CALL(*client, notifyCancelNeedMediaData(5)); + sut.notifyCancelNeedMediaData(5); + EXPECT_CALL(*client, notifyQos(6, Ref(qosInfo))); + sut.notifyQos(6, qosInfo); + EXPECT_CALL(*client, notifyBufferUnderflow(7)); + sut.notifyBufferUnderflow(7); + EXPECT_CALL(*client, notifyFirstFrameReceived(8)); + sut.notifyFirstFrameReceived(8); + EXPECT_CALL(*client, notifyPlaybackError(9, PlaybackError::DECRYPTION)); + sut.notifyPlaybackError(9, PlaybackError::DECRYPTION); + EXPECT_CALL(*client, notifySourceFlushed(10)); + sut.notifySourceFlushed(10); + EXPECT_CALL(*client, notifyPlaybackInfo(Ref(playbackInfo))); + sut.notifyPlaybackInfo(playbackInfo); +} + +TEST(MetricsClientsTests, webAudioStateIsReportedAndForwarded) +{ + constexpr int kHandle{9}; + auto client{std::make_shared>()}; + StrictMock metricsService; + WebAudioPlayerMetricsClient sut{kHandle, client, metricsService}; + + EXPECT_CALL(metricsService, + notifyWebAudioPlayerStateChanged(kHandle, WebAudioPlayerState::UNKNOWN, WebAudioPlayerState::PLAYING)); + EXPECT_CALL(*client, notifyState(WebAudioPlayerState::PLAYING)); + sut.notifyState(WebAudioPlayerState::PLAYING); + + EXPECT_CALL(metricsService, + notifyWebAudioPlayerStateChanged(kHandle, WebAudioPlayerState::PLAYING, WebAudioPlayerState::PAUSED)); + EXPECT_CALL(*client, notifyState(WebAudioPlayerState::PAUSED)); + sut.notifyState(WebAudioPlayerState::PAUSED); +} diff --git a/tests/unittests/media/server/service/metrics/PrivateMetricsServiceTests.cpp b/tests/unittests/media/server/service/metrics/PrivateMetricsServiceTests.cpp new file mode 100644 index 000000000..565f5966c --- /dev/null +++ b/tests/unittests/media/server/service/metrics/PrivateMetricsServiceTests.cpp @@ -0,0 +1,101 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Sky UK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MetricsCollectorClientMock.h" +#include "MetricsCollectorMock.h" +#include "PrivateMetricsService.h" +#include + +using namespace firebolt::rialto; +using namespace firebolt::rialto::server; +using namespace firebolt::rialto::server::service; +using testing::_; +using testing::ByMove; +using testing::Return; +using testing::StrictMock; + +class PrivateMetricsServiceTests : public testing::Test +{ +protected: + static constexpr int kClientId{4}; + + void addCollector() + { + auto collector{std::make_unique>()}; + m_collector = collector.get(); + EXPECT_CALL(*m_factory, create(kClientId, m_client, ApplicationState::UNKNOWN)) + .WillOnce(Return(ByMove(std::move(collector)))); + m_sut.clientReady(kClientId, m_client); + } + + std::shared_ptr> m_factory{ + std::make_shared>()}; + std::shared_ptr m_client{std::make_shared>()}; + PrivateMetricsService m_sut{m_factory}; + StrictMock *m_collector{nullptr}; +}; + +TEST_F(PrivateMetricsServiceTests, routesMetricsAndStateChangesToCollector) +{ + addCollector(); + ClientMetricsData metrics; + metrics.sampleId = 17; + + EXPECT_CALL(*m_collector, processMetrics(testing::Ref(metrics))); + m_sut.reportMetrics(kClientId, metrics); + EXPECT_CALL(*m_collector, + notifyPlaybackStateChanged(1, PlaybackState::PLAYING, PlaybackState::PAUSED)); + m_sut.notifyPlaybackStateChanged(1, PlaybackState::PLAYING, PlaybackState::PAUSED); + EXPECT_CALL(*m_collector, + notifyWebAudioPlayerStateChanged(2, WebAudioPlayerState::PLAYING, WebAudioPlayerState::PAUSED)); + m_sut.notifyWebAudioPlayerStateChanged(2, WebAudioPlayerState::PLAYING, WebAudioPlayerState::PAUSED); + EXPECT_CALL(*m_collector, + notifyApplicationStateChanged(ApplicationState::UNKNOWN, ApplicationState::RUNNING)); + m_sut.notifyApplicationStateChanged(ApplicationState::UNKNOWN, ApplicationState::RUNNING); +} + +TEST_F(PrivateMetricsServiceTests, disconnectRemovesCollector) +{ + addCollector(); + m_sut.clientDisconnected(kClientId); + m_collector = nullptr; + + m_sut.reportMetrics(kClientId, ClientMetricsData{}); + m_sut.notifyPlaybackStateChanged(1, PlaybackState::UNKNOWN, PlaybackState::PLAYING); +} + +TEST_F(PrivateMetricsServiceTests, collectorInheritsCurrentApplicationStateWhenClientConnects) +{ + m_sut.notifyApplicationStateChanged(ApplicationState::UNKNOWN, ApplicationState::INACTIVE); + m_sut.notifyApplicationStateChanged(ApplicationState::INACTIVE, ApplicationState::RUNNING); + + auto collector{std::make_unique>()}; + m_collector = collector.get(); + EXPECT_CALL(*m_factory, create(kClientId, m_client, ApplicationState::RUNNING)) + .WillOnce(Return(ByMove(std::move(collector)))); + m_sut.clientReady(kClientId, m_client); +} + +TEST_F(PrivateMetricsServiceTests, ignoresFactoryFailureAndUnknownDisconnect) +{ + EXPECT_CALL(*m_factory, create(kClientId, m_client, ApplicationState::UNKNOWN)) + .WillOnce(Return(ByMove(std::unique_ptr{}))); + m_sut.clientReady(kClientId, m_client); + m_sut.clientDisconnected(kClientId); +} diff --git a/tests/unittests/media/server/service/playbackService/PlaybackServiceTests.cpp b/tests/unittests/media/server/service/playbackService/PlaybackServiceTests.cpp index 2b765f62f..803413494 100644 --- a/tests/unittests/media/server/service/playbackService/PlaybackServiceTests.cpp +++ b/tests/unittests/media/server/service/playbackService/PlaybackServiceTests.cpp @@ -52,6 +52,12 @@ TEST_F(PlaybackServiceTests, shouldSetMaxWebAudioPlayers) getMaxWebAudioPlayersShouldSucceed(); } +TEST_F(PlaybackServiceTests, shouldExposePrivateMetricsService) +{ + createPlaybackServiceShouldSuccess(); + getPrivateMetricsServiceShouldSucceed(); +} + TEST_F(PlaybackServiceTests, shouldSetClientDisplayName) { createPlaybackServiceShouldSuccess(); diff --git a/tests/unittests/media/server/service/playbackService/PlaybackServiceTestsFixture.cpp b/tests/unittests/media/server/service/playbackService/PlaybackServiceTestsFixture.cpp index 1b6591d2e..e079db64e 100644 --- a/tests/unittests/media/server/service/playbackService/PlaybackServiceTestsFixture.cpp +++ b/tests/unittests/media/server/service/playbackService/PlaybackServiceTestsFixture.cpp @@ -148,6 +148,11 @@ void PlaybackServiceTests::getMaxWebAudioPlayersShouldSucceed() EXPECT_EQ(m_sut->getMaxWebAudioPlayers(), kMaxWebAudioPlayers); } +void PlaybackServiceTests::getPrivateMetricsServiceShouldSucceed() +{ + EXPECT_NE(&m_sut->getPrivateMetricsService(), nullptr); +} + void PlaybackServiceTests::clientDisplayNameShouldBeSet() { EXPECT_EQ(std::string(getenv("WAYLAND_DISPLAY")), kClientDisplayName); diff --git a/tests/unittests/media/server/service/playbackService/PlaybackServiceTestsFixture.h b/tests/unittests/media/server/service/playbackService/PlaybackServiceTestsFixture.h index 10f9d705c..02a8fdfa7 100644 --- a/tests/unittests/media/server/service/playbackService/PlaybackServiceTestsFixture.h +++ b/tests/unittests/media/server/service/playbackService/PlaybackServiceTestsFixture.h @@ -57,6 +57,7 @@ class PlaybackServiceTests : public testing::Test void getShmBufferShouldFail(); void getMaxPlaybacksShouldSucceed(); void getMaxWebAudioPlayersShouldSucceed(); + void getPrivateMetricsServiceShouldSucceed(); void clientDisplayNameShouldBeSet(); private: diff --git a/tests/unittests/media/server/service/sessionServerManager/SessionServerManagerTestsFixture.cpp b/tests/unittests/media/server/service/sessionServerManager/SessionServerManagerTestsFixture.cpp index 53199dcb7..69320cfca 100644 --- a/tests/unittests/media/server/service/sessionServerManager/SessionServerManagerTestsFixture.cpp +++ b/tests/unittests/media/server/service/sessionServerManager/SessionServerManagerTestsFixture.cpp @@ -67,7 +67,7 @@ SessionServerManagerTests::SessionServerManagerTests() { EXPECT_CALL(m_ipcFactoryMock, createApplicationManagementServer(_)) .WillOnce(Return(ByMove(std::move(m_applicationManagementServer)))); - EXPECT_CALL(m_ipcFactoryMock, createSessionManagementServer(_, _, _, _)) + EXPECT_CALL(m_ipcFactoryMock, createSessionManagementServer(_, _, _)) .WillOnce(Return(ByMove(std::move(m_sessionManagementServer)))); m_sut = std::make_unique(m_ipcFactoryMock, m_playbackServiceMock, m_cdmServiceMock, m_controlServiceMock, std::move(m_heartbeatProcedureFactory)); @@ -171,6 +171,8 @@ void SessionServerManagerTests::willFailToSetConfigurationWhenSessionManagementS EXPECT_CALL(m_playbackServiceMock, setResourceManagerAppName(kAppId)); EXPECT_CALL(m_playbackServiceMock, switchToInactive()); EXPECT_CALL(m_cdmServiceMock, switchToInactive()); + EXPECT_CALL(m_sessionManagementServerMock, + notifyApplicationStateChanged(ApplicationState::RUNNING, ApplicationState::INACTIVE)); EXPECT_CALL(m_applicationManagementServerMock, sendStateChangedEvent(SessionServerState::INACTIVE)) .WillOnce(Return(false)); EXPECT_TRUE(m_sut); @@ -191,6 +193,8 @@ void SessionServerManagerTests::willSetConfiguration() EXPECT_CALL(m_playbackServiceMock, setResourceManagerAppName(kAppId)); EXPECT_CALL(m_playbackServiceMock, switchToInactive()); EXPECT_CALL(m_cdmServiceMock, switchToInactive()); + EXPECT_CALL(m_sessionManagementServerMock, + notifyApplicationStateChanged(ApplicationState::RUNNING, ApplicationState::INACTIVE)); EXPECT_CALL(m_controlServiceMock, setApplicationState(ApplicationState::INACTIVE)); EXPECT_CALL(m_applicationManagementServerMock, sendStateChangedEvent(SessionServerState::INACTIVE)) .WillOnce(Return(true)); @@ -217,6 +221,8 @@ void SessionServerManagerTests::willSetConfigurationWithFd() EXPECT_CALL(m_playbackServiceMock, setResourceManagerAppName(kAppId)); EXPECT_CALL(m_playbackServiceMock, switchToInactive()); EXPECT_CALL(m_cdmServiceMock, switchToInactive()); + EXPECT_CALL(m_sessionManagementServerMock, + notifyApplicationStateChanged(ApplicationState::RUNNING, ApplicationState::INACTIVE)); EXPECT_CALL(m_controlServiceMock, setApplicationState(ApplicationState::INACTIVE)); EXPECT_CALL(m_applicationManagementServerMock, sendStateChangedEvent(SessionServerState::INACTIVE)) .WillOnce(Return(true)); @@ -257,6 +263,8 @@ void SessionServerManagerTests::willSetStateActive() { EXPECT_CALL(m_playbackServiceMock, switchToActive()).WillOnce(Return(true)); EXPECT_CALL(m_cdmServiceMock, switchToActive()).WillOnce(Return(true)); + EXPECT_CALL(m_sessionManagementServerMock, + notifyApplicationStateChanged(ApplicationState::INACTIVE, ApplicationState::RUNNING)); EXPECT_CALL(m_controlServiceMock, setApplicationState(ApplicationState::RUNNING)); EXPECT_CALL(m_applicationManagementServerMock, sendStateChangedEvent(SessionServerState::ACTIVE)).WillOnce(Return(true)); } @@ -265,6 +273,8 @@ void SessionServerManagerTests::willFailToSetStateInactive() { EXPECT_CALL(m_playbackServiceMock, switchToInactive()); EXPECT_CALL(m_cdmServiceMock, switchToInactive()); + EXPECT_CALL(m_sessionManagementServerMock, + notifyApplicationStateChanged(ApplicationState::RUNNING, ApplicationState::INACTIVE)); EXPECT_CALL(m_applicationManagementServerMock, sendStateChangedEvent(SessionServerState::INACTIVE)) .WillOnce(Return(false)); } @@ -273,6 +283,8 @@ void SessionServerManagerTests::willFailToSetStateInactiveAndGoBackToActive() { EXPECT_CALL(m_playbackServiceMock, switchToInactive()); EXPECT_CALL(m_cdmServiceMock, switchToInactive()); + EXPECT_CALL(m_sessionManagementServerMock, + notifyApplicationStateChanged(ApplicationState::RUNNING, ApplicationState::INACTIVE)); EXPECT_CALL(m_playbackServiceMock, switchToActive()).WillOnce(Return(false)); EXPECT_CALL(m_cdmServiceMock, switchToActive()).WillOnce(Return(false)); EXPECT_CALL(m_applicationManagementServerMock, sendStateChangedEvent(SessionServerState::INACTIVE)) @@ -283,6 +295,8 @@ void SessionServerManagerTests::willSetStateInactive() { EXPECT_CALL(m_playbackServiceMock, switchToInactive()); EXPECT_CALL(m_cdmServiceMock, switchToInactive()); + EXPECT_CALL(m_sessionManagementServerMock, + notifyApplicationStateChanged(ApplicationState::RUNNING, ApplicationState::INACTIVE)); EXPECT_CALL(m_controlServiceMock, setApplicationState(ApplicationState::INACTIVE)); EXPECT_CALL(m_applicationManagementServerMock, sendStateChangedEvent(SessionServerState::INACTIVE)) .WillOnce(Return(true)); diff --git a/tests/unittests/media/server/service/webAudioPlayerService/WebAudioPlayerServiceTestsFixture.cpp b/tests/unittests/media/server/service/webAudioPlayerService/WebAudioPlayerServiceTestsFixture.cpp index f273b8c15..2fbba7963 100644 --- a/tests/unittests/media/server/service/webAudioPlayerService/WebAudioPlayerServiceTestsFixture.cpp +++ b/tests/unittests/media/server/service/webAudioPlayerService/WebAudioPlayerServiceTestsFixture.cpp @@ -198,7 +198,8 @@ void WebAudioPlayerServiceTests::playbackServiceWillReturnSharedMemoryBuffer() void WebAudioPlayerServiceTests::createWebAudioPlayerService() { m_sut = std::make_unique(m_playbackServiceMock, - m_webAudioPlayerFactoryMock); + m_webAudioPlayerFactoryMock, + m_metricsServiceMock); } void WebAudioPlayerServiceTests::createWebAudioPlayerShouldSucceed() diff --git a/tests/unittests/media/server/service/webAudioPlayerService/WebAudioPlayerServiceTestsFixture.h b/tests/unittests/media/server/service/webAudioPlayerService/WebAudioPlayerServiceTestsFixture.h index f8377da0c..01590363e 100644 --- a/tests/unittests/media/server/service/webAudioPlayerService/WebAudioPlayerServiceTestsFixture.h +++ b/tests/unittests/media/server/service/webAudioPlayerService/WebAudioPlayerServiceTestsFixture.h @@ -22,6 +22,7 @@ #include "HeartbeatProcedureMock.h" #include "PlaybackServiceMock.h" +#include "PrivateMetricsServiceMock.h" #include "SharedMemoryBufferMock.h" #include "WebAudioPlayerServerInternalFactoryMock.h" #include "WebAudioPlayerServerInternalMock.h" @@ -100,6 +101,7 @@ class WebAudioPlayerServiceTests : public testing::Test std::unique_ptr m_webAudioPlayer; StrictMock &m_webAudioPlayerMock; StrictMock m_playbackServiceMock; + StrictMock m_metricsServiceMock; std::shared_ptr> m_heartbeatProcedureMock; std::unique_ptr m_sut; std::shared_ptr m_shmInfo; From dfb886b69366bf89b9944c1106e1df52c8270db1 Mon Sep 17 00:00:00 2001 From: Douglas Adler Date: Wed, 5 Aug 2026 16:50:22 -0500 Subject: [PATCH 08/11] Fix duplicate messages when CPU is over the threshold Adjust threshold for CPU change on the metrics sample periodic message Signed-off-by: Douglas Adler --- .../server/main/source/LogMetricsReporter.cpp | 8 ++-- .../main/source/MetricsThresholdChecker.cpp | 13 +++--- .../main/metrics/LogMetricsReporterTests.cpp | 41 +++++++++++++++---- .../main/metrics/MetricsHelpersTests.cpp | 40 ++++++++++++++++-- 4 files changed, 81 insertions(+), 21 deletions(-) diff --git a/media/server/main/source/LogMetricsReporter.cpp b/media/server/main/source/LogMetricsReporter.cpp index 282d1c247..6be12d587 100644 --- a/media/server/main/source/LogMetricsReporter.cpp +++ b/media/server/main/source/LogMetricsReporter.cpp @@ -27,7 +27,7 @@ namespace { constexpr std::uint64_t kActiveReportIntervalMs{10 * 60 * 1000}; constexpr double kRelativeChangeTolerance{0.10}; -constexpr double kCpuAbsoluteFloor{1.0}; +constexpr double kCpuChangeThresholdPercentagePoints{10.0}; constexpr double kMemoryAbsoluteFloorKb{1024.0}; } // namespace @@ -68,9 +68,9 @@ bool LogMetricsReporter::shouldReportPeriodicSample(const PeriodicMetricsReport const auto &previous{*m_lastReportedSample}; const bool stateChanged{report.applicationState != previous.applicationState}; const bool metricsChanged{ - changedSignificantly(report.clientCpuPercent, previous.clientCpuPercent, kCpuAbsoluteFloor) || - changedSignificantly(report.serverCpuPercent, previous.serverCpuPercent, kCpuAbsoluteFloor) || - changedSignificantly(report.combinedCpuPercent, previous.combinedCpuPercent, kCpuAbsoluteFloor) || + std::abs(report.clientCpuPercent - previous.clientCpuPercent) >= kCpuChangeThresholdPercentagePoints || + std::abs(report.serverCpuPercent - previous.serverCpuPercent) >= kCpuChangeThresholdPercentagePoints || + std::abs(report.combinedCpuPercent - previous.combinedCpuPercent) >= kCpuChangeThresholdPercentagePoints || changedSignificantly(static_cast(report.clientMemoryKb), static_cast(previous.clientMemoryKb), kMemoryAbsoluteFloorKb) || changedSignificantly(static_cast(report.serverMemoryKb), diff --git a/media/server/main/source/MetricsThresholdChecker.cpp b/media/server/main/source/MetricsThresholdChecker.cpp index bdfc237f5..c2d3e9331 100644 --- a/media/server/main/source/MetricsThresholdChecker.cpp +++ b/media/server/main/source/MetricsThresholdChecker.cpp @@ -55,6 +55,8 @@ void MetricsThresholdChecker::checkMetric(const MetricsThreshold &threshold, dou if (value >= threshold.criticalLevel) { state.belowCriticalCount = 0; + state.belowWarningCount = 0; + state.warningFired = true; if (!state.criticalFired) { state.criticalFired = true; @@ -65,14 +67,13 @@ void MetricsThresholdChecker::checkMetric(const MetricsThreshold &threshold, dou alert.severity = ThresholdSeverity::CRITICAL; m_reporter->reportThresholdExceeded(alert); } + return; } - else + + ++state.belowCriticalCount; + if (state.belowCriticalCount >= kDebounceSamples) { - ++state.belowCriticalCount; - if (state.belowCriticalCount >= kDebounceSamples) - { - state.criticalFired = false; - } + state.criticalFired = false; } // Warning check diff --git a/tests/unittests/media/server/main/metrics/LogMetricsReporterTests.cpp b/tests/unittests/media/server/main/metrics/LogMetricsReporterTests.cpp index 24734363a..f2cab4857 100644 --- a/tests/unittests/media/server/main/metrics/LogMetricsReporterTests.cpp +++ b/tests/unittests/media/server/main/metrics/LogMetricsReporterTests.cpp @@ -72,22 +72,49 @@ class LogMetricsReporterTests : public testing::Test RIALTO_DEBUG_LEVEL m_previousLevels{RIALTO_DEBUG_LEVEL_DEFAULT}; }; -TEST_F(LogMetricsReporterTests, inactiveSamplesOnlyLogAfterSignificantGaugeChange) +TEST_F(LogMetricsReporterTests, allCpuGaugesUseTenPercentagePointThreshold) +{ + for (const auto cpuGauge : {&PeriodicMetricsReport::clientCpuPercent, + &PeriodicMetricsReport::serverCpuPercent, + &PeriodicMetricsReport::combinedCpuPercent}) + { + g_logCount = 0; + LogMetricsReporter sut; + auto report{makeReport(ApplicationState::INACTIVE, 0)}; + const double baseline{report.*cpuGauge}; + sut.reportPeriodicSample(report); + EXPECT_EQ(g_logCount, 1); + + report.*cpuGauge = baseline + 9.99; + sut.reportPeriodicSample(report); + EXPECT_EQ(g_logCount, 1); + + report.*cpuGauge = baseline + 10.0; + sut.reportPeriodicSample(report); + EXPECT_EQ(g_logCount, 2); + + report.*cpuGauge = baseline + 0.01; + sut.reportPeriodicSample(report); + EXPECT_EQ(g_logCount, 2); + + report.*cpuGauge = baseline; + sut.reportPeriodicSample(report); + EXPECT_EQ(g_logCount, 3); + } +} + +TEST_F(LogMetricsReporterTests, inactiveMemoryChangesKeepRelativeThreshold) { LogMetricsReporter sut; auto report{makeReport(ApplicationState::INACTIVE, 0)}; sut.reportPeriodicSample(report); EXPECT_EQ(g_logCount, 1); - report.monotonicTimeMs = 10 * 60 * 1000; - report.clientCpuTimeMs = 1000000; - report.serverCpuTimeMs = 2000000; - report.clientCpuPercent = 21.9; - report.clientMemoryKb = 109000; + report.clientMemoryKb = 109999; sut.reportPeriodicSample(report); EXPECT_EQ(g_logCount, 1); - report.clientCpuPercent = 22.0; + report.clientMemoryKb = 110000; sut.reportPeriodicSample(report); EXPECT_EQ(g_logCount, 2); } diff --git a/tests/unittests/media/server/main/metrics/MetricsHelpersTests.cpp b/tests/unittests/media/server/main/metrics/MetricsHelpersTests.cpp index d73ef20ef..8d2bff9ba 100644 --- a/tests/unittests/media/server/main/metrics/MetricsHelpersTests.cpp +++ b/tests/unittests/media/server/main/metrics/MetricsHelpersTests.cpp @@ -106,18 +106,50 @@ TEST(MetricsThresholdCheckerTests, reportsOnceAndRearmsAfterTwoLowerSamples) MetricsThresholdChecker sut{config, &reporter}; EXPECT_CALL(reporter, reportThresholdExceeded(_)) - .Times(2) - .WillRepeatedly(Invoke([](const ThresholdAlert &alert) - { EXPECT_EQ(alert.metricName, "client_cpu"); })); + .WillOnce(Invoke([](const ThresholdAlert &alert) + { + EXPECT_EQ(alert.metricName, "client_cpu"); + EXPECT_EQ(alert.severity, ThresholdSeverity::CRITICAL); + })); sut.checkSample(96.0, 0.0, 0.0, 0, 0, 0, 0); sut.checkSample(96.0, 0.0, 0.0, 0, 0, 0, 0); sut.checkSample(0.0, 0.0, 0.0, 0, 0, 0, 0); sut.checkSample(0.0, 0.0, 0.0, 0, 0, 0, 0); - EXPECT_CALL(reporter, reportThresholdExceeded(_)).Times(2); + EXPECT_CALL(reporter, reportThresholdExceeded(_)) + .WillOnce(Invoke([](const ThresholdAlert &alert) + { EXPECT_EQ(alert.severity, ThresholdSeverity::CRITICAL); })); sut.checkSample(96.0, 0.0, 0.0, 0, 0, 0, 0); } +TEST(MetricsThresholdCheckerTests, warningEscalatesToCriticalWithoutDuplicateWarning) +{ + StrictMock reporter; + MetricsThresholdChecker sut{MetricsThresholdConfig{}, &reporter}; + + { + testing::InSequence sequence; + EXPECT_CALL(reporter, reportThresholdExceeded(_)) + .WillOnce(Invoke([](const ThresholdAlert &alert) + { EXPECT_EQ(alert.severity, ThresholdSeverity::WARNING); })); + EXPECT_CALL(reporter, reportThresholdExceeded(_)) + .WillOnce(Invoke([](const ThresholdAlert &alert) + { EXPECT_EQ(alert.severity, ThresholdSeverity::CRITICAL); })); + + sut.checkSample(81.0, 0.0, 0.0, 0, 0, 0, 0); + sut.checkSample(96.0, 0.0, 0.0, 0, 0, 0, 0); + } + + sut.checkSample(90.0, 0.0, 0.0, 0, 0, 0, 0); + sut.checkSample(0.0, 0.0, 0.0, 0, 0, 0, 0); + sut.checkSample(0.0, 0.0, 0.0, 0, 0, 0, 0); + + EXPECT_CALL(reporter, reportThresholdExceeded(_)) + .WillOnce(Invoke([](const ThresholdAlert &alert) + { EXPECT_EQ(alert.severity, ThresholdSeverity::WARNING); })); + sut.checkSample(81.0, 0.0, 0.0, 0, 0, 0, 0); +} + TEST(MetricsThresholdCheckerTests, reportsConfiguredMetricsIncludingCgroupPercentage) { StrictMock reporter; From fef4b13089d4a0fedb549fd496a11fb9228e55ad Mon Sep 17 00:00:00 2001 From: Douglas Adler Date: Wed, 5 Aug 2026 16:52:00 -0500 Subject: [PATCH 09/11] Remove local WPEFramework CMake configs --- cmake/wpeframeworkcom-config.cmake | 51 ----------------------------- cmake/wpeframeworkcore-config.cmake | 51 ----------------------------- 2 files changed, 102 deletions(-) delete mode 100644 cmake/wpeframeworkcom-config.cmake delete mode 100644 cmake/wpeframeworkcore-config.cmake diff --git a/cmake/wpeframeworkcom-config.cmake b/cmake/wpeframeworkcom-config.cmake deleted file mode 100644 index a83d16276..000000000 --- a/cmake/wpeframeworkcom-config.cmake +++ /dev/null @@ -1,51 +0,0 @@ -# -# If not stated otherwise in this file or this component's LICENSE file the -# following copyright and licenses apply: -# -# Copyright 2024 Sky UK -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -set(WPEFRAMEWORK_COM_VERSION 1.0.0) - - -####### Expanded from @PACKAGE_INIT@ by configure_package_config_file() ####### -####### Any changes to this file will be overwritten by the next CMake run #### -####### The input file was wpeframeworkcom-config.cmake.in ######## - -get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../../rialto-build/install" ABSOLUTE) - -macro(set_and_check _var _file) - set(${_var} "${_file}") - if(NOT EXISTS "${_file}") - message(FATAL_ERROR "File or directory ${_file} referenced by variable ${_var} does not exist !") - endif() -endmacro() - -macro(check_required_components _NAME) - foreach(comp ${${_NAME}_FIND_COMPONENTS}) - if(NOT ${_NAME}_${comp}_FOUND) - if(${_NAME}_FIND_REQUIRED_${comp}) - set(${_NAME}_FOUND FALSE) - endif() - endif() - endforeach() -endmacro() - -#################################################################################### - -set_and_check(WPEFRAMEWORK_COM_INCLUDE_DIRS "/home/dadler/development/rialto/rialto/stubs/wpeframework-com/third-party/Source/") - - -check_required_components(WPEFrameworkCOM) diff --git a/cmake/wpeframeworkcore-config.cmake b/cmake/wpeframeworkcore-config.cmake deleted file mode 100644 index c20847e4e..000000000 --- a/cmake/wpeframeworkcore-config.cmake +++ /dev/null @@ -1,51 +0,0 @@ -# -# If not stated otherwise in this file or this component's LICENSE file the -# following copyright and licenses apply: -# -# Copyright 2024 Sky UK -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -set(WPEFRAMEWORK_CORE_VERSION 1.0.0) - - -####### Expanded from @PACKAGE_INIT@ by configure_package_config_file() ####### -####### Any changes to this file will be overwritten by the next CMake run #### -####### The input file was wpeframeworkcore-config.cmake.in ######## - -get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../../rialto-build/install" ABSOLUTE) - -macro(set_and_check _var _file) - set(${_var} "${_file}") - if(NOT EXISTS "${_file}") - message(FATAL_ERROR "File or directory ${_file} referenced by variable ${_var} does not exist !") - endif() -endmacro() - -macro(check_required_components _NAME) - foreach(comp ${${_NAME}_FIND_COMPONENTS}) - if(NOT ${_NAME}_${comp}_FOUND) - if(${_NAME}_FIND_REQUIRED_${comp}) - set(${_NAME}_FOUND FALSE) - endif() - endif() - endforeach() -endmacro() - -#################################################################################### - -set_and_check(WPEFRAMEWORK_CORE_INCLUDE_DIRS "/home/dadler/development/rialto/rialto/stubs/wpeframework-core/third-party/Source/") - - -check_required_components(WPEFrameworkCore) From 54eb72f9556f90451293d5f37c9b72732d485240 Mon Sep 17 00:00:00 2001 From: Douglas Adler Date: Wed, 5 Aug 2026 17:02:09 -0500 Subject: [PATCH 10/11] CoPilot Review suggestions Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- media/server/service/source/PrivateMetricsService.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/media/server/service/source/PrivateMetricsService.cpp b/media/server/service/source/PrivateMetricsService.cpp index fb5643286..59a24e218 100644 --- a/media/server/service/source/PrivateMetricsService.cpp +++ b/media/server/service/source/PrivateMetricsService.cpp @@ -19,6 +19,7 @@ #include "PrivateMetricsService.h" #include "RialtoServerLogging.h" +#include #include #include #include From 08beb4ad0ce7f63e8caf6612b870e4f1d352788c Mon Sep 17 00:00:00 2001 From: Douglas Adler Date: Wed, 5 Aug 2026 17:14:11 -0500 Subject: [PATCH 11/11] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- media/client/ipc/source/PrivateMetricsIpc.cpp | 2 ++ media/client/main/source/ClientController.cpp | 2 +- media/server/service/source/PrivateMetricsService.cpp | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/media/client/ipc/source/PrivateMetricsIpc.cpp b/media/client/ipc/source/PrivateMetricsIpc.cpp index 283e78e9a..b6394851c 100644 --- a/media/client/ipc/source/PrivateMetricsIpc.cpp +++ b/media/client/ipc/source/PrivateMetricsIpc.cpp @@ -33,6 +33,8 @@ const char *sampleReasonToString(::firebolt::rialto::MetricsSampleReason reason) return "CONNECTED"; case firebolt::rialto::METRICS_SAMPLE_REASON_PERIODIC: return "PERIODIC"; + case firebolt::rialto::METRICS_SAMPLE_REASON_STATE_TRANSITION: + return "STATE_TRANSITION"; case firebolt::rialto::METRICS_SAMPLE_REASON_UNKNOWN: default: return "UNKNOWN"; diff --git a/media/client/main/source/ClientController.cpp b/media/client/main/source/ClientController.cpp index 0cd4b942d..13f9c9def 100644 --- a/media/client/main/source/ClientController.cpp +++ b/media/client/main/source/ClientController.cpp @@ -23,9 +23,9 @@ #include #include #include +#include #include #include -#include #include #include #include diff --git a/media/server/service/source/PrivateMetricsService.cpp b/media/server/service/source/PrivateMetricsService.cpp index 59a24e218..7e6d5a149 100644 --- a/media/server/service/source/PrivateMetricsService.cpp +++ b/media/server/service/source/PrivateMetricsService.cpp @@ -42,6 +42,11 @@ void PrivateMetricsService::clientReady(int clientId, const std::shared_ptr &client) { std::lock_guard lock{m_mutex}; + if (!m_collectorFactory) + { + RIALTO_SERVER_LOG_ERROR("MetricsCollectorFactory is null; cannot create MetricsCollector for client %d", clientId); + return; + } auto collector = m_collectorFactory->create(clientId, client, m_currentApplicationState); if (collector) {