Browse Source

Merge branch 'master' into open-beta

Krzysztof Krysiński 8 months ago
parent
commit
80110d8b79

+ 21 - 0
src/PixiEditor/Models/AnalyticsAPI/AnalyticEvent.cs

@@ -2,11 +2,32 @@
 
 public class AnalyticEvent
 {
+    private Semaphore? _endTimeReportedSemaphore;
+    
     public string EventType { get; set; }
     
     public DateTime Time { get; set; }
     
     public DateTime End { get; set; }
     
+    public bool ExpectingEndTimeReport { get; set; }
+
+    public void ReportEndTime()
+    {
+        End = DateTime.UtcNow;
+        ExpectingEndTimeReport = false;
+        
+        _endTimeReportedSemaphore?.Release();
+    }
+
+    public void WaitForEndTime(TimeSpan timeout)
+    {
+        _endTimeReportedSemaphore = new Semaphore(0, int.MaxValue);
+
+        _endTimeReportedSemaphore.WaitOne(timeout);
+        _endTimeReportedSemaphore?.Dispose();
+        _endTimeReportedSemaphore = null;
+    }
+    
     public Dictionary<string, object>? Data { get; set; }
 }

+ 1 - 0
src/PixiEditor/Models/AnalyticsAPI/AnalyticEventTypes.cs

@@ -13,6 +13,7 @@ public class AnalyticEventTypes
     public static string SwitchTool { get; } = GetEventType("SwitchTool");
     public static string UseTool { get; } = GetEventType("UseTool");
     public static string ResumeSession { get; } = GetEventType("ResumeSession");
+    public static string PeriodicPerformanceReport { get; } = GetEventType("PeriodicPerformanceReport");
 
     private static string GetEventType(string value) => $"PixiEditor.{value}";
 }

+ 9 - 4
src/PixiEditor/Models/AnalyticsAPI/Analytics.cs

@@ -1,4 +1,5 @@
-using System.Reflection;
+using System.Diagnostics;
+using System.Reflection;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.Models.Commands.CommandContext;
 using PixiEditor.Models.Files;
@@ -40,13 +41,16 @@ public static class Analytics
     internal static AnalyticEvent? SendSwitchToTool(IToolHandler? newTool, IToolHandler? oldTool, ICommandExecutionSourceInfo? sourceInfo) =>
         SendEvent(AnalyticEventTypes.SwitchTool, ("NewTool", newTool?.ToolName), ("OldTool", oldTool?.ToolName), ("Source", sourceInfo));
 
-    internal static AnalyticEvent? SendCommand(string commandName, ICommandExecutionSourceInfo? source) =>
-        source is ShortcutSourceInfo { IsRepeat: true } ? null : SendEvent(AnalyticEventTypes.GeneralCommand, ("CommandName", commandName), ("Source", source));
+    internal static AnalyticEvent? SendCommand(string commandName, ICommandExecutionSourceInfo? source, bool expectingEndTime = false) =>
+        source is ShortcutSourceInfo { IsRepeat: true } ? null : SendEvent(AnalyticEventTypes.GeneralCommand, expectingEndTime, ("CommandName", commandName), ("Source", source));
 
     private static AnalyticEvent? SendEvent(string name, params (string, object)[] data) =>
         SendEvent(name, data.ToDictionary());
+    
+    private static AnalyticEvent? SendEvent(string name, bool expectingEndTime, params (string, object)[] data) =>
+        SendEvent(name, data.ToDictionary(), expectingEndTime);
 
-    private static AnalyticEvent? SendEvent(string name, Dictionary<string, object> data)
+    private static AnalyticEvent? SendEvent(string name, Dictionary<string, object> data, bool expectingEndTime = false)
     {
         var reporter = AnalyticsPeriodicReporter.Instance;
 
@@ -59,6 +63,7 @@ public static class Analytics
         {
             EventType = name,
             Time = DateTime.UtcNow,
+            ExpectingEndTimeReport = expectingEndTime,
             Data = data
         };
         

+ 23 - 0
src/PixiEditor/Models/AnalyticsAPI/AnalyticsPeriodicReporter.cs

@@ -9,6 +9,7 @@ public class AnalyticsPeriodicReporter
     
     private readonly SemaphoreSlim _semaphore = new(1, 1);
     private readonly AnalyticsClient _client;
+    private readonly PeriodicPerformanceReporter _performanceReporter;
     
     private readonly List<AnalyticEvent> _backlog = new();
     private readonly CancellationTokenSource _cancellationToken = new();
@@ -27,6 +28,7 @@ public class AnalyticsPeriodicReporter
         Instance = this;
         
         _client = client;
+        _performanceReporter = new PeriodicPerformanceReporter(this);
     }
 
     public void Start(Guid? sessionId)
@@ -40,6 +42,7 @@ public class AnalyticsPeriodicReporter
         }
 
         Task.Run(RunAsync);
+        _performanceReporter.StartPeriodicReporting();
     }
 
     public async Task StopAsync()
@@ -92,6 +95,9 @@ public class AnalyticsPeriodicReporter
         {
             try
             {
+                if (_backlog.Any(x => x.ExpectingEndTimeReport))
+                    WaitForEndTimes();
+                
                 await SendBacklogAsync();
 
                 await Task.Delay(TimeSpan.FromSeconds(10));
@@ -104,6 +110,23 @@ public class AnalyticsPeriodicReporter
         }
     }
 
+    private void WaitForEndTimes()
+    {
+        var totalTimeout = DateTime.Now + TimeSpan.FromSeconds(10);
+
+        foreach (var backlog in _backlog)
+        {
+            var timeout = totalTimeout - DateTime.Now;
+
+            if (timeout < TimeSpan.Zero)
+            {
+                break;
+            }
+
+            backlog.WaitForEndTime(timeout);
+        }
+    }
+
     private async Task SendBacklogAsync()
     {
         await _semaphore.WaitAsync();

+ 74 - 0
src/PixiEditor/Models/AnalyticsAPI/PeriodicPerformanceReporter.cs

@@ -0,0 +1,74 @@
+using System.Diagnostics;
+using Timer = System.Timers.Timer;
+
+namespace PixiEditor.Models.AnalyticsAPI;
+
+public class PeriodicPerformanceReporter(AnalyticsPeriodicReporter analyticsReporter)
+{
+    private long _lastTotalAllocatedBytes;
+    private TimeSpan _lastTotalGcPauseTime;
+
+    public void StartPeriodicReporting()
+    {
+        var timer = new Timer
+        {
+            Interval = TimeSpan.FromSeconds(45).TotalMilliseconds,
+            AutoReset = true
+        };
+
+        timer.Elapsed += (_, _) => Task.Run(CollectPerformanceMetricAsync);
+        
+        timer.Start();
+    }
+    
+    private async Task CollectPerformanceMetricAsync()
+    {
+        var process = Process.GetCurrentProcess();
+        
+        var data = new Dictionary<string, object>();
+
+        var collectionStartTime = DateTime.Now;
+        
+        var processorTime = await SampleProcessorTimeAsync(process);
+        data["UserTime"] = processorTime.userTime;
+        data["PrivilegedTime"] = processorTime.privilegedTime;
+        
+        data["PrivateMemorySize"] = process.PrivateMemorySize64;
+        data["WorkingSet"] = process.WorkingSet64;
+
+        var currentTotalGcPauseTime = GC.GetTotalPauseDuration();
+        data["GcPauseTime"] = _lastTotalGcPauseTime;
+        _lastTotalGcPauseTime = currentTotalGcPauseTime;
+
+        var handles = process.HandleCount;
+        var threads = process.Threads.Count;
+        
+        data["Handles"] = handles;
+        data["Threads"] = threads;
+        
+        data["CollectionTime"] = DateTime.Now - collectionStartTime;
+        
+        var e = new AnalyticEvent
+        {
+            EventType = AnalyticEventTypes.PeriodicPerformanceReport,
+            Time = DateTime.UtcNow,
+            Data = data
+        };
+        
+        analyticsReporter.AddEvent(e);
+    }
+
+    private async Task<(TimeSpan userTime, TimeSpan privilegedTime)> SampleProcessorTimeAsync(Process process)
+    {
+        var userStartTime = process.UserProcessorTime;
+        var privilegedStartTime = process.PrivilegedProcessorTime;
+
+        await Task.Delay(TimeSpan.FromSeconds(10));
+        
+        process.Refresh();
+        var userTime = process.UserProcessorTime - userStartTime;
+        var privilegedTime = process.PrivilegedProcessorTime - privilegedStartTime;
+        
+        return (userTime, privilegedTime);
+    }
+}

+ 15 - 1
src/PixiEditor/Models/Commands/CommandController.cs

@@ -372,17 +372,25 @@ internal class CommandController
     private static void CommandMethodInvoker(MethodInfo method, string name, object? instance, object parameter, ParameterInfo[] parameterInfos, bool isTracking)
     {
         var parameters = GetParameters(parameter, parameterInfos);
+        AnalyticEvent? analytics = null;
                 
         if (isTracking)
         {
-            Analytics.SendCommand(name, (parameter as CommandExecutionContext)?.SourceInfo);
+            analytics = Analytics.SendCommand(name, (parameter as CommandExecutionContext)?.SourceInfo, expectingEndTime: true);
         }
 
         try
         {
             object result = method.Invoke(instance, parameters);
             if (result is Task task)
+            {
                 task.ContinueWith(ActionOnException, TaskContinuationOptions.OnlyOnFaulted);
+                task.ContinueWith(ReportEndTime, TaskContinuationOptions.OnlyOnRanToCompletion);
+            }
+            else
+            {
+                analytics?.ReportEndTime();
+            }
         }
         catch (TargetInvocationException e)
         {
@@ -398,6 +406,12 @@ internal class CommandController
             await faultedTask; // this instantly throws the exception from the already faulted task
         }
 
+        ValueTask ReportEndTime(Task originalTask)
+        {
+            analytics?.ReportEndTime();
+            return ValueTask.CompletedTask;
+        }
+
         static object?[]? GetParameters(object parameter, ParameterInfo[] parameterInfos)
         {
             object?[]? parameters;