Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
63e51b6
Persist dashboard run history in SQLite
JamesNK Jul 14, 2026
36562b5
Fix
JamesNK Jul 14, 2026
4460235
Add dashboard persistence modes
JamesNK Jul 14, 2026
340c469
Embed native libraries in managed bundle
JamesNK Jul 14, 2026
7c6be41
Default AppHost dashboard persistence to runs
JamesNK Jul 14, 2026
d4d9647
Fix dashboard persistence tests
JamesNK Jul 14, 2026
aebafa0
Address dashboard persistence review feedback
JamesNK Jul 14, 2026
be9f032
Optimize dashboard SQLite repositories
JamesNK Jul 14, 2026
7e1b399
Optimize telemetry field value queries
JamesNK Jul 15, 2026
2c6c3a3
Address dashboard persistence review feedback
JamesNK Jul 15, 2026
197d6e7
Handle in-use dashboard run directories
JamesNK Jul 15, 2026
b41f58b
Use native SQLite string matching
JamesNK Jul 15, 2026
5874636
Push trace error filtering into repository
JamesNK Jul 15, 2026
96670cc
Optimize traces page queries
JamesNK Jul 15, 2026
80383bc
Order traces by trace ID
JamesNK Jul 15, 2026
241f28e
Optimize structured logs page queries
JamesNK Jul 15, 2026
7062bb6
Show dashboard run selector in header
JamesNK Jul 16, 2026
c9167c2
Update selector
JamesNK Jul 16, 2026
e71aaed
Switch dashboard runs without reloading
JamesNK Jul 16, 2026
0b0e992
Update
JamesNK Jul 16, 2026
8bb600c
Address dashboard persistence review feedback
JamesNK Jul 16, 2026
cfb2ad0
Fix culture-sensitive dashboard run test
JamesNK Jul 16, 2026
6784a2f
Simplify metric telemetry repository API
JamesNK Jul 17, 2026
a05a871
Refactor dashboard repository ownership
JamesNK Jul 17, 2026
b4bb79a
Fix current-run console log subscription
JamesNK Jul 17, 2026
7e48442
Track console log capture by run
JamesNK Jul 17, 2026
86536ff
Enable dashboard SQLite connection pooling
JamesNK Jul 17, 2026
28f8b4a
Address dashboard run lifecycle review feedback
JamesNK Jul 17, 2026
c51b0aa
Add dashboard SQLite tracing and optimize metric ingestion
JamesNK Jul 17, 2026
4790916
Optimize dashboard telemetry storage and self-tracing
JamesNK Jul 17, 2026
a03c9cb
Address dashboard run history review feedback
JamesNK Jul 18, 2026
31ee009
Clean up
JamesNK Jul 18, 2026
9b982c9
Optimize dashboard SQLite metric ingestion
JamesNK Jul 18, 2026
c83937c
WIP
JamesNK Jul 18, 2026
0fd9cb6
WIP
JamesNK Jul 18, 2026
8f66128
Fix dashboard run schema and URL handling
JamesNK Jul 18, 2026
2e3878e
Address dashboard run history review feedback
JamesNK Jul 19, 2026
a1fa0f6
Fix dashboard resource snapshot state tracking
JamesNK Jul 19, 2026
6b1da4c
Respect explicit dashboard persistence configuration
JamesNK Jul 19, 2026
849b908
Fix resource watcher disposal race
JamesNK Jul 20, 2026
d6bd0f2
Add shared dashboard activity source
JamesNK Jul 20, 2026
64af917
Fix dashboard repository resolution hang
JamesNK Jul 20, 2026
92c79f1
Defer dashboard repository dependencies
JamesNK Jul 20, 2026
46708d5
Scope dashboard SQLite pool cleanup
JamesNK Jul 20, 2026
5222579
Batch dashboard console log inserts
JamesNK Jul 20, 2026
8bf75e2
Stream dashboard peer recalculation
JamesNK Jul 20, 2026
08dff81
Optimize dashboard run selection loading
JamesNK Jul 20, 2026
0ce4329
Simplify dashboard run IDs
JamesNK Jul 20, 2026
e0f225b
Add dashboard run persistence controls
JamesNK Jul 20, 2026
9a33a62
Optimize dashboard trace persistence
JamesNK Jul 21, 2026
54507ad
Validate dashboard span relationships
JamesNK Jul 21, 2026
e67d164
Improve dashboard telemetry import profiling
JamesNK Jul 21, 2026
2c72a61
Optimize dashboard trace ingestion
JamesNK Jul 21, 2026
9a0859a
Optimize dashboard SQLite batch inserts
JamesNK Jul 21, 2026
bf2a6b1
Fix ManageDataDialog test service registration
JamesNK Jul 21, 2026
af44e70
Throttle dashboard telemetry callbacks
JamesNK Jul 21, 2026
a2a060d
Order instrumented trace resources before peers
JamesNK Jul 21, 2026
ee52073
Fix ManageDataDialog repository test after merge
JamesNK Jul 22, 2026
1cab86a
Avoid dashboard OTLP telemetry loop
JamesNK Jul 22, 2026
2538a7d
WIP
JamesNK Jul 22, 2026
150cd9e
Improve historical telemetry and metrics queries
JamesNK Jul 22, 2026
05f9f74
Improve metrics aggregation and chart updates
JamesNK Jul 22, 2026
2b76f14
Use SQLite telemetry in dashboard component tests
JamesNK Jul 22, 2026
f564dec
Fix metric aggregation across dimensions
JamesNK Jul 22, 2026
42ee3c5
Improve metric queries with incremental rollups
JamesNK Jul 22, 2026
b457c13
Optimize dashboard metric queries
JamesNK Jul 22, 2026
6921bd8
Refine dashboard metric rollup intervals
JamesNK Jul 22, 2026
dcf27f7
Move metric instrument storage to repositories
JamesNK Jul 23, 2026
57f10d5
Use telemetry repository read-only state
JamesNK Jul 23, 2026
56d98cb
Move metric instrument state to in-memory repository
JamesNK Jul 23, 2026
21e6165
Improve dashboard metric chart performance
JamesNK Jul 23, 2026
da1192b
Update dashboard metric refresh and injections
JamesNK Jul 23, 2026
6c5f0d5
Combine scope and attribute lookup
JamesNK Jul 23, 2026
50dff24
Improve dashboard reliability under telemetry load
JamesNK Jul 24, 2026
4afb5dd
Improve dashboard telemetry storage scalability
JamesNK Jul 24, 2026
aab7df6
Address dashboard run history review feedback
JamesNK Jul 24, 2026
93903f2
Fix ApplicationName dashboard client tests
JamesNK Jul 24, 2026
8715908
Improve stress telemetry commands and chart updates
JamesNK Jul 24, 2026
bbefc2f
WIP
JamesNK Jul 24, 2026
0f89649
Fix disabled argument validation for interactive resource commands
JamesNK Jul 25, 2026
8d24915
Improve dashboard test performance
JamesNK Jul 25, 2026
38cc4e9
Optimize SQLite generated ID batch inserts
JamesNK Jul 25, 2026
ee5e0cb
Share dashboard databases across data sources
JamesNK Jul 25, 2026
4bbbdf1
Centralize dashboard SQLite pool cleanup
JamesNK Jul 26, 2026
a6fa615
Configure dashboard SQLite connections via connection string
JamesNK Jul 26, 2026
e11fcb4
Initialize dashboard SQLite schema during startup
JamesNK Jul 26, 2026
393ad15
Improve dashboard telemetry persistence
JamesNK Jul 26, 2026
78cf145
Normalize metric dimension storage
JamesNK Jul 26, 2026
44d2b30
Extract metric dimension materialization
JamesNK Jul 26, 2026
42d772a
Stabilize dashboard run lease test
JamesNK Jul 26, 2026
de07857
Fix flaky interactions initialization test
JamesNK Jul 26, 2026
38e8b0e
Fix dashboard test failures
JamesNK Jul 27, 2026
c2b7adb
Improve dashboard telemetry handling under load
JamesNK Jul 27, 2026
b79fcac
Skip trace resource deletes for new traces
JamesNK Jul 27, 2026
5e19884
Optimize SQLite trace resource summaries
JamesNK Jul 27, 2026
db1df9b
Inject TimeProvider into SqliteTelemetryRepository
JamesNK Jul 27, 2026
7ba4d2e
Document dashboard persistence
JamesNK Jul 28, 2026
f84853d
Description
JamesNK Jul 28, 2026
e7b8cee
Update sqlite version
JamesNK Jul 28, 2026
e20b8b0
Log unexpected chart update failures
JamesNK Jul 28, 2026
08cfaac
Fix flaky dashboard data source tests
JamesNK Jul 28, 2026
7933dfa
Address dashboard run history review feedback
JamesNK Jul 29, 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.10" />
<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);
}
}
217 changes: 217 additions & 0 deletions benchmarks/Aspire.Dashboard.Benchmarks/SqliteTraceBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// 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 static readonly DateTime s_generatedTraceStartTime = DateTime.UnixEpoch;

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"));
for (var spanIndex = 0; spanIndex < GeneratedSpanCount; spanIndex++)
{
var spanStartTime = s_generatedTraceStartTime.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(),
TimeProvider.System,
[]);
}

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 All @@ -161,6 +174,7 @@ private static TelemetryRepository CreateRepository()
}
}),
new PauseManager(),
TimeProvider.System,
[]);
}

Expand Down
Loading
Loading