トレースとJavascript





アプリケーションのランタイムをトレースしたことがありますか? 灰色のエンドポイントが行うリクエストの数を知っていますか? また、リクエストに返される必要のあるエンティティの各ページから、同様のタイプのリソースの相互参照がどのくらい差し引かれますか? ユーザーが随時追加するオプションの要求フィールドのために、ユーザーが待機しなければならない時間を測定しようとしましたか? これらの6つのクエリをこれらの2つのデータベースに並列化するとどうなるのか疑問に思ったことはありませんか?



上記の何かがおもしろい、または少なくともなじみがあると思われる場合は、猫を歓迎します。



chrome://トレース



chrome:// Chromeのアドレスバーでトレースすると、次のページが表示されます。







ツール自体は、V8自体(およびクロム)のトレースデータと非常に親しい友人です。 さらに最近では、ノードは、クロムをサポートするランタイムのネイティブトレースも可能にします。//フロントエンドとしてトレースします。 トレースするには、大切な「記録」をクリックして次のタブを数回通過します。その後、ウィンドウ内でカーソルを慎重にチャットし、マウスホイールをランダムに回転させたときのブラウザーの動作を詳細に確認できます。 私たちの便宜のために、親切な人々は結果を解釈する方法について非常に詳細な記事を書きさえしました。



このビジュアライザーが使用するデータの形式は十分に文書化されており、レンダリング、マークアップの計算、ガベージレンダリングのトラップから遠く離れたもののトレースにも最適です。 そのオープン性と外部ファイルをロードする機能により、このトレーサーはあらゆるI / Oが可能な環境と非常に簡単に統合できます。 視覚化自体に加えて、イベントグループを分析するための組み込み機能がいくつかあります。 一般に、便利で柔軟なツールは、従来のケースでは、確実性を求めて大量のログを解析する必要がある場合に便利で非常に役立ちます。



トレーサーのフロントエンド全体は、ちなみにバニラJSで記述されおり、 GitHubにあります 。 Chrome://トレース自体は通常のWebページであり、他のツールと同様にDevToolsで検査できます。 つまり、ツールがあなたを満足させることができない場合...完全に、それは必要なシナリオの下で安全にマッシュすることができます。



著者の経験では、このような機器の必要性は、物事の目的のためにサーバーレスシステムで作業しているときに現れました。 特定のシナリオでは、1つの操作を実行するために、ラムダ関数の開始数が数十および数百に達しました。 ログのみを使用してこれらの起動のすべてを分析することは決して楽しいことではありませんでした。 全体像を一目で確認できるようなものが欲しかった。 samopisnyトレーサーが役に立ちました。これにより、関数の内部とそれらが行うすべての要求を計測することができました。 悲しみについては、作者はクロムの存在を疑いませんでした://トレース、したがって、d3のフロントエンドはトレーサー用にゼロから書かれており、別のポストを専用にすることができます(tldr; DOMを介して複雑なものはレンダリングせず、キャンバスとWebGLのみ)。 トレースが最適化に役立ついくつかの例を見てみましょう。



温暖化キャッシュ
与えられた:非同期のメモ化サービスとそれを呼び出すいくつかのサブサービス。



問題:キャッシングサービスへの複数の呼び出しが連続して行われると、結果はそれぞれ単一のリクエストの代わりにキャッシュに入る時間がないため、サービスはnを作成します。



解決策:キャッシュをウォームアップする(プリフェッチ、一連の呼び出しが同じ依存関係を必要とする場合に機能する)、または遅延ブロッキングメモ化(即興名)



class CachingService { constructor() { this.cache = null; } doAction() { if (this.cache) { return this.process(); } else { return anotherService() .then(result => this.cache = result) .then(this.process); } } } Promise.all([ cachingService.doAction(), cachingService.doAction(), cachingService.doAction() ]); // Prefetch: anotherService() .then(result => cachingService.cache = result) .then( () => Promise.all([ cachingService.doAction(), cachingService.doAction(), cachingService.doAction() ]) ) // Lazy blocking memoization: class FixedService { constructor() { this.pendingRequest = null; } doAction() { const pendingRequest = this.pendingRequest ? this.pendingRequest : this.pendingRequest = anotherService(); return pendingRequest .then(this.process); } }
      
      







文書化されていないクエリ
与えられた: SDKの1つが追加のブロックリクエストを行い(そして全世界を待ちましょう)、それについては疑わしくありませんでした。



解決策:この「有害な」リクエストを無効にするSDKのインスツルメンテーション。



ユーザースクリプトの異常に長い実行
与えられた:トレースは、数行のユーザースクリプトが予想よりも2桁長い時間実行されることを示しました。 Debagは、問題がグローバルスコープからの変数の過剰な使用であることを示しました。



解決策:関数の引数を使用してユーザーコードのグローバルスコープをシミュレートする



 const vm = require('vm'); const scope = { property: 'value', // ... propertyN: 'valueN' }; const script = ` property = '0.6911'; propertyN = '42'; `; // Overusing global scope vm.runInNewContext( script, scope ); // Argument based scoping vm.runInNewContext( ` ( // Turn scope keys into arguments of wrapper function ({ ${Object.keys(scope).join(',') } }) => { ${script} } )(this.scope); `, { scope } );
      
      





上記のコードは、仮想関数(hello、CommonJS)の引数を使用して、スクリプトのグローバル変数をシミュレートします。



時期尚早のデータの正規化
与えられた:いくつかのエンティティは非常に大きい(2.5-3Mb)

問題:そのようなエンティティはネットワーク経由でダウンロードするのに長くて費用がかかるという疑い。



解決策:トレースにより、このようなエンティティの読み込みは最も安価な操作の1つであり、解決する必要があるさらに深い問題があることが示されました。 このデータは、システムの一部にリファクタリングを集中させるのに役立ち、改善後の生産性が大幅に向上しました。



これらの例に加えて、トレーサーが変更の結果を迅速かつ正確に評価し、「行ごとの」情報の吸収を回避し、全体として生活を簡素化するツールとして現れることを可能にする多くのケースがありました。



最も単純なトレーサーデバイス



最も単純なトレーサーの実用的な実装に目を向けると、コードインストルメンテーションの方法に従って、条件付きで2つの大きなグループに分けることができます。





集中化されたアプローチは、インストルメント化されたコードがクリーンであり、トレーサーからほとんど独立しているという点で優れています。 ただし、同時に、明示的にサポートする必要がある暗黙的な依存関係が表示されます。 この場合、たとえば、特別な注釈が役立つ場合があります。または、トレーサーの個別の統合テストが、キャプチャポイントと共にその場所に必要なすべてのモジュールを検証します。



分散アプローチは、有用なコードのブロック全体を含むイベントの期間と内容を非常に状況に応じて決定できるという点で優れています。 また、トレーサは潜在的なエラーを抽象的にキャッチする必要がなく、変更のない実行フローを提供するため、初期実装が容易です。



2番目のタイプのトレーサーの最も一般的な例は、次のようになります。



 class Tracer { constructor() { this.events = {}; } start(meta) { const id = Math.random().toString(16).substr(2); const start = Date.now(); this.events[id] = { start, id, meta }; return id; } stop(id) { this.events[id].stop = Date.now(); } }
      
      





すべてが十分に簡単であり、トレーサーはインストルメントされたコードのサードパーティの影響を心配するべきではありません。 また、 `Math.random()。ToString(16).substr(2);`のようなものは決して使用しないでください。この例は純粋に説明のためのものであり、はるかに正しい 方法があります



一元化されたアプローチに移行したいとすぐに、キャプチャポイントを設定するための抽象的なインターフェイスが必要になります。



 addHook(path, method) { const module = require(path); const originalMethod = module[method]; module[method] = function() { const event = this.start(); const result = originalMethod.apply(module, arguments); this.end(event); return result; } }
      
      





このようなメソッドは、構成の中心となる唯一の場所で呼び出すことができます。 普通のモンキーパッチ、追加するものもありません。 この方法にはいくつかの欠点があることに気付くのは簡単です。





最初の欠点は、この関数のインターフェイスがよく考えられていないことに起因します。 トレーサーにインストルメンテーション用のファイルパスを許可することは、疑わしい作業です。 エンドユーザーの場合、これにより、暗黙的な処理のレイヤーが追加され、トレーサーのインターフェイスに関する疑問が生じます。 「そのパスを渡しましたか?」、「パスは自分のファイルまたはトレーサーファイルからの相対パスですか?」、「__ dirnameおよびpath.resolveを使用して標準のフットクロスを作成する必要がありますか?」計測する必要があります:



 addHook(module, method) { const originalMethod = module[method]; module[method] = function() { const event = this.start(); const result = originalMethod.apply(module, arguments); this.end(event); return result; } }
      
      





ユーザーに対するより明示的な動作に加えて、これは存在の検証を簡素化します。



 isInstrumentable(module, method) { return module && (typeof module[method] === 'function'); } addHook(module, method) { if (this.isInstrumentable(module, method)) { const originalMethod = module[method]; module[method] = function() { const event = this.start(); const result = originalMethod.apply(module, arguments); this.end(event); return result; } } }
      
      





無効な構成での動作は、ハード(エラーをスロー)またはソフト(モジュールを無視して動作を継続)のいずれかです。 理論的には、優れたトレーサーを使用すると、構成を使用して任意のパスをたどることができます。これは、両方のオプションが異なる場合に正当化されるためです。



非同期関数の処理は、もう少し興味深いタスクです。 まず、インスツルメントされたメソッドが非同期かどうかを判断し、トレーサーの動作を変更してイベントの終わりを判断する必要があります。 これを行うために、次のようにオーバーロードメソッドを変更できます。



 addHook(module, method) { if (this.isInstumentable(module, method)) { const tracer = this; const originalMethod = service[method]; service[method] = function () { let result; const eventId = tracer.start(); try { result = originalMethod.apply(this, arguments); } catch (error) { tracer.stop(eventId, { success: false, error: errorToString(error) }); // rethrow to keep the flow as it would // be without having tracker at all throw error; } if (result instanceof Promise) { // Handle services returning promises return result .then( tracer.handleResolved(eventId), tracer.handleRejected(eventId) ); } else { // Handle synchronous services tracer.stop(eventId); return result; } }; } } handleResolved(event) { return res => { this.stop(event); return res; }; } handleRejected(event) { return err => { this.stop(event); return Promise.reject(err); } }
      
      





興味深いことに、トレーサーは実行時にサードパーティの影響がないことを確認する必要があります。 したがって、キャッチエラーをアプリケーションストリームに転送する必要があります。



それだけです。このようなプリミティブトレーサーは、既に何か有用なものに使用できます。 不可解な追加機能のうち:





イベントが記録された後は、それらをchrome://トレースに適した形式で保存するだけです。 これを行うには、次の機能を使用できます。



 function toChromeTracingEvent(event) { const parsed = url.parse(event.meta.url); const base = { id: event.id, pid: process.pid, tid: 0, cat: event.type, name: event.meta.method, args: event.meta }; return [ Object.assign({}, base, { ph: 'b', ts: event.start * 1000 }), Object.assign({}, base, { ph: 'e', ts: event.stop * 1000 }) ]; }
      
      





一番下の行は、各トレーサーイベントが、クロムの開始と終了を表す2つのオブジェクトに展開されることです。//非同期イベントをトレースします。 非同期イベントに加えて、「完了」イベントを作成することもできます。 しかし、それらはクロムでレンダリングするときに互いにオーバーラップします://トレースは、分析がそれほど容易ではないためです:3



すべてのイベントが変換された後、 `traceEvents`オブジェクトのフィールドの配列にそれらをきちんと配置して、ファイルエクスプローラーを調べるのに苦痛なく長くなるどこかにそれを保存するだけです。



最後のステップは、暗黙的な何かが発生するアプリケーションの部分にインストルメンテーションを追加し、それを実行して、結果ファイルをchrome://トレースにアップロードすることです。



この記事のインスピレーションとなったトレーサー



仕事を面白くしましょう!



All Articles