Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 15 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,48 +156,47 @@ The [.NET Aspire Sample](https://github.com/purview-dev/telemetry-sourcegenerato

## Performance

Benchmarked on 13th Gen Intel Core i9-13900KF, .NET SDK 10.0.201. See the [Performance](https://github.com/purview-dev/telemetry-sourcegenerator/wiki/Performance) wiki page for full cross-runtime results.
Benchmarked on 13th Gen Intel Core i9-13900KF, .NET SDK 10.0.401. See the [Performance](https://github.com/purview-dev/telemetry-sourcegenerator/wiki/Performance) wiki page for full cross-runtime results.

### Activities (.NET 10.0)

| Scenario | HasListener | Manual | Generated | Ratio |
| --- | --- | --- | --- | --- |
| start + complete | False | 0.56 ns | 0.55 ns | 0.99x |
| start + complete | True | 218 ns / 1008 B | 204 ns / 1008 B | 0.94x |
| start + fail | True | 198 ns / 920 B | 189 ns / 920 B | 0.87x |
| start + complete | True | 212 ns / 1008 B | 213 ns / 1008 B | 1.01x |
| start + fail | True | 206 ns / 920 B | 208 ns / 920 B | 0.98x |

Generated activities match or outperform hand-written code with identical allocations.
Generated activities are within ~1% of hand-written code with identical allocations.

### Logging (.NET 10.0)

| Scenario | HasLogging | LoggerMessage.Define | Generated v1 | Generated v2 |
| --- | --- | --- | --- | --- |
| single Info call | False | 0.21 ns | 0.18 ns | 0.21 ns |
| single Info call | True | 4.29 ns | 4.24 ns (0.99x) | 4.20 ns (0.98x) |
| full lifecycle (4 calls) | True | 17.73 ns | 19.52 ns (1.10x) | 18.81 ns (1.06x) |
| single Info call | True | 4.59 ns | 7.35 ns (1.60x) | 4.57 ns (1.00x) |
| full lifecycle (4 calls) | True | 16.77 ns | 18.76 ns (1.12x) | 18.58 ns (1.11x) |

Both v1 and v2 allocate **0 bytes** per call on all runtimes. Both generated variants are within ~2% of the manual baseline for single calls; full lifecycle cost is within ~10%.
Both v1 and v2 allocate **0 bytes** per call on all runtimes. Generated v2 (state-based) matches the manual baseline for single calls on .NET 10.0; v1 (LoggerMessage.Define pattern) is slower for single calls on this runtime.

### Multi-Target (.NET 10.0, Activity + Logging + Metrics)

| Scenario | HasListener | Single-target | Multi-target generated | Multi-target manual |
|---|---|---|---|---|
| start + complete | True | 203 ns / 1008 B | 230 ns / 1032 B (1.13x) | 233 ns / 1032 B (1.15x) |
| start + complete | True | 221 ns / 1008 B | 246 ns / 1032 B (1.11x) | 248 ns / 1032 B (1.13x) |

Adding full Activity+Logging+Metrics multi-target generation costs ~13% over Activity-only on .NET 10.0 — matching hand-written multi-target code within 2%.
Adding full Activity+Logging+Metrics multi-target generation costs ~11% over Activity-only on .NET 10.0 — matching hand-written multi-target code within ~1%.

### Metrics (.NET 10.0)

| Scenario | Generated | Notes |
| --- | --- | --- |
| auto-counter (0 tags) | 0.37 ns | - |
| auto-counter (1 tag) | 0.37 ns | - |
| up-down counter | 0.35 ns | - |
| histogram (0 tags) | 0.36 ns | - |
| auto-counter (0 tags) | 0.38 ns | - |
| auto-counter (1 tag) | 0.35 ns | - |
| up-down counter | 0.39 ns | - |
| histogram (0 tags) | 0.37 ns | - |
| histogram (1 tag) | 0.36 ns | - |
| 4+ tags (TagList) | 4-7 ns | Stack-allocated `TagList` |
| 4+ tags (TagList) | 3.5-9.2 ns | Stack-allocated `TagList` |

All instruments are **0 allocations**. Manual baselines are JIT-eliminated on .NET 10.0 (no active listener), so absolute times are shown. On .NET 8/9, generated and manual are within ~25%.
All instruments are **0 allocations** on all runtimes.

## v4 Breaking Changes

Expand Down
180 changes: 106 additions & 74 deletions docs/wiki/Performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,55 +12,55 @@ dotnet run --project benchmarks/Purview.Telemetry.Benchmarks/Purview.Telemetry.B
Results are written to `BenchmarkDotNet.Artifacts/results/` as `*-report-github.md`, `*.csv`, and `*.html`.

> [!IMPORTANT]
> BenchmarkDotNet numbers are only meaningful from **Release** builds. The tables below reflect the latest published run. Regenerate them with `just benchmark-docs` before relying on them for a specific machine/SDK combination.
> BenchmarkDotNet numbers are only meaningful from **Release** builds. Run the suite fresh (`just benchmark-docs`) before relying on the tables below for a specific machine/SDK combination.

## Environment

```
BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8117/25H2/2025Update)
BenchmarkDotNet v0.15.8, Windows 11 (10.0.28020.2991)
13th Gen Intel Core i9-13900KF 3.00GHz, 1 CPU, 32 logical and 24 physical cores
.NET SDK 10.0.201
.NET 10.0 : .NET 10.0.5 (10.0.5, 10.0.526.15411), X64 RyuJIT x86-64-v3
.NET 8.0 : .NET 8.0.25 (8.0.25, 8.0.2526.11203), X64 RyuJIT x86-64-v3
.NET 9.0 : .NET 9.0.14 (9.0.14, 9.0.1426.11910), X64 RyuJIT x86-64-v3
.NET SDK 10.0.401
.NET 10.0 : .NET 10.0.12 (10.0.12, 10.0.1226.42308), X64 RyuJIT x86-64-v3
.NET 8.0 : .NET 8.0.31 (8.0.31, 8.0.3126.42015), X64 RyuJIT x86-64-v3
.NET 9.0 : .NET 9.0.20 (9.0.20, 9.0.2026.41315), X64 RyuJIT x86-64-v3
```

> **Note:** .NET Framework targets did not produce results in this run and are excluded from the tables below.
> **Note:** the .NET Framework 4.7/4.8 jobs were executed but produced no results in this run and are excluded from the tables below.

## Activities

**Source:** `ActivityBenchmarks`

Compares the source-generator-produced `ActivityOnlyTelemetryCore` against a hand-written `ManualActivityTelemetry` under two conditions: no listener registered (fast-path) and a full-sampling `ActivityListener` active (production path).

| Method | Runtime | HasListener | Mean | Ratio | Allocated | Alloc Ratio |
| --- | --- | --- | --- | --- | --- | --- |
| Manual: start + complete | .NET 10.0 | False | 0.56 ns | 1.00 | - | NA |
| Generated: start + complete | .NET 10.0 | False | 0.55 ns | 0.99 | - | NA |
| Manual: start + fail | .NET 10.0 | False | 0.72 ns | 1.29 | - | NA |
| Generated: start + fail | .NET 10.0 | False | 0.52 ns | 0.93 | - | NA |
| Manual: start + complete | .NET 8.0 | False | 0.71 ns | 1.00 | - | NA |
| Generated: start + complete | .NET 8.0 | False | 0.71 ns | 1.01 | - | NA |
| Manual: start + fail | .NET 8.0 | False | 0.91 ns | 1.30 | - | NA |
| Generated: start + fail | .NET 8.0 | False | 0.90 ns | 1.28 | - | NA |
| Manual: start + complete | .NET 9.0 | False | 0.54 ns | 1.00 | - | NA |
| Generated: start + complete | .NET 9.0 | False | 0.55 ns | 1.01 | - | NA |
| Manual: start + fail | .NET 9.0 | False | 0.73 ns | 1.36 | - | NA |
| Generated: start + fail | .NET 9.0 | False | 0.70 ns | 1.30 | - | NA |
| **Manual: start + complete** | **.NET 10.0** | **True** | **217.75 ns** | **1.00** | **1008 B** | **1.00** |
| Generated: start + complete | .NET 10.0 | True | 204.03 ns | 0.94 | 1008 B | 1.00 |
| Manual: start + fail | .NET 10.0 | True | 198.43 ns | 0.91 | 920 B | 0.91 |
| Generated: start + fail | .NET 10.0 | True | 189.26 ns | 0.87 | 920 B | 0.91 |
| Manual: start + complete | .NET 8.0 | True | 241.11 ns | 1.00 | 1008 B | 1.00 |
| Generated: start + complete | .NET 8.0 | True | 250.49 ns | 1.04 | 1008 B | 1.00 |
| Manual: start + fail | .NET 8.0 | True | 223.87 ns | 0.93 | 920 B | 0.91 |
| Generated: start + fail | .NET 8.0 | True | 222.14 ns | 0.92 | 920 B | 0.91 |
| Manual: start + complete | .NET 9.0 | True | 216.84 ns | 1.00 | 1008 B | 1.00 |
| Generated: start + complete | .NET 9.0 | True | 214.30 ns | 0.99 | 1008 B | 1.00 |
| Manual: start + fail | .NET 9.0 | True | 200.43 ns | 0.92 | 920 B | 0.91 |
| Generated: start + fail | .NET 9.0 | True | 222.14 ns | 1.03 | 920 B | 0.91 |

**Interpretation:** Generated activities match or outperform hand-written code and allocate identically across all tested runtimes.
| Method | Runtime | HasListener | Mean | Ratio | Allocated |
| --- | --- | --- | --- | --- | --- |
| Manual: start + complete | .NET 10.0 | False | 0.56 ns | 1.00 | - |
| Generated: start + complete | .NET 10.0 | False | 0.55 ns | 0.99 | - |
| Manual: start + fail | .NET 10.0 | False | 0.73 ns | 1.32 | - |
| Generated: start + fail | .NET 10.0 | False | 0.56 ns | 1.00 | - |
| Manual: start + complete | .NET 8.0 | False | 0.72 ns | 1.00 | - |
| Generated: start + complete | .NET 8.0 | False | 0.94 ns | 1.31 | - |
| Manual: start + fail | .NET 8.0 | False | 0.92 ns | 1.29 | - |
| Generated: start + fail | .NET 8.0 | False | 0.72 ns | 1.00 | - |
| Manual: start + complete | .NET 9.0 | False | 0.57 ns | 1.00 | - |
| Generated: start + complete | .NET 9.0 | False | 0.74 ns | 1.29 | - |
| Manual: start + fail | .NET 9.0 | False | 0.38 ns | 0.66 | - |
| Generated: start + fail | .NET 9.0 | False | 0.91 ns | 1.60 | - |
| **Manual: start + complete** | **.NET 10.0** | **True** | **211.68 ns** | **1.00** | **1008 B** |
| Generated: start + complete | .NET 10.0 | True | 213.33 ns | 1.01 | 1008 B |
| Manual: start + fail | .NET 10.0 | True | 205.59 ns | 0.97 | 920 B |
| Generated: start + fail | .NET 10.0 | True | 207.58 ns | 0.98 | 920 B |
| Manual: start + complete | .NET 8.0 | True | 244.55 ns | 1.00 | 1008 B |
| Generated: start + complete | .NET 8.0 | True | 257.92 ns | 1.05 | 1008 B |
| Manual: start + fail | .NET 8.0 | True | 235.83 ns | 0.96 | 920 B |
| Generated: start + fail | .NET 8.0 | True | 234.55 ns | 0.96 | 920 B |
| Manual: start + complete | .NET 9.0 | True | 221.63 ns | 1.00 | 1008 B |
| Generated: start + complete | .NET 9.0 | True | 221.03 ns | 1.00 | 1008 B |
| Manual: start + fail | .NET 9.0 | True | 206.32 ns | 0.93 | 920 B |
| Generated: start + fail | .NET 9.0 | True | 203.48 ns | 0.92 | 920 B |

**Interpretation:** Generated activities are within ~5% of hand-written code and allocate identically across all tested runtimes. When a listener is active the absolute times are dominated by the framework's Activity creation, not the generated code.

## Logging

Expand All @@ -70,29 +70,26 @@ Compares three logging approaches: hand-written `LoggerMessage.Define` (gold-sta

| Method | Runtime | HasLogging | Mean | Ratio | Allocated |
| --- | --- | --- | --- | --- | --- |
| Manual: LoggerMessage.Define — single Info | .NET 10.0 | False | 0.21 ns | 1.01 | - |
| Generated v1 — single Info | .NET 10.0 | False | 0.18 ns | 0.89 | - |
| Generated v2 — single Info | .NET 10.0 | False | 0.21 ns | 1.00 | - |
| **Manual: LoggerMessage.Define — single Info** | **.NET 10.0** | **True** | **4.29 ns** | **1.00** | **-** |
| Generated v1 — single Info | .NET 10.0 | True | 4.24 ns | 0.99 | - |
| Generated v2 — single Info | .NET 10.0 | True | 4.20 ns | 0.98 | - |
| Manual: LoggerMessage.Define — full lifecycle | .NET 10.0 | True | 17.73 ns | 4.13 | - |
| Generated v1 — full lifecycle | .NET 10.0 | True | 19.52 ns | 4.55 | - |
| Generated v2 — full lifecycle | .NET 10.0 | True | 18.81 ns | 4.38 | - |
| Manual: LoggerMessage.Define — single Info | .NET 8.0 | True | 7.57 ns | 1.00 | - |
| Generated v1 — single Info | .NET 8.0 | True | 7.34 ns | 0.97 | - |
| Generated v2 — single Info | .NET 8.0 | True | 7.26 ns | 0.96 | - |
| Manual: LoggerMessage.Define — full lifecycle | .NET 8.0 | True | 28.73 ns | 3.79 | - |
| Generated v1 — full lifecycle | .NET 8.0 | True | 29.90 ns | 3.95 | - |
| Generated v2 — full lifecycle | .NET 8.0 | True | 29.85 ns | 3.94 | - |
| Manual: LoggerMessage.Define — single Info | .NET 9.0 | True | 6.10 ns | 1.00 | - |
| Generated v1 — single Info | .NET 9.0 | True | 6.22 ns | 1.02 | - |
| Generated v2 — single Info | .NET 9.0 | True | 6.25 ns | 1.03 | - |
| Manual: LoggerMessage.Define — full lifecycle | .NET 9.0 | True | 23.88 ns | 3.92 | - |
| Generated v1 — full lifecycle | .NET 9.0 | True | 24.55 ns | 4.03 | - |
| Generated v2 — full lifecycle | .NET 9.0 | True | 24.75 ns | 4.06 | - |

**Interpretation:** Generated v1 and v2 both allocate **zero bytes** across all runtimes. On .NET 10.0 with logging active, v1 (4.24 ns) and v2 (4.20 ns) are within ~1–2% of the manual `LoggerMessage.Define` baseline (4.29 ns).
| **Manual: single Info call** | **.NET 10.0** | **True** | **4.59 ns** | **1.00** | **-** |
| Generated v1 — single Info call | .NET 10.0 | True | 7.35 ns | 1.60 | - |
| Generated v2 — single Info call | .NET 10.0 | True | 4.57 ns | 1.00 | - |
| Manual: full lifecycle (4 calls) | .NET 10.0 | True | 16.77 ns | 3.66 | - |
| Generated v1 — full lifecycle | .NET 10.0 | True | 18.76 ns | 4.09 | - |
| Generated v2 — full lifecycle | .NET 10.0 | True | 18.58 ns | 4.05 | - |
| Manual: single Info call | .NET 8.0 | True | 7.40 ns | 1.00 | - |
| Generated v1 — single Info call | .NET 8.0 | True | 7.61 ns | 1.03 | - |
| Generated v2 — single Info call | .NET 8.0 | True | 7.38 ns | 1.00 | - |
| Manual: full lifecycle | .NET 8.0 | True | 29.68 ns | 4.01 | - |
| Generated v1 — full lifecycle | .NET 8.0 | True | 30.96 ns | 4.18 | - |
| Generated v2 — full lifecycle | .NET 8.0 | True | 30.36 ns | 4.10 | - |
| Manual: single Info call | .NET 9.0 | True | 8.19 ns | 1.00 | - |
| Generated v1 — single Info call | .NET 9.0 | True | 7.24 ns | 0.88 | - |
| Generated v2 — single Info call | .NET 9.0 | True | 6.95 ns | 0.85 | - |
| Manual: full lifecycle | .NET 9.0 | True | 25.42 ns | 3.10 | - |
| Generated v1 — full lifecycle | .NET 9.0 | True | 25.99 ns | 3.17 | - |
| Generated v2 — full lifecycle | .NET 9.0 | True | 26.61 ns | 3.25 | - |

**Interpretation:** Generated v1 and v2 both allocate **zero bytes** per call across all runtimes. On .NET 10.0, v2 (state-based) matches hand-written `LoggerMessage.Define` (4.57 vs 4.59 ns single call; 18.58 vs 16.77 ns full lifecycle); on .NET 9.0 the generated variants are faster than the manual baseline. The full-lifecycle cost is dominated by the four `IsEnabled` checks and message-formatting paths that all three implementations share.

## Multi-target

Expand All @@ -102,30 +99,65 @@ Measures the overhead of emitting Activity + Logging + Metrics from a single met

| Method | Runtime | HasListener | Mean | Ratio | Allocated | Alloc Ratio |
| --- | --- | --- | --- | --- | --- | --- |
| **Single-target (generated): start + complete** | **.NET 10.0** | **True** | **203.33 ns** | **1.00** | **1008 B** | **1.00** |
| Multi-target (generated): start + complete | .NET 10.0 | True | 229.91 ns | 1.13 | 1032 B | 1.02 |
| Multi-target (manual): start + complete | .NET 10.0 | True | 233.48 ns | 1.15 | 1032 B | 1.02 |
| Multi-target (generated): start + complete + record latency | .NET 10.0 | True | 224.27 ns | 1.10 | 1032 B | 1.02 |
| Multi-target (manual): start + complete + record latency | .NET 10.0 | True | 217.24 ns | 1.07 | 1032 B | 1.02 |
| **Single-target (generated): start + complete** | **.NET 10.0** | **True** | **220.82 ns** | **1.00** | **1008 B** | **1.00** |
| Multi-target (generated): start + complete | .NET 10.0 | True | 245.55 ns | 1.11 | 1032 B | 1.02 |
| Multi-target (manual): start + complete | .NET 10.0 | True | 248.35 ns | 1.13 | 1032 B | 1.02 |
| Multi-target (generated): start + complete + record latency | .NET 10.0 | True | 227.38 ns | 1.03 | 1032 B | 1.02 |
| Multi-target (manual): start + complete + record latency | .NET 10.0 | True | 213.89 ns | 0.97 | 1032 B | 1.02 |
| Single-target (generated): start + complete | .NET 8.0 | True | 242.55 ns | 1.00 | 1008 B | 1.00 |
| Multi-target (generated): start + complete | .NET 8.0 | True | 262.91 ns | 1.08 | 1032 B | 1.02 |
| Multi-target (manual): start + complete | .NET 8.0 | True | 273.61 ns | 1.13 | 1032 B | 1.02 |
| Single-target (generated): start + complete | .NET 9.0 | True | 218.89 ns | 1.00 | 1008 B | 1.00 |
| Multi-target (generated): start + complete | .NET 9.0 | True | 242.07 ns | 1.11 | 1032 B | 1.02 |
| Multi-target (manual): start + complete | .NET 9.0 | True | 233.33 ns | 1.07 | 1032 B | 1.02 |

**Interpretation:** When an Activity listener is active (production path), multi-target generation adds ~11% overhead over single-target Activity-only on .NET 10.0 — the real cost of the extra log call and metric increment, not generated-code overhead. The generated multi-target code matches hand-written multi-target code within ~1%. See the [Generated Output](Generated-Output.md) page for what the multi-target implementation looks like.

## Logger multi-target

**Source:** `LoggerMultiTargetBenchmarks`

**Interpretation:** When an Activity listener is active (production path), multi-target generation adds ~13% overhead over single-target Activity-only on .NET 10.0, matching hand-written multi-target code within ~2%. See the [Generated Output](Generated-Output.md) page for what the multi-target implementation looks like.
Compares single-target logging-only vs. multi-target (Activity + Logging + Metrics) generated code with a hand-written multi-target baseline.

| Method | Runtime | HasListener | Mean | Ratio | Allocated |
| --- | --- | --- | --- | --- | --- |
| **Multi-target (manual): start + complete** | **.NET 10.0** | **True** | **272.34 ns** | **1.00** | **1032 B** |
| Multi-target (generated v1): start + complete | .NET 10.0 | True | 260.98 ns | 0.96 | 1032 B |
| Multi-target (generated v2): start + complete | .NET 10.0 | True | 264.47 ns | 0.97 | 1032 B |
| Multi-target (manual): full lifecycle | .NET 10.0 | True | 240.42 ns | 0.88 | 1032 B |
| Multi-target (generated v1): full lifecycle | .NET 10.0 | True | 241.50 ns | 0.89 | 1032 B |
| Multi-target (generated v2): full lifecycle | .NET 10.0 | True | 239.78 ns | 0.88 | 1032 B |
| Single-target (generated v1): full lifecycle | .NET 10.0 | True | 19.86 ns | 0.07 | - |
| Single-target (generated v2): full lifecycle | .NET 10.0 | True | 18.24 ns | 0.07 | - |

**Interpretation:** Generated and manual multi-target implementations are within ~4% of each other on .NET 10.0, allocating identically (1032 B with a listener active). Logging-only accounts for a small fraction (~19 ns) of the multi-target cost (~260 ns); Activity creation dominates when a listener is active.

## Metrics

**Source:** `MetricsBenchmarks` and `TagListBenchmarks`

All instruments are **0 allocations**. Manual baselines are JIT-eliminated on .NET 10.0 (no active listener), so absolute times are shown for generated instruments; on .NET 8/9 generated and manual are within ~25%.
All instruments are **0 allocations** on every runtime.

| Scenario | Generated (.NET 10.0) | Generated (.NET 8.0) | Generated (.NET 9.0) |
| --- | --- | --- | --- |
| auto-counter (0 tags) | 0.38 ns | 0.55 ns | 0.21 ns |
| auto-counter (1 tag) | 0.35 ns | 0.56 ns | 0.30 ns |
| up-down counter | 0.39 ns | 0.39 ns | 0.20 ns |
| histogram (0 tags) | 0.37 ns | 0.35 ns | 0.18 ns |
| histogram (1 tag) | 0.36 ns | 0.36 ns | 0.37 ns |

The source generator uses a tag-count optimization: methods with fewer than 4 tags pass inline `KeyValuePair` parameters (no heap allocation), while methods with 4 or more tags use a stack-allocated [`TagList`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.taglist) struct.

| Scenario | Generated | Notes |
| --- | --- | --- |
| auto-counter (0 tags) | 0.37 ns | - |
| auto-counter (1 tag) | 0.37 ns | - |
| up-down counter | 0.35 ns | - |
| histogram (0 tags) | 0.36 ns | - |
| histogram (1 tag) | 0.36 ns | - |
| 4+ tags (TagList) | 4–7 ns | Stack-allocated `TagList` |
| Scenario | .NET 10.0 | .NET 8.0 | .NET 9.0 |
| --- | --- | --- | --- |
| 0 tags: histogram record | 0.37 ns | 0.56 ns | 0.19 ns |
| 1 tag: auto-counter add | 0.37 ns | 0.36 ns | 0.19 ns |
| 3 tags: histogram record | 0.55 ns | 0.74 ns | 0.55 ns |
| 4 tags (TagList): auto-counter add | 3.52 ns | 7.79 ns | 3.62 ns |
| 5 tags (TagList): auto-counter add | 9.22 ns | 8.95 ns | 9.37 ns |
| 6 tags (TagList): histogram record | 5.57 ns | 4.32 ns | 3.99 ns |

The source generator uses a tag-count optimization: methods with fewer than 4 tags pass inline `KeyValuePair` parameters (no heap allocation), while methods with 4 or more tags use a stack-allocated [`TagList`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.taglist) struct. The TagList path costs 12–19× more CPU than the inline path on .NET 10.0, but both remain in the single-digit-nanosecond range with zero allocations.
The TagList path costs 9–25× more CPU than the inline path on .NET 10.0, but both remain in the single-digit-nanosecond range with zero allocations.

## Observable instruments

Expand Down