Only raise Windows timer resolution while blocking with timeout (#48379)

John Tur created

Power usage

Release Notes:

- N/A

Change summary

crates/gpui/src/platform.rs                    | 17 +++++++++++++++++
crates/gpui/src/platform/windows/dispatcher.rs | 14 +++++++++++++-
crates/gpui/src/platform/windows/platform.rs   |  6 ------
crates/gpui/src/platform_scheduler.rs          | 10 +++++++++-
4 files changed, 39 insertions(+), 8 deletions(-)

Detailed changes

crates/gpui/src/platform.rs 🔗

@@ -611,6 +611,19 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
 #[doc(hidden)]
 pub type RunnableVariant = Runnable<RunnableMeta>;
 
+#[doc(hidden)]
+pub struct TimerResolutionGuard {
+    cleanup: Option<Box<dyn FnOnce() + Send>>,
+}
+
+impl Drop for TimerResolutionGuard {
+    fn drop(&mut self) {
+        if let Some(cleanup) = self.cleanup.take() {
+            cleanup();
+        }
+    }
+}
+
 /// This type is public so that our test macro can generate and use it, but it should not
 /// be considered part of our public API.
 #[doc(hidden)]
@@ -627,6 +640,10 @@ pub trait PlatformDispatcher: Send + Sync {
         Instant::now()
     }
 
+    fn increase_timer_resolution(&self) -> TimerResolutionGuard {
+        TimerResolutionGuard { cleanup: None }
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     fn as_test(&self) -> Option<&TestDispatcher> {
         None

crates/gpui/src/platform/windows/dispatcher.rs 🔗

@@ -12,6 +12,7 @@ use windows::{
     },
     Win32::{
         Foundation::{LPARAM, WPARAM},
+        Media::{timeBeginPeriod, timeEndPeriod},
         System::Threading::{
             GetCurrentThread, HIGH_PRIORITY_CLASS, SetPriorityClass, SetThreadPriority,
             THREAD_PRIORITY_TIME_CRITICAL,
@@ -22,7 +23,7 @@ use windows::{
 
 use crate::{
     GLOBAL_THREAD_TIMINGS, HWND, PlatformDispatcher, Priority, PriorityQueueSender,
-    RunnableVariant, SafeHwnd, THREAD_TIMINGS, TaskTiming, ThreadTaskTimings,
+    RunnableVariant, SafeHwnd, THREAD_TIMINGS, TaskTiming, ThreadTaskTimings, TimerResolutionGuard,
     WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD, profiler,
 };
 
@@ -193,4 +194,15 @@ impl PlatformDispatcher for WindowsDispatcher {
             f();
         });
     }
+
+    fn increase_timer_resolution(&self) -> TimerResolutionGuard {
+        unsafe {
+            timeBeginPeriod(1);
+        }
+        TimerResolutionGuard {
+            cleanup: Some(Box::new(|| unsafe {
+                timeEndPeriod(1);
+            })),
+        }
+    }
 }

crates/gpui/src/platform/windows/platform.rs 🔗

@@ -20,7 +20,6 @@ use windows::{
     Win32::{
         Foundation::*,
         Graphics::{Direct3D11::ID3D11Device, Gdi::*},
-        Media::{timeBeginPeriod, timeEndPeriod},
         Security::Credentials::*,
         System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*},
         UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*},
@@ -98,10 +97,6 @@ impl WindowsPlatform {
     pub(crate) fn new(headless: bool) -> Result<Self> {
         unsafe {
             OleInitialize(None).context("unable to initialize Windows OLE")?;
-            // Set the system timer resolution to 1ms so that short timeouts
-            // (e.g. in Scheduler::block) are not rounded up to the default
-            // ~15.6ms tick interval.
-            timeBeginPeriod(1);
         }
         let (directx_devices, text_system, direct_write_text_system) = if !headless {
             let devices = DirectXDevices::new().context("Creating DirectX devices")?;
@@ -991,7 +986,6 @@ impl WindowsPlatformInner {
 impl Drop for WindowsPlatform {
     fn drop(&mut self) {
         unsafe {
-            timeEndPeriod(1);
             DestroyWindow(self.handle)
                 .context("Destroying platform window")
                 .log_err();

crates/gpui/src/platform_scheduler.rs 🔗

@@ -56,9 +56,17 @@ impl Scheduler for PlatformScheduler {
         if let Poll::Ready(()) = future.as_mut().poll(&mut cx) {
             return true;
         }
+
+        let park_deadline = |deadline: Instant| {
+            // Timer expirations are only delivered every ~15.6 milliseconds by default on Windows.
+            // We increase the resolution during this wait so that short timeouts stay reasonably short.
+            let _timer_guard = self.dispatcher.increase_timer_resolution();
+            parker.park_deadline(deadline)
+        };
+
         loop {
             match deadline {
-                Some(deadline) if !parker.park_deadline(deadline) && deadline <= Instant::now() => {
+                Some(deadline) if !park_deadline(deadline) && deadline <= Instant::now() => {
                     return false;
                 }
                 Some(_) => (),