Finding a memory leak?

I’ve been using a modified form of Nigel Garvey’s ChimeHours script from a thread started by Kel for a long time now. I rarely reboot or log out, but occasionally notice that the script below (my version) gets huge (over 100 MB). Stopping and restarting it cures that, of course, but I’m wondering how to find out what part of it is causing the problem.

The Script


on idle
	local h
	
	tell (time of (current date))
		set h to ((it div hours + 11) mod 12) + 1
		tell (it mod hours)
			if (it < 10) then
				-- If it's now less than ten seconds after an hour, chime.
				my chime(h)
			else if (it < 300) then
				-- Otherwise, if it's within the first five minutes, make a belated announcement.
				my waffle(h)
			end if
		end tell
		
		-- Idle until the next five-minute boundary.
		return (86399 - it) mod 300 + 1 -- ie. (days - (time of (current date)) - 1) mod (5 * minutes) + 1
	end tell
end idle

to chime(h)
	tell application "Play Sound"
		«event µMCSplay» (("" & (path to "dlib" from system domain) & "Sounds:" & "Glass.aiff") as alias) given «class repe»:(h - 1)
		quit
	end tell
end chime

on waffle(h)
	say "Oops! It's a little past " & h & " o'clock."
end waffle

Which column of top is displaying the memory usage you see?

I guess, each time the sound to play will be loaded into memory
but never released (like load image in Xcode)

Making assumptions here. Let’s get more information first.

Remember that there are tools that can tell us what’s going on, if we know where to look.

Been out; sorry for the delay. The figure I quoted was the resident number, the virtual number was even larger. Since this thing only fires once an hour, it takes quite a while.

Crap. An idling AppleScript will crash when you record its memory allocation activity with Sampler. ObjectAlloc is similarly problematic, though not crashing, with a vanilla idling AppleScript, it seems. Hangs. [Edit: See below regarding memory analysis tools and AppleScript.]

Hmm. It’s your script that is showing the huge memory usage, not “Play Sound”, right?

What’s Play Sound?

Hello Adam

May you check in the app dictionary if there is a command allowing it to free the memory grabbed to load the used sound?

If there is not, you may try a simple trick:

play the wanted sound then play a dummy one: only one second of silence.
Maybe playing a new one free the used space.

Yvan KOENIG (from FRANCE mercredi 21 mars 2007 20:40:10)

I have never seen an AppleScript interface that allowed for memory management control. Is there one in modern use? Bad implementation if so. :frowning:

From the Play Sound page: Play Sound is a simple, no-fuss sound player for Mac OS X. It supports QuickTime sound files (for example, AIFF or MP3 files), Classic Mac OS System 7 sound files (files with a file type of ‘sfil’ that contain 'snd ’ resources), or any 'snd ’ resources embedded into any file. You can play an unlimited number of sounds concurrently or one at a time. You can loop sounds, repeat sounds, play specified portions of sounds, control the volume of sounds played, pause and continue sound play, and press the Escape or Command-Period keys to abort sound play.

Notice that I quit it in the script. Would it be leaving resources behind?

Play Sound can’t leave anything unfreed when it terminates. That memory is reclaimed by the system.

I’m running a version of your script, modified not to talk to other applications or send raw Apple Events, in a tight idle loop. Let this idle for a few hours and report what you see:

on idle
	local h
	
	tell (time of (current date))
		set h to ((it div hours + 11) mod 12) + 1
		tell (it mod hours)
			if (it < 10) then
				my testCall1()
			else if (it < 300) then
				my testCall2()
			end if
		end tell
		
		return 1
	end tell
end run

on testCall1()
	log "test call 1"
end testCall1

on testCall2()
	log "test call 2"
end testCall2

I don’t know if this helps because I never used it, and I don’t know how to use it, but I came across something recently that sounds like it might help. It’s a unix command called “leaks”. The man page for it seems detailed enough so maybe it can be of use to you.

Ah ha.

MallocDebug and ObjectAlloc freak out over AppleScript applications that are not Mach-O bundles. (The leaks utility also requires that AppleScript applications are Mach-O bundles as well. Otherwise, it will report that it doesn’t know how to inspect the target’s allocation zone.)

I’m not finding any leaks with the test case I posted. Compile your original script in Script Editor and save it as an application bundle. Launch the bundle with MallocDebug and let it run for a bit, then start hunting for leaks.

I’ll give that a try tomorrow M-S. Thanks, Adam

Hi Adam,

I’m running this script every hour with cron and leave Play Sound running.


set cur_hour to ((((time of (current date)) div hours) + 11) mod 12) + 1
repeat cur_hour times
	tell application "Play Sound"
		play alias "Macintosh HD:System:Library:Sounds:Purr.aiff"
	end tell
end repeat

You probably know this, but in previous AppleScript versions, there was a known memory leak with idle handlers. Maybe they didn’t get all the kinks out of it.

I was thinking that your script might be erroring. Wierd things might happen when idle handlers error.

I’d take the return statements out of the tell blocks and use one return statement as the last statement of every handler.

gl,

Thanks, Kel. I’ll look into running it with cron (or launchd). I really don’t want to give it up - it keeps me from getting so absorbed that I forget what time it is.

Yeah, idle handlers are interesting.

I just had an idea. Maybe you can create two apps with idle handlers that periodicly quits and restarts each other! :slight_smile: Like the Earth and the Moon.

Yeah, although I have lots of memory (2GB) and the ChimeHours idler stay-open doesn’t interfere with anything, it is up to 102/462 MB since I quit it and restarted it yesterday. Interesting because it doesn’t get much larger than that - those were the figures (roughly) after several weeks. This may not be a leak at all - the system might just be keeping its parts because there’s lots of room. In addition to Play Sound, there’s also Cepstral Voices (David) for the “Oops” - best speaking voice I’ve heard on a computer (though not free). I’ll continue to monitor it and see if it grows further.

Thanks for the suggestions, folks.

Hi Adam,

I agree totally, I bought the voice a few months ago.
The difference to the built-in voices is like the difference between pixel and vector graphics :wink:

Since Tiger launchd has been introduced, there is a easy way to define scheduled tasks,
which can e.g. trigger a script. Based on Craig’s Using launchd with AppleScript… article,
I wrote an installer, which creates the .plist file at the right place and loads the task via launchctl.
An unload function is also included, which unloads the task and removes the .plist file.
The script to be triggered must be in ~/Library/Scripts, but you can change the parameters in the first three lines.
The task runs on every clock hour:

property pListLabel : "com.chimehours"
property triggeredScriptName : "ChimeHours.scpt"

set triggeredScriptFolder to path to scripts folder as Unicode text

set userLibraryPath to (path to home folder as Unicode text) & "Library:"
set LaunchAgentsFolder to userLibraryPath & "LaunchAgents:"
set pListfile to LaunchAgentsFolder  & pListLabel & ".plist"
try
	tell application "Finder" to make new folder at userLibraryPath with properties {name:"LaunchAgents"}
end try

set PLIST_data to "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\">
<dict>
	<key>Label</key>
	<string>" & pListLabel & "</string>
	<key>LowPriorityIO</key>
	<true/>
	<key>Program</key>
	<string>/usr/bin/osascript</string>
	<key>ProgramArguments</key>
	<array>
		<string>osascript</string>
		<string>" & POSIX path of triggeredScriptFolder & triggeredScriptName & "</string>
	</array>
	<key>StartCalendarInterval</key>
	<dict>
		<key>Minute</key>
		<integer>0</integer>
	</dict>
</dict>
</plist>
"

set b to button returned of (display dialog "Load or Unload " & quote & pListLabel & ".plist" & quote buttons {"Cancel", "Unload", "Load"})
if b is "Load" then
	try
		set ff to open for access file pListfile with write permission
		write PLIST_data to ff as «class utf8»
		close access ff
	on error
		try
			close access file target
		end try
		display dialog pListLabel & " couldn't be created" buttons {"Cancel"} default button 1
	end try
	launchctl("load", pListfile)
else
	launchctl("unload", pListfile)
end if

----------------------
on launchctl(load, pListfile)
	try
		set launchctlCmd to "launchctl " & load & " -w " & quoted form of POSIX path of pListfile
		do shell script launchctlCmd
		if load is "unload" then do shell script "rm " & quoted form of POSIX path of pListfile
		display dialog quote & pListLabel & "\" successfully " & load & "ed" buttons {"OK"} default button 1
	on error
		display dialog load & " failed" buttons {"Cancel"} default button 1 with icon stop
	end try
end launchctl

PS: loading the task can take a while. In every case a status message will be displayed

Thanks, Stephan; You’ve saved me going back to read Craig’s article for a second or third time. :wink:

launchd looks more difficult than it is actually.
I used Lingon to figure out, what’s happening in the .plist file syntax when changing a parameter.

launchd is so much smarter than stay open scripts with idle handlers