admin管理员组

文章数量:1188831

This article hit the top of HackerNews recently: .html#

In which it states:

The cell radio is one of the biggest battery drains on a phone. Every time you send data, no matter how small, the radio is powered on for up for 20-30 seconds. Every decision you make should be based on minimizing the number of times the radio powers up. Battery life can be dramatically improved by changing the way your apps handle data transfers. Users want their data now, the trick is balancing user experience with transferring data and minimizing power usage. A balance is achieved by apps carefully bundling all repeating and intermittent transfers together and then aggressively prefetching the intermittent transfers.

I would like to modify $.ajax to add an option like "doesn't need to be done right now, just do this request when another request is launched". What would be a good way to go about this?

I started with this:

(function($) {
    var batches = [];
    var oldAjax = $.fn.ajax;
    var lastAjax = 0;
    var interval = 5*60*1000; // Should be between 2-5 minutes
    $.fn.extend({batchedAjax: function() {
        batches.push(arguments);
    }});
    var runBatches = function() {
        var now = new Date().getTime();
        var batched;
        if (lastAjax + interval < now) {
            while (batched = batches.pop()) {
                oldAjax.apply(null, batched);
            }
        }
    }
    setInterval(runBatches, interval);
    $.fn.ajax = function() {
        runBatches();
        oldAjax.apply(null, arguments);
        lastAjax = now;
    };
})(jQuery);

I can't tell by the wording of the paper, I guess a good batch "interval" is 2-5 minutes, so I just used 5.

Is this a good implementation?

  • How can I make this a true modification of just the ajax method, by adding a {batchable:true} option to the method? I haven't quite figured that out either.

  • Does setInterval also keep the phone awake all the time? Is that a bad thing to do? Is there a better way to not do that?

  • Are there other things here that would cause a battery to drain faster?

  • Is this kind of approach even worthwhile? There are so many things going on at once in a modern smartphone, that if my app isn't using the cell, surely some other app is. Javascript can't detect if the cell is on or not, so why bother? Is it worth bothering?

This article hit the top of HackerNews recently: http://highscalability.com/blog/2013/9/18/if-youre-programming-a-cell-phone-like-a-server-youre-doing.html#

In which it states:

The cell radio is one of the biggest battery drains on a phone. Every time you send data, no matter how small, the radio is powered on for up for 20-30 seconds. Every decision you make should be based on minimizing the number of times the radio powers up. Battery life can be dramatically improved by changing the way your apps handle data transfers. Users want their data now, the trick is balancing user experience with transferring data and minimizing power usage. A balance is achieved by apps carefully bundling all repeating and intermittent transfers together and then aggressively prefetching the intermittent transfers.

I would like to modify $.ajax to add an option like "doesn't need to be done right now, just do this request when another request is launched". What would be a good way to go about this?

I started with this:

(function($) {
    var batches = [];
    var oldAjax = $.fn.ajax;
    var lastAjax = 0;
    var interval = 5*60*1000; // Should be between 2-5 minutes
    $.fn.extend({batchedAjax: function() {
        batches.push(arguments);
    }});
    var runBatches = function() {
        var now = new Date().getTime();
        var batched;
        if (lastAjax + interval < now) {
            while (batched = batches.pop()) {
                oldAjax.apply(null, batched);
            }
        }
    }
    setInterval(runBatches, interval);
    $.fn.ajax = function() {
        runBatches();
        oldAjax.apply(null, arguments);
        lastAjax = now;
    };
})(jQuery);

I can't tell by the wording of the paper, I guess a good batch "interval" is 2-5 minutes, so I just used 5.

Is this a good implementation?

  • How can I make this a true modification of just the ajax method, by adding a {batchable:true} option to the method? I haven't quite figured that out either.

  • Does setInterval also keep the phone awake all the time? Is that a bad thing to do? Is there a better way to not do that?

  • Are there other things here that would cause a battery to drain faster?

  • Is this kind of approach even worthwhile? There are so many things going on at once in a modern smartphone, that if my app isn't using the cell, surely some other app is. Javascript can't detect if the cell is on or not, so why bother? Is it worth bothering?

Share Improve this question edited Sep 24, 2013 at 23:41 000 asked Sep 20, 2013 at 5:06 000000 27.2k10 gold badges72 silver badges102 bronze badges 4
  • I see some significant problems with this approach if you have an app of significant complexity, or want to get different events that could trigger AJAX different levels of priority. For example, maybe some actions in the app require immediate AJAX calls to provide the best user experience (in which case you would want to also execute any lower priority calls). Also, this doesn't seem to allow for cases where "batched" AJAX calls may have interaction effects with one another (i.e. changes caused by one request might make parameters of another pending request need to change). – Mike Brant Commented Sep 24, 2013 at 23:55
  • 2 Certainly though it is good to think about such use cases for apps with heavy data usage and I think your approach is at least a good start in addressing the problem. I would question whether you have demonstrated that battery usage is a significant enough problem with your application at this point to cause you to want to take on the additional overhead of introducing such an approach. I would think that one might move away from a web app implementation to a native app implementation far before your reach that point. – Mike Brant Commented Sep 24, 2013 at 23:59
  • 3 I see some significant problems in other apps that destroy the battery anyway... – john Smith Commented Sep 25, 2013 at 1:05
  • off the top of my head i'm wondering if it would be smart to add a new callback to $.ajax that would check for batch requests that had been queued up that would attach to $.ajax (like .done()) and check for a value that would indicate how important/urgent a request would be – b_dubb Commented Oct 1, 2013 at 13:14
Add a comment  | 

6 Answers 6

Reset to default 7

I made some progress on adding the option to $.ajax, started to edit the question, and realized it's better as an answer:

(function($) {
    var batches = [];
    var oldAjax = $.fn.ajax;
    var lastAjax = 0;
    var interval = 5*60*1000; // Should be between 2-5 minutes
    var runBatches = function() {
        var now = new Date().getTime();
        var batched;
        if (lastAjax + interval < now) {
            while (batched = batches.pop()) {
                oldAjax.apply(null, batched);
            }
        }
    }
    setInterval(runBatches, interval);
    $.fn.ajax = function(url, options) {
        if (options.batchable) {
            batches.push(arguments);
            return;
        }
        runBatches();
        oldAjax.apply(null, arguments);
        lastAjax = now;
    };
})(jQuery);

That was actually fairly straightforward. Is love to see a better answer though.

  • Does setInterval also keep the phone awake all the time? Is that a bad thing to do? Is there a better way to not do that?

From an iPhone 4, iOS 6.1.0 Safari environment:

A wrote an app with a countdown timer that updated an element's text on one-second intervals. The DOM tree had about medium complexity. The app was a relatively-simple calculator that didn't do any AJAX. However, I always had a sneaking suspicion that those once-per-second reflows were killing me. My battery sure seemed to deplete rather quickly, whenever I left it turned-on on a table, with Safari on the app's webpage.

And there were only two timeouts in that app. Now, I don't have any quantifiable proof that the timeouts were draining my battery, but losing about 10% every 45 minutes from this dopey calculator was a little unnerving. (Who knows though, maybe it was the backlight.)

On that note: You may want to build a test app that does AJAX on intervals, other things on intervals, etc, and compare how each function drains your battery under similar conditions. Getting a controlled environment might be tricky, but if there is a big enough difference in drain, then even "imperfect" testing conditions will yield noticeable-enough results for you to draw a conclusion.


However, I found out an interesting thing about how iOS 6.1.0 Safari handles timeouts:

  • The timeouts don't run their callbacks if you turn off the screen.
  • Consequentially, long-term timeouts will "miss their mark."

If my app's timer was to display the correct time (even after I closed and reopened the screen), then I couldn't go the easy route and do secondsLeft -= 1. If I turned off the screen, then the secondsLeft (relative to my starting time) would have been "behind," and thus incorrect. (The setTimeout callback did not run while the screen was turned off.)

The solution was that I had to recalculate timeLeft = fortyMinutes - (new Date().getTime() - startTime) on each interval.

Also, the timer in my app was supposed to change from green, to lime, to yellow, to red, as it got closer to expiry. Since, at this point, I was worried about the efficiency of my interval-code, I suspected that it would be better to "schedule" my color changes for their appropriate time (lime: 20 minutes after starting time, yellow: 30 mins, red: 35) (this seemed preferable to a quadruple-inequality-check on every interval, which would be futile 99% of the time).

However, if I scheduled such a color change, and my phone's screen was turned off at the target time, then that color change would never happen.

The solution was to check, on each interval, if the time elapsed since the last 1-second timer update had been ">= 2 seconds". (This way, the app could know if my phone had had its screen turned off; it was able to realize when it had "fallen behind.") At that point, if necessary, I would "forcibly" apply a color change and schedule the next one.

(Needless to say, I later removed the color-changer...)

So, I believe this confirms my claim that

iOS 6.1.0 Safari does not execute setTimeout callback functions if the screen is turned off.

So keep this in mind when "scheduling" your AJAX calls, because you will probably be affected by this behavior as well.

And, using my proposition, I can answer your question:

  • At least for iOS, we know that setTimeout sleeps while the screen is off.
  • Thus setTimeout won't give your phone "nightmares" ("keep it awake").

  • Is this kind of approach even worthwhile? There are so many things going on at once in a modern smartphone, that if my app isn't using the cell, surely some other app is. Javascript can't detect if the cell is on or not, so why bother? Is it worth bothering?

If you can get this implementation to work correctly then it seems like it would be worthwhile.

You will incur latency for every AJAX request you make, which will slow down your app to some degree. (Latency is the bane of page loading time, after all.) So you will definitely achieve some gain by "bundling" requests. Extending $.ajax such that you can "batch" requests will definitely have some merit.

The article you've linked clearly focuses on optimizing power consumption for apps (yes, the weather widget example is horrifying). Actively using a browser is, by definition, a foreground task; plus something like ApplicationCache is already available to reduce the need for network requests. You can then programmatically update the cache as required and avoid DIY.

Sceptical side note: if you are using jQuery as part of your HTML5 app (perhaps wrapped in Sencha or similar), perhaps the mobile app framework has more to do with request optimization than the code itself. I have no proof whatsoever, but goddammit this sounds about right :)

  • How can I make this a true modification of just the ajax method, by adding a {batchable:true} option to the method? I haven't quite figured that out either.

A perfectly valid approach but to me this sounds like duck punching gone wrong. I wouldn't. Even if you correctly default batchable to false, personally I would rather use a facade (perhaps even in its own namespace?)

var gQuery = {}; //gQuery = green jQuery, patent pending :)
gQuery.ajax = function(options,callback){
  //your own .ajax with blackjack and hooking timeouts, ultimately just calling
  $.ajax(options);
}
  • Does setInterval also keep the phone awake all the time? Is that a bad thing to do? Is there a better way to not do that?

Native implementations of setInterval and setTimeout are very similar afaik; think of the latter not firing while the website is in the background for online banking inactivity prompts; when a page is not in the foreground its execution is basically halted. If an API is available for such "deferrals" (the article mentions of some relevant iOS7 capabilities) then it's likely a preferable approach, otherwise I see no reason to avoid setInterval.

  • Are there other things here that would cause a battery to drain faster?

I'd speculate that any heavy load would (from calculating pi to pretty 3d transitions perhaps). But this sounds like premature optimization to me and reminds me of an e-reader with battery-saving mode that turned the LCD screen completely off :)

  • Is this kind of approach even worthwhile? There are so many things going on at once in a modern smartphone, that if my app isn't using the cell, surely some other app is. Javascript can't detect if the cell is on or not, so why bother? Is it worth bothering?

The article pointed out a weather app being unreasonably greedy, and that would concern me. It seems to be a development oversight though more than anything else, as in fetching data more often than it's really needed. In an ideal world, this should be nicely handled on OS level, otherwise you'd end up with an array of competing workarounds. IMO: don't bother until highscalability posts another article telling you to :)

Here is my version:

(function($) {
    var batches = [],
        ajax = $.fn.ajax,
        interval =  5*60*1000, // Should be between 2-5 minutes
        timeout = setTimeout($.fn.ajax, interval);

    $.fn.ajax=function(url, options) {
        var batched, returns;
        if(typeof url === "string") {
            batches.push(arguments);
            if(options.batchable) {
                return;
            }
        }
        while (batched = batches.shift()) {
            returns = ajax.apply(null, batched);
        }
        clearTimeout(timeout);
        timeout = setTimeout($.fn.ajax, interval);
        return returns;
    }
})(jQuery);

I think this version has the following main advantages:

  • If there is a non-batchable ajax call, the connection is used to send all batches. This Resets the timer.
  • Returns the expected return value on direct ajax calls
  • A direct processing of the batches can be triggered by calling $.fn.ajax() without parameters

As far as hacking the $.ajax method, I would :

  • try to also preserve the Promise mechanism provided by $.ajax,
  • take advantage of one of the global ajax events to trigger ajax calls,
  • maybe add a timer, to have the batch being called anyways in case no "immediate" $.ajax call is made,
  • give a new name to this function (in my code : $.batchAjax) and keep the orginal $.ajax.

Here is my go :

(function ($) {
    var queue = [],
        timerID = 0;

    function ajaxQueue(url, settings) {
    // cutom deferred used to forward the $.ajax' promise
        var dfd = new $.Deferred();

        // when called, this function executes the $.ajax call
        function call() {
            $.ajax(url, settings)
            .done(function () {
                dfd.resolveWith(this, arguments);
            })
            .fail(function () {
                dfd.rejectWith(this, arguments);
            });
        }

        // set a global timer, which will trigger the dequeuing in case no ajax call is ever made ...
        if (timerID === 0) {
            timerID = window.setTimeout(ajaxCallOne, 5000);
        }

        // enqueue this function, for later use
        queue.push(call);
        // return the promise
        return dfd.promise();
    }

    function ajaxCallOne() {
        window.clearTimeout(timerID);
        timerID = 0;

        if (queue.length > 0) {
            f = queue.pop();
        // async call : wait for the current ajax events
        //to be processed before triggering a new one ...
            setTimeout(f, 0);
        }
    }

    // use the two functions :

    $(document).bind('ajaxSend', ajaxCallOne);
    // or : 
    //$(document).bind('ajaxComplete', ajaxCallOne);

    $.batchAjax = ajaxQueue;
}(jQuery));

In this example, the hard coded delay fo 5 seconds defeats the purpose of "if less than 20 seconds between calls, it drains the battery". You can put a bigger one (5 minutes ?), or remove it altogether - it all depends on your app really.

fiddle


Regarding the general question "How do I write a web app which doesn't burn a phone's battery in 5 minutes ?" : it will take more than one magic arrow to deal with that one. It is a whole set of design decisions you will have to take, which really depends on your app.

You will have to arbitrate between loading as much data as possible in one go (and possibly send data which won't be used) vs fetching what you need (and possibly send many small individual requests).

Some parameters to take into account are :

  • volume of data (you don't want to drain your clients data plan either ...),
  • server load,
  • how much can be cached,
  • importance of being "up to date" (5 minutes delay for a chat app won't work),
  • frequency of client updates (a network game will probably require lots of updates from the client, a news app probably less ...).

One rather general suggestion : you can add a "live update" checkbox, and store its state client side. When unchecked, the client should hit a "refresh" button to download new data.

Here is my go, it somewhat grew out of what @Joe Frambach posted but I wanted the following additions:

  1. retain the jXHR and error/success callbacks if they were provided
  2. Debounce identical requests (by url and options match) while still triggering the callbacks or jqXHRs provided for EACH call
  3. Use AjaxSettings to make configuration easier
  4. Don't have each non batched ajax flush the batch, those should be separate processes IMO, but thus supply an option to force a batch flush as well.

Either way, this sucker would mostly likely be better done as a separate plugin rather than overriding and affecting the default .ajax function... enjoy:

(function($) {
    $.ajaxSetup({
        batchInterval: 5*60*1000,
        flushBatch: false,
        batchable: false,
        batchDebounce: true
    });

    var batchRun = 0;
    var batches = {};
    var oldAjax = $.fn.ajax;

    var queueBatch = function(url, options) {
        var match = false;
        var dfd = new $.Deferred();
        batches[url] = batches[url] || [];
        if(options.batchDebounce || $.ajaxSettings.batchDebounce) {

            if(!options.success && !options.error) {
                $.each(batches[url], function(index, batchedAjax) {
                    if($.param(batchedAjax.options) == $.param(options)) {
                        match = index;
                        return false;
                    }
                });
            }

            if(match === false) {
                batches[url].push({options:options, dfds:[dfd]});
            } else {
                batches[url][match].dfds.push(dfd);
            }

        } else {
            batches[url].push({options:options, dfds:[dfd]);
        }
        return dfd.promise();
    }

    var runBatches = function() {
        $.each(batches, function(url, batchedOptions) {
            $.each(batchedOptions, function(index, batchedAjax) {
                oldAjax.apply(null, url, batchedAjax.options).then(
                    function(data, textStatus, jqXHR) {
                        var args = arguments;
                        $.each(batchedAjax.dfds, function(index, dfd) {
                            dfd.resolve(args);
                        });
                    }, function(jqXHR, textStatus, errorThrown) {
                        var args = arguments;
                        $.each(batchedAjax.dfds, function(index, dfd) {
                            dfd.reject(args);
                        });
                    }
                )
            });
        });
        batches = {};
        batchRun = new Date.getTime();
    }

    setInterval(runBatches, $.ajaxSettings.batchInterval);

    $.fn.ajax = function(url, options) {
        if (options.batchable) {
            var xhr = queueBatch(url, options);
            if((new Date.getTime()) - batchRun >= options.batchInterval) {
                runBatches();
            }
            return xhr;
        }
        if (options.flushBatch) {
            runBatches();
        }
        return oldAjax.call(null, url, options);
    };
})(jQuery);

本文标签: javascriptBatching requests to minimize cell drainStack Overflow