Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
092c896
Persist dashboard run history in SQLite
JamesNK Jul 14, 2026
d8aeaaf
Fix
JamesNK Jul 14, 2026
de9c229
Add dashboard persistence modes
JamesNK Jul 14, 2026
e9d36cc
Embed native libraries in managed bundle
JamesNK Jul 14, 2026
7cfecfe
Default AppHost dashboard persistence to runs
JamesNK Jul 14, 2026
af7bbc6
Fix dashboard persistence tests
JamesNK Jul 14, 2026
fdc4e59
Address dashboard persistence review feedback
JamesNK Jul 14, 2026
884a6fe
Optimize dashboard SQLite repositories
JamesNK Jul 14, 2026
2ddb14a
Optimize telemetry field value queries
JamesNK Jul 15, 2026
05d0b77
Address dashboard persistence review feedback
JamesNK Jul 15, 2026
9c89b70
Handle in-use dashboard run directories
JamesNK Jul 15, 2026
0c4847a
Use native SQLite string matching
JamesNK Jul 15, 2026
c73fd2c
Push trace error filtering into repository
JamesNK Jul 15, 2026
53407a8
Optimize traces page queries
JamesNK Jul 15, 2026
4e2d2b2
Order traces by trace ID
JamesNK Jul 15, 2026
1a6d889
Optimize structured logs page queries
JamesNK Jul 15, 2026
ea5cf90
Show dashboard run selector in header
JamesNK Jul 16, 2026
5efcea4
Update selector
JamesNK Jul 16, 2026
f44c609
Switch dashboard runs without reloading
JamesNK Jul 16, 2026
2f8f442
Update
JamesNK Jul 16, 2026
fc0608c
Address dashboard persistence review feedback
JamesNK Jul 16, 2026
db3864b
Fix culture-sensitive dashboard run test
JamesNK Jul 16, 2026
4d94581
Simplify metric telemetry repository API
JamesNK Jul 17, 2026
6bb9c7a
Refactor dashboard repository ownership
JamesNK Jul 17, 2026
1ca6b9c
Fix current-run console log subscription
JamesNK Jul 17, 2026
dc34175
Track console log capture by run
JamesNK Jul 17, 2026
9032210
Enable dashboard SQLite connection pooling
JamesNK Jul 17, 2026
72f35f1
Address dashboard run lifecycle review feedback
JamesNK Jul 17, 2026
89ff07c
Add dashboard SQLite tracing and optimize metric ingestion
JamesNK Jul 17, 2026
3106ab5
Optimize dashboard telemetry storage and self-tracing
JamesNK Jul 17, 2026
c38c3b5
Address dashboard run history review feedback
JamesNK Jul 18, 2026
8cb31ba
Clean up
JamesNK Jul 18, 2026
1642af8
Optimize dashboard SQLite metric ingestion
JamesNK Jul 18, 2026
9c71f3b
WIP
JamesNK Jul 18, 2026
65ab73a
WIP
JamesNK Jul 18, 2026
5161186
Fix dashboard run schema and URL handling
JamesNK Jul 18, 2026
2a80651
Address dashboard run history review feedback
JamesNK Jul 19, 2026
7bfa93d
Fix dashboard resource snapshot state tracking
JamesNK Jul 19, 2026
93f3b8f
Respect explicit dashboard persistence configuration
JamesNK Jul 19, 2026
7ed8004
Fix resource watcher disposal race
JamesNK Jul 20, 2026
0caeaed
Add shared dashboard activity source
JamesNK Jul 20, 2026
4f375d1
Fix dashboard repository resolution hang
JamesNK Jul 20, 2026
bee6b2f
Defer dashboard repository dependencies
JamesNK Jul 20, 2026
e4bafe3
Scope dashboard SQLite pool cleanup
JamesNK Jul 20, 2026
7b5c453
Batch dashboard console log inserts
JamesNK Jul 20, 2026
98dbb84
Stream dashboard peer recalculation
JamesNK Jul 20, 2026
bddf914
Optimize dashboard run selection loading
JamesNK Jul 20, 2026
a1efd7b
Simplify dashboard run IDs
JamesNK Jul 20, 2026
6388684
Add dashboard run persistence controls
JamesNK Jul 20, 2026
da8639e
Optimize dashboard trace persistence
JamesNK Jul 21, 2026
dd69dff
Validate dashboard span relationships
JamesNK Jul 21, 2026
4a780b1
Improve dashboard telemetry import profiling
JamesNK Jul 21, 2026
3c804da
Optimize dashboard trace ingestion
JamesNK Jul 21, 2026
3e9ed89
Optimize dashboard SQLite batch inserts
JamesNK Jul 21, 2026
5d175cd
Fix ManageDataDialog test service registration
JamesNK Jul 21, 2026
3098ebe
Throttle dashboard telemetry callbacks
JamesNK Jul 21, 2026
5f1d25f
Order instrumented trace resources before peers
JamesNK Jul 21, 2026
facd5c2
Fix ManageDataDialog repository test after merge
JamesNK Jul 22, 2026
523a89d
Avoid dashboard OTLP telemetry loop
JamesNK Jul 22, 2026
6d7fcea
WIP
JamesNK Jul 22, 2026
5aa9f43
Improve historical telemetry and metrics queries
JamesNK Jul 22, 2026
44ebcdc
Improve metrics aggregation and chart updates
JamesNK Jul 22, 2026
5ebc325
Use SQLite telemetry in dashboard component tests
JamesNK Jul 22, 2026
6c69393
Fix metric aggregation across dimensions
JamesNK Jul 22, 2026
f67224e
Improve metric queries with incremental rollups
JamesNK Jul 22, 2026
fa47ba2
Optimize dashboard metric queries
JamesNK Jul 22, 2026
c8311fa
Refine dashboard metric rollup intervals
JamesNK Jul 22, 2026
cee0951
Move metric instrument storage to repositories
JamesNK Jul 23, 2026
c5f6ba2
Use telemetry repository read-only state
JamesNK Jul 23, 2026
dd7fd53
Move metric instrument state to in-memory repository
JamesNK Jul 23, 2026
69bc5f0
Improve dashboard metric chart performance
JamesNK Jul 23, 2026
3ade89e
Update dashboard metric refresh and injections
JamesNK Jul 23, 2026
30ce412
Combine scope and attribute lookup
JamesNK Jul 23, 2026
67ab96e
Improve dashboard reliability under telemetry load
JamesNK Jul 24, 2026
6dae9cc
Improve dashboard telemetry storage scalability
JamesNK Jul 24, 2026
2d28438
Address dashboard run history review feedback
JamesNK Jul 24, 2026
6f58399
Fix ApplicationName dashboard client tests
JamesNK Jul 24, 2026
832564e
Improve stress telemetry commands and chart updates
JamesNK Jul 24, 2026
3eeae1a
WIP
JamesNK Jul 24, 2026
82ae07a
Fix disabled argument validation for interactive resource commands
JamesNK Jul 25, 2026
2866ec5
Improve dashboard test performance
JamesNK Jul 25, 2026
ec38162
Optimize SQLite generated ID batch inserts
JamesNK Jul 25, 2026
9ad5dcd
Share dashboard databases across data sources
JamesNK Jul 25, 2026
c7b7cb7
Centralize dashboard SQLite pool cleanup
JamesNK Jul 26, 2026
eee96de
Configure dashboard SQLite connections via connection string
JamesNK Jul 26, 2026
6ce37a7
Initialize dashboard SQLite schema during startup
JamesNK Jul 26, 2026
9661d3c
Improve dashboard telemetry persistence
JamesNK Jul 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/instructions/dashboard.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,16 @@ applyTo: "src/Aspire.Dashboard/**/*.{cs,razor,js}"
- For bounded channels feeding one consumer, prefer `BoundedChannelFullMode.DropOldest` and set `SingleReader = true`.
- Use `FormatHelpers` for culture-aware date/time/number display. Reserve invariant formatting for intentionally fixed diagnostic formats.
- Localize user-visible dashboard text with resource-backed localizers. Prefer typed localizers and `nameof` keys when practical, but existing model/helpers also generate localized UI text.

## Blazor components

- Avoid `@code` blocks and substantial C# logic in `.razor` files. Keep `.razor` files focused on markup, directives, and simple binding or event expressions; put component state, lifecycle methods, event handlers, and other logic in the matching `.razor.cs` code-behind partial class for better IDE and compiler support.
- Declare injected component dependencies as public, required, init-only properties:

```csharp
[Inject]
public required IDashboardClient DashboardClient { get; init; }
```

- `public` keeps dependencies visible to component and test infrastructure, `required` expresses that the component cannot operate without the service, and `init` prevents reassignment after component activation.
- Do not use non-public injected properties, mutable `set` accessors, or null-forgiving initializers such as `= null!;`. These weaken compile-time validation and hide missing dependencies when components are constructed in tests.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
<PackageVersion Include="Microsoft.Agents.AI.OpenAI" Version="1.5.0" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="7.0.1" />
<PackageVersion Include="Microsoft.Data.SqlClient.Extensions.Azure" Version="1.0.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.9" />
Comment thread
JamesNK marked this conversation as resolved.
<PackageVersion Include="Microsoft.FluentUI.AspNetCore.Components" Version="4.14.1" />
<PackageVersion Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="4.14.1" />
<PackageVersion Include="Milvus.Client" Version="2.3.0-preview.1" /> <!-- No stable release available -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" />
<PackageReference Include="Microsoft.Data.Sqlite" />
</ItemGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions benchmarks/Aspire.Dashboard.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ internal static class Program
{
private static void Main(string[] args)
{
SQLitePCL.Batteries_V2.Init();
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
}
}
216 changes: 216 additions & 0 deletions benchmarks/Aspire.Dashboard.Benchmarks/SqliteTraceBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text;
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.ServiceClient;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Toolchains.InProcess.NoEmit;
using Google.Protobuf;
using Google.Protobuf.Collections;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using OpenTelemetry.Proto.Collector.Trace.V1;
using OpenTelemetry.Proto.Common.V1;
using OpenTelemetry.Proto.Resource.V1;
using OpenTelemetry.Proto.Trace.V1;
using OtlpProtoSpan = OpenTelemetry.Proto.Trace.V1.Span;

namespace Aspire.Dashboard.Benchmarks;

[MemoryDiagnoser]
[Config(typeof(Config))]
public class SqliteTraceBenchmarks
{
private const string TraceFileEnvironmentVariable = "ASPIRE_DASHBOARD_TRACE_BENCHMARK_FILE";
private const int GeneratedSpanCount = 10_000;

private string _temporaryDirectory = null!;
private DashboardSqliteDatabase _database = null!;
private SqliteTelemetryRepository _repository = null!;
private RepeatedField<ResourceSpans> _appendResourceSpans = null!;
private long _appendIndex;

[GlobalSetup]
public async Task Setup()
{
_temporaryDirectory = Directory.CreateTempSubdirectory("aspire-dashboard-trace-benchmark-").FullName;
_database = new DashboardSqliteDatabase(Path.Combine(_temporaryDirectory, "dashboard.db"));
_repository = CreateRepository(_database);

var resourceSpans = LoadResourceSpans();
var context = new AddContext();
await _repository.AddTracesAsync(context, resourceSpans);
if (context.FailureCount != 0)
{
throw new InvalidOperationException($"Failed to ingest {context.FailureCount} benchmark spans.");
}

_appendResourceSpans = CreateAppendResourceSpans(resourceSpans);
}

[GlobalCleanup]
public void Cleanup()
{
_repository.Dispose();
_database.ClearPool();
_database.Dispose();
Directory.Delete(_temporaryDirectory, recursive: true);
}

[Benchmark(Description = "SQLite: summarize large trace")]
public int GetTraceSummaries()
{
var response = _repository.GetTraceSummaries(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 100,
Filters = []
});

return response.PagedResult.Items.Count;
}

[Benchmark(Description = "SQLite: append one span to large trace")]
public async Task<int> AppendSpan()
{
var appendSpan = _appendResourceSpans[0].ScopeSpans[0].Spans[0];
appendSpan.SpanId = ByteString.CopyFrom(BitConverter.GetBytes(long.MaxValue - Interlocked.Increment(ref _appendIndex)));
var context = new AddContext();
await _repository.AddTracesAsync(context, _appendResourceSpans);
return context.SuccessCount;
}

private static RepeatedField<ResourceSpans> LoadResourceSpans()
{
var traceFile = Environment.GetEnvironmentVariable(TraceFileEnvironmentVariable);
if (string.IsNullOrWhiteSpace(traceFile))
{
return CreateGeneratedResourceSpans();
}

var request = JsonParser.Default.Parse<ExportTraceServiceRequest>(File.ReadAllText(traceFile));
return request.ResourceSpans;
}

private static RepeatedField<ResourceSpans> CreateGeneratedResourceSpans()
{
var resourceSpans = new ResourceSpans
{
Resource = CreateResource("benchmark-app"),
ScopeSpans =
{
new ScopeSpans
{
Scope = new InstrumentationScope { Name = "BenchmarkScope" }
}
}
};
var scopeSpans = resourceSpans.ScopeSpans[0];
var traceId = ByteString.CopyFrom(Encoding.UTF8.GetBytes("benchmark-trace"));
var startTime = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
for (var spanIndex = 0; spanIndex < GeneratedSpanCount; spanIndex++)
{
var spanStartTime = startTime.AddTicks(spanIndex);
scopeSpans.Spans.Add(new OtlpProtoSpan
{
TraceId = traceId,
SpanId = CreateSpanId(spanIndex),
ParentSpanId = spanIndex == 0 ? ByteString.Empty : CreateSpanId(spanIndex - 1),
Name = spanIndex == 0 ? "root-span" : $"span-{spanIndex}",
Kind = OtlpProtoSpan.Types.SpanKind.Internal,
StartTimeUnixNano = DateTimeToUnixNanoseconds(spanStartTime),
EndTimeUnixNano = DateTimeToUnixNanoseconds(spanStartTime.AddMilliseconds(5))
});
}

return [resourceSpans];
}

private static RepeatedField<ResourceSpans> CreateAppendResourceSpans(RepeatedField<ResourceSpans> resourceSpans)
{
var firstSpan = resourceSpans.SelectMany(resource => resource.ScopeSpans).SelectMany(scope => scope.Spans).First();
return
[
new ResourceSpans
{
Resource = CreateResource("append-app"),
ScopeSpans =
{
new ScopeSpans
{
Scope = new InstrumentationScope { Name = "AppendScope" },
Spans =
{
new OtlpProtoSpan
{
TraceId = firstSpan.TraceId,
SpanId = ByteString.CopyFrom(Encoding.UTF8.GetBytes("append-span-0001")),
ParentSpanId = firstSpan.SpanId,
Name = "appended-span",
Kind = OtlpProtoSpan.Types.SpanKind.Internal,
StartTimeUnixNano = firstSpan.EndTimeUnixNano + 100,
EndTimeUnixNano = firstSpan.EndTimeUnixNano + 200
}
}
}
}
}
];
}

private static SqliteTelemetryRepository CreateRepository(DashboardSqliteDatabase database)
{
return new SqliteTelemetryRepository(
database,
NullLoggerFactory.Instance,
Options.Create(new DashboardOptions
{
TelemetryLimits = new TelemetryLimitOptions { MaxTraceCount = 1_000 }
}),
new PauseManager(),
[]);
}

private static Resource CreateResource(string name)
{
return new Resource
{
Attributes =
{
new KeyValue { Key = "service.name", Value = new AnyValue { StringValue = name } }
}
};
}

private static ByteString CreateSpanId(int spanIndex) =>
ByteString.CopyFrom(Encoding.UTF8.GetBytes($"span-{spanIndex:0000}"));

private static ulong DateTimeToUnixNanoseconds(DateTime dateTime)
{
var timeSinceEpoch = dateTime.ToUniversalTime() - DateTime.UnixEpoch;
return (ulong)timeSinceEpoch.Ticks * 100;
}

private sealed class Config : ManualConfig
{
public Config()
{
AddJob(Job.Default
.WithToolchain(InProcessNoEmitToolchain.Instance)
.WithWarmupCount(1)
.WithIterationCount(3)
.WithInvocationCount(1)
.WithUnrollFactor(1));

AddDiagnoser(MemoryDiagnoser.Default);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Aspire.Dashboard.Model.Otlp;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.ServiceClient;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
Expand Down Expand Up @@ -63,39 +64,53 @@ public class TelemetryRepositoryBenchmarks
];

private RepeatedField<ResourceSpans> _resourceSpans = [];
private TelemetryRepository _queryRepository = null!;
private string _temporaryDirectory = null!;
private DashboardSqliteDatabase _queryDatabase = null!;
private SqliteTelemetryRepository _queryRepository = null!;

[GlobalSetup]
public void Setup()
public async Task Setup()
{
_resourceSpans = CreateResourceSpans(TraceCount, SpansPerTrace);
_queryRepository = CreateRepository();
_queryRepository.AddTraces(new AddContext(), _resourceSpans);
_temporaryDirectory = Directory.CreateTempSubdirectory("aspire-dashboard-telemetry-benchmark-").FullName;
_queryDatabase = new DashboardSqliteDatabase(Path.Combine(_temporaryDirectory, "query.db"));
_queryRepository = CreateRepository(_queryDatabase);
await _queryRepository.AddTracesAsync(new AddContext(), _resourceSpans);
}

[GlobalCleanup]
public void Cleanup()
{
_queryRepository.Dispose();
_queryDatabase.ClearPool();
_queryDatabase.Dispose();
Directory.Delete(_temporaryDirectory, recursive: true);
}

[Benchmark(Description = "TelemetryRepository: add 10k spans")]
public int AddTracesLargeBatch()
public async Task<int> AddTracesLargeBatch()
{
using var repository = CreateRepository();
var context = new AddContext();
repository.AddTraces(context, _resourceSpans);
var temporaryDirectory = Directory.CreateTempSubdirectory("aspire-dashboard-telemetry-add-benchmark-");
using var database = new DashboardSqliteDatabase(Path.Combine(temporaryDirectory.FullName, "add.db"));
int successCount;
using (var repository = CreateRepository(database))
{
var context = new AddContext();
await repository.AddTracesAsync(context, _resourceSpans);
successCount = context.SuccessCount;
}
database.ClearPool();
Directory.Delete(temporaryDirectory.FullName, recursive: true);

return context.SuccessCount;
return successCount;
}

[Benchmark(Description = "TelemetryRepository: query no filters")]
public int GetTracesNoFilters()
{
var result = _queryRepository.GetTraces(new GetTracesRequest
{
ResourceKey = null,
FilterText = string.Empty,
ResourceKeys = [],
Filters = [],
StartIndex = 0,
Count = 100
Expand All @@ -109,8 +124,7 @@ public int GetTracesDurationFilter()
{
var result = _queryRepository.GetTraces(new GetTracesRequest
{
ResourceKey = null,
FilterText = string.Empty,
ResourceKeys = [],
Filters = _durationFilters,
StartIndex = 0,
Count = 100
Expand All @@ -124,8 +138,7 @@ public int GetTracesNoMatchDurationFilter()
{
var result = _queryRepository.GetTraces(new GetTracesRequest
{
ResourceKey = null,
FilterText = string.Empty,
ResourceKeys = [],
Filters = _noMatchDurationFilters,
StartIndex = 0,
Count = 100
Expand All @@ -139,8 +152,7 @@ public int GetTracesNoMatchAttributeFilter()
{
var result = _queryRepository.GetTraces(new GetTracesRequest
{
ResourceKey = null,
FilterText = string.Empty,
ResourceKeys = [],
Filters = _noMatchAttributeFilters,
StartIndex = 0,
Count = 100
Expand All @@ -149,9 +161,10 @@ public int GetTracesNoMatchAttributeFilter()
return result.PagedResult.Items.Count;
}

private static TelemetryRepository CreateRepository()
private static SqliteTelemetryRepository CreateRepository(DashboardSqliteDatabase database)
{
return new TelemetryRepository(
return new SqliteTelemetryRepository(
database,
NullLoggerFactory.Instance,
Options.Create(new DashboardOptions
{
Expand Down
Loading
Loading