Merge pull request #6392 from hwsmm/nanosleep

Use nanosleep for non-Windows platforms
This commit is contained in:
Dan Balasescu
2025-06-08 19:56:47 +09:00
committed by GitHub
5 changed files with 199 additions and 41 deletions

View File

@@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading;
using BenchmarkDotNet.Attributes;
using osu.Framework.Platform;
using osu.Framework.Platform.Linux.Native;
using osu.Framework.Platform.Windows.Native;
namespace osu.Framework.Benchmarks
{
public class BenchmarkSleep : BenchmarkTest
{
private INativeSleep nativeSleep = null!;
private readonly TimeSpan timeSpan = TimeSpan.FromMilliseconds(1.5);
public override void SetUp()
{
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
nativeSleep = new WindowsNativeSleep();
else if (RuntimeInfo.IsUnix && UnixNativeSleep.Available)
nativeSleep = new UnixNativeSleep();
}
[Benchmark]
public void TestThreadSleep()
{
Thread.Sleep(timeSpan);
}
[Benchmark]
public void TestNativeSleep()
{
nativeSleep.Sleep(timeSpan);
}
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
namespace osu.Framework.Platform
{
public interface INativeSleep : IDisposable
{
bool Sleep(TimeSpan duration);
}
}

View File

@@ -0,0 +1,79 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime.InteropServices;
namespace osu.Framework.Platform.Linux.Native
{
internal class UnixNativeSleep : INativeSleep
{
[StructLayout(LayoutKind.Sequential)]
public struct TimeSpec
{
public nint Seconds;
public nint NanoSeconds;
}
[DllImport("libc", CallingConvention = CallingConvention.Cdecl, SetLastError = true)]
private static extern int nanosleep(in TimeSpec duration, out TimeSpec rem);
private const int interrupt_error = 4;
public static bool Available { get; private set; }
// Just a safe check before actually using it.
// .NET tries possible library names if 'libc' is given, but it may fail to find it.
private static bool testNanoSleep()
{
TimeSpec test = new TimeSpec
{
Seconds = 0,
NanoSeconds = 1,
};
try
{
nanosleep(in test, out _);
return true;
}
catch
{
return false;
}
}
static UnixNativeSleep()
{
Available = testNanoSleep();
}
public bool Sleep(TimeSpan duration)
{
const int ns_per_second = 1000 * 1000 * 1000;
long ns = (long)duration.TotalNanoseconds;
TimeSpec timeSpec = new TimeSpec
{
Seconds = (nint)(ns / ns_per_second),
NanoSeconds = (nint)(ns % ns_per_second),
};
int ret;
while ((ret = nanosleep(in timeSpec, out var remaining)) == -1 && Marshal.GetLastPInvokeError() == interrupt_error)
{
// The pause can be interrupted by a signal that was delivered to the thread.
// Sleep again with remaining time if it happened.
timeSpec = remaining;
}
return ret == 0; // Any errors other than interrupt_error should return false.
}
public void Dispose()
{
}
}
}

View File

@@ -0,0 +1,60 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
namespace osu.Framework.Platform.Windows.Native
{
internal class WindowsNativeSleep : INativeSleep
{
private IntPtr waitableTimer;
public WindowsNativeSleep()
{
createWaitableTimer();
}
private void createWaitableTimer()
{
try
{
// Attempt to use CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, only available since Windows 10, version 1803.
waitableTimer = Execution.CreateWaitableTimerEx(IntPtr.Zero, null,
Execution.CreateWaitableTimerFlags.CREATE_WAITABLE_TIMER_MANUAL_RESET | Execution.CreateWaitableTimerFlags.CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, Execution.TIMER_ALL_ACCESS);
if (waitableTimer == IntPtr.Zero)
{
// Fall back to a more supported version. This is still far more accurate than Thread.Sleep.
waitableTimer = Execution.CreateWaitableTimerEx(IntPtr.Zero, null, Execution.CreateWaitableTimerFlags.CREATE_WAITABLE_TIMER_MANUAL_RESET, Execution.TIMER_ALL_ACCESS);
}
}
catch
{
// Any kind of unexpected exception should fall back to Thread.Sleep.
}
}
public bool Sleep(TimeSpan duration)
{
if (waitableTimer == IntPtr.Zero) return false;
// Not sure if we want to fall back to Thread.Sleep on failure here, needs further investigation.
if (Execution.SetWaitableTimerEx(waitableTimer, Execution.CreateFileTime(duration), 0, null, default, IntPtr.Zero, 0))
{
Execution.WaitForSingleObject(waitableTimer, Execution.INFINITE);
return true;
}
return false;
}
public void Dispose()
{
if (waitableTimer != IntPtr.Zero)
{
Execution.CloseHandle(waitableTimer);
waitableTimer = IntPtr.Zero;
}
}
}
}

View File

@@ -3,8 +3,9 @@
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
using osu.Framework.Platform;
using osu.Framework.Platform.Linux.Native;
using osu.Framework.Platform.Windows.Native;
namespace osu.Framework.Timing
@@ -32,11 +33,14 @@ namespace osu.Framework.Timing
/// </summary>
public double TimeSlept { get; private set; }
private IntPtr waitableTimer;
private readonly INativeSleep? nativeSleep;
internal ThrottledFrameClock()
{
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) createWaitableTimer();
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
nativeSleep = new WindowsNativeSleep();
else if (RuntimeInfo.IsUnix && UnixNativeSleep.Available)
nativeSleep = new UnixNativeSleep();
}
public override void ProcessFrame()
@@ -91,7 +95,7 @@ namespace osu.Framework.Timing
TimeSpan timeSpan = TimeSpan.FromMilliseconds(milliseconds);
if (!waitWaitableTimer(timeSpan))
if (nativeSleep?.Sleep(timeSpan) != true)
Thread.Sleep(timeSpan);
return (CurrentTime = SourceTime) - before;
@@ -99,43 +103,7 @@ namespace osu.Framework.Timing
public void Dispose()
{
if (waitableTimer != IntPtr.Zero)
Execution.CloseHandle(waitableTimer);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool waitWaitableTimer(TimeSpan timeSpan)
{
if (waitableTimer == IntPtr.Zero) return false;
// Not sure if we want to fall back to Thread.Sleep on failure here, needs further investigation.
if (Execution.SetWaitableTimerEx(waitableTimer, Execution.CreateFileTime(timeSpan), 0, null, default, IntPtr.Zero, 0))
{
Execution.WaitForSingleObject(waitableTimer, Execution.INFINITE);
return true;
}
return false;
}
private void createWaitableTimer()
{
try
{
// Attempt to use CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, only available since Windows 10, version 1803.
waitableTimer = Execution.CreateWaitableTimerEx(IntPtr.Zero, null,
Execution.CreateWaitableTimerFlags.CREATE_WAITABLE_TIMER_MANUAL_RESET | Execution.CreateWaitableTimerFlags.CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, Execution.TIMER_ALL_ACCESS);
if (waitableTimer == IntPtr.Zero)
{
// Fall back to a more supported version. This is still far more accurate than Thread.Sleep.
waitableTimer = Execution.CreateWaitableTimerEx(IntPtr.Zero, null, Execution.CreateWaitableTimerFlags.CREATE_WAITABLE_TIMER_MANUAL_RESET, Execution.TIMER_ALL_ACCESS);
}
}
catch
{
// Any kind of unexpected exception should fall back to Thread.Sleep.
}
nativeSleep?.Dispose();
}
}
}