Support high precision in TaskQueueGcd delayed tasks

TaskQueueGcd previously ignored PostDelayedTaskTraits::high_precision
and scheduled all delayed tasks with dispatch_after_f(), which is
subject to OS timer coalescing (up to 100 ms under background policy).

Schedule high-precision delayed tasks using a one-shot
DISPATCH_TIMER_STRICT timer source with leeway capped at 1 ms. Low-
precision tasks continue to use dispatch_after_f().

Bug: webrtc:470337728
Change-Id: Iafd6b1296d6bcd2446c3fade1f624907128e684a
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/510942
Commit-Queue: Tomas Gunnarsson <tommi@webrtc.org>
Reviewed-by: Markus Handell <handellm@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#49042}
diff --git a/api/task_queue/task_queue_test.cc b/api/task_queue/task_queue_test.cc
index ba07e39..98815ab 100644
--- a/api/task_queue/task_queue_test.cc
+++ b/api/task_queue/task_queue_test.cc
@@ -30,7 +30,8 @@
 #include "absl/strings/string_view.h"
 #include "api/make_ref_counted.h"
 #include "api/ref_count.h"
-#if defined(WEBRTC_WIN)
+#if !defined(WEBRTC_CHROMIUM_BUILD) && \
+    (defined(WEBRTC_WIN) || defined(WEBRTC_MAC) || defined(WEBRTC_IOS))
 #include "api/task_queue/default_task_queue_factory.h"
 #endif
 #include "api/task_queue/task_queue_base.h"
@@ -417,6 +418,68 @@
 }
 #endif  // !defined(WEBRTC_CHROMIUM_BUILD) && defined(WEBRTC_WIN)
 
+#if !defined(WEBRTC_CHROMIUM_BUILD) && \
+    (defined(WEBRTC_MAC) || defined(WEBRTC_IOS))
+// Only TaskQueueGcd (the default factory on macOS and iOS) distinguishes
+// PostDelayedHighPrecisionTask() from PostDelayedTask() in standalone WebRTC.
+// Change these to `TEST_P(TaskQueueTest, ...)` using GetParam() when all
+// implementations support high-precision delayed tasks and Priority::kHigh.
+TEST(TaskQueueTest, PostDelayedHighPrecisionZero) {
+  std::unique_ptr<TaskQueueFactory> factory =
+      CreateDefaultTaskQueueFactory(nullptr);
+  Event event;
+  auto queue = CreateTaskQueue(factory, "PostDelayedHighPrecisionZero",
+                               TaskQueueFactory::Priority::kHigh);
+
+  queue->PostDelayedHighPrecisionTask([&event] { event.Set(); },
+                                      TimeDelta::Zero());
+  EXPECT_TRUE(event.Wait(TimeDelta::Seconds(1)));
+}
+
+TEST(TaskQueueTest, PostDelayedHighPrecision) {
+  std::unique_ptr<TaskQueueFactory> factory =
+      CreateDefaultTaskQueueFactory(nullptr);
+  Event event;
+  auto queue = CreateTaskQueue(factory, "PostDelayedHighPrecision",
+                               TaskQueueFactory::Priority::kHigh);
+
+  int64_t start = TimeMillis();
+  int64_t end = 0;
+  queue->PostDelayedHighPrecisionTask(
+      [&event, &queue, &end] {
+        EXPECT_TRUE(queue->IsCurrent());
+        end = TimeMillis();
+        event.Set();
+      },
+      TimeDelta::Millis(100));
+  EXPECT_TRUE(event.Wait(TimeDelta::Seconds(1)));
+  EXPECT_GE(end - start, 90u);
+  EXPECT_NEAR(end - start, 190u, 100u);  // Accept 90-290.
+}
+
+TEST(TaskQueueTest, PostMultipleDelayedHighPrecision) {
+  std::unique_ptr<TaskQueueFactory> factory =
+      CreateDefaultTaskQueueFactory(nullptr);
+  auto queue = CreateTaskQueue(factory, "PostMultipleDelayedHighPrecision",
+                               TaskQueueFactory::Priority::kHigh);
+
+  std::vector<Event> events(100);
+  for (int i = 0; i < 100; ++i) {
+    Event* event = &events[i];
+    queue->PostDelayedHighPrecisionTask(
+        [event, &queue] {
+          EXPECT_TRUE(queue->IsCurrent());
+          event->Set();
+        },
+        TimeDelta::Millis(i));
+  }
+
+  for (Event& e : events)
+    EXPECT_TRUE(e.Wait(TimeDelta::Seconds(1)));
+}
+#endif  // !defined(WEBRTC_CHROMIUM_BUILD) && (defined(WEBRTC_MAC) ||
+        // defined(WEBRTC_IOS))
+
 #ifdef BUILD_EXPERIMENTAL_TASK_QUEUE_COROUTINE_TESTS
 
 struct TaskQueueCoroutine {
diff --git a/rtc_base/BUILD.gn b/rtc_base/BUILD.gn
index a89ec8f..c415f5b 100644
--- a/rtc_base/BUILD.gn
+++ b/rtc_base/BUILD.gn
@@ -2638,6 +2638,7 @@
           ":rtc_event",
           ":rtc_task_queue_gcd",
           "../api/task_queue",
+          "../api/units:time_delta",
           "../test:test_support",
         ]
       }
diff --git a/rtc_base/task_queue_gcd.cc b/rtc_base/task_queue_gcd.cc
index 9e31530..92234f2 100644
--- a/rtc_base/task_queue_gcd.cc
+++ b/rtc_base/task_queue_gcd.cc
@@ -15,18 +15,20 @@
 #include "rtc_base/task_queue_gcd.h"
 
 #include <dispatch/dispatch.h>
-#include <string.h>
 
+#include <algorithm>
+#include <cstdint>
 #include <memory>
 #include <string>
+#include <utility>
 
 #include "absl/functional/any_invocable.h"
 #include "absl/strings/string_view.h"
 #include "api/location.h"
 #include "api/task_queue/task_queue_base.h"
+#include "api/task_queue/task_queue_factory.h"
 #include "api/units/time_delta.h"
 #include "rtc_base/checks.h"
-#include "rtc_base/logging.h"
 #include "rtc_base/system/gcd_helpers.h"
 
 namespace webrtc {
@@ -48,6 +50,13 @@
   }
 }
 
+// Returns the leeway of the strict timer that runs a high precision task. Like
+// for dispatch_after(), the leeway is 10% of the delay, but it's capped at
+// 1 ms.
+uint64_t HighPrecisionLeewayNs(TimeDelta delay) {
+  return std::min(delay / 10, TimeDelta::Millis(1)).ns<uint64_t>();
+}
+
 class TaskQueueGcd final : public TaskQueueBase {
  public:
   TaskQueueGcd(absl::string_view queue_name, int gcd_priority);
@@ -71,10 +80,14 @@
 
     TaskQueueGcd* const queue;
     absl::AnyInvocable<void() &&> task;
+    // Only set for tasks that are run by a strict timer. Released when the
+    // timer fires.
+    dispatch_source_t timer = nullptr;
   };
 
   ~TaskQueueGcd() override;
   static void RunTask(void* task_context);
+  static void RunTimerTask(void* task_context);
   static void SetNotActive(void* task_queue);
   static void DeleteQueue(void* task_queue);
 
@@ -131,8 +144,32 @@
                                        const PostDelayedTaskTraits& traits,
                                        const Location& location) {
   auto* context = new TaskContext(this, std::move(task));
-  dispatch_after_f(dispatch_time(DISPATCH_TIME_NOW, delay.us() * NSEC_PER_USEC),
-                   queue_, context, &RunTask);
+  const dispatch_time_t when =
+      dispatch_time(DISPATCH_TIME_NOW, delay.us() * NSEC_PER_USEC);
+  // dispatch_after() posts tasks without a positive delay right away, so a
+  // timer is only needed for high precision tasks with a positive delay.
+  if (!traits.high_precision || delay <= TimeDelta::Zero()) {
+    dispatch_after_f(when, queue_, context, &RunTask);
+    return;
+  }
+
+  // The OS may coalesce the timers of dispatch_after() with other timers, which
+  // can delay tasks by much more than the timer leeway, up to 100 ms when the
+  // process runs with background policy. A strict timer asks the OS to observe
+  // the leeway instead. Like the timer behind dispatch_after(), the timer
+  // source retains its target queue, `queue_`, until it's released.
+  dispatch_source_t timer = dispatch_source_create(
+      DISPATCH_SOURCE_TYPE_TIMER, 0, DISPATCH_TIMER_STRICT, queue_);
+  RTC_CHECK(timer);
+  context->timer = timer;
+  // The event handler is called with the context that the source has when the
+  // handler is set.
+  dispatch_set_context(timer, context);
+  dispatch_source_set_event_handler_f(timer, &RunTimerTask);
+  dispatch_source_set_timer(timer, when, DISPATCH_TIME_FOREVER,
+                            HighPrecisionLeewayNs(delay));
+  // `context` may be deleted on `queue_` as soon as the timer is activated.
+  dispatch_activate(timer);
 }
 
 // static
@@ -148,6 +185,18 @@
 }
 
 // static
+void TaskQueueGcd::RunTimerTask(void* task_context) {
+  auto* tc = static_cast<TaskContext*>(task_context);
+  // The timer is one-shot. Cancel it so that it never fires again and release
+  // it before running the task. GCD keeps the source alive while this handler
+  // runs.
+  dispatch_source_cancel(tc->timer);
+  dispatch_release(tc->timer);
+  tc->timer = nullptr;
+  RunTask(tc);  // Takes ownership of `tc`.
+}
+
+// static
 void TaskQueueGcd::SetNotActive(void* task_queue) {
   static_cast<TaskQueueGcd*>(task_queue)->is_active_ = false;
 }
diff --git a/rtc_base/task_queue_gcd_unittest.cc b/rtc_base/task_queue_gcd_unittest.cc
index a4efe77..9292e8d 100644
--- a/rtc_base/task_queue_gcd_unittest.cc
+++ b/rtc_base/task_queue_gcd_unittest.cc
@@ -11,10 +11,10 @@
 #include "rtc_base/task_queue_gcd.h"
 
 #include <memory>
-#include <utility>
 
 #include "api/task_queue/task_queue_base.h"
 #include "api/task_queue/task_queue_factory.h"
+#include "api/units/time_delta.h"
 #include "rtc_base/event.h"
 #include "test/gtest.h"
 
@@ -34,5 +34,20 @@
   EXPECT_TRUE(done.Wait(TimeDelta::Seconds(1)));
 }
 
+TEST(TaskQueueGcdTest, DeleteFromHighPrecisionTask) {
+  std::unique_ptr<TaskQueueFactory> factory = CreateTaskQueueGcdFactory();
+  auto queue = factory->CreateTaskQueue("DeleteFromHighPrecisionTask",
+                                        TaskQueueFactory::Priority::kNormal);
+  Event done;
+  queue->PostDelayedHighPrecisionTask(
+      [&queue, &done] {
+        // This will call TaskQueueGcd::Delete() from the current queue.
+        queue.reset();
+        done.Set();
+      },
+      TimeDelta::Millis(1));
+  EXPECT_TRUE(done.Wait(TimeDelta::Seconds(1)));
+}
+
 }  // namespace
 }  // namespace webrtc