Tuesday, 22 July 2014

Boss Flox

I've mentioned before that we are big fans of Gamua and the frameworks/services they are responsible for.  We've been using Flox a fair bit recently, first for Proleague and more recently Pogosheep, and it is a really awesome library (we even wrote a port for GameMaker). Because it is a relatively new service, there aren't too many guides out there, so I thought I'd write something a bit different, an n-part (probably 2-part) series on tips-and-tricks when using Flox. More after the jump.



Query Restrictions

So you may have noticed that there is are some pretty tight restrictions imposed on Queries, you can't index more than two properties and you can't use more than one inequality filter (that's any of <, <=, >=, > !=). They're there for good reason, Flox is build on the Google App Engine and going outside these constraints would cause prices to skyrocket. So how do you get around this problem? Easy! You select everything and then filter in flash right?

var pid:String = Player.current.id;
var q:Query = new Query("Match");
// Select all the matches that this player is a part of
q.where("ownerId == ? || opponentId == ?",pid,pid);
q.find(function onComplete(broad:Array):void {
    // Reduce the results by removing all the matches that have been seen
    var results:Array = broad.filter(function(m:Match,i:int,a:Array):Boolean {
         return ((m.ownerId == pid && m.ownerHasSeen == false) ||
                (m.opponentId == pid && m.opponentHasSeen == false));
    }
}, function onError(error:String,httpStatus:int):void {
    // Do something with the error
});

In the above example we are selecting all the Matches that the current player is a part of (either as the owner or the opponent) and then we are filtering out all the matches they have already seen. Though it may seem *okay* this method has two pretty serious drawbacks;
  1. If you have more results than the limit (50 by default) then you may be dropping credible results in place of results that have already been seen. You can get around this by paginating, but it means you need to load every page before doing the filter pass.
  2. This whole process has two very critical performance implications. The first is that the filter pass can be a big sticking point depending on how complex the query and the second is that if you are serialising a number of results just to toss them back out, you create A LOT of garbage, something that can absolutely kill AS3 performance.
Despite the above issues, this example is actually exactly what we used as a first pass for Proleague. Perhaps you are like we were and you know that this feels yuck, but what else can you do?

Combining Properties

Using the example from above, lets start by thinking of the ideal query we'd like to make. We always want matches that haven't been seen by the owner, so something like the following would work.

"(ownerId == ? AND ownerHasSeen == false) OR 
(opponentId == ? AND opponentHasSeen == false)"

But of course, this is is using four property indexes, something that can not be done with Flox. So what can we do? Well what if we had a single property like the following?

"ownerId_ownerHasSeen == ? OR opponentId_opponentHasSeen == ?"

From a single property we could find both the player id and whether or not they had seen the match. But what would those properties look like?

"abcdefg123-1" // Player id + true for the player having seen the match
"abcdefg123-0" // Player id + false for the player having not seen the match

In this example abcdefg123 is the player id and then we concat a '-' followed by '1' or '0' for whether or not the player has seen the match. The way you construct this combined property is irrelevant, you just need to decide on a format that will easily allow you to extract the information from the property. Now we can find out all the information we need and we only need two indices to do it! Turns out the code we need is pretty neat too...

Implementation

Because this is an optimisation specific to the Match entities I feel that this is a great time for some static helper functions.

public class Match {
    
    // Combines the id and hasSeen properties into a single string
    public static function mergeIdAndHasSeen(id:String,hasSeen:Boolean):String {
        return id + "-" + (hasSeen)?"1":"0";
    }
    
    // Separates the id from the combined string
    public static function unmergeId(mergedString:String):String {
        var i:int = mergedString.indexOf("-");
        return mergedString.substring(0,i);
    }
    
    // Separates 'has seen' from the combined string
    public static function unmergeHasSeen(mergedString:String):Boolean {
        return mergedString.charAt(mergedString.length-1) == "1";
    }
    
    // Constructor + properties omitted
}

Hopefully this is pretty self explanatory but basically we have three functions, one for combining the two properties and another two for separating them back out again. Using these helper functions it should be really easy to tweak our code to make use of them. First we need to add the combined properties to the Match class.

public class Match {
    
    // Static helper functions omitted
    
    public function get ownerId_ownerHasSeen():String {
        return Match.mergeIdAndHasSeen(this.ownerId,this.ownerHasSeen);
    }
    public function set ownerId_ownerHasSeen(value:String):void {
        this.ownerId = Match.unmergeId(value);
        this.ownerHasSeen = Match.unmergeHasSeen(value);
    }
    
    public function get opponentId_opponentHasSeen():String {
        return Match.mergeIdAndHasSeen(this.opponentId,this.opponentHasSeen);
    }
    public function set opponentId_opponentHasSeen(value:String):void {
        this.opponentId = Match.unmergeId(value);
        this.opponentHasSeen = Match.unmergeHasSeen(value);
    }
    
    // Constructor + other properties omitted
}

By converting the properties inside the getters and setters, we ensure that the wrong value will never be returned from these functions. Because our helper methods are static we can use the same merge/unmerge code when building our query.

var pid:String = Player.current.id;
var p:String = Match.mergeIdAndHasSeen(pid,false);
var q:Query = new Query("Match");
q.where("ownerId_ownerHasSeen == ? || opponentId_opponentHasSeen == ?",p,p);

All that's left to do is to index these two new properties and hey presto! Query optimised! If you want to get really fancy you can even add a [NonSerialized] tag to the opponentId, opponentHasSeen and ownerHasSeen properties now, as all their information is already stored in these two new properties.

So that's it! The optimisation is well worth your trouble, even if you aren't hitting the index limit, reducing two indexes down to one can save you a tonne on operations too, so make sure to do it! The implementation ends up clean enough that you need not worry about loose bits of code, everything ends up nice and neat. 

Hope this helps someone! Next time we'll discuss some entity refresh headaches and also the very cool email login system that Flox uses. If you have a request/question/comment, sing out! We'd love to hear from you.

No comments:

Post a Comment