diff --git a/DD.Persistence.Database/Entity/TagValue.cs b/DD.Persistence.Database/Entity/TagValue.cs index fac478c..fbc7ca1 100644 --- a/DD.Persistence.Database/Entity/TagValue.cs +++ b/DD.Persistence.Database/Entity/TagValue.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace DD.Persistence.Database.Entity; @@ -35,8 +36,21 @@ public class TagSetValue: ITimestamped public float RotorTorque {get;set;} public float RotorSpeed {get;set;} public float Flow {get;set;} - public float Mse {get;set;} + public int Mse {get;set;} + public int Mode {get;set;} public float Pump0Flow {get;set;} public float Pump1Flow {get;set;} public float Pump2Flow { get; set; } +} + +[PrimaryKey(nameof(BagId), nameof(Timestamp))] +public class TagBag : ITimestamped +{ + [Comment("Временная отметка"), Key] + public DateTimeOffset Timestamp { get; set; } + + public Guid BagId { get; set; } + + [Column(TypeName = "jsonb")] + public object[] Values { get; set; } } \ No newline at end of file diff --git a/DD.Persistence.Database/PersistenceDbContext.cs b/DD.Persistence.Database/PersistenceDbContext.cs index b69bb98..698f9b1 100644 --- a/DD.Persistence.Database/PersistenceDbContext.cs +++ b/DD.Persistence.Database/PersistenceDbContext.cs @@ -13,6 +13,7 @@ public class PersistenceDbContext : DbContext public DbSet TagValues => Set(); public DbSet TagSetValues => Set(); + public DbSet TagBag => Set(); public DbSet Setpoint => Set(); @@ -41,5 +42,9 @@ public class PersistenceDbContext : DbContext modelBuilder.Entity() .Property(e => e.Value) .HasJsonConversion(); + + modelBuilder.Entity() + .Property(e => e.Values) + .HasJsonConversion(); } } diff --git a/DD.Persistence.TestTelemetryStress/DD.Persistence.TestTelemetryStress.csproj b/DD.Persistence.TestTelemetryStress/DD.Persistence.TestTelemetryStress.csproj index a2d6e5a..e478ef7 100644 --- a/DD.Persistence.TestTelemetryStress/DD.Persistence.TestTelemetryStress.csproj +++ b/DD.Persistence.TestTelemetryStress/DD.Persistence.TestTelemetryStress.csproj @@ -5,8 +5,17 @@ net9.0 enable enable + DD.Persistence.TestTelemetryStress.Program_v2 + + + + + + + + diff --git a/DD.Persistence.TestTelemetryStress/Program.cs b/DD.Persistence.TestTelemetryStress/Program_v1.cs similarity index 99% rename from DD.Persistence.TestTelemetryStress/Program.cs rename to DD.Persistence.TestTelemetryStress/Program_v1.cs index e493ccc..5245cf6 100644 --- a/DD.Persistence.TestTelemetryStress/Program.cs +++ b/DD.Persistence.TestTelemetryStress/Program_v1.cs @@ -9,7 +9,7 @@ using static System.Runtime.InteropServices.JavaScript.JSType; namespace DD.Persistence.TestTelemetryStress; -internal class Program +internal class Program_v1 { record TestDataItem(TagValue[] TagValues, TagSetValue[] TagSetValue); enum Table { TagValue, TagSetValue } diff --git a/DD.Persistence.TestTelemetryStress/Program_v2.cs b/DD.Persistence.TestTelemetryStress/Program_v2.cs new file mode 100644 index 0000000..676d2fb --- /dev/null +++ b/DD.Persistence.TestTelemetryStress/Program_v2.cs @@ -0,0 +1,354 @@ +using DD.Persistence.Database.Entity; +using Microsoft.EntityFrameworkCore; +using System.Diagnostics; +using System.IO; + +namespace DD.Persistence.TestTelemetryStress; + +internal class Program_v2 +{ + record TestDataItem(TagBag[] TagBags, TagSetValue[] TagSetValue); + enum Table { TagBag, 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 значений с одной отметкой времени. + * - TagBag - строка таблицы БД - 1 значение с отметкой времени и id параметра, для записи одной телеметрии используется 14 строк. + * Данные генерируются одинаковые и за все бурение - 30 дней *24 часа * 60 минут * 60 сек + * + * Вставка производится порциями по 172_800 записей (2 дня) + * Замеряется время каждой вставки для построения графика. Таблицы вставки чередуются. + * + * После вставки контекст уничтожается и делается пауза + * + * Создается новые контексты для тестирования каждой выборки по каждой таблице по отдельности. + * Параметры выборок для тестирования: + * - 10 раз за произвольные 10 минут, интервалы распределены по всему диапазону дат + * - 10 раз за произвольные 60 минут, интервалы распределены по всему диапазону дат + * - 10 раз за произвольные 24*60 минут, интервалы распределены по всему диапазону дат + */ + var begin = DateTimeOffset.Now; + Console.WriteLine($"Started at {begin}"); + var insertLogs = FillDB(); + Pause(); + var selectLogs = TestSelect(); + var logs = insertLogs.Union(selectLogs); + + var end = DateTimeOffset.Now; + Console.WriteLine($"complete at {end}, estimate {end - begin}"); + + AnalyzeAndExportLogs(logs); + + /* Анализ логов: + * - не замечено замедление вставки при увеличении размера таблиц 2592000 записей + * - размер таблиц в БД TagBag - 778Мб TagSetValue - 285Мб. В 2,73 раз больше + * - время вставки всех чанков TagBag - 2м11с, TagSetValue - 3м40с + * - время выборки из TagBag 21.69c (256180 записей) на 65% медленнее времени выборки из TagSetValue 13,11с (256180 записей) + * Вывод: приемлемо + */ + } + + private static void AnalyzeAndExportLogs(IEnumerable logs) + { + var orderedLogs = logs + .OrderBy(l => l.Action) + .ThenBy(l => l.Table) + .ThenBy(l => l.Timestamp); + + var groups = orderedLogs + .GroupBy(log => new { log.Action , log.Table }) + .OrderBy(group => group.Key.Action) + .ThenBy(group => group.Key.Table); + + using var analysisStream = File.CreateText($"Analysis_{DateTime.Now:yyyy-MM-dd-HH-mm-ss}.csv"); + + analysisStream.WriteLine("{Table}, {Action}, Sum(Time), Sum(Count), Max(TableLength)"); + + foreach (var group in groups) + { + var max = group.Max(i => i.tableLength); + var count = group.Sum(i => i.count); + var timeMs = group.Sum(i => i.TimeSpan.TotalMilliseconds); + var time = TimeSpan.FromMilliseconds(timeMs); + var line = $"{group.Key.Table}, {group.Key.Action}, {time}, {count}, {max}"; + analysisStream.WriteLine(line); + } + + analysisStream.WriteLine(string.Empty); + analysisStream.WriteLine("{Table}, {Action}, Avg(Time), Avg(Count), Max(TableLength)"); + + foreach (var group in groups.Where(g => g.Key.Action == Action.select)) + { + var subGroups = group.GroupBy(i => Math.Round( 0.01d * i.count)); + + foreach (var subGroup in subGroups) + { + var max = subGroup.Max(i => i.tableLength); + var count = subGroup.Average(i => i.count); + var timeMs = subGroup.Average(i => i.TimeSpan.TotalMilliseconds); + var time = TimeSpan.FromMilliseconds(timeMs); + var line = $"{group.Key.Table}, {group.Key.Action}, {time}, {count}, {max}"; + analysisStream.WriteLine(line); + } + } + + analysisStream.WriteLine(string.Empty); + analysisStream.WriteLine("Table, Action, Time, Count, TableLength"); + + foreach (var log in orderedLogs) + { + analysisStream.WriteLine($"{log.Table}, {log.Action}, {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 logStopwatch = Stopwatch.StartNew(); + var data = GenerateData(count, begin, increment); + Console.WriteLine($"Data[{data.Length}] generated {logStopwatch.Elapsed}"); + + logStopwatch.Restart(); + using var db = GetDb(); + db.Database.EnsureDeleted(); + db.Database.EnsureCreated(); + Console.WriteLine($"Database recreated {logStopwatch.Elapsed}"); + + + var tagBagSet = db.Set(); + var tagSetValueSet = db.Set(); + + var tagBagInsertedCount = 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, tagBagInsertedCount, inserted)); + tagBagInsertedCount += inserted; + db.ChangeTracker.Clear(); + + Console.WriteLine($"chunk {i} of {data.Length} TagSetValue[{chunk.TagSetValue.Length}] added {stopwatch.Elapsed}"); + stopwatch.Restart(); + + tagBagSet.AddRange(chunk.TagBags); + inserted = db.SaveChanges(); + logs.Add(new Log(Table.TagBag, Action.insert, DateTimeOffset.UtcNow, stopwatch.Elapsed, tagSetValueInsertedCount, inserted)); + tagSetValueInsertedCount += inserted; + db.ChangeTracker.Clear(); + + Console.WriteLine($"chunk {i} of {data.Length} TagBags[{chunk.TagBags.Length}] added {stopwatch.Elapsed}"); + } + + return logs.ToArray(); + } + + private static void Pause() + { + GC.Collect(); + Thread.Sleep(1000); + } + + private static Log[] TestSelect() + { + var testDateRanges = GetTestDateRanges(); + var logs1 = TestSelect(Table.TagSetValue, testDateRanges); + var logs2 = TestSelect(Table.TagBag, 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(); + var count = selected.Length; + var maxTime = selected.Max(e => e.Timestamp); + logs.Add(new Log(table, Action.select, DateTimeOffset.UtcNow, stopwatch.Elapsed, totalCount, count)); + } + 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 ? max1 : max2; + + var list1 = CalculateRanges(min, max, TimeSpan.FromMinutes(60), 10); + var list2 = CalculateRanges(min, max, TimeSpan.FromMinutes(12 * 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 + range; + result[i] = (b, e); + } + return result; + } + + private static DbContext GetDb() + { + var factory = new Database.Postgres.DesignTimeDbContextFactory(); + var context = factory.CreateDbContext(Array.Empty()); + return context; + } + + private static TestDataItem[] GenerateData(int count, DateTimeOffset begin, TimeSpan increment) + { + var chunkLimit = 172_800; + 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 tagBags = []; + List tagSetValue = []; + for (int i = 0; i < chunkLimit; i++) + { + var tagSet = GenerateTagSetValue(begin); + tagSetValue.Add(tagSet); + + var tagBag = MakeTagBag(tagSet); + tagBags.Add(tagBag); + begin += increment; + } + + return new(tagBags.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 = random.Next(), + Mode = random.Next(), + Pump0Flow = 100f * random.NextSingle(), + Pump1Flow = 100f * random.NextSingle(), + Pump2Flow = 100f * random.NextSingle(), + }; + return result; + } + + private static Guid BagId = Guid.NewGuid(); + private static TagBag MakeTagBag(TagSetValue tagSet) + { + TagBag data = new() + { + BagId = BagId, + Timestamp = tagSet.Timestamp, + Values = + [ + tagSet.WellDepth, + tagSet.BitDepth, + tagSet.BlockPosition, + tagSet.BlockSpeed, + tagSet.Pressure, + tagSet.AxialLoad, + tagSet.HookWeight, + tagSet.RotorTorque, + tagSet.RotorSpeed, + tagSet.Flow, + tagSet.Mse, + tagSet.Mode, + tagSet.Pump0Flow, + tagSet.Pump1Flow, + tagSet.Pump2Flow, + ], + //new Dictionary() + //{ + // {"WellDepth", tagSet.WellDepth }, + // {"BitDepth", tagSet.BitDepth}, + // {"BlockPosition", tagSet.BlockPosition}, + // {"BlockSpeed", tagSet.BlockSpeed}, + // {"Pressure", tagSet.Pressure}, + // {"AxialLoad", tagSet.AxialLoad}, + // {"HookWeight", tagSet.HookWeight}, + // {"RotorTorque", tagSet.RotorTorque}, + // {"RotorSpeed", tagSet.RotorSpeed}, + // {"Flow", tagSet.Flow}, + // {"Mse", tagSet.Mse}, + // {"Pump0Flow", tagSet.Pump0Flow}, + // {"Pump1Flow", tagSet.Pump1Flow}, + // {"Pump2Flow", tagSet.Pump2Flow}, + //} + + }; + return data; + } + +} diff --git a/DD.Persistence.TestTelemetryStress/Results.md b/DD.Persistence.TestTelemetryStress/Results.md new file mode 100644 index 0000000..0bb8f1d --- /dev/null +++ b/DD.Persistence.TestTelemetryStress/Results.md @@ -0,0 +1,26 @@ +## Dictionary + +TagBag insert 00:02:31,55 2592000 2419200 +TagSetValue insert 00:04:14,89 2592000 2419200 +TagBag select 00:00:16,91 905997 2592000 +TagSetValue select 00:00:02,42 905997 2592000 + +sizeof(TagBag) = 1200M +sizeof(TagSetValue) = 285M +Tot = 7m + +## object[] + +{Table} {Action} Sum(Time) Sum(Count) Max(TableLength) +TagBag insert 00:02:13,39 2592000 2419200 +TagSetValue insert 00:03:57,70 2592000 2419200 +TagBag select 00:00:12,60 1331997 2592000 +TagSetValue select 00:00:02,88 1331997 2592000 + +sizeof(TagBag) = 805M +sizeof(TagSetValue) = 305M + +## multi rows +размер таблиц в БД TagValue - 1200Мб TagSetValue - 119Мб. В 10 раз больше +время вставки чанка TagValue в 7.3 раза больше времени вставки чанка TagSetValue +время выборки из TagValue в 16,4 раза больше времени выборки из TagSetValue \ No newline at end of file