blob: 5edb43e2a2edd1d8dd375826022523c10d8401db [file]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cast/streaming/impl/statistics_analyzer.h"
#include <cstdint>
#include <numeric>
#include "cast/streaming/public/statistics.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "platform/base/span.h"
#include "platform/test/fake_clock.h"
#include "platform/test/fake_task_runner.h"
#include "util/chrono_helpers.h"
#include "util/enum_name_table.h"
#include "util/raw_ptr.h"
using testing::_;
using testing::AtLeast;
using testing::InvokeWithoutArgs;
using testing::Mock;
using testing::NiceMock;
using testing::Return;
using testing::Sequence;
using testing::StrictMock;
namespace openscreen::cast {
extern const EnumNameTable<StatisticType,
static_cast<size_t>(StatisticType::kNumTypes)>
kStatisticTypeNames;
extern const EnumNameTable<HistogramType,
static_cast<size_t>(HistogramType::kNumTypes)>
kHistogramTypeNames;
namespace {
constexpr int kDefaultStatsAnalysisIntervalMs = 500;
constexpr int kDefaultNumEvents = 20;
constexpr int kDefaultSizeBytes = 10;
constexpr int kDefaultStatIntervalMs = 5;
constexpr FrameEvent kDefaultFrameEvent(FrameId::first(),
StatisticsEvent::Type::kFrameEncoded,
StatisticsEvent::MediaType::kVideo,
RtpTimeTicks(),
kDefaultSizeBytes,
Clock::time_point::min(),
Clock::time_point::min(),
640,
480,
milliseconds(20),
false,
0);
constexpr PacketEvent kDefaultPacketEvent(
FrameId::first(),
StatisticsEvent::Type::kPacketSentToNetwork,
StatisticsEvent::MediaType::kVideo,
RtpTimeTicks(),
kDefaultSizeBytes,
Clock::time_point::min(),
Clock::time_point::min(),
0u,
100u);
void ExpectStatEq(SenderStats::StatisticsList stats_list,
StatisticType stat,
double expected_value) {
EXPECT_DOUBLE_EQ(stats_list[static_cast<int>(stat)], expected_value)
<< GetEnumName(kStatisticTypeNames, stat).value();
}
// Checks that the first `expected_buckets.size()` entries of `recorded_buckets`
// matches the entries of `expected_buckets`. Also, checks that the total number
// of events matches for both vectors.
void ExpectHistoBuckets(const SenderStats::HistogramsList& actual_buckets_list,
HistogramType key,
const Span<const int> expected_buckets) {
const std::vector<int>& actual_buckets =
actual_buckets_list[static_cast<int>(key)].buckets;
for (size_t i = 0; i < expected_buckets.size(); i++) {
EXPECT_EQ(actual_buckets[i], expected_buckets[i])
<< GetEnumName(kHistogramTypeNames, key).value() << ", bucket=" << i;
}
const int total_recorded_events =
std::accumulate(actual_buckets.begin(), actual_buckets.end(), 0);
const int total_expected_events =
std::accumulate(expected_buckets.begin(), expected_buckets.end(), 0);
EXPECT_EQ(total_recorded_events, total_expected_events)
<< GetEnumName(kHistogramTypeNames, key).value();
}
class FakeSenderStatsClient : public SenderStatsClient {
public:
MOCK_METHOD(void, OnStatisticsUpdated, (const SenderStats&), (override));
};
class FakeClockOffsetEstimator : public ClockOffsetEstimator {
public:
MOCK_METHOD(void, OnFrameEvent, (const FrameEvent&), (override));
MOCK_METHOD(void, OnPacketEvent, (const PacketEvent&), (override));
MOCK_METHOD(std::optional<Clock::duration>,
GetEstimatedOffset,
(),
(const, override));
MOCK_METHOD(std::optional<Clock::duration>,
GetEstimatedLatency,
(),
(const, override));
};
} // namespace
class StatisticsAnalyzerTest : public ::testing::Test {
public:
StatisticsAnalyzerTest()
: fake_clock_(Clock::now()), fake_task_runner_(fake_clock_) {}
void SetUp() {
// In general, use an estimator that doesn't have an offset.
// TODO(issuetracker.google.com/298085631): add test coverage for the
// estimator usage in this class.
auto fake_estimator_unique_ptr =
std::make_unique<NiceMock<FakeClockOffsetEstimator>>();
fake_estimator_ = fake_estimator_unique_ptr.get();
ON_CALL(*fake_estimator_, GetEstimatedOffset())
.WillByDefault(Return(Clock::duration{}));
analyzer_ = std::make_unique<StatisticsAnalyzer>(
&stats_client_, fake_clock_.now, fake_task_runner_,
std::move(fake_estimator_unique_ptr));
collector_ = analyzer_->statistics_collector();
}
FrameEvent MakeFrameEvent(int frame_id, RtpTimeTicks rtp_timestamp) {
FrameEvent event = kDefaultFrameEvent;
event.frame_id = FrameId(frame_id);
event.rtp_timestamp = rtp_timestamp;
event.timestamp = fake_clock_.now();
event.received_timestamp = event.timestamp;
return event;
}
PacketEvent MakePacketEvent(int frame_and_packet_id,
RtpTimeTicks rtp_timestamp) {
OSP_CHECK_LT(frame_and_packet_id, static_cast<int>(UINT16_MAX));
PacketEvent event = kDefaultPacketEvent;
event.packet_id = static_cast<uint16_t>(frame_and_packet_id);
event.rtp_timestamp = rtp_timestamp;
event.frame_id = FrameId(frame_and_packet_id);
event.timestamp = fake_clock_.now();
event.received_timestamp = event.timestamp;
return event;
}
protected:
StrictMock<FakeSenderStatsClient> stats_client_;
FakeClock fake_clock_;
FakeTaskRunner fake_task_runner_;
std::unique_ptr<StatisticsAnalyzer> analyzer_;
raw_ptr<NiceMock<FakeClockOffsetEstimator>> fake_estimator_;
raw_ptr<StatisticsCollector> collector_ = nullptr;
};
TEST_F(StatisticsAnalyzerTest, FrameEncoded) {
analyzer_->ScheduleAnalysis();
Clock::time_point first_event_time = fake_clock_.now();
Clock::time_point last_event_time = Clock::time_point::min();
RtpTimeTicks rtp_timestamp;
for (int i = 0; i < kDefaultNumEvents; i++) {
FrameEvent event = MakeFrameEvent(i, rtp_timestamp);
collector_->CollectFrameEvent(event);
last_event_time = fake_clock_.now();
fake_clock_.Advance(milliseconds(kDefaultStatIntervalMs));
rtp_timestamp += RtpTimeDelta::FromTicks(90);
}
EXPECT_CALL(stats_client_, OnStatisticsUpdated(_))
.WillOnce([&](const SenderStats& stats) {
double expected_fps =
kDefaultNumEvents / (kDefaultStatsAnalysisIntervalMs / 1000.0);
ExpectStatEq(stats.video_statistics, StatisticType::kEnqueueFps,
expected_fps);
double expected_kbps =
kDefaultSizeBytes * 8 * kDefaultNumEvents /
static_cast<double>(kDefaultStatsAnalysisIntervalMs);
ExpectStatEq(stats.video_statistics, StatisticType::kEncodeRateKbps,
expected_kbps);
ExpectStatEq(
stats.video_statistics, StatisticType::kFirstEventTimeMs,
static_cast<double>(
to_milliseconds(first_event_time.time_since_epoch()).count()));
ExpectStatEq(
stats.video_statistics, StatisticType::kLastEventTimeMs,
static_cast<double>(
to_milliseconds(last_event_time.time_since_epoch()).count()));
});
fake_clock_.Advance(
milliseconds(kDefaultStatsAnalysisIntervalMs -
(kDefaultStatIntervalMs * kDefaultNumEvents)));
}
TEST_F(StatisticsAnalyzerTest, FrameEncodedAndAckSent) {
analyzer_->ScheduleAnalysis();
Clock::duration total_frame_latency = milliseconds(0);
RtpTimeTicks rtp_timestamp;
for (int i = 0; i < kDefaultNumEvents; i++) {
FrameEvent event1 = MakeFrameEvent(i, rtp_timestamp);
// Let random frame delay be anywhere from 20 - 39 ms
Clock::duration random_latency = milliseconds(20 + (rand() % 20));
total_frame_latency += random_latency;
FrameEvent event2 = MakeFrameEvent(i, rtp_timestamp);
event2.type = StatisticsEvent::Type::kFrameAckSent;
event2.timestamp += random_latency;
event2.received_timestamp += random_latency * 2;
collector_->CollectFrameEvent(event1);
collector_->CollectFrameEvent(event2);
fake_clock_.Advance(milliseconds(kDefaultStatIntervalMs));
rtp_timestamp += RtpTimeDelta::FromTicks(90);
}
EXPECT_CALL(stats_client_, OnStatisticsUpdated(_))
.WillOnce([&](const SenderStats& stats) {
double expected_avg_frame_latency =
static_cast<double>(to_milliseconds(total_frame_latency).count()) /
kDefaultNumEvents;
ExpectStatEq(stats.video_statistics, StatisticType::kAvgFrameLatencyMs,
expected_avg_frame_latency);
});
fake_clock_.Advance(
milliseconds(kDefaultStatsAnalysisIntervalMs -
(kDefaultStatIntervalMs * kDefaultNumEvents)));
}
TEST_F(StatisticsAnalyzerTest, FramePlayedOut) {
analyzer_->ScheduleAnalysis();
RtpTimeTicks rtp_timestamp;
int total_late_frames = 0;
for (int i = 0; i < kDefaultNumEvents; i++) {
FrameEvent event1 = MakeFrameEvent(i, rtp_timestamp);
// Let random frame delay be anywhere from 20 - 39 ms.
Clock::duration random_latency = milliseconds(20 + (rand() % 20));
// Frames will have delay_deltas of -20, 0, 20, 40, or 60 ms.
auto delay_delta = milliseconds(60 - (20 * (i % 5)));
FrameEvent event2 = MakeFrameEvent(i, rtp_timestamp);
event2.type = StatisticsEvent::Type::kFramePlayedOut;
event2.timestamp += random_latency;
event2.received_timestamp += random_latency * 2;
event2.delay_delta = delay_delta;
if (delay_delta > milliseconds(0)) {
total_late_frames++;
}
collector_->CollectFrameEvent(event1);
collector_->CollectFrameEvent(event2);
fake_clock_.Advance(milliseconds(kDefaultStatIntervalMs));
rtp_timestamp += RtpTimeDelta::FromTicks(90);
}
EXPECT_CALL(stats_client_, OnStatisticsUpdated(_))
.WillOnce([&](const SenderStats& stats) {
ExpectStatEq(stats.video_statistics, StatisticType::kNumLateFrames,
total_late_frames);
constexpr std::array<int, 6> kExpectedBuckets = {/* <0 */ 0,
/* 0-19 */ 0,
/* 20-39 */ 4,
/* 40-59 */ 4,
/* 60-79 */ 4,
/* 80-99 */ 0};
ExpectHistoBuckets(stats.video_histograms,
HistogramType::kFrameLatenessMs, kExpectedBuckets);
});
fake_clock_.Advance(
milliseconds(kDefaultStatsAnalysisIntervalMs -
(kDefaultStatIntervalMs * kDefaultNumEvents)));
}
TEST_F(StatisticsAnalyzerTest, FrameDroppedByEncoder) {
analyzer_->ScheduleAnalysis();
RtpTimeTicks rtp_timestamp;
for (int i = 0; i < kDefaultNumEvents; i++) {
FrameEvent event = MakeFrameEvent(i, rtp_timestamp);
event.type = StatisticsEvent::Type::kFrameDroppedByEncoder;
collector_->CollectFrameEvent(event);
fake_clock_.Advance(milliseconds(kDefaultStatIntervalMs));
rtp_timestamp += RtpTimeDelta::FromTicks(90);
}
EXPECT_CALL(stats_client_, OnStatisticsUpdated(_))
.WillOnce([&](const SenderStats& stats) {
ExpectStatEq(stats.video_statistics,
StatisticType::kNumFramesDroppedByEncoder,
kDefaultNumEvents);
});
fake_clock_.Advance(
milliseconds(kDefaultStatsAnalysisIntervalMs -
(kDefaultStatIntervalMs * kDefaultNumEvents)));
}
TEST_F(StatisticsAnalyzerTest, AllFrameEvents) {
constexpr std::array<StatisticsEvent::Type, 5> kEventsToReport{
StatisticsEvent::Type::kFrameCaptureBegin,
StatisticsEvent::Type::kFrameCaptureEnd,
StatisticsEvent::Type::kFrameEncoded,
StatisticsEvent::Type::kFrameAckSent,
StatisticsEvent::Type::kFramePlayedOut};
constexpr int kNumFrames = 5;
constexpr int kNumEvents = kNumFrames * kEventsToReport.size();
constexpr int kFramePlayoutDelayDeltasMs[]{10, 14, 3, 40, 1};
constexpr int kTimestampOffsetsMs[]{
// clang-format off
0, 13, 39, 278, 552, // Frame One.
0, 14, 34, 239,373, // Frame Two.
0, 19, 29, 245, 389, // Frame Three.
0, 17, 37, 261, 390, // Frame Four.
0, 14, 44, 290, 440, // Frame Five.
// clang-format on
};
analyzer_->ScheduleAnalysis();
RtpTimeTicks rtp_timestamp;
int current_event = 0;
for (int frame_id = 0; frame_id < kNumFrames; frame_id++) {
for (StatisticsEvent::Type event_type : kEventsToReport) {
FrameEvent event = MakeFrameEvent(frame_id, rtp_timestamp);
event.type = event_type;
event.timestamp += milliseconds(kTimestampOffsetsMs[current_event]);
event.delay_delta = milliseconds(kFramePlayoutDelayDeltasMs[frame_id]);
collector_->CollectFrameEvent(std::move(event));
current_event++;
}
fake_clock_.Advance(
milliseconds(kDefaultStatIntervalMs * kEventsToReport.size()));
rtp_timestamp += RtpTimeDelta::FromTicks(90);
}
constexpr std::array<std::pair<StatisticType, double>, 7> kExpectedStats{{
{StatisticType::kNumLateFrames, 5},
{StatisticType::kNumFramesCaptured, 5},
{StatisticType::kAvgEndToEndLatencyMs, 428.8},
{StatisticType::kAvgCaptureLatencyMs, 15.4},
{StatisticType::kAvgFrameLatencyMs, 226},
{StatisticType::kAvgEncodeTimeMs, 21.2},
{StatisticType::kEnqueueFps, 10},
}};
constexpr std::array<std::pair<HistogramType, std::array<int, 30>>, 4>
kExpectedHistograms{{{HistogramType::kCaptureLatencyMs, {0, 5}},
{HistogramType::kEncodeTimeMs, {0, 1, 4}},
{HistogramType::kEndToEndLatencyMs,
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 2, 0, 0, 1, 0, 0, 0, 0, 1
}},
{HistogramType::kFrameLatenessMs, {0, 4, 0, 1}}}};
EXPECT_CALL(stats_client_, OnStatisticsUpdated(_))
.WillOnce([&](const SenderStats& stats) {
for (const auto& stat_pair : kExpectedStats) {
ExpectStatEq(stats.video_statistics, stat_pair.first,
stat_pair.second);
}
for (const auto& histogram_pair : kExpectedHistograms) {
ExpectHistoBuckets(stats.video_histograms, histogram_pair.first,
histogram_pair.second);
}
});
fake_clock_.Advance(milliseconds(kDefaultStatsAnalysisIntervalMs -
(kDefaultStatIntervalMs * kNumEvents)));
}
TEST_F(StatisticsAnalyzerTest, FrameEncodedAndPacketSent) {
analyzer_->ScheduleAnalysis();
Clock::duration total_queueing_latency = milliseconds(0);
RtpTimeTicks rtp_timestamp;
for (int i = 0; i < kDefaultNumEvents; i++) {
FrameEvent event1 = MakeFrameEvent(i, rtp_timestamp);
// Let queueing latency be either 0, 20, 40, 60, or 80 ms.
const Clock::duration queueing_latency = milliseconds(80 - (20 * (i % 5)));
total_queueing_latency += queueing_latency;
PacketEvent event2 = MakePacketEvent(i, rtp_timestamp);
event2.timestamp += queueing_latency;
collector_->CollectFrameEvent(event1);
collector_->CollectPacketEvent(event2);
fake_clock_.Advance(milliseconds(kDefaultStatIntervalMs));
rtp_timestamp += RtpTimeDelta::FromTicks(90);
}
EXPECT_CALL(stats_client_, OnStatisticsUpdated(_))
.WillOnce([&](const SenderStats& stats) {
constexpr double kExpectedKbps =
kDefaultSizeBytes * 8 * kDefaultNumEvents /
static_cast<double>(kDefaultStatsAnalysisIntervalMs);
ExpectStatEq(stats.video_statistics,
StatisticType::kPacketTransmissionRateKbps, kExpectedKbps);
const double expected_avg_queueing_latency =
static_cast<double>(
to_milliseconds(total_queueing_latency).count()) /
kDefaultNumEvents;
ExpectStatEq(stats.video_statistics,
StatisticType::kAvgQueueingLatencyMs,
expected_avg_queueing_latency);
constexpr std::array<int, 7> kExpectedBuckets = {/* <0 */ 0,
/* 0-19 */ 4,
/* 20-39 */ 4,
/* 40-59 */ 4,
/* 60-79 */ 4,
/* 80-99 */ 4,
/* 100-119 */ 0};
ExpectHistoBuckets(stats.video_histograms,
HistogramType::kQueueingLatencyMs, kExpectedBuckets);
});
fake_clock_.Advance(
milliseconds(kDefaultStatsAnalysisIntervalMs -
(kDefaultStatIntervalMs * kDefaultNumEvents)));
}
TEST_F(StatisticsAnalyzerTest, PacketSentAndReceived) {
analyzer_->ScheduleAnalysis();
Clock::duration total_network_latency = milliseconds(0);
RtpTimeTicks rtp_timestamp;
for (int i = 0; i < kDefaultNumEvents; i++) {
PacketEvent event1 = MakePacketEvent(i, rtp_timestamp);
// Let network latency be either 0, 20, 40, 60, or 80 ms.
Clock::duration network_latency = milliseconds(80 - (20 * (i % 5)));
total_network_latency += network_latency;
PacketEvent event2 = MakePacketEvent(i, rtp_timestamp);
event2.frame_id = FrameId(i);
event2.timestamp += network_latency;
event2.received_timestamp += network_latency * 2;
event2.type = StatisticsEvent::Type::kPacketReceived;
collector_->CollectPacketEvent(event1);
collector_->CollectPacketEvent(event2);
fake_clock_.Advance(milliseconds(kDefaultStatIntervalMs));
rtp_timestamp += RtpTimeDelta::FromTicks(90);
}
EXPECT_CALL(stats_client_, OnStatisticsUpdated(_))
.WillOnce([&](const SenderStats& stats) {
const double expected_avg_network_latency =
static_cast<double>(
to_milliseconds(total_network_latency).count()) /
kDefaultNumEvents;
ExpectStatEq(stats.video_statistics,
StatisticType::kAvgNetworkLatencyMs,
expected_avg_network_latency);
constexpr std::array<int, 7> kExpectedBuckets = {/* <0 */ 0,
/* 0-19 */ 4,
/* 20-39 */ 4,
/* 40-59 */ 4,
/* 60-79 */ 4,
/* 80-99 */ 4,
/* 100-119 */ 0};
ExpectHistoBuckets(stats.video_histograms,
HistogramType::kNetworkLatencyMs, kExpectedBuckets);
});
fake_clock_.Advance(
milliseconds(kDefaultStatsAnalysisIntervalMs -
(kDefaultStatIntervalMs * kDefaultNumEvents)));
}
TEST_F(StatisticsAnalyzerTest, FrameEncodedPacketSentAndReceived) {
EXPECT_CALL(*fake_estimator_, GetEstimatedLatency())
.WillRepeatedly(Return(std::optional(milliseconds(40))));
analyzer_->ScheduleAnalysis();
Clock::duration total_packet_latency = milliseconds(0);
RtpTimeTicks rtp_timestamp;
Clock::time_point last_event_time = Clock::time_point::min();
for (int i = 0; i < kDefaultNumEvents; i++) {
FrameEvent event1 = MakeFrameEvent(i, rtp_timestamp);
PacketEvent event2 = MakePacketEvent(i, rtp_timestamp);
// Let packet latency be either 20, 40, 60, 80, or 100 ms.
Clock::duration packet_latency = milliseconds(100 - (20 * (i % 5)));
total_packet_latency += packet_latency;
PacketEvent event3 = MakePacketEvent(i, rtp_timestamp);
event3.timestamp += packet_latency;
event3.received_timestamp += packet_latency * 2;
event3.type = StatisticsEvent::Type::kPacketReceived;
if (event3.type == StatisticsEvent::Type::kPacketReceived &&
event3.received_timestamp > last_event_time) {
last_event_time = event3.received_timestamp;
}
collector_->CollectFrameEvent(event1);
collector_->CollectPacketEvent(event2);
collector_->CollectPacketEvent(event3);
fake_clock_.Advance(milliseconds(kDefaultStatIntervalMs));
rtp_timestamp += RtpTimeDelta::FromTicks(90);
}
EXPECT_CALL(stats_client_, OnStatisticsUpdated(_))
.WillOnce([&](const SenderStats& stats) {
ExpectStatEq(stats.video_statistics, StatisticType::kNumPacketsSent,
kDefaultNumEvents);
ExpectStatEq(stats.video_statistics, StatisticType::kNumPacketsReceived,
kDefaultNumEvents);
const double expected_time_since_last_receiver_response =
static_cast<double>(
to_milliseconds(fake_clock_.now() -
(last_event_time - milliseconds(40)))
.count());
ExpectStatEq(stats.video_statistics,
StatisticType::kTimeSinceLastReceiverResponseMs,
expected_time_since_last_receiver_response);
const double expected_avg_packet_latency =
static_cast<double>(to_milliseconds(total_packet_latency).count()) /
kDefaultNumEvents;
ExpectStatEq(stats.video_statistics, StatisticType::kAvgPacketLatencyMs,
expected_avg_packet_latency);
constexpr std::array<int, 8> kExpectedBuckets = {/* <0 */ 0,
/* 0-19 */ 0,
/* 20-39 */ 4,
/* 40-59 */ 4,
/* 60-79 */ 4,
/* 80-99 */ 4,
/* 100-119 */ 4,
/* 120-139 */ 0};
ExpectHistoBuckets(stats.video_histograms,
HistogramType::kPacketLatencyMs, kExpectedBuckets);
});
fake_clock_.Advance(
milliseconds(kDefaultStatsAnalysisIntervalMs -
(kDefaultStatIntervalMs * kDefaultNumEvents)));
}
TEST_F(StatisticsAnalyzerTest, AudioAndVideoFrameEncodedPacketSentAndReceived) {
analyzer_->ScheduleAnalysis();
const int num_events = 100;
const int frame_interval_ms = 2;
RtpTimeTicks rtp_timestamp;
Clock::duration total_audio_packet_latency = milliseconds(0);
Clock::duration total_video_packet_latency = milliseconds(0);
int total_audio_events = 0;
int total_video_events = 0;
for (int i = 0; i < num_events; i++) {
StatisticsEvent::MediaType media_type = StatisticsEvent::MediaType::kVideo;
if (i % 2 == 0) {
media_type = StatisticsEvent::MediaType::kAudio;
}
FrameEvent event1 = MakeFrameEvent(i, rtp_timestamp);
event1.media_type = media_type;
PacketEvent event2 = MakePacketEvent(i, rtp_timestamp);
event2.timestamp += milliseconds(5);
event2.media_type = media_type;
// Let packet latency be either 20, 40, 60, 80, or 100 ms.
Clock::duration packet_latency = milliseconds(100 - (20 * (i % 5)));
if (media_type == StatisticsEvent::MediaType::kAudio) {
total_audio_events++;
total_audio_packet_latency += packet_latency;
} else if (media_type == StatisticsEvent::MediaType::kVideo) {
total_video_events++;
total_video_packet_latency += packet_latency;
}
PacketEvent event3 = MakePacketEvent(i, rtp_timestamp);
event3.timestamp += packet_latency;
event3.type = StatisticsEvent::Type::kPacketReceived;
event3.media_type = media_type;
collector_->CollectFrameEvent(event1);
collector_->CollectPacketEvent(event2);
collector_->CollectPacketEvent(event3);
fake_clock_.Advance(milliseconds(frame_interval_ms));
rtp_timestamp += RtpTimeDelta::FromTicks(90);
}
EXPECT_CALL(stats_client_, OnStatisticsUpdated(_))
.WillOnce([&](const SenderStats& stats) {
ExpectStatEq(stats.audio_statistics, StatisticType::kNumPacketsSent,
total_audio_events);
ExpectStatEq(stats.audio_statistics, StatisticType::kNumPacketsReceived,
total_audio_events);
ExpectStatEq(stats.video_statistics, StatisticType::kNumPacketsSent,
total_video_events);
ExpectStatEq(stats.video_statistics, StatisticType::kNumPacketsReceived,
total_video_events);
double expected_audio_avg_packet_latency =
static_cast<double>(
to_milliseconds(total_audio_packet_latency).count()) /
total_audio_events;
ExpectStatEq(stats.audio_statistics, StatisticType::kAvgPacketLatencyMs,
expected_audio_avg_packet_latency);
double expected_video_avg_packet_latency =
static_cast<double>(
to_milliseconds(total_video_packet_latency).count()) /
total_video_events;
ExpectStatEq(stats.video_statistics, StatisticType::kAvgPacketLatencyMs,
expected_video_avg_packet_latency);
});
fake_clock_.Advance(milliseconds(kDefaultStatsAnalysisIntervalMs -
(frame_interval_ms * num_events)));
}
TEST_F(StatisticsAnalyzerTest, LotsOfEventsStillWorksProperly) {
constexpr std::array<StatisticsEvent::Type, 5> kEventsToReport{
StatisticsEvent::Type::kFrameCaptureBegin,
StatisticsEvent::Type::kFrameCaptureEnd,
StatisticsEvent::Type::kFrameEncoded,
StatisticsEvent::Type::kFrameAckSent,
StatisticsEvent::Type::kFramePlayedOut};
constexpr int kNumFrames = 1000;
constexpr int kNumEvents = kNumFrames * kEventsToReport.size();
constexpr std::array<int, 5> kFramePlayoutDelayDeltasMs{10, 14, 3, 40, 1};
constexpr std::array<int, 25> kTimestampOffsetsMs{
// clang-format off
0, 13, 39, 278, 552, // Frame One.
0, 14, 34, 239, 373, // Frame Two.
0, 19, 29, 245, 389, // Frame Three.
0, 17, 37, 261, 390, // Frame Four.
0, 14, 44, 290, 440, // Frame Five.
// clang-format on
};
constexpr std::array<std::pair<StatisticType, double>, 7> kExpectedStats{{
{StatisticType::kNumLateFrames, 1000},
{StatisticType::kNumFramesCaptured, 1000},
{StatisticType::kAvgEndToEndLatencyMs, 428.8},
{StatisticType::kAvgCaptureLatencyMs, 15.4},
{StatisticType::kAvgFrameLatencyMs, 226},
{StatisticType::kAvgEncodeTimeMs, 21.2},
{StatisticType::kEnqueueFps, 40},
}};
constexpr std::array<std::pair<HistogramType, std::array<int, 30>>, 4>
kExpectedHistograms{
{{HistogramType::kCaptureLatencyMs, {0, 1000}},
{HistogramType::kEncodeTimeMs, {0, 200, 800}},
{HistogramType::kEndToEndLatencyMs,
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 200, 400, 0, 0, 200, 0, 0, 0, 0, 200
}},
{HistogramType::kFrameLatenessMs, {0, 800, 0, 200}}}};
// We don't check stats the first 49 times, only the last.
{
testing::InSequence s;
EXPECT_CALL(stats_client_, OnStatisticsUpdated(_)).Times(49);
EXPECT_CALL(stats_client_, OnStatisticsUpdated(_))
.WillOnce([&](const SenderStats& stats) {
for (const auto& stat_pair : kExpectedStats) {
ExpectStatEq(stats.video_statistics, stat_pair.first,
stat_pair.second);
}
for (const auto& histogram_pair : kExpectedHistograms) {
ExpectHistoBuckets(stats.video_histograms, histogram_pair.first,
histogram_pair.second);
}
});
}
analyzer_->ScheduleAnalysis();
RtpTimeTicks rtp_timestamp;
int current_event = 0;
for (int frame_id = 0; frame_id < kNumFrames; frame_id++) {
for (StatisticsEvent::Type event_type : kEventsToReport) {
FrameEvent event = MakeFrameEvent(frame_id, rtp_timestamp);
event.type = event_type;
event.timestamp += milliseconds(
kTimestampOffsetsMs[current_event % kTimestampOffsetsMs.size()]);
event.delay_delta = milliseconds(
kFramePlayoutDelayDeltasMs[frame_id %
kFramePlayoutDelayDeltasMs.size()]);
collector_->CollectFrameEvent(std::move(event));
current_event++;
}
fake_clock_.Advance(
milliseconds(kDefaultStatIntervalMs * kEventsToReport.size()));
rtp_timestamp += RtpTimeDelta::FromTicks(90);
}
fake_clock_.Advance(milliseconds(kDefaultStatsAnalysisIntervalMs -
(kDefaultStatIntervalMs * kNumEvents)));
}
} // namespace openscreen::cast