diff --git a/Assets/Mirror/Transports/Threaded/ThreadedTransport.cs b/Assets/Mirror/Transports/Threaded/ThreadedTransport.cs index cbf11bd51..deaaadec0 100644 --- a/Assets/Mirror/Transports/Threaded/ThreadedTransport.cs +++ b/Assets/Mirror/Transports/Threaded/ThreadedTransport.cs @@ -5,10 +5,12 @@ // note that ThreadLog.cs is required for Debug.Log from threads to work in builds. using System; using System.Collections.Concurrent; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Net; using System.Threading; using UnityEngine; +using Debug = UnityEngine.Debug; namespace Mirror { @@ -43,6 +45,9 @@ enum ThreadEventType DoClientSend, DoClientDisconnect, + Sleep, + Wake, + DoShutdown } @@ -142,6 +147,10 @@ public abstract class ThreadedTransport : Transport // very large limit to prevent deadlocks. const int MaxProcessingPerTick = 10_000_000; + [Tooltip("Detect device sleep mode and automatically disconnect + hibernate the thread after 'sleepTimeout' seconds.\nFor example: on mobile / VR, we don't want to drain the battery after putting down the device.")] + public bool sleepDetection = true; + public float sleepTimeoutInSeconds = 30; + // communication between main & worker thread ////////////////////////// [MethodImpl(MethodImplOptions.AggressiveInlining)] void EnqueueClientMain( @@ -173,10 +182,19 @@ protected virtual void Awake() { // start the thread. // if main application terminates, this thread needs to terminate too. + EnsureThread(); + } + + // starts the thread if not created or not active yet. + void EnsureThread() + { + if (thread != null && thread.IsAlive) return; + thread = new WorkerThread(ToString()); thread.Tick = ThreadTick; thread.Cleanup = ThreadedShutdown; thread.Start(); + Debug.Log($"ThreadedTransport: started worker thread!"); } protected virtual void OnDestroy() @@ -188,6 +206,8 @@ protected virtual void OnDestroy() } // worker thread /////////////////////////////////////////////////////// + // sleep timeout to automatically end if the device was put to sleep. + Stopwatch sleepTimer = null; // NOT THREAD SAFE: ONLY USE THIS IN WORKER THREAD! void ProcessThreadQueue() { // TODO deadlock protection. worker thread may be to slow to process all. @@ -252,6 +272,28 @@ void ProcessThreadQueue() break; } + // SLEEP //////////////////////////////////////////////// + case ThreadEventType.Sleep: + { + // start the sleep timer if not started yet + if (sleepTimer == null) + { + Debug.Log($"ThreadedTransport: sleep detected, sleeping in {sleepTimeoutInSeconds:F0}s!"); + sleepTimer = Stopwatch.StartNew(); + } + break; + } + case ThreadEventType.Wake: + { + // stop the sleep timer (if any) + if (sleepTimer != null) + { + Debug.Log($"ThreadedTransport: Woke up, interrupting sleep timer!"); + sleepTimer = null; + } + break; + } + // SHUTDOWN //////////////////////////////////////////////// case ThreadEventType.DoShutdown: { @@ -267,6 +309,21 @@ void ProcessThreadQueue() // without needing to throw InterruptExceptions or similar. bool ThreadTick() { + // was the device put to sleep? + if (sleepTimer != null && + sleepTimer.Elapsed.TotalSeconds >= sleepTimeoutInSeconds) + { + Debug.Log("ThreadedTransport: entering sleep mode and stopping/disconnecting."); + ThreadedServerStop(); + ThreadedClientDisconnect(); + sleepTimer = null; + + // if the device was put to sleep, end the thread gracefully. + // all threads must end, otherwise putting down the device would + // slowly drain the battery after a day or more. + return false; + } + // early update the implementation first ThreadedClientEarlyUpdate(); ThreadedServerEarlyUpdate(); @@ -279,7 +336,6 @@ bool ThreadTick() ThreadedServerLateUpdate(); // save some cpu power. - // TODO update interval and sleep extra time would be ideal Thread.Sleep(1); return true; } @@ -456,6 +512,9 @@ public override void ClientConnect(string address) return; } + // start worker thread if not started yet + EnsureThread(); + // enqueue to process in worker thread EnqueueThread(ThreadEventType.DoClientConnect, address, null, null); @@ -473,6 +532,9 @@ public override void ClientConnect(Uri uri) return; } + // start worker thread if not started yet + EnsureThread(); + // enqueue to process in worker thread EnqueueThread(ThreadEventType.DoClientConnect, uri, null, null); @@ -586,6 +648,9 @@ public override void ServerStart() return; } + // start worker thread if not started yet + EnsureThread(); + // enqueue to process in worker thread EnqueueThread(ThreadEventType.DoServerStart, null, null, null); @@ -631,6 +696,30 @@ public override void ServerStop() serverActive = false; } + // sleep /////////////////////////////////////////////////////////////// + // when a device goes to sleep, we must end the worker thread after a while. + // otherwise putting down the device would slowly drain the battery after a day or more. + void OnApplicationPause(bool pauseStatus) + { + Debug.Log($"{GetType()}: OnApplicationPause={pauseStatus}"); + + // is sleep detection feature enabled? + if (!sleepDetection) return; + + // pause thread if application pauses + if (pauseStatus) + { + // enqueue to process in worker thread + EnqueueThread(ThreadEventType.Sleep, null, null, null); + } + // resume thread if application resumes + else + { + // enqueue to process in worker thread + EnqueueThread(ThreadEventType.Wake, null, null, null); + } + } + // shutdown //////////////////////////////////////////////////////////// public override void Shutdown() {