diff --git a/DD.Persistence.Database/Entity/TagValue.cs b/DD.Persistence.Database/Entity/TagValue.cs new file mode 100644 index 0000000..fac478c --- /dev/null +++ b/DD.Persistence.Database/Entity/TagValue.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; + +namespace DD.Persistence.Database.Entity; + +public interface ITimestamped +{ + DateTimeOffset Timestamp { get; set; } +} + +[PrimaryKey(nameof(ParameterId), nameof(Timestamp))] +public class TagValue: ITimestamped +{ + [Comment("Id параметра")] + public int ParameterId { get; set; } + + [Comment("Временная отметка")] + public DateTimeOffset Timestamp { get; set; } + + [Comment("Значение параметра")] + public float Value { get; set; } +} + +public class TagSetValue: ITimestamped +{ + [Comment("Временная отметка"), Key] + public DateTimeOffset Timestamp { get; set; } + public float WellDepth {get;set;} + public float BitDepth {get;set;} + public float BlockPosition {get;set;} + public float BlockSpeed {get;set;} + public float Pressure {get;set;} + public float AxialLoad {get;set;} + public float HookWeight {get;set;} + public float RotorTorque {get;set;} + public float RotorSpeed {get;set;} + public float Flow {get;set;} + public float Mse {get;set;} + public float Pump0Flow {get;set;} + public float Pump1Flow {get;set;} + public float Pump2Flow { get; set; } +} \ No newline at end of file diff --git a/DD.Persistence.Database/PersistenceDbContext.cs b/DD.Persistence.Database/PersistenceDbContext.cs index a0587cd..b69bb98 100644 --- a/DD.Persistence.Database/PersistenceDbContext.cs +++ b/DD.Persistence.Database/PersistenceDbContext.cs @@ -11,6 +11,9 @@ public class PersistenceDbContext : DbContext { public DbSet DataSaub => Set(); + public DbSet TagValues => Set(); + public DbSet TagSetValues => Set(); + public DbSet Setpoint => Set(); public DbSet TimestampedSets => Set(); diff --git a/DD.Persistence.TestTelemetryStress/DD.Persistence.TestTelemetryStress.csproj b/DD.Persistence.TestTelemetryStress/DD.Persistence.TestTelemetryStress.csproj new file mode 100644 index 0000000..a2d6e5a --- /dev/null +++ b/DD.Persistence.TestTelemetryStress/DD.Persistence.TestTelemetryStress.csproj @@ -0,0 +1,14 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + diff --git a/DD.Persistence.TestTelemetryStress/Program.cs b/DD.Persistence.TestTelemetryStress/Program.cs new file mode 100644 index 0000000..e493ccc --- /dev/null +++ b/DD.Persistence.TestTelemetryStress/Program.cs @@ -0,0 +1,267 @@ + +using DD.Persistence.Database.Entity; +using Microsoft.EntityFrameworkCore; +using Npgsql.Internal; +using System.Diagnostics; +using System.Linq; +using System.Runtime.Intrinsics.X86; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace DD.Persistence.TestTelemetryStress; + +internal class Program +{ + record TestDataItem(TagValue[] TagValues, TagSetValue[] TagSetValue); + enum Table { TagValue, TagSetValue } + enum Action { insert, select } + + record Log( + Table Table, + Action Action, + DateTimeOffset Timestamp, + TimeSpan TimeSpan, + int tableLength, + int count); + + static Random random = new Random((int)(DateTimeOffset.UtcNow.Ticks % 0x7FFFFFFF)); + + static void Main(string[] args) + { + /* Цель теста сравнить время отклика БД с телеметрией за все бурение + * по 2м схемам хранения: + * - TagSetValue - строка таблицы БД - 14 значений с одной отметкой времени. + * - TagValue - строка таблицы БД - 1 значение с отметкой времени и id параметра, для записи одной телеметрии используется 14 строк. + * Данные генерируются одинаковые и за все бурение - 30 дней *24 часа * 60 минут * 60 сек + * + * Вставка производится порциями по 56_000 записей для TagValue и по 4000 (56к/14) для TagSetValue. + * Замеряется время каждой вставки для построения графика. Таблицы вставки чередуются. + * + * После вставки контекст уничтожается и делается пауза + * + * Создается новые контексты для тестирования каждой выборки по каждой таблице по отдельности. + * Параметры выборок для тестирования: + * - 10 раз за произвольные 10 минут, интервалы распределены по всему диапазону дат + * - 10 раз за произвольные 60 минут, интервалы распределены по всему диапазону дат + * - 10 раз за произвольные 24*60 минут, интервалы распределены по всему диапазону дат + */ + + var insertLogs = FillDB(); + Pause(); + var selectLogs = TestSelect(); + var logs = insertLogs.Union(selectLogs); + ExportLogs(logs); + /* Анализ логов: + * - не замечено замедление вставки при увеличении размера таблиц + * - размер таблиц в БД TagValue - 1200Мб TagSetValue - 119Мб. В 10 раз больше + * - время вставки чанка TagValue в 7.3 раза больше времени вставки чанка TagSetValue + * - время выборки из TagValue в 16,4 раза больше времени выборки из TagSetValue + */ + } + + private static void ExportLogs(IEnumerable logs) + { + using var stream = File.CreateText("log.csv"); + foreach (Log log in logs) + { + stream.WriteLine($"{log.Table}, {log.Action}, {log.Timestamp}, {log.TimeSpan}, {log.count}, {log.tableLength}"); + } + } + + private static Log[] FillDB() + { + var begin = DateTimeOffset.UtcNow; + var increment = TimeSpan.FromSeconds(1); + var count = 30 * 24 * 60 * 60; + var data = GenerateData(count, begin, increment); + + using var db = GetDb(); + var tagValueSet = db.Set(); + var tagSetValueSet = db.Set(); + + var tagValueInsertedCount = 0; + var tagSetValueInsertedCount = 0; + var logs = new List(data.Length * 2); + + for (var i = 0; i < data.Length; i++) + { + var chunk = data[i]; + var inserted = 0; + + var stopwatch = Stopwatch.StartNew(); + tagSetValueSet.AddRange(chunk.TagSetValue); + inserted = db.SaveChanges(); + logs.Add(new Log(Table.TagSetValue, Action.insert, DateTimeOffset.UtcNow, stopwatch.Elapsed, tagSetValueInsertedCount, count)); + tagValueInsertedCount += inserted; + db.ChangeTracker.Clear(); + + stopwatch.Restart(); + + tagValueSet.AddRange(chunk.TagValues); + inserted = db.SaveChanges(); + logs.Add(new Log(Table.TagValue, Action.insert, DateTimeOffset.UtcNow, stopwatch.Elapsed, tagValueInsertedCount, count)); + tagSetValueInsertedCount += inserted; + db.ChangeTracker.Clear(); + } + + return logs.ToArray(); + } + + private static void Pause() + { + Thread.Sleep(1000); + } + + private static Log[] TestSelect() + { + var testDateRanges = GetTestDateRanges(); + var logs1 = TestSelect(Table.TagSetValue, testDateRanges); + var logs2 = TestSelect(Table.TagValue, testDateRanges); + return [.. logs1, .. logs2]; + } + + private static Log[] TestSelect(Table table, (DateTimeOffset begin, DateTimeOffset end)[] ranges) + where T : class, ITimestamped + { + using var db = GetDb(); + var dbSet = db.Set(); + var totalCount = dbSet.Count(); + + var logs = new List(ranges.Length); + + foreach (var range in ranges) + { + var stopwatch = Stopwatch.StartNew(); + var selected = dbSet + .Where(e => e.Timestamp > range.begin) + .Where(e => e.Timestamp < range.end) + .ToArray(); + logs.Add(new(table, Action.select, DateTimeOffset.UtcNow, stopwatch.Elapsed, totalCount, selected.Length)); + } + return logs.ToArray(); + } + + private static (DateTimeOffset begin, DateTimeOffset end)[] GetTestDateRanges() + { + using var db = GetDb(); + + var dbSet1 = db.Set(); + var min1 = dbSet1.Min(e => e.Timestamp); + var max1 = dbSet1.Max(e => e.Timestamp); + + var dbSet2 = db.Set(); + var min2 = dbSet2.Min(e => e.Timestamp); + var max2 = dbSet2.Max(e => e.Timestamp); + + var min = min1 < min2 ? min1 : min2; + var max = max1 < max2 ? max2 : max1; + + var list1 = CalculateRanges(min, max, TimeSpan.FromMinutes(10), 10); + var list2 = CalculateRanges(min, max, TimeSpan.FromMinutes(60), 10); + var list3 = CalculateRanges(min, max, TimeSpan.FromMinutes(24 * 60), 10); + return [..list1, ..list2, ..list3]; + } + + private static (DateTimeOffset begin, DateTimeOffset end)[] CalculateRanges(DateTimeOffset min, DateTimeOffset max, TimeSpan range, int count) + { + if (max - min < range) + throw new ArgumentException("max - min < range", nameof(range)); + + var result = new (DateTimeOffset begin, DateTimeOffset end)[count]; + var max1 = max - range; + var delta = max1 - min; + var step = delta / count; + for (var i = 0; i < count; i++) + { + var b = min + i * step; + var e = b + step; + result[i] = (b, e); + } + return result; + } + + private static DbContext GetDb() + { + var factory = new Database.Postgres.DesignTimeDbContextFactory(); + var context = factory.CreateDbContext(Array.Empty()); + context.Database.EnsureCreated(); + return context; + } + + private static TestDataItem[] GenerateData(int count, DateTimeOffset begin, TimeSpan increment) + { + var chunkLimit = 4000; + var chunks = new List((count + chunkLimit) / chunkLimit); + + for (int i = 0; i < count; i += chunkLimit) + { + var item = GenerateDataChunk(begin, increment, chunkLimit); + chunks.Add(item); + begin += increment * chunkLimit; + } + + return chunks.ToArray(); + } + + private static TestDataItem GenerateDataChunk(DateTimeOffset begin, TimeSpan increment, int chunkLimit) + { + List tagValues = []; + List tagSetValue = []; + for (int i = 0; i < chunkLimit; i++) + { + var tagSet = GenerateTagSetValue(begin); + tagSetValue.Add(tagSet); + + var items = MakeTagValues(tagSet); + tagValues.AddRange(items); + begin += increment; + } + + return new(tagValues.ToArray(), tagSetValue.ToArray()); + } + + private static TagSetValue GenerateTagSetValue(DateTimeOffset begin) + { + var result = new TagSetValue + { + Timestamp = begin, + WellDepth = 100f * random.NextSingle(), + BitDepth = 100f * random.NextSingle(), + BlockPosition = 100f * random.NextSingle(), + BlockSpeed = 100f * random.NextSingle(), + Pressure = 100f * random.NextSingle(), + AxialLoad = 100f * random.NextSingle(), + HookWeight = 100f * random.NextSingle(), + RotorTorque = 100f * random.NextSingle(), + RotorSpeed = 100f * random.NextSingle(), + Flow = 100f * random.NextSingle(), + Mse = 100f * random.NextSingle(), + Pump0Flow = 100f * random.NextSingle(), + Pump1Flow = 100f * random.NextSingle(), + Pump2Flow = 100f * random.NextSingle(), + }; + return result; + } + + private static TagValue[] MakeTagValues(TagSetValue tagSet) + { + TagValue[] data = + [ + new (){ Timestamp = tagSet.Timestamp, ParameterId = 1, Value = tagSet.WellDepth}, + new (){ Timestamp = tagSet.Timestamp, ParameterId = 2, Value = tagSet.BitDepth}, + new (){ Timestamp = tagSet.Timestamp, ParameterId = 3, Value = tagSet.BlockPosition}, + new (){ Timestamp = tagSet.Timestamp, ParameterId = 4, Value = tagSet.BlockSpeed}, + new (){ Timestamp = tagSet.Timestamp, ParameterId = 5, Value = tagSet.Pressure}, + new (){ Timestamp = tagSet.Timestamp, ParameterId = 6, Value = tagSet.AxialLoad}, + new (){ Timestamp = tagSet.Timestamp, ParameterId = 7, Value = tagSet.HookWeight}, + new (){ Timestamp = tagSet.Timestamp, ParameterId = 8, Value = tagSet.RotorTorque}, + new (){ Timestamp = tagSet.Timestamp, ParameterId = 9, Value = tagSet.RotorSpeed}, + new (){ Timestamp = tagSet.Timestamp, ParameterId = 10, Value = tagSet.Flow}, + new (){ Timestamp = tagSet.Timestamp, ParameterId = 11, Value = tagSet.Mse}, + new (){ Timestamp = tagSet.Timestamp, ParameterId = 12, Value = tagSet.Pump0Flow}, + new (){ Timestamp = tagSet.Timestamp, ParameterId = 13, Value = tagSet.Pump1Flow}, + new (){ Timestamp = tagSet.Timestamp, ParameterId = 14, Value = tagSet.Pump2Flow}, + ]; + return data; + } + +} diff --git a/DD.Persistence.sln b/DD.Persistence.sln index 22c6b2a..c8b5be5 100644 --- a/DD.Persistence.sln +++ b/DD.Persistence.sln @@ -19,6 +19,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DD.Persistence.Client", "DD EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DD.Persistence.App", "DD.Persistence.App\DD.Persistence.App.csproj", "{063238BF-E982-43FA-9DDB-7D7D279086D8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DD.Persistence.TestTelemetryStress", "DD.Persistence.TestTelemetryStress\DD.Persistence.TestTelemetryStress.csproj", "{A75B3712-1E1D-4C5B-B97C-72C80E7D6BDE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,6 +59,10 @@ Global {063238BF-E982-43FA-9DDB-7D7D279086D8}.Debug|Any CPU.Build.0 = Debug|Any CPU {063238BF-E982-43FA-9DDB-7D7D279086D8}.Release|Any CPU.ActiveCfg = Release|Any CPU {063238BF-E982-43FA-9DDB-7D7D279086D8}.Release|Any CPU.Build.0 = Release|Any CPU + {A75B3712-1E1D-4C5B-B97C-72C80E7D6BDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A75B3712-1E1D-4C5B-B97C-72C80E7D6BDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A75B3712-1E1D-4C5B-B97C-72C80E7D6BDE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A75B3712-1E1D-4C5B-B97C-72C80E7D6BDE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE