diff --git a/docs/MetricsDesign.md b/docs/MetricsDesign.md new file mode 100644 index 000000000..aaad296b9 --- /dev/null +++ b/docs/MetricsDesign.md @@ -0,0 +1,451 @@ +# 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 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 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`) + +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 Architecture + +The server follows Rialto's standard three-layer architecture (ipc → service → main): + +``` +┌──────────────────────────────────────────────────────────────┐ +│ 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 │ +└──────────────────────────────────────────────────────────────┘ +``` + +#### IPC Layer (`server/ipc`) + +`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 + +#### 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); +} +``` + +- `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` (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 + +```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 --> 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| 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` | 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 | +| `media/server/service/source/SessionServerManager.cpp` | Triggers app state notifications | + +### Metrics Framework + +| File | Purpose | +|------|---------| +| `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 + +| File | Purpose | +|------|---------| +| `proto/privatemetricsmodule.proto` | IPC message and service definitions | 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/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/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..be10f066d --- /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, std::uint64_t processMemoryKb) 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..a23dffae7 --- /dev/null +++ b/media/client/ipc/interface/IPrivateMetricsIpc.h @@ -0,0 +1,74 @@ +/* + * 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, + std::uint64_t processMemoryKb) = 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 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 new file mode 100644 index 000000000..b6394851c --- /dev/null +++ b/media/client/ipc/source/PrivateMetricsIpc.cpp @@ -0,0 +1,214 @@ +/* + * 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_STATE_TRANSITION: + return "STATE_TRANSITION"; + 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, + std::uint64_t processMemoryKb) +{ + 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); + metrics->set_process_memory_kb(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(); + auto blockingClosure = m_ipc.createBlockingClosure(); + m_privateMetricsStub->reportClientMetrics(ipcController.get(), &request, &response, blockingClosure.get()); + + blockingClosure->wait(); + + if (ipcController->Failed()) + { + RIALTO_CLIENT_LOG_DEBUG("Failed to report client metrics due to '%s'", ipcController->ErrorText().c_str()); + return false; + } + + RIALTO_CLIENT_LOG_DEBUG("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_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/include/ClientController.h b/media/client/main/include/ClientController.h index c209b3e3f..af522b917 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,31 @@ 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 process RSS memory usage in kilobytes. + */ + std::uint64_t getProcessMemoryKb() const; + + /** + * @brief Gets the process name used for metrics reporting. + */ + std::string getProcessName() const; + private: /** * @brief Mutex protection for class attributes. @@ -106,6 +134,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 5f9fe6b6f..13f9c9def 100644 --- a/media/client/main/source/ClientController.cpp +++ b/media/client/main/source/ClientController.cpp @@ -20,10 +20,15 @@ #include "ClientController.h" #include "RialtoClientLogging.h" #include "SharedMemoryHandle.h" +#include +#include +#include +#include #include -#include +#include #include #include +#include #include #include @@ -45,11 +50,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:"); @@ -78,6 +85,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() @@ -279,4 +292,79 @@ 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(), + getProcessMemoryKb())) + { + RIALTO_CLIENT_LOG_DEBUG("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"; +} + +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/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 a374ffb0e..2e71d1728 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.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); + } + + 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); @@ -1965,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( @@ -1986,18 +2119,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/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/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/IMediaPipelineModuleService.h b/media/server/ipc/include/IMediaPipelineModuleService.h index 877d3912b..a26f4091d 100644 --- a/media/server/ipc/include/IMediaPipelineModuleService.h +++ b/media/server/ipc/include/IMediaPipelineModuleService.h @@ -28,7 +28,6 @@ namespace firebolt::rialto::server::ipc { class IMediaPipelineModuleService; - /** * @brief IMediaPipelineModuleService factory class, returns a concrete implementation of IMediaPipelineModuleService */ @@ -82,6 +81,7 @@ 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; + }; } // namespace firebolt::rialto::server::ipc diff --git a/media/server/ipc/include/IPrivateMetricsModuleService.h b/media/server/ipc/include/IPrivateMetricsModuleService.h new file mode 100644 index 000000000..5ea0a75c7 --- /dev/null +++ b/media/server/ipc/include/IPrivateMetricsModuleService.h @@ -0,0 +1,65 @@ +/* + * 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 "ControlCommon.h" +#include "IPrivateMetricsService.h" +#include "MediaCommon.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(service::IPrivateMetricsService &metricsService) 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; + + virtual void notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) = 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..5684f7297 --- /dev/null +++ b/media/server/ipc/include/PrivateMetricsModuleService.h @@ -0,0 +1,83 @@ +/* + * 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 "IMetricsCollectorClient.h" +#include "IPrivateMetricsModuleService.h" +#include "IPrivateMetricsService.h" +#include +#include +#include +#include +#include + +namespace firebolt::rialto::server::ipc +{ +class PrivateMetricsModuleServiceFactory : public IPrivateMetricsModuleServiceFactory +{ +public: + PrivateMetricsModuleServiceFactory() = default; + ~PrivateMetricsModuleServiceFactory() override = default; + + std::shared_ptr + create(service::IPrivateMetricsService &metricsService) const override; +}; + +class PrivateMetricsModuleService : public IPrivateMetricsModuleService, + public firebolt::rialto::server::IMetricsCollectorClient +{ +public: + 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 notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) override; + + // PrivateMetricsModule RPC handlers + 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; + + // IMetricsCollectorClient + void requestMetricsSample(int clientId, std::uint64_t sampleId, + firebolt::rialto::server::MetricsSampleReason reason) override; + +private: + service::IPrivateMetricsService &m_metricsService; + std::atomic m_nextClientId{1}; + std::mutex m_mutex; + + // 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 + +#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..9c227c390 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); @@ -66,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); @@ -81,6 +84,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/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/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 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/MediaPipelineClient.cpp b/media/server/ipc/source/MediaPipelineClient.cpp index a020d8ac6..da48e5a4b 100644 --- a/media/server/ipc/source/MediaPipelineClient.cpp +++ b/media/server/ipc/source/MediaPipelineClient.cpp @@ -137,7 +137,8 @@ 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) +MediaPipelineClient::MediaPipelineClient(int sessionId, + const std::shared_ptr<::firebolt::rialto::ipc::IClient> &ipcClient) : m_sessionId{sessionId}, m_ipcClient{ipcClient} { } diff --git a/media/server/ipc/source/MediaPipelineModuleService.cpp b/media/server/ipc/source/MediaPipelineModuleService.cpp index 1b2d29a12..5b6cca472 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" diff --git a/media/server/ipc/source/PrivateMetricsModuleService.cpp b/media/server/ipc/source/PrivateMetricsModuleService.cpp new file mode 100644 index 000000000..4770768e2 --- /dev/null +++ b/media/server/ipc/source/PrivateMetricsModuleService.cpp @@ -0,0 +1,255 @@ +/* + * 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 + +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(service::IPrivateMetricsService &metricsService) const +{ + std::shared_ptr privateMetricsModule; + + try + { + privateMetricsModule = std::make_shared(metricsService); + } + 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(service::IPrivateMetricsService &metricsService) + : m_metricsService{metricsService} +{ +} + +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}; + // Don't assign a clientId yet — wait for notifyClientReady + } + 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"); + 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, + 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()}; + const int kClientId{m_nextClientId.fetch_add(1)}; + { + std::lock_guard lock{m_mutex}; + m_clientIds[ipcClient] = kClientId; + m_ipcClients[kClientId] = ipcClient; + } + + RIALTO_SERVER_LOG_MIL("Client ready for private metrics samples, assigned clientId=%d", kClientId); + done->Run(); + + // 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) +{ + 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; + } + + auto ipcClient{ipcController->getClient()}; + int clientId{0}; + { + std::lock_guard lock{m_mutex}; + auto iter = m_clientIds.find(ipcClient); + if (iter != m_clientIds.end()) + { + clientId = iter->second; + } + } + + if (clientId == 0) + { + RIALTO_SERVER_LOG_WARN("reportClientMetrics from unknown client"); + done->Run(); + return; + } + + 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(); + + // Convert proto reason to our enum + switch (protoMetrics.reason()) + { + case firebolt::rialto::METRICS_SAMPLE_REASON_CONNECTED: + metrics.reason = firebolt::rialto::server::MetricsSampleReason::CONNECTED; + break; + case firebolt::rialto::METRICS_SAMPLE_REASON_PERIODIC: + metrics.reason = firebolt::rialto::server::MetricsSampleReason::PERIODIC; + break; + case firebolt::rialto::METRICS_SAMPLE_REASON_STATE_TRANSITION: + metrics.reason = firebolt::rialto::server::MetricsSampleReason::STATE_TRANSITION; + break; + default: + metrics.reason = firebolt::rialto::server::MetricsSampleReason::UNKNOWN; + break; + } + + done->Run(); + + m_metricsService.reportMetrics(clientId, metrics); +} + +void PrivateMetricsModuleService::notifyApplicationStateChanged(ApplicationState oldState, ApplicationState newState) +{ + m_metricsService.notifyApplicationStateChanged(oldState, newState); +} + +void PrivateMetricsModuleService::requestMetricsSample(int clientId, std::uint64_t sampleId, + firebolt::rialto::server::MetricsSampleReason reason) +{ + std::shared_ptr<::firebolt::rialto::ipc::IClient> ipcClient; + { + std::lock_guard lock{m_mutex}; + auto iter = m_ipcClients.find(clientId); + if (iter == m_ipcClients.end()) + { + return; + } + ipcClient = iter->second; + } + + if (!ipcClient || !ipcClient->isConnected()) + { + return; + } + + auto event{std::make_shared()}; + event->set_sample_id(sampleId); + + // Convert our enum to proto enum + switch (reason) + { + 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; + } + + RIALTO_SERVER_LOG_DEBUG("Requesting metrics sample=%" PRIu64 " from client %d", sampleId, clientId); + + if (!ipcClient->sendEvent(event)) + { + 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 ab9954a92..44f91e926 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(playbackService.getPrivateMetricsService())}, m_controlModule{controlModuleFactory->create(playbackService, controlService)} { m_ipcServer = ipcFactory->create(); @@ -161,6 +164,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"); @@ -170,6 +181,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 +194,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/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/main/include/CompositeMetricsReporter.h b/media/server/main/include/CompositeMetricsReporter.h new file mode 100644 index 000000000..b0aef77e1 --- /dev/null +++ b/media/server/main/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_COMPOSITE_METRICS_REPORTER_H_ +#define FIREBOLT_RIALTO_SERVER_COMPOSITE_METRICS_REPORTER_H_ + +#include "IMetricsReporter.h" +#include +#include + +namespace firebolt::rialto::server +{ +/** + * @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 + +#endif // FIREBOLT_RIALTO_SERVER_COMPOSITE_METRICS_REPORTER_H_ diff --git a/media/server/main/include/IMetricsReporter.h b/media/server/main/include/IMetricsReporter.h new file mode 100644 index 000000000..56914cda1 --- /dev/null +++ b/media/server/main/include/IMetricsReporter.h @@ -0,0 +1,104 @@ +/* + * 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_REPORTER_H_ +#define FIREBOLT_RIALTO_SERVER_I_METRICS_REPORTER_H_ + +#include "ControlCommon.h" +#include "StateMetricsAggregator.h" +#include +#include +#include + +namespace firebolt::rialto::server +{ +/** + * @brief Periodic sample data reported each sampling interval. + */ +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}; + 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}; + std::uint64_t shmMemoryKb{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 + +#endif // FIREBOLT_RIALTO_SERVER_I_METRICS_REPORTER_H_ diff --git a/media/server/main/include/LogMetricsReporter.h b/media/server/main/include/LogMetricsReporter.h new file mode 100644 index 000000000..017286e59 --- /dev/null +++ b/media/server/main/include/LogMetricsReporter.h @@ -0,0 +1,51 @@ +/* + * 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_LOG_METRICS_REPORTER_H_ +#define FIREBOLT_RIALTO_SERVER_LOG_METRICS_REPORTER_H_ + +#include "IMetricsReporter.h" +#include +#include + +namespace firebolt::rialto::server +{ +/** + * @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; + +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 + +#endif // FIREBOLT_RIALTO_SERVER_LOG_METRICS_REPORTER_H_ diff --git a/media/server/main/include/MetricsAccumulator.h b/media/server/main/include/MetricsAccumulator.h new file mode 100644 index 000000000..7a53243c9 --- /dev/null +++ b/media/server/main/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_METRICS_ACCUMULATOR_H_ +#define FIREBOLT_RIALTO_SERVER_METRICS_ACCUMULATOR_H_ + +#include +#include +#include + +namespace firebolt::rialto::server +{ +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 + +#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..6f03b6075 --- /dev/null +++ b/media/server/main/include/MetricsCollector.h @@ -0,0 +1,125 @@ +/* + * 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, + ApplicationState initialApplicationState) override; +}; + +class MetricsCollector : public IMetricsCollector +{ +public: + MetricsCollector(int clientId, const std::shared_ptr &client, + 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: + 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}; + std::uint64_t shmMemoryKb{0}; + }; + + struct PreviousSample + { + std::uint64_t clientMonotonicTimeMs{0}; + std::uint64_t clientCpuTimeMs{0}; + std::uint64_t clientMemoryKb{0}; + ProcessMetricsSample serverMetrics; + }; + + struct SessionMetricsState + { + std::string currentState; + 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 *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-player state tracking (typed context -> 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/main/include/MetricsThresholdChecker.h b/media/server/main/include/MetricsThresholdChecker.h new file mode 100644 index 000000000..af4e0c575 --- /dev/null +++ b/media/server/main/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_METRICS_THRESHOLD_CHECKER_H_ +#define FIREBOLT_RIALTO_SERVER_METRICS_THRESHOLD_CHECKER_H_ + +#include "IMetricsReporter.h" +#include +#include +#include + +namespace firebolt::rialto::server +{ +/** + * @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 + +#endif // FIREBOLT_RIALTO_SERVER_METRICS_THRESHOLD_CHECKER_H_ diff --git a/media/server/main/include/StateMetricsAggregator.h b/media/server/main/include/StateMetricsAggregator.h new file mode 100644 index 000000000..adf2279c0 --- /dev/null +++ b/media/server/main/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_STATE_METRICS_AGGREGATOR_H_ +#define FIREBOLT_RIALTO_SERVER_STATE_METRICS_AGGREGATOR_H_ + +#include "MetricsAccumulator.h" +#include +#include + +namespace firebolt::rialto::server +{ +/** + * @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 + +#endif // FIREBOLT_RIALTO_SERVER_STATE_METRICS_AGGREGATOR_H_ diff --git a/media/server/main/interface/IMainThread.h b/media/server/main/interface/IMainThread.h index 3a920a67a..f8bd50e2e 100644 --- a/media/server/main/interface/IMainThread.h +++ b/media/server/main/interface/IMainThread.h @@ -20,6 +20,7 @@ #ifndef FIREBOLT_RIALTO_SERVER_I_MAIN_THREAD_H_ #define FIREBOLT_RIALTO_SERVER_I_MAIN_THREAD_H_ +#include #include #include #include diff --git a/media/server/main/interface/IMetricsCollector.h b/media/server/main/interface/IMetricsCollector.h new file mode 100644 index 000000000..5e8088239 --- /dev/null +++ b/media/server/main/interface/IMetricsCollector.h @@ -0,0 +1,124 @@ +/* + * 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. + * @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, + ApplicationState initialApplicationState) = 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 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). + * + * 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/main/source/CompositeMetricsReporter.cpp b/media/server/main/source/CompositeMetricsReporter.cpp new file mode 100644 index 000000000..20181707b --- /dev/null +++ b/media/server/main/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 +{ +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 diff --git a/media/server/main/source/LogMetricsReporter.cpp b/media/server/main/source/LogMetricsReporter.cpp new file mode 100644 index 000000000..6be12d587 --- /dev/null +++ b/media/server/main/source/LogMetricsReporter.cpp @@ -0,0 +1,127 @@ +/* + * 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 +#include +#include + +namespace +{ +constexpr std::uint64_t kActiveReportIntervalMs{10 * 60 * 1000}; +constexpr double kRelativeChangeTolerance{0.10}; +constexpr double kCpuChangeThresholdPercentagePoints{10.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 ", " + "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); +} + +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{ + 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), + 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}; + 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 diff --git a/media/server/main/source/MetricsCollector.cpp b/media/server/main/source/MetricsCollector.cpp new file mode 100644 index 000000000..c19838d03 --- /dev/null +++ b/media/server/main/source/MetricsCollector.cpp @@ -0,0 +1,593 @@ +/* + * 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}; +constexpr unsigned int kResponseTimeoutTimerCount{2}; +} // 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, + ApplicationState initialApplicationState) +{ + std::unique_ptr collector; + try + { + auto timerFactory = firebolt::rialto::common::ITimerFactory::getFactory(); + collector = std::make_unique(clientId, client, timerFactory, initialApplicationState); + } + 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, + 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); + + // Request initial baseline sample + m_client->requestMetricsSample(m_clientId, m_nextSampleId++, MetricsSampleReason::CONNECTED); +} + +MetricsCollector::~MetricsCollector() +{ + if (m_timer) + { + m_timer->cancel(); + } +} + +void MetricsCollector::onTimerFired() +{ + 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) +{ + 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()) + { + // 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.monotonicTimeMs = kServerMetrics.monotonicTimeMs; + periodicReport.reason = sampleReasonToString(metrics.reason); + periodicReport.applicationState = applicationState; + 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; + periodicReport.shmMemoryKb = kServerMetrics.shmMemoryKb; + 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 &[unusedContext, sessionState] : m_sessionStates) + { + (void)unusedContext; + 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) +{ + 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; + 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(context)}; + if (m_sessionStates.end() == sessionIter) + { + // First state notification for this session — create entry + SessionMetricsState sessionState; + sessionState.currentState = newState; + sessionState.aggregator.begin(newState, kNowMs); + m_sessionStates.emplace(context, 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 = context; + transitionReport.metrics = report; + m_reporter->reportStateTransition(transitionReport); + } + + if (terminalState) + { + // Terminal state — remove session tracking + m_sessionStates.erase(sessionIter); + } + else + { + // Begin accumulating for new state + sessionState.currentState = newState; + sessionState.aggregator.begin(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; + } + + 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, shmMemoryKb}; +} + +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::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) + { + 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/main/source/MetricsThresholdChecker.cpp b/media/server/main/source/MetricsThresholdChecker.cpp new file mode 100644 index 000000000..c2d3e9331 --- /dev/null +++ b/media/server/main/source/MetricsThresholdChecker.cpp @@ -0,0 +1,103 @@ +/* + * 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 +{ +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; + state.belowWarningCount = 0; + state.warningFired = true; + 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); + } + return; + } + + ++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 diff --git a/media/server/service/CMakeLists.txt b/media/server/service/CMakeLists.txt index 0ec9d559d..9737fd11b 100644 --- a/media/server/service/CMakeLists.txt +++ b/media/server/service/CMakeLists.txt @@ -35,7 +35,10 @@ 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 ( RialtoServerService 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 new file mode 100644 index 000000000..e3c9f1d50 --- /dev/null +++ b/media/server/service/include/IPrivateMetricsService.h @@ -0,0 +1,97 @@ +/* + * 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 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). + * + * 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/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 8a3b06ff0..90f74984c 100644 --- a/media/server/service/source/PlaybackService.cpp +++ b/media/server/service/source/PlaybackService.cpp @@ -18,11 +18,14 @@ */ #include "PlaybackService.h" +#include "IMetricsCollector.h" +#include "PrivateMetricsService.h" #include "IMediaPipelineServerInternal.h" #include "IWebAudioPlayerServerInternal.h" #include "RialtoServerLogging.h" #include #include +#include #include #include #include @@ -35,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"); } @@ -72,6 +77,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) @@ -147,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 new file mode 100644 index 000000000..7e6d5a149 --- /dev/null +++ b/media/server/service/source/PrivateMetricsService.cpp @@ -0,0 +1,220 @@ +/* + * 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" +#include +#include +#include +#include + +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}; + 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) + { + 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::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); + } + + // 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/PrivateMetricsService.h b/media/server/service/source/PrivateMetricsService.h new file mode 100644 index 000000000..7349951d9 --- /dev/null +++ b/media/server/service/source/PrivateMetricsService.h @@ -0,0 +1,53 @@ +/* + * 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 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 + +#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 b63a7752f..a97298ddc 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; @@ -229,6 +231,9 @@ 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)) { m_controlService.setApplicationState(ApplicationState::INACTIVE); 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/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..1f96137ad --- /dev/null +++ b/proto/privatemetricsmodule.proto @@ -0,0 +1,68 @@ +/* + * 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; + METRICS_SAMPLE_REASON_STATE_TRANSITION = 3; +} + +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; + optional uint64 process_memory_kb = 8; +} + +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/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 be9a61329..5044d10bc 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,29 @@ 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); } + +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/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..a420f7aca --- /dev/null +++ b/tests/unittests/media/client/mocks/ipc/PrivateMetricsIpcMock.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_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, + std::uint64_t processMemoryKb), + (override)); +}; +} // namespace firebolt::rialto::client + +#endif // FIREBOLT_RIALTO_CLIENT_PRIVATE_METRICS_IPC_MOCK_H_ 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..f2cab4857 --- /dev/null +++ b/tests/unittests/media/server/main/metrics/LogMetricsReporterTests.cpp @@ -0,0 +1,163 @@ +/* + * 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, 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.clientMemoryKb = 109999; + sut.reportPeriodicSample(report); + EXPECT_EQ(g_logCount, 1); + + report.clientMemoryKb = 110000; + 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..8d2bff9ba --- /dev/null +++ b/tests/unittests/media/server/main/metrics/MetricsHelpersTests.cpp @@ -0,0 +1,166 @@ +/* + * 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(_)) + .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(_)) + .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; + 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/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/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 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 14b8e0342..69320cfca 100644 --- a/tests/unittests/media/server/service/sessionServerManager/SessionServerManagerTestsFixture.cpp +++ b/tests/unittests/media/server/service/sessionServerManager/SessionServerManagerTestsFixture.cpp @@ -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;