sql >> データベース >  >> RDS >> Database

SQLの完了。成功と失敗の物語

    私は5年以上、データベースの相互作用のためのIDEを開発している会社で働いています。この記事を書き始める前は、いくつの派手な物語が先にあるのかわかりませんでした。

    私のチームはIDE言語機能を開発およびサポートしており、コードのオートコンプリートが主要な機能です。私は多くのエキサイティングなことが起こっていることに直面しました。最初の試みからうまくいったこともあれば、数回のショットの後でも失敗したこともありました。

    SQLと方言の解析

    SQLは自然言語のように見せかける試みであり、その試みは非常に成功しています。方言にもよりますが、数千のキーワードがあります。あるステートメントを別のステートメントと区別するために、多くの場合、1つまたは2つの単語(トークン)を先に探す必要があります。このアプローチは先読みと呼ばれます 。

    LA(1)、LA(2)、またはLA(*)のいずれかでパーサーの分類があります。これは、パーサーが適切なフォークを定義するために必要なだけ先を見ることができることを意味します。

    オプションの句の終わりが別のオプションの句の始まりと一致する場合があります。これらの状況では、解析の実行がはるかに困難になります。 T-SQLは物事を簡単にするものではありません。また、一部のSQLステートメントには、必ずしもそうとは限りませんが、前のステートメントの先頭と競合する可能性のある末尾が含まれている場合があります。

    信じられませんか?文法を介して形式言語を記述する方法があります。このツールまたはそのツールを使用して、パーサーを生成できます。文法を説明する最も注目すべきツールと言語は、YACCとANTLRです。

    YACC -生成されたパーサーは、MySQL、MariaDB、およびPostgreSQLエンジンで使用されます。それらをソースコードから直接取得して、コード補完を開発することができます。 およびこれらのパーサーを使用したSQL分析に基づくその他の関数。さらに、この製品は無料の開発アップデートを受け取り、パーサーはソースエンジンと同じように動作します。

    では、なぜまだ ANTLRを使用しているのですか ?それはC#/。NETをしっかりとサポートし、まともなツールキットを持っており、その構文は読み書きがはるかに簡単です。 ANTLR構文は非常に便利になったため、Microsoftは現在公式のC#ドキュメントで使用しています。

    しかし、構文解析に関しては、SQLの複雑さに戻りましょう。公開されている言語の文法サイズを比較したいと思います。 dbForgeでは、文法の断片を使用します。それらは他のものよりも完全です。残念ながら、さまざまな機能をサポートするためのC#コードの挿入で過負荷になっています。

    さまざまな言語の文法サイズは次のとおりです:

    JS –475パーサー行+273レクサー=748行

    Java –615パーサー行+211レクサー=826行

    C#–1159パーサー行+433レクサー=1592行

    С++–1933行

    MySQL –2515パーサー行+1189レクサー=3704行

    T-SQL –4035パーサー行+896レクサー=4931行

    PL SQL –6719パーサー行+2366レクサー=9085行

    一部のレクサーの末尾には、その言語で使用可能なUnicode文字のリストが含まれています。これらのリストは、言語の複雑さの評価に関しては役に立ちません。したがって、私が取った行数は常にこれらのリストの前で終了しました。

    言語文法の行数に基づいて言語解析の複雑さを評価することは議論の余地があります。それでも、大きな食い違いを示す数字を示すことが重要だと思います。

    それがすべてではありません。 IDEを開発しているので、不完全または無効なスクリプトを処理する必要があります。私たちは多くのトリックを考え出す必要がありましたが、顧客はまだ未完成のスクリプトで多くの作業シナリオを送信します。これを解決する必要があります。

    述語戦争

    コードの解析中に、2つの選択肢のどちらを選択するかが単語でわからない場合があります。この種の不正確さを解決するメカニズムは先読みです。 ANTLRで。パーサーメソッドは、 if’sの挿入されたチェーンです。 、そしてそれらのそれぞれは一歩先を見ています。この種の不確実性を生成する文法の例を参照してください:

    rule1:
      'a' rule2 | rule3
    ;
    
    rule2:
      'b' 'c' 'd'
    ;
    
    rule3:
      'b' 'c' 'e'
    ;
    

    ルール1の途中で、トークン「a」がすでに渡されている場合、パーサーは、従うルールを選択するために2つのステップを先読みします。このチェックはもう一度実行されますが、この文法を書き直して、先読みを除外することができます。 。欠点は、このような最適化によって構造が損なわれる一方で、パフォーマンスの向上はかなり小さいことです。

    この種の不確実性を解決するためのより複雑な方法があります。たとえば、構文述語(SynPred) ANTLR3のメカニズム 。句のオプションの終了が次のオプションの句の開始と交差する場合に役立ちます。

    ANTLR3に関して、述語は、代替案の1つに従って仮想テキストエントリを実行する生成されたメソッドです。 。成功すると、 trueを返します。 値、および述部の完了は成功します。仮想エントリの場合、バックトラッキングと呼ばれます モードエントリ。述語が正常に機能する場合、実際のエントリが発生します。

    述語が別の述語の内部で始まる場合にのみ問題になります。そうすると、1つの距離が数百回または数千回交差する可能性があります。

    簡単な例を見てみましょう。不確実性には3つのポイントがあります:(A、B、C)。

    1. パーサーはAに入り、テキスト内の位置を記憶し、レベル1の仮想エントリを開始します。
    2. パーサーはBに入り、テキスト内の位置を記憶し、レベル2の仮想エントリを開始します。
    3. パーサーはCに入り、テキスト内の位置を記憶し、レベル3の仮想エントリを開始します。
    4. パーサーはレベル3の仮想エントリを完了し、レベル2に戻り、もう一度Cを渡します。
    5. パーサーはレベル2の仮想エントリを完了し、レベル1に戻り、BとCをもう一度渡します。
    6. パーサーは仮想エントリを完了し、戻り、A、B、およびCを介して実際のエントリを実行します。

    その結果、C内のすべてのチェックは4回、B – 3回、A –2回以内に実行されます。

    しかし、適切な代替案がリストの2番目または3番目にある場合はどうなるでしょうか。次に、述語ステージの1つが失敗します。テキスト内のその位置がロールバックされ、別の述語が実行を開始します。

    アプリがフリーズする理由を分析するとき、 SynPredの痕跡に出くわすことがよくあります。 数千回実行されました。 SynPred s 再帰ルールでは特に問題があります。悲しいことに、SQLはその性質上再帰的です。ほとんどすべての場所でサブクエリを使用できることには、代償があります。ただし、ルールを操作して述語を削除することは可能です。

    SynPredはパフォーマンスを低下させます。ある時点で、彼らの数は厳格な管理下に置かれました。ただし、問題は、文法コードを作成するときに、SynPredがわかりにくいように見える可能性があることです。さらに、あるルールを変更すると、SynPredが別のルールに表示される可能性があり、そのため、それらを制御することは事実上不可能になります。

    単純な正規表現を作成しました 特別なMSBuildタスクによって実行される述語の数を制御するためのツール 。述語の数がファイルで指定された数と一致しなかった場合、タスクはすぐにビルドに失敗し、エラーについて警告しました。

    エラーが発生した場合、開発者はルールのコードを数回書き直して、冗長な述語を削除する必要があります。述語を避けられない場合、開発者はそれを特別なファイルに追加して、レビューのために特別な注意を引き付けます。

    まれに、ANTLRで生成された述語を避けるために、C#を使用して述語を記述したこともあります。幸い、この方法も存在します。

    文法の継承

    サポートされているDBMSに変更があった場合は、ツールでそれらに対応する必要があります。文法構文のサポートは常に出発点です。

    SQL方言ごとに特別な文法を作成します。コードの繰り返しが可能になりますが、共通点を見つけるよりも簡単です。

    文法の継承を行う独自のANTLR文法プリプロセッサを作成しました。

    また、ポリモーフィズムのメカニズム、つまり子孫のルールを再定義するだけでなく、基本的なルールを呼び出す機能が必要であることが明らかになりました。また、基本ルールを呼び出すときに位置を制御したいと思います。

    ANTLRを他の言語認識ツール、Visual Studio、およびANTLRWorksと比較すると、ツールは間違いなくプラスです。そして、継承を実装している間、この利点を失いたくありません。解決策は、ANTLR解説形式の継承された文法で基本的な文法を指定することでした。 ANTLRツールの場合、これは単なるコメントですが、必要なすべての情報をそこから抽出できます。

    ビルド前のアクションとしてビルドシステム全体に埋め込まれたMsBuildタスクを作成しました。タスクは、ベースおよび継承されたピアから結果の文法を生成することにより、ANTLR文法のプリプロセッサの仕事をすることでした。結果の文法はANTLR自体によって処理されました。

    ANTLR後処理

    多くのプログラミング言語では、キーワードをサブジェクト名として使用することはできません。方言に応じて、SQLには800から3000のキーワードがあります。それらのほとんどは、データベース内のコンテキストに関連付けられています。したがって、オブジェクト名としてそれらを禁止すると、ユーザーを苛立たせます。これが、SQLが予約済みおよび未予約のキーワードを持っている理由です。

    オブジェクトに引用符を付けずに予約語(SELECT、FROMなど)として名前を付けることはできませんが、予約語(CONVERSATION、AVAILABILITYなど)に対してこれを行うことはできます。この相互作用により、パーサーの開発が困難になります。

    字句解析中、コンテキストは不明ですが、パーサーはすでに識別子とキーワードに異なる番号を必要とします。そのため、ANTLRパーサーに別の後処理を追加しました。すべての明白な識別子チェックを特別なメソッドの呼び出しに置き換えました。

    この方法には、より詳細なチェックがあります。エントリが識別子を呼び出し、識別子がそれ以降に満たされることが期待される場合、それはすべて問題ありません。ただし、予約されていない単語がエントリである場合は、それを再確認する必要があります。この追加のチェックでは、この予約されていないキーワードがキーワードになる可能性がある現在のコンテキストでのブランチ検索を確認します。そのようなブランチがない場合は、識別子として使用できます。

    技術的には、この問題はANTLRを使用して解決できますが、この決定は最適ではありません。 ANTLRの方法は、予約されていないすべてのキーワードと語彙素識別子をリストするルールを作成することです。さらに、語彙素識別子の代わりに特別なルールが機能します。このソリューションにより、開発者は、キーワードが使用されている場所と特別なルールにキーワードを追加することを忘れないようになります。また、費やす時間を最適化します。

    ツリーなしの構文解析のエラー

    構文ツリーは通常、パーサーの作業の結果です。これは、形式文法を通じてプログラムテキストを反映するデータ構造です。言語のオートコンプリートを使用してコードエディタを実装する場合は、次のアルゴリズムを使用する可能性があります。

    1. エディターでテキストを解析します。次に、構文ツリーを取得します。
    2. キャリッジの下のノードを見つけて、文法と照合します。
    3. ポイントで利用できるキーワードとオブジェクトタイプを確認します。

    この場合、文法はグラフまたはステートマシンとして簡単に想像できます。

    残念ながら、dbForge IDEの開発が開始されたときは、ANTLRの3番目のバージョンしか利用できませんでした。ただし、それはそれほど機敏ではなく、ANTLRにツリーの構築方法を教えることはできましたが、使用法はスムーズではありませんでした。

    さらに、このトピックに関する多くの記事は、パーサーがルールを通過するときにコードを実行するための「アクション」メカニズムの使用を提案しました。このメカニズムは非常に便利ですが、アーキテクチャ上の問題が発生し、新しい機能のサポートがより複雑になっています。

    重要なのは、1つの文法ファイルが「アクション」の蓄積を開始したことです。これは、さまざまなビルドに分散する必要のある機能が多数あるためです。アクションハンドラーをさまざまなビルドに配布し、そのメジャーに対して卑劣なサブスクライバー通知パターンのバリエーションを作成することができました。

    私たちの測定によれば、ANTLR3はANTLR4よりも6倍高速に動作します。また、大きなスクリプトの構文ツリーはRAMを大量に消費する可能性があり、これは朗報ではありませんでした。そのため、VisualStudioとSQLManagementStudioの32ビットアドレス空間内で操作する必要がありました。

    ANTLRパーサーの後処理

    文字列を操作する場合、最も重要な瞬間の1つは、スクリプトを別々の単語に分割する字句解析の段階です。

    ANTLRは、言語を指定する入力文法を受け取り、使用可能な言語の1つでパーサーを出力します。ある時点で、生成されたパーサーは、デバッグすることを恐れるほど大きくなりました。デバッグ時にF11(ステップイン)を押してパーサーファイルに移動すると、VisualStudioがクラッシュします。

    パーサーファイルの分析時にOutOfMemory例外が原因で失敗したことが判明しました。このファイルには、200,000行を超えるコードが含まれていました。

    ただし、パーサーのデバッグは作業プロセスの重要な部分であり、省略できません。 C#部分クラスの助けを借りて、正規表現を使用して生成されたパーサーを分析し、それをいくつかのファイルに分割しました。 VisualStudioは完全に機能しました。

    SpanAPIの前に部分文字列を使用しない字句解析

    字句解析の主なタスクは分類です。単語の境界を定義し、辞書と照合します。単語が見つかった場合、レクサーはそのインデックスを返します。そうでない場合、その単語はオブジェクト識別子と見なされます。これはアルゴリズムの簡単な説明です。

    ファイルを開く際のバックグラウンドの字句解析

    構文の強調表示は、字句解析に基づいています。この操作は通常、ディスクからテキストを読み取る場合に比べてはるかに時間がかかります。キャッチは何ですか?あるスレッドでは、テキストがファイルから読み取られていますが、字句解析は別のスレッドで実行されています。

    レクサーはテキストを行ごとに読み取ります。存在しない行を要求すると、停止して待機します。

    BCLのBlockingCollectionも同様に機能し、アルゴリズムは同時生産者/消費者パターンの典型的なアプリケーションで構成されます。メインスレッドで作業しているエディターは、最初に強調表示された行に関するデータを要求します。それが利用できない場合は、停止して待機します。私たちのエディターでは、生産者/消費者パターンとブロッキングコレクションを2回使用しました:

    1. ファイルからの読み取りはプロデューサーであり、レクサーはコンシューマーです。
    2. レクサーはすでにプロデューサーであり、テキストエディターはコンシューマーです。

    この一連のトリックにより、大きなファイルを開くために費やす時間を大幅に短縮できます。ドキュメントの最初のページは非常にすばやく表示されますが、ユーザーが最初の数秒以内にファイルの最後に移動しようとすると、ドキュメントがフリーズする場合があります。これは、バックグラウンドリーダーとレクサーがドキュメントの最後に到達する必要があるために発生します。ただし、ユーザーがドキュメントの最初から最後に向かってゆっくりと移動する場合、目立ったフリーズは発生しません。

    あいまいな最適化:部分的な字句解析

    構文分析は通常、2つのレベルに分けられます。

    • 入力文字ストリームは、言語規則に基づいて語彙素(トークン)を取得するために処理されます。これは字句解析と呼ばれます
    • パーサーはトークンストリームを使用して、正式な文法規則に従ってそれをチェックし、多くの場合、構文ツリーを構築します。

    文字列処理はコストのかかる操作です。それを最適化するために、毎回テキストの完全な字句解析を実行するのではなく、変更された部分のみを再解析することにしました。しかし、ブロックコメントや行のような複数行の構成をどのように処理するのでしょうか?すべての行の行終了状態を保存しました:「複数行トークンなし」=0、「ブロックコメントの開始」=1、「複数行文字列リテラルの開始」=2。字句解析は、変更されたセクションから開始します。行末の状態が保存されている状態と等しくなると終了します。

    このソリューションには1つの問題がありました。行が挿入または削除されると、それに応じて次の行の番号を更新する必要があるため、このような構造の行番号を監視することは非常に不便ですが、行番号はANTLRトークンの必須属性です。トークンをパーサーに渡す前に、すぐに行番号を設定することで問題を解決しました。後で実行したテストでは、パフォーマンスが15〜25%向上したことが示されています。実際の改善はさらに大きかった。

    これらすべてに必要なRAMの量は、予想よりもはるかに多いことがわかりました。 ANTLRトークンは、開始点– 8バイト、終了点– 8バイト、単語のテキストへのリンク– 4または8バイト(文字列自体は言及していません)、ドキュメントのテキストへのリンク– 4または8バイト、およびトークンタイプ–4バイト。

    では、何を結論付けることができますか?パフォーマンスに重点を置き、予期しない場所でRAMを過剰に消費しました。クラスの代わりに軽量構造を使用しようとしたため、これが発生するとは想定していませんでした。それらを重いオブジェクトに置き換えることで、パフォーマンスを向上させるために、意図的に追加のメモリ費用をかけました。幸い、これは私たちに重要な教訓を教えてくれたので、今では各パフォーマンスの最適化はメモリ消費のプロファイリングで終わり、その逆も同様です。

    これは道徳的な話です。一部の機能はほぼ瞬時に機能し始め、他の機能は少し速く動作し始めました。結局のところ、スレッドの1つがトークンを格納できるオブジェクトがなければ、バックグラウンドの字句解析トリックを実行することは不可能です。

    .NETスタックでのデスクトップ開発のコンテキストでは、さらにすべての問題が発生します。

    32ビットの問題

    一部のユーザーは、当社製品のスタンドアロンバージョンを使用することを選択します。 VisualStudioおよびSQLServerManagementStudio内での作業に固執する人もいます。それらのために多くの拡張機能が開発されています。これらの拡張機能の1つはSQLCompleteです。明確にするために、標準のCodeCompletionSSMSおよびVSforSQLよりも多くの機能と機能を提供します。

    SQL解析は、CPUリソースとRAMリソースの両方の観点から、非常にコストのかかるプロセスです。サーバーを不必要に呼び出さずに、ユーザースクリプトでオブジェクトのリストを表示するために、オブジェクトキャッシュをRAMに保存します。多くの場合、それは多くのスペースを占有しませんが、一部のユーザーは最大25万のオブジェクトを含むデータベースを持っています。

    SQLの操作は、他の言語の操作とはまったく異なります。 C#では、1000行のコードがあっても実質的にファイルはありません。一方、SQLでは、開発者は数百万行のコードで構成されるデータベースダンプを操作できます。珍しいことは何もありません。

    DLL-VS内の地獄

    .NETFrameworkでプラグインを開発するための便利なツールがあります。これはアプリケーションドメインです。すべてが分離された方法で実行されます。荷降ろしが可能です。ほとんどの場合、拡張機能の実装が、おそらく、アプリケーションドメインが導入された主な理由です。

    また、プログラムへのアドオン作成の問題を解決するためにMSによって設計されたMAFフレームワークがあります。これらのアドオンを分離して、別のプロセスに送信し、すべての通信を引き継ぐことができるようにします。率直に言って、このソリューションは面倒であまり人気がありません。

    残念ながら、MicrosoftVisualStudioとその上に構築されたSQLServerManagement Studioは、拡張システムの実装が異なります。プラグインのアクセスホスティングアプリケーションを簡素化しますが、プラグインを1つのプロセス内で、別のプロセスとドメイン内で一緒に適合させる必要があります。

    21世紀の他のアプリケーションと同様に、私たちのアプリケーションには多くの依存関係があります。それらの大部分は、.NETの世界でよく知られており、実績があり、人気のあるライブラリです。

    ロック内のメッセージのプル

    .NETFrameworkがすべてのWaitHandle内にWindowsメッセージキューを送り込むことは広く知られていません。すべてのロック内に配置するために、このロックがカーネルモードに切り替わる時間があり、スピン待機フェーズ中に解放されない場合は、アプリケーション内の任意のイベントのハンドラーを呼び出すことができます。

    これにより、非常に予期しない場所で再入可能になる可能性があります。数回、「列挙中にコレクションが変更された」やさまざまなArgumentOutOfRangeExceptionなどの問題が発生しました。

    SQLを使用してソリューションにアセンブリを追加する

    プロジェクトが大きくなると、最初は単純なアセンブリを追加するタスクが、数十の複雑なステップに発展します。かつて、ソリューションに数十の異なるアセンブリを追加する必要があったため、大規模なリファクタリングを実行しました。製品やテストソリューションを含む約80のソリューションが、約300の.NETプロジェクトに基づいて作成されました。

    製品ソリューションに基づいて、InnoSetupファイルを作成しました。それらには、ユーザーがダウンロードしたインストールにパッケージ化されたアセンブリのリストが含まれていました。プロジェクトを追加するアルゴリズムは次のとおりです。

    1. 新しいプロジェクトを作成します。
    2. 証明書を追加します。ビルドのタグを設定します。
    3. バージョンファイルを追加します。
    4. プロジェクトが進むパスを再構成します。
    5. 内部仕様に一致するようにフォルダの名前を変更します。
    6. プロジェクトをもう一度ソリューションに追加します。
    7. すべてのプロジェクトにリンクが必要なアセンブリをいくつか追加します。
    8. 必要なすべてのソリューション(テストと製品)にビルドを追加します。
    9. すべての製品ソリューションについて、アセンブリをインストールに追加します。

    これらの9つのステップを約10回繰り返す必要がありました。手順8と9はそれほど簡単ではなく、どこにでもビルドを追加するのを忘れがちです。

    このような大きくて日常的なタスクに直面すると、通常のプログラマーはそれを自動化したいと思うでしょう。それがまさに私たちがやりたかったことです。しかし、新しく作成されたプロジェクトに追加するソリューションとインストールを正確に示すにはどうすればよいでしょうか。非常に多くのシナリオがあり、さらに、それらのいくつかを予測することは困難です。

    私たちはクレイジーなアイデアを思いついた。ソリューションは、多対多のようなプロジェクト、同じようにインストールされたプロジェクトに接続されており、SQLは私たちが持っていた種類のタスクを正確に解決できます。

    ソースフォルダー内のすべての.slnファイルをスキャンし、DotNet CLIを使用してプロジェクトのリストを取得し、SQLiteデータベースに配置する.Netコアコンソールアプリを作成しました。プログラムにはいくつかのモードがあります:

    • 新規–プロジェクトと必要なすべてのフォルダーを作成し、証明書を追加し、タグを設定し、バージョンを追加し、最低限必要なアセンブリを追加します。
    • Add-Project –パラメーターの1つとして指定されるSQLクエリを満たすすべてのソリューションにプロジェクトを追加します。プロジェクトをソリューションに追加するために、内部のプログラムはDotNetCLIを使用します。
    • Add-ISS –SQLクエリを満たすすべてのインストールにプロジェクトを追加します。

    SQLクエリを使用してソリューションのリストを示すというアイデアは面倒に思えるかもしれませんが、既存のすべてのケースと、おそらく将来発生する可能性のあるすべてのケースを完全に閉じました。

    シナリオを示しましょう。プロジェクトを作成する「A」 プロジェクトが「B」であるすべてのソリューションに追加します 使用される:

    dbforgeasm add-project Folder1\Folder2\A "SELECT s.Id FROM Projects p JOIN Solutions s ON p.SolutionId = s.Id WHERE p.Name = 'B'"

    LiteDBの問題

    数年前、ユーザードキュメントを保存するためのバックグラウンド関数を開発するタスクがありました。主なアプリケーションフローは2つありました。IDEをすぐに閉じて終了する機能と、中断したところから再開する機能と、停電やプログラムのクラッシュなどの緊急の状況で復元する機能です。

    このタスクを実装するには、ファイルの内容を横のどこかに保存し、頻繁かつ迅速に実行する必要がありました。内容とは別に、メタデータを保存する必要があったため、ファイルシステムに直接保存するのは不便でした。

    その時点で、そのシンプルさとパフォーマンスに感銘を受けたLiteDBライブラリを見つけました。 LiteDBは、完全にC#で記述された、高速で軽量な組み込みデータベースです。スピードと全体的なシンプルさが私たちを魅了しました。

    開発プロセスの過程で、チーム全体がLiteDBでの作業に満足しました。ただし、主な問題はリリース後に始まりました。

    公式ドキュメントは、データベースが複数のスレッドおよび複数のプロセスからの同時アクセスで適切に機能することを保証しました。積極的な合成テストでは、データベースがマルチスレッド環境で正しく機能しないことが示されました。

    この問題をすばやく修正するために、自己作成のプロセス間ReadWriteLockを使用してプロセスを同期しました。現在、ほぼ3年後、LiteDBははるかにうまく機能しています。

    StreamStringList

    この問題は、部分字句解析の場合とは逆です。テキストを操作するときは、文字列リストとして操作する方が便利です。文字列はランダムな順序で要求できますが、特定のメモリアクセス密度は依然として存在します。ある時点で、メモリを完全にロードせずに非常に大きなファイルを処理するには、いくつかのタスクを実行する必要がありました。アイデアは次のとおりです。

    1. ファイルを1行ずつ読み取る。ファイル内のオフセットを覚えておいてください。
    2. リクエストに応じて、必要なオフセットを設定した次の行を発行し、データを返します。

    メインタスクが完了しました。この構造は、ファイルサイズに比べて多くのスペースを占有しません。テスト段階では、大きなファイルと非常に大きなファイルのメモリフットプリントを徹底的にチェックします。大きなファイルは長い間処理され、小さなファイルはすぐに処理されます。

    実行時間を確認するための参照はありませんでした 。 RAMはランダムアクセスメモリと呼ばれ、SSD、特にHDDよりも競争上の優位性があります。これらのドライバーは、ランダムアクセスに対してうまく機能しなくなります。このアプローチでは、ファイルをメモリに完全にロードする場合と比較して、作業が約40倍遅くなることが判明しました。さらに、コンテキストに応じて、ファイルを2,5-10回フルタイムで読み取ります。

    解決策は単純で、ファイルがメモリに完全にロードされたときよりも操作に少し時間がかかるように改善するだけで十分でした。

    同様に、RAMの消費も重要ではありませんでした。 RAMからキャッシュプロセッサにデータをロードするという原則に着想を得ました。配列要素にアクセスすると、必要な要素が近くにあることが多いため、プロセッサは数十の隣接する要素をキャッシュにコピーします。

    多くのデータ構造は、このプロセッサ最適化を使用して最高のパフォーマンスを実現します。配列要素へのランダムアクセスがシーケンシャルアクセスよりもはるかに遅いのは、この特殊性のためです。同様のメカニズムを実装しました。1000個の文字列のセットを読み取り、それらのオフセットを記憶しました。 1001番目の文字列にアクセスするとき、最初の500文字列を削除し、次の500をロードします。最初の500行のいずれかが必要な場合は、すでにオフセットがあるため、個別に移動します。

    プログラマーは、必ずしも非機能要件を注意深く策定してチェックする必要はありません。その結果、将来のケースのために、永続メモリを順番に処理する必要があることを思い出しました。

    例外の分析

    Web上でユーザーアクティビティデータを簡単に収集できます。ただし、デスクトップアプリケーションの分析には当てはまりません。グーグルアナリティクスのような信じられないほどのメトリックと視覚化ツールのセットを与えることができるそのようなツールはありません。なんで?ここに私の仮定は次のとおりです:

    1. デスクトップアプリケーション開発の歴史の大部分を通じて、彼らはWebへの安定した永続的なアクセスを持っていませんでした。
    2. デスクトップアプリケーション用の開発ツールはたくさんあります。したがって、すべてのUIフレームワークとテクノロジーに対応する多目的ユーザーデータ収集ツールを構築することは不可能です。

    データ収集の重要な側面は、例外を追跡することです。たとえば、クラッシュに関するデータを収集します。以前は、ユーザーはカスタマーサポートのメールに自分で書き込む必要があり、特別なアプリウィンドウからコピーされたエラーのスタックトレースを追加していました。これらすべての手順を実行したユーザーはほとんどいませんでした。収集されたデータは完全に匿名化されているため、複製手順やその他の情報をユーザーから見つける機会がありません。

    一方、エラーデータはPostgresデータベースにあり、これにより、数十の仮説を即座にチェックすることができます。データベースに対してSQLクエリを実行するだけで、すぐに答えを得ることができます。多くの場合、1つのスタックまたは例外タイプから、例外がどのように発生したかが不明です。そのため、このすべての情報が問題を調査するために重要です。

    それに加えて、収集されたすべてのデータを分析し、最も問題のあるモジュールとクラスを見つける機会があります。分析の結果に基づいて、プログラムのこれらの部分をカバーするためのリファクタリングまたは追加のテストを計画できます。

    スタックデコードサービス

    .NETビルドにはILコードが含まれており、いくつかの特別なプログラムを使用して、オペレーターが正確にC#コードに簡単に変換できます。プログラムコードを保護する方法の1つは、その難読化です。プログラムの名前を変更できます。メソッド、変数、およびクラスは置き換えることができます。コードは同等のものに置き換えることができますが、実際には理解できません。

    ユーザーがアプリケーションのビルドを取得することを示唆する方法で製品を配布する場合、ソースコードを難読化する必要があります。デスクトップアプリケーションはそのような場合です。テスターの中間ビルドを含むすべてのビルドは、慎重に難読化されています。

    当社の品質保証ユニットは、難読化ツールの開発者によるデコードスタックツールを使用しています。デコードを開始するには、アプリケーションを実行し、特定のビルド用にCIによって公開された難読化解除マップを見つけて、入力フィールドに例外スタックを挿入する必要があります。

    さまざまなバージョンとエディターがさまざまな方法で難読化されていたため、開発者は問題を調査することが困難であり、間違った方向に進む可能性さえありました。このプロセスを自動化する必要があることは明らかでした。

    難読化解除マップの形式は非常に単純であることが判明しました。簡単に解析を解除して、スタックデコードプログラムを作成しました。その少し前に、製品バージョンごとに例外をレンダリングし、スタックごとにグループ化するためのWebUIが開発されました。これは、SQLiteのデータベースを備えた.NETCoreWebサイトでした。

    SQLiteは、小規模なソリューション向けの優れたツールです。そこにも難読化解除マップを配置しようとしました。すべてのビルドで、約50万の暗号化と復号化のペアが生成されました。 SQLiteはそのような積極的な挿入率を処理できませんでした。

    1つのビルドのデータがデータベースに挿入されている間に、さらに2つのビルドがキューに追加されました。その問題の少し前に、私はClickhouseに関するレポートを聞いていて、それを試してみたいと思っていました。優れていることが証明され、挿入率は200倍以上高速化されました。

    とはいえ、スタックのデコード(データベースからの読み取り)は50倍近く遅くなりましたが、各スタックの所要時間は1ミリ秒未満であったため、この問題の調査に時間を費やすことは費用効果がありませんでした。

    ML.NET for classification of exceptions

    On the subject of the automatic processing of exceptions, we made a few more enhancements.

    We already had the Web-UI for a convenient review of exceptions grouped by stacks. We had a Grafana for high-level analysis of product stability at the level of versions and product lines. But a programmer’s eye, constantly craving optimization, caught another aspect of this process.

    Historically, dbForge line development was divided among 4 teams. Each team had its own functionality to work on, though the borderline was not always obvious. Our technical support team, relying on their experience, read the stack and assigned it to this or that team. They managed it quite well, yet, in some cases, mistakes occurred. The information on errors from analytics came to Jira on its own, but the support team still needed to classify tasks by team.

    In the meantime, Microsoft introduced a new library – ML.NET. And we still had this classification task. A library of that kind was exactly what we needed. It extracted stacks of all resolved exceptions from Redmine, a project management system that we used earlier, and Jira, which we use at present.

    We obtained a data array that contained some 5 thousand pairs of Exception StackTrace and command. We put 100 exceptions aside and used the rest of the exceptions to teach a model. The accuracy was about 75%. Again, we had 4 teams, hence, random and round-robin would only attain 25%. It sufficiently saved up their time.

    To my way of thinking, if we considerably clean up incoming data array, make a thorough examination of the ML.NET library, and theoretical foundation in machine learning, on the whole, we can improve these results. At the same time, I was impressed with the simplicity of this library:with no special knowledge in AI and ML, we managed to gain real cost-benefits in less than an hour.

    結論

    Hopefully, some of the readers happen to be users of the products I describe in this article, and some lines shed light on the reasons why this or that function was implemented this way.

    And now, let me conclude:

    We should make decisions based on data and not assumptions. It is about behavior analytics and insights that we can obtain from it.

    We ought to constantly invest in tools. There is nothing wrong if we need to develop something for it. In the next few months, it will save us a lot of time and rid us of routine. Routine on top of time expenditure can be very demotivating.

    When we develop some internal tools, we get a super chance to try out new technologies, which can be applied in production solutions later on.

    There are infinitely many tools for data analysis. Still, we managed to extract some crucial information using SQL tools. This is the most powerful tool to formulate a question to data and receive an answer in a structured form.


    1. MySQL外部キーエラー1005errno150外部キーとしての主キー

    2. 誤ってpostgresのデフォルトのスーパーユーザー権限を削除しました-元に戻すことはできますか?

    3. SQLiteで2つの日付の差を計算する方法

    4. ゲームの先を行くSQLServerパフォーマンスメトリクス