Reland "Structured ICE logging via RtcEventLog."

This is a reland of eed5aa8904d09179971d3f4e7e10c109d7c62bfc
Original change's description:
> Structured ICE logging via RtcEventLog.
>
> This change list contains the structured logging module for ICE using
> the RtcEventLog infrastructure, and also extension to the log parser
> and analyzer.
>
> Bug: None
> Change-Id: I6539cf282155c2cde4d3161c53500c0746671a02
> Reviewed-on: https://webrtc-review.googlesource.com/34622
> Commit-Queue: Qingsi Wang <qingsi@google.com>
> Reviewed-by: Björn Terelius <terelius@webrtc.org>
> Reviewed-by: Peter Thatcher <pthatcher@webrtc.org>
> Cr-Commit-Position: refs/heads/master@{#21816}

TBR=pthatcher@webrtc.org,terelius@webrtc.org,deadbeef@webrtc.org

Bug: None
Change-Id: I3df585bf636315ceb0273967146111346a83be86
Reviewed-on: https://webrtc-review.googlesource.com/47545
Commit-Queue: Qingsi Wang <qingsi@google.com>
Reviewed-by: Qingsi Wang <qingsi@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#21881}
diff --git a/rtc_tools/event_log_visualizer/analyzer.cc b/rtc_tools/event_log_visualizer/analyzer.cc
index 3c6a074..2bbb09c 100644
--- a/rtc_tools/event_log_visualizer/analyzer.cc
+++ b/rtc_tools/event_log_visualizer/analyzer.cc
@@ -62,6 +62,8 @@
 
 namespace {
 
+const int kNumMicrosecsPerSec = 1000000;
+
 void SortPacketFeedbackVector(std::vector<PacketFeedback>* vec) {
   auto pred = [](const PacketFeedback& packet_feedback) {
     return packet_feedback.arrival_time_ms == PacketFeedback::kNotReceived;
@@ -88,9 +90,10 @@
 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 1000000 to convert to microseconds.
+  // time in seconds and then multiply by kNumMicrosecsPerSec to convert to
+  // microseconds.
   static constexpr double kTimestampToMicroSec =
-      1000000.0 / static_cast<double>(1ul << 18);
+      static_cast<double>(kNumMicrosecsPerSec) / static_cast<double>(1ul << 18);
   return abs_send_time * kTimestampToMicroSec;
 }
 
@@ -193,7 +196,9 @@
     RTC_LOG(LS_WARNING) << "New capture time " << new_packet.header.timestamp
                         << ", received time " << new_packet.timestamp;
     RTC_LOG(LS_WARNING) << "Receive time difference " << recv_time_diff << " = "
-                        << static_cast<double>(recv_time_diff) / 1000000 << "s";
+                        << static_cast<double>(recv_time_diff) /
+                               kNumMicrosecsPerSec
+                        << "s";
     RTC_LOG(LS_WARNING) << "Send time difference " << send_time_diff << " = "
                         << static_cast<double>(send_time_diff) /
                                kVideoSampleRate
@@ -211,7 +216,8 @@
     uint64_t begin_time,
     TimeSeries* result) {
   for (size_t i = 0; i < data.size(); i++) {
-    float x = static_cast<float>(data[i].timestamp - begin_time) / 1000000;
+    float x = static_cast<float>(data[i].timestamp - begin_time) /
+              kNumMicrosecsPerSec;
     rtc::Optional<float> y = get_y(data[i]);
     if (y)
       result->points.emplace_back(x, *y);
@@ -229,7 +235,8 @@
     uint64_t begin_time,
     TimeSeries* result) {
   for (size_t i = 1; i < data.size(); i++) {
-    float x = static_cast<float>(data[i].timestamp - begin_time) / 1000000;
+    float x = static_cast<float>(data[i].timestamp - begin_time) /
+              kNumMicrosecsPerSec;
     rtc::Optional<ResultType> y = get_y(data[i - 1], data[i]);
     if (y)
       result->points.emplace_back(x, static_cast<float>(*y));
@@ -246,7 +253,8 @@
     TimeSeries* result) {
   ResultType sum = 0;
   for (size_t i = 0; i < data.size(); i++) {
-    float x = static_cast<float>(data[i].timestamp - begin_time) / 1000000;
+    float x = static_cast<float>(data[i].timestamp - begin_time) /
+              kNumMicrosecsPerSec;
     rtc::Optional<ResultType> y = extract(data[i]);
     if (y) {
       sum += *y;
@@ -267,7 +275,8 @@
     TimeSeries* result) {
   ResultType sum = 0;
   for (size_t i = 1; i < data.size(); i++) {
-    float x = static_cast<float>(data[i].timestamp - begin_time) / 1000000;
+    float x = static_cast<float>(data[i].timestamp - begin_time) /
+              kNumMicrosecsPerSec;
     rtc::Optional<ResultType> y = extract(data[i - 1], data[i]);
     if (y)
       sum += *y;
@@ -307,13 +316,118 @@
         sum_in_window -= *value;
       ++window_index_begin;
     }
-    float window_duration_s = static_cast<float>(window_duration_us) / 1000000;
-    float x = static_cast<float>(t - begin_time) / 1000000;
+    float window_duration_s =
+        static_cast<float>(window_duration_us) / kNumMicrosecsPerSec;
+    float x = static_cast<float>(t - begin_time) / kNumMicrosecsPerSec;
     float y = sum_in_window / window_duration_s;
     result->points.emplace_back(x, y);
   }
 }
 
+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 ParsedRtcEventLog::IceCandidatePairConfig& 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.
+  std::stringstream 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.str();
+}
+
 }  // namespace
 
 EventLogAnalyzer::EventLogAnalyzer(const ParsedRtcEventLog& log)
@@ -526,6 +640,16 @@
         alr_state_events_.push_back(parsed_log_.GetAlrState(i));
         break;
       }
+      case ParsedRtcEventLog::ICE_CANDIDATE_PAIR_CONFIG: {
+        ice_candidate_pair_configs_.push_back(
+            parsed_log_.GetIceCandidatePairConfig(i));
+        break;
+      }
+      case ParsedRtcEventLog::ICE_CANDIDATE_PAIR_EVENT: {
+        ice_candidate_pair_events_.push_back(
+            parsed_log_.GetIceCandidatePairEvent(i));
+        break;
+      }
       case ParsedRtcEventLog::UNKNOWN_EVENT: {
         break;
       }
@@ -538,7 +662,7 @@
   }
   begin_time_ = first_timestamp;
   end_time_ = last_timestamp;
-  call_duration_s_ = static_cast<float>(end_time_ - begin_time_) / 1000000;
+  call_duration_s_ = ToCallTime(end_time_);
   if (last_log_start) {
     // The log was missing the last LOG_END event. Fake it.
     log_segments_.push_back(std::make_pair(*last_log_start, end_time_));
@@ -625,7 +749,7 @@
     last_rtp_timestamp = unwrapper.Unwrap(packets[i].header.timestamp);
     last_log_timestamp = packets[i].timestamp;
   }
-  if (last_log_timestamp - first_log_timestamp < 1000000) {
+  if (last_log_timestamp - first_log_timestamp < kNumMicrosecsPerSec) {
     RTC_LOG(LS_WARNING)
         << "Failed to estimate RTP clock frequency: Stream too short. ("
         << packets.size() << " packets, "
@@ -633,7 +757,8 @@
     return rtc::nullopt;
   }
   double duration =
-      static_cast<double>(last_log_timestamp - first_log_timestamp) / 1000000;
+      static_cast<double>(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}) {
@@ -648,7 +773,7 @@
 }
 
 float EventLogAnalyzer::ToCallTime(int64_t timestamp) const {
-  return static_cast<float>(timestamp - begin_time_) / 1000000;
+  return static_cast<float>(timestamp - begin_time_) / kNumMicrosecsPerSec;
 }
 
 void EventLogAnalyzer::CreatePacketGraph(PacketDirection desired_direction,
@@ -699,8 +824,7 @@
     std::string label = label_prefix + " " + GetStreamName(stream_id);
     TimeSeries time_series(label, LineStyle::kStep);
     for (size_t i = 0; i < packet_stream.size(); i++) {
-      float x = static_cast<float>(packet_stream[i].timestamp - begin_time_) /
-                1000000;
+      float x = ToCallTime(packet_stream[i].timestamp);
       time_series.points.emplace_back(x, i + 1);
     }
 
@@ -738,7 +862,7 @@
       parsed_log_.GetAudioPlayout(i, &ssrc);
       uint64_t timestamp = parsed_log_.GetTimestamp(i);
       if (MatchingSsrc(ssrc, desired_ssrc_)) {
-        float x = static_cast<float>(timestamp - begin_time_) / 1000000;
+        float x = ToCallTime(timestamp);
         float y = static_cast<float>(timestamp - last_playout[ssrc]) / 1000;
         if (time_series[ssrc].points.size() == 0) {
           // There were no previusly logged playout for this SSRC.
@@ -776,7 +900,7 @@
     //             streams. Tracking bug: webrtc:6399
     for (auto& packet : packet_stream) {
       if (packet.header.extension.hasAudioLevel) {
-        float x = static_cast<float>(packet.timestamp - begin_time_) / 1000000;
+        float x = ToCallTime(packet.timestamp);
         // 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<float>(-packet.header.extension.audioLevel);
@@ -867,7 +991,7 @@
             std::max(highest_prior_seq_number, sequence_number);
         ++window_index_begin;
       }
-      float x = static_cast<float>(t - begin_time_) / 1000000;
+      float x = ToCallTime(t);
       int64_t expected_packets = highest_seq_number - highest_prior_seq_number;
       if (expected_packets > 0) {
         int64_t received_packets = window_index_end - window_index_begin;
@@ -956,7 +1080,7 @@
   TimeSeries time_series("Fraction lost", LineStyle::kLine,
                          PointStyle::kHighlight);
   for (auto& bwe_update : bwe_loss_updates_) {
-    float x = static_cast<float>(bwe_update.timestamp - begin_time_) / 1000000;
+    float x = ToCallTime(bwe_update.timestamp);
     float y = static_cast<float>(bwe_update.fraction_loss) / 255 * 100;
     time_series.points.emplace_back(x, y);
   }
@@ -1016,8 +1140,8 @@
       ++window_index_begin;
     }
     float window_duration_in_seconds =
-        static_cast<float>(window_duration_) / 1000000;
-    float x = static_cast<float>(time - begin_time_) / 1000000;
+        static_cast<float>(window_duration_) / kNumMicrosecsPerSec;
+    float x = ToCallTime(time);
     float y = bytes_in_window * 8 / window_duration_in_seconds / 1000;
     bitrate_series.points.emplace_back(x, y);
   }
@@ -1027,8 +1151,7 @@
   if (desired_direction == kOutgoingPacket) {
     TimeSeries loss_series("Loss-based estimate", LineStyle::kStep);
     for (auto& loss_update : bwe_loss_updates_) {
-      float x =
-          static_cast<float>(loss_update.timestamp - begin_time_) / 1000000;
+      float x = ToCallTime(loss_update.timestamp);
       float y = static_cast<float>(loss_update.new_bitrate) / 1000;
       loss_series.points.emplace_back(x, y);
     }
@@ -1046,8 +1169,7 @@
     BandwidthUsage last_detector_state = BandwidthUsage::kBwNormal;
 
     for (auto& delay_update : bwe_delay_updates_) {
-      float x =
-          static_cast<float>(delay_update.timestamp - begin_time_) / 1000000;
+      float x = ToCallTime(delay_update.timestamp);
       float y = static_cast<float>(delay_update.bitrate_bps) / 1000;
 
       if (last_detector_state != delay_update.detector_state) {
@@ -1079,7 +1201,7 @@
     TimeSeries created_series("Probe cluster created.", LineStyle::kNone,
                               PointStyle::kHighlight);
     for (auto& cluster : bwe_probe_cluster_created_events_) {
-      float x = static_cast<float>(cluster.timestamp - begin_time_) / 1000000;
+      float x = ToCallTime(cluster.timestamp);
       float y = static_cast<float>(cluster.bitrate_bps) / 1000;
       created_series.points.emplace_back(x, y);
     }
@@ -1088,7 +1210,7 @@
                              PointStyle::kHighlight);
     for (auto& result : bwe_probe_result_events_) {
       if (result.bitrate_bps) {
-        float x = static_cast<float>(result.timestamp - begin_time_) / 1000000;
+        float x = ToCallTime(result.timestamp);
         float y = static_cast<float>(*result.bitrate_bps) / 1000;
         result_series.points.emplace_back(x, y);
       }
@@ -1150,7 +1272,7 @@
   for (const auto& kv : remb_packets) {
     const LoggedRtcpPacket* const rtcp = kv.second;
     const rtcp::Remb* const remb = static_cast<rtcp::Remb*>(rtcp->packet.get());
-    float x = static_cast<float>(rtcp->timestamp - begin_time_) / 1000000;
+    float x = ToCallTime(rtcp->timestamp);
     float y = static_cast<float>(remb->bitrate_bps()) / 1000;
     remb_series.points.emplace_back(x, y);
   }
@@ -1290,8 +1412,7 @@
             acked_bitrate.Update(packet.payload_size, packet.arrival_time_ms);
           bitrate_bps = acked_bitrate.Rate(feedback.back().arrival_time_ms);
         }
-        float x = static_cast<float>(clock.TimeInMicroseconds() - begin_time_) /
-                  1000000;
+        float x = ToCallTime(clock.TimeInMicroseconds());
         float y = bitrate_bps.value_or(0) / 1000;
         acked_time_series.points.emplace_back(x, y);
 #if !(BWE_TEST_LOGGING_COMPILE_TIME_ENABLE)
@@ -1322,8 +1443,7 @@
     if (observer.GetAndResetBitrateUpdated() ||
         time_us - last_update_us >= 1e6) {
       uint32_t y = observer.last_bitrate_bps() / 1000;
-      float x = static_cast<float>(clock.TimeInMicroseconds() - begin_time_) /
-                1000000;
+      float x = ToCallTime(clock.TimeInMicroseconds());
       time_series.points.emplace_back(x, y);
       last_update_us = time_us;
     }
@@ -1396,15 +1516,13 @@
     rtc::Optional<uint32_t> bitrate_bps = acked_bitrate.Rate(arrival_time_ms);
     if (bitrate_bps) {
       uint32_t y = *bitrate_bps / 1000;
-      float x = static_cast<float>(clock.TimeInMicroseconds() - begin_time_) /
-                1000000;
+      float x = ToCallTime(clock.TimeInMicroseconds());
       acked_time_series.points.emplace_back(x, y);
     }
     if (packet_router.GetAndResetBitrateUpdated() ||
         clock.TimeInMicroseconds() - last_update_us >= 1e6) {
       uint32_t y = packet_router.last_bitrate_bps() / 1000;
-      float x = static_cast<float>(clock.TimeInMicroseconds() - begin_time_) /
-                1000000;
+      float x = ToCallTime(clock.TimeInMicroseconds());
       time_series.points.emplace_back(x, y);
       last_update_us = clock.TimeInMicroseconds();
     }
@@ -1475,9 +1593,7 @@
             feedback_adapter.GetTransportFeedbackVector();
         SortPacketFeedbackVector(&feedback);
         for (const PacketFeedback& packet : feedback) {
-          float x =
-              static_cast<float>(clock.TimeInMicroseconds() - begin_time_) /
-              1000000;
+          float x = ToCallTime(clock.TimeInMicroseconds());
           if (packet.send_time_ms == PacketFeedback::kNoSendTime) {
             late_feedback_series.points.emplace_back(x, prev_y);
             continue;
@@ -1587,7 +1703,7 @@
                                *estimated_frequency * 1000;
       double send_time_ms =
           static_cast<double>(packet.timestamp - first_send_timestamp) / 1000;
-      float x = static_cast<float>(packet.timestamp - begin_time_) / 1000000;
+      float x = ToCallTime(packet.timestamp);
       float y = send_time_ms - capture_time_ms;
       pacer_delay_series.points.emplace_back(x, y);
     }
@@ -1609,7 +1725,7 @@
       TimeSeries timestamp_data(GetStreamName(stream_id) + " capture-time",
                                 LineStyle::kLine, PointStyle::kHighlight);
       for (LoggedRtpPacket packet : rtp_packets) {
-        float x = static_cast<float>(packet.timestamp - begin_time_) / 1000000;
+        float x = ToCallTime(packet.timestamp);
         float y = packet.header.timestamp;
         timestamp_data.points.emplace_back(x, y);
       }
@@ -1628,7 +1744,7 @@
             continue;
           rtcp::SenderReport* sr;
           sr = static_cast<rtcp::SenderReport*>(rtcp.packet.get());
-          float x = static_cast<float>(rtcp.timestamp - begin_time_) / 1000000;
+          float x = ToCallTime(rtcp.timestamp);
           float y = sr->rtp_timestamp();
           timestamp_data.points.emplace_back(x, y);
         }
@@ -1986,6 +2102,81 @@
   plot->SetTitle("NetEq timing");
 }
 
+void EventLogAnalyzer::CreateIceCandidatePairConfigGraph(Plot* plot) {
+  std::map<uint32_t, TimeSeries> configs_by_cp_id;
+  for (const auto& config : 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(
+          candidate_pair_desc, LineStyle::kNone, PointStyle::kHighlight);
+      candidate_pair_desc_by_id_[config.candidate_pair_id] =
+          candidate_pair_desc;
+    }
+    float x = ToCallTime(config.timestamp);
+    float y = static_cast<float>(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(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin);
+  plot->SetSuggestedYAxis(0, 3, "Numeric Config Type", kBottomMargin,
+                          kTopMargin);
+  plot->SetTitle("[IceEventLog] ICE candidate pair configs");
+}
+
+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 : 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) {
+  std::map<uint32_t, TimeSeries> checks_by_cp_id;
+  for (const auto& event : 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(
+          GetCandidatePairLogDescriptionFromId(event.candidate_pair_id),
+          LineStyle::kNone, PointStyle::kHighlight);
+    }
+    float x = ToCallTime(event.timestamp);
+    float y = static_cast<float>(event.type);
+    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(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin);
+  plot->SetSuggestedYAxis(0, 4, "Numeric Connectivity State", kBottomMargin,
+                          kTopMargin);
+  plot->SetTitle("[IceEventLog] ICE connectivity checks");
+}
+
 void EventLogAnalyzer::Notification(
     std::unique_ptr<TriageNotification> notification) {
   notifications_.push_back(std::move(notification));
diff --git a/rtc_tools/event_log_visualizer/analyzer.h b/rtc_tools/event_log_visualizer/analyzer.h
index 5d0faab..fafce66 100644
--- a/rtc_tools/event_log_visualizer/analyzer.h
+++ b/rtc_tools/event_log_visualizer/analyzer.h
@@ -109,6 +109,9 @@
                                     int file_sample_rate_hz,
                                     Plot* plot);
 
+  void CreateIceCandidatePairConfigGraph(Plot* plot);
+  void CreateIceConnectivityCheckGraph(Plot* plot);
+
   // Returns a vector of capture and arrival timestamps for the video frames
   // of the stream with the most number of frames.
   std::vector<std::pair<int64_t, int64_t>> GetFrameTimestamps() const;
@@ -159,6 +162,8 @@
 
   void Notification(std::unique_ptr<TriageNotification> notification);
 
+  std::string GetCandidatePairLogDescriptionFromId(uint32_t candidate_pair_id);
+
   const ParsedRtcEventLog& parsed_log_;
 
   // A list of SSRCs we are interested in analysing.
@@ -204,6 +209,14 @@
 
   std::vector<ParsedRtcEventLog::AlrStateEvent> alr_state_events_;
 
+  std::vector<ParsedRtcEventLog::IceCandidatePairConfig>
+      ice_candidate_pair_configs_;
+
+  std::vector<ParsedRtcEventLog::IceCandidatePairEvent>
+      ice_candidate_pair_events_;
+
+  std::map<uint32_t, std::string> candidate_pair_desc_by_id_;
+
   // Window and step size used for calculating moving averages, e.g. bitrate.
   // The generated data points will be |step_| microseconds apart.
   // Only events occuring at most |window_duration_| microseconds before the
diff --git a/rtc_tools/event_log_visualizer/main.cc b/rtc_tools/event_log_visualizer/main.cc
index 4a6ec64..2e7a79e 100644
--- a/rtc_tools/event_log_visualizer/main.cc
+++ b/rtc_tools/event_log_visualizer/main.cc
@@ -115,6 +115,12 @@
 DEFINE_bool(plot_audio_jitter_buffer,
             false,
             "Plot the audio jitter buffer delay profile.");
+DEFINE_bool(plot_ice_candidate_pair_config,
+            false,
+            "Plot the ICE candidate pair config events.");
+DEFINE_bool(plot_ice_connectivity_check,
+            false,
+            "Plot the ICE candidate pair connectivity checks.");
 
 DEFINE_string(
     force_fieldtrials,
@@ -318,6 +324,13 @@
                                           collection->AppendNewPlot());
   }
 
+  if (FLAG_plot_ice_candidate_pair_config) {
+    analyzer.CreateIceCandidatePairConfigGraph(collection->AppendNewPlot());
+  }
+  if (FLAG_plot_ice_connectivity_check) {
+    analyzer.CreateIceConnectivityCheckGraph(collection->AppendNewPlot());
+  }
+
   collection->Draw();
 
   if (FLAG_print_triage_notifications) {
@@ -356,4 +369,6 @@
   FLAG_plot_audio_encoder_dtx = setting;
   FLAG_plot_audio_encoder_num_channels = setting;
   FLAG_plot_audio_jitter_buffer = setting;
+  FLAG_plot_ice_candidate_pair_config = setting;
+  FLAG_plot_ice_connectivity_check = setting;
 }