A Windows memory analyser for developer workstations. It answers one question — what is actually eating my RAM, and how much do I get back if I kill it? — and a second one that no snapshot can answer: what got bigger while the machine was up?
Terminal dashboard, one-shot reports, and a background recorder that captures a week of uptime to CSV for trend analysis. No dependencies, no installer, no service.
Memory 17.9 GB used of 31.7 GB (56%), 13.8 GB free
Commit 18.9 GB of 85.7 GB
Procs 312, 8.54 GB private, 9.31 GB shared + kernel
PROCESS CNT PRIVATE SHARED CPU TAGS
chrome.exe 20 1.61 GB 1.13 GB 0.0% browser
sqlservr.exe 1 855 MB 67 MB 0.0% db RECLAIM
svchost.exe 98 439 MB 1.33 GB 0.0% system
dwm.exe 1 384 MB 125 MB 12.2% system
explorer.exe 1 332 MB 323 MB 0.0%
MsMpEng.exe 1 329 MB 191 MB 0.0% system
Dell.TechHub.Instrumentation. 1 224 MB 262 MB 0.0% oem
DDPM.Subagent.User.exe 1 195 MB 200 MB 0.0% oem
Task Manager tells you what is big right now. That turns out to be the least useful of the three things you need to know.
It cannot tell you what you would get back. The number that answers "how much RAM does killing this free" is the private working set. A process can be the single largest resident thing on the machine while owning almost none of it — on the machine this was written for, Dell's update service holds ~10 GB of memory-mapped pages against ~18 MB private. Killing it frees 18 MB. DevMem shows private and shared as separate columns and refuses to add them up.
It cannot see a leak. A process that grows 40 MB an hour looks completely ordinary at any one
instant and is unmissable over six days. --record samples continuously and --report fits growth
rates against uptime, so a slow leak becomes a number instead of a hunch.
It cannot see an aggregate. Preinstalled vendor software is 25–40 background processes that are
individually unremarkable and collectively larger than any IDE. Sorted by size, none of them ever
reach the top of the list. --oem groups them by vendor and says what each one is for.
Requires Windows x64. The released binary is self-contained — no .NET runtime, no installer, no dependencies.
Download DevMem.exe from the latest release and run it. It is not code-signed, so SmartScreen warns the first time; the release page publishes a SHA-256 to check the download against.
.\DevMem.exe --install--install copies the binary to %LOCALAPPDATA%\Programs\DevMem and adds it to the current
user's PATH — HKCU only, so no elevation and no UAC prompt, and nothing changes for other
accounts on the machine. DevMem --uninstall reverses it. Open a new terminal afterwards; existing
ones keep the environment they were started with.
DevMem runs unelevated, and every memory number is correct that way — process sizes, kernel pool and pool tags all come from calls that need no privilege. Elevation adds two things: it can kill processes owned by other accounts, and it can read the image path of services running as SYSTEM. That second one matters for vendor attribution — unelevated, only about a fifth of the preload processes on the test machine expose a path, and the rest are identified by executable name instead.
Requires the .NET 10 SDK.
git clone https://github.com/nothotscott/DevMem.git
cd DevMem
dotnet build -c Release
.\bin\Release\net10.0\DevMem.exe --installOr skip the install and run it in place with dotnet run -- <args>.
DevMem interactive dashboard
DevMem --once one-shot text snapshot (pipeable)
DevMem --oem vendor preload, kernel drivers and pool tags
DevMem --record [options] record continuously until stopped
DevMem --sessions list recorded sessions
DevMem --report [session] print the trend digest for a session
DevMem --export [session] write digest.md into the session folder
DevMem --install add to the current user's PATH
DevMem --help
↑↓ move · ↵ expand a group into its processes · k kill · d dev-tooling filter ·
s cycle sort (private / shared / cpu / count / name) · r refresh · q quit
Processes are grouped by executable name, because 20 chrome.exe processes are one decision, not
twenty. Groups are tagged by kind (node, build, ide, lsp, container, browser, oem,
system, …), and idle or orphaned dev tooling is flagged ← reclaim.
Killing is guarded: system-critical processes are refused outright, DevMem will not kill itself, and every PID is re-verified against its start time before the kill so a recycled PID cannot be hit by mistake.
DevMem --record # leave it running
DevMem --report # check in wheneverA session covers one boot. Your working sets all reset when the machine restarts, so a series that spans a reboot is not a series — and scoping it this way means restarting the recorder just appends to the same session instead of fragmenting the week into pieces you have to stitch back together.
Costs about 20–30 MB of disk for a full week. Options: --interval 1m (system sample rate),
--detail 5m (per-process sample rate), --min-mb 4 (skip smaller groups), --for 7d (stop
automatically), --dir PATH (where sessions live).
To have it start at logon instead of living in a terminal:
schtasks /Create /TN "DevMem Recorder" /SC ONLOGON /RL LIMITED /F `
/TR "$env:LOCALAPPDATA\Programs\DevMem\DevMem.exe --record"--report prints a markdown digest: memory pressure and its trend, kernel pool growth by
allocation tag, steady growers separated from step changes, largest consumers by mean size, vendor
rollups, process churn, and what survived to the end.
Real output after 13 hours of uptime:
## Memory pressure
- Used: 14.7 GB at start, 17.8 GB at end, peak 17.8 GB of 31.7 GB installed.
- Trend: **+148.50 MB/h** (+3564.07 MB/day), r2 0.74.
## Kernel pool
- Paged pool: 639 MB to 1516 MB, +52.54 MB/h (r2 0.63).
- Nonpaged pool: 870 MB to 1350 MB, +28.79 MB/h (r2 0.46).
## Steady growers
| process | kind | vendor | MB/h | MB/day | start MB | end MB | r2 |
|-------------------------------------------|-------|--------|-----:|-------:|---------:|-------:|-----:|
| Dell.CoreServices.Client.exe | oem | Dell | 4.63 | 111 | 50 | 104 | 0.61 |
| SqlServerExtension.Service.exe | other | | 3.43 | 82 | 75 | 116 | 0.55 |
| Dell.TechHub.Analytics.SubAgent.exe | oem | Dell | 2.92 | 70 | 52 | 86 | 0.58 |
| Dell.TechHub.Instrumentation.SubAgent.exe | oem | Dell | 2.03 | 49 | 190 | 224 | 0.55 |
| NVDisplay.Container.exe | other | | 1.98 | 48 | 55 | 86 | 0.99 |
Growth is a least-squares fit against uptime, reported with r². That matters more than it sounds: a process that allocated 500 MB once and then sat flat has exactly the same endpoints as one that leaked steadily, and without r² the step change outranks the real leak every time. Rates are suppressed entirely below an hour of samples — ten minutes of data will happily turn a 2 MB wobble into "+120 MB/h", which is worse than reporting nothing.
DevMem --exportWrites digest.md and a copy of PROMPT.md into the session folder and lists the
files to attach. PROMPT.md is a self-contained analysis brief: it documents every CSV column and
spells out the Windows memory-accounting rules that make or break the conclusions, because a model
reading these numbers without them will confidently reach the wrong answer.
Everything is CSV without a BOM, appended and flushed every tick — the failure mode worth designing against is losing six days of data to a hard power-off on day seven.
| File | Contents |
|---|---|
system.csv |
One row per tick: used/free/total, commit, paged and nonpaged pool, cache, private total, and the shared+kernel remainder. |
groups.csv |
One row per executable name per detail tick: private, shared, CPU, count, age, orphans, vendor, necessity. |
processes.csv |
One row per process lifetime, written when it exits. Peak and final size, total CPU, image path. |
live.csv |
The same shape for processes still running, rewritten each detail tick. |
pooltags.csv |
Kernel pool by four-character allocation tag — the same data poolmon shows. |
modules.csv |
Kernel drivers loaded at session start, with vendor attribution. |
meta.json |
Machine, RAM, commit limit, sampling configuration, and every recorder run in the session. |
live.csv exists because processes.csv only gets a row when something dies — without it, a
process that ran for the entire week would appear nowhere at all.
--oem exists because the reason a machine is short on RAM may have nothing to do with
development. On the Dell this was written against, immediately after a reboot:
Preload 42 of 312 processes, 1.95 GB private (11% of used memory)
BY VENDOR
Dell 28 proc 1.60 GB private 2.35 GB shared 1.16 GB optional
Intel 8 proc 176 MB private 181 MB shared 149 MB optional
Waves 3 proc 167 MB private 272 MB shared 167 MB optional
Realtek 3 proc 17 MB private 58 MB shared - optional
It reports a rollup per vendor, a per-component breakdown with a curated note on what each one
actually does and whether it is essential, situational or optional, the vendor kernel drivers
that are loaded, and kernel pool by tag — including EtwB, the ETW trace-session buffers that
always-on telemetry agents consume in nonpaged pool.
The necessity ratings are guidance for deciding what to disable, not permission to kill anything. Nothing in that table feeds the kill path: most of these are services a service manager restarts within seconds, and killing an audio-stack member mid-session is genuinely disruptive.
Attribution is by install path first and company name second — several vendor binaries ship with
CompanyName left as the assembly name, so matching on company alone misses them. Services running
as SYSTEM will not hand out their image path to an unelevated caller, so those fall back to a
curated table keyed on executable name; run elevated if you want paths for everything.
Process data comes from NtQuerySystemInformation(SystemProcessInformation), not
System.Diagnostics.Process. The managed API cannot report the private working set at all —
Process.PrivateMemorySize64 is private commit, a different quantity. One ntdll call returns
private working set for every process on the machine with no per-process handle, which also
sidesteps Access Denied on protected and other-user processes entirely.
Kernel memory comes from GetPerformanceInfo and SystemPoolTagInformation. Pool tags are the
only way to ask "which driver is holding nonpaged pool" without a kernel debugger. Tag tracking is
on by default on modern Windows and the query needs no elevation.
No NuGet dependencies, deliberately. The TUI is hand-rolled ANSI, as are the CSV reader and the
argument parser. Microsoft.Win32.Registry is used for the PATH install and ships in the shared
framework.
Struct offsets are x64-only. SYSTEM_PROCESS_INFORMATION, SYSTEM_MODULE_INFORMATION and
SYSTEM_POOLTAG are parsed at fixed byte offsets; the 32-bit layouts differ and are not supported,
so the sampler refuses to run in a 32-bit process rather than reporting garbage.
- Windows x64 only. It reads Windows kernel structures at hardcoded offsets.
- Undocumented information classes.
NtQuerySystemInformationis not a stable contract. Microsoft can change these layouts. Pool-tag and module reads degrade to "unavailable" rather than throwing; the process layout is validated againstProcess.WorkingSet64if you suspect it. - Shared memory is never summed. Shared pages are counted once per process that maps them, so a total built from them is meaningless. Treat the shared column as a per-process order of magnitude.
- Private working set never sums to "used". The gap is mapped pages, kernel pool and the compressed store. DevMem shows that remainder explicitly rather than letting the columns imply the numbers add up.
- Peak far above final size means trimmed, not freed. Windows trims working sets under pressure; those pages went to the pagefile or the compressed store and come back on next touch.
- The component necessity table is opinion, drawn from one machine's preload. Read it as a starting point.
MIT © Scott Maday