| /* |
| * 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 <cstddef> |
| #include <cstdint> |
| #include <map> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #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<uint32_t, std::string> BuildCandidateIdLogDescriptionMap( |
| const std::vector<LoggedIceCandidatePairConfig>& |
| ice_candidate_pair_configs) { |
| std::map<uint32_t, std::string> 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<uint32_t, TimeSeries> 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<float>(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<float>(IceCandidatePairConfigType::kAdded), "ADDED"}, |
| {static_cast<float>(IceCandidatePairConfigType::kUpdated), "UPDATED"}, |
| {static_cast<float>(IceCandidatePairConfigType::kDestroyed), |
| "DESTROYED"}, |
| {static_cast<float>(IceCandidatePairConfigType::kSelected), |
| "SELECTED"}}); |
| } |
| |
| void CreateIceConnectivityCheckGraph(const ParsedRtcEventLog& parsed_log, |
| const AnalyzerConfig& config, |
| Plot* plot) { |
| constexpr int kEventTypeOffset = |
| static_cast<int>(IceCandidatePairConfigType::kNumValues); |
| std::map<uint32_t, TimeSeries> checks_by_cp_id; |
| std::map<uint32_t, std::string> 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<float>(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<float>(IceCandidatePairEventType::kCheckSent) + |
| kEventTypeOffset, |
| "CHECK SENT"}, |
| {static_cast<float>(IceCandidatePairEventType::kCheckReceived) + |
| kEventTypeOffset, |
| "CHECK RECEIVED"}, |
| {static_cast<float>(IceCandidatePairEventType::kCheckResponseSent) + |
| kEventTypeOffset, |
| "RESPONSE SENT"}, |
| {static_cast<float>(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<float>(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<float>(DtlsTransportState::kNumValues), |
| "Transport State", kBottomMargin, kTopMargin); |
| plot->SetTitle("DTLS Transport State"); |
| plot->SetYAxisTickLabels( |
| {{static_cast<float>(DtlsTransportState::kNew), "NEW"}, |
| {static_cast<float>(DtlsTransportState::kConnecting), "CONNECTING"}, |
| {static_cast<float>(DtlsTransportState::kConnected), "CONNECTED"}, |
| {static_cast<float>(DtlsTransportState::kClosed), "CLOSED"}, |
| {static_cast<float>(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<float>(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 |