SCReAMv2: Visualize newly lost, recovered, and CE marked packet events

Calculates newly lost, recovered, and CE marked packet feedback events over a rolling window of one smoothed RTT (using ScreamV2::rtt()), as well as per individual feedback message (filtering out zero events).

Adds a new graph to analyze_bwe.cc displaying:
- Lost per smoothed RTT (continuous line)
- Recovered per smoothed RTT (continuous line)
- CE marked per smoothed RTT (continuous line)
- Lost per feedback (scatter points, non-zero only)
- Recovered per feedback (scatter points, non-zero only)
- CE marked per feedback (scatter points, non-zero only)

Bug: webrtc:436707095
Change-Id: I5fda4d5ec12c998ba32e56739fd629d75d0ef50c
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/475501
Reviewed-by: Björn Terelius <terelius@webrtc.org>
Commit-Queue: Per Kjellander <perkj@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#47831}
diff --git a/rtc_tools/rtc_event_log_visualizer/analyze_bwe.cc b/rtc_tools/rtc_event_log_visualizer/analyze_bwe.cc
index e30a546..d52729d 100644
--- a/rtc_tools/rtc_event_log_visualizer/analyze_bwe.cc
+++ b/rtc_tools/rtc_event_log_visualizer/analyze_bwe.cc
@@ -862,6 +862,60 @@
   plot->SetTitle("Simulated Scream Ratios");
 }
 
+void CreateScreamSimulationFeedbackEventsPerRttGraph(
+    const ParsedRtcEventLog& parsed_log,
+    const AnalyzerConfig& config,
+    Plot* plot) {
+  TimeSeries lost_series("Lost per smoothed RTT", LineStyle::kLine);
+  TimeSeries recovered_series("Recovered per smoothed RTT", LineStyle::kLine);
+  TimeSeries ce_marked_series("CE marked per smoothed RTT", LineStyle::kLine);
+
+  TimeSeries lost_per_feedback_series("Lost per feedback", LineStyle::kNone,
+                                      PointStyle::kHighlight);
+  TimeSeries recovered_per_feedback_series(
+      "Recovered per feedback", LineStyle::kNone, PointStyle::kHighlight);
+  TimeSeries ce_marked_per_feedback_series(
+      "CE marked per feedback", LineStyle::kNone, PointStyle::kHighlight);
+
+  LogScreamSimulation simulation({.rate_window = config.window_duration_},
+                                 config.env_);
+  simulation.ProcessEventsInLog(parsed_log);
+
+  for (const LogScreamSimulation::State& state : simulation.updates()) {
+    lost_series.points.emplace_back(config.GetCallTimeSec(state.time),
+                                    state.packets_lost_per_rtt);
+    recovered_series.points.emplace_back(config.GetCallTimeSec(state.time),
+                                         state.packets_recovered_per_rtt);
+    ce_marked_series.points.emplace_back(config.GetCallTimeSec(state.time),
+                                         state.ce_marked_per_rtt);
+
+    if (state.packets_lost_per_feedback > 0) {
+      lost_per_feedback_series.points.emplace_back(
+          config.GetCallTimeSec(state.time), state.packets_lost_per_feedback);
+    }
+    if (state.packets_recovered_per_feedback > 0) {
+      recovered_per_feedback_series.points.emplace_back(
+          config.GetCallTimeSec(state.time),
+          state.packets_recovered_per_feedback);
+    }
+    if (state.ce_marked_per_feedback > 0) {
+      ce_marked_per_feedback_series.points.emplace_back(
+          config.GetCallTimeSec(state.time), state.ce_marked_per_feedback);
+    }
+  }
+  plot->AppendTimeSeries(std::move(lost_series));
+  plot->AppendTimeSeries(std::move(recovered_series));
+  plot->AppendTimeSeries(std::move(ce_marked_series));
+  plot->AppendTimeSeriesIfNotEmpty(std::move(lost_per_feedback_series));
+  plot->AppendTimeSeriesIfNotEmpty(std::move(recovered_per_feedback_series));
+  plot->AppendTimeSeriesIfNotEmpty(std::move(ce_marked_per_feedback_series));
+
+  plot->SetXAxis(config.CallBeginTimeSec(), config.CallEndTimeSec(), "Time (s)",
+                 kLeftMargin, kRightMargin);
+  plot->SetSuggestedYAxis(0, 10, "Packets", kBottomMargin, kTopMargin);
+  plot->SetTitle("Simulated Scream feedback events per smoothed RTT");
+}
+
 void CreateScreamRefWindowGraph(const ParsedRtcEventLog& parsed_log,
                                 const AnalyzerConfig& config,
                                 Plot* plot) {
diff --git a/rtc_tools/rtc_event_log_visualizer/analyze_bwe.h b/rtc_tools/rtc_event_log_visualizer/analyze_bwe.h
index 5de8598..0216381 100644
--- a/rtc_tools/rtc_event_log_visualizer/analyze_bwe.h
+++ b/rtc_tools/rtc_event_log_visualizer/analyze_bwe.h
@@ -55,6 +55,11 @@
                                        const AnalyzerConfig& config,
                                        Plot* plot);
 
+void CreateScreamSimulationFeedbackEventsPerRttGraph(
+    const ParsedRtcEventLog& parsed_log,
+    const AnalyzerConfig& config,
+    Plot* plot);
+
 void CreateScreamRefWindowGraph(const ParsedRtcEventLog& parsed_log,
                                 const AnalyzerConfig& config,
                                 Plot* plot);
diff --git a/rtc_tools/rtc_event_log_visualizer/analyzer.cc b/rtc_tools/rtc_event_log_visualizer/analyzer.cc
index 5821ad7..c878b18 100644
--- a/rtc_tools/rtc_event_log_visualizer/analyzer.cc
+++ b/rtc_tools/rtc_event_log_visualizer/analyzer.cc
@@ -190,6 +190,10 @@
   plots_.RegisterPlot("simulated_scream_ratios", [this](Plot* plot) {
     this->CreateScreamSimulationRatiosGraph(plot);
   });
+  plots_.RegisterPlot(
+      "simulated_scream_feedback_events_per_rtt", [this](Plot* plot) {
+        this->CreateScreamSimulationFeedbackEventsPerRttGraph(plot);
+      });
   plots_.RegisterPlot("outgoing_loss", [this](Plot* plot) {
     this->CreateOutgoingLossRateGraph(plot);
   });
@@ -428,6 +432,12 @@
   webrtc::CreateScreamSimulationRatiosGraph(parsed_log_, config_, plot);
 }
 
+void EventLogAnalyzer::CreateScreamSimulationFeedbackEventsPerRttGraph(
+    Plot* plot) const {
+  webrtc::CreateScreamSimulationFeedbackEventsPerRttGraph(parsed_log_, config_,
+                                                          plot);
+}
+
 void EventLogAnalyzer::CreateScreamRefWindowGraph(Plot* plot) const {
   webrtc::CreateScreamRefWindowGraph(parsed_log_, config_, plot);
 }
diff --git a/rtc_tools/rtc_event_log_visualizer/analyzer.h b/rtc_tools/rtc_event_log_visualizer/analyzer.h
index 60d2244..6ccf192 100644
--- a/rtc_tools/rtc_event_log_visualizer/analyzer.h
+++ b/rtc_tools/rtc_event_log_visualizer/analyzer.h
@@ -135,6 +135,7 @@
   void CreateScreamSimulationBitrateGraph(Plot* plot) const;
   void CreateScreamSimulationRefWindowGraph(Plot* plot) const;
   void CreateScreamSimulationRatiosGraph(Plot* plot) const;
+  void CreateScreamSimulationFeedbackEventsPerRttGraph(Plot* plot) const;
   void CreateSendSideBweSimulationGraph(Plot* plot) const;
   void CreateReceiveSideBweSimulationGraph(Plot* plot) const;
 
diff --git a/rtc_tools/rtc_event_log_visualizer/log_scream_simulation.cc b/rtc_tools/rtc_event_log_visualizer/log_scream_simulation.cc
index 2ae08b7..3aa26b4 100644
--- a/rtc_tools/rtc_event_log_visualizer/log_scream_simulation.cc
+++ b/rtc_tools/rtc_event_log_visualizer/log_scream_simulation.cc
@@ -14,6 +14,7 @@
 #include <vector>
 
 #include "api/environment/environment.h"
+#include "api/transport/ecn_marking.h"
 #include "api/transport/network_types.h"
 #include "api/units/data_rate.h"
 #include "api/units/data_size.h"
@@ -123,10 +124,11 @@
       // ScreamNetworkController::OnNetworkRouteChange.
       scream_.emplace(env_);
       scream_->SetTargetBitrateConstraints(
-          /*min=*/DataRate::Zero() /*max=*/, DataRate::PlusInfinity(),
+          /*min=*/DataRate::Zero(), /*max=*/DataRate::PlusInfinity(),
           /*start=*/DataRate::KilobitsPerSec(300));
       local_candidate_type_ = candidate.local_candidate_type;
       remote_candidate_type_ = candidate.remote_candidate_type;
+      feedback_history_.clear();
     }
   }
 }
@@ -160,6 +162,43 @@
 }
 
 void LogScreamSimulation::LogState(const TransportPacketsFeedback& msg) {
+  int lost_count = 0;
+  int recovered_count = 0;
+  int ce_marked_count = 0;
+  for (const auto& packet : msg.packet_feedbacks) {
+    if (packet.reported_lost_for_the_first_time) {
+      lost_count++;
+    }
+    if (packet.reported_recovered_for_the_first_time) {
+      recovered_count++;
+    }
+    if (packet.ecn == EcnMarking::kCe) {
+      ce_marked_count++;
+    }
+  }
+
+  feedback_history_.push_back(FeedbackEvent{
+      .time = msg.feedback_time,
+      .lost_count = lost_count,
+      .recovered_count = recovered_count,
+      .ce_marked_count = ce_marked_count,
+  });
+
+  TimeDelta rtt = scream_->rtt();
+  while (!feedback_history_.empty() &&
+         feedback_history_.front().time < msg.feedback_time - rtt) {
+    feedback_history_.pop_front();
+  }
+
+  int total_lost = 0;
+  int total_recovered = 0;
+  int total_ce_marked = 0;
+  for (const auto& event : feedback_history_) {
+    total_lost += event.lost_count;
+    total_recovered += event.recovered_count;
+    total_ce_marked += event.ce_marked_count;
+  }
+
   state_.emplace_back(State{
       .time = msg.feedback_time,
       .target_rate = scream_->target_rate(),
@@ -192,6 +231,12 @@
       .l4s_alpha = scream_->l4s_alpha(),
       .l4s_alpha_v = scream_->delay_based_congestion_control().l4s_alpha_v(),
       .loss_event_rate = scream_->loss_event_rate(),
+      .packets_lost_per_rtt = total_lost,
+      .packets_recovered_per_rtt = total_recovered,
+      .ce_marked_per_rtt = total_ce_marked,
+      .packets_lost_per_feedback = lost_count,
+      .packets_recovered_per_feedback = recovered_count,
+      .ce_marked_per_feedback = ce_marked_count,
   });
 }
 
diff --git a/rtc_tools/rtc_event_log_visualizer/log_scream_simulation.h b/rtc_tools/rtc_event_log_visualizer/log_scream_simulation.h
index 6c6e88c..a9458f6 100644
--- a/rtc_tools/rtc_event_log_visualizer/log_scream_simulation.h
+++ b/rtc_tools/rtc_event_log_visualizer/log_scream_simulation.h
@@ -11,6 +11,7 @@
 #define RTC_TOOLS_RTC_EVENT_LOG_VISUALIZER_LOG_SCREAM_SIMULATION_H_
 
 #include <cstdint>
+#include <deque>
 #include <optional>
 #include <vector>
 
@@ -68,6 +69,12 @@
     double l4s_alpha = 0.0;
     double l4s_alpha_v = 0.0;
     double loss_event_rate = 0.0;
+    int packets_lost_per_rtt = 0;
+    int packets_recovered_per_rtt = 0;
+    int ce_marked_per_rtt = 0;
+    int packets_lost_per_feedback = 0;
+    int packets_recovered_per_feedback = 0;
+    int ce_marked_per_feedback = 0;
   };
 
   struct Config {
@@ -105,6 +112,14 @@
   std::optional<IceCandidateType> local_candidate_type_;
   std::optional<IceCandidateType> remote_candidate_type_;
 
+  struct FeedbackEvent {
+    Timestamp time;
+    int lost_count = 0;
+    int recovered_count = 0;
+    int ce_marked_count = 0;
+  };
+  std::deque<FeedbackEvent> feedback_history_;
+
   std::vector<State> state_;
 };
 }  // namespace webrtc
diff --git a/rtc_tools/rtc_event_log_visualizer/main.cc b/rtc_tools/rtc_event_log_visualizer/main.cc
index f299d9a..3068855 100644
--- a/rtc_tools/rtc_event_log_visualizer/main.cc
+++ b/rtc_tools/rtc_event_log_visualizer/main.cc
@@ -253,7 +253,8 @@
       {"scream",
        {"scream_ref_window", "simulated_scream_delay",
         "simulated_scream_bitrates", "simulated_scream_ref_window",
-        "simulated_scream_ratios", "network_delay_feedback", "pacer_delay"}}};
+        "simulated_scream_ratios", "simulated_scream_feedback_events_per_rtt",
+        "network_delay_feedback", "pacer_delay"}}};
 
   if (absl::GetFlag(FLAGS_list_plots)) {
     std::cerr << "List of registered plots (for use with the --plot flag):"