diff --git a/Directory.Packages.props b/Directory.Packages.props index 19ce069eaf..842e7ef139 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -50,6 +50,11 @@ + + + + + diff --git a/Orleans.sln b/Orleans.sln index b9967bcc95..3281faaf42 100644 --- a/Orleans.sln +++ b/Orleans.sln @@ -221,6 +221,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Orleans.Streaming.AdoNet", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks.AdoNet", "test\Benchmarks.AdoNet\Benchmarks.AdoNet.csproj", "{B8F43537-2D2E-42A0-BE67-5E07E4313AEA}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "playground", "playground", "{A41DE3D1-F8AA-4234-BE6F-3C9646A1507A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DashboardToy", "DashboardToy", "{316CDCC7-323F-4264-9FC9-667662BB1F80}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DashboardToy.Frontend", "playground\DashboardToy\DashboardToy.Frontend\DashboardToy.Frontend.csproj", "{C4DD4F96-3EC6-47C6-97AA-9B14F0F2099B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DashboardToy.AppHost", "playground\DashboardToy\DashboardToy.AppHost\DashboardToy.AppHost.csproj", "{84B44F1D-B7FE-40E3-82F0-730A55AC8613}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -587,6 +595,14 @@ Global {B8F43537-2D2E-42A0-BE67-5E07E4313AEA}.Debug|Any CPU.Build.0 = Debug|Any CPU {B8F43537-2D2E-42A0-BE67-5E07E4313AEA}.Release|Any CPU.ActiveCfg = Release|Any CPU {B8F43537-2D2E-42A0-BE67-5E07E4313AEA}.Release|Any CPU.Build.0 = Release|Any CPU + {C4DD4F96-3EC6-47C6-97AA-9B14F0F2099B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4DD4F96-3EC6-47C6-97AA-9B14F0F2099B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4DD4F96-3EC6-47C6-97AA-9B14F0F2099B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4DD4F96-3EC6-47C6-97AA-9B14F0F2099B}.Release|Any CPU.Build.0 = Release|Any CPU + {84B44F1D-B7FE-40E3-82F0-730A55AC8613}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {84B44F1D-B7FE-40E3-82F0-730A55AC8613}.Debug|Any CPU.Build.0 = Debug|Any CPU + {84B44F1D-B7FE-40E3-82F0-730A55AC8613}.Release|Any CPU.ActiveCfg = Release|Any CPU + {84B44F1D-B7FE-40E3-82F0-730A55AC8613}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -695,6 +711,9 @@ Global {A073C0EE-8732-42F9-A22E-D47034E25076} = {4CD3AA9E-D937-48CA-BB6C-158E12257D23} {2B994F33-16CF-4679-936A-5AEABC529D2C} = {EB2EDE59-5021-42EE-A97A-D59939B39C66} {B8F43537-2D2E-42A0-BE67-5E07E4313AEA} = {2CAB7894-777C-42B1-8B1E-322868CE92C7} + {316CDCC7-323F-4264-9FC9-667662BB1F80} = {A41DE3D1-F8AA-4234-BE6F-3C9646A1507A} + {C4DD4F96-3EC6-47C6-97AA-9B14F0F2099B} = {316CDCC7-323F-4264-9FC9-667662BB1F80} + {84B44F1D-B7FE-40E3-82F0-730A55AC8613} = {316CDCC7-323F-4264-9FC9-667662BB1F80} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7BFB3429-B5BB-4DB1-95B4-67D77A864952} diff --git a/distributed-tests.yml b/distributed-tests.yml index f4db953da7..16cff5e388 100644 --- a/distributed-tests.yml +++ b/distributed-tests.yml @@ -2,7 +2,7 @@ variables: clusterId: '{{ "now" | date: "%s" }}' serviceId: '{{ "now" | date: "%s" }}' secretSource: KeyVault - framework: net7.0 + framework: net8.0 jobs: server: @@ -10,7 +10,7 @@ jobs: localFolder: Artifacts/DistributedTests/DistributedTests.Server/{{framework}} executable: DistributedTests.Server.exe readyStateText: Orleans Silo started. - framework: net7.0 + framework: net8.0 arguments: "{{configurator}} --clusterId {{clusterId}} --serviceId {{serviceId}} --secretSource {{secretSource}} {{configuratorOptions}}" onConfigure: - if (job.endpoints.Count > 0) { @@ -21,7 +21,7 @@ jobs: localFolder: Artifacts/DistributedTests/DistributedTests.Client/{{framework}} executable: DistributedTests.Client.exe waitForExit: true - framework: net7.0 + framework: net8.0 arguments: "{{command}} --clusterId {{clusterId}} --serviceId {{serviceId}} --secretSource {{secretSource}} {{commandOptions}}" onConfigure: - if (job.endpoints.Count > 0) { @@ -46,6 +46,22 @@ scenarios: requestsPerBlock: 500 duration: 120 commandOptions: "--numWorkers {{numWorkers}} --blocksPerWorker {{blocksPerWorker}} --requestsPerBlock {{requestsPerBlock}} --duration {{duration}}" + fanout: + server: + job: server + variables: + instances: 10 + configurator: SimpleSilo + client: + job: client + variables: + command: fan-out + instances: 1 + numWorkers: 1 + blocksPerWorker: 0 + requestsPerBlock: 50 + duration: 240 + commandOptions: "--numWorkers {{numWorkers}} --blocksPerWorker {{blocksPerWorker}} --requestsPerBlock {{requestsPerBlock}} --duration {{duration}}" streaming: server: job: server @@ -125,6 +141,35 @@ scenarios: duration: 180 commandOptions: "--numWorkers {{numWorkers}} --blocksPerWorker {{blocksPerWorker}} --requestsPerBlock {{requestsPerBlock}} --duration {{duration}}" +counters: +- provider: Microsoft.Orleans + values: + - name: app-requests + measurement: orleans-counter/requests-per-second + description: Request rate + + - name: activation-count + measurement: orleans-counter/grain-activation-count + description: Total number of grains + +results: +# Microsoft.Orleans counters +- name: orleans-counter/requests-per-second + measurement: orleans-counter/requests-per-second + description: Request rate + format: "n0" + aggregate: max + reduce: max +- name: orleans-counter/requests-per-second/95 + measurement: orleans-counter/requests-per-second + description: Request rate + format: "n0" + aggregate: percentile95 + reduce: max +- name: activation-count + measurement: orleans-counter/grain-activation-count + description: Active grains + profiles: local: variables: diff --git a/playground/DashboardToy/DashboardToy.AppHost/DashboardToy.AppHost.csproj b/playground/DashboardToy/DashboardToy.AppHost/DashboardToy.AppHost.csproj new file mode 100644 index 0000000000..b00b2d429c --- /dev/null +++ b/playground/DashboardToy/DashboardToy.AppHost/DashboardToy.AppHost.csproj @@ -0,0 +1,24 @@ + + + + Exe + net8.0 + enable + enable + + + + 6a521b87-2bf9-4af8-b7c7-4947536e1d50 + + + + + + + + + + + + + diff --git a/playground/DashboardToy/DashboardToy.AppHost/Program.cs b/playground/DashboardToy/DashboardToy.AppHost/Program.cs new file mode 100644 index 0000000000..22ba50d04b --- /dev/null +++ b/playground/DashboardToy/DashboardToy.AppHost/Program.cs @@ -0,0 +1,16 @@ +using Projects; + +var builder = DistributedApplication.CreateBuilder(args); +var redis = builder.AddRedis("orleans-redis"); + +var orleans = builder.AddOrleans("cluster") + .WithClustering(redis); + +/* +// Comment this out once Aspire no longer requires a 'workload' to build. +builder.AddProject("frontend") + .WithReference(orleans) + .WithReplicas(5); +*/ + +builder.Build().Run(); diff --git a/playground/DashboardToy/DashboardToy.AppHost/Properties/launchSettings.json b/playground/DashboardToy/DashboardToy.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000000..f6511e66f9 --- /dev/null +++ b/playground/DashboardToy/DashboardToy.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17234;http://localhost:15087", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21284", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22143" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15087", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19030", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20232" + } + } + } +} diff --git a/playground/DashboardToy/DashboardToy.AppHost/appsettings.Development.json b/playground/DashboardToy/DashboardToy.AppHost/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/playground/DashboardToy/DashboardToy.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/playground/DashboardToy/DashboardToy.AppHost/appsettings.json b/playground/DashboardToy/DashboardToy.AppHost/appsettings.json new file mode 100644 index 0000000000..31c092aa45 --- /dev/null +++ b/playground/DashboardToy/DashboardToy.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/playground/DashboardToy/DashboardToy.Frontend/DashboardToy.Frontend.csproj b/playground/DashboardToy/DashboardToy.Frontend/DashboardToy.Frontend.csproj new file mode 100644 index 0000000000..510c6a3a04 --- /dev/null +++ b/playground/DashboardToy/DashboardToy.Frontend/DashboardToy.Frontend.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + diff --git a/playground/DashboardToy/DashboardToy.Frontend/Data/ClusterDiagnosticsService.cs b/playground/DashboardToy/DashboardToy.Frontend/Data/ClusterDiagnosticsService.cs new file mode 100644 index 0000000000..5126d68992 --- /dev/null +++ b/playground/DashboardToy/DashboardToy.Frontend/Data/ClusterDiagnosticsService.cs @@ -0,0 +1,141 @@ +using System.Runtime.InteropServices; +using Orleans.Core.Internal; + +namespace DashboardToy.Frontend.Data; + +public class ClusterDiagnosticsService(IGrainFactory grainFactory) +{ + private readonly Dictionary _hostKeys = []; + private readonly Dictionary _hostDetails = []; + private readonly Dictionary _grainDetails = []; // Grain to host id + private readonly Dictionary _edges = []; + private readonly IManagementGrain _managementGrain = grainFactory.GetGrain(0); + private readonly record struct GrainDetails(int GrainKey, int HostKey); + private readonly record struct HostDetails(int HostKey, int ActivationCount); + private int _version; + + public async ValueTask GetGrainCallFrequencies() + { + var loaderGrain = grainFactory.GetGrain("root"); + var loaderGrainType = loaderGrain.GetGrainId().Type; + var resetCount = await loaderGrain.GetResetCount(); + if (resetCount > _version) + { + _version = resetCount; + await ResetAsync(); + } + + _edges.Clear(); + var maxEdgeValue = 0; + var maxActivationCount = 0; + + var silos = (await _managementGrain.GetHosts(onlyActive: true)).Keys.Order(); + foreach (var silo in silos) + { + var hostKey = GetHostVertex(silo); + var activationCount = 0; + foreach (var activation in await _managementGrain.GetDetailedGrainStatistics(hostsIds: [silo])) + { + if (activation.GrainId.Type.Equals(loaderGrainType)) continue; + if (activation.GrainId.IsSystemTarget()) continue; + var details = GetGrainVertex(activation.GrainId, hostKey); + _grainDetails[activation.GrainId] = new(details.GrainKey, hostKey); + ++activationCount; + } + + maxActivationCount = Math.Max(maxActivationCount, activationCount); + _hostDetails[silo] = new(hostKey, activationCount); + } + + foreach (var edge in await _managementGrain.GetGrainCallFrequencies()) + { + if (edge.TargetGrain.Type.Equals(loaderGrainType) || edge.SourceGrain.Type.Equals(loaderGrainType)) continue; + if (edge.TargetGrain.IsSystemTarget() || edge.SourceGrain.IsSystemTarget()) continue; + var sourceHostId = GetHostVertex(edge.SourceHost); + var targetHostId = GetHostVertex(edge.TargetHost); + var sourceVertex = GetGrainVertex(edge.SourceGrain, sourceHostId); + var targetVertex = GetGrainVertex(edge.TargetGrain, targetHostId); + maxEdgeValue = Math.Max(maxEdgeValue, (int)edge.CallCount); + UpdateEdge(new(sourceVertex.GrainKey, targetVertex.GrainKey), edge.CallCount); + } + + var grainIds = new List(_grainDetails.Count); + CollectionsMarshal.SetCount(grainIds, _grainDetails.Count); + foreach ((var grainId, var (grainKey, hostKey)) in _grainDetails) + { + grainIds[grainKey] = new(grainId.ToString(), grainId.Key.ToString()!, hostKey, 1.0); + } + + var hostIds = new List(_hostKeys.Count); + CollectionsMarshal.SetCount(hostIds, _hostKeys.Count); + foreach ((var hostId, var key) in _hostKeys) + { + var details = _hostDetails[hostId]; + hostIds[key] = new(hostId.ToString(), details.ActivationCount); + } + + var edges = new List(); + + foreach (var edge in _edges) + { + edges.Add(new(edge.Key.Source, edge.Key.Target, edge.Value)); + } + + return new(grainIds, hostIds, edges, maxEdgeValue, maxActivationCount); + } + + internal async ValueTask ResetAsync() + { + var fanoutType = grainFactory.GetGrain(0, "0").GetGrainId().Type; + foreach (var activation in await _managementGrain.GetDetailedGrainStatistics()) + { + if (!activation.GrainId.Type.Equals(fanoutType)) continue; + await grainFactory.GetGrain(activation.GrainId).DeactivateOnIdle(); + } + + Reset(); + } + + internal void Reset() + { + _hostKeys.Clear(); + _hostDetails.Clear(); + _grainDetails.Clear(); + _edges.Clear(); + } + + private GrainDetails GetGrainVertex(GrainId grainId, int hostKey) + { + ref var key = ref CollectionsMarshal.GetValueRefOrAddDefault(_grainDetails, grainId, out var exists); + if (!exists) + { + key = new (_grainDetails.Count - 1, hostKey); + } + + return key; + } + + private int GetHostVertex(SiloAddress silo) + { + ref var key = ref CollectionsMarshal.GetValueRefOrAddDefault(_hostKeys, silo, out var exists); + if (!exists) + { + key = _hostKeys.Count - 1; + } + + return key; + } + + private void UpdateEdge(Key key, ulong increment) + { + ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_edges, key, out var exists); + count += increment; + } +} + +public record class CallGraph(List GrainIds, List HostIds, List Edges, int MaxEdgeValue, int MaxActivationCount); + +public record struct HostNode(string Name, int ActivationCount); +public record struct GraphNode(string Name, string Key, int Host, double Weight); +public record struct Key(int Source, int Target); +public record struct GraphEdge(int Source, int Target, double Weight); diff --git a/playground/DashboardToy/DashboardToy.Frontend/Program.cs b/playground/DashboardToy/DashboardToy.Frontend/Program.cs new file mode 100644 index 0000000000..c40c7c38be --- /dev/null +++ b/playground/DashboardToy/DashboardToy.Frontend/Program.cs @@ -0,0 +1,138 @@ +using DashboardToy.Frontend.Data; +using Microsoft.AspNetCore.Mvc; +using Orleans.Configuration; +using Orleans.Placement.Repartitioning; + +var builder = WebApplication.CreateBuilder(args); +builder.AddKeyedRedisClient("orleans-redis"); +#pragma warning disable ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +builder.UseOrleans(orleans => +{ + orleans.AddActivationRepartitioner(); + orleans.Configure(o => + { + o.MinRoundPeriod = TimeSpan.FromSeconds(5); + o.MaxRoundPeriod = TimeSpan.FromSeconds(15); + o.RecoveryPeriod = TimeSpan.FromSeconds(2); + }); +}); +#pragma warning restore ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +// Add services to the container. +builder.Services.AddSingleton(); + +var app = builder.Build(); + +var clusterDiagnosticsService = app.Services.GetRequiredService(); +app.MapGet("/data.json", ([FromServices] ClusterDiagnosticsService clusterDiagnosticsService) => clusterDiagnosticsService.GetGrainCallFrequencies()); +app.MapPost("/reset", async ([FromServices] IGrainFactory grainFactory) => +{ + await grainFactory.GetGrain("root").Reset(); +}); +app.MapPost("/add", async ([FromServices] IGrainFactory grainFactory) => +{ + await grainFactory.GetGrain("root").AddForest(); +}); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); + +app.UseDefaultFiles(); +app.UseStaticFiles(); + +app.UseRouting(); + +await app.StartAsync(); + +var loadGrain = app.Services.GetRequiredService().GetGrain("root"); +await loadGrain.AddForest(); +await loadGrain.AddForest(); +await loadGrain.AddForest(); + +await app.WaitForShutdownAsync(); + +public interface ILoaderGrain : IGrainWithStringKey +{ + ValueTask AddForest(); + ValueTask Reset(); + ValueTask GetResetCount(); +} + +public class LoaderGrain : Grain, ILoaderGrain +{ + private int _numForests = 0; + private int _resetCount; + + public async ValueTask AddForest() + { + var forest = _numForests++; + var loadGrain = GrainFactory.GetGrain(0, forest.ToString()); + await loadGrain.Ping(); + } + + public async ValueTask Reset() + { + ++_resetCount; + _numForests = 0; + await ServiceProvider.GetRequiredService().ResetAsync(); + await GrainFactory.GetGrain(0).ResetGrainCallFrequencies(); + } + + public ValueTask GetResetCount() => new(_resetCount); +} + +public interface IFanOutGrain : IGrainWithIntegerCompoundKey +{ + public ValueTask Ping(); +} + +public class FanOutGrain : Grain, IFanOutGrain +{ + public const int FanOutFactor = 4; + public const int MaxLevel = 2; + private readonly List _children; + + public FanOutGrain() + { + var id = this.GetPrimaryKeyLong(out var forest); + + var level = id == 0 ? 0 : (int)Math.Log(id, FanOutFactor); + var numChildren = level < MaxLevel ? FanOutFactor : 0; + _children = new List(numChildren); + var childBase = (id + 1) * FanOutFactor; + for (var i = 1; i <= numChildren; i++) + { + var child = GrainFactory.GetGrain(childBase + i, forest); + _children.Add(child); + } + + this.RegisterGrainTimer(() => Ping().AsTask(), TimeSpan.FromSeconds(0.5), TimeSpan.FromSeconds(0.5)); + } + + public async ValueTask Ping() + { + var tasks = new List(_children.Count); + foreach (var child in _children) + { + tasks.Add(child.Ping()); + } + + // Wait for the tasks to complete. + foreach (var task in tasks) + { + await task; + } + } +} + +internal sealed class HardLimitRule : IImbalanceToleranceRule +{ + public bool IsSatisfiedBy(uint imbalance) => imbalance <= 30; +} diff --git a/playground/DashboardToy/DashboardToy.Frontend/Properties/launchSettings.json b/playground/DashboardToy/DashboardToy.Frontend/Properties/launchSettings.json new file mode 100644 index 0000000000..c91c906ed2 --- /dev/null +++ b/playground/DashboardToy/DashboardToy.Frontend/Properties/launchSettings.json @@ -0,0 +1,37 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14770", + "sslPort": 44343 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5022", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7000;http://localhost:5022", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/playground/DashboardToy/DashboardToy.Frontend/appsettings.Development.json b/playground/DashboardToy/DashboardToy.Frontend/appsettings.Development.json new file mode 100644 index 0000000000..770d3e9314 --- /dev/null +++ b/playground/DashboardToy/DashboardToy.Frontend/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/playground/DashboardToy/DashboardToy.Frontend/appsettings.json b/playground/DashboardToy/DashboardToy.Frontend/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/playground/DashboardToy/DashboardToy.Frontend/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/playground/DashboardToy/DashboardToy.Frontend/wwwroot/css/bootstrap/bootstrap.min.css b/playground/DashboardToy/DashboardToy.Frontend/wwwroot/css/bootstrap/bootstrap.min.css new file mode 100644 index 0000000000..02ae65b5fe --- /dev/null +++ b/playground/DashboardToy/DashboardToy.Frontend/wwwroot/css/bootstrap/bootstrap.min.css @@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * Bootstrap v5.1.0 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-rgb:33,37,41;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-right:calc(var(--bs-gutter-x) * -.5);margin-left:calc(var(--bs-gutter-x) * -.5)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:last-child)>:last-child>*{border-bottom-color:currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--bs-aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--bs-aspect-ratio:calc(9 / 21 * 100%)}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/playground/DashboardToy/DashboardToy.Frontend/wwwroot/css/bootstrap/bootstrap.min.css.map b/playground/DashboardToy/DashboardToy.Frontend/wwwroot/css/bootstrap/bootstrap.min.css.map new file mode 100644 index 0000000000..afcd9e33e9 --- /dev/null +++ b/playground/DashboardToy/DashboardToy.Frontend/wwwroot/css/bootstrap/bootstrap.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Bootstrap v5.1.0 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\n// scss-docs-start import-stack\n// Configuration\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"utilities\";\n\n// Layout & components\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"containers\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"accordion\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"alert\";\n@import \"progress\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"offcanvas\";\n@import \"placeholders\";\n\n// Helpers\n@import \"helpers\";\n\n// Utilities\n@import \"utilities/api\";\n// scss-docs-end import-stack\n",":root {\n // Note: Custom variable values only support SassScript inside `#{}`.\n\n // Colors\n //\n // Generate palettes for full colors, grays, and theme colors.\n\n @each $color, $value in $colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $grays {\n --#{$variable-prefix}gray-#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors-rgb {\n --#{$variable-prefix}#{$color}-rgb: #{$value};\n }\n\n --#{$variable-prefix}white-rgb: #{to-rgb($white)};\n --#{$variable-prefix}black-rgb: #{to-rgb($black)};\n --#{$variable-prefix}body-rgb: #{to-rgb($body-color)};\n\n // Fonts\n\n // Note: Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$variable-prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$variable-prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$variable-prefix}gradient: #{$gradient};\n\n // Root and body\n // stylelint-disable custom-property-empty-line-before\n // scss-docs-start root-body-variables\n @if $font-size-root != null {\n --#{$variable-prefix}root-font-size: #{$font-size-root};\n }\n --#{$variable-prefix}body-font-family: #{$font-family-base};\n --#{$variable-prefix}body-font-size: #{$font-size-base};\n --#{$variable-prefix}body-font-weight: #{$font-weight-base};\n --#{$variable-prefix}body-line-height: #{$line-height-base};\n --#{$variable-prefix}body-color: #{$body-color};\n @if $body-text-align != null {\n --#{$variable-prefix}body-text-align: #{$body-text-align};\n }\n --#{$variable-prefix}body-bg: #{$body-bg};\n // scss-docs-end root-body-variables\n // stylelint-enable custom-property-empty-line-before\n}\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n @if $font-size-root != null {\n font-size: var(--#{$variable-prefix}-root-font-size);\n }\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\n// scss-docs-start reboot-body-rules\nbody {\n margin: 0; // 1\n font-family: var(--#{$variable-prefix}body-font-family);\n @include font-size(var(--#{$variable-prefix}body-font-size));\n font-weight: var(--#{$variable-prefix}body-font-weight);\n line-height: var(--#{$variable-prefix}body-line-height);\n color: var(--#{$variable-prefix}body-color);\n text-align: var(--#{$variable-prefix}body-text-align);\n background-color: var(--#{$variable-prefix}body-bg); // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n// scss-docs-end reboot-body-rules\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n// 2. Set correct height and prevent the `size` attribute to make the `hr` look like an input field\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n background-color: currentColor;\n border: 0;\n opacity: $hr-opacity;\n}\n\nhr:not([size]) {\n height: $hr-height; // 2\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-bs-original-title] { // 1\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n text-decoration-skip-ink: none; // 4\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n\n &:hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n direction: ltr #{\"/* rtl:ignore */\"};\n unicode-bidi: bidi-override;\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-` + + + + diff --git a/src/Orleans.Core.Abstractions/IDs/GrainId.cs b/src/Orleans.Core.Abstractions/IDs/GrainId.cs index b7bb724693..1958f457f5 100644 --- a/src/Orleans.Core.Abstractions/IDs/GrainId.cs +++ b/src/Orleans.Core.Abstractions/IDs/GrainId.cs @@ -138,7 +138,7 @@ public static bool TryParse(string? value, IFormatProvider? provider, out GrainI public override bool Equals(object? obj) => obj is GrainId id && Equals(id); /// - public bool Equals(GrainId other) => _type.Equals(other._type) && _key.Equals(other._key); + public bool Equals(GrainId other) => _key.Equals(other._key) && _type.Equals(other._type); /// public override int GetHashCode() => HashCode.Combine(_type, _key); diff --git a/src/Orleans.Core.Abstractions/Manifest/GrainProperties.cs b/src/Orleans.Core.Abstractions/Manifest/GrainProperties.cs index 81d575df4b..41181c248d 100644 --- a/src/Orleans.Core.Abstractions/Manifest/GrainProperties.cs +++ b/src/Orleans.Core.Abstractions/Manifest/GrainProperties.cs @@ -162,6 +162,11 @@ public static class WellKnownGrainTypeProperties /// Specifies the name of a method used to determine if a request can interleave other requests. /// public const string MayInterleavePredicate = "may-interleave-predicate"; + + ///

+ /// Whether a grain can be migrated by active-rebalancing or not. + /// + public const string Immovable = "immovable"; } /// diff --git a/src/Orleans.Core.Abstractions/Placement/PlacementAttribute.cs b/src/Orleans.Core.Abstractions/Placement/PlacementAttribute.cs index ae10b343d0..ccb668b3d6 100644 --- a/src/Orleans.Core.Abstractions/Placement/PlacementAttribute.cs +++ b/src/Orleans.Core.Abstractions/Placement/PlacementAttribute.cs @@ -111,4 +111,16 @@ public ResourceOptimizedPlacementAttribute() : base(ResourceOptimizedPlacement.Singleton) { } } + + /// + /// Ensures that when active-rebalancing is enabled, activations of this grain type will not be migrated automatically. + /// + /// Activations can still be migrated by user initiated code. + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public sealed class ImmovableAttribute : Attribute, IGrainPropertiesProviderAttribute + { + /// + public void Populate(IServiceProvider services, Type grainClass, GrainType grainType, Dictionary properties) + => properties[WellKnownGrainTypeProperties.Immovable] = "true"; + } } diff --git a/src/Orleans.Core/Core/DefaultClientServices.cs b/src/Orleans.Core/Core/DefaultClientServices.cs index a418ffbacf..9f077f9525 100644 --- a/src/Orleans.Core/Core/DefaultClientServices.cs +++ b/src/Orleans.Core/Core/DefaultClientServices.cs @@ -25,6 +25,7 @@ using Orleans.Hosting; using System.Reflection; using Microsoft.Extensions.Configuration; +using Orleans.Placement.Repartitioning; namespace Orleans { @@ -114,6 +115,7 @@ public static void AddDefaultServices(IClientBuilder builder) services.AddSingleton(); // Networking + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/src/Orleans.Core/Diagnostics/Metrics/CatalogInstruments.cs b/src/Orleans.Core/Diagnostics/Metrics/CatalogInstruments.cs index 30a5d292df..8186d924b6 100644 --- a/src/Orleans.Core/Diagnostics/Metrics/CatalogInstruments.cs +++ b/src/Orleans.Core/Diagnostics/Metrics/CatalogInstruments.cs @@ -12,10 +12,10 @@ internal static class CatalogInstruments internal static Counter ActivationShutdown = Instruments.Meter.CreateCounter(InstrumentNames.CATALOG_ACTIVATION_SHUTDOWN); - internal static void ActiviationShutdownViaCollection() => ActivationShutdown.Add(1, new KeyValuePair("via", "collection")); - internal static void ActiviationShutdownViaDeactivateOnIdle() => ActivationShutdown.Add(1, new KeyValuePair("via", "deactivateOnIdle")); - internal static void ActiviationShutdownViaMigration() => ActivationShutdown.Add(1, new KeyValuePair("via", "migration")); - internal static void ActiviationShutdownViaDeactivateStuckActivation() => ActivationShutdown.Add(1, new KeyValuePair("via", "deactivateStuckActivation")); + internal static void ActivationShutdownViaCollection() => ActivationShutdown.Add(1, new KeyValuePair("via", "collection")); + internal static void ActivationShutdownViaDeactivateOnIdle() => ActivationShutdown.Add(1, new KeyValuePair("via", "deactivateOnIdle")); + internal static void ActivationShutdownViaMigration() => ActivationShutdown.Add(1, new KeyValuePair("via", "migration")); + internal static void ActivationShutdownViaDeactivateStuckActivation() => ActivationShutdown.Add(1, new KeyValuePair("via", "deactivateStuckActivation")); internal static Counter NonExistentActivations = Instruments.Meter.CreateCounter(InstrumentNames.CATALOG_ACTIVATION_NON_EXISTENT_ACTIVATIONS); diff --git a/src/Orleans.Core/Messaging/Message.cs b/src/Orleans.Core/Messaging/Message.cs index ce6d13400d..88b45548b0 100644 --- a/src/Orleans.Core/Messaging/Message.cs +++ b/src/Orleans.Core/Messaging/Message.cs @@ -74,7 +74,8 @@ public Directions Direction public bool HasDirection => _headers.Direction != Directions.None; - public bool IsFullyAddressed => TargetSilo is not null && !TargetGrain.IsDefault; + public bool IsSenderFullyAddressed => SendingSilo is not null && !SendingGrain.IsDefault; + public bool IsTargetFullyAddressed => TargetSilo is not null && !TargetGrain.IsDefault; public bool IsExpired => _timeToExpiry is { IsDefault: false, ElapsedMilliseconds: > 0 }; diff --git a/src/Orleans.Core/Networking/Connection.cs b/src/Orleans.Core/Networking/Connection.cs index f4d9f24179..ae37c06933 100644 --- a/src/Orleans.Core/Networking/Connection.cs +++ b/src/Orleans.Core/Networking/Connection.cs @@ -274,9 +274,7 @@ public virtual void Send(Message message) protected abstract void RecordMessageReceive(Message msg, int numTotalBytes, int headerBytes); protected abstract void RecordMessageSend(Message msg, int numTotalBytes, int headerBytes); - protected abstract void OnReceivedMessage(Message message); - protected abstract void OnSendMessageFailure(Message message, string error); private async Task ProcessIncoming() @@ -355,6 +353,7 @@ private async Task ProcessOutgoing() Exception error = default; var serializer = this.shared.ServiceProvider.GetRequiredService(); + var messageStatisticsSink = this.shared.MessageStatisticsSink; try { var output = this._transport.Output; @@ -376,6 +375,7 @@ private async Task ProcessOutgoing() inflight.Add(message); var (headerLength, bodyLength) = serializer.Write(output, message); RecordMessageSend(message, headerLength + bodyLength, headerLength); + messageStatisticsSink.RecordMessage(message); message = null; } } diff --git a/src/Orleans.Core/Networking/ConnectionShared.cs b/src/Orleans.Core/Networking/ConnectionShared.cs index a6b21f76cc..4d7c1e6424 100644 --- a/src/Orleans.Core/Networking/ConnectionShared.cs +++ b/src/Orleans.Core/Networking/ConnectionShared.cs @@ -1,24 +1,19 @@ -using System; +using System; +using Orleans.Placement.Repartitioning; namespace Orleans.Runtime.Messaging { - internal sealed class ConnectionCommon + internal sealed class ConnectionCommon( + IServiceProvider serviceProvider, + MessageFactory messageFactory, + MessagingTrace messagingTrace, + NetworkingTrace networkingTrace, + IMessageStatisticsSink messageStatisticsSink) { - public ConnectionCommon( - IServiceProvider serviceProvider, - MessageFactory messageFactory, - MessagingTrace messagingTrace, - NetworkingTrace networkingTrace) - { - this.ServiceProvider = serviceProvider; - this.MessageFactory = messageFactory; - this.MessagingTrace = messagingTrace; - this.NetworkingTrace = networkingTrace; - } - - public MessageFactory MessageFactory { get; } - public IServiceProvider ServiceProvider { get; } - public NetworkingTrace NetworkingTrace { get; } - public MessagingTrace MessagingTrace { get; } + public MessageFactory MessageFactory { get; } = messageFactory; + public IServiceProvider ServiceProvider { get; } = serviceProvider; + public NetworkingTrace NetworkingTrace { get; } = networkingTrace; + public IMessageStatisticsSink MessageStatisticsSink { get; } = messageStatisticsSink; + public MessagingTrace MessagingTrace { get; } = messagingTrace; } } diff --git a/src/Orleans.Core/Placement/Repartitioning/IActivationRepartitionerSystemTarget.cs b/src/Orleans.Core/Placement/Repartitioning/IActivationRepartitionerSystemTarget.cs new file mode 100644 index 0000000000..eb696d8cee --- /dev/null +++ b/src/Orleans.Core/Placement/Repartitioning/IActivationRepartitionerSystemTarget.cs @@ -0,0 +1,210 @@ +using System.Threading.Tasks; +using System.Collections.Immutable; +using Orleans.Runtime; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System; + +namespace Orleans.Placement.Repartitioning; + +[Alias("IActivationRepartitionerSystemTarget")] +internal interface IActivationRepartitionerSystemTarget : ISystemTarget +{ + static IActivationRepartitionerSystemTarget GetReference(IGrainFactory grainFactory, SiloAddress targetSilo) + => grainFactory.GetGrain(SystemTargetGrainId.Create(Constants.ActivationRepartitionerType, targetSilo).GrainId); + + [ResponseTimeout("00:10:00")] + ValueTask TriggerExchangeRequest(); + + [ResponseTimeout("00:10:00")] + ValueTask AcceptExchangeRequest(AcceptExchangeRequest request); + + /// + /// For use in testing only! + /// + ValueTask ResetCounters(); + + /// + /// For use in testing only! + /// + ValueTask GetActivationCount(); + + /// + /// For use in testing only! + /// + ValueTask SetActivationCountOffset(int activationCountOffset); + + /// + /// For diagnostics only. + /// + ValueTask> GetGrainCallFrequencies(); +} + +// We use a readonly struct so that we can fully decouple the message-passing and potentially modifications to the Silo fields. +/// +/// Data structure representing a 'communication edge' between a source and target. +/// +[GenerateSerializer, Immutable, DebuggerDisplay("Source: [{Source.Id} - {Source.Silo}] | Target: [{Target.Id} - {Target.Silo}]")] +internal readonly struct Edge(EdgeVertex source, EdgeVertex target) : IEquatable +{ + [Id(0)] + public EdgeVertex Source { get; } = source; + + [Id(1)] + public EdgeVertex Target { get; } = target; + + public static bool operator ==(Edge left, Edge right) => left.Equals(right); + public static bool operator !=(Edge left, Edge right) => !left.Equals(right); + + public override bool Equals([NotNullWhen(true)] object obj) => obj is Edge other && Equals(other); + public bool Equals(Edge other) => Source == other.Source && Target == other.Target; + + public override int GetHashCode() => HashCode.Combine(Source, Target); + + /// + /// Returns a copy of this but with flipped sources and targets. + /// + public Edge Flip() => new(source: Target, target: Source); + + public override string ToString() => $"[{Source} -> {Target}]"; +} + +/// +/// Data structure representing one side of a . +/// +[GenerateSerializer, Immutable] +public readonly struct EdgeVertex( + GrainId id, + SiloAddress silo, + bool isMigratable) : IEquatable +{ + [Id(0)] + public readonly GrainId Id = id; + + [Id(1)] + public readonly SiloAddress Silo = silo; + + [Id(2)] + public readonly bool IsMigratable = isMigratable; + + public static bool operator ==(EdgeVertex left, EdgeVertex right) => left.Equals(right); + public static bool operator !=(EdgeVertex left, EdgeVertex right) => !left.Equals(right); + + public override bool Equals([NotNullWhen(true)] object obj) => obj is EdgeVertex other && Equals(other); + public bool Equals(EdgeVertex other) => Id == other.Id && Silo == other.Silo && IsMigratable == other.IsMigratable; + + public override int GetHashCode() => HashCode.Combine(Id, Silo, IsMigratable); + + public override string ToString() => $"[{Id}@{Silo}{(IsMigratable ? "" : "/NotMigratable")}]"; +} + +/// +/// A candidate vertex to be transferred to another silo. +/// +[GenerateSerializer, DebuggerDisplay("Id = {Id} | Accumulated = {AccumulatedTransferScore}")] +internal sealed class CandidateVertex +{ + /// + /// The id of the candidate grain. + /// + [Id(0), Immutable] + public GrainId Id { get; init; } + + /// + /// The cost reduction expected from migrating the vertex with to another silo. + /// + [Id(1)] + public long AccumulatedTransferScore { get; set; } + + /// + /// These are all the vertices connected to the vertex with . + /// + /// These will be important when this vertex is removed from the max-sorted heap on the receiver silo. + [Id(2), Immutable] + public ImmutableArray ConnectedVertices { get; init; } = []; + + public override string ToString() => $"[{Id} * {AccumulatedTransferScore} -> [{string.Join(", ", ConnectedVertices)}]]"; +} + +[GenerateSerializer, Immutable] +public readonly struct CandidateConnectedVertex(GrainId id, long transferScore) +{ + [Id(0)] + public GrainId Id { get; } = id; + + [Id(1)] + public long TransferScore { get; } = transferScore; + + public static bool operator ==(CandidateConnectedVertex left, CandidateConnectedVertex right) => left.Equals(right); + public static bool operator !=(CandidateConnectedVertex left, CandidateConnectedVertex right) => !left.Equals(right); + + public override bool Equals([NotNullWhen(true)] object obj) => obj is CandidateConnectedVertex other && Equals(other); + public bool Equals(CandidateConnectedVertex other) => Id == other.Id && TransferScore == other.TransferScore; + + public override int GetHashCode() => HashCode.Combine(Id, TransferScore); + + public override string ToString() => $"[{Id} * {TransferScore}]"; +} + +[GenerateSerializer, Immutable] +internal sealed class AcceptExchangeRequest(SiloAddress sendingSilo, ImmutableArray exchangeSet, int activationCountSnapshot) +{ + /// + /// The silo which is offering to transfer grains to us. + /// + [Id(0)] + public SiloAddress SendingSilo { get; } = sendingSilo; + + /// + /// The set of grains which the sending silo is offering to transfer to us. + /// + [Id(1)] + public ImmutableArray ExchangeSet { get; } = exchangeSet; + + /// + /// The activation count of the sending silo at the time of the exchange request. + /// + [Id(2)] + public int ActivationCountSnapshot { get; } = activationCountSnapshot; +} + +[GenerateSerializer, Immutable] +internal sealed class AcceptExchangeResponse(AcceptExchangeResponse.ResponseType type, ImmutableArray acceptedGrains, ImmutableArray givenGrains) +{ + public static readonly AcceptExchangeResponse CachedExchangedRecently = new(ResponseType.ExchangedRecently, [], []); + public static readonly AcceptExchangeResponse CachedMutualExchangeAttempt = new(ResponseType.MutualExchangeAttempt, [], []); + + [Id(0)] + public ResponseType Type { get; } = type; + + /// + /// The grains which the sender is asking the receiver to transfer. + /// + [Id(1)] + public ImmutableArray AcceptedGrainIds { get; } = acceptedGrains; + + /// + /// The grains which the receiver is transferring to the sender. + /// + [Id(2)] + public ImmutableArray GivenGrainIds { get; } = givenGrains; + + [GenerateSerializer] + public enum ResponseType + { + /// + /// The exchange was accepted and an exchange set is returned. + /// + Success, + + /// + /// The other silo has been recently involved in another exchange. + /// + ExchangedRecently, + + /// + /// An attempt to do an exchange between this and the other silo was about to happen at the same time. + /// + MutualExchangeAttempt + } +} diff --git a/src/Orleans.Core/Placement/Repartitioning/IImbalanceToleranceRule.cs b/src/Orleans.Core/Placement/Repartitioning/IImbalanceToleranceRule.cs new file mode 100644 index 0000000000..d0aadd5f9d --- /dev/null +++ b/src/Orleans.Core/Placement/Repartitioning/IImbalanceToleranceRule.cs @@ -0,0 +1,13 @@ +namespace Orleans.Placement.Repartitioning; + +/// +/// Represents a rule that controls the degree of imbalance between the number of grain activations (that is considered tolerable), when any pair of silos are exchanging activations. +/// +public interface IImbalanceToleranceRule +{ + /// + /// Checks if this rule is satisfied by . + /// + /// The imbalance between the exchanging silo pair that will be, if this method were to return + bool IsSatisfiedBy(uint imbalance); +} \ No newline at end of file diff --git a/src/Orleans.Core/Placement/Repartitioning/IMessageStatisticsSink.cs b/src/Orleans.Core/Placement/Repartitioning/IMessageStatisticsSink.cs new file mode 100644 index 0000000000..1973ea9fdf --- /dev/null +++ b/src/Orleans.Core/Placement/Repartitioning/IMessageStatisticsSink.cs @@ -0,0 +1,13 @@ +using Orleans.Runtime; + +namespace Orleans.Placement.Repartitioning; + +internal interface IMessageStatisticsSink +{ + void RecordMessage(Message message); +} + +internal sealed class NoOpMessageStatisticsSink : IMessageStatisticsSink +{ + public void RecordMessage(Message message) { } +} \ No newline at end of file diff --git a/src/Orleans.Core/Runtime/Constants.cs b/src/Orleans.Core/Runtime/Constants.cs index fbba2f9bc6..39aaa6ac12 100644 --- a/src/Orleans.Core/Runtime/Constants.cs +++ b/src/Orleans.Core/Runtime/Constants.cs @@ -25,6 +25,7 @@ internal static class Constants public static readonly GrainType StreamPullingAgentType = SystemTargetGrainId.CreateGrainType("stream.agent"); public static readonly GrainType ManifestProviderType = SystemTargetGrainId.CreateGrainType("manifest"); public static readonly GrainType ActivationMigratorType = SystemTargetGrainId.CreateGrainType("migrator"); + public static readonly GrainType ActivationRepartitionerType = SystemTargetGrainId.CreateGrainType("repartitioner"); public static readonly GrainId SiloDirectConnectionId = GrainId.Create( GrainType.Create(GrainTypePrefix.SystemPrefix + "silo"), @@ -50,6 +51,8 @@ internal static class Constants {StreamPullingAgentManagerType, "PullingAgentsManagerSystemTarget"}, {StreamPullingAgentType, "PullingAgentSystemTarget"}, {ManifestProviderType, "ManifestProvider"}, + {ActivationMigratorType, "ActivationMigrator"}, + {ActivationRepartitionerType, "ActivationRepartitioner"}, }.ToFrozenDictionary(); public static string SystemTargetName(GrainType id) => SingletonSystemTargetNames.TryGetValue(id, out var name) ? name : id.ToString(); diff --git a/src/Orleans.Core/SystemTargetInterfaces/IManagementGrain.cs b/src/Orleans.Core/SystemTargetInterfaces/IManagementGrain.cs index 2c77d8a851..42ef83ed92 100644 --- a/src/Orleans.Core/SystemTargetInterfaces/IManagementGrain.cs +++ b/src/Orleans.Core/SystemTargetInterfaces/IManagementGrain.cs @@ -124,5 +124,61 @@ public interface IManagementGrain : IGrainWithIntegerKey, IVersionManager /// The type. /// A list of all active grains of the specified type. ValueTask> GetActiveGrains(GrainType type); + + /// + /// Gets estimated grain call frequency statistics from the specified hosts. + /// + /// The hosts to request grain call frequency counts from. + /// A list of estimated grain call frequencies. + /// + /// Note that this resulting collection does not necessarily contain all grain calls. It contains an estimation of the calls with the highest frequency. + /// + Task> GetGrainCallFrequencies(SiloAddress[] hostsIds = null); + + /// + /// For testing only. Resets grain call frequency counts on the specified hosts. + /// + /// The hosts to invoke the operation on. + /// A task representing the work performed. + ValueTask ResetGrainCallFrequencies(SiloAddress[] hostsIds = null); + } + + /// + /// Represents an estimation of the frequency calls made from a source grain to a target grain. + /// + [GenerateSerializer] + [Alias("Orleans.Runtime.GrainCallFrequency")] + [Immutable] + public struct GrainCallFrequency + { + /// + /// The source grain. + /// + [Id(0)] + public GrainId SourceGrain { get; set; } + + /// + /// The target grain. + /// + [Id(1)] + public GrainId TargetGrain { get; set; } + + /// + /// The source host. + /// + [Id(2)] + public SiloAddress SourceHost { get; set; } + + /// + /// The target host. + /// + [Id(3)] + public SiloAddress TargetHost { get; set; } + + /// + /// The estimated number of calls made. + /// + [Id(4)] + public ulong CallCount { get; set; } } } diff --git a/src/Orleans.Runtime/Activation/IGrainContextActivator.cs b/src/Orleans.Runtime/Activation/IGrainContextActivator.cs index e96b61a7c6..da33143fd8 100644 --- a/src/Orleans.Runtime/Activation/IGrainContextActivator.cs +++ b/src/Orleans.Runtime/Activation/IGrainContextActivator.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Orleans.Concurrency; @@ -168,63 +169,23 @@ public class GrainTypeSharedContextResolver private readonly ConcurrentDictionary _components = new(); private readonly IConfigureGrainTypeComponents[] _configurators; private readonly GrainPropertiesResolver _grainPropertiesResolver; - private readonly GrainReferenceActivator _grainReferenceActivator; private readonly Func _createFunc; - private readonly IClusterManifestProvider _clusterManifestProvider; - private readonly GrainClassMap _grainClassMap; - private readonly IOptions _messagingOptions; - private readonly IOptions _collectionOptions; - private readonly IOptions _schedulingOptions; - private readonly PlacementStrategyResolver _placementStrategyResolver; - private readonly IGrainRuntime _grainRuntime; - private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; - private readonly SerializerSessionPool _serializerSessionPool; /// /// Initializes a new instance of the class. /// /// The grain type component configuration providers. /// The grain properties resolver. - /// The grain reference activator. - /// The cluster manifest provider. - /// The grain class map. - /// The grain placement strategy resolver. - /// The messaging options. - /// The grain activation collection options - /// The scheduling options - /// The grain runtime. - /// The logger. /// The service provider. - /// The serializer session pool. public GrainTypeSharedContextResolver( IEnumerable configurators, GrainPropertiesResolver grainPropertiesResolver, - GrainReferenceActivator grainReferenceActivator, - IClusterManifestProvider clusterManifestProvider, - GrainClassMap grainClassMap, - PlacementStrategyResolver placementStrategyResolver, - IOptions messagingOptions, - IOptions collectionOptions, - IOptions schedulingOptions, - IGrainRuntime grainRuntime, - ILogger logger, - IServiceProvider serviceProvider, - SerializerSessionPool serializerSessionPool) + IServiceProvider serviceProvider) { _configurators = configurators.ToArray(); _grainPropertiesResolver = grainPropertiesResolver; - _grainReferenceActivator = grainReferenceActivator; - _clusterManifestProvider = clusterManifestProvider; - _grainClassMap = grainClassMap; - _placementStrategyResolver = placementStrategyResolver; - _messagingOptions = messagingOptions; - _collectionOptions = collectionOptions; - _schedulingOptions = schedulingOptions; - _grainRuntime = grainRuntime; - _logger = logger; _serviceProvider = serviceProvider; - _serializerSessionPool = serializerSessionPool; _createFunc = Create; } @@ -237,19 +198,7 @@ public GrainTypeSharedContextResolver( private GrainTypeSharedContext Create(GrainType grainType) { - var result = new GrainTypeSharedContext( - grainType, - _clusterManifestProvider, - _grainClassMap, - _placementStrategyResolver, - _messagingOptions, - _collectionOptions, - _schedulingOptions, - _grainRuntime, - _logger, - _grainReferenceActivator, - _serviceProvider, - _serializerSessionPool); + var result = ActivatorUtilities.CreateInstance(_serviceProvider, grainType); var properties = _grainPropertiesResolver.GetGrainProperties(grainType); foreach (var configurator in _configurators) { diff --git a/src/Orleans.Runtime/Catalog/ActivationCollector.cs b/src/Orleans.Runtime/Catalog/ActivationCollector.cs index ea8f7e8c55..1fa6a1933e 100644 --- a/src/Orleans.Runtime/Catalog/ActivationCollector.cs +++ b/src/Orleans.Runtime/Catalog/ActivationCollector.cs @@ -17,7 +17,6 @@ namespace Orleans.Runtime /// internal class ActivationCollector : IActivationWorkingSetObserver, ILifecycleParticipant { - internal Action Debug_OnDecideToCollectActivation; private readonly TimeSpan quantum; private readonly TimeSpan shortestAgeLimit; private readonly ConcurrentDictionary buckets = new(); @@ -322,10 +321,8 @@ private static DeactivationReason GetDeactivationReason() private void AddActivationToList(ICollectibleGrainContext activation, ref List condemned) { - condemned ??= new(); + condemned ??= []; condemned.Add(activation); - - this.Debug_OnDecideToCollectActivation?.Invoke(activation.GrainId); } private void ThrowIfTicketIsInvalid(DateTime ticket) @@ -512,7 +509,7 @@ private async Task DeactivateActivationsFromCollector(List mtcs.SetOneResult(); var reason = GetDeactivationReason(); diff --git a/src/Orleans.Runtime/Catalog/ActivationData.cs b/src/Orleans.Runtime/Catalog/ActivationData.cs index 50c636ee4e..573253915c 100644 --- a/src/Orleans.Runtime/Catalog/ActivationData.cs +++ b/src/Orleans.Runtime/Catalog/ActivationData.cs @@ -1738,19 +1738,19 @@ private async Task FinishDeactivating(CancellationToken cancellationToken) if (IsStuckDeactivating) { - CatalogInstruments.ActiviationShutdownViaDeactivateStuckActivation(); + CatalogInstruments.ActivationShutdownViaDeactivateStuckActivation(); } else if (migrated) { - CatalogInstruments.ActiviationShutdownViaMigration(); + CatalogInstruments.ActivationShutdownViaMigration(); } else if (_isInWorkingSet) { - CatalogInstruments.ActiviationShutdownViaDeactivateOnIdle(); + CatalogInstruments.ActivationShutdownViaDeactivateOnIdle(); } else { - CatalogInstruments.ActiviationShutdownViaCollection(); + CatalogInstruments.ActivationShutdownViaCollection(); } _shared.InternalRuntime.ActivationWorkingSet.OnDeactivated(this); @@ -1761,7 +1761,7 @@ private async Task FinishDeactivating(CancellationToken cancellationToken) } catch (Exception exception) { - _shared.Logger.LogWarning(exception, "Exception disposing activation {Activation}", (ActivationData)this); + _shared.Logger.LogWarning(exception, "Exception disposing activation {Activation}", this); } UnregisterMessageTarget(); diff --git a/src/Orleans.Runtime/Catalog/ActivationDirectory.cs b/src/Orleans.Runtime/Catalog/ActivationDirectory.cs index 7e43f51453..0d3a9d7fd6 100644 --- a/src/Orleans.Runtime/Catalog/ActivationDirectory.cs +++ b/src/Orleans.Runtime/Catalog/ActivationDirectory.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections; using System.Collections.Concurrent; @@ -20,7 +21,7 @@ public ActivationDirectory() public int Count => _activationsCount; - public IGrainContext FindTarget(GrainId key) + public IGrainContext? FindTarget(GrainId key) { _activations.TryGetValue(key, out var result); return result; diff --git a/src/Orleans.Runtime/Catalog/Catalog.cs b/src/Orleans.Runtime/Catalog/Catalog.cs index d870a46dbc..6558349335 100644 --- a/src/Orleans.Runtime/Catalog/Catalog.cs +++ b/src/Orleans.Runtime/Catalog/Catalog.cs @@ -64,16 +64,14 @@ public Catalog( MessagingProcessingInstruments.RegisterActivationDataAllObserve(() => { long counter = 0; - lock (activations) + foreach (var activation in activations) { - foreach (var activation in activations) + if (activation.Value is ActivationData data) { - if (activation.Value is ActivationData data) - { - counter += data.GetRequestCount(); - } + counter += data.GetRequestCount(); } } + return counter; }); grainDirectory.SetSiloRemovedCatalogCallback(this.OnSiloStatusChange); diff --git a/src/Orleans.Runtime/Catalog/GrainTypeSharedContext.cs b/src/Orleans.Runtime/Catalog/GrainTypeSharedContext.cs index d7115c7787..1cc7afc325 100644 --- a/src/Orleans.Runtime/Catalog/GrainTypeSharedContext.cs +++ b/src/Orleans.Runtime/Catalog/GrainTypeSharedContext.cs @@ -12,198 +12,197 @@ using Orleans.Serialization.Session; using Orleans.Serialization.TypeSystem; -namespace Orleans.Runtime +namespace Orleans.Runtime; + +/// +/// Functionality which is shared between all instances of a grain type. +/// +public class GrainTypeSharedContext { - /// - /// Functionality which is shared between all instances of a grain type. - /// - public class GrainTypeSharedContext + private readonly IServiceProvider _serviceProvider; + private readonly Dictionary _components = new(); + private InternalGrainRuntime? _internalGrainRuntime; + + public GrainTypeSharedContext( + GrainType grainType, + IClusterManifestProvider clusterManifestProvider, + GrainClassMap grainClassMap, + PlacementStrategyResolver placementStrategyResolver, + IOptions messagingOptions, + IOptions collectionOptions, + IOptions schedulingOptions, + IGrainRuntime grainRuntime, + ILoggerFactory loggerFactory, + GrainReferenceActivator grainReferenceActivator, + IServiceProvider serviceProvider, + SerializerSessionPool serializerSessionPool) { - private readonly IServiceProvider _serviceProvider; - private readonly Dictionary _components = new(); - private InternalGrainRuntime? _internalGrainRuntime; - - public GrainTypeSharedContext( - GrainType grainType, - IClusterManifestProvider clusterManifestProvider, - GrainClassMap grainClassMap, - PlacementStrategyResolver placementStrategyResolver, - IOptions messagingOptions, - IOptions collectionOptions, - IOptions schedulingOptions, - IGrainRuntime grainRuntime, - ILogger logger, - GrainReferenceActivator grainReferenceActivator, - IServiceProvider serviceProvider, - SerializerSessionPool serializerSessionPool) + if (!grainClassMap.TryGetGrainClass(grainType, out var grainClass)) { - if (!grainClassMap.TryGetGrainClass(grainType, out var grainClass)) - { - throw new KeyNotFoundException($"Could not find corresponding grain class for grain of type {grainType.ToString()}"); - } - - SerializerSessionPool = serializerSessionPool; - GrainTypeName = RuntimeTypeNameFormatter.Format(grainClass); - Logger = logger; - MessagingOptions = messagingOptions.Value; - GrainReferenceActivator = grainReferenceActivator; - _serviceProvider = serviceProvider; - MaxWarningRequestProcessingTime = messagingOptions.Value.ResponseTimeout.Multiply(5); - MaxRequestProcessingTime = messagingOptions.Value.MaxRequestProcessingTime; - PlacementStrategy = placementStrategyResolver.GetPlacementStrategy(grainType); - SchedulingOptions = schedulingOptions.Value; - Runtime = grainRuntime; - MigrationManager = _serviceProvider.GetService(); - - CollectionAgeLimit = GetCollectionAgeLimit( - grainType, - grainClass, - clusterManifestProvider.LocalGrainManifest, - collectionOptions.Value); + throw new KeyNotFoundException($"Could not find corresponding grain class for grain of type {grainType}"); } - /// - /// Gets the grain instance type name, if available. - /// - public string? GrainTypeName { get; } + SerializerSessionPool = serializerSessionPool; + GrainTypeName = RuntimeTypeNameFormatter.Format(grainClass); + Logger = loggerFactory.CreateLogger("Orleans.Grain"); + MessagingOptions = messagingOptions.Value; + GrainReferenceActivator = grainReferenceActivator; + _serviceProvider = serviceProvider; + MaxWarningRequestProcessingTime = messagingOptions.Value.ResponseTimeout.Multiply(5); + MaxRequestProcessingTime = messagingOptions.Value.MaxRequestProcessingTime; + PlacementStrategy = placementStrategyResolver.GetPlacementStrategy(grainType); + SchedulingOptions = schedulingOptions.Value; + Runtime = grainRuntime; + MigrationManager = _serviceProvider.GetService(); + + CollectionAgeLimit = GetCollectionAgeLimit( + grainType, + grainClass, + clusterManifestProvider.LocalGrainManifest, + collectionOptions.Value); + } + + /// + /// Gets the grain instance type name, if available. + /// + public string? GrainTypeName { get; } - private TimeSpan GetCollectionAgeLimit(GrainType grainType, Type grainClass, GrainManifest siloManifest, GrainCollectionOptions collectionOptions) + private static TimeSpan GetCollectionAgeLimit(GrainType grainType, Type grainClass, GrainManifest siloManifest, GrainCollectionOptions collectionOptions) + { + if (siloManifest.Grains.TryGetValue(grainType, out var properties) + && properties.Properties.TryGetValue(WellKnownGrainTypeProperties.IdleDeactivationPeriod, out var idleTimeoutString)) { - if (siloManifest.Grains.TryGetValue(grainType, out var properties) - && properties.Properties.TryGetValue(WellKnownGrainTypeProperties.IdleDeactivationPeriod, out var idleTimeoutString)) + if (string.Equals(idleTimeoutString, WellKnownGrainTypeProperties.IndefiniteIdleDeactivationPeriodValue)) { - if (string.Equals(idleTimeoutString, WellKnownGrainTypeProperties.IndefiniteIdleDeactivationPeriodValue)) - { - return Timeout.InfiniteTimeSpan; - } - - if (TimeSpan.TryParse(idleTimeoutString, out var result)) - { - return result; - } + return Timeout.InfiniteTimeSpan; } - if (collectionOptions.ClassSpecificCollectionAge.TryGetValue(grainClass.FullName!, out var specified)) + if (TimeSpan.TryParse(idleTimeoutString, out var result)) { - return specified; + return result; } - - return collectionOptions.CollectionAge; } - /// - /// Gets a component. - /// - /// The type specified in the corresponding call. - public TComponent? GetComponent() + if (collectionOptions.ClassSpecificCollectionAge.TryGetValue(grainClass.FullName!, out var specified)) { - if (typeof(TComponent) == typeof(PlacementStrategy) && PlacementStrategy is TComponent component) - { - return component; - } - - if (_components is null) return default; - _components.TryGetValue(typeof(TComponent), out var resultObj); - return (TComponent?)resultObj; + return specified; } - /// - /// Registers a component. - /// - /// The type which can be used as a key to . - public void SetComponent(TComponent? instance) - { - if (instance == null) - { - _components.Remove(typeof(TComponent)); - return; - } - - _components[typeof(TComponent)] = instance; - } + return collectionOptions.CollectionAge; + } - /// - /// Gets the duration after which idle grains are eligible for collection. - /// - public TimeSpan CollectionAgeLimit { get; } - - /// - /// Gets the logger. - /// - public ILogger Logger { get; } - - /// - /// Gets the serializer session pool. - /// - public SerializerSessionPool SerializerSessionPool { get; } - - /// - /// Gets the silo messaging options. - /// - public SiloMessagingOptions MessagingOptions { get; } - - /// - /// Gets the grain reference activator. - /// - public GrainReferenceActivator GrainReferenceActivator { get; } - - /// - /// Gets the maximum amount of time we expect a request to continue processing before it is considered hung. - /// - public TimeSpan MaxRequestProcessingTime { get; } - - /// - /// Gets the maximum amount of time we expect a request to continue processing before a warning may be logged. - /// - public TimeSpan MaxWarningRequestProcessingTime { get; } - - /// - /// Gets the placement strategy used by grains of this type. - /// - public PlacementStrategy PlacementStrategy { get; } - - /// - /// Gets the scheduling options. - /// - public SchedulingOptions SchedulingOptions { get; } - - /// - /// Gets the grain runtime. - /// - public IGrainRuntime Runtime { get; } - - /// - /// Gets the local activation migration manager. - /// - internal IActivationMigrationManager? MigrationManager { get; } - - /// - /// Gets the internal grain runtime. - /// - internal InternalGrainRuntime InternalRuntime => _internalGrainRuntime ??= _serviceProvider.GetRequiredService(); - - /// - /// Called on creation of an activation. - /// - /// The grain activation. - public void OnCreateActivation(IGrainContext grainContext) + /// + /// Gets a component. + /// + /// The type specified in the corresponding call. + public TComponent? GetComponent() + { + if (typeof(TComponent) == typeof(PlacementStrategy) && PlacementStrategy is TComponent component) { - GrainInstruments.IncrementGrainCounts(GrainTypeName); + return component; } - /// - /// Called when an activation is disposed. - /// - /// The grain activation. - public void OnDestroyActivation(IGrainContext grainContext) + if (_components is null) return default; + _components.TryGetValue(typeof(TComponent), out var resultObj); + return (TComponent?)resultObj; + } + + /// + /// Registers a component. + /// + /// The type which can be used as a key to . + public void SetComponent(TComponent? instance) + { + if (instance == null) { - GrainInstruments.DecrementGrainCounts(GrainTypeName); + _components.Remove(typeof(TComponent)); + return; } + + _components[typeof(TComponent)] = instance; + } + + /// + /// Gets the duration after which idle grains are eligible for collection. + /// + public TimeSpan CollectionAgeLimit { get; } + + /// + /// Gets the logger. + /// + public ILogger Logger { get; } + + /// + /// Gets the serializer session pool. + /// + public SerializerSessionPool SerializerSessionPool { get; } + + /// + /// Gets the silo messaging options. + /// + public SiloMessagingOptions MessagingOptions { get; } + + /// + /// Gets the grain reference activator. + /// + public GrainReferenceActivator GrainReferenceActivator { get; } + + /// + /// Gets the maximum amount of time we expect a request to continue processing before it is considered hung. + /// + public TimeSpan MaxRequestProcessingTime { get; } + + /// + /// Gets the maximum amount of time we expect a request to continue processing before a warning may be logged. + /// + public TimeSpan MaxWarningRequestProcessingTime { get; } + + /// + /// Gets the placement strategy used by grains of this type. + /// + public PlacementStrategy PlacementStrategy { get; } + + /// + /// Gets the scheduling options. + /// + public SchedulingOptions SchedulingOptions { get; } + + /// + /// Gets the grain runtime. + /// + public IGrainRuntime Runtime { get; } + + /// + /// Gets the local activation migration manager. + /// + internal IActivationMigrationManager? MigrationManager { get; } + + /// + /// Gets the internal grain runtime. + /// + internal InternalGrainRuntime InternalRuntime => _internalGrainRuntime ??= _serviceProvider.GetRequiredService(); + + /// + /// Called on creation of an activation. + /// + /// The grain activation. + public void OnCreateActivation(IGrainContext grainContext) + { + GrainInstruments.IncrementGrainCounts(GrainTypeName); } - internal interface IActivationLifecycleObserver + /// + /// Called when an activation is disposed. + /// + /// The grain activation. + public void OnDestroyActivation(IGrainContext grainContext) { - void OnCreateActivation(IGrainContext grainContext); - void OnDestroyActivation(IGrainContext grainContext); + GrainInstruments.DecrementGrainCounts(GrainTypeName); } } + +internal interface IActivationLifecycleObserver +{ + void OnCreateActivation(IGrainContext grainContext); + void OnDestroyActivation(IGrainContext grainContext); +} diff --git a/src/Orleans.Runtime/Configuration/Options/ActivationRepartitionerOptions.cs b/src/Orleans.Runtime/Configuration/Options/ActivationRepartitionerOptions.cs new file mode 100644 index 0000000000..fae2a0b7d0 --- /dev/null +++ b/src/Orleans.Runtime/Configuration/Options/ActivationRepartitionerOptions.cs @@ -0,0 +1,155 @@ +using System; +using Microsoft.Extensions.Options; +using Orleans.Runtime; + +namespace Orleans.Configuration; + +public sealed class ActivationRepartitionerOptions +{ + /// + /// + /// The maximum number of edges to retain in-memory during a repartitioning round. An edge represents how many calls were made from one grain to another. + /// + /// + /// If this number is N, it does not mean that N activations will be migrated after a repartitioning round. + /// It also does not mean that if any activation ranked very high, that it will rank high at the next cycle. + /// At the most extreme case, the number of activations that will be migrated, will equal this number, so this should give you some idea as to setting a reasonable value for this. + /// + /// + /// + /// In order to preserve memory, the most heaviest links are recorded in a probabilistic way, so there is an inherent error associated with that. + /// That error is inversely proportional to this value, so values under 100 are not recommended. If you notice that the system is not converging fast enough, do consider increasing this number. + /// + public int MaxEdgeCount { get; set; } = DEFAULT_MAX_EDGE_COUNT; + + /// + /// The default value of . + /// + public const int DEFAULT_MAX_EDGE_COUNT = 10_000; + + /// + /// The minimum time between initiating a repartitioning round. + /// + /// The actual due time is picked randomly between this and . + public TimeSpan MinRoundPeriod { get; set; } = DEFAULT_MINUMUM_ROUND_PERIOD; + + /// + /// The default value of . + /// + public static readonly TimeSpan DEFAULT_MINUMUM_ROUND_PERIOD = TimeSpan.FromMinutes(1); + + /// + /// The maximum time between initiating a repartitioning round. + /// + /// + /// The actual due time is picked randomly between this and . + /// For optimal results, you should aim to give this an extra 10 seconds multiplied by the maximum anticipated silo count in the cluster. + /// + public TimeSpan MaxRoundPeriod { get; set; } = DEFAULT_MAXIMUM_ROUND_PERIOD; + + /// + /// The default value of . + /// + public static readonly TimeSpan DEFAULT_MAXIMUM_ROUND_PERIOD = TimeSpan.FromMinutes(2); + + /// + /// The minimum time needed for a silo to recover from a previous repartitioning round. + /// Until this time has elapsed, this silo will not take part in any repartitioning attempt from another silo. + /// + public TimeSpan RecoveryPeriod { get; set; } = DEFAULT_RECOVERY_PERIOD; + + /// + /// The default value of . + /// + public static readonly TimeSpan DEFAULT_RECOVERY_PERIOD = TimeSpan.FromMinutes(1); + + /// + /// The maximum number of unprocessed edges to buffer. If this number is exceeded, the oldest edges will be discarded. + /// + public int MaxUnprocessedEdges { get; set; } = DEFAULT_MAX_UNPROCESSED_EDGES; + + /// + /// The default value of . + /// + public const int DEFAULT_MAX_UNPROCESSED_EDGES = 100_000; + + /// + /// Gets or sets a value indicating whether to enable the local vertex filter. This filter tracks which + /// vertices are well-partitioned (moving them from the local host would be detrimental) and collapses them + /// into a single per-silo vertex to reduce the space required to track edges involving that vertex. The result + /// is a reduction in accuracy but a potentially significant increase in effectiveness of the repartitioner, since + /// well-partitioned edges will not dominate the top-K data structure, leaving sufficient room to track + /// non-well-partitioned vertices. This is enabled by default. + /// + public bool AnchoringFilterEnabled { get; set; } = DEFAULT_ANCHORING_FILTER_ENABLED; + + /// + /// The default value of . + /// + public const bool DEFAULT_ANCHORING_FILTER_ENABLED = true; + + /// + /// The maximum allowed error rate when is set to , otherwise this does not apply. + /// + /// Allowed range: [0.001 - 0.01](0.1% - 1%) + public double ProbabilisticFilteringMaxAllowedErrorRate { get; set; } = DEFAULT_PROBABILISTIC_FILTERING_MAX_ALLOWED_ERROR; + + /// + /// The default value of . + /// + public const double DEFAULT_PROBABILISTIC_FILTERING_MAX_ALLOWED_ERROR = 0.01d; +} + +internal sealed class ActivationRepartitionerOptionsValidator(IOptions options) : IConfigurationValidator +{ + private readonly ActivationRepartitionerOptions _options = options.Value; + + public void ValidateConfiguration() + { + if (_options.MaxEdgeCount <= 0) + { + ThrowMustBeGreaterThanZero(nameof(ActivationRepartitionerOptions.MaxEdgeCount)); + } + + if (_options.MaxUnprocessedEdges <= 0) + { + ThrowMustBeGreaterThanZero(nameof(ActivationRepartitionerOptions.MaxUnprocessedEdges)); + } + + if (_options.MinRoundPeriod <= TimeSpan.Zero) + { + ThrowMustBeGreaterThanZero(nameof(ActivationRepartitionerOptions.MinRoundPeriod)); + } + + if (_options.MaxRoundPeriod <= TimeSpan.Zero) + { + ThrowMustBeGreaterThanZero(nameof(ActivationRepartitionerOptions.MaxRoundPeriod)); + } + + if (_options.RecoveryPeriod <= TimeSpan.Zero) + { + ThrowMustBeGreaterThanZero(nameof(ActivationRepartitionerOptions.RecoveryPeriod)); + } + + if (_options.MaxRoundPeriod < _options.MinRoundPeriod) + { + ThrowMustBeGreaterThanOrEqualTo(nameof(ActivationRepartitionerOptions.MaxRoundPeriod), nameof(ActivationRepartitionerOptions.MinRoundPeriod)); + } + + if (_options.MinRoundPeriod < _options.RecoveryPeriod) + { + ThrowMustBeGreaterThanOrEqualTo(nameof(ActivationRepartitionerOptions.MinRoundPeriod), nameof(ActivationRepartitionerOptions.RecoveryPeriod)); + } + + if (_options.ProbabilisticFilteringMaxAllowedErrorRate < 0.001d || _options.ProbabilisticFilteringMaxAllowedErrorRate > 0.01d) + { + throw new OrleansConfigurationException($"{nameof(ActivationRepartitionerOptions.ProbabilisticFilteringMaxAllowedErrorRate)} must be inclusive between [0.001 - 0.01](0.1% - 1%)"); + } + } + + private static void ThrowMustBeGreaterThanZero(string propertyName) + => throw new OrleansConfigurationException($"{propertyName} must be greater than 0"); + + private static void ThrowMustBeGreaterThanOrEqualTo(string name1, string name2) + => throw new OrleansConfigurationException($"{name1} must be greater than or equal to {name2}"); +} \ No newline at end of file diff --git a/src/Orleans.Runtime/Core/ManagementGrain.cs b/src/Orleans.Runtime/Core/ManagementGrain.cs index 0e8767b97f..2a6552ea21 100644 --- a/src/Orleans.Runtime/Core/ManagementGrain.cs +++ b/src/Orleans.Runtime/Core/ManagementGrain.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Orleans.Concurrency; using Orleans.Metadata; +using Orleans.Placement.Repartitioning; using Orleans.Providers; using Orleans.Runtime.GrainDirectory; using Orleans.Runtime.MembershipService; @@ -371,5 +372,49 @@ public async ValueTask> GetActiveGrains(GrainType grainType) return results; } + + public async Task> GetGrainCallFrequencies(SiloAddress[] hostsIds = null) + { + if (hostsIds == null) + { + var hosts = await GetHosts(true); + hostsIds = [.. hosts.Keys]; + } + + var results = new List(); + foreach (var host in hostsIds) + { + var siloBalancer = IActivationRepartitionerSystemTarget.GetReference(internalGrainFactory, host); + var frequencies = await siloBalancer.GetGrainCallFrequencies(); + foreach (var frequency in frequencies) + { + results.Add(new GrainCallFrequency + { + SourceGrain = frequency.Item1.Source.Id, + TargetGrain = frequency.Item1.Target.Id, + SourceHost = frequency.Item1.Source.Silo, + TargetHost = frequency.Item1.Target.Silo, + CallCount = frequency.Item2 + }); + } + } + + return results; + } + + public async ValueTask ResetGrainCallFrequencies(SiloAddress[] hostsIds = null) + { + if (hostsIds == null) + { + var hosts = await GetHosts(true); + hostsIds = [.. hosts.Keys]; + } + + foreach (var host in hostsIds) + { + var siloBalancer = IActivationRepartitionerSystemTarget.GetReference(internalGrainFactory, host); + await siloBalancer.ResetCounters(); + } + } } } diff --git a/src/Orleans.Runtime/Hosting/ActivationRepartitioningExtensions.cs b/src/Orleans.Runtime/Hosting/ActivationRepartitioningExtensions.cs new file mode 100644 index 0000000000..38e28b8954 --- /dev/null +++ b/src/Orleans.Runtime/Hosting/ActivationRepartitioningExtensions.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.DependencyInjection; +using Orleans.Configuration.Internal; +using Orleans.Runtime; +using Orleans.Configuration; +using Orleans.Runtime.Placement.Repartitioning; +using System.Diagnostics.CodeAnalysis; +using Orleans.Placement.Repartitioning; + +namespace Orleans.Hosting; + +#nullable enable +public static class ActivationRepartitioningExtensions +{ + /// + /// Enables activation repartitioning for this silo. + /// + /// + /// Activation repartitioning attempts to optimize grain call locality by collocating activations which communicate frequently, + /// while keeping the number of activations on each silo approximately equal. + /// + [Experimental("ORLEANSEXP001")] + public static ISiloBuilder AddActivationRepartitioner(this ISiloBuilder builder) + => builder.AddActivationRepartitioner(); + + /// + /// Enables activation repartitioning for this silo. + /// + /// + /// Activation repartitioning attempts to optimize grain call locality by collocating activations which communicate frequently, + /// while keeping the number of activations on each silo approximately equal. + /// + /// The type of the imbalance rule to use. + [Experimental("ORLEANSEXP001")] + public static ISiloBuilder AddActivationRepartitioner(this ISiloBuilder builder) where TRule : class, IImbalanceToleranceRule + => builder + .ConfigureServices(services => services.AddActivationRepartitioner()); + + private static IServiceCollection AddActivationRepartitioner(this IServiceCollection services) where TRule : class, IImbalanceToleranceRule + { + services.AddSingleton(); + services.AddSingleton(); + services.AddFromExisting(); + services.AddFromExisting, ActivationRepartitioner>(); + services.AddTransient(); + + services.AddSingleton(); + services.AddFromExisting(); + if (typeof(TRule).IsAssignableTo(typeof(ILifecycleParticipant))) + { + services.AddFromExisting(typeof(ILifecycleParticipant), typeof(TRule)); + } + + return services; + } +} \ No newline at end of file diff --git a/src/Orleans.Runtime/Hosting/DefaultSiloServices.cs b/src/Orleans.Runtime/Hosting/DefaultSiloServices.cs index f814a109db..2e9d5d3e42 100644 --- a/src/Orleans.Runtime/Hosting/DefaultSiloServices.cs +++ b/src/Orleans.Runtime/Hosting/DefaultSiloServices.cs @@ -42,6 +42,7 @@ using Microsoft.Extensions.Configuration; using Orleans.Serialization.Internal; using Orleans.Core; +using Orleans.Placement.Repartitioning; namespace Orleans.Hosting { @@ -359,6 +360,7 @@ internal static void AddDefaultServices(ISiloBuilder builder) (sp, _) => sp.GetRequiredService()); // Networking + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/src/Orleans.Runtime/MembershipService/ISiloStatusOracle.cs b/src/Orleans.Runtime/MembershipService/ISiloStatusOracle.cs index 5babdbb7dd..dccaa7b321 100644 --- a/src/Orleans.Runtime/MembershipService/ISiloStatusOracle.cs +++ b/src/Orleans.Runtime/MembershipService/ISiloStatusOracle.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.Immutable; namespace Orleans.Runtime { @@ -22,6 +23,11 @@ public interface ISiloStatusOracle /// SiloAddress SiloAddress { get; } + /// + /// Gets the currently active silos. + /// + ImmutableArray GetActiveSilos(); + /// /// Gets the status of a given silo. /// This method returns an approximate view on the status of a given silo. diff --git a/src/Orleans.Runtime/MembershipService/SiloStatusOracle.cs b/src/Orleans.Runtime/MembershipService/SiloStatusOracle.cs index c37471f69b..2700d6f52d 100644 --- a/src/Orleans.Runtime/MembershipService/SiloStatusOracle.cs +++ b/src/Orleans.Runtime/MembershipService/SiloStatusOracle.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Microsoft.Extensions.Logging; using System.Threading; +using System.Collections.Immutable; namespace Orleans.Runtime.MembershipService { @@ -14,6 +15,7 @@ internal class SiloStatusOracle : ISiloStatusOracle private MembershipTableSnapshot cachedSnapshot; private Dictionary siloStatusCache = new Dictionary(); private Dictionary siloStatusCacheOnlyActive = new Dictionary(); + private ImmutableArray _activeSilos = []; public SiloStatusOracle( ILocalSiloDetails localSiloDetails, @@ -49,35 +51,53 @@ public SiloStatus GetApproximateSiloStatus(SiloAddress silo) return status; } + public ImmutableArray GetActiveSilos() + { + EnsureFreshCache(); + return _activeSilos; + } + public Dictionary GetApproximateSiloStatuses(bool onlyActive = false) { - if (ReferenceEquals(this.cachedSnapshot, this.membershipTableManager.MembershipTableSnapshot)) + EnsureFreshCache(); + return onlyActive ? this.siloStatusCacheOnlyActive : this.siloStatusCache; + } + + private void EnsureFreshCache() + { + var currentMembership = this.membershipTableManager.MembershipTableSnapshot; + if (ReferenceEquals(this.cachedSnapshot, currentMembership)) { - return onlyActive ? this.siloStatusCacheOnlyActive : this.siloStatusCache; + return; } lock (this.cacheUpdateLock) { - var currentMembership = this.membershipTableManager.MembershipTableSnapshot; + currentMembership = this.membershipTableManager.MembershipTableSnapshot; if (ReferenceEquals(this.cachedSnapshot, currentMembership)) { - return onlyActive ? this.siloStatusCacheOnlyActive : this.siloStatusCache; + return; } var newSiloStatusCache = new Dictionary(); var newSiloStatusCacheOnlyActive = new Dictionary(); + var newActiveSilos = ImmutableArray.CreateBuilder(); foreach (var entry in currentMembership.Entries) { var silo = entry.Key; var status = entry.Value.Status; newSiloStatusCache[silo] = status; - if (status == SiloStatus.Active) newSiloStatusCacheOnlyActive[silo] = status; + if (status == SiloStatus.Active) + { + newSiloStatusCacheOnlyActive[silo] = status; + newActiveSilos.Add(silo); + } } Interlocked.Exchange(ref this.cachedSnapshot, currentMembership); this.siloStatusCache = newSiloStatusCache; this.siloStatusCacheOnlyActive = newSiloStatusCacheOnlyActive; - return onlyActive ? newSiloStatusCacheOnlyActive : newSiloStatusCache; + _activeSilos = newActiveSilos.ToImmutable(); } } diff --git a/src/Orleans.Runtime/Messaging/MessageCenter.cs b/src/Orleans.Runtime/Messaging/MessageCenter.cs index 96564a950f..24eb05e86e 100644 --- a/src/Orleans.Runtime/Messaging/MessageCenter.cs +++ b/src/Orleans.Runtime/Messaging/MessageCenter.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Orleans.Configuration; +using Orleans.Placement.Repartitioning; using Orleans.Runtime.GrainDirectory; using Orleans.Runtime.Placement; using Orleans.Serialization.Invocation; @@ -21,6 +22,7 @@ internal class MessageCenter : IMessageCenter, IAsyncDisposable private readonly SiloMessagingOptions messagingOptions; private readonly PlacementService placementService; private readonly GrainLocator _grainLocator; + private readonly IMessageStatisticsSink _messageStatisticsSink; private readonly ILogger log; private readonly Catalog catalog; private bool stopped; @@ -38,7 +40,8 @@ public MessageCenter( RuntimeMessagingTrace messagingTrace, IOptions messagingOptions, PlacementService placementService, - GrainLocator grainLocator) + GrainLocator grainLocator, + IMessageStatisticsSink messageStatisticsSink) { this.catalog = catalog; this.messagingOptions = messagingOptions.Value; @@ -47,6 +50,7 @@ public MessageCenter( this.messagingTrace = messagingTrace; this.placementService = placementService; _grainLocator = grainLocator; + _messageStatisticsSink = messageStatisticsSink; this.log = logger; this.messageFactory = messageFactory; this._siloAddress = siloDetails.SiloAddress; @@ -66,8 +70,14 @@ public MessageCenter( public bool TryDeliverToProxy(Message msg) { if (!msg.TargetGrain.IsClient()) return false; - if (this.Gateway is Gateway gateway && gateway.TryDeliverToProxy(msg)) return true; - return this.hostedClient is HostedClient client && client.TryDispatchToClient(msg); + if (this.Gateway is Gateway gateway && gateway.TryDeliverToProxy(msg) + || this.hostedClient is HostedClient client && client.TryDispatchToClient(msg)) + { + _messageStatisticsSink.RecordMessage(msg); + return true; + } + + return false; } public async Task StopAsync() @@ -173,6 +183,7 @@ public void SendMessage(Message msg) } messagingTrace.OnSendMessage(msg); + if (targetSilo.Matches(_siloAddress)) { if (log.IsEnabled(LogLevel.Trace)) @@ -532,6 +543,7 @@ public void ReceiveMessage(Message msg) } targetActivation.ReceiveMessage(msg); + _messageStatisticsSink.RecordMessage(msg); } } catch (Exception ex) diff --git a/src/Orleans.Runtime/Placement/PlacementService.cs b/src/Orleans.Runtime/Placement/PlacementService.cs index 3428b7905b..9c8dee8154 100644 --- a/src/Orleans.Runtime/Placement/PlacementService.cs +++ b/src/Orleans.Runtime/Placement/PlacementService.cs @@ -68,7 +68,7 @@ public PlacementService( /// public Task AddressMessage(Message message) { - if (message.IsFullyAddressed) return Task.CompletedTask; + if (message.IsTargetFullyAddressed) return Task.CompletedTask; if (message.TargetGrain.IsDefault) ThrowMissingAddress(); var grainId = message.TargetGrain; diff --git a/src/Orleans.Runtime/Placement/Repartitioning/ActivationRepartitioner.Log.cs b/src/Orleans.Runtime/Placement/Repartitioning/ActivationRepartitioner.Log.cs new file mode 100644 index 0000000000..270615d9b7 --- /dev/null +++ b/src/Orleans.Runtime/Placement/Repartitioning/ActivationRepartitioner.Log.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace Orleans.Runtime.Placement.Repartitioning; + +internal partial class ActivationRepartitioner +{ + [LoggerMessage(Level = LogLevel.Debug, Message = "I will periodically initiate the exchange protocol every {MinPeriod} to {MaxPeriod} starting in {DueTime}.")] + private partial void LogPeriodicallyInvokeProtocol(TimeSpan minPeriod, TimeSpan maxPeriod, TimeSpan dueTime); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Activation repartitioning is enabled, but the cluster contains only one silo. Waiting for at least another silo to join the cluster to proceed.")] + private partial void LogSingleSiloCluster(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Exchange set for candidate silo {CandidateSilo} is empty. I will try the next best candidate (if one is available), otherwise I will wait for my next period to come.")] + private partial void LogExchangeSetIsEmpty(SiloAddress candidateSilo); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Beginning exchange protocol between {ThisSilo} and {CandidateSilo}.")] + private partial void LogBeginningProtocol(SiloAddress thisSilo, SiloAddress candidateSilo); + + [LoggerMessage(Level = LogLevel.Debug, Message = "I got an exchange request from {SendingSilo}, but I have been recently involved in another exchange {LastExchangeDuration} ago. My recovery period is {RecoveryPeriod}")] + private partial void LogExchangedRecently(SiloAddress sendingSilo, TimeSpan lastExchangeDuration, TimeSpan recoveryPeriod); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Exchange request from {ThisSilo} rejected: {CandidateSilo} was recently involved in another exchange. Attempting the next best candidate (if one is available) or waiting for my next period to come.")] + private partial void LogExchangedRecentlyResponse(SiloAddress thisSilo, SiloAddress candidateSilo); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Rejecting exchange request from {SendingSilo} since we are already exchanging with that host.")] + private partial void LogMutualExchangeAttempt(SiloAddress sendingSilo); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Exchange request from {ThisSilo} superseded by a mutual exchange attempt with {CandidateSilo}.")] + private partial void LogMutualExchangeAttemptResponse(SiloAddress thisSilo, SiloAddress candidateSilo); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Finalized exchange protocol: migrating {GivingActivationCount} activations, receiving {TakingActivationCount} activations.")] + private partial void LogProtocolFinalized(int givingActivationCount, int takingActivationCount); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Error performing exchange request from {ThisSilo} to {CandidateSilo}. I will try the next best candidate (if one is available), otherwise I will wait for my next period to come.")] + private partial void LogErrorOnProtocolExecution(Exception exception, SiloAddress thisSilo, SiloAddress candidateSilo); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Error migrating exchange set.")] + private partial void LogErrorOnMigratingActivations(Exception exception); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Received AcceptExchangeRequest from {SendingSilo}, offering to send {ExchangeSetCount} activations from a total of {ActivationCount} activations.")] + private partial void LogReceivedExchangeRequest(SiloAddress sendingSilo, int exchangeSetCount, int activationCount); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Imbalance is {Imbalance} (remote: {RemoteCount} vs local {LocalCount})")] + private partial void LogImbalance(int imbalance, int remoteCount, int localCount); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Computing transfer set took {Elapsed}. Anticipated imbalance after transfer is {AnticipatedImbalance}.")] + private partial void LogTransferSetComputed(TimeSpan elapsed, int anticipatedImbalance); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Error accepting exchange request from {SendingSilo}.")] + private partial void LogErrorAcceptingExchangeRequest(Exception exception, SiloAddress sendingSilo); +} diff --git a/src/Orleans.Runtime/Placement/Repartitioning/ActivationRepartitioner.MessageSink.cs b/src/Orleans.Runtime/Placement/Repartitioning/ActivationRepartitioner.MessageSink.cs new file mode 100644 index 0000000000..b35686179d --- /dev/null +++ b/src/Orleans.Runtime/Placement/Repartitioning/ActivationRepartitioner.MessageSink.cs @@ -0,0 +1,135 @@ +#nullable enable +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Orleans.Placement.Repartitioning; +using Orleans.Runtime.Internal; + +namespace Orleans.Runtime.Placement.Repartitioning; + +internal partial class ActivationRepartitioner : IMessageStatisticsSink +{ + private readonly CancellationTokenSource _shutdownCts = new(); + + // This filter contains grain ids which will are anchored to the current silo. + // Ids are inserted when a grain is found to have a negative transfer score. + private readonly BlockedBloomFilter? _anchoredFilter; + private Task? _processPendingEdgesTask; + + public void StartProcessingEdges() + { + using var _ = new ExecutionContextSuppressor(); + _processPendingEdgesTask = ProcessPendingEdges(_shutdownCts.Token); + + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.LogTrace("{Service} has started.", nameof(ActivationRepartitioner)); + } + } + + public async Task StopProcessingEdgesAsync(CancellationToken cancellationToken) + { + _shutdownCts.Cancel(); + if (_processPendingEdgesTask is null) + { + return; + } + + _pendingMessageEvent.Signal(); + await _processPendingEdgesTask.WaitAsync(cancellationToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.LogTrace("{Service} has stopped.", nameof(ActivationRepartitioner)); + } + } + + private async Task ProcessPendingEdges(CancellationToken cancellationToken) + { + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding | ConfigureAwaitOptions.ContinueOnCapturedContext); + + var drainBuffer = new Message[128]; + var iteration = 0; + const int MaxIterationsPerYield = 128; + while (!cancellationToken.IsCancellationRequested) + { + var count = _pendingMessages.DrainTo(drainBuffer); + if (count > 0) + { + foreach (var message in drainBuffer[..count]) + { + if (!_messageFilter.IsAcceptable(message, out var isSenderMigratable, out var isTargetMigratable)) + { + continue; + } + + EdgeVertex sourceVertex; + if (_anchoredFilter != null && _anchoredFilter.Contains(message.SendingGrain) && Silo.Equals(message.SendingSilo)) + { + sourceVertex = new(GrainId, Silo, isMigratable: false); + } + else + { + sourceVertex = new(message.SendingGrain, message.SendingSilo, isSenderMigratable); + } + + EdgeVertex destinationVertex; + if (_anchoredFilter != null && _anchoredFilter.Contains(message.TargetGrain) && Silo.Equals(message.TargetSilo)) + { + destinationVertex = new(GrainId, Silo, isMigratable: false); + } + else + { + destinationVertex = new(message.TargetGrain, message.TargetSilo, isTargetMigratable); + } + + if (!sourceVertex.IsMigratable && !destinationVertex.IsMigratable) + { + // Ignore edges between two non-migratable grains. + continue; + } + + Edge edge = new(sourceVertex, destinationVertex); + _edgeWeights.Add(edge); + } + + if (++iteration >= MaxIterationsPerYield) + { + iteration = 0; + await Task.Delay(TimeSpan.FromTicks(TimeSpan.TicksPerMillisecond), CancellationToken.None); + } + } + else + { + iteration = 0; + await _pendingMessageEvent.WaitAsync(); + } + } + } + + public void RecordMessage(Message message) + { + if (!_enableMessageSampling || message.IsSystemMessage) + { + return; + } + + // It must have a direction, and must not be a 'response' as it would skew analysis. + if (message.Direction is Message.Directions.None or Message.Directions.Response) + { + return; + } + + // Sender and target need to be fully addressable to know where to move to or towards. + if (!message.IsSenderFullyAddressed || !message.IsTargetFullyAddressed) + { + return; + } + + if (_pendingMessages.TryAdd(message) == Utilities.BufferStatus.Success) + { + _pendingMessageEvent.Signal(); + } + } +} \ No newline at end of file diff --git a/src/Orleans.Runtime/Placement/Repartitioning/ActivationRepartitioner.cs b/src/Orleans.Runtime/Placement/Repartitioning/ActivationRepartitioner.cs new file mode 100644 index 0000000000..79403bfb1d --- /dev/null +++ b/src/Orleans.Runtime/Placement/Repartitioning/ActivationRepartitioner.cs @@ -0,0 +1,780 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Runtime.CompilerServices; +using System.Diagnostics; +using System.Collections.Immutable; +using System.Data; +using System.Threading; +using Orleans.Internal; +using Orleans.Configuration; +using Orleans.Runtime.Utilities; +using System.Runtime.InteropServices; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Orleans.Placement.Repartitioning; + +namespace Orleans.Runtime.Placement.Repartitioning; + +// See: https://www.microsoft.com/en-us/research/wp-content/uploads/2016/06/eurosys16loca_camera_ready-1.pdf +internal sealed partial class ActivationRepartitioner : SystemTarget, IActivationRepartitionerSystemTarget, ILifecycleParticipant, IDisposable, ISiloStatusListener +{ + private readonly ILogger _logger; + private readonly ISiloStatusOracle _siloStatusOracle; + private readonly IInternalGrainFactory _grainFactory; + private readonly IRepartitionerMessageFilter _messageFilter; + private readonly IImbalanceToleranceRule _toleranceRule; + private readonly IActivationMigrationManager _migrationManager; + private readonly ActivationDirectory _activationDirectory; + private readonly TimeProvider _timeProvider; + private readonly ActivationRepartitionerOptions _options; + private readonly StripedMpscBuffer _pendingMessages; + private readonly SingleWaiterAutoResetEvent _pendingMessageEvent = new() { RunContinuationsAsynchronously = true }; + private readonly FrequentEdgeCounter _edgeWeights; + private readonly IGrainTimer _timer; + private SiloAddress? _currentExchangeSilo; + private CoarseStopwatch _lastExchangedStopwatch; + private int _activationCountOffset; + private bool _enableMessageSampling; + + public ActivationRepartitioner( + ISiloStatusOracle siloStatusOracle, + ILocalSiloDetails localSiloDetails, + ILoggerFactory loggerFactory, + IInternalGrainFactory internalGrainFactory, + IRepartitionerMessageFilter messageFilter, + IImbalanceToleranceRule toleranceRule, + IActivationMigrationManager migrationManager, + ActivationDirectory activationDirectory, + Catalog catalog, + IOptions options, + TimeProvider timeProvider) + : base(Constants.ActivationRepartitionerType, localSiloDetails.SiloAddress, loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + _options = options.Value; + _siloStatusOracle = siloStatusOracle; + _grainFactory = internalGrainFactory; + _messageFilter = messageFilter; + _toleranceRule = toleranceRule; + _migrationManager = migrationManager; + _activationDirectory = activationDirectory; + _timeProvider = timeProvider; + _edgeWeights = new(options.Value.MaxEdgeCount); + _pendingMessages = new StripedMpscBuffer(Environment.ProcessorCount, options.Value.MaxUnprocessedEdges / Environment.ProcessorCount); + _anchoredFilter = options.Value.AnchoringFilterEnabled ? + new BlockedBloomFilter(100_000, options.Value.ProbabilisticFilteringMaxAllowedErrorRate) : + null; + + _lastExchangedStopwatch = CoarseStopwatch.StartNew(); + catalog.RegisterSystemTarget(this); + _siloStatusOracle.SubscribeToSiloStatusEvents(this); + _timer = RegisterTimer(_ => TriggerExchangeRequest().AsTask(), null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + } + + private Task OnActiveStart(CancellationToken cancellationToken) + { + Scheduler.QueueAction(() => + { + // Schedule the first timer tick. + UpdateTimer(); + StartProcessingEdges(); + }); + + return Task.CompletedTask; + } + + public ValueTask ResetCounters() + { + _pendingMessages.Clear(); + _edgeWeights.Clear(); + _anchoredFilter?.Reset(); + return ValueTask.CompletedTask; + } + + ValueTask IActivationRepartitionerSystemTarget.GetActivationCount() => new(_activationDirectory.Count); + ValueTask IActivationRepartitionerSystemTarget.SetActivationCountOffset(int activationCountOffset) + { + _activationCountOffset = activationCountOffset; + return ValueTask.CompletedTask; + } + + private void UpdateTimer() => UpdateTimer(RandomTimeSpan.Next(_options.MinRoundPeriod, _options.MaxRoundPeriod)); + private void UpdateTimer(TimeSpan dueTime) + { + _timer.Change(dueTime, dueTime); + LogPeriodicallyInvokeProtocol(_options.MinRoundPeriod, _options.MaxRoundPeriod, dueTime); + } + + public async ValueTask TriggerExchangeRequest() + { + var coolDown = _options.RecoveryPeriod - _lastExchangedStopwatch.Elapsed; + if (coolDown > TimeSpan.Zero) + { + _logger.LogDebug("Waiting an additional {CoolDown} to cool down before initiating the exchange protocol.", coolDown); + await Task.Delay(coolDown, _timeProvider); + } + + // Schedule the next timer tick. + UpdateTimer(); + + if (_currentExchangeSilo is not null) + { + // Skip this round if we are already in the process of exchanging with another silo. + return; + } + + var silos = _siloStatusOracle.GetActiveSilos(); + if (silos.Length == 1) + { + LogSingleSiloCluster(); + return; + } + else if (!_enableMessageSampling) + { + return; + } + + var sw = ValueStopwatch.StartNew(); + var migrationCandidates = GetMigrationCandidates(); + var sets = CreateCandidateSets(migrationCandidates, silos); + var anchoredSet = ComputeAnchoredGrains(migrationCandidates); + _logger.LogInformation("Candidate sets computed in {Elapsed} ms.", sw.Elapsed.TotalMilliseconds); + foreach ((var candidateSilo, var offeredGrains, var _) in sets) + { + if (offeredGrains.Count == 0) + { + LogExchangeSetIsEmpty(candidateSilo); + continue; + } + + try + { + // Set the exchange partner for the duration of the operation. + // This prevents other requests from interleaving. + _currentExchangeSilo = candidateSilo; + + LogBeginningProtocol(Silo, candidateSilo); + var remoteRef = IActivationRepartitionerSystemTarget.GetReference(_grainFactory, candidateSilo); + var response = await remoteRef.AcceptExchangeRequest(new(Silo, [.. offeredGrains], GetLocalActivationCount())); + + switch (response.Type) + { + case AcceptExchangeResponse.ResponseType.Success: + // Exchange was successful, no need to iterate over another candidate. + await FinalizeProtocol(response.AcceptedGrainIds, response.GivenGrainIds, candidateSilo, anchoredSet); + return; + case AcceptExchangeResponse.ResponseType.ExchangedRecently: + // The remote silo has been recently involved in another exchange, try the next best candidate. + LogExchangedRecentlyResponse(Silo, candidateSilo); + continue; + case AcceptExchangeResponse.ResponseType.MutualExchangeAttempt: + // The remote silo is exchanging with this silo already and the exchange the remote silo initiated + // took precedence over the one this silo is initiating. + LogMutualExchangeAttemptResponse(Silo, candidateSilo); + return; + } + } + catch (Exception ex) + { + LogErrorOnProtocolExecution(ex, Silo, candidateSilo); + continue; // there was some problem, try the next best candidate + } + finally + { + _currentExchangeSilo = null; + } + } + } + + private int GetLocalActivationCount() => _activationDirectory.Count + _activationCountOffset; + + public async ValueTask AcceptExchangeRequest(AcceptExchangeRequest request) + { + LogReceivedExchangeRequest(request.SendingSilo, request.ExchangeSet.Length, request.ActivationCountSnapshot); + if (request.SendingSilo.Equals(_currentExchangeSilo) && Silo.CompareTo(request.SendingSilo) <= 0) + { + // Reject the request, as we are already in the process of exchanging with the sending silo. + // The '<=' comparison here is used to break the tie in case both silos are exchanging with each other. + + // We pick some random time between 'min' and 'max' and than subtract from it 'min'. We do this so this silo doesn't have to wait for 'min + random', + // as it did the very first time this was started. It is guaranteed that 'random - min' >= 0; as 'random' will be at the least equal to 'min'. + UpdateTimer(RandomTimeSpan.Next(_options.MinRoundPeriod, _options.MaxRoundPeriod) - _options.MinRoundPeriod); + LogMutualExchangeAttempt(request.SendingSilo); + + return AcceptExchangeResponse.CachedMutualExchangeAttempt; + } + + var lastExchangeElapsed = _lastExchangedStopwatch.Elapsed; + if (lastExchangeElapsed < _options.RecoveryPeriod) + { + LogExchangedRecently(request.SendingSilo, lastExchangeElapsed, _options.RecoveryPeriod); + return AcceptExchangeResponse.CachedExchangedRecently; + } + + // Set the exchange silo for the duration of the request. + // This prevents other requests from interleaving. + _currentExchangeSilo = request.SendingSilo; + + try + { + var acceptedGrains = ImmutableArray.CreateBuilder(); + var givingGrains = ImmutableArray.CreateBuilder(); + var remoteSet = request.ExchangeSet; + var migrationCandidates = GetMigrationCandidates(); + var localSet = GetCandidatesForSilo(migrationCandidates, request.SendingSilo); + var anchoredSet = ComputeAnchoredGrains(migrationCandidates); + + // We need to determine 2 subsets: + // - One that originates from sending silo (request.ExchangeSet) and will be (partially) accepted from this silo. + // - One that originates from this silo (candidateSet) and will be (fully) accepted from the sending silo. + var remoteActivations = request.ActivationCountSnapshot; + var localActivations = GetLocalActivationCount(); + + var initialImbalance = CalculateImbalance(remoteActivations, localActivations); + int imbalance = initialImbalance; + LogImbalance(imbalance, remoteActivations, localActivations); + + var stopwatch = ValueStopwatch.StartNew(); + var (localHeap, remoteHeap) = CreateCandidateHeaps(localSet, remoteSet); + _logger.LogInformation("Candidate heaps created in {Elapsed} ms.", stopwatch.Elapsed.TotalMilliseconds); + stopwatch.Restart(); + + var iterations = 0; + var yieldStopwatch = CoarseStopwatch.StartNew(); + while (true) + { + if (++iterations % 128 == 0 && yieldStopwatch.ElapsedMilliseconds > 25) + { + // Give other tasks a chance to execute periodically. + yieldStopwatch.Restart(); + await Task.Delay(1); + } + + // If more is gained by giving grains to the remote silo than taking from it, we will try giving first. + var localScore = localHeap.FirstOrDefault()?.AccumulatedTransferScore ?? 0; + var remoteScore = remoteHeap.FirstOrDefault()?.AccumulatedTransferScore ?? 0; + if (localScore > remoteScore || localScore == remoteScore && localActivations > remoteActivations) + { + if (TryMigrateLocalToRemote()) continue; + if (TryMigrateRemoteToLocal()) continue; + } + else + { + if (TryMigrateRemoteToLocal()) continue; + if (TryMigrateLocalToRemote()) continue; + } + + // No more migrations can be made, so the candidate set has been calculated. + break; + } + + LogTransferSetComputed(stopwatch.Elapsed, imbalance); + var giving = givingGrains.ToImmutable(); + var accepting = acceptedGrains.ToImmutable(); + await FinalizeProtocol(giving, accepting, request.SendingSilo, anchoredSet); + + return new(AcceptExchangeResponse.ResponseType.Success, accepting, giving); + + bool TryMigrateLocalToRemote() + { + if (!TryMigrateCore(localHeap, localDelta: -1, remoteDelta: 1, out var chosenVertex)) + { + return false; + } + + givingGrains.Add(chosenVertex.Id); + foreach (var (connectedVertex, transferScore) in chosenVertex.ConnectedVertices) + { + switch (connectedVertex.Location) + { + case VertexLocation.Local: + // Add the transfer score as these two vectors will now be remote to each other. + connectedVertex.AccumulatedTransferScore += transferScore; + localHeap.OnIncreaseElementPriority(connectedVertex); + break; + case VertexLocation.Remote: + // Subtract the transfer score as these two vectors will now be local to each other. + connectedVertex.AccumulatedTransferScore -= transferScore; + remoteHeap.OnDecreaseElementPriority(connectedVertex); + break; + } + } + + // We will perform any future operations assuming the vector is remote. + chosenVertex.Location = VertexLocation.Remote; + Debug.Assert(((IHeapElement)chosenVertex).HeapIndex == -1); + + return true; + } + + bool TryMigrateRemoteToLocal() + { + if (!TryMigrateCore(remoteHeap, localDelta: 1, remoteDelta: -1, out var chosenVertex)) + { + return false; + } + + acceptedGrains.Add(chosenVertex.Id); + foreach (var (connectedVertex, transferScore) in chosenVertex.ConnectedVertices) + { + switch (connectedVertex.Location) + { + case VertexLocation.Local: + // Subtract the transfer score as these two vectors will now be local to each other. + connectedVertex.AccumulatedTransferScore -= transferScore; + localHeap.OnDecreaseElementPriority(connectedVertex); + break; + case VertexLocation.Remote: + // Add the transfer score as these two vectors will now be remote to each other. + connectedVertex.AccumulatedTransferScore += transferScore; + remoteHeap.OnIncreaseElementPriority(connectedVertex); + break; + } + } + + // We will perform any future operations assuming the vector is local. + chosenVertex.Location = VertexLocation.Local; + + return true; + } + + bool TryMigrateCore(MaxHeap sourceHeap, int localDelta, int remoteDelta, [NotNullWhen(true)] out CandidateVertexHeapElement? chosenVertex) + { + var anticipatedImbalance = CalculateImbalance(localActivations + localDelta, remoteActivations + remoteDelta); + if (anticipatedImbalance >= imbalance && !_toleranceRule.IsSatisfiedBy((uint)anticipatedImbalance)) + { + // Taking from this heap would not improve imbalance. + chosenVertex = null; + return false; + } + + if (!sourceHeap.TryPop(out chosenVertex)) + { + // Heap is empty. + return false; + } + + if (chosenVertex.AccumulatedTransferScore <= 0) + { + // If it got affected by a previous run, and the score is zero or negative, simply pop and ignore it. + return false; + } + + localActivations += localDelta; + remoteActivations += remoteDelta; + imbalance = anticipatedImbalance; + return true; + } + + } + catch (Exception exception) + { + LogErrorAcceptingExchangeRequest(exception, request.SendingSilo); + throw; + } + finally + { + _currentExchangeSilo = null; + } + } + + private static int CalculateImbalance(int left, int right) => Math.Abs(Math.Abs(left) - Math.Abs(right)); + private static (MaxHeap Local, MaxHeap Remote) CreateCandidateHeaps(List local, ImmutableArray remote) + { + Dictionary sourceIndex = new(local.Count + remote.Length); + foreach (var element in local) + { + sourceIndex[element.Id] = element; + } + + foreach (var element in remote) + { + sourceIndex[element.Id] = element; + } + + Dictionary heapIndex = []; + List localVertexList = new(local.Count); + foreach (var element in local) + { + var vertex = CreateVertex(sourceIndex, heapIndex, element); + vertex.Location = VertexLocation.Local; + localVertexList.Add(vertex); + } + + List remoteVertexList = new(remote.Length); + foreach (var element in remote) + { + var vertex = CreateVertex(sourceIndex, heapIndex, element); + if (vertex.Location is not VertexLocation.Unknown) + { + // This vertex is already part of the local set, so assume that the vertex is local and ignore the remote vertex. + continue; + } + + vertex.Location = VertexLocation.Remote; + remoteVertexList.Add(vertex); + } + + var localHeap = new MaxHeap(localVertexList); + var remoteHeap = new MaxHeap(remoteVertexList); + return (localHeap, remoteHeap); + + static CandidateVertexHeapElement CreateVertex(Dictionary sourceIndex, Dictionary index, CandidateVertex element) + { + var vertex = GetOrAddVertex(index, element); + foreach (var connectedVertex in element.ConnectedVertices) + { + if (sourceIndex.TryGetValue(connectedVertex.Id, out var connected)) + { + vertex.ConnectedVertices.Add((GetOrAddVertex(index, connected), connectedVertex.TransferScore)); + } + else + { + // The connected vertex is not part of either migration candidate set, so we will ignore it. + } + } + + return vertex; + + static CandidateVertexHeapElement GetOrAddVertex(Dictionary index, CandidateVertex element) + { + ref var vertex = ref CollectionsMarshal.GetValueRefOrAddDefault(index, element.Id, out var exists); + vertex ??= new(element); + return vertex; + } + } + } + + /// + /// + /// Initiates the actual migration process of to 'this' silo. + /// Updates the affected counters within to reflect all . + /// + /// + /// The grain ids to migrate to the remote host. + /// The grain ids to which are migrating to the local host. + private async Task FinalizeProtocol(ImmutableArray giving, ImmutableArray accepting, SiloAddress targetSilo, HashSet newlyAnchoredGrains) + { + // The protocol concluded that 'this' silo should take on 'set', so we hint to the director accordingly. + try + { + Dictionary migrationRequestContext = new() { [IPlacementDirector.PlacementHintKey] = targetSilo }; + foreach (var grainId in giving) + { + if (_activationDirectory.FindTarget(grainId) is { } localActivation) + { + localActivation.Migrate(migrationRequestContext); + } + } + } + catch (Exception exception) + { + // This should happen rarely, but at this point we cant really do much, as its out of our control. + // Even if some fail, the algorithm will eventually run again, so activations will have more chances to migrate. + LogErrorOnMigratingActivations(exception); + } + + // Avoid mutating the source while enumerating it. + var iterations = 0; + var toRemove = new List(); + var affected = new HashSet(giving.Length + accepting.Length); + + _logger.LogInformation("Adding {NewlyAnchoredGrains} newly anchored grains to set on host {Silo}. EdgeWeights contains {EdgeWeightCount} elements.", newlyAnchoredGrains.Count, Silo, _edgeWeights.Count); + if (_anchoredFilter is { } filter) + { + foreach (var id in newlyAnchoredGrains) + { + filter.Add(id); + } + } + + foreach (var id in accepting) + { + affected.Add(id); + } + + foreach (var id in giving) + { + affected.Add(id); + } + + var yieldStopwatch = CoarseStopwatch.StartNew(); + if (affected.Count > 0) + { + foreach (var (edge, _, _) in _edgeWeights.Elements) + { + if (affected.Contains(edge.Source.Id) || affected.Contains(edge.Target.Id) || _anchoredFilter is not null && (_anchoredFilter.Contains(edge.Source.Id) || _anchoredFilter.Contains(edge.Target.Id))) + { + toRemove.Add(edge); + } + } + + foreach (var edge in toRemove) + { + if (++iterations % 128 == 0 && yieldStopwatch.ElapsedMilliseconds > 25) + { + // Give other tasks a chance to execute periodically. + yieldStopwatch.Restart(); + await Task.Delay(1); + } + + // Totally remove this counter, as one or both vertices has migrated. By not doing this it would skew results for the next protocol cycle. + // We remove only the affected counters, as there could be other counters that 'this' silo has connections with another silo (which is not part of this exchange cycle). + _edgeWeights.Remove(edge); + } + } + + // Stamp this silos exchange for a potential next pair exchange request. + _lastExchangedStopwatch.Restart(); + LogProtocolFinalized(giving.Length, accepting.Length); + } + + private List<(SiloAddress Silo, List Candidates, long TransferScore)> CreateCandidateSets(List> migrationCandidates, ImmutableArray silos) + { + List<(SiloAddress Silo, List Candidates, long TransferScore)> candidateSets = new(silos.Length - 1); + + foreach (var siloAddress in silos) + { + if (siloAddress.Equals(Silo)) + { + // We aren't going to exchange anything with ourselves, so skip this silo. + continue; + } + + var candidatesForRemote = GetCandidatesForSilo(migrationCandidates, siloAddress); + var totalAccTransferScore = candidatesForRemote.Sum(x => x.AccumulatedTransferScore); + + candidateSets.Add(new(siloAddress, [.. candidatesForRemote], totalAccTransferScore)); + } + + // Order them by the highest accumulated transfer score + candidateSets.Sort(static (a, b) => -a.TransferScore.CompareTo(b.TransferScore)); + + return candidateSets; + } + + private List GetCandidatesForSilo(List> migrationCandidates, SiloAddress otherSilo) + { + Debug.Assert(!otherSilo.Equals(Silo)); + + List result = []; + + // We skip types that cant be migrated. Instead the same edge will be recorded from the receiver, so its hosting silo will add it as a candidate to be migrated (over here). + // We are sure that the receiver is an migratable grain, because the gateway forbids edges that have non-migratable vertices on both sides. + foreach (var grainEdges in migrationCandidates) + { + var accLocalScore = 0L; + var accRemoteScore = 0L; + + foreach (var edge in grainEdges) + { + if (edge.Direction is Direction.LocalToLocal) + { + // Since its L2L, it means the partner silo will be 'this' silo, so we don't need to filter by the partner silo. + accLocalScore += edge.Weight; + } + else if (edge.PartnerSilo.Equals(otherSilo)) + { + Debug.Assert(edge.Direction is Direction.RemoteToLocal or Direction.LocalToRemote); + + // We need to filter here by 'otherSilo' since any L2R or R2L edge can be between the current vertex and a vertex in a silo that is not in 'otherSilo'. + accRemoteScore += edge.Weight; + } + } + + if (accLocalScore >= accRemoteScore) + { + // We skip vertices for which local calls outweigh the remote ones. + continue; + } + + var totalAccScore = accRemoteScore - accLocalScore; + var connVertices = ImmutableArray.CreateBuilder(); + foreach (var edge in grainEdges) + { + // Note that the connected vertices can be of types which are not migratable, it is important to keep them, + // as they too impact the migration cost of the current candidate vertex, especially if they are local to the candidate + // as those calls would be potentially converted to remote calls, after the migration of the current candidate. + // 'Weight' here represent the weight of a single edge, not the accumulated like above. + connVertices.Add(new CandidateConnectedVertex(edge.TargetId, edge.Weight)); + } + + CandidateVertex candidate = new() + { + Id = grainEdges.Key, + AccumulatedTransferScore = totalAccScore, + ConnectedVertices = connVertices.ToImmutable() + }; + + result.Add(candidate); + } + + return result; + } + + private static HashSet ComputeAnchoredGrains(List> migrationCandidates) + { + HashSet anchoredGrains = []; + foreach (var grainEdges in migrationCandidates) + { + var accLocalScore = 0L; + var accRemoteScore = 0L; + + foreach (var edge in grainEdges) + { + if (edge.Direction is Direction.LocalToLocal) + { + accLocalScore += edge.Weight; + } + else + { + Debug.Assert(edge.Direction is Direction.RemoteToLocal or Direction.LocalToRemote); + accRemoteScore += edge.Weight; + } + } + + if (accLocalScore > accRemoteScore) + { + anchoredGrains.Add(grainEdges.Key); + } + } + + return anchoredGrains; + } + + private List> GetMigrationCandidates() => CreateLocalVertexEdges().Where(x => x.IsMigratable).GroupBy(x => x.SourceId).ToList(); + + /// + /// Creates a collection of 'local' vertex edges. Multiple entries can have the same Id. + /// + /// The is guaranteed to belong to a grain that is local to this silo, while might belong to a local or remote silo. + private IEnumerable CreateLocalVertexEdges() + { + foreach (var (edge, count, error) in _edgeWeights.Elements) + { + if (count == 0) + { + continue; + } + + var vertexEdge = CreateVertexEdge(edge, count); + if (vertexEdge.Direction is Direction.Unspecified) + { + // This can occur when a message is re-routed via this silo. + continue; + } + + yield return vertexEdge; + + if (vertexEdge.Direction == Direction.LocalToLocal) + { + // The reason we do this flipping is because when the edge is Local-to-Local, we have 2 grains that are linked via an communication edge. + // Once an edge exists it means 2 grains are temporally linked, this means that there is a cost associated to potentially move either one of them. + // Since the construction of the candidate set takes into account also local connection (which increases the cost of migration), we need + // to take into account the edge not only from a source's perspective, but also the target's one, as it too will take part on the candidate set. + var flippedEdge = CreateVertexEdge(edge.Flip(), count); + yield return flippedEdge; + } + } + + VertexEdge CreateVertexEdge(in Edge edge, long weight) + { + return (IsSourceLocal(edge), IsTargetLocal(edge)) switch + { + (true, true) => new( + SourceId: edge.Source.Id, // 'local' vertex was the 'source' of the communication + TargetId: edge.Target.Id, + IsMigratable: edge.Source.IsMigratable, + PartnerSilo: Silo, // the partner was 'local' (note: this.Silo = Source.Silo = Target.Silo) + Direction: Direction.LocalToLocal, + Weight: weight), + (true, false) => new( + SourceId: edge.Source.Id, // 'local' vertex was the 'source' of the communication + TargetId: edge.Target.Id, + IsMigratable: edge.Source.IsMigratable, + PartnerSilo: edge.Target.Silo, // the partner was 'remote' + Direction: Direction.LocalToRemote, + Weight: weight), + (false, true) => new( + SourceId: edge.Target.Id, // 'local' vertex was the 'target' of the communication + TargetId: edge.Source.Id, + IsMigratable: edge.Target.IsMigratable, + PartnerSilo: edge.Source.Silo, // the partner was 'remote' + Direction: Direction.RemoteToLocal, + Weight: weight), + _ => default + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + bool IsSourceLocal(in Edge edge) => edge.Source.Silo.Equals(Silo); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + bool IsTargetLocal(in Edge edge) => edge.Target.Silo.Equals(Silo); + } + + public void Participate(ISiloLifecycle observer) + { + // Start when the silo becomes active. + observer.Subscribe( + nameof(ActivationRepartitioner), + ServiceLifecycleStage.Active, + OnActiveStart, + ct => Task.CompletedTask); + + // Stop when the silo stops application services. + observer.Subscribe( + nameof(ActivationRepartitioner), + ServiceLifecycleStage.ApplicationServices, + ct => Task.CompletedTask, + StopProcessingEdgesAsync); + } + + void IDisposable.Dispose() + { + base.Dispose(); + _enableMessageSampling = false; + _siloStatusOracle.UnSubscribeFromSiloStatusEvents(this); + _shutdownCts.Cancel(); + } + + void ISiloStatusListener.SiloStatusChangeNotification(SiloAddress updatedSilo, SiloStatus status) + { + _enableMessageSampling = _siloStatusOracle.GetActiveSilos().Length > 1; + } + + public ValueTask> GetGrainCallFrequencies() + { + var result = ImmutableArray.CreateBuilder<(Edge, ulong)>(_edgeWeights.Count); + foreach (var (edge, count, _) in _edgeWeights.Elements) + { + result.Add((edge, count)); + } + + return new(result.ToImmutable()); + } + + private enum Direction : byte + { + Unspecified, + LocalToLocal, + LocalToRemote, + RemoteToLocal + } + + /// + /// Represents a connection between 2 vertices. + /// + /// The id of the grain it represents. + /// The id of the connected vertex (the one the communication took place with). + /// Specifies if the vertex with is a migratable type. + /// The silo partner which interacted with the silo of vertex with . + /// The edge's direction + /// The number of estimated messages exchanged between and . + private readonly record struct VertexEdge(GrainId SourceId, GrainId TargetId, bool IsMigratable, SiloAddress PartnerSilo, Direction Direction, long Weight); +} diff --git a/src/Orleans.Runtime/Placement/Repartitioning/BlockedBloomFilter.cs b/src/Orleans.Runtime/Placement/Repartitioning/BlockedBloomFilter.cs new file mode 100644 index 0000000000..9eed1856ba --- /dev/null +++ b/src/Orleans.Runtime/Placement/Repartitioning/BlockedBloomFilter.cs @@ -0,0 +1,129 @@ +using System; +using System.Diagnostics; +using System.IO.Hashing; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Orleans.Runtime.Placement.Repartitioning; + +/// +/// A tuned version of a blocked bloom filter implementation. +/// +/// +/// This is a tuned version of BBF in order to meet the required FP rate. +/// Tuning takes a lot of time so this filter can accept FP rates in the rage of [0.1% - 1%] +/// Any value with the range, at any precision is supported as the FP rate is regressed via polynomial regression +/// More information can be read from Section 3: https://www.cs.amherst.edu/~ccmcgeoch/cs34/papers/cacheefficientbloomfilters-jea.pdf +/// +internal sealed class BlockedBloomFilter +{ + private const int BlockSize = 32; // higher value yields better speed, but at a high cost of space + private const double Ln2Squared = 0.4804530139182014246671025263266649717305529515945455; + private const double MinFpRate = 0.001; // 0.1% + private const double MaxFpRate = 0.01; // 1% + + private readonly int _blocks; + private readonly int[] _filter; + + // Regression coefficients (derived via polynomial regression) to match 'fpRate' as the actual deviates significantly with lower and lower 'fpRate' + // Eg, see https://gist.github.com/ledjon-behluli/d339cbd54568ceb5464d3a947ac8f08e + private static readonly double[] Coefficients = + [ + 4.0102253166524500e-003, + -1.6272682781603145e+001, + 2.7169897602930665e+004, + -2.4527698904812500e+007, + 1.3273846004698063e+010, + -4.4943809759769805e+012, + 9.5588839677303638e+014, + -1.2081452101930328e+017, + 6.8958853188430172e+018, + 2.6889929911921561e+020, + -7.1061179529975569e+022, + 4.4109449793357217e+024, + -9.8041203512310751e+025 + ]; + + /// The estimated population size. + /// Bounded within [ - ] + /// + public BlockedBloomFilter(int capacity, double fpRate) + { + if (fpRate is < MinFpRate or > MaxFpRate) + { + throw new ArgumentOutOfRangeException($"False positive rate '{fpRate}', is outside of the allowed range '{MinFpRate} - {MaxFpRate}'"); + } + + var adjFpRate = RegressFpRate(fpRate); + Debug.Assert(adjFpRate < fpRate); + var bits = (int)(-1 * capacity * Math.Log(adjFpRate) / Ln2Squared); + + _blocks = bits / BlockSize; + _filter = new int[_blocks + 1]; + } + + private static double RegressFpRate(double fpRate) + { + double temp = 1; + double result = 0; + + foreach (var coefficient in Coefficients) + { + result += coefficient * temp; + temp *= fpRate; + } + + return Math.Abs(result); + } + + public void Add(GrainId id) + { + var hash = XxHash3.HashToUInt64(id.Key.AsSpan(), id.GetUniformHashCode()); + var index = GetBlockIndex(hash, _blocks); // important to get index before rotating the hash + + hash ^= BitOperations.RotateLeft(hash, 32); + + // We use 2 masks to distribute the bits of the hash value across multiple positions in the filter + var mask1 = ComputeMask1(hash); + var mask2 = ComputeMask2(hash); + + // We set the bits across 2 blocks so that the bits from a single hash value, are spread out more evenly across the filter. + _filter[index] |= mask1; + _filter[index + 1] |= mask2; + } + + public bool Contains(GrainId id) + { + var hash = XxHash3.HashToUInt64(id.Key.AsSpan(), id.GetUniformHashCode()); + var index = GetBlockIndex(hash, _blocks); // important to get index before rotating the hash + + hash ^= BitOperations.RotateLeft(hash, 32); + + var block1 = _filter[index]; + var block2 = _filter[index + 1]; + + var mask1 = ComputeMask1(hash); + var mask2 = ComputeMask2(hash); + + return (mask1 & block1) == mask1 && (mask2 & block2) == mask2; + } + + public void Reset() => Array.Clear(_filter); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetBlockIndex(ulong hash, int buckets) => (int)(((int)hash & 0xffffffffL) * buckets >> 32); + + /// + /// Sets the bits of corresponding to the lower-order bits, and the bits shifted by 6 positions to the right + /// + /// The rotated hash + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ComputeMask1(ulong hash) => (1 << (int)hash) | (1 << ((int)hash >> 6)); + + /// + /// Sets the bits of , and the bits shifted by 12 and 18 positions to the right + /// + /// The rotated hash + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ComputeMask2(ulong hash) => (1 << ((int)hash >> 12)) | (1 << ((int)hash >> 18)); +} \ No newline at end of file diff --git a/src/Orleans.Runtime/Placement/Repartitioning/DefaultImbalanceRule.cs b/src/Orleans.Runtime/Placement/Repartitioning/DefaultImbalanceRule.cs new file mode 100644 index 0000000000..1ece66059f --- /dev/null +++ b/src/Orleans.Runtime/Placement/Repartitioning/DefaultImbalanceRule.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using System.Threading; +using Orleans.Placement.Repartitioning; + +namespace Orleans.Runtime.Placement.Repartitioning; + +/// +/// Tolerance rule which is aware of the cluster size. +/// +internal sealed class DefaultImbalanceRule(ISiloStatusOracle siloStatusOracle) : IImbalanceToleranceRule, + ILifecycleParticipant, ILifecycleObserver, ISiloStatusListener +{ + private const double Baseline = 10.1d; + private readonly object _lock = new(); + private readonly ConcurrentDictionary _silos = new(); + private readonly ISiloStatusOracle _siloStatusOracle = siloStatusOracle; + + private uint _allowedImbalance = 0; + + public bool IsSatisfiedBy(uint imbalance) => imbalance <= _allowedImbalance; + + public void SiloStatusChangeNotification(SiloAddress silo, SiloStatus status) + { + _ = _silos.AddOrUpdate(silo, static (_, arg) => arg, static (_, _, arg) => arg, status); + lock (_lock) + { + var activeSilos = _silos.Count(s => s.Value == SiloStatus.Active); + var percentageOfBaseline = 100d / (1 + Math.Exp(0.07d * activeSilos - 4.8d)); // inverted sigmoid + if (percentageOfBaseline < 10d) + { + percentageOfBaseline = 10d; + } + + // silos: 2 => tolerance: ~ 1000 + // silos: 100 => tolerance: ~ 100 + _allowedImbalance = (uint)Math.Round(Baseline * percentageOfBaseline, 0); + } + } + + public void Participate(ISiloLifecycle lifecycle) + => lifecycle.Subscribe(nameof(DefaultImbalanceRule), ServiceLifecycleStage.ApplicationServices, this); + + public Task OnStart(CancellationToken cancellationToken = default) + { + _siloStatusOracle.SubscribeToSiloStatusEvents(this); + return Task.CompletedTask; + } + public Task OnStop(CancellationToken cancellationToken = default) + { + _siloStatusOracle.UnSubscribeFromSiloStatusEvents(this); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Orleans.Runtime/Placement/Repartitioning/FrequentItemCollection.cs b/src/Orleans.Runtime/Placement/Repartitioning/FrequentItemCollection.cs new file mode 100644 index 0000000000..252a760fdc --- /dev/null +++ b/src/Orleans.Runtime/Placement/Repartitioning/FrequentItemCollection.cs @@ -0,0 +1,397 @@ +#nullable enable +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using Orleans.Placement.Repartitioning; + +namespace Orleans.Runtime.Placement.Repartitioning; + +internal sealed class FrequentEdgeCounter(int capacity) : FrequentItemCollection(capacity) +{ + protected override ulong GetKey(in Edge element) => (ulong)element.Source.Id.GetUniformHashCode() << 32 | element.Target.Id.GetUniformHashCode(); + public void Clear() => ClearCore(); + public void Remove(in Edge element) => RemoveCore(GetKey(element)); +} + +// This is Implementation of "Filtered Space-Saving" from "Finding top-k elements in data streams" +// by Nuno Homem & Joao Paulo Carvalho (https://www.hlt.inesc-id.pt/~fmmb/references/misnis.ref0a.pdf). +// In turn, this is a modification of the "Space-Saving" algorithm by Metwally, Agrawal, and Abbadi, +// Described in "Efficient Computation of Frequent and Top-k Elements in Data Streams" (https://www.cs.emory.edu/~cheung/Courses/584/Syllabus/papers/Frequency-count/2005-Metwally-Top-k-elements.pdf). +// This is implemented using an in-lined version of .NET's PriorityQueue which has been modified +// to support incrementing a value and with an index mapping key hashes to heap indexes. +internal abstract class FrequentItemCollection(int capacity) where TElement : notnull where TKey : notnull +{ + /// + /// Represents an implicit heap-ordered complete d-ary tree, stored as an array. + /// + private Counter[] _heap = []; + + /// + /// A dictionary that maps the hash of a key to its index in the heap. + /// + private readonly Dictionary _heapIndex = []; + + /// + /// The number of nodes in the heap. + /// + private int _heapSize; + + /// + /// Specifies the arity of the d-ary heap, which here is quaternary. + /// It is assumed that this value is a power of 2. + /// + private const int Arity = 4; + + /// + /// The binary logarithm of . + /// + private const int Log2Arity = 2; + + /// + /// Contains count estimates for keys that are not being tracked, indexed by the hash of the key. + /// Collisions are expected. + /// + private readonly uint[] _sketch = new uint[GetSketchSize(capacity)]; + + /// + /// Gets the number of elements contained in the . + /// + public int Count => _heapSize; + + /// + /// Gets the number of elements which the will track. + /// + public int Capacity { get; } = capacity; + +#if DEBUG + static FrequentItemCollection() + { + Debug.Assert(Log2Arity > 0 && Math.Pow(2, Log2Arity) == Arity); + } +#endif + + /// + /// Returns a collection of up to keys, along with their count estimates, in unspecified order. + /// + public ElementEnumerator Elements => new(this); + + protected abstract TKey GetKey(in TElement element); + + public void Add(in TElement element) + { + const int Increment = 1; + var nodeIndexHash = GetKey(element); + + // Increase count of a key that is already being tracked. + // There is a minute chance of a hash collision, which is deemed acceptable and ignored. + if (_heapIndex.TryGetValue(nodeIndexHash, out var index)) + { + ref var counter = ref _heap[index]; + counter.Count += Increment; + MoveUpHeap(counter, index, nodeIndexHash); + return; + } + + // Key is not being tracked, but can fit in the top K, so add it. + if (Count < Capacity) + { + InsertHeap(new Counter(element, Increment, error: 0), nodeIndexHash); + return; + } + + var min = _heap[0]; + + // Filter out values which are estimated to have appeared less frequently than the minimum. + var sketchMask = _sketch.Length - 1; + var sketchHash = nodeIndexHash.GetHashCode(); + var countEstimate = _sketch[sketchHash & sketchMask]; + if (countEstimate + Increment < min.Count) + { + // Increase the count estimate. + _sketch[sketchHash & sketchMask] += Increment; + return; + } + + // Remove the minimum element from the hash index. + var minIndexHash = GetKey(min.Element); + _heapIndex.Remove(minIndexHash); + + // While evicting the minimum element, update its counter in the sketch to improve the chance of it + // passing the filter in the future. + var minHash = minIndexHash.GetHashCode(); + _sketch[minHash & sketchMask] = min.Count; + + // Push the new element in place of the last and move it down until it's in position. + MoveDownHeap(new Counter(element, countEstimate + Increment, error: countEstimate), 0, nodeIndexHash); + } + + /// + /// Removes the counter corresponding to the specified hash. + /// + /// The key of the value to remove. + /// if matching entry was found and removed, otherwise. + protected bool RemoveCore(TKey key) + { + // Remove the element from the sketch + var sketchMask = _sketch.Length - 1; + var sketchHash = key.GetHashCode(); + _sketch[sketchHash & sketchMask] = 0; + + // Remove the element from the heap index + if (!_heapIndex.Remove(key, out var index)) + { + return false; + } + + // Remove the element from the heap + var nodes = _heap; + var newSize = --_heapSize; + if (index < newSize) + { + // We're removing an element from the middle of the heap. + // Pop the last element in the collection and sift downward from the removed index. + var lastNode = nodes[newSize]; + + MoveDownHeap(lastNode, index, GetKey(lastNode.Element)); + } + + nodes[newSize] = default; + + return true; + } + + protected void ClearCore() + { + Array.Clear(_heap, 0, _heapSize); + _heapIndex.Clear(); + Array.Clear(_sketch); + _heapSize = 0; + } + + private static int GetSketchSize(int capacity) + { + // Suggested constants in the paper "Finding top-k elements in data streams", chap 6. equation (24) + // Round to nearest power of 2 for cheaper binning without modulo + const int SketchEntriesPerHeapEntry = 6; + + return 1 << 32 - int.LeadingZeroCount(capacity * SketchEntriesPerHeapEntry); + } + + /// + /// Adds the specified element to the . + /// + /// The element to add. + private void InsertHeap(Counter element, TKey key) + { + // Virtually add the node at the end of the underlying array. + // Note that the node being enqueued does not need to be physically placed + // there at this point, as such an assignment would be redundant. + + var currentSize = _heapSize; + + if (_heap.Length == currentSize) + { + GrowHeap(currentSize + 1); + } + + _heapSize = currentSize + 1; + + MoveUpHeap(element, currentSize, key); + } + + /// + /// Grows the priority queue to match the specified min capacity. + /// + private void GrowHeap(int minCapacity) + { + Debug.Assert(_heap.Length < minCapacity); + + const int GrowFactor = 2; + const int MinimumGrow = 4; + + var newCapacity = GrowFactor * _heap.Length; + + // Allow the queue to grow to maximum possible capacity (~2G elements) before encountering overflow. + // Note that this check works even when _nodes.Length overflowed thanks to the (uint) cast + if ((uint)newCapacity > Array.MaxLength) newCapacity = Array.MaxLength; + + // Ensure minimum growth is respected. + newCapacity = Math.Max(newCapacity, _heap.Length + MinimumGrow); + + // If the computed capacity is still less than specified, set to the original argument. + // Capacities exceeding Array.MaxLength will be surfaced as OutOfMemoryException by Array.Resize. + if (newCapacity < minCapacity) newCapacity = minCapacity; + + Array.Resize(ref _heap, newCapacity); + } + + /// + /// Gets the index of an element's parent. + /// + private static int GetParentIndex(int index) => index - 1 >> Log2Arity; + + /// + /// Gets the index of the first child of an element. + /// + private static int GetFirstChildIndex(int index) => (index << Log2Arity) + 1; + + /// + /// Moves a node up in the tree to restore heap order. + /// + private void MoveUpHeap(Counter node, int nodeIndex, TKey nodeKey) + { + // Instead of swapping items all the way to the root, we will perform + // a similar optimization as in the insertion sort. + + Debug.Assert(0 <= nodeIndex && nodeIndex < _heapSize); + + var nodes = _heap; + var hashIndex = _heapIndex; + + while (nodeIndex > 0) + { + var parentIndex = GetParentIndex(nodeIndex); + var parent = nodes[parentIndex]; + + if (node.CompareTo(parent) < 0) + { + nodes[nodeIndex] = parent; + hashIndex[GetKey(parent.Element)] = nodeIndex; + nodeIndex = parentIndex; + } + else + { + break; + } + } + + nodes[nodeIndex] = node; + hashIndex[nodeKey] = nodeIndex; + } + + /// + /// Moves a node down in the tree to restore heap order. + /// + private void MoveDownHeap(Counter node, int nodeIndex, TKey nodeKey) + { + // The node to move down will not actually be swapped every time. + // Rather, values on the affected path will be moved up, thus leaving a free spot + // for this value to drop in. Similar optimization as in the insertion sort. + + Debug.Assert(0 <= nodeIndex && nodeIndex < _heapSize); + + var nodes = _heap; + var size = _heapSize; + var hashIndex = _heapIndex; + + int i; + while ((i = GetFirstChildIndex(nodeIndex)) < size) + { + // Find the child node with the minimal priority + var minChild = nodes[i]; + var minChildIndex = i; + + var childIndexUpperBound = Math.Min(i + Arity, size); + while (++i < childIndexUpperBound) + { + var nextChild = nodes[i]; + if (nextChild.CompareTo(minChild) < 0) + { + minChild = nextChild; + minChildIndex = i; + } + } + + // Heap property is satisfied; insert node in this location. + if (node.CompareTo(minChild) <= 0) + { + break; + } + + // Move the minimal child up by one node and continue recursively from its location. + nodes[nodeIndex] = minChild; + hashIndex[GetKey(minChild.Element)] = nodeIndex; + nodeIndex = minChildIndex; + } + + hashIndex[nodeKey] = nodeIndex; + nodes[nodeIndex] = node; + } + + private struct Counter(TElement element, uint count, uint error) : IComparable + { + public readonly TElement Element = element; + public uint Count = count; + public uint Error = error; + + public readonly int CompareTo(Counter other) => ((ulong)Count << 32 | uint.MaxValue - Error).CompareTo((ulong)other.Count << 32 | uint.MaxValue - other.Error); + + public override readonly string ToString() => $"{Element}: Count: {Count} Error: {Error}"; + } + + /// + /// Enumerates the element and priority pairs of a , + /// without any ordering guarantees. + /// + public struct ElementEnumerator : IEnumerator<(TElement Element, uint Count, uint Error)>, IEnumerable<(TElement Element, uint Count, uint Error)> + { + private readonly FrequentItemCollection _heap; + private int _index; + private Counter _current; + + internal ElementEnumerator(FrequentItemCollection heap) + { + _heap = heap; + _index = 0; + _current = default; + } + + /// + /// Releases all resources used by the . + /// + public readonly void Dispose() { } + + /// + /// Advances the enumerator to the next element of the heap. + /// + /// if the enumerator was successfully advanced to the next element; if the enumerator has passed the end of the collection. + public bool MoveNext() + { + var localHeap = _heap; + + if ((uint)_index < (uint)localHeap._heapSize) + { + _current = localHeap._heap[_index]; + _index++; + return true; + } + + return MoveNextRare(); + } + + private bool MoveNextRare() + { + _index = _heap._heapSize + 1; + _current = default; + return false; + } + + /// + /// Gets the element at the current position of the enumerator. + /// + public readonly (TElement Element, uint Count, uint Error) Current => (_current.Element, _current.Count, _current.Error); + + readonly object IEnumerator.Current => _current; + + void IEnumerator.Reset() + { + _index = 0; + _current = default; + } + + public readonly ElementEnumerator GetEnumerator() => this; + readonly IEnumerator<(TElement Element, uint Count, uint Error)> IEnumerable<(TElement Element, uint Count, uint Error)>.GetEnumerator() => this; + readonly IEnumerator IEnumerable.GetEnumerator() => this; + } +} diff --git a/src/Orleans.Runtime/Placement/Repartitioning/MaxHeap.cs b/src/Orleans.Runtime/Placement/Repartitioning/MaxHeap.cs new file mode 100644 index 0000000000..1f4fb58590 --- /dev/null +++ b/src/Orleans.Runtime/Placement/Repartitioning/MaxHeap.cs @@ -0,0 +1,395 @@ +#nullable enable +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Orleans.Placement.Repartitioning; + +namespace Orleans.Runtime.Placement.Repartitioning; + +internal enum VertexLocation +{ + Unknown, + Local, + Remote +} + +[DebuggerDisplay("{Vertex} @ {Location}")] +internal sealed class CandidateVertexHeapElement(CandidateVertex value) : IHeapElement +{ + public CandidateVertex Vertex { get; } = value; + public List<(CandidateVertexHeapElement Element, long TransferScore)> ConnectedVertices { get; } = []; + public GrainId Id => Vertex.Id; + public long AccumulatedTransferScore { get => Vertex.AccumulatedTransferScore; set => Vertex.AccumulatedTransferScore = value; } + public VertexLocation Location { get; set; } + int IHeapElement.HeapIndex { get; set; } = -1; + int IHeapElement.CompareTo(CandidateVertexHeapElement other) => AccumulatedTransferScore.CompareTo(other.AccumulatedTransferScore); +} + +internal interface IHeapElement where TElement : notnull +{ + int HeapIndex { get; set; } + int CompareTo(TElement other); +} + +/// +/// Represents a max heap. +/// +/// Specifies the type of elements in the heap. +/// +/// Implements an array-backed quaternary max-heap. +/// Elements with the lowest priority get removed first. +/// Note: this is based on .NET's PriorityQueue: https://github.com/dotnet/runtime/blob/e78b72b1fdf43d9678877400bcfe801b38c14681/src/libraries/System.Collections/src/System/Collections/Generic/PriorityQueue.cs +/// +[DebuggerDisplay("Count = {Count}")] +internal sealed class MaxHeap where TElement : notnull, IHeapElement +{ + /// + /// Represents an implicit heap-ordered complete d-ary tree, stored as an array. + /// + private readonly TElement?[] _nodes; + + /// + /// The number of nodes in the heap. + /// + private int _size; + + /// + /// Specifies the arity of the d-ary heap, which here is quaternary. + /// It is assumed that this value is a power of 2. + /// + private const int Arity = 4; + + /// + /// The binary logarithm of . + /// + private const int Log2Arity = 2; + +#if DEBUG + static MaxHeap() + { + Debug.Assert(Log2Arity > 0 && Math.Pow(2, Log2Arity) == Arity); + } +#endif + + /// + /// Initializes a new instance of the class + /// that is populated with the specified elements and priorities. + /// + /// The pairs of elements and priorities with which to populate the queue. + /// + /// The specified argument was . + /// + /// + /// Constructs the heap using a heapify operation, + /// which is generally faster than enqueuing individual elements sequentially. + /// + public MaxHeap(List items) + { + ArgumentNullException.ThrowIfNull(items); + + _size = items.Count; + var nodes = new TElement[_size]; + + var i = 0; + foreach (var item in items) + { + nodes[i] = item; + Debug.Assert(item.HeapIndex == -1); + item.HeapIndex = i; + i++; + } + + _nodes = nodes; + if (_size > 1) + { + Heapify(); + } + else if (_size == 1) + { + _nodes[0]!.HeapIndex = 0; + } + } + + /// + /// Gets the number of elements contained in the . + /// + public int Count => _size; + + public TElement? FirstOrDefault() => _size > 0 ? _nodes[0] : default; + + public bool TryPeek([NotNullWhen(true)] out TElement value) + { + if (_size > 0) + { + value = _nodes[0]!; + return true; + } + + value = default!; + return false; + } + + /// + /// Returns the maximal element from the without removing it. + /// + /// The is empty. + /// The maximal element of the . + public TElement Peek() + { + if (_size == 0) + { + throw new InvalidOperationException("Collection is empty."); + } + + return _nodes[0]!; + } + + public bool TryPop([NotNullWhen(true)] out TElement value) + { + if (_size > 0) + { + value = Pop(); + return true; + } + + value = default!; + return false; + } + + /// + /// Removes and returns the maximal element from the . + /// + /// The queue is empty. + /// The maximal element of the . + public TElement Pop() + { + if (_size == 0) + { + throw new InvalidOperationException("Collection is empty."); + } + + var element = _nodes[0]!; + RemoveRootNode(); + element.HeapIndex = -1; + return element; + + void RemoveRootNode() + { + var lastNodeIndex = --_size; + + if (lastNodeIndex > 0) + { + var lastNode = _nodes[lastNodeIndex]!; + MoveDown(lastNode, 0); + } + + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + _nodes[lastNodeIndex] = default!; + } + } + } + + /// + /// Gets the index of an element's parent. + /// + private static int GetParentIndex(int index) => (index - 1) >> Log2Arity; + + /// + /// Gets the index of the first child of an element. + /// + private static int GetFirstChildIndex(int index) => (index << Log2Arity) + 1; + + public void OnDecreaseElementPriority(TElement element) + { + // If the element has already been removed from the heap, this is a no-op. + if (element.HeapIndex < 0) + { + return; + } + + // The element's priority has decreased, so move it down as necessary to restore the heap property. + MoveDown(element, element.HeapIndex); + } + + public void OnIncreaseElementPriority(TElement element) + { + // If the element has already been removed from the heap, this is a no-op. + if (element.HeapIndex <= 0) + { + return; + } + + // The element's priority has increased, so move it down as necessary to restore the heap property. + MoveUp(element, element.HeapIndex); + } + + /// + /// Converts an unordered list into a heap. + /// + public void Heapify() + { + // Leaves of the tree are in fact 1-element heaps, for which there + // is no need to correct them. The heap property needs to be restored + // only for higher nodes, starting from the first node that has children. + // It is the parent of the very last element in the array. + + var nodes = _nodes; + var lastParentWithChildren = GetParentIndex(_size - 1); + for (var index = lastParentWithChildren; index >= 0; --index) + { + MoveDown(nodes[index]!, index); + } + } + + /// + /// Gets the elements in this collection with specified order. + /// + public UnorderedElementEnumerable UnorderedElements => new(this); + + /// + /// Moves a node up in the tree to restore heap order. + /// + private void MoveUp(TElement node, int nodeIndex) + { + Debug.Assert(0 <= nodeIndex && nodeIndex < _size); + + var nodes = _nodes; + + while (nodeIndex > 0) + { + var parentIndex = GetParentIndex(nodeIndex); + var parentNode = nodes[parentIndex]!; + + if (node.CompareTo(parentNode) <= 0) + { + // The parent is more larger than the current node. + break; + } + + nodes[nodeIndex] = parentNode; + parentNode.HeapIndex = nodeIndex; + nodeIndex = parentIndex; + } + + nodes[nodeIndex] = node; + node.HeapIndex = nodeIndex; + } + + /// + /// Moves a node down in the tree to restore heap order. + /// + private void MoveDown(TElement node, int nodeIndex) + { + // The node to move down will not actually be swapped every time. + // Rather, values on the affected path will be moved up, thus leaving a free spot + // for this value to drop in. Similar optimization as in the insertion sort. + + Debug.Assert(0 <= nodeIndex && nodeIndex < _size); + + var nodes = _nodes; + var size = _size; + + int i; + while ((i = GetFirstChildIndex(nodeIndex)) < size) + { + // Find the child node with the maximal priority + var maxChild = nodes[i]!; + var maxChildIndex = i; + + var childIndexUpperBound = Math.Min(i + Arity, size); + while (++i < childIndexUpperBound) + { + var nextChild = nodes[i]!; + if (nextChild.CompareTo(maxChild) > 0) + { + maxChild = nextChild; + maxChildIndex = i; + } + } + + // Heap property is satisfied; insert node in this location. + if (node.CompareTo(maxChild) >= 0) + { + break; + } + + // Move the maximal child up by one node and + // continue recursively from its location. + nodes[nodeIndex] = maxChild; + maxChild.HeapIndex = nodeIndex; + nodeIndex = maxChildIndex; + } + + nodes[nodeIndex] = node; + node.HeapIndex = nodeIndex; + } + + /// + /// Enumerates the element and priority pairs of a + /// without any ordering guarantees. + /// + public struct UnorderedElementEnumerable : IEnumerator, IEnumerable + { + private readonly MaxHeap _heap; + private int _index; + private TElement? _current; + + internal UnorderedElementEnumerable(MaxHeap heap) + { + _heap = heap; + _index = 0; + _current = default; + } + + /// + /// Releases all resources used by the . + /// + public readonly void Dispose() { } + + /// + /// Advances the enumerator to the next element of the heap. + /// + /// if the enumerator was successfully advanced to the next element; if the enumerator has passed the end of the collection. + public bool MoveNext() + { + var localHeap = _heap; + + if ((uint)_index < (uint)localHeap._size) + { + _current = localHeap._nodes[_index]; + _index++; + return true; + } + + return MoveNextRare(); + } + + private bool MoveNextRare() + { + _index = _heap._size + 1; + _current = default; + return false; + } + + /// + /// Gets the element at the current position of the enumerator. + /// + public readonly TElement Current => _current ?? throw new InvalidOperationException("Current element is not valid."); + + readonly object IEnumerator.Current => Current; + + public readonly UnorderedElementEnumerable GetEnumerator() => this; + readonly IEnumerator IEnumerable.GetEnumerator() => this; + void IEnumerator.Reset() + { + _index = 0; + _current = default; + } + + readonly IEnumerator IEnumerable.GetEnumerator() => this; + } +} diff --git a/src/Orleans.Runtime/Placement/Repartitioning/RepartitionerMessageFilter.cs b/src/Orleans.Runtime/Placement/Repartitioning/RepartitionerMessageFilter.cs new file mode 100644 index 0000000000..e5fbcb3d94 --- /dev/null +++ b/src/Orleans.Runtime/Placement/Repartitioning/RepartitionerMessageFilter.cs @@ -0,0 +1,98 @@ +#nullable enable +using Orleans; +using Orleans.Metadata; +using System; +using System.Collections.Concurrent; +using System.Collections.Frozen; +using System.Runtime.CompilerServices; + +namespace Orleans.Runtime.Placement.Repartitioning; + +internal interface IRepartitionerMessageFilter +{ + bool IsAcceptable(Message message, out bool isSenderMigratable, out bool isTargetMigratable); +} + +internal sealed class RepartitionerMessageFilter( + PlacementStrategyResolver strategyResolver, + IClusterManifestProvider clusterManifestProvider, + TimeProvider timeProvider) : IRepartitionerMessageFilter +{ + private readonly GrainManifest _localManifest = clusterManifestProvider.LocalGrainManifest; + private readonly PlacementStrategyResolver _strategyResolver = strategyResolver; + private readonly TimeProvider _timeProvider = timeProvider; + private readonly ConcurrentDictionary _migratableStatuses = new(); + private FrozenDictionary? _migratableStatusesCache; + private long _lastRegeneratedCacheTimestamp = timeProvider.GetTimestamp(); + + public bool IsAcceptable(Message message, out bool isSenderMigratable, out bool isTargetMigratable) + { + isSenderMigratable = false; + isTargetMigratable = false; + + // There are some edge cases when this can happen i.e. a grain invoking another one of its methods via AsReference<>, but we still exclude it + // as wherever this grain would be located in the cluster, it would always be a local call (since it targets itself), this would add negative transfer cost + // which would skew a potential relocation of this grain, while it shouldn't, because whenever this grain is located, it would still make local calls to itself. + if (message.SendingGrain == message.TargetGrain) + { + return false; + } + + isSenderMigratable = IsMigratable(message.SendingGrain.Type); + isTargetMigratable = IsMigratable(message.TargetGrain.Type); + + // If both are not migratable types we ignore this. But if one of them is not, then we allow passing, as we wish to move grains closer to them, as with any type of grain. + return isSenderMigratable || isTargetMigratable; + + bool IsMigratable(GrainType grainType) + { + var hash = grainType.GetUniformHashCode(); + if (_migratableStatusesCache is { } cache && cache.TryGetValue(hash, out var isMigratable)) + { + return isMigratable; + } + + return IsMigratableRare(grainType, hash); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + bool IsStatelessWorker(GrainType grainType) => + _strategyResolver.GetPlacementStrategy(grainType).GetType() == typeof(StatelessWorkerPlacement); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + bool IsImmovable(GrainType grainType) + { + if (_localManifest.Grains.TryGetValue(grainType, out var props)) + { + // If there is no 'Immovable' property, it is not immovable. + // If the value fails to parse, assume it's immovable. + // If the value is true, it's immovable. + return props.Properties.TryGetValue(WellKnownGrainTypeProperties.Immovable, out var value) && (!bool.TryParse(value, out var result) || result); + } + + // Assume unknown grains are immovable. + return true; + } + + bool IsMigratableRare(GrainType grainType, uint hash) + { + // _migratableStatuses holds statuses for each grain type if its migratable type or not, so we can make fast lookups. + // since we don't anticipate a huge number of grain *types*, i think its just fine to have this in place as fast-check. + if (!_migratableStatuses.TryGetValue(hash, out var isMigratable)) + { + isMigratable = !(grainType.IsClient() || grainType.IsSystemTarget() || grainType.IsGrainService() || IsStatelessWorker(grainType) || IsImmovable(grainType)); + _migratableStatuses.TryAdd(hash, isMigratable); + } + + // Regenerate the cache periodically. + var currentTimestamp = _timeProvider.GetTimestamp(); + if (_timeProvider.GetElapsedTime(_lastRegeneratedCacheTimestamp, currentTimestamp) > TimeSpan.FromSeconds(5)) + { + _migratableStatusesCache = _migratableStatuses.ToFrozenDictionary(); + _lastRegeneratedCacheTimestamp = currentTimestamp; + } + + return isMigratable; + } + } + } +} \ No newline at end of file diff --git a/src/Orleans.Runtime/Properties/AssemblyInfo.cs b/src/Orleans.Runtime/Properties/AssemblyInfo.cs index dc9bc9a6bd..4c2864c6a2 100644 --- a/src/Orleans.Runtime/Properties/AssemblyInfo.cs +++ b/src/Orleans.Runtime/Properties/AssemblyInfo.cs @@ -11,6 +11,7 @@ [assembly: InternalsVisibleTo("Tester.AdoNet")] [assembly: InternalsVisibleTo("TesterInternal")] [assembly: InternalsVisibleTo("TestInternalGrains")] +[assembly: InternalsVisibleTo("Benchmarks")] // Mocking libraries [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/src/Orleans.Runtime/Utilities/StripedMpscBuffer.cs b/src/Orleans.Runtime/Utilities/StripedMpscBuffer.cs new file mode 100644 index 0000000000..8b0eb9d95d --- /dev/null +++ b/src/Orleans.Runtime/Utilities/StripedMpscBuffer.cs @@ -0,0 +1,428 @@ +using System; +using System.Linq; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Orleans.Runtime.Utilities; + +/// +/// Provides a striped bounded buffer. Add operations use thread ID to index into +/// the underlying array of buffers, and if TryAdd is contended the thread ID is +/// rehashed to select a different buffer to retry up to 3 times. Using this approach +/// writes scale linearly with number of concurrent threads. +/// +/// +/// Note: this implementation was originally authored by Alex Peck and was copied from BitFaster.Caching: https://github.com/bitfaster/BitFaster.Caching/blob/275b9b072c0218e20f549b769cd183df1374e2ee/BitFaster.Caching/Buffers/StripedMpscBuffer.cs +/// +[DebuggerDisplay("Count = {Count}/{Capacity}")] +internal sealed class StripedMpscBuffer where T : class +{ + private const int MaxAttempts = 3; + + private readonly MpscBoundedBuffer[] _buffers; + + /// + /// Initializes a new instance of the StripedMpscBuffer class with the specified stripe count and buffer size. + /// + /// The stripe count. + /// The buffer size. + public StripedMpscBuffer(int stripeCount, int bufferSize) + { + _buffers = new MpscBoundedBuffer[stripeCount]; + + for (var i = 0; i < stripeCount; i++) + { + _buffers[i] = new MpscBoundedBuffer(bufferSize); + } + } + + /// + /// Gets the number of items contained in the buffer. + /// + public int Count => _buffers.Sum(b => b.Count); + + /// + /// The bounded capacity. + /// + public int Capacity => _buffers.Length * _buffers[0].Capacity; + + /// + /// Drains the buffer into the specified array. + /// + /// The output buffer + /// The number of items written to the output buffer. + /// + /// Thread safe for single try take/drain + multiple try add. + /// + public int DrainTo(T[] outputBuffer) => DrainTo(outputBuffer.AsSpan()); + + /// + /// Drains the buffer into the specified span. + /// + /// The output buffer + /// The number of items written to the output buffer. + /// + /// Thread safe for single try take/drain + multiple try add. + /// + public int DrainTo(Span outputBuffer) + { + var count = 0; + + for (var i = 0; i < _buffers.Length; i++) + { + if (count == outputBuffer.Length) + { + break; + } + + var segment = outputBuffer[count..]; + + count += _buffers[i].DrainTo(segment); + } + + return count; + } + + /// + /// Tries to add the specified item. + /// + /// The item to be added. + /// A BufferStatus value indicating whether the operation succeeded. + /// + /// Thread safe. + /// + public BufferStatus TryAdd(T item) + { + var z = BitOps.Mix64((ulong)Environment.CurrentManagedThreadId); + var inc = (int)(z >> 32) | 1; + var h = (int)z; + + var mask = _buffers.Length - 1; + + var result = BufferStatus.Empty; + + for (var i = 0; i < MaxAttempts; i++) + { + result = _buffers[h & mask].TryAdd(item); + + if (result == BufferStatus.Success) + { + break; + } + + h += inc; + } + + return result; + } + + /// + /// Removes all values from the buffer. + /// + /// + /// Not thread safe. + /// + public void Clear() + { + for (var i = 0; i < _buffers.Length; i++) + { + _buffers[i].Clear(); + } + } +} + +/// +/// Provides a multi-producer, single-consumer thread-safe ring buffer. When the buffer is full, +/// TryAdd fails and returns false. When the buffer is empty, TryTake fails and returns false. +/// +/// Based on the BoundedBuffer class in the Caffeine library by ben.manes@gmail.com (Ben Manes). +[DebuggerDisplay("Count = {Count}/{Capacity}")] +internal sealed class MpscBoundedBuffer where T : class +{ + private T[] _buffer; + private readonly int _mask; + private PaddedHeadAndTail _headAndTail; // mutable struct, don't mark readonly + + /// + /// Initializes a new instance of the MpscBoundedBuffer class with the specified bounded capacity. + /// + /// The bounded length. + /// + public MpscBoundedBuffer(int boundedLength) + { + ArgumentOutOfRangeException.ThrowIfLessThan(boundedLength, 0); + + // must be power of 2 to use & slotsMask instead of % + boundedLength = BitOps.CeilingPowerOfTwo(boundedLength); + + _buffer = new T[boundedLength]; + _mask = boundedLength - 1; + } + + /// + /// The bounded capacity. + /// + public int Capacity => _buffer.Length; + + /// + /// Gets the number of items contained in the buffer. + /// + public int Count + { + get + { + var spinner = new SpinWait(); + while (true) + { + var headNow = Volatile.Read(ref _headAndTail.Head); + var tailNow = Volatile.Read(ref _headAndTail.Tail); + + if (headNow == Volatile.Read(ref _headAndTail.Head) && + tailNow == Volatile.Read(ref _headAndTail.Tail)) + { + return GetCount(headNow, tailNow); + } + + spinner.SpinOnce(); + } + } + } + + private int GetCount(int head, int tail) + { + if (head != tail) + { + head &= _mask; + tail &= _mask; + + return head < tail ? tail - head : _buffer.Length - head + tail; + } + return 0; + } + + /// + /// Tries to add the specified item. + /// + /// The item to be added. + /// A BufferStatus value indicating whether the operation succeeded. + /// + /// Thread safe. + /// + public BufferStatus TryAdd(T item) + { + int head = Volatile.Read(ref _headAndTail.Head); + int tail = _headAndTail.Tail; + int size = tail - head; + + if (size >= _buffer.Length) + { + return BufferStatus.Full; + } + + if (Interlocked.CompareExchange(ref _headAndTail.Tail, tail + 1, tail) == tail) + { + int index = tail & _mask; + Volatile.Write(ref _buffer[index], item); + + return BufferStatus.Success; + } + + return BufferStatus.Contended; + } + + + /// + /// Tries to remove an item. + /// + /// The item to be removed. + /// A BufferStatus value indicating whether the operation succeeded. + /// + /// Thread safe for single try take/drain + multiple try add. + /// + public BufferStatus TryTake(out T item) + { + int head = Volatile.Read(ref _headAndTail.Head); + int tail = _headAndTail.Tail; + int size = tail - head; + + if (size == 0) + { + item = default; + return BufferStatus.Empty; + } + + int index = head & _mask; + + item = Volatile.Read(ref _buffer[index]); + + if (item == null) + { + // not published yet + return BufferStatus.Contended; + } + + _buffer[index] = null; + Volatile.Write(ref _headAndTail.Head, ++head); + return BufferStatus.Success; + } + + /// + /// Drains the buffer into the specified array segment. + /// + /// The output buffer + /// The number of items written to the output buffer. + /// + /// Thread safe for single try take/drain + multiple try add. + /// + public int DrainTo(ArraySegment output) => DrainTo(output.AsSpan()); + + /// + /// Drains the buffer into the specified span. + /// + /// The output buffer + /// The number of items written to the output buffer. + /// + /// Thread safe for single try take/drain + multiple try add. + /// + public int DrainTo(Span output) => DrainToImpl(output); + + // use an outer wrapper method to force the JIT to inline the inner adaptor methods + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int DrainToImpl(Span output) + { + int head = Volatile.Read(ref _headAndTail.Head); + int tail = _headAndTail.Tail; + int size = tail - head; + + if (size == 0) + { + return 0; + } + + var localBuffer = _buffer.AsSpan(); + + int outCount = 0; + + do + { + int index = head & _mask; + + T item = Volatile.Read(ref localBuffer[index]); + + if (item == null) + { + // not published yet + break; + } + + localBuffer[index] = null; + Write(output, outCount++, item); + head++; + } + while (head != tail && outCount < Length(output)); + + _headAndTail.Head = head; + + return outCount; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Write(Span output, int index, T item) => output[index] = item; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Length(Span output) => output.Length; + + /// + /// Removes all values from the buffer. + /// + /// + /// Not thread safe. + /// + public void Clear() + { + _buffer = new T[_buffer.Length]; + _headAndTail = new PaddedHeadAndTail(); + } +} + +/// +/// Specifies the status of buffer operations. +/// +internal enum BufferStatus +{ + /// + /// The buffer is full. + /// + Full, + + /// + /// The buffer is empty. + /// + Empty, + + /// + /// The buffer operation succeeded. + /// + Success, + + /// + /// The buffer operation was contended. + /// + Contended, +} + +/// +/// Provides utility methods for bit-twiddling operations. +/// +internal static class BitOps +{ + /// + /// Calculate the smallest power of 2 greater than the input parameter. + /// + /// The input parameter. + /// Smallest power of two greater than or equal to x. + public static int CeilingPowerOfTwo(int x) => (int)CeilingPowerOfTwo((uint)x); + + /// + /// Calculate the smallest power of 2 greater than the input parameter. + /// + /// The input parameter. + /// Smallest power of two greater than or equal to x. + public static uint CeilingPowerOfTwo(uint x) => 1u << -BitOperations.LeadingZeroCount(x - 1); + + /// + /// Computes Stafford variant 13 of 64-bit mix function. + /// + /// The input parameter. + /// A bit mix of the input parameter. + /// + /// See http://zimbry.blogspot.com/2011/09/better-bit-mixing-improving-on.html + /// + public static ulong Mix64(ulong z) + { + z = (z ^ z >> 30) * 0xbf58476d1ce4e5b9L; + z = (z ^ z >> 27) * 0x94d049bb133111ebL; + return z ^ z >> 31; + } +} + +[DebuggerDisplay("Head = {Head}, Tail = {Tail}")] +[StructLayout(LayoutKind.Explicit, Size = 3 * Padding.CACHE_LINE_SIZE)] // padding before/between/after fields +internal struct PaddedHeadAndTail +{ + [FieldOffset(1 * Padding.CACHE_LINE_SIZE)] public int Head; + [FieldOffset(2 * Padding.CACHE_LINE_SIZE)] public int Tail; +} + +internal class Padding +{ +#if TARGET_ARM64 || TARGET_LOONGARCH64 + internal const int CACHE_LINE_SIZE = 128; +#else + internal const int CACHE_LINE_SIZE = 64; +#endif +} + diff --git a/test/Benchmarks/Ping/FanoutBenchmark.cs b/test/Benchmarks/Ping/FanoutBenchmark.cs new file mode 100644 index 0000000000..9a5365f4c3 --- /dev/null +++ b/test/Benchmarks/Ping/FanoutBenchmark.cs @@ -0,0 +1,152 @@ +using System.Net; +using BenchmarkDotNet.Attributes; +using BenchmarkGrainInterfaces.Ping; +using BenchmarkGrains.Ping; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; +using Orleans.Configuration; + +namespace Benchmarks.Ping +{ + [MemoryDiagnoser] + public class FanoutBenchmark : IDisposable + { + private readonly ConsoleCancelEventHandler _onCancelEvent; + private readonly List hosts = new(); + private readonly ITreeGrain grain; + private readonly IClusterClient client; + private readonly IHost clientHost; + + public FanoutBenchmark() : this(2, true) { } + + public FanoutBenchmark(int numSilos, bool startClient, bool grainsOnSecondariesOnly = false) + { + for (var i = 0; i < numSilos; ++i) + { + var primary = i == 0 ? null : new IPEndPoint(IPAddress.Loopback, 11111); + var hostBuilder = new HostBuilder().UseOrleans((ctx, siloBuilder) => + { +#pragma warning disable ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + siloBuilder.AddActivationRepartitioner(); +#pragma warning restore ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + siloBuilder.ConfigureLogging(l => + { + l.AddSimpleConsole(o => + { + o.UseUtcTimestamp = true; + o.TimestampFormat = "HH:mm:ss "; + o.ColorBehavior = LoggerColorBehavior.Enabled; + }); + l.AddFilter("Orleans.Runtime.Placement.Repartitioning", LogLevel.Debug); + }); + siloBuilder.Configure(o => + { + }); + siloBuilder.UseLocalhostClustering( + siloPort: 11111 + i, + gatewayPort: 30000 + i, + primarySiloEndpoint: primary); + + if (i == 0 && grainsOnSecondariesOnly) + { + siloBuilder.Configure(options => options.Classes.Remove(typeof(PingGrain))); + } + }); + + var host = hostBuilder.Build(); + + host.StartAsync().GetAwaiter().GetResult(); + this.hosts.Add(host); + } + + if (grainsOnSecondariesOnly) Thread.Sleep(4000); + + if (startClient) + { + var hostBuilder = new HostBuilder().UseOrleansClient((ctx, clientBuilder) => + { + if (numSilos == 1) + { + clientBuilder.UseLocalhostClustering(); + } + else + { + var gateways = Enumerable.Range(30000, numSilos).Select(i => new IPEndPoint(IPAddress.Loopback, i)).ToArray(); + clientBuilder.UseStaticClustering(gateways); + } + }); + + this.clientHost = hostBuilder.Build(); + this.clientHost.StartAsync().GetAwaiter().GetResult(); + + this.client = this.clientHost.Services.GetRequiredService(); + var grainFactory = this.client; + + this.grain = grainFactory.GetGrain(0, keyExtension: "0"); + this.grain.Ping().AsTask().GetAwaiter().GetResult(); + } + + _onCancelEvent = CancelPressed; + Console.CancelKeyPress += _onCancelEvent; + AppDomain.CurrentDomain.FirstChanceException += (object sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e) => Console.WriteLine("FIRST CHANCE EXCEPTION: " + LogFormatter.PrintException(e.Exception)); + AppDomain.CurrentDomain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) => Console.WriteLine("UNHANDLED EXCEPTION: " + LogFormatter.PrintException((Exception)e.ExceptionObject)); + } + + private void CancelPressed(object sender, ConsoleCancelEventArgs e) + { + Environment.Exit(0); + } + + [Benchmark] + public ValueTask Ping() => grain.Ping(); + + public async Task PingForever() + { + while (true) + { + await grain.Ping(); + } + } + + public async Task Shutdown() + { + if (clientHost is { } client) + { + await client.StopAsync(); + if (client is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + else + { + client.Dispose(); + } + } + + this.hosts.Reverse(); + foreach (var host in this.hosts) + { + await host.StopAsync(); + if (host is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + else + { + host.Dispose(); + } + } + } + + [GlobalCleanup] + public void Dispose() + { + (this.client as IDisposable)?.Dispose(); + this.hosts.ForEach(h => h.Dispose()); + + Console.CancelKeyPress -= _onCancelEvent; + } + } +} diff --git a/test/Benchmarks/Ping/PingBenchmark.cs b/test/Benchmarks/Ping/PingBenchmark.cs index 34ffc24309..83ce28af22 100644 --- a/test/Benchmarks/Ping/PingBenchmark.cs +++ b/test/Benchmarks/Ping/PingBenchmark.cs @@ -4,6 +4,7 @@ using BenchmarkGrains.Ping; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Orleans.Configuration; namespace Benchmarks.Ping @@ -26,6 +27,17 @@ public PingBenchmark(int numSilos, bool startClient, bool grainsOnSecondariesOnl var primary = i == 0 ? null : new IPEndPoint(IPAddress.Loopback, 11111); var hostBuilder = new HostBuilder().UseOrleans((ctx, siloBuilder) => { +#pragma warning disable ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + siloBuilder.AddActivationRepartitioner(); +#pragma warning restore ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + siloBuilder.ConfigureLogging(l => + { + l.AddConsole(); + l.AddFilter("Orleans.Runtime.Placement.Repartitioning", LogLevel.Debug); + }); + siloBuilder.Configure(o => + { + }); siloBuilder.UseLocalhostClustering( siloPort: 11111 + i, gatewayPort: 30000 + i, diff --git a/test/Benchmarks/Program.cs b/test/Benchmarks/Program.cs index f47c927a5d..d553934471 100644 --- a/test/Benchmarks/Program.cs +++ b/test/Benchmarks/Program.cs @@ -139,6 +139,12 @@ internal class Program { new PingBenchmark(numSilos: 2, startClient: true).PingConcurrent().GetAwaiter().GetResult(); }, + ["ConcurrentPing_TwoSilos_Forever"] = _ => + { + Console.WriteLine("## Client to 2 Silos ##"); + var test = new PingBenchmark(numSilos: 2, startClient: true); + test.PingConcurrentForever().GetAwaiter().GetResult(); + }, ["ConcurrentPing_HostedClient"] = _ => { new PingBenchmark(numSilos: 1, startClient: false).PingConcurrentHostedClient().GetAwaiter().GetResult(); @@ -205,6 +211,10 @@ internal class Program ThreadPool.SetMaxThreads(1, 1); new PingBenchmark().PingForever().GetAwaiter().GetResult(); }, + ["FanoutForever"] = _ => + { + new FanoutBenchmark().PingForever().GetAwaiter().GetResult(); + }, ["GrainStorage.Memory"] = _ => { RunBenchmark( diff --git a/test/Benchmarks/Properties/launchSettings.json b/test/Benchmarks/Properties/launchSettings.json index 82bc182482..58217d2419 100644 --- a/test/Benchmarks/Properties/launchSettings.json +++ b/test/Benchmarks/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Benchmarks": { "commandName": "Project", - "commandLineArgs": "ConcurrentPing_SiloToSilo_Forever" + "commandLineArgs": "FanoutForever" } } } \ No newline at end of file diff --git a/test/Benchmarks/TopK/BloomFilterBenchmark.cs b/test/Benchmarks/TopK/BloomFilterBenchmark.cs new file mode 100644 index 0000000000..66d08ca5eb --- /dev/null +++ b/test/Benchmarks/TopK/BloomFilterBenchmark.cs @@ -0,0 +1,327 @@ +using System.Collections; +using System.Diagnostics; +using System.IO.Hashing; +using System.Numerics; +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Reports; +using Benchmarks.Utilities; +using Orleans.Runtime.Placement.Repartitioning; + +namespace Benchmarks.TopK; + +[MemoryDiagnoser] +[FalsePositiveRateColumn] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory), CategoriesColumn] +public class BloomFilterBenchmark +{ + private BloomFilter _bloomFilter; + private BloomFilter _bloomFilterWithSamples; + private OriginalBloomFilter _originalBloomFilter; + private OriginalBloomFilter _originalBloomFilterWithSamples; + private BlockedBloomFilter _blockedBloomFilter; + private BlockedBloomFilter _blockedBloomFilterWithSamples; + private GrainId[] _population; + private HashSet _set; + private ZipfRejectionSampler _sampler; + private GrainId[] _samples; + + [Params(1_000_000, Priority = 4)] + public int Pop { get; set; } + + [Params(/*0.2, 0.4, 0.6, 0.8, */1.02 /*, 1.2, 1.4, 1.6*/, Priority = 3)] + public double Skew { get; set; } + + [Params(1_000_000, Priority = 1)] + public int Cap { get; set; } + + [Params(0.01, 0.001, Priority = 2)] + public double FP { get; set; } + + [Params(10_000, Priority = 5)] + public int Samples { get; set; } + + [GlobalSetup] + public void GlobalSetup() + { + _population = new GrainId[Pop]; + _sampler = new(new Random(42), Pop, Skew); + for (var i = 0; i < Pop; i++) + { + _population[i] = GrainId.Create($"grain_{i}", i.ToString()); + } + + _bloomFilter = new(Cap, FP); + _bloomFilterWithSamples = new(Cap, FP); + _originalBloomFilter = new(); + _originalBloomFilterWithSamples = new(); + _blockedBloomFilter = new(Cap, FP); + _blockedBloomFilterWithSamples = new(Cap, FP); + + _samples = new GrainId[Samples]; + _set = new(Samples); + for (var i = 0; i < Samples; i++) + { + //var sample = _sampler.Sample(); + var value = _population[i]; + _samples[i] = value; + _set.Add(value); + _bloomFilterWithSamples.Add(value); + _originalBloomFilterWithSamples.Add(value); + } + } + + [Benchmark] + [BenchmarkCategory("Add")] + public void BloomFilter_Add() + { + foreach (var sample in _samples) + { + _bloomFilter.Add(sample); + } + } + + [Benchmark] + [BenchmarkCategory("Contains")] + public void BloomFilter_Contains() + { + foreach (var sample in _samples) + { + _bloomFilterWithSamples.Contains(sample); + } + } + + [Benchmark] + [BenchmarkCategory("FP rate")] + public int BloomFilter_FPR() + { + var correct = 0; + var incorrect = 0; + foreach (var sample in _population) + { + if (!_bloomFilterWithSamples.Contains(sample) == _set.Contains(sample)) + { + correct++; + } + else + { + incorrect++; + } + } + + return incorrect; + } + + + [Benchmark(Baseline = true)] + [BenchmarkCategory("Add")] + public void OriginalBloomFilter_Add() + { + foreach (var sample in _samples) + { + _originalBloomFilter.Add(sample); + } + } + + [Benchmark(Baseline = true)] + [BenchmarkCategory("Contains")] + public void OriginalBloomFilter_Contains() + { + foreach (var sample in _samples) + { + _originalBloomFilterWithSamples.Contains(sample); + } + } + + /* + [Benchmark(Baseline = true)] + [BenchmarkCategory("FP rate")] + public int OriginalBloomFilter_FPR() + { + var correct = 0; + var incorrect = 0; + foreach (var sample in _population) + { + if (!_originalBloomFilterWithSamples.Contains(sample) == _set.Contains(sample)) + { + correct++; + } + else + { + incorrect++; + } + } + + return incorrect; + } + */ + + [Benchmark] + [BenchmarkCategory("Add")] + public void BlockedBloomFilter_Add() + { + foreach (var sample in _samples) + { + _blockedBloomFilter.Add(sample); + } + } + + [Benchmark] + [BenchmarkCategory("Contains")] + public void BlockedBloomFilter_Contains() + { + foreach (var sample in _samples) + { + _blockedBloomFilterWithSamples.Contains(sample); + } + } + + // This is expected to yield a slighly higher FP rate, due to tuning + [Benchmark] + [BenchmarkCategory("FP rate")] + public int BlockedBloomFilter_FPR() + { + var correct = 0; + var incorrect = 0; + foreach (var sample in _population) + { + if (!_blockedBloomFilterWithSamples.Contains(sample) == _set.Contains(sample)) + { + correct++; + } + else + { + incorrect++; + } + } + + return incorrect; + } +} + +[AttributeUsage(AttributeTargets.Class)] +public class FalsePositiveRateColumnAttribute : Attribute, IConfigSource +{ + public FalsePositiveRateColumnAttribute(string columnName = "FP %") + { + var config = ManualConfig.CreateEmpty(); + config.AddColumn( + new MethodResultColumn(columnName, + val => + { + return $"{val}"; + })); + Config = config; + } + + public IConfig Config { get; } +} +public class OriginalBloomFilter +{ + private const int bitArraySize = 1_198_132; // formula 8 * n / ln(2) -> for 1000 elements, 0.01% + private readonly int[] hashFuncSeeds = Enumerable.Range(0, 6).Select(p => (int)unchecked(p * 0xFBA4C795 + 1)).ToArray(); + private readonly BitArray filterBits = new(bitArraySize); + + public void Add(GrainId id) + { + foreach (int s in hashFuncSeeds) + { + uint i = XxHash32.HashToUInt32(id.Key.AsSpan(), s); + filterBits.Set((int)(i % (uint)filterBits.Length), true); + } + } + + public bool Contains(GrainId id) + { + foreach (int s in hashFuncSeeds) + { + uint i = XxHash32.HashToUInt32(id.Key.AsSpan(), s); + if (!filterBits.Get((int)(i % (uint)filterBits.Length))) + { + return false; + } + } + return true; + } +} + +internal sealed class BloomFilter +{ + private const double Ln2Squared = 0.4804530139182014246671025263266649717305529515945455; + private const double Ln2 = 0.6931471805599453094172321214581765680755001343602552; + private readonly ulong[] _hashFuncSeeds; + private readonly int[] _filter; + private readonly int _indexMask; + + public BloomFilter(int capacity, double falsePositiveRate) + { + // Calculate the ideal bloom filter size and hash code count for the given (estimated) capacity and desired false positive rate. + // See https://en.wikipedia.org/wiki/Bloom_filter. + var minBitCount = (int)(-1 / Ln2Squared * capacity * Math.Log(falsePositiveRate)) / 8; + var arraySize = (int)CeilingPowerOfTwo((uint)(minBitCount - 1 + (1 << 5)) >> 5); + _indexMask = arraySize - 1; + _filter = new int[arraySize]; + + // Divide the hash count by 2 since we are using 64-bit hash codes split into two 32-bit hash codes. + var hashFuncCount = (int)Math.Min(minBitCount * 8 / capacity * Ln2 / 2, 8); + Debug.Assert(hashFuncCount > 0); + _hashFuncSeeds = Enumerable.Range(0, hashFuncCount).Select(p => unchecked((ulong)p * 0xFBA4C795FBA4C795 + 1)).ToArray(); + Debug.Assert(_hashFuncSeeds.Length == hashFuncCount); + } + + public void Add(GrainId id) + { + var hash = XxHash3.HashToUInt64(id.Key.AsSpan(), id.GetUniformHashCode()); + foreach (var seed in _hashFuncSeeds) + { + hash = Mix64(hash ^ seed); + Set((int)hash); + Set((int)(hash >> 32)); + } + } + + public bool Contains(GrainId id) + { + var hash = XxHash3.HashToUInt64(id.Key.AsSpan(), id.GetUniformHashCode()); + foreach (var seed in _hashFuncSeeds) + { + hash = Mix64(hash ^ seed); + var clear = IsClear((int)hash); + clear |= IsClear((int)(hash >> 32)); + if (clear) + { + return false; + } + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsClear(int index) => (_filter[(index >> 5) & _indexMask] & (1 << index)) == 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(int index) => _filter[(index >> 5) & _indexMask] |= 1 << index; + + /// + /// Computes Stafford variant 13 of 64-bit mix function. + /// + /// The input parameter. + /// A bit mix of the input parameter. + /// + /// See http://zimbry.blogspot.com/2011/09/better-bit-mixing-improving-on.html + /// + public static ulong Mix64(ulong z) + { + z = (z ^ z >> 30) * 0xbf58476d1ce4e5b9L; + z = (z ^ z >> 27) * 0x94d049bb133111ebL; + return z ^ z >> 31; + } + + public void Reset() => Array.Clear(_filter); + + private static uint CeilingPowerOfTwo(uint x) => 1u << -BitOperations.LeadingZeroCount(x - 1); +} + diff --git a/test/Benchmarks/TopK/TopKBenchmark.cs b/test/Benchmarks/TopK/TopKBenchmark.cs new file mode 100644 index 0000000000..f2b40cfa37 --- /dev/null +++ b/test/Benchmarks/TopK/TopKBenchmark.cs @@ -0,0 +1,389 @@ +using System.Diagnostics; +using System.Net; +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes; +using Orleans.Placement.Repartitioning; +using Orleans.Runtime.Placement.Repartitioning; + +namespace Benchmarks.TopK; + +[MemoryDiagnoser] +public class TopKBenchmark +{ + private ZipfRejectionSampler _sampler; + private ulong[] ULongSamples; + private Edge[] EdgeSamples; + private EdgeClass[] EdgeClassSamples; + private UlongFrequentItemCollection _fss; + private EdgeClassFrequentItemCollection _fssClass; + private EdgeFrequentItemCollection _fssEdge; + private FrequencySink _sink; + + [Params(100_000, Priority = 3)] + public int Pop { get; set; } + + [Params(0.2, 0.4, 0.6, 0.8, 1.02, 1.2, 1.4, 1.6, Priority = 2)] + public double Skew { get; set; } + + [Params(10_000, Priority = 1)] + public int Cap { get; set; } + + [Params(1_000_000, Priority = 4)] + public int Samples { get; set; } + + [GlobalSetup] + public void GlobalSetup() + { + _sampler = new(new Random(42), Pop, Skew); + + var silos = new SiloAddress[100]; + for (var i = 0; i < silos.Length; i++) + { + silos[i] = SiloAddress.New(new IPEndPoint(IPAddress.Loopback, i), i); + } + + var grains = new GrainId[Pop]; + for (var i = 0; i < Pop; i++) + { + grains[i] = GrainId.Create("grain", i.ToString()); + } + + var grainEdges = new Edge[Pop]; + for (var i = 0; i < Pop; i++) + { + grainEdges[i] = new Edge(new(grains[i % grains.Length], silos[i % silos.Length], true), new(grains[(i + 1) % grains.Length], silos[(i + 1) % silos.Length], true)); + } + + var grainEdgeClasses = new EdgeClass[Pop]; + for (var i = 0; i < Pop; i++) + { + grainEdgeClasses[i] = new(grainEdges[i]); + } + + ULongSamples = new ulong[Samples]; + EdgeSamples = new Edge[Samples]; + EdgeClassSamples = new EdgeClass[Samples]; + for (var i = 0; i < Samples; i++) + { + var sample = _sampler.Sample(); + ULongSamples[i] = (ulong)sample; + EdgeSamples[i] = grainEdges[sample % grainEdges.Length]; + EdgeClassSamples[i] = grainEdgeClasses[sample % grainEdgeClasses.Length]; + } + + _fss = new UlongFrequentItemCollection(Cap); + _fssClass = new EdgeClassFrequentItemCollection(Cap); + _fssEdge = new EdgeFrequentItemCollection(Cap); + _sink = new FrequencySink(Cap); + } + + internal sealed record class EdgeClass(Edge Edge); + + [IterationSetup] + public void IterationSetup() + { + /* + _fss.Clear(); + _fssEdge.Clear(); + _fssClass.Clear(); + */ + //_sink = new FrequencySink(Cap); + } + + /* + [Benchmark] + [BenchmarkCategory("Add")] + public void FssULongAdd() + { + foreach (var sample in ULongSamples) + { + _fss.Add(sample); + } + } + */ + + /* + [Benchmark] + [BenchmarkCategory("Add")] + public void FssClassAdd() + { + foreach (var sample in EdgeClassSamples) + { + _fssClass.Add(sample); + } + } + */ + + [Benchmark] + [BenchmarkCategory("FSS")] + public void FssAdd() + { + foreach (var sample in EdgeSamples) + { + _fssEdge.Add(sample); + } + } + + [Benchmark] + [BenchmarkCategory("SS")] + public void SinkAdd() + { + foreach (var sample in EdgeSamples) + { + _sink.Add(sample); + } + } + + private sealed class EdgeFrequentItemCollection(int capacity) : FrequentItemCollection(capacity) + { + protected override ulong GetKey(in Edge element) => (ulong)element.Source.Id.GetUniformHashCode() << 32 | element.Target.Id.GetUniformHashCode(); + public void Clear() => ClearCore(); + } + + private sealed class EdgeClassFrequentItemCollection(int capacity) : FrequentItemCollection(capacity) + { + static ulong GetKey(in Edge element) => (ulong)element.Source.Id.GetUniformHashCode() << 32 | element.Target.Id.GetUniformHashCode(); + protected override ulong GetKey(in EdgeClass element) => GetKey(element.Edge); + public void Clear() => ClearCore(); + } + + private sealed class UlongFrequentItemCollection(int capacity) : FrequentItemCollection(capacity) + { + protected override ulong GetKey(in ulong element) => element; + public void Remove(in ulong element) => Remove(GetKey(element)); + public void Clear() => ClearCore(); + } + + internal class EdgeCounter(ulong value, Edge edge) + { + public ulong Value { get; set; } = value; + public Edge Edge { get; } = edge; + + } + + /// + /// Implementation of the Space-Saving algorithm: https://www.cse.ust.hk/~raywong/comp5331/References/EfficientComputationOfFrequentAndTop-kElementsInDataStreams.pdf + /// + internal sealed class FrequencySink(int capacity) + { + public ulong GetKey(in Edge element) => (ulong)element.Source.Id.GetUniformHashCode() << 32 | element.Target.Id.GetUniformHashCode(); + private readonly Dictionary _counters = new(capacity); + private readonly UpdateableMinHeap _heap = new(capacity); + + public int Capacity { get; } = capacity; + public Dictionary.ValueCollection Counters => _counters.Values; + + public void Add(Edge edge) + { + var combinedHash = GetKey(edge); + if (_counters.TryGetValue(combinedHash, out var counter)) + { + counter.Value++; + _heap.Update(combinedHash, counter.Value); + + return; + } + + if (_counters.Count == Capacity) + { + var minHash = _heap.Dequeue(); + _counters.Remove(minHash); + } + + _counters.Add(combinedHash, new EdgeCounter(1, edge)); + _heap.Enqueue(combinedHash, _counters[combinedHash].Value); + } + + public void Remove(uint sourceHash, uint targetHash) + { + var combinedHash = CombineHashes(sourceHash, targetHash); + var reversedHash = CombineHashes(targetHash, sourceHash); + + if (_counters.Remove(combinedHash)) + { + _ = _heap.Remove(combinedHash); + } + + if (_counters.Remove(reversedHash)) + { + _ = _heap.Remove(reversedHash); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong CombineHashes(uint sourceHash, uint targetHash) + => (ulong)sourceHash << 32 | targetHash; + + // Inspired by: https://github.com/DesignEngrLab/TVGL/blob/master/TessellationAndVoxelizationGeometryLibrary/Miscellaneous%20Functions/UpdatablePriorityQueue.cs + private class UpdateableMinHeap(int capacity) + { + private const int Arity = 4; + private const int Log2Arity = 2; + + private readonly Dictionary _hashIndexes = new(capacity); + private readonly (ulong Hash, ulong Value)[] _nodes = new (ulong, ulong)[capacity]; + + private int _size; + + public void Enqueue(ulong hash, ulong value) + { + var currentSize = _size; + _size = currentSize + 1; + + MoveNodeUp((hash, value), currentSize); + } + + public ulong Dequeue() + { + var hash = _nodes[0].Hash; + _hashIndexes.Remove(hash); + + var lastNodeIndex = --_size; + if (lastNodeIndex > 0) + { + var lastNode = _nodes[lastNodeIndex]; + MoveNodeDown(lastNode, 0); + } + + return hash; + } + + public bool Remove(ulong hash) + { + if (!_hashIndexes.TryGetValue(hash, out var index)) + { + return false; + } + + var nodes = _nodes; + var newSize = --_size; + + if (index < newSize) + { + var lastNode = nodes[newSize]; + MoveNodeDown(lastNode, index); + } + + _hashIndexes.Remove(hash); + nodes[newSize] = default; + + return true; + } + + public void Update(ulong hash, ulong newValue) + { + Remove(hash); + Enqueue(hash, newValue); + } + + private void MoveNodeUp((ulong Hash, ulong Value) node, int nodeIndex) + { + Debug.Assert(0 <= nodeIndex && nodeIndex < _size); + + var nodes = _nodes; + + while (nodeIndex > 0) + { + var parentIndex = GetParentIndex(nodeIndex); + var parent = nodes[parentIndex]; + + if (Comparer.Default.Compare(node.Value, parent.Value) < 0) + { + nodes[nodeIndex] = parent; + _hashIndexes[parent.Hash] = nodeIndex; + nodeIndex = parentIndex; + } + else + { + break; + } + } + + _hashIndexes[node.Hash] = nodeIndex; + nodes[nodeIndex] = node; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static int GetParentIndex(int index) => (index - 1) >> Log2Arity; + } + + private void MoveNodeDown((ulong Hash, ulong Value) node, int nodeIndex) + { + Debug.Assert(0 <= nodeIndex && nodeIndex < _size); + + var nodes = _nodes; + var size = _size; + + int i; + while ((i = GetFirstChildIndex(nodeIndex)) < size) + { + var minChild = nodes[i]; + var minChildIndex = i; + + var childIndexUpperBound = Math.Min(i + Arity, size); + while (++i < childIndexUpperBound) + { + var nextChild = nodes[i]; + if (nextChild.Value < minChild.Value) + { + minChild = nextChild; + minChildIndex = i; + } + } + + if (node.Value <= minChild.Value) + { + break; + } + + nodes[nodeIndex] = minChild; + _hashIndexes[minChild.Hash] = nodeIndex; + nodeIndex = minChildIndex; + } + + _hashIndexes[node.Hash] = nodeIndex; + nodes[nodeIndex] = node; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static int GetFirstChildIndex(int index) => (index << Log2Arity) + 1; + } + } + } +} + + // https://jasoncrease.medium.com/rejection-sampling-the-zipf-distribution-6b359792cffa + internal sealed class ZipfRejectionSampler + { + private readonly Random _rand; + private readonly double _skew; + private readonly double _t; + + public ZipfRejectionSampler(Random random, long cardinality, double skew) + { + _rand = random; + _skew = skew; + _t = (Math.Pow(cardinality, 1 - skew) - skew) / (1 - skew); + } + + public long Sample() + { + while (true) + { + double invB = bInvCdf(_rand.NextDouble()); + long sampleX = (long)(invB + 1); + double yRand = _rand.NextDouble(); + double ratioTop = Math.Pow(sampleX, -_skew); + double ratioBottom = sampleX <= 1 ? 1 / _t : Math.Pow(invB, -_skew) / _t; + double rat = (ratioTop) / (ratioBottom * _t); + + if (yRand < rat) + return sampleX; + } + } + private double bInvCdf(double p) + { + if (p * _t <= 1) + return p * _t; + else + return Math.Pow((p * _t) * (1 - _skew) + _skew, 1 / (1 - _skew)); + } + } diff --git a/test/DistributedTests/DistributedTests.Client/LoadGeneratorScenario/ConcurrentLoadGenerator.cs b/test/DistributedTests/DistributedTests.Client/LoadGeneratorScenario/ConcurrentLoadGenerator.cs index 5c51cc5bbc..dab5ecf0a9 100644 --- a/test/DistributedTests/DistributedTests.Client/LoadGeneratorScenario/ConcurrentLoadGenerator.cs +++ b/test/DistributedTests/DistributedTests.Client/LoadGeneratorScenario/ConcurrentLoadGenerator.cs @@ -1,6 +1,7 @@ using System.Threading.Channels; using System.Diagnostics; using Microsoft.Extensions.Logging; +using Microsoft.Crank.EventSources; namespace DistributedTests.Client { @@ -126,6 +127,11 @@ public async Task Run(CancellationToken ct) if (!more) break; while (completedBlockReader.TryRead(out var block)) { + // Register the measurement values + BenchmarksEventSource.Measure("requests", block.Completed); + BenchmarksEventSource.Measure("failures", block.Failures); + BenchmarksEventSource.Measure("rps", block.RequestsPerSecond); + blocks.Add(block); } diff --git a/test/DistributedTests/DistributedTests.Client/LoadGeneratorScenario/LoadGeneratorScenarioRunner.cs b/test/DistributedTests/DistributedTests.Client/LoadGeneratorScenario/LoadGeneratorScenarioRunner.cs index 475020d26c..761534e936 100644 --- a/test/DistributedTests/DistributedTests.Client/LoadGeneratorScenario/LoadGeneratorScenarioRunner.cs +++ b/test/DistributedTests/DistributedTests.Client/LoadGeneratorScenario/LoadGeneratorScenarioRunner.cs @@ -35,6 +35,11 @@ public LoadGeneratorScenarioRunner(ILoadGeneratorScenario scenario, ILoggerFa public async Task Run(ClientParameters clientParams, LoadGeneratorParameters loadParams) { + // Register the measurements. n0 -> format as natural number + BenchmarksEventSource.Register("requests", Operations.Sum, Operations.Sum, "Requests", "Number of requests completed", "n0"); + BenchmarksEventSource.Register("failures", Operations.Sum, Operations.Sum, "Failures", "Number of failures", "n0"); + BenchmarksEventSource.Register("rps", Operations.Sum, Operations.Median, "Median RPS", "Rate per second", "n0"); + var secrets = SecretConfiguration.Load(clientParams.SecretSource); var hostBuilder = new HostBuilder().UseOrleansClient((ctx, builder) => builder.Configure(options => { options.ClusterId = clientParams.ClusterId; options.ServiceId = clientParams.ServiceId; }) @@ -65,15 +70,8 @@ public async Task Run(ClientParameters clientParams, LoadGeneratorParameters loa _logger.LogInformation("Running"); var report = await generator.Run(cts.Token); - // Register the measurements. n0 -> format as natural number - BenchmarksEventSource.Register("requests", Operations.First, Operations.Sum, "Requests", "Number of requests completed", "n0"); - BenchmarksEventSource.Register("failures", Operations.First, Operations.Sum, "Failures", "Number of failures", "n0"); - BenchmarksEventSource.Register("rps", Operations.First, Operations.Sum, "Rate per second", "Rate per seconds", "n0"); - - // Register the measurement values - BenchmarksEventSource.Measure("requests", report.Completed); - BenchmarksEventSource.Measure("failures", report.Failures); - BenchmarksEventSource.Measure("rps", report.RatePerSecond); + BenchmarksEventSource.Register("overall-rps", Operations.Last, Operations.Last, "Overall RPS", "RPS", "n0"); + BenchmarksEventSource.Measure("overall-rps", report.RatePerSecond); await host.StopAsync(); } diff --git a/test/DistributedTests/DistributedTests.Client/LoadGeneratorScenario/LoadGeneratorScenarios.cs b/test/DistributedTests/DistributedTests.Client/LoadGeneratorScenario/LoadGeneratorScenarios.cs index 18230bdf45..c0ad25980f 100644 --- a/test/DistributedTests/DistributedTests.Client/LoadGeneratorScenario/LoadGeneratorScenarios.cs +++ b/test/DistributedTests/DistributedTests.Client/LoadGeneratorScenario/LoadGeneratorScenarios.cs @@ -19,4 +19,13 @@ public class PingScenario : ILoadGeneratorScenario public ValueTask IssueRequest(IPingGrain state) => state.Ping(); } + + public class FanOutScenario : ILoadGeneratorScenario + { + public string Name => "fan-out"; + + public ITreeGrain GetStateForWorker(IClusterClient client, int workerId) => client.GetGrain(primaryKey: 0, keyExtension: workerId.ToString()); + + public ValueTask IssueRequest(ITreeGrain root) => root.Ping(); + } } diff --git a/test/DistributedTests/DistributedTests.Client/Program.cs b/test/DistributedTests/DistributedTests.Client/Program.cs index 0d7086d792..93ecce55b2 100644 --- a/test/DistributedTests/DistributedTests.Client/Program.cs +++ b/test/DistributedTests/DistributedTests.Client/Program.cs @@ -10,6 +10,7 @@ var root = new RootCommand(); root.Add(Scenario.CreateCommand(new PingScenario(), loggerFactory)); +root.Add(Scenario.CreateCommand(new FanOutScenario(), loggerFactory)); root.Add(new CounterCaptureCommand(loggerFactory.CreateLogger())); root.Add(new ChaosAgentCommand(loggerFactory.CreateLogger())); diff --git a/test/DistributedTests/DistributedTests.Common/GrainInterfaces/IPingGrain.cs b/test/DistributedTests/DistributedTests.Common/GrainInterfaces/IPingGrain.cs index fbb1e1e633..f49b3e2f82 100644 --- a/test/DistributedTests/DistributedTests.Common/GrainInterfaces/IPingGrain.cs +++ b/test/DistributedTests/DistributedTests.Common/GrainInterfaces/IPingGrain.cs @@ -1,7 +1,7 @@ -namespace DistributedTests.GrainInterfaces +namespace DistributedTests.GrainInterfaces; + +public interface IPingGrain : IGrainWithGuidKey { - public interface IPingGrain : IGrainWithGuidKey - { - ValueTask Ping(); - } + ValueTask Ping(); } + diff --git a/test/DistributedTests/DistributedTests.Common/GrainInterfaces/ITreeGrain.cs b/test/DistributedTests/DistributedTests.Common/GrainInterfaces/ITreeGrain.cs new file mode 100644 index 0000000000..d67ff34b8d --- /dev/null +++ b/test/DistributedTests/DistributedTests.Common/GrainInterfaces/ITreeGrain.cs @@ -0,0 +1,7 @@ +namespace DistributedTests.GrainInterfaces; + +public interface ITreeGrain : IGrainWithIntegerCompoundKey +{ + public ValueTask Ping(); +} + diff --git a/test/DistributedTests/DistributedTests.Grains/PingGrain.cs b/test/DistributedTests/DistributedTests.Grains/PingGrain.cs index 0d00061df9..4e01b65693 100644 --- a/test/DistributedTests/DistributedTests.Grains/PingGrain.cs +++ b/test/DistributedTests/DistributedTests.Grains/PingGrain.cs @@ -1,9 +1,8 @@ using DistributedTests.GrainInterfaces; -namespace DistributedTests.Grains +namespace DistributedTests.Grains; + +public class PingGrain : Grain, IPingGrain { - public class PingGrain : Grain, IPingGrain - { - public ValueTask Ping() => default; - } + public ValueTask Ping() => default; } diff --git a/test/DistributedTests/DistributedTests.Grains/TreeGrain.cs b/test/DistributedTests/DistributedTests.Grains/TreeGrain.cs new file mode 100644 index 0000000000..b5c8817455 --- /dev/null +++ b/test/DistributedTests/DistributedTests.Grains/TreeGrain.cs @@ -0,0 +1,41 @@ +using DistributedTests.GrainInterfaces; + +namespace DistributedTests.Grains; + +public class TreeGrain : Grain, ITreeGrain +{ + // 16^4 grains (~65K) + public const int FanOutFactor = 16; + public const int MaxLevel = 4; + private readonly List _children; + + public TreeGrain() + { + var id = this.GetPrimaryKeyLong(out var forestName); + + var level = id == 0 ? 0 : (int)Math.Log(id, FanOutFactor); + var numChildren = level < MaxLevel ? FanOutFactor : 0; + _children = new List(numChildren); + var childBase = (id + 1) * FanOutFactor; + for (var i = 1; i <= numChildren; i++) + { + var child = GrainFactory.GetGrain(childBase + i, keyExtension: forestName); + _children.Add(child); + } + } + + public async ValueTask Ping() + { + var tasks = new List(_children.Count); + foreach (var child in _children) + { + tasks.Add(child.Ping()); + } + + // Wait for the tasks to complete. + foreach (var task in tasks) + { + await task; + } + } +} diff --git a/test/DistributedTests/DistributedTests.Server/ServerCommand.cs b/test/DistributedTests/DistributedTests.Server/ServerCommand.cs index b1d0e67a5d..2b8d049028 100644 --- a/test/DistributedTests/DistributedTests.Server/ServerCommand.cs +++ b/test/DistributedTests/DistributedTests.Server/ServerCommand.cs @@ -19,6 +19,7 @@ public ServerCommand(ISiloConfigurator siloConfigurator) AddOption(OptionHelper.CreateOption("--siloPort", defaultValue: 11111)); AddOption(OptionHelper.CreateOption("--gatewayPort", defaultValue: 30000)); AddOption(OptionHelper.CreateOption("--secretSource", defaultValue: SecretConfiguration.SecretSource.File)); + AddOption(OptionHelper.CreateOption("--ActivationRepartitioning", defaultValue: true)); foreach (var opt in siloConfigurator.Options) { diff --git a/test/DistributedTests/DistributedTests.Server/ServerRunner.cs b/test/DistributedTests/DistributedTests.Server/ServerRunner.cs index ec3588755b..22626be9e1 100644 --- a/test/DistributedTests/DistributedTests.Server/ServerRunner.cs +++ b/test/DistributedTests/DistributedTests.Server/ServerRunner.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Hosting; using Orleans.Configuration; using DistributedTests.Common.MessageChannel; +using Microsoft.Extensions.Logging; namespace DistributedTests.Server { @@ -12,6 +13,7 @@ public class CommonParameters public int SiloPort { get; set; } public int GatewayPort { get; set; } public SecretConfiguration.SecretSource SecretSource { get; set; } + public bool ActivationRepartitioning { get; set; } } public class ServerRunner @@ -38,6 +40,10 @@ public async Task Run(CommonParameters commonParameters, T configuratorParameter { var host = Host .CreateDefaultBuilder() + .ConfigureLogging(logging => + { + logging.AddFilter("Orleans.Runtime.Placement.Repartitioning", LogLevel.Debug); + }) .UseOrleans((ctx, siloBuilder) => ConfigureOrleans(siloBuilder, commonParameters, configuratorParameters)) .Build(); @@ -70,6 +76,13 @@ private void ConfigureOrleans(ISiloBuilder siloBuilder, CommonParameters commonP .ConfigureEndpoints(siloPort: commonParameters.SiloPort, gatewayPort: commonParameters.GatewayPort) .UseAzureStorageClustering(options => options.TableServiceClient = new(_secrets.ClusteringConnectionString)); + if (commonParameters.ActivationRepartitioning) + { +#pragma warning disable ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + siloBuilder.AddActivationRepartitioner(); +#pragma warning restore ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } + _siloConfigurator.Configure(siloBuilder, configuratorParameters); } } diff --git a/test/Grains/BenchmarkGrainInterfaces/Ping/ITreeGrain.cs b/test/Grains/BenchmarkGrainInterfaces/Ping/ITreeGrain.cs new file mode 100644 index 0000000000..75540f8508 --- /dev/null +++ b/test/Grains/BenchmarkGrainInterfaces/Ping/ITreeGrain.cs @@ -0,0 +1,7 @@ +namespace BenchmarkGrainInterfaces.Ping; + +public interface ITreeGrain : IGrainWithIntegerCompoundKey +{ + public ValueTask Ping(); +} + diff --git a/test/Grains/BenchmarkGrains/Ping/TreeGrain.cs b/test/Grains/BenchmarkGrains/Ping/TreeGrain.cs new file mode 100644 index 0000000000..03b1c56c75 --- /dev/null +++ b/test/Grains/BenchmarkGrains/Ping/TreeGrain.cs @@ -0,0 +1,41 @@ +using BenchmarkGrainInterfaces.Ping; + +namespace BenchmarkGrains.Ping; + +public class TreeGrain : Grain, ITreeGrain +{ + // 16^4 grains (~65K) + public const int FanOutFactor = 16; + public const int MaxLevel = 4; + private readonly List _children; + + public TreeGrain() + { + var id = this.GetPrimaryKeyLong(out var forestName); + + var level = id == 0 ? 0 : (int)Math.Log(id, FanOutFactor); + var numChildren = level < MaxLevel ? FanOutFactor : 0; + _children = new List(numChildren); + var childBase = (id + 1) * FanOutFactor; + for (var i = 1; i <= numChildren; i++) + { + var child = GrainFactory.GetGrain(childBase + i, keyExtension: forestName); + _children.Add(child); + } + } + + public async ValueTask Ping() + { + var tasks = new List(_children.Count); + foreach (var child in _children) + { + tasks.Add(child.Ping()); + } + + // Wait for the tasks to complete. + foreach (var task in tasks) + { + await task; + } + } +} diff --git a/test/Grains/TestInternalGrainInterfaces/ActivationGCTestGrainInterfaces.cs b/test/Grains/TestInternalGrainInterfaces/ActivationGCTestGrainInterfaces.cs index 4dea262ac3..c11f64f218 100644 --- a/test/Grains/TestInternalGrainInterfaces/ActivationGCTestGrainInterfaces.cs +++ b/test/Grains/TestInternalGrainInterfaces/ActivationGCTestGrainInterfaces.cs @@ -1,4 +1,4 @@ -namespace UnitTests.GrainInterfaces +namespace UnitTests.GrainInterfaces { public interface IIdleActivationGcTestGrain1 : IGrainWithGuidKey { @@ -15,7 +15,6 @@ public interface IBusyActivationGcTestGrain1 : IGrainWithGuidKey Task Nop(); Task Delay(TimeSpan dt); Task IdentifyActivation(); - Task EnableBurstOnCollection(int count); } public interface IBusyActivationGcTestGrain2 : IGrainWithGuidKey diff --git a/test/Grains/TestInternalGrains/ActivationGCTestGrains.cs b/test/Grains/TestInternalGrains/ActivationGCTestGrains.cs index f83523e2b8..51ad43d37f 100644 --- a/test/Grains/TestInternalGrains/ActivationGCTestGrains.cs +++ b/test/Grains/TestInternalGrains/ActivationGCTestGrains.cs @@ -26,8 +26,6 @@ internal class BusyActivationGcTestGrain1: Grain, IBusyActivationGcTestGrain1 private readonly ActivationCollector activationCollector; private readonly IGrainContext _grainContext; - private int burstCount = 0; - public BusyActivationGcTestGrain1(ActivationCollector activationCollector, IGrainContext grainContext) { this.activationCollector = activationCollector; @@ -48,32 +46,6 @@ public Task IdentifyActivation() { return Task.FromResult(_id); } - - public Task EnableBurstOnCollection(int count) - { - if (0 == count) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - burstCount = count; - this.activationCollector.Debug_OnDecideToCollectActivation = OnCollectActivation; - return Task.CompletedTask; - } - - private void OnCollectActivation(GrainId grainId) - { - var other = grainId.Type; - var self = _grainContext.Address.GrainId.Type; - if (other == self) - { - IBusyActivationGcTestGrain1 g = GrainFactory.GetGrain(grainId); - for (int i = 0; i < burstCount; ++i) - { - g.Delay(TimeSpan.FromMilliseconds(10)).Ignore(); - } - } - } } public class BusyActivationGcTestGrain2: Grain, IBusyActivationGcTestGrain2 diff --git a/test/NonSilo.Tests/General/RingTests_Standalone.cs b/test/NonSilo.Tests/General/RingTests_Standalone.cs index ac8de6598a..97fc2738ec 100644 --- a/test/NonSilo.Tests/General/RingTests_Standalone.cs +++ b/test/NonSilo.Tests/General/RingTests_Standalone.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.Net; using Microsoft.Extensions.Logging.Abstractions; using Orleans.Runtime; @@ -259,6 +260,7 @@ public bool TryGetSiloName(SiloAddress siloAddress, out string siloName) } public bool UnSubscribeFromSiloStatusEvents(ISiloStatusListener observer) => _subscribers.Remove(observer); + public ImmutableArray GetActiveSilos() => [.. GetApproximateSiloStatuses(onlyActive: true).Keys]; } internal class RangeBreakable diff --git a/test/TestInfrastructure/TestExtensions/TestUtils.cs b/test/TestInfrastructure/TestExtensions/TestUtils.cs index aeb51ebf46..7ffdae82a4 100644 --- a/test/TestInfrastructure/TestExtensions/TestUtils.cs +++ b/test/TestInfrastructure/TestExtensions/TestUtils.cs @@ -111,7 +111,7 @@ public static void ConfigureClientThreadPoolSettingsForStorageTests(int NumDotNe ServicePointManager.UseNagleAlgorithm = false; } - public static async Task GetActivationCount(IGrainFactory grainFactory, string fullTypeName) + public static async Task GetActivationCount(IGrainFactory grainFactory, string grainTypeName) { int result = 0; @@ -119,8 +119,10 @@ public static async Task GetActivationCount(IGrainFactory grainFactory, str SimpleGrainStatistic[] stats = await mgmtGrain.GetSimpleGrainStatistics(); foreach (var stat in stats) { - if (stat.GrainType == fullTypeName) + if (string.Equals(stat.GrainType, grainTypeName, StringComparison.Ordinal)) + { result += stat.ActivationCount; + } } return result; } diff --git a/test/TesterInternal/ActivationRepartitioningTests/BlockedBloomFilterTests.cs b/test/TesterInternal/ActivationRepartitioningTests/BlockedBloomFilterTests.cs new file mode 100644 index 0000000000..95418089f2 --- /dev/null +++ b/test/TesterInternal/ActivationRepartitioningTests/BlockedBloomFilterTests.cs @@ -0,0 +1,24 @@ +using Orleans.Runtime.Placement.Repartitioning; +using Xunit; + +namespace UnitTests.ActivationRepartitioningTests; + +public class BlockedBloomFilterTests +{ + [Fact] + public void AddAndCheck() + { + var bloomFilter = new BlockedBloomFilter(100, 0.01); + var sample = new GrainId(GrainType.Create("type"), IdSpan.Create("key")); + bloomFilter.Add(sample); + Assert.True(bloomFilter.Contains(sample)); + } + + [Fact] + public void DoesNotContainSome() + { + var bloomFilter = new BlockedBloomFilter(100, 0.01); + var sample = new GrainId(GrainType.Create("type"), IdSpan.Create("key")); + Assert.False(bloomFilter.Contains(sample)); + } +} diff --git a/test/TesterInternal/ActivationRepartitioningTests/CustomToleranceTests.cs b/test/TesterInternal/ActivationRepartitioningTests/CustomToleranceTests.cs new file mode 100644 index 0000000000..de30b589a9 --- /dev/null +++ b/test/TesterInternal/ActivationRepartitioningTests/CustomToleranceTests.cs @@ -0,0 +1,225 @@ +using System.ComponentModel; +using Microsoft.Extensions.DependencyInjection; +using Orleans.Configuration; +using Orleans.Placement; +using Orleans.Placement.Repartitioning; +using Orleans.Runtime.Placement; +using Orleans.Runtime.Placement.Repartitioning; +using Orleans.TestingHost; +using TestExtensions; +using Xunit; + +namespace UnitTests.ActivationRepartitioningTests; + +// Scenarios can be seen visually here: https://github.com/dotnet/orleans/pull/8877 +[TestCategory("Functional"), TestCategory("ActivationRepartitioning"), Category("BVT")] +public class CustomToleranceTests(CustomToleranceTests.Fixture fixture) : RepartitioningTestBase(fixture), IClassFixture +{ + [Fact] + public async Task Should_ConvertAllRemoteCalls_ToLocalCalls_WhileRespectingTolerance() + { + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)); + await AdjustActivationCountOffsets(); + + var e1 = GrainFactory.GetGrain(1); + var e2 = GrainFactory.GetGrain(2); + var e3 = GrainFactory.GetGrain(3); + + RequestContext.Set(IPlacementDirector.PlacementHintKey, Silo1); + + await e1.FirstPing(Silo2); + await e2.FirstPing(Silo2); + await e3.FirstPing(Silo2); + + RequestContext.Set(IPlacementDirector.PlacementHintKey, Silo2); + var x = GrainFactory.GetGrain(0); + await x.Ping(); + + var i = 0; + while (i < 3) + { + await e1.Ping(); + await e2.Ping(); + await e3.Ping(); + await x.Ping(); + i++; + } + + var f1 = GrainFactory.GetGrain(1); + var f2 = GrainFactory.GetGrain(2); + var f3 = GrainFactory.GetGrain(3); + + var e1_host = await e1.GetAddress(); + var e2_host = await e2.GetAddress(); + var e3_host = await e3.GetAddress(); + + var f1_host = await f1.GetAddress(); + var f2_host = await f2.GetAddress(); + var f3_host = await f3.GetAddress(); + + Assert.Equal(Silo1, e1_host); + Assert.Equal(Silo1, e2_host); + Assert.Equal(Silo1, e3_host); + + Assert.Equal(Silo2, f1_host); + Assert.Equal(Silo2, f2_host); + Assert.Equal(Silo2, f3_host); + + Assert.Equal(Silo2, await x.GetAddress()); // X remains in silo 2 + + await Silo1Repartitioner.TriggerExchangeRequest(); + + do + { + e2_host = await e2.GetAddress(); + f1_host = await f1.GetAddress(); + cts.Token.ThrowIfCancellationRequested(); + } + while (e2_host == Silo1 || f1_host == Silo2); + + // At this point the layout is like follows: + + // S1: E1-F1, E3-F3, sys.svc.clustering.dev, rest (default activations, present in both silos) + // S2: E2-F2, X, rest (default activations, present in both silos) + + // Tolerance <= 2, and if we ignore defaults once, sys.svc.clustering.dev, and X (which is used to counter-balance sys.svc.clustering.dev) + // we end up with a total of 4 activations in silo1, and 2 in silo 2, which means the tolerance has been respected, and all remote calls have + // been converted to local calls. + + await Test(); + + // To make sure, we trigger 's1_rebalancer' again, which should yield to no further migrations. + i = 0; + while (i < 3) + { + await e1.Ping(); + await e2.Ping(); + await e3.Ping(); + await x.Ping(); + i++; + } + + await Silo1Repartitioner.TriggerExchangeRequest(); + await Test(); + + // To make extra sure, we now trigger 's2_rebalancer', which again should yield to no further migrations. + i = 0; + while (i < 3) + { + await e1.Ping(); + await e2.Ping(); + await e3.Ping(); + await x.Ping(); + i++; + } + + await Silo2Repartitioner.TriggerExchangeRequest(); + await Test(); + + //await ResetCounters(); uncomment if you add more tests + + async Task Test() + { + e1_host = await e1.GetAddress(); + e2_host = await e2.GetAddress(); + e3_host = await e3.GetAddress(); + + f1_host = await f1.GetAddress(); + f2_host = await f2.GetAddress(); + f3_host = await f3.GetAddress(); + + Assert.Equal(Silo1, e1_host); // E1 is still in silo 1 + Assert.Equal(Silo2, e2_host); // E2 is now in silo 2 + Assert.Equal(Silo1, e3_host); // E3 is still in silo 1 + + Assert.Equal(Silo1, f1_host); // F1 is now in silo 1 + Assert.Equal(Silo2, f2_host); // F2 is still in silo 2 + Assert.Equal(Silo1, f3_host); // F3 is now in silo 1 + + Assert.Equal(Silo2, await x.GetAddress()); // X remains in silo 2 + } + } + + public interface IE : IGrainWithIntegerKey + { + Task FirstPing(SiloAddress silo2); + Task Ping(); + Task GetAddress(); + } + public interface IF : IGrainWithIntegerKey + { + Task Ping(); + Task GetAddress(); + } + public interface IX : IGrainWithIntegerKey + { + Task Ping(); + Task GetAddress(); + } + + public class E : Grain, IE + { + public async Task FirstPing(SiloAddress silo2) + { + RequestContext.Set(IPlacementDirector.PlacementHintKey, silo2); + await GrainFactory.GetGrain(this.GetPrimaryKeyLong()).Ping(); + } + + public Task Ping() => GrainFactory.GetGrain(this.GetPrimaryKeyLong()).Ping(); + public Task GetAddress() => Task.FromResult(GrainContext.Address.SiloAddress); + } + + public class F : Grain, IF + { + public Task Ping() => Task.CompletedTask; + public Task GetAddress() => Task.FromResult(GrainContext.Address.SiloAddress); + } + + /// + /// This is simply to achive initial balance between the 2 silos, as by default the primary + /// will have 1 more activation than the secondary. That activations is 'sys.svc.clustering.dev' + /// + [Immovable] + public class X : Grain, IX + { + public Task Ping() => Task.CompletedTask; + public Task GetAddress() => Task.FromResult(GrainContext.Address.SiloAddress); + } + + public class Fixture : BaseTestClusterFixture + { + protected override void ConfigureTestCluster(TestClusterBuilder builder) + { + builder.Options.InitialSilosCount = 2; + builder.AddSiloBuilderConfigurator(); + } + + private class SiloConfigurator : ISiloConfigurator + { + public void Configure(ISiloBuilder hostBuilder) +#pragma warning disable ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + => hostBuilder + .Configure(o => + { + o.AssumeHomogenousSilosForTesting = true; + o.ClientGatewayShutdownNotificationTimeout = default; + }) + .Configure(o => + { + // Make these so that the timers practically never fire! We will invoke the protocol manually. + o.MinRoundPeriod = TimeSpan.FromSeconds(299); + o.MaxRoundPeriod = TimeSpan.FromSeconds(300); + // Make this practically zero, so we can invoke the protocol more than once without needing to put a delay in the tests. + o.RecoveryPeriod = TimeSpan.FromMilliseconds(1); + }) + .AddActivationRepartitioner() + .ConfigureServices(service => service.AddSingleton()); +#pragma warning restore ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } + + private class HardLimitRule : IImbalanceToleranceRule + { + public bool IsSatisfiedBy(uint imbalance) => imbalance <= 2; + } + } +} \ No newline at end of file diff --git a/test/TesterInternal/ActivationRepartitioningTests/DefaultToleranceTests.cs b/test/TesterInternal/ActivationRepartitioningTests/DefaultToleranceTests.cs new file mode 100644 index 0000000000..9bc8b1a174 --- /dev/null +++ b/test/TesterInternal/ActivationRepartitioningTests/DefaultToleranceTests.cs @@ -0,0 +1,565 @@ +using System.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Orleans.Configuration; +using Orleans.Placement; +using Orleans.Runtime.Placement; +using Orleans.Runtime.Placement.Repartitioning; +using Orleans.Streams; +using Orleans.TestingHost; +using TestExtensions; +using Xunit; + +namespace UnitTests.ActivationRepartitioningTests; + +// Scenarious can be seen visually here: https://github.com/dotnet/orleans/pull/8877 +[TestCategory("Functional"), TestCategory("ActivationRepartitioning")] +public class DefaultToleranceTests(DefaultToleranceTests.Fixture fixture) : RepartitioningTestBase(fixture), IClassFixture +{ + [Fact] + public async Task A_ShouldMoveToSilo2__B_And_C_ShouldStayOnSilo2() + { + RequestContext.Set(IPlacementDirector.PlacementHintKey, Silo1); + + var scenario = Scenario._1; + var a = GrainFactory.GetGrain($"a{scenario}"); + await a.FirstPing(scenario, Silo1, Silo2); + + var i = 0; + while (i < 3) + { + await a.Ping(scenario); + i++; + } + + var b = GrainFactory.GetGrain($"b{scenario}"); + var c = GrainFactory.GetGrain($"c{scenario}"); + + var a_host = await a.GetAddress(); + var b_host = await b.GetAddress(); + var c_host = await c.GetAddress(); + + Assert.Equal(Silo1, a_host); + Assert.Equal(Silo2, b_host); + Assert.Equal(Silo2, c_host); + + await Silo1Repartitioner.TriggerExchangeRequest(); + + do + { + a_host = await a.GetAddress(); + } + while (a_host == Silo1); + + // refresh + a_host = await a.GetAddress(); + b_host = await b.GetAddress(); + c_host = await c.GetAddress(); + + Assert.Equal(Silo2, a_host); // A is now in silo 2 + Assert.Equal(Silo2, b_host); + Assert.Equal(Silo2, c_host); + + await ResetCounters(); + } + + [Fact] + public async Task C_ShouldMoveToSilo1__A_And_B_ShouldStayOnSilo1() + { + RequestContext.Set(IPlacementDirector.PlacementHintKey, Silo1); + + var scenario = Scenario._2; + var a = GrainFactory.GetGrain($"a{scenario}"); + var b = GrainFactory.GetGrain($"b{scenario}"); + + await a.FirstPing(scenario, Silo1, Silo2); + await b.Ping(scenario); + + var i = 0; + while (i < 3) + { + await a.Ping(scenario); + await b.Ping(scenario); + i++; + } + + var c = GrainFactory.GetGrain($"c{scenario}"); + + var a_host = await a.GetAddress(); + var b_host = await b.GetAddress(); + var c_host = await c.GetAddress(); + + Assert.Equal(Silo1, a_host); + Assert.Equal(Silo1, b_host); + Assert.Equal(Silo2, c_host); + + await Silo1Repartitioner.TriggerExchangeRequest(); + + do + { + c_host = await c.GetAddress(); + } + while (c_host == Silo2); + + // refresh + a_host = await a.GetAddress(); + b_host = await b.GetAddress(); + c_host = await c.GetAddress(); + + Assert.Equal(Silo1, a_host); + Assert.Equal(Silo1, b_host); + Assert.Equal(Silo1, c_host); // C is now in silo 1 + + await ResetCounters(); + } + + [Fact] + public async Task Immovable_C_ShouldStayOnSilo2__A_And_B_ShouldMoveToSilo2() + { + RequestContext.Set(IPlacementDirector.PlacementHintKey, Silo1); + + var scenario = Scenario._3; + var a = GrainFactory.GetGrain($"a{scenario}"); + var b = GrainFactory.GetGrain($"b{scenario}"); + + await a.FirstPing(scenario, Silo1, Silo2); + await b.Ping(scenario); + + var i = 0; + while (i < 3) + { + await a.Ping(scenario); + await b.Ping(scenario); + i++; + } + + var c = GrainFactory.GetGrain($"c{scenario}"); + + var a_host = await a.GetAddress(); + var b_host = await b.GetAddress(); + var c_host = await c.GetAddress(); + + Assert.Equal(Silo1, a_host); + Assert.Equal(Silo1, b_host); + Assert.Equal(Silo2, c_host); + + await Silo1Repartitioner.TriggerExchangeRequest(); + + do + { + a_host = await a.GetAddress(); + b_host = await b.GetAddress(); + } + while (a_host == Silo1 || b_host == Silo1); + + // refresh + a_host = await a.GetAddress(); + b_host = await b.GetAddress(); + c_host = await c.GetAddress(); + + Assert.Equal(Silo2, a_host); // A is now in silo 2 + Assert.Equal(Silo2, b_host); // B is now in silo 2 + Assert.Equal(Silo2, c_host); // C is still in silo 2 + + await ResetCounters(); + } + + [Fact] + public async Task A_ShouldMoveToSilo2_Or_C_ShouldMoveToSilo1__B_And_D_ShouldStayOnTheirSilos() + { + RequestContext.Set(IPlacementDirector.PlacementHintKey, Silo1); + + var scenario = Scenario._4; + var a = GrainFactory.GetGrain($"a{scenario}"); + + await a.FirstPing(scenario, Silo1, Silo2); + + var i = 0; + while (i < 3) + { + await a.Ping(scenario); + i++; + } + + var b = GrainFactory.GetGrain($"b{scenario}"); + var c = GrainFactory.GetGrain($"c{scenario}"); + var d = GrainFactory.GetGrain($"d{scenario}"); + + var a_host = await a.GetAddress(); + var b_host = await b.GetAddress(); + var c_host = await c.GetAddress(); + var d_host = await d.GetAddress(); + + Assert.Equal(Silo1, a_host); + Assert.Equal(Silo1, b_host); + Assert.Equal(Silo2, c_host); + Assert.Equal(Silo2, d_host); + + await Silo1Repartitioner.TriggerExchangeRequest(); + + do + { + a_host = await a.GetAddress(); + c_host = await c.GetAddress(); + } + while (a_host == Silo1 && c_host == Silo2); + + // refresh + a_host = await a.GetAddress(); + b_host = await b.GetAddress(); + c_host = await c.GetAddress(); + d_host = await d.GetAddress(); + + // A can go to Silo 2, or C can come to Silo 1, both are valid, so we need to check for both. + if (a_host == Silo2 && c_host == Silo2) + { + Assert.Equal(Silo2, a_host); // A is now in silo 2 + Assert.Equal(Silo1, b_host); + Assert.Equal(Silo2, c_host); + Assert.Equal(Silo2, d_host); + + return; + } + + if (a_host == Silo1 && c_host == Silo1) + { + Assert.Equal(Silo1, a_host); + Assert.Equal(Silo1, b_host); + Assert.Equal(Silo1, c_host); // C is now in silo 1 + Assert.Equal(Silo2, d_host); + + return; + } + + await ResetCounters(); + } + + [SkippableFact] + public async Task Receivers_ShouldMoveCloseTo_PullingAgent() + { + var sp1 = GrainFactory.GetGrain("s1"); + var sp2 = GrainFactory.GetGrain("s2"); + var sp3 = GrainFactory.GetGrain("s3"); + + RequestContext.Set(IPlacementDirector.PlacementHintKey, Silo1); + await sp1.FirstPing(); + await sp2.FirstPing(); + + RequestContext.Set(IPlacementDirector.PlacementHintKey, Silo2); + await sp3.FirstPing(); + + var i = 0; + while (i < 3) + { + await sp1.StreamPing(); + await sp2.StreamPing(); + await sp3.StreamPing(); + i++; + } + + var sr1 = GrainFactory.GetGrain("s1"); + var sr2 = GrainFactory.GetGrain("s2"); + var sr3 = GrainFactory.GetGrain("s3"); + + var sr1_GotHit = false; + var sr2_GotHit = false; + var sr3_GotHit = false; + + while (!sr1_GotHit || !sr2_GotHit || !sr3_GotHit) + { + sr1_GotHit = await sr1.GotStreamHit(); + sr2_GotHit = await sr2.GotStreamHit(); + sr3_GotHit = await sr3.GotStreamHit(); + } + + var sr1_host = await sr1.GetAddress(); + var sr2_host = await sr2.GetAddress(); + var sr3_host = await sr3.GetAddress(); + + Assert.Equal(Silo1, sr1_host); + Assert.Equal(Silo1, sr2_host); + Assert.Equal(Silo2, sr3_host); + + await Silo1Repartitioner.TriggerExchangeRequest(); + await Task.Delay(100); // leave some breathing room - may not be enough though, thats why this test is skippable + + var allowedDuration = TimeSpan.FromSeconds(3); + Stopwatch stopwatch = new(); + stopwatch.Start(); + + do + { + sr1_host = await sr1.GetAddress(); + sr2_host = await sr2.GetAddress(); + + Skip.If(stopwatch.Elapsed > allowedDuration); + } + while (sr1_host == Silo1 || sr2_host == Silo1); + + // refresh + sr1_host = await sr1.GetAddress(); + sr2_host = await sr2.GetAddress(); + sr3_host = await sr3.GetAddress(); + + Assert.Equal(Silo2, sr1_host); // SR1 is now in silo 2, as there is 1 pulling agent (which is moved to silo 2 by the streaming runtime) + Assert.Equal(Silo2, sr2_host); // SR2 is now in silo 2, as there is 1 pulling agent (which is moved to silo 2 by the streaming runtime) + Assert.Equal(Silo2, sr3_host); + + await ResetCounters(); + } + + public enum Scenario { _1, _2, _3, _4 } + + public interface IBase : IGrainWithStringKey + { + Task Ping(Scenario scenario); + Task GetAddress(); + } + public interface IA : IBase + { + Task FirstPing(Scenario scenario, SiloAddress silo1, SiloAddress silo2); + } + public interface IB : IBase { } + public interface IC : IBase + { + Task Ping(Scenario scenario, SiloAddress silo2); + } + public interface ICImmovable : IBase { } + public interface ID : IBase { } + public interface ISP : IGrainWithStringKey + { + Task FirstPing(); + Task StreamPing(); + Task GetAddress(); + } + public interface ISR : IGrainWithStringKey + { + Task Ping(); + Task GotStreamHit(); + Task GetAddress(); + } + + public abstract class GrainBase : Grain + { + public Task GetAddress() => Task.FromResult(GrainContext.Address.SiloAddress); + } + + public class A : GrainBase, IA + { + private SiloAddress _silo1; + private SiloAddress _silo2; + + public async Task FirstPing(Scenario scenario, SiloAddress silo1, SiloAddress silo2) + { + _silo1 = silo1; + _silo2 = silo2; + + switch (scenario) + { + case Scenario._1: + { + RequestContext.Set(IPlacementDirector.PlacementHintKey, _silo2); + + await GrainFactory.GetGrain($"b{scenario}").Ping(scenario); + await GrainFactory.GetGrain($"c{scenario}").Ping(scenario); + } + break; + case Scenario._2: + { + RequestContext.Set(IPlacementDirector.PlacementHintKey, _silo2); + await GrainFactory.GetGrain($"c{scenario}").Ping(scenario); + } + break; + case Scenario._3: + { + RequestContext.Set(IPlacementDirector.PlacementHintKey, _silo2); + await GrainFactory.GetGrain($"c{scenario}").Ping(scenario); + } + break; + case Scenario._4: + { + RequestContext.Set(IPlacementDirector.PlacementHintKey, _silo1); + await GrainFactory.GetGrain($"b{scenario}").Ping(scenario); + + RequestContext.Set(IPlacementDirector.PlacementHintKey, _silo2); + await GrainFactory.GetGrain($"c{scenario}").Ping(scenario); + await GrainFactory.GetGrain($"c{scenario}").Ping(scenario, _silo2); + } + break; + default: throw new NotSupportedException(); + } + } + + public async Task Ping(Scenario scenario) + { + switch (scenario) + { + case Scenario._1: + { + await GrainFactory.GetGrain($"b{scenario}").Ping(scenario); + await GrainFactory.GetGrain($"c{scenario}").Ping(scenario); + } + break; + case Scenario._2: + { + await GrainFactory.GetGrain($"c{scenario}").Ping(scenario); + } + break; + case Scenario._3: + { + await GrainFactory.GetGrain($"c{scenario}").Ping(scenario); + } + break; + case Scenario._4: + { + await GrainFactory.GetGrain($"b{scenario}").Ping(scenario); + await GrainFactory.GetGrain($"c{scenario}").Ping(scenario); + await GrainFactory.GetGrain($"c{scenario}").Ping(scenario, _silo2); + } + break; + default: throw new NotSupportedException(); + } + } + } + + public class B : GrainBase, IB + { + public Task Ping(Scenario scenario) => + scenario switch + { + Scenario._1 => Task.CompletedTask, + Scenario._2 => GrainFactory.GetGrain($"c{scenario}").Ping(scenario), + Scenario._3 => GrainFactory.GetGrain($"c{scenario}").Ping(scenario), + Scenario._4 => Task.CompletedTask, + _ => throw new NotSupportedException(), + }; + } + + public class C : GrainBase, IC + { + public Task Ping(Scenario scenario) => + scenario switch + { + Scenario._1 => GrainFactory.GetGrain($"b{scenario}").Ping(scenario), + Scenario._2 => Task.CompletedTask, + Scenario._3 => Task.CompletedTask, + Scenario._4 => Task.CompletedTask, + _ => throw new NotSupportedException(), + }; + + public async Task Ping(Scenario scenario, SiloAddress silo2) + { + switch (scenario) + { + case Scenario._4: + { + RequestContext.Set(IPlacementDirector.PlacementHintKey, silo2); + await GrainFactory.GetGrain($"d{scenario}").Ping(scenario); + } + break; + default: throw new NotSupportedException(); + } + } + } + + [Immovable] + public class CImmovable : GrainBase, ICImmovable + { + public Task Ping(Scenario scenario) => + scenario switch + { + Scenario._3 => Task.CompletedTask, + _ => throw new NotSupportedException(), + }; + } + + public class D : GrainBase, ID + { + public Task Ping(Scenario scenario) => + scenario switch + { + Scenario._4 => Task.CompletedTask, + _ => throw new NotSupportedException(), + }; + } + + [Immovable] + public class SP : GrainBase, ISP + { + // We are just 'Immovable' on this type, because we just want it to push messages to the stream, + // as for some reason pushing to a stream via the cluster client isnt invoking the consumer grains. + + private IAsyncStream _stream; + + public override Task OnActivateAsync(CancellationToken cancellationToken) + { + _stream = this.GetStreamProvider(Fixture.StreamProviderName) + .GetStream(StreamId.Create(Fixture.StreamNamespaceName, this.GetPrimaryKeyString())); + + return Task.CompletedTask; + } + + public Task FirstPing() => GrainFactory.GetGrain(this.GetPrimaryKeyString()).Ping(); + public Task StreamPing() => _stream.OnNextAsync(Random.Shared.Next()); + } + + [ImplicitStreamSubscription(Fixture.StreamNamespaceName)] + public class SR : GrainBase, ISR + { + private bool _streamHit = false; + + public override async Task OnActivateAsync(CancellationToken cancellationToken) + { + var sp = this.GetStreamProvider(Fixture.StreamProviderName) + .GetStream(StreamId.Create(Fixture.StreamNamespaceName, this.GetPrimaryKeyString())); + + await sp.SubscribeAsync((_, _) => + { + _streamHit = true; + return Task.CompletedTask; + }); + } + + public Task Ping() => Task.CompletedTask; + public Task GotStreamHit() => Task.FromResult(_streamHit); + } + + public class Fixture : BaseTestClusterFixture + { + public const string StreamProviderName = "arsp"; + public const string StreamNamespaceName = "arns"; + + protected override void ConfigureTestCluster(TestClusterBuilder builder) + { + builder.Options.InitialSilosCount = 2; + builder.AddSiloBuilderConfigurator(); + } + + private class SiloConfigurator : ISiloConfigurator + { + public void Configure(ISiloBuilder hostBuilder) +#pragma warning disable ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + => hostBuilder + .Configure(o => + { + o.AssumeHomogenousSilosForTesting = true; + o.ClientGatewayShutdownNotificationTimeout = default; + }) + .Configure(o => + { + // Make these so that the timers practically never fire! We will invoke the protocol manually. + o.MinRoundPeriod = TimeSpan.FromSeconds(299); + o.MaxRoundPeriod = TimeSpan.FromSeconds(300); + // Make this practically zero, so we can invoke the protocol more than once without needing to put a delay in the tests. + o.RecoveryPeriod = TimeSpan.FromMilliseconds(1); + }) + .AddMemoryStreams(StreamProviderName, c => + { + c.ConfigurePartitioning(1); + c.ConfigureStreamPubSub(StreamPubSubType.ImplicitOnly); + }) + .AddActivationRepartitioner() + .ConfigureServices(service => service.AddSingleton()); +#pragma warning restore ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } + } +} \ No newline at end of file diff --git a/test/TesterInternal/ActivationRepartitioningTests/FrequencyFilterTests.cs b/test/TesterInternal/ActivationRepartitioningTests/FrequencyFilterTests.cs new file mode 100644 index 0000000000..e71a68c9ce --- /dev/null +++ b/test/TesterInternal/ActivationRepartitioningTests/FrequencyFilterTests.cs @@ -0,0 +1,140 @@ +using System.Text; +using Orleans.Runtime.Placement.Repartitioning; +using Xunit; + +namespace UnitTests.ActivationRepartitioningTests; + +public class FrequencyFilterTests +{ + [Fact] + public void GetExpectedTopK() + { + const int NumSamples = 10_000; + var sink = new UlongFrequentItemCollection(100); + var random = new Random(); + var distribution = new ZipfRejectionSampler(random, 1000, 0.5); + for (var i = 0; i < NumSamples; i++) + { + var sample = (ulong)distribution.Sample(); + sink.Add(new TestKey(sample)); + + if (i == 4 * NumSamples / 5) + { + sink.Remove(new TestKey(3)); + } + } + + var allCounters = sink.Elements.ToList(); + allCounters.Sort((left, right) => right.Count.CompareTo(left.Count)); + var sb = new StringBuilder(); + foreach (var (key, count, error) in allCounters) + { + if (error == 0) + { + sb.AppendLine($"{key.Key,3}: {count}"); + } + else + { + sb.AppendLine($"{key.Key,3}: {count} ε{error}"); + } + } + + var result = sb.ToString(); + Assert.NotEmpty(result); + } + + public readonly struct TestKey(ulong key) + { + private static ulong _nextKey; + public static TestKey GetNext() => new(_nextKey++); + public readonly ulong Key = key; + + public override string ToString() => $"[{Key}]"; + } + + private sealed class UlongFrequentItemCollection(int capacity) : FrequentItemCollection(capacity) + { + protected override ulong GetKey(in TestKey element) => element.Key; + public void Remove(in TestKey element) => RemoveCore(GetKey(element)); + } + + /// + /// Generates an approximate Zipf distribution. Previous method was 20x faster than MathNet.Numerics, but could only generate 250 samples/sec. + /// This approximate method can generate > 1,000,000 samples/sec. + /// + public class FastZipf + { + private static readonly Random SeededPrng = new(42); + + /// + /// Generate a zipf distribution. + /// + /// The random number generator to use. + /// The number of samples. + /// The skew. s=0 is a uniform distribution. As s increases, high-rank items become rapidly more likely than the rare low-ranked items. + /// N: the cardinality. The total number of items. + /// A zipf distribution. + public static long[] Generate(Random random, int sampleCount, double skew, int cardinality) + { + var sampler = new ZipfRejectionSampler(random, cardinality, skew); + + var samples = new long[sampleCount]; + for (var i = 0; i < sampleCount; i++) + { + samples[i] = sampler.Sample(); + } + + return samples; + } + + /// + /// Generate a zipf distribution. + /// + /// The number of samples. + /// The skew. s=0 is a uniform distribution. As s increases, high-rank items become rapidly more likely than the rare low-ranked items. + /// N: the cardinality. The total number of items. + /// A zipf distribution. + public static long[] Generate(int sampleCount, double skew, int cardinality) => Generate(SeededPrng, sampleCount, skew, cardinality); + } + + // https://jasoncrease.medium.com/rejection-sampling-the-zipf-distribution-6b359792cffa + public class ZipfRejectionSampler + { + private readonly Random _rand; + private readonly double _skew; + private readonly double _t; + + public ZipfRejectionSampler(Random random, long cardinality, double skew) + { + _rand = random; + _skew = skew; + _t = (Math.Pow(cardinality, 1 - skew) - skew) / (1 - skew); + } + + public long Sample() + { + while (true) + { + var invB = bInvCdf(_rand.NextDouble()); + var sampleX = (long)(invB + 1); + var yRand = _rand.NextDouble(); + var ratioTop = Math.Pow(sampleX, -_skew); + var ratioBottom = sampleX <= 1 ? 1 / _t : Math.Pow(invB, -_skew) / _t; + var rat = ratioTop / (ratioBottom * _t); + + if (yRand < rat) + { + return sampleX; + } + } + } + private double bInvCdf(double p) + { + return p * _t switch + { + <= 1 => p * _t, + _ => Math.Pow(p * _t * (1 - _skew) + _skew, 1 / (1 - _skew)) + }; + } + } +} \ No newline at end of file diff --git a/test/TesterInternal/ActivationRepartitioningTests/FrequentEdgeCounterTests.cs b/test/TesterInternal/ActivationRepartitioningTests/FrequentEdgeCounterTests.cs new file mode 100644 index 0000000000..0f0452d4d2 --- /dev/null +++ b/test/TesterInternal/ActivationRepartitioningTests/FrequentEdgeCounterTests.cs @@ -0,0 +1,98 @@ +using Orleans.Placement.Repartitioning; +using Orleans.Runtime.Placement.Repartitioning; +using Xunit; + +namespace UnitTests.ActivationRepartitioningTests; + +[Alias("UnitTests.ActivationRepartitioningTests.IMyPartitionableGrain")] +public interface IMyPartitionableGrain : IGrainWithStringKey +{ + [Alias("GetValue")] + Task GetValue(); + + [Alias("GetValue1")] + Task GetValue(); +} + +[Alias("UnitTests.ActivationRepartitioningTests.IMyGrain`1")] +public interface IMyActiveBalancingGrain : IGrainWithStringKey +{ + Task GetValue(); +} + +[TestCategory("Functional"), TestCategory("ActivationRepartitioning")] +public class FrequentEdgeCounterTests +{ + private static readonly GrainId Id_A = GrainId.Create("A", Guid.NewGuid().ToString()); + private static readonly GrainId Id_B = GrainId.Create("B", Guid.NewGuid().ToString()); + private static readonly GrainId Id_C = GrainId.Create("C", Guid.NewGuid().ToString()); + private static readonly GrainId Id_D = GrainId.Create("D", Guid.NewGuid().ToString()); + private static readonly GrainId Id_E = GrainId.Create("E", Guid.NewGuid().ToString()); + private static readonly GrainId Id_F = GrainId.Create("F", Guid.NewGuid().ToString()); + + [Fact] + public void Add_ShouldIncrementCounter_WhenEdgeIsAdded() + { + var sink = new FrequentEdgeCounter(capacity: 10); + var edge = new Edge(new(Id_A, SiloAddress.Zero, true), new(Id_B, SiloAddress.Zero, true)); + + sink.Add(edge); + + var counters = sink.Elements.ToList(); + + Assert.Single(counters); + Assert.Equal(1u, counters[0].Count); + Assert.Equal(edge, counters[0].Element); + } + + [Fact] + public void Add_ShouldUpdateExistingCounter_WhenSameEdgeIsAddedAgain() + { + var sink = new FrequentEdgeCounter(capacity: 10); + var edge = new Edge(new(Id_A, SiloAddress.Zero, true), new(Id_B, SiloAddress.Zero, true)); + + sink.Add(edge); + sink.Add(edge); + + var counters = sink.Elements.ToList(); + + Assert.Single(counters); + Assert.Equal(2u, counters[0].Count); + Assert.Equal(edge, counters[0].Element); + } + + [Fact] + public void Add_ShouldRemoveMinCounter_WhenCapacityIsReached() + { + var sink = new FrequentEdgeCounter(capacity: 2); + + var edge1 = new Edge(new(Id_A, SiloAddress.Zero, true), new(Id_B, SiloAddress.Zero, true)); + var edge2 = new Edge(new(Id_C, SiloAddress.Zero, true), new(Id_D, SiloAddress.Zero, true)); + + sink.Add(edge1); + sink.Add(edge1); + sink.Add(edge2); + + Assert.Equal(2, sink.Count); + + var edge3 = new Edge(new(Id_E, SiloAddress.Zero, true), new(Id_F, SiloAddress.Zero, true)); + sink.Add(edge3); // should remove the minimum counter (edge2) since capacity is 2 + + var counters = sink.Elements.ToList(); + + Assert.Equal(2, counters.Count); + Assert.DoesNotContain(counters, c => c.Element == edge2); + } + + [Fact] + public void Remove_ShouldRemoveCounter_WhenEdgeIsRemoved() + { + var sink = new FrequentEdgeCounter(capacity: 10); + var edge = new Edge(new(Id_A, SiloAddress.Zero, true), new(Id_B, SiloAddress.Zero, true)); + + sink.Add(edge); + sink.Remove(edge); + + Assert.Empty(sink.Elements); + } +} diff --git a/test/TesterInternal/ActivationRepartitioningTests/MaxHeapTests.cs b/test/TesterInternal/ActivationRepartitioningTests/MaxHeapTests.cs new file mode 100644 index 0000000000..3233c6a1e3 --- /dev/null +++ b/test/TesterInternal/ActivationRepartitioningTests/MaxHeapTests.cs @@ -0,0 +1,76 @@ +using Orleans.Runtime.Placement.Repartitioning; +using Xunit; + +namespace UnitTests.ActivationRepartitioningTests; + +public sealed class MaxHeapTests +{ + public class MyHeapElement(int value) : IHeapElement + { + public int Value { get; set; } = value; + + public int HeapIndex { get; set; } = -1; + + public int CompareTo(MyHeapElement other) => Value.CompareTo(other.Value); + public override string ToString() => $"{Value} @ {HeapIndex}"; + } + + [Fact] + public void HeapPropertyIsMaintained() + { + var edges = new MyHeapElement[100]; + for (int i = 0; i < edges.Length; i++) + { + edges[i] = new MyHeapElement(i); + } + + Random.Shared.Shuffle(edges); + var heap = new MaxHeap([.. edges]); + Assert.Equal(100, heap.Count); + Assert.Equal(99, heap.Peek().Value); + Assert.Equal(99, heap.Peek().Value); + Assert.Equal(99, heap.Pop().Value); + Assert.Equal(98, heap.Pop().Value); + Assert.Equal(98, heap.Count); + Assert.Equal(98, heap.UnorderedElements.Count()); + + var unorderedElements = heap.UnorderedElements.ToArray(); + var edge = unorderedElements[Random.Shared.Next(unorderedElements.Length)]; + edge.Value = 2000; + heap.OnIncreaseElementPriority(edge); + Assert.Equal(2000, heap.Peek().Value); + + // Randomly re-assign priorities to edges + var newScore = 100; + var elements = heap.UnorderedElements.ToArray(); + Random.Shared.Shuffle(elements); + foreach (var element in elements) + { + var originalValue = element.Value; + element.Value = newScore--; + if (element.Value > originalValue) + { + heap.OnIncreaseElementPriority(element); + } + else + { + heap.OnDecreaseElementPriority(element); + } + } + + Assert.Equal(98, heap.UnorderedElements.Count()); + var allElements = new List(); + while (heap.Count > 0) + { + allElements.Add(heap.Pop()); + } + + Assert.Equal(98, allElements.Count); + + var copy = allElements.ToList(); + copy.Sort((a, b) => b.Value.CompareTo(a.Value)); + var expected = string.Join(", ", Enumerable.Range(0, 98).Select(i => 100 - i)); + var actual = string.Join(", ", allElements.Select(c => c.Value)); + Assert.Equal(expected, actual); + } +} diff --git a/test/TesterInternal/ActivationRepartitioningTests/OptionsTests.cs b/test/TesterInternal/ActivationRepartitioningTests/OptionsTests.cs new file mode 100644 index 0000000000..4f58341cf4 --- /dev/null +++ b/test/TesterInternal/ActivationRepartitioningTests/OptionsTests.cs @@ -0,0 +1,52 @@ +using Microsoft.Extensions.Options; +using Orleans.Runtime; +using Orleans.Configuration; +using Xunit; + +namespace UnitTests.ActivationRepartitioningTests; + +[TestCategory("Functional"), TestCategory("ActivationRepartitioning")] +public class OptionsTests +{ + [Fact] + public void ConstantsShouldNotChange() + { + Assert.True(ActivationRepartitionerOptions.DEFAULT_ANCHORING_FILTER_ENABLED); + Assert.Equal(0.01d, ActivationRepartitionerOptions.DEFAULT_PROBABILISTIC_FILTERING_MAX_ALLOWED_ERROR); + Assert.Equal(10_000, ActivationRepartitionerOptions.DEFAULT_MAX_EDGE_COUNT); + Assert.Equal(TimeSpan.FromMinutes(1), ActivationRepartitionerOptions.DEFAULT_MINUMUM_ROUND_PERIOD); + Assert.Equal(TimeSpan.FromMinutes(2), ActivationRepartitionerOptions.DEFAULT_MAXIMUM_ROUND_PERIOD); + Assert.Equal(TimeSpan.FromMinutes(1), ActivationRepartitionerOptions.DEFAULT_RECOVERY_PERIOD); + } + + [Theory] + [InlineData(0, 1, 1, 1, 1, 0.01d)] + [InlineData(1, 0, 1, 1, 1, 0.01d)] + [InlineData(1, 1, 0, 1, 1, 0.01d)] + [InlineData(1, 1, 1, 0, 1, 0.01d)] + [InlineData(1, 1, 1, 1, 0, 0.01d)] + [InlineData(1, 1, 2, 1, 1, 0.01d)] + [InlineData(1, 1, 2, 1, 2, 0.01d)] + [InlineData(1, 1, 2, 1, 2, 0.1d)] + public void InvalidOptionsShouldThrow( + int topHeaviestCommunicationLinks, + int maxUnprocessedEdges, + int minRebalancingPeriodMinutes, + int maxRebalancingPeriodMinutes, + int recoveryPeriodMinutes, + double probabilisticFilteringMaxAllowedErrorRate) + { + var options = new ActivationRepartitionerOptions + { + MaxEdgeCount = topHeaviestCommunicationLinks, + MinRoundPeriod = TimeSpan.FromMinutes(minRebalancingPeriodMinutes), + MaxRoundPeriod = TimeSpan.FromMinutes(maxRebalancingPeriodMinutes), + RecoveryPeriod = TimeSpan.FromMinutes(recoveryPeriodMinutes), + MaxUnprocessedEdges = maxUnprocessedEdges, + ProbabilisticFilteringMaxAllowedErrorRate = probabilisticFilteringMaxAllowedErrorRate + }; + + var validator = new ActivationRepartitionerOptionsValidator(Options.Create(options)); + Assert.Throws(validator.ValidateConfiguration); + } +} \ No newline at end of file diff --git a/test/TesterInternal/ActivationRepartitioningTests/RepartitioningTestBase.cs b/test/TesterInternal/ActivationRepartitioningTests/RepartitioningTestBase.cs new file mode 100644 index 0000000000..2c5877c0c2 --- /dev/null +++ b/test/TesterInternal/ActivationRepartitioningTests/RepartitioningTestBase.cs @@ -0,0 +1,69 @@ +using Orleans.Placement.Repartitioning; +using Orleans.Runtime; +using Orleans.TestingHost; +using TestExtensions; +using Xunit; + +namespace UnitTests.ActivationRepartitioningTests; + +public abstract class RepartitioningTestBase : IAsyncLifetime where TFixture : BaseTestClusterFixture, new() +{ + private readonly TFixture _fixture; + + internal IInternalGrainFactory GrainFactory => _fixture.HostedCluster.InternalGrainFactory; + internal IActivationRepartitionerSystemTarget Silo1Repartitioner { get; } + internal IActivationRepartitionerSystemTarget Silo2Repartitioner { get; } + protected SiloAddress Silo1 { get; } + protected SiloAddress Silo2 { get; } + + public RepartitioningTestBase(TFixture fixture) + { + _fixture = fixture; + + var silos = _fixture.HostedCluster.GetActiveSilos().Select(h => h.SiloAddress).OrderBy(s => s).ToArray(); + Silo1 = silos[0]; + Silo2 = silos[1]; + + Silo1Repartitioner = IActivationRepartitionerSystemTarget.GetReference(GrainFactory, Silo1); + Silo2Repartitioner = IActivationRepartitionerSystemTarget.GetReference(GrainFactory, Silo2); + } + + public virtual async Task InitializeAsync() + { + await GrainFactory.GetGrain(0).ForceActivationCollection(TimeSpan.FromSeconds(0)); + await ResetCounters(); + await AdjustActivationCountOffsets(); + } + + public virtual Task DisposeAsync() + { + return Task.CompletedTask; + } + + public async ValueTask ResetCounters() + { + await Silo1Repartitioner.ResetCounters(); + await Silo2Repartitioner.ResetCounters(); + } + + public async Task AdjustActivationCountOffsets() + { + // Account for imbalances in the initial activation counts. + Dictionary counts = []; + int max = 0; + foreach (var silo in (IEnumerable)_fixture.HostedCluster.Silos) + { + var sysTarget = GrainFactory.GetSystemTarget(Constants.ActivationRepartitionerType, silo.SiloAddress); + var count = counts[silo.SiloAddress] = await sysTarget.GetActivationCount(); + max = Math.Max(max, count); + } + + foreach (var silo in (IEnumerable)_fixture.HostedCluster.Silos) + { + var sysTarget = GrainFactory.GetSystemTarget(Constants.ActivationRepartitionerType, silo.SiloAddress); + var myCount = counts[silo.SiloAddress]; + await sysTarget.SetActivationCountOffset(max - myCount); + } + } + +} diff --git a/test/TesterInternal/ActivationRepartitioningTests/TestMessageFilter.cs b/test/TesterInternal/ActivationRepartitioningTests/TestMessageFilter.cs new file mode 100644 index 0000000000..19e9205776 --- /dev/null +++ b/test/TesterInternal/ActivationRepartitioningTests/TestMessageFilter.cs @@ -0,0 +1,19 @@ +using Orleans.Runtime.Placement; +using Orleans.Runtime.Placement.Repartitioning; + +namespace UnitTests.ActivationRepartitioningTests; + +/// +/// Ignores client messages to make testing easier +/// +internal sealed class TestMessageFilter( + PlacementStrategyResolver strategyResolver, + IClusterManifestProvider clusterManifestProvider, + TimeProvider timeProvider) : IRepartitionerMessageFilter +{ + private readonly RepartitionerMessageFilter _messageFilter = new(strategyResolver, clusterManifestProvider, timeProvider); + + public bool IsAcceptable(Message message, out bool isSenderMigratable, out bool isTargetMigratable) => + _messageFilter.IsAcceptable(message, out isSenderMigratable, out isTargetMigratable) && + !message.SendingGrain.IsClient() && !message.TargetGrain.IsClient(); +} \ No newline at end of file diff --git a/test/TesterInternal/ActivationsLifeCycleTests/ActivationCollectorTests.cs b/test/TesterInternal/ActivationsLifeCycleTests/ActivationCollectorTests.cs index 6144a79055..bf18050afc 100644 --- a/test/TesterInternal/ActivationsLifeCycleTests/ActivationCollectorTests.cs +++ b/test/TesterInternal/ActivationsLifeCycleTests/ActivationCollectorTests.cs @@ -489,56 +489,6 @@ async Task workerFunc() } } - [Fact, TestCategory("ActivationCollector"), TestCategory("Performance"), TestCategory("CorePerf")] - public async Task ActivationCollectorShouldNotCauseMessageLoss() - { - await Initialize(DEFAULT_IDLE_TIMEOUT); - - const int idleGrainCount = 0; - const int busyGrainCount = 500; - var idleGrainTypeName = RuntimeTypeNameFormatter.Format(typeof(IdleActivationGcTestGrain1)); - var busyGrainTypeName = RuntimeTypeNameFormatter.Format(typeof(BusyActivationGcTestGrain1)); - const int burstCount = 100; - - List tasks0 = new List(); - List busyGrains = new List(); - logger.LogInformation("ActivationCollectorShouldNotCauseMessageLoss: activating {Count} busy grains.", busyGrainCount); - for (var i = 0; i < busyGrainCount; ++i) - { - IBusyActivationGcTestGrain1 g = this.testCluster.GrainFactory.GetGrain(Guid.NewGuid()); - busyGrains.Add(g); - tasks0.Add(g.Nop()); - } - - await busyGrains[0].EnableBurstOnCollection(burstCount); - - logger.LogInformation( - "ActivationCollectorShouldNotCauseMessageLoss: activating {Count} idle grains.", - idleGrainCount); - tasks0.Clear(); - for (var i = 0; i < idleGrainCount; ++i) - { - IIdleActivationGcTestGrain1 g = this.testCluster.GrainFactory.GetGrain(Guid.NewGuid()); - tasks0.Add(g.Nop()); - } - await Task.WhenAll(tasks0); - - int activationsCreated = await TestUtils.GetActivationCount(this.testCluster.GrainFactory, idleGrainTypeName) + await TestUtils.GetActivationCount(this.testCluster.GrainFactory, busyGrainTypeName); - Assert.Equal(idleGrainCount + busyGrainCount, activationsCreated); - - logger.LogInformation( - "ActivationCollectorShouldNotCauseMessageLoss: grains activated; waiting {WaitSeconds} sec (activation GC idle timeout is {DefaultIdleTimeout} sec).", - WAIT_TIME.TotalSeconds, - DEFAULT_IDLE_TIMEOUT.TotalSeconds); - await Task.Delay(WAIT_TIME); - - // we should have only collected grains from the idle category (IdleActivationGcTestGrain1). - int idleActivationsNotCollected = await TestUtils.GetActivationCount(this.testCluster.GrainFactory, idleGrainTypeName); - int busyActivationsNotCollected = await TestUtils.GetActivationCount(this.testCluster.GrainFactory, busyGrainTypeName); - Assert.Equal(0, idleActivationsNotCollected); - Assert.Equal(busyGrainCount, busyActivationsNotCollected); - } - [Fact, TestCategory("ActivationCollector"), TestCategory("Functional")] public async Task ActivationCollectorShouldCollectByCollectionSpecificAgeLimitForTwelveSeconds() { diff --git a/test/TesterInternal/GrainDirectoryPartitionTests.cs b/test/TesterInternal/GrainDirectoryPartitionTests.cs index 00b9c06481..1e9f373295 100644 --- a/test/TesterInternal/GrainDirectoryPartitionTests.cs +++ b/test/TesterInternal/GrainDirectoryPartitionTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Orleans.Configuration; @@ -168,6 +169,8 @@ public Dictionary GetApproximateSiloStatuses(bool onlyA : new Dictionary(_content); } + public ImmutableArray GetActiveSilos() => _content.Keys.ToImmutableArray(); + public void SetSiloStatus(SiloAddress siloAddress, SiloStatus status) => _content[siloAddress] = status; public bool IsDeadSilo(SiloAddress silo) => GetApproximateSiloStatus(silo) == SiloStatus.Dead; diff --git a/test/TesterInternal/LivenessTests/ConsistentRingProviderTests.cs b/test/TesterInternal/LivenessTests/ConsistentRingProviderTests.cs index b998746aa4..d4c536251c 100644 --- a/test/TesterInternal/LivenessTests/ConsistentRingProviderTests.cs +++ b/test/TesterInternal/LivenessTests/ConsistentRingProviderTests.cs @@ -7,6 +7,7 @@ using Xunit.Abstractions; using TestExtensions; using System.Net; +using System.Collections.Immutable; namespace UnitTests.LivenessTests { @@ -181,6 +182,7 @@ public bool TryGetSiloName(SiloAddress siloAddress, out string siloName) } public bool UnSubscribeFromSiloStatusEvents(ISiloStatusListener observer) => _subscribers.Remove(observer); + public ImmutableArray GetActiveSilos() => [.. GetApproximateSiloStatuses(onlyActive: true).Keys]; } } } diff --git a/test/TesterInternal/TesterInternal.csproj b/test/TesterInternal/TesterInternal.csproj index 2ffde2ac10..ceefb25f91 100644 --- a/test/TesterInternal/TesterInternal.csproj +++ b/test/TesterInternal/TesterInternal.csproj @@ -7,6 +7,7 @@ +