$ mol_app_bench:JSベンチマークをすばやく簡単に準備する

こんにちは、私の名前はドミトリー・カルロフスキーです。私も彼はグルメです。 私は、すでに痛いおなじみの問題をエレガントに、そして単に解決するグルメ料理を作るのが好きです。 さまざまなアプローチの利点、安価なサポート、開発の迅速化、デバッグの容易さについて長い間話すことができますが、これらはすべて非常に主観的な評価であり、考慮する必要があります。 したがって、遅かれ早かれ(ただし通常は時期尚早)、ディスカッション全体が多少なりとも測定可能な量(作業速度、ダウンロード速度、その他の速度)にスライドします。 そして、比較するものがあるように、異なるテクノロジーでいくつかの実装を行う必要があるだけでなく、人が理解できる出力でインターフェースを描画することも良いでしょう。 そしてこれはすべて、特にうまく行けば、常に不足している時間です。







ベンチマークの開発を簡素化するために、テスト済みオプションの選択から結果の視覚的表示までのインターフェイス全体を描画する別のアプリケーションで共通部分を選択しました。変数部分は外部から接続され、かなり単純なインターフェイスを実装します。 すべての「スティッキー」状態はリンクに保存されるため、他のグルメと簡単に共有できます。 さらに、さまざまな基準による結果のローカライズとソートがサポートされています。これは、すべてのファーストフード愛好家にとっての本当のごちそうです。







より速く、そうでなければ彼らはおいしいものをすべて食べるでしょう







次に学習します









独身の朝食



シンプルで上品な







さまざまなHTML要素をDOMに追加するのにかかる費用を知りたいとしましょう。 たとえば、Chromeでは、「Q」要素は「BLOCKQUOTE」の5倍、「IFRAME」要素は「OBJECT」の20倍重いことをご存知ですか? これらおよび他の興味深い事実を見つけるために、この単純なベンチマークを実装します。







ベンチマーク全体は、任意のサイトに配置できるHTMLページにすぎません。 $ mol_app_benchはフレーム内でそれを開き、メッセージを使用して対話します。 ページフレームは非常に簡単です。







<!doctype html> <meta charset="utf-8" /> <style> * { /*                */ position: absolute; } </style> <script> //       </script>
      
      





要素を表示する予定の量を前もって設定する必要があります。この数値は将来的に役立ちます。







 var count = 500
      
      





ベンチマークが$ mol_app_benchを介さずに直接開かれた場合、現在のベンチマークを開くための指示とともに$ mol_app_benchにリダイレクトします。







 if( window.parent === window ) document.location = '//eigenmethod.github.io/mol/app/bench/#bench=' + encodeURIComponent( location.href )
      
      





ベンチマークとインターフェイス間の通信は、 [ ' ' , ...[ ] ]



形式の最も単純なRPCを介して行われ[ ' ' , ...[ ] ]



。 簡単にするために、適切なパラメーターを使用して対応する関数を呼び出すだけです。







 window.addEventListener( 'message' , function( event ) { window[ event.data[0] ].apply( null , event.data.slice( 1 ) ) } )
      
      





$ mol_app_benchが起動すると、フレーム内でベンチマークを開き、メッセージ[ 'meta' ]



を送信するため、対応する関数を実装します。







 function meta() { done( metaData ) }
      
      





$ mol_app_benchの呼び出しは、メッセージ[ 'done' , ...[ ] ]



の形式のRPC呼び出しを想定しているため、ここでもすべてが簡単です。







 function done( result ) { parent.postMessage( [ 'done' , result ] , '*' ) }
      
      





meta



メソッドは$ mol_app_benchに次のベンチマークメタ情報を返す必要があります。









すべてのテキストは言語で指定されます。 この場合、メタ情報は次のように表示されます。







 var metaData = { title : { 'en' : 'HTML Elements rendering time' , 'ru' : '  HTML ' , } , descr : { 'en' : 'Simply add **' + count + ' elements**.' , 'ru' : '    **' + count + ' **  .' , } , samples : { } , steps : { 'fill' : { title : { 'en' : 'Adding elements' , 'ru' : ' ' , } , } , } , }
      
      





ここには1つの測定値-fillのみがあり、実装オプションは要素名のリストからプログラムで生成する必要があるため、まったく示されていません。 ところで、彼は:







 var tagNames = [ 'a' , 'abbr' , 'acronym' , 'address' , 'applet' , 'area' , 'article' , 'aside' , 'audio' , 'b' , 'base' , 'basefont' , 'bdi' , 'bdo' , 'bgsound' , 'big' , 'blink' , 'blockquote' , 'body' , 'br' , 'button' , 'canvas' , 'caption' , 'center' , 'cite' , 'code' , 'col' , 'colgroup' , 'command' , 'content' , 'data' , 'datalist' , 'dd' , 'del' , 'details' , 'dfn' , 'dialog' , 'dir' , 'div' , 'dl' , 'dt' , 'element' , 'em' , 'embed' , 'fieldset' , 'figcaption' , 'figure' , 'font' , 'footer' , 'form' , 'frame' , 'frameset' , 'head' , 'header' , 'hgroup' , 'hr' , 'html' , 'i' , 'iframe' , 'image' , 'img' , 'input' , 'ins' , 'isindex' , 'kbd' , 'keygen' , 'label' , 'legend' , 'li' , 'link' , 'listing' , 'main' , 'map' , 'mark' , 'marquee' , 'menu' , 'menuitem' , 'meta' , 'meter' , 'multicol' , 'nav' , 'nobr' , 'noembed' , 'noframes' , 'noscript' , 'object' , 'ol' , 'optgroup' , 'option' , 'output' , 'p' , 'param' , 'picture' , 'plaintext' , 'pre' , 'progress' , 'q' , 'rp' , 'rt' , 'rtc' , 'ruby' , 's' , 'samp' , 'script' , 'section' , 'select' , 'shadow' , 'small' , 'source' , 'spacer' , 'span' , 'strike' , 'strong' , 'style' , 'sub' , 'summary' , 'sup' , 'table' , 'tbody' , 'td' , 'template' , 'textarea' , 'tfoot' , 'th' , 'thead' , 'time' , 'title' , 'tr' , 'track' , 'tt' , 'u' , 'ul' , 'var' , 'video' , 'wbr' , 'xmp' ]
      
      





次に、実装オプションの構成とともにメタ情報を追加します。







 tagNames.forEach( function( tagName ) { metaData.samples[ tagName ] = { title : { 'en' : tagName } } } )
      
      





メタ情報を受け取った後、$ mol_app_benchは測定するものを選択して実装のメニューを描画します。 実装ごとに、ステップの名前に対応するメソッドを連続して呼び出し、実装バリアントの名前を引数として渡します。 私たちの場合、ステップは1つだけです。







 function fill( sample ) { var body = document.body while( body.firstChild ) { body.removeChild( body.firstChild ) } requestAnimationFrame( function() { var start = Date.now() var frag = document.createDocumentFragment() for( var i = 0 ; i < count ; ++ i ) { frag.appendChild( document.createElement( sample ) ) } body.appendChild( frag ) setImmediate( function() { done( Date.now() - start + ' ms' ) } ) } ) }
      
      





ここでは、最初にドキュメントの本文をクリーンアップし、次のアニメーションフレームを待ち、ブラウザがすべての処理を完了するようにします。次に、必要な数の要素を作成し、それらを一度にDOMに追加します。 ブラウザは一部の操作を非同期的に実行するため、 setImmediate



を介して、終了して測定時間を返すまで待機します。







ただし、setImmediateは比較的新しいAPIであり、まだすべてのブラウザーに実装されていないため、既に使用しているpostMessageを使用して最も単純なバージョンを実装します。







 var setImmediate_task function setImmediate( task ) { setImmediate_task = task postMessage( [ 'setImmediate_task' ] , '*' ) }
      
      





それだけです、実際には:









家族全員の夕食



あらゆる味の料理







どのJSフレームワークが最速かを知りたいとしましょう。 これを行うには、すべてのアプリケーションに同じアプリケーションを実装し、それぞれのアプリケーションで同じ使用シナリオを実行して、完了時間を測定する必要があります。 JSには非常に多くのフレームワークが存在するため、それぞれを理解し、最も単純なアプリケーションを実装する場合でも、1か月以上かかります。 したがって、 ToDoMVCプロジェクトから既製のアプリケーションを取得します。 これを行うために、 フォークしベンチマークを追加してgithub.ioに配置し ます (対応するマージリクエストはまだ最高の時間を待っています)。







ベンチマークでは、実装を別のフレームで開きます。







 <iframe id="sandbox"></iframe>
      
      





3つの測定ステップがあります。









 var metaData = { title : { 'en' : 'ToDoMVC workflow benchmark' , 'ru' : 'ToDoMVC -  ' , } , descr : { 'en' : 'Sample applications is [ToDOMVC](todomvc.com) implementations. Benchmark creates **' + count + ' tasks** in sequence and then removes them.' , 'ru' : '    [ToDOMVC](todomvc.com)     .       **' + count + ' **    .' , } , samples : { } , steps : { 'start' : { title : { 'en' : 'Load and init' , 'ru' : '  ' , } , } , 'fill' : { title : { 'en' : 'Tasks creating' , 'ru' : ' ' , } , } , 'clear' : { title : { 'en' : 'Tasks removing' , 'ru' : ' ' , } , } , } , }
      
      





実装オプションに関する情報は、learn.jsonファイルにあります。このファイルは、メタ情報を同期してダウンロードおよび追加しません。







 var xhr = new XMLHttpRequest xhr.open( 'get' , '../learn.json' , false ) xhr.send() var learn = JSON.parse( xhr.responseText ) for( var lib in learn ) { if( lib === 'templates' ) continue learn[ lib ].examples.forEach( function( example ) { if( !/^examples\/[-a-zA-Z0-9_\/]+$/.test( example.url ) ) return metaData.samples[ example.url.replace( /(^examples\/|\/$)/g , '' ) ] = { title : { 'en' : learn[ lib ].name + ' ' + example.name } } } ) }
      
      





測定の実装に着手する前に、フレームへのリンクを覚えて、アプリケーションで必要な要素を検索するセレクターを作成します。







 var sandbox = document.getElementById( 'sandbox' ) var selector = { adder : '#new-todo,.new-todo,.todo__new,[mol_app_todomvc_add]' , adderForm : '#todo-form,.todo-form,#header form' , dropper : '.destroy,[mol_app_todomvc_task_row_drop]' , }
      
      





タスクを追加するための要素の出現により、アプリケーションの開始を検出します。







 function start( sample ) { var start = Date.now() sandbox.src = '../examples/' + sample + '/' sandbox.onload = function() { step() function step() { if( sandbox.contentDocument.querySelector( selector.adder ) ) done( Date.now() - start + ' ms' ) else setTimeout( step , 10 ) } } }
      
      





タスクを追加するには、ユーザーが名前を入力して「ENTER」を押したときにブラウザーで発生するすべてのイベントをシミュレートします。







 function fill( sample ) { var adder = sandbox.contentDocument.querySelector( selector.adder ) var adderForm = sandbox.contentDocument.querySelector( selector.adderForm ) var i = 1 var start = Date.now() step() function step() { adder.value = 'Something to do ' + i adder.dispatchEvent( new Event( 'input' , { bubbles : true } ) ) adder.dispatchEvent( new Event( 'change' , { bubbles : true } ) ) var event = new Event( 'keydown' , { bubbles : true } ) event.keyCode = 13 event.which = 13 event.key = 'Enter' adder.dispatchEvent( event ) var event = new Event( 'keypress' , { bubbles : true } ) event.keyCode = 13 event.which = 13 event.key = 'Enter' adder.dispatchEvent( event ) var event = new Event( 'compositionend' , { bubbles : true } ) event.keyCode = 13 event.which = 13 event.key = 'Enter' adder.dispatchEvent( event ) var event = new Event( 'keyup' , { bubbles : true } ) event.keyCode = 13 event.which = 13 event.key = 'Enter' adder.dispatchEvent( event ) var event = new Event( 'blur' , { bubbles : true } ) adder.dispatchEvent( event ) if( adderForm ) { var event = new Event( 'submit' , { bubbles : true } ) event.keyCode = 13 event.which = 13 event.key = 'Enter' adderForm.dispatchEvent( event ) } if( ++i <= count ) setImmediate( step ) else done( Date.now() - start + ' ms' ) } }
      
      





さまざまなフレームワークがさまざまなイベントに反応して同じ機能を実装するため、あらゆる種類の非常に多くのイベントをスローします。 イベントのセットと構成は、ベンチマークを台無しにした実装を見て手動で選択する必要がありました。 現在、ほとんどの実装は正しく動作していますが、一部はまだ刈り取り中であり、開始時に肉眼で見ることができます。 これらの実装でベンチマークを修正するコミュニティプールリクエストは非常に役立ちます;-)







最後のステップは、すべてのタスクを順番に削除することです。 ここではすべてが簡単です:







 function clear( sample ) { var start = Date.now() step() function step() { var dropper = sandbox.contentDocument.querySelector( selector.dropper ) if( !dropper ) return done( Date.now() - start + ' ms' ) dropper.dispatchEvent( new Event( 'mousedown' , { bubbles : true } ) ) dropper.dispatchEvent( new Event( 'mouseup' , { bubbles : true } ) ) dropper.dispatchEvent( new Event( 'click' , { bubbles : true } ) ) setImmediate( step ) } }
      
      





それだけです、それほど難しくはありませんでしたか?









$ mol_app_benchの肉



私の魅力







まず、アプリケーションの構造の概要を説明します。 これは2つのパネルで構成されます。メインパネルにはベンチマークと結果があります。 実装を選択するためのメニューを追加:







 $mol_app_bench $mol_view sub / <= Addon_page $mol_page <= Main_page $mol_page
      
      





追加セットについては、タイトルと実装の実際のリストを作成します。これらは将来プログラムで作成されます。







 Addon_page $mol_page title <= addon_title @ \Samples body / <= Menu $mol_list rows <= menu_options /
      
      





ここで英語のテキストを作成したので、気にしないでください。 犬のおかげで、組み立て中に英語の文字列を含むファイルに抽出されます。







次に、各メニュー項目が、対応する実装の測定を有効または無効にするチェックボックスに過ぎないように設定します。







 Menu_option!id $mol_check_box checked?val <=> menu_option_checked!id?val false label / <= menu_option_title!id \
      
      





ご覧のとおり、ここでは埋め込みコンポーネントとやり取りするために一方向および双方向のバインディングを使用します。 矢印の右側の名前はコンポーネント(この場合はアプリケーションコンポーネント)のプロパティの名前であり、左側は埋め込みコンポーネントのプロパティです。







メインパネルに、説明と結果を含む情報ブロックと、アプリケーションをダウンロードするサンドボックスを表示します。







 Main_page $mol_page title <= title - body / <= Inform $mol_view <= Sandbox $mol_view dom_name \iframe
      
      





ご覧のとおり、このパネルのタイトルはアプリケーション全体のタイトルになり、ブラウザータブのタイトルにも表示されます。







情報ブロックでは、視覚化コンポーネントのマークダウンを使用して説明を表示し、結果は比較テーブルの出力コンポーネントを介して表示されます。







 Inform $mol_view sub / <= Descr $mol_text text <= description \ <= Result $mol_bench result <= result null col_head_label!id / <= result_col_title!id / col_sort?val <=> result_col_sort?val \
      
      





最後に、出力比較テーブルの実装の名前を使用して列の見出しを設定します。これについては後で使用します。







 result_col_title_sample @ \Sample
      
      





これらのすべてのコードを組み合わせて、アプリケーションの構造の完全な説明を取得します







これでビヘイビアを追加して追加できます。構造の説明から自動的に生成されたクラスを展開するだけで十分です。







 namespace $.$mol { export class $mol_app_bench extends $.$mol_app_bench { //           } }
      
      





まず、アドレスバーからベンチマークへのリンクを取得する必要があります。







 @ $mol_mem() bench() { return $mol_state_arg.value( this.state_key( 'bench' ) ) || 'list/' }
      
      





ご覧のとおり、ベンチマークが設定されていない場合、異なるフレームワークで出力を一覧表示するベンチマークが開きます。







次に、サンドボックスへのリンクが必要です。リンクだけでなく、ベンチマークが既にロードされているため、フレームの「load」イベントの後にのみプロパティの値を記述します。







 @ $mol_mem() sandbox( next? : HTMLIFrameElement , force? : $mol_atom_force ) : HTMLIFrameElement { const next2 = this.Sandbox().dom_node() as HTMLIFrameElement next2.src = this.bench() next2.onload = event => { next2.onload = null this.sandbox( next2 , $mol_atom_force ) } throw new $mol_atom_wait( `Loading sandbox...` ) }
      
      





sandbox



プロパティの実装コードは非同期であることが判明しましたが、この非同期性は周囲のコードから分離されているため、このプロパティの外部インターフェイスは反応性魔法により同期を維持します。







最大の複雑さは、「リモートプロシージャを呼び出した結果」プロパティに隠されています。 非同期性自体をカプセル化するという事実に加えて、もう1つの制限があります-サンドボックスが1つしかないため、一度に1つのプロシージャしか呼び出すことができず、同時に複数のベンチマークを実行するのは悪い兆候です。 したがって、現在の実行可能コマンドはcommand_current



プロパティに書き込まれ、 command_current



は最初に別のコマンドが現在実行されていないことを確認し、実行中の場合は最初にその完了を待ちます。







 'command_current()' : any[] @ $mol_mem() command_current( next? : any[] , force? : $mol_atom_force ) { if( this['command_current()'] ) return return next } @ $mol_mem_key() command_result< Result >( command : any[] , next? : Result ) : Result { const sandbox = this.sandbox() sandbox.valueOf() if( next !== void 0 ) return next const current = this.command_current( command ) if( current !== command ) throw new $mol_atom_wait( `Waiting for ${ JSON.stringify( current ) }...` ) requestAnimationFrame( ()=> { sandbox.contentWindow.postMessage( command , '*' ) window.onmessage = event => { if( event.data[ 0 ] !== 'done' ) return window.onmessage = null this.command_current( null , $mol_atom_force ) this.command_result( command , event.data[ 1 ] ) } } ) throw new $mol_atom_wait( `Running ${ command }...` ) }
      
      





マルチスレッドの愛好家は、ここで「比較とスワップ」メカニズムによって実装された同期プリミティブ「mutex」を見つけることができます。 そして、これはカジュアルではありません。可能な場合、デフォルトで$ mol_atomはタスクを並列化しようとするからです。 このコードを理解することは、反応性の魔法を理解しないと難しい場合があるため、上記の記事を読むことをお勧めします。 しかし、主なことは、すべての複雑さがこのプロパティにカプセル化されており、さらなる作業が簡単で楽しいことです。 たとえば、ベンチマークからメタ情報を取得します。







 meta() { type meta = { title : { [ lang : string ] : string } descr : { [ lang : string ] : string } samples : { [ sample : string ] : { title : { [ lang : string ] : string } } } steps : { [ step : string ] : { title : { [ lang : string ] : string } } } } return this.command_result< meta >([ 'meta' ]) }
      
      





そして今、すべての実装のリストをそれらの名前でソートして取得します:







 @ $mol_mem() samples_all( next? : string[] ) { return Object.keys( this.meta().samples ).sort( ( a , b )=> { const titleA = this.menu_option_title( a ).toLowerCase() const titleB = this.menu_option_title( a ).toLowerCase() return titleA > titleB ? 1 : titleA < titleB ? -1 : 0 } ) }
      
      





実装の名前は、現在の言語に基づいて形成されます。







 menu_option_title( sample : string ) { const title = this.meta().samples[ sample ].title return title[ $mol_locale.lang() ] || title[ 'en' ] }
      
      





ベンチマークの名前と説明、同様の方法で取得します。







 @ $mol_mem() title() { const title = this.meta().title return title[ $mol_locale.lang() ] || title[ 'en' ] || super.title() } @ $mol_mem() description() { const descr = this.meta().descr return descr[ $mol_locale.lang() ] || descr[ 'en' ] || '' }
      
      





メニュー項目のリストを作成します。







 menu_options() { return this.samples_all().map( sample => this.Menu_option( sample ) ) }
      
      





実装の選択された状態は、選択された実装のリストに依存します。







 @ $mol_mem_key() menu_option_checked( sample : string , next? : boolean ) { if( next === void 0 ) return this.samples().indexOf( sample ) !== -1 if( next ) this.samples( this.samples().concat( sample ) ) else this.samples( this.samples().filter( s => s !== sample ) ) return next }
      
      





ご覧のとおり、プロパティの実装は同時にゲッターとセッターの両方です。 選択した実装のリストも同様にリンクに保存されます。







 @ $mol_mem() samples( next? : string[] ) : string[] { const arg = $mol_state_arg.value( this.state_key( 'sample' ) , next && next.join( '~' ) ) return arg ? arg.split( '~' ).sort() : [] }
      
      





実際の測定に進む前に、実行するすべての手順を書き留めます。







 @ $mol_mem() steps( next? : string[] ) { return Object.keys( this.meta().steps ) }
      
      





各実装のすべてのステップを見ていきましょう。







 @ $mol_mem_key() result_sample( sampleId : string ) { const result : { [ key : string ] : any } = { sample : this.menu_option_title( sampleId ) , } this.steps().forEach( step => { result[ step ] = this.command_result<string>([ step , sampleId ]) } ) return result }
      
      





次に、選択したすべての実装を調べて、一般的な結果を形成します。これは、アプリケーション構造の説明に示されているように、比較テーブルの出力コンポーネントに転送されます。







 @ $mol_mem() result() { const result : { [ sample : string ] : { [ step : string ] : any } } = {} this.samples().forEach( sample => { result[ sample ] = this.result_sample( sample ) } ) return result }
      
      





コンポーネント$mol_bench



col_head_label



プロパティも再定義しcol_head_label



。そこでは、空の配列を返すresult_col_title



プロパティをバインドしresult_col_title



。 ローカライズされた列ヘッダーを返すように再定義しましょう。







 result_col_title( col_id : string ) { if( col_id === 'sample' ) return [ this.result_col_title_sample() ] const title = this.meta().steps[ col_id ].title return [ title[ $mol_locale.lang() ] || title[ 'en' ] ] }
      
      





さらに、 col_sort



プロパティとしてcol_sort



プロパティを提供しました。 リンクにも残るように、次のように再定義します。







 @ $mol_mem() result_col_sort( next? : string ) { return $mol_state_arg.value( this.state_key( 'sort' ) , next ) }
      
      





振る舞いを理解しました。これで、自動生成されたBEM属性に応じたスタイルで料理を飾ることができ、提供できます。







糖菓



$ mol_app_benchページで、ベンチマークの作成に関するドキュメントと、既に実装されているものへのリンクを見つけることができます。







いくつかの例:









計画:









速度を落とさないで、ここで説明する一般的なインターフェースに何がどのようにテストされているのか、何が欠けているのか、あなたのレシピを試して、コミュニティと共有してください。







賢い考えはいつも私を悩ませますが、私は速いです








All Articles