通过Promise的AJAX调用缓存使浏览器使用CPU(并泄漏内存)



UPDATE:经过长时间的jslint会话后,问题神秘地消失了,所以我不得不得出结论,这是我代码中的某个愚蠢错误,通过删除一些看似无害的拼写错误来修复。

初始问题

我有一个页面,它通过jQuery.post调用加载大量(随机可变)AJAX对象。

所以我会打很多电话,比如

/entry/get/orange
/entry/get/red
/entry/get/yellow
/entry/get/red      <--- duplicate!
/entry/get/red      <--- again!
/entry/get/yellow
/entry/get/red      <--- dude, what's wrong with you?
...

当以这种方式运行时,除了效率非常低和不必要地访问服务器之外,一切都正常工作。

解决方案尝试

所以我通过Promise切换到缓存,就像这样:

// I have a global colorCache variable
function retrieve(color, callback) {
    console.log("retrieving " + color);
    var found = -1;
    // Each entry in colorCache is an object with some other values
    for (var i in colorCache) {
        if (colorCache[i].color === color) {
             found = i;
             break;
        }
    }
    if (found < 0) {
        console.log("effecting call for " + color);
        var url  = "blah/blah/blah/" + color;
        var ppar = {
            foo: "bar",
            answer: 42
        };
        /* push returns the new length of colorCache */
        found = colorCache.push({ 
            color   : color,
            promise : $.post(url, ppar),
            ts      : new Date().getTime()
        }) - 1;
    } else {
        console.log("cache hit for " + color);
    }
    // now found is valid. Return the Promise too, for appending.
    return colorCache[found].success(callback);
}

这样,如果相关的.post已经发出,则使用相同的数据再次调用回调,无论调用本身是否已经终止或仍在进行中。

再说一遍,一切都正常显然

现在我有另一个问题

有了这个新版本,

  • 页面加载
  • 所有对象都已加载(比以前快得多,正如预期的那样)
  • 页面停止,一切静止。。。没有动画,没有时钟,什么都没有
  • 。。。但Process Explorer现在显示Firefox不断消耗26-30%的CPU。它将继续这样做,直到并且除非我关闭加载页面的选项卡。此外,Firefox的内存消耗稳步上升。我看到它在不到一个小时的时间里从800M上升到1.7GB,这不是完全有规律的,但有明显的上升趋势

这里可能有什么问题?

重要提示:Stack Overflow上有几个"浏览器泄漏"问答,但它们都涉及重复的jQuery调用,这些调用以某种方式分配对象或迭代。在这里,一旦我运行了大约50个jQuery调用,就没有其他事情发生了

我(到目前为止)尝试了什么

  • 我已经检查了Firebug:没有控制台警告或错误,没有指示日志(我调用的所有函数都有一个),没有网络活动(从Apache日志服务器端进行了双重检查)
  • 我在记忆中看过Firefox的进程;主线程正在运行,但我不擅长解释Process Explorer的线程信息——它显示了一些关于"forInIterator"的东西,告诉我的很少
  • 除了CPU之外,似乎没有任何活动正在进行(网络、Windows注册表读取/写入)
  • 我在谷歌上搜索了很多,发现了一些可能相关的东西(但可能不是)我还没有尝试过这个建议,但即使我看不出它有什么帮助,我也会很快尝试,而且无论如何,即使类似的可能已经在jQuery中出现,我也不会使用when.js,因为:一个潜在的解决方法是确保你的promise回调和errbacks返回了一些东西(而不是递延的美元)
  • 按照Jaromanda X的建议,用Promise.resolve封装.post调用,并在函数内移动缓存对象,以避免污染全局范围(这始终是一种很好的做法)
  • 直接返回Promise对象,并通过附加一个external.then调用回调
  • 经过铬测试。相同的结果(在某些方面令人放心),只是CPU负载恒定在17%
  • 关闭选项卡会使CPU负载消失

TL;DR问题根本与缓存无关。缓存,或者更确切地说:缓存允许的更高的调用周转率,似乎会在jQuery/bootstrapSwitch关系中的其他地方引发误解。也有可能是我使用bootstrapSwitch不正确。


调试和发现

由于没有运行XHR请求,CPU问题一定是由代码显式循环或隐式循环(例如setInterval)引起的。

检测浏览器定时器功能

window.timers = {}; 
originalSetTimeout = window.setTimeout;
originalSetInterval = window.setInterval;
window.setTimeout = function(fu, t) {
    var id = originalSetTimeout(function() {
        console.log(id+" has timed out");
        delete window.timers[id]; // do not track popped timers 
        fu();
    }, t);
    window.timers[id] = {id:id, type: 'timeout', setAt: new Date(),  timeout: t };
    console.log(id+" has been set to pop in "+t+"ms");
}
window.setInterval = function(fu, t) {
    var id = originalSetInterval(function() {
        window.timers[id].fired++;
        fu();
    }, t);
    window.timers[id] = {id:id, type: 'interval', setAt: new Date(),  timeout: t, fired: 0 };
    console.log(id+" has been set to pop every "+t+"ms");
}

显示有几个计时器连续运行

调查计时器

代码是"严格的",但一旦知道了特定的计时器,我就可以在setInterval中抛出一个Exception,并能够检查调用堆栈。

因此,罪魁祸首是这个代码(删除它会使问题消失):

    .bootstrapSwitch({
        onSwitchChange: function(e, state) {
            ...
        }
    })

结论

问题的原因是我在生成的页面中有很多复选框:它们用bootstrapSwitch进行了美化,但它们一开始是隐藏的。在这种情况下,bootstrapSwitch启动一个计时器,等待复选框在丰富其DOM之前被呈现。显然,这是(其中一个?)这样做的功能:

 return initInterval = window.setInterval((function(_this) {
      return function() {
        if (_this.$wrapper.is(":visible")) {
          init();
          return window.clearInterval(initInterval);
        }
      };
    })(this), 50);

通常情况下,这些功能不会显著地加载CPU。但由于不清楚的原因,短时间内大量(40+)的AJAX请求,其中一些请求调用bootstrapSwitch,使上述定时器产生了更高的负载。

强制复选框开关一个接一个地显示,然后立即隐藏它们,也有一定的效果:保留了两个计时器,CPU负载低于5%,但所有其他计时器都正确终止。不过,复选框闪烁很尴尬。

最后,我编写了一个定时器函数,它将延迟bootstrapSwitch,直到相关的复选框确实可见为止。一旦它们被调用,bootstrapSwitch就会被调用,但由于它们是可见的,因此不会创建额外的计时器。这样,无论定时器内部发生什么,代码都不会被触发。

最新更新