Skip to content

fix(cli): report available memory instead of free memory in doctor#1204

Merged
miguel-heygen merged 1 commit into
mainfrom
worktree-fix+doctor-command
Jun 4, 2026
Merged

fix(cli): report available memory instead of free memory in doctor#1204
miguel-heygen merged 1 commit into
mainfrom
worktree-fix+doctor-command

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Fix hyperframes doctor memory check reporting ~0.1 GB free on macOS machines with 24 GB RAM. os.freemem() only counts truly free pages, ignoring inactive/purgeable/speculative pages the kernel reclaims on demand. Added getAvailableMemoryMb() that uses vm_stat on macOS and MemAvailable from /proc/meminfo on Linux, falling back to os.freemem() elsewhere.
  • Trim FFmpeg/FFprobe version strings from "ffmpeg version 8.1.1 Copyright (c) 2000-2026 the FFmpeg developers" to just "ffmpeg 8.1.1".

Before / After

Before:

  ✗ Memory           24.0 GB total · 0.1 GB free
                     Low memory — renders may fail. Close other apps or increase RAM.
  ✓ FFmpeg           ffmpeg version 8.1.1 Copyright (c) 2000-2026 the FFmpeg developers
  ✓ FFprobe          ffprobe version 8.1.1 Copyright (c) 2007-2026 the FFmpeg developers

After:

  ✓ Memory           24.0 GB total · 4.0 GB available
  ✓ FFmpeg           ffmpeg 8.1.1
  ✓ FFprobe          ffprobe 8.1.1

The memory check was the critical fix — os.freemem() on macOS only counts truly free pages, ignoring ~4.9 GB of inactive/purgeable/speculative pages that the kernel reclaims instantly on demand. Every macOS user was getting a false "Low memory" warning. The new code parses vm_stat on macOS and /proc/meminfo on Linux to get the actual available memory.

Test plan

  • Added 5 tests for getAvailableMemoryMb() covering macOS vm_stat parsing, Linux /proc/meminfo parsing, fallback behavior on parse errors, and unsupported platforms
  • Added 4 tests for parseToolVersion() covering ffmpeg/ffprobe extraction, Windows gyan.dev builds, and unrecognized input fallback
  • All 659 CLI tests pass
  • Typecheck, lint, and format pass
  • Verified manually: npx hyperframes doctor and npx hyperframes doctor --json show correct values

os.freemem() on macOS returns only truly free pages (~0.1 GB on a 24 GB
machine), ignoring inactive/purgeable/speculative pages the kernel
reclaims on demand. This caused a false "Low memory" warning on every
macOS machine.

Add getAvailableMemoryMb() that uses vm_stat on macOS and MemAvailable
from /proc/meminfo on Linux, falling back to os.freemem() elsewhere.

Also trim FFmpeg/FFprobe version strings to just "toolname X.Y.Z"
instead of the full copyright line.

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. macOS available-memory formula is right (free + inactive + purgeable + speculative — matching what Activity Monitor reports under "Memory Available"; wired + active correctly excluded). Linux uses /proc/meminfo MemAvailable which is the canonical kernel-3.14+ field. Fallback to os.freemem() everywhere (Windows, vm_stat fail, /proc/meminfo missing, parse fail) is the right degradation — no crashes on weird environments.

What I checked

  • macOS formula: free + inactive + purgeable + speculative (pages) × pageSize. Wired and active are correctly excluded (kernel-locked / in-use). This matches the canonical Mach "available" definition.
  • Linux: MemAvailable is the kernel-computed "what apps can actually claim" — superset of free that includes reclaimable cache. Correct field; correct kB → MB conversion (Math.trunc(.../1024)).
  • Fallback chain: try-block on both platforms catches execSync / readFileSync failures and returns bytesToMb(freemem()). The page-size parse miss (if (!pageSize) return fallback) and the MemAvailable: regex miss (return fallback) both degrade gracefully without crashing.
  • parseToolVersion regex: /(ffmpeg|ffprobe)\s+version\s+([\d][\d.\-\w]*)/i — leading [\d] so an N-12345-abc dev-snapshot version falls through to the trimmed-input fallback (acceptable), and [\d.\-\w]* after that absorbs the gyan.dev 7.1.1-essentials_build-www.gyan.dev suffix the test pins.
  • Tests: vi.resetModules() + vi.doMock("node:os") per-test correctly isolates platform-mocked imports; the dynamic await import("./system.js") after each mock setup picks up the fresh mocked deps.
  • Docker run-context (worth noting because hyperframes Docker workflows exist): in a Docker container running on Linux, /proc/meminfo is the host's by default but cgroup-aware tools (cgmemtop) report container limits instead. This change reports host MemAvailable, not cgroup-limited container memory — which is what users probably want for "does my dev box have enough RAM," but if a container has a 2GB limit on a 32GB host, the check would say "fine" while the container OOMs. Out-of-scope for this fix; flagging as future awareness, not a blocker.

Cross-platform

  • macOS ✓ (vm_stat parse)
  • Linux ✓ (MemAvailable)
  • Windows: explicit fallback to freemem(). On Windows os.freemem() already wraps GlobalMemoryStatusEx().ullAvailPhys, which IS "available physical" (free + standby) — so the value is correct even though the label still reads "available" (was "free" before). Net: Windows users get an accurate memory figure post-fix; the label change is the visible delta.

Nits — none

CI green across the board (CodeQL, CLI smoke, format/lint, preflight). Approving.

— Jerrai

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — clean fix for a real, user-facing papercut.

Design

  • macOS formula Pages free + inactive + purgeable + speculative matches Apple's "App Memory + Cached Files" mental model. Wired/active are correctly excluded.
  • Linux: parsing MemAvailable directly is the kernel-authoritative answer — better than reconstructing from MemFree + Buffers + Cached.
  • Threshold (< 2048 MB) against available rather than free is more permissive, which is the whole point of the fix — matches what users would expect from free -h.
  • Windows path falls through to os.freemem(), which on Win32 maps to GlobalMemoryStatusEx().ullAvailPhys — that's already "available physical memory" semantics, so labeling the fallback "GB available" is honest. Coverage is fine.

Correctness

  • ?? "0" parse fallbacks degrade gracefully (undercount → safer false-positive on the low-memory warning, never a false-negative). Pages purgeable line presence varies by macOS version; this handles that correctly.
  • parseToolVersion regex captures the Windows gyan.dev suffix, ffmpeg/ffprobe, and trims unrecognized input to a sensible fallback. Tests cover all four paths.
  • Tests use vi.doMock + vi.resetModules properly to swap node:os / node:child_process / node:fs per case. Coverage on getAvailableMemoryMb() is thorough.

Follow-up (nit, not a blocker)

  • packages/cli/src/commands/render.ts:996 still emits memoryFreeMb: bytesToMb(freemem()) in render telemetry. Now that doctor reports available, the telemetry value diverges from what users see in doctor. The field name is memoryFreeMb so keeping it as freemem() is defensible, but worth a follow-up to either rename to memoryAvailableMb and switch to getAvailableMemoryMb(), or add a sibling field. Whichever — just calling it out so the divergence is intentional.

Minor

  • parseToolVersion regex [\d][\d.\-\w]* — the leading [\d] character class is redundant with a bare \d. Style nit.

CI is green on all required checks. Ship it.

Review by Vai

@miguel-heygen miguel-heygen merged commit 9679503 into main Jun 4, 2026
52 checks passed
@miguel-heygen miguel-heygen deleted the worktree-fix+doctor-command branch June 4, 2026 22:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants