Files

272 lines
9.5 KiB
C#

using System.Diagnostics;
using System.Threading;
using UnityEditor;
using Codice.LogWrapper;
using PlasticGui;
using PlasticPipe.Client;
using Unity.PlasticSCM.Editor.UI;
namespace Unity.PlasticSCM.Editor
{
internal static class WaitForPendingOperations
{
internal static void ClearProgressBarAndEnterPlayModeIfNeeded()
{
// If play mode is pending (we were waiting for operations when domain reload hit),
// clear the progress bar and resume entering play mode.
if (!SessionState.GetBool(PLAY_MODE_PENDING_KEY, false))
return;
mLog.Debug("Clear progress bar and enter play mode after domain reload");
// Wait for editor to be ready to clear the progress bar and enter play mode
Execute.WhenEditorIsReady(ClearProgressBarAndEnterPlayMode);
}
internal static void BeforeAssemblyReload()
{
mLog.Debug("BeforeAssemblyReload started");
CancelPlayModeWaitIfNeeded();
// If we already timed out waiting for play mode, skip the wait loop entirely,
// to avoid waiting and showing a progress bar again.
if (mPlayModeWaitTimeoutReached)
{
mPlayModeWaitTimeoutReached = false;
return;
}
mLastLoggedSecond = -1;
Stopwatch stopwatch = Stopwatch.StartNew();
bool progressBarShown = false;
try
{
while (true)
{
// Tick manually the EditorDispatcher to allow UVCS operations
// to report completion in the main thread through the afterOperationDelegate.
EditorDispatcher.Update();
if (!HasRunningOperations())
{
mLog.DebugFormat(
"All operations finished after {0}ms",
stopwatch.ElapsedMilliseconds);
break;
}
bool hasLongRunningOperations = HasLongRunningOperations();
// Always wait for UVCS operations without timeout.
// Only apply timeout to ThreadWaiters and InUseConnections.
bool timeoutReached = stopwatch.ElapsedMilliseconds >= MAX_WAIT_TIMEOUT_MS;
if (timeoutReached && !hasLongRunningOperations)
{
LogTimeoutReached(stopwatch.ElapsedMilliseconds, "Forcing shutdown.");
break;
}
// Show progress bar after the delay to avoid flicker for quick operations
if (!progressBarShown && stopwatch.ElapsedMilliseconds >= PROGRESS_BAR_DELAY_MS)
{
DisplayProgressBar();
progressBarShown = true;
}
LogWaitingForOperations(stopwatch.ElapsedMilliseconds);
Thread.Sleep(POLL_INTERVAL_MS);
}
}
finally
{
if (progressBarShown)
{
EditorUtility.ClearProgressBar();
}
mLog.DebugFormat(
"WaitForPendingOperations completed in {0}ms",
stopwatch.ElapsedMilliseconds);
}
}
internal static void OnPlayModeStateChanged(PlayModeStateChange change)
{
if (change != PlayModeStateChange.ExitingEditMode)
return;
bool isDomainReloadDisabled = EditorSettings.enterPlayModeOptionsEnabled &&
(EditorSettings.enterPlayModeOptions &
EnterPlayModeOptions.DisableDomainReload) != 0;
if (isDomainReloadDisabled)
return;
if (!HasRunningOperations())
return;
// If we already timed out, don't delay play mode again to avoid infinite loop.
// This happens when FinishWaitAndEnterPlayMode() calls EnterPlaymode() after timeout,
// which triggers this callback again while operations are still running.
// Note: mPlayModeWaitTimeoutReached is reset in BeforeAssemblyReload().
if (mPlayModeWaitTimeoutReached)
return;
mLog.Debug("OnPlayModeStateChanged delaying play mode");
// Cancel entering play mode to wait for operations to finish
EditorApplication.ExitPlaymode();
// Prevent reentrancy: the ProgressBar doesn't lock the UI.
if (mPlayModeWaitStopwatch != null)
return;
// Mark play mode as pending. Cleared on success, or used to resume after reload.
SessionState.SetBool(PLAY_MODE_PENDING_KEY, true);
mLastLoggedSecond = -1;
mPlayModeWaitStopwatch = Stopwatch.StartNew();
EditorApplication.update += EnterPlayModeWhenOperationsFinish;
}
static void EnterPlayModeWhenOperationsFinish()
{
if (mPlayModeWaitStopwatch == null)
return;
// Show the progress bar on every frame to ensure it stays displayed regardless of any
// interruptions (like a popup on Editor Wants To Quit, or taking a screenshot!)
DisplayProgressBar();
bool hasLongRunningOperations = HasLongRunningOperations();
// Always wait for UVCS operations without timeout.
// Only apply timeout to ThreadWaiters and InUseConnections.
mPlayModeWaitTimeoutReached = mPlayModeWaitStopwatch.ElapsedMilliseconds >= MAX_WAIT_TIMEOUT_MS;
if (mPlayModeWaitTimeoutReached && !hasLongRunningOperations)
{
LogTimeoutReached(
mPlayModeWaitStopwatch.ElapsedMilliseconds,
"Forcing play mode.");
FinishWaitAndEnterPlayMode();
return;
}
if (HasRunningOperations())
{
LogWaitingForOperations(mPlayModeWaitStopwatch.ElapsedMilliseconds);
return;
}
FinishWaitAndEnterPlayMode();
}
static void FinishWaitAndEnterPlayMode()
{
mLog.Debug("FinishWaitAndEnterPlayMode");
EditorApplication.update -= EnterPlayModeWhenOperationsFinish;
mPlayModeWaitStopwatch = null;
ClearProgressBarAndEnterPlayMode();
}
static void ClearProgressBarAndEnterPlayMode()
{
ClearProgressBar();
mLog.Debug("Enter play mode");
EditorApplication.EnterPlaymode();
SessionState.SetBool(PLAY_MODE_PENDING_KEY, false);
}
static void CancelPlayModeWaitIfNeeded()
{
if (mPlayModeWaitStopwatch == null)
return;
mLog.Debug("CancelPlayModeWait");
EditorApplication.update -= EnterPlayModeWhenOperationsFinish;
mPlayModeWaitStopwatch = null;
}
static bool HasRunningOperations()
{
return HasLowLevelRunningOperations() || HasLongRunningOperations();
}
static bool HasLowLevelRunningOperations()
{
return ThreadWaiterRegistry.HasRunningOperations() ||
ClientConnectionPool.HasInUseConnections();
}
static bool HasLongRunningOperations()
{
return UVCSPlugin.Instance.HasRunningOperation();
}
static void DisplayProgressBar()
{
EditorUtility.DisplayProgressBar(
UnityConstants.UVCS_WINDOW_TITLE,
PlasticLocalization.Name.WaitingForPendingOperationsToFinish.GetString(),
1f);
}
static void ClearProgressBar()
{
mLog.Debug("ClearProgressBar");
EditorUtility.ClearProgressBar();
}
static void LogTimeoutReached(long elapsedMs, string action)
{
mLog.WarnFormat(
"Timeout reached after {0}ms. {1} " +
"Remaining: ThreadWaiters={2}, InUseConnections={3}",
elapsedMs,
action,
ThreadWaiterRegistry.GetRunningOperationsCount(),
ClientConnectionPool.GetInUseConnectionsCount());
}
static void LogWaitingForOperations(long elapsedMs)
{
// Only log once per second to avoid excessive log spam
long currentSecond = elapsedMs / 1000;
if (currentSecond == mLastLoggedSecond)
return;
mLastLoggedSecond = currentSecond;
mLog.DebugFormat(
"Waiting: ThreadWaiters={0}, InUseConnections={1}, " +
"UVCSOperations={2}, elapsed={3}ms",
ThreadWaiterRegistry.GetRunningOperationsCount(),
ClientConnectionPool.GetInUseConnectionsCount(),
UVCSPlugin.Instance.HasRunningOperation(),
elapsedMs);
}
const int MAX_WAIT_TIMEOUT_MS = 10000;
const int PROGRESS_BAR_DELAY_MS = 1000;
const int POLL_INTERVAL_MS = 50;
const string PLAY_MODE_PENDING_KEY = "WaitForPendingOperations.PlayModePending";
static long mLastLoggedSecond = -1;
static bool mPlayModeWaitTimeoutReached;
static Stopwatch mPlayModeWaitStopwatch;
static readonly ILog mLog = PlasticApp.GetLogger("WaitForPendingOperations");
}
}