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; } }