Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
4 changes: 2 additions & 2 deletions Explorer/Assets/DCL/Audio/UIAudioPlaybackController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void Dispose()
UIAudioEventsBus.Instance.PlayContinuousUIAudioEvent -= OnPlayContinuousUIAudioEvent;
UIAudioEventsBus.Instance.StopContinuousUIAudioEvent -= OnStopContinuousUIAudioEvent;
UIAudioEventsBus.Instance.MuteContinuousUIAudioEvent -= OnMuteContinuousUIAudioEvent;
ExitUtils.BeforeApplicationQuitting -= OnBeforeApplicationQuitting;
ExitUtils.UnregisterCleanUpCandidate(nameof(UIAudioPlaybackController));
mainCancellationTokenSource.SafeCancelAndDispose();

foreach (KeyValuePair<AudioClipConfig, ContinuousPlaybackAudioData> audioData in audioDataPerAudioClipConfig)
Expand All @@ -66,7 +66,7 @@ public void Initialize(AudioMixerVolumesController mixerVolumesController)
UIAudioEventsBus.Instance.MuteContinuousUIAudioEvent += OnMuteContinuousUIAudioEvent;
audioSourcePool = new GameObjectPool<AudioSource>(transform, OnCreateAudioSource);
mainCancellationTokenSource = new CancellationTokenSource();
ExitUtils.BeforeApplicationQuitting += OnBeforeApplicationQuitting;
ExitUtils.RegisterCleanUpCandidate(new OnQuittingCleanUpCandidate(nameof(UIAudioPlaybackController), OnBeforeApplicationQuitting));
}

private CancellationTokenSource CreateLinkedCancellationTokenSource() =>
Expand Down
113 changes: 110 additions & 3 deletions Explorer/Assets/DCL/Infrastructure/Utility/ExitUtils.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,130 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using DCL.Diagnostics;
using UnityEngine;
using Utility.Multithreading;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace DCL.Utility
{
public class OnQuittingCleanUpCandidate
{
private readonly string name;
private readonly Action callback;

public string Name => name;

public OnQuittingCleanUpCandidate(string name, Action callback)
{
this.name = name;
this.callback = callback;
}

internal void Execute(Stopwatch stopwatch)
Comment thread
NickKhalow marked this conversation as resolved.
{
long startedAtMs = stopwatch.ElapsedMilliseconds;

try
{
callback.Invoke();
}
catch (Exception e)
{
ReportHub.LogException(e, ReportCategory.UNSPECIFIED);
}

long elapsedMs = stopwatch.ElapsedMilliseconds - startedAtMs;
ReportHub.LogProductionInfo($"[ExitUtils] '{name}' cleanup took {elapsedMs}ms (total {stopwatch.ElapsedMilliseconds}ms)");
}
}

public static class ExitUtils
{
public static event Action BeforeApplicationQuitting;
private static readonly Mutex<List<OnQuittingCleanUpCandidate>> candidates = new (new ());
private static readonly Atomic<bool> isExiting = new (false);

[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void SubscribeToApplicationQuitting()
{
Application.quitting += OnApplicationQuitting;
Comment thread
NickKhalow marked this conversation as resolved.
}
Comment thread
NickKhalow marked this conversation as resolved.

private static void OnApplicationQuitting()
{
Application.quitting -= OnApplicationQuitting;
ReportHub.LogProductionInfo("[ExitUtils] triggered by Unity's Application.quitting");
Exit();
}

public static void RegisterCleanUpCandidate(OnQuittingCleanUpCandidate candidate)
{
if (isExiting)
{
ReportHub.LogProductionInfo($"[ExitUtils] Ignored RegisterCleanUpCandidate('{candidate.Name}') because Exit() is already in progress");
return;
}

using var scope = candidates.Lock();

for (var i = 0; i < scope.Value.Count; i++)
{
if (scope.Value[i].Name == candidate.Name)
throw new InvalidOperationException($"[ExitUtils] Cleanup candidate '{candidate.Name}' is already registered");
}

scope.Value.Add(candidate);
}

public static void UnregisterCleanUpCandidate(string name)
{
if (isExiting)
{
ReportHub.LogProductionInfo($"[ExitUtils] Ignored UnregisterCleanUpCandidate('{name}') because Exit() is already in progress");
return;
}

using var scope = candidates.Lock();

for (var i = 0; i < scope.Value.Count; i++)
{
if (scope.Value[i].Name == name)
{
scope.Value.RemoveAt(i);
return;
}
}
}

public static void Exit()
{
BeforeApplicationQuitting?.Invoke();
Stopwatch stopwatch = Stopwatch.StartNew();
ReportHub.LogProductionInfo($"[ExitUtils] Exit requested at {stopwatch.ElapsedMilliseconds}ms");
Comment thread
NickKhalow marked this conversation as resolved.
Outdated
Comment thread
NickKhalow marked this conversation as resolved.
Outdated
Comment thread
NickKhalow marked this conversation as resolved.
Outdated
Comment thread
NickKhalow marked this conversation as resolved.
Outdated

if (isExiting)
{
ReportHub.LogProductionInfo("[ExitUtils] Exit() ignored because it is already in progress");
return;
}

isExiting.Set(true);

using (var scope = candidates.Lock())
{
foreach (OnQuittingCleanUpCandidate candidate in scope.Value)
candidate.Execute(stopwatch);
}

ReportHub.LogProductionInfo($"[ExitUtils] CleanUpCandidates finished at {stopwatch.ElapsedMilliseconds}ms");

#if UNITY_EDITOR
EditorApplication.isPlaying = false;
#else
UnityEngine.Application.Quit();
#endif
ReportHub.LogProductionInfo($"[ExitUtils] Quit call dispatched at {stopwatch.ElapsedMilliseconds}ms");
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using DCL.AvatarRendering.AvatarShape.UnityInterface;
using DCL.CharacterMotion.Animation;
using DCL.Utilities;
using DCL.Utility;
using Newtonsoft.Json.Linq;
using System;
using System.Threading;
Expand Down Expand Up @@ -37,7 +38,7 @@ public void Initialize()
cts = new CancellationTokenSource();
SubscribeToPlayerStepAsync(cts.Token).Forget();

Application.quitting += Dispose;
ExitUtils.RegisterCleanUpCandidate(new OnQuittingCleanUpCandidate(nameof(WalkedDistanceAnalytics), Dispose));
AppDomain.CurrentDomain.ProcessExit += (_, _) => Dispose();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Arch.SystemGroups.DefaultSystemGroups;
using DCL.Diagnostics;
using DCL.PerformanceAndDiagnostics.Analytics;
using DCL.Utility;
using ECS;
using ECS.Abstract;
using Newtonsoft.Json.Linq;
Expand Down Expand Up @@ -32,7 +33,7 @@ public override void Initialize()
{
base.Initialize();

Application.quitting += Dispose;
ExitUtils.RegisterCleanUpCandidate(new OnQuittingCleanUpCandidate(nameof(TimeSpentInWorldAnalyticsSystem), Dispose));
AppDomain.CurrentDomain.ProcessExit += (_, _) => Dispose();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using DCL.Diagnostics;
using DCL.Utility;
using Sentry;
using Sentry.Unity;
using System.Collections.Generic;
Expand Down Expand Up @@ -26,7 +27,7 @@ void SetQuitting()
SentrySdk.AddBreadcrumb("Application is quitting");
}

Application.quitting += SetQuitting;
ExitUtils.RegisterCleanUpCandidate(new OnQuittingCleanUpCandidate(nameof(UnityObjectUtils), SetQuitting));
}

// This code fixes the following situation: enter play mode, exit play
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,7 @@ public partial class SentryTransactionManager
public SentryTransactionManager()
{
// Register for application lifecycle events to ensure transactions are finished
ExitUtils.BeforeApplicationQuitting += OnApplicationQuitting;
Application.quitting += OnApplicationQuitting;
ExitUtils.RegisterCleanUpCandidate(new OnQuittingCleanUpCandidate(nameof(SentryTransactionManager), OnApplicationQuitting));
}

// Otherwise Sentry creates a new dictionary for every transaction
Expand Down
7 changes: 6 additions & 1 deletion Explorer/Assets/DCL/Prefs/DCLPlayerPrefs.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Unity.Multiplayer.PlayMode;
Expand Down Expand Up @@ -125,14 +126,18 @@ private static void Initialize(bool inMemory)
throw new InvalidOperationException("DCLPrefs already initialized.");

dclPrefs = inMemory ? new InMemoryDCLPlayerPrefs() : new FileDCLPlayerPrefs();
// ExitUtils lives in Utility which already depends on DCL.Prefs (via PersistentSetting), so subscribe directly here
Application.quitting += OnQuitting;
}

private static void OnQuitting()
{
Comment thread
NickKhalow marked this conversation as resolved.
Application.quitting -= OnQuitting;

Stopwatch stopwatch = Stopwatch.StartNew();
(dclPrefs as IDisposable)?.Dispose();
dclPrefs = null;
UnityEngine.Debug.Log($"[DCLPlayerPrefs] cleanup took {stopwatch.ElapsedMilliseconds}ms");
Comment thread
NickKhalow marked this conversation as resolved.
Outdated
}

#if UNITY_EDITOR
Expand All @@ -153,7 +158,7 @@ private static bool ValidateClearDCLPlayerPrefs() =>
private static void ResetNearbyVoiceIntroTip()
{
DeleteKey(DCLPrefKeys.NEARBY_VOICE_TIP_DISMISSED, save: true);
Debug.Log("Nearby Voice Intro Tip has been reset.");
UnityEngine.Debug.Log("Nearby Voice Intro Tip has been reset.");
}
#endif
}
Expand Down
14 changes: 14 additions & 0 deletions Explorer/Assets/DCL/Tests/Editor/CodeConventionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,20 @@ public void VerifyShouldNotUseDirectFileIO()
);
}

[Test]
public void VerifyShouldNotUseApplicationQuitting()
{
const string pattern = @"Application\.quitting";
string[] ignorePaths = new []
{
// ExitUtils is the infrastructural wrapper that funnels Unity's quit event into the cleanup pipeline
"Assets/DCL/Infrastructure/Utility/ExitUtils.cs",
// DCL.Prefs cannot reference the Utility asmdef (cycle via PersistentSetting)
"Assets/DCL/Prefs/DCLPlayerPrefs.cs",
};
ValidateNoForbiddenApiUsed(pattern, "Use ExitUtils.RegisterCleanUpCandidate instead of subscribing to Unity's quit event directly.", ignorePaths);
}

[Test]
public void VerifyShouldNotUseConcurrentCollection()
{
Expand Down
Loading