Live .NET runtime metrics in a Neovim side panel — CPU, memory, GC, thread pool, exceptions and HTTP latency, with btop-style graphs — right next to the debugger.

Recorded while debugging jellyfin under netcoredbg,
with a curl loop calling a few endpoints. The demo is a script:
assets/demo.tape.
The panel opens by itself when an nvim-dap session starts a .NET process, shows when the debugger has paused it, and closes when the session ends. It can also attach to any running .NET process, debugger or not.
The numbers come from dotnet-counters,
Microsoft's diagnostics tool, which reads the runtime's EventPipe counters. The plugin runs
dotnet-counters collect --format json against the process, reads its output as it grows and
renders the panel once per sample.
CPU Usage (%)),
.NET 9+ publishes System.Runtime meters (dotnet.process.cpu.time); both are mapped onto the
same metrics.dotnet-counters: dotnet tool install -g dotnet-counters:DotnetCounters attach works without it:checkhealth dotnet-counters checks all of the above.
With lazy.nvim:
{
"alessandropietrobelli/dotnet-counters.nvim",
-- Load early (setup is cheap) so it can hook into nvim-dap before the first session
event = "VeryLazy",
cmd = "DotnetCounters",
opts = {},
keys = {
{ "<leader>dm", "<cmd>DotnetCounters toggle<cr>", desc = ".NET metrics panel" },
{ "<leader>dM", "<cmd>DotnetCounters attach<cr>", desc = ".NET metrics: attach to process" },
},
}
| Command | |
|---|---|
:DotnetCounters / :DotnetCounters toggle |
Show or hide the panel |
:DotnetCounters attach [pid] |
Attach to a process; without a pid, pick one from dotnet-counters ps |
:DotnetCounters detach |
Stop collecting, keep the last data on screen |
:DotnetCounters reset |
Clear history, peaks and totals |
:DotnetCounters open / close |
Open or close the panel |
In the panel, q closes it and r resets the history.
Defaults:
require("dotnet-counters").setup({
-- dotnet-counters executable; nil looks in PATH and then ~/.dotnet/tools
cmd = nil,
-- Providers passed to `dotnet-counters collect --counters`
counters = "System.Runtime,Microsoft.AspNetCore.Hosting",
-- Seconds between samples
refresh_interval = 1,
-- Restart the collector when its output file grows past this many bytes
max_file_size = 32 * 1024 * 1024,
-- Samples kept for the graphs
history = 200,
dap = {
-- Attach and open the panel when a debug session of one of these adapters starts a process
enabled = true,
adapters = { "coreclr", "netcoredbg" },
-- Close the panel when the debug session ends
close_on_exit = true,
},
window = {
position = "right",
width = 46,
graph_height = 2,
},
-- Graph colours: {warn, critical} in each metric's unit. Below warn is green, then yellow, then
-- red. Graph heights stay relative to the visible maximum; only the colour uses these.
thresholds = {
cpu = { 25, 60 }, -- % of the whole machine
memory = { 0.10, 0.25 }, -- fraction of the machine's RAM (working set and GC heap)
alloc_rate = { 256 * 1024 * 1024, 1024 * 1024 * 1024 }, -- bytes/s
gc_time = { 5, 10 }, -- % of time paused by the GC
http_active = nil, -- requests in flight: no threshold, always green
http_p95 = { 0.2, 1 }, -- seconds
},
})
Every group links to a standard one by default, so the panel follows your colour scheme:
| Group | Default link |
|---|---|
DotnetCountersTitle |
Title |
DotnetCountersSection |
Special |
DotnetCountersLabel |
Normal |
DotnetCountersValue |
Number |
DotnetCountersMuted |
Comment |
DotnetCountersRunning |
DiagnosticOk |
DotnetCountersPaused |
DiagnosticWarn |
DotnetCountersLow / Mid / High |
DiagnosticOk / DiagnosticWarn / DiagnosticError |
A graph's height is relative to its visible maximum, so its shape is always readable, but its
colour comes from absolute thresholds (thresholds in the configuration): a memory graph that
fills its height at a steady 300 MB stays green, and only turns yellow and red as the process uses
10% and 25% of the machine's RAM.
dotnet-counters, dotnet-monitor or IDE profiler is already collecting from the same process,
attaching fails with "concurrent sessions are not supported" and the panel says so.dotnet-counters only writes to a file, in chunks of ~10 KB, so
samples land with roughly one refresh interval of lag.max_file_size (a gap of a sample or two) and deleted when collection stops.dotnet-counters reports as deltas; the panel shows work items per second instead. On .NET 8
all three are shown.git clone --depth 1 https://github.com/nvim-lua/plenary.nvim deps/plenary.nvim
nvim --headless -u tests/minimal_init.lua -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/minimal_init.lua'}"
The fixtures in tests/fixtures/ are real dotnet-counters output from a .NET 8 app, a .NET 10
app and an ASP.NET Core app, cut mid-array the way the plugin reads them while they are written.
Two more plugins for .NET in Neovim, by the same author:
dotnet-counters.nvim is an independent project. It is not affiliated with, endorsed or sponsored by
Microsoft or the .NET Foundation. It runs Microsoft's dotnet-counters tool, whose name it takes to
say what it works with; .NET and ASP.NET Core are trademarks of their respective owners.