55 Minutes

Welcome to the 55 Minutes blog.
55 Minutes is a web development consultancy in San Francisco building apps for design-led businesses and startups. On this blog we discuss the technology tools of our trade: Ruby on Rails, Django, Sass, OS X, and more.

Taming the Lion Inactive Memory Problem

OS X Lion has a nasty habit of hanging on to inactive memory, thus causing unnecessary disk paging. As you’ve probably observed—unless you’re fortunate enough to be running on an SSD—any sustained disk activity brings Lion to a grinding halt. So, what can you do to alleviate this problem?

Stop reading if your computer works fine

You are most likely not suffering from this problem because:

  1. You use your Mac like most normal people, or
  2. You’re running on an SSD (lucky you)

This symptom only shows up during very specific use cases; we observe it consistently when we have to start/stop/reboot our virtual machines many times during development and testing.

If you’re not suffering from this problem, please stop here. This technique is a pretty low-level tweak so you should be willing to understand the trade-offs1 before implementing it.


First, terminology

To understand the issue, first familiarize yourself with how OS X manages memory. Basically, once physical memory is exhausted, the OS starts paging to disk (yuck!), which is indicated by the number of page outs. To see if your computer is paging excessively, calculate the paging ratio: page outs ÷ page ins. If the ratio is less than 10% then you’re in pretty good shape.

Inactive vs. free

When an application releases memory back to the OS, instead of being freed directly, that memory is put into the inactive holding area. If the same information is requested again, instead of fetching from disk, the OS can just mark the requested inactive memory as active. This optimization works well most of the time since we tend to open and close the same sets of apps and documents in a given session. Over time, you’ll see inactive memory creep up and free memory creep down.

When you launch a new app that requires more memory than is available from the free memory pool, Lion is supposed to return some memory from the inactive pool back to the free pool so that no paging has to take place. In practice, however, Lion doesn’t always do this gracefully when large chunks of memory are released and acquired in relatively quick succession.

As we have come to know, disk access is the enemy of Lion.

Arriving at an acceptable compromise

1. Add more memory

For me the obvious thing to do was to bump my MacBook Pro’s memory up from 4 to 8 GB. This had the effect of delaying the symptoms, but the computer still came to a screeching halt when it started paging out gigabytes of memory.

2. purge to the rescue, sort of

Lion ships with the purge command line utility that, among other things, flushes inactive memory. Using purge, though, is reactive rather than preemptive. How it typically goes:

  1. Notice the computer is unresponsive
  2. Launch Activity Monitor to see that the computer is paging out like mad
  3. Launch Terminal
  4. Run purge
  5. Wait for 30 seconds to one minute, during which time you can’t use your machine at all

Far too disruptive!

3. Thank goodness for scripting and launchd

To address the shortcomings above, we need to make these improvements:

  1. Instead of waiting for the system to become unresponsive, run purge before free memory runs out.
  2. Make sure running purge has minimal impact on overall system performance.

We use a Python script to do the heavy lifting, invoking purge when:

  1. Free memory falls below a specified threshold (500 MB in this case) and
  2. Inactive memory is above a specified threshold (1 GB in this case)
#!/usr/bin/env python

import os
import re
import sys

from subprocess import call, Popen, PIPE

INACTIVE_THRESHOLD = 1024  # Number of MBs
FREE_THRESHOLD = INACTIVE_THRESHOLD / 2
RE_INACTIVE = re.compile('Pages inactive:\s+(\d+)')
RE_FREE = re.compile('Pages free:\s+(\d+)')
RE_SPECULATIVE = re.compile('Pages speculative:\s+(\d+)')
LOCK_FILE = '/var/tmp/releasemem.lock'


def acquire_lock():
    try:
        os.open(LOCK_FILE, os.O_CREAT | os.O_EXLOCK | os.O_NDELAY)
    except OSError:
        sys.exit('Could not acquire lock.')


def pages2mb(page_count):
    return int(page_count) * 4096 / 1024 ** 2


def free_inactive():
    vmstat = Popen('vm_stat', shell=True, stdout=PIPE).stdout.read()
    inactive = pages2mb(RE_INACTIVE.search(vmstat).group(1))
    free = pages2mb(RE_FREE.search(vmstat).group(1)) + \
            pages2mb(RE_SPECULATIVE.search(vmstat).group(1))
    return free, inactive


def main():
    acquire_lock()
    free, inactive = free_inactive()
    if (free < FREE_THRESHOLD) and (inactive > INACTIVE_THRESHOLD):
        print("Free: %dmb < %dmb" % (free, FREE_THRESHOLD))
        print("Inactive: %dmb > %dmb" % (inactive, INACTIVE_THRESHOLD))
        print('Purging...')
        call('/usr/bin/purge', shell=True)


if __name__ == '__main__':
    main()
releasemem.py – View Gist

Then we use launchd to periodically (every 60 seconds in this case) run the script with a nice value of 20 and low IO priority:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>net.damacy.releasemem</string>
    <key>ProgramArguments</key>
    <array>
      <string>/Users/george/.bin/releasemem.py</string>
    </array>
    <key>StartInterval</key>
    <integer>60</integer>
    <key>Nice</key>
    <integer>20</integer>
    <key>LowPriorityIO</key>
    <true/>
  </dict>
</plist>
net.damacy.releasemem.plist – View Gist

When the conditions are met, you’ll see log entries in Console:

4/30/12 2:48:34.059 PM net.damacy.releasemem: Free: 298mb < 512mb
4/30/12 2:48:34.059 PM net.damacy.releasemem: Inactive: 3785mb > 1024mb
4/30/12 2:48:34.059 PM net.damacy.releasemem: Purging...

Be sure to check out the Gist comment for detailed instructions.

The result

I’ve been using this script since early February, and have observed no problems. My computer’s paging ratio has stayed healthy and purge seems to obey its nice directive properly. According to my Console log, purge was run five times in the past week.

Final verdict: happy Mac, happy user.

Feedback? Leave your comments on the Gist for this post.


Other implementations

Do a search for “os x purge” and you’ll find alternative implementations, such as:


Notes

  1. Using purge is pretty controversial. Read up on the various debates in Macworld and OS X Daily.

comments powered by Disqus