/* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. An additional intellectual property rights grant can be found * in the file PATENTS. All contributing project authors may * be found in the AUTHORS file in the root of the source tree. */ #include "rtc_tools/rtc_event_log_visualizer/analyzer.h" #include #include #include #include #include #include #include #include "absl/algorithm/container.h" #include "absl/functional/bind_front.h" #include "absl/strings/string_view.h" #include "api/function_view.h" #include "api/network_state_predictor.h" #include "api/transport/field_trial_based_config.h" #include "api/transport/goog_cc_factory.h" #include "call/audio_receive_stream.h" #include "call/audio_send_stream.h" #include "call/call.h" #include "call/video_receive_stream.h" #include "call/video_send_stream.h" #include "logging/rtc_event_log/rtc_event_processor.h" #include "logging/rtc_event_log/rtc_stream_config.h" #include "modules/audio_coding/audio_network_adaptor/include/audio_network_adaptor.h" #include "modules/congestion_controller/goog_cc/acknowledged_bitrate_estimator.h" #include "modules/congestion_controller/goog_cc/bitrate_estimator.h" #include "modules/congestion_controller/goog_cc/delay_based_bwe.h" #include "modules/congestion_controller/include/receive_side_congestion_controller.h" #include "modules/congestion_controller/rtp/transport_feedback_adapter.h" #include "modules/rtp_rtcp/include/rtp_rtcp_defines.h" #include "modules/rtp_rtcp/source/rtcp_packet.h" #include "modules/rtp_rtcp/source/rtcp_packet/common_header.h" #include "modules/rtp_rtcp/source/rtcp_packet/receiver_report.h" #include "modules/rtp_rtcp/source/rtcp_packet/remb.h" #include "modules/rtp_rtcp/source/rtcp_packet/sender_report.h" #include "modules/rtp_rtcp/source/rtcp_packet/transport_feedback.h" #include "modules/rtp_rtcp/source/rtp_header_extensions.h" #include "modules/rtp_rtcp/source/rtp_rtcp_interface.h" #include "rtc_base/checks.h" #include "rtc_base/logging.h" #include "rtc_base/numerics/sequence_number_unwrapper.h" #include "rtc_base/rate_statistics.h" #include "rtc_base/strings/string_builder.h" #include "rtc_tools/rtc_event_log_visualizer/log_simulation.h" #include "test/explicit_key_value_config.h" namespace webrtc { namespace { std::string SsrcToString(uint32_t ssrc) { rtc::StringBuilder ss; ss << "SSRC " << ssrc; return ss.Release(); } // Checks whether an SSRC is contained in the list of desired SSRCs. // Note that an empty SSRC list matches every SSRC. bool MatchingSsrc(uint32_t ssrc, const std::vector& desired_ssrc) { if (desired_ssrc.empty()) return true; return std::find(desired_ssrc.begin(), desired_ssrc.end(), ssrc) != desired_ssrc.end(); } double AbsSendTimeToMicroseconds(int64_t abs_send_time) { // The timestamp is a fixed point representation with 6 bits for seconds // and 18 bits for fractions of a second. Thus, we divide by 2^18 to get the // time in seconds and then multiply by kNumMicrosecsPerSec to convert to // microseconds. static constexpr double kTimestampToMicroSec = static_cast(kNumMicrosecsPerSec) / static_cast(1ul << 18); return abs_send_time * kTimestampToMicroSec; } // Computes the difference `later` - `earlier` where `later` and `earlier` // are counters that wrap at `modulus`. The difference is chosen to have the // least absolute value. For example if `modulus` is 8, then the difference will // be chosen in the range [-3, 4]. If `modulus` is 9, then the difference will // be in [-4, 4]. int64_t WrappingDifference(uint32_t later, uint32_t earlier, int64_t modulus) { RTC_DCHECK_LE(1, modulus); RTC_DCHECK_LT(later, modulus); RTC_DCHECK_LT(earlier, modulus); int64_t difference = static_cast(later) - static_cast(earlier); int64_t max_difference = modulus / 2; int64_t min_difference = max_difference - modulus + 1; if (difference > max_difference) { difference -= modulus; } if (difference < min_difference) { difference += modulus; } if (difference > max_difference / 2 || difference < min_difference / 2) { RTC_LOG(LS_WARNING) << "Difference between" << later << " and " << earlier << " expected to be in the range (" << min_difference / 2 << "," << max_difference / 2 << ") but is " << difference << ". Correct unwrapping is uncertain."; } return difference; } // This is much more reliable for outgoing streams than for incoming streams. template absl::optional EstimateRtpClockFrequency( const RtpPacketContainer& packets, int64_t end_time_us) { RTC_CHECK(packets.size() >= 2); SeqNumUnwrapper unwrapper; int64_t first_rtp_timestamp = unwrapper.Unwrap(packets[0].rtp.header.timestamp); int64_t first_log_timestamp = packets[0].log_time_us(); int64_t last_rtp_timestamp = first_rtp_timestamp; int64_t last_log_timestamp = first_log_timestamp; for (size_t i = 1; i < packets.size(); i++) { if (packets[i].log_time_us() > end_time_us) break; last_rtp_timestamp = unwrapper.Unwrap(packets[i].rtp.header.timestamp); last_log_timestamp = packets[i].log_time_us(); } if (last_log_timestamp - first_log_timestamp < kNumMicrosecsPerSec) { RTC_LOG(LS_WARNING) << "Failed to estimate RTP clock frequency: Stream too short. (" << packets.size() << " packets, " << last_log_timestamp - first_log_timestamp << " us)"; return absl::nullopt; } double duration = static_cast(last_log_timestamp - first_log_timestamp) / kNumMicrosecsPerSec; double estimated_frequency = (last_rtp_timestamp - first_rtp_timestamp) / duration; for (uint32_t f : {8000, 16000, 32000, 48000, 90000}) { if (std::fabs(estimated_frequency - f) < 0.15 * f) { return f; } } RTC_LOG(LS_WARNING) << "Failed to estimate RTP clock frequency: Estimate " << estimated_frequency << " not close to any standard RTP frequency." << " Last timestamp " << last_rtp_timestamp << " first timestamp " << first_rtp_timestamp; return absl::nullopt; } absl::optional NetworkDelayDiff_AbsSendTime( const LoggedRtpPacketIncoming& old_packet, const LoggedRtpPacketIncoming& new_packet) { if (old_packet.rtp.header.extension.hasAbsoluteSendTime && new_packet.rtp.header.extension.hasAbsoluteSendTime) { int64_t send_time_diff = WrappingDifference( new_packet.rtp.header.extension.absoluteSendTime, old_packet.rtp.header.extension.absoluteSendTime, 1ul << 24); int64_t recv_time_diff = new_packet.log_time_us() - old_packet.log_time_us(); double delay_change_us = recv_time_diff - AbsSendTimeToMicroseconds(send_time_diff); return delay_change_us / 1000; } else { return absl::nullopt; } } absl::optional NetworkDelayDiff_CaptureTime( const LoggedRtpPacketIncoming& old_packet, const LoggedRtpPacketIncoming& new_packet, const double sample_rate) { int64_t send_time_diff = WrappingDifference(new_packet.rtp.header.timestamp, old_packet.rtp.header.timestamp, 1ull << 32); int64_t recv_time_diff = new_packet.log_time_us() - old_packet.log_time_us(); double delay_change = static_cast(recv_time_diff) / 1000 - static_cast(send_time_diff) / sample_rate * 1000; if (delay_change < -10000 || 10000 < delay_change) { RTC_LOG(LS_WARNING) << "Very large delay change. Timestamps correct?"; RTC_LOG(LS_WARNING) << "Old capture time " << old_packet.rtp.header.timestamp << ", received time " << old_packet.log_time_us(); RTC_LOG(LS_WARNING) << "New capture time " << new_packet.rtp.header.timestamp << ", received time " << new_packet.log_time_us(); RTC_LOG(LS_WARNING) << "Receive time difference " << recv_time_diff << " = " << static_cast(recv_time_diff) / kNumMicrosecsPerSec << "s"; RTC_LOG(LS_WARNING) << "Send time difference " << send_time_diff << " = " << static_cast(send_time_diff) / sample_rate << "s"; } return delay_change; } template TimeSeries CreateRtcpTypeTimeSeries(const std::vector& rtcp_list, AnalyzerConfig config, std::string rtcp_name, int category_id) { TimeSeries time_series(rtcp_name, LineStyle::kNone, PointStyle::kHighlight); for (const auto& rtcp : rtcp_list) { float x = config.GetCallTimeSec(rtcp.timestamp); float y = category_id; time_series.points.emplace_back(x, y); } return time_series; } const char kUnknownEnumValue[] = "unknown"; const char kIceCandidateTypeLocal[] = "local"; const char kIceCandidateTypeStun[] = "stun"; const char kIceCandidateTypePrflx[] = "prflx"; const char kIceCandidateTypeRelay[] = "relay"; const char kProtocolUdp[] = "udp"; const char kProtocolTcp[] = "tcp"; const char kProtocolSsltcp[] = "ssltcp"; const char kProtocolTls[] = "tls"; const char kAddressFamilyIpv4[] = "ipv4"; const char kAddressFamilyIpv6[] = "ipv6"; const char kNetworkTypeEthernet[] = "ethernet"; const char kNetworkTypeLoopback[] = "loopback"; const char kNetworkTypeWifi[] = "wifi"; const char kNetworkTypeVpn[] = "vpn"; const char kNetworkTypeCellular[] = "cellular"; std::string GetIceCandidateTypeAsString(webrtc::IceCandidateType type) { switch (type) { case webrtc::IceCandidateType::kLocal: return kIceCandidateTypeLocal; case webrtc::IceCandidateType::kStun: return kIceCandidateTypeStun; case webrtc::IceCandidateType::kPrflx: return kIceCandidateTypePrflx; case webrtc::IceCandidateType::kRelay: return kIceCandidateTypeRelay; default: return kUnknownEnumValue; } } std::string GetProtocolAsString(webrtc::IceCandidatePairProtocol protocol) { switch (protocol) { case webrtc::IceCandidatePairProtocol::kUdp: return kProtocolUdp; case webrtc::IceCandidatePairProtocol::kTcp: return kProtocolTcp; case webrtc::IceCandidatePairProtocol::kSsltcp: return kProtocolSsltcp; case webrtc::IceCandidatePairProtocol::kTls: return kProtocolTls; default: return kUnknownEnumValue; } } std::string GetAddressFamilyAsString( webrtc::IceCandidatePairAddressFamily family) { switch (family) { case webrtc::IceCandidatePairAddressFamily::kIpv4: return kAddressFamilyIpv4; case webrtc::IceCandidatePairAddressFamily::kIpv6: return kAddressFamilyIpv6; default: return kUnknownEnumValue; } } std::string GetNetworkTypeAsString(webrtc::IceCandidateNetworkType type) { switch (type) { case webrtc::IceCandidateNetworkType::kEthernet: return kNetworkTypeEthernet; case webrtc::IceCandidateNetworkType::kLoopback: return kNetworkTypeLoopback; case webrtc::IceCandidateNetworkType::kWifi: return kNetworkTypeWifi; case webrtc::IceCandidateNetworkType::kVpn: return kNetworkTypeVpn; case webrtc::IceCandidateNetworkType::kCellular: return kNetworkTypeCellular; default: return kUnknownEnumValue; } } std::string GetCandidatePairLogDescriptionAsString( const LoggedIceCandidatePairConfig& config) { // Example: stun:wifi->relay(tcp):cellular@udp:ipv4 // represents a pair of a local server-reflexive candidate on a WiFi network // and a remote relay candidate using TCP as the relay protocol on a cell // network, when the candidate pair communicates over UDP using IPv4. rtc::StringBuilder ss; std::string local_candidate_type = GetIceCandidateTypeAsString(config.local_candidate_type); std::string remote_candidate_type = GetIceCandidateTypeAsString(config.remote_candidate_type); if (config.local_candidate_type == webrtc::IceCandidateType::kRelay) { local_candidate_type += "(" + GetProtocolAsString(config.local_relay_protocol) + ")"; } ss << local_candidate_type << ":" << GetNetworkTypeAsString(config.local_network_type) << ":" << GetAddressFamilyAsString(config.local_address_family) << "->" << remote_candidate_type << ":" << GetAddressFamilyAsString(config.remote_address_family) << "@" << GetProtocolAsString(config.candidate_pair_protocol); return ss.Release(); } std::string GetDirectionAsString(PacketDirection direction) { if (direction == kIncomingPacket) { return "Incoming"; } else { return "Outgoing"; } } std::string GetDirectionAsShortString(PacketDirection direction) { if (direction == kIncomingPacket) { return "In"; } else { return "Out"; } } } // namespace EventLogAnalyzer::EventLogAnalyzer(const ParsedRtcEventLog& log, bool normalize_time) : parsed_log_(log) { config_.window_duration_ = TimeDelta::Millis(250); config_.step_ = TimeDelta::Millis(10); if (!log.start_log_events().empty()) { config_.rtc_to_utc_offset_ = log.start_log_events()[0].utc_time() - log.start_log_events()[0].log_time(); } config_.normalize_time_ = normalize_time; config_.begin_time_ = parsed_log_.first_timestamp(); config_.end_time_ = parsed_log_.last_timestamp(); if (config_.end_time_ < config_.begin_time_) { RTC_LOG(LS_WARNING) << "No useful events in the log."; config_.begin_time_ = config_.end_time_ = Timestamp::Zero(); } RTC_LOG(LS_INFO) << "Log is " << (parsed_log_.last_timestamp().ms() - parsed_log_.first_timestamp().ms()) / 1000 << " seconds long."; } EventLogAnalyzer::EventLogAnalyzer(const ParsedRtcEventLog& log, const AnalyzerConfig& config) : parsed_log_(log), config_(config) { RTC_LOG(LS_INFO) << "Log is " << (parsed_log_.last_timestamp().ms() - parsed_log_.first_timestamp().ms()) / 1000 << " seconds long."; } class BitrateObserver : public RemoteBitrateObserver { public: BitrateObserver() : last_bitrate_bps_(0), bitrate_updated_(false) {} void Update(NetworkControlUpdate update) { if (update.target_rate) { last_bitrate_bps_ = update.target_rate->target_rate.bps(); bitrate_updated_ = true; } } void OnReceiveBitrateChanged(const std::vector& ssrcs, uint32_t bitrate) override {} uint32_t last_bitrate_bps() const { return last_bitrate_bps_; } bool GetAndResetBitrateUpdated() { bool bitrate_updated = bitrate_updated_; bitrate_updated_ = false; return bitrate_updated; } private: uint32_t last_bitrate_bps_; bool bitrate_updated_; }; void EventLogAnalyzer::CreatePacketGraph(PacketDirection direction, Plot* plot) { for (const auto& stream : parsed_log_.rtp_packets_by_ssrc(direction)) { // Filter on SSRC. if (!MatchingSsrc(stream.ssrc, desired_ssrc_)) { continue; } TimeSeries time_series(GetStreamName(parsed_log_, direction, stream.ssrc), LineStyle::kBar); auto GetPacketSize = [](const LoggedRtpPacket& packet) { return absl::optional(packet.total_length); }; auto ToCallTime = [this](const LoggedRtpPacket& packet) { return this->config_.GetCallTimeSec(packet.timestamp); }; ProcessPoints(ToCallTime, GetPacketSize, stream.packet_view, &time_series); plot->AppendTimeSeries(std::move(time_series)); } plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 1, "Packet size (bytes)", kBottomMargin, kTopMargin); plot->SetTitle(GetDirectionAsString(direction) + " RTP packets"); } void EventLogAnalyzer::CreateRtcpTypeGraph(PacketDirection direction, Plot* plot) { plot->AppendTimeSeries(CreateRtcpTypeTimeSeries( parsed_log_.transport_feedbacks(direction), config_, "TWCC", 1)); plot->AppendTimeSeries(CreateRtcpTypeTimeSeries( parsed_log_.receiver_reports(direction), config_, "RR", 2)); plot->AppendTimeSeries(CreateRtcpTypeTimeSeries( parsed_log_.sender_reports(direction), config_, "SR", 3)); plot->AppendTimeSeries(CreateRtcpTypeTimeSeries( parsed_log_.extended_reports(direction), config_, "XR", 4)); plot->AppendTimeSeries(CreateRtcpTypeTimeSeries(parsed_log_.nacks(direction), config_, "NACK", 5)); plot->AppendTimeSeries(CreateRtcpTypeTimeSeries(parsed_log_.rembs(direction), config_, "REMB", 6)); plot->AppendTimeSeries( CreateRtcpTypeTimeSeries(parsed_log_.firs(direction), config_, "FIR", 7)); plot->AppendTimeSeries( CreateRtcpTypeTimeSeries(parsed_log_.plis(direction), config_, "PLI", 8)); plot->AppendTimeSeries( CreateRtcpTypeTimeSeries(parsed_log_.byes(direction), config_, "BYE", 9)); plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 1, "RTCP type", kBottomMargin, kTopMargin); plot->SetTitle(GetDirectionAsString(direction) + " RTCP packets"); plot->SetYAxisTickLabels({{1, "TWCC"}, {2, "RR"}, {3, "SR"}, {4, "XR"}, {5, "NACK"}, {6, "REMB"}, {7, "FIR"}, {8, "PLI"}, {9, "BYE"}}); } template void EventLogAnalyzer::CreateAccumulatedPacketsTimeSeries( Plot* plot, const IterableType& packets, const std::string& label) { TimeSeries time_series(label, LineStyle::kStep); for (size_t i = 0; i < packets.size(); i++) { float x = config_.GetCallTimeSec(packets[i].log_time()); time_series.points.emplace_back(x, i + 1); } plot->AppendTimeSeries(std::move(time_series)); } void EventLogAnalyzer::CreateAccumulatedPacketsGraph(PacketDirection direction, Plot* plot) { for (const auto& stream : parsed_log_.rtp_packets_by_ssrc(direction)) { if (!MatchingSsrc(stream.ssrc, desired_ssrc_)) continue; std::string label = std::string("RTP ") + GetStreamName(parsed_log_, direction, stream.ssrc); CreateAccumulatedPacketsTimeSeries(plot, stream.packet_view, label); } std::string label = std::string("RTCP ") + "(" + GetDirectionAsShortString(direction) + ")"; if (direction == kIncomingPacket) { CreateAccumulatedPacketsTimeSeries( plot, parsed_log_.incoming_rtcp_packets(), label); } else { CreateAccumulatedPacketsTimeSeries( plot, parsed_log_.outgoing_rtcp_packets(), label); } plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 1, "Received Packets", kBottomMargin, kTopMargin); plot->SetTitle(std::string("Accumulated ") + GetDirectionAsString(direction) + " RTP/RTCP packets"); } void EventLogAnalyzer::CreatePacketRateGraph(PacketDirection direction, Plot* plot) { auto CountPackets = [](auto packet) { return 1.0; }; for (const auto& stream : parsed_log_.rtp_packets_by_ssrc(direction)) { // Filter on SSRC. if (!MatchingSsrc(stream.ssrc, desired_ssrc_)) { continue; } TimeSeries time_series( std::string("RTP ") + GetStreamName(parsed_log_, direction, stream.ssrc), LineStyle::kLine); MovingAverage(CountPackets, stream.packet_view, config_, &time_series); plot->AppendTimeSeries(std::move(time_series)); } TimeSeries time_series( std::string("RTCP ") + "(" + GetDirectionAsShortString(direction) + ")", LineStyle::kLine); if (direction == kIncomingPacket) { MovingAverage( CountPackets, parsed_log_.incoming_rtcp_packets(), config_, &time_series); } else { MovingAverage( CountPackets, parsed_log_.outgoing_rtcp_packets(), config_, &time_series); } plot->AppendTimeSeries(std::move(time_series)); plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 1, "Packet Rate (packets/s)", kBottomMargin, kTopMargin); plot->SetTitle("Rate of " + GetDirectionAsString(direction) + " RTP/RTCP packets"); } void EventLogAnalyzer::CreateTotalPacketRateGraph(PacketDirection direction, Plot* plot) { // Contains a log timestamp to enable counting logged events of different // types using MovingAverage(). class LogTime { public: explicit LogTime(Timestamp log_time) : log_time_(log_time) {} Timestamp log_time() const { return log_time_; } private: Timestamp log_time_; }; std::vector packet_times; auto handle_rtp = [&](const LoggedRtpPacket& packet) { packet_times.emplace_back(packet.log_time()); }; RtcEventProcessor process; for (const auto& stream : parsed_log_.rtp_packets_by_ssrc(direction)) { process.AddEvents(stream.packet_view, handle_rtp); } if (direction == kIncomingPacket) { auto handle_incoming_rtcp = [&](const LoggedRtcpPacketIncoming& packet) { packet_times.emplace_back(packet.log_time()); }; process.AddEvents(parsed_log_.incoming_rtcp_packets(), handle_incoming_rtcp); } else { auto handle_outgoing_rtcp = [&](const LoggedRtcpPacketOutgoing& packet) { packet_times.emplace_back(packet.log_time()); }; process.AddEvents(parsed_log_.outgoing_rtcp_packets(), handle_outgoing_rtcp); } process.ProcessEventsInOrder(); TimeSeries time_series(std::string("Total ") + "(" + GetDirectionAsShortString(direction) + ") packets", LineStyle::kLine); MovingAverage([](auto packet) { return 1; }, packet_times, config_, &time_series); plot->AppendTimeSeries(std::move(time_series)); plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 1, "Packet Rate (packets/s)", kBottomMargin, kTopMargin); plot->SetTitle("Rate of all " + GetDirectionAsString(direction) + " RTP/RTCP packets"); } // For each SSRC, plot the time between the consecutive playouts. void EventLogAnalyzer::CreatePlayoutGraph(Plot* plot) { for (const auto& playout_stream : parsed_log_.audio_playout_events()) { uint32_t ssrc = playout_stream.first; if (!MatchingSsrc(ssrc, desired_ssrc_)) continue; absl::optional last_playout_ms; TimeSeries time_series(SsrcToString(ssrc), LineStyle::kBar); for (const auto& playout_event : playout_stream.second) { float x = config_.GetCallTimeSec(playout_event.log_time()); int64_t playout_time_ms = playout_event.log_time_ms(); // If there were no previous playouts, place the point on the x-axis. float y = playout_time_ms - last_playout_ms.value_or(playout_time_ms); time_series.points.push_back(TimeSeriesPoint(x, y)); last_playout_ms.emplace(playout_time_ms); } plot->AppendTimeSeries(std::move(time_series)); } plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 1, "Time since last playout (ms)", kBottomMargin, kTopMargin); plot->SetTitle("Audio playout"); } void EventLogAnalyzer::CreateNetEqSetMinimumDelay(Plot* plot) { for (const auto& playout_stream : parsed_log_.neteq_set_minimum_delay_events()) { uint32_t ssrc = playout_stream.first; if (!MatchingSsrc(ssrc, desired_ssrc_)) continue; TimeSeries time_series(SsrcToString(ssrc), LineStyle::kStep, PointStyle::kHighlight); for (const auto& event : playout_stream.second) { float x = config_.GetCallTimeSec(event.log_time()); float y = event.minimum_delay_ms; time_series.points.push_back(TimeSeriesPoint(x, y)); } plot->AppendTimeSeries(std::move(time_series)); } plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 1000, "Minimum Delay (ms)", kBottomMargin, kTopMargin); plot->SetTitle("Set Minimum Delay"); } // For audio SSRCs, plot the audio level. void EventLogAnalyzer::CreateAudioLevelGraph(PacketDirection direction, Plot* plot) { for (const auto& stream : parsed_log_.rtp_packets_by_ssrc(direction)) { if (!IsAudioSsrc(parsed_log_, direction, stream.ssrc)) continue; TimeSeries time_series(GetStreamName(parsed_log_, direction, stream.ssrc), LineStyle::kLine); for (auto& packet : stream.packet_view) { if (packet.header.extension.hasAudioLevel) { float x = config_.GetCallTimeSec(packet.log_time()); // The audio level is stored in -dBov (so e.g. -10 dBov is stored as 10) // Here we convert it to dBov. float y = static_cast(-packet.header.extension.audioLevel); time_series.points.emplace_back(TimeSeriesPoint(x, y)); } } plot->AppendTimeSeries(std::move(time_series)); } plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetYAxis(-127, 0, "Audio level (dBov)", kBottomMargin, kTopMargin); plot->SetTitle(GetDirectionAsString(direction) + " audio level"); } // For each SSRC, plot the sequence number difference between consecutive // incoming packets. void EventLogAnalyzer::CreateSequenceNumberGraph(Plot* plot) { for (const auto& stream : parsed_log_.incoming_rtp_packets_by_ssrc()) { // Filter on SSRC. if (!MatchingSsrc(stream.ssrc, desired_ssrc_)) { continue; } TimeSeries time_series( GetStreamName(parsed_log_, kIncomingPacket, stream.ssrc), LineStyle::kBar); auto GetSequenceNumberDiff = [](const LoggedRtpPacketIncoming& old_packet, const LoggedRtpPacketIncoming& new_packet) { int64_t diff = WrappingDifference(new_packet.rtp.header.sequenceNumber, old_packet.rtp.header.sequenceNumber, 1ul << 16); return diff; }; auto ToCallTime = [this](const LoggedRtpPacketIncoming& packet) { return this->config_.GetCallTimeSec(packet.log_time()); }; ProcessPairs( ToCallTime, GetSequenceNumberDiff, stream.incoming_packets, &time_series); plot->AppendTimeSeries(std::move(time_series)); } plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 1, "Difference since last packet", kBottomMargin, kTopMargin); plot->SetTitle("Incoming sequence number delta"); } void EventLogAnalyzer::CreateIncomingPacketLossGraph(Plot* plot) { for (const auto& stream : parsed_log_.incoming_rtp_packets_by_ssrc()) { const std::vector& packets = stream.incoming_packets; // Filter on SSRC. if (!MatchingSsrc(stream.ssrc, desired_ssrc_) || packets.empty()) { continue; } TimeSeries time_series( GetStreamName(parsed_log_, kIncomingPacket, stream.ssrc), LineStyle::kLine, PointStyle::kHighlight); // TODO(terelius): Should the window and step size be read from the class // instead? const TimeDelta kWindow = TimeDelta::Millis(1000); const TimeDelta kStep = TimeDelta::Millis(1000); SeqNumUnwrapper unwrapper_; SeqNumUnwrapper prior_unwrapper_; size_t window_index_begin = 0; size_t window_index_end = 0; uint64_t highest_seq_number = unwrapper_.Unwrap(packets[0].rtp.header.sequenceNumber) - 1; uint64_t highest_prior_seq_number = prior_unwrapper_.Unwrap(packets[0].rtp.header.sequenceNumber) - 1; for (Timestamp t = config_.begin_time_; t < config_.end_time_ + kStep; t += kStep) { while (window_index_end < packets.size() && packets[window_index_end].rtp.log_time() < t) { uint64_t sequence_number = unwrapper_.Unwrap( packets[window_index_end].rtp.header.sequenceNumber); highest_seq_number = std::max(highest_seq_number, sequence_number); ++window_index_end; } while (window_index_begin < packets.size() && packets[window_index_begin].rtp.log_time() < t - kWindow) { uint64_t sequence_number = prior_unwrapper_.Unwrap( packets[window_index_begin].rtp.header.sequenceNumber); highest_prior_seq_number = std::max(highest_prior_seq_number, sequence_number); ++window_index_begin; } float x = config_.GetCallTimeSec(t); uint64_t expected_packets = highest_seq_number - highest_prior_seq_number; if (expected_packets > 0) { int64_t received_packets = window_index_end - window_index_begin; int64_t lost_packets = expected_packets - received_packets; float y = static_cast(lost_packets) / expected_packets * 100; time_series.points.emplace_back(x, y); } } plot->AppendTimeSeries(std::move(time_series)); } plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 1, "Loss rate (in %)", kBottomMargin, kTopMargin); plot->SetTitle("Incoming packet loss (derived from incoming packets)"); } void EventLogAnalyzer::CreateIncomingDelayGraph(Plot* plot) { for (const auto& stream : parsed_log_.incoming_rtp_packets_by_ssrc()) { // Filter on SSRC. if (!MatchingSsrc(stream.ssrc, desired_ssrc_) || IsRtxSsrc(parsed_log_, kIncomingPacket, stream.ssrc)) { continue; } const std::vector& packets = stream.incoming_packets; if (packets.size() < 100) { RTC_LOG(LS_WARNING) << "Can't estimate the RTP clock frequency with " << packets.size() << " packets in the stream."; continue; } int64_t segment_end_us = parsed_log_.first_log_segment().stop_time_us(); absl::optional estimated_frequency = EstimateRtpClockFrequency(packets, segment_end_us); if (!estimated_frequency) continue; const double frequency_hz = *estimated_frequency; if (IsVideoSsrc(parsed_log_, kIncomingPacket, stream.ssrc) && frequency_hz != 90000) { RTC_LOG(LS_WARNING) << "Video stream should use a 90 kHz clock but appears to use " << frequency_hz / 1000 << ". Discarding."; continue; } auto ToCallTime = [this](const LoggedRtpPacketIncoming& packet) { return this->config_.GetCallTimeSec(packet.log_time()); }; auto ToNetworkDelay = [frequency_hz]( const LoggedRtpPacketIncoming& old_packet, const LoggedRtpPacketIncoming& new_packet) { return NetworkDelayDiff_CaptureTime(old_packet, new_packet, frequency_hz); }; TimeSeries capture_time_data( GetStreamName(parsed_log_, kIncomingPacket, stream.ssrc) + " capture-time", LineStyle::kLine); AccumulatePairs( ToCallTime, ToNetworkDelay, packets, &capture_time_data); plot->AppendTimeSeries(std::move(capture_time_data)); TimeSeries send_time_data( GetStreamName(parsed_log_, kIncomingPacket, stream.ssrc) + " abs-send-time", LineStyle::kLine); AccumulatePairs( ToCallTime, NetworkDelayDiff_AbsSendTime, packets, &send_time_data); plot->AppendTimeSeriesIfNotEmpty(std::move(send_time_data)); } plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 1, "Delay (ms)", kBottomMargin, kTopMargin); plot->SetTitle("Incoming network delay (relative to first packet)"); } // Plot the fraction of packets lost (as perceived by the loss-based BWE). void EventLogAnalyzer::CreateFractionLossGraph(Plot* plot) { TimeSeries time_series("Fraction lost", LineStyle::kLine, PointStyle::kHighlight); for (auto& bwe_update : parsed_log_.bwe_loss_updates()) { float x = config_.GetCallTimeSec(bwe_update.log_time()); float y = static_cast(bwe_update.fraction_lost) / 255 * 100; time_series.points.emplace_back(x, y); } plot->AppendTimeSeries(std::move(time_series)); plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 10, "Loss rate (in %)", kBottomMargin, kTopMargin); plot->SetTitle("Outgoing packet loss (as reported by BWE)"); } // Plot the total bandwidth used by all RTP streams. void EventLogAnalyzer::CreateTotalIncomingBitrateGraph(Plot* plot) { // TODO(terelius): This could be provided by the parser. std::multimap packets_in_order; for (const auto& stream : parsed_log_.incoming_rtp_packets_by_ssrc()) { for (const LoggedRtpPacketIncoming& packet : stream.incoming_packets) packets_in_order.insert( std::make_pair(packet.rtp.log_time(), packet.rtp.total_length)); } auto window_begin = packets_in_order.begin(); auto window_end = packets_in_order.begin(); size_t bytes_in_window = 0; if (!packets_in_order.empty()) { // Calculate a moving average of the bitrate and store in a TimeSeries. TimeSeries bitrate_series("Bitrate", LineStyle::kLine); for (Timestamp time = config_.begin_time_; time < config_.end_time_ + config_.step_; time += config_.step_) { while (window_end != packets_in_order.end() && window_end->first < time) { bytes_in_window += window_end->second; ++window_end; } while (window_begin != packets_in_order.end() && window_begin->first < time - config_.window_duration_) { RTC_DCHECK_LE(window_begin->second, bytes_in_window); bytes_in_window -= window_begin->second; ++window_begin; } float window_duration_in_seconds = static_cast(config_.window_duration_.us()) / kNumMicrosecsPerSec; float x = config_.GetCallTimeSec(time); float y = bytes_in_window * 8 / window_duration_in_seconds / 1000; bitrate_series.points.emplace_back(x, y); } plot->AppendTimeSeries(std::move(bitrate_series)); } // Overlay the outgoing REMB over incoming bitrate. TimeSeries remb_series("Remb", LineStyle::kStep); for (const auto& rtcp : parsed_log_.rembs(kOutgoingPacket)) { float x = config_.GetCallTimeSec(rtcp.log_time()); float y = static_cast(rtcp.remb.bitrate_bps()) / 1000; remb_series.points.emplace_back(x, y); } plot->AppendTimeSeriesIfNotEmpty(std::move(remb_series)); if (!parsed_log_.generic_packets_received().empty()) { TimeSeries time_series("Incoming generic bitrate", LineStyle::kLine); auto GetPacketSizeKilobits = [](const LoggedGenericPacketReceived& packet) { return packet.packet_length * 8.0 / 1000.0; }; MovingAverage( GetPacketSizeKilobits, parsed_log_.generic_packets_received(), config_, &time_series); plot->AppendTimeSeries(std::move(time_series)); } plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 1, "Bitrate (kbps)", kBottomMargin, kTopMargin); plot->SetTitle("Incoming RTP bitrate"); } // Plot the total bandwidth used by all RTP streams. void EventLogAnalyzer::CreateTotalOutgoingBitrateGraph( Plot* plot, bool show_detector_state, bool show_alr_state, bool show_link_capacity) { // TODO(terelius): This could be provided by the parser. std::multimap packets_in_order; for (const auto& stream : parsed_log_.outgoing_rtp_packets_by_ssrc()) { for (const LoggedRtpPacketOutgoing& packet : stream.outgoing_packets) packets_in_order.insert( std::make_pair(packet.rtp.log_time(), packet.rtp.total_length)); } auto window_begin = packets_in_order.begin(); auto window_end = packets_in_order.begin(); size_t bytes_in_window = 0; if (!packets_in_order.empty()) { // Calculate a moving average of the bitrate and store in a TimeSeries. TimeSeries bitrate_series("Bitrate", LineStyle::kLine); for (Timestamp time = config_.begin_time_; time < config_.end_time_ + config_.step_; time += config_.step_) { while (window_end != packets_in_order.end() && window_end->first < time) { bytes_in_window += window_end->second; ++window_end; } while (window_begin != packets_in_order.end() && window_begin->first < time - config_.window_duration_) { RTC_DCHECK_LE(window_begin->second, bytes_in_window); bytes_in_window -= window_begin->second; ++window_begin; } float window_duration_in_seconds = static_cast(config_.window_duration_.us()) / kNumMicrosecsPerSec; float x = config_.GetCallTimeSec(time); float y = bytes_in_window * 8 / window_duration_in_seconds / 1000; bitrate_series.points.emplace_back(x, y); } plot->AppendTimeSeries(std::move(bitrate_series)); } // Overlay the send-side bandwidth estimate over the outgoing bitrate. TimeSeries loss_series("Loss-based estimate", LineStyle::kStep); for (auto& loss_update : parsed_log_.bwe_loss_updates()) { float x = config_.GetCallTimeSec(loss_update.log_time()); float y = static_cast(loss_update.bitrate_bps) / 1000; loss_series.points.emplace_back(x, y); } TimeSeries link_capacity_lower_series("Link-capacity-lower", LineStyle::kStep); TimeSeries link_capacity_upper_series("Link-capacity-upper", LineStyle::kStep); for (auto& remote_estimate_event : parsed_log_.remote_estimate_events()) { float x = config_.GetCallTimeSec(remote_estimate_event.log_time()); if (remote_estimate_event.link_capacity_lower.has_value()) { float link_capacity_lower = static_cast( remote_estimate_event.link_capacity_lower.value().kbps()); link_capacity_lower_series.points.emplace_back(x, link_capacity_lower); } if (remote_estimate_event.link_capacity_upper.has_value()) { float link_capacity_upper = static_cast( remote_estimate_event.link_capacity_upper.value().kbps()); link_capacity_upper_series.points.emplace_back(x, link_capacity_upper); } } TimeSeries delay_series("Delay-based estimate", LineStyle::kStep); IntervalSeries overusing_series("Overusing", "#ff8e82", IntervalSeries::kHorizontal); IntervalSeries underusing_series("Underusing", "#5092fc", IntervalSeries::kHorizontal); IntervalSeries normal_series("Normal", "#c4ffc4", IntervalSeries::kHorizontal); IntervalSeries* last_series = &normal_series; float last_detector_switch = 0.0; BandwidthUsage last_detector_state = BandwidthUsage::kBwNormal; for (auto& delay_update : parsed_log_.bwe_delay_updates()) { float x = config_.GetCallTimeSec(delay_update.log_time()); float y = static_cast(delay_update.bitrate_bps) / 1000; if (last_detector_state != delay_update.detector_state) { last_series->intervals.emplace_back(last_detector_switch, x); last_detector_state = delay_update.detector_state; last_detector_switch = x; switch (delay_update.detector_state) { case BandwidthUsage::kBwNormal: last_series = &normal_series; break; case BandwidthUsage::kBwUnderusing: last_series = &underusing_series; break; case BandwidthUsage::kBwOverusing: last_series = &overusing_series; break; case BandwidthUsage::kLast: RTC_DCHECK_NOTREACHED(); } } delay_series.points.emplace_back(x, y); } RTC_CHECK(last_series); last_series->intervals.emplace_back(last_detector_switch, config_.CallEndTimeSec()); TimeSeries created_series("Probe cluster created.", LineStyle::kNone, PointStyle::kHighlight); for (auto& cluster : parsed_log_.bwe_probe_cluster_created_events()) { float x = config_.GetCallTimeSec(cluster.log_time()); float y = static_cast(cluster.bitrate_bps) / 1000; created_series.points.emplace_back(x, y); } TimeSeries result_series("Probing results.", LineStyle::kNone, PointStyle::kHighlight); for (auto& result : parsed_log_.bwe_probe_success_events()) { float x = config_.GetCallTimeSec(result.log_time()); float y = static_cast(result.bitrate_bps) / 1000; result_series.points.emplace_back(x, y); } TimeSeries probe_failures_series("Probe failed", LineStyle::kNone, PointStyle::kHighlight); for (auto& failure : parsed_log_.bwe_probe_failure_events()) { float x = config_.GetCallTimeSec(failure.log_time()); probe_failures_series.points.emplace_back(x, 0); } IntervalSeries alr_state("ALR", "#555555", IntervalSeries::kHorizontal); bool previously_in_alr = false; Timestamp alr_start = Timestamp::Zero(); for (auto& alr : parsed_log_.alr_state_events()) { float y = config_.GetCallTimeSec(alr.log_time()); if (!previously_in_alr && alr.in_alr) { alr_start = alr.log_time(); previously_in_alr = true; } else if (previously_in_alr && !alr.in_alr) { float x = config_.GetCallTimeSec(alr_start); alr_state.intervals.emplace_back(x, y); previously_in_alr = false; } } if (previously_in_alr) { float x = config_.GetCallTimeSec(alr_start); float y = config_.GetCallTimeSec(config_.end_time_); alr_state.intervals.emplace_back(x, y); } if (show_detector_state) { plot->AppendIntervalSeries(std::move(overusing_series)); plot->AppendIntervalSeries(std::move(underusing_series)); plot->AppendIntervalSeries(std::move(normal_series)); } if (show_alr_state) { plot->AppendIntervalSeries(std::move(alr_state)); } if (show_link_capacity) { plot->AppendTimeSeriesIfNotEmpty(std::move(link_capacity_lower_series)); plot->AppendTimeSeriesIfNotEmpty(std::move(link_capacity_upper_series)); } plot->AppendTimeSeries(std::move(loss_series)); plot->AppendTimeSeriesIfNotEmpty(std::move(probe_failures_series)); plot->AppendTimeSeries(std::move(delay_series)); plot->AppendTimeSeries(std::move(created_series)); plot->AppendTimeSeries(std::move(result_series)); // Overlay the incoming REMB over the outgoing bitrate. TimeSeries remb_series("Remb", LineStyle::kStep); for (const auto& rtcp : parsed_log_.rembs(kIncomingPacket)) { float x = config_.GetCallTimeSec(rtcp.log_time()); float y = static_cast(rtcp.remb.bitrate_bps()) / 1000; remb_series.points.emplace_back(x, y); } plot->AppendTimeSeriesIfNotEmpty(std::move(remb_series)); if (!parsed_log_.generic_packets_sent().empty()) { { TimeSeries time_series("Outgoing generic total bitrate", LineStyle::kLine); auto GetPacketSizeKilobits = [](const LoggedGenericPacketSent& packet) { return packet.packet_length() * 8.0 / 1000.0; }; MovingAverage( GetPacketSizeKilobits, parsed_log_.generic_packets_sent(), config_, &time_series); plot->AppendTimeSeries(std::move(time_series)); } { TimeSeries time_series("Outgoing generic payload bitrate", LineStyle::kLine); auto GetPacketSizeKilobits = [](const LoggedGenericPacketSent& packet) { return packet.payload_length * 8.0 / 1000.0; }; MovingAverage( GetPacketSizeKilobits, parsed_log_.generic_packets_sent(), config_, &time_series); plot->AppendTimeSeries(std::move(time_series)); } } plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 1, "Bitrate (kbps)", kBottomMargin, kTopMargin); plot->SetTitle("Outgoing RTP bitrate"); } // For each SSRC, plot the bandwidth used by that stream. void EventLogAnalyzer::CreateStreamBitrateGraph(PacketDirection direction, Plot* plot) { for (const auto& stream : parsed_log_.rtp_packets_by_ssrc(direction)) { // Filter on SSRC. if (!MatchingSsrc(stream.ssrc, desired_ssrc_)) { continue; } TimeSeries time_series(GetStreamName(parsed_log_, direction, stream.ssrc), LineStyle::kLine); auto GetPacketSizeKilobits = [](const LoggedRtpPacket& packet) { return packet.total_length * 8.0 / 1000.0; }; MovingAverage( GetPacketSizeKilobits, stream.packet_view, config_, &time_series); plot->AppendTimeSeries(std::move(time_series)); } plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 1, "Bitrate (kbps)", kBottomMargin, kTopMargin); plot->SetTitle(GetDirectionAsString(direction) + " bitrate per stream"); } // Plot the bitrate allocation for each temporal and spatial layer. // Computed from RTCP XR target bitrate block, so the graph is only populated if // those are sent. void EventLogAnalyzer::CreateBitrateAllocationGraph(PacketDirection direction, Plot* plot) { std::map time_series; const auto& xr_list = parsed_log_.extended_reports(direction); for (const auto& rtcp : xr_list) { const absl::optional& target_bitrate = rtcp.xr.target_bitrate(); if (!target_bitrate.has_value()) continue; for (const auto& bitrate_item : target_bitrate->GetTargetBitrates()) { LayerDescription layer(rtcp.xr.sender_ssrc(), bitrate_item.spatial_layer, bitrate_item.temporal_layer); auto time_series_it = time_series.find(layer); if (time_series_it == time_series.end()) { std::string layer_name = GetLayerName(layer); bool inserted; std::tie(time_series_it, inserted) = time_series.insert( std::make_pair(layer, TimeSeries(layer_name, LineStyle::kStep))); RTC_DCHECK(inserted); } float x = config_.GetCallTimeSec(rtcp.log_time()); float y = bitrate_item.target_bitrate_kbps; time_series_it->second.points.emplace_back(x, y); } } for (auto& layer : time_series) { plot->AppendTimeSeries(std::move(layer.second)); } plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 1, "Bitrate (kbps)", kBottomMargin, kTopMargin); if (direction == kIncomingPacket) plot->SetTitle("Target bitrate per incoming layer"); else plot->SetTitle("Target bitrate per outgoing layer"); } void EventLogAnalyzer::CreateGoogCcSimulationGraph(Plot* plot) { TimeSeries target_rates("Simulated target rate", LineStyle::kStep, PointStyle::kHighlight); TimeSeries delay_based("Logged delay-based estimate", LineStyle::kStep, PointStyle::kHighlight); TimeSeries loss_based("Logged loss-based estimate", LineStyle::kStep, PointStyle::kHighlight); TimeSeries probe_results("Logged probe success", LineStyle::kNone, PointStyle::kHighlight); LogBasedNetworkControllerSimulation simulation( std::make_unique(), [&](const NetworkControlUpdate& update, Timestamp at_time) { if (update.target_rate) { target_rates.points.emplace_back( config_.GetCallTimeSec(at_time), update.target_rate->target_rate.kbps()); } }); simulation.ProcessEventsInLog(parsed_log_); for (const auto& logged : parsed_log_.bwe_delay_updates()) delay_based.points.emplace_back(config_.GetCallTimeSec(logged.log_time()), logged.bitrate_bps / 1000); for (const auto& logged : parsed_log_.bwe_probe_success_events()) probe_results.points.emplace_back(config_.GetCallTimeSec(logged.log_time()), logged.bitrate_bps / 1000); for (const auto& logged : parsed_log_.bwe_loss_updates()) loss_based.points.emplace_back(config_.GetCallTimeSec(logged.log_time()), logged.bitrate_bps / 1000); plot->AppendTimeSeries(std::move(delay_based)); plot->AppendTimeSeries(std::move(loss_based)); plot->AppendTimeSeries(std::move(probe_results)); plot->AppendTimeSeries(std::move(target_rates)); plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 10, "Bitrate (kbps)", kBottomMargin, kTopMargin); plot->SetTitle("Simulated BWE behavior"); } void EventLogAnalyzer::CreateSendSideBweSimulationGraph(Plot* plot) { using RtpPacketType = LoggedRtpPacketOutgoing; using TransportFeedbackType = LoggedRtcpPacketTransportFeedback; // TODO(terelius): This could be provided by the parser. std::multimap outgoing_rtp; for (const auto& stream : parsed_log_.outgoing_rtp_packets_by_ssrc()) { for (const RtpPacketType& rtp_packet : stream.outgoing_packets) outgoing_rtp.insert( std::make_pair(rtp_packet.rtp.log_time_us(), &rtp_packet)); } const std::vector& incoming_rtcp = parsed_log_.transport_feedbacks(kIncomingPacket); SimulatedClock clock(0); BitrateObserver observer; RtcEventLogNull null_event_log; TransportFeedbackAdapter transport_feedback; auto factory = GoogCcNetworkControllerFactory(); TimeDelta process_interval = factory.GetProcessInterval(); // TODO(holmer): Log the call config and use that here instead. static const uint32_t kDefaultStartBitrateBps = 300000; NetworkControllerConfig cc_config; cc_config.constraints.at_time = Timestamp::Micros(clock.TimeInMicroseconds()); cc_config.constraints.starting_rate = DataRate::BitsPerSec(kDefaultStartBitrateBps); cc_config.event_log = &null_event_log; auto goog_cc = factory.Create(cc_config); TimeSeries time_series("Delay-based estimate", LineStyle::kStep, PointStyle::kHighlight); TimeSeries acked_time_series("Raw acked bitrate", LineStyle::kLine, PointStyle::kHighlight); TimeSeries robust_time_series("Robust throughput estimate", LineStyle::kLine, PointStyle::kHighlight); TimeSeries acked_estimate_time_series("Ackednowledged bitrate estimate", LineStyle::kLine, PointStyle::kHighlight); auto rtp_iterator = outgoing_rtp.begin(); auto rtcp_iterator = incoming_rtcp.begin(); auto NextRtpTime = [&]() { if (rtp_iterator != outgoing_rtp.end()) return static_cast(rtp_iterator->first); return std::numeric_limits::max(); }; auto NextRtcpTime = [&]() { if (rtcp_iterator != incoming_rtcp.end()) return static_cast(rtcp_iterator->log_time_us()); return std::numeric_limits::max(); }; int64_t next_process_time_us_ = std::min({NextRtpTime(), NextRtcpTime()}); auto NextProcessTime = [&]() { if (rtcp_iterator != incoming_rtcp.end() || rtp_iterator != outgoing_rtp.end()) { return next_process_time_us_; } return std::numeric_limits::max(); }; RateStatistics raw_acked_bitrate(750, 8000); test::ExplicitKeyValueConfig throughput_config( "WebRTC-Bwe-RobustThroughputEstimatorSettings/" "enabled:true,required_packets:10," "window_packets:25,window_duration:1000ms,unacked_weight:1.0/"); std::unique_ptr robust_throughput_estimator( AcknowledgedBitrateEstimatorInterface::Create(&throughput_config)); FieldTrialBasedConfig field_trial_config; std::unique_ptr acknowledged_bitrate_estimator( AcknowledgedBitrateEstimatorInterface::Create(&field_trial_config)); int64_t time_us = std::min({NextRtpTime(), NextRtcpTime(), NextProcessTime()}); int64_t last_update_us = 0; while (time_us != std::numeric_limits::max()) { clock.AdvanceTimeMicroseconds(time_us - clock.TimeInMicroseconds()); if (clock.TimeInMicroseconds() >= NextRtpTime()) { RTC_DCHECK_EQ(clock.TimeInMicroseconds(), NextRtpTime()); const RtpPacketType& rtp_packet = *rtp_iterator->second; if (rtp_packet.rtp.header.extension.hasTransportSequenceNumber) { RtpPacketSendInfo packet_info; packet_info.media_ssrc = rtp_packet.rtp.header.ssrc; packet_info.transport_sequence_number = rtp_packet.rtp.header.extension.transportSequenceNumber; packet_info.rtp_sequence_number = rtp_packet.rtp.header.sequenceNumber; packet_info.length = rtp_packet.rtp.total_length; if (IsRtxSsrc(parsed_log_, PacketDirection::kOutgoingPacket, rtp_packet.rtp.header.ssrc)) { // Don't set the optional media type as we don't know if it is // a retransmission, FEC or padding. } else if (IsVideoSsrc(parsed_log_, PacketDirection::kOutgoingPacket, rtp_packet.rtp.header.ssrc)) { packet_info.packet_type = RtpPacketMediaType::kVideo; } else if (IsAudioSsrc(parsed_log_, PacketDirection::kOutgoingPacket, rtp_packet.rtp.header.ssrc)) { packet_info.packet_type = RtpPacketMediaType::kAudio; } transport_feedback.AddPacket( packet_info, 0u, // Per packet overhead bytes. Timestamp::Micros(rtp_packet.rtp.log_time_us())); } rtc::SentPacket sent_packet; sent_packet.send_time_ms = rtp_packet.rtp.log_time_ms(); sent_packet.info.included_in_allocation = true; sent_packet.info.packet_size_bytes = rtp_packet.rtp.total_length; if (rtp_packet.rtp.header.extension.hasTransportSequenceNumber) { sent_packet.packet_id = rtp_packet.rtp.header.extension.transportSequenceNumber; sent_packet.info.included_in_feedback = true; } auto sent_msg = transport_feedback.ProcessSentPacket(sent_packet); if (sent_msg) observer.Update(goog_cc->OnSentPacket(*sent_msg)); ++rtp_iterator; } if (clock.TimeInMicroseconds() >= NextRtcpTime()) { RTC_DCHECK_EQ(clock.TimeInMicroseconds(), NextRtcpTime()); auto feedback_msg = transport_feedback.ProcessTransportFeedback( rtcp_iterator->transport_feedback, Timestamp::Millis(clock.TimeInMilliseconds())); if (feedback_msg) { observer.Update(goog_cc->OnTransportPacketsFeedback(*feedback_msg)); std::vector feedback = feedback_msg->SortedByReceiveTime(); if (!feedback.empty()) { acknowledged_bitrate_estimator->IncomingPacketFeedbackVector( feedback); robust_throughput_estimator->IncomingPacketFeedbackVector(feedback); for (const PacketResult& packet : feedback) { raw_acked_bitrate.Update(packet.sent_packet.size.bytes(), packet.receive_time.ms()); } absl::optional raw_bitrate_bps = raw_acked_bitrate.Rate(feedback.back().receive_time.ms()); float x = config_.GetCallTimeSec(clock.CurrentTime()); if (raw_bitrate_bps) { float y = raw_bitrate_bps.value() / 1000; acked_time_series.points.emplace_back(x, y); } absl::optional robust_estimate = robust_throughput_estimator->bitrate(); if (robust_estimate) { float y = robust_estimate.value().kbps(); robust_time_series.points.emplace_back(x, y); } absl::optional acked_estimate = acknowledged_bitrate_estimator->bitrate(); if (acked_estimate) { float y = acked_estimate.value().kbps(); acked_estimate_time_series.points.emplace_back(x, y); } } } ++rtcp_iterator; } if (clock.TimeInMicroseconds() >= NextProcessTime()) { RTC_DCHECK_EQ(clock.TimeInMicroseconds(), NextProcessTime()); ProcessInterval msg; msg.at_time = Timestamp::Micros(clock.TimeInMicroseconds()); observer.Update(goog_cc->OnProcessInterval(msg)); next_process_time_us_ += process_interval.us(); } if (observer.GetAndResetBitrateUpdated() || time_us - last_update_us >= 1e6) { uint32_t y = observer.last_bitrate_bps() / 1000; float x = config_.GetCallTimeSec(clock.CurrentTime()); time_series.points.emplace_back(x, y); last_update_us = time_us; } time_us = std::min({NextRtpTime(), NextRtcpTime(), NextProcessTime()}); } // Add the data set to the plot. plot->AppendTimeSeries(std::move(time_series)); plot->AppendTimeSeries(std::move(robust_time_series)); plot->AppendTimeSeries(std::move(acked_time_series)); plot->AppendTimeSeriesIfNotEmpty(std::move(acked_estimate_time_series)); plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 10, "Bitrate (kbps)", kBottomMargin, kTopMargin); plot->SetTitle("Simulated send-side BWE behavior"); } void EventLogAnalyzer::CreateReceiveSideBweSimulationGraph(Plot* plot) { using RtpPacketType = LoggedRtpPacketIncoming; class RembInterceptor { public: void SendRemb(uint32_t bitrate_bps, std::vector ssrcs) { last_bitrate_bps_ = bitrate_bps; bitrate_updated_ = true; } uint32_t last_bitrate_bps() const { return last_bitrate_bps_; } bool GetAndResetBitrateUpdated() { bool bitrate_updated = bitrate_updated_; bitrate_updated_ = false; return bitrate_updated; } private: // We don't know the start bitrate, but assume that it is the default 300 // kbps. uint32_t last_bitrate_bps_ = 300000; bool bitrate_updated_ = false; }; std::multimap incoming_rtp; for (const auto& stream : parsed_log_.incoming_rtp_packets_by_ssrc()) { if (IsVideoSsrc(parsed_log_, kIncomingPacket, stream.ssrc)) { for (const auto& rtp_packet : stream.incoming_packets) incoming_rtp.insert( std::make_pair(rtp_packet.rtp.log_time_us(), &rtp_packet)); } } SimulatedClock clock(0); RembInterceptor remb_interceptor; ReceiveSideCongestionController rscc( &clock, [](auto...) {}, absl::bind_front(&RembInterceptor::SendRemb, &remb_interceptor), nullptr); // TODO(holmer): Log the call config and use that here instead. // static const uint32_t kDefaultStartBitrateBps = 300000; // rscc.SetBweBitrates(0, kDefaultStartBitrateBps, -1); TimeSeries time_series("Receive side estimate", LineStyle::kLine, PointStyle::kHighlight); TimeSeries acked_time_series("Received bitrate", LineStyle::kLine); RateStatistics acked_bitrate(250, 8000); int64_t last_update_us = 0; for (const auto& kv : incoming_rtp) { const RtpPacketType& packet = *kv.second; int64_t arrival_time_ms = packet.rtp.log_time_us() / 1000; size_t payload = packet.rtp.total_length; /*Should subtract header?*/ clock.AdvanceTimeMicroseconds(packet.rtp.log_time_us() - clock.TimeInMicroseconds()); rscc.OnReceivedPacket(arrival_time_ms, payload, packet.rtp.header); acked_bitrate.Update(payload, arrival_time_ms); absl::optional bitrate_bps = acked_bitrate.Rate(arrival_time_ms); if (bitrate_bps) { uint32_t y = *bitrate_bps / 1000; float x = config_.GetCallTimeSec(clock.CurrentTime()); acked_time_series.points.emplace_back(x, y); } if (remb_interceptor.GetAndResetBitrateUpdated() || clock.TimeInMicroseconds() - last_update_us >= 1e6) { uint32_t y = remb_interceptor.last_bitrate_bps() / 1000; float x = config_.GetCallTimeSec(clock.CurrentTime()); time_series.points.emplace_back(x, y); last_update_us = clock.TimeInMicroseconds(); } } // Add the data set to the plot. plot->AppendTimeSeries(std::move(time_series)); plot->AppendTimeSeries(std::move(acked_time_series)); plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 10, "Bitrate (kbps)", kBottomMargin, kTopMargin); plot->SetTitle("Simulated receive-side BWE behavior"); } void EventLogAnalyzer::CreateNetworkDelayFeedbackGraph(Plot* plot) { TimeSeries time_series("Network delay", LineStyle::kLine, PointStyle::kHighlight); int64_t min_send_receive_diff_ms = std::numeric_limits::max(); int64_t min_rtt_ms = std::numeric_limits::max(); std::vector matched_rtp_rtcp = GetNetworkTrace(parsed_log_); absl::c_stable_sort(matched_rtp_rtcp, [](const MatchedSendArrivalTimes& a, const MatchedSendArrivalTimes& b) { return a.feedback_arrival_time_ms < b.feedback_arrival_time_ms || (a.feedback_arrival_time_ms == b.feedback_arrival_time_ms && a.arrival_time_ms < b.arrival_time_ms); }); for (const auto& packet : matched_rtp_rtcp) { if (packet.arrival_time_ms == MatchedSendArrivalTimes::kNotReceived) continue; float x = config_.GetCallTimeSecFromMs(packet.feedback_arrival_time_ms); int64_t y = packet.arrival_time_ms - packet.send_time_ms; int64_t rtt_ms = packet.feedback_arrival_time_ms - packet.send_time_ms; min_rtt_ms = std::min(rtt_ms, min_rtt_ms); min_send_receive_diff_ms = std::min(y, min_send_receive_diff_ms); time_series.points.emplace_back(x, y); } // We assume that the base network delay (w/o queues) is equal to half // the minimum RTT. Therefore rescale the delays by subtracting the minimum // observed 1-ways delay and add half the minimum RTT. const int64_t estimated_clock_offset_ms = min_send_receive_diff_ms - min_rtt_ms / 2; for (TimeSeriesPoint& point : time_series.points) point.y -= estimated_clock_offset_ms; // Add the data set to the plot. plot->AppendTimeSeriesIfNotEmpty(std::move(time_series)); plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 10, "Delay (ms)", kBottomMargin, kTopMargin); plot->SetTitle("Outgoing network delay (based on per-packet feedback)"); } void EventLogAnalyzer::CreatePacerDelayGraph(Plot* plot) { for (const auto& stream : parsed_log_.outgoing_rtp_packets_by_ssrc()) { const std::vector& packets = stream.outgoing_packets; if (IsRtxSsrc(parsed_log_, kOutgoingPacket, stream.ssrc)) { continue; } if (packets.size() < 2) { RTC_LOG(LS_WARNING) << "Can't estimate a the RTP clock frequency or the " "pacer delay with less than 2 packets in the stream"; continue; } int64_t segment_end_us = parsed_log_.first_log_segment().stop_time_us(); absl::optional estimated_frequency = EstimateRtpClockFrequency(packets, segment_end_us); if (!estimated_frequency) continue; if (IsVideoSsrc(parsed_log_, kOutgoingPacket, stream.ssrc) && *estimated_frequency != 90000) { RTC_LOG(LS_WARNING) << "Video stream should use a 90 kHz clock but appears to use " << *estimated_frequency / 1000 << ". Discarding."; continue; } TimeSeries pacer_delay_series( GetStreamName(parsed_log_, kOutgoingPacket, stream.ssrc) + "(" + std::to_string(*estimated_frequency / 1000) + " kHz)", LineStyle::kLine, PointStyle::kHighlight); SeqNumUnwrapper timestamp_unwrapper; uint64_t first_capture_timestamp = timestamp_unwrapper.Unwrap(packets.front().rtp.header.timestamp); uint64_t first_send_timestamp = packets.front().rtp.log_time_us(); for (const auto& packet : packets) { double capture_time_ms = (static_cast(timestamp_unwrapper.Unwrap( packet.rtp.header.timestamp)) - first_capture_timestamp) / *estimated_frequency * 1000; double send_time_ms = static_cast(packet.rtp.log_time_us() - first_send_timestamp) / 1000; float x = config_.GetCallTimeSec(packet.rtp.log_time()); float y = send_time_ms - capture_time_ms; pacer_delay_series.points.emplace_back(x, y); } plot->AppendTimeSeries(std::move(pacer_delay_series)); } plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 10, "Pacer delay (ms)", kBottomMargin, kTopMargin); plot->SetTitle( "Delay from capture to send time. (First packet normalized to 0.)"); } void EventLogAnalyzer::CreateTimestampGraph(PacketDirection direction, Plot* plot) { for (const auto& stream : parsed_log_.rtp_packets_by_ssrc(direction)) { TimeSeries rtp_timestamps( GetStreamName(parsed_log_, direction, stream.ssrc) + " capture-time", LineStyle::kLine, PointStyle::kHighlight); for (const auto& packet : stream.packet_view) { float x = config_.GetCallTimeSec(packet.log_time()); float y = packet.header.timestamp; rtp_timestamps.points.emplace_back(x, y); } plot->AppendTimeSeries(std::move(rtp_timestamps)); TimeSeries rtcp_timestamps( GetStreamName(parsed_log_, direction, stream.ssrc) + " rtcp capture-time", LineStyle::kLine, PointStyle::kHighlight); // TODO(terelius): Why only sender reports? const auto& sender_reports = parsed_log_.sender_reports(direction); for (const auto& rtcp : sender_reports) { if (rtcp.sr.sender_ssrc() != stream.ssrc) continue; float x = config_.GetCallTimeSec(rtcp.log_time()); float y = rtcp.sr.rtp_timestamp(); rtcp_timestamps.points.emplace_back(x, y); } plot->AppendTimeSeriesIfNotEmpty(std::move(rtcp_timestamps)); } plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 1, "RTP timestamp", kBottomMargin, kTopMargin); plot->SetTitle(GetDirectionAsString(direction) + " timestamps"); } void EventLogAnalyzer::CreateSenderAndReceiverReportPlot( PacketDirection direction, rtc::FunctionView fy, std::string title, std::string yaxis_label, Plot* plot) { std::map sr_reports_by_ssrc; const auto& sender_reports = parsed_log_.sender_reports(direction); for (const auto& rtcp : sender_reports) { float x = config_.GetCallTimeSec(rtcp.log_time()); uint32_t ssrc = rtcp.sr.sender_ssrc(); for (const auto& block : rtcp.sr.report_blocks()) { float y = fy(block); auto sr_report_it = sr_reports_by_ssrc.find(ssrc); bool inserted; if (sr_report_it == sr_reports_by_ssrc.end()) { std::tie(sr_report_it, inserted) = sr_reports_by_ssrc.emplace( ssrc, TimeSeries(GetStreamName(parsed_log_, direction, ssrc) + " Sender Reports", LineStyle::kLine, PointStyle::kHighlight)); } sr_report_it->second.points.emplace_back(x, y); } } for (auto& kv : sr_reports_by_ssrc) { plot->AppendTimeSeries(std::move(kv.second)); } std::map rr_reports_by_ssrc; const auto& receiver_reports = parsed_log_.receiver_reports(direction); for (const auto& rtcp : receiver_reports) { float x = config_.GetCallTimeSec(rtcp.log_time()); uint32_t ssrc = rtcp.rr.sender_ssrc(); for (const auto& block : rtcp.rr.report_blocks()) { float y = fy(block); auto rr_report_it = rr_reports_by_ssrc.find(ssrc); bool inserted; if (rr_report_it == rr_reports_by_ssrc.end()) { std::tie(rr_report_it, inserted) = rr_reports_by_ssrc.emplace( ssrc, TimeSeries(GetStreamName(parsed_log_, direction, ssrc) + " Receiver Reports", LineStyle::kLine, PointStyle::kHighlight)); } rr_report_it->second.points.emplace_back(x, y); } } for (auto& kv : rr_reports_by_ssrc) { plot->AppendTimeSeries(std::move(kv.second)); } plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 1, yaxis_label, kBottomMargin, kTopMargin); plot->SetTitle(title); } void EventLogAnalyzer::CreateIceCandidatePairConfigGraph(Plot* plot) { std::map configs_by_cp_id; for (const auto& config : parsed_log_.ice_candidate_pair_configs()) { if (configs_by_cp_id.find(config.candidate_pair_id) == configs_by_cp_id.end()) { const std::string candidate_pair_desc = GetCandidatePairLogDescriptionAsString(config); configs_by_cp_id[config.candidate_pair_id] = TimeSeries("[" + std::to_string(config.candidate_pair_id) + "]" + candidate_pair_desc, LineStyle::kNone, PointStyle::kHighlight); candidate_pair_desc_by_id_[config.candidate_pair_id] = candidate_pair_desc; } float x = config_.GetCallTimeSec(config.log_time()); float y = static_cast(config.type); configs_by_cp_id[config.candidate_pair_id].points.emplace_back(x, y); } // TODO(qingsi): There can be a large number of candidate pairs generated by // certain calls and the frontend cannot render the chart in this case due to // the failure of generating a palette with the same number of colors. for (auto& kv : configs_by_cp_id) { plot->AppendTimeSeries(std::move(kv.second)); } plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 3, "Config Type", kBottomMargin, kTopMargin); plot->SetTitle("[IceEventLog] ICE candidate pair configs"); plot->SetYAxisTickLabels( {{static_cast(IceCandidatePairConfigType::kAdded), "ADDED"}, {static_cast(IceCandidatePairConfigType::kUpdated), "UPDATED"}, {static_cast(IceCandidatePairConfigType::kDestroyed), "DESTROYED"}, {static_cast(IceCandidatePairConfigType::kSelected), "SELECTED"}}); } std::string EventLogAnalyzer::GetCandidatePairLogDescriptionFromId( uint32_t candidate_pair_id) { if (candidate_pair_desc_by_id_.find(candidate_pair_id) != candidate_pair_desc_by_id_.end()) { return candidate_pair_desc_by_id_[candidate_pair_id]; } for (const auto& config : parsed_log_.ice_candidate_pair_configs()) { // TODO(qingsi): Add the handling of the "Updated" config event after the // visualization of property change for candidate pairs is introduced. if (candidate_pair_desc_by_id_.find(config.candidate_pair_id) == candidate_pair_desc_by_id_.end()) { const std::string candidate_pair_desc = GetCandidatePairLogDescriptionAsString(config); candidate_pair_desc_by_id_[config.candidate_pair_id] = candidate_pair_desc; } } return candidate_pair_desc_by_id_[candidate_pair_id]; } void EventLogAnalyzer::CreateIceConnectivityCheckGraph(Plot* plot) { constexpr int kEventTypeOffset = static_cast(IceCandidatePairConfigType::kNumValues); std::map checks_by_cp_id; for (const auto& event : parsed_log_.ice_candidate_pair_events()) { if (checks_by_cp_id.find(event.candidate_pair_id) == checks_by_cp_id.end()) { checks_by_cp_id[event.candidate_pair_id] = TimeSeries( "[" + std::to_string(event.candidate_pair_id) + "]" + GetCandidatePairLogDescriptionFromId(event.candidate_pair_id), LineStyle::kNone, PointStyle::kHighlight); } float x = config_.GetCallTimeSec(event.log_time()); float y = static_cast(event.type) + kEventTypeOffset; checks_by_cp_id[event.candidate_pair_id].points.emplace_back(x, y); } // TODO(qingsi): The same issue as in CreateIceCandidatePairConfigGraph. for (auto& kv : checks_by_cp_id) { plot->AppendTimeSeries(std::move(kv.second)); } plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 4, "Connectivity State", kBottomMargin, kTopMargin); plot->SetTitle("[IceEventLog] ICE connectivity checks"); plot->SetYAxisTickLabels( {{static_cast(IceCandidatePairEventType::kCheckSent) + kEventTypeOffset, "CHECK SENT"}, {static_cast(IceCandidatePairEventType::kCheckReceived) + kEventTypeOffset, "CHECK RECEIVED"}, {static_cast(IceCandidatePairEventType::kCheckResponseSent) + kEventTypeOffset, "RESPONSE SENT"}, {static_cast(IceCandidatePairEventType::kCheckResponseReceived) + kEventTypeOffset, "RESPONSE RECEIVED"}}); } void EventLogAnalyzer::CreateDtlsTransportStateGraph(Plot* plot) { TimeSeries states("DTLS Transport State", LineStyle::kNone, PointStyle::kHighlight); for (const auto& event : parsed_log_.dtls_transport_states()) { float x = config_.GetCallTimeSec(event.log_time()); float y = static_cast(event.dtls_transport_state); states.points.emplace_back(x, y); } plot->AppendTimeSeries(std::move(states)); plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, static_cast(DtlsTransportState::kNumValues), "Transport State", kBottomMargin, kTopMargin); plot->SetTitle("DTLS Transport State"); plot->SetYAxisTickLabels( {{static_cast(DtlsTransportState::kNew), "NEW"}, {static_cast(DtlsTransportState::kConnecting), "CONNECTING"}, {static_cast(DtlsTransportState::kConnected), "CONNECTED"}, {static_cast(DtlsTransportState::kClosed), "CLOSED"}, {static_cast(DtlsTransportState::kFailed), "FAILED"}}); } void EventLogAnalyzer::CreateDtlsWritableStateGraph(Plot* plot) { TimeSeries writable("DTLS Writable", LineStyle::kNone, PointStyle::kHighlight); for (const auto& event : parsed_log_.dtls_writable_states()) { float x = config_.GetCallTimeSec(event.log_time()); float y = static_cast(event.writable); writable.points.emplace_back(x, y); } plot->AppendTimeSeries(std::move(writable)); plot->SetXAxis(config_.CallBeginTimeSec(), config_.CallEndTimeSec(), "Time (s)", kLeftMargin, kRightMargin); plot->SetSuggestedYAxis(0, 1, "Writable", kBottomMargin, kTopMargin); plot->SetTitle("DTLS Writable State"); } } // namespace webrtc