Garbage Collection Gotchas in webOS

by on Apr.02, 2010, under Palm, webOS

Making the jump to mobile development from desktop and server development can lead to quite a few gotchas due to the resource constraints inherent to mobile environments. This became very clear to me today while debugging an intermittent failure in the Facebook application’s news and notifications scenes.

Like every other debugging session for intermittent issues there were quite a few choice words used in the process and many dead ends, but eventually it became apparent that some vital Mojo.Service.Request service requests were not calling any of their notification callbacks. Expecting the onComplete callback to occur at the very least, I was very perplexed until I spoke with some of my esteemed colleagues at Palm and they pointed me to the garbage collector as the possible culprit for this situation.

The code in question was instantiating Request objects but not maintaining references to these objects. This is fine up until the point that the garbage collector runs and the request object as well as all callbacks are collected. At this point these requests could no longer notify the application of their completion, blocking further processing of the queues associated with these requests.

After storing the instantiated Request objects until completion all of the calls that were previously disappearing into the ether began to return as expected.

The moral of the story is that you need to be very careful about what references are maintained when working with garbage-collected languages in memory-constrained environments. If you want something to stay around, like callback methods, you need to be certain that it is referenced somewhere. To ease this task for Mojo.Service.Request calls (controller.serviceRequest calls are safe as the controller maintains a reference to the request object or cancels the request prior to popping the scene), we implemented the following wrapper:

var ServiceRequestWrapper = Class.create({
    initialize: function() {
        this.requestId = 0;
        this.requests = {};
    },

    request: function(url, optionsIn, resubscribe) {
        Mojo.Log.info("Service request wrapper url: '%s' method: '%s' CALLED", url, optionsIn.method);
        var options = Object.clone(optionsIn),
            requestId = this.requestId++;
        options.onComplete = this.completeHandler.bind(this, url, optionsIn, requestId);

        this.requests[requestId] = new Mojo.Service.Request(url, options, resubscribe);
        return this.requests[requestId];
    },

    completeHandler: function(url, optionsIn, requestId, response) {
        Mojo.Log.info("Service request wrapper url: '%s' method: '%s' requestId: %d COMPLETE", url, optionsIn.method, requestId);
        delete this.requests[requestId];

        optionsIn.onComplete && optionsIn.onComplete(response);
    }
});

By instantiating this on a location that will not be collected during the application’s lifetime and using the request method rather than direct Mojo.Service.Request calls for all requests that the result is necessary you can avoid this problematic scenario as well as save the choice words for a later time :)

:, , ,

7 Comments for this entry

  • Steve Witham

    @kpdecker, you are being WAY too easy on the Palm people, and trying to internalize their problem as if you should have anticipated it.

    GC is supposed to hold onto things until they’re not referenced. Period. The whole point of GC is you’re not supposed to have to worry about when deallocation happens. It only happens after it doesn’t matter.

    The OS needs to keep a reference to the request object in order to route the response through it. If they weren’t going to treat that as a normal reference, they should have documented that loud and clear, and if they didn’t, their code has a bug. Even if they did document it, this is so unexpected it’s at least a misfeature, tight memory or not.

    I get that less is automatically taken care of in a smaller system. But does it make sense to try to anticipate future GC problems in tight memory, or are API bugs a lot more random than that? I think the moral is, we rely on APIs to work as advertised, we can’t anticipate API bugs based on hindsight, and our main hope is to convince their maintainers to fix bugs and document gotchas rather than rationalize them.

  • kpdecker

    @Steve Witham, I completely understand. This was very surprising for me when I found out about it.

    One of the goals of this blog post was to help document this behavior and provide some resources for developers to manage this behavior. I have also let our documentation team know that it would be helpful to add documentation on this behavior.

  • Johan

    @kdpdecker, Shouldn’t you special case requests that actually are subscriptions (ie, not delete them)? (May not be an issue in your code, but for completeness)

  • kpdecker

    @Johan, Good catch. Update to follow!

  • Arcticus

    It would seem that this approach would be needed for any type of async operation such as an AJAX call. So it would seem a more generic wrapper would be needed which begs the question shouldn’t this be implemented at the OS level?

  • kpdecker

    @Arcticus I’m following up with our Engineering department to see (1) if this applies to AJAX requests and (2) what can be done at a higher level to handle this in a more general way.

  • kpdecker

    @Arcticus
    AJAX request objects should be maintained for the duration of the the request. The main difference in cleanup for the service request objects is the different lifetime that subscription requests may have. We are investigating improving this situation in a later release, but in the mean time I would recommend using techniques like ServiceRequestWrapper to help prevent errors under the current implementation.

Visit our friends!

A few highly recommended friends...