Line data Source code
1 : /*
2 : * If not stated otherwise in this file or this component's LICENSE file the
3 : * following copyright and licenses apply:
4 : *
5 : * Copyright 2026 Sky UK
6 : *
7 : * Licensed under the Apache License, Version 2.0 (the "License");
8 : * you may not use this file except in compliance with the License.
9 : * You may obtain a copy of the License at
10 : *
11 : * http://www.apache.org/licenses/LICENSE-2.0
12 : *
13 : * Unless required by applicable law or agreed to in writing, software
14 : * distributed under the License is distributed on an "AS IS" BASIS,
15 : * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 : * See the License for the specific language governing permissions and
17 : * limitations under the License.
18 : */
19 :
20 : #include "GstProfiler.h"
21 : #include "RialtoServerLogging.h"
22 : #include "Utils.h"
23 :
24 : #include <algorithm>
25 : #include <array>
26 : #include <cctype>
27 : #include <string>
28 : #include <string_view>
29 :
30 : #include <glib.h>
31 : #include <gst/gst.h>
32 :
33 : namespace firebolt::rialto::server
34 : {
35 : std::weak_ptr<IGstProfilerFactory> GstProfilerFactory::m_factory;
36 :
37 1 : std::shared_ptr<IGstProfilerFactory> IGstProfilerFactory::getFactory()
38 : {
39 1 : std::shared_ptr<IGstProfilerFactory> factory = GstProfilerFactory::m_factory.lock();
40 :
41 1 : if (!factory)
42 : {
43 : try
44 : {
45 1 : factory = std::make_shared<GstProfilerFactory>();
46 : }
47 0 : catch (const std::exception &e)
48 : {
49 0 : RIALTO_SERVER_LOG_ERROR("Failed to create the gst profiler factory, reason: %s", e.what());
50 : }
51 :
52 1 : GstProfilerFactory::m_factory = factory;
53 : }
54 :
55 1 : return factory;
56 : }
57 :
58 1 : std::unique_ptr<IGstProfiler> GstProfilerFactory::createGstProfiler(GstElement *pipeline,
59 : const std::shared_ptr<IGstWrapper> &gstWrapper,
60 : const std::shared_ptr<IGlibWrapper> &glibWrapper) const
61 : {
62 1 : return std::make_unique<GstProfiler>(pipeline, gstWrapper, glibWrapper);
63 : }
64 :
65 : namespace
66 : {
67 : inline constexpr std::array kKlassTokens{
68 : std::string_view{"Source"},
69 : std::string_view{"Decryptor"},
70 : std::string_view{"Decoder"},
71 : };
72 : inline constexpr std::string_view kModuleName{"GstProfiler"};
73 :
74 : using Clock = std::chrono::system_clock;
75 : using IProfiler = firebolt::rialto::common::IProfiler;
76 :
77 3 : GstPadProbeReturn probeCb(GstPad *pad, GstPadProbeInfo *info, gpointer userData)
78 : {
79 3 : firebolt::rialto::server::IGstProfilerPrivate *self =
80 : static_cast<firebolt::rialto::server::IGstProfilerPrivate *>(userData);
81 3 : return self->handleProbeCb(pad, info);
82 : }
83 :
84 0 : std::optional<int64_t> diffMs(const std::optional<Clock::time_point> &end, const std::optional<Clock::time_point> &start)
85 : {
86 0 : if (!end || !start)
87 0 : return std::nullopt;
88 :
89 0 : return std::chrono::duration_cast<std::chrono::milliseconds>(*end - *start).count();
90 : }
91 :
92 0 : std::optional<Clock::time_point> maxTime(const std::optional<Clock::time_point> &a,
93 : const std::optional<Clock::time_point> &b)
94 : {
95 0 : if (a && b)
96 0 : return std::max(*a, *b);
97 0 : if (a)
98 0 : return a;
99 0 : if (b)
100 0 : return b;
101 0 : return std::nullopt;
102 : }
103 : } // namespace
104 :
105 20 : GstProfiler::GstProfiler(GstElement *pipeline, const std::shared_ptr<firebolt::rialto::wrappers::IGstWrapper> &gstWrapper,
106 20 : const std::shared_ptr<firebolt::rialto::wrappers::IGlibWrapper> &glibWrapper)
107 20 : : m_pipeline{pipeline}, m_gstWrapper{gstWrapper}, m_glibWrapper{glibWrapper}
108 : {
109 20 : auto profilerFactory = firebolt::rialto::common::IProfilerFactory::createFactory();
110 60 : m_profiler = profilerFactory ? std::shared_ptr<IProfiler>{profilerFactory->createProfiler(std::string{kModuleName})}
111 20 : : nullptr;
112 20 : m_enabled = (m_profiler != nullptr) && m_profiler->isEnabled();
113 :
114 20 : if (m_enabled && m_pipeline)
115 3 : m_gstWrapper->gstObjectRef(m_pipeline);
116 20 : }
117 :
118 40 : GstProfiler::~GstProfiler()
119 : {
120 21 : while (!m_probeCtxs.empty())
121 : {
122 1 : auto &probeCtx = m_probeCtxs.back();
123 1 : if (probeCtx.id != 0)
124 : {
125 1 : m_gstWrapper->gstPadRemoveProbe(probeCtx.pad, probeCtx.id);
126 : }
127 1 : m_gstWrapper->gstObjectUnref(probeCtx.pad);
128 1 : m_probeCtxs.pop_back();
129 : }
130 :
131 20 : if (m_enabled && m_pipeline)
132 3 : m_gstWrapper->gstObjectUnref(m_pipeline);
133 40 : }
134 :
135 11 : std::optional<GstProfiler::RecordId> GstProfiler::createRecord(const std::string &stage)
136 : {
137 11 : if (!m_enabled || !m_profiler)
138 1 : return std::nullopt;
139 :
140 10 : auto id = m_profiler->record(stage);
141 10 : if (!id)
142 0 : return std::nullopt;
143 :
144 10 : return static_cast<GstProfiler::RecordId>(*id);
145 : }
146 :
147 2 : std::optional<GstProfiler::RecordId> GstProfiler::createRecord(const std::string &stage, const std::string &info)
148 : {
149 2 : if (!m_enabled || !m_profiler)
150 0 : return std::nullopt;
151 :
152 2 : auto id = m_profiler->record(stage, info);
153 2 : if (!id)
154 0 : return std::nullopt;
155 :
156 2 : return static_cast<GstProfiler::RecordId>(*id);
157 : }
158 :
159 10 : void GstProfiler::scheduleGstElementRecord(GstElement *element)
160 : {
161 10 : if (!m_enabled || !m_profiler)
162 6 : return;
163 :
164 9 : if (!element)
165 1 : return;
166 :
167 8 : auto stage = getFirstBufferExitStage(element);
168 8 : if (!stage)
169 0 : return;
170 :
171 8 : GstPad *pad = m_gstWrapper->gstElementGetStaticPad(element, "src");
172 8 : if (!pad)
173 3 : return;
174 :
175 5 : std::string elementInfo;
176 5 : if (isVideo(*m_gstWrapper, element))
177 : {
178 1 : elementInfo = "Video";
179 : }
180 4 : else if (isAudio(*m_gstWrapper, element))
181 : {
182 0 : elementInfo = "Audio";
183 : }
184 : else
185 : {
186 4 : gchar *rawName = m_gstWrapper->gstElementGetName(element);
187 4 : elementInfo = deriveElementInfoFromName(rawName ? rawName : "<null>");
188 4 : if (rawName)
189 4 : m_glibWrapper->gFree(rawName);
190 : }
191 :
192 5 : const auto probeId = m_gstWrapper->gstPadAddProbe(pad, GST_PAD_PROBE_TYPE_BUFFER, &probeCb,
193 : static_cast<IGstProfilerPrivate *>(this), nullptr);
194 5 : if (probeId == 0)
195 : {
196 1 : m_gstWrapper->gstObjectUnref(pad);
197 1 : return;
198 : }
199 :
200 4 : m_probeCtxs.emplace_back(ProbeCtx{m_profiler, stage.value(), std::move(elementInfo), pad, probeId});
201 9 : }
202 :
203 3 : std::vector<IGstProfiler::Record> GstProfiler::getRecords() const
204 : {
205 3 : if (!m_profiler)
206 0 : return {};
207 :
208 3 : return m_profiler->getRecords();
209 : }
210 :
211 4 : void GstProfiler::logRecord(GstProfiler::RecordId id)
212 : {
213 4 : if (!m_enabled || !m_profiler)
214 0 : return;
215 :
216 4 : m_profiler->log(static_cast<firebolt::rialto::common::IProfiler::RecordId>(id));
217 : }
218 :
219 4 : void GstProfiler::dumpToFile() const
220 : {
221 4 : if (!m_enabled || !m_profiler)
222 0 : return;
223 :
224 4 : if (!m_profiler->dumpToFile())
225 : {
226 1 : RIALTO_SERVER_LOG_WARN("Failed to dump profiler records to file");
227 : }
228 : }
229 :
230 2 : void GstProfiler::logPipelineSummary() const
231 : {
232 2 : if (!m_enabled || !m_profiler)
233 0 : return;
234 :
235 2 : const auto metrics = calculateMetrics();
236 2 : if (metrics)
237 : {
238 0 : RIALTO_SERVER_LOG_MIL("PROFILER | TUNETIME: %lld, %lld, %lld, %lld, %lld, %lld, %lld, %lld, %lld, "
239 : "%lld, %lld, %lld, %lld",
240 : metrics->preparation ? static_cast<long long>(*metrics->preparation) : -1, // NOLINT
241 : metrics->videoDownload ? static_cast<long long>(*metrics->videoDownload) : -1, // NOLINT
242 : metrics->audioDownload ? static_cast<long long>(*metrics->audioDownload) : -1, // NOLINT
243 : metrics->videoSource ? static_cast<long long>(*metrics->videoSource) : -1, // NOLINT
244 : metrics->audioSource ? static_cast<long long>(*metrics->audioSource) : -1, // NOLINT
245 : metrics->videoDecryption ? static_cast<long long>(*metrics->videoDecryption) : -1, // NOLINT
246 : metrics->audioDecryption ? static_cast<long long>(*metrics->audioDecryption) : -1, // NOLINT
247 : metrics->videoDecode ? static_cast<long long>(*metrics->videoDecode) : -1, // NOLINT
248 : metrics->audioDecode ? static_cast<long long>(*metrics->audioDecode) : -1, // NOLINT
249 : metrics->preRoll ? static_cast<long long>(*metrics->preRoll) : -1, // NOLINT
250 : metrics->play ? static_cast<long long>(*metrics->play) : -1, // NOLINT
251 : metrics->total ? static_cast<long long>(*metrics->total) : -1, // NOLINT
252 : metrics->totalWithoutApp ? static_cast<long long>(*metrics->totalWithoutApp) : -1); // NOLINT
253 : }
254 : }
255 :
256 3 : GstPadProbeReturn GstProfiler::handleProbeCb(GstPad *pad, GstPadProbeInfo *info)
257 : {
258 3 : if (!(info->type & GST_PAD_PROBE_TYPE_BUFFER))
259 0 : return GST_PAD_PROBE_OK;
260 :
261 3 : if (!GST_PAD_PROBE_INFO_BUFFER(info))
262 0 : return GST_PAD_PROBE_OK;
263 :
264 : const auto probeCtx =
265 6 : std::find_if(m_probeCtxs.begin(), m_probeCtxs.end(), [pad](const auto &ctx) { return ctx.pad == pad; });
266 3 : if (probeCtx == m_probeCtxs.end())
267 0 : return GST_PAD_PROBE_REMOVE;
268 :
269 3 : if (probeCtx->profiler)
270 : {
271 3 : const auto id = probeCtx->profiler->record(probeCtx->stage, probeCtx->info);
272 3 : if (id)
273 : {
274 3 : probeCtx->profiler->log(id.value());
275 : }
276 : }
277 :
278 3 : removeProbeCtx(pad);
279 3 : return GST_PAD_PROBE_REMOVE;
280 : }
281 :
282 8 : std::optional<std::string> GstProfiler::getFirstBufferExitStage(GstElement *element)
283 : {
284 8 : const gchar *klass = getElementClassMetadata(element);
285 8 : if (!klass)
286 0 : return std::nullopt;
287 :
288 8 : for (auto token : kKlassTokens)
289 : {
290 8 : if (m_glibWrapper->gStrrstr(klass, token.data()) != nullptr)
291 : {
292 16 : return std::string(token.data()) + " FB Exit";
293 : }
294 : }
295 :
296 0 : return std::nullopt;
297 : }
298 :
299 8 : const gchar *GstProfiler::getElementClassMetadata(GstElement *element)
300 : {
301 8 : return m_gstWrapper->gstElementClassGetMetadata(GST_ELEMENT_CLASS(G_OBJECT_GET_CLASS(element)),
302 8 : GST_ELEMENT_METADATA_KLASS);
303 : }
304 :
305 4 : std::string GstProfiler::deriveElementInfoFromName(const std::string &name) const
306 : {
307 4 : std::string lower = name;
308 54 : std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return std::tolower(c); });
309 :
310 4 : if (lower.find("vid") != std::string::npos || lower.find("video") != std::string::npos)
311 : {
312 6 : return "Video";
313 : }
314 :
315 1 : if (lower.find("aud") != std::string::npos || lower.find("audio") != std::string::npos)
316 : {
317 0 : return "Audio";
318 : }
319 :
320 1 : return name;
321 4 : }
322 :
323 3 : void GstProfiler::removeProbeCtx(GstPad *pad)
324 : {
325 : const auto probeCtx =
326 6 : std::find_if(m_probeCtxs.begin(), m_probeCtxs.end(), [pad](const auto &ctx) { return ctx.pad == pad; });
327 3 : if (probeCtx == m_probeCtxs.end())
328 0 : return;
329 :
330 3 : m_gstWrapper->gstObjectUnref(probeCtx->pad);
331 3 : m_probeCtxs.erase(probeCtx);
332 : }
333 :
334 2 : std::optional<GstProfiler::PipelineMetrics> GstProfiler::calculateMetrics() const
335 : {
336 2 : const auto records = m_profiler->getRecords();
337 :
338 2 : PipelineStageTimestamps timestamps;
339 :
340 4 : for (const auto &record : records)
341 : {
342 2 : const auto &stage = record.stage;
343 2 : const auto &info = record.info;
344 :
345 2 : if (!timestamps.pipelineCreated && stage == "Pipeline Created")
346 0 : timestamps.pipelineCreated = record.time;
347 2 : else if (!timestamps.allSourcesAttached && stage == "All Sources Attached")
348 0 : timestamps.allSourcesAttached = record.time;
349 2 : else if (!timestamps.firstSegmentReceivedVideo && stage == "First Segment Received" && info == "Video")
350 0 : timestamps.firstSegmentReceivedVideo = record.time;
351 2 : else if (!timestamps.firstSegmentReceivedAudio && stage == "First Segment Received" && info == "Audio")
352 0 : timestamps.firstSegmentReceivedAudio = record.time;
353 2 : else if (!timestamps.sourceFbExitVideo && stage == "Source FB Exit" && info == "Video")
354 0 : timestamps.sourceFbExitVideo = record.time;
355 2 : else if (!timestamps.sourceFbExitAudio && stage == "Source FB Exit" && info == "Audio")
356 0 : timestamps.sourceFbExitAudio = record.time;
357 2 : else if (!timestamps.decryptorFbExitVideo && stage == "Decryptor FB Exit" && info == "Video")
358 0 : timestamps.decryptorFbExitVideo = record.time;
359 2 : else if (!timestamps.decryptorFbExitAudio && stage == "Decryptor FB Exit" && info == "Audio")
360 0 : timestamps.decryptorFbExitAudio = record.time;
361 2 : else if (!timestamps.decoderFbExitVideo && stage == "Decoder FB Exit" && info == "Video")
362 0 : timestamps.decoderFbExitVideo = record.time;
363 2 : else if (!timestamps.decoderFbExitAudio && stage == "Decoder FB Exit" && info == "Audio")
364 0 : timestamps.decoderFbExitAudio = record.time;
365 2 : else if (!timestamps.pipelinePaused && stage == "Pipeline State Changed" && info == "PAUSED")
366 0 : timestamps.pipelinePaused = record.time;
367 2 : else if (!timestamps.pipelinePlaying && stage == "Pipeline State Changed" && info == "PLAYING")
368 0 : timestamps.pipelinePlaying = record.time;
369 : }
370 :
371 2 : if (!timestamps.pipelineCreated || !timestamps.allSourcesAttached || !timestamps.firstSegmentReceivedVideo ||
372 0 : !timestamps.firstSegmentReceivedAudio || !timestamps.sourceFbExitVideo || !timestamps.sourceFbExitAudio ||
373 2 : !timestamps.decoderFbExitVideo || !timestamps.decoderFbExitAudio || !timestamps.pipelinePaused ||
374 0 : !timestamps.pipelinePlaying)
375 : {
376 2 : return std::nullopt;
377 : }
378 :
379 0 : PipelineMetrics metrics;
380 :
381 0 : metrics.preparation = diffMs(timestamps.allSourcesAttached, timestamps.pipelineCreated);
382 0 : metrics.videoDownload = diffMs(timestamps.firstSegmentReceivedVideo, timestamps.allSourcesAttached);
383 0 : metrics.audioDownload = diffMs(timestamps.firstSegmentReceivedAudio, timestamps.allSourcesAttached);
384 0 : metrics.videoSource = diffMs(timestamps.sourceFbExitVideo, timestamps.firstSegmentReceivedVideo);
385 0 : metrics.audioSource = diffMs(timestamps.sourceFbExitAudio, timestamps.firstSegmentReceivedAudio);
386 :
387 0 : if (timestamps.decryptorFbExitVideo && timestamps.decryptorFbExitAudio)
388 : {
389 0 : metrics.videoDecryption = diffMs(timestamps.decryptorFbExitVideo, timestamps.sourceFbExitVideo);
390 0 : metrics.audioDecryption = diffMs(timestamps.decryptorFbExitAudio, timestamps.sourceFbExitAudio);
391 0 : metrics.videoDecode = diffMs(timestamps.decoderFbExitVideo, timestamps.decryptorFbExitVideo);
392 0 : metrics.audioDecode = diffMs(timestamps.decoderFbExitAudio, timestamps.decryptorFbExitAudio);
393 : }
394 : else
395 : {
396 0 : metrics.videoDecode = diffMs(timestamps.decoderFbExitVideo, timestamps.sourceFbExitVideo);
397 0 : metrics.audioDecode = diffMs(timestamps.decoderFbExitAudio, timestamps.sourceFbExitAudio);
398 : }
399 :
400 0 : const auto firstMediaReady = maxTime(timestamps.firstSegmentReceivedVideo, timestamps.firstSegmentReceivedAudio);
401 :
402 0 : metrics.preRoll = diffMs(timestamps.pipelinePaused, firstMediaReady);
403 0 : metrics.play = diffMs(timestamps.pipelinePlaying, timestamps.pipelinePaused);
404 0 : metrics.total = diffMs(timestamps.pipelinePlaying, timestamps.pipelineCreated);
405 0 : metrics.totalWithoutApp = diffMs(timestamps.pipelinePlaying, firstMediaReady);
406 :
407 0 : return metrics;
408 2 : }
409 :
410 : } // namespace firebolt::rialto::server
|