From 8b73333381288c59d237dbfbb7867d8f00621832 Mon Sep 17 00:00:00 2001 From: Artem Marakhovskyi Date: Sat, 7 Jun 2025 16:23:53 +0200 Subject: [PATCH 1/5] no message --- FluidSharp/Configuration/Config.cs | 12 ++++++ FluidSharp/Diagnostics/VisualFrame.cs | 60 +++++++++++++++++++++++++++ FluidSharp/Layouts/LayoutSurface.cs | 10 ++++- FluidSharp/Widgets/Widget.cs | 32 ++++++++++---- 4 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 FluidSharp/Configuration/Config.cs create mode 100644 FluidSharp/Diagnostics/VisualFrame.cs diff --git a/FluidSharp/Configuration/Config.cs b/FluidSharp/Configuration/Config.cs new file mode 100644 index 0000000..4784aa9 --- /dev/null +++ b/FluidSharp/Configuration/Config.cs @@ -0,0 +1,12 @@ +namespace FluidSharp.Configuration; + +public sealed class Config +{ + private static readonly Config _instance = new Config(); + + public static Config Instance => _instance; + + private Config() { } + + public bool IsDiagnosticsEnabled { get; set; } +} \ No newline at end of file diff --git a/FluidSharp/Diagnostics/VisualFrame.cs b/FluidSharp/Diagnostics/VisualFrame.cs new file mode 100644 index 0000000..f0d4347 --- /dev/null +++ b/FluidSharp/Diagnostics/VisualFrame.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluidSharp.Widgets; +using SkiaSharp; + +namespace FluidSharp.Diagnostics; + +public static class VisualFrameProvider +{ + public static VisualFrame LastVisualFrame; + + public static VisualFrame CreateVisualFrame() + { + LastVisualFrame = new VisualFrame + { + Timestamp = DateTime.UtcNow + }; + return LastVisualFrame; + } +} + +public class VisualFrame +{ + public DateTime Timestamp { get; set; } + public List<(SKRect, VisualFrameElement)> Children { get; } = new(); + + public void AddChild(SKRect rect, Widget element) + { + Children.Add((rect, new VisualFrameElement(element))); + } + + public override string ToString() + { + return System.Text.Json.JsonSerializer.Serialize( + new + { + Timestamp, + Children = + Children + .Select(x => new + { + Rect = new + { + Left = x.Item1.Left, + Right = x.Item1.Right, + Top = x.Item1.Top, + Bottom = x.Item1.Bottom + }, + x.Item2.widget.TestId, + x.Item2.widget.DebugTag, + x.Item2.widget.TestText, + x.Item2.widget.GetType().Name + }) + .ToList() + }); + } + + public record VisualFrameElement(Widget widget); +} \ No newline at end of file diff --git a/FluidSharp/Layouts/LayoutSurface.cs b/FluidSharp/Layouts/LayoutSurface.cs index f4e4071..5150056 100644 --- a/FluidSharp/Layouts/LayoutSurface.cs +++ b/FluidSharp/Layouts/LayoutSurface.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Text; +using FluidSharp.Diagnostics; namespace FluidSharp { @@ -34,6 +35,8 @@ public class LayoutSurface public virtual void SetCanvas(SKCanvas canvas) { Canvas = canvas; } protected void SetThisCanvas(SKCanvas canvas) { Canvas = canvas; } + private VisualFrame _visualFrame = VisualFrameProvider.CreateVisualFrame(); + public LayoutSurface(Device device, MeasureCache measureCache, SKCanvas canvas, VisualState visualState) { Device = device; @@ -58,7 +61,11 @@ public virtual SKRect Paint(Widget widget, SKRect rect) } } - var result = widget.PaintInternal(this, rect); + var result = Configuration.Config.Instance.IsDiagnosticsEnabled + ? widget.PaintInternalWithDiagnostics(this, rect) + : widget.PaintInternal(this, rect); + + if (Canvas != canvas) { @@ -204,5 +211,6 @@ public void DebugSpacing(SKRect dest, Func text, SKColor color) } + public void TrackVisualFrame(SKRect rect, Widget widget) => _visualFrame.AddChild(rect, widget); } } diff --git a/FluidSharp/Widgets/Widget.cs b/FluidSharp/Widgets/Widget.cs index 44152da..d72517a 100644 --- a/FluidSharp/Widgets/Widget.cs +++ b/FluidSharp/Widgets/Widget.cs @@ -1,17 +1,16 @@ using FluidSharp.Layouts; using SkiaSharp; using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Input; +using Config = FluidSharp.Configuration.Config; namespace FluidSharp.Widgets { public abstract class Widget { - - + public string? TestId { get; set; } + public string? TestText { get; set; } + public virtual string? Tag { get; set; } + public static Action? WidgetAllocated; #if DEBUG @@ -19,9 +18,26 @@ public abstract class Widget public bool IsNew = true; #endif - public abstract SKSize Measure(MeasureCache measureCache, SKSize boundaries); + public bool ShouldBeTrackedForDiagnostics => + Config.Instance.IsDiagnosticsEnabled && + (TestId != null || TestText != null || Tag != null); + + public abstract SKSize Measure(MeasureCache measureCache, + SKSize boundaries); - public abstract SKRect PaintInternal(LayoutSurface layoutsurface, SKRect rect); + public SKRect PaintInternalWithDiagnostics(LayoutSurface layoutsurface, + SKRect rect) + { + if (Config.Instance.IsDiagnosticsEnabled && ShouldBeTrackedForDiagnostics) + { + layoutsurface.TrackVisualFrame(rect, this); + } + + return PaintInternal(layoutsurface, rect); + } + + public abstract SKRect PaintInternal(LayoutSurface layoutsurface, + SKRect rect); public Widget() From cf6e3689b4ae5389c64e2693fda3bc5df608b6d7 Mon Sep 17 00:00:00 2001 From: Artem Marakhovskyi Date: Mon, 7 Jul 2025 20:38:41 +0200 Subject: [PATCH 2/5] referencing local project --- FluidSharp/FluidSharp.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FluidSharp/FluidSharp.csproj b/FluidSharp/FluidSharp.csproj index 27a3c2d..7e03d4c 100644 --- a/FluidSharp/FluidSharp.csproj +++ b/FluidSharp/FluidSharp.csproj @@ -27,7 +27,7 @@ - + From 45ba6f055f5e515b4eccaad67082bf2edf03f30d Mon Sep 17 00:00:00 2001 From: Artem Marakhovskyi Date: Tue, 15 Jul 2025 22:08:33 +0200 Subject: [PATCH 3/5] no message --- .../Diagnostics/FrameDiagnosticsView.cs | 28 ++++++++ FluidSharp/Configuration/Config.cs | 51 +++++++++++++- FluidSharp/Diagnostics/TestSessionClient.cs | 66 +++++++++++++++++++ FluidSharp/Diagnostics/VisualFrame.cs | 47 ++++++++++--- 4 files changed, 181 insertions(+), 11 deletions(-) create mode 100644 FluidSharp.Views/FluidSharp.Views.Android/Diagnostics/FrameDiagnosticsView.cs create mode 100644 FluidSharp/Diagnostics/TestSessionClient.cs diff --git a/FluidSharp.Views/FluidSharp.Views.Android/Diagnostics/FrameDiagnosticsView.cs b/FluidSharp.Views/FluidSharp.Views.Android/Diagnostics/FrameDiagnosticsView.cs new file mode 100644 index 0000000..bfbf12f --- /dev/null +++ b/FluidSharp.Views/FluidSharp.Views.Android/Diagnostics/FrameDiagnosticsView.cs @@ -0,0 +1,28 @@ +using Android.Content; +using Android.Widget; + +namespace FluidSharp.Views.Android.Diagnostics; + +public class FrameDiagnosticsView : LinearLayout +{ + public EditText FrameDiagnosticsField { get; private set; } + public Button ActionButton { get; private set; } + + public FrameDiagnosticsView(Context context) : base(context) + { + Orientation = Orientation.Vertical; + SetPadding(32, 32, 32, 32); + + var label = new TextView(context) + { + Text = "Diagnostics:" + }; + AddView(label); + + FrameDiagnosticsField = new EditText(context) + { + Id = Resources.GetIdentifier("FrameDiagnosticsField", "id", context.PackageName) + }; + AddView(FrameDiagnosticsField); + } +} \ No newline at end of file diff --git a/FluidSharp/Configuration/Config.cs b/FluidSharp/Configuration/Config.cs index 4784aa9..c58fccd 100644 --- a/FluidSharp/Configuration/Config.cs +++ b/FluidSharp/Configuration/Config.cs @@ -1,12 +1,61 @@ +using System; +using System.Diagnostics.Metrics; +using System.Threading.Tasks; +using FluidSharp.Diagnostics; +using FluidSharp.Layouts; + namespace FluidSharp.Configuration; public sealed class Config { - private static readonly Config _instance = new Config(); + private static Config _instance = new Config(); public static Config Instance => _instance; private Config() { } + private TestSessionsClient _testSessionsClient; public bool IsDiagnosticsEnabled { get; set; } + + public static void EnableDiagnostics( + string snapshotsBackendUri, + string apiKey, + string deviceKey, + float screenScale, + Margins margin, + Action onError) + { + var instance = new Config(); + instance.IsDiagnosticsEnabled = true; + instance._testSessionsClient = new TestSessionsClient(snapshotsBackendUri, apiKey, margin, screenScale); + + _ = Task.Run(async () => + { + var lastVisualFrameTimestamp = DateTime.MinValue; + while (instance.IsDiagnosticsEnabled) + { + var visualFrame = VisualFrameProvider.LastVisualFrame; + + if (visualFrame is null || visualFrame.Timestamp <= lastVisualFrameTimestamp) + { + await Task.Delay(1000); + continue; + } + + try + { + await instance._testSessionsClient.PostSessionTreeAsync( + deviceKey, visualFrame); + } + catch (Exception e) + { + onError(e); + } + + await Task.Delay(1000); + } + }); + + _instance = instance; + } } \ No newline at end of file diff --git a/FluidSharp/Diagnostics/TestSessionClient.cs b/FluidSharp/Diagnostics/TestSessionClient.cs new file mode 100644 index 0000000..9de3191 --- /dev/null +++ b/FluidSharp/Diagnostics/TestSessionClient.cs @@ -0,0 +1,66 @@ +using FluidSharp.Layouts; + +namespace FluidSharp.Diagnostics; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Threading.Tasks; + +public class TestSessionsClient +{ + private readonly HttpClient _httpClient; + private readonly string _baseUrl; + private readonly string _apiKey; + private readonly float _screenScale; + private readonly Margins _margins; + + public TestSessionsClient(string baseUrl, string apiKey, Margins margins, float screenScale) + { + _baseUrl = baseUrl; + _apiKey = apiKey; + _screenScale = screenScale; + _margins = margins; + _httpClient = new HttpClient(); + } + + private void AddApiKeyHeader(HttpRequestMessage request) + { + request.Headers.Remove("x-api-key"); + request.Headers.Add("x-api-key", _apiKey); + } + + public async Task PostSessionTreeAsync(string deviceKey, object tree) + { + var treeJson = JsonContent.Create( + new + { + tree, + ratio = _screenScale, + margin = new + { + near = _margins.Near, + far = _margins.Far, + top = _margins.Top, + bottom = _margins.Bottom, + } + }); + + var request = new HttpRequestMessage(HttpMethod.Post, $"{_baseUrl}/test-sessions/{deviceKey}") + { + Content = treeJson + }; + AddApiKeyHeader(request); + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + var session = await response.Content.ReadFromJsonAsync(); + return session; + } +} + +public class DeviceSession +{ + public string deviceKey { get; set; } = string.Empty; + public JsonElement tree { get; set; } +} \ No newline at end of file diff --git a/FluidSharp/Diagnostics/VisualFrame.cs b/FluidSharp/Diagnostics/VisualFrame.cs index f0d4347..a85add9 100644 --- a/FluidSharp/Diagnostics/VisualFrame.cs +++ b/FluidSharp/Diagnostics/VisualFrame.cs @@ -20,14 +20,41 @@ public static VisualFrame CreateVisualFrame() } } + +public class VisualFrameChild +{ + public SKRectDto Rect { get; set; } + public VisualFrame.VisualFrameElement Element { get; set; } +} + +public class SKRectDto +{ + public float Left { get; set; } + public float Top { get; set; } + public float Right { get; set; } + public float Bottom { get; set; } + + public static SKRectDto FromSKRect(SKRect rect) => new SKRectDto + { + Left = rect.Left, + Top = rect.Top, + Right = rect.Right, + Bottom = rect.Bottom + }; +} + public class VisualFrame { public DateTime Timestamp { get; set; } - public List<(SKRect, VisualFrameElement)> Children { get; } = new(); + public List Children { get; } = new(); public void AddChild(SKRect rect, Widget element) { - Children.Add((rect, new VisualFrameElement(element))); + Children.Add( + new VisualFrameChild() { + Rect = SKRectDto.FromSKRect(rect), + Element = new VisualFrameElement(element) + }); } public override string ToString() @@ -42,15 +69,15 @@ public override string ToString() { Rect = new { - Left = x.Item1.Left, - Right = x.Item1.Right, - Top = x.Item1.Top, - Bottom = x.Item1.Bottom + Left = x.Rect.Left, + Right = x.Rect.Right, + Top = x.Rect.Top, + Bottom = x.Rect.Bottom }, - x.Item2.widget.TestId, - x.Item2.widget.DebugTag, - x.Item2.widget.TestText, - x.Item2.widget.GetType().Name + x.Element.widget.TestId, + x.Element.widget.DebugTag, + x.Element.widget.TestText, + x.Element.widget.GetType().Name }) .ToList() }); From 007ac0944f7d68e3113019bfee77f130dae72574 Mon Sep 17 00:00:00 2001 From: Artem Marakhovskyi Date: Tue, 15 Jul 2025 22:08:45 +0200 Subject: [PATCH 4/5] no message --- FluidSharp/Layouts/LayoutSurface.cs | 15 +++++++++++---- FluidSharp/Widgets/Widget.cs | 4 +++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/FluidSharp/Layouts/LayoutSurface.cs b/FluidSharp/Layouts/LayoutSurface.cs index 5150056..161f934 100644 --- a/FluidSharp/Layouts/LayoutSurface.cs +++ b/FluidSharp/Layouts/LayoutSurface.cs @@ -35,14 +35,21 @@ public class LayoutSurface public virtual void SetCanvas(SKCanvas canvas) { Canvas = canvas; } protected void SetThisCanvas(SKCanvas canvas) { Canvas = canvas; } - private VisualFrame _visualFrame = VisualFrameProvider.CreateVisualFrame(); - - public LayoutSurface(Device device, MeasureCache measureCache, SKCanvas canvas, VisualState visualState) + private readonly VisualFrame _visualFrame; + + + public LayoutSurface(Device device, MeasureCache measureCache, + SKCanvas canvas, VisualState visualState, + bool shouldCreateVisualFrame = true) { Device = device; MeasureCache = measureCache; Canvas = canvas; VisualState = visualState; + if (shouldCreateVisualFrame) + { + _visualFrame = VisualFrameProvider.CreateVisualFrame(); + } } public virtual SKRect Paint(Widget widget, SKRect rect) @@ -211,6 +218,6 @@ public void DebugSpacing(SKRect dest, Func text, SKColor color) } - public void TrackVisualFrame(SKRect rect, Widget widget) => _visualFrame.AddChild(rect, widget); + public void TrackVisualFrame(SKRect rect, Widget widget) => _visualFrame?.AddChild(rect, widget); } } diff --git a/FluidSharp/Widgets/Widget.cs b/FluidSharp/Widgets/Widget.cs index d72517a..737aed3 100644 --- a/FluidSharp/Widgets/Widget.cs +++ b/FluidSharp/Widgets/Widget.cs @@ -20,7 +20,9 @@ public abstract class Widget public bool ShouldBeTrackedForDiagnostics => Config.Instance.IsDiagnosticsEnabled && - (TestId != null || TestText != null || Tag != null); + (!string.IsNullOrWhiteSpace(TestId) + || !string.IsNullOrWhiteSpace(TestText) + || !string.IsNullOrWhiteSpace(Tag)); public abstract SKSize Measure(MeasureCache measureCache, SKSize boundaries); From 95a82db6078e5c68369e3c149f93e9507aa398af Mon Sep 17 00:00:00 2001 From: Artem Marakhovskyi Date: Thu, 17 Jul 2025 18:57:39 +0200 Subject: [PATCH 5/5] Enabled sending widget data to diagnostics server --- FluidSharp/Diagnostics/VisualFrame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FluidSharp/Diagnostics/VisualFrame.cs b/FluidSharp/Diagnostics/VisualFrame.cs index a85add9..0d92276 100644 --- a/FluidSharp/Diagnostics/VisualFrame.cs +++ b/FluidSharp/Diagnostics/VisualFrame.cs @@ -75,7 +75,7 @@ public override string ToString() Bottom = x.Rect.Bottom }, x.Element.widget.TestId, - x.Element.widget.DebugTag, + x.Element.widget.Tag, x.Element.widget.TestText, x.Element.widget.GetType().Name })