Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use nanosleep for non-Windows platforms #6392

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions osu.Framework.Benchmarks/BenchmarkSleep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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);
}
}
}
12 changes: 12 additions & 0 deletions osu.Framework/Platform/INativeSleep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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);
}
}
79 changes: 79 additions & 0 deletions osu.Framework/Platform/Linux/Native/UnixNativeSleep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://man7.org/linux/man-pages/man3/errno.3.html

The error numbers that correspond to each symbolic name vary
across UNIX systems, and even across different architectures on
Linux. Therefore, numeric values are not included as part of the
list of error names below. The perror(3) and strerror(3)
functions can be used to convert these names to corresponding
textual error messages.

We can't really define a numerical constant for this...

Copy link
Contributor Author

@hwsmm hwsmm Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EINTR seems to be 4 on all platforms we support. I need confirmation on Apple devices, though.

I also agree that this is not reliable, but anyway..

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, this is pedantry. As long as it works it's fine to hardcode. If it doesn't work somewhere, then we'll find out eventually.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at the decompiled SDL3 code in SDL3-CS/native and it's always 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()
{
}
}
}
57 changes: 57 additions & 0 deletions osu.Framework/Platform/Windows/Native/WindowsNativeSleep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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);
}
}
}
50 changes: 9 additions & 41 deletions osu.Framework/Timing/ThrottledFrameClock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -32,11 +33,14 @@ public class ThrottledFrameClock : FramedClock, IDisposable
/// </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()
Expand Down Expand Up @@ -91,51 +95,15 @@ private double sleepAndUpdateCurrent(double milliseconds)

TimeSpan timeSpan = TimeSpan.FromMilliseconds(milliseconds);

if (!waitWaitableTimer(timeSpan))
if (nativeSleep?.Sleep(timeSpan) != true)
Thread.Sleep(timeSpan);

return (CurrentTime = SourceTime) - before;
}

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();
}
}
}
Loading