using System.Collections.Concurrent; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates; using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport; using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase; using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories; using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Services; using static ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests.Import.ImportRoundTripBudgetTests; namespace ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests.Import; /// /// Gitea #47 (large-bundle hardening): the apply's post-flush template rewire passes /// (alarm → on-trigger script links, composition edges, inheritance edges) looked every /// bundled template up with a DbSet.Local scan — each one a full /// DetectChanges over everything the import had staged — and re-read each template's /// children one query at a time. Apply time grew quadratically (measured on the in-memory /// provider: 100 / 400 / 800 composing, derived templates took 0.5 s / 8.1 s / 26.5 s; /// after the fix 0.13 s / 0.62 s / 0.64 s). These tests pin the cost to the bundle-size- /// independent shape: a constant number of DetectChanges passes and batched /// children reads, on both the Add and the Overwrite path, without changing the result. /// public sealed class ImportApplyScalingTests { private sealed class Env : IDisposable { public Env() { var services = new ServiceCollection(); services.AddSingleton( new Microsoft.AspNetCore.DataProtection.EphemeralDataProtectionProvider()); services.AddSingleton(new ConfigurationBuilder().AddInMemoryCollection().Build()); var dbName = $"ImportApplyScalingTests_{Guid.NewGuid()}"; services.AddDbContext(opts => opts .UseInMemoryDatabase(dbName) .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning))); services.AddSingleton(); services.AddScoped(sp => CountingProxy.Wrap( new TemplateEngineRepository( sp.GetRequiredService(), sp.GetRequiredService()), Calls)); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddTransport(); Provider = services.BuildServiceProvider(); } public ServiceProvider Provider { get; } public ConcurrentDictionary Calls { get; } = new(StringComparer.Ordinal); public int CallsTo(string method) => Calls.TryGetValue(method, out var n) ? n : 0; public void Dispose() => Provider.Dispose(); } private sealed record ApplyCost(int DetectChanges, int PerTemplateReads, int BatchedReads, ImportResult Result); /// /// Seeds templates that derive from a base, compose a module and /// carry an alarm whose on-trigger script is one of their own scripts, then exports them. /// private static async Task ExportLibraryAsync(int count) { using var source = new Env(); await using (var scope = source.Provider.CreateAsyncScope()) { var ctx = scope.ServiceProvider.GetRequiredService(); var module = new Template("Module"); module.Attributes.Add(new TemplateAttribute("Speed") { DataType = DataType.Float, Value = "1" }); var baseT = new Template("Base"); baseT.Attributes.Add(new TemplateAttribute("Flow") { DataType = DataType.Float, Value = "1" }); ctx.Templates.AddRange(module, baseT); await ctx.SaveChangesAsync(); var templates = new List