By Dennis Austin
It takes a lot of planning to put together a memory policy, and you never get it right the first time. No matter how much time you spend, it always seems you could be using memory more efficiently.
The memory manager offers a wealth of tools for its control. The grow zone function is probably the most powerful, but it is only a tool, not a solution. This article explains ways you can use the grow zone function to implement a memory policy.
All of us who program the Macintosh understand the memory manager. We all use it and rely on it (and get bitten by it) daily. But the memory manager only provides a mechanism for viewing your heap as a collection of blocks. Either by design or by default, every program must also have some policy for those blocks — how the code will be segmented, how the data will be organized into blocks, whether it can be moved out to disk when necessary, which resources can be purged and which not, how much memory is enough and — most critical — what to do when you run out.
The Grow Zone Function — a Review
Every heap zone can have its own grow zone function. The memory manager calls it only when it has tried to honor a memory request, but failed. It has tried moving blocks around to collect more space in a single spot. It has tried throwing away blocks that are marked purgeable. It has tried growing the heap zone.
The grow zone function is a last resort. If the grow zone function can’t turn up some memory, the original request (NewHandle, etc.) will fail. The traditional advice to programmers is to purge resources or other rebuildable blocks. Unloading code segments is a possibility. Even unlocking blocks can help because it may allow memory to be shuffled in such a way that more free space can be coalesced into a single block.
There does not seem to be any restriction on the possibilities. Even allocating some smaller amount of memory is possible; the memory manager is re-entrant. This might be necessary if you wanted to write some data out to disk, for example. That could result in the grow zone function being called again, of course, so you have to know what you are doing.
There are a couple of technicalities that you have to watch out for. First of all, Technote #136 warns that A5 (the pointer to your global data) may not be set properly when the memory manager calls the grow zone function. I’ve never seen this happen, but, just in case, you should call SetupA5 at the beginning of your routine and RestoreA5 at the end. This is the sort of thing the memory manager really ought to do, but apparently doesn’t.
Second, there may be a relocatable block, usually one in the process of being resized, that you must be careful not to move. You can call GZSaveHnd to find out if there is such a block. If it returns a non-nil handle, you need to be sure your grow zone function doesn’t move it. As you know, the block will not move unless you make further calls on the memory manager. If you make such calls, you should lock this handle until you are done. Be sure to return the handle to its original state; it may have been locked when you started. The best way to do this is with HGetState and HSetState.
The grow zone function returns a longint, but the result is used as a Boolean. Non-zero tells the Memory Manager that the grow zone function has done something and that it should try allocating again. Zero tells the Memory Manager to give up and return a MemFullErr. In the case of a MemFullErr, NewHandle and NewPtr return NIL. SetHandleSize, SetPtrSize, and ReallocHandle will do nothing.
When an allocation fails, any number of things might happen. The code that was doing the allocation may have a suitable recovery or it may not. Your program may have excellent recovery for any allocation failure, but what if the fatal call comes from system? It uses the memory manager too, and you don’t know what it might do in case of failure.
Actually, the system does a pretty good job. In many instances, a trap fails to complete its job (in NewWindow, for example) and so reports its failure back to its caller in turn. Proper recovery is pushed back another level, eventually reaching the application so that it can do the real recovery. In other cases, the system may have a more time-consuming alternative algorithm that can still succeed without as much memory.
There are times, though, when recovery is out of the question. If allocation fails in a LoadSeg call, for example, there isn’t any place to go but SysError. And that’s just where it goes.
Why Use a Grow Zone Function?
It isn’t necessary to have a grow zone function. You can check the result of every allocation to be sure it succeeded, do graceful recovery, and make sure you warn if memory is too low. But the grow zone function offers the following advantages:
- It allows you to handle memory crunches without interrupting your program flow. If there are actions you can take to free up memory, it’s much better to do them in the grow zone function because it avoids having to write the same logic at every allocation call.
- It allows you to handle memory crunches that don’t originate in your own code. The system allocates memory too, and the only way you can help it in a pinch is to supply a grow zone function.
- It gives you a handy check on the adequacy of your memory supply. If the grow zone function is called, there is a good chance that you should warn the user about memory being low.
Job One: Finding Memory
Sooner or later, your grow zone function is going to get a call. Its first job is to find some memory for the memory manager. To make this possible, you better plan it so there is some method that is likely to work.
The one suggested in Inside Macintosh is to keep important resources or data marked non-purgeable for normal use, but purge them yourself in the grow zone function. You may also have data that can be written out to disk when memory gets tight. Both of these are good ideas that you can extend or modify to fit your needs.
Scott Knaster, in his book How to Write Macintosh Software, suggested that an application might set aside a memory reserve that the grow zone function could tap when necessary. To use this technique, you allocate a relocatable block that you use as a monitor. If the grow zone function is called, you can give up part of the reserve with SetHandleSize. You now know, however, that memory is getting low. When you get back to the event loop, you should give the user a warning and suggest (or mandate) that she take some action that would free some memory. Closing a window is the usual candidate.
Also, in your idle loop, you can try to regain the original size of the reserve. That way you’ll be ready the next time some extra memory is needed and you can give another warning.
Scott suggests that even in the worst case, when your reserve is totally gone, you still not return zero from the grow zone function. Instead, force a save of the open files and then quit. You’ll probably need some memory to do the saves, though, so you better have a little emergency memory in your back pocket. This can usually be obtained by unloading some segments that you know you won’t use anymore because you’re quitting.
Notice that using a reserve block to monitor the fullness of memory has achieved a wonderful result. None of your requests for memory will ever fail! If you ever run out out of memory, the grow zone function will see to it that the rest of your program never knows about it.
Of course, the user is going to know about it, and she’s not likely to be too happy. But if the grow zone function did its job right, there were warnings — even grave warnings. Only in the case where the user ignores repeated warnings do you completely run out. Even then, the grow zone function can quit the program smoothly rather than going through SysError. What else can a user expect?
Knowing that none of your allocations will fail is a great convenience in writing your program. Recovery from allocation failure isn’t always hard, but it usually is. The worst case is when you need to back out of a partially completed operation. Recovery is also tough to test adequately. If you can assume that allocations always succeed, your program will be much more reliable.
Job Two: When the Cupboard is Bare
Monitoring memory with a reserve block is a great idea. Unfortunately, quitting in the event of complete memory exhaustion sometimes isn’t. Even though your program may be expecting its allocations to always succeed, there are times when you need to return zero and fail.
First, realize that your application is not the only guy allocating memory. The ROM sometimes wants temporary memory. There may be guests in your heap that also want some. Desk Accessories are a good example, but printer drivers are the most greedy. These callers don’t know what kind of grow zone function is installed, so unlike your program, they must always be ready for allocation failures.
Their preparedness means that you can return zero, but it doesn’t explain why you must. The real problem is that these programs sometimes need to have their allocations to fail! They have algorithms that perform better when they can get more memory. The LaserWriter SC driver is an example. It can print faster if it can image more of the page at once, but that requires more memory.
These guys find out how much they can get by asking for it. The idea is to start with a large amount, and, if that fails, try successively smaller amounts until one of them succeeds. Then you really know what was available.
Why don’t they just ask how much memory is free instead of belligerently allocating it? The memory manager offers a number of ways to ask about the amount of memory available, but unfortunately they don’t quite address the problem. Some applications shepherd a lot of memory that they could free up, but only if their grow zone function is called. The only way to find out if you can get it is to ask for it.
The existence of this strategy makes it harder to design a good grow zone function. Returning zero may cause your application to crash in some cases, but in other cases it is the right thing to do. How do you tell how serious the request is? The memory manager doesn’t offer any help.
The second job of the grow zone function, then, is what to do when it can’t find any more memory. There are really only two choices:
• Return zero and hope that the there will be adequate recovery for an allocation failure, or
• Try to save the data and then die with dignity.
Who’s Asking for All This Memory?
Before you can decide whether to return zero, you need to consider who might be asking for the memory, and what its potential recovery strategy is.
- Calls made on NewHandle and friends (NewPtr, SetHandleSize, etc.) from your application for your own data structures. If you return zero for these, recovery is up to you.
- Calls made from the system that are allocating structures on your behalf. NewWindow is an example of a trap that allocates memory that henceforth belongs to the application. Others, like OpenResFile, may allocate memory that is only indirectly under application control. Again, recovery will be up to you because routines like this have some way of reporting their failure.
- Calls made from the system when it needs temporary space. A good example of this is DrawPicture. If the picture contains a bitmap, it must be unpacked into a temporary bitmap that may be much larger than the picture being drawn. The font manager and the color manager are also frequent consumers of temporary memory. You can return zero for these calls without courting disaster, but the results can sometimes be nearly as bad. Performance can degrade unacceptably and strange things may appear on the screen. Also, recovery from allocation failure is often the least tested code, and it may not work right.
- Calls from the segment loader. If the grow zone function returns zero for these, you’ll get SysError 15. Note: Somewhere in the system, there are a few other critical calls that can’t handle an allocation failure. These result in SysError 25 (out of memory) if they fail. Unfortunately, I don’t know what they are or how to tell when they are calling. To my knowledge, however, none of them ask for very much memory.
- Calls made from guests in your heap. Any properly written DA or driver must be prepared for memory errors in every instance, because it is not in control of memory in general. Returning zero is usually safe for these calls. Indeed, some of them may simply be probes to see how much memory is available.
Distinguishing the Callers
To allow the program to live in its happy all-allocations-succeed world, the grow zone function must able to recognize which calls are coming from the application and are expected to succeed or die. One way to do this is to set a “must-have” flag whenever you ask for memory. The grow zone function can see this flag and know that the request is from the application. You could make the flag setting automatic by writing your own version of NewHandle, SetHandleSize, and so on, setting the flag before calling the trap (and resetting after), but that strategy breaks down if you want to cover all the routines like NewWindow too. You have to write your own version of a long list of toolbox routines.
Most of these toolbox routines don’t allocate very much memory, though. Let’s just assume that any request less than some fixed limit should be treated as a “must-have.” That will cover a host of routines like NewWindow. If you choose this fixed size to be larger than your largest resource (not counting code), the same technique will assure that you can always get any resource. The size must, however, be small compared to the total reserve size or a couple of calls could chew up the whole reserve in one operation.
We can still leave the must-have flag mechanism in place for NewHandle, however, so that the application can demand memory without having to worry about whether it is over the fixed size limit.
There is one caller that we know for sure we never want to return zero to, and that is the segment loader. Segments can be up to 32K bytes, which is undoubtedly larger than the fixed limit we set for surefire allocations. One choice is to patch the LoadSeg trap. In your patch, you can set the must-have flag so that your grow zone function will not return zero. Depending on your program, you may be able to think of some other way to recognize the segment loader.
If you decide to patch the LoadSeg trap, your patch should set the flag and jump to the real LoadSeg. Don’t call it as a subroutine because LoadSeg uses its return address, which should not be in your patch. You also need to unpatch the trap when you exit. One way to do this is to also patch ExitToShell. That one unpatches everything, including itself, and then re-executes the ExitToShell trap.
Look Before You Leap
Now we have a pretty complete strategy. Still, we have to admit that everything we have talked about is not enough. In spite of the careful preparations you make for your grow zone function, you can still run out of memory without warning.
How? The warnings come from code in the event loop that notices the reserve being low. What if some command chews up the rest of your memory without ever getting back to the event loop? A single command may call for many small allocations.
Opening a document is the perfect example. You need space for the document data and your own internal overhead on a semi-permanent basis. You’ll even want to make sure there is some extra space to work in after the document is open.
Commands like this must be able to fail gracefully and report something like “There isn’t enough memory to… ” This is hard to do with the grow zone function because you don’t find out that there is a problem until you are part way through.
Commands like this demand preflighting. That means you must estimate the memory requirements in advance and check that it is available before you begin. Use routines like PurgeSpace to discover current memory availability. The term comes from the preflight check that is always performed on aircraft. It is always easier to handle problems before you take off than it is in mid-flight.
What Happens Where
In summary, there are five parts to implementing this memory policy using a grow zone function.
- Initialization: allocate a relocatable block to monitor the application heap free space. Install your grow zone function. Patch LoadSeg if appropriate.
- Idle loop: Check to see how the monitor block looks. If it has gotten smaller, it’s time to warn the user that memory is running low. If it has gotten real small, you should try to force a window closing or some other action that will free memory.
- Grow zone function: Start by purging resources or any technique that suits your program. If that doesn’t work, you can free up some of the reserve block.
- If it can’t satisfy the request even by freeing all of the reserve, it has to decide whether to return zero or to quit. Basically, you should return zero when the request is large and it didn’t originate in your program, and it didn’t come from the segment loader.
- At memory allocations: If you want to be sure the allocation will succeed, set your must-have flag before calling. Reset the flag when the trap returns.
- User commands: There are some types of memory usage where a grow zone function is of no help. Use preflight checking for these.
About the Author
Dennis Austin is one of the original creators of PowerPoint at Forethought, Inc., and is now responsible for Macintosh development at Microsoft’s Graphics Business Unit in Menlo Park, California. He has been programming Macs since the days of the Lisa, having converted to application programming after a decade of work in compilers, operating systems, and computer architecture.