Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ramcheck

Task Manager says Memory 62% and that number tells you almost nothing.

It hides every memory metric that actually explains why your PC stutters. ramcheck shows the ones that matter: the hard page faults that cause the stutter, the commit charge that causes "out of memory" errors, how much of your RAM is reclaimable cache, and the gap between the RAM you bought and the RAM you can use.

Read-only. Zero dependencies. One PowerShell file.

This is not a hardware memory tester. It will not find a faulty DIMM — use MemTest86 for that. This tells you how Windows is using the memory you have.

powershell -NoProfile -ExecutionPolicy Bypass -File ramcheck.ps1
  ramcheck - what Task Manager will not tell you about your RAM
  ══════════════════════════════════════════════════════════════

  INSTALLED HARDWARE
    32.00 GiB    installed   2 modules  SK Hynix  3,733 MT/s
    31.60 GiB    usable   413.96 MiB reserved by hardware

  WHERE YOUR RAM ACTUALLY IS
    in use by apps      7.27 GiB  ██████░░░░░░░░░░░░░░░░░░░░
    modified (dirty)  594.86 MiB  ░░░░░░░░░░░░░░░░░░░░░░░░░░  queued for the page file
    standby cache       3.90 GiB  ███░░░░░░░░░░░░░░░░░░░░░░░  reclaimable - counted as available
    free               19.84 GiB  ████████████████░░░░░░░░░░

    Task Manager would say   24.9% used
    actually available       23.74 GiB of 31.60 GiB  (75.1%)
    of which reclaimable     3.90 GiB is file cache an app can take instantly

  COMMIT CHARGE   the number that causes "out of memory", never shown
    committed                8.29 GiB
    limit                    36.60 GiB   = 31.60 GiB RAM + 5.00 GiB page file
    in use                    22.6%  ██████░░░░░░░░░░░░░░░░░░░░

  PAGE FAULTS OVER 4.1s   only hard faults touch the disk
    total                      4,181.2 /s
      hard  (from disk)            0.0 /s   THIS is what causes stutter
        pages pulled in            0.0 /s
      soft  (from RAM)         4,181.2 /s   costs nanoseconds, harmless
    hard share                     0.000%   of all faults went to disk
    paged out                      0.0 /s   pages written to the page file

    by type, as Windows counts them (these overlap and do not sum
    to the total - demand zero alone can exceed it):
      demand zero              5,447.1 /s
      transition               1,059.8 /s
      cache                       11.9 /s
      copy-on-write                7.8 /s

  TOP PROCESSES BY PRIVATE WORKING SET
    copilot                          9908   342.68 MiB
    svchost                         14172   337.43 MiB
    Discord                         13188   266.43 MiB
    MsMpEng                          4808   254.98 MiB
    msedgewebview2                   1896   144.69 MiB
    dwm                              1796   133.97 MiB

  VERDICT: ok - no memory pressure

That is real output from the machine this was built on, not a mock-up.


Why "Memory 62%" is the wrong number

Because "used" counts cache that any app can take back instantly. On the machine above, 3.90 GiB of what looks occupied is standby cache — file data Windows is keeping around because the RAM was otherwise idle. It is already counted as available. A high "used" figure with a large standby cache is a machine using its RAM well, not a machine running out.

Because the number that actually fails is commit charge, and Task Manager does not show it against its limit. When an allocation fails with "out of memory", the limit it hit was the commit limit — physical RAM plus your page files. You can hit it at 60% "memory used". ramcheck shows commit against that limit, and shows the arithmetic: 36.60 GiB = 31.60 GiB RAM + 5.00 GiB page file.

Because stutter comes from hard faults, and Windows shows you neither kind. A soft fault is served from RAM and costs nanoseconds — a healthy desktop does thousands per second, forever, and it means nothing. A hard fault goes to the disk and costs milliseconds. That is the one you feel. They are completely different events and the ratio is the whole story.


Three things this found that Windows will not tell you

1. You did not get the RAM you bought — and nothing shows the gap

Source Value
Win32_PhysicalMemory (the SPD data on the sticks) 34,359,738,368 = 32.00 GiB
Win32_ComputerSystem.TotalPhysicalMemory 33,925,664,768 = 31.60 GiB
Win32_OperatingSystem.TotalVisibleMemorySize 33,130,532 KB = exactly the same

414 MiB is reserved by the hardware and is not addressable by Windows at all. That gap appears in no Windows UI. ramcheck prints both numbers and the difference.

2. Windows' own page-fault categories do not add up

Demand-zero, transition, cache and copy-on-write faults overlap, and the categories can sum to more than the total. On this machine, cumulatively since boot:

Page Faults          72,152,701
Demand Zero          92,611,282   <-- larger than the total
Transition           13,208,015
Write Copies            390,168

A tool that adds those up and calls it "soft faults" is printing a number that does not mean anything. ramcheck computes soft faults as total - hard and shows the categories separately, labelled as overlapping.

3. A hard fault is not a page — and the difference is ~8x

Page Reads/sec counts read operations (one per stall). Pages Input/sec counts the pages that arrived, which is larger because the kernel clusters read-ahead into the same operation. Measured live on this machine during a normal desktop session:

  hard  (from disk)            0.6 /s
    pages pulled in            5.4 /s   8.5 pages per read

8.5 pages per read. Reporting pages as if they were faults overstates your stall count by that factor. ramcheck reports the count and the volume as separate things.


What it checks

Verdict Meaning
ok no memory pressure
warn something worth knowing about
problem something is actively hurting you
Code Fires when
hard-faulting-heavy ≥ 100 hard faults/s — this is what stutter feels like
hard-faulting ≥ 10 hard faults/s sustained
commit-critical commit ≥ 95% of the limit; allocations are about to fail
commit-pressure commit ≥ 85% of the limit
available-critical < 5% of RAM available
available-low < 12% of RAM available
modified-backlog ≥ 10% of RAM is dirty pages queued for the page file
no-pagefile no page file, so the commit limit is RAM alone
layout-inconsistent the counters contradict themselves — numbers withheld
commit-inconsistent the commit percentage disagrees with itself
commit-limit-unexplained the limit is not RAM + page files

Exit code is 0 for ok, 1 for warn/problem, 2 for a usage error.

The last three are data-integrity checks. When two independently-derived values of the same quantity disagree, the parse is wrong, and ramcheck says so instead of reporting numbers it cannot stand behind.

Usage

ramcheck.ps1                      # 5-second sample, human report
ramcheck.ps1 -Seconds 30          # longer window for fault rates
ramcheck.ps1 -Info                # instant values only, no sampling window
ramcheck.ps1 -Procs -Top 15       # add the top memory-holding processes
ramcheck.ps1 -Json                # machine-readable
ramcheck.ps1 -Json > cap.json
ramcheck.ps1 -FromJson cap.json   # replay a capture, on any machine
ramcheck.ps1 -Quiet               # exit code only

-FromJson reads a capture saved anywhere, so you can have someone send you their ramcheck -Json output and analyse their machine on yours. It is also the non-admin escape hatch and how the test suite replays fixtures.

Rate-derived fields need a time window. With -Info they come back null, never a misleading 0 — "unknown" must not masquerade as "perfect".

Requirements

Windows PowerShell 5.1 or later. No modules, no downloads, no installer. No admin rights needed for the memory data.

Execution policy blocks .ps1 on most machines. Run it with -ExecutionPolicy Bypass as shown above — that is process-scoped and changes nothing on your system. You do not need to run Set-ExecutionPolicy, and this tool never asks you to.

It is read-only, and that is tested

ramcheck starts no processes, writes no files, and changes no setting. That claim is verified two independent ways in realcheck.ps1:

  • the source is scanned for all 33 mutating calls (Set-ItemProperty, Remove-Item, SetValue(, WriteAllText, Stop-Process, Set-ExecutionPolicy, SetEnvironmentVariable, …) and must contain none — plus a negative control proving the scan can find a call that is present, so its silence means something;
  • the user environment block, page-file settings and execution-policy list are snapshotted before and after a run and must be byte-identical.

Because it changes nothing, there is no undo and none is needed.


Test evidence

Two suites, and neither one just checks that the tool printed something.

selftest.ps1 — 112 assertions against synthetic ground truth

powershell -NoProfile -ExecutionPolicy Bypass -File selftest.ps1
  112 passed, 0 failed

Every field in the fixtures gets a different distinctive value, so a tool that mixes two fields up cannot pass by accident — the nine fault counters are given deltas producing nine unique rates (1000, 22, 330, 4, 700, 200, 3000, 50, 10 /s), and the suite asserts those nine are distinct before relying on them.

Both sides of every threshold are tested (9.99 vs 10 hard faults/s, 84.99 vs 85% commit, 12 vs 11.9% available, 9.9 vs 10% modified). A synthetic healthy machine must produce exactly zero risks — the cry-wolf test.

Specific traps covered: a UInt64 above Int64.MaxValue (18446744073113190961, observed in the wild — [long] throws on it, [decimal] does not); a counter that went backwards yielding null and never 0; division-by-zero paths yielding null; percentages clamped into [0,100].

mutate.ps1 — 41 of 41 mutants killed

A suite that passes proves nothing until it is shown capable of failing. mutate.ps1 makes one plausible bug at a time in ramcheck.ps1 and requires selftest to catch every one.

powershell -NoProfile -ExecutionPolicy Bypass -File mutate.ps1
  mutation score: 41 / 41 killed

Including: hard faults counted as pages, the commit percentage missing its ×100, the availability identity hard-coded to true, the standby reserve list dropped, in-use forgetting the modified list, each risk threshold moved so it never fires, and each threshold moved so it always fires.

This harness earned its keep immediately. Its first run reported a survivor — "bar clamp removed" — which turned out to be an equivalent mutant: Format-Bar clamps both the fraction and the resulting fill count, so removing one changes nothing observable. Rather than delete the assertion, the control was rewritten to remove both clamps together, which is killed. It also caught one invalid control whose anchor text did not exist in the source, and therefore had been silently testing nothing.

realcheck.ps1 — 72 assertions against live system data

powershell -NoProfile -ExecutionPolicy Bypass -File realcheck.ps1
  72 / 72 passed, 0 skipped

Synthetic fixtures are clean and predictable, which is exactly why they miss things. This suite feeds the parser genuine live data and checks it against independent reference implementations using different techniques.

R1 — WMI vs PDH, field for field. Two different Windows APIs (Win32_PerfRawData_PerfOS_Memory via CIM, and Get-Counter via PDH) reading the same kernel data through different plumbing, across 11 fields and 8 rounds.

R2 — the tool's rate arithmetic vs PDH's own cooked values, over the identical window: worst relative error 0.00000000% across 18 readings. PDH exposes RawValue and SecondValue alongside CookedValue, so the tool's formula can be run over the exact window PDH used. This is how the formula was derived rather than guessed:

rate = Δ(RawValue) / (Δ(SecondValue) / 10,000,000)

SecondValue is the PerfTime timestamp, not a second data value. Two plausible alternatives (Timestamp100NSec and Timestamp.Ticks) give ~0.05% error — close enough to look right, wrong enough to be wrong. Negative control: the same comparison with three deliberately wrong time bases is rejected in all three cases.

R3 — known ground truth planted inside a real snapshot. Distinctive values are written into a genuine 29-field live snapshot, and the tool must find exactly them while surrounded by real, irrelevant data — with an explicit assertion that no real value leaked into the result.

R4 — independent cross-checks. GlobalMemoryStatusEx via P/Invoke (a raw kernel struct, not a counter provider) agrees exactly on total usable RAM and on the commit limit. Win32_OperatingSystem.TotalVisibleMemorySize is in kilobytes — a classic unit trap, so there is a negative control asserting that forgetting the ×1024 is caught. Win32_PageFileUsage confirms the page-file total. Process names are verified pid-by-pid against Get-Process: 199 pids cross-checked, 0 mismatches, of which 132 raw perf names carried a #N suffix (svchost#75, msedgewebview2#11) that had to be normalised first.

R5 — the headline feature, verified by generating the condition it detects. An API that returns a number without erroring has not been shown to measure anything; a stale value or a constant looks identical. So the suite allocates 512 MiB in a child process, touches every 4 KiB page, and requires the tool's numbers to move by the known amount:

  committed rose 600.00 MiB (allocated 512 MiB)
  page faults +363243, demand-zero +534250, hard reads +1 (expected ~131072 page touches)
  soft:hard ratio 363,243:1
  idle delta over 0.7 s: 1963 faults vs 363243 under load

The discriminating assertion is the last one: soft faults spike by a third of a million while hard faults stay at 0 or 1. A tool that conflated the two would show a hard-fault storm here. The allocator signals READY rather than being given a fixed head start, because .NET has to JIT and pages are only faulted in on touch. Use -SkipLoad to skip this test.

R6 — the tool's own -Json output fed back through a bounds check, including the aggregate check inUse + available + modified == usable. Every individual reading can be clamped and an aggregate still be wrong. Also verifies free + standby == available, the standby sub-lists summing to the standby total, -FromJson replay preserving every value, a UTF-8 BOM not breaking the replay, and the error paths emitting valid JSON with exit code 2 rather than a stack trace.

R7 — the read-only proof described above.

Stability

Machine-state tests that pass on a cool, quiet machine can fail on the same machine 60 seconds later. Both suites were run repeatedly back to back: 4 consecutive runs at selftest 112/112 and realcheck 72/72, then 3 further realcheck runs after the R1 rework — 7 green runs of the real-data suite in total.


The measurement problem this had to solve

The first version of R1 compared WMI and PDH and demanded exact agreement. It failed, and why it failed is the most interesting thing in this repository.

These counters are not monotonic counters. They are level gauges that oscillate in quantised 4 KiB page steps around an equilibrium. Two APIs physically cannot be sampled at the same instant, so exact comparison is measuring the machine's stillness, not the tool's correctness.

The obvious fix — bracket the PDH read between two WMI reads and treat "both WMI reads identical" as proof the field held still — is also wrong, and the machine disproved it inside one run:

[MISMATCH] AvailableBytes            wmi=25479143424  pdh=25479208960   (65,536 = 16 pages)
[MISMATCH] FreeAndZeroPageListBytes  wmi=21442437120  pdh=21442502656   (65,536 = 16 pages)
[MISMATCH] PoolPagedBytes            wmi=  306765824  pdh=  306749440   (16,384 =  4 pages)

Every divergence is a whole number of pages. A process that allocates 16 pages and frees them again inside the bracket returns the gauge to a byte-identical value while genuinely having moved. Bit width is irrelevant.

So fields are classified empirically by what they actually did during the run:

  • static — never varied in either API. Compared exactly. This is the decisive claim, and it carries the full negative control including single-byte corruption.
  • oscillating — varied, and therefore cannot be compared exactly by anyone. Checked against a self-calibrating tolerance: the observed width of the WMI bracket is how much the machine churned between reads, so the tolerance is derived from it (floor 4 MiB, else 4× the observed churn). A quiet machine gets a strict test; a machine mid-game gets a fair one.

The two claims are counted and reported separately, because claiming "0 exact mismatches" about a gauge that cannot be exactly compared would be a lie:

  EXACT: 2 static field(s) agree byte-for-byte between WMI and PDH
  BOUNDED: 72 oscillating observations within their self-calibrated tolerance
  mutation score 64/64; 6 equivalent mutants excluded
  worst divergence outside the bracket: 6127616 bytes (1496 pages) on CommittedBytes

The tolerance-based claim gets a restricted mutator set — only corruptions larger than the tolerance (wrong scale, wrong field, doubled, halved, zeroed, sign flipped), all of which are orders of magnitude above it. A sub-tolerance corruption is not detectable on an oscillating gauge, so the suite says so explicitly and prints any such mutant as [UNDETECTABLE] rather than quietly counting it as a pass. Equivalent mutants (zeroing a field that was already zero) are excluded from the score rather than counted, because counting them would inflate it.

Things that will bite you if you write one of these

  • Win32_PerfRawData_PerfOS_Memory's *Persec fields are cumulative counters, not rates, despite the name. PageFaultsPersec reads 72152701 — that is the total since boot. You must difference two samples yourself.
  • Memory Compression is invisible to Get-Process and to Win32_PerfRawData_PerfProc_Process (which returns zero rows for it). Task Manager uses undocumented APIs. ramcheck reports it as null and says Windows does not expose it — a confidently wrong diagnosis is worse than none.
  • PDH counter paths come back lowercased (\\machine\memory\available bytes) while WMI uses PascalCase field names. Route every lookup through one canonical form or half your data attaches to nothing.
  • [int][math]::Pow(2, 31) throws. Any 32-bit flags field needs [long].
  • A raw perf-counter field can be a UInt64 above Int64.MaxValue. [long] throws; [decimal] represents every UInt64 exactly and still allows signed subtraction.
  • Under Set-StrictMode, (<pipeline>).Count throws when the pipeline yields a single object. Always @(<pipeline>).Count.
  • '{0:N1}' -f 2650 yields "2,650.0"N inserts a thousands separator. Use F for any exact string comparison in a test.
  • $anInt + ' some text' throws in PowerShell, because the left operand picks the operation. [string]$anInt + ' some text' is what you meant.
  • Dot-sourcing a script imports its param() type constraints into your scope. After . .\ramcheck.ps1, a later $Json = @(1,2) in your script throws "Cannot convert System.Object[] to SwitchParameter" and blames your file. Every local in the test suites here is prefixed t_ / r_ for exactly this reason, and ramcheck.ps1 takes a -NoRun switch so it can be dot-sourced safely.

Encoding

ramcheck.ps1 is pure ASCII on disk — 0 bytes above 127, asserted by the test suite. The box-drawing characters in the output are built from code points at runtime, so the file needs no BOM and cannot be corrupted in transit or misread as ANSI by PowerShell 5.1.

If your console cannot render them, it falls back to ASCII automatically: each glyph is round-tripped through [Console]::OutputEncoding and replaced with #/./= if it does not survive. Both branches are tested — verified rendering Unicode on UTF-8 and code page 437, and falling back cleanly on US-ASCII (20127) and Latin-1 (28591).

See also

  • obs-4k60-recorder — OBS settings for real 4K60 capture
  • gpucheck — whether the GPU was the bottleneck
  • cpuclock — what your CPU is actually clocked at
  • diskrate — whether the disk was the bottleneck
  • framecheck — find dropped frames in a recording

License

MIT — see LICENSE.

About

Task Manager says Memory 62% and that number tells you almost nothing. Shows the hard page faults that actually cause stutter, commit charge against the real limit, how much of your RAM is reclaimable cache, and what memory compression is hiding. Read-only, zero dependencies. Not a hardware memory tester.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages