Add SchedulableNetworkBehavior and tests.

This is a network behaviour that can change its parameters over time as specified with a schedule proto.

Bug: webrtc:14525
Change-Id: Idd34cc48c8e3e8311975615f2c3dc3ffb522a708
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/352140
Reviewed-by: Björn Terelius <terelius@webrtc.org>
Reviewed-by: Mirko Bonadei <mbonadei@webrtc.org>
Commit-Queue: Per Kjellander <perkj@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#42390}
diff --git a/BUILD.gn b/BUILD.gn
index f090f16..7328df1 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -678,7 +678,10 @@
     data = rtc_unittests_resources
 
     if (rtc_enable_protobuf) {
-      deps += [ "logging:rtc_event_log_tests" ]
+      deps += [
+        "api/test/network_emulation:network_config_schedule_proto",
+        "logging:rtc_event_log_tests",
+      ]
     }
 
     if (is_ios) {
diff --git a/api/test/network_emulation/BUILD.gn b/api/test/network_emulation/BUILD.gn
index f28fd35..d4aa23b 100644
--- a/api/test/network_emulation/BUILD.gn
+++ b/api/test/network_emulation/BUILD.gn
@@ -6,8 +6,30 @@
 # in the file PATENTS.  All contributing project authors may
 # be found in the AUTHORS file in the root of the source tree.
 
+import("//third_party/protobuf/proto_library.gni")
 import("../../../webrtc.gni")
 
+if (rtc_enable_protobuf) {
+  proto_library("network_config_schedule_proto") {
+    visibility = [ "*" ]
+    sources = [ "network_config_schedule.proto" ]
+    proto_out_dir = "api/test/network_emulation/"
+  }
+
+  rtc_source_set("schedulable_network_node_builder") {
+    visibility = [ "*" ]
+    sources = [
+      "schedulable_network_node_builder.cc",
+      "schedulable_network_node_builder.h",
+    ]
+    deps = [
+      ":network_config_schedule_proto",
+      "../..:network_emulation_manager_api",
+      "../../../test/network:schedulable_network_behavior",
+    ]
+  }
+}
+
 rtc_library("network_emulation") {
   visibility = [ "*" ]
 
diff --git a/api/test/network_emulation/network_config_schedule.proto b/api/test/network_emulation/network_config_schedule.proto
new file mode 100644
index 0000000..0e1036c
--- /dev/null
+++ b/api/test/network_emulation/network_config_schedule.proto
@@ -0,0 +1,27 @@
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+package webrtc.network_behaviour;
+
+message NetworkConfigScheduleItem {
+  // Time since the first sent packet when this item should be applied.
+  // This should typically be 0 for the first item in the schedule.
+  optional int64 time_since_first_sent_packet_ms = 1;
+
+  // Network parameters, See webrtc::BuiltInNetworkBehaviorConfig.
+  optional int64 queue_length_packets = 2;
+  optional int64 queue_delay_ms = 3;
+  optional int64 link_capacity_kbps = 4;
+  optional int64 loss_percent = 5;
+  optional int64 delay_standard_deviation_ms = 6;
+  optional bool allow_reordering = 7;
+  optional int64 avg_burst_loss_length = 8;
+  optional int64 packet_overhead = 9;
+}
+
+// Schedule describing network parameters in a simulated network.
+message NetworkConfigSchedule {
+  optional int64 repeat_schedule_after_last_ms = 1;
+  // Items should be sorted by time_since_first_sent_packet_ms.
+  repeated NetworkConfigScheduleItem item = 2;
+}
diff --git a/api/test/network_emulation/schedulable_network_node_builder.cc b/api/test/network_emulation/schedulable_network_node_builder.cc
new file mode 100644
index 0000000..fd5a26c
--- /dev/null
+++ b/api/test/network_emulation/schedulable_network_node_builder.cc
@@ -0,0 +1,30 @@
+/*
+ *  Copyright (c) 2024 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 "api/test/network_emulation/schedulable_network_node_builder.h"
+
+#include <memory>
+#include <utility>
+
+#include "api/test/network_emulation/network_config_schedule.pb.h"
+#include "api/test/network_emulation_manager.h"
+#include "test/network/schedulable_network_behavior.h"
+
+namespace webrtc {
+
+SchedulableNetworkNodeBuilder::SchedulableNetworkNodeBuilder(
+    webrtc::NetworkEmulationManager& net,
+    network_behaviour::NetworkConfigSchedule schedule)
+    : net_(net), schedule_(std::move(schedule)) {}
+
+webrtc::EmulatedNetworkNode* SchedulableNetworkNodeBuilder::Build() {
+  return net_.CreateEmulatedNode(std::make_unique<SchedulableNetworkBehavior>(
+      std::move(schedule_), *net_.time_controller()->GetClock()));
+}
+}  // namespace webrtc
diff --git a/api/test/network_emulation/schedulable_network_node_builder.h b/api/test/network_emulation/schedulable_network_node_builder.h
new file mode 100644
index 0000000..5e705c9
--- /dev/null
+++ b/api/test/network_emulation/schedulable_network_node_builder.h
@@ -0,0 +1,33 @@
+/*
+ *  Copyright (c) 2024 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.
+ */
+#ifndef API_TEST_NETWORK_EMULATION_SCHEDULABLE_NETWORK_NODE_BUILDER_H_
+#define API_TEST_NETWORK_EMULATION_SCHEDULABLE_NETWORK_NODE_BUILDER_H_
+
+#include "api/test/network_emulation/network_config_schedule.pb.h"
+#include "api/test/network_emulation_manager.h"
+
+namespace webrtc {
+
+class SchedulableNetworkNodeBuilder {
+ public:
+  SchedulableNetworkNodeBuilder(
+      webrtc::NetworkEmulationManager& net,
+      network_behaviour::NetworkConfigSchedule schedule);
+
+  webrtc::EmulatedNetworkNode* Build();
+
+ private:
+  webrtc::NetworkEmulationManager& net_;
+  network_behaviour::NetworkConfigSchedule schedule_;
+};
+
+}  // namespace webrtc
+
+#endif  // API_TEST_NETWORK_EMULATION_SCHEDULABLE_NETWORK_NODE_BUILDER_H_
diff --git a/test/network/BUILD.gn b/test/network/BUILD.gn
index 10c0c4e..abcd110 100644
--- a/test/network/BUILD.gn
+++ b/test/network/BUILD.gn
@@ -199,6 +199,9 @@
         ":network_emulation_unittest",
         ":simulated_network_unittest",
       ]
+      if (rtc_enable_protobuf) {
+        deps += [ ":schedulable_network_behavior_test" ]
+      }
     }
   }
 }
@@ -241,3 +244,49 @@
     ]
   }
 }
+
+if (rtc_enable_protobuf) {
+  rtc_library("schedulable_network_behavior") {
+    sources = [
+      "schedulable_network_behavior.cc",
+      "schedulable_network_behavior.h",
+    ]
+    deps = [
+      ":simulated_network",
+      "../../api:network_emulation_manager_api",
+      "../../api:sequence_checker",
+      "../../api:simulated_network_api",
+      "../../api/task_queue:task_queue",
+      "../../api/test/network_emulation:network_config_schedule_proto",
+      "../../api/units:data_rate",
+      "../../api/units:time_delta",
+      "../../api/units:timestamp",
+      "../../api/units:timestamp",
+      "../../rtc_base:macromagic",
+      "../../rtc_base/task_utils:repeating_task",
+      "../../system_wrappers",
+    ]
+  }
+
+  if (rtc_include_tests) {
+    rtc_library("schedulable_network_behavior_test") {
+      testonly = true
+      sources = [ "schedulable_network_behavior_test.cc" ]
+      deps = [
+        ":schedulable_network_behavior",
+        "../:test_support",
+        "../../api:create_network_emulation_manager",
+        "../../api:network_emulation_manager_api",
+        "../../api:simulated_network_api",
+        "../../api/test/network_emulation:network_config_schedule_proto",
+        "../../api/units:data_rate",
+        "../../api/units:data_size",
+        "../../api/units:time_delta",
+        "../../api/units:timestamp",
+        "../../system_wrappers",
+        "//testing/gtest",
+        "//third_party/abseil-cpp/absl/algorithm:container",
+      ]
+    }
+  }
+}
diff --git a/test/network/schedulable_network_behavior.cc b/test/network/schedulable_network_behavior.cc
new file mode 100644
index 0000000..32f5305
--- /dev/null
+++ b/test/network/schedulable_network_behavior.cc
@@ -0,0 +1,138 @@
+/*
+ *  Copyright (c) 2024 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 "test/network/schedulable_network_behavior.h"
+
+#include <utility>
+
+#include "api/sequence_checker.h"
+#include "api/task_queue/task_queue_base.h"
+#include "api/test/network_emulation/network_config_schedule.pb.h"
+#include "api/test/simulated_network.h"
+#include "api/units/data_rate.h"
+#include "api/units/time_delta.h"
+#include "api/units/timestamp.h"
+#include "rtc_base/task_utils/repeating_task.h"
+#include "system_wrappers/include/clock.h"
+#include "test/network/simulated_network.h"
+
+namespace webrtc {
+
+namespace {
+
+using ::webrtc::BuiltInNetworkBehaviorConfig;
+
+void UpdateConfigFromSchedule(
+    const network_behaviour::NetworkConfigScheduleItem& schedule_item,
+    BuiltInNetworkBehaviorConfig& config) {
+  if (schedule_item.has_queue_length_packets()) {
+    config.queue_length_packets = schedule_item.queue_length_packets();
+  }
+  if (schedule_item.has_queue_delay_ms()) {
+    config.queue_delay_ms = schedule_item.queue_delay_ms();
+  }
+  if (schedule_item.has_link_capacity_kbps()) {
+    config.link_capacity =
+        DataRate::KilobitsPerSec(schedule_item.link_capacity_kbps());
+  }
+  if (schedule_item.has_loss_percent()) {
+    config.loss_percent = schedule_item.loss_percent();
+  }
+  if (schedule_item.has_delay_standard_deviation_ms()) {
+    config.delay_standard_deviation_ms =
+        schedule_item.delay_standard_deviation_ms();
+  }
+  if (schedule_item.has_allow_reordering()) {
+    config.allow_reordering = schedule_item.allow_reordering();
+  }
+  if (schedule_item.has_avg_burst_loss_length()) {
+    config.avg_burst_loss_length = schedule_item.avg_burst_loss_length();
+  }
+  if (schedule_item.has_packet_overhead()) {
+    config.packet_overhead = schedule_item.packet_overhead();
+  }
+}
+
+BuiltInNetworkBehaviorConfig GetInitialConfig(
+    const network_behaviour::NetworkConfigSchedule& schedule) {
+  BuiltInNetworkBehaviorConfig config;
+  if (!schedule.item().empty()) {
+    UpdateConfigFromSchedule(schedule.item(0), config);
+  }
+  return config;
+}
+
+}  // namespace
+
+SchedulableNetworkBehavior::SchedulableNetworkBehavior(
+    network_behaviour::NetworkConfigSchedule schedule,
+    webrtc::Clock& clock)
+    : SimulatedNetwork(GetInitialConfig(schedule)),
+      schedule_(std::move(schedule)),
+      clock_(clock),
+      config_(GetInitialConfig(schedule_)) {
+  if (schedule_.item().size() > 1) {
+    next_schedule_index_ = 1;
+  }
+  sequence_checker_.Detach();
+}
+
+bool SchedulableNetworkBehavior::EnqueuePacket(
+    webrtc::PacketInFlightInfo packet_info) {
+  RTC_DCHECK_RUN_ON(&sequence_checker_);
+  if (first_send_time_.IsMinusInfinity()) {
+    first_send_time_ = webrtc::Timestamp::Micros(packet_info.send_time_us);
+    if (schedule_.item().size() > 1) {
+      RTC_CHECK_LT(next_schedule_index_, schedule_.item().size());
+      webrtc::TimeDelta delay =
+          webrtc::TimeDelta::Millis(schedule_.item()[next_schedule_index_]
+                                        .time_since_first_sent_packet_ms());
+      schedule_task_ = RepeatingTaskHandle::DelayedStart(
+          webrtc::TaskQueueBase::Current(), delay,
+          [this] { return UpdateConfigAndReschedule(); });
+    }
+  }
+  return SimulatedNetwork::EnqueuePacket(packet_info);
+}
+
+TimeDelta SchedulableNetworkBehavior::UpdateConfigAndReschedule() {
+  RTC_DCHECK_RUN_ON(&sequence_checker_);
+  Timestamp reschedule_time = clock_.CurrentTime();
+  RTC_CHECK_LT(next_schedule_index_, schedule_.item().size());
+
+  auto next_config = schedule_.item()[next_schedule_index_];
+  UpdateConfigFromSchedule(next_config, config_);
+  SimulatedNetwork::SetConfig(config_, reschedule_time);
+  next_schedule_index_ = ++next_schedule_index_ % schedule_.item().size();
+  webrtc::TimeDelta delay = webrtc::TimeDelta::Zero();
+  webrtc::TimeDelta time_since_first_sent_packet =
+      reschedule_time - first_send_time_;
+  if (next_schedule_index_ != 0) {
+    delay = webrtc::TimeDelta::Millis(schedule_.item()[next_schedule_index_]
+                                          .time_since_first_sent_packet_ms()) -
+            (time_since_first_sent_packet - wrap_time_delta_);
+  } else if (!schedule_.has_repeat_schedule_after_last_ms()) {
+    // No more schedule items.
+    schedule_task_.Stop();
+    return TimeDelta::Zero();  // This is ignored.
+  } else {
+    // Wrap around to the first schedule item.
+    wrap_time_delta_ +=
+        TimeDelta::Millis(schedule_.repeat_schedule_after_last_ms()) +
+        TimeDelta::Millis(schedule_.item()[schedule_.item().size() - 1]
+                              .time_since_first_sent_packet_ms());
+    delay =
+        webrtc::TimeDelta::Millis(schedule_.repeat_schedule_after_last_ms());
+    RTC_DCHECK_GE(delay, TimeDelta::Zero());
+  }
+
+  return delay;
+}
+
+}  // namespace webrtc
diff --git a/test/network/schedulable_network_behavior.h b/test/network/schedulable_network_behavior.h
new file mode 100644
index 0000000..1781368
--- /dev/null
+++ b/test/network/schedulable_network_behavior.h
@@ -0,0 +1,56 @@
+/*
+ *  Copyright (c) 2024 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.
+ */
+
+#ifndef TEST_NETWORK_SCHEDULABLE_NETWORK_BEHAVIOR_H_
+#define TEST_NETWORK_SCHEDULABLE_NETWORK_BEHAVIOR_H_
+
+#include "api/sequence_checker.h"
+#include "api/test/network_emulation/network_config_schedule.pb.h"
+#include "api/test/simulated_network.h"
+#include "api/units/time_delta.h"
+#include "api/units/timestamp.h"
+#include "rtc_base/task_utils/repeating_task.h"
+#include "rtc_base/thread_annotations.h"
+#include "system_wrappers/include/clock.h"
+#include "test/network/simulated_network.h"
+
+namespace webrtc {
+
+// Network behaviour implementation where parameters change over time as
+// specified with a schedule proto.
+class SchedulableNetworkBehavior : public SimulatedNetwork {
+ public:
+  SchedulableNetworkBehavior(network_behaviour::NetworkConfigSchedule schedule,
+                             Clock& clock);
+
+  bool EnqueuePacket(PacketInFlightInfo packet_info) override;
+
+ private:
+  TimeDelta UpdateConfigAndReschedule();
+
+  SequenceChecker sequence_checker_;
+  const network_behaviour::NetworkConfigSchedule schedule_;
+  Timestamp first_send_time_ RTC_GUARDED_BY(&sequence_checker_) =
+      Timestamp::MinusInfinity();
+
+  Clock& clock_ RTC_GUARDED_BY(&sequence_checker_);
+  BuiltInNetworkBehaviorConfig config_ RTC_GUARDED_BY(&sequence_checker_);
+  // Index of the next schedule item to apply.
+  int next_schedule_index_ RTC_GUARDED_BY(&sequence_checker_) = 0;
+  // Total time from the first sent packet, until the last time the schedule
+  // repeat.
+  TimeDelta wrap_time_delta_ RTC_GUARDED_BY(&sequence_checker_) =
+      TimeDelta::Zero();
+  RepeatingTaskHandle schedule_task_ RTC_GUARDED_BY(&sequence_checker_);
+};
+
+}  // namespace webrtc
+
+#endif  // TEST_NETWORK_SCHEDULABLE_NETWORK_BEHAVIOR_H_
diff --git a/test/network/schedulable_network_behavior_test.cc b/test/network/schedulable_network_behavior_test.cc
new file mode 100644
index 0000000..054ea69
--- /dev/null
+++ b/test/network/schedulable_network_behavior_test.cc
@@ -0,0 +1,241 @@
+/*
+ *  Copyright (c) 2024 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 "test/network/schedulable_network_behavior.h"
+
+#include <cstdint>
+#include <memory>
+#include <vector>
+
+#include "api/test/create_network_emulation_manager.h"
+#include "api/test/network_emulation_manager.h"
+#include "api/test/simulated_network.h"
+#include "api/units/time_delta.h"
+#include "api/units/timestamp.h"
+#include "system_wrappers/include/clock.h"
+#include "test/gmock.h"
+#include "test/gtest.h"
+
+namespace webrtc {
+namespace {
+
+using ::testing::Mock;
+using ::testing::MockFunction;
+using ::testing::SizeIs;
+
+class SchedulableNetworkBehaviorTestFixture {
+ public:
+  SchedulableNetworkBehaviorTestFixture()
+      : manager_(webrtc::CreateNetworkEmulationManager(
+            {.time_mode = TimeMode::kSimulated})) {}
+
+  webrtc::Clock& clock() const {
+    return *manager_->time_controller()->GetClock();
+  }
+  void AdvanceTime(webrtc::TimeDelta delta) {
+    manager_->time_controller()->AdvanceTime(delta);
+  }
+  void AdvanceTimeTo(int64_t timestamp_us) {
+    TimeDelta delta = Timestamp::Micros(timestamp_us) - TimeNow();
+    ASSERT_GE(delta, TimeDelta::Zero());
+    manager_->time_controller()->AdvanceTime(delta);
+  }
+
+  webrtc::Timestamp TimeNow() const {
+    return manager_->time_controller()->GetClock()->CurrentTime();
+  }
+
+ private:
+  const std::unique_ptr<webrtc::NetworkEmulationManager> manager_;
+};
+
+TEST(SchedulableNetworkBehaviorTest, NoSchedule) {
+  SchedulableNetworkBehaviorTestFixture fixture;
+
+  network_behaviour::NetworkConfigSchedule schedule;
+  SchedulableNetworkBehavior network_behaviour(schedule, fixture.clock());
+  webrtc::Timestamp send_time = fixture.TimeNow();
+  EXPECT_TRUE(network_behaviour.EnqueuePacket({/*size=*/1000 / 8,
+                                               /*send_time_us=*/send_time.us(),
+                                               /*packet_id=*/1}));
+  ASSERT_TRUE(network_behaviour.NextDeliveryTimeUs().has_value());
+  fixture.AdvanceTimeTo(*network_behaviour.NextDeliveryTimeUs());
+  EXPECT_THAT(
+      network_behaviour.DequeueDeliverablePackets(fixture.TimeNow().us()),
+      SizeIs(1));
+}
+
+TEST(SchedulableNetworkBehaviorTest, ScheduleWithoutUpdates) {
+  SchedulableNetworkBehaviorTestFixture fixture;
+
+  network_behaviour::NetworkConfigSchedule schedule;
+  auto initial_config = schedule.add_item();
+  initial_config->set_link_capacity_kbps(10);
+  initial_config->set_queue_delay_ms(70);
+
+  SchedulableNetworkBehavior network_behaviour(schedule, fixture.clock());
+  webrtc::Timestamp send_time = fixture.TimeNow();
+  EXPECT_TRUE(network_behaviour.EnqueuePacket({/*size=*/1000 / 8,
+                                               /*send_time_us=*/send_time.us(),
+                                               /*packet_id=*/1}));
+
+  // 1000 bits, on a 10kbps link should take 100ms +  70 extra.
+  // The network_behaviour at the time of writing this test needs two calls
+  // to NextDeliveryTimeUs to before the packet is delivered (one for the link
+  // capacity queue and one for the queue delay).
+  std::vector<webrtc::PacketDeliveryInfo> packet_delivery_infos;
+  while (packet_delivery_infos.empty()) {
+    ASSERT_TRUE(network_behaviour.NextDeliveryTimeUs().has_value());
+    fixture.AdvanceTimeTo(*network_behaviour.NextDeliveryTimeUs());
+    packet_delivery_infos =
+        network_behaviour.DequeueDeliverablePackets(fixture.TimeNow().us());
+  }
+  EXPECT_EQ(fixture.TimeNow(), send_time + TimeDelta::Millis(170));
+  ASSERT_THAT(packet_delivery_infos, SizeIs(1));
+  EXPECT_EQ(packet_delivery_infos[0].packet_id, 1u);
+  EXPECT_EQ(packet_delivery_infos[0].receive_time_us, send_time.us() + 170'000);
+}
+
+TEST(SchedulableNetworkBehaviorTest,
+     TriggersDeliveryTimeChangedCallbackOnScheduleIfPacketInLinkCapacityQueue) {
+  SchedulableNetworkBehaviorTestFixture fixture;
+  network_behaviour::NetworkConfigSchedule schedule;
+  auto initial_config = schedule.add_item();
+  // A packet of size 1000 bits should take 100ms to send.
+  initial_config->set_link_capacity_kbps(10);
+  initial_config->set_queue_delay_ms(10);
+  auto updated_capacity = schedule.add_item();
+  updated_capacity->set_time_since_first_sent_packet_ms(50);
+  // A packet of size 1000 bits should take 10ms to send. But since "half" the
+  // first packet has passed the narrow section, it should take 50ms + 500/100 =
+  // 55ms.
+  updated_capacity->set_link_capacity_kbps(100);
+
+  SchedulableNetworkBehavior network_behaviour(schedule, fixture.clock());
+  MockFunction<void()> delivery_time_changed_callback;
+  network_behaviour.RegisterDeliveryTimeChangedCallback(
+      delivery_time_changed_callback.AsStdFunction());
+
+  webrtc::Timestamp first_packet_send_time = fixture.TimeNow();
+  EXPECT_CALL(delivery_time_changed_callback, Call).WillOnce([&]() {
+    EXPECT_EQ(fixture.TimeNow(),
+              first_packet_send_time + TimeDelta::Millis(50));
+    ASSERT_TRUE(network_behaviour.NextDeliveryTimeUs().has_value());
+  });
+  EXPECT_TRUE(network_behaviour.EnqueuePacket(
+      {/*size=*/1000 / 8,
+       /*send_time_us=*/first_packet_send_time.us(),
+       /*packet_id=*/1}));
+  fixture.AdvanceTime(
+      TimeDelta::Millis(updated_capacity->time_since_first_sent_packet_ms()));
+  Mock::VerifyAndClearExpectations(&delivery_time_changed_callback);
+  ASSERT_TRUE(network_behaviour.NextDeliveryTimeUs().has_value());
+  fixture.AdvanceTime(
+      TimeDelta::Micros(*network_behaviour.NextDeliveryTimeUs()));
+  std::vector<PacketDeliveryInfo> dequeued_packets =
+      network_behaviour.DequeueDeliverablePackets(fixture.TimeNow().us());
+  ASSERT_FALSE(dequeued_packets.empty());
+  EXPECT_EQ(dequeued_packets[0].receive_time_us,
+            (first_packet_send_time + TimeDelta::Millis(55) +
+             /*queue_delay=*/TimeDelta::Millis(10))
+                .us());
+}
+
+TEST(SchedulableNetworkBehaviorTest, ScheduleWithRepeat) {
+  SchedulableNetworkBehaviorTestFixture fixture;
+  network_behaviour::NetworkConfigSchedule schedule;
+  auto initial_config = schedule.add_item();
+  // A packet of size 1000 bits should take 100ms to send.
+  initial_config->set_link_capacity_kbps(10);
+  auto updated_capacity = schedule.add_item();
+  updated_capacity->set_time_since_first_sent_packet_ms(150);
+  // A packet of size 1000 bits should take 10ms to send.
+  updated_capacity->set_link_capacity_kbps(100);
+  // A packet of size 1000 bits, scheduled 200ms after the last update to the
+  // config should again take 100ms to send.
+  schedule.set_repeat_schedule_after_last_ms(200);
+
+  SchedulableNetworkBehavior network_behaviour(schedule, fixture.clock());
+
+  webrtc::Timestamp first_packet_send_time = fixture.TimeNow();
+  EXPECT_TRUE(network_behaviour.EnqueuePacket(
+      {/*size=*/1000 / 8,
+       /*send_time_us=*/first_packet_send_time.us(),
+       /*packet_id=*/1}));
+  ASSERT_TRUE(network_behaviour.NextDeliveryTimeUs().has_value());
+  EXPECT_EQ(*network_behaviour.NextDeliveryTimeUs(),
+            fixture.TimeNow().us() + TimeDelta::Millis(100).us());
+  fixture.AdvanceTimeTo(*network_behaviour.NextDeliveryTimeUs());
+  EXPECT_THAT(
+      network_behaviour.DequeueDeliverablePackets(fixture.TimeNow().us()),
+      SizeIs(1));
+  fixture.AdvanceTime(
+      TimeDelta::Millis(updated_capacity->time_since_first_sent_packet_ms() +
+                        schedule.repeat_schedule_after_last_ms() -
+                        /*time already advanced*/ 100));
+  // Schedule should be repeated.
+  // A packet of size 1000 bits should take 100ms to send.
+  EXPECT_TRUE(
+      network_behaviour.EnqueuePacket({/*size=*/1000 / 8,
+                                       /*send_time_us=*/fixture.TimeNow().us(),
+                                       /*packet_id=*/2}));
+  ASSERT_TRUE(network_behaviour.NextDeliveryTimeUs().has_value());
+  EXPECT_EQ(*network_behaviour.NextDeliveryTimeUs(),
+            fixture.TimeNow().us() + TimeDelta::Millis(100).us());
+}
+
+TEST(SchedulableNetworkBehaviorTest, ScheduleWithoutRepeat) {
+  SchedulableNetworkBehaviorTestFixture fixture;
+  network_behaviour::NetworkConfigSchedule schedule;
+  auto initial_config = schedule.add_item();
+  // A packet of size 1000 bits should take 100ms to send.
+  initial_config->set_link_capacity_kbps(10);
+  auto updated_capacity = schedule.add_item();
+  updated_capacity->set_time_since_first_sent_packet_ms(150);
+  // A packet of size 1000 bits should take 10ms to send.
+  updated_capacity->set_link_capacity_kbps(100);
+
+  SchedulableNetworkBehavior network_behaviour(schedule, fixture.clock());
+
+  webrtc::Timestamp first_packet_send_time = fixture.TimeNow();
+  EXPECT_TRUE(network_behaviour.EnqueuePacket(
+      {/*size=*/1000 / 8,
+       /*send_time_us=*/first_packet_send_time.us(),
+       /*packet_id=*/1}));
+  ASSERT_TRUE(network_behaviour.NextDeliveryTimeUs().has_value());
+  EXPECT_EQ(*network_behaviour.NextDeliveryTimeUs(),
+            fixture.TimeNow().us() + TimeDelta::Millis(100).us());
+  fixture.AdvanceTimeTo(*network_behaviour.NextDeliveryTimeUs());
+  EXPECT_THAT(
+      network_behaviour.DequeueDeliverablePackets(fixture.TimeNow().us()),
+      SizeIs(1));
+  // Advance time to when the updated capacity should be in effect and add one
+  // minute. The updated capacity should still be in affect.
+  fixture.AdvanceTime(
+      TimeDelta::Millis(updated_capacity->time_since_first_sent_packet_ms() -
+                        /*time already advanced*/ 100) +
+      TimeDelta::Minutes(1));
+
+  // Schedule should not be repeated.
+  // A packet of size 1000 bits should take 10ms to send.
+  EXPECT_TRUE(
+      network_behaviour.EnqueuePacket({/*size=*/1000 / 8,
+                                       /*send_time_us=*/fixture.TimeNow().us(),
+                                       /*packet_id=*/2}));
+  ASSERT_TRUE(network_behaviour.NextDeliveryTimeUs().has_value());
+  EXPECT_EQ(*network_behaviour.NextDeliveryTimeUs(),
+            fixture.TimeNow().us() + TimeDelta::Millis(10).us());
+  fixture.AdvanceTimeTo(*network_behaviour.NextDeliveryTimeUs());
+  EXPECT_THAT(
+      network_behaviour.DequeueDeliverablePackets(fixture.TimeNow().us()),
+      SizeIs(1));
+}
+
+}  // namespace
+}  // namespace webrtc
diff --git a/test/peer_scenario/tests/BUILD.gn b/test/peer_scenario/tests/BUILD.gn
index 5e72e4e..7e457c0 100644
--- a/test/peer_scenario/tests/BUILD.gn
+++ b/test/peer_scenario/tests/BUILD.gn
@@ -19,6 +19,7 @@
     ]
     deps = [
       "..:peer_scenario",
+      "../../:create_frame_generator_capturer",
       "../../:field_trial",
       "../../:test_support",
       "../../../api:rtc_stats_api",
@@ -30,5 +31,11 @@
       "../../../pc:pc_test_utils",
       "../../../pc:session_description",
     ]
+    if (rtc_enable_protobuf) {
+      deps += [
+        "../../../api/test/network_emulation:network_config_schedule_proto",
+        "../../../api/test/network_emulation:schedulable_network_node_builder",
+      ]
+    }
   }
 }
diff --git a/test/peer_scenario/tests/bwe_ramp_up_test.cc b/test/peer_scenario/tests/bwe_ramp_up_test.cc
index 909d989..9879ed5 100644
--- a/test/peer_scenario/tests/bwe_ramp_up_test.cc
+++ b/test/peer_scenario/tests/bwe_ramp_up_test.cc
@@ -8,6 +8,9 @@
  *  be found in the AUTHORS file in the root of the source tree.
  */
 
+#include <atomic>
+#include <utility>
+
 #include "api/stats/rtcstats_objects.h"
 #include "api/units/data_rate.h"
 #include "api/units/time_delta.h"
@@ -16,11 +19,16 @@
 #include "modules/rtp_rtcp/source/rtp_util.h"
 #include "pc/media_session.h"
 #include "pc/test/mock_peer_connection_observers.h"
+#include "test/create_frame_generator_capturer.h"
 #include "test/gmock.h"
 #include "test/gtest.h"
 #include "test/peer_scenario/peer_scenario.h"
 #include "test/peer_scenario/peer_scenario_client.h"
 
+#if WEBRTC_ENABLE_PROTOBUF
+#include "api/test/network_emulation/schedulable_network_node_builder.h"
+#endif
+
 namespace webrtc {
 namespace test {
 
@@ -49,6 +57,55 @@
   return DataRate::BitsPerSec(*stats[0]->available_outgoing_bitrate);
 }
 
+#if WEBRTC_ENABLE_PROTOBUF
+TEST(BweRampupTest, BweRampUpWhenCapacityIncrease) {
+  PeerScenario s(*test_info_);
+
+  PeerScenarioClient* caller = s.CreateClient({});
+  PeerScenarioClient* callee = s.CreateClient({});
+
+  network_behaviour::NetworkConfigSchedule schedule;
+  auto initial_config = schedule.add_item();
+  initial_config->set_link_capacity_kbps(500);
+  auto updated_capacity = schedule.add_item();
+  updated_capacity->set_time_since_first_sent_packet_ms(3000);
+  updated_capacity->set_link_capacity_kbps(3000);
+  SchedulableNetworkNodeBuilder schedulable_builder(*s.net(),
+                                                    std::move(schedule));
+
+  auto caller_node = schedulable_builder.Build();
+  auto callee_node = s.net()->NodeBuilder().capacity_kbps(5000).Build().node;
+  s.net()->CreateRoute(caller->endpoint(), {caller_node}, callee->endpoint());
+  s.net()->CreateRoute(callee->endpoint(), {callee_node}, caller->endpoint());
+
+  FrameGeneratorCapturerConfig::SquaresVideo video_resolution = {
+      .framerate = 30, .width = 1280, .height = 720};
+  PeerScenarioClient::VideoSendTrack track = caller->CreateVideo(
+      "VIDEO", {.generator = {.squares_video = video_resolution}});
+
+  auto signaling =
+      s.ConnectSignaling(caller, callee, {caller_node}, {callee_node});
+
+  signaling.StartIceSignaling();
+
+  std::atomic<bool> offer_exchange_done(false);
+  signaling.NegotiateSdp([&](const SessionDescriptionInterface& answer) {
+    offer_exchange_done = true;
+  });
+  // Wait for SDP negotiation.
+  s.WaitAndProcess(&offer_exchange_done);
+
+  s.ProcessMessages(TimeDelta::Seconds(5));
+  DataRate bwe_before_capacity_increase =
+      GetAvailableSendBitrate(GetStatsAndProcess(s, caller));
+  EXPECT_GT(bwe_before_capacity_increase.kbps(), 300);
+  EXPECT_LT(bwe_before_capacity_increase.kbps(), 650);
+  s.ProcessMessages(TimeDelta::Seconds(15));
+  EXPECT_GT(GetAvailableSendBitrate(GetStatsAndProcess(s, caller)).kbps(),
+            1000);
+}
+#endif  // WEBRTC_ENABLE_PROTOBUF
+
 // Test that caller BWE can rampup even if callee can not demux incoming RTP
 // packets.
 TEST(BweRampupTest, RampUpWithUndemuxableRtpPackets) {