Recently, I’ve been asked more than once which is better: AMF or JSON for AIR mobile applications. This post is to highlight some performance comparisons, and a sample testing application that I put together. First, it is important to know what both AMF and JSON are.
AMF
Action Message Format (AMF) is a compact binary format that is used to serialize
ActionScript object graphs. Once serialized an AMF encoded object graph may be used
to persist and retrieve the public state of an application across sessions or allow two
endpoints to communicate through the exchange of strongly typed data.
JSON (JavaScript Object Notation) is a lightweight data-interchange format. It is easy for humans to read and write. It is easy for machines to parse and generate. It is based on a subset of the JavaScript Programming Language, Standard ECMA-262 3rd Edition – December 1999. JSON is a text format that is completely language independent but uses conventions that are familiar to programmers of the C-family of languages, including C, C++, C#, Java, JavaScript, Perl, Python, and many others.
Both AMF and JSON are compact serialization formats and provide efficient data transport. The main differences between the two formats are as follows:
AMF is a binary format that is not easily readable by humans, JSON is a text-based format that is easily readable.
AMF allows for serialization of strongly typed objects in transactions between the client and server, JSON only supports generic or loosely-typed objects.
Former Adobe Evangelist James Ward put together a suite of benchmarks comparing JSON, SOAP, and AMF that show comparable performance between AMF and JSON. Recently, AIR 3.0 and Flash Player 11 brought native JSON support, which greatly improves JSON parsing in Flash & AIR runtimes. This is a huge boost, especially for mobile applications that consume JSON data.
I put together a very basic test case where a mobile application makes requests of simple data objects from a ColdFusion CFC. In each test iteration, a request is made for 1, 10, 100, 1000, and 10000 value objects, in both AMF and JSON formats. The total round trip time from request to deserialization is measured and compared for each case, for a total of 5 iterations through each cycle. My findings are that AMF and JSON have comparable performance in smaller record sets. However, AMF seems to have better performance as data sets grow. In my test cases, the 1000+ record results were consistently faster using AMF. However, in smaller data sets, JSON was often faster (however not consistently, or by much of a margin). I tested these times on both an iPhone 4 and Motorolla Atrix, both running on the carrier networks (not over wifi).
Below is a video of the serialization testing application at work.
Here are a few screenshots of the application.
The Tests
For these tests I created two basic CFCs (ColdFusion Components). One is a simple data value object. The other CFC is a gateway to expose a remote service that returns the value objects to the client. I chose a ColdFusion CFC for this case b/c it can easily be serialized as AMF or JSON just by changing the endpoint used to consume the service.
Here is the service CFC used to return data to the client:
component {
remote array function getRecords(numeric records=1) {
var result = [];
for (var x = 0; x < records; x=x+1) {
var item = new SampleVO();
item.itemId = x;
ArrayAppend( result, item );
}
return result;
}
}
Obviously, this is a fictional data object with randomly generated values. However, it still represents a reasonable service payload for data serialization. By accessing the data via the ColdFusion Flex/Remoting gateway, you access the remote services via AMF3.
remoteObject = new RemoteObject("ColdFusion");
remoteObject.source = "com.tricedesigns.mobileTest.Services";
remoteObject.endpoint = "http://tricedesigns.com/flex2gateway/";
var token : AsyncToken = remoteObject.getRecords( RECORD_COUNT[ recordCountIndex ] );
token.addResponder( new mx.rpc.Responder( onAMFResult, onFault ) );
By accessing the data via an http endpoint, with returnformat=josn, you will invoke the same CFC remote method exposed as JSON.
httpService = new HTTPService();
httpService.url = "http://tricedesigns.com/com/tricedesigns/mobileTest/Services.cfc?method=getrecords&records=" + RECORD_COUNT[ recordCountIndex ] + "&returnformat=json";
var token : AsyncToken = httpService.send();
token.addResponder( new mx.rpc.Responder( onJSONResult, onFault ) );
The JSON-formatted data will look something like this:
In the mobile client application, I have a SerializationTestController class that handles all of the test logic and communications back and forth with the server. The time for each test is measured from immediately before the the request is made to the server, until after the data has been deserialized to an ArrayCollection. You can view the SerializationTestController class below:
package control
{
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.IEventDispatcher;
import flash.utils.getTimer;
import model.TestSummaryVO;
import model.TestVO;
import mx.collections.ArrayCollection;
import mx.rpc.AsyncToken;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.rpc.http.HTTPService;
import mx.rpc.remoting.RemoteObject;
import views.SummaryView;
[Event(name="testStatusChange", type="control.TestUpdateEvent")]
[Event(name="testUpdate", type="control.TestUpdateEvent")]
public class SerializationTestController extends EventDispatcher
{
private var remoteObject : RemoteObject;
private var httpService : HTTPService;
private var _testing : Boolean = false;
private var testIndex : int = 0;
private var iterationIndex : int = 0;
private var recordCountIndex : int = 0;
private var testInstanceIndex : int = 0;
private var _results : ArrayCollection;
private var currentTest : TestVO;
public static const ITERATIONS : int = 5;
public static const RECORD_COUNT : Array = [1,10,100,1000,10000];
public static const TESTS : Array = [ TestVO.TYPE_AMF, TestVO.TYPE_JSON ];
public function SerializationTestController(target:IEventDispatcher=null)
{
super(target);
remoteObject = new RemoteObject("ColdFusion");
remoteObject.source = "com.tricedesigns.mobileTest.Services";
remoteObject.endpoint = "http://tricedesigns.com/flex2gateway/";
httpService = new HTTPService();
_results = new ArrayCollection();
}
[Bindable(event="testStatusChange")]
public function get testing ():Boolean
{
return _testing;
}
public function get results():ArrayCollection
{
return _results;
}
public function get chartResults() : ArrayCollection
{
var result : ArrayCollection = new ArrayCollection();
for ( var index : int = 0; index < ITERATIONS; index++ ) { var summaryVO : TestSummaryVO = new TestSummaryVO(); summaryVO.iteration = index+1; result.addItem( summaryVO ); } for each ( var vo : TestVO in results ) { summaryVO = result.getItemAt( vo.iteration ) as TestSummaryVO; if ( vo.type == TestVO.TYPE_AMF ) summaryVO[ "amfDuration" + SerializationTestController.RECORD_COUNT[ vo.recordIndex ] ] = vo.endTime - vo.startTime; else summaryVO[ "jsonDuration" + SerializationTestController.RECORD_COUNT[ vo.recordIndex ] ] = vo.endTime - vo.startTime; } return result; } public function startTest() : void { if ( _testing ) return; _testing = true; testIndex = 0; iterationIndex = 0; recordCountIndex = 0; testInstanceIndex = 0; updateTest(); dispatchEvent( new TestUpdateEvent( TestUpdateEvent.TEST_STATUS ) ); dispatchEvent( new TestProgressEvent( "STARTING TEST..." ) ); } private function completeTest() : void { _testing = false; dispatchEvent( new TestUpdateEvent( TestUpdateEvent.TEST_STATUS ) ); dispatchEvent( new TestProgressEvent( "TEST COMPLETE" ) ); } private function createTestVO() : void { currentTest = new TestVO(); currentTest.startTime = getTimer(); currentTest.index = testInstanceIndex; currentTest.iteration = iterationIndex; currentTest.recordIndex = recordCountIndex; currentTest.type = TESTS[ testIndex ]; } private function finalizeTestVO(error : Boolean = false) : void { if ( error ) currentTest.endTime = -1 else currentTest.endTime = getTimer(); _results.addItem( currentTest ); dispatchEvent( new TestUpdateEvent( TestUpdateEvent.TEST_UPDATE, currentTest ) ); dispatchEvent( new TestProgressEvent( "task completed in " + (currentTest.endTime - currentTest.startTime) + " milliseconds" ) ); currentTest = null; } private function updateTest() : void { if ( iterationIndex >= ITERATIONS )
return completeTest();
createTestVO();
if ( TESTS[ testIndex ] == TestVO.TYPE_AMF )
{
doAMFTest();
recordCountIndex ++;
if ( recordCountIndex >= RECORD_COUNT.length )
{
recordCountIndex = 0;
testIndex++;
}
}
else if ( TESTS[ testIndex ] == TestVO.TYPE_JSON )
{
doJSONTest();
recordCountIndex ++;
if ( recordCountIndex >= RECORD_COUNT.length )
{
recordCountIndex = 0;
testIndex = 0;
iterationIndex ++;
}
}
testInstanceIndex++;
}
private function doAMFTest() : void
{
dispatchEvent( new TestProgressEvent( "AMF Requesting " + RECORD_COUNT[ recordCountIndex ] ) );
var token : AsyncToken = remoteObject.getRecords( RECORD_COUNT[ recordCountIndex ] );
token.addResponder( new mx.rpc.Responder( onAMFResult, onFault ) );
}
protected function onAMFResult( event : ResultEvent ) : void
{
var result : ArrayCollection = event.result as ArrayCollection;
finalizeTestVO();
updateTest();
}
private function doJSONTest() : void
{
dispatchEvent( new TestProgressEvent( "JSON Requesting " + RECORD_COUNT[ recordCountIndex ] ) );
httpService.url = "http://tricedesigns.com/com/tricedesigns/mobileTest/Services.cfc?method=getrecords&records=" + RECORD_COUNT[ recordCountIndex ] + "&returnformat=json";
var token : AsyncToken = httpService.send();
token.addResponder( new mx.rpc.Responder( onJSONResult, onFault ) );
}
protected function onJSONResult( event : ResultEvent ) : void
{
var resultString : String = event.result as String;
var result : ArrayCollection = new ArrayCollection( JSON.parse( resultString ) as Array );
finalizeTestVO();
updateTest();
}
protected function onFault( event : FaultEvent ) : void
{
trace( event.fault.toString() );
finalizeTestVO(true);
updateTest();
}
}
}
Also, here is the TestVO value object that shows the information captured for each test:
package model
{
public class TestVO
{
public static const TYPE_JSON : String = "json";
public static const TYPE_AMF : String = "amf";
public var index : int;
public var iteration : int = 0;
public var startTime : int;
public var endTime : int;
public var type : String;
public var recordIndex : int;
public function TestVO()
{
}
}
}
Summary
Both JSON and AMF are acceptable serialization formats for mobile applications built with AIR. Both are compact serialization formats that minimize packet size. Both have native parsing/decoding by the AIR runtime. AMF will generally provide better performance for larger data sets. JSON *may* provide marginally better performance for small data sets. AMF also allows for strongly typed object serialization & deserialization, where JSON does not.
The answer to the question of “should I use AMF or JSON” is subjective… What kind of data are you returning, and how much data is it? Do you already have AMF services built? Do you already have JSON services built? Are the services consumed by multiple endpoints, with multiple technologies? Do you rely upon strongly typed objects in you development and maintenance processes? Both AMF and JSON are viable solutions for mobile applications.
Have you noticed when using twitter, google plus, or certain areas of facebook that when you scroll the page, it automatically loads more data? You don’t have to continually hit “next” to go through page after page of data. Instead, the content just “appears” as you need it. In this post we will explore a technique for making Flex list components behave in this exact way. As you scroll through the list, it continually requests more data from the server. Take a look at the video preview below, and afterwards we’ll explore the code.
The basic workflow is that you need to detect when you’ve scrolled to the bottom of the list, then load additional data to be displayed further in that list. Since you know how many records are currently in the list, you always know which “page” you are viewing. When you scroll down again, just request the next set of results that are subsequent to the last results that you requested. Each time you request data, append the list items to the data provider of the list.
First things first, you need to detect when you’ve scrolled to the bottom of the list. Here’s a great example showing how to detect when you have scrolled to the bottom of the list. You can just add an event listener to the list’s scroller viewport. Once you have a vertical scroll event where the new value is equal to the viewport max height minus the item renderer height, then you have scrolled to the end. At this point, request more data from the server.
One other trick that I am using here is that I am using conditional item renderers based upon the type of object being displayed. I have a dummy “LoadingVO” value object that is appended to the end of the list data provider. The item renderer function for the list will return a LoadingItemRenderer instance if the data passed to it is a LoadingVO.
Here it is up close, in case you missed it:
Here’s my InfiniteScrollList class:
package components
{
import model.InfiniteListModel;
import model.LoadingVO;
import mx.core.ClassFactory;
import mx.events.PropertyChangeEvent;
import spark.components.IconItemRenderer;
import spark.components.List;
import views.itemRenderer.LoadingItemRenderer;
public class InfiniteScrollList extends List
{
override protected function createChildren():void
{
super.createChildren();
scroller.viewport.addEventListener( PropertyChangeEvent.PROPERTY_CHANGE, propertyChangeHandler );
itemRendererFunction = itemRendererFunctionImpl;
}
protected function propertyChangeHandler( event : PropertyChangeEvent ) : void
{
//trace( event.property, event.oldValue, event.newValue );
if ( event.property == "verticalScrollPosition" )
{
if ( event.newValue == ( event.currentTarget.measuredHeight - event.currentTarget.height ))
{
fetchNextPage();
}
}
}
protected function fetchNextPage() : void
{
if ( dataProvider is InfiniteListModel )
InfiniteListModel( dataProvider ).getNextPage();
}
private function itemRendererFunctionImpl(item:Object):ClassFactory
{
var cla:Class = IconItemRenderer;
if ( item is LoadingVO )
cla = LoadingItemRenderer;
return new ClassFactory(cla);
}
}
}
You may have noticed in the fetchNextPage() function that the dataProvider is referenced as an InfiniteListModel class… let’s examine this class next. The InfiniteListModel class is simply an ArrayCollection which gets populated by the getNextPage() function. Inside of the getNextPage() function, it calls a remote service which returns data to the client, based on the current “page”. In the result handler, you can see that I disable binding events using disableAutoUpdate(), remove the dummy LoadingVO, append the service results to the collection, add a new LoadingVO, and then re-enable binding events using enableAutoUpdate(). Also, notice that I have a boolean _loading value that is true while requesting data from the server. This boolean flag is used to prevent multiple service calls for the same data.
Let’s take a look at the InfiniteListModel class:
package model
{
import flash.events.Event;
import flash.utils.setTimeout;
import mx.collections.ArrayCollection;
import mx.rpc.AsyncToken;
import mx.rpc.Responder;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.rpc.remoting.RemoteObject;
public class InfiniteListModel extends ArrayCollection
{
private var _remoteObject : RemoteObject;
protected var _loading : Boolean = false;
public function get remoteObject():RemoteObject
{
return _remoteObject;
}
public function set remoteObject(value:RemoteObject):void
{
_remoteObject = value;
if ( _remoteObject )
getNextPage();
}
public function InfiniteListModel(source:Array=null)
{
super(source);
addItem( new LoadingVO() );
}
public function getNextPage() : void
{
if ( !_loading)
{
_loading = true;
trace( "fetching data starting at " + (this.length-1).toString() );
var token : AsyncToken = remoteObject.getData( this.length-1 );
var responder : Responder = new Responder( resultHandler, faultHandler );
token.addResponder( responder );
}
}
protected function resultHandler(event:ResultEvent):void
{
this.disableAutoUpdate();
if ( this.getItemAt( this.length-1 ) is LoadingVO )
this.removeItemAt( this.length-1 );
for each ( var item : * in event.result )
{
addItem( item );
}
addItem( new LoadingVO() );
this.enableAutoUpdate();
_loading = false;
}
protected function faultHandler(event:FaultEvent):void
{
trace( event.fault.toString() );
}
}
}
Now, let’s take a look at the root view that puts everything together. There is an InfiniteScrollList whose dataProvider is an InfiniteListModel instance. The InfiniteListModel also references a RemoteObject instance, which loads data from a remote server.
Let’s not forget the remote service. In this case, I’m calling into a very basic remote CFC that returns an Array of string values. You can see the code below:
I was recently asked by a friend and former colleague about the best way to get text within a s:Label to behave and scroll properly, especially in the Flex mobile SDK. In particular, having a large block of text wrap correctly and scroll only in the vertical direction. By default if you don’t set a size on the label, the behavior of the Flex framework is that the views containing the label will resize, and the text will be displayed as entered (without word wrap or truncation). This may cause some layout issues and confusion as to “what the heck is going on with my text”.
I’ve found that the best way to achieve the desired behavior is to set a maxWidth on the label to force proper word wrapping, and then wrap the label in a s:Scroller to have it scroll properly. I chose to set a maxWidth to allow the label to determine it’s own size, and only to wrap if it needs to. An easy shortcut for proper wrapping is to bind the maxWidth of the label to the width of the scroller component. Also, DO NOT set a static height or a max height. This will cause the text within the label to be truncated, and it will not scroll at all if the static height is less than the height of the scroller. I’ve also noticed that setting cacheAsBitmap=true on the label also helps scroll performance in some circumstances, but this is not required.
Check out a video showing the scroll behavior of a large text block using this approach:
Below is the code that makes it work, which follows the method described above:
<s:Scroller height="100%" width="100%" id="scroller1">
<s:Group>
<s:Label id="labelInstance"
cacheAsBitmap="true"
maxWidth="{ scroller1.width }"
color="#FF0000" fontSize="32">
<s:text>Bacon ipsum dolor sit amet sint cow irure et magna, meatball aliquip qui. Tempor turkey capicola, eiusmod sed nisi dolore. Pig nisi rump boudin in culpa chuck. In ex sausage filet mignon shankle ut. Flank ball tip cillum aute. Nulla frankfurter culpa, elit et esse aute pork salami. Laborum mollit short ribs, in meatloaf eu irure dolor consectetur elit strip steak.
{note: text has been truncated to emphasize actual code, not the random text}
</s:text>
</s:Label>
</s:Group>
</s:Scroller>
The recordings of my presentations don’t seem to be available yet on Adobe TV, but here is the content, as promised. I spoke at MAX this year on “Multi Device Best Practices using Flex & AIR for Mobile”, and “Create beautiful, immersive content and applications with HTML5 and CSS3″, and the content from these presentations is below.
Multi Device Best Practices using Flex & AIR for Mobile
In the multi-device best practices session I covered the basics for building a multi-device/multi-form-factor application that conforms to device constraints (phone and tablet), using a single codebase that is able to detect device dimensions and orientation. This was followed by online/offline detection for occasionally-connected applications, and then followed by device-specific layout using CSS media queries and MultiDPIBitmapSource images. The presentation slides are below.
Create beautiful, immersive content and applications with HTML5 and CSS3
In this session, I gave a “crash course” in developing rich content experiences with HTML5 and CSS3. I started with a general overview presentation, followed by diving directly into code. I covered <video>, <audio>, dynamic graphics with <canvas>, <svg>, HTML5 Form elements, CSS3 Web Fonts, Visual Styles (shadows, corners), CSS3 Color spaces (RGBA, HSLA), graidents, transforms, animations, and media queries. In the presentation, I also discussed the necessity of client-side solution accelerator frameowrks (jQuery or other JS framework), in addition to graceful degradation and HTML5 feature detection using Modernizr. The presentation slides are below:
Although there were no official announcements around Flex, Flash & AIR (other than the release of FP11 & AIR3), don’t think that the platform is going away or becoming stale… In fact, it is quite the opposite. The Flash Platform will continue to thrive and innovate, providing outstanding solutions that set the pace for other technologies to follow. In case you missed the session, here is the “Flash Platform Roadmap”, provided by Scott Castle, Adam Lehman, and Raghu Thricovil, Product Managers for Flash Platform tooling:
If that wasn’t enough, did you see the new “Monocle” tool, shown by Deepa Subramaniam? Monocle is the new realtime profiling tool for Flash-based content which will provide additional insight into what’s happening at runtime, and how you can optimize your applications.
Did you also see the latest demos showing the Epic Games & the Unreal engine running INSIDE of the Flash Player?
Yes, this is really the Flash Player. You can read more here.
Here’s a quick tip for detecting device form factor (tablet vs phone) within your Flex mobile applications. First, get the stage dimensions for the screen size in pixels, then divide that by the applicationDPI (screen pixel density). This will give you the approximate size in inches of the device’s screen. I say “approximate” because the pixel densities are rounded to 160, 240, or 320, depending on the device. In my code, I make the assumption that if the landscape width is greater than or equal to 5 inches, then its a tablet. I used view states, but you can also layout components manually.
Check out the code below:
<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:views="views.*"
resize="view1_resizeHandler(event)">
<fx:Script>
<![CDATA[
import mx.core.FlexGlobals;
import mx.events.ResizeEvent;
protected function view1_resizeHandler(event:ResizeEvent):void
{
var _width : Number = Math.max( stage.stageWidth, stage.stageHeight );
var _height : Number = Math.min( stage.stageWidth, stage.stageHeight );
_width = _width / FlexGlobals.topLevelApplication.applicationDPI;
_height = _height / FlexGlobals.topLevelApplication.applicationDPI;
//this will resolve to the physical size in inches...
//if greater than 5 inches, assume its a tablet
if ( _width >= 5 )
currentState = "tablet";
else
currentState = "phone";
}
]]>
</fx:Script>
<s:states>
<s:State name="tablet" />
<s:State name="phone" />
</s:states>
<views:PhoneView includeIn="phone"
width="100%" height="100%" />
<views:TabletView includeIn="tablet"
width="100%" height="100%" />
</s:View>
My recent Developer Deep Dive webinar “Visualizing Complex Enterprise Data in a Tablet World” is now available on the Adobe Enterprise Developers Portal. In this webinar, I walk through the fundamentals of a rich, enterprise-data driven mobile experience, powered by Flex, AIR, LCDS, & LCCS.
Take a look at this short clip to get a feel for what’s covered in this webinar…
Not only were Flash Player 11 & Air 3 announced yesterday, but also the arrival of Flex & Flash Buidler 4.6. Flex & Flash Buidler 4.6 bring forth a new benchmark in performance, as well as a new & enhanced set of tools for developing mobile, desktop, and web applications. Bonus!!! – Flash Builder 4.6 will be a FREE update to all Flash Builder 4.5 customers.
Adobe remains focused on performance and in Flex 4.6 we’ve made considerable improvements. Many key performance optimizations were introduced, giving mobile applications the native feel you expect. Simply repackaging, an existing Flex mobile application with Flex 4.6 can yield up to a 50% performance gain. Creating a new application in Flex 4.6 will deliver near-native performance with the superior customization you expect from Flex.
If that wasn’t reason enough alone, let’s talk about the framework:
SplitViewNavigator – A new top-level application component specifically designed for the tablet experience. With only a few lines of code, manage the layout of multiple views and have them adapt automatically based on device orientation.
CallOutButton – A versatile component that pops over existing content and can contain text, components or even entire views.
SpinnerList – This popular tablet component is an adaption of the existing List component. It not only has a new look, but also gives options like recirculating content and a position based selection model.
DateSpinner – A highly flexible component that is not only locale-aware, but provides multiple out-of-the-box configurations to fit most date/time entry needs.
Text Enhancements – Flex 4.6 solves the problem of cross-device text input. Flex exposes the native text-editing controls on EVERY platform—this enables the developer to customize the keyboard and the user to experience the native UI of common operations like selection, copy/paste and spelling checking.
ToggleSwitch -
This simple and much-requested control is now available in Flex 4.6.
Can you feel the excitement?!?!?! Adobe has just announced the availability of AIR 3 and Flash Player 11 for early October! This release will bring a wave of change to the Internet and development tooling as you know them. From console-quality hardware accelerated 3D, to AIR captive runtime, to native extensions… Adobe tools will enable the next wave of innovation in games, rich media, and multi-device applications. In this release there are a lot of new features to get excited about.
Flash Player 11 and AIR 3 will be publicly available in early October. Flash Builder and Flex will offer support for the new features in an upcoming release before the end of the year. We’ll have a lot of news and new content at MAX, and in the meantime, you can download the Flash Player and AIR betas on Labs, and start checking out some of the amazing content that’s already been built by developers!
Recent Comments