Skip to content

Windows Memory Diagnostics

Martin Vallevand edited this page Jul 13, 2026 · 1 revision

Windows Memory Diagnostics

This build system supports producing debug-symbol builds specifically tuned for investigating memory issues with Visual Studio's Native Memory diagnostic tools. This page covers the two relevant build flags, how to get and use cv2pdb, and the actual profiling workflow.

This is a diagnostic build path — not something to ship. Expect a real performance cost, particularly with NO_INLINE=1 set.


Build flags

DEBUG_SYMBOLS=1

DEBUG_SYMBOLS=1 ./build-libmpv-mingw64.sh v0.41.0

Keeps -g (debug info) through the build, disables stripping, and triggers cv2pdb PDB generation for the resulting DLLs. This is not a full unoptimized debug build — FFmpeg/mpv/libplacebo are still built with their normal optimization level (debugoptimized buildtype where applicable), so runtime timing and reproduction behavior stay close to a normal release build. Only debug info retention changes.

NO_INLINE=1

DEBUG_SYMBOLS=1 NO_INLINE=1 ./build-libmpv-mingw64.sh v0.41.0

Additionally disables compiler inlining (-fno-inline and related flags), while keeping other -O2 optimizations. Use this alongside DEBUG_SYMBOLS=1, not instead of it.

Why this matters: cv2pdb's DWARF-to-PDB conversion frequently cannot cleanly separate an inlined callee from its caller. With inlining left on, Visual Studio's Native Memory profiler call-stack view shows a lot of [anon_XXXXXXX] / Unknown frames instead of real function names — frequently one function's code gets folded into a neighboring one's symbol range, making the profiler's attribution actively misleading rather than just incomplete. NO_INLINE=1 forces every function boundary to stay real and separately addressable, so the profiler's symbol names become trustworthy.

Expect meaningfully slower playback with this set — fine for a short diagnostic session, not for extended testing or anything you'd ship.


Getting cv2pdb

cv2pdb converts the DWARF debug info that GCC/MinGW produces into a PDB file Visual Studio can actually use — MinGW-built DLLs don't produce PDBs natively, so without this step VS has no symbols for them at all, debug build or not.

Upstream: rainers/cv2pdb

There's no simple installer. Two practical ways to get a working cv2pdb.exe:

  1. GitHub Releasesrainers/cv2pdb/releases has prebuilt binaries attached to recent releases. This is the simplest path if a recent-enough release exists for your needs.
  2. Bundled with Visual D — the Visual D Visual Studio extension (a D-language plugin) ships cv2pdb.exe as part of its installation, even if you have no interest in D itself. Historically this was the most reliable way to get a working build before GitHub Releases were consistently published.

Once you have cv2pdb.exe, put it somewhere on PATH (or reference it directly) — the build script invokes it automatically when DEBUG_SYMBOLS=1 is set; no separate manual conversion step is needed as part of the normal build flow.


Profiling workflow (Visual Studio)

  1. Build with DEBUG_SYMBOLS=1 NO_INLINE=1 (see above).
  2. Deploy the resulting DLLs (and their generated .pdb files) alongside your application executable.
  3. In Visual Studio: Debug → Windows → Show Diagnostic Tools (or just start debugging — the Diagnostic Tools window can open automatically depending on your settings).
  4. Open the Memory Usage tab.
  5. Take a snapshot at a meaningful baseline point (e.g. right after startup, or right before the action you want to investigate).
  6. Let the scenario run (playback, a stop/restart cycle, whatever you're investigating).
  7. Take a second snapshot.
  8. Set Compare With Baseline to the first snapshot.
  9. Switch to the Stacks view (not Types) — this groups allocations by call stack, which is what actually lets you trace growth back to a specific function.
  10. Sort by Size Diff (Bytes), descending, and drill into the largest contributors.

Reading the result

  • With NO_INLINE=1, every frame in the call stack should resolve to a real function name — no Unknown, no [anon_*]. If you still see unresolved frames, double check the PDB actually matches the DLL being debugged (rebuild + redeploy, don't mix an old DLL with a new PDB or vice versa).
  • "Heap Size (Diff)" in a snapshot comparison reflects net-still-live bytes — an allocation made and freed between the two snapshots does not show up in the diff. A large diff means something is genuinely still allocated at the second snapshot, not just that a lot of allocation activity happened in between.
  • A large number attributed to one call stack does not by itself mean that function is broken — it may be legitimate, bounded buffering (demuxer cache, decode queues) that simply hasn't been exercised long enough yet to plateau. Compare multiple snapshots over a longer session before concluding something is unbounded — several real investigations here initially looked like leaks in a short test and turned out to cleanly plateau given enough time.

A note on Compare With Baseline: None

The very first snapshot in a session, with no baseline set, shows total current allocations — not net growth. A large number here during the first ~10-30 seconds is normal (startup buffering) and isn't itself evidence of anything. Always compare against a second snapshot, taken after a meaningful gap, before drawing conclusions.


Forcing a clean rebuild for diagnostic builds

Same caveat as the patch-content caching gap described on the Custom Patches page: the build script's stamp caching doesn't distinguish a DEBUG_SYMBOLS=1/NO_INLINE=1 build from a normal one by content, only by tag and the debug-symbols suffix. If you've previously built a given tag without these flags, force a clean rebuild before adding them:

rm -rf work/deps-ffmpeg7 work/deps-ffmpeg6   # whichever applies
git -C work/src/ffmpeg reset --hard
git -C work/src/ffmpeg clean -fd

Clone this wiki locally