BlazorAmpowering.Coverage.Runtime 1.0.0

dotnet add package BlazorAmpowering.Coverage.Runtime --version 1.0.0
                    
NuGet\Install-Package BlazorAmpowering.Coverage.Runtime -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="BlazorAmpowering.Coverage.Runtime" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="BlazorAmpowering.Coverage.Runtime" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="BlazorAmpowering.Coverage.Runtime" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add BlazorAmpowering.Coverage.Runtime --version 1.0.0
                    
#r "nuget: BlazorAmpowering.Coverage.Runtime, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package BlazorAmpowering.Coverage.Runtime@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=BlazorAmpowering.Coverage.Runtime&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=BlazorAmpowering.Coverage.Runtime&version=1.0.0
                    
Install as a Cake Tool

BlazorAmpowering.Coverage.Runtime

Runtime registry and OpenTelemetry/Prometheus exporter for production code coverage in .NET and Blazor applications.

This package is the runtime counterpart of BlazorAmpowering.Coverage (the Fody IL weaver). The weaver injects call-tracking instructions into your assemblies at build time; this package provides the shared registry those instructions write into, and the OTel instrumentation that exports the data to Prometheus / Mimir.


Relationship between the two packages

Build time
──────────
  BlazorAmpowering.Coverage (weaver)
    ├── Injects Hit(slot) calls into every method body
    └── Generates a module initializer that calls CoverageRegistry.Register()

Runtime
───────
  BlazorAmpowering.Coverage.Runtime
    ├── CoverageRegistry  ← receives Register() and Hit() calls from instrumented assemblies
    └── OTel instruments  ← ObservableGauge + ObservableCounter → Prometheus / Mimir

The weaver package is a build-time-only tool (PrivateAssets="all" in the consuming project). Your libraries do not carry a dependency on OpenTelemetry — only the host entry project does, through this runtime package.


Packages

Package Role
BlazorAmpowering.Coverage Fody weaver — add to every project you want to instrument
BlazorAmpowering.Coverage.Runtime Runtime registry + OTel/Prometheus export — entry project only

Installation

Add this package to the project that starts your application (Blazor Server, ASP.NET Core Web API, etc.):

<ItemGroup>
  <PackageReference Include="BlazorAmpowering.Coverage.Runtime" Version="1.0.0" />
</ItemGroup>

Register the OTel pipeline in Program.cs:

builder.AddProductionCoverage();

Place this after builder.AddBlazorTelemetry() when using BlazorAmpowering.Observability, so both pipelines share the same OTel MeterProvider and push to the same endpoint.

This package is needed once, in the entry project. Every library project only needs the weaver (BlazorAmpowering.Coverage). Libraries do not reference this package directly.


How CoverageRegistry works internally

Registration phase (Register)

When the CLR loads an instrumented assembly for the first time it executes the module initializer generated by the weaver. That initializer calls:

int baseSlot = CoverageRegistry.Register(methodCount, methodMeta);

Register() is thread-safe and additive:

  1. Acquires an internal lock around the shared arrays.
  2. Calls Array.Resize to extend every array by methodCount slots.
  3. Copies the metadata strings (class, method, file, line) into the newly allocated slots.
  4. Returns the base offset — the index of the first slot allocated for this assembly.

The base offset is stored in a static int s_baseSlot field generated by the weaver inside the assembly. Instrumented methods compute their absolute slot as s_baseSlot + localSlot at call time.

Multiple assemblies can register concurrently (e.g., during parallel static initialization). Because Register() serializes all resizes through the lock, there are no torn reads or overlapping slot ranges.

Hit recording phase (Hit)

Every instrumented method calls:

CoverageRegistry.Hit(baseSlot + localSlot);

Hit() is designed to be as fast as possible:

  1. Snapshots _hitMap and _callMap array references locally (one read each, no lock).
  2. Performs an unsigned bounds check ((uint)slotIndex >= (uint)hitMap.Length) — a single branch, predicted as not-taken after the first call.
  3. If the method has not been hit yet, calls Volatile.Write(ref hitMap[slotIndex], 1) — a store-release that makes the hit visible to the OTel export thread without a full memory barrier.
  4. Always calls Interlocked.Increment(ref callMap[slotIndex]) for the call counter.

Because _hitMap and _callMap are snapshotted before access, a concurrent Register() call that replaces the array reference with a larger one is safe — Hit() will use the old (still valid) snapshot for that invocation and pick up the new array on the next call.

Per-call overhead: ~4–5 ns on a modern x64 CPU.


Prometheus metrics exported

coverage_method_info — Gauge, always 1

coverage_method_info{method_id="a1b2c3d4", class="MyApp.Services.OrderService",
                     method="PlaceOrder", file="OrderService.cs", line="42"} 1

This is a Prometheus info metric — a gauge permanently equal to 1 whose value is irrelevant and whose labels carry structured metadata. The same pattern is used by kube_pod_info, go_build_info, and many exporters. It allows PromQL joins (* on(method_id) group_left(class,method,file,line)) to attach rich metadata to the hit and counter metrics without repeating those strings on every scrape.

Exported once per registered method. The labels are:

Label Content
method_id FNV-32 hash of the full CLR method signature — stable across compilations
class Fully qualified class name
method Method name as it appears in IL (includes compiler-generated suffixes for async state machines)
file Source file name extracted from the PDB sequence points at weave time
line First source line of the method body

coverage_method_hit — Gauge, 0 or 1

coverage_method_hit{method_id="a1b2c3d4"} 1

1 if Hit() has been called for this method at least once since the process started, 0 otherwise. Resets to 0 on restart (no persistence). Use this metric to identify dead code: methods that are never 1 in production across a meaningful observation window are strong candidates for removal.

coverage_method_calls_total — Counter

coverage_method_calls_total{method_id="a1b2c3d4"} 4271

Monotonically increasing call count since startup. Use this metric to identify hot paths, measure load distribution across methods, and detect sudden changes in calling frequency.


Stable method_id

The method_id is computed by the weaver using the FNV-32a (Fowler–Noll–Vo) hash algorithm applied to the full CLR method name:

MyApp.Services.OrderService::PlaceOrder(System.Int32,System.Decimal)

Properties:

  • Deterministic across compilations — as long as the method name and parameter types are unchanged, the ID is the same in every build. This means Prometheus time series survive a redeployment without creating new label combinations.
  • Collision-resistant at scale — FNV-32 produces a 32-bit value; collisions are theoretically possible in very large assemblies (> ~65 000 methods), but in practice the probability is negligible for typical application sizes.
  • Printable as hex — the ID appears in Prometheus as an 8-character lowercase hex string (e.g., a1b2c3d4).

PromQL queries

Overall coverage rate:

count(coverage_method_hit == 1) / count(coverage_method_hit) * 100

Methods with full metadata (for the Grafana panel — hit view):

coverage_method_hit * on(method_id) group_left(class, method, file, line)
  coverage_method_info

Call counts with full metadata (for the Grafana panel — calls view):

coverage_method_calls_total * on(method_id) group_left(class, method, file, line)
  coverage_method_info

Top 20 most called methods (hot paths):

topk(20, coverage_method_calls_total * on(method_id) group_left(class, method, file, line)
  coverage_method_info)

Methods never called (dead code candidates):

coverage_method_hit == 0

Coverage rate per source file:

sum by(file) (coverage_method_hit * on(method_id) group_left(file) coverage_method_info)
  /
sum by(file) (coverage_method_info)
* 100

Grafana panel

Install the BlazorAmpowering Grafana panel plugin and configure two Prometheus queries on the panel:

Ref ID Query
A coverage_method_hit * on(method_id) group_left(class,method,file,line) coverage_method_info
B coverage_method_calls_total * on(method_id) group_left(class,method,file,line) coverage_method_info

In the panel options set Ref ID — Hit to A and Ref ID — Total calls to B. The panel renders a collapsible file → class → method tree with hit/miss badges, call counts, and coverage percentages at every level.


OTel pipeline

AddProductionCoverage() registers three OTel instruments under the meter name BlazorAmpowering.Coverage:

Instrument OTel type Maps to
coverage_method_info ObservableGauge<int> Prometheus Gauge
coverage_method_hit ObservableGauge<int> Prometheus Gauge
coverage_method_calls_total ObservableCounter<long> Prometheus Counter

The instruments are compatible with any OTel metrics exporter: Prometheus pull (UseOpenTelemetryPrometheusScrapingEndpoint), OTLP push to Grafana Mimir, Azure Monitor, etc. No additional configuration is required beyond what is already set up for BlazorAmpowering.Observability.


License

Apache-2.0

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 82 5/17/2026