/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "WebrtcGlobalStatsHistory.h" #include #include "domstubs.h" #include "mozilla/LinkedList.h" #include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/RTCStatsReportBinding.h" // for RTCStatsReportInternal #include "mozilla/ClearOnShutdown.h" #include "mozilla/StaticPrefs_media.h" #include "mozilla/fallible.h" #include "mozilla/mozalloc_oom.h" #include "nsDOMNavigationTiming.h" namespace mozilla::dom { constexpr auto SEC_TO_MS(const DOMHighResTimeStamp sec) -> DOMHighResTimeStamp { return sec * 1000.0; } constexpr auto MIN_TO_MS(const DOMHighResTimeStamp min) -> DOMHighResTimeStamp { return SEC_TO_MS(min * 60.0); } // Prefs auto WebrtcGlobalStatsHistory::Pref::Enabled() -> bool { return mozilla::StaticPrefs::media_aboutwebrtc_hist_enabled(); } auto WebrtcGlobalStatsHistory::Pref::PollIntervalMs() -> uint32_t { return mozilla::StaticPrefs::media_aboutwebrtc_hist_poll_interval_ms(); } auto WebrtcGlobalStatsHistory::Pref::StorageWindowS() -> uint32_t { return mozilla::StaticPrefs::media_aboutwebrtc_hist_storage_window_s(); } auto WebrtcGlobalStatsHistory::Pref::PruneAfterM() -> uint32_t { return mozilla::StaticPrefs::media_aboutwebrtc_hist_prune_after_m(); } auto WebrtcGlobalStatsHistory::Pref::ClosedStatsToRetain() -> uint32_t { return mozilla::StaticPrefs::media_aboutwebrtc_hist_closed_stats_to_retain(); } auto WebrtcGlobalStatsHistory::Get() -> WebrtcGlobalStatsHistory::StatsMap& { static StaticAutoPtr sHist; if (!sHist) { sHist = new StatsMap(); ClearOnShutdown(&sHist); } return *sHist; } auto WebrtcGlobalStatsHistory::Entry::ReportElement::Timestamp() const -> DOMHighResTimeStamp { return report->mTimestamp; } auto WebrtcGlobalStatsHistory::Entry::SdpElement::Timestamp() const -> DOMHighResTimeStamp { return sdp.mTimestamp; } auto WebrtcGlobalStatsHistory::Entry::MakeReportElement( UniquePtr aReport) -> WebrtcGlobalStatsHistory::Entry::ReportElement* { auto* elem = new ReportElement(); elem->report = std::move(aReport); // We don't want to store a copy of the SDP history with each stats entry. // SDP History is stored seperately, see MakeSdpElements. elem->report->mSdpHistory.Clear(); return elem; } auto WebrtcGlobalStatsHistory::Entry::MakeSdpElementsSince( Sequence&& aSdpHistory, const Maybe& aSdpAfter) -> AutoCleanLinkedList { AutoCleanLinkedList result; for (auto& sdpHist : aSdpHistory) { if (!aSdpAfter || aSdpAfter.value() < sdpHist.mTimestamp) { auto* element = new SdpElement(); element->sdp = sdpHist; result.insertBack(element); } } return result; } template auto FindFirstEntryAfter(const T* first, const Maybe& aAfter) -> const T* { const auto* current = first; while (aAfter && current && current->Timestamp() <= aAfter.value()) { current = current->getNext(); } return current; } template auto CountElementsToEndInclusive(const LinkedListElement* elem) -> size_t { size_t count = 0; const auto* cursor = elem; while (cursor && cursor->isInList()) { count++; cursor = cursor->getNext(); } return count; } auto WebrtcGlobalStatsHistory::Entry::Since( const Maybe& aAfter) const -> nsTArray { nsTArray results; const auto* cursor = FindFirstEntryAfter(mReports.getFirst(), aAfter); const auto count = CountElementsToEndInclusive(cursor); if (!results.SetCapacity(count, fallible)) { mozalloc_handle_oom(0); } while (cursor) { results.AppendElement(RTCStatsReportInternal(*cursor->report)); cursor = cursor->getNext(); } return results; } auto WebrtcGlobalStatsHistory::Entry::SdpSince( const Maybe& aAfter) const -> RTCSdpHistoryInternal { RTCSdpHistoryInternal results; results.mPcid = mPcid; // If no timestamp was passed copy the entire history const auto* cursor = FindFirstEntryAfter(mSdp.getFirst(), aAfter); const auto count = CountElementsToEndInclusive(cursor); if (!results.mSdpHistory.SetCapacity(count, fallible)) { mozalloc_handle_oom(0); } while (cursor) { if (!results.mSdpHistory.AppendElement( RTCSdpHistoryEntryInternal(cursor->sdp), fallible)) { mozalloc_handle_oom(0); } cursor = cursor->getNext(); } return results; } auto WebrtcGlobalStatsHistory::Entry::Prune(const DOMHighResTimeStamp aBefore) -> void { // Clear everything in the case that we don't keep stats if (mIsLongTermStatsDisabled) { mReports.clear(); } // Clear everything before the cutoff for (auto* element = mReports.getFirst(); element && element->report->mTimestamp < aBefore; element = mReports.getFirst()) { delete mReports.popFirst(); } // I don't think we should prune SDPs but if we did it would look like this: // Note: we always keep the most recent SDP } auto WebrtcGlobalStatsHistory::InitHistory(const nsAString& aPcId, const bool aIsLongTermStatsDisabled) -> void { MOZ_ASSERT(XRE_IsParentProcess()); if (WebrtcGlobalStatsHistory::Get().MaybeGet(aPcId)) { return; } WebrtcGlobalStatsHistory::Get().GetOrInsertNew(aPcId, nsString(aPcId), aIsLongTermStatsDisabled); }; auto WebrtcGlobalStatsHistory::Record(UniquePtr aReport) -> void { MOZ_ASSERT(XRE_IsParentProcess()); // Use the report timestamp as "now" for determining time depth // based pruning. const auto now = aReport->mTimestamp; const auto earliest = now - SEC_TO_MS(Pref::StorageWindowS()); // Store closed state before moving the report const auto closed = aReport->mClosed; const auto pcId = aReport->mPcid; auto history = WebrtcGlobalStatsHistory::GetHistory(aReport->mPcid); if (history && Pref::Enabled()) { auto entry = history.value(); // Remove expired entries entry->Prune(earliest); // Find new SDP entries auto sdpAfter = Maybe(Nothing()); if (auto* lastSdp = entry->mSdp.getLast(); lastSdp) { sdpAfter = Some(lastSdp->Timestamp()); } entry->mSdp.extendBack( Entry::MakeSdpElementsSince(std::move(aReport->mSdpHistory), sdpAfter)); // Reports must be in ascending order by mTimestamp const auto* latest = entry->mReports.getLast(); // Maintain sorted order if (!latest || latest->report->mTimestamp < aReport->mTimestamp) { entry->mReports.insertBack(Entry::MakeReportElement(std::move(aReport))); } } // Close the history if needed if (closed) { CloseHistory(pcId); } } auto WebrtcGlobalStatsHistory::CloseHistory(const nsAString& aPcId) -> void { MOZ_ASSERT(XRE_IsParentProcess()); auto maybeHist = WebrtcGlobalStatsHistory::Get().MaybeGet(aPcId); if (!maybeHist) { return; } { auto&& hist = maybeHist.value(); hist->mIsClosed = true; if (hist->mIsLongTermStatsDisabled) { WebrtcGlobalStatsHistory::Get().Remove(aPcId); return; } } size_t remainingClosedStatsToRetain = WebrtcGlobalStatsHistory::Pref::ClosedStatsToRetain(); WebrtcGlobalStatsHistory::Get().RemoveIf([&](auto& iter) { auto& entry = iter.Data(); if (!entry->mIsClosed) { return false; } if (entry->mIsLongTermStatsDisabled) { return true; } if (remainingClosedStatsToRetain > 0) { remainingClosedStatsToRetain -= 1; return false; } return true; }); } auto WebrtcGlobalStatsHistory::Clear() -> void { MOZ_ASSERT(XRE_IsParentProcess()); WebrtcGlobalStatsHistory::Get().RemoveIf([](auto& aIter) { // First clear all the closed histories. if (aIter.Data()->mIsClosed) { return true; } // For all remaining histories clear their stored reports aIter.Data()->mReports.clear(); // As an optimization we don't clear the SDP, because that would // be reconstitued in the very next stats gathering polling period. // Those are potentially large allocations which we can skip. return false; }); } auto WebrtcGlobalStatsHistory::PcIds() -> dom::Sequence { MOZ_ASSERT(XRE_IsParentProcess()); dom::Sequence pcIds; for (const auto& pcId : WebrtcGlobalStatsHistory::Get().Keys()) { if (!pcIds.AppendElement(pcId, fallible)) { mozalloc_handle_oom(0); } } return pcIds; } auto WebrtcGlobalStatsHistory::GetHistory(const nsAString& aPcId) -> Maybe > { MOZ_ASSERT(XRE_IsParentProcess()); const auto pcid = NS_ConvertUTF16toUTF8(aPcId); return WebrtcGlobalStatsHistory::Get().MaybeGet(aPcId); } } // namespace mozilla::dom