Monday, March 26, 2012

Picross HD Localization - Part II

At a Mobile Mondays meeting in Ann Arbor, Michael Antaran founder of Marvel Apps, was talking about the benefits of localizing AppStore Apps.  That was enough to inspire me to give it a try.

In the first part, I explained that we decided to localize Picross HD into three languages: German, French, and Japanese along with some of the lessons we had learned.  Now that the localized versions have been in the AppStore for about a month, I thought it would be good to share the results along with some other interesting tidbits we learned.

German & French

We didn't see any positive impact to new user growth for localizing in German.


We did see a short term spike in new users for the French version, but after a few days it returned to its normal trends.



I expected to see bigger results for localizing Picross HD into German and French.  I would love to hear back on what other people have experienced when localizing into German and French.   Are these results typical?  Is English just not an issue in those countries for people using iOS devices for puzzle style gaming?

Japanese

We have had great growth in Japan sense we localized.  Japanese New User acquisition is allmost an order of magnitude greater then all other locals combined.  And it continues to grow.  I want to say thank you to all our new Japanese users.


Why the great growth?  Well for starters, I believe Picross as a game style is better know in Japan then it is in other parts of the world.  We also totally overhauled the Keywords for Japanese.  We used keywords that would be meaningfully for the Japanese people.  Even with all that, this totally exceeded my expectations.

iPhone vs iPad

Almost all of our games are Universal apps, and we usually see close to 50/50 spread across iPhone and iPad usage.  For example, in Germany about 55% of Picross HD sessions are run on an iPad.  However, in Japan that number is about 4%.  We where surprised about that difference.  What have other people seen for iPhone vs iPad usage of their apps in different locals?

What's Next

We want to continue to experiment with different locals.  The next version of Picross HD, which is pending Apple approval, will include support for Korean.  We took a similar approach to Korean as we did for Japanese by providing local specific keywords so that should be interesting.

We have also been working on some major updates to Picross HD given its new found growth.  All the puzzles in the next version will have unique solutions and be solvable by logic.  That should help address two of the big feedback items we received from our Japanese users.

We are also looking at how to make the iPhone version even better.   Much of our focus has been on the iPad version, but with this much usage on the iPhone we are going to have to pay even more attention to the iPhone version.

Be sure to give Picross HD a try and let us know what you think.  It's a free download. :)

Thanks,
Tod and Ken
Five Lakes Studio, LLC

Monday, February 27, 2012

Picross HD Localization

At a Mobile Mondays meeting in Ann Arbor, Michael Antaran founder of Marvel Apps, was talking about the benefits of localizing AppStore Apps.  That was enough to inspire me to give it a try.  I spent the last week localizing Picross HD, our best revenue generating App so far, and I thought I would share my experiences thus far.

Languages

Using some metrics and based on the appeal of Picross HD, the first thing I decided was to localize in 3 languages: German, French, and Japanese.


Contractors

We used odesk to find the people to do the localization and that has been a very good experience.  It was really easy to find people to do French and German.  It was a little harder finding someone to do a good job on Japanese. However, everyone we ended up working with was responsive, quick, affordable, offered good advice, and did an all around great job.  It took about a week to get all 3 translations completed.

How to Localize

Before I connected with the odesk people, I first needed to figure out how we wanted to approach the actual localization and how to get the strings extracted.  A great reference for this is on Ray Wenderlich's blog in a Localization article by Sean Berry.  I wanted to keep the localization really simple as I didn't want the odesk people to have to have special tools such as resource editors.  So I put all the strings into a few files I could send to them:
  • description.rtf - Containing the app store description
  • help.rtf - Text for the app help
  • Localizable.strings - App strings, Game Center Leader board Strings, and in-app store strings
I put some extra strings in the Localizable.strings even through they aren't directly used by the App such as the Game Center Strings and in-app store strings.  This just made it easier to communicate with odesk and gave me a good place to manage the strings over time.

I gathered strings from a few different places:
  • From XML puzzle files - Using a XSLT transform (this was a majority of the strings).  The name of each puzzle needed to be localized and we have over 400 puzzles in Picross HD.
  • From each XIB (resource) file - I did this manually.  I didn't want the contractors to have to deal with a resource editor so I just manually added the UILabel and UIButton text into the Localizable.strings file.
  • Look for hard coded strings in the app and added the NSLocalizedString(@"english text", nil) macro for each string needing translation.
Strings coming from a resource file won't automatically lookup their translation via Localizable.strings.  The resource files assume you will localize each resource file separately which I didn't want to do.  So I just added some simple code to the UIViewControllers of each xib that needed to be localized:

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    // Localize the labels in this view
    //
    for( id foundView in self.rateThisPuzzleButtonView.subviews )
    {
        if( [foundView isKindOfClass:[UILabel class]] )
        {
            UILabel *label = foundView;
            label.text = NSLocalizedString( label.text, nil );
        }
    }
}

In order to do the buttons, I decided to just do those manually as I didn't have that many buttons to do:
    [self.puzzleSuccessTryAgainButton setTitle:NSLocalizedString([self.puzzleSuccessTryAgainButton titleForState:UIControlStateNormal],nil) forState:UIControlStateNormal];
    [self.puzzleSuccessNextButton setTitle:NSLocalizedString([self.puzzleSuccessNextButton titleForState:UIControlStateNormal],nil) forState:UIControlStateNormal];

I also had to adjust the font and/or button sizes in order to accommodate the length of the German text.  This was a little tricky given how some of our buttons worked.  I ended up doing a StackOverflow posting once I figured out how to solve button text wrapping issues.

Lessons Learned

Doing three languages at once was a bit much for the first time trying to localize an app. It would have been better to start with just one language.  There where several mistakes I made that required me to add more work on the localization teams.  Every time I did this, I had to communicate  back with 3 different people and it was just more work then getting it right the first time.  The types of mistakes I made where:

  • When I sent the strings to the localization teams I didn't realize I was missing about 50 puzzles.  The XSLT transform I wrote missed some puzzles.
  • I should have sent screenshots to the localization team up front as they didn't have enough context to do some of the translations.  They ended up doing some redo work once I sent them the screenshots.
  • I often use images for the help screens, but I was missing the original layered image for one of the help screens.  I ended up spending a few hours recreating a help screen so I could replace the text easily.  Save your original layered artwork (especially if it has text in it).
  • Images with text == pain.  I knew this before, but zero thought had went into the idea we might localize this App when we first wrote it over a year ago.  To help this, I wrote some code to dynamically load a localized version of an image file based on the active language.
Overall, this was a very easy effort.  It only took about a week of my part-time schedule to go from nothing localized to having three languages and ready for posting to the AppStore.  Apple's tools and handling of fonts and languages works great.


Next Steps

This just got released to the AppStore so wish us luck.  I will report back when we know more how this experiment went.  Ken and I are hoping to see a noticeable uptick and we hope more people enjoy Picross HD in their native language.

Be sure to give Picross HD a try and let us know what you think.

Thanks,
Tod and Ken
Five Lakes Studio, LLC



Thursday, February 9, 2012

Michigan Swimmers Using English Channel Record Attempt to Raise Money for ALS Cure

Six Michigan women are training to break the world record for fastest two-way relay swim crossing of the English Channel to raise money to find a cure for ALS, or Lou Gehrig’s disease.

Michigan Swimmers Using English Channel Record Attempt to Raise Money for ALS Cure

Ken and I put together a special version of Kento to help support their effort and this good cause.



Monday, November 21, 2011

Game Art for the Artistically Challenged (Part 2)

Hello again, this is Ken Vadella blogging for Five Lakes Studio, LLC (FLS).

In my last article on the subject of Game Art I discussed the FLS strategy for acquiring artwork for our iOS projects. A reader of this posting (plicatibu) provided, what looks to be, a very valuable website for hiring artists to produce custom artwork at reasonable prices - thanks plicatibu. The site is called oDesk and though we have not tried to use it as of yet we plan to give it a shot in the near future. Check it out.

This month I would like to discuss the editing tools we use to turn our art findings into usable game components. Let me start with a small sidebar discussing how we choose tools for our efforts. The following list summarizes our evaluation technique. The list is in order of importance.

Costs
We don't want to spend an arm and a leg for image editing tools.

Features
A text editor with a limited feature set isn't very useful to us. Here are some of the items we look for in our image editing tools:
  • File Format Support - PNG, JPG, etc.
  • Layers - No layers, no tool. It's really that simple for us. We love layers for image editing and find it nearly impossible to live without this feature.
  • History - No undo/redo, why bother. See above.
  • Effects - Blurs, distortion, noise, embossing, etc.
Simplicity
What is simple to some is difficult for others. Tod and I are not new to image editing but we are also far from experts; this is not a task that we do everyday so we look for easy to use tools.

Availability
A product that runs on multiple platforms is a plus but not a show-stopper. We have access to many brands of computers that run many different operating systems. If a tool meets our other criteria then we won't rule it out just because it doesn't work on a Mac OS.

The Gold Standard
For us Adobe Photoshop CS5 is the gold standard of image editing but it does not fit the shoe string budget of a startup company like FLS. Don't get me wrong, if we didn't have alternatives we would pay the money and acquire copies of CS5. But, we do have alternatives that suit our needs very well and cost significantly less money to acquire.

The List
Without further ado, let's move to that list. These are the tools that we use for our image editing tasks. This list is arranged in order of preference as it relates to FLS.

My preferred application for image editing. This may be sacrilegious to some because it does not run on a Mac OS. But, as I mentioned above, we have access to many brands of computers running many different operating systems and this tool has a lot of positive attributes:
  • It's very easy to use.
  • It performs well.
  • It's very stable - we can't remember the last time it crashed.
  • It fits all of the criteria cited above with the exception of Availability.
  • It is 100% free. Yes, free.
Tod's preferred editor and for those on Mac OS. It exhibits many of the positives cited above for Paint.NET. Some other positive things that can be said about Pixelmator include:
  • Reasonable pricing. It's not free but you won't need to do much more than skip a restaurant dinner with your significant other to save enough money to buy a copy.
  • It performs well - it's fast; really fast.
  • It reads native Adobe Photoshop files relatively well - Paint.NET does not do this.
But it comes with some minuses as well:
  • It lacks basic drawing tools. This seems like a major oversight. I have heard that newer versions of Pixelmator will support drawing tools.
  • It lacks that ease of use inherent to Paint.NET. It's not difficult to use, but it isn't as easy to use as Paint.NET.
Still, it is a wonderful editor for the money.

Pixlr is an oddity in this list. It's not a product that you put on your machine and run. Rather, it runs in the context of a web browser. It works surprisingly well and is very full-featured. But, the whole edit-in-browser experience feels, somehow, too crammed for space and for this reason we tend to use it less than the other editors mentioned above. Still, it's free, cross-platform and powerful. What more could you really ask for?

The GIMP
You can say many positive things about GIMP:
  • It is very powerful.
  • It's free. Yes, free.
  • It's cross-platofrm.
It's everything that Paint.NET is and more. And yet, we don't use it that often. We use it less than any other editor in this article. Why? Because it's lacking in simplicity and user-friendliness. At least, we feel this way about it. Maybe we are just not that bright. Give it a try though, you might find otherwise.

Snagit
We also use Snagit to take screenshots and do basic image editing to help communicate with our customers and each other.  Snagit gets heavily used for support, it's so much easier and clearer sending a picture then a bunch of text.  Snagit does a great job of taking screenshots of the iOS simulator, saving and tagging them, and allowing awesome markup.  It is affordable and works on both Mac and PC.  I should say that Tod works at TechSmith the makers of Snagit.

Final Comments
That's it for this Blog entry. I hope you find one or more of these editors to your liking.

In a future Blog entry I will discuss some additional tools that we use to make our art assets even better. I'd also be interested in what image editors you use in your development efforts. It never hurts to evaluate new products recommended by others.

Thanks for reading.

Ken


PS (7/28/2016): One thing that was missing from this review was some web based photo editors.

One of the folks over at Canva asked that give them a shout out.  They have a basic free photo editor at https://photo-editor.canva.com and their full product is at https://www.canva.com.  Ken and I haven't used it yet in a real project yet, but it might be worth checking out.  Leave some feedback and let us know if you tried it out and if you liked it or not.

- Tod

Monday, November 7, 2011

Integrating Social Media - Epic Fail

I plan on returning to my multi-part series on creating a Star Rating System in a future post. This week, I wanted to take a small side detour and talk about how Ken and I have tried to integrate social media into our games.   Or perhaps I should say, how we have failed at it miserably.  I'm hoping someone will share some words of wisdom as there must be something fundamentally wrong with our approach thus far.


iOS 5 Twitter

With iOS5 Twitter integration, I thought it would be great to add twitter to Kento.  I created a little twitter helper class based on the iDevBlogADay article by Tony Ngo.  The class source is just two files and is available for download.

I added a twitter button to two places within Kento: on the main screen and when you solve a puzzle.

On the main screen, there is a tweet button that pre-fills in the number of tokens the player has won and a link to Kento.





When the player solves a puzzle, we place a tweet button in the upper left corner that pre-fills in some nice information about the puzzle along with a picture.














The version with twitter was released about two weeks ago and we have over 17,000 updates/downloads.  Can you guess how many times I have seen #KentoApp tweeted or received a tweet to @fivelakesstudio from Kento?  The answer is .......... ZERO, if you don't count my test ones.






A Previous Attempt

We tried integrating facebook into #Lost in Space with the same result: ZERO usage.  People just don't click on the facebook icon.



We show a little icon and some game text when the player tries to share their score through Facebook.




Conclusion

The puzzle star rating system we added continues to get great usage, but we have totally failed at engaging other types of social media such as Facebook or Twitter within our games.  We have also tried things such as HeyZap which has had better results then Facebook or Twitter, but it still doesn't have the level of engagement and usage we would like to see.  Has anybody had better luck at integrating social platforms?  I'm thinking that slapping a button in a UI just isn't the right way to do this type of social integration.

Thanks,
Tod






Sunday, November 6, 2011

UIAlert with Blocks

I wish Apple would make a version of UIAlert with blocks.  I have seen a fair number of simple examples on how to do this, but I have been unhappy with most of them.  Most use Categories on UIAlert, and I would rather not do that especially when the alert handler is also involved.

This very simple implementation uses a separate class to implement a UIAlert.  Here is how you use it:
    
TCAlert *alert = [[TCAlert alloc] initWithTitle:@"Reset Puzzles" 
                                        message:@"Reset ALL puzzles in this puzzle pack?"
                              cancelButtonTitle:@"Cancel" 
                              otherButtonTitles:@"Yes", nil];
   
[alert showAlertWithCompletionBlock:^(int buttonIndex)
                                    {
                                        NSLog( @"Hello I got it %d", buttonIndex );
                                        [alert release];
                                    }];
It is important that the alert object stay retained through the life of the Alert, but that is easy to do by releasing it in the block.  The block will retain the alert!  If the system closes the alert the completion block will be called with a buttonIndex of -1 (kAlertViewCanceled).

The code is just a small source and header file.  Please feel free to download it and let me know what you think.  I haven't switched over to ARC yet, so I'm not sure what would need to be changed to support ARC.  It would be great if someone would like to do that.

Thanks,
- Tod





Monday, October 24, 2011

Star Rating System - Part 1: Google App Engine

We have been looking for ways for our users to share their experiences playing our games.  We tried things like Game Center, HeyZay, twitter, facebook, and prompting for people to rate the app on the AppStore with moderate success.

One day, I had this "inspiration" to allow people to easily rate individual puzzles and share those ratings.  I was currently working on Kento: A Distorted Jigsaw, so I decided to just do it.

This is a very simple star rating system that allows puzzles to be rated from 1 to 5 stars.  That information is sent to a server where it is combined with other people's ratings.  The server keeps track of the total of each puzzle's rating along with the number of users who rated it.  With this information, an average can be caclulated.


I broke the idea up into 3 parts:
  1. Server to collect and share ratings (Google App Engine)
  2. iOS API to access the server
  3. iOS User Interface
This first article is going to focus on writing a simple web service with Google App Engine.  I'm not going to go into how to create the full web service.  There are lots of good examples on the App Engine site that will show how to do that.  Instead, I will focus on some of the tools and tips I've learned doing this project mostly around how to deal with the datastore and transactions.

Tools

I'm just using a free GoogleAppEngine account.  You can download a developer environment for either PC or Mac.  It also supports either Java or Python.  I really haven't done much with Python, so I decided to use Python as a good learning experience.

In the past, I have tried just using a plain text editor for python which is such a throw back to the 80s.  This time, I remembered my car pool buddy, Chris, talking about an IDE for Python called PyCharm by JetBrains.

While not my favorite IDE, PyCharm has support for GoogleAppEngine development.  This includes code completion and a DEBUGGER.  This was a huge timesaver given my limited Python skills.  Make sure you checkout their quick start guide for google app engine.  Also make sure you run the GoogleAppEngineLauncher downloaded from google before trying to setup PyCharm.  You don't need it running while using PyCharm, but the launcher will finish the GoogleAppEngine install and setup symlinks on your system needed by PyCharm.


Datastore

You can configure your app to use either the High Replication Datastore (HRD) or the master/slave datastore.  I wanted to keep the application as cheap as possible, and this application doesn't require rock solid availability.  If the service goes down for maintenance for a short period of time, the end users won't notice or care.  This made the master/slave option the ideal choice.
The Master/Slave datastore uses a master-slave replication system, which asynchronously replicates data as you write it to a physical data center. Since only one data center is the master for writing at any given time, this option offers strong consistency for all reads and queries, at the cost of periods of temporary unavailability during data center issues or planned downtime. This option also offers the lowest storage and CPU costs for storing data. 
You should also be aware that the App Engine datastore's are not SQL databases.


Entity Group

One of the tricky things with Master/Slave transactions is that a transaction can't cross entity groups.  In other words, each table involved in the transaction must be part of the same entity group.  Well, that leads to an interesting question of how to put tables into the same entity group.

I made sure that the tables involved in the transaction had the same parent (they where siblings).  That was enough to put them into the same entity group.


Transaction Errors

If a transaction fails, any changes will be rolled back.  However, just because a transaction throws an exception doesn't mean it has failed.
If your app receives an exception when submitting a transaction, it does not always mean that the transaction failed. You can receiveTimeout, TransactionFailedError, or InternalError exceptions in cases where transactions have been committed and eventually will be applied successfully. Whenever possible, make your datastore transactions idempotent so that if you repeat a transaction, the end result will be the same.
When writing this service, it was important that if you run the same transaction twice the "right" thing will happen.  This means that we need to keep the average data accurate even if the same rating is applied multiple times.


The Tables
I used 3 datastore tables for this project.

The AppPuzzleDB table is used to represent each puzzle in a game.  Since I want to be able to use this for multiple games there is a gameId unique to each game.  I also tend to package groups of puzzles together so there is a packageId to represent a grouping of puzzles.  Finally, there is a puzzleId itself which uniquely identifies a single puzzle.
class AppPuzzleDB (db.Model):
    gameId    = db.StringProperty(indexed=True)
    packageId = db.StringProperty(indexed=True)
    puzzleId  = db.StringProperty(indexed=True)
The server itself doesn't calculate the average.  It just holds the data necessary to calculate an average.  The RatingAverage table keeps the current running total and number of ratings (count) for each AppPuzzleDB.
class AppPuzzleRatingAverageDB (db.Model):
    gameId          = db.StringProperty(indexed=True)
    packageId       = db.StringProperty(indexed=True)
    puzzleId        = db.StringProperty(indexed=True)
    puzzleItem      = db.ReferenceProperty(AppPuzzleDB)
    ratingCount     = db.IntegerProperty()
    ratingTotal     = db.FloatProperty()
I know it is a waste of storage and bad design to repeat the gameId, packageId, and puzzleId within this table.  However, doing so reduces the number of quires I have to perform and it just makes it easier to access this information.  I know poor excuses, but I get to be lazy sometimes!

The last table, AppPuzzleRatingDB, is the individual ratings supplied by each user.  We keep this data around for awhile so that if the user re-rates the puzzle we can back out their old rating and apply their new rating.
class AppPuzzleRatingDB (db.Model):
    puzzleRatingAverage = db.ReferenceProperty(AppPuzzleRatingAverageDB)
    userId              = db.StringProperty(indexed=True)
    rating              = db.FloatProperty()
    dateCreated         = db.DateTimeProperty(auto_now_add=True)
This design allows for really fast retrieval of the current averages, we don't need to do a bunch of lookups and summations.  It also allows us to purge the individual user ratings in the event we need to free up space.  In addition, this is what allows us to handle the same request being sent multiple times to the server.  We use this table to back out any old values before applying the new ones.


Making keys

In order to do fast lookups of the tables, I used custom keys. For example, the follow code snibbit creates a key for the AppPuzzleDB table:
def appPuzzleKey(gameId, packageId, puzzleId):
    keyName = "puzzle(" + gameId + "_" + packageId + "_" + puzzleId + ")"
    return db.Key.from_path( "AppPuzzleDB", keyName, parent=None )
It gets a bit more interesting for the child tables. You can see below that the AppPuzzleRatingAverageDB uses the puzzle key to refer back to its parent.
def appPuzzleRatingAverageKey(puzzleKey):
    keyName = "puzzleRatingAverage(" + puzzleKey.name() + ")"
    return db.Key.from_path( "AppPuzzleRatingAverageDB", keyName, parent=puzzleKey )
Finally, the AppPuzzleRatingDB also has to take into account a userId as each user can have their own rating per puzzle.
def appPuzzleRatingKey(puzzleKey, userId):
    keyName = "puzzleRating(" + userId + "_" + puzzleKey.name() + ")"
    return db.Key.from_path( "AppPuzzleRatingDB", keyName, parent=puzzleKey )

Running a Transaction

In order to run a transaction, just call run_in_transaction with a function reference.  For example:
puzzleItem = db.run_in_transaction( transactionGetAppPuzzleDBItem, gameId, packageId, puzzleId )
As a convention we prefix transactionGetAppPuzzleDBItem with the name transaction to indicate that we call this function in the context of a transaction.  We do this in a transaction so that when we create a new AppPuzzleDB entry, we can also create a corresponding AppPuzzleRatingAverageDB that is initialized correctly and ready to go for when we add items.
def transactionGetAppPuzzleDBItem(gameId, packageId, puzzleId):
    puzzleKey  = appPuzzleKey( gameId, packageId, puzzleId )
    puzzleItem = AppPuzzleDB.get( puzzleKey )

    if puzzleItem is None:
        puzzleItem = AppPuzzleDB( key_name=puzzleKey.name(),
                                  parent=None,
                                  gameId=gameId,
                                  packageId=packageId,
                                  puzzleId=puzzleId )
        puzzleItem.put()

        # Create a new AppPuzzleRatingAverageDB to go along with the puzzleItem
        puzzleRatingAverageKey  = appPuzzleRatingAverageKey( puzzleItem.key() )
        puzzleRatingAverageItem = AppPuzzleRatingAverageDB( key_name = puzzleRatingAverageKey.name(),
                                                            parent = puzzleItem.key(),
                                                            gameId = gameId,
                                                            packageId = packageId,
                                                            puzzleId = puzzleId,
                                                            puzzleItem=puzzleItem,
                                                            ratingCount = 0,
                                                            ratingTotal = 0.0 )
        puzzleRatingAverageItem.put()

    return puzzleItem

Optimization

The rating system had been live for a short awhile, and we got featured again by FreeAppMagic.  This time we ended up being number #1 in our category on the UK store.  We got a big influx of downloads and users.  Checking the status showed that we had reached 70% usage of our cpu allocation.


The server was using a Django template to form XML to send back to the client, and it was doing this every time a client asked for the average data.  Fortunately, the client was written to only ask for this information once a day.  However, even with that it was taking a good amount of CPU time to do this work.

To decrease CPU usage, we modified the server to cache the generated xml in a memcache that expires every 6 hours.  That way we can use the cached value without having to regenerate the xml.
class AppPuzzleRatingAveragePackageXML(webapp.RequestHandler):
    def get(self):
        gameId    = self.request.get('gameId')
        packageId = self.request.get('packageId')

        # See if we already have the XML document cached
        # If we do, this should save a ton of CPU time as we won't have to keep regenerating the XML document
        xmlRatingKey = appPuzzleRatingAverageXMLMemCacheKeyName( gameId, packageId )
        xmlRating = memcache.get( xmlRatingKey )

        # If not, then we need to generate a new one
        if xmlRating is None:
            puzzleRatingAverageQuery = db.GqlQuery( "SELECT * FROM AppPuzzleRatingAverageDB where gameId = :1 and packageId = :2", gameId, packageId )

            template_values = {
                'gameId' : gameId,
                'packageId' : packageId,
                'puzzleRatingAverageQuery' : puzzleRatingAverageQuery,
                }

            oneHour = 3600     # 60 * 60 = 3600
            path = os.path.join(os.path.dirname(__file__), "AppPuzzleRatingAveragePackage.xml" )
            xmlRating = template.render(path, template_values)
            memcache.add(key=xmlRatingKey, value=xmlRating, time=oneHour*6)

        self.response.headers["Content-Type"] = "text/xml"
        self.response.out.write( xmlRating )

This did have a significant effect on the overall CPU usage.




Wrap Up

It took about a day to get this implemented on the server side, and we have had over 4,000 ratings in just a few weeks.  Do you think a service like this is worthwhile?  Also be sure to check out Kento and our other iOS apps.

Thanks,
Tod