Implement QuicAlarmMultiplexer.

The alarm multiplexer allows all alarms for the connection to use the same underlying platform alarm, and to defer updates for that alarm until all connection alarms are processed.  This reduces the number of update calls to the underlying platform alarm, which can be fairly expensive.

Protected by FLAGS_quic_reloadable_flag_quic_use_alarm_multiplexer.

PiperOrigin-RevId: 690982683
diff --git a/build/source_list.bzl b/build/source_list.bzl
index 61fdd9f..360be8d 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -1270,6 +1270,7 @@
     "quic/core/quic_chaos_protector_test.cc",
     "quic/core/quic_coalesced_packet_test.cc",
     "quic/core/quic_config_test.cc",
+    "quic/core/quic_connection_alarms_test.cc",
     "quic/core/quic_connection_context_test.cc",
     "quic/core/quic_connection_id_manager_test.cc",
     "quic/core/quic_connection_id_test.cc",
diff --git a/build/source_list.gni b/build/source_list.gni
index 3e43b5f..c5b8040 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -1271,6 +1271,7 @@
     "src/quiche/quic/core/quic_chaos_protector_test.cc",
     "src/quiche/quic/core/quic_coalesced_packet_test.cc",
     "src/quiche/quic/core/quic_config_test.cc",
+    "src/quiche/quic/core/quic_connection_alarms_test.cc",
     "src/quiche/quic/core/quic_connection_context_test.cc",
     "src/quiche/quic/core/quic_connection_id_manager_test.cc",
     "src/quiche/quic/core/quic_connection_id_test.cc",
diff --git a/build/source_list.json b/build/source_list.json
index 634dd54..0869e11 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -1270,6 +1270,7 @@
     "quiche/quic/core/quic_chaos_protector_test.cc",
     "quiche/quic/core/quic_coalesced_packet_test.cc",
     "quiche/quic/core/quic_config_test.cc",
+    "quiche/quic/core/quic_connection_alarms_test.cc",
     "quiche/quic/core/quic_connection_context_test.cc",
     "quiche/quic/core/quic_connection_id_manager_test.cc",
     "quiche/quic/core/quic_connection_id_test.cc",
diff --git a/quiche/common/quiche_feature_flags_list.h b/quiche/common/quiche_feature_flags_list.h
index 710809f..13fbc65 100755
--- a/quiche/common/quiche_feature_flags_list.h
+++ b/quiche/common/quiche_feature_flags_list.h
@@ -52,6 +52,7 @@
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_test_peer_addr_change_after_normalize, false, false, "If true, QuicConnection::ProcessValidatedPacket will use normalized address to test peer address changes.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_testonly_default_false, false, false, "A testonly reloadable flag that will always default to false.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_testonly_default_true, true, true, "A testonly reloadable flag that will always default to true.")
+QUICHE_FLAG(bool, quiche_reloadable_flag_quic_use_alarm_multiplexer, false, false, "Manages all of the connection alarms via QuicAlarmMultiplexer.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_use_received_client_addresses_cache, true, true, "If true, use a LRU cache to record client addresses of packets received on server's original address.")
 QUICHE_FLAG(bool, quiche_restart_flag_quic_support_ect1, false, false, "When true, allows sending of QUIC packets marked ECT(1). A different flag (TBD) will actually utilize this capability to send ECT(1).")
 QUICHE_FLAG(bool, quiche_restart_flag_quic_support_flow_label, false, false, "If true, QUIC will support reading and writing IPv6 flow labels.")
diff --git a/quiche/common/quiche_protocol_flags_list.h b/quiche/common/quiche_protocol_flags_list.h
index 16417d5..84cded7 100644
--- a/quiche/common/quiche_protocol_flags_list.h
+++ b/quiche/common/quiche_protocol_flags_list.h
@@ -275,4 +275,9 @@
 QUICHE_PROTOCOL_FLAG(bool, quic_client_allow_invalid_sni_for_test, false,
                      "If true, QUIC client will allow sending invalid SNI to "
                      "the server. TLS only.")
+
+QUICHE_PROTOCOL_FLAG(
+    uint64_t, quic_multiplexer_alarm_granularity_us, 1000,
+    "Alarm update granularity used by the QUICHE multiplexer alarm");
+
 #endif
diff --git a/quiche/quic/core/quic_connection.cc b/quiche/quic/core/quic_connection.cc
index c2f8412..21c7dad 100644
--- a/quiche/quic/core/quic_connection.cc
+++ b/quiche/quic/core/quic_connection.cc
@@ -4713,6 +4713,10 @@
 void QuicConnection::CancelAllAlarms() {
   QUIC_DVLOG(1) << "Cancelling all QuicConnection alarms.";
 
+  // Only active in new multiplexer code.
+  alarms_.CancelAllAlarms();
+
+  // PermanentCancel() is a no-op in multiplexer case.
   ack_alarm().PermanentCancel();
   ping_manager_.Stop();
   retransmission_alarm().PermanentCancel();
@@ -4820,7 +4824,7 @@
 QuicConnection::ScopedPacketFlusher::ScopedPacketFlusher(
     QuicConnection* connection)
     : connection_(connection),
-      flush_and_set_pending_retransmission_alarm_on_delete_(false),
+      active_(false),
       handshake_packet_sent_(connection != nullptr &&
                              connection->handshake_packet_sent_) {
   if (connection_ == nullptr) {
@@ -4828,8 +4832,9 @@
   }
 
   if (!connection_->packet_creator_.PacketFlusherAttached()) {
-    flush_and_set_pending_retransmission_alarm_on_delete_ = true;
+    active_ = true;
     connection->packet_creator_.AttachPacketFlusher();
+    connection_->alarms_.DeferUnderlyingAlarmScheduling();
   }
 }
 
@@ -4838,7 +4843,7 @@
     return;
   }
 
-  if (flush_and_set_pending_retransmission_alarm_on_delete_) {
+  if (active_) {
     const QuicTime ack_timeout =
         connection_->uber_received_packet_manager_.GetEarliestAckTimeout();
     if (ack_timeout.IsInitialized()) {
@@ -4922,8 +4927,10 @@
       connection_->SetRetransmissionAlarm();
       connection_->pending_retransmission_alarm_ = false;
     }
+
+    connection_->alarms_.ResumeUnderlyingAlarmScheduling();
   }
-  QUICHE_DCHECK_EQ(flush_and_set_pending_retransmission_alarm_on_delete_,
+  QUICHE_DCHECK_EQ(active_,
                    !connection_->packet_creator_.PacketFlusherAttached());
 }
 
diff --git a/quiche/quic/core/quic_connection.h b/quiche/quic/core/quic_connection.h
index f21bc78..9f647c8 100644
--- a/quiche/quic/core/quic_connection.h
+++ b/quiche/quic/core/quic_connection.h
@@ -870,7 +870,7 @@
     return default_path_.client_connection_id;
   }
   void set_client_connection_id(QuicConnectionId client_connection_id);
-  const QuicClock* clock() const { return clock_; }
+  const QuicClock* clock() const override { return clock_; }
   QuicRandom* random_generator() const { return random_generator_; }
   QuicByteCount max_packet_length() const;
   void SetMaxPacketLength(QuicByteCount length);
@@ -1015,9 +1015,10 @@
 
    private:
     QuicConnection* connection_;
-    // If true, when this flusher goes out of scope, flush connection and set
-    // retransmission alarm if there is one pending.
-    bool flush_and_set_pending_retransmission_alarm_on_delete_;
+    // If true, when this flusher goes out of scope, flush connection, set
+    // retransmission alarm if there is one pending, and update the platform
+    // alarm associated with the connection.
+    bool active_;
     // Latched connection's handshake_packet_sent_ on creation of this flusher.
     const bool handshake_packet_sent_;
   };
diff --git a/quiche/quic/core/quic_connection_alarms.cc b/quiche/quic/core/quic_connection_alarms.cc
index 347ede0..61dd9ab 100644
--- a/quiche/quic/core/quic_connection_alarms.cc
+++ b/quiche/quic/core/quic_connection_alarms.cc
@@ -4,10 +4,25 @@
 
 #include "quiche/quic/core/quic_connection_alarms.h"
 
+#include <algorithm>
+#include <cstddef>
+#include <cstdlib>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "absl/algorithm/container.h"
+#include "absl/base/nullability.h"
+#include "absl/container/inlined_vector.h"
+#include "absl/strings/str_format.h"
 #include "quiche/quic/core/quic_alarm.h"
 #include "quiche/quic/core/quic_alarm_factory.h"
 #include "quiche/quic/core/quic_connection_context.h"
 #include "quiche/quic/core/quic_one_block_arena.h"
+#include "quiche/quic/core/quic_time.h"
+#include "quiche/quic/core/quic_types.h"
+#include "quiche/quic/platform/api/quic_flags.h"
+#include "quiche/common/platform/api/quiche_bug_tracker.h"
 #include "quiche/common/platform/api/quiche_logging.h"
 
 namespace quic {
@@ -55,9 +70,7 @@
  public:
   using QuicConnectionAlarmDelegate::QuicConnectionAlarmDelegate;
 
-  void OnAlarm() override {
-    connection_->OnSendAlarm();
-  }
+  void OnAlarm() override { connection_->OnSendAlarm(); }
 };
 
 class MtuDiscoveryAlarmDelegate : public QuicConnectionAlarmDelegate {
@@ -137,9 +150,256 @@
   void OnAlarm() override { connection_->OnPingAlarm(); }
 };
 
+class MultiplexerAlarmDelegate : public QuicAlarm::Delegate {
+ public:
+  explicit MultiplexerAlarmDelegate(QuicAlarmMultiplexer* multiplexer)
+      : multiplexer_(multiplexer) {}
+  MultiplexerAlarmDelegate(const QuicConnectionAlarmDelegate&) = delete;
+  MultiplexerAlarmDelegate& operator=(const MultiplexerAlarmDelegate&) = delete;
+
+  QuicConnectionContext* GetConnectionContext() override {
+    return multiplexer_->delegate()->context();
+  }
+
+  void OnAlarm() override { multiplexer_->FireAlarms(); }
+
+ protected:
+  QuicAlarmMultiplexer* multiplexer_;
+};
+
 }  // namespace
 
-QuicConnectionAlarms::QuicConnectionAlarms(
+std::string QuicAlarmSlotName(QuicAlarmSlot slot) {
+  switch (slot) {
+    case QuicAlarmSlot::kAck:
+      return "Ack";
+    case QuicAlarmSlot::kRetransmission:
+      return "Retransmission";
+    case QuicAlarmSlot::kSend:
+      return "Send";
+    case QuicAlarmSlot::kMtuDiscovery:
+      return "MtuDiscovery";
+    case QuicAlarmSlot::kProcessUndecryptablePackets:
+      return "ProcessUndecryptablePackets";
+    case QuicAlarmSlot::kDiscardPreviousOneRttKeys:
+      return "DiscardPreviousOneRttKeys";
+    case QuicAlarmSlot::kDiscardZeroRttDecryptionKeys:
+      return "DiscardZeroRttDecryptionKeys";
+    case QuicAlarmSlot::kMultiPortProbing:
+      return "MultiPortProbing";
+    case QuicAlarmSlot::kIdleNetworkDetector:
+      return "IdleNetworkDetector";
+    case QuicAlarmSlot::kNetworkBlackholeDetector:
+      return "NetworkBlackholeDetector";
+    case QuicAlarmSlot::kPing:
+      return "Ping";
+    case QuicAlarmSlot::kSlotCount:
+      break;
+  }
+  return "[unknown]";
+}
+
+QuicAlarmMultiplexer::QuicAlarmMultiplexer(
+    absl::Nonnull<QuicConnectionAlarmsDelegate*> connection,
+    QuicConnectionArena& arena, QuicAlarmFactory& alarm_factory)
+    : deadlines_({QuicTime::Zero(), QuicTime::Zero(), QuicTime::Zero(),
+                  QuicTime::Zero(), QuicTime::Zero(), QuicTime::Zero(),
+                  QuicTime::Zero(), QuicTime::Zero(), QuicTime::Zero(),
+                  QuicTime::Zero(), QuicTime::Zero()}),
+      now_alarm_(alarm_factory.CreateAlarm(
+          arena.New<MultiplexerAlarmDelegate>(this), &arena)),
+      later_alarm_(alarm_factory.CreateAlarm(
+          arena.New<MultiplexerAlarmDelegate>(this), &arena)),
+      connection_(connection),
+      underlying_alarm_granularity_(QuicTimeDelta::FromMicroseconds(
+          GetQuicFlag(quic_multiplexer_alarm_granularity_us))) {}
+
+void QuicAlarmMultiplexer::Set(QuicAlarmSlot slot, QuicTime new_deadline) {
+  QUICHE_DCHECK(!IsSet(slot));
+  QUICHE_DCHECK(new_deadline.IsInitialized());
+  if (permanently_cancelled_) {
+    QUICHE_BUG(quic_alarm_multiplexer_illegal_set)
+        << "Set called after alarms are permanently cancelled. new_deadline:"
+        << new_deadline;
+    return;
+  }
+  SetDeadlineFor(slot, new_deadline);
+  MaybeRescheduleUnderlyingAlarms();
+}
+
+void QuicAlarmMultiplexer::Update(QuicAlarmSlot slot, QuicTime new_deadline,
+                                  QuicTimeDelta granularity) {
+  if (permanently_cancelled_) {
+    QUICHE_BUG(quic_alarm_multiplexer_illegal_update)
+        << "Update called after alarm is permanently cancelled. new_deadline:"
+        << new_deadline << ", granularity:" << granularity;
+    return;
+  }
+
+  if (!new_deadline.IsInitialized()) {
+    Cancel(slot);
+    return;
+  }
+  if (std::abs((new_deadline - GetDeadline(slot)).ToMicroseconds()) <
+      granularity.ToMicroseconds()) {
+    return;
+  }
+  SetDeadlineFor(slot, new_deadline);
+  MaybeRescheduleUnderlyingAlarms();
+}
+
+void QuicAlarmMultiplexer::DeferUnderlyingAlarmScheduling() {
+  defer_updates_of_underlying_alarms_ = true;
+}
+
+void QuicAlarmMultiplexer::ResumeUnderlyingAlarmScheduling() {
+  QUICHE_DCHECK(defer_updates_of_underlying_alarms_);
+  defer_updates_of_underlying_alarms_ = false;
+  RescheduleUnderlyingAlarms();
+}
+
+void QuicAlarmMultiplexer::FireAlarms() {
+  if (permanently_cancelled_) {
+    QUICHE_BUG(multiplexer_fire_alarms_permanently_cancelled)
+        << "FireAlarms() called when all alarms have been permanently "
+           "cancelled.";
+    return;
+  }
+
+  QuicTime now = connection_->clock()->ApproximateNow();
+
+  // Create a fixed list of alarms that are due.
+  absl::InlinedVector<QuicAlarmSlot, kNumberOfSlots> scheduled;
+  for (size_t slot_number = 0; slot_number < deadlines_.size(); ++slot_number) {
+    if (deadlines_[slot_number].IsInitialized() &&
+        deadlines_[slot_number] <= now) {
+      scheduled.push_back(static_cast<QuicAlarmSlot>(slot_number));
+    }
+  }
+
+  // Execute them in order of scheduled deadlines.
+  absl::c_sort(scheduled, [this](QuicAlarmSlot a, QuicAlarmSlot b) {
+    return GetDeadline(a) < GetDeadline(b);
+  });
+  for (QuicAlarmSlot slot : scheduled) {
+    Fire(slot);
+  }
+  MaybeRescheduleUnderlyingAlarms();
+}
+
+void QuicAlarmMultiplexer::RescheduleUnderlyingAlarms() {
+  if (permanently_cancelled_) {
+    return;
+  }
+
+  QuicTime now = connection_->clock()->ApproximateNow();
+  bool schedule_now = false;
+  QuicTime later_alarm_deadline = QuicTime::Infinite();
+  for (const QuicTime deadline : deadlines_) {
+    if (!deadline.IsInitialized()) {
+      continue;
+    }
+    if (deadline <= now) {
+      schedule_now = true;
+    } else {
+      later_alarm_deadline = std::min(later_alarm_deadline, deadline);
+    }
+  }
+
+  if (schedule_now && !now_alarm_->IsSet()) {
+    now_alarm_->Set(now);
+  }
+  if (!schedule_now && now_alarm_->IsSet()) {
+    now_alarm_->Cancel();
+  }
+
+  if (later_alarm_deadline != QuicTime::Infinite()) {
+    later_alarm_->Update(later_alarm_deadline, underlying_alarm_granularity_);
+  } else {
+    later_alarm_->Cancel();
+  }
+
+  QUICHE_DVLOG(1) << "Rescheduled alarms; now = "
+                  << (schedule_now ? "true" : "false")
+                  << "; later = " << later_alarm_deadline;
+  QUICHE_DVLOG(1) << "Alarms: " << DebugString();
+}
+
+void QuicAlarmMultiplexer::Fire(QuicAlarmSlot slot) {
+  if (!IsSet(slot)) {
+    return;
+  }
+  SetDeadlineFor(slot, QuicTime::Zero());
+
+  switch (slot) {
+    case QuicAlarmSlot::kAck:
+      connection_->OnAckAlarm();
+      return;
+    case QuicAlarmSlot::kRetransmission:
+      connection_->OnRetransmissionAlarm();
+      return;
+    case QuicAlarmSlot::kSend:
+      connection_->OnSendAlarm();
+      return;
+    case QuicAlarmSlot::kMtuDiscovery:
+      connection_->OnMtuDiscoveryAlarm();
+      return;
+    case QuicAlarmSlot::kProcessUndecryptablePackets:
+      connection_->OnProcessUndecryptablePacketsAlarm();
+      return;
+    case QuicAlarmSlot::kDiscardPreviousOneRttKeys:
+      connection_->OnDiscardPreviousOneRttKeysAlarm();
+      return;
+    case QuicAlarmSlot::kDiscardZeroRttDecryptionKeys:
+      connection_->OnDiscardZeroRttDecryptionKeysAlarm();
+      return;
+    case QuicAlarmSlot::kMultiPortProbing:
+      connection_->MaybeProbeMultiPortPath();
+      return;
+    case QuicAlarmSlot::kIdleNetworkDetector:
+      connection_->OnIdleDetectorAlarm();
+      return;
+    case QuicAlarmSlot::kNetworkBlackholeDetector:
+      connection_->OnNetworkBlackholeDetectorAlarm();
+      return;
+    case QuicAlarmSlot::kPing:
+      connection_->OnPingAlarm();
+      return;
+    case QuicAlarmSlot::kSlotCount:
+      break;
+  }
+  QUICHE_NOTREACHED();
+}
+
+std::string QuicAlarmMultiplexer::DebugString() {
+  std::vector<std::pair<QuicTime, QuicAlarmSlot>> scheduled;
+  for (size_t i = 0; i < deadlines_.size(); ++i) {
+    if (deadlines_[i].IsInitialized()) {
+      scheduled.emplace_back(deadlines_[i], static_cast<QuicAlarmSlot>(i));
+    }
+  }
+  absl::c_sort(scheduled);
+
+  QuicTime now = connection_->clock()->Now();
+  std::string result;
+  for (const auto& [deadline, slot] : scheduled) {
+    QuicTimeDelta relative = deadline - now;
+    absl::StrAppendFormat(&result, "        %.1fms --- %s\n",
+                          relative.ToMicroseconds() / 1000.f,
+                          QuicAlarmSlotName(slot));
+  }
+  return result;
+}
+
+void QuicAlarmMultiplexer::CancelAllAlarms() {
+  QUICHE_DVLOG(1) << "Cancelling all QuicConnection alarms.";
+  permanently_cancelled_ = true;
+  deadlines_.fill(QuicTime::Zero());
+  now_alarm_->PermanentCancel();
+  later_alarm_->PermanentCancel();
+}
+
+QuicConnectionAlarmHolder::QuicConnectionAlarmHolder(
     QuicConnectionAlarmsDelegate* delegate, QuicAlarmFactory& alarm_factory,
     QuicConnectionArena& arena)
     : ack_alarm_(alarm_factory.CreateAlarm(
@@ -167,4 +427,14 @@
       ping_alarm_(alarm_factory.CreateAlarm(
           arena.New<PingAlarmDelegate>(delegate), &arena)) {}
 
+QuicConnectionAlarms::QuicConnectionAlarms(
+    QuicConnectionAlarmsDelegate* delegate, QuicAlarmFactory& alarm_factory,
+    QuicConnectionArena& arena)
+    : use_multiplexer_(GetQuicReloadableFlag(quic_use_alarm_multiplexer)) {
+  if (use_multiplexer_) {
+    multiplexer_.emplace(delegate, arena, alarm_factory);
+  } else {
+    holder_.emplace(delegate, alarm_factory, arena);
+  }
+}
 }  // namespace quic
diff --git a/quiche/quic/core/quic_connection_alarms.h b/quiche/quic/core/quic_connection_alarms.h
index 4a40ccf..b3ace88 100644
--- a/quiche/quic/core/quic_connection_alarms.h
+++ b/quiche/quic/core/quic_connection_alarms.h
@@ -5,10 +5,18 @@
 #ifndef QUICHE_QUIC_CORE_QUIC_CONNECTION_ALARMS_H_
 #define QUICHE_QUIC_CORE_QUIC_CONNECTION_ALARMS_H_
 
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <string>
+
 #include "absl/base/nullability.h"
+#include "absl/types/variant.h"
 #include "quiche/quic/core/quic_alarm.h"
 #include "quiche/quic/core/quic_alarm_factory.h"
 #include "quiche/quic/core/quic_arena_scoped_ptr.h"
+#include "quiche/quic/core/quic_clock.h"
 #include "quiche/quic/core/quic_connection_context.h"
 #include "quiche/quic/core/quic_one_block_arena.h"
 #include "quiche/quic/core/quic_time.h"
@@ -33,13 +41,191 @@
   virtual void OnPingAlarm() = 0;
 
   virtual QuicConnectionContext* context() = 0;
+  virtual const QuicClock* clock() const = 0;
 };
 
 namespace test {
+class QuicAlarmMultiplexerPeer;
 class QuicConnectionAlarmsPeer;
-}
+}  // namespace test
 
-class QUICHE_EXPORT QuicConnectionAlarms {
+enum class QuicAlarmSlot : uint8_t {
+  // An alarm that is scheduled when the SentPacketManager requires a delay
+  // before sending packets and fires when the packet may be sent.
+  kSend,
+  // An alarm that fires when an ACK should be sent to the peer.
+  kAck,
+  // An alarm that fires when a packet needs to be retransmitted.
+  kRetransmission,
+  // An alarm that fires when an MTU probe should be sent.
+  kMtuDiscovery,
+  // An alarm that fires to process undecryptable packets when new decryption
+  // keys are available.
+  kProcessUndecryptablePackets,
+  // An alarm that fires to discard keys for the previous key phase some time
+  // after a key update has completed.
+  kDiscardPreviousOneRttKeys,
+  // An alarm that fires to discard 0-RTT decryption keys some time after the
+  // first 1-RTT packet has been decrypted. Only used on server connections with
+  // TLS handshaker.
+  kDiscardZeroRttDecryptionKeys,
+  // An alarm that fires to keep probing the multi-port path.
+  kMultiPortProbing,
+  // An alarm for QuicIdleNetworkDetector.
+  kIdleNetworkDetector,
+  // An alarm for QuicNetworkBlackholeDetection.
+  kNetworkBlackholeDetector,
+  // An alarm for QuicPingManager.
+  kPing,
+
+  // Must be the last element.
+  kSlotCount
+};
+std::string QuicAlarmSlotName(QuicAlarmSlot slot);
+
+// QuicAlarmMultiplexer manages the alarms used by the QuicConnection. Its main
+// purpose is to minimize the cost of scheduling and rescheduling the multiple
+// alarms that QuicConnection has by reducing all of those alarms to just two.
+class QUICHE_EXPORT QuicAlarmMultiplexer {
+ public:
+  // Proxy classes that allow an individual alarm to be accessed via
+  // a QuicAlarm-compatible API.
+  class AlarmProxy {
+   public:
+    AlarmProxy(QuicAlarmMultiplexer* multiplexer, QuicAlarmSlot slot)
+        : multiplexer_(multiplexer), slot_(slot) {}
+
+    bool IsSet() const { return multiplexer_->IsSet(slot_); }
+    QuicTime deadline() const { return multiplexer_->GetDeadline(slot_); }
+    bool IsPermanentlyCancelled() const {
+      return multiplexer_->IsPermanentlyCancelled();
+    }
+
+    void Set(QuicTime new_deadline) { multiplexer_->Set(slot_, new_deadline); }
+    void Update(QuicTime new_deadline, QuicTime::Delta granularity) {
+      multiplexer_->Update(slot_, new_deadline, granularity);
+    }
+    void Cancel() { multiplexer_->Cancel(slot_); }
+
+    void PermanentCancel() {}
+
+   private:
+    friend class ::quic::test::QuicConnectionAlarmsPeer;
+
+    QuicAlarmMultiplexer* multiplexer_;
+    QuicAlarmSlot slot_;
+  };
+
+  // Proxy classes that allow an individual alarm to be accessed via
+  // a QuicAlarm-compatible API.
+  class ConstAlarmProxy {
+   public:
+    ConstAlarmProxy(const QuicAlarmMultiplexer* multiplexer, QuicAlarmSlot slot)
+        : multiplexer_(multiplexer), slot_(slot) {}
+
+    bool IsSet() const { return multiplexer_->IsSet(slot_); }
+    QuicTime deadline() const { return multiplexer_->GetDeadline(slot_); }
+    bool IsPermanentlyCancelled() const {
+      return multiplexer_->IsPermanentlyCancelled();
+    }
+
+   private:
+    const QuicAlarmMultiplexer* multiplexer_;
+    QuicAlarmSlot slot_;
+  };
+
+  static constexpr size_t kNumberOfSlots =
+      static_cast<size_t>(QuicAlarmSlot::kSlotCount);
+
+  QuicAlarmMultiplexer(absl::Nonnull<QuicConnectionAlarmsDelegate*> connection,
+                       QuicConnectionArena& arena,
+                       QuicAlarmFactory& alarm_factory);
+
+  // QuicAlarmMultiplexer is not movable, as it has platform alarms that retain
+  // a long-term pointer to it.
+  QuicAlarmMultiplexer(const QuicAlarmMultiplexer&) = delete;
+  QuicAlarmMultiplexer(QuicAlarmMultiplexer&&) = delete;
+  QuicAlarmMultiplexer& operator=(const QuicAlarmMultiplexer&) = delete;
+  QuicAlarmMultiplexer& operator=(QuicAlarmMultiplexer&&) = delete;
+
+  // Implementation of QuicAlarm methods.
+  void Set(QuicAlarmSlot slot, QuicTime new_deadline);
+  void Update(QuicAlarmSlot slot, QuicTime new_deadline,
+              QuicTimeDelta granularity);
+  void Cancel(QuicAlarmSlot slot) {
+    SetDeadlineFor(slot, QuicTime::Zero());
+    MaybeRescheduleUnderlyingAlarms();
+  }
+  bool IsSet(QuicAlarmSlot slot) const {
+    return GetDeadline(slot).IsInitialized();
+  }
+  bool IsPermanentlyCancelled() const { return permanently_cancelled_; }
+  QuicTime GetDeadline(QuicAlarmSlot slot) const {
+    return deadlines_[static_cast<size_t>(slot)];
+  }
+
+  void CancelAllAlarms();
+
+  // Executes callbacks for all of the alarms that are currently due.
+  void FireAlarms();
+
+  // Methods used by ScopedPacketFlusher to defer updates to the underlying
+  // platform alarm.
+  void DeferUnderlyingAlarmScheduling();
+  void ResumeUnderlyingAlarmScheduling();
+
+  QuicConnectionAlarmsDelegate* delegate() { return connection_; }
+
+  // Outputs a formatted list of active alarms.
+  std::string DebugString();
+
+ private:
+  friend class ::quic::test::QuicConnectionAlarmsPeer;
+  friend class ::quic::test::QuicAlarmMultiplexerPeer;
+
+  void SetDeadlineFor(QuicAlarmSlot slot, QuicTime deadline) {
+    deadlines_[static_cast<size_t>(slot)] = deadline;
+  }
+
+  // Fires an individual alarm if it is set.
+  void Fire(QuicAlarmSlot slot);
+
+  void MaybeRescheduleUnderlyingAlarms() {
+    if (defer_updates_of_underlying_alarms_ || permanently_cancelled_) {
+      return;
+    }
+    RescheduleUnderlyingAlarms();
+  }
+  // Updates the underlying platform alarm.
+  void RescheduleUnderlyingAlarms();
+
+  // Deadlines for all of the alarms that can be placed into the multiplexer,
+  // indexed by the values of QuicAlarmSlot enum.
+  std::array<QuicTime, kNumberOfSlots> deadlines_;
+
+  // Actual alarms provided by the underlying platform. Note that there are two
+  // of them: the first is used for alarms that are scheduled for now or
+  // earlier, and the latter is used for alarms that are scheduled in the
+  // future.  The reason those are split is that QUIC has a lot of alarms that
+  // are only fired immediately, and splitting those allows to avoid having
+  // extra reschedules.
+  QuicArenaScopedPtr<QuicAlarm> now_alarm_;
+  QuicArenaScopedPtr<QuicAlarm> later_alarm_;
+
+  // Underlying connection and individual connection components. Not owned.
+  QuicConnectionAlarmsDelegate* connection_;
+
+  // Latched value of --quic_multiplexer_alarm_granularity_us.
+  QuicTimeDelta underlying_alarm_granularity_;
+
+  // If true, all of the alarms have been permanently cancelled.
+  bool permanently_cancelled_ = false;
+  // If true, the actual underlying alarms won't be rescheduled until
+  // ResumeUnderlyingAlarmScheduling() is called.
+  bool defer_updates_of_underlying_alarms_ = false;
+};
+
+class QUICHE_EXPORT QuicConnectionAlarmHolder {
  public:
   // Provides a QuicAlarm-like interface to an alarm contained within
   // QuicConnectionAlarms.
@@ -65,6 +251,9 @@
 
     absl::Nonnull<QuicAlarm*> alarm_;
   };
+
+  // Provides a QuicAlarm-like interface to an alarm contained within
+  // QuicConnectionAlarms.
   class ConstAlarmProxy {
    public:
     explicit ConstAlarmProxy(const QuicAlarm* alarm) : alarm_(alarm) {}
@@ -81,9 +270,9 @@
     const QuicAlarm* alarm_;
   };
 
-  QuicConnectionAlarms(QuicConnectionAlarmsDelegate* delegate,
-                       QuicAlarmFactory& alarm_factory,
-                       QuicConnectionArena& arena);
+  QuicConnectionAlarmHolder(QuicConnectionAlarmsDelegate* delegate,
+                            QuicAlarmFactory& alarm_factory,
+                            QuicConnectionArena& arena);
 
   AlarmProxy ack_alarm() { return AlarmProxy(ack_alarm_.get()); }
   AlarmProxy retransmission_alarm() {
@@ -177,6 +366,295 @@
   QuicArenaScopedPtr<QuicAlarm> ping_alarm_;
 };
 
+// A class for holding all QuicAlarms belonging to a single connection.
+// Dispatches all calls to either QuicConnectionAlarmHolder of
+// QuicAlarmMultiplexer.
+class QUICHE_EXPORT QuicConnectionAlarms {
+ public:
+  // Wraps a ConstAlarmProxy provided by either QuicConnectionAlarmHolder or
+  // QuicAlarmMultiplexer.
+  class ConstAlarmProxy {
+   public:
+    explicit ConstAlarmProxy(QuicConnectionAlarmHolder::ConstAlarmProxy alarm)
+        : alarm_(alarm) {}
+    explicit ConstAlarmProxy(QuicAlarmMultiplexer::ConstAlarmProxy alarm)
+        : alarm_(alarm) {}
+
+    bool IsSet() const {
+      return absl::visit([](auto& alarm) { return alarm.IsSet(); }, alarm_);
+    }
+    QuicTime deadline() const {
+      return absl::visit([](auto& alarm) { return alarm.deadline(); }, alarm_);
+    }
+    bool IsPermanentlyCancelled() const {
+      return absl::visit(
+          [](auto& alarm) { return alarm.IsPermanentlyCancelled(); }, alarm_);
+    }
+
+   private:
+    friend class ::quic::test::QuicConnectionAlarmsPeer;
+
+    absl::variant<QuicConnectionAlarmHolder::ConstAlarmProxy,
+                  QuicAlarmMultiplexer::ConstAlarmProxy>
+        alarm_;
+  };
+
+  // Wraps an AlarmProxy provided by either QuicConnectionAlarmHolder or
+  // QuicAlarmMultiplexer.
+  class AlarmProxy {
+   public:
+    explicit AlarmProxy(QuicConnectionAlarmHolder::AlarmProxy alarm)
+        : alarm_(alarm) {}
+    explicit AlarmProxy(QuicAlarmMultiplexer::AlarmProxy alarm)
+        : alarm_(alarm) {}
+
+    bool IsSet() const {
+      return absl::visit([](auto& alarm) { return alarm.IsSet(); }, alarm_);
+    }
+    QuicTime deadline() const {
+      return absl::visit([](auto& alarm) { return alarm.deadline(); }, alarm_);
+    }
+    bool IsPermanentlyCancelled() const {
+      return absl::visit(
+          [](auto& alarm) { return alarm.IsPermanentlyCancelled(); }, alarm_);
+    }
+
+    void Set(QuicTime new_deadline) {
+      absl::visit([&](auto& alarm) { alarm.Set(new_deadline); }, alarm_);
+    }
+    void Update(QuicTime new_deadline, QuicTime::Delta granularity) {
+      absl::visit([&](auto& alarm) { alarm.Update(new_deadline, granularity); },
+                  alarm_);
+    }
+    void Cancel() {
+      absl::visit([&](auto& alarm) { alarm.Cancel(); }, alarm_);
+    }
+    void PermanentCancel() {
+      absl::visit([&](auto& alarm) { alarm.PermanentCancel(); }, alarm_);
+    }
+
+   private:
+    friend class ::quic::test::QuicConnectionAlarmsPeer;
+
+    absl::variant<QuicConnectionAlarmHolder::AlarmProxy,
+                  QuicAlarmMultiplexer::AlarmProxy>
+        alarm_;
+  };
+
+  QuicConnectionAlarms(QuicConnectionAlarmsDelegate* delegate,
+                       QuicAlarmFactory& alarm_factory,
+                       QuicConnectionArena& arena);
+
+  AlarmProxy ack_alarm() {
+    if (use_multiplexer_) {
+      return AlarmProxy(QuicAlarmMultiplexer::AlarmProxy(&*multiplexer_,
+                                                         QuicAlarmSlot::kAck));
+    }
+    return AlarmProxy(
+        QuicConnectionAlarmHolder::AlarmProxy(holder_->ack_alarm()));
+  }
+  ConstAlarmProxy ack_alarm() const {
+    if (use_multiplexer_) {
+      return ConstAlarmProxy(QuicAlarmMultiplexer::ConstAlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kAck));
+    }
+    return ConstAlarmProxy(
+        QuicConnectionAlarmHolder::ConstAlarmProxy(holder_->ack_alarm()));
+  }
+
+  AlarmProxy retransmission_alarm() {
+    if (use_multiplexer_) {
+      return AlarmProxy(QuicAlarmMultiplexer::AlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kRetransmission));
+    }
+    return AlarmProxy(
+        QuicConnectionAlarmHolder::AlarmProxy(holder_->retransmission_alarm()));
+  }
+  ConstAlarmProxy retransmission_alarm() const {
+    if (use_multiplexer_) {
+      return ConstAlarmProxy(QuicAlarmMultiplexer::ConstAlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kRetransmission));
+    }
+    return ConstAlarmProxy(QuicConnectionAlarmHolder::ConstAlarmProxy(
+        holder_->retransmission_alarm()));
+  }
+
+  AlarmProxy send_alarm() {
+    if (use_multiplexer_) {
+      return AlarmProxy(QuicAlarmMultiplexer::AlarmProxy(&*multiplexer_,
+                                                         QuicAlarmSlot::kSend));
+    }
+    return AlarmProxy(
+        QuicConnectionAlarmHolder::AlarmProxy(holder_->send_alarm()));
+  }
+  ConstAlarmProxy send_alarm() const {
+    if (use_multiplexer_) {
+      return ConstAlarmProxy(QuicAlarmMultiplexer::ConstAlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kSend));
+    }
+    return ConstAlarmProxy(
+        QuicConnectionAlarmHolder::ConstAlarmProxy(holder_->send_alarm()));
+  }
+
+  AlarmProxy mtu_discovery_alarm() {
+    if (use_multiplexer_) {
+      return AlarmProxy(QuicAlarmMultiplexer::AlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kMtuDiscovery));
+    }
+    return AlarmProxy(
+        QuicConnectionAlarmHolder::AlarmProxy(holder_->mtu_discovery_alarm()));
+  }
+  ConstAlarmProxy mtu_discovery_alarm() const {
+    if (use_multiplexer_) {
+      return ConstAlarmProxy(QuicAlarmMultiplexer::ConstAlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kMtuDiscovery));
+    }
+    return ConstAlarmProxy(QuicConnectionAlarmHolder::ConstAlarmProxy(
+        holder_->mtu_discovery_alarm()));
+  }
+
+  AlarmProxy process_undecryptable_packets_alarm() {
+    if (use_multiplexer_) {
+      return AlarmProxy(QuicAlarmMultiplexer::AlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kProcessUndecryptablePackets));
+    }
+    return AlarmProxy(QuicConnectionAlarmHolder::AlarmProxy(
+        holder_->process_undecryptable_packets_alarm()));
+  }
+  ConstAlarmProxy process_undecryptable_packets_alarm() const {
+    if (use_multiplexer_) {
+      return ConstAlarmProxy(QuicAlarmMultiplexer::ConstAlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kProcessUndecryptablePackets));
+    }
+    return ConstAlarmProxy(QuicConnectionAlarmHolder::ConstAlarmProxy(
+        holder_->process_undecryptable_packets_alarm()));
+  }
+
+  AlarmProxy discard_previous_one_rtt_keys_alarm() {
+    if (use_multiplexer_) {
+      return AlarmProxy(QuicAlarmMultiplexer::AlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kDiscardPreviousOneRttKeys));
+    }
+    return AlarmProxy(QuicConnectionAlarmHolder::AlarmProxy(
+        holder_->discard_previous_one_rtt_keys_alarm()));
+  }
+  ConstAlarmProxy discard_previous_one_rtt_keys_alarm() const {
+    if (use_multiplexer_) {
+      return ConstAlarmProxy(QuicAlarmMultiplexer::ConstAlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kDiscardPreviousOneRttKeys));
+    }
+    return ConstAlarmProxy(QuicConnectionAlarmHolder::ConstAlarmProxy(
+        holder_->discard_previous_one_rtt_keys_alarm()));
+  }
+
+  AlarmProxy discard_zero_rtt_decryption_keys_alarm() {
+    if (use_multiplexer_) {
+      return AlarmProxy(QuicAlarmMultiplexer::AlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kDiscardZeroRttDecryptionKeys));
+    }
+    return AlarmProxy(QuicConnectionAlarmHolder::AlarmProxy(
+        holder_->discard_zero_rtt_decryption_keys_alarm()));
+  }
+  ConstAlarmProxy discard_zero_rtt_decryption_keys_alarm() const {
+    if (use_multiplexer_) {
+      return ConstAlarmProxy(QuicAlarmMultiplexer::ConstAlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kDiscardZeroRttDecryptionKeys));
+    }
+    return ConstAlarmProxy(QuicConnectionAlarmHolder::ConstAlarmProxy(
+        holder_->discard_zero_rtt_decryption_keys_alarm()));
+  }
+
+  AlarmProxy multi_port_probing_alarm() {
+    if (use_multiplexer_) {
+      return AlarmProxy(QuicAlarmMultiplexer::AlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kMultiPortProbing));
+    }
+    return AlarmProxy(QuicConnectionAlarmHolder::AlarmProxy(
+        holder_->multi_port_probing_alarm()));
+  }
+  ConstAlarmProxy multi_port_probing_alarm() const {
+    if (use_multiplexer_) {
+      return ConstAlarmProxy(QuicAlarmMultiplexer::ConstAlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kMultiPortProbing));
+    }
+    return ConstAlarmProxy(QuicConnectionAlarmHolder::ConstAlarmProxy(
+        holder_->multi_port_probing_alarm()));
+  }
+
+  AlarmProxy idle_network_detector_alarm() {
+    if (use_multiplexer_) {
+      return AlarmProxy(QuicAlarmMultiplexer::AlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kIdleNetworkDetector));
+    }
+    return AlarmProxy(QuicConnectionAlarmHolder::AlarmProxy(
+        holder_->idle_network_detector_alarm()));
+  }
+  ConstAlarmProxy idle_network_detector_alarm() const {
+    if (use_multiplexer_) {
+      return ConstAlarmProxy(QuicAlarmMultiplexer::ConstAlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kIdleNetworkDetector));
+    }
+    return ConstAlarmProxy(QuicConnectionAlarmHolder::ConstAlarmProxy(
+        holder_->idle_network_detector_alarm()));
+  }
+
+  AlarmProxy network_blackhole_detector_alarm() {
+    if (use_multiplexer_) {
+      return AlarmProxy(QuicAlarmMultiplexer::AlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kNetworkBlackholeDetector));
+    }
+    return AlarmProxy(QuicConnectionAlarmHolder::AlarmProxy(
+        holder_->network_blackhole_detector_alarm()));
+  }
+  ConstAlarmProxy network_blackhole_detector_alarm() const {
+    if (use_multiplexer_) {
+      return ConstAlarmProxy(QuicAlarmMultiplexer::ConstAlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kNetworkBlackholeDetector));
+    }
+    return ConstAlarmProxy(QuicConnectionAlarmHolder::ConstAlarmProxy(
+        holder_->network_blackhole_detector_alarm()));
+  }
+
+  AlarmProxy ping_alarm() {
+    if (use_multiplexer_) {
+      return AlarmProxy(QuicAlarmMultiplexer::AlarmProxy(&*multiplexer_,
+                                                         QuicAlarmSlot::kPing));
+    }
+    return AlarmProxy(
+        QuicConnectionAlarmHolder::AlarmProxy(holder_->ping_alarm()));
+  }
+  ConstAlarmProxy ping_alarm() const {
+    if (use_multiplexer_) {
+      return ConstAlarmProxy(QuicAlarmMultiplexer::ConstAlarmProxy(
+          &*multiplexer_, QuicAlarmSlot::kPing));
+    }
+    return ConstAlarmProxy(
+        QuicConnectionAlarmHolder::ConstAlarmProxy(holder_->ping_alarm()));
+  }
+
+  void CancelAllAlarms() {
+    if (use_multiplexer_) {
+      multiplexer_->CancelAllAlarms();
+    }
+  }
+
+  void DeferUnderlyingAlarmScheduling() {
+    if (use_multiplexer_) {
+      multiplexer_->DeferUnderlyingAlarmScheduling();
+    }
+  }
+  void ResumeUnderlyingAlarmScheduling() {
+    if (use_multiplexer_) {
+      multiplexer_->ResumeUnderlyingAlarmScheduling();
+    }
+  }
+
+ private:
+  std::optional<QuicConnectionAlarmHolder> holder_;
+  std::optional<QuicAlarmMultiplexer> multiplexer_;
+  const bool use_multiplexer_;
+};
+
 using QuicAlarmProxy = QuicConnectionAlarms::AlarmProxy;
 using QuicConstAlarmProxy = QuicConnectionAlarms::ConstAlarmProxy;
 
diff --git a/quiche/quic/core/quic_connection_alarms_test.cc b/quiche/quic/core/quic_connection_alarms_test.cc
new file mode 100644
index 0000000..0ba8053
--- /dev/null
+++ b/quiche/quic/core/quic_connection_alarms_test.cc
@@ -0,0 +1,288 @@
+// Copyright 2024 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "quiche/quic/core/quic_connection_alarms.h"
+
+#include <string>
+
+#include "quiche/quic/core/quic_one_block_arena.h"
+#include "quiche/quic/core/quic_time.h"
+#include "quiche/quic/test_tools/mock_clock.h"
+#include "quiche/quic/test_tools/mock_quic_connection_alarms.h"
+#include "quiche/quic/test_tools/quic_test_utils.h"
+#include "quiche/common/platform/api/quiche_expect_bug.h"
+#include "quiche/common/platform/api/quiche_test.h"
+
+namespace quic::test {
+
+class QuicAlarmMultiplexerPeer {
+ public:
+  static MockAlarmFactory::TestAlarm* GetNowAlarm(
+      QuicAlarmMultiplexer& multiplexer) {
+    return static_cast<MockAlarmFactory::TestAlarm*>(
+        multiplexer.now_alarm_.get());
+  }
+  static MockAlarmFactory::TestAlarm* GetLaterAlarm(
+      QuicAlarmMultiplexer& multiplexer) {
+    return static_cast<MockAlarmFactory::TestAlarm*>(
+        multiplexer.later_alarm_.get());
+  }
+};
+
+namespace {
+
+using ::testing::HasSubstr;
+using ::testing::Not;
+
+class QuicAlarmMultiplexerTest : public quiche::test::QuicheTest {
+ public:
+  QuicAlarmMultiplexerTest()
+      : clock_(delegate_.clock()),
+        multiplexer_(&delegate_, arena_, alarm_factory_),
+        now_alarm_(QuicAlarmMultiplexerPeer::GetNowAlarm(multiplexer_)),
+        later_alarm_(QuicAlarmMultiplexerPeer::GetLaterAlarm(multiplexer_)) {
+    clock_->AdvanceTime(QuicTimeDelta::FromSeconds(1234));
+  }
+
+ protected:
+  MockConnectionAlarmsDelegate delegate_;
+  MockClock* clock_;
+  QuicConnectionArena arena_;
+  MockAlarmFactory alarm_factory_;
+  QuicAlarmMultiplexer multiplexer_;
+
+  MockAlarmFactory::TestAlarm* now_alarm_;
+  MockAlarmFactory::TestAlarm* later_alarm_;
+};
+
+TEST_F(QuicAlarmMultiplexerTest, SetUpdateCancel) {
+  EXPECT_FALSE(multiplexer_.IsSet(QuicAlarmSlot::kSend));
+  EXPECT_FALSE(multiplexer_.IsPermanentlyCancelled());
+  EXPECT_EQ(multiplexer_.GetDeadline(QuicAlarmSlot::kSend), QuicTime::Zero());
+
+  const QuicTime time1 = clock_->Now();
+  const QuicTime time2 = time1 + QuicTimeDelta::FromMilliseconds(10);
+
+  multiplexer_.Set(QuicAlarmSlot::kSend, time1);
+  EXPECT_TRUE(multiplexer_.IsSet(QuicAlarmSlot::kSend));
+  EXPECT_EQ(multiplexer_.GetDeadline(QuicAlarmSlot::kSend), time1);
+
+  multiplexer_.Update(QuicAlarmSlot::kSend, time2, QuicTimeDelta::Zero());
+  EXPECT_TRUE(multiplexer_.IsSet(QuicAlarmSlot::kSend));
+  EXPECT_EQ(multiplexer_.GetDeadline(QuicAlarmSlot::kSend), time2);
+
+  multiplexer_.Cancel(QuicAlarmSlot::kSend);
+  EXPECT_FALSE(multiplexer_.IsSet(QuicAlarmSlot::kSend));
+  EXPECT_FALSE(multiplexer_.IsPermanentlyCancelled());
+  EXPECT_EQ(multiplexer_.GetDeadline(QuicAlarmSlot::kSend), QuicTime::Zero());
+
+  // Test set-via-update.
+  multiplexer_.Update(QuicAlarmSlot::kSend, time1, QuicTimeDelta::Zero());
+  EXPECT_TRUE(multiplexer_.IsSet(QuicAlarmSlot::kSend));
+  EXPECT_EQ(multiplexer_.GetDeadline(QuicAlarmSlot::kSend), time1);
+
+  // Test granularity.
+  multiplexer_.Update(QuicAlarmSlot::kSend, time2,
+                      QuicTimeDelta::FromSeconds(1000));
+  EXPECT_TRUE(multiplexer_.IsSet(QuicAlarmSlot::kSend));
+  EXPECT_EQ(multiplexer_.GetDeadline(QuicAlarmSlot::kSend), time1);
+
+  // Test cancel-via-update.
+  multiplexer_.Update(QuicAlarmSlot::kSend, QuicTime::Zero(),
+                      QuicTimeDelta::Zero());
+  EXPECT_FALSE(multiplexer_.IsSet(QuicAlarmSlot::kSend));
+}
+
+TEST_F(QuicAlarmMultiplexerTest, PermanentlyCancel) {
+  const QuicTime time = clock_->Now();
+
+  multiplexer_.Set(QuicAlarmSlot::kSend, time);
+  EXPECT_TRUE(multiplexer_.IsSet(QuicAlarmSlot::kSend));
+  EXPECT_FALSE(multiplexer_.IsPermanentlyCancelled());
+  EXPECT_TRUE(now_alarm_->IsSet());
+
+  multiplexer_.CancelAllAlarms();
+  EXPECT_FALSE(multiplexer_.IsSet(QuicAlarmSlot::kSend));
+  EXPECT_TRUE(multiplexer_.IsPermanentlyCancelled());
+  EXPECT_FALSE(now_alarm_->IsSet());
+  EXPECT_TRUE(now_alarm_->IsPermanentlyCancelled());
+
+  EXPECT_QUICHE_BUG(multiplexer_.Set(QuicAlarmSlot::kSend, time),
+                    "permanently cancelled");
+  EXPECT_QUICHE_BUG(
+      multiplexer_.Update(QuicAlarmSlot::kSend, time, QuicTimeDelta::Zero()),
+      "permanently cancelled");
+}
+
+TEST_F(QuicAlarmMultiplexerTest, SingleAlarmScheduledForNow) {
+  multiplexer_.Set(QuicAlarmSlot::kMtuDiscovery, clock_->Now());
+  EXPECT_EQ(now_alarm_->deadline(), clock_->Now());
+  EXPECT_FALSE(later_alarm_->IsSet());
+}
+
+TEST_F(QuicAlarmMultiplexerTest, SingleAlarmScheduledForPast) {
+  multiplexer_.Set(QuicAlarmSlot::kMtuDiscovery,
+                   clock_->Now() - QuicTimeDelta::FromMilliseconds(100));
+  EXPECT_EQ(now_alarm_->deadline(), clock_->Now());
+  EXPECT_FALSE(later_alarm_->IsSet());
+}
+
+TEST_F(QuicAlarmMultiplexerTest, SingleAlarmScheduledForFuture) {
+  multiplexer_.Set(QuicAlarmSlot::kMtuDiscovery,
+                   clock_->Now() + QuicTimeDelta::FromMilliseconds(100));
+  EXPECT_FALSE(now_alarm_->IsSet());
+  EXPECT_EQ(later_alarm_->deadline(),
+            clock_->Now() + QuicTimeDelta::FromMilliseconds(100));
+}
+
+TEST_F(QuicAlarmMultiplexerTest, MultipleAlarmsNowAndFuture) {
+  multiplexer_.Set(QuicAlarmSlot::kMtuDiscovery, clock_->Now());
+  multiplexer_.Set(QuicAlarmSlot::kAck,
+                   clock_->Now() + QuicTimeDelta::FromMilliseconds(100));
+  EXPECT_TRUE(now_alarm_->IsSet());
+  EXPECT_EQ(later_alarm_->deadline(),
+            clock_->Now() + QuicTimeDelta::FromMilliseconds(100));
+}
+
+TEST_F(QuicAlarmMultiplexerTest, FireSingleAlarmNow) {
+  multiplexer_.Set(QuicAlarmSlot::kPing, clock_->Now());
+  ASSERT_TRUE(now_alarm_->IsSet());
+  EXPECT_CALL(delegate_, OnPingAlarm());
+  now_alarm_->Fire();
+  EXPECT_FALSE(multiplexer_.IsSet(QuicAlarmSlot::kPing));
+  EXPECT_FALSE(now_alarm_->IsSet());
+}
+
+TEST_F(QuicAlarmMultiplexerTest, FireSingleAlarmFuture) {
+  const QuicTime start = clock_->Now();
+  const QuicTime end = start + QuicTimeDelta::FromMilliseconds(100);
+  multiplexer_.Set(QuicAlarmSlot::kPing, end);
+  ASSERT_TRUE(later_alarm_->IsSet());
+
+  // Ensure that even if we fire the platform alarm prematurely, this works
+  // correctly.
+  EXPECT_CALL(delegate_, OnPingAlarm()).Times(0);
+  later_alarm_->Fire();
+  EXPECT_TRUE(multiplexer_.IsSet(QuicAlarmSlot::kPing));
+  EXPECT_TRUE(later_alarm_->IsSet());
+
+  clock_->AdvanceTime(end - start);
+  ASSERT_EQ(later_alarm_->deadline(), end);
+  EXPECT_CALL(delegate_, OnPingAlarm()).Times(1);
+  later_alarm_->Fire();
+  EXPECT_FALSE(multiplexer_.IsSet(QuicAlarmSlot::kPing));
+  EXPECT_FALSE(now_alarm_->IsSet());
+  EXPECT_FALSE(later_alarm_->IsSet());
+}
+
+TEST_F(QuicAlarmMultiplexerTest, AlarmReschedulesItself) {
+  multiplexer_.Set(QuicAlarmSlot::kPing, clock_->Now());
+  ASSERT_TRUE(now_alarm_->IsSet());
+  EXPECT_CALL(delegate_, OnPingAlarm()).Times(1).WillRepeatedly([&] {
+    multiplexer_.Set(QuicAlarmSlot::kPing, clock_->Now());
+  });
+  now_alarm_->Fire();
+  EXPECT_TRUE(multiplexer_.IsSet(QuicAlarmSlot::kPing));
+}
+
+TEST_F(QuicAlarmMultiplexerTest, FireMultipleAlarmsNow) {
+  multiplexer_.Set(QuicAlarmSlot::kPing, clock_->Now());
+  multiplexer_.Set(QuicAlarmSlot::kRetransmission, clock_->Now());
+  ASSERT_TRUE(now_alarm_->IsSet());
+  EXPECT_CALL(delegate_, OnPingAlarm());
+  EXPECT_CALL(delegate_, OnRetransmissionAlarm());
+  now_alarm_->Fire();
+}
+
+TEST_F(QuicAlarmMultiplexerTest, FireMultipleAlarmsLater) {
+  QuicTimeDelta delay = QuicTimeDelta::FromMilliseconds(10);
+  multiplexer_.Set(QuicAlarmSlot::kPing, clock_->Now() + delay);
+  multiplexer_.Set(QuicAlarmSlot::kRetransmission, clock_->Now() + delay);
+  ASSERT_TRUE(later_alarm_->IsSet());
+
+  later_alarm_->Fire();
+  ASSERT_TRUE(later_alarm_->IsSet());
+
+  clock_->AdvanceTime(delay);
+  EXPECT_CALL(delegate_, OnPingAlarm());
+  EXPECT_CALL(delegate_, OnRetransmissionAlarm());
+  later_alarm_->Fire();
+}
+
+TEST_F(QuicAlarmMultiplexerTest, FireMultipleAlarmsLaterDifferentDelays) {
+  QuicTimeDelta delay = QuicTimeDelta::FromMilliseconds(10);
+  multiplexer_.Set(QuicAlarmSlot::kPing, clock_->Now() + delay);
+  multiplexer_.Set(QuicAlarmSlot::kRetransmission, clock_->Now() + 2 * delay);
+  ASSERT_TRUE(later_alarm_->IsSet());
+
+  EXPECT_CALL(delegate_, OnPingAlarm()).Times(0);
+  EXPECT_CALL(delegate_, OnRetransmissionAlarm()).Times(0);
+  later_alarm_->Fire();
+  ASSERT_TRUE(later_alarm_->IsSet());
+
+  clock_->AdvanceTime(delay);
+  EXPECT_CALL(delegate_, OnPingAlarm()).Times(1);
+  EXPECT_CALL(delegate_, OnRetransmissionAlarm()).Times(0);
+  later_alarm_->Fire();
+  ASSERT_TRUE(later_alarm_->IsSet());
+
+  clock_->AdvanceTime(delay);
+  EXPECT_CALL(delegate_, OnPingAlarm()).Times(0);
+  EXPECT_CALL(delegate_, OnRetransmissionAlarm()).Times(1);
+  later_alarm_->Fire();
+  EXPECT_FALSE(later_alarm_->IsSet());
+}
+
+TEST_F(QuicAlarmMultiplexerTest, FireMultipleAlarmsLaterDifferentDelaysAtOnce) {
+  QuicTimeDelta delay = QuicTimeDelta::FromMilliseconds(10);
+  multiplexer_.Set(QuicAlarmSlot::kMtuDiscovery, clock_->Now() + delay);
+  multiplexer_.Set(QuicAlarmSlot::kAck, clock_->Now() + 2 * delay);
+  ASSERT_TRUE(later_alarm_->IsSet());
+
+  clock_->AdvanceTime(2 * delay);
+  testing::Sequence seq;
+  EXPECT_CALL(delegate_, OnMtuDiscoveryAlarm()).InSequence(seq);
+  EXPECT_CALL(delegate_, OnAckAlarm()).InSequence(seq);
+  later_alarm_->Fire();
+  EXPECT_FALSE(later_alarm_->IsSet());
+}
+
+TEST_F(QuicAlarmMultiplexerTest, DeferUpdates) {
+  QuicTimeDelta delay = QuicTimeDelta::FromMilliseconds(10);
+  multiplexer_.DeferUnderlyingAlarmScheduling();
+  multiplexer_.Set(QuicAlarmSlot::kMtuDiscovery, clock_->Now());
+  multiplexer_.Set(QuicAlarmSlot::kAck, clock_->Now() + delay);
+  EXPECT_FALSE(now_alarm_->IsSet());
+  EXPECT_FALSE(later_alarm_->IsSet());
+  multiplexer_.ResumeUnderlyingAlarmScheduling();
+  EXPECT_TRUE(now_alarm_->IsSet());
+  EXPECT_TRUE(later_alarm_->IsSet());
+}
+
+TEST_F(QuicAlarmMultiplexerTest, DeferUpdatesAlreadySet) {
+  QuicTime deadline1 = clock_->Now() + QuicTimeDelta::FromMilliseconds(50);
+  QuicTime deadline2 = clock_->Now() + QuicTimeDelta::FromMilliseconds(10);
+  multiplexer_.Set(QuicAlarmSlot::kAck, deadline1);
+  EXPECT_EQ(later_alarm_->deadline(), deadline1);
+
+  multiplexer_.DeferUnderlyingAlarmScheduling();
+  multiplexer_.Set(QuicAlarmSlot::kSend, deadline2);
+  EXPECT_EQ(later_alarm_->deadline(), deadline1);
+
+  multiplexer_.ResumeUnderlyingAlarmScheduling();
+  EXPECT_EQ(later_alarm_->deadline(), deadline2);
+}
+
+TEST_F(QuicAlarmMultiplexerTest, DebugString) {
+  multiplexer_.Set(QuicAlarmSlot::kMtuDiscovery, clock_->Now());
+  multiplexer_.Set(QuicAlarmSlot::kPing,
+                   clock_->Now() + QuicTimeDelta::FromMilliseconds(123));
+  std::string debug_view = multiplexer_.DebugString();
+  EXPECT_THAT(debug_view, HasSubstr("MtuDiscovery"));
+  EXPECT_THAT(debug_view, HasSubstr("Ping"));
+  EXPECT_THAT(debug_view, Not(HasSubstr("Ack")));
+}
+
+}  // namespace
+}  // namespace quic::test
diff --git a/quiche/quic/test_tools/mock_quic_connection_alarms.h b/quiche/quic/test_tools/mock_quic_connection_alarms.h
index 702cb8e..8c29ee7 100644
--- a/quiche/quic/test_tools/mock_quic_connection_alarms.h
+++ b/quiche/quic/test_tools/mock_quic_connection_alarms.h
@@ -5,8 +5,11 @@
 #ifndef QUICHE_QUIC_TEST_TOOLS_MOCK_QUIC_CONNECTION_ALARMS_H_
 #define QUICHE_QUIC_TEST_TOOLS_MOCK_QUIC_CONNECTION_ALARMS_H_
 
+#include "quiche/quic/core/quic_clock.h"
 #include "quiche/quic/core/quic_connection_alarms.h"
-#include "quiche/quic/platform/api/quic_test.h"
+#include "quiche/quic/core/quic_connection_context.h"
+#include "quiche/quic/test_tools/mock_clock.h"
+#include "quiche/common/platform/api/quiche_test.h"
 
 namespace quic::test {
 
@@ -25,6 +28,11 @@
   MOCK_METHOD(void, OnPingAlarm, (), (override));
 
   QuicConnectionContext* context() override { return nullptr; }
+  const QuicClock* clock() const override { return &clock_; }
+  MockClock* clock() { return &clock_; }
+
+ private:
+  MockClock clock_;
 };
 
 }  // namespace quic::test
diff --git a/quiche/quic/test_tools/quic_connection_peer.cc b/quiche/quic/test_tools/quic_connection_peer.cc
index 8b728ef..7037c92 100644
--- a/quiche/quic/test_tools/quic_connection_peer.cc
+++ b/quiche/quic/test_tools/quic_connection_peer.cc
@@ -9,6 +9,7 @@
 
 
 #include "absl/strings/string_view.h"
+#include "absl/types/variant.h"
 #include "quiche/quic/core/congestion_control/send_algorithm_interface.h"
 #include "quiche/quic/core/quic_connection_alarms.h"
 #include "quiche/quic/core/quic_packet_writer.h"
@@ -25,8 +26,16 @@
 
 // static
 void QuicConnectionAlarmsPeer::Fire(QuicAlarmProxy alarm) {
-  auto* real_alarm = static_cast<TestAlarmFactory::TestAlarm*>(alarm.alarm_);
-  real_alarm->Fire();
+  struct {
+    void operator()(QuicConnectionAlarmHolder::AlarmProxy alarm) {
+      auto* real_alarm = static_cast<TestAlarmFactory::TestAlarm*>(alarm.alarm_);
+      real_alarm->Fire();
+    }
+    void operator()(QuicAlarmMultiplexer::AlarmProxy alarm) {
+      alarm.multiplexer_->Fire(alarm.slot_);
+    }
+  } visitor;
+  absl::visit(visitor, alarm.alarm_);
 }
 
 // static