2013-10-16 65 views
18

相關話題:requestAnimationFrame garbage collection鉻requestAnimationFrame發出

我一直朝我正在創建觸摸設備的一個部件流暢的動畫,並且我發現幫助我的工具之一本已經在Chrome內存時間線屏幕。

這有助於我評估我在rAF循環中的內存消耗情況,但我對此時在Chrome 30中觀察到的一些行爲方面感到困擾。

初次進入我的頁面,其中有英國皇家空軍循環運行,我看到這一點。 enter image description here

看起來不錯。如果我已經完成了我的工作並在我的內部循環中消除了對象分配,那麼不應該有鋸齒。這是與鏈接主題一致的行爲,也就是說,無論何時使用rAF,Chrome都有內置泄漏。 (哎呀!)

當我開始在頁面做各種事情就變得更有趣。

enter image description here

我真的不這樣做有什麼不同,只是暫時增加了兩個元素當中去CSS3 3D轉換申請了幾幀的款式,然後我停止與他們互動。

我們在這裏看到的是Chrome報告突然間每一次射擊(16ms)都會導致Animation Frame Fired x 3

這個重複和它的速率單調增加,直到頁面刷新。

您已經可以在第二個screencap中看到在從Animation Frame FiredAnimation Frame Fired x 3的初始跳躍之後鋸齒坡度急劇增加。

過一會兒它已躍升至x 21

enter image description here

這樣看來,我的代碼正在運行一大堆額外的時間,但所有的額外多輪的只是廢熱,放棄計算。

當我走的是第三擷取畫面,我的MacBook明顯升溫相當嚴重。不久之後,在我能夠清理時間線到最後一點(大約8分鐘)之前,看看x號碼增加了多少,檢查員窗口變得完全沒有響應,並且我被提示說我的頁面沒有響應,並且不得不被終止。

下面是在頁面上運行的代碼的全部:

// ============================================================================ 
// Copyright (c) 2013 Steven Lu 

// Permission is hereby granted, free of charge, to any person obtaining a 
// copy of this software and associated documentation files (the "Software"), 
// to deal in the Software without restriction, including without limitation 
// the rights to use, copy, modify, merge, publish, distribute, sublicense, 
// and/or sell copies of the Software, and to permit persons to whom the 
// Software is furnished to do so, subject to the following conditions: 

// The above copyright notice and this permission notice shall be included in 
// all copies or substantial portions of the Software. 

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 
// IN THE SOFTWARE. 
// ============================================================================ 

// This is meant to be a true velocity verlet integrator, which means sending 
// in for the force and torque a function (not a value). If the forces provided 
// are evaluated at the current time step then I think we are left with plain 
// old Euler integration. This is a 3 DOF integrator that is meant for use 
// with 2D rigid bodies, but it should be equally useful for modeling 3d point 
// dynamics. 

// this attempts to minimize memory waste by operating on state in-place. 

function vel_verlet_3(state, acc, dt) { 
    var x = state[0], 
     y = state[1], 
     z = state[2], 
     vx = state[3], 
     vy = state[4], 
     vz = state[5], 
     ax = state[6], 
     ay = state[7], 
     az = state[8], 
     x1 = x + vx * dt + 0.5 * ax * dt * dt, 
     y1 = y + vy * dt + 0.5 * ay * dt * dt, 
     z1 = z + vz * dt + 0.5 * az * dt * dt, // eqn 1 
     a1 = acc(x1, y1, z1), 
     ax1 = a1[0], 
     ay1 = a1[1], 
     az1 = a1[2]; 
    state[0] = x1; 
    state[1] = y1; 
    state[2] = z1; 
    state[3] = vx + 0.5 * (ax + ax1) * dt, 
    state[4] = vy + 0.5 * (ay + ay1) * dt, 
    state[5] = vz + 0.5 * (az + az1) * dt; // eqn 2 
    state[6] = ax1; 
    state[7] = ay1; 
    state[8] = az1; 
} 

// velocity indepedent acc --- shit this is gonna need to change soon 
var acc = function(x, y, z) { 
    return [0,0,0]; 
}; 
$("#lock").click(function() { 
    var values = [Number($('#ax').val()), Number($('#ay').val()), Number($('#az').val())]; 
    acc = function() { 
    return values; 
    }; 
}); 

// Obtain the sin and cos from an angle. 
// Allocate nothing. 
function getRotation(angle, cs) { 
    cs[0] = Math.cos(angle); 
    cs[1] = Math.sin(angle); 
} 

// Provide the localpoint as [x,y]. 
// Allocate nothing. 
function global(bodystate, localpoint, returnpoint) { 
    getRotation(bodystate[2], returnpoint); 
    // now returnpoint contains cosine+sine of angle. 
    var px = bodystate[0], py = bodystate[1]; 
    var x = localpoint[0], y = localpoint[1]; 
    // console.log('global():', cs, [px, py], localpoint, 'with', [x,y]); 
    // [ c -s px ] [x] 
    // [ s c py ] * [y] 
    //    [1] 
    var c = returnpoint[0]; 
    var s = returnpoint[1]; 
    returnpoint[0] = c * x - s * y + px; 
    returnpoint[1] = s * x + c * y + py; 
} 

function local(bodystate, globalpoint, returnpoint) { 
    getRotation(bodystate[2], returnpoint); 
    // now returnpoint contains cosine+sine of angle 
    var px = bodystate[0], py = bodystate[1]; 
    var x = globalpoint[0], y = globalpoint[1]; 
    // console.log('local():', cs, [px, py], globalpoint, 'with', [x,y]); 
    // [ c s ] [x - px] 
    // [ -s c ] * [y - py] 
    var xx = x - px, yy = y - py; 
    var c = returnpoint[0], s = returnpoint[1]; 
    returnpoint[0] = c * xx + s * yy; 
    returnpoint[1] = -s * xx + c * yy; 
} 

var cumulativeOffset = function(element) { 
    var top = 0, left = 0; 
    do { 
    top += element.offsetTop || 0; 
    left += element.offsetLeft || 0; 
    element = element.offsetParent; 
    } while (element); 
    return { 
    top: top, 
    left: left 
    }; 
}; 

// helper to create/assign position debugger (handles a single point) 
// offset here is a boundingclientrect offset and needs window.scrollXY correction 
var hasDPOffsetRun = false; 
var dpoff = false; 
function debugPoint(position, id, color, offset) { 
    if (offset) { 
    position[0] += offset.left; 
    position[1] += offset.top; 
    } 
    // if (position[0] >= 0) { console.log('debugPoint:', id, color, position); } 
    var element = $('#point' + id); 
    if (!element.length) { 
    element = $('<div></div>') 
    .attr('id', 'point' + id) 
    .css({ 
      pointerEvents: 'none', 
      position: 'absolute', 
      backgroundColor: color, 
      border: '#fff 1px solid', 
      top: -2, 
      left: -2, 
      width: 2, 
      height: 2, 
      borderRadius: 300, 
      boxShadow: '0 0 6px 0 ' + color 
     }); 
    $('body').append(
     $('<div></div>') 
     .addClass('debugpointcontainer') 
     .css({ 
      position: 'absolute', 
      top: 0, 
      left: 0 
     }) 
     .append(element) 
    ); 
    if (!hasDPOffsetRun) { 
     // determine the offset of the body-appended absolute element. body's margin 
     // is the primary offender that tends to throw a wrench into our shit. 
     var dpoffset = $('.debugpointcontainer')[0].getBoundingClientRect(); 
     dpoff = [dpoffset.left + window.scrollX, dpoffset.top + window.scrollY]; 
     hasDPOffsetRun = true; 
    } 
    } 
    if (dpoff) { 
    position[0] -= dpoff[0]; 
    position[1] -= dpoff[1]; 
    } 
    // set position 
    element[0].style.webkitTransform = 'translate3d(' + position[0] + 'px,' + position[1] + 'px,0)'; 
} 

var elements_tracked = []; 

/* 
var globaleventhandler = function(event) { 
    var t = event.target; 
    if (false) { // t is a child of a tracked element... 

    } 
}; 

// when the library is loaded the global event handler for GRAB is not 
// installed. It is lazily installed when GRAB_global is first called, and so 
// if you only ever call GRAB then the document does not get any handlers 
// attached to it. This will remain unimplemented as it's not clear what the 
// semantics for defining behavior are. It's much more straightforward to use 
// the direct API 
function GRAB_global(element, custom_behavior) { 
    // this is the entry point that will initialize a grabbable element all state 
    // for the element will be accessible through its __GRAB__ element through 
    // the DOM, and the DOM is never accessed (other than through initial 
    // assignment) by the code. 

    // event handlers are attached to the document, so use GRAB_direct if your 
    // webpage relies on preventing event bubbling. 
    if (elements_tracked.indexOf(element) !== -1) { 
    console.log('You tried to call GRAB() on an element more than once.', 
       element, 'existing elements:', elements_tracked); 
    } 
    elements_tracked.push(element); 
    if (elements_tracked.length === 1) { // this is the initial call 
    document.addEventListener('touchstart', globaleventhandler, true); 
    document.addEventListener('mousedown', globaleventhandler, true); 
    } 
} 

// cleanup function cleans everything up, returning behavior to normal. 
// may provide a boolean true argument to indicate that you want the CSS 3D 
// transform value to be cleared 
function GRAB_global_remove(cleartransform) { 
    document.removeEventListener('touchstart', globaleventhandler, true); 
    document.removeEventListener('mousedown', globaleventhandler, true); 
} 

*/ 

var mousedownelement = false; 
var stop = false; 
// there is only one mouse, and the only time when we need to handle release 
// of pointer is when the one mouse is let go somewhere far away. 
function GRAB(element, onfinish, center_of_mass) { 
    // This version directly assigns the event handlers to the element 
    // it is less efficient but more "portable" and self-contained, and also 
    // potentially more friendly by using a regular event handler rather than 
    // a capture event handler, so that you can customize the grabbing behavior 
    // better and also more easily define it per element 
    var offset = center_of_mass; 
    var pageOffset = cumulativeOffset(element); 
    var bcrOffset = element.getBoundingClientRect(); 
    bcrOffset = { 
    left: bcrOffset.left + window.scrollX, 
    right: bcrOffset.right + window.scrollX, 
    top: bcrOffset.top + window.scrollY, 
    bottom: bcrOffset.bottom + window.scrollY 
    }; 
    if (!offset) { 
    offset = [element.offsetWidth/2, element.offsetHeight/2]; 
    } 
    var model = { 
    state: [0, 0, 0, 0, 0, 0, 0, 0, 0], 
    offset: offset, 
    pageoffset: bcrOffset // remember, these values are pre-window.scroll[XY]-corrected 
    }; 
    element.__GRAB__ = model; 
    var eventhandlertouchstart = function(event) { 
    // set 
    var et0 = event.touches[0]; 
    model.anchor = [0,0]; 
    local(model.state, [et0.pageX - bcrOffset.left - offset[0], et0.pageY - bcrOffset.top - offset[1]], model.anchor); 
    debugPoint([et0.pageX, et0.pageY], 1, 'red'); 
    event.preventDefault(); 
    requestAnimationFrame(step); 
    }; 
    var eventhandlermousedown = function(event) { 
    console.log('todo: reject right clicks'); 
    // console.log('a', document.body.scrollLeft); 
    // set 
    // model.anchor = [event.offsetX - offset[0], event.offsetY - offset[1]]; 
    model.anchor = [0,0]; 
    var globalwithoffset = [event.pageX - bcrOffset.left - offset[0], event.pageY - bcrOffset.top - offset[1]]; 
    local(model.state, globalwithoffset, model.anchor); 
    debugPoint([event.pageX, event.pageY], 1, 'red'); 
    mousedownelement = element; 
    requestAnimationFrame(step); 
    }; 
    var eventhandlertouchend = function(event) { 
    // clear 
    model.anchor = false; 
    requestAnimationFrame(step); 
    }; 
    element.addEventListener('touchstart', eventhandlertouchstart, false); 
    element.addEventListener('mousedown', eventhandlermousedown, false); 
    element.addEventListener('touchend', eventhandlertouchend, false); 
    elements_tracked.push(element); 
    // assign some favorable properties to grabbable element. 
    element.style.webkitTouchCallout = 'none'; 
    element.style.webkitUserSelect = 'none'; 
    // TODO: figure out the proper values for these 
    element.style.MozUserSelect = 'none'; 
    element.style.msUserSelect = 'none'; 
    element.style.MsUserSelect = 'none'; 
} 
document.addEventListener('mouseup', function() { 
    if (mousedownelement) { 
    mousedownelement.__GRAB__.anchor = false; 
    mousedownelement = false; 
    requestAnimationFrame(step); 
    } 
}, false); 

function GRAB_remove(element, cleartransform) {} 
// unimpld 
function GRAB_remove_all(cleartransform) {} 

GRAB($('#content2')[0]); 

(function() { 
    var requestAnimationFrame = window.mozRequestAnimationFrame || 
     window.webkitRequestAnimationFrame || 
     window.msRequestAnimationFrame || 
     window.requestAnimationFrame; 
    window.requestAnimationFrame = requestAnimationFrame; 
})(); 

var now = function() { return window.performance ? performance.now() : Date.now(); }; 
var lasttime = 0; 
var abs = Math.abs; 
var dt = 0; 
var scratch0 = [0,0]; 
var scratch1 = [0,0]; // memory pool 
var step = function(time) { 
    dt = (time - lasttime) * 0.001; 
    if (time < 1e12) { 
    // highres timer 
    } else { 
    // ms since unix epoch 
    if (dt > 1e9) { 
     dt = 0; 
    } 
    } 
    // console.log('dt: ' + dt); 
    lasttime = time; 
    var foundnotstopped = false; 
    for (var i = 0; i < elements_tracked.length; ++i) { 
    var e = elements_tracked[i]; 
    var data = e.__GRAB__; 
    if (data.anchor) { 
     global(data.state, data.anchor, scratch0); 
     scratch1[0] = scratch0[0] + data.offset[0]; 
     scratch1[1] = scratch0[1] + data.offset[1]; 
     //console.log("output of global", point); 
     debugPoint(scratch1, 
       0, 'blue', data.pageoffset); 
    } else { 
     scratch1[0] = -1000; 
     scratch1[1] = -1000; 
     debugPoint(scratch1, 0, 'blue'); 
    } 
    // timestep is dynamic and based on reported time. clamped to 100ms. 
    if (dt > 0.3) { 
     //console.log('clamped from ' + dt + ' @' + now()); 
     dt = 0.3; 
    } 
    vel_verlet_3(data.state, acc, dt); 
    e.style.webkitTransform = 'translate3d(' + data.state[0] + 'px,' + data.state[1] + 'px,0)' + 
     'rotateZ(' + data.state[2] + 'rad)'; 
    } 
    requestAnimationFrame(step); 
}; 

requestAnimationFrame(step); 

爲了完整這裏是測試頁面HTML:

<!DOCTYPE html> 
<html lang="en"> 
<head> 
    <meta charset="utf-8" /> 
    <meta http-equiv="cache-control" content="max-age=0" /> 
    <meta http-equiv="cache-control" content="no-cache" /> 
    <meta http-equiv="expires" content="0" /> 
    <meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" /> 
    <meta http-equiv="pragma" content="no-cache" /> 
    <title>symplectic integrator test page</title> 
    <script src="zepto.js"></script> 
    <script src="d3.v3.js"></script> 
    <style type='text/css'> 
     body { 
      position: relative; 
      margin: 80px; 
     } 
     #content { 
      width: 800px; 
      height: 40px; 
      display: inline-block; 
      background: lightgreen; 
      padding: 20px; 
      margin: 30px; 
      border: green dashed 1px; 
     } 
     #content2 { 
      top: 200px; 
      width: 600px; 
      height: 200px; 
      display: inline-block; 
      background: lightblue; 
      padding: 20px; 
      margin: 30px; 
      border: blue dashed 1px; 
     } 
    </style> 
</head> 
<body> 
    <div id='scrolling-placeholder' style='background-color: #eee; height: 1000px;'></div> 
    <label>dt:<input id='dt' type='number' step='0.001' value='0.016666666' /></label> 
    <label>ax:<input id='ax' type='number' step='0.25' value='0' /></label> 
    <label>ay:<input id='ay' type='number' step='0.25' value='0' /></label> 
    <label>t:<input id='az' type='number' step='0.01' value='0' /></label> 
    <button id='lock'>Set</button> 
    <button id='zerof' onclick='$("#ax,#ay,#az").val(0);'>Zero forces</button> 
    <button id='zerov'>Zero velocities</button> 
    <div> 
     <span id='content'>content</span> 
     <span id='content2'>content2</span> 
    </div> 
    <div id='debuglog'></div> 
    <script src="rb2.js"></script> 
</body> 
</html> 

這應該滿足任何「向我們展示的代號爲」請求。

現在我不會在這上面打賭,但我非常確定我至少做了一個正確使用rAF的好工作。我沒有濫用任何東西,並且我已經通過這一點改進了代碼,使其對Javascript內存分配非常輕鬆。

所以,真的,有絕對沒有理由鉻藉此並試圖騎我的筆記本電腦進入軌道像火箭一樣。沒有理由。

Safari總體上似乎更好地處理它(它並不會最終死掉),而且我會注意到iOS通常能夠保持200x600px的div平移和60fps的旋轉。

但是,我承認,我沒有看到Chrome真的死了,除非我已經記錄了內存時間表。

在這一點上我只是在撓頭。這可能只是一些意想不到的,無法預料的與此特定開發工具功能的互動(據我所知,這是唯一的一種開發工具功能)。

於是我嘗試了一些新的東西,以至少幫助調查該問題與內存時間表額外的回調燒:

增加這些線路。

window.rafbuf = []; 
var step = function(time) { 
    window.rafbuf.push(time); 

這基本上註銷我的RAF程序(該step()函數)被調用的所有時間。

當它正常運行時,它大約每16.7毫秒記下一次時間。

我得到這個:

enter image description here

這清楚地表明它是重新運行step()與同一時間輸入參數至少22倍,就像時間表的想告訴我。

所以我敢說,互聯網,告訴我,這是打算的行爲。 :)

+3

+ 1,但我覺得它會爭取從人們更多的興趣,如果問題的長度短一點 –

+0

@Onaseriousnote我正在建議 –

+0

這可能會感興趣:https://groups.google.com/forum/#!topic/google-chrome-developer-tools/S_mKrF42a4Y – jedierikb

回答

2

我創建動畫的http://www.testufo.com並在http://www.testufo.com/animation-time-graph

的支持requestAnimationFrame()的自動同步到計算機顯示器的刷新率(即使比其他Web瀏覽器的列表中requestAnimationFrame()一致性檢查60Hz)列在http://www.testufo.com/browser.html ......這意味着在75Hz監視器上,如果網頁當前處於前景中並且CPU /圖形性能允許,則requestAnimationFrame()現在在支持的瀏覽器上每秒調用75次。

的Chrome 29和31工作正常,一樣的Chrome 30的新版本幸運的是,鉻33金絲雀似乎更充分,據我所知固定我看到的問題。它更順利地運行動畫,而不會對requestAnimationFrame()進行不必要的調用。

此外,我也注意到電源管理(CPU放緩/節流也節省電池電量)可以對requestAnimationFrame()的回調率造成嚴重破壞......它表現爲奇怪的尖峯向上/向下幀渲染時間(http://www.testufo.com/#test=animation-time-graph&measure=rendering

+0

我沒有看到這是如何涉及完全不正確的行爲運行rAF回調許多額外次刷新時間。無論如何,似乎還有很多工作要做,比如setTimeout和rAF一樣強大。 –

3

我認爲你有一個問題,因爲你每mousedownmouseup事件調用requestAnimationFrame(step);。由於您的step()函數(因爲它應該)調用requestAnimationFrame(step);你基本上開始每個mousedownmouseup事件新的「動畫循環」,因爲你永遠不會阻止他們,他們積累。

我可以看到,你也在代碼的最後開始「動畫循環」。如果你想在鼠標事件中立即重畫,你應該畫出step()函數,並直接從鼠標事件處理函數調用。

Samething這樣的:

function redraw() { 
    // drawing logic 
} 
function onmousedown() { 
    // ... 
    redraw() 
} 
function onmouseup() { 
    // ... 
    redraw() 
} 

function step() { 
    redraw(); 
    requestAnimationFrame(step); 
} 
requestAnimationFrame(step); 
+0

是的,您的僞代碼示例是管理rAF渲染循環的正確方法的一個非常好的升級,並且實際上通過鼠標單擊來渲染額外的「線程」是錯誤的,並且會導致難以解開的情況。 ..我需要重新審視這些代碼,並確定這是否是我遇到的問題中的一個重要因素。 –