Skip to content

Commit f647eeb

Browse files
authored
Merge pull request #6392 from hwsmm/nanosleep
Use nanosleep for non-Windows platforms
2 parents c2fc17b + 662bbad commit f647eeb

File tree

5 files changed

+199
-41
lines changed

5 files changed

+199
-41
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System;
5+
using System.Threading;
6+
using BenchmarkDotNet.Attributes;
7+
using osu.Framework.Platform;
8+
using osu.Framework.Platform.Linux.Native;
9+
using osu.Framework.Platform.Windows.Native;
10+
11+
namespace osu.Framework.Benchmarks
12+
{
13+
public class BenchmarkSleep : BenchmarkTest
14+
{
15+
private INativeSleep nativeSleep = null!;
16+
17+
private readonly TimeSpan timeSpan = TimeSpan.FromMilliseconds(1.5);
18+
19+
public override void SetUp()
20+
{
21+
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
22+
nativeSleep = new WindowsNativeSleep();
23+
else if (RuntimeInfo.IsUnix && UnixNativeSleep.Available)
24+
nativeSleep = new UnixNativeSleep();
25+
}
26+
27+
[Benchmark]
28+
public void TestThreadSleep()
29+
{
30+
Thread.Sleep(timeSpan);
31+
}
32+
33+
[Benchmark]
34+
public void TestNativeSleep()
35+
{
36+
nativeSleep.Sleep(timeSpan);
37+
}
38+
}
39+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System;
5+
6+
namespace osu.Framework.Platform
7+
{
8+
public interface INativeSleep : IDisposable
9+
{
10+
bool Sleep(TimeSpan duration);
11+
}
12+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System;
5+
using System.Runtime.InteropServices;
6+
7+
namespace osu.Framework.Platform.Linux.Native
8+
{
9+
internal class UnixNativeSleep : INativeSleep
10+
{
11+
[StructLayout(LayoutKind.Sequential)]
12+
public struct TimeSpec
13+
{
14+
public nint Seconds;
15+
public nint NanoSeconds;
16+
}
17+
18+
[DllImport("libc", CallingConvention = CallingConvention.Cdecl, SetLastError = true)]
19+
private static extern int nanosleep(in TimeSpec duration, out TimeSpec rem);
20+
21+
private const int interrupt_error = 4;
22+
23+
public static bool Available { get; private set; }
24+
25+
// Just a safe check before actually using it.
26+
// .NET tries possible library names if 'libc' is given, but it may fail to find it.
27+
private static bool testNanoSleep()
28+
{
29+
TimeSpec test = new TimeSpec
30+
{
31+
Seconds = 0,
32+
NanoSeconds = 1,
33+
};
34+
35+
try
36+
{
37+
nanosleep(in test, out _);
38+
return true;
39+
}
40+
catch
41+
{
42+
return false;
43+
}
44+
}
45+
46+
static UnixNativeSleep()
47+
{
48+
Available = testNanoSleep();
49+
}
50+
51+
public bool Sleep(TimeSpan duration)
52+
{
53+
const int ns_per_second = 1000 * 1000 * 1000;
54+
55+
long ns = (long)duration.TotalNanoseconds;
56+
57+
TimeSpec timeSpec = new TimeSpec
58+
{
59+
Seconds = (nint)(ns / ns_per_second),
60+
NanoSeconds = (nint)(ns % ns_per_second),
61+
};
62+
63+
int ret;
64+
65+
while ((ret = nanosleep(in timeSpec, out var remaining)) == -1 && Marshal.GetLastPInvokeError() == interrupt_error)
66+
{
67+
// The pause can be interrupted by a signal that was delivered to the thread.
68+
// Sleep again with remaining time if it happened.
69+
timeSpec = remaining;
70+
}
71+
72+
return ret == 0; // Any errors other than interrupt_error should return false.
73+
}
74+
75+
public void Dispose()
76+
{
77+
}
78+
}
79+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System;
5+
6+
namespace osu.Framework.Platform.Windows.Native
7+
{
8+
internal class WindowsNativeSleep : INativeSleep
9+
{
10+
private IntPtr waitableTimer;
11+
12+
public WindowsNativeSleep()
13+
{
14+
createWaitableTimer();
15+
}
16+
17+
private void createWaitableTimer()
18+
{
19+
try
20+
{
21+
// Attempt to use CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, only available since Windows 10, version 1803.
22+
waitableTimer = Execution.CreateWaitableTimerEx(IntPtr.Zero, null,
23+
Execution.CreateWaitableTimerFlags.CREATE_WAITABLE_TIMER_MANUAL_RESET | Execution.CreateWaitableTimerFlags.CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, Execution.TIMER_ALL_ACCESS);
24+
25+
if (waitableTimer == IntPtr.Zero)
26+
{
27+
// Fall back to a more supported version. This is still far more accurate than Thread.Sleep.
28+
waitableTimer = Execution.CreateWaitableTimerEx(IntPtr.Zero, null, Execution.CreateWaitableTimerFlags.CREATE_WAITABLE_TIMER_MANUAL_RESET, Execution.TIMER_ALL_ACCESS);
29+
}
30+
}
31+
catch
32+
{
33+
// Any kind of unexpected exception should fall back to Thread.Sleep.
34+
}
35+
}
36+
37+
public bool Sleep(TimeSpan duration)
38+
{
39+
if (waitableTimer == IntPtr.Zero) return false;
40+
41+
// Not sure if we want to fall back to Thread.Sleep on failure here, needs further investigation.
42+
if (Execution.SetWaitableTimerEx(waitableTimer, Execution.CreateFileTime(duration), 0, null, default, IntPtr.Zero, 0))
43+
{
44+
Execution.WaitForSingleObject(waitableTimer, Execution.INFINITE);
45+
return true;
46+
}
47+
48+
return false;
49+
}
50+
51+
public void Dispose()
52+
{
53+
if (waitableTimer != IntPtr.Zero)
54+
{
55+
Execution.CloseHandle(waitableTimer);
56+
waitableTimer = IntPtr.Zero;
57+
}
58+
}
59+
}
60+
}

osu.Framework/Timing/ThrottledFrameClock.cs

Lines changed: 9 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33

44
using System;
55
using System.Diagnostics;
6-
using System.Runtime.CompilerServices;
76
using System.Threading;
7+
using osu.Framework.Platform;
8+
using osu.Framework.Platform.Linux.Native;
89
using osu.Framework.Platform.Windows.Native;
910

1011
namespace osu.Framework.Timing
@@ -32,11 +33,14 @@ public class ThrottledFrameClock : FramedClock, IDisposable
3233
/// </summary>
3334
public double TimeSlept { get; private set; }
3435

35-
private IntPtr waitableTimer;
36+
private readonly INativeSleep? nativeSleep;
3637

3738
internal ThrottledFrameClock()
3839
{
39-
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) createWaitableTimer();
40+
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
41+
nativeSleep = new WindowsNativeSleep();
42+
else if (RuntimeInfo.IsUnix && UnixNativeSleep.Available)
43+
nativeSleep = new UnixNativeSleep();
4044
}
4145

4246
public override void ProcessFrame()
@@ -91,51 +95,15 @@ private double sleepAndUpdateCurrent(double milliseconds)
9195

9296
TimeSpan timeSpan = TimeSpan.FromMilliseconds(milliseconds);
9397

94-
if (!waitWaitableTimer(timeSpan))
98+
if (nativeSleep?.Sleep(timeSpan) != true)
9599
Thread.Sleep(timeSpan);
96100

97101
return (CurrentTime = SourceTime) - before;
98102
}
99103

100104
public void Dispose()
101105
{
102-
if (waitableTimer != IntPtr.Zero)
103-
Execution.CloseHandle(waitableTimer);
104-
}
105-
106-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
107-
private bool waitWaitableTimer(TimeSpan timeSpan)
108-
{
109-
if (waitableTimer == IntPtr.Zero) return false;
110-
111-
// Not sure if we want to fall back to Thread.Sleep on failure here, needs further investigation.
112-
if (Execution.SetWaitableTimerEx(waitableTimer, Execution.CreateFileTime(timeSpan), 0, null, default, IntPtr.Zero, 0))
113-
{
114-
Execution.WaitForSingleObject(waitableTimer, Execution.INFINITE);
115-
return true;
116-
}
117-
118-
return false;
119-
}
120-
121-
private void createWaitableTimer()
122-
{
123-
try
124-
{
125-
// Attempt to use CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, only available since Windows 10, version 1803.
126-
waitableTimer = Execution.CreateWaitableTimerEx(IntPtr.Zero, null,
127-
Execution.CreateWaitableTimerFlags.CREATE_WAITABLE_TIMER_MANUAL_RESET | Execution.CreateWaitableTimerFlags.CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, Execution.TIMER_ALL_ACCESS);
128-
129-
if (waitableTimer == IntPtr.Zero)
130-
{
131-
// Fall back to a more supported version. This is still far more accurate than Thread.Sleep.
132-
waitableTimer = Execution.CreateWaitableTimerEx(IntPtr.Zero, null, Execution.CreateWaitableTimerFlags.CREATE_WAITABLE_TIMER_MANUAL_RESET, Execution.TIMER_ALL_ACCESS);
133-
}
134-
}
135-
catch
136-
{
137-
// Any kind of unexpected exception should fall back to Thread.Sleep.
138-
}
106+
nativeSleep?.Dispose();
139107
}
140108
}
141109
}

0 commit comments

Comments
 (0)