Javascript Pocket OLAPおよびIndexedDBのパフォーマンス

こんにちは、Habr!



最近、私は、弱く構造化されたデータに基づいてピボットテーブルを構築できる単純なWEBアプリケーションを作成して、Javascriptのパフォーマンスをテストすることにしました。 ExcelまたはアダルトOLAPシステムのすべての機能を繰り返すことは意図していませんでしたが、さまざまなデスクトップおよびモバイルブラウザーでJavascript全般およびIndexedDBのパフォーマンスをテストしたかったのです。 作業の第1段階を完了した後、事実を保存するためのシングルパスアルゴリズムを使用してピボットテーブルを構築した後(頻繁に使用されるセクションのインデックス付けと計算された集計のキャッシュは将来延期されます)、IndexedDBからの読み取りのパフォーマンスに失望し、モバイルブラウザーが実際にそうしなかったことに驚きましたデスクトップに遅れ、テストの1つで私の最愛のFirefoxの大失敗に困惑しました。 合計で、さまざまなバリエーションの2つのテストがありました。





テストは2万のファクトを含むJSONファイルで実行され、そのうち9つのエントリは製品ディレクトリであり、残りは売買操作を示しています。 ネットブックと電話でのテスト結果(秒単位の時間)を備えたタブレット、および実装の詳細と結論は削減中です。



Firefox_linux 64.0 Chromium_linux 71.0.3578.80 Falkon_linux QtWebEngine 5.9.5
1 20,000の事実の完全な計算-フィルター、行のグループ化、集計の計算 12.31、10.21、10.69 4.76、4.38、4.43 5.08、4.76、4.73
2 フィルターなし、計算された属性および集計-行のグループ化のみ 8.08、8.14、8.15 3.4、3.5、3.48 3.39、3.37、3.42
3 フィルター、グループ化、計算なし-IndexedDBカーソルの「裸」ループ 7.83、7.72、7.71 3.36、3.38、3.44 3.23、3.11、3.17
4 復号化(ドリルスルー)、2万行のhtmlテーブルへの出力 108 90.5 100
5 DOMへの出力なしのドリルスルー-キーの配列に対する「裸の」ループ、および1つずつレコードの抽出 11.57、14.71、11.52 18.62、18.91、18.27 18.01、18.09、18.11


Firefox_android 63.0.2 Chrome_android 71.0.3578.98 Edge_android 42.0.0.2804
電話での2万の事実の完全な計算 13.58、13.15、13.67 5.89、5.84、5.91 6.48、6.45、6.51


ソースデータ



データベースはNoSQLであるため、データスキーマとエンティティ間の関係はこれまで説明されていません。 オブジェクトの配列を含む任意のJSONファイルを入力に送信できます。入力には、ディレクトリの要素、トランザクション、ドキュメントの行などを混在させることができます。 関係は、一致する属性値に基づいて文字列をグループ化する(または数式を計算する)時点で確立されるため、人間が読み取れるテキストキーが使用されていると言えます。 たとえば、製品情報と購入/販売トランザクションは、次のエントリとともにファクトデータベースに表示されます。



[ {"product": "milk-2.5", "EAN-code": "4770148229477-3"}, {"product": "petrol-95", "EAN-code": "74820123490016-3"}, {"product": "fish-pollock", "EAN-code": "4640014465660-3"}, {"operation": "purchase", "partner": "nalchik-moloko", "product": "milk-2.5", "price": 15.5, "quantity": 50}, {"operation": "purchase", "partner": "lukoil", "product": "petrol-95", "price": 35, "quantity": 200}, {"operation": "purchase", "partner": "china-fish", "product": "fish-pollock", "price": 90, "quantity": 100}, {"operation": "sale", "partner": "ivanov-petr", "product": "milk-2.5", "price": 20.30, "quantity": 3.5}, {"operation": "sale", "partner": "smith-john", "product": "petrol-95", "price": 42, "quantity": 30}, {"operation": "sale", "partner": "ivanov-petr", "product": "fish-pollock", "price": 120, "quantity": 2} ]
      
      





数式の構文



Javascript構文が使用され、3つの追加機能があります。





フィルターの式の例:



 ['purchase', 'sale'].includes(fact('operation'))
      
      





計算されたファクト属性の例:



 "amount": "round(fact('price') * fact('quantity'), 2)"
      
      





ピボットテーブルの列の数式の例(集計、集計後の計算、参照クエリ):



 { "quantity-sum": "row('quantity-sum') + fact('quantity')", "amount-sum": "round(row('amount-sum') + fact('amount'), 2)", "quantity-avg": "round(row('quantity-sum') / row('count'), 2)", "price-max": "fact('price') > row('price-max') ? fact('price') : row('price-max')", "price-min": "row('price-min') == null || fact('price') < row('price-min') ? fact('price') : row('price-min')", "price-sum": "row('price-sum') + fact('price')", "price-avg": "round(row('price-sum') / row('count'), 2)", "price-avg-weight": "round(row('amount-sum') / row('quantity-sum'), 2)", "EAN-code": "selectlast('EAN-code', 'fact(\"product\") == row(\"product\")')" }
      
      





アルゴリズム機能



ワンパスアルゴリズムがあるため、各時点で、現在のファクトと、このファクトが含まれるピボットテーブルの1行があります。 したがって、関数fact()およびrow()を使用して、sum、min / max、averageなどの最も単純な集計を計算できます。 値の配列全体をメモリに保存する必要があるより複雑な統計関数はまだ準備ができていません。



ファクトストレージの単一サイクルのフレームワーク内で、ディレクトリから(本質的に他のファクトから)追加の属性を抽出する目的で、左結合(selectlast()関数)を実装することはより困難でした。 ディレクトリエントリの数は常に運用データの量よりも数桁少ないと仮定し、次のように問題を解決しました-行のグループ化と集計の計算と同時に-すべてのディレクトリが個別のサンドボックス(つまり、目的の属性を持つファクト)で選択されます。 ピボットテーブルの行の形成が完了した後、サンドボックスを2回目に通過することで、不足している属性をピボットテーブルの対応する列に引き込みます。 したがって、重いサイクルは1つだけです。



最後の最適化は、ループでのeval()の使用を避けるために、開始時にすべての式をJS関数に変換することでした。



性能試験



驚いたことに、インデックス付けされていないObjectStoreに20,000のファクトを挿入するのに約1秒かかりましたが、データの取得には重大な問題があります。



行4のパフォーマンスが低いことは理解できます-DOMでの出力、さらにはテーブルでの出力-予想通り難しい操作です。さらに、このようなテーブルでは適切に動作しないため、製品ではかなり無意味です。



興味深いのは3行目と5行目で、IndexedDBのサンプルの「裸の」パフォーマンスを表しています。 これらのテストでは、データベース全体の操作のみを残してアルゴリズム全体をコメントアウトしました.3行目では1つの大きなカーソルと5行目の反復を使用し、反対に5行目では(事前に準備された)キーの配列を反復処理し、一度に1つずつレコードを取得しました。 キーは整数で、自動インクリメントされます。



テスト3および5のコードスニペットをそれぞれ以下に示します。



 // single cycle on facts db.transaction('facts', 'readonly').objectStore('facts').openCursor(undefined, 'prev').onsuccess = (ev) => { let cur = ev.target.result; if (cur) { cfact = cur.value; ; // next fact cur.continue(); } }
      
      





 rowout(0); function rowout(i) { if (i < ids.length) { db.transaction('facts', 'readonly').objectStore('facts').get(ids[i]).onsuccess = (ev) => { cfact = ev.target.result; ; rowout(i+1); } } }
      
      





IndexedDBインターフェースは非同期であるため、どちらの場合も、エンジンによって最適化されなければならないテール再帰を観察します(そうでなければ、スタックオーバーフローから落ちたでしょう)が、そのような壮大なブレーキの理由はわかりません。 さらに、すべてのテストの敗者であるFirefoxは、単一クエリのテストで大幅に勝ちました。これにより、インデックスによるクエリに確実に対応できることが期待されます。



おわりに



私は悲しく感じました、そして、私は実験を続けるかどうか確信がありません。 もちろん、インデックスを作成してフルスキャンを中止することもできますが、それでもループ内で集計を合計する必要があります。また、ご覧のとおり、カーソルからのフェッチは最もコストのかかる操作です。 IndexedDBを完全に放棄し、データをJSON形式でディスクに保存し(残念なことに、解析には数秒かかります)、アルゴリズムをwasmに書き換える価値があります。 または、ブラウザでのRustの実装を待ちます(冗談です)。



アプリケーションはここから入手できます 。テストデータを含むファイルはこちらです。



Javascriptは私の母国語ではないので、アドバイスと賢い考えに感謝します。



PS

シニア仲間は何が起こっていたかを説明しました。 IndexedDBへのすべてのクエリは個別のスレッド(ワーカー)で実行され、カーソルがバイパスされる場合でも、各レコードに対してスレッド間呼び出しが行われます。これは非常に高価です。メッセージの作成とすべてのデータのコピーのコストがコールバックのコストに追加されるため(複雑なオブジェクトの送信Javascriptはリンクできません)。 したがって、高性能は非同期スレッドインターフェイスと互換性がありません。 ブラウザが時間の経過とともにこの問題を解決することを願っています。



PPSS

続き1:パフォーマンスの問題は、ブロックデータストレージを整理することで解決しました。

続き2:マルチスレッドコンピューティングによって加速されます。



All Articles