unity/EditorPlugin/Profiler/SnapshotAnalysis/SnapshotCollectorDaemon.cs (176 lines of code) (raw):
#nullable enable
using System;
using System.Threading.Tasks;
using JetBrains.Collections.Viewable;
using JetBrains.Diagnostics;
using JetBrains.Lifetimes;
using JetBrains.Rd.Base;
using JetBrains.Rd.Tasks;
using JetBrains.Rider.Model.Unity;
using JetBrains.Rider.Model.Unity.BackendUnity;
using JetBrains.Rider.Unity.Editor.Profiler.Adapters.Interfaces;
using UnityEditor;
using UnityEditorInternal;
namespace JetBrains.Rider.Unity.Editor.Profiler.SnapshotAnalysis
{
public interface ISnapshotCollectorDaemon
{
void Update(EditorWindow? ourLastProfilerWindow);
void Deinit();
void Advise(Lifetime connectionLifetime, UnityProfilerModel model);
}
internal class SnapshotCollectorDaemon : ISnapshotCollectorDaemon
{
private static readonly ILog ourLogger = Log.GetLog(nameof(SnapshotCollectorDaemon));
private readonly IProfilerAdaptersFactory myAdaptersFactory;
private readonly ViewableProperty<UnityProfilerSnapshot?> myLastSnapshot = new(null);
private readonly ViewableProperty<UnityProfilerSnapshotStatus?> myProfilerStatus = new(null);
private readonly SequentialLifetimes mySequentialLifetimes;
private readonly ProfilerSnapshotCrawler mySnapshotCrawler;
private IProfilerWindowAdapter? myProfilerWindowAdapter;
internal SnapshotCollectorDaemon(IProfilerAdaptersFactory adaptersFactory, Lifetime appDomainLifetime)
{
myAdaptersFactory = adaptersFactory;
mySequentialLifetimes = new SequentialLifetimes(appDomainLifetime);
mySnapshotCrawler = new ProfilerSnapshotCrawler(myAdaptersFactory.CreateProfilerSnapshotDriverAdapter());
// Update the status to "UpToDate" when a snapshot becomes ready
myLastSnapshot.Advise(appDomainLifetime, snapshot =>
ourLogger.Verbose(
$"Set {nameof(myLastSnapshot)}: " +
$"{nameof(snapshot.FrameIndex)}:{snapshot?.FrameIndex ?? -1} " +
$"{nameof(snapshot.ThreadIndex)}:{snapshot?.ThreadIndex ?? -1} " +
$"{nameof(snapshot.Samples)}:{snapshot?.Samples.Count ?? -1}"));
myProfilerStatus.Advise(appDomainLifetime,
status => ourLogger.Verbose($"Set {nameof(myProfilerStatus)}: {status}"));
}
void ISnapshotCollectorDaemon.Deinit()
{
ourLogger.Verbose("Deinit");
mySequentialLifetimes.TerminateCurrent();
myLastSnapshot.Set(null);
myProfilerWindowAdapter = null;
}
//Multiple advise calls could be ((
void ISnapshotCollectorDaemon.Advise(Lifetime connectionLifetime, UnityProfilerModel model)
{
ourLogger.Verbose("Advise");
//if myLastSnapshot already exists - mark status as ready to fetch
myProfilerStatus.Set(myLastSnapshot.Value.ToSnapshotStatus(SnapshotStatus.HasNewSnapshotDataToFetch));
model.GetUnityProfilerSnapshot.Set(async (lifetime, request) =>
{
var fetchNewSnapshotData = await FetchNewSnapshotData(lifetime, request);
myLastSnapshot.Set(fetchNewSnapshotData);
myProfilerStatus.Set(fetchNewSnapshotData.ToSnapshotStatus(SnapshotStatus.SnapshotDataIsUpToDate));
return fetchNewSnapshotData;
});
myProfilerStatus.Advise(connectionLifetime, status => model.ProfilerSnapshotStatus.Set(status));
}
void ISnapshotCollectorDaemon.Update(EditorWindow? lastKnownProfilerWindow)
{
// Cancel all fetching tasks if the Editor is playing or Profiler is recording
if (EditorApplication.isPlaying || (ProfilerDriver.enabled && ProfilerDriver.profileEditor))
{
if(!mySequentialLifetimes.IsCurrentTerminated)
mySequentialLifetimes.TerminateCurrent();
return;
}
// Get the current profiler window object once to avoid multiple property access
var profilerWindowObject = myProfilerWindowAdapter?.ProfilerWindowObject as EditorWindow;
// If the profiler window has changed - create adapter for a new window
if (profilerWindowObject != lastKnownProfilerWindow)
{
ourLogger.Verbose(
$"Update {nameof(myProfilerWindowAdapter)} because of {nameof(profilerWindowObject)} change: {lastKnownProfilerWindow}");
myProfilerWindowAdapter = myAdaptersFactory.CreateProfilerWindowAdapter(lastKnownProfilerWindow);
// Update the profiler window object after creating a new adapter
profilerWindowObject = myProfilerWindowAdapter?.ProfilerWindowObject as EditorWindow;
}
// If the profiler window is closed or destroyed - clear existing cached snapshot information
if (profilerWindowObject == null || myProfilerWindowAdapter == null)
{
if(!mySequentialLifetimes.IsCurrentTerminated)
mySequentialLifetimes.TerminateCurrent();
myLastSnapshot.Set(null);
myProfilerStatus.Set(myLastSnapshot.Value.ToSnapshotStatus(SnapshotStatus.HasNewSnapshotDataToFetch));
return;
}
UpdateSnapshotStatus(myProfilerWindowAdapter);
}
private Task<UnityProfilerSnapshot?> FetchNewSnapshotData(Lifetime lifetime,
ProfilerSnapshotRequest snapshotRequest)
{
ourLogger.Verbose("FetchNewSnapshotData:");
if (myProfilerWindowAdapter == null)
{
ourLogger.Verbose("FetchNewSnapshotData: myProfilerWindowAdapter is null");
return Task.FromResult<UnityProfilerSnapshot?>(null);
}
if (myLastSnapshot.Value != null &&
myLastSnapshot.Value.FrameIndex == snapshotRequest.FrameIndex &&
myLastSnapshot.Value.ThreadIndex == snapshotRequest.ThreadIndex)
{
ourLogger.Verbose("FetchNewSnapshotData: myLastSnapshot is the same as the requested one");
myProfilerStatus.Value = myLastSnapshot.Value.ToSnapshotStatus(SnapshotStatus.SnapshotDataIsUpToDate);
return Task.FromResult<UnityProfilerSnapshot?>(myLastSnapshot.Value);
}
return StartSnapshotFetchingTask(snapshotRequest, lifetime);
}
private void UpdateSnapshotStatus(IProfilerWindowAdapter profilerWindowAdapter)
{
// Early return if adapter is null
if (myProfilerWindowAdapter == null)
return;
// Get the selected frame index once
var selectedFrameIndex = profilerWindowAdapter.GetSelectedFrameIndex();
// Early return if no frame is selected
if (selectedFrameIndex == -1)
return;
// Cache the current status value to avoid multiple property access
var currentStatus = myProfilerStatus.Value;
// Skip update if the status is already up-to-date for this frame (unless it's NoSnapshotDataAvailable)
if (currentStatus != null &&
currentStatus.FrameIndex == selectedFrameIndex &&
currentStatus.Status != SnapshotStatus.NoSnapshotDataAvailable)
return;
// Get and set the new status
var newStatusInfo = mySnapshotCrawler.GetCurrentProfilerSnapshotStatusInfo(selectedFrameIndex, 0);
myProfilerStatus.Set(newStatusInfo);
}
private Task<UnityProfilerSnapshot?> StartSnapshotFetchingTask(ProfilerSnapshotRequest snapshotRequest,
Lifetime lifetime)
{
ourLogger.Verbose("StartSnapshotFetchingTask");
// Create a lifetime that will be terminated when a new task starts or the parent lifetime ends
var snapshotFetchingLifetime = lifetime.Intersect(mySequentialLifetimes.Next());
// Start a new task
try
{
// Create progress reporter once and reuse it
var progress = new Progress<UnityProfilerSnapshotStatus>(snapshotStatus =>
{
if (snapshotFetchingLifetime.IsAlive)
snapshotFetchingLifetime.Execute(() => myProfilerStatus.Set(snapshotStatus));
});
return Task.Run(async () =>
{
try
{
// Get the snapshot data
var profilerFrameSnapshot =
await mySnapshotCrawler.GetUnityProfilerSnapshot(snapshotRequest, snapshotFetchingLifetime, progress);
// Update the last snapshot if the lifetime is still alive
if (snapshotFetchingLifetime.IsAlive)
myLastSnapshot.Set(profilerFrameSnapshot);
return profilerFrameSnapshot;
}
catch (OperationCanceledException)
{
ourLogger.Verbose($"Task {nameof(mySnapshotCrawler.GetUnityProfilerSnapshot)} was canceled");
// Only set to null if the lifetime is still alive
if (snapshotFetchingLifetime.IsAlive)
myLastSnapshot.Set(null);
return null;
}
catch (Exception ex)
{
ourLogger.Error($"Task {nameof(mySnapshotCrawler.GetUnityProfilerSnapshot)} failed with exception", ex);
// Only set to null if the lifetime is still alive
if (snapshotFetchingLifetime.IsAlive)
myLastSnapshot.Set(null);
return null;
}
}, snapshotFetchingLifetime);
}
catch (LifetimeCanceledException)
{
ourLogger.Verbose($"Task {nameof(StartSnapshotFetchingTask)} was canceled");
}
catch (Exception ex)
{
ourLogger.Error($"Task {nameof(StartSnapshotFetchingTask)} failed with exception", ex);
throw;
}
myLastSnapshot.Set(null);
return Task.FromResult<UnityProfilerSnapshot?>(null);
}
}
}