There has been substantial interest in the difficult task of finding and fixing leaks in EOF, OpenStep and WOF applications.  As the EOF team was preparing for the final release of WebObjects 3.5, they wrote a small test app to help them find leaks in the framework.  This example will help you learn how to write an application that should be able to run indefinitely with running out of memory. Also, if you think you have encountered a serious leak in the framework, you can use this app as a minimal test case for illuminating the problem.

The app takes a few input arguments:

-batchsize m   [number, default 1]
-runs n        [number, default 1]
-trials t      [number, default 1]
-hang yn       [YES/NO, default NO]

For each trial, at runtime the app will, using the Movies EOmodel:

  - Empty the tables
  - Insert movies (insert m * n movies)
  - Add directors and actors (m*n objects fetched, m*n to-one
     faults fired, m*n to-many faults fired, m*n+m^2*n objects inserted)
  - Modify objects (m*n objects fetched, m*n to-one faults fired, m*n  
     to-many faults fired, m*n+m^2*n objects faulted in, m*n objects  
     deleted, m*n objects inserted, m*n+m^2*n objects mutated)

The -hang option makes the app pause before quitting.  This allows you to use pview or ps after the app finishes, rather than monitoring the app during its entire run.

The following results were obtained on Windows NT, using pview.exe to examine the image at the end of the run.  This app has only been tested using EOF 2.2, the EOF version that ships with WebObjects 3.5.  You might notice that the results aren't quite as deterministic as one might hope, but this is the raw untouched data we got.

Oracle...
Batch	Runs	Trials	Virtual-Size
 6	 4	  1	27980
 6	 4	 10	27980
 6	 4	 25	27980
10	10	 10	27980
--
10	10	100	28740
10	10	 10	28740

Sybase...
Batch	Runs	Trials	Virtual-Size
 6	 4	 10	32860
 6	 4	  1	32792
 6	 4	 25	32860
--
10	10	  1	33520
10	10	 10	33588
10	10	100	33588

Informix...
Batch	Runs	Trials	Virtual-Size
10	10	 10	25492
10	10	  1	23444
 6	 4	 10	23444
 6	 4	 25	23444
 6	 4	  5	23460
--
10	10	 10	25508
10	10	  1	23460
--
 6	 4	100	25508
10	10	(37+)	30352	[This test was aborted after 37 trials due to server failure.]

Here is the source code. You'll need to drop the Movies EOmodel into the app before compiling.

#import <Foundation/Foundation.h>
#import <EOControl/EOControl.h>
#import <EOAccess/EOAccess.h>
#import "Movie.h"
#import "Actor.h"
#import "Director.h"

void usage(void)
{
    NSLog(@"Usage: LeakTester -batchsize <batchsize> -runs <number of 
runs> -trials <number of trials> -hang <BOOL> 
WARNING!!!! THIS PROGRAM 
REMOVES ALL EXISTING DATA IN THE TABLES USED");
}

void logReport(NSString *modelPath, int batchsize, int runs, int trials)
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    EOModel *model;

    model = [[[EOModel alloc] initWithContentsOfFile:modelPath]
autorelease];

    NSLog(@"%@, batchsize = %i, runs = %i, trials = %i", [model
adaptorName], batchsize, runs, trials);

    [pool release];
}

void clearTables(NSString *modelPath)
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    EOModel *model;
    EOAdaptor *adaptor;
    EOAdaptorContext *context;
    EOAdaptorChannel *channel;
    EOSQLExpression *expression;

    model = [[[EOModel alloc] initWithContentsOfFile:modelPath]
autorelease];
    adaptor = [EOAdaptor adaptorWithModel:model];
    context = [adaptor createAdaptorContext];
    channel = [context createAdaptorChannel];

    NSLog(@"Dumping old data from tables...");

    [channel openChannel];

    expression = [[adaptor expressionClass] expressionForString:@"delete 
from LKMOVIE"];
    [channel evaluateExpression:expression];

    expression = [[adaptor expressionClass] expressionForString:@"delete 
from LKACTOR"];
    [channel evaluateExpression:expression];

    expression = [[adaptor expressionClass] expressionForString:@"delete 
from LKDIRECTOR"];
    [channel evaluateExpression:expression];

    // Informix complains if we do a COMMIT here; OK for Sybase and
Oracle.
    // expression = [[adaptor expressionClass]
expressionForString:@"commit"];
    // [channel evaluateExpression:expression];

    [channel closeChannel];

    [pool release];
}

void doit(NSString *modelPath, int batchsize, int runs)
{
    NSAutoreleasePool *pool, *pool0, *pool1, *pool2;
    EOEditingContext *editingContext = [EOEditingContext new]; // context 
with defaultCoordinator
    Movie *movie;
    Movie *lastMovie;
    NSArray *movies;
    NSArray *actors;
    NSArray *lastActors;
    Actor *actor;
    Director *director;
    int i;
    int j;
    int priKey;
    int m;

    // For apps with models stored in the main bundle, the following is
    // unnecessary, since the defaultGroup will be automatically built 
from
    // the contents of the main bundle and and any referenced frameworks.
    pool = [[NSAutoreleasePool alloc] init];
    [EOModelGroup setDefaultGroup:[[EOModelGroup new] autorelease]];
    [[EOModelGroup defaultGroup] addModelWithFile:modelPath];

    pool0 = [[NSAutoreleasePool alloc] init];
    // Create and insert root set
    NSLog(@"Inserting root set...");
    priKey = 0;
    for ( i = 0 ; i < runs ; i++ ) {
        pool1 = [[NSAutoreleasePool alloc] init];
        for ( j = 0 ; j < batchsize ; j++ ) {
            priKey++;
            movie = [[[Movie alloc] init] autorelease];
            [movie setMovieId:[NSDecimalNumber
decimalNumberWithMantissa:priKey exponent:0 isNegative:NO]];
            [movie setTitle:[NSString
stringWithFormat:@"Movie_%i",priKey]];
            [editingContext insertObject:movie];
        }
        [editingContext saveChanges];
        [[editingContext rootObjectStore] invalidateAllObjects];
        [[editingContext undoManager] forgetAll];
        [pool1 release];
    }
    [pool0 release];

    priKey = 0;
    for ( i = 0 ; i < runs ; i++ ) {
        pool1 = [[NSAutoreleasePool alloc] init];
        pool2 = [[NSAutoreleasePool alloc] init];
        NSLog(@"Fetching Movies batch and adding objects for run
#%i...",i);
        movies = [editingContext
objectsWithFetchSpecification:[EOFetchSpecification
fetchSpecificationWithEntityName:@"Movie" qualifier:[EOQualifier
qualifierWithQualifierFormat:@"(movieId > %d) AND (movieId <= %d)",
i*batchsize, (i+1)*batchsize] sortOrderings:nil]];

        for ( j = 0 ; j < [movies count] ; j++ ) {
            movie = [movies objectAtIndex:j];
            priKey++;
            director = [[movie director] self];  // Just to tickle the 
fault
            director = [[Director alloc] init];
            [editingContext insertObject:director];
            [director setDirectorId:[NSDecimalNumber
decimalNumberWithMantissa:priKey exponent:0 isNegative:NO]];
            [director setName:[NSString
stringWithFormat:@"First_%i",priKey]];
            [movie addObject:director
toBothSidesOfRelationshipWithKey:@"director"];
            [director release];
            for ( m = 0 ; m < batchsize ; m++ ) {
                priKey++;
                actor = [[Actor alloc] init];
                [editingContext insertObject:actor];
                [actor setActorId:[NSDecimalNumber
decimalNumberWithMantissa:priKey exponent:0 isNegative:NO]];
                [actor setName:[NSString
stringWithFormat:@"First_%i",priKey]];
                [movie addObject:actor
toBothSidesOfRelationshipWithKey:@"actors"];
                [actor release];
            }
        }

        NSLog(@"Saving...");
        [editingContext saveChanges];

        [editingContext invalidateAllObjects];
        [[editingContext rootObjectStore] invalidateAllObjects];
        [[editingContext undoManager] forgetAll];
        [pool2 release];
        [pool1 release];
    }

    for ( i = 0 ; i < runs ; i++ ) {
        pool1 = [[NSAutoreleasePool alloc] init];
        pool2 = [[NSAutoreleasePool alloc] init];
        NSLog(@"Fetching Movies batch and mutating for run #%i...",i);
        movies = [editingContext
objectsWithFetchSpecification:[EOFetchSpecification
fetchSpecificationWithEntityName:@"Movie" qualifier:[EOQualifier
qualifierWithQualifierFormat:@"(movieId > %d) AND (movieId <= %d)",
i*batchsize, (i+1)*batchsize] sortOrderings:nil]];

        for ( j = 0 ; j < [movies count] ; j++ ) {
            movie = [movies objectAtIndex:j];
            priKey++;
            director = [[movie director] self];  // Just to tickle the 
fault
            director = [[Director alloc] init];
            [editingContext insertObject:director];
            [director setDirectorId:[NSDecimalNumber
decimalNumberWithMantissa:priKey exponent:0 isNegative:NO]];
            [director setName:[NSString
stringWithFormat:@"First_%i",priKey]];
            [movie addObject:director
toBothSidesOfRelationshipWithKey:@"director"];
            [director release];
            if ( j % 2 ) {
                actors = [[movie actors] shallowCopy];
                lastMovie = [movies objectAtIndex:j-1];
                lastActors = [[lastMovie actors] shallowCopy];
                for ( m = 0 ; m < [actors count] ; m++ ) {
                    [movie removeFromActors:[actors objectAtIndex:m]];
                }
                for ( m = 0 ; m < [lastActors count] ; m++ ) {
                    [movie addToActors:[lastActors objectAtIndex:m]];
                }
                for ( m = 0 ; m < [lastActors count] ; m++ ) {
                    [lastMovie removeFromActors:[lastActors
objectAtIndex:m]];
                }
                for ( m = 0 ; m < [actors count] ; m++ ) {
                    [lastMovie addToActors:[actors objectAtIndex:m]];
                }
                [actors release];
                [lastActors release];
            }
        }

        NSLog(@"Saving mutations...");
        [editingContext saveChanges];

        [editingContext invalidateAllObjects];
        [[editingContext rootObjectStore] invalidateAllObjects];
        [[editingContext undoManager] forgetAll];
        [pool2 release];
        [pool1 release];
    }
    [EOModelGroup setDefaultGroup: nil];
    [pool release];
    [editingContext release];
}

void main (int argc, const char *argv[])
{
    NSAutoreleasePool * pool;
    BOOL hang;
    int batchsize, runs, trials;
    NSString *modelPath;
    int trial;

    pool = [[NSAutoreleasePool alloc] init];
    hang = [[NSUserDefaults standardUserDefaults] boolForKey:@"hang"];
    batchsize = [[NSUserDefaults standardUserDefaults]
integerForKey:@"batchsize"];
    if (!batchsize) batchsize = 1;
    runs = [[NSUserDefaults standardUserDefaults] integerForKey:@"runs"];
    if (!runs) runs = 1;
    trials = [[NSUserDefaults standardUserDefaults]
integerForKey:@"trials"];
    if (!trials) trials = 1;
    modelPath = [[[NSBundle mainBundle] bundlePath]
stringByAppendingPathComponent: @"LeakTester.eomodeld"];

    logReport(modelPath, batchsize, runs, trials);

    for (trial = 0; trial < trials; trial++) {
        NSLog(@"Trial %i beginning...", trial);
        clearTables(modelPath);
        doit(modelPath, batchsize, runs);
        NSLog(@"Trial %i done.", trial);
    }

    logReport(modelPath, batchsize, runs, trials);

    [pool release];

    if (hang) {
        NSLog(@"Hit RETURN to exit...");
        getchar();
    }

    NSLog(@"Done.");
    exit(0);       // insure the process exit status is 0
}