Thursday, December 2, 2010

This is not LISP

Patrick Logan counters complaints about LISP's syntax with the following example of LISP's power:
(apply + (take 1000 (iterate inc 1)))
Hmm..I find the following preferable:
(1 to: 1000 ) reduce + 0

Sunday, May 9, 2010

iPhone XML: from DOM to incremental SAX

In my last post, I extended Ray Wenderlich's XML parser comparison to MAX, and performance seemed to be about 2x better than the nearest competitor, TBXML and around 7x faster than NSXMLParser, Cocoa Touch's built-in XML parser.

While the resulting code has been posted here, I haven't yet explained how I got there.  First, I downloaded both Ray's project and Objective-XML 5.3.   iPhone OS requires a bit of trickiness to get a reusable library, partly due to the fact that frameworks or dynamic libraries are not supported, partly due to the fact that simulator and device are not just different Xcode architectures of a single SDK, and not even just different SDKs, but actually different platforms.  If anyone can tell me how to create a normal target that can compile/links for both the simulator and the device, I'd love to hear about it!

So, in order to compile for iPhoneOS, you'll need to select the iXmlKit target:

selector.png

You'll need to build twice, changing Active SDK settings once to the the device and once to the simulator.  You will then have two copies of the libiXmlKit.a library, one in the directory Release-iphoneos, the other in Release-iphonsimulator (both relative to your base build directory):

 

marcel@mpwls[LD]ls -la ~/programming/Build/Release-iphoneos/ 
-rw-r--r--   1 marcel  staff   521640 May  9 19:52 libiXmlKit.a

 

These two copies then need to be joined together using the  lipo command to create a single library that can be used both with the simulator and the device.

lipo -create Release-iphoneos/libiXmlKit.a Release-iphonesimulator/libiXmlKit.a -output  Release/libiXmlKit.a

(Newer Objective-XML versions will have a shell-script target that automates this process).  Once I had the fat libiXmlKit.a library, I created a new MAX group in the XMLPerformance project, and copied both the library and the MAX header file into that group:

MAX-in-XMLPerf.png

 

I then created a new MAX Song parser class:

#import "iTunesRSSParser.h"

 

@interface MAXSongParser : iTunesRSSParser {

  id parser;

  NSDateFormatter *parseFormatter;

}

@end

The implementation is also fairly straightforward, with your basic init method:

 

#import "MAXSongParser.h"

#import "MPWMAXParser.h"

#import "Song.h"

 

@implementation MAXSongParser

 

-init

{

 self=[super init];

    parseFormatter = [[NSDateFormatter alloc] init];

    [parseFormatter setDateStyle:NSDateFormatterLongStyle];

    [parseFormatter setTimeStyle:NSDateFormatterNoStyle];

    MPWMAXParser *newParser=[MPWMAXParser parser];

    [newParser setUndefinedTagAction:MAX_ACTION_NONE];

    [newParser setHandler:self forElements:[NSArray arrayWithObjects:                 @"item",@"album",@"title",@"channel",@"rss",@"category",nil]

               inNamespace:nil prefix:@"" map:nil];

    [newParser setHandler:self forElements:[NSArray arrayWithObjects:

                 @"releasedate",@"artist",@"album",nil]

             inNamespace:@"http://phobos.apple.com/rss/1.0/modules/itms/" 

             prefix:@"" map:nil];

    parser=[newParser retain];

   return self;

}

The MAX parser is initialized in this init method.  We define the elements we care about using the two "setHandler:forElements:inNamespace:prefix:map:" messages, one for each namespace we will be dealing with.  In the default (RSS) namespace, we are interested in the "item", "album", "title", "channel", "rss" and "category" elements.  In Apple's special "itms" namespace, we will handle "releasdate", "artist" and "album".  Setting MAX_ACTION_NONE as the undefined tag action means that the parser will ignore elements not listed as interesting and all their sub-elements.

Songs are created in the -itemElement:... method, which turns the relevant child-elements of the item element into Song attributes:

-itemElement:children attributes:attributes parser:parser

{
   Song *song=[[Songalloc] init];
   [song setAlbum:[children objectForUniqueKey:@"album"]];
   [song setTitle:[children objectForUniqueKey:@"title"]];
   [song setArtist:[children objectForUniqueKey:@"artist"]];
   [song setAlbum:[children objectForUniqueKey:@"album"]];
   [song setCategory:[children objectForUniqueKey:@"category"]];
   [song setReleaseDate:[parseFormatter dateFromString:[children objectForUniqueKey:@"releasedate"]] ];
   return song;
}

Two more methods make the actual parse process complete:  <channel> elements have one or more <item> elements, so we want to return all of them, using "objectsForKey:":

-channelElement:children attributes:attributes parser:parser
{
  return [[children objectsForKey:@"item"] retain];
}

Finally, there are a bunch of elements that we have defined interest in but treat identically...these can be handled using the "default" element handler:

 

-defaultElement:children attributes:attributes parser:parser
{
  return [[children lastObject] retain];
}

 

That concludes the routines that actually parse the XML into objects, now for kicking off the parser. With the timing code removed, the method is fairly straightforward:

 

- (void)downloadAndParse:(NSURL *)url {
   id pool=[NSAutoreleasePool new];
   [parserparse: [NSData dataWithContentsOfURL:url]];
   for ( id song in [parserparseResult] ) {
       [self performSelectorOnMainThread:@selector(parsedSong:)
             withObject:song waitUntilDone:NO];
   }
   [pool release];
}
With the timing code, it all gets a bit messier:

 

- (void)downloadAndParse:(NSURL *)url {
   id pool=[NSAutoreleasePool new];
   [self performSelectorOnMainThread:@selector(downloadStarted) withObject:nil waitUntilDone:NO];
   NSData *data=[NSData dataWithContentsOfURL:url];
   [self performSelectorOnMainThread:@selector(downloadEnded) withObject:nil waitUntilDone:NO];
   NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate];
   [parserparse:data];
   for ( id song in [parserparseResult] ) {
      [selfperformSelectorOnMainThread:@selector(parsedSong:) withObject:song waitUntilDone:NO];
   }
   NSTimeInterval duration = [NSDatetimeIntervalSinceReferenceDate] - start;
   [selfperformSelectorOnMainThread:@selector(addToParseDuration:) withObject:[NSNumbernumberWithDouble:duration] waitUntilDone:NO];
   [selfperformSelectorOnMainThread:@selector(parseEnded) withObject:nilwaitUntilDone:NO];

[[NSURLCachesharedURLCache] removeAllCachedResponses];

 [pool release];

}

 

This produces a non-incremental DOM-style parser, so we first download the data, then process it into a DOM and finally transfer the processed objects to the main thread for display.  It differs from other DOM-syle XML parsers in that it actually produces domain objects (as a Domain Object Model parser arguably should), rather than a generic XML DOM that must then be converted to objects.

Turning the DOM-style parser into a SAX-stye parser is almost completely trivial.  Instead of returning the Song objects at the end of itemElement:..

 

   [song setReleaseDate:[parseFormatterdateFromString:[children objectForUniqueKey:@"releasedate"]] ];
  return song;
}

 

 

we instead pass them to the delegate there and return nil so no tree is constructed:

   [song setReleaseDate:[parseFormatterdateFromString:[children objectForUniqueKey:@"releasedate"]] ];

[self performSelectorOnMainThread:@selector(parsedSong:)

withObject:song waitUntilDone:NO];

[song release];

return nil;

}

 

This means we can also remove the "channelElement" method and the for loop in downloadAndParse: that passed the Song objects to the main thread.  This is a SAX-style parser (though it doesn't use the SAX methods and does produce domain objects), but it is still non-incremental because it first downloads all the data and then parses it.  If we want to turn the SAX parser into an incremental parser that overlaps processing with downloading, there is one final tweak that fortunately further simplifies the downloadAndParse method (again with timing code removed):

 

- (void)downloadAndParse:(NSURL *)url {
   id pool=[NSAutoreleasePoolnew];
  [parserparseDataFromURL:url];
   [[NSURLCachesharedURLCache] removeAllCachedResponses];
   [pool release];
}

While this is probably best not only in terms of performance and responsiveness, but also in terms of code size, it doesn't play well with the XMLPerformance example, because there are no external measurement hooks that allow us to separate downloading from parsing for performance measurement purposes.

In addition, the XMLPerformance example is odd in that is both multi-threaded and measure real time rather than CPU time when measuring parse performance.  The reason this is odd is that when both parsing and display are active, the scheduler can take the CPU away from the XML parsing thread at any time and switch to the display thread, but this time will be counted as XML parsing time by the measurement routines.  This is obviously incorrect and penalizes all the incremental parsers, which is why Ray's comparison showed all the DOM parsers as performing better than the SAX parsers.

I hope these explanations show how to create different styles of parsers using MAX.

 

 

 

 

Sunday, May 2, 2010

iPhone XML Performance Revisited

Ray Wenderlich has done a great comparison of iPhone XML parsers, using the same sample I had looked at earlier in the context of responsiveness.

 

As Ray was comparing performance, my hobby-horse, I was obviously curious as to how MAX stacked up against all the upstart competition. Quite well, as it turns out (average of 5 runs on an iPad with 3.2):


Figure 1: total times (seconds)


MAX was about 50% faster than the closest competition, TBXML, at 0.43s vs. 0.61s.

 

However, the XMLPerformance sample is a bit weird in that it measures elapsed time, not CPU time, and is multi-threaded, updating the display as results come in.

 

In order to account for this overhead that has nothing to do with the XML parsers, I added a "Dummy" parser that doesn't actually parse anything, but rather just generates dummy Song entries as quickly as possible. This takes around 0.2 seconds all by itself. Subtracting this non-XML overhead from the total times yields the following results:


Figure 2: XML parse times (seconds)


When measuring just the XML parsers themselves, MAX is around twice as fast as the closest competition and seven times as fast as the built in NSXMLParser.

 

Sweet.

[Update]  I forgot the link to where I had uploaded the source: XMLPerformance.tgz at http://www.metaobject.com/downloads/Objective-C/

Friday, April 23, 2010

This blog has moved

This blog is now located at http://metaoblog.blogspot.com/. You will be automatically redirected in 30 seconds, or you may click here. For feed subscribers, please update your feed subscriptions to http://metaoblog.blogspot.com/feeds/posts/default.

Monday, January 25, 2010

Objective-XML 5.3

New in this release:
  • Cocotron targets for Windows support.
  • XMLRPC support.
  • No longer uses 'private' API that was causing AppStore rejections for some iPhone apps using Objective-XML.
  • Support for numeric entitites.
http://www.metaobject.com/downloads/Objective-C/Objective-XML-5.3.tgz.

Sunday, December 13, 2009

The Responder Chain is a Collection

Timothy Wood voices some great ideas on modernizing the Cocoa responder chain. I'd like to venture that if we treat the Responder Chain as a simple collection, a singly-linked list, then such alternatives become easier to model and reason about.


NSEnumerator *responderEnumerator = [[firstResponder mapToNextObjectFromMessage] nextResponder];

I am currently abstracting from the intricate delegate mapping and other ops, these could be handled in an analog fashion. With the enumerator in place, we can obviously snapshot it to get the current state of the responder chain, and also log that.
NSArray *responders = [responderEnumerator allObjects];
NSLog(@"full responder chain:  %@",responders);

Now we can express both current features and possible variations of the Responder Chain architecture compactly as common collection operations. The current dispatch mechanism simply sends the message to the first object that is capable of responding. This corresponds to using the first object of a -select, which is expressed in the -selectFirst convenience method.

Current dispatch

[[[responders selectFirst] respondsToSelector:action] performSelector:action withObject:sender];


If I understood him correctly, Tim wants the objects in the responder chain to return an object that they would like to respond to the message. This turns the -select into a -collect (without a -collectFirst), but is otherwise very similar.

Tim's dispatch

possibleResponders = [[responders collect] responsibleTargetForAction:theAction sender:sender]];
[[possibleResponders objectAtIndex:0] performSelector:action withObject:sender];

I hope this does Tim's ideas justice, but I think the succinct formulation should make it easy to tell wether it does or not.

In terms of combining validation with target/action, I'd be somewhat wary of accidentally triggering actions when validation was meant, though I do appreciate the advantages of combining the two operations. I am not sure what value the block is adding over just having an additional BOOL parameter in the target/action method.


Combined action and validation

typedef BOOL IBAction;
-(IBAction)delete:sender  :(BOOL)onlyValidate
{
    NSArray *selection = [self selectedItems];

   if ( onlyValidate || [selection count] == 0 ) {
        return NO;
   }
   // perform the action
}
// or if you're worried about the naming issues
-(IBAction)delete:sender
{
}

Wednesday, December 9, 2009

Simple Objective-XML example

Many times now, I've been asked about more Objective-XML examples. Here's a very simple one. It is adapted from Marcus Zarra's very helpful libxml and xmlreader tutorial. That tutorial shows how to parse a very simple XML format using libxml2.

The XML file parsed is the following:

<?xml version="1.0" encoding="UTF-8"?>

<root>

  <person>

    <name>John Doe</name>

    <age>14</age>

  </person>

  <person>

    <name>Mary Doe</name>

    <age>14</age>

  </person>

  <person>

    <name>John Smith</name>

    <age>15</age>

  </person>

</root>

It is parsed using at application startup using the following code:

- (void)applicationDidFinishLaunching:(NSNotification*)notification

{

NSString *path = [[NSBundle mainBundle] pathForResource:@"xmlExample" ofType:@"xml"];

NSData *xmlData = [NSData dataWithContentsOfFile:path];

xmlTextReaderPtr reader = xmlReaderForMemory([xmlData bytes],

[xmlData length], 

[path UTF8String], nil, 

(XML_PARSE_NOBLANKS | XML_PARSE_NOCDATA | XML_PARSE_NOERROR | XML_PARSE_NOWARNING));

if (!reader) {

NSLog(@"Failed to load xmlreader");

return;

}

NSString *currentTagName = nil;

NSDictionary *currentPerson = nil;

NSString *currentTagValue = nil;

NSMutableArray *people = [NSMutableArray array];

char* temp;

while (true) {

if (!xmlTextReaderRead(reader)) break;

switch (xmlTextReaderNodeType(reader)) {

case XML_READER_TYPE_ELEMENT:

//We are starting an element

temp =  (char*)xmlTextReaderConstName(reader);

currentTagName = [NSString stringWithCString:temp

encoding:NSUTF8StringEncoding];

if ([currentTagName isEqualToString:@"person"]) {

currentPerson = [NSMutableDictionary dictionary];

[people addObject:currentPerson];

}

continue;

case XML_READER_TYPE_TEXT:

//The current tag has a text value, stick it into the current person

temp = (char*)xmlTextReaderConstValue(reader);

currentTagValue = [NSString stringWithCString:temp

encoding:NSUTF8StringEncoding];

if (!currentPerson) return;

[currentPerson setValue:currentTagValue forKey:currentTagName];

currentTagValue = nil;

currentTagName = nil;

default: continue;

}

}

NSLog(@"%@:%s Final data: %@", [self class], _cmd, people);

[self setRecords:people];

}

To parse it using MAX you need to add MPWXmlKit and MPWFoundation to your project, and then replace the code above with the following:

- (void)applicationDidFinishLaunching:(NSNotification*)notification

{

NSString *path = [[NSBundle mainBundle] pathForResource:@"xmlExample" ofType:@"xml"];

NSArray *people=[[MPWMAXParser parser] parsedDataFromURL:[NSURL fileURLWithPath:path]];

[self setRecords:people];

}