From 6904a137e17c47e982a6efccd1bc17a820944b59 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 19 Apr 2021 17:43:46 +0200 Subject: [PATCH] Device visualizer - Fixed memory leak Device visualizer - Improved performance on very large devices Dynamic data model - Reworked API --- .../Events/DynamicDataModelChildEventArgs.cs | 4 +- .../Profile/DataModel/DataModelPathSegment.cs | 14 +- .../Plugins/DataModelExpansions/DataModel.cs | 362 +++++++++--------- .../DataModelExpansions/DynamicChild.cs | 66 ++++ src/Artemis.Core/Services/RgbService.cs | 2 +- .../Controls/DeviceVisualizer.cs | 10 +- .../Controls/DeviceVisualizerLed.cs | 15 +- 7 files changed, 274 insertions(+), 199 deletions(-) create mode 100644 src/Artemis.Core/Plugins/DataModelExpansions/DynamicChild.cs diff --git a/src/Artemis.Core/Events/DynamicDataModelChildEventArgs.cs b/src/Artemis.Core/Events/DynamicDataModelChildEventArgs.cs index 61e69c6d1..9248a716a 100644 --- a/src/Artemis.Core/Events/DynamicDataModelChildEventArgs.cs +++ b/src/Artemis.Core/Events/DynamicDataModelChildEventArgs.cs @@ -8,7 +8,7 @@ namespace Artemis.Core /// public class DynamicDataModelChildEventArgs : EventArgs { - internal DynamicDataModelChildEventArgs(object? dynamicChild, string key) + internal DynamicDataModelChildEventArgs(DynamicChild dynamicChild, string key) { DynamicChild = dynamicChild; Key = key; @@ -17,7 +17,7 @@ namespace Artemis.Core /// /// Gets the dynamic data model child /// - public object? DynamicChild { get; } + public DynamicChild DynamicChild { get; } /// /// Gets the key of the dynamic data model on the parent diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs index f78bc7c54..27f4294ed 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs @@ -15,8 +15,8 @@ namespace Artemis.Core { private Expression>? _accessorLambda; private DataModel? _dynamicDataModel; - private Type _dynamicDataModelType; - private DataModelPropertyAttribute _dynamicDataModelAttribute; + private Type? _dynamicDataModelType; + private DataModelPropertyAttribute? _dynamicDataModelAttribute; internal DataModelPathSegment(DataModelPath dataModelPath, string identifier, string path) { @@ -184,8 +184,8 @@ namespace Artemis.Core // If a dynamic data model is found the use that bool hasDynamicChild = _dynamicDataModel.DynamicChildren.TryGetValue(Identifier, out DynamicChild? dynamicChild); - if (hasDynamicChild && dynamicChild?.Value != null) - DetermineDynamicType(dynamicChild.Value, dynamicChild.Attribute); + if (hasDynamicChild && dynamicChild?.BaseValue != null) + DetermineDynamicType(dynamicChild.BaseValue, dynamicChild.Attribute); _dynamicDataModel.DynamicChildAdded += DynamicChildOnDynamicChildAdded; _dynamicDataModel.DynamicChildRemoved += DynamicChildOnDynamicChildRemoved; @@ -212,12 +212,14 @@ namespace Artemis.Core accessorExpression = Expression.PropertyOrField(expression, Identifier); // A dynamic segment calls the generic method DataModel.DynamicChild and provides the identifier as an argument else + { accessorExpression = Expression.Call( expression, - nameof(DataModel.DynamicChild), + nameof(DataModel.GetDynamicChildValue), _dynamicDataModelType != null ? new[] { _dynamicDataModelType } : null, Expression.Constant(Identifier) ); + } _accessorLambda = Expression.Lambda>( // Wrap with a null check @@ -290,7 +292,7 @@ namespace Artemis.Core private void DynamicChildOnDynamicChildRemoved(object? sender, DynamicDataModelChildEventArgs e) { - if (e.DynamicChild == _dynamicDataModel) + if (e.DynamicChild.BaseValue == _dynamicDataModel) DataModelPath.Initialize(); } diff --git a/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs b/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs index 392995a4c..2013a9e25 100644 --- a/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs +++ b/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using Artemis.Core.Modules; @@ -66,183 +67,6 @@ namespace Artemis.Core.DataModelExpansions return new List().AsReadOnly(); } - /// - /// Adds a dynamic child to this data model - /// - /// The dynamic child to add - /// The key of the child, must be unique to this data model - /// An optional human readable name, if not provided the key will be used in a humanized form - public T AddDynamicChild(T dynamicChild, string key, string? name = null) - { - if (dynamicChild == null) - throw new ArgumentNullException(nameof(dynamicChild)); - if (key == null) - throw new ArgumentNullException(nameof(key)); - if (key.Contains('.')) - throw new ArtemisCoreException("The provided key contains an illegal character (.)"); - if (_dynamicChildren.ContainsKey(key)) - { - throw new ArtemisCoreException($"Cannot add a dynamic child with key '{key}' " + - "because the key is already in use on by another dynamic property this data model."); - } - - if (GetType().GetProperty(key) != null) - { - throw new ArtemisCoreException($"Cannot add a dynamic child with key '{key}' " + - "because the key is already in use by a static property on this data model."); - } - - DataModelPropertyAttribute attribute = new() - { - Name = string.IsNullOrWhiteSpace(name) ? key.Humanize() : name - }; - if (dynamicChild is DataModel dynamicDataModel) - { - dynamicDataModel.Feature = Feature; - dynamicDataModel.DataModelDescription = attribute; - } - - _dynamicChildren.Add(key, new DynamicChild(attribute, dynamicChild)); - - OnDynamicDataModelAdded(new DynamicDataModelChildEventArgs(dynamicChild, key)); - return dynamicChild; - } - - /// - /// Adds a dynamic child to this data model - /// - /// The dynamic child to add - /// The key of the child, must be unique to this data model - /// A human readable for your dynamic child, shown in the UI - /// An optional description, shown in the UI - public T AddDynamicChild(T dynamicChild, string key, string name, string description) - { - if (dynamicChild == null) - throw new ArgumentNullException(nameof(dynamicChild)); - if (key == null) - throw new ArgumentNullException(nameof(key)); - if (key.Contains('.')) - throw new ArtemisCoreException("The provided key contains an illegal character (.)"); - if (_dynamicChildren.ContainsKey(key)) - { - throw new ArtemisCoreException($"Cannot add a dynamic child with key '{key}' " + - "because the key is already in use on by another dynamic property this data model."); - } - - if (GetType().GetProperty(key) != null) - { - throw new ArtemisCoreException($"Cannot add a dynamic child with key '{key}' " + - "because the key is already in use by a static property on this data model."); - } - - DataModelPropertyAttribute attribute = new() - { - Name = string.IsNullOrWhiteSpace(name) ? key.Humanize() : name, - Description = description - }; - if (dynamicChild is DataModel dynamicDataModel) - { - dynamicDataModel.Feature = Feature; - dynamicDataModel.DataModelDescription = attribute; - } - - _dynamicChildren.Add(key, new DynamicChild(attribute, dynamicChild)); - - OnDynamicDataModelAdded(new DynamicDataModelChildEventArgs(dynamicChild, key)); - return dynamicChild; - } - - /// - /// Adds a dynamic child to this data model - /// - /// The dynamic child to add - /// The key of the child, must be unique to this data model - /// A data model property attribute describing the dynamic child - public T AddDynamicChild(T dynamicChild, string key, DataModelPropertyAttribute attribute) - { - if (dynamicChild == null) throw new ArgumentNullException(nameof(dynamicChild)); - if (key == null) throw new ArgumentNullException(nameof(key)); - if (attribute == null) throw new ArgumentNullException(nameof(attribute)); - if (key.Contains('.')) - throw new ArtemisCoreException("The provided key contains an illegal character (.)"); - if (_dynamicChildren.ContainsKey(key)) - { - throw new ArtemisCoreException($"Cannot add a dynamic child with key '{key}' " + - "because the key is already in use on by another dynamic property this data model."); - } - - if (GetType().GetProperty(key) != null) - { - throw new ArtemisCoreException($"Cannot add a dynamic child with key '{key}' " + - "because the key is already in use by a static property on this data model."); - } - - // Make sure a name is on the attribute or funny things might happen - attribute.Name ??= key.Humanize(); - if (dynamicChild is DataModel dynamicDataModel) - { - dynamicDataModel.Feature = Feature; - dynamicDataModel.DataModelDescription = attribute; - } - - _dynamicChildren.Add(key, new DynamicChild(attribute, dynamicChild)); - - OnDynamicDataModelAdded(new DynamicDataModelChildEventArgs(dynamicChild, key)); - return dynamicChild; - } - - /// - /// Removes a dynamic child from the data model by its key - /// - /// The key of the dynamic child to remove - public void RemoveDynamicChildByKey(string key) - { - if (!_dynamicChildren.TryGetValue(key, out DynamicChild? dynamicChild)) - return; - - _dynamicChildren.Remove(key); - OnDynamicDataModelRemoved(new DynamicDataModelChildEventArgs(dynamicChild.Value, key)); - } - - /// - /// Removes a dynamic child from this data model - /// - /// The dynamic data child to remove - public void RemoveDynamicChild(T dynamicChild) where T : class - { - List keys = _dynamicChildren.Where(kvp => kvp.Value.Value == dynamicChild).Select(kvp => kvp.Key).ToList(); - foreach (string key in keys) - { - _dynamicChildren.Remove(key); - OnDynamicDataModelRemoved(new DynamicDataModelChildEventArgs(dynamicChild, key)); - } - } - - /// - /// Removes all dynamic children from this data model - /// - public void ClearDynamicChildren() - { - while (_dynamicChildren.Any()) - RemoveDynamicChildByKey(_dynamicChildren.First().Key); - } - - /// - /// Gets a dynamic child of type by its key - /// - /// The type of data model you expect - /// The unique key of the dynamic child - /// If found, the dynamic child otherwise null - public T? DynamicChild(string key) - { - if (!_dynamicChildren.TryGetValue(key, out DynamicChild? dynamicChild)) - return default; - - if (dynamicChild.Value is not T) - return default; - return (T?) dynamicChild.Value; - } - /// /// Occurs when a dynamic child has been added to this data model /// @@ -268,10 +92,184 @@ namespace Artemis.Core.DataModelExpansions { DynamicChildRemoved?.Invoke(this, e); } - } - /// - /// Represents a record of a dynamic child value with its property attribute - /// - public record DynamicChild(DataModelPropertyAttribute Attribute, object? Value); + #region Dynamic children + + /// + /// Adds a dynamic child to this data model + /// + /// The key of the child, must be unique to this data model + /// The initial value of the dynamic child + /// The resulting dynamic child which can be used to further update the value + public DynamicChild AddDynamicChild(string key, T initialValue) + { + return AddDynamicChild(key, initialValue, new DataModelPropertyAttribute()); + } + + /// + /// Adds a dynamic child to this data model + /// + /// The key of the child, must be unique to this data model + /// The initial value of the dynamic child + /// A human readable name for your dynamic child, shown in the UI + /// An optional description, shown in the UI + /// The resulting dynamic child which can be used to further update the value + public DynamicChild AddDynamicChild(string key, T initialValue, string name, string? description = null) + { + return AddDynamicChild(key, initialValue, new DataModelPropertyAttribute {Name = name, Description = description}); + } + + /// + /// Adds a dynamic child to this data model + /// + /// The key of the child, must be unique to this data model + /// The initial value of the dynamic child + /// A data model property attribute describing the dynamic child + /// The resulting dynamic child which can be used to further update the value + public DynamicChild AddDynamicChild(string key, T initialValue, DataModelPropertyAttribute attribute) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + if (initialValue == null) throw new ArgumentNullException(nameof(initialValue)); + if (attribute == null) throw new ArgumentNullException(nameof(attribute)); + if (key.Contains('.')) + throw new ArtemisCoreException("The provided key contains an illegal character (.)"); + if (_dynamicChildren.ContainsKey(key)) + throw new ArtemisCoreException($"Cannot add a dynamic child with key '{key}' " + + "because the key is already in use on by another dynamic property this data model."); + + if (GetType().GetProperty(key) != null) + throw new ArtemisCoreException($"Cannot add a dynamic child with key '{key}' " + + "because the key is already in use by a static property on this data model."); + + // Make sure a name is on the attribute or funny things might happen + attribute.Name ??= key.Humanize(); + if (initialValue is DataModel dynamicDataModel) + { + dynamicDataModel.Feature = Feature; + dynamicDataModel.DataModelDescription = attribute; + } + + DynamicChild dynamicChild = new(initialValue, key, attribute); + _dynamicChildren.Add(key, dynamicChild); + + OnDynamicDataModelAdded(new DynamicDataModelChildEventArgs(dynamicChild, key)); + return dynamicChild; + } + + /// + /// Gets a previously added dynamic child by its key + /// + /// The key of the dynamic child + public DynamicChild GetDynamicChild(string key) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + return DynamicChildren[key]; + } + + /// + /// Gets a previously added dynamic child by its key + /// + /// The typer of dynamic child you are expecting + /// The key of the dynamic child + /// + public DynamicChild GetDynamicChild(string key) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + return (DynamicChild) DynamicChildren[key]; + } + + /// + /// Gets a previously added dynamic child by its key + /// + /// The key of the dynamic child + /// + /// When this method returns, the associated with the specified key, + /// if the key is found; otherwise, . This parameter is passed uninitialized. + /// + /// + /// if the data model contains the dynamic child; otherwise + /// + public bool TryGetDynamicChild(string key, [MaybeNullWhen(false)] out DynamicChild dynamicChild) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + + dynamicChild = null; + if (!DynamicChildren.TryGetValue(key, out DynamicChild? value)) + return false; + + dynamicChild = value; + return true; + } + + /// + /// Gets a previously added dynamic child by its key + /// + /// The typer of dynamic child you are expecting + /// The key of the dynamic child + /// + /// When this method returns, the associated with the specified + /// key, if the key is found; otherwise, . This parameter is passed uninitialized. + /// + /// + /// if the data model contains the dynamic child; otherwise + /// + public bool TryGetDynamicChild(string key, [MaybeNullWhen(false)] out DynamicChild dynamicChild) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + + dynamicChild = null; + if (!DynamicChildren.TryGetValue(key, out DynamicChild? value)) + return false; + if (value is not DynamicChild typedDynamicChild) + return false; + dynamicChild = typedDynamicChild; + return true; + } + + /// + /// Removes a dynamic child from the data model by its key + /// + /// The key of the dynamic child to remove + public void RemoveDynamicChildByKey(string key) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + if (!_dynamicChildren.TryGetValue(key, out DynamicChild? dynamicChild)) + return; + + _dynamicChildren.Remove(key); + OnDynamicDataModelRemoved(new DynamicDataModelChildEventArgs(dynamicChild, key)); + } + + /// + /// Removes a dynamic child from this data model + /// + /// The dynamic data child to remove + public void RemoveDynamicChild(DynamicChild dynamicChild) + { + if (dynamicChild == null) throw new ArgumentNullException(nameof(dynamicChild)); + List keys = _dynamicChildren.Where(kvp => kvp.Value.BaseValue == dynamicChild).Select(kvp => kvp.Key).ToList(); + foreach (string key in keys) + { + _dynamicChildren.Remove(key); + OnDynamicDataModelRemoved(new DynamicDataModelChildEventArgs(dynamicChild, key)); + } + } + + /// + /// Removes all dynamic children from this data model + /// + public void ClearDynamicChildren() + { + while (_dynamicChildren.Any()) + RemoveDynamicChildByKey(_dynamicChildren.First().Key); + } + + // Used a runtime by data model paths only + internal T? GetDynamicChildValue(string key) + { + return TryGetDynamicChild(key, out DynamicChild? dynamicChild) ? dynamicChild.Value : default; + } + + #endregion + } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/DataModelExpansions/DynamicChild.cs b/src/Artemis.Core/Plugins/DataModelExpansions/DynamicChild.cs new file mode 100644 index 000000000..5e5732b55 --- /dev/null +++ b/src/Artemis.Core/Plugins/DataModelExpansions/DynamicChild.cs @@ -0,0 +1,66 @@ +using System; + +namespace Artemis.Core.DataModelExpansions +{ + /// + /// Represents a dynamic child value with its property attribute + /// + public class DynamicChild : DynamicChild + { + internal DynamicChild(T value, string key, DataModelPropertyAttribute attribute) : base(key, attribute, typeof(T)) + { + Value = value; + } + + /// + /// Gets or sets the current value of the dynamic child + /// + public new T Value { get; set; } + + /// + protected override object? GetValue() + { + return Value; + } + } + + /// + /// Represents a dynamic child value with its property attribute + /// + public abstract class DynamicChild + { + internal DynamicChild(string key, DataModelPropertyAttribute attribute, Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + Key = key ?? throw new ArgumentNullException(nameof(key)); + Attribute = attribute ?? throw new ArgumentNullException(nameof(attribute)); + Type = type; + } + + /// + /// Gets the key of the dynamic child + /// + public string Key { get; } + + /// + /// Gets the attribute describing the dynamic child + /// + public DataModelPropertyAttribute Attribute { get; } + + /// + /// Gets the type of + /// + public Type Type { get; } + + /// + /// Gets the current value of the dynamic child + /// + public object? BaseValue => GetValue(); + + /// + /// Gets the current value of the dynamic child + /// + /// The current value of the dynamic child + protected abstract object? GetValue(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs index 5e4a5066a..26c66ed54 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -113,7 +113,7 @@ namespace Artemis.Core.Services private void SurfaceOnException(ExceptionEventArgs args) { - _logger.Warning("Surface threw e"); + _logger.Warning(args.Exception, "Surface caught exception"); throw args.Exception; } diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs index fb3b77506..e3fd07f40 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs @@ -314,6 +314,7 @@ namespace Artemis.UI.Shared ImageBrush bitmapBrush = new(bitmap); bitmapBrush.Freeze(); _backingStore.OpacityMask = bitmapBrush; + _backingStore.Children.Clear(); InvalidateMeasure(); } @@ -331,14 +332,19 @@ namespace Artemis.UI.Shared private void Render() { DrawingContext drawingContext = _backingStore.Append(); - // DrawingContext drawingContext = _backingStore.Open(); if (HighlightedLeds != null && HighlightedLeds.Any()) + { + // Faster on large devices, maybe a bit slower on smaller ones but that's ok + ILookup toHighlight = HighlightedLeds.ToLookup(l => l); foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) - deviceVisualizerLed.RenderColor(_backingStore, drawingContext, !HighlightedLeds.Contains(deviceVisualizerLed.Led)); + deviceVisualizerLed.RenderColor(_backingStore, drawingContext, !toHighlight.Contains(deviceVisualizerLed.Led)); + } else + { foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) deviceVisualizerLed.RenderColor(_backingStore, drawingContext, false); + } drawingContext.Close(); } diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs index 45dc4ac41..cf0699306 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs @@ -14,10 +14,10 @@ namespace Artemis.UI.Shared { private const byte Dimmed = 100; private const byte NonDimmed = 255; - private GeometryDrawing? _geometryDrawing; private Color _renderColor; - + private GeometryDrawing? _geometryDrawing; private SolidColorBrush? _renderColorBrush; + private DrawingGroup? _lastBackingStore; public DeviceVisualizerLed(ArtemisLed led) { @@ -43,12 +43,12 @@ namespace Artemis.UI.Shared public void RenderColor(DrawingGroup backingStore, DrawingContext drawingContext, bool isDimmed) { - if (DisplayGeometry == null) + if (DisplayGeometry == null || backingStore == null) return; _renderColorBrush ??= new SolidColorBrush(); _geometryDrawing ??= new GeometryDrawing(_renderColorBrush, null, new RectangleGeometry(LedRect)); - + byte r = Led.RgbLed.Color.GetR(); byte g = Led.RgbLed.Color.GetG(); byte b = Led.RgbLed.Color.GetB(); @@ -59,9 +59,12 @@ namespace Artemis.UI.Shared _renderColor.B = b; _renderColorBrush.Color = _renderColor; - - if (!backingStore.Children.Contains(_geometryDrawing)) + if (_lastBackingStore != backingStore) + { backingStore.Children.Add(_geometryDrawing); + _lastBackingStore = backingStore; + } + } public void RenderImage(DrawingContext drawingContext)