糞コードのリリースは許されるのか?

糞コードをリリースすることの是非みたいな発言をXで見かけたので私見を纏めておく。

糞コードの問題は、理解するのに時間が掛かるとか、改修が困難とか、バグを出しやすいとか色々と言われるけど、纏めるとランニングコストが高いと言うことになる。単純にコードの保守性だけの問題では無くて、処理が最適化されていないために多くのコンピューターリソースが必要となるといった事も含まれます。

ランニングコストが高いと何が不味いのかを、簡易的にグラフにしてみた。

時間経過とともに機能が追加されたりデータが増えたりするため、システム規模は日に日に大きくなり、累積コストは指数的に増加していきます。糞コードは初期コストが安くても運用コストが高くなります。対してクールコードは初期コストが高くても運用コストが低く抑えられています。初期は糞コードの方が運用コストが低く済んでいますが、いずれクールコードと逆転するわけです。

糞コードの収益性が悪いだけなら良いのですが、場合によっては採算ラインを超えてしまう事が起こります。独占企業なら問題にならないかもしれません。ですが競合他社が居る場合、運用コストの高さは長期的にビジネスの継続を危うくするわけです。

糞コードは後で書き換えれば良いという主張もあります。ビジネスとして継続する為に、システムを再構築してクールコードに書き換えるわけです。ですがこれは開発コストを二重に負担しているので、コストが大幅に増えて収益を圧迫します。

特に新規のビジネスの場合には資金的にも人員的にも余裕がないため、実際には糞コードの再構築は後回しになりがちです。再構築が遅延すると、その間にコードもデータも増えていくため、再構築にかかるコストがより大きくなります。最悪の場合、再構築を行うと採算ラインを越えるため出来ないが、このままだと運用コストが採算ラインを越えるため放置も出来ないという、詰んだ状態に陥るわけです。

以上のことから、糞コードのリリースについて三行に纏めておく。

  • 時間的な制約から糞コードをリリースする場合はあり得る。
  • だが糞コードは最小限にし、速やかに改修しておく必要がある。
  • 糞コードの放置はビジネスを終わらせる時限爆弾となり得る。

SeleniumからChromeのインスタンス作成時にエラー

OpenQA.Selenium.WebDriverException: 'disconnected: Unable to receive message from renderer
  (failed to check if window was closed: disconnected: not connected to DevTools)
  (Session info: chrome=119.0.6045.124)'

C#環境でSeleniumからChromeのインスタンスを作成しようとすると上記のエラーが発生する。

この場合は下記の通り”–no-sandbox”オプションを追記すると問題が解決する場合がある。どうもコンソールリダイレクトのパイプライン処理などが引っかかる場合があるらしい。

using OpenQA.Selenium.Chrome;
using OpenQA.Selenium;
・・・
var options = new OpenQA.Selenium.Chrome.ChromeOptions();
options.AddArgument("--no-sandbox");
var newWebDriver = new ChromeDriver(options);

sandboxはWEB閲覧中のScript動作などの安全性を担保する上で重要な機能になる。接続先WEBサイトに不正なScriptが置かれていた場合、損失を受ける可能性もあるので注意が必要になる。Firefoxなど別のブラウザに移行した方が良いのかもしれない。

習作:FacebookのLikeボタンをクリックする

C#でSeleniumを用いて、Facebookのページ内にある「いいね!」を自動的にクリックするサンプル。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

using OpenQA.Selenium;

namespace LikeClicker
{
    class Program
    {
        static void Main(string[] args)
        {
            string userID = "test@example.co.jp";
            string password = "password";

            // AppSettings.BrowserName.Firefoxを変更することによって対象のブラウザを変更できます
            using (IWebDriver webDriver = WebDriverFactory.CreateInstance(AppSettings.BrowserName.HeadlessChrome))
            {
                webDriver.Url = @"https://www.facebook.com/";

                IWebElement userIdElement = webDriver.FindElement(By.Id("email"));
                IWebElement passwordElement = webDriver.FindElement(By.Id("pass"));

                userIdElement.SendKeys(userID);
                passwordElement.SendKeys(password);
                userIdElement.Submit();
                Console.WriteLine("認証が終了したら任意のキーを押して下さい。");
                Console.Read();

                string[] followURL = {
                        "https://www.facebook.com/pg/username1/posts/?ref=page_internal",
                        "https://www.facebook.com/pg/username2/posts/?ref=page_internal"};
                while (true)
                {
                    foreach (var tergetURL in followURL)
                    {
                        webDriver.Url = tergetURL;
                        Thread.Sleep(5000);

                        int findCount = 0;
                        foreach (var divElement in webDriver.FindElements(By.ClassName("_37uu")))
                        {
                            foreach (var refElement in divElement.FindElements(By.ClassName("UFILikeLink")))
                            {
                                if (refElement.GetAttribute("class").IndexOf("UFILinkBright") <= 0)
                                {
                                    refElement.Click();
                                    Thread.Sleep(3000);
                                }
                            }
                        }
                    }

                    Thread.Sleep(1000 * 60 * 10);
                }
            }
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Edge;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.IE;
using OpenQA.Selenium.Safari;

namespace LikeClicker
{
    internal class WebDriverFactory
    {
        public static IWebDriver CreateInstance(AppSettings.BrowserName browserName)
        {
            switch (browserName)
            {
                case AppSettings.BrowserName.None:
                    throw new ArgumentException(string.Format("Not Definition. BrowserName:{0}", browserName));

                case AppSettings.BrowserName.Chrome:
                    return new ChromeDriver();

                case AppSettings.BrowserName.HeadlessChrome:
                    ChromeOptions option = new ChromeOptions();
                    option.AddArgument("--headless");
                    return new ChromeDriver(option);

                case AppSettings.BrowserName.Firefox:
                    FirefoxDriverService driverService = FirefoxDriverService.CreateDefaultService();
                    driverService.FirefoxBinaryPath = @"C:\Program Files (x86)\Mozilla Firefox\firefox.exe";
                    driverService.HideCommandPromptWindow = true;
                    driverService.SuppressInitialDiagnosticInformation = true;
                    return new FirefoxDriver(driverService);

                case AppSettings.BrowserName.InternetExplorer:
                    return new InternetExplorerDriver();

                case AppSettings.BrowserName.Edge:
                    return new EdgeDriver();

                case AppSettings.BrowserName.Safari:
                    return new SafariDriver();

                default:
                    throw new ArgumentException(string.Format("Not Definition. BrowserName:{0}", browserName));
            }
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LikeClicker
{
    internal static class AppSettings
    {
        public enum BrowserName
        {
            None,
            Chrome,
            HeadlessChrome,
            Firefox,
            InternetExplorer,
            Edge,
            Safari
        }
    }
}

Microsoft Flowの実行回数

無料の場合の実行回数は750回/月です。

右上の設定ボタンから残回数を確認できます。flow_jikkou_kaisuu

原則としてトリガーが起動される毎に1回としてカウントされます。

Microsoft Flowは無料版の場合15分毎(有料版の場合は1分毎)にトリガーの状態を確認し、トリガー条件を満たしていれば実行します。

トリガー条件を満たしていなければ、実行せずにスキップします。スキップした場合は実行回数にカウントしません。

ちょっと困ったことにエラーが発生してFailedとなったときにもカウントされます。

特にトリガーでエラーとなった場合が困りもので、15分毎なので1日で96回消費してしまいます。これ、無料版だから96回で済んでいますけど、有料版だと毎分トリガー条件をチェックするので、1日で1440回消費してしまうことになります。スクリプトを登録した場合は、実行結果に注意してみておくようにしましょう。

・・・というか、私自身もトリガーでエラーが発生したせいで、今現在は今月の実行数上限まで消費してしまってもう動かない(T_T)

Microsoft FlowにあるGoogleスプレッドシートのUpdate Row/Get Rowの使い方

Microsoft FlowをGoogle スプレッドシートと連携しようとしたけどRow idの指定などが全く分からなかったので覚え書き。

GoogleスプレッドシートのUpdate row/Get rowの組み合わせはFlowスクリプトを組む上で強力なツールになります。と言うのもFlow自体には正規表現置換や演算機能がありません。Googleスプレッドシートの数式と組み合わせて使うことで、スプレッドシートの関数群による、正規表現置換や文字列編集、演算式を使って結果を取得する事ができるからです。

まず最初にGoogle Drive上でスプレッドシートを新規に作成します。先頭の1行目はデータを識別するためのカラム名として使われます。適当な名前を設定して下さい。flow_spread_sheet_1

新しいステップから「Googleスプレッドシート Update Row」を選択して追加します。flow_update_row_1

Fileの項目は右側のフォルダアイコンをクリックするとGoogle Driveのフォルダ構成が表示されます。フォルダをたどりながら先に作成したスプレッドシートを選択して下さい。Fileを選択後にWorksheetを選ぶとシート名の一覧がドロップダウンするので、対象となるワークシートを選びます。flow_update_row_2

シートを選択すると先ほどスプレッドシートを作成したときに1行目に入力したカラム名が表示されます。
分かりにくいのが「Row id」です。行を特定するための任意の文字列を指定します。ここでは取り急ぎ「_DATA_1_」としましょう。

ここで最初に作成したスプレッドシートに戻ります。するとセルD1に「__PowerAppsId__」と言う項目が追加されています。「Update Row」ステップは「__PowerAppsId__」カラムが「Row id」に指定した文字列と一致する行に対して更新します。ここではセルD2を「_DATA_1_」としておきます。flow_spread_sheet_2

この状態でFlowを動作させると、スプレッドシートの2行目に、「Update Row」ステップの「カラム1~3」に指定した値が入力されます。

ここまで来ると「Get row」で何を指定すれば良いかなんとなく分かりますね。次のように指定するとSampleスプレッドシートの2行目の内容が読み込まれます。カラムに計算式が入力されていた場合、「Get row」は計算結果を取得します。これを利用してFlow単独では出来ない演算機能を実装することができます。flow_get_row

もし3行目の内容が読み込みたければ、スプレッドシートのセルD3に「_DATA_2_」といった文字列を入力しておいて、「Get row」ステップの「Row id」に「_DATA_2_」と指定すれば良いです。

Microsoft Flowの基本文法

Microsoft Flowのプログラム制御に使われるステップです。スクリプト言語として考えると命令の種類が非常に少ないですが、必要十分な機能を厳選して実装しており、大抵のロジックは組むことができます。演算や文字列を扱う命令は提供されていませんが、その辺りは外部のサービスを呼び出す前提で設計されているの感じです。

1.変数

数値型の変数はありません。文字列型の変数として作成ステップが用意されています。

1.1.作成(Compose string variables)

flow_create_step作成ステップはMicrosoft Flowにおける唯一の変数ですが、文字列しか格納できません。例えば同じ内容を複数のSNSに書き込むような処理において、それぞれのSNSのステップにおいて文字列の結合処理を行うのはあまりにも冗長です。これを避けるために整形済みの文字列を一時的に格納するテンポラリとして実装されています。単純な文字列の結合処理を行うだけで、書式指定などの機能も持ち合わせていません。

2.数値演算命令

ありません。数値演算が必要な場合には外部のサービスを利用する事になります。

3.論理演算命令

ありません。
条件ステップの組み合わせによって論理処理を行う事になります。
それ以上の複雑な論理演算が必要であれば、外部のサービスを利用することになります。

4.流れ制御命令

流れ制御命令は以下の5つがあります。必要最低限の命令しか用意されていませんが、基本的なスクリプトを作成するには十分な命令が用意されています。

4.1.条件(Nested conditionals)

flow_nasted_step言わずとしれた条件分岐です。

4.2.それぞれへの適用(Apply to each)

flow_apply_step配列の要素を列挙して、それぞれの要素に対して処理を記述できます。

4.3.Do Until(これだけ日本語に訳されていない)

flow_until_step脱出条件付きのループです。

4.4.配列のフィルター処理(Filter arrays)

flow_filter_step「それぞれへの適用」と「条件」を組み合わせても実装できますが、負荷を考えると処理の最初の方で「配列のフィルター」で件数を絞った方が良いでしょう。

4.5.終了(Abort)

flow_abort_step異常終了として処理を中断できます。

Threadを使用した処理の高速化

HadoopやGPGPU、FPGAなんかを使うほどじゃないけど、Many CoreのCPUを最大限に使うための考え方とか覚え書きしておく。

1.スレッドの構成

マルチスレッドで効率を高めるためには同じデータやデバイスへのアクセスが少なく、各スレッドの独立性が高いことが大事になる。もし独立性が低いと、同じデバイスやデータにアクセスすることによって、同期や排他、あるいはスヌーピング(CPUのキャッシュメモリの保持している内容と、メインメモリ上の内容が一致しなくなった時に、この同期をとるための動作)が発生して効率が上がらない。

マルチスレッドで効率良くデータを処理するためには、の設計パターンは3種類に集約される。

1.1.並列化(パラレル)

パラレル
同一の処理を行うスレッドを複数作成する事で、スレッドに処理を分散する。言語レベルでサポートされている場合もあり、対応するのは容易。その代わり処理内容によってはスレッドの独立性が低くなり、効率が上がりにくい。

1.2.直列化(パイプライン)

パイプライン
一連の処理を何段階かのステップに分けて、ステップごとにスレッドを作成する。手前の処理を行うスレッドから、次の処理を行うスレッドへとデータを受け渡すことで処理を行う。各ステップごとの処理にかかる時間が一様ではないため、効率が上がりにくい。実際には並列化とあわせて、ステップ毎の処理量の差を吸収させるようになる。

1.3.星型(スター)

スター
複数のデータ処理を行うスレッドと、データ処理を行うスレッドに対して処理要求を送るスレッドにて構成する。各スレッドのデータ処理量が一様で無くとも処理待ちになりにくく、スレッドの独立性も高いように設計しやすい。ただし処理速度が向上すると、処理要求を送るスレッドの処理能力がボトルネックとなるため、性能向上が頭打ちになる。

2.データの保持

HDDたデータベースなど低速なデバイスへのアクセスは大きなペナルティとなるので、可能な限りデータをメモリ上に保持する。メモリ上に保持したデータへの検索処理はハッシュテーブル等を使うとよい。最近はインメモリデータベースも多様な選択肢があるので検討する。

マルチスレッドプログラミングとvolatile

マルチスレッドを使った最適化のWEB記事を続けて見かけたのだが、みんなvolaileについてはスルーしているので補足してみる。volatileは変数単位でコンパイラの最適化機能を無効にする修飾詞です。C++にもC#にもJavaにも、主要な言語には大抵用意されています。volatile宣言を忘れると、Releaseビルドでしか発生しない、再現性の低い、達の悪いバグに襲われる事になります。
何故volatile宣言が必要なのかと言うと、最近のコンパイラは高度に最適化作業をおこないます。その結果として、プログラマが記述したとおりの順序で処理を実行しない事もあります。マルチスレッドで特に問題になるのが、命令の順番の入れ替えや、演算処理のループ外への移動です。
例えばループの中でA*B*2という計算をしていたとします。コンパイラは局所的に見て、ループ内で変数Bが変更される可能性が無い事を判断した場合、B*2演算をループの外に移動したりします。すると別スレッドで変数Bの値を変更しても、他スレッドのループ内の演算には反映されないという事になってしまいます。

コンパイラが最適化する前:
int a, b, d;
void funcA(void)
{
while (true)
{
a = funcB();
d = a * b * 2;
}
}

コンパイラが最適化した後:
int a, b, d;
void funcA(void)
{
int e = b * 2;
while (true)
{
a = funcB();
d = a * e;
}
}

実はVisual C++やC#ではstatic変数やグローバル変数を無条件にvolatileな変数として扱うようになっています。スレッド間で共有するような変数の多くはstatic変数やグローバル変数ですから、殆どの場合volatile宣言を忘れていても動いてしまいます。でもそのために、ケアが必要だと言う事も忘れがちなのです。

スタックダンプの読み方

レジスタのEBPを基準にして読み解いていきます。EBPで示されるアドレスの手前4バイトが呼び出し元アドレス、EBP以降が引数です。EBPで示される場所に格納されている4バイトが関数呼び出し前のEBPアドレスです。
EBPが0x00C00018の場合を例としてスタックを読み解きます。

0x00C00010 : 00 00 00 04 // 引数、または、スタック変数
0x00C00014 : 00 00 00 03 // 引数、または、スタック変数
0x00C00018 : 00 C0 00 28 // 手前のEBPアドレス
0x00C0001C : 00 40 10 40 // 呼び出し元アドレス
0x00C00020 : 00 00 00 02 // 引数、または、スタック変数
0x00C00024 : 00 00 00 01 // 引数、または、スタック変数
0x00C00028 : 00 C0 00 40 // 手前のEBPアドレス
0x00C0002C : 00 40 10 80 // 呼び出し元アドレス

cdecl呼び出し規約や、stdcall呼び出し規約では、引数は右に書かれているものから順にスタックに格納されます。したがってアドレスの大きいものから順に、右から左の引数となります。Pascal呼び出し規約では逆に左から順に格納されます。
C++の場合this ポインタがスタックに追加されます。thisポインタはすべての引数をスタックに格納した後、最後にスタックに積まれます。Windowsでは関数が引数を伴わない場合には、thisポインタをスタックに積まずに、ECXレジスタに格納して関数を呼び出す場合があります。
スタック変数の積まれる順序は保障されません。最適化の結果、順不同にスタックに積まれます。

スタックの取得(ネィテブデバッガを作る2)

デバッグを行う上で実行位置に続いて重要なのがスタックの情報です。
別プロセスのスタック情報を取得するには、ReadProcessMemory APIを使用します。「ネィティブデバッガを作る」で取得したCONTEXT構造体のEspレジスタで示されるアドレス以降のデータを読み出します。

BYTE stackPointer[64];
DWORD readedSize(0);
HANDLE hProcess(0);
hProcess = ::OpenProcess(PROCESS_VM_READ, FALSE, debugEvent->dwProcessId);
if (NULL != hProcess)
{
// スタック領域からメモリを読み出す
if (::ReadProcessMemory(hProcess, (LPVOID )context.Esp, &readBuff, sizeof(readBuff), &readedSize))
{
// スタックダンプを表示する
}
}

ReadProcessMemory APIで確保されていないメモリ領域にアクセスすると、デバッグ対象となっているプロセスで不正なメモリアクセスがおこりDebugEventが発生します。ReadProcessMemoryで不正なメモリアクセスを起こし、DebugEventで再びReadProcessMemoryを呼び出し・・・という無限ループに陥らないように注意が必要です。
ReadProcessMemoryの第二パラメータや第三パラメータで割り当てられていないメモリを指定しても、ReadProcessMemoryはエラーを返さずに正常終了します。ReadProcessMemoryがエラーになるか否かで、正しくメモリを読み出せたか判断することはできません。