Skip to main content
Participant
October 31, 2012
Answered

Excessive connection latency... Does Adobe Air support connection pooling on mobile?

  • October 31, 2012
  • 2 replies
  • 2898 views

Hello,

 

I am developing a mobile app which loads thumbnail images from a remote server. During testing on the Android platform, however, I have discovered that images are very slow to load. By monitoring server logs I have determined that the poor performance is caused by the lack of connection pooling, meaning that each request builds a new connection. Running the sample code below on a mobile device produces 20 requests and 20 connection attempts. By comparison, the same web or desktop app creates 2 connections and reuses those connections for subsequent requests. The substantial overhead and latency associated with generating new connections has a substantial affect on performance, with 20 thumbnails taking approximately 4-5 seconds to load on mobile versus 0.5 - 1 second on a desktop.

 

I have included a sample app below to emphasis the performance issue. The image itself is very small (290 bytes) to focus the issue on connection latency. I have confirmed this behavior on numerous Android devices, running 4.1, 4.0, and 2.3. I have also attempted using Loader v. URLLoader v. URLStream and sequential v. simultaneous loading with no change in connection behavior. Attempting to set the connection to 'keep-alive' in the URLRequest also has no affect.

package

{

          import flash.display.Loader;

          import flash.display.Sprite;

          import flash.display.StageAlign;

          import flash.display.StageScaleMode;

          import flash.events.Event;

          import flash.net.URLRequest;

          import flash.utils.getTimer;

 

          public class Main extends Sprite

          {

 

                    private var _count:int = 0;

 

                    public function Main()

                    {

                              super();

 

                              stage.align = StageAlign.TOP_LEFT;

                              stage.scaleMode = StageScaleMode.NO_SCALE;

 

                              trace("Start time " + getTimer() + " ms");

 

                              var loader:Loader;

                              var url:String = "http://fbcdn-profile-a.akamaihd.net/static-ak/rsrc.php/v2/yo/r/UlIqmHJn-SK.gif";  // 290 bytes

 

                              for (var i:int = 0; i < 20; i++) {

                                        loader = this.addChild(new Loader()) as Loader;

                                        loader.contentLoaderInfo.addEventListener(Event.COMPLETE, complete);

                                        loader.load(new URLRequest(url));

                              }

                    }

 

                    private function complete(event:Event):void

                    {

                              _count++

                              trace("Finished " + _count + " at " + getTimer() + " ms");

                    }

 

          }

}

So, I have a couple questions:

1) Is there something that I can do to enable connection reuse?

2) Is this an inherent limitation with Adobe Air for mobile?

3) Can someone confirm whether this limitation exists on other mobile platforms (iOS or Blackberry)?

 

Any help that you can provide would be greatly appreciated. I am really hoping that this isn't a fundamental limitation of Adobe Air as it causes my app to feel very sluggish.

 

Thanks,

 

Adam

This topic has been closed for replies.
Correct answer Gaius Coffey

Hi Gaius,

 

Thanks for taking the time to help me get to the root of this problem and for evaluating the performance on the iOS platform. I did some further testing in an attempt to replicate your results. Here is where things get a bit interesting.

 

Initially, I could not see any improvement between the debug and release versions on the Android platform. However, when I exported the release with 'captive runtime' option (versus shared), the performance did improve. Upon digging further, I realized that it was not an issue of captive versus shared runtime, but rather a difference in the runtime versions. The captive runtime included in my build (and presumably yours) was version 3.1. The share runtime, however, was 3.4. If I set the target version to 3.4 using the latest SDK, and included the captive runtime, the performance was once again degraded. In summary here are my findings for the Android platform.

 

1) Debug (target 3.1, shared 3.4 runtime) : Slow.

2) Release (target 3.1, shared 3.4 runtime) : Slow.

3) Release (target 3.1, captive 3.1 runtime) : Fast.

4) Release (target 3.4, shared or captive 3.4 runtime) : Slow.

 

The difference between slow and fast are night and day (as you discovered) and seem to represent the difference between pooled and non pooled connections. I believe that connection pooling has somehow become broken between versions 3.1 and 3.4. Can you confirm these findings?

 

Thanks again,

Adam


Hmm. You were absolutely correct, that's a bit disappointing!

This is Android recompiled for 3.4 rather than 3.1.

null

COMPLETED 50 with 8 loaders in 11327 milliseconds or 226.54 per load.   50           8              11327    226.54

COMPLETED 50 with 50 loaders in 8899 milliseconds or 177.98 per load.   50           50           8899       177.98

COMPLETED 50 with 50 loaders in 9280 milliseconds or 185.6 per load.     50           50           9280       185.6

COMPLETED 50 with 50 loaders in 9513 milliseconds or 190.26 per load.   50           50           9513       190.26

COMPLETED 50 with 8 loaders in 9744 milliseconds or 194.88 per load.     50           8              9744       194.88

COMPLETED 50 with 1 loaders in 16383 milliseconds or 327.66 per load.   50           1              16383    327.66

Versus Apple iPad recompiled for 3.4 rather than 3.1.

null

COMPLETED 50 with 8 loaders in 502 milliseconds or 10.04 per load. 50      8        502    10.04

COMPLETED 50 with 50 loaders in 100 milliseconds or 2 per load.     50      50      100    2

COMPLETED 50 with 50 loaders in 117 milliseconds or 2.34 per load. 50      50      117    2.34

COMPLETED 50 with 50 loaders in 93 milliseconds or 1.86 per load.  50      50      93      1.86

COMPLETED 50 with 8 loaders in 270 milliseconds or 5.4 per load.    50      8        270    5.4

COMPLETED 50 with 8 loaders in 307 milliseconds or 6.14 per load.  50      8        307    6.14

COMPLETED 50 with 8 loaders in 316 milliseconds or 6.32 per load.  50      8        316    6.32

COMPLETED 50 with 4 loaders in 555 milliseconds or 11.1 per load.  50      4        555    11.1

COMPLETED 50 with 4 loaders in 547 milliseconds or 10.94 per load. 50      4        547    10.94

COMPLETED 50 with 4 loaders in 535 milliseconds or 10.7 per load.  50      4        535    10.7

COMPLETED 50 with 2 loaders in 1038 milliseconds or 20.76 per load.        50      2        1038          20.76

COMPLETED 50 with 2 loaders in 1042 milliseconds or 20.84 per load.        50      2        1042          20.84

COMPLETED 50 with 1 loaders in 2107 milliseconds or 42.14 per load.        50      1        2107          42.14

COMPLETED 50 with 1 loaders in 2099 milliseconds or 41.98 per load.        50      1        2099          41.98

Both are on release compile, which should take out all of the variability. So, yes, it looks as if the 3.4 AIR runtime has lost the connection pooling but ONLY for Android.

PS: Code I used in my test (which was a conventional View based Mobile app) is below in case you want to include it when you submit a bug report.

<?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" title="TestLLatency" xmlns:mx="library://ns.adobe.com/flex/mx">
<fx:Script>
  <![CDATA[
   import mx.events.FlexEvent;
  
   protected function uicomponent1_creationCompleteHandler(event:FlexEvent):void
   {
    startTest();

   }
    
   public function startTest():void {
    if(queue) throw new Error("Please wait for previous test to end.");
    queue = new Array();
    _count = 0;
    var loader:Loader;
    var i:int;
    for (i = 0; i < numToQueue; i++) {
     loader = bob.addChild(new Loader()) as Loader;
     loader.x = (i%10)*50;
     loader.y = Math.floor(i/5)*50;
     loader.contentLoaderInfo.addEventListener(Event.COMPLETE, complete);
     queue.push(loader);
    } 
    startTime = getTimer();
    for(i=0;i<numLoaders;i++) {
     nextQueue();
    }  
   }
   protected var startTime:int;
   protected var endTime:int;
   protected var numToQueue:int = 50;
   protected var numLoaders:int = 8;
   protected var queue:Array;
   private var _count:int;
   private function complete(event:Event):void
   {
    _count++
    nextQueue();
   }
   [Bindable] protected var results:String;
   protected function nextQueue():void {
    var url:String = "http://fbcdn-profile-a.akamaihd.net/static-ak/rsrc.php/v2/yo/r/UlIqmHJn-SK.gif";  // 290 bytes
    if(queue && queue.length) {
     var loader:Loader = queue.pop() as Loader;
     loader.load(new URLRequest(url));
    } else if(_count==numToQueue) {
     endTime = getTimer();
     var elapsed:int = endTime-startTime;
     results += "\n"+("COMPLETED "+numToQueue+" with "+numLoaders+" loaders in "+(elapsed)+" milliseconds or "+(elapsed/numToQueue)+" per load.\t"+[numToQueue,numLoaders,elapsed,elapsed/numToQueue].join("\t"));
     queue = null;
     while(bob.numChildren>5) {
      bob.removeChildAt(bob.numChildren-1);
     }
     bob.getChildAt(0).addEventListener(MouseEvent.CLICK,repeatTest);
     bob.getChildAt(1).addEventListener(MouseEvent.CLICK,repeatTest);
     bob.getChildAt(2).addEventListener(MouseEvent.CLICK,repeatTest);
     bob.getChildAt(3).addEventListener(MouseEvent.CLICK,repeatTest);
     bob.getChildAt(4).addEventListener(MouseEvent.CLICK,repeatTest);
    }
   }
   protected function repeatTest(event:MouseEvent):void {
    var dob:DisplayObject = event.target as DisplayObject;
    var testBehaviour:int = dob.parent.getChildIndex(dob);
    try {
     switch(testBehaviour) {
      case 4 :
       this.numLoaders = this.numToQueue;
       break;
      case 3 :
       this.numLoaders = 8;
       break;
      case 2 :
       this.numLoaders = 4;
       break;
      case 1 :
       this.numLoaders = 2;
       break;
      case 0 :
       this.numLoaders = 1;
       break;
     }
     startTest();
     dob.removeEventListener(MouseEvent.CLICK,repeatTest);
     for(var i:int=0;i<5;i++) bob.removeChildAt(0);
    } catch(e:Error) {
     trace(e.message); 
    }
   }  
 
  ]]>
</fx:Script>
<fx:Declarations>
  <!-- Place non-visual elements (e.g., services, value objects) here -->
</fx:Declarations>
<s:HGroup width="100%" height="100%">
 
  <mx:UIComponent id="bob" width="100%" height="100%" creationComplete="uicomponent1_creationCompleteHandler(event)" />
  <s:TextArea text="{results}" width="100%" height="100%" />
</s:HGroup>
</s:View>

2 replies

Kalvyn Rasquinha
Participating Frequently
January 11, 2013

Apologies for the inconvenience, folks. The bug with connection pooling on Android got introduced when fixing the issue reported in this thread: http://forums.adobe.com/message/4563418. The connection pooling issue will be fixed in an upcoming version of AIR (3.6), which, I believe, will be available in early February. For those interested, these are the results I got with the test app run on a Xoom with ICS (on WiFi).

50          8          1650                    33

50          8          1861                    37.22

50          8          1696                    33.92

50          8          1624                    32.48

50          8          1652                    33.04

Legend
January 11, 2013

Hi,

I am out of the office until Tuesday, January 22nd. If you require an urgent answer, please try contacting me by mobile. Otherwise, I will respond to email on my return.

Thanks,

Gaius

Legend
November 1, 2012

Wow. That's shocking.

My results were a bit different - but still awful. Monitoring in Charles, PC seems to batch requests into up to four simultaneous calls where Motorola Xoom appears to do them in pairs, initially. However, after ending a batch test early, the pairs behaviour stopped and requests became sequential - which knocked everything until I restarted Xoom.

Real difference appeared to be in the switch-over between requests.  Not only could PC run four simultaneously, but subsequent requests started almost instantaneously so that I could average download of each item in about a quarter of the time it took to request it! Whereas... Xoom running in pairs would get two together, but them take 150 milliseconds or so to start the next pair of requests so that the best I achieved was three times the time it took to carry out the request.

I modified your code a bit to see if using different batch sizes has any affect - eg: I throttled the number of simultaneous loader requests running - and... well, you can make it worse. Essentially, if you have at least two loaders running at any time, the results are very similar.

This was Android Xoom (restarted to allow dual, simultaneous download): 

COMPLETED 50 with 1 loaders in 18602 milliseconds or 372.04 per load. 50 1 18602 372.04

COMPLETED 50 with 1 loaders in 17229 milliseconds or 344.58 per load. 50 1 17229 344.58

COMPLETED 50 with 50 loaders in 8023 milliseconds or 160.46 per load. 50 50 8023 160.46

COMPLETED 50 with 50 loaders in 7973 milliseconds or 159.46 per load. 50 50 7973 159.46

COMPLETED 50 with 2 loaders in 8889 milliseconds or 177.78 per load. 50 2 8889 177.78

COMPLETED 50 with 2 loaders in 10251 milliseconds or 205.02 per load. 50 2 10251 205.02

COMPLETED 50 with 4 loaders in 8245 milliseconds or 164.9 per load. 50 4 8245 164.9

COMPLETED 50 with 4 loaders in 9413 milliseconds or 188.26 per load. 50 4 9413 188.26

COMPLETED 50 with 8 loaders in 8223 milliseconds or 164.46 per load. 50 8 8223 164.46

COMPLETED 50 with 8 loaders in 8282 milliseconds or 165.64 per load. 50 8 8282 165.64

And this was my PC with four simultaneous downloads visible in Charles:

COMPLETED 50 with 1 loaders in 2998 milliseconds or 59.96 per load. 50 1 2998 59.96
COMPLETED 50 with 1 loaders in 2752 milliseconds or 55.04 per load. 50 1 2752 55.04
COMPLETED 50 with 2 loaders in 1468 milliseconds or 29.36 per load. 50 2 1468 29.36
COMPLETED 50 with 2 loaders in 1805 milliseconds or 36.1 per load. 50 2 1805 36.1
COMPLETED 50 with 4 loaders in 650 milliseconds or 13 per load. 50 4 650 13
COMPLETED 50 with 4 loaders in 932 milliseconds or 18.64 per load. 50 4 932 18.64
COMPLETED 50 with 8 loaders in 471 milliseconds or 9.42 per load. 50 8 471 9.42
COMPLETED 50 with 8 loaders in 432 milliseconds or 8.64 per load. 50 8 432 8.64
COMPLETED 50 with 50 loaders in 460 milliseconds or 9.2 per load. 50 50 460 9.2
COMPLETED 50 with 50 loaders in 532 milliseconds or 10.64 per load. 50 50 532 10.64

[code]

package
{
import flash.display.DisplayObject;
import flash.display.Loader;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.net.URLRequest;
import flash.utils.getTimer;

public class Latency extends Sprite
{
  public function Latency()
  {
   super();
   // support autoOrients
   stage.align = StageAlign.TOP_LEFT;
   stage.scaleMode = StageScaleMode.NO_SCALE;
   startTest();
  }
  public function startTest():void {
   if(queue) throw new Error("Please wait for previous test to end.");
   queue = new Array();
   _count = 0;
   var loader:Loader;
   var i:int;
   for (i = 0; i < numToQueue; i++) {
    loader = this.addChild(new Loader()) as Loader;
    loader.x = (i%10)*50;
    loader.y = Math.floor(i/5)*50;
    loader.contentLoaderInfo.addEventListener(Event.COMPLETE, complete);
    queue.push(loader);
   } 
   startTime = getTimer();
   for(i=0;i<numLoaders;i++) {
    nextQueue();
   }  
  }
  protected var startTime:int;
  protected var endTime:int;
  protected var numToQueue:int = 50;
  protected var numLoaders:int = 8;
  protected var queue:Array;
  private var _count:int;
  private function complete(event:Event):void
  {
   _count++
   nextQueue();
  }
  protected function nextQueue():void {
   var url:String = "http://fbcdn-profile-a.akamaihd.net/static-ak/rsrc.php/v2/yo/r/UlIqmHJn-SK.gif";  // 290 bytes
   if(queue && queue.length) {
    var loader:Loader = queue.pop() as Loader;
    loader.load(new URLRequest(url));
   } else if(_count==numToQueue) {
    endTime = getTimer();
    var elapsed:int = endTime-startTime;
    trace("COMPLETED "+numToQueue+" with "+numLoaders+" loaders in "+(elapsed)+" milliseconds or "+(elapsed/numToQueue)+" per load.\t"+[numToQueue,numLoaders,elapsed,elapsed/numToQueue].join("\t"));
    queue = null;
    while(numChildren>5) {
     removeChildAt(numChildren-1);
    }
    getChildAt(0).addEventListener(MouseEvent.CLICK,repeatTest);
    getChildAt(1).addEventListener(MouseEvent.CLICK,repeatTest);
    getChildAt(2).addEventListener(MouseEvent.CLICK,repeatTest);
    getChildAt(3).addEventListener(MouseEvent.CLICK,repeatTest);
    getChildAt(4).addEventListener(MouseEvent.CLICK,repeatTest);
   }
  }
  protected function repeatTest(event:MouseEvent):void {
   var dob:DisplayObject = event.target as DisplayObject;
   var testBehaviour:int = dob.parent.getChildIndex(dob);
   try {
    switch(testBehaviour) {
     case 4 :
      this.numLoaders = this.numToQueue;
      break;
     case 3 :
      this.numLoaders = 8;
      break;
     case 2 :
      this.numLoaders = 4;
      break;
     case 1 :
      this.numLoaders = 2;
      break;
     case 0 :
      this.numLoaders = 1;
      break;
    }
    startTest();
    dob.removeEventListener(MouseEvent.CLICK,repeatTest);
    for(var i:int=0;i<5;i++) removeChildAt(0);
   } catch(e:Error) {
    trace(e.message); 
   }
  }
}
}

[/code]

Legend
November 1, 2012

Um.

Compare and contrast for the same test done against an IPad2 (using "Standard" compile rather than "Fast" compile).

COMPLETED 50 with 50 loaders in 850 milliseconds or 17 per load. 50 50 850 17

COMPLETED 50 with 50 loaders in 534 milliseconds or 10.68 per load. 50 50 534 10.68

COMPLETED 50 with 8 loaders in 691 milliseconds or 13.82 per load. 50 8 691 13.82

COMPLETED 50 with 8 loaders in 635 milliseconds or 12.7 per load. 50 8 635 12.7

COMPLETED 50 with 4 loaders in 1374 milliseconds or 27.48 per load. 50 4 1374 27.48

COMPLETED 50 with 4 loaders in 1077 milliseconds or 21.54 per load. 50 4 1077 21.54

COMPLETED 50 with 2 loaders in 2072 milliseconds or 41.44 per load. 50 2 2072 41.44

COMPLETED 50 with 2 loaders in 2133 milliseconds or 42.66 per load. 50 2 2133 42.66

COMPLETED 50 with 1 loaders in 4165 milliseconds or 83.3 per load. 50 1 4165 83.3

COMPLETED 50 with 1 loaders in 4516 milliseconds or 90.32 per load. 50 1 4516 90.32

Looks like it is using four simultaneous channels, and it is using them _very_ well.

G

Legend
November 1, 2012

Realised I'm not comparing like with like here as Android would have been running a debugger plugin which will be slower.

Am compiling now for a release build test.

G