From 4863d48753a48edbbd6ce9336ad8ebbb80bd02cf Mon Sep 17 00:00:00 2001 From: Tomas Date: Tue, 4 Feb 2025 21:13:27 +0100 Subject: [PATCH 1/2] Add performance tests --- .../Performance/PerfClient/PerfClient.csproj | 40 + tests/Performance/PerfClient/Program.cs | 31 + tests/Performance/PerfClient/apps/Config.yaml | 2 + .../PerfClient/apps/PerformanceTestApp.cs | 37 + .../apps/PerformanceTestSecondApp.cs | 36 + .../apps/PerformanceTestThirdApp.cs | 36 + tests/Performance/PerfClient/appsettings.json | 33 + tests/Performance/PerfServer/GlobalUsings.cs | 21 + .../PerfServer/Internal/Commands.cs | 37 + .../PerfServer/Internal/Messages.cs | 709 ++++++++++++++++++ .../Performance/PerfServer/Internal/Model.cs | 21 + .../PerfServer/Internal/PerfServer.cs | 275 +++++++ .../PerfServer/Internal/WebHostExtensions.cs | 67 ++ .../Performance/PerfServer/PerfServer.csproj | 21 + tests/Performance/PerfServer/Program.cs | 22 + tests/Performance/PerfServer/appsettings.json | 29 + tests/Performance/README.md | 38 + 17 files changed, 1455 insertions(+) create mode 100644 tests/Performance/PerfClient/PerfClient.csproj create mode 100644 tests/Performance/PerfClient/Program.cs create mode 100644 tests/Performance/PerfClient/apps/Config.yaml create mode 100644 tests/Performance/PerfClient/apps/PerformanceTestApp.cs create mode 100644 tests/Performance/PerfClient/apps/PerformanceTestSecondApp.cs create mode 100644 tests/Performance/PerfClient/apps/PerformanceTestThirdApp.cs create mode 100644 tests/Performance/PerfClient/appsettings.json create mode 100644 tests/Performance/PerfServer/GlobalUsings.cs create mode 100644 tests/Performance/PerfServer/Internal/Commands.cs create mode 100644 tests/Performance/PerfServer/Internal/Messages.cs create mode 100644 tests/Performance/PerfServer/Internal/Model.cs create mode 100644 tests/Performance/PerfServer/Internal/PerfServer.cs create mode 100644 tests/Performance/PerfServer/Internal/WebHostExtensions.cs create mode 100644 tests/Performance/PerfServer/PerfServer.csproj create mode 100644 tests/Performance/PerfServer/Program.cs create mode 100644 tests/Performance/PerfServer/appsettings.json create mode 100644 tests/Performance/README.md diff --git a/tests/Performance/PerfClient/PerfClient.csproj b/tests/Performance/PerfClient/PerfClient.csproj new file mode 100644 index 00000000..33e54bcb --- /dev/null +++ b/tests/Performance/PerfClient/PerfClient.csproj @@ -0,0 +1,40 @@ + + + + Exe + net9.0 + enable + enable + + + + + Always + + + + + + + + + + + + + + + + + + ..\..\..\.linting\roslynator.ruleset + + + + Always + + + + + + diff --git a/tests/Performance/PerfClient/Program.cs b/tests/Performance/PerfClient/Program.cs new file mode 100644 index 00000000..268ffb2f --- /dev/null +++ b/tests/Performance/PerfClient/Program.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.Hosting; +using NetDaemon.Runtime; +using NetDaemon.AppModel; +using System.Reflection; +using Serilog; +#pragma warning disable CA1812 + +try +{ + await Host.CreateDefaultBuilder(args) + .UseNetDaemonAppSettings() + .UseSerilog((context, provider, logConfig) => + { + logConfig.ReadFrom.Configuration(context.Configuration); + }) + .UseNetDaemonRuntime() + .ConfigureServices((_, services) => + services + .AddAppsFromAssembly(Assembly.GetEntryAssembly()!) + // Remove this is you are not running the integration! + //.AddNetDaemonStateManager() + ) + .Build() + .RunAsync() + .ConfigureAwait(false); +} +catch (Exception e) +{ + Console.WriteLine($"Failed to start host... {e}"); + throw; +} diff --git a/tests/Performance/PerfClient/apps/Config.yaml b/tests/Performance/PerfClient/apps/Config.yaml new file mode 100644 index 00000000..8a942a0a --- /dev/null +++ b/tests/Performance/PerfClient/apps/Config.yaml @@ -0,0 +1,2 @@ +# Future configuration file for the project. + diff --git a/tests/Performance/PerfClient/apps/PerformanceTestApp.cs b/tests/Performance/PerfClient/apps/PerformanceTestApp.cs new file mode 100644 index 00000000..e7eaa13a --- /dev/null +++ b/tests/Performance/PerfClient/apps/PerformanceTestApp.cs @@ -0,0 +1,37 @@ +using System.Diagnostics; +using System.Reactive.Linq; +using Microsoft.Extensions.Logging; +using NetDaemon.AppModel; +using NetDaemon.HassModel; + +#pragma warning disable CA1050 + +/// +/// First starts the performance test and process all state changes and logs the performance +/// +[NetDaemonApp] +public class PerformanceTestApp +{ + public PerformanceTestApp(IHaContext ha, ILogger logger) + { + var counter = 0; + var timer = new Stopwatch(); + ha.StateChanges() + .Subscribe(x => + { + if (counter == 0) + { + timer.Start(); + } + if (x.New?.State == "stop") + { + timer.Stop(); + logger.LogInformation("Performance test completed in {Time}ms, with {Counter} call with performance {MsgPerSec} msg/sec", timer.ElapsedMilliseconds, counter, Math.Round( counter / (timer.ElapsedMilliseconds / 1000.0))); + timer.Reset(); + } + counter++; + }); + logger.LogInformation("Starting performance test by sending service call with service start_performance_test"); + ha.CallService("netdaemon", "start_performance_test"); + } +} diff --git a/tests/Performance/PerfClient/apps/PerformanceTestSecondApp.cs b/tests/Performance/PerfClient/apps/PerformanceTestSecondApp.cs new file mode 100644 index 00000000..2b532e85 --- /dev/null +++ b/tests/Performance/PerfClient/apps/PerformanceTestSecondApp.cs @@ -0,0 +1,36 @@ +using System.Diagnostics; +using System.Reactive.Linq; +using Microsoft.Extensions.Logging; +using NetDaemon.AppModel; +using NetDaemon.HassModel; + +#pragma warning disable CA1050 + +/// +/// Second app process all state changes and logs the performance +/// +[NetDaemonApp] +public class PerformanceTestSecondApp +{ + public PerformanceTestSecondApp(IHaContext ha, ILogger logger) + { + var counter = 0; + var timer = new Stopwatch(); + ha.StateChanges() + .Subscribe(x => + { + if (counter == 0) + { + timer.Start(); + } + if (x.New?.State == "stop") + { + timer.Stop(); + logger.LogInformation("Performance test completed in {Time}ms, with {Counter} call with performance {MsgPerSec} msg/sec", timer.ElapsedMilliseconds, counter, Math.Round( counter / (timer.ElapsedMilliseconds / 1000.0))); + timer.Reset(); + } + counter++; + }); + logger.LogInformation("Waiting for starting performance test"); + } +} diff --git a/tests/Performance/PerfClient/apps/PerformanceTestThirdApp.cs b/tests/Performance/PerfClient/apps/PerformanceTestThirdApp.cs new file mode 100644 index 00000000..b9d7d0c7 --- /dev/null +++ b/tests/Performance/PerfClient/apps/PerformanceTestThirdApp.cs @@ -0,0 +1,36 @@ +using System.Diagnostics; +using System.Reactive.Linq; +using Microsoft.Extensions.Logging; +using NetDaemon.AppModel; +using NetDaemon.HassModel; + +#pragma warning disable CA1050 + +/// +/// Third app process all state changes and logs the performance +/// +[NetDaemonApp] +public class PerformanceTestThirdApp +{ + public PerformanceTestThirdApp(IHaContext ha, ILogger logger) + { + var counter = 0; + var timer = new Stopwatch(); + ha.StateChanges() + .Subscribe(x => + { + if (counter == 0) + { + timer.Start(); + } + if (x.New?.State == "stop") + { + timer.Stop(); + logger.LogInformation("Performance test completed in {Time}ms, with {Counter} call with performance {MsgPerSec} msg/sec", timer.ElapsedMilliseconds, counter, Math.Round( counter / (timer.ElapsedMilliseconds / 1000.0))); + timer.Reset(); + } + counter++; + }); + logger.LogInformation("Waiting for starting performance test"); + } +} diff --git a/tests/Performance/PerfClient/appsettings.json b/tests/Performance/PerfClient/appsettings.json new file mode 100644 index 00000000..4733ab40 --- /dev/null +++ b/tests/Performance/PerfClient/appsettings.json @@ -0,0 +1,33 @@ +{ + "Serilog": { + "MinimumLevel": { + "Default": "Verbose", + "Override": { + "System": "Information", + "Microsoft": "Information", + "System.Net.Http.HttpClient": "Warning", + "daemonapp.app": "Verbose" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console", + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext:l}: {Message:lj}{NewLine}{Exception}" + } + } + ] + }, + "HomeAssistant": { + "Host": "localhost", + "Port": 8002, + "Ssl": false, + "Token": "somefakeone", + "InsecureBypassCertificateErrors": false + }, + "NetDaemon": { + "Admin": true, + "ApplicationConfigurationFolder": "./apps" + } +} diff --git a/tests/Performance/PerfServer/GlobalUsings.cs b/tests/Performance/PerfServer/GlobalUsings.cs new file mode 100644 index 00000000..dbe55b95 --- /dev/null +++ b/tests/Performance/PerfServer/GlobalUsings.cs @@ -0,0 +1,21 @@ +global using System; +global using System.Net.WebSockets; +global using System.Reactive.Linq; +global using System.Reactive.Threading.Tasks; +global using System.Text; +global using System.Text.Json; +global using System.Text.Json.Serialization; +global using System.Threading.Channels; +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Options; +global using Microsoft.AspNetCore.Hosting.Server; +global using Microsoft.AspNetCore.Hosting.Server.Features; +global using NetDaemon.Client; +global using NetDaemon.Client.HomeAssistant.Model; +global using NetDaemon.Client.Internal; +global using NetDaemon.Client.Internal.Helpers; +global using NetDaemon.Client.Internal.Net; +global using NetDaemon.Client.Internal.Extensions; +global using NetDaemon.Client.Internal.HomeAssistant.Commands; +global using NetDaemon.Client.Settings; +global using NetDaemon.Client.HomeAssistant.Extensions; diff --git a/tests/Performance/PerfServer/Internal/Commands.cs b/tests/Performance/PerfServer/Internal/Commands.cs new file mode 100644 index 00000000..724c9eef --- /dev/null +++ b/tests/Performance/PerfServer/Internal/Commands.cs @@ -0,0 +1,37 @@ +// Contains commands that being sent from ND that needs to be de-serialized by the fake sever + +namespace NetDaemon.Tests.Performance; + +#pragma warning disable CA1852 + +public record CommandMessage +{ + [JsonPropertyName("type")] public string Type { get; init; } = string.Empty; + [JsonPropertyName("id")] public int Id { get; set; } +} + +internal record CreateInputBooleanCommand : CommandMessage +{ + public CreateInputBooleanCommand() + { + Type = "input_boolean/create"; + } + + [JsonPropertyName("name")] public required string Name { get; init; } +} + +internal record CallServiceCommand : CommandMessage +{ + public CallServiceCommand() + { + Type = "call_service"; + } + + [JsonPropertyName("domain")] public string Domain { get; init; } = string.Empty; + + [JsonPropertyName("service")] public string Service { get; init; } = string.Empty; + + [JsonPropertyName("service_data")] public object? ServiceData { get; init; } + + [JsonPropertyName("target")] public HassTarget? Target { get; init; } +} diff --git a/tests/Performance/PerfServer/Internal/Messages.cs b/tests/Performance/PerfServer/Internal/Messages.cs new file mode 100644 index 00000000..c1852772 --- /dev/null +++ b/tests/Performance/PerfServer/Internal/Messages.cs @@ -0,0 +1,709 @@ +// Contains all message templates that are sent from the fake server to the ND + +namespace NetDaemon.Tests.Performance; + + +internal static class Messages +{ + public static string AuthRequiredMsg => + $$""" + { + "type": "auth_required" + } + """; + + public static string AuthOkMsg => + $$""" + { + "type": "auth_ok", + "ha_version": "22.1.0" + } + """; + + public static string ResultMsg(int id) => + $$""" + { + "id": {{id}}, + "type": "result", + "success": true, + "result": null + } + """; + + public static string GetStatesResultMsg(int id) => + $$""" + { + "id": {{id}}, + "type": "result", + "success": true, + "result": [ + { + "entity_id": "zone.test", + "state": "zoning", + "attributes": { + "hidden": true, + "latitude": 61.388466, + "longitude": 12.295939, + "radius": 200.0, + "friendly_name": "Test", + "icon": "mdi:briefcase" + }, + "last_changed": "2019-02-16T18:11:44.183673+00:00", + "last_updated": "2019-02-16T18:11:44.183673+00:00", + "context": { + "id": "f48f9a312c68402a81230b9f14a00a23", + "parent_id": null, + "user_id": null + } + }, + { + "entity_id": "zone.test2", + "state": "zoning", + "attributes": { + "hidden": true, + "latitude": 63.438067, + "longitude": 19.321611, + "radius": 300.0, + "friendly_name": "Test2", + "icon": "mdi:hospital" + }, + "last_changed": "2019-02-16T18:11:44.184844+00:00", + "last_updated": "2019-02-16T18:11:44.184844+00:00", + "context": { + "id": "40a40e1aebe14464b2f7097c0a522810", + "parent_id": null, + "user_id": null + } + }, + { + "entity_id": "zone.home", + "state": "zoning", + "attributes": { + "hidden": true, + "latitude": 61.4348599, + "longitude": 16.1413237, + "radius": 100, + "friendly_name": "Home", + "icon": "mdi:home" + }, + "last_changed": "2019-02-16T18:11:44.187462+00:00", + "last_updated": "2019-02-16T18:11:44.187462+00:00", + "context": { + "id": "cd7715bcd37b4b528540dc4bfa634de9", + "parent_id": null, + "user_id": null + } + }, + { + "entity_id": "sun.sun", + "state": "below_horizon", + "attributes": { + "next_dawn": "2019-02-17T05:47:33+00:00", + "next_dusk": "2019-02-17T16:24:13+00:00", + "next_midnight": "2019-02-16T23:05:52+00:00", + "next_noon": "2019-02-17T11:05:53+00:00", + "next_rising": "2019-02-17T06:35:00+00:00", + "next_setting": "2019-02-17T15:36:46+00:00", + "elevation": -39.06, + "azimuth": 348.2, + "friendly_name": "Sun" + }, + "last_changed": "2019-02-16T18:11:44.204384+00:00", + "last_updated": "2019-02-16T22:28:30.002772+00:00", + "context": { + "id": "2f6c1d0cee6a454d853150c7df4104c5", + "parent_id": null, + "user_id": null + } + }, + { + "entity_id": "group.default_view", + "state": "on", + "attributes": { + "entity_id": [ + "group.climate", + "group.uteclimate", + "group.upper_floor", + "group.lower_floor", + "group.outside", + "sensor.my_alarm", + "input_select.house_mode_select", + "input_boolean.good_night_house", + "sensor.house_mode", + "group.frysar_temperatur", + "group.people_status", + "sensor.occupancy", + "weather.smhi_hemma", + "weather.yweather" + ], + "order": 0, + "view": true, + "friendly_name": "default_view", + "icon": "mdi:home", + "hidden": true, + "assumed_state": true + }, + "last_changed": "2019-02-16T18:11:45.104467+00:00", + "last_updated": "2019-02-16T18:11:45.691968+00:00", + "context": { + "id": "d3b3e20ea93f49b190f56e08d5af5e80", + "parent_id": null, + "user_id": null + } + }, + { + "entity_id": "scene.stadning", + "state": "scening", + "attributes": { + "entity_id": [ + "group.dummy" + ], + "friendly_name": "St\u00e4dning" + }, + "last_changed": "2019-02-16T18:11:44.703590+00:00", + "last_updated": "2019-02-16T18:11:44.703590+00:00", + "context": { + "id": "441f99ad4a4d44358b3aaa73e248a187", + "parent_id": null, + "user_id": null + } + }, + { + "entity_id": "sensor.house_mode", + "state": "Kv\u00e4ll", + "attributes": { + "friendly_name": "Hus status" + }, + "last_changed": "2019-02-16T18:11:49.853247+00:00", + "last_updated": "2019-02-16T18:11:49.853247+00:00", + "context": { + "id": "ff7c757b76f746da800158f70f1639a1", + "parent_id": null, + "user_id": null + } + }, + { + "entity_id": "sensor.sally_phone_mqtt_bt", + "state": "home", + "attributes": { + "confidence": "100", + "friendly_name": "Sallys mobil BT", + "entity_picture": "/local/sally.jpg" + }, + "last_changed": "2019-02-16T18:32:22.167753+00:00", + "last_updated": "2019-02-16T18:32:22.167753+00:00", + "context": { + "id": "0c9978ae1ce24a208c9a25272c44950b", + "parent_id": null, + "user_id": null + } + }, + { + "entity_id": "binary_sensor.vardagsrum_pir", + "state": "on", + "attributes": { + "battery_level": 100, + "on": true, + "friendly_name": "R\u00f6relsedetektor vardagsrum uppe", + "device_class": "motion", + "icon": "mdi:run-fast" + }, + "last_changed": "2019-02-16T21:57:39.435927+00:00", + "last_updated": "2019-02-16T21:57:39.435927+00:00", + "context": { + "id": "5f88441613664f2f94bfcb7202dabb3b", + "parent_id": null, + "user_id": null + } + }, + { + "entity_id": "group.room_view", + "state": "on", + "attributes": { + "entity_id": [ + "group.room_sally", + "group.room_bedroom" + ], + "order": 1, + "view": true, + "friendly_name": "room_view", + "icon": "mdi:home-circle", + "hidden": true, + "assumed_state": true + }, + "last_changed": "2019-02-16T18:11:45.102432+00:00", + "last_updated": "2019-02-16T18:11:45.689337+00:00", + "context": { + "id": "3f5092353d354d16abc4383e2b859278", + "parent_id": null, + "user_id": null + } + }, + { + "entity_id": "group.ligths_view", + "state": "on", + "attributes": { + "entity_id": [ + "light.tomas_fonster", + "light.tvrummet" + ], + "order": 5, + "view": true, + "friendly_name": "ligths_view", + "icon": "mdi:lightbulb-outline", + "hidden": true, + "assumed_state": true + }, + "last_changed": "2019-02-16T18:11:46.025370+00:00", + "last_updated": "2019-02-16T18:11:46.025370+00:00", + "context": { + "id": "b1806cf9774149c38d72d27c221539c1", + "parent_id": null, + "user_id": null + } + }, + { + "entity_id": "switch.switch1", + "state": "off", + "attributes": { + "friendly_name": "switch1", + "assumed_state": true + }, + "last_changed": "2019-02-16T18:11:45.613212+00:00", + "last_updated": "2019-02-16T18:11:45.613212+00:00", + "context": { + "id": "9b38bb49ad474ed1a839eb36030e89c6", + "parent_id": null, + "user_id": null + } + }, + { + "entity_id": "group.all_switches", + "state": "on", + "attributes": { + "entity_id": [ + "switch.switch1", + "switch.tv" + ], + "order": 35, + "auto": true, + "friendly_name": "all switches", + "hidden": true, + "assumed_state": true + }, + "last_changed": "2019-02-16T18:11:46.665404+00:00", + "last_updated": "2019-02-16T18:11:46.665404+00:00", + "context": { + "id": "94d4ee3f3503466388c43b435237b0d4", + "parent_id": null, + "user_id": null + } + }, + { + "entity_id": "group.all_lights", + "state": "on", + "attributes": { + "entity_id": [ + "light.sallys_rum", + "light.tvrummet" + ], + "order": 36, + "auto": true, + "friendly_name": "all lights", + "hidden": true + }, + "last_changed": "2019-02-16T18:11:45.806440+00:00", + "last_updated": "2019-02-16T18:12:02.869362+00:00", + "context": { + "id": "3cab55e957bd45d083229c9c441e806b", + "parent_id": null, + "user_id": null + } + }, + { + "entity_id": "light.tvrummet_vanster", + "state": "on", + "attributes": { + "min_mireds": 153, + "max_mireds": 500, + "brightness": 63, + "color_temp": 442, + "is_deconz_group": false, + "friendly_name": "tvrummet_vanster", + "supported_features": 43 + }, + "last_changed": "2019-02-16T18:11:45.977388+00:00", + "last_updated": "2019-02-16T18:11:45.977388+00:00", + "context": { + "id": "ae2a620ae48d48fb91907d6f6e379419", + "parent_id": null, + "user_id": null + } + }, + { + "entity_id": "light.sovrum_fonster", + "state": "on", + "attributes": { + "brightness": 63, + "is_deconz_group": false, + "friendly_name": "sovrum_fonster", + "supported_features": 41 + }, + "last_changed": "2019-02-16T18:11:45.978600+00:00", + "last_updated": "2019-02-16T18:11:45.978600+00:00", + "context": { + "id": "c7f79ba696334de7a18c895c0491d499", + "parent_id": null, + "user_id": null + } + }, + { + "entity_id": "sensor.occupancy", + "state": "home", + "attributes": { + "last_seen": "R\u00f6relsedetektor Tomas rum 3 minutes ago", + "time_last_seen": "2019-02-16 23:24" + }, + "last_changed": "2019-02-16T18:11:53.596418+00:00", + "last_updated": "2019-02-16T22:28:00.107816+00:00", + "context": { + "id": "f26aa4b08eb34fa58680bc2a0ca3b7bd", + "parent_id": null, + "user_id": "5a6c8d01ee2f4e4b90c7df8ea1ddd526" + } + }, + { + "entity_id": "sensor.presence_tomas", + "state": "Home", + "attributes": { + "entity_picture": "/local/tomas.jpg", + "friendly_name": "Tomas tracker", + "latitude": 61.233804, + "longitude": 16.0312271, + "proxi_direction": "stationary", + "proxi_distance": 0, + "source_type": "gps", + "gps_accuracy": 16 + }, + "last_changed": "2019-02-16T18:11:53.650806+00:00", + "last_updated": "2019-02-16T19:01:07.960903+00:00", + "context": { + "id": "8e768b8dc56a4257bb0540f9734a06de", + "parent_id": null, + "user_id": "5a6c8d01ee2f4e4b90c7df8ea1ddd526" + } + }, + { + "entity_id": "persistent_notification.http_login", + "state": "notifying", + "attributes": { + "title": "Login attempt failed", + "message": "Login attempt or request with invalid authentication from 111.22.3.4" + }, + "last_changed": "2019-02-16T21:45:59.552536+00:00", + "last_updated": "2019-02-16T21:45:59.552536+00:00", + "context": { + "id": "e7f1576b60f1406397b9a8cfc75f1329", + "parent_id": null, + "user_id": null + } + } + ] + } + """; + + public static string GetConfigResultMsg(int id) => + $$""" + { + "id": {{id}}, + "type": "result", + "success": true, + "result": { + "latitude": 63.1394549, + "longitude": 12.3412267, + "elevation": 29, + "state": "RUNNING", + "unit_system": { + "length": "km", + "mass": "g", + "temperature": "\u00b0C", + "volume": "L" + }, + "location_name": "Home", + "time_zone": "Europe/Stockholm", + "components": [ + "config.auth", + "binary_sensor.deconz", + "websocket_api", + "deconz", + "input_boolean", + "api", + "config.automation", + "zone", + "automation", + "media_player", + "scene.homeassistant", + "system_health", + "auth", + "config.config_entries", + "logbook", + "config.script", + "influxdb", + "logger", + "input_number", + "sensor.template", + "config.core", + "light", + "tts", + "system_log", + "sensor", + "switch", + "binary_sensor", + "recorder", + "input_select", + "weather", + "onboarding", + "media_player.cast", + "hassio", + "history", + "script", + "group", + "config.entity_registry", + "map", + "config.device_registry", + "mqtt", + "config", + "discovery", + "cast", + "http", + "device_tracker", + "notify", + "config.customize", + "config.auth_provider_homeassistant", + "switch.template", + "scene", + "config.group", + "camera", + "updater", + "sensor.systemmonitor", + "proximity", + "cover", + "sensor.time_date", + "frontend", + "sun", + "config.area_registry", + "lovelace" + ], + "config_dir": "/config", + "whitelist_external_dirs": [ + "/config/www" + ], + "version": "0.87.0" + } + } + """; + + public static string GetAreasResultMsg(int id) => + $$""" + { + "id": {{id}}, + "type": "result", + "success": true, + "result": [ + { + "name": "Bedroom", + "area_id": "5a30cdc2fd7f44d5a77f2d6f6d2ccd76" + }, + { + "name": "Kitchen", + "area_id": "42a6048dc0404595b136545f6745c5d1" + }, + { + "name": "Livingroom", + "area_id": "4e65b6fe3cea4604ab318b0f9c2b8432" + } + ] + } + """; + + public static string GetLabelsResultMsg(int id) => + $$""" + { + "id": {{id}}, + "type": "result", + "success": true, + "result": [ + { + "color": "green", + "description": null, + "icon": "mdi:chair-rolling", + "label_id": "label1", + "name": "Label 1" + }, + { + "color": "indigo", + "description": null, + "icon": "mdi:lightbulb-night", + "label_id": "nightlights", + "name": "NightLights" + } + ] + } + """; + + public static string GetFloorsResultMsg(int id) => + $$""" + { + "id": {{id}}, + "type": "result", + "success": true, + "result": [ + { + "aliases": [], + "floor_id": "floor0", + "icon": null, + "level": 0, + "name": "Floor 0" + }, + { + "aliases": [], + "floor_id": "floor1", + "icon": null, + "level": 1, + "name": "Floor 1" + } + ] + } + """; + + public static string GetDevicesResultMsg(int id) => + $$""" + { + "id": {{id}}, + "type": "result", + "success": true, + "result": [ + { + "config_entries": [], + "connections": [], + "manufacturer": "Google Inc.", + "model": "Chromecast", + "name": "My TV", + "sw_version": null, + "id": "42cdda32a2a3428e86c2e27699d79ead", + "via_device_id": null, + "area_id": null, + "name_by_user": null + }, + { + "config_entries": [ + "4b85129c61c74b27bd90e593f6b7482e" + ], + "connections": [], + "manufacturer": "Plex", + "model": "Plex Web", + "name": "Plex (Plex Web - Chrome)", + "sw_version": "4.22.3", + "id": "49b27477238a4c8fb6cc8fbac32cebbc", + "via_device_id": "6e17380a6d2744d18045fe4f627db706", + "area_id": null, + "name_by_user": null + } + ] + } + """; + + public static string GetEntitiesResultMsg(int id) => + $$""" + { + "id": {{id}}, + "type": "result", + "success": true, + "result": [ + { + "config_entry_id": null, + "device_id": "42cdda32a2a3428e86c2e27699d79ead", + "disabled_by": null, + "entity_id": "media_player.tv_uppe2", + "area_id": "42cdda1212a3428e86c2e27699d79ead", + "name": null, + "icon": null, + "platform": "cast" + }, + { + "config_entry_id": "4b85129c61c74b27bd90e593f6b7482e", + "device_id": "6e17380a6d2744d18045fe4f627db706", + "disabled_by": null, + "entity_id": "sensor.plex_plex", + "name": null, + "icon": null, + "platform": "plex" + } + ] + } + """; + + internal static string InputBooleanCreateResultMsg(int id) => + $$""" + { + "id": {{id}}, + "type": "result", + "success": true, + "result": null + } + """; + + public static string EventResultMsg(int id, string stateFrom, string stateTo) => + $$""" + { + "id": {{id}}, + "type": "event", + "event": { + "event_type": "state_changed", + "data": { + "entity_id": "binary_sensor.vardagsrum_pir", + "old_state": { + "entity_id": "binary_sensor.vardagsrum_pir", + "state": "{{stateFrom}}", + "attributes": { + "battery_level": 100, + "on": true, + "friendly_name": "R\u00f6relsedetektor TV-rum", + "device_class": "motion", + "icon": "mdi:run-fast" + }, + "last_changed": "2019-02-17T11:41:08.015070+00:00", + "last_updated": "2019-02-17T11:42:08.015070+00:00", + "context": { + "id": "09c2e2ed8eef43e7885f478084e61d80", + "user_id": null + } + }, + "new_state": { + "entity_id": "binary_sensor.vardagsrum_pir", + "state": "{{stateTo}}", + "attributes": { + "battery_level": 100, + "on": true, + "friendly_name": "R\u00f6relsedetektor vardagsrum", + "device_class": "motion", + "icon": "mdi:run-fast" + }, + "last_changed": "2019-02-17T11:43:47.090473+00:00", + "last_updated": "2019-02-17T11:43:47.090473+00:00", + "context": { + "id": "849ebede7b294a019c724a07dac43f9c", + "user_id": null + } + } + }, + "origin": "LOCAL", + "time_fired": "2019-02-17T11:43:47.090511+00:00", + "context": { + "id": "849ebede7b294a019c724a07dac43f9c", + "user_id": null + } + } + } + """; +} diff --git a/tests/Performance/PerfServer/Internal/Model.cs b/tests/Performance/PerfServer/Internal/Model.cs new file mode 100644 index 00000000..80c91ab8 --- /dev/null +++ b/tests/Performance/PerfServer/Internal/Model.cs @@ -0,0 +1,21 @@ +namespace NetDaemon.Tests.Performance; + +public record InputBoolean +{ + [JsonPropertyName("name")] public string Name { get; init; } = string.Empty; + [JsonPropertyName("icon")] public string? Icon { get; init; } + [JsonPropertyName("id")] public string Id { get; init; } = string.Empty; +} + +public record EntityState +{ + + [JsonPropertyName("attributes")] public IDictionary? Attributes { get; init; } = null; + + [JsonPropertyName("entity_id")] public string EntityId { get; init; } = ""; + + [JsonPropertyName("last_changed")] public DateTime LastChanged { get; init; } = DateTime.MinValue; + [JsonPropertyName("last_updated")] public DateTime LastUpdated { get; init; } = DateTime.MinValue; + [JsonPropertyName("state")] public string? State { get; init; } = ""; + [JsonPropertyName("context")] public HassContext? Context { get; init; } +} diff --git a/tests/Performance/PerfServer/Internal/PerfServer.cs b/tests/Performance/PerfServer/Internal/PerfServer.cs new file mode 100644 index 00000000..29d30609 --- /dev/null +++ b/tests/Performance/PerfServer/Internal/PerfServer.cs @@ -0,0 +1,275 @@ +using System.Collections.Concurrent; + +namespace NetDaemon.Tests.Performance; + +/// +/// The class implementing the mock hass server +/// +public sealed class PerfServerStartup(ILogger logger) : IAsyncDisposable +{ + private readonly CancellationTokenSource _cancelSource = new(); + + private const int DefaultTimeOut = 5000; + private const int RecieiveBufferSize = 1024 * 4; + + private readonly ConcurrentBag _eventSubscriptions = []; + private Task? _perfTestTask; + + public static readonly ConcurrentBag _inputBooleans = []; + + /// + /// Sends a websocket message to the client + /// + private async Task SendWebsocketMessage(WebSocket ws, string message) + { + var byteMessage = Encoding.UTF8.GetBytes(message); + await ws.SendAsync(new ArraySegment(byteMessage, 0, byteMessage.Length), + WebSocketMessageType.Text, true, _cancelSource.Token).ConfigureAwait(false); + } + + /// + /// Process incoming websocket requests to simulate Home Assistant websocket API + /// + /// + /// This implements just enough of the HA websocket API to make NetDaemon happy + /// + public async Task ProcessWebsocket(WebSocket webSocket) + { + logger.LogDebug("Processing websocket"); + + var buffer = new byte[RecieiveBufferSize]; + + try + { + // First send auth required to the client + var authRequiredMessage = Messages.AuthRequiredMsg; + await SendWebsocketMessage(webSocket, authRequiredMessage).ConfigureAwait(false); + + while (true) + { + // Wait for incoming messages + var result = + await webSocket.ReceiveAsync(new ArraySegment(buffer), _cancelSource.Token) + .ConfigureAwait(false); + + logger.LogDebug("Received message: {Message}", Encoding.UTF8.GetString(buffer, 0, result.Count)); + + _cancelSource.Token.ThrowIfCancellationRequested(); + + if (result.CloseStatus.HasValue && webSocket.State == WebSocketState.CloseReceived) + { + logger.LogDebug("Close message received, closing websocket"); + break; + } + + var hassMessage = + JsonSerializer.Deserialize(new ReadOnlySpan(buffer, 0, result.Count)) + ?? throw new ApplicationException("Unexpected not able to deserialize to HassMessage"); + + switch (hassMessage.Type) + { + case "auth": + // Just auth ok anything + var authOkMessage = Messages.AuthOkMsg; + await SendWebsocketMessage(webSocket, authOkMessage).ConfigureAwait(false); + break; + case "subscribe_events": + _eventSubscriptions.Add(hassMessage.Id); + var resultMsg = Messages.ResultMsg(hassMessage.Id); + await SendWebsocketMessage(webSocket, resultMsg).ConfigureAwait(false); + break; + case "get_states": + var getStatesResultMsg = Messages.GetStatesResultMsg(hassMessage.Id); + await SendWebsocketMessage(webSocket, getStatesResultMsg).ConfigureAwait(false); + break; + case "call_service": + var callServiceMsg = Messages.ResultMsg(hassMessage.Id); + var callServiceCommand = + JsonSerializer.Deserialize( + new ReadOnlySpan(buffer, 0, result.Count)) + ?? throw new ApplicationException("Unexpected not able to deserialize call service"); + + if (callServiceCommand.Service == "start_performance_test") + { + _perfTestTask = StartPerformanceTest(webSocket); + } + await SendWebsocketMessage(webSocket, callServiceMsg).ConfigureAwait(false); + break; + case "get_config": + var getConfigResultMsg = Messages.GetConfigResultMsg(hassMessage.Id); + await SendWebsocketMessage(webSocket, getConfigResultMsg).ConfigureAwait(false); + break; + case "config/area_registry/list": + var getAreasResultMsg = Messages.GetAreasResultMsg(hassMessage.Id); + await SendWebsocketMessage(webSocket, getAreasResultMsg).ConfigureAwait(false); + break; + case "config/label_registry/list": + var getLabelsResultMsg = Messages.GetLabelsResultMsg(hassMessage.Id); + await SendWebsocketMessage(webSocket, getLabelsResultMsg).ConfigureAwait(false); + break; + case "config/floor_registry/list": + var getFloorsResultMsg = Messages.GetFloorsResultMsg(hassMessage.Id); + await SendWebsocketMessage(webSocket, getFloorsResultMsg).ConfigureAwait(false); + break; + case "config/device_registry/list": + var devicesResultMsg = Messages.GetDevicesResultMsg(hassMessage.Id); + await SendWebsocketMessage(webSocket, devicesResultMsg).ConfigureAwait(false); + break; + case "config/entity_registry/list": + var entitiesResultMsg = Messages.GetEntitiesResultMsg(hassMessage.Id); + await SendWebsocketMessage(webSocket, entitiesResultMsg).ConfigureAwait(false); + break; + case "input_boolean/create": + var createInputBooleanCommand = + JsonSerializer.Deserialize( + new ReadOnlySpan(buffer, 0, result.Count)) + ?? throw new ApplicationException("Unexpected not able to deserialize input boolean"); + var inputBoolean = new InputBoolean { Id = createInputBooleanCommand.Name, Name = createInputBooleanCommand.Name }; + logger.LogInformation("Creating input_boolean {InputBoolean}", inputBoolean); + _inputBooleans.Add(inputBoolean); + var inputBooleanCreateResultMsg = Messages.ResultMsg(hassMessage.Id); + await SendWebsocketMessage(webSocket, inputBooleanCreateResultMsg).ConfigureAwait(false); + break; + case "input_boolean/list": + var inputBooleans = _inputBooleans.ToArray(); + var jsonInputBooleans = JsonSerializer.Serialize(inputBooleans); + var jsonDoc = JsonDocument.Parse(jsonInputBooleans); + var response = new HassMessage { + Id = hassMessage.Id, + Type = "result", + Success = true, + ResultElement = jsonDoc.RootElement }; + var responseMsg = JsonSerializer.Serialize(response); + logger.LogInformation("Sending input_boolean/list response {Response}", response); + await SendWebsocketMessage(webSocket, responseMsg).ConfigureAwait(false); + break; + case "start_performance_test": + await SendWebsocketMessage(webSocket, Messages.ResultMsg(hassMessage.Id)).ConfigureAwait(false); + _perfTestTask = StartPerformanceTest(webSocket); + break; + + default: + throw new ApplicationException($"Unknown message type {hassMessage.Type}"); + } + } + } + catch (OperationCanceledException) + { + logger.LogDebug("Cancelled operation"); + await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Normal", CancellationToken.None) + .ConfigureAwait(false); + } + catch (Exception e) + { + logger.LogError(e, "Failed to process websocket"); + } + finally + { + try + { + await SendCorrectCloseFrameToRemoteWebSocket(webSocket).ConfigureAwait(false); + } + catch + { + // Just fail silently + } + } + logger.LogInformation("Websocket processing done"); + } + + /// + /// Starts the performance test, sends 1 000 000 state changes as fast as possible + /// and then stops the performance test. + /// Todo: Make the number of sent messages configurable + /// + private async Task StartPerformanceTest(WebSocket webSocket) + { + // Make a small delay to make sure all websocket messages are processed + // Not worth it to make something more fancy since this is not run in the CI + await Task.Delay(1000).ConfigureAwait(false); + + var subscription = _eventSubscriptions.FirstOrDefault(); + if (subscription == 0) + { + logger.LogWarning("No subscriptions found, cannot start performance test"); + return; + } + + logger.LogInformation("Starting performance test"); + + var eventMessage = Messages.EventResultMsg(subscription, "on", "off"); + for (var i = 0; i < 1000000; i++) + { + await SendWebsocketMessage(webSocket, eventMessage).ConfigureAwait(false); + } + + // Sends the last state change with state "stop" to make the client stop the performance test + eventMessage = Messages.EventResultMsg(subscription, "on", "stop"); + await SendWebsocketMessage(webSocket, eventMessage).ConfigureAwait(false); + logger.LogInformation("Performance test done"); + } + + /// + /// Closes correctly the websocket depending on websocket state + /// + /// + /// + /// Closing a websocket has special handling. When the client + /// wants to close it calls CloseAsync and the websocket takes + /// care of the proper close handling. + /// + /// + /// If the remote websocket wants to close the connection dotnet + /// implementation requires you to use CloseOutputAsync instead. + /// + /// + /// We do not want to cancel operations until we get closed state + /// this is why own timer cancellation token is used and we wait + /// for correct state before returning and disposing any connections + /// + /// + private static async Task SendCorrectCloseFrameToRemoteWebSocket(WebSocket ws) + { + using var timeout = new CancellationTokenSource(DefaultTimeOut); + + try + { + switch (ws.State) + { + case WebSocketState.CloseReceived: + { + // after this, the socket state which change to CloseSent + await ws.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", timeout.Token) + .ConfigureAwait(false); + // now we wait for the server response, which will close the socket + while (ws.State != WebSocketState.Closed && !timeout.Token.IsCancellationRequested) + await Task.Delay(100).ConfigureAwait(false); + break; + } + case WebSocketState.Open: + { + // Do full close + await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", timeout.Token) + .ConfigureAwait(false); + if (ws.State != WebSocketState.Closed) + throw new ApplicationException("Expected the websocket to be closed!"); + break; + } + } + } + catch (OperationCanceledException) + { + // normal upon task/token cancellation, disregard + } + } + + public async ValueTask DisposeAsync() + { + await _cancelSource.CancelAsync(); + if (_perfTestTask != null && !_perfTestTask.IsCompleted) + { + await _perfTestTask; + } + _cancelSource.Dispose(); + } +} diff --git a/tests/Performance/PerfServer/Internal/WebHostExtensions.cs b/tests/Performance/PerfServer/Internal/WebHostExtensions.cs new file mode 100644 index 00000000..3b1cc5ae --- /dev/null +++ b/tests/Performance/PerfServer/Internal/WebHostExtensions.cs @@ -0,0 +1,67 @@ +using Serilog; + +namespace NetDaemon.Tests.Performance; + +internal static class WebHostExtensions +{ + public static WebApplicationBuilder UsePerfServerSettings(this WebApplicationBuilder builder) + { + builder.Host + .UseSerilog((context, provider, logConfig) => + { + logConfig.ReadFrom.Configuration(context.Configuration); + }); + + builder.Services.AddScoped(); + + return builder; + } + + public static void AddPerfServer(this WebApplication app) + { + app.UseWebSockets(); + + // NetDaemon is checking the state of input_boolean so let's fake a response + app.MapGet("/api/states/{entityId}", (string entityId) => + { + if (entityId.StartsWith("input_boolean.", StringComparison.Ordinal)) + { + var id = entityId[14..]; + var inputBoolean = PerfServerStartup._inputBooleans.FirstOrDefault(n => n.Name == id); + if (inputBoolean != null) + { + return Results.Ok(new EntityState + { + EntityId = entityId, + State = "on", + }); + } + } + return Results.NotFound(); + }); + + app.Use(async (context, next) => + { + if (context.Request.Path == "/api/websocket") + { + if (context.WebSockets.IsWebSocketRequest) + { + var perfServer = context.RequestServices.GetRequiredService(); + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + await perfServer.ProcessWebsocket(webSocket); + return; + } + else + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + } + } + else + { + await next(context); + } + + }); + app.UseRouting(); + } +} diff --git a/tests/Performance/PerfServer/PerfServer.csproj b/tests/Performance/PerfServer/PerfServer.csproj new file mode 100644 index 00000000..2a637e54 --- /dev/null +++ b/tests/Performance/PerfServer/PerfServer.csproj @@ -0,0 +1,21 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + + + + + + diff --git a/tests/Performance/PerfServer/Program.cs b/tests/Performance/PerfServer/Program.cs new file mode 100644 index 00000000..bb78426d --- /dev/null +++ b/tests/Performance/PerfServer/Program.cs @@ -0,0 +1,22 @@ +using NetDaemon.Tests.Performance; + +#pragma warning disable CA1812 + + +var builder = WebApplication.CreateBuilder(args); + +var app = builder.UsePerfServerSettings() + .Build(); + +app.AddPerfServer(); + +try +{ + await app.RunAsync().ConfigureAwait(false); +} +catch (OperationCanceledException) { } // Ignore +catch (Exception e) +{ + Console.WriteLine($"Failed to start host... {e}"); + throw; +} diff --git a/tests/Performance/PerfServer/appsettings.json b/tests/Performance/PerfServer/appsettings.json new file mode 100644 index 00000000..8405c6ad --- /dev/null +++ b/tests/Performance/PerfServer/appsettings.json @@ -0,0 +1,29 @@ +{ + "Kestrel": { + "EndPoints": { + "Http": { + "Url": "http://localhost:8002" + } + } + }, + "Serilog": { + "MinimumLevel": { + "Default": "Debug", + "Override": { + "System": "Information", + "Microsoft": "Information", + "System.Net.Http.HttpClient": "Warning", + "daemonapp.app": "Verbose" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console", + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext:l}: {Message:lj}{NewLine}{Exception}" + } + } + ] + } +} diff --git a/tests/Performance/README.md b/tests/Performance/README.md new file mode 100644 index 00000000..4fc05d74 --- /dev/null +++ b/tests/Performance/README.md @@ -0,0 +1,38 @@ +# Performance tests + +The following directory contains a simple performance test for NetDaemon runtime. + +It has two parts, one fake Home Assistant server that fakes on-boarding and waits for command to start pushing state changes as fast as possible. +The other part is the NetDaemon runtime with three identical apps that listens to the same event state changes as calculate the performance of +number of messages per second processed in each app. + +## How to run the performance test + +Open two terminal windows. `cd` to the root of the repository in both terminals. + +1. Start the fake Home Assistant fake server in `tests/Performance/PerfServer`: + +```bash + +cd tests/Performance/PerfServer +dotnet run -c Release + +``` + +2. Start the NetDaemon runtime in `tests/Performance/PerfClient`: + +```bash +cd tests/Performance/PerfClient +dotnet run -c Release +``` + +## Expected output + +The performance test will start and you will see the number of messages processed per second in the console after one million messages. + +## Future improvements + +These tests are very basic MVP and can be improved in many ways. Example the tests can be dockerized and run in a CI/CD pipeline to ensure that performance is not degraded over time. + + + From 68802ac603d86773344fda82647c471e09d8ff52 Mon Sep 17 00:00:00 2001 From: Tomas Date: Sun, 9 Feb 2025 13:14:28 +0100 Subject: [PATCH 2/2] The performance server now sends 4 messages per time and set buffer size --- .../Net/WebSocketTransportPipeline.cs | 2 +- .../PerfClient/apps/PerformanceTestApp.cs | 3 +- .../PerfServer/Internal/Messages.cs | 205 ++++++++++++++++++ .../PerfServer/Internal/PerfServer.cs | 3 +- 4 files changed, 210 insertions(+), 3 deletions(-) diff --git a/src/Client/NetDaemon.HassClient/Internal/Net/WebSocketTransportPipeline.cs b/src/Client/NetDaemon.HassClient/Internal/Net/WebSocketTransportPipeline.cs index 6367f71a..0631ff21 100644 --- a/src/Client/NetDaemon.HassClient/Internal/Net/WebSocketTransportPipeline.cs +++ b/src/Client/NetDaemon.HassClient/Internal/Net/WebSocketTransportPipeline.cs @@ -139,7 +139,7 @@ private async Task ReadMessageFromWebSocketAndWriteToPipelineAsync(CancellationT { while (!cancelToken.IsCancellationRequested && !_ws.CloseStatus.HasValue) { - var memory = _pipe.Writer.GetMemory(); + var memory = _pipe.Writer.GetMemory(8192); var result = await _ws.ReceiveAsync(memory, cancelToken).ConfigureAwait(false); if ( _ws.State == WebSocketState.Open && diff --git a/tests/Performance/PerfClient/apps/PerformanceTestApp.cs b/tests/Performance/PerfClient/apps/PerformanceTestApp.cs index e7eaa13a..bf3c33fc 100644 --- a/tests/Performance/PerfClient/apps/PerformanceTestApp.cs +++ b/tests/Performance/PerfClient/apps/PerformanceTestApp.cs @@ -16,12 +16,13 @@ public PerformanceTestApp(IHaContext ha, ILogger logger) { var counter = 0; var timer = new Stopwatch(); - ha.StateChanges() + ha.StateAllChanges() .Subscribe(x => { if (counter == 0) { timer.Start(); + logger.LogInformation("Performance test started"); } if (x.New?.State == "stop") { diff --git a/tests/Performance/PerfServer/Internal/Messages.cs b/tests/Performance/PerfServer/Internal/Messages.cs index c1852772..8ab07c85 100644 --- a/tests/Performance/PerfServer/Internal/Messages.cs +++ b/tests/Performance/PerfServer/Internal/Messages.cs @@ -653,6 +653,211 @@ internal static string InputBooleanCreateResultMsg(int id) => } """; + public static string EventResultMsgMultiple(int id, string stateFrom, string stateTo) => + $$""" + [ + { + "id": {{id}}, + "type": "event", + "event": { + "event_type": "state_changed", + "data": { + "entity_id": "binary_sensor.vardagsrum_pir", + "old_state": { + "entity_id": "binary_sensor.vardagsrum_pir", + "state": "{{stateFrom}}", + "attributes": { + "battery_level": 100, + "on": true, + "friendly_name": "R\u00f6relsedetektor TV-rum", + "device_class": "motion", + "icon": "mdi:run-fast" + }, + "last_changed": "2019-02-17T11:41:08.015070+00:00", + "last_updated": "2019-02-17T11:42:08.015070+00:00", + "context": { + "id": "09c2e2ed8eef43e7885f478084e61d80", + "user_id": null + } + }, + "new_state": { + "entity_id": "binary_sensor.vardagsrum_pir", + "state": "{{stateTo}}", + "attributes": { + "battery_level": 100, + "on": true, + "friendly_name": "R\u00f6relsedetektor vardagsrum", + "device_class": "motion", + "icon": "mdi:run-fast" + }, + "last_changed": "2019-02-17T11:43:47.090473+00:00", + "last_updated": "2019-02-17T11:43:47.090473+00:00", + "context": { + "id": "849ebede7b294a019c724a07dac43f9c", + "user_id": null + } + } + }, + "origin": "LOCAL", + "time_fired": "2019-02-17T11:43:47.090511+00:00", + "context": { + "id": "849ebede7b294a019c724a07dac43f9c", + "user_id": null + } + } + }, + { + "id": {{id}}, + "type": "event", + "event": { + "event_type": "state_changed", + "data": { + "entity_id": "binary_sensor.vardagsrum_pir", + "old_state": { + "entity_id": "binary_sensor.vardagsrum_pir", + "state": "{{stateFrom}}", + "attributes": { + "battery_level": 100, + "on": true, + "friendly_name": "R\u00f6relsedetektor TV-rum", + "device_class": "motion", + "icon": "mdi:run-fast" + }, + "last_changed": "2019-02-17T11:41:08.015070+00:00", + "last_updated": "2019-02-17T11:42:08.015070+00:00", + "context": { + "id": "09c2e2ed8eef43e7885f478084e61d80", + "user_id": null + } + }, + "new_state": { + "entity_id": "binary_sensor.vardagsrum_pir", + "state": "{{stateTo}}", + "attributes": { + "battery_level": 100, + "on": true, + "friendly_name": "R\u00f6relsedetektor vardagsrum", + "device_class": "motion", + "icon": "mdi:run-fast" + }, + "last_changed": "2019-02-17T11:43:47.090473+00:00", + "last_updated": "2019-02-17T11:43:47.090473+00:00", + "context": { + "id": "849ebede7b294a019c724a07dac43f9c", + "user_id": null + } + } + }, + "origin": "LOCAL", + "time_fired": "2019-02-17T11:43:47.090511+00:00", + "context": { + "id": "849ebede7b294a019c724a07dac43f9c", + "user_id": null + } + } + }, + { + "id": {{id}}, + "type": "event", + "event": { + "event_type": "state_changed", + "data": { + "entity_id": "binary_sensor.vardagsrum_pir", + "old_state": { + "entity_id": "binary_sensor.vardagsrum_pir", + "state": "{{stateFrom}}", + "attributes": { + "battery_level": 100, + "on": true, + "friendly_name": "R\u00f6relsedetektor TV-rum", + "device_class": "motion", + "icon": "mdi:run-fast" + }, + "last_changed": "2019-02-17T11:41:08.015070+00:00", + "last_updated": "2019-02-17T11:42:08.015070+00:00", + "context": { + "id": "09c2e2ed8eef43e7885f478084e61d80", + "user_id": null + } + }, + "new_state": { + "entity_id": "binary_sensor.vardagsrum_pir", + "state": "{{stateTo}}", + "attributes": { + "battery_level": 100, + "on": true, + "friendly_name": "R\u00f6relsedetektor vardagsrum", + "device_class": "motion", + "icon": "mdi:run-fast" + }, + "last_changed": "2019-02-17T11:43:47.090473+00:00", + "last_updated": "2019-02-17T11:43:47.090473+00:00", + "context": { + "id": "849ebede7b294a019c724a07dac43f9c", + "user_id": null + } + } + }, + "origin": "LOCAL", + "time_fired": "2019-02-17T11:43:47.090511+00:00", + "context": { + "id": "849ebede7b294a019c724a07dac43f9c", + "user_id": null + } + } + }, + { + "id": {{id}}, + "type": "event", + "event": { + "event_type": "state_changed", + "data": { + "entity_id": "binary_sensor.vardagsrum_pir", + "old_state": { + "entity_id": "binary_sensor.vardagsrum_pir", + "state": "{{stateFrom}}", + "attributes": { + "battery_level": 100, + "on": true, + "friendly_name": "R\u00f6relsedetektor TV-rum", + "device_class": "motion", + "icon": "mdi:run-fast" + }, + "last_changed": "2019-02-17T11:41:08.015070+00:00", + "last_updated": "2019-02-17T11:42:08.015070+00:00", + "context": { + "id": "09c2e2ed8eef43e7885f478084e61d80", + "user_id": null + } + }, + "new_state": { + "entity_id": "binary_sensor.vardagsrum_pir", + "state": "{{stateTo}}", + "attributes": { + "battery_level": 100, + "on": true, + "friendly_name": "R\u00f6relsedetektor vardagsrum", + "device_class": "motion", + "icon": "mdi:run-fast" + }, + "last_changed": "2019-02-17T11:43:47.090473+00:00", + "last_updated": "2019-02-17T11:43:47.090473+00:00", + "context": { + "id": "849ebede7b294a019c724a07dac43f9c", + "user_id": null + } + } + }, + "origin": "LOCAL", + "time_fired": "2019-02-17T11:43:47.090511+00:00", + "context": { + "id": "849ebede7b294a019c724a07dac43f9c", + "user_id": null + } + } + } + ] + """; public static string EventResultMsg(int id, string stateFrom, string stateTo) => $$""" { diff --git a/tests/Performance/PerfServer/Internal/PerfServer.cs b/tests/Performance/PerfServer/Internal/PerfServer.cs index 29d30609..d307b952 100644 --- a/tests/Performance/PerfServer/Internal/PerfServer.cs +++ b/tests/Performance/PerfServer/Internal/PerfServer.cs @@ -197,8 +197,9 @@ private async Task StartPerformanceTest(WebSocket webSocket) logger.LogInformation("Starting performance test"); - var eventMessage = Messages.EventResultMsg(subscription, "on", "off"); + var eventMessage = Messages.EventResultMsgMultiple(subscription, "on", "off"); for (var i = 0; i < 1000000; i++) + /*for (var i = 0; i < 5; i++)*/ { await SendWebsocketMessage(webSocket, eventMessage).ConfigureAwait(false); }