Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DevMem

Build

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

Why not Task Manager

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.

Install

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.

Build from source

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 --install

Or skip the install and run it in place with dotnet run -- <args>.

Usage

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

Dashboard

↑↓ 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.

Recording over a week

DevMem --record          # leave it running
DevMem --report          # check in whenever

A 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"

Reading the results

--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.

Handing the data to an LLM

DevMem --export

Writes 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.

What gets recorded

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.

Vendor preload

--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.

How it works

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.

Caveats

  • Windows x64 only. It reads Windows kernel structures at hardcoded offsets.
  • Undocumented information classes. NtQuerySystemInformation is 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 against Process.WorkingSet64 if 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.

License

MIT © Scott Maday

About

A Windows memory analyzer for developer workstations.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages