/* * Copyright (c) 2026 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/analyze_connectivity.h" #include #include #include #include #include #include #include "absl/strings/string_view.h" #include "api/candidate.h" #include "api/dtls_transport_interface.h" #include "logging/rtc_event_log/events/rtc_event_ice_candidate_pair.h" #include "logging/rtc_event_log/events/rtc_event_ice_candidate_pair_config.h" #include "logging/rtc_event_log/rtc_event_log_parser.h" #include "rtc_base/checks.h" #include "rtc_base/strings/string_builder.h" #include "rtc_tools/rtc_event_log_visualizer/analyzer_common.h" #include "rtc_tools/rtc_event_log_visualizer/plot_base.h" namespace webrtc { namespace { const char kUnknownEnumValue[] = "unknown"; // TODO(tommi): This should be "host". const char kIceCandidateTypeLocal[] = "local"; // TODO(tommi): This should be "srflx". 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"; absl::string_view GetIceCandidateTypeAsString(IceCandidateType type) { switch (type) { case IceCandidateType::kHost: return kIceCandidateTypeLocal; case IceCandidateType::kSrflx: return kIceCandidateTypeStun; case IceCandidateType::kPrflx: return kIceCandidateTypePrflx; case IceCandidateType::kRelay: return kIceCandidateTypeRelay; default: RTC_DCHECK_NOTREACHED(); return kUnknownEnumValue; } } std::string GetProtocolAsString(IceCandidatePairProtocol protocol) { switch (protocol) { case IceCandidatePairProtocol::kUdp: return kProtocolUdp; case IceCandidatePairProtocol::kTcp: return kProtocolTcp; case IceCandidatePairProtocol::kSsltcp: return kProtocolSsltcp; case IceCandidatePairProtocol::kTls: return kProtocolTls; default: return kUnknownEnumValue; } } std::string GetAddressFamilyAsString(IceCandidatePairAddressFamily family) { switch (family) { case IceCandidatePairAddressFamily::kIpv4: return kAddressFamilyIpv4; case IceCandidatePairAddressFamily::kIpv6: return kAddressFamilyIpv6; default: return kUnknownEnumValue; } } std::string GetNetworkTypeAsString(IceCandidateNetworkType type) { switch (type) { case IceCandidateNetworkType::kEthernet: return kNetworkTypeEthernet; case IceCandidateNetworkType::kLoopback: return kNetworkTypeLoopback; case IceCandidateNetworkType::kWifi: return kNetworkTypeWifi; case IceCandidateNetworkType::kVpn: return kNetworkTypeVpn; case 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. StringBuilder ss; ss << GetIceCandidateTypeAsString(config.local_candidate_type); if (config.local_candidate_type == IceCandidateType::kRelay) { ss << "(" << GetProtocolAsString(config.local_relay_protocol) << ")"; } ss << ":" << GetNetworkTypeAsString(config.local_network_type) << ":" << GetAddressFamilyAsString(config.local_address_family) << "->" << GetIceCandidateTypeAsString(config.remote_candidate_type) << ":" << GetAddressFamilyAsString(config.remote_address_family) << "@" << GetProtocolAsString(config.candidate_pair_protocol); return ss.Release(); } std::map BuildCandidateIdLogDescriptionMap( const std::vector& ice_candidate_pair_configs) { std::map candidate_pair_desc_by_id; for (const auto& config : 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; } } // namespace void CreateIceCandidatePairConfigGraph(const ParsedRtcEventLog& parsed_log, const AnalyzerConfig& config, Plot* plot) { std::map configs_by_cp_id; for (const auto& config_item : parsed_log.ice_candidate_pair_configs()) { if (configs_by_cp_id.find(config_item.candidate_pair_id) == configs_by_cp_id.end()) { const std::string candidate_pair_desc = GetCandidatePairLogDescriptionAsString(config_item); configs_by_cp_id[config_item.candidate_pair_id] = TimeSeries("[" + std::to_string(config_item.candidate_pair_id) + "]" + candidate_pair_desc, LineStyle::kNone, PointStyle::kHighlight); } float x = config.GetCallTimeSec(config_item.log_time()); float y = static_cast(config_item.type); configs_by_cp_id[config_item.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"}}); } void CreateIceConnectivityCheckGraph(const ParsedRtcEventLog& parsed_log, const AnalyzerConfig& config, Plot* plot) { constexpr int kEventTypeOffset = static_cast(IceCandidatePairConfigType::kNumValues); std::map checks_by_cp_id; std::map candidate_pair_desc_by_id = BuildCandidateIdLogDescriptionMap( parsed_log.ice_candidate_pair_configs()); 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) + "]" + candidate_pair_desc_by_id[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 CreateDtlsTransportStateGraph(const ParsedRtcEventLog& parsed_log, const AnalyzerConfig& config, 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 CreateDtlsWritableStateGraph(const ParsedRtcEventLog& parsed_log, const AnalyzerConfig& config, 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