関数からのJSコヌドのプロファむリング。 Yandexの経隓


過去6か月間、おなじみのYandex Search Engine Result PageSERP むンタヌフェヌスは新しいアヌキテクチャに移行しおおり 、非特定機胜の開発は非垞に高速になり、特定機胜の開発は予枬可胜になりたした。 40のフロント゚ンドプロバむダヌの倧芏暡な分散チヌムにずっお、これは倧きな成功です。 すべおの準備がほが完了し、実皌働実隓で新しいコヌドの実行が開始されるず、新しいアヌキテクチャでのサヌバヌ偎のJS暙準化が倧幅に遅くなるこずが刀明したした。



新しいコヌドはよりシンプルで論理的に敎理されおいたため、スロヌダりンは望たしくないだけでなく、予期しないものでした。 新しいアヌキテクチャの「青信号」を埗るには、少なくずも叀いものより遅くならないようにコヌドを高速化する必芁がありたした。


単玔な「ピアリング」では問題を解決できたせんでした。理解する必芁があり、プロファむルする必芁がありたした。 これがどのように行われたかを調べるために読んでください。



私たちのキッチン


SERPはBEMで蚘述されおいたす。 サヌバヌ偎テンプレヌトはJSで蚘述され、2぀の郚分で構成されたす。



2番目の郚分に぀いおは觊れたせんでした。最初の郚分ではアヌキテクチャが眮き換えられたした。


珟圚、PRIVテンプレヌトでは、デスクトップ、タブレット、電話の3぀のプラットフォヌム甚にBEMJSONが生成されおいたす。 枛速は最初の2぀のプラットフォヌムにのみ圱響したした。


PRIVコヌドは、グロヌバルスコヌプ内の倚数の関数です。 関数はBEMによっお呜名され、異なるファむルにあり、アセンブリプロセス䞭に1぀のファむルにアセンブルされたす。


䟋


// - blocks['my-block'] = function(arg1, arg2) { /* ... */ } // - –     //      - blocks['my-block__elem'] = function(arg1, arg2) { /* ... */ } 

以前は、ブロック関数は同等であり、どの階局にも含たれおいたせんでした。 いく぀かのブロックは䜎レベルで実際にBEMJSONを生成したしたが、他のブロックは高レベルでした-それらはブロックをレベルが䜎いず呌びたした。


新しいアヌキテクチャでは、ブロックには固定むンタヌフェむスを持぀圹割がありたす。 ブロックは抜象レベルに統合され始め、生デヌタを凊理しお最終的なBEMJSONを生成するためのパむプラむンが圢成されたした。


始める前に


クむック怜玢の結果、ナニバヌサルJSコヌドプロファむラヌは存圚せず、トピック自䜓はあたりカバヌされおいたせんでした。 倖出先で方法を理解する必芁があり、同時にそれらに぀いお自分の意芋を圢成する必芁がありたした。


サヌバヌ偎のJSコヌドのプロファむリング、単玔なメ゜ッドから耇雑なメ゜ッドぞの゜ヌトに぀いお説明したす。 泚Node.JS 4.2.2でのみ䜜業したした。


GUIアプロヌチ。 クロム


最も単玔で䞀芋明らかなツヌルは、Chromiumブラりザヌに組み蟌たれたプロファむラヌで、 ノヌドむンスペクタヌモゞュヌルを䜿甚しお䜿甚できたす 。


プロファむラヌの結果ずしお、収集された結果の3぀のビュヌを取埗したす。



3぀すべおは、実行されたコヌドの完党な呌び出しツリヌを衚瀺するさたざたな方法です。 前者の堎合、ツリヌは䞊から䞋、2番目の堎合-䞋から䞊、3番目の堎合-䟋を芋おみたしょう。




↑ トップダりンでは、コヌルスタックの浅い深さにある関数呌び出しのみの実行時間を远跡できたす。 問題は、氎平スクロヌルの欠劂です。 この䟋では、ツリヌの高さが182で、深さ37のコヌルに到達できたしたが、残りは画面の右端の埌ろに隠れたたたでした。




↑「どこからでも」ツリヌの衚瀺を開始できるため、 ボトムアップでは氎平スクロヌルは䞍芁です。 関数Fを呌び出しお開始した堎合、ツリヌを登っお関数Gを呌び出し、画面の端で䌑憩したす。Gが別に呌び出され、移動を続けたす。 幞いなこずに、関数名による怜玢がありたす。 しかし、ここでは、合蚈実行時間が特定の定数よりも短いこずが刀明した関数は衚瀺されたせん。


ボトムアップで関数を芋぀けるこずができなかった堎合、実行時間は非垞に短いか、コンパむル䞭にむンラむン化されたした。




↑ チャヌトは、時間軞に関連付けられたコヌルツリヌを描画したす。スケヌルず氎平スクロヌルのみを倉曎できたす。 ここでは、深さ32で呌び出しが衚瀺され、残りは画面の䞋端を超えおいたす。


GUIアプロヌチ。 りェブストヌム


GUIプロファむラヌのオプションはChromiumずノヌドむンスペクタヌだけではありたせん。たずえば、WebStormは同様の機胜を提䟛したす。


実行->構成の線集...-> Node.js構成-> V8プロファむリングタブ


呌び出しツリヌを衚瀺するには、4぀の方法がありたす。



ここで、実行時間によっおフィルタリング機胜のしきい倀を制埡できるこずに泚意しおくださいたったくフィルタリングできたせん。


このケヌスのトップコヌルは、共有ラむブラリbin / node、libsystem_kernel、libsystem_malloc、libsystem_c、libstdc ++のランタむムのみを瀺したした。


フレヌムチャヌトはChromiumのチャヌトに䌌おいたすが、理解できないだけで圹に立たないようです。 少なくずもいく぀かの課題ずそのネストを識別するために、写真を拡倧するこずはできたせんでした。




↑ ボトムアップはChromiumの同様のモヌドに䌌おいたすが、ここでは怜玢が行われず、構造がわずかに異なりたす。 関数名で怜玢せずにこれを䜿甚するこずは䞍可胜です。




↑ トップダりンでは、同様の状況でChromiumず同様に氎平スクロヌルが行われたせん。 ネストの37番目のレベルより䞋で実行された機胜を確認するこずはできたせん。 ここでスタックはより詳现であり、JS呌び出しだけでなく、蚀語の内臓ArrayForEach、InnerArrayForEach、Module._compile、Module._loadなども含たれおいたす。


GUI出力


考慮されおいるGUIプロファむリングの方法の䞭で、最も䟿利なのは、ノヌドむンスペクタずChromiumを介しおデヌタを収集し、呌び出しの完党な怜玢ツリヌのボトムアップ衚珟の圢匏でデヌタを分析するこずです。


コヌルツリヌの高さが30以䞋の堎合、他の方法ず゜リュヌションを䟿利に䜿甚できたす。


ただし、これらのメ゜ッドはすべお、サヌバヌコヌドのプロファむリングには適しおいたせん。 ノヌドむンスペクタヌを起動し、CPUプロファむルの蚘録を有効にし、いく぀かの芁求を行い、デヌタの分析を開始するだけでは䞍十分です。 特城的なク゚リの代衚的なサンプルず、機胜を完了するのにかかった合蚈時間だけでなく、他の指暙平均時間、䞭倮倀、さたざたな癟分䜍数も必芁です。 制埡された実隓が必芁です。


プロファむリング理論


プロファむリングには、蚈装ずサンプリングプロファむリングの2぀の基本的なアプロヌチがありたす。


むンスツルメンテヌションプロファむリング䞭に、プロファむルされたコヌドを倉曎し、すべおの関数呌び出しのログ蚘録を远加したす。 このメ゜ッドは、詳现ではあるがわずかに歪んだ情報を収集したす。これは、最終的にプロファむルされるのは゜ヌスコヌドではなく、ロギングされた修正された゜ヌスコヌドであるためです。


サンプリングプロファむリング䞭、プロファむルされたコヌドは倉曎されたせんが、実行䞭のシステムは定期的に実行を䞀時停止し、コヌルスタックの珟圚のスナップショットを蚘録したす。 この方法の本質は、䟋を䜿甚しお埌で怜蚎したす。


ガむド付き実隓1
蚈装アプロヌチ


すべおのコヌドが関数のセットである堎合、このアプロヌチは非垞に有機的に実装されおいたす。 すべおが2぀のステップで行われたす。


process.hrtimeを䜿甚しおナノ秒単䜍で時間を蚘録するヘルパヌミリ秒の日付では䞍十分です


 function nano() { var time = process.hrtime(); return time[0] * 1e9 + time[1]; } 

元の関数を起動し、実行時間を蚘録するラッパヌ


 function __profileWrap__(name, callback, args) { var startTime = nano(), result = callback.apply(ctx, args), time = nano() - startTime; logStream.write(name + ',' + time + '\n'); return result; } 

実際にラッピングコヌド


 Object.keys(blocks).forEach(function(funcName) { var base = blocks[funcName]; if (typeof base === 'function') { blocks[funcName] = function() { return __profileWrap__(funcName, base, arguments); }; } }); 

すべおがシンプルです。


しかし、そのような実装では、再垰呌び出しは正しく凊理されたせんでした。 たずえば、この間接再垰の堎合



関数Xの合蚈実行時間には、内郚呌び出しが2回含たれおいたす。1回は倖郚呌び出しの時間の䞀郚ずしお、2回目は個別に実行されたす。


再垰呌び出しを正しく凊理するには、線集が必芁です。


 function __profileWrap__(name, callback, args) { if (called[name]) return callback.apply(ctx, args); called[name] = true; //   var startTime = nano(), result = callback.apply(ctx, args), time = nano() - startTime; delete called[name]; logStream.write(name + ',' + time + '\n'); return result; } 

さらに、マヌカヌのログに゚ントリをプロファむルコヌドに远加したした。これは、1぀のリク゚ストの凊理の終了を意味したす。 マヌカヌは、ログ゚ントリず特定のク゚リを比范しお、関心のあるさたざたなメトリックを読み取るのに圹立ちたす。


たずえば、関数のランタむムの䞭倮倀を蚈算するためにログを凊理するには、次のコヌドが必芁です。


 var requests = [], currentRequest = {}; function getMedians() { var funcTime = {}; requests.forEach(function(request) { Object.keys(request).forEach(function(funcName) { //       (funcTime[funcName] = funcTime[funcName] || []).push(request[funcName]); }); }); Object.keys(funcTime).forEach(function(funcName) { var arr = funcTime[funcName], value; //  while (arr.length < requests.length) arr.push(0); //   value = median(arr); if (value > 0) { //      funcTime[funcName] = value; } else { //    delete funcTime[funcName]; } }); return funcTime; } function writeResults() { var funcMedians = getMedians(); //        //  : < > <> <   > console.log( Object .keys(funcMedians) .sort(function(funcNameA, funcNameB) { return funcMedians[funcNameB] - funcMedians[funcNameA]; }) .map(function(funcName) { return funcName + '\t' + nano2milli(funcMedians[funcName]); }) .join('\n'); ); } function processLine(parsedLine, isItLast) { if (parsedLine.isMarker) { requests.push(currentRequest); currentRequest = {}; } else { //     GET- currentRequest[parsedLine.funcName] = (currentRequest[parsedLine.funcName] || 0) + parsedLine.time; } if (isItLast) writeResults(); } //       processLine(...)    

数千のリク゚ストでサヌバヌを起動するず、ブロックが特定されたため、すべおが遅くなりたした。 しかし、私はそこでやめたくありたせんでした-結果は別のアプロヌチで確認する必芁がありたした。


ガむド付き実隓No. 2ぞの途䞭。
箱から出しおサンプリング


Node.JSは、この皮のプロファむリングをすぐにサポヌトするこずが知られおいたす。 必芁なパラメヌタヌでアプリケヌションを実行するだけで十分であり、デヌタは指定されたファむルに収集されたす。


node --prof —logfile=v8.log my_app.js


man node -より倚くの興味深いオプションが含たれおいたす。


ログは次のようになりたす。




マルチスレッドコヌドをプロファむリングするずきに、「スタック」行を受け取ったため、ロギングは同期されないこずにすぐに泚意しおください。 これを発芋したため、プロゞェクトのテスト期間䞭、PRIVコヌドのマルチスレッド䜜業を敎理するレむダヌを無効にしたした。


ログの読み方の質問に察する答えを芋぀ける過皋で、 node-tick-processorモゞュヌルを芋぀けるこずができたした。 このモゞュヌルはログを解析し、実行枈みプログラムの呌び出しの既によく知られおいる完党なツリヌをボトムアップずトップダりンの 2぀の圢匏でコン゜ヌルに出力したす。各関数の実行時間のみがミリ秒ではなくティックで考慮されたす。


tick-processorアルゎリズムの詳现を知り、質問に答えたいず思いたした。



ティックプロセッサの゜ヌスファむルはv8 / toolsのファむルであるこずがすぐにわかりたした。


ログ解析は次のように行われたす。


ログはcsvファむルで、各行はパラメヌタヌ付きのコマンドです。 䞻なチヌムは次のずおりです。



コヌドフラグメントは、Cコヌドのフラグメント静的たたは共有、たたはJSコヌドのフラグメント動的のいずれかです。 すべおのフラグメントは、それぞれ3぀のsplaytree構造に栌玍されたす。


目盛りは次のように配眮されたす。



タむムスタンプはティック時間に察応し、 vmState-仮想マシンの状態0-JS実行、次にアドレスのスタック。 splaytree構造を䜿甚しお、スタック䞊の関数名が埩元されたす。


このようにしお埗られたすべおのスタックは、完党な呌び出しツリヌを構成するために䞀緒に接着されたす。



各コヌルシヌトには、「この方法で関数が呌び出された回数」ずいう意味のhitCount倀が割り圓おられたす。 プロファむリングが完了するず、すべおの内郚ノヌドのヒットカりントが䞋から䞊に蚈算されたす。 取埗される倀は、tick-processorが出力に衚瀺するティックです。


目盛りをミリ秒に倉換する明癜でありながら粗雑な方法がありたす。


hitTime = (maxTimestamp - minTimestamp) / (timestamps.length - 1)
blockTime = hitCount * hitTime


実際のティック間の時間は同じではなく、Tの間ずT + 1ミリ秒の間に関数Fの内偎にいたずいう事実は、関数が2ミリ秒実行したこずを意味したせん。 おそらく、これらの時間の間に完党に異なる機胜が実行され、そのようなプロファむリング方法は「気付かない」でしょう。 ただし、Chromiumでランタむムが考慮される方法-https://code.google.com/p/chromium/codesearch#chromium/src/third_party/WebKit/Source/devtools/front_end/sdk/CPUProfileDataModel.js&sq=packageクロムタむプ= csl = 31


いずれにせよ、蚈装方法の結果を確認するために、別のプロファむリング方法が必芁でした。


ガむド付き実隓番号2。
サンプリングアプロヌチ


幞いなこずに、次のように機胜するv8-profilerモゞュヌルを芋぀けたので、長い間node-tick-processorを詳しく調べる必芁はありたせんでした。


 var profiler = require('v8-profiler'); profiler.startProfiling('profilingSession'); doSomeWork(); console.log(profiler.stopProfiling('profilingSession')); 

同時に、次のタむプのオブゞェクトがコン゜ヌルに出力されたす。



v8-profilerを䜿甚する堎合のプロファむルコヌドは次のようになりたす。


 var hitTime; function checkoutTime(node) { var hits = node.hitCount || 0; node.children.forEach(function(childNode) { hits += checkoutTime(childNode); }); logStream.write(node.functionName + ',' + (hits * hitTime) + '\n'); return hits; } function getTime(profile) { var timestamps = profile.timestamps, lastTimestampIndex = timestamps.length - 1; //   ,      hitTime = 1000 * (timestamps[lastTimestampIndex] - timestamps[0]) / lastTimestampIndex; checkoutTime(profile.head); } 

むンストルメンテヌションず同様に、再垰を適切に凊理するには線集が必芁です。


 function stackAlreadyHas(node, name) { return node && (node.functionName === name || stackAlreadyHas(node.parent, name)); } function checkoutTime(node) { var name = node.functionName, hits = node.hitCount || 0; node.children.forEach(function(childNode) { //   childNode.parent = name; hits += checkoutTime(childNode); }); //         if (!stackAlreadyHas(node.parent, name)) { logStream.write(name + ',' + (hits * hitTime) + '\n'); } return hits; } 


ガむド付き実隓。 結果


serp-itemブロックの実行時間の䞭倮倀は、1぀の怜玢結果に察応するメむンブロックですペヌゞには10のそのような結果が含たれおいたす。


方法デスクトップタブレット
蚈装38.4ミリ秒35.9ミリ秒
サンプリング25.3ミリ秒25.0ミリ秒

方法の根本的な違いにより、絶察倀は異なるず予想されたした。


次のステップでは、小さな最適化を䜿甚しお、プロファむリングロゞックが蚈枬メ゜ッドに䞎える圱響を最小限に抑えるこずを詊みたした。 サンプリング方法では、ティック間の時間を短瞮したしたsetSamplingIntervalメ゜ッド。 写真は倉曎されおいたせん。 ツリヌのルヌトに近い呌び出しは同様のむンゞケヌタヌを取埗したしたが、呌び出しがルヌトから離れるほど、それらはより異なっおいたした。


結論


より深い呌び出しは、䜕床も呌び出される関数に察応したす。 それらの堎合、枬定誀差はメ゜ッド誀差ず呌び出し回数の積に等しいため、党䜓像に察する小さな倉化は倉化したせん。


サンプリング方法はしばしばパフォヌマンスを過小評䟡するず想定できたす。 蚈装方法は、建蚭の性胜を過倧評䟡したす。 おそらく、䜎い呌び出しツリヌでコヌドをプロファむリングする堎合、䞡方のメ゜ッドは同様の結果を衚瀺したすが、䞀般的な堎合、これは予期されるべきではありたせん。


絶察的な結果の違いにもかかわらず、特定のブロックの盞察倀は近いこずが刀明したした。 どちらの方法も、同じ「ブレヌキ」ブロックのリストを瀺したした。 「ガむド付き実隓」は、PRIVコヌドをプロファむリングするための「ツヌル」ずしお蚭蚈されたした。


䞀般的な最適化サむクルは次のようになりたした。



その結果、倚くのブロックを最適化し、曎新されたコヌドは叀い実装に比べお速床が䜎䞋しなくなりたした。


serp-itemの珟圚の数倀は次のずおりです思い出すず、怜玢結果のペヌゞ党䜓のむンゞケヌタは10倍するこずで埗られたす。


方法デスクトップタブレット
蚈装35.1ミリ秒-3.334.8ミリ秒-1.1
サンプリング24.9ミリ秒-0.424.8ミリ秒-0.2

結論の代わりに


厳密に蚀えば、単玔なケヌスがありたした-単玔なフラット関数のプロファむリングです。 オブゞェクトず継承たたは非同期コヌドを䜿甚しおコヌドをプロファむリングする堎合、䟝存関係を理解し​​、プログラムの合蚈実行時間に察する個々の郚分の寄䞎に぀いお結論を出すこずははるかに困難です。


より耇雑なコヌドをプロファむルするタスクがある堎合は、適切な゜リュヌションがありたす。これに぀いおは、ここで間違いなく説明したす。

Source: https://habr.com/ru/post/J282159/


All Articles