2013-10-16 13 views
18

関連トピックを発行します。requestAnimationFrame garbage collectionクロームrequestAnimationFrameのは

私はタッチデバイス用に構築していたウィジェットにスムーズなアニメーションに向かって取り組んできた、と私はこれで私を助けるためにあるツールの一つは、クロムとなっていますメモリタイムライン画面。

私はrAFループでのメモリ消費量を評価するのに少し助けになりましたが、私はこの時点でChrome 30で観察している動作のいくつかの側面に悩まされています。

rAFループが実行されているページを最初に入力すると、これが表示されます。 enter image description here

大丈夫です。私の仕事をして、内側のループでオブジェクトの割り当てを取り除いた場合は、鋸歯があってはいけません。これはリンクされたトピックと一致する動作です。つまり、rAFを使用するたびにChromeにはリークが組み込まれています。 (yikes!)

私はページでいろいろなことをやってみると面白いです。

enter image description here

私は本当にただ、一時的に数フレームのために適用されるスタイルを変換した後、私は彼らとの対話を停止CSS3 3Dを取得し、さらに2つの要素を追加し、別の何かをやっていませんよ。

私たちがここに見るのは、突発的なrAFの発射(16ms)のすべてがAnimation Frame Fired x 3になると報告しているChromeです。

この繰り返しとそのレートは、ページが更新されるまで単調に増加します。

Animation Frame FiredからAnimation Frame Fired x 3への最初のジャンプ後に鋸歯状の勾配が大幅に増加していることを、2番目のスクリーンキャップで確認できます。

しばらく後にはx 21にジャンプした:

enter image description here

私のコードは、余分な時間の全体の束を実行されているが、余分な複数の実行のすべてがちょうど熱を無駄にしているように思われます、捨てられた計算。

私が3番目のスクリーンキャプチャを取っている間、私の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のメモリ割り当てに非常に軽いコードを洗練しました。

実際にはが絶対にです。これはChromeがこれを受け取り、ノートパソコンをロケットのように軌道に乗せようとする理由はありません。理由はありません。

一般的にサファリはそれをよりよく処理しているようです(最終的には死ぬことはありません)。また、iOSは一般に60fpsで翻訳と回転を200x600px divに維持できます。

しかし、メモリのタイムラインを記録しない限り、Chromeは実際にこのように死んでいるとは思わなかったことは認めています。

私はちょうどこの時点で私の頭を傷つける。おそらく、この特定の開発ツールの機能(私の知る限り、この種の唯一のもの)と意図しない予期しないやりとりがあります。

だから、私は、少なくともメモリのタイムラインエクストラコールバック・焼成して、この問題を調査する助けに何か新しいものを試してみました:

は、これらの行を追加しました。

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

これは基本的に(step()機能)私のRAFルーチンが呼び出されるすべての回をログアウトします。

正常に実行されているときは、およそ16.7msごとに書き込みが行われます。

私はこれだ:はっきりちょうどタイムラインのは、私に教えしようとしているような、それは再実行するのと同じ時間の入力パラメータとstep()少なくとも22倍を示していること

enter image description here

を。

だから私はインターネットを敢えてこれが意図された動作だと教えてくれる。 :)

+3

+ 1が、私は質問の長さは、私は提案を取って少し短く –

+0

@Onaseriousnote –

+0

これは興味があるかもしれませんであれば、それは人々から多くの関心を集めるだろうと感じ:https://groups.google.com/forum/#!topic/google-chrome-developer-tools/S_mKrF42a4Y – jedierikb

答えて

2

私は、コンピュータのモニタのリフレッシュレートにhttp://www.testufo.comともrequestAnimationFrameの(の自動同期をサポートするWebブラウザのhttp://www.testufo.com/animation-time-graph

リストでrequestAnimationFrameの()一貫性チェッカー)のためのアニメーションを作成した(でも、他の場合よりもWebページが現在フォアグラウンドにあり、CPU /グラフィックスのパフォーマンスが許可されている場合、サポートされているブラウザでrequestAnimationFrame()が75回/秒と呼ばれるようになりました。

クローム29と31作品罰金、幸いなことに、クロム33カナリアは、より完全に私の知る限りでは、私が見る問題を修正しているようです。 requestAnimationFrame()を不必要に呼び出すことなく、よりスムーズにアニメーションを実行します。

また、パワーマネージメント(CPUスローダウン/スロットルもバッテリー消費を抑える)がrequestAnimationFrame()のコールバック率に大きな影響を与えることに気付きました。それは、私はあなたがすべてのmousedownmouseupイベントにrequestAnimationFrame(step);を呼び出すので、あなたが問題を持っていると思うタイムズ(http://www.testufo.com/#test=animation-time-graph&measure=rendering

+0

これは、rAFコールバックを多数実行した場合の動作が完全に間違っていることを示していませんリフレッシュ・ティック当たりの回数。とにかく、rAFがsetTimeoutと同じくらい防弾になるまでには、まだ多くの作業が残っているようです。 –

3

レンダリングフレームに上向き/下向きの奇妙なスパイクとして現れます。 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レンダリングループを管理するための適切な方法の非常に優れた点です。マウスクリックによるレンダリングの余分な「スレッド」が間違っていて、解くのが難しい状況に繋がります。私は、このコードを再訪し、これが私が経験している問題の重要な要素であるかどうかを判断する必要があります。 –