批量请求以最小化细胞排出

时间:2013-09-20 05:06:59

标签: javascript jquery ajax asynchronous

这篇文章最近登上了HackerNews的顶部:http://highscalability.com/blog/2013/9/18/if-youre-programming-a-cell-phone-like-a-server-youre-doing.html#

其中声明:

  

手机收音机是手机上最大的电池电量之一。每次发送数据时,无论多小,收音机都会通电20-30秒。您做出的每一个决定都应该基于最小化无线电启动的次数。通过更改应用处理数据传输的方式,可以显着提高电池寿命。用户现在需要他们的数据,诀窍是平衡用户体验与传输数据和最小化电源使用。通过小心地将所有重复和间歇传输捆绑在一起然后积极地预取间歇传输的应用程序来实现平衡。

我想修改$.ajax以添加一个选项,例如“不需要立即完成 ,只需在启动另一个请求时执行此请求”。有什么好办法可以解决这个问题?

我从这开始:

(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);

我无法用文章的措辞来判断,我猜一个好的批次“间隔”是2-5分钟,所以我只用了5。

这是一个很好的实施吗?

  • 如何通过在方法中添加ajax选项,使其成为{batchable:true}方法的真正修改?我也没想出来。

  • setInterval是否也让手机一直保持清醒状态?这是件坏事吗?有没有更好的方法不这样做?

  • 这里还有其他可能导致电池耗电更快的事情吗?

  • 这种方法是否值得?在现代智能手机中,有很多事情一次发生,如果我的应用程序没有使用单元格,肯定还有其他应用程序。 Javascript无法检测单元格是否打开,所以为什么要打扰?值得打扰吗?

6 个答案:

答案 0 :(得分:7)

我在向$.ajax添加选项方面取得了一些进展,开始编辑问题,并意识到答案更好:

(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);

这实际上相当简单。我喜欢看到更好的答案。

答案 1 :(得分:5)

  
      
  • setInterval是否也一直让手机保持清醒状态?这是件坏事吗?有没有更好的方法不这样做?
  •   

从iPhone 4,iOS 6.1.0 Safari环境:

A写了一个带有倒数计时器的应用程序,它以一秒的间隔更新元素的文本。 DOM树具有中等复杂性。该应用程序是一个相对简单的计算器,没有做任何AJAX。然而,我总是怀疑那些每秒一次的回流正在扼杀我。我的电池肯定似乎耗尽相当快,每当我把它挂在桌子上,Safari在应用程序的网页上。

该应用只有两次超时。现在,我没有任何可量化的证明超时耗尽我的电池,但是每45分钟从这个重要的计算器中损失大约10%是有点令人不安的。 (谁知道,也许是背光。)

关于这一点:您可能希望构建一个测试应用程序,该应用程序按时间间隔执行AJAX,其他时间间隔等,并比较每个功能在类似条件下如何耗尽电池。获得受控制的环境可能会很棘手,但如果排水量存在足够大的差异,那么即使是“不完美”的测试条件也会产生足够明显的结果,以便您得出结论。


但是,我发现iOS 6.1.0 Safari处理超时的方式很有趣:

  • 如果关闭屏幕,超时不会运行回调。
  • 因此,长期超时会“错过他们的标记。”

如果我的应用程序的计时器显示正确的时间(即使在我关闭并重新打开屏幕后),那么我就无法轻松完成secondsLeft -= 1如果我关闭了屏幕,那么secondsLeft(相对于我的开始时间)就会“落后”,因而不正确。 (屏幕关闭时,setTimeout回调没有运行。)

解决方案是我必须在每个间隔重新计算timeLeft = fortyMinutes - (new Date().getTime() - startTime)

此外,我的应用程序中的计时器应该从绿色,石灰,黄色变为红色,因为它接近到期。因为在这一点上,我担心我的间隔码的效率,我怀疑在适当的时间“安排”我的颜色变化会更好(石灰:开始时间后20分钟,黄色:30分钟,红色:35)(这似乎比每次间隔的四重不等式检查更好,99%的时间都是徒劳的。)

但是,如果我安排了这样的换色,并且我的手机屏幕在目标时间关闭了,那么这种颜色变化永远不会发生。

解决方案是在每个时间间隔检查自上次1秒计时器更新以来经过的时间是否为“>= 2秒”。 (通过这种方式,应用程序可以知道我的手机屏幕是否已关闭;它能够意识到它何时“落后”。)此时,如果有必要,我会“强行”应用颜色更改和时间表下一个。

(不用说,我后来删除了换色器......)

所以,我相信这证实了我的主张

  

如果屏幕关闭,iOS 6.1.0 Safari不会执行setTimeout回调函数。

因此,在“安排”您的AJAX调用时请记住这一点,因为您可能也会受到此行为的影响。

并且,使用我的命题,我可以回答你的问题:

  • 至少在iOS上,我们知道setTimeout在屏幕关闭时会休眠。
  • 因此setTimeout不会让你的手机“做噩梦”(“保持清醒”)。

  
      
  • 这种方法是否值得?在现代智能手机中,有很多事情一次发生,如果我的应用程序没有使用单元格,肯定还有其他应用程序。 Javascript无法检测到单元格是否打开,为什么还要麻烦?值得打扰吗?
  •   

如果你能让这个实现正常工作,那么它似乎 是值得的。

您的每个AJAX请求都会产生延迟,这会降低您的应用程序的速度某些度。 (毕竟,延迟是页面加载时间的祸根。)因此,通过“捆绑”请求,您肯定会获得某些增益。扩展$ .ajax以便您可以“批量”请求肯定会有一些优点。

答案 2 :(得分:3)

您链接的文章明确侧重于优化功耗 for apps (是的,天气小工具示例令人恐惧)。根据定义,主动使用浏览器是前台任务;加上ApplicationCache之类的内容已经可用于减少对网络请求的需求。然后,您可以根据需要programmatically update the cache避免DIY。

持怀疑态度的注意事项:如果您使用jQuery作为HTML5应用程序的一部分(可能包含在Sencha或类似内容中),那么移动应用程序框架可能与代码本身有更多关系。我没有任何证据,但该死的听起来是对的:)

  
      
  • 如何才能真正修改ajax方法   在方法中添加{batchable:true}选项?我还没有   想出来也是。
  •   

一种完全有效的方法,但对我而言,这听起来像duck punching出错了。我不会。即使你正确地将batchable默认为false,我个人更愿意使用一个外观(甚至可能在它自己的命名空间中?)

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);
}
  
      
  • setInterval是否也会让手机一直保持清醒状态?那是一个   坏事要做?有没有更好的方法不这样做?
  •   

setIntervalsetTimeout的原生实现非常相似;当网站处于网上银行不活动提示的后台时,可以想到后者没有解雇;当页面不在前台时,其执行基本上停止。如果API可用于此类“延期”(文章提及某些相关的iOS7功能),那么它可能是一种更好的方法,否则我认为没有理由避免setInterval

  
      
  • 这里还有其他可能导致电池耗尽的事情   快?
  •   

我推测任何重负荷都会(从计算pi到漂亮的3d过渡)。但这听起来像是对我的过早优化,并让我想起一个电池省电模式的电子阅读器完全关闭了液晶显示屏:)

  
      
  • 这种方法是否值得?有很多东西   如果我的应用程序没有使用,那么在现代智能手机中立即进行   细胞,肯定是其他一些应用程序。 Javascript无法检测到   细胞是否开启,为什么要打扰呢?值得打扰吗?
  •   

文章指出天气应用程序不合理地贪婪,这会让我感到担忧。这似乎是一种发展监督,但最重要的是,在获取数据的过程中,这比实际需要的更多。在一个理想的世界中,这应该在操作系统级别上得到很好的处理,否则你最终会得到一系列相互竞争的解决方法。国际海事组织:在高度可扩展性发布另一篇文章告诉你:)之前不要打扰。

答案 3 :(得分:1)

这是我的版本:

(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);

我认为这个版本有以下主要优点:

  • 如果存在非batramble ajax调用,则该连接用于发送所有批次。这会重置计时器。
  • 返回直接ajax调用的预期返回值
  • 通过调用不含参数的$ .fn.ajax()来触发批量的直接处理

答案 4 :(得分:1)

就黑客攻击$.ajax方法而言,我会:

  • 尝试同时保留Promise
  • 提供的$.ajax机制
  • 利用其中一个全局ajax事件来触发ajax调用,
  • 可能会添加一个计时器,以便在没有“立即”$.ajax来电的情况下调用批次,
  • 为此函数指定一个新名称(在我的代码中:$.batchAjax)并保留原始$.ajax

这是我的去处:

(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));

在这个例子中,5秒的硬编码延迟违背了“如果通话之间少于20秒,它会耗尽电池”的目的。你可以放一个更大的(5分钟?),或者完全删除它 - 这完全取决于你的应用程序。

fiddle


关于一般性问题“如何编写不会在5分钟内烧毁手机电池的网络应用程序?” :它将需要不止一个魔箭来处理那个。这是您必须采取的一整套设计决策,这取决于您的应用程序。

您必须在一次性加载尽可能多的数据之间进行仲裁(并且可能发送不会被使用的数据)与获取您需要的内容(并且可能发送许多小的个别请求)之间进行仲裁。

要考虑的一些参数是:

  • 数据量(您不想耗尽客户数据计划......),
  • 服务器负载,
  • 可以缓存多少,
  • “最新”的重要性(聊天应用程序延迟5分钟无效),
  • 客户端更新频率(网络游戏可能需要客户端进行大量更新,新闻应用可能更少......)。

一个相当普遍的建议:您可以添加“实时更新”复选框,并存储其状态客户端。取消选中后,客户端应按“刷新”按钮下载新数据。

答案 5 :(得分:0)

这是我的开始,它有点源于@Joe Frambach发布的内容,但我希望增加以下内容:

  1. 保留jXHR和错误/成功回调(如果已提供)
  2. 在触发为每次通话提供的回调或jqXHR时,对相同的请求(通过网址和选项匹配)进行辩解
  3. 使用AjaxSettings使配置更容易
  4. 不要让每个非批量的ajax冲洗批次,那些应该是单独的进程IMO,但是因此提供了强制批量刷新的选项。
  5. 无论哪种方式,这个吸盘很可能最好作为一个单独的插件完成而不是覆盖并影响默认的.ajax函数...享受:

    (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);