かまたま日記3

プログラミングメイン、たまに日常

Chromeで特定のドメインのアクセスをリダイレクトさせる

Twitterのブックマーク機能は現在PC版のサイトでは提供されておらず、mobile.twitter.com を使ったモバイル版でアクセスする必要があります。

An easier way to save and share Tweets

Bookmarks are now rolling out globally on Twitter for iOS and Android, Twitter Lite, and mobile.twitter.com.

なので、外部のサイトから特定のツイートのリンクを踏んでそれをブックマークしたいとなると、ブラウザバーでドメインmobile. 加えてという作業が発生します。いちいち面倒だったので、ブラウザの設定でリダイレクトできないか色々調べると、こちらの記事が当たりました、2018-6-28公開、タイムリー!

www.lifehacker.jp

ということで、最初に紹介されているSwitcherooを今は使ってます。設定はこんな感じです。

f:id:kamatama_41:20180701232636p:plain

最初は from: twitter.com to: mobile.twitter.com としていたらうまく動きませんでした。部分一致で探しているようで mobile.twitter.com でアクセスしたときも mobile.mobile.twitter.com にアクセスしようとしているようでした。最終的にURLスキームをつけることで解決しました。

続きを読む

Parallel Streamの並列数を調整する

Streamの parallel メソッドを呼ぶとストリームの処理を並列に実行できますが、これは内部的には前回紹介したForkJoinPoolが使われています。ForkJoinPoolは内部でcommon poolと呼ばれる共通プールを持っており、明示的にPoolを指定しない ForkJoinTask#invoke などはこのプールが利用されるようです。

public static void main(String[] args) {
    IntStream.range(0, 16).parallel()
            .forEach(i -> System.out.println(Thread.currentThread().getName() + ": " + i));
}

自分のマシン (MacBook Pro 15 inch) ではプールのサイズは7でした。実装を見るとデフォルトでは "コア数-1" になるようです。

main: 10
ForkJoinPool.commonPool-worker-3: 2
main: 11
ForkJoinPool.commonPool-worker-3: 3
ForkJoinPool.commonPool-worker-3: 1
ForkJoinPool.commonPool-worker-4: 13
ForkJoinPool.commonPool-worker-4: 12
ForkJoinPool.commonPool-worker-1: 5
ForkJoinPool.commonPool-worker-1: 8
ForkJoinPool.commonPool-worker-4: 15
ForkJoinPool.commonPool-worker-3: 6
ForkJoinPool.commonPool-worker-6: 0
main: 9
ForkJoinPool.commonPool-worker-7: 4
ForkJoinPool.commonPool-worker-2: 14
ForkJoinPool.commonPool-worker-5: 7

これは、システムプロパティ -Djava.util.concurrent.ForkJoinPool.common.parallelism で変更可能で、試しに2にしてみると、こんな感じの出力になります。

main: 10
main: 11
ForkJoinPool.commonPool-worker-1: 4
ForkJoinPool.commonPool-worker-1: 5
ForkJoinPool.commonPool-worker-0: 2
ForkJoinPool.commonPool-worker-1: 6
ForkJoinPool.commonPool-worker-1: 7
main: 8
main: 9
ForkJoinPool.commonPool-worker-1: 0
ForkJoinPool.commonPool-worker-1: 1
ForkJoinPool.commonPool-worker-0: 3
ForkJoinPool.commonPool-worker-1: 12
ForkJoinPool.commonPool-worker-1: 13
main: 14
main: 15

また、ParallelStreamで自前のForkJoinPoolを使いたい場合は、少しトリッキーですが、ストリーム処理自体をRunnableでラップしてForkJoinPoolに渡すと言った方法が使えるようです。 参考リンク

public static void main(String[] args) throws Exception {
    ForkJoinPool pool = new ForkJoinPool(2);
    pool.submit(() -> IntStream.range(0, 16).parallel()
            .forEach(i -> System.out.println(Thread.currentThread().getName() + ": " + i))
    ).get();
}
ForkJoinPool-1-worker-1: 10
ForkJoinPool-1-worker-0: 5
ForkJoinPool-1-worker-0: 4
ForkJoinPool-1-worker-0: 7
ForkJoinPool-1-worker-1: 11
ForkJoinPool-1-worker-1: 9
ForkJoinPool-1-worker-0: 6
ForkJoinPool-1-worker-0: 2
ForkJoinPool-1-worker-1: 8
ForkJoinPool-1-worker-1: 14
ForkJoinPool-1-worker-1: 15
ForkJoinPool-1-worker-0: 3
ForkJoinPool-1-worker-0: 1
ForkJoinPool-1-worker-1: 13
ForkJoinPool-1-worker-1: 12
ForkJoinPool-1-worker-0: 0

ForkJoinPoolについて

ForkJoinPoolJava 7から導入された新しいExecutorのフレームワークです。 旧来のExecutorと違うのは、タスクのスケジュールのアルゴリズムとして、work-stealingを採用していることです。これは再帰処理やタスクの中で更に細かな子タスクが生成されるような計算処理に適しています(例えばWebクローラなど)

ForkJoinkPoolに登録されている各ワーカースレッドは、それぞれワーカーキュー(実際はLIFO型のスタック)を持っていて、ForkJoinTaskを積むことができます。ForkJoinTaskは外部からForkJoinPoolの execute, invoke, submit メソッドを使って登録したり、もしくはタスクの中で直接別タスクを生成しその fork メソッドを呼ぶことで登録することもできます。forkされたタスクは join メソッドを使い、計算結果を待ちます。

ということで、WikipediaのWork stealingにあるモデルをForkJoinPoolとForkJoinTaskを使って実装してみます。ForkJoinTaskにはいくつかの抽象サブクラスがあり、今回はその中のRecursiveTaskを使います。

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

class ForkJoinPoolExample {
    public static void main(String[] args) {
        int poolSize = Integer.parseInt(args[0]);

        ForkJoinPool pool = new ForkJoinPool(poolSize);
        int result = pool.invoke(new F(1, 2));
        log("Result is " + result);
    }

    static class F extends RecursiveTask<Integer> {
        private final int a, b;

        F(int a, int b) {
            this.a = a;
            this.b = b;
        }

        @Override
        protected Integer compute() {
            log(String.format("Start compute of f(%d, %d) = g(%d) + h(%d)", a, b, a, b));
            G g = new G(a);
            g.fork();
            sleep(1000);
            H h = new H(b);
            final int result = h.compute() + g.join();
            log(String.format("f(%d, %d) = %d", a, b, result));
            return result;
        }
    }

    static class G extends RecursiveTask<Integer> {
        private final int a;

        G(int a) {
            this.a = a;
        }

        @Override
        protected Integer compute() {
            log(String.format("Start compute of g(%d) = %<d * 2", a));
            final int result = a * 2;
            log(String.format("g(%d) = %d", a, result));
            return result;
        }
    }

    static class H extends RecursiveTask<Integer> {
        private final int a;

        H(int a) {
            this.a = a;
        }

        @Override
        protected Integer compute() {
            log(String.format("Start compute of h(%d) = g(%<d) + (%<d + 1)", a));
            G g = new G(a);
            g.fork();
            sleep(1000);
            int c = a + 1;
            final int result = c + g.join();
            log(String.format("h(%d) = %d", a, result));
            return result;
        }
    }

    private static void log(String message) {
        System.out.println(String.format("%tT.%<tL [%s] %s", System.currentTimeMillis(), Thread.currentThread().getName(), message));
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

最初は処理順を確認するため、プールのサイズを1にしてみます。

$ java ForkJoinPoolExample 1
01:37:23.239 [ForkJoinPool-1-worker-1] Start compute of f(1, 2) = g(1) + h(2)
01:37:24.293 [ForkJoinPool-1-worker-1] Start compute of h(2) = g(2) + (2 + 1)
01:37:25.295 [ForkJoinPool-1-worker-1] Start compute of g(2) = 2 * 2
01:37:25.295 [ForkJoinPool-1-worker-1] g(2) = 4
01:37:25.295 [ForkJoinPool-1-worker-1] h(2) = 7
01:37:25.296 [ForkJoinPool-1-worker-1] Start compute of g(1) = 1 * 2
01:37:25.296 [ForkJoinPool-1-worker-1] g(1) = 2
01:37:25.297 [ForkJoinPool-1-worker-1] f(1, 2) = 9
01:37:25.297 [main] Result is 9

プールサイズが2以上の場合はforkされたタスクは空きスレッドがあれば順次消費されていきます。

$ java ForkJoinPoolExample 2
01:37:43.282 [ForkJoinPool-1-worker-1] Start compute of f(1, 2) = g(1) + h(2)
01:37:43.292 [ForkJoinPool-1-worker-0] Start compute of g(1) = 1 * 2
01:37:43.293 [ForkJoinPool-1-worker-0] g(1) = 2
01:37:44.300 [ForkJoinPool-1-worker-1] Start compute of h(2) = g(2) + (2 + 1)
01:37:44.300 [ForkJoinPool-1-worker-0] Start compute of g(2) = 2 * 2
01:37:44.301 [ForkJoinPool-1-worker-0] g(2) = 4
01:37:45.305 [ForkJoinPool-1-worker-1] h(2) = 7
01:37:45.306 [ForkJoinPool-1-worker-1] f(1, 2) = 9
01:37:45.306 [main] Result is 9

退職しました3

4月で約2年半所属した前職を退職しました。お世話になった皆さんありがとうございました。

これまで

SREチームとしてインフラ周りを幅広く見ていました。アプリケーション開発以外の仕事は大体やったと思います。一部ですが具体的には以下の様なことをやってました。

また、SREからロールチェンジを希望して半年ほどReactを使った新規のアプリケーション開発を経験させてもらったり、それに合わせてフィリピンに出張に行ったりしました。フィリピン出張は会社生活の中で一番印象に残ってる出来事です。

退職/転職の理由とか

一番大きなトリガーになったのは、昨年子供が誕生したことです。仕事以外のプライベートの時間がほぼ子育てに費やされることになり、勉強時間や趣味プログラミングの時間がが激減しました。その上で、今後の自分のキャリアをどうしたいかと考えたとき、インフラではなくもっと直接的なソフトウェア開発の方に重きを置きたいと考えるようになりました。また、経済面においても子供にかかる費用(消費財系, 保育料, 将来に向けた教育費, 第2子など)を考えると、今までより多くの給与をいただきたくなった次第です。

これから

5月からTD社のお世話になっています。Hosted EmbulkであるData Connector/Result Outputを提供しているIntegrationチームというところに所属しています。技術力や英語力的なところで足りない部分も多いと思いますが、早くバリュー出せるように頑張ります。

例のもの

http://amzn.asia/b8ubFYx

embulk-input-remote v0.4.1 リリース

約1年ぶりのリリース、0.4.0はGemはリリースできたのですが、CircleCIからの自動タグ付けに失敗したので新しいのをリリースしました..w
0.3系から仕様は変わってませんがEmbulk 0.9にあげたりKotlinとかその他の依存ライブラリやGradleのバージョンを全部最新にしました。

CircleCI上のRSpecのテスト時間をparallel_split_testを使って短縮する

CircleCIを使ってテストを実行する場合、テストはcircleci tests splitコマンドで分割した上で複数コンテナを使うことでテストの実行時間を短縮することができます(参考リンク)。ただ、コンテナ数を増やすのも限界がありますし*1、少し前までtiming-based splittingがCircleCI Workflowでは有効にならないという問題もあり、コンテナごとのテストケース数や実行時間に偏りがあって、結局一番遅いコンテナに引きづられてスループットが思ったより上がらないという問題がありました。*2

そこで、コンテナ上でさらにテスト(RSpec)を並列実行することで、コンテナごとの実行時間を短縮し、それによって全体のスループットを上げるというのを試みてみました。

Gem探し

RSpecを並列実行するGemは何個かあって、rrrspec, parallel_tests, rspec-parallelなどがありますが、一長一短があり、どれもしっくり来ない感じでした。悩みポイントとしては、こちらのqiitaの記事で書かれていることがかなり近いです。

qiita.com

こちらの記事のyuku_tさんがが作られたparallel-rspecも試してみたのですが、まだProduction Readyでないようで通常のRSpecの挙動と違うところがあり、自分のケースでは使えませんでした。

それで色々他を探した結果 parallel_split_test というGemに行き着きました。こちらは最小限のハックでRSpecを拡張していて、なおかつファイルベースではなくspecベースでテストを分散できるという、まさに自分の求めるものでした。*3

使ってみる on CircleCI

使い方は至ってシンプルで、 rspec コマンドを parallel_split_test に変えるだけです、rspecで使っているオプションは全部使える(はず)です。 CircleCI的な実行の仕方としてはこんな感じになると思います。デフォルトのmediumコンテナは2CPUなので並列実行数(PARALLEL_SPLIT_TEST_PROCESSES)は2を指定しています。

export PARALLEL_SPLIT_TEST_PROCESSES=2
bundle exec parallel_split_test --profile 10 \
  --format RspecJunitFormatter \
  --out /tmp/test-results/rspec.xml \
  --no-merge \
  --format progress \
  $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)

--no-merge オプション

このオプションはparallel_split_test独自のオプションです。これを付けない場合、 アウトプットで指定したファイル*4に各プロセスの実行結果がすべてマージされます。 RspecJunitFormatter の場合各プロセスの実行結果それぞれが個別のxmlファイルになって、マージされるとxmlのフォーマットではなくなってしまうので、--no-mergeオプションをつけることで別々のファイルに書き出します。ファイル名は、もともと指定したものにプロセス番号が追加されます*5。CircleCIは複数の結果ファイルをよしなにマージして集計してくれるので、この方法で全部のテスト結果を集計できます。

パッチ当てた

また、使ってる中で何個かはまったポイントはPRだして修正したりして、自分の使っているケースでは今はちゃんと動いています。 https://github.com/grosser/parallel_split_test/pull/13 https://github.com/grosser/parallel_split_test/pull/14 https://github.com/grosser/parallel_split_test/pull/15

よかったら、使ってみて下さい!

*1:お金が無限にあればその限りではない

*2:time-based効かない問題は2018/01ごろに解決しています、参考リンク

*3:ちょっとgemの名前がジェネラル過ぎるんじゃないかとか、このGem自体のテストが難しいというのもあったりしますが...w

*4:ここでは /tmp/test-results/rspec.xml

*5:この例の場合 /tmp/test-results/rspec.1.xml のようになる