Memory Reclaimers, Low Memory Situations, and Severity Levels

The Openwave Phone Suite Version 7.0 platform runs well on mobile devices, including some with tiny memories. Sometimes code tries to allocate memory but no more is available. You have learned to write code that checks for failed allocations. Your code should also give up memory when that memory is needed for something more important. For example, a game with a dancing fish animation in the background should free that memory if the phone application would otherwise fail to allocate memory to handle an incoming call.

When you allocate memory, you can specify a severity level. This expresses this memory's importance. For example, a cached copy of a dancing fish animation should have a low severity level; the sole copy of a received SMS message should have a high severity level.

Most memory allocation functions use the highest severity level, kOpMemSeverityFatal. However, some allow you to specify a lower severity level. For example, the OpMidAlloc function allocates a block of moveable memory with severity kOpMemSeverityFatal, but the OpMidAllocX function takes a severity parameter.

(The header file op_mem_severity.h #defines many other names for the severity levels. Openwave Phone Suite Version 7.0 platform internal code uses these names when allocating memory. If you are choosing a severity level, you might look in this file for hints. However, it is not easy to figure out what these names are used for.)

In the Do_Nothing application, you saw that allocating memory could result in an kOutOfMem_OpException exception. If you specify a non-fatal severity level, then a failed memory allocation does not throw an exception; instead, it returns nil. (There is no non-fatal version of operator new.)

There are two approaches to relinquishing memory in low-memory situations:

Moveable, Purgable Memory

Your code can allocate moveable memory, referring to it by an OpMID handle. When you need a pointer to the memory, lock it in place on the heap. When you don't need a pointer to the memory, unlock it. The memory manager manipulates unlocked moveable memory blocks. It moves them. It may also purge (free) them.

You can specify a purge level for an OpMID. By default, an OpMID is not purgable. Call the OpMidSetPurgable function to make an OpMID purgable. This function takes a severity level as a parameter; it associates this severity level with the OpMID. If code tries to allocate more memory than is available, the memory manager considers all unlocked moveable memory blocks. If the memory manager finds an unlocked moveable memory block with a purge level less than than or equal the allocation's severity level, the memory manager purges (frees) the block.

Recall this code snippet from the Do_Nothing application:

OpAutoMLock handleLock(dataHandle);
char * dataString = (char *) handleLock.get();
if (!dataString) { /* ... */ }

If locking an OpMID returns a nil pointer, then the memory manager purged that block. Your code must handle this gracefully. It might reconstruct the data. It might quit. (For example, the Do_Nothing application quits.) It might continue without that resource. (For example, a game might continue to play without its decorative animated dancing fish in the background.)

The Do_Nothing application doesn't set its memory purgable, so the memory manager should never free that memory. It need not handle this case.

Memory Reclaimer Functions

You can write and register a memory reclaimer function. The memory manager calls this function in low-memory situations. The memory reclaimer function can try to free memory.

Writing a good, deadlock-safe memory reclaimer function is not easy. Given a choice between writing a memory reclaimer function and using purgable moveable memory, use purgable moveable memory.

The memory reclaimer function is of type OP_MEMORY_RECLAIM_FX:

typedef U32 OP_MEMORY_RECLAIM_FX( U32 bytesRequired, U32 severity, void* context );

MEMRECLAIMER OpAddMemReclaimer( OP_MEMORY_RECLAIM_FX* fx, void* context );

If code tries to allocate more memory than is available, the memory manager calls registered memory reclaimer functions. Each function may be called a few times for one allocation attempt. For example, if code tries to allocate memory with medium severity, the memory manager calls the registered memory reclaimer functions, passing a low severity. If this does not free enough memory, the memory manager calls the memory reclaimer functions again, this time passing a medium severity. If this does not free enough memory, the memory manager purges moveable memory. If this does not free enough memory, the memory manager gives up.

Your memory reclaimer function might look like:

/* pseudo-code */
static U32 FishGameMemReclaim(U32 needed, U32 severity, void* context) 
{
  FishGameApplication* app = (FishGameApplication*) context;
  switch(severity) {
  case kOpMemSeverityLow: {
    app->decorationMutex.acquire();
    size_t gotBack = app->freeDecoration();
    return gotBack;
  }
  case kOpMemSeverityHigh: {
    app->vitalsMutex.acquire();
    size_t gotBack = app->freeVitals();
    app->requestQuit();
    return gotBack;
  }
  default:
    break;
  }
  return 0;
}

You set the context parameter when registering the function:

bool FishGameApplication::onAppEvent(OpAppEvent::Msg msg, OpEvent &evt) {
  switch(msg) {
  case OpAppEvent::kInit: 
    fReclaim = OpAddMemReclaimer(FishGameMemReclaim, /* context = */ this);
    // ...
  case OpAppEvent::kRequestQuit: 
    if (fReclaim) OpRemoveMemReclaimer(fReclaim); fReclaim = nil;
    // ...
}

Be aware of threading issues if you write a memory reclaimer. If you write an application and a memory reclaimer to reclaim that application's memory, the memory reclaimer is not likely to run in your application's thread. This example's pseudo-code uses semaphores to make sure that a memory reclaimer in one thread doesn't free resources in use by another thread. This introduces new opportunities for deadlock as unrelated applications block on each other.

/* If the "new MyObject" triggers the memory reclaimer, 
   this will deadlock. Don't do this. */

MyObject* gReclaimableObject; 
void MyFunction() {
  myMutex.acquire();
  gReclaimableObject = new MyObject; // DANGER!
  myMutex.release();
}

U32 MyReclaimer( U32 request, U32 severity, void* context ) {
  myMutex.acquire(); // DANGER!
  delete gReclaimableObject;
  gReclaimableObject = NULL; 
  myMutex.release();
}

To avoid the deadlock shown above, don't allocate the memory while acquiring the same semaphore which the memory reclaimer grabs:

void MyFunction() {
  MyObject * mo = new MyObject;
  myMutex.acquire();
  gReclaimableObject = mo;
  myMutex.release();
}

Beware deadlock in any memory reclaimer. Any call that allocates or reallocates memory might call the memory reclaimer.

For more information about memory reclaimer API, see the op_malloc.h file reference.