Преглед изворни кода

Add diagnostic metrics/activities (#18314)

* Add System.Diagnostics.DiagnosticSource nuget package for pre-.NET 8 targets

* Move System.Memory reference to Base.props

* Add CompositorRenderPass and CompositorUpdatePass metrics

* Layout measure/arrange pass metrics

* Add UI render pass and input pass meters

* Add observable metrics

* Add RaisingRoutedEvent activity

* Add FindingResourceActivity activity

* Add AttachingStyleActivity and EvaluatingStyleActivator activities

* Add PerformingHitTest activity

* Add MeasuingLayoutable/ArrangingLayoutable activities

* Missed RaisingRoutedEvent definition

* Missed tag definitions

* Start FindingResourceActivity on static resources too

* Fix compilation

* Naming

* Add Avalonia.Diagnostics.Diagnostic.IsEnabled runtime switch

* Maybe make it more trimmable as well
Maxwell Katz пре 7 месеци
родитељ
комит
acd4653411
26 измењених фајлова са 291 додато и 24 уклоњено
  1. 0 1
      Avalonia.sln
  2. 6 0
      build/Base.props
  3. 0 6
      build/System.Memory.props
  4. 0 1
      src/Avalonia.Base/Avalonia.Base.csproj
  5. 7 1
      src/Avalonia.Base/Controls/ResourceNodeExtensions.cs
  6. 25 0
      src/Avalonia.Base/Diagnostics/Diagnostic.Activities.cs
  7. 51 0
      src/Avalonia.Base/Diagnostics/Diagnostic.Consts.cs
  8. 94 0
      src/Avalonia.Base/Diagnostics/Diagnostic.Metrics.cs
  9. 22 0
      src/Avalonia.Base/Diagnostics/Diagnostic.cs
  10. 5 0
      src/Avalonia.Base/Interactivity/EventRoute.cs
  11. 3 0
      src/Avalonia.Base/Interactivity/Interactive.cs
  12. 5 0
      src/Avalonia.Base/Interactivity/RoutedEvent.cs
  13. 3 0
      src/Avalonia.Base/Layout/LayoutManager.cs
  14. 7 0
      src/Avalonia.Base/Layout/Layoutable.cs
  15. 5 1
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  16. 16 12
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  17. 7 0
      src/Avalonia.Base/Styling/ControlTheme.cs
  18. 6 0
      src/Avalonia.Base/Styling/Style.cs
  19. 8 0
      src/Avalonia.Base/Styling/StyleInstance.cs
  20. 4 0
      src/Avalonia.Base/Threading/DispatcherTimer.cs
  21. 4 0
      src/Avalonia.Base/Utilities/StopwatchHelper.cs
  22. 5 0
      src/Avalonia.Base/Visual.cs
  23. 3 0
      src/Avalonia.Controls/TopLevel.cs
  24. 5 0
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs
  25. 0 1
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  26. 0 1
      tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj

+ 0 - 1
Avalonia.sln

@@ -115,7 +115,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
 		build\SkiaSharp.props = build\SkiaSharp.props
 		build\SourceGenerators.props = build\SourceGenerators.props
 		build\SourceLink.props = build\SourceLink.props
-		build\System.Memory.props = build\System.Memory.props
 		build\TargetFrameworks.props = build\TargetFrameworks.props
 		build\TrimmingEnable.props = build\TrimmingEnable.props
 		build\UnitTests.NetFX.props = build\UnitTests.NetFX.props

+ 6 - 0
build/Base.props

@@ -1,6 +1,12 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <!-- '!NET6_0_OR_GREATER' equivalent -->
+  <ItemGroup Condition="!('$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '6.0')))">
+    <PackageReference Include="System.Memory" Version="4.5.5" />
+  </ItemGroup>
   <ItemGroup Condition="!('$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '6.0')))">
     <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" />
   </ItemGroup>
+  <ItemGroup Condition="!('$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '8.0')))">
+    <PackageReference Include="System.Diagnostics.DiagnosticSource" Version="8.0.1" />
+  </ItemGroup>
 </Project>

+ 0 - 6
build/System.Memory.props

@@ -1,6 +0,0 @@
-<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <!-- '!NET6_0_OR_GREATER' equivalent -->
-  <ItemGroup Condition="!('$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '6.0')))">
-    <PackageReference Include="System.Memory" Version="4.5.3" />
-  </ItemGroup>
-</Project>

+ 0 - 1
src/Avalonia.Base/Avalonia.Base.csproj

@@ -12,7 +12,6 @@
   </ItemGroup>
   <Import Project="..\..\build\Base.props" />
   <Import Project="..\..\build\Binding.props" />
-  <Import Project="..\..\build\System.Memory.props" />
   <Import Project="..\..\build\NullableEnable.props" />
   <Import Project="..\..\build\TrimmingEnable.props" />
   <Import Project="..\..\build\DevAnalyzers.props" />

+ 7 - 1
src/Avalonia.Base/Controls/ResourceNodeExtensions.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Diagnostics;
 using Avalonia.Reactive;
 using Avalonia.Styling;
 
@@ -75,12 +76,17 @@ namespace Avalonia.Controls
             control = control ?? throw new ArgumentNullException(nameof(control));
             key = key ?? throw new ArgumentNullException(nameof(key));
 
-            IResourceHost? current = control;
+            using var activity = Diagnostic.FindingResource()?
+                .AddTag(Diagnostic.Tags.Key, key)
+                .AddTag(Diagnostic.Tags.ThemeVariant, theme);
+
+            var current = control;
 
             while (current != null)
             {
                 if (current.TryGetResource(key, theme, out value))
                 {
+                    activity?.AddTag(Diagnostic.Tags.Result, true);
                     return true;
                 }
 

+ 25 - 0
src/Avalonia.Base/Diagnostics/Diagnostic.Activities.cs

@@ -0,0 +1,25 @@
+using System.Diagnostics;
+
+// ReSharper disable ExplicitCallerInfoArgument
+
+namespace Avalonia.Diagnostics;
+
+internal static partial class Diagnostic
+{
+    private static ActivitySource? s_activitySource;
+
+    public static void InitActivitySource()
+    {
+        s_activitySource = new("Avalonia.Diagnostic.Source");
+    }
+
+    private static Activity? StartActivity(string name) => s_activitySource?.StartActivity(name);
+
+    public static Activity? AttachingStyle() => StartActivity("Avalonia.AttachingStyle");
+    public static Activity? FindingResource() => StartActivity("Avalonia.FindingResource");
+    public static Activity? EvaluatingStyle() => StartActivity("Avalonia.EvaluatingStyle");
+    public static Activity? MeasuringLayoutable() => StartActivity("Avalonia.MeasuringLayoutable");
+    public static Activity? ArrangingLayoutable() => StartActivity("Avalonia.ArrangingLayoutable");
+    public static Activity? PerformingHitTest() => StartActivity("Avalonia.PerformingHitTest");
+    public static Activity? RaisingRoutedEvent() => StartActivity("Avalonia.RaisingRoutedEvent");
+}

+ 51 - 0
src/Avalonia.Base/Diagnostics/Diagnostic.Consts.cs

@@ -0,0 +1,51 @@
+namespace Avalonia.Diagnostics;
+
+internal static partial class Diagnostic
+{
+    public static class Meters
+    {
+        public const string SecondsUnit = "s";
+        public const string MillisecondsUnit = "ms";
+
+        public const string CompositorRenderPassName = "avalonia.comp.render.time";
+        public const string CompositorRenderPassDescription = "Duration of the compositor render pass on render thread";
+        public const string CompositorUpdatePassName = "avalonia.comp.update.time";
+        public const string CompositorUpdatePassDescription = "Duration of the compositor update pass on render thread";
+
+        public const string LayoutMeasurePassName = "avalonia.ui.measure.time";
+        public const string LayoutMeasurePassDescription = "Duration of layout measurement pass on UI thread";
+        public const string LayoutArrangePassName = "avalonia.ui.arrange.time";
+        public const string LayoutArrangePassDescription = "Duration of layout arrangement pass on UI thread";
+        public const string LayoutRenderPassName = "avalonia.ui.render.time";
+        public const string LayoutRenderPassDescription = "Duration of render recording pass on UI thread";
+        public const string LayoutInputPassName = "avalonia.ui.input.time";
+        public const string LayoutInputPassDescription = "Duration of input processing on UI thread";
+
+        public const string TotalEventHandleCountName = "avalonia.ui.event.handler.count";
+        public const string TotalEventHandleCountDescription = "Number of event handlers currently registered in the application";
+        public const string TotalEventHandleCountUnit = "{handler}";
+        public const string TotalVisualCountName = "avalonia.ui.visual.count";
+        public const string TotalVisualCountDescription = "Number of visual elements currently present in the visual tree";
+        public const string TotalVisualCountUnit = "{visual}";
+        public const string TotalDispatcherTimerCountName = "avalonia.ui.dispatcher.timer.count";
+        public const string TotalDispatcherTimerCountDescription = "Number of active dispatcher timers in the application";
+        public const string TotalDispatcherTimerCountUnit = "{timer}";
+    }
+
+    public static class Tags
+    {
+        public const string Style = nameof(Style);
+        public const string SelectorResult = nameof(SelectorResult);
+
+        public const string Key = nameof(Key);
+        public const string ThemeVariant = nameof(ThemeVariant);
+        public const string Result = nameof(Result);
+
+        public const string Activator = nameof(Activator);
+        public const string IsActive = nameof(IsActive);
+        public const string Selector = nameof(Selector);
+        public const string Control = nameof(Control);
+
+        public const string RoutedEvent = nameof(RoutedEvent);
+    }
+}

+ 94 - 0
src/Avalonia.Base/Diagnostics/Diagnostic.Metrics.cs

@@ -0,0 +1,94 @@
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+using Avalonia.Utilities;
+
+namespace Avalonia.Diagnostics;
+
+internal static partial class Diagnostic
+{
+    private static Histogram<double>? s_compositorRender;
+    private static Histogram<double>? s_compositorUpdate;
+    private static Histogram<double>? s_layoutMeasure;
+    private static Histogram<double>? s_layoutArrange;
+    private static Histogram<double>? s_layoutRender;
+    private static Histogram<double>? s_layoutInput;
+
+    public static void InitMetrics()
+    {
+        // Metrics
+        var meter = new Meter("Avalonia.Diagnostic.Meter");
+        s_compositorRender = meter.CreateHistogram<double>(
+            Meters.CompositorRenderPassName,
+            Meters.MillisecondsUnit,
+            Meters.CompositorRenderPassDescription);
+        s_compositorUpdate = meter.CreateHistogram<double>(
+            Meters.CompositorUpdatePassName,
+            Meters.MillisecondsUnit,
+            Meters.CompositorUpdatePassDescription);
+        s_layoutMeasure = meter.CreateHistogram<double>(
+            Meters.LayoutMeasurePassName,
+            Meters.MillisecondsUnit,
+            Meters.LayoutMeasurePassDescription);
+        s_layoutArrange = meter.CreateHistogram<double>(
+            Meters.LayoutArrangePassName,
+            Meters.MillisecondsUnit,
+            Meters.LayoutArrangePassDescription);
+        s_layoutRender = meter.CreateHistogram<double>(
+            Meters.LayoutRenderPassName,
+            Meters.MillisecondsUnit,
+            Meters.LayoutRenderPassDescription);
+        s_layoutInput = meter.CreateHistogram<double>(
+            Meters.LayoutInputPassName,
+            Meters.MillisecondsUnit,
+            Meters.LayoutInputPassDescription);
+        meter.CreateObservableUpDownCounter(
+            Meters.TotalEventHandleCountName,
+            () => Interactive.TotalHandlersCount,
+            Meters.TotalEventHandleCountUnit,
+            Meters.TotalEventHandleCountDescription);
+        meter.CreateObservableUpDownCounter(
+            Meters.TotalVisualCountName,
+            () => Visual.RootedVisualChildrenCount,
+            Meters.TotalVisualCountUnit,
+            Meters.TotalVisualCountDescription);
+        meter.CreateObservableUpDownCounter(
+            Meters.TotalDispatcherTimerCountName,
+            () => DispatcherTimer.ActiveTimersCount,
+            Meters.TotalDispatcherTimerCountUnit,
+            Meters.TotalDispatcherTimerCountDescription);
+    }
+
+    public static HistogramReportDisposable BeginCompositorRenderPass() => Begin(s_compositorRender);
+    public static HistogramReportDisposable BeginCompositorUpdatePass() => Begin(s_compositorUpdate);
+    public static HistogramReportDisposable BeginLayoutMeasurePass() => Begin(s_layoutMeasure);
+    public static HistogramReportDisposable BeginLayoutArrangePass() => Begin(s_layoutArrange);
+    public static HistogramReportDisposable BeginLayoutInputPass() => Begin(s_layoutInput);
+    public static HistogramReportDisposable BeginLayoutRenderPass() => Begin(s_layoutRender);
+
+    private static HistogramReportDisposable Begin(Histogram<double>? histogram) => histogram is not null ? new(histogram) : default;
+
+    internal readonly ref struct HistogramReportDisposable
+    {
+        private readonly Histogram<double> _histogram;
+        private readonly long _timestamp;
+
+        public HistogramReportDisposable(Histogram<double> histogram)
+        {
+            _histogram = histogram;
+            if (histogram.Enabled)
+            {
+                _timestamp = Stopwatch.GetTimestamp();
+            }
+        }
+
+        public void Dispose()
+        {
+            if (_timestamp > 0)
+            {
+                _histogram.Record(StopwatchHelper.GetElapsedTimeMs(_timestamp));
+            }
+        }
+    }
+}

+ 22 - 0
src/Avalonia.Base/Diagnostics/Diagnostic.cs

@@ -0,0 +1,22 @@
+using System;
+
+namespace Avalonia.Diagnostics;
+
+internal static partial class Diagnostic
+{
+    public static bool IsEnabled { get; }
+
+    private static bool InitializeIsEnabled() => AppContext.TryGetSwitch("Avalonia.Diagnostics.Diagnostic.IsEnabled", out var isEnabled) && isEnabled;
+
+    static Diagnostic()
+    {
+        IsEnabled = InitializeIsEnabled();
+        if (!IsEnabled)
+        {
+            return;
+        }
+
+        InitActivitySource();
+        InitMetrics();
+    }
+}

+ 5 - 0
src/Avalonia.Base/Interactivity/EventRoute.cs

@@ -1,5 +1,6 @@
 using System;
 using Avalonia.Collections.Pooled;
+using Avalonia.Diagnostics;
 
 namespace Avalonia.Interactivity
 {
@@ -80,6 +81,10 @@ namespace Avalonia.Interactivity
 
             e.Source = source;
 
+            using var _ = Diagnostic.RaisingRoutedEvent()?
+                .AddTag(Diagnostic.Tags.Control, e.Source)
+                .AddTag(Diagnostic.Tags.RoutedEvent, e.RoutedEvent);
+
             if (_event.RoutingStrategies == RoutingStrategies.Direct)
             {
                 e.Route = RoutingStrategies.Direct;

+ 3 - 0
src/Avalonia.Base/Interactivity/Interactive.cs

@@ -12,6 +12,7 @@ namespace Avalonia.Interactivity
     /// </summary>
     public class Interactive : Layoutable
     {
+        internal static int TotalHandlersCount { get; private set; }
         private Dictionary<RoutedEvent, List<EventSubscription>>? _eventHandlers;
 
         /// <summary>
@@ -90,6 +91,7 @@ namespace Avalonia.Interactivity
                     if (subscriptions[i].Handler == handler)
                     {
                         subscriptions.RemoveAt(i);
+                        TotalHandlersCount--;
                     }
                 }
             }
@@ -185,6 +187,7 @@ namespace Avalonia.Interactivity
             }
 
             subscriptions.Add(subscription);
+            TotalHandlersCount++;
         }
 
         private void AddToEventRoute(RoutedEvent routedEvent, EventRoute route)

+ 5 - 0
src/Avalonia.Base/Interactivity/RoutedEvent.cs

@@ -103,6 +103,11 @@ namespace Avalonia.Interactivity
         {
             _routeFinished.OnNext(e);
         }
+
+        public override string ToString()
+        {
+            return FormattableString.Invariant($"{OwnerType.Name}.{Name}");
+        }
     }
 
     public class RoutedEvent<TEventArgs> : RoutedEvent

+ 3 - 0
src/Avalonia.Base/Layout/LayoutManager.cs

@@ -2,6 +2,7 @@ using System;
 using System.Buffers;
 using System.Collections.Generic;
 using System.Diagnostics;
+using Avalonia.Diagnostics;
 using Avalonia.Logging;
 using Avalonia.Media;
 using Avalonia.Metadata;
@@ -246,6 +247,7 @@ namespace Avalonia.Layout
 
         private void ExecuteMeasurePass()
         {
+            using var _ = Diagnostic.BeginLayoutMeasurePass();
             while (_toMeasure.Count > 0)
             {
                 var control = _toMeasure.Dequeue();
@@ -261,6 +263,7 @@ namespace Avalonia.Layout
 
         private void ExecuteArrangePass()
         {
+            using var _ = Diagnostic.BeginLayoutArrangePass();
             while (_toArrange.Count > 0)
             {
                 var control = _toArrange.Dequeue();

+ 7 - 0
src/Avalonia.Base/Layout/Layoutable.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Diagnostics;
 using Avalonia.Logging;
 using Avalonia.Reactive;
 using Avalonia.VisualTree;
@@ -367,6 +368,9 @@ namespace Avalonia.Layout
 
             if (!IsMeasureValid || _previousMeasure != availableSize)
             {
+                using var activity = Diagnostic.MeasuringLayoutable()?
+                    .AddTag(Diagnostic.Tags.Control, this);
+
                 var previousDesiredSize = DesiredSize;
                 var desiredSize = default(Size);
 
@@ -417,6 +421,9 @@ namespace Avalonia.Layout
 
             if (!IsArrangeValid || _previousArrange != rect)
             {
+                using var activity = Diagnostic.ArrangingLayoutable()?
+                    .AddTag(Diagnostic.Tags.Control, this);
+
                 Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Arrange to {Rect} ", rect);
 
                 IsArrangeValid = true;

+ 5 - 1
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@@ -6,6 +6,7 @@ using System.Numerics;
 using System.Threading.Tasks;
 using Avalonia.Collections;
 using Avalonia.Collections.Pooled;
+using Avalonia.Diagnostics;
 using Avalonia.Media;
 using Avalonia.Rendering.Composition.Drawing;
 using Avalonia.Threading;
@@ -96,6 +97,8 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester
     /// <inheritdoc/>
     public IEnumerable<Visual> HitTest(Point p, Visual? root, Func<Visual, bool>? filter)
     {
+        using var _ = Diagnostic.PerformingHitTest();
+
         CompositionVisual? rootVisual = null;
         if (root != null)
         {
@@ -198,7 +201,8 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester
         _updating = true;
         try
         {
-            UpdateCore();
+            using (Diagnostic.BeginLayoutRenderPass())
+                UpdateCore();
         }
         finally
         {

+ 16 - 12
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using System.Threading;
 using Avalonia.Collections.Pooled;
+using Avalonia.Diagnostics;
 using Avalonia.Media;
 using Avalonia.Media.Imaging;
 using Avalonia.Media.Immutable;
@@ -122,22 +123,24 @@ namespace Avalonia.Rendering.Composition.Server
             Revision++;
 
             _overlays.MarkUpdateCallStart();
+            using (Diagnostic.BeginCompositorUpdatePass())
+            {
+                var transform = Matrix.CreateScale(Scaling, Scaling);
+                // Update happens in a separate phase to extend dirty rect if needed
+                Root.Update(this, transform);
 
-            var transform = Matrix.CreateScale(Scaling, Scaling);
-            // Update happens in a separate phase to extend dirty rect if needed
-            Root.Update(this, transform);
+                while (_adornerUpdateQueue.Count > 0)
+                {
+                    var adorner = _adornerUpdateQueue.Dequeue();
+                    adorner.Update(this, transform);
+                }
 
-            while (_adornerUpdateQueue.Count > 0)
-            {
-                var adorner = _adornerUpdateQueue.Dequeue();
-                adorner.Update(this, transform);
-            }
+                _updateRequested = false;
+                Readback.CompleteWrite(Revision);
 
-            _updateRequested = false;
-            Readback.CompleteWrite(Revision);
+                _overlays.MarkUpdateCallEnd();
+            }
 
-            _overlays.MarkUpdateCallEnd();
-            
             if (!_redrawRequested)
                 return;
 
@@ -151,6 +154,7 @@ namespace Avalonia.Rendering.Composition.Server
             
             using (var renderTargetContext = _renderTarget.CreateDrawingContextWithProperties(
                        this.PixelSize, out var properties))
+            using (var renderTiming = Diagnostic.BeginCompositorRenderPass())
             {
                 if(needLayer && (PixelSize != _layerSize || _layer == null || _layer.IsCorrupted))
                 {

+ 7 - 0
src/Avalonia.Base/Styling/ControlTheme.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Diagnostics;
+using Avalonia.Diagnostics;
 using Avalonia.PropertyStore;
 
 namespace Avalonia.Styling
@@ -46,12 +47,18 @@ namespace Avalonia.Styling
             if (TargetType is null)
                 throw new InvalidOperationException("ControlTheme has no TargetType.");
 
+            using var activity = Diagnostic.AttachingStyle()?
+                .AddTag(Diagnostic.Tags.Style, this);
+            
             if (HasSettersOrAnimations && TargetType.IsAssignableFrom(StyledElement.GetStyleKey(target)))
             {
                 Attach(target, null, type, true);
+                activity?.AddTag(Diagnostic.Tags.SelectorResult, SelectorMatchResult.AlwaysThisType);
+
                 return SelectorMatchResult.AlwaysThisType;
             }
 
+            activity?.AddTag(Diagnostic.Tags.SelectorResult, SelectorMatchResult.NeverThisType);
             return SelectorMatchResult.NeverThisType;
         }
     }

+ 6 - 0
src/Avalonia.Base/Styling/Style.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Diagnostics;
 using Avalonia.PropertyStore;
 
 namespace Avalonia.Styling
@@ -67,11 +68,16 @@ namespace Avalonia.Styling
 
             if (HasSettersOrAnimations)
             {
+                using var activity = Diagnostic.AttachingStyle()?
+                    .AddTag(Diagnostic.Tags.Style, this);
+
                 var match = Selector?.Match(target, Parent, true) ??
                     (target == host ?
                         SelectorMatch.AlwaysThisInstance :
                         SelectorMatch.NeverThisInstance);
 
+                activity?.AddTag(Diagnostic.Tags.SelectorResult, match.Result);
+
                 if (match.IsMatch)
                 {
                     Attach(target, match.Activator, type, Selector is not OrSelector);

+ 8 - 0
src/Avalonia.Base/Styling/StyleInstance.cs

@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using Avalonia.Animation;
 using Avalonia.Data;
+using Avalonia.Diagnostics;
 using Avalonia.PropertyStore;
 using Avalonia.Reactive;
 using Avalonia.Styling.Activators;
@@ -100,8 +101,15 @@ namespace Avalonia.Styling
                 _animationTrigger?.OnNext(_activator.GetIsActive());
             }
 
+            using var activity = _activator is null ? null : Diagnostic.EvaluatingStyle()?
+                .AddTag(Diagnostic.Tags.Activator, _activator)
+                .AddTag(Diagnostic.Tags.Selector, (Source as Style)?.Selector)
+                .AddTag(Diagnostic.Tags.Style, Source as StyleBase);
+
             _isActive = _activator?.GetIsActive() ?? true;
             hasChanged = _isActive != previous;
+
+            activity?.AddTag(Diagnostic.Tags.IsActive, _isActive);
             return _isActive;
         }
 

+ 4 - 0
src/Avalonia.Base/Threading/DispatcherTimer.cs

@@ -9,6 +9,8 @@ namespace Avalonia.Threading;
 /// </summary>
 public partial class DispatcherTimer
 {
+    internal static int ActiveTimersCount { get; private set; }
+
     /// <summary>
     ///     Creates a timer that uses theUI thread's Dispatcher2 to
     ///     process the timer event at background priority.
@@ -147,6 +149,7 @@ public partial class DispatcherTimer
             if (!_isEnabled)
             {
                 _isEnabled = true;
+                ActiveTimersCount++;
 
                 Restart();
             }
@@ -165,6 +168,7 @@ public partial class DispatcherTimer
             if (_isEnabled)
             {
                 _isEnabled = false;
+                ActiveTimersCount--;
                 updateOSTimer = true;
 
                 // If the operation is in the queue, abort it.

+ 4 - 0
src/Avalonia.Base/Utilities/StopwatchHelper.cs

@@ -10,10 +10,14 @@ namespace Avalonia.Utilities;
 internal static class StopwatchHelper
 {
     private static readonly double s_timestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency;
+    private static readonly double s_timestampToMs = s_timestampToTicks / TimeSpan.TicksPerMillisecond;
 
     public static TimeSpan GetElapsedTime(long startingTimestamp)
         => GetElapsedTime(startingTimestamp, Stopwatch.GetTimestamp());
 
     public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp)
         => new((long)((endingTimestamp - startingTimestamp) * s_timestampToTicks));
+
+    public static double GetElapsedTimeMs(long startingTimestamp)
+        => (Stopwatch.GetTimestamp() - startingTimestamp) * s_timestampToMs;
 }

+ 5 - 0
src/Avalonia.Base/Visual.cs

@@ -7,6 +7,7 @@ using System.Collections;
 using System.Collections.Specialized;
 using Avalonia.Collections;
 using Avalonia.Data;
+using Avalonia.Diagnostics;
 using Avalonia.Logging;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
@@ -31,6 +32,8 @@ namespace Avalonia
     [UsableDuringInitialization]
     public partial class Visual : StyledElement, IAvaloniaListItemValidator<Visual>
     {
+        internal static int RootedVisualChildrenCount { get; private set; }
+
         /// <summary>
         /// Defines the <see cref="Bounds"/> property.
         /// </summary>
@@ -493,6 +496,7 @@ namespace Avalonia
             Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Attached to visual tree");
 
             _visualRoot = e.Root;
+            RootedVisualChildrenCount++;
             if (_visualParent is null)
             {
                 throw new InvalidOperationException("Visual was attached to the root without being added to the visual parent first.");
@@ -541,6 +545,7 @@ namespace Avalonia
             Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Detached from visual tree");
 
             _visualRoot = null;
+            RootedVisualChildrenCount--;
 
             if (RenderTransform is IMutableTransform mutableTransform)
             {

+ 3 - 0
src/Avalonia.Controls/TopLevel.cs

@@ -23,6 +23,7 @@ using Avalonia.Utilities;
 using Avalonia.Input.Platform;
 using System.Linq;
 using System.Threading.Tasks;
+using Avalonia.Diagnostics;
 using Avalonia.Rendering.Composition;
 using Avalonia.Threading;
 
@@ -839,6 +840,8 @@ namespace Avalonia.Controls
             {
                 Dispatcher.UIThread.Send(static state =>
                 {
+                    using var _ = Diagnostic.BeginLayoutInputPass();
+
                     var (topLevel, e) = (ValueTuple<TopLevel, RawInputEventArgs>)state!;
                     if (e is RawPointerEventArgs pointerArgs)
                     {

+ 5 - 0
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Reflection;
 using System.Runtime.CompilerServices;
 using Avalonia.Controls;
+using Avalonia.Diagnostics;
 using Avalonia.Markup.Data;
 using Avalonia.Markup.Xaml.Converters;
 using Avalonia.Markup.Xaml.XamlIl.Runtime;
@@ -64,6 +65,10 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
             // which might be able to give us the resource.
             if (stack is not null)
             {
+                using var activity = Diagnostic.FindingResource()?
+                    .AddTag(Diagnostic.Tags.Key, resourceKey)
+                    .AddTag(Diagnostic.Tags.ThemeVariant, themeVariant);
+
                 // avoid allocations iterating the parents when possible
                 if (stack is IAvaloniaXamlIlEagerParentStackProvider eagerStack)
                 {

+ 0 - 1
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

@@ -10,7 +10,6 @@
   <ItemGroup>
     <ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
   </ItemGroup>
-  <Import Project="..\..\..\build\System.Memory.props" />
   <Import Project="..\..\..\build\NullableEnable.props" />
   <Import Project="..\..\..\build\DevAnalyzers.props" />
   <Import Project="..\..\..\build\TrimmingEnable.props" />

+ 0 - 1
tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj

@@ -11,7 +11,6 @@
   <Import Project="..\..\build\XUnit.props" />
   <Import Project="..\..\build\Rx.props" />
   <Import Project="..\..\build\Microsoft.Reactive.Testing.props" />
-  <Import Project="..\..\build\Base.props" />
   <Import Project="..\..\build\SharedVersion.props" />
   <ItemGroup>
     <ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />