JavaでGPU演算(Aparapi)してみた

今更感は有るが、Aparapi触ってみた。特に何か使う予定は無いけど、寒さに負けて生賴範義展に行けなかったのでなんとなく。

(1/31追記) 社内LTで再利用.

Aparapiとは

github.com

aparapi.com

JavaVMからOpenCL API経由でGPU演算を行うライブラリ。GPU側へ持っていけるのはJava プリミティブ型の配列のみだけれど、簡単に書ける。

とりあえず、任意の画像をグレースケール化する処理をループで書いた場合とAparapiで書いた場合を比較。

JVM内でループを回した場合

  private void convertToGrayScaleInJvm(final int[] pixels) {
    for (int i = 0; i < pixels.length; i++) {
      int pixel = pixels[i];
      int alpha = pixel >> 24 & 0xFF;
      int red   = pixel >> 16 & 0xFF;
      int green = pixel >>  8 & 0xFF;
      int blue  = pixel & 0xFF;
      int y = (int)(0.298912 * red + 0.586611 * green + 0.114478 * blue);
      pixels[i] = alpha << 24  | y << 16 | y << 8 | y;
    }
  }

Aparapiを使ってGPU演算した場合

  private void convertToGrayScaleInGpu(final int[] pixels) {
    
    Kernel kernel = new Kernel() {
      @Override
      public void run() {
        //GPU world
        int i = getGlobalId();
        int pixel = pixels[i];
        int alpha = pixel >> 24 & 0xFF;
        int red   = pixel >> 16 & 0xFF;
        int green = pixel >>  8 & 0xFF;
        int blue  = pixel & 0xFF;
        int y = (int)(0.298912 * red + 0.586611 * green + 0.114478 * blue);
        pixels[i] = alpha << 24  | y << 16 | y << 8 | y;
      }};
      int size = pixels.length;
      kernel.execute(Range.create(size));
  }

グレースケール化の方法については

ofo.jp

を参考とした。
ソース全体はこちら

https://gist.github.com/ka-ka-xyz/232bc0368ebf3c44bb46a7e1cb03810b

Aparapi注意点

JNI経由でOpenCLへアクセスするためにはネイティブライブラリが必要らしい。

aparapi/QuickReference.pdf at master · aparapi/aparapi · GitHub

無くても動くけれど、GPUは使わないよ的な事が書かれてた。 *1
あと、このPDFではAparapi関連のシステムプロパティのキー名が`com.amd.aparapi` で始まっているけれど、現在のv1.4.1では`com.aparapi` になっているので注意。

今回はここからdllを取得したけれど、これで良いんだろうか...
github.com

dllなりsoなりが存在するフォルダをJVMシステムプロパティ `java.library.path` で指定すれば読み込まれる。

実際に動かしてみる

www.pexels.com

から取得した画像をあれこれリサイズしてグレースケールへ変換してみた。

カラー版
f:id:ka-ka_xyz:20150913194329j:plain

グレースケール化した画像
f:id:ka-ka_xyz:20180128235338j:plain

ほんとにGPU呼んでるの?

NVIDIS GeForce GTX 1050の負荷をWin標準のパフォーマンスモニタで見てみた画像。
f:id:ka-ka_xyz:20180128235601p:plain

実行時にGPUさんが忙しそうにしてたので、呼ばれてるんだろう。多分。

パフォーマンスはどうよ?

f:id:ka-ka_xyz:20180128235656p:plain

X軸が画像のピクセル数、Y軸が処理にかかった時間(ms)。この時間はあくまでグレースケール化の処理時間で、ファイルの読み書きについては勘定していない。青がGPU演算のパフォーマンス。オレンジがJVM内での演算パフォーマンス。画像ピクセル数をあれこれ振り、CPU・JVM各5回の処理時間を取得して平均値をグラフ化。

グラフを見ると、GPU処理のためのオーバーヘッドがかなり高く、画像のピクセル数が少ないとJVMの中でループ回してたほうが圧倒的に速い。
13500x8598pxぐらいのサイズだとまだJVM内の処理が僅かに速く、18000x11464pxではGPU側が速い。

GPU演算だと並列処理のはずなのに計算量がO(N)に見えるのが気になる。仮説として

  • 処理自体は瞬時に終わるもののGPU側のメモリとJavaヒープとの間でのデータ転送に大半の時間が消費されている

と憶測してる(未検証だけど、上で挙げたパフォーマンスモニタはそれっぽく見える)。

*1: 一方で 「Aparapi:任意の計算タスクを実行するための新たな “Pure JavaAPIhttps://www.infoq.com/jp/news/2010/10/aparapi-java-and-gpus みたいな記事もあり、正直良くわからない。けど理屈としてはJVMの外のデバイスへアクセスするにはネイティブライブラリは必要だと思う

Redisのsortedsetで得点をランキング出来るのはわかった。で、自己最高得点でランキングする一番いい方法を頼む

ソート済みセット型 — redis 2.0.3 documentation

Redisのsorted set型を使うと、キーとなる文字列(member)と数値データ(score)のペアをソート済みの状態で保存できて、「scoreの上位100位までのランキングを行いたい」ようなシチュエーションで大変重宝します。

で、単純にscoreのランキング情報を得る場合は良いのですが、例えば「memberが過去に登録したscoreのうち、最も高いscoreのみを用いてランキングを行いたい」といった場合にどういう戦略を取るのが有効なのかなと言う点についてゆるく検証していと思います。

詳細

検証に使ったJavaソースはこちら。
Redis perftest to get max score. · GitHub

redisクライアントとしてjedisを使用。
github.com

使用するデータ

memberとしてUUID文字列を使用し、UUIDから生成したDouble型のhash値をscoreとします。

  private Map<String, Double> initData(int size) {
    Map<String, Double> data = new HashMap<String, Double>();
    System.out.println("------------ Generated value - score pair. ------------");
    for (int i = 0; i < size; i ++) {
      UUID uuid = UUID.randomUUID();
      System.out.println(uuid + ", " + Math.abs(Double.valueOf(uuid.getMostSignificantBits())));
      data.put(uuid.toString(), Math.abs(Double.valueOf(uuid.getMostSignificantBits())));
    }
    System.out.println("------------------------");
    return data;
  }

件数は10万件ぐらい。

戦略1: 過去のscoreを取得して最新scoreより大きいかどうかを比較し、大きければsorted setへ保存

Javaで書くとこんな感じ。
scoreの値を1, 2, 3...10倍することで、一つのmemberに複数のscoreを紐付けています(超適当だけど)。

  private void zscoreThenZadd(String key, int loop) {
    del(key);
    long start = System.currentTimeMillis();
    for (String value : data.keySet()) {
      for (int i = 1; i <= loop; i++) {
        try (Jedis jedis = this.pool.getResource()){
          Double currentScore = data.get(value) * i;
          Double oldScore = jedis.zscore(key, value);
          if (oldScore == null || currentScore.compareTo(oldScore) > 0) {
            jedis.zadd(key, currentScore, value);
          }
        }
      }
    }
    long total = System.currentTimeMillis() - start;
    System.out.println(key + ":" + total);
  }

毎回Redisからzscoreで古いscoreを取ってきて新しいscoreと比較。新しいscoreのほうが大きい場合は改めてzaddでmemberとscoreのペアをsorted setへ入れる。
補足すると、RedisではTransactionも使えますが、同じTransaction内でzscoreの値を取れるのはTransactionの完了時になります。SQLでいうselect for updateのように、zscoreの結果を取得してその結果をもとにzaddするかどうかを分岐するという処理は出来ません。あやうげ。

戦略2: Luaスクリプトを使い、Redis側で過去scoreの比較と保存を行う

javaで書くとこんな感じ。

  private final String scriptTemplate = 
    "local new_score = tonumber(KEYS[1])\n" +
    "local data = KEYS[2]\n" +
    "local bulk_old_score = redis.call('zscore', '%s', data)\n" +
    "local old_score = -1\n" +
    "if bulk_old_score then old_score = tonumber(bulk_old_score) end\n" +
    "if old_score < new_score then\n" +
    "  return redis.call('zadd', '%s', new_score, data)\n" +
    "else\n" +
    "  return nil\n" +
    "end";
  
  private void zscoreThenZaddInScript(String key, int loop) {
    del(key);
    String sha = null;
    try (Jedis jedis = this.pool.getResource()){
      String script = String.format(scriptTemplate, key, key);
      sha = jedis.scriptLoad(script);  
    }

    long start = System.currentTimeMillis();
    for (String value : data.keySet()) {
      for (int i = 1; i <= loop; i++) {
        try (Jedis jedis = this.pool.getResource()){
          Double currentScore = data.get(value) * i;
          String[] args = {currentScore.toString(), value};
          jedis.evalsha(sha, args.length, args);
        }
      }
    }
    long total = System.currentTimeMillis() - start;
    System.out.println(key + ":" + total);
  }

スクリプト内でzscoreとzaddを行う処理。スクリプト内の処理はatomicなので、戦略1にあったトランザクション問題も解決。ただし、スクリプト処理を行う分だけRedisの負荷が増えます。

戦略3: scoreとmemberのペアをsetとして保存しておき、アプリ側で大小比較後に最も大きいscoreをsorted setとして保存する
  private static final String KV_SPLITTER = "::";
  private void saddAllPreProcessedData(String preProcessedStoreKey, int loop) {
    del(preProcessedStoreKey);
    long start = System.currentTimeMillis();
    for (String value : data.keySet()) {
      for (int i = 1; i <= loop; i++) {
        try (Jedis jedis = this.pool.getResource()){
          String score = String.valueOf(data.get(value) * i);
          jedis.sadd(preProcessedStoreKey, value + KV_SPLITTER + score);
        }
      }
    }
    long total = System.currentTimeMillis() - start;
    System.out.println(preProcessedStoreKey + ":" + total);
  }

  private void smembersAllPreProcessedDataThenAggregateAndZadd(String preProcessedStoreKey, String key) {
    del(key);
    try (Jedis jedis = this.pool.getResource()){
      long start = System.currentTimeMillis();
      Set<String> preProcessed = jedis.smembers(preProcessedStoreKey);
      jedis.del(preProcessedStoreKey);
      Map<String, Double> map = new HashMap<String, Double>();
      for (String kv : preProcessed) {
        String[] kvAry = kv.split(KV_SPLITTER);
        String value = kvAry[0];
        Double score = Double.valueOf(kvAry[1]);
        if (map.putIfAbsent(value, score) != null) {
          map.computeIfPresent(value, (val, oldScore) -> {
            return oldScore < score? score: oldScore;
          });
        }
      }
      jedis.zadd(key,map);
      long total = System.currentTimeMillis() - start;
      System.out.println(key + ":" + total);
    }    
  }

最初にmember - valueペアの文字列表現を全てRedisへsetとして保存しておいて、改めてRedisから呼び出してscoreの大小比較とzaddでの保存を行います。実環境だとまあ、別々のスレッドでrpush+blpopで非同期に処理してくようなパターン。Redis側のメモリも食うし転送量も多いしであまりパフォーマンスは良くないんだろうなと予想してました。

(12/04追加) 戦略4: スコアを別々のキーに保存しておいて、zuniscoreで集計
  private void zunionstore(String key, int loop) {
    del(key + "::*");

    long start = System.currentTimeMillis();
    for (String value : data.keySet()) {
      for (int i = 1; i <= loop; i++) {
        try (Jedis jedis = this.pool.getResource()){
          Double currentScore = data.get(value) * i;
          jedis.zadd(key + "::" + value, currentScore, value);
        }
      }
    }
    
    try (Jedis jedis = this.pool.getResource()) {
      Set<String> keys = jedis.keys(key  + "::*");
      double[] weights = new double[keys.size()];
      Arrays.fill(weights, 1d);
      ZParams params = new ZParams();
      params.weightsByDouble(weights);
      params.aggregate(Aggregate.MAX);
      jedis.zunionstore(key, params, keys.toArray(new String[keys.size()]));
      del(key + "::*");
    }
    
    long total = System.currentTimeMillis() - start;
    System.out.println(key + ":" + total);
  }

最初にmember - valueペアの文字列表現をキー名としてzscoreとして保存したあとで、改めてzuniscoreで最大値を集計するパターン。戦略3と同じく、これも別スレッドで非同期に実行する想定。

例えば定期テストの点数など、全てのmemberが同時に何かの評価を受けてscoreを保存するという要な場合であれば、「第一回定期テスト」「第二回定期テスト」のようなキーを作ってそこにmember-valueのペアを入れてからzuniscoreで集計すれば良いんですが.........
あるmemberがスコアを100回残す一方で、もう一方のmemberは10回しかスコアを残さないような想定だと、member - valueペアをキー名として保存しとくしかないのかなと。キー名が爆発的に増加するけれど。

(12/05追加) 戦略5: 更新時に毎回zunionstoreで集計
  private void zunionstore2(String key, int loop) {

    long start = System.currentTimeMillis();
    for (String value : data.keySet()) {
      for (int i = 1; i <= loop; i++) {
        try (Jedis jedis = this.pool.getResource()){
          Double currentScore = data.get(value) * i;
          String tempStoreKey = key + "::" + UUID.randomUUID().toString();
          jedis.zadd(tempStoreKey, currentScore, value);
          double[] weights = new double[2];
          Arrays.fill(weights, 1d);
          ZParams params = new ZParams();
          params.weightsByDouble(weights);
          params.aggregate(Aggregate.MAX);
          jedis.zunionstore(key, params, tempStoreKey, key);
          jedis.del(tempStoreKey);
        }
      }
    }
    long total = System.currentTimeMillis() - start;
    System.out.println(key + ":" + total);
  }

戦略4だとキー数が爆発する件について、お昼のおしごとで指摘された改善案。
値を追加するたびに一旦テンポラリキーへ値を入れ、zunionstoreで集計する。


結果

データ1万件

f:id:ka-ka_xyz:20171205090437p:plain

戦略 処理時間 (ms)
戦略1 7648
戦略2 4985
戦略3合計 3844
戦略3(未処理データの保存) 3504
戦略3(sortedsetデータの保存) 340
戦略4 4042
戦略5 362037
戦略5(Transactionあり) 362933
データ10万件

f:id:ka-ka_xyz:20171205090410p:plain

戦略 処理時間 (ms)
戦略1 77007
戦略2 51638
戦略3合計 34856
戦略3(未処理データの保存) 36049
戦略3(sortedsetデータの保存) 4069
戦略4 41087
戦略5 NA
戦略5(Transactionあり) NA

戦略1と比べると戦略2の処理時間は7割程度。戦略3の処理時間は半分程度。

一応、Redis側に10万件のデータが保存され、またいくつかのmemberについて抜取調査をして最も高いscoreが保存されていることも確認済み。

これだけ見れば戦略3が圧勝に見えるけれど、今回の結果は同じマシン内でJavaアプリとRedisが同居していてネットワーク負荷がボトルネックとならない環境であることを考慮する必要があります。JavaアプリとRedisが別サーバー上で動いているような場合であれば、ネットワーク速度がボトルネックとなり、転送量が多い戦略3が不利になる可能性もあるでしょう。
また、戦略2は他の戦略と比べるとRedisサーバー側でのスクリプト処理が重いはずです。Redisが使用できるCPU能力が低い場合には、スクリプト処理がボトルネックとなる可能性もあります。

戦略4は戦略3とほぼ同じぐらいのパフォーマンス。データ転送量は抑えられるものの、想定に寄ってはキーがかなり増えるのが欠点と言えば欠点。ただし、定期テストや大会の結果を集計するような処理であればキーの増加は無い。

戦略5は他の戦略と比べると桁一つ遅い。ただし、トランザクションが使えるのでデータ量が少数の場合だと使い勝手は良さそう。

いやほんとは負荷状況とかちゃんとモニタリングする必要はあるんだろうけれど休み中にそんなことしたくないよね。かといって、会社にいるとこの手の測定ってやりにくいんだよなあ。

それにしても、RDBの常識的には10万件を全件取得するようなTable Scanが走りまくる処理なんて考えられないよなーと思っていたものの、かなり良い数字が出てたのでびっくりしました。RDBでの「当たり前」を一回投げ捨てないと、Redis(だけではなくNoSQL)を上手く使うのは難しいなあ。

『この世界の片隅に』を観たときの頭の抽き出し整理

konosekai.jp

とりあえず、ここでは映画のレビューとか感想は書きません。
なんというかこー

と言う感じで、感想を書いて一区切り置いてしまうのがあまりに勿体ない。

ということで、感想の代わりに映画を見てたとき「そういえばあの本にこういう事を書いてたよな」と思っていたことについて、さらっと再読したメモ。目的としては「頭の抽き出し整理」というか、あの映画を受け止めたときの自分の手持ちカードを晒す的な。



「月給百円」のサラリーマン―戦前日本の「平和」な生活 (講談社現代新書)

「月給百円」のサラリーマン―戦前日本の「平和」な生活 (講談社現代新書)

冒頭の、昭和8年の広島の場面で思い出してたのがこの本。
昭和一桁時代の物価は大体今の二千分の一ぐらい(当時の1円 = 現在の2,000円)。都市部の中流層が「そこそこ暮らしていける」ぐらいの目安になる月収が大体100円程度。
ただし、中流層と言っても人口比でいうと10%ぐらいの少数派で、貧困層は子供を身売りすることも当たり前という状態(この辺は映画版で大幅カットされたエピソードの背景)。
日中戦争が始まってからはインフレが進み(そして、公定価格は抑えられたものの統計に現れない闇価格は高騰し)、戦後の物価上昇を経て最終的に「キャラメル一箱百円、靴下三足千円」な現在に至る。

それにしても、すずさんってあの時まで闇市場のお世話に成ったことが無かったんだろうか?そうだとしたら、それが一般的な話なのか特殊事例なのか、よくわからない。東京を舞台にした作品だと、もうちょっとカジュアルに闇に頼ってる気がする。


戦前戦中の主婦雑誌に掲載されていたレシピを元に戦時の食生活を描く本。
日中戦争が始まった頃は、意識高い系奥様向けに「軍艦サラダ」「飛行機メンチボール」のような手の込んだ料理が紹介されていたのに、戦争が進むにつれて「如何に米を水増しするか」という方向に成っていき、さらには配給制度が始まり、最後には配給では必須カロリーを満たせなくなり...という。
そういえば、この本だと隣組をベースにして複数の家から材料を持ち寄って共同炊事をやっていたということだけど、『この世界の片隅に』だと漫画版も映画版も共同炊事の描写は無かった気がする。
この辺の事情は都市部と地方で大分違うのかもしれない。

それにしても、男手二人が海軍関係に勤務していて(流石にお義父さんは年齢的に大丈夫だろうけど、周作さんもぎりぎりまで招集されず)、ふたりとも家から通勤可能で(週末に労働力としてあてに出来る)、さらに畑もあるという北條家って当時としては大分恵まれていた方なんじゃなかろうか。

あと、1939年の段階で米の国内消費量のうち20%が植民地からの移入米というデータも出てて、八月十五日慟哭シーンの背景になってる。


決戦下のユートピア (文春文庫)

決戦下のユートピア (文春文庫)

そいやこの本で、モンペ着用については余りのダサさに「都会の娘 vs. 田舎のおばさん」的な対立があるという話だったけど、地方都市だとこの辺の対立はあんまり無かったんだろうかと気になった。すずさんがその辺に頓着しないのは分かるけど、元モガの徑子さんはどういう思いでモンペを着てたんだろうか。


造船士官の回想〈上〉 (新戦史シリーズ)

造船士官の回想〈上〉 (新戦史シリーズ)

造船士官の回想〈下〉 (新戦史シリーズ)

造船士官の回想〈下〉 (新戦史シリーズ)

著者は戦争中に呉工廠へ赴任。大和や武蔵の修理に関わってたりする。あと、第十一海軍航空廠で航空関連の仕事もしているので、北條のお義父さんと関わりが有ってもおかしくないのな。度重なる呉空襲についての記述も有ったりと、『この世界の片隅に』と関連しそうな部分が結構ある。
あと、海軍病院で兵員が多い病棟ではレコードでアメリカの曲ばかりがかかっていたという記述も有ったりと、映画中のワンシーンを思い起こさせる。


源田の剣 改訂増補版 米軍が見た「紫電改」戦闘機隊全記録

源田の剣 改訂増補版 米軍が見た「紫電改」戦闘機隊全記録

艦載機による呉空襲について、米軍側から見た記録を中心に再現した記述あり。"色彩豊かに弾幕が張られていた"というあのシーンについての米側証言も出てる。
あと、呉上空でドッグファイトしていたシーンが有ったけれど、あのときの日本機は第343海軍航空隊 戦闘301飛行隊所属で他に該当する部隊は無いとの事*1。隊長は最近一部でものっそ有名になってる、あの菅野直大尉。
第十一海軍航空廠で紫電改を作ってたし、あの光景を見たらそりゃお義父さんもたぎるよね。



とまあ色々書いたけど、特にこの手の知識とか無くても楽しめる映画です。画面一杯に情報がつぎ込まれているものの、別にそれを全て受け止める必要は無いはずです。むしろそういう余計なモノは放り投げて、笑い、泣き、あの「日常」を過ごした人たちに思いを馳せれば良いと思います。

*1:旧版による。増補改訂版だと変わってるかもしれない

『日本・韓国・台湾は「核」を持つのか』感想

日本・韓国・台湾は「核」を持つのか?

日本・韓国・台湾は「核」を持つのか?

核実験やミサイルの発射を繰り返す北朝鮮。核を持ち強大な軍事力を背景に領土拡張をやめない中国。これらに隣接する日本、韓国、台湾が実際に「核兵器」を保有する日は来るのか? それは連鎖的な「核ドミノ」をもたらすのか?1 核開発の概況、2歴史的経緯、3開発レベルと対外関係、4核兵器保有の動機、4抑止要因などの視点から、北東アジアにおける「核」のリアルを冷静に分析。中国が暴走し、米国のアジア戦略が揺れる現在の必読書!

刺激的なタイトルとテーマではあるけれど、内容はいたって穏健。
韓国・日本・台湾(核兵器を持ちうる可能性が高い順)についてそれぞれの国情や過去の核兵器開発の経緯をまとめた上で

  • 韓国
    • 世論的には過半数が核兵器保有に賛成
    • 過去に核兵器開発を試みた歴史があるが、現在の監査体制の下ではまず秘密裏の自力開発は無理
    • 北朝鮮崩壊時に、核兵器開発インフラは米国が責任を持って破壊しないとまずい
  • 日本
    • 冷戦期から、(潜在的核兵器へ転換できるような)民間核技術や宇宙開発技術を積み上げてきている
    • また、これらの技術を自覚的に「核ヘッジング」として対米外交で使っても居る(「アジアへの介入に及び腰になるなら、民生技術を核兵器開発に転用しちゃる」的なシグナル)
    • ただ、一方で日本は核の平和利用と拡散防止の国際的な旗手でもある
    • この姿勢は一見矛盾するようだけれど米国による「(核を含む)安全保障の傘」に依存することで両立しうる
    • 実際、核兵器作ると成るとそれなりの時間はかかるし、透明性が高い社会と核兵器への反発が強い世論の中で極秘開発はほぼ無理
  • 台湾
    • 現在の監査体制の下では核兵器の極秘開発は無理
    • 仮に開発が露見した場合、米国からの(暗黙の)安全保障が切れ、かつ中国からの先制攻撃の口実も与えてしまうので誰得

という感じで、これらの国では核兵器の開発はまず無理だし、そもそもそんなことをやる必然性も薄いという感じでまとまっています。
ただ、これらの結論は前提として

  • 核兵器の極秘開発がバレたら国際的に爪弾きとなり、米も本気で制裁してくる
  • 米国が東アジアで十分な軍事力を保持し続ける
  • 米国は同盟国への軍事的義務を果たすために(必要であれば核兵器の先制使用を含む)軍事力行使を厭わない

というあたりについて、米側も同盟国側も確信していることが大前提になります。


で、ここからが感想の本題。普通だとまあ大統領が変わろうがこの辺の基本ラインが変わることは無いはずですし、この本の本文でも「アメリカはそういう役割を果たし続ける」ことが自明の理として書かれています。しかし、トランプ氏が大統領選で勝利した今、この辺の信頼が(建前的な部分ではなく本音の部分で)大きく削がれる可能性があります*1

著者による日本語刊行版への序文でこの辺の発言に触れられていますが、短くまとめると「対立候補クリントン氏だから安心」て感じでトランプ政権の誕生については想定外の様子。なにせ「アメリカが同盟国への軍事的義務を果たさない可能性と、その場合に想定される情勢」について、本書ではほぼ触れられてません。


ほんの数年前までは自明の理だったことがひっくり返るあたり、百年後ぐらいに歴史の教科書で読むとしたら凄い面白い時代だろうなと。
でも、そんな時代に生きたくないよぬ

*1:実際にトランプ政権が誕生した時、この辺の信頼回復に力を注ぐかもしれないし、逆に国際的な安全保障とかかなぐり捨てて米大陸に引きこもるかもしれない。ただ、現時点でどちらに転ぶのか、あるいは他の方向へ進むのか断言できる人は多分居ない

JMeter WebDriver Plugin を使ってみた

はじめに

http://jmeter.apache.org/index.html

Apache JMeterは主にWebアプリの負荷テストで使用されるツールです。
基本的にはシナリオにしたがってリクエストを投げて、想定どおりのレスポンスが戻るまでの時間を測定し、集計するというツールですが、ユーザーのインタラクティブな操作に対応したテストを行うのは苦手です。例えば、「ユーザーがWebアプリ上である操作を行った時、裏で多数の非同期通信が行われた結果がUI上に反映されるまでの時間を測定する」といったシナリオを書くのは結構面倒なのです。

そこで、JMeterのシナリオ上からブラウザを Selenium WebDriverを通じて呼び出してしまえというWebDriverプラグインの出番になります。

jmeter-plugins.org

JMeterシナリオ上でブラウザへの操作を記述し、リクエストはJMeterではなくあくまでブラウザから行い、レスポンス(とDOMへの反映)もブラウザが受け取ります。ブラウザを起動する必要が有るため、例えば大規模ユーザの同時アクセスを想定しているような、JMeterが得意な負荷テストには向きません。しかし、ブラウザを使用することで、ユーザの操作に近い形でのテストシナリオを記述する事が可能になります。

jMeterとWebDriver APIそれぞれについてある程度知識のある人向けに、JMeter WebDriver Plugin を使って Mozilla Firefox によるテストシナリオを作成してみます。

インストール方法

1. plugins-managerの導入

jmeter-plugins.org

から plugins-managerのjarファイルを入手し、以下のフォルダへコピーします。

${JMeterインストールフォルダ}/lib/ext

現時点ではjmeter-plugins-manager-0.10.jarです。

2. WebDriver Pluginのインストール

plugins-managerの導入後にJMeterを起動すると、メニューの [オプション]配下に[Plugins Manager]が追加されるので、クリックします。
Plugins Mnagerダイアログが表示されるので、[Available Plugins]タブを選択し、インストール候補プラグイン一覧から[Selenium WebDriver Support]にチェックを入れ、「Apply Changes and Restart JMeter」ボタンをクリックします

現時点では

${JMeterインストールフォルダ}/lib/ext

jmeter-plugins-webdriver-1.4.0.jarがダウンロードされ、さらにlibフォルダに関連jarファイル群がダウンロードされます。

3. selenium-firefox-driverのjarファイルを更新

jmeter-plugins-manager v1.10, jmeter-plugins-webdriver v1.4.0の組み合わせでは以下の手順が必要です)

いったんJMeterを停止した後、

${JMeterインストールフォルダ}/lib

フォルダに"selenium-firefox-driver-2.xx.x.jar"(xは任意のバージョン番号)が存在する事を確認します。そして

Maven Repository: org.seleniumhq.selenium » selenium-firefox-driver


にv2.xx.x系のより新しいバージョンのjarファイルが存在するようであれば、jarファイルを入手して入れ替えます。Selenium WebDriverは、ブラウザが更新されると動かなくなる場合が多々有ります。そのつど上記jarをより新しいバージョンに入れ替えてください。

4. firefox 停止時のエラーを対策

jmeter-plugins-manager v1.10, jmeter-plugins-webdriver v1.4.0の組み合わせでは以下の手順が必要です)

ここまでJMeterをセットアップした状態で、WebDriver Pluginを使ってFirefoxを起動すると、シナリオ終了時に以下のような例外が出力されます。

2016/09/12 20:47:28 ERROR - jmeter.JMeter: Uncaught exception:  java.lang.NoClassDefFoundError: com/sun/jna/platform/win32/Kernel32
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)

この問題に対応するため、

${JMeterインストールフォルダ}/lib

へplatform-3.5.1.jar およびjna-4.2.1.jarを導入し、JMeterを再起動します。
詳細は以下のサイトを参照。

hellotestworld.com

JMeterシナリオの作成

ここまでセットアップした後、「設定エレメント」として各ブラウザに対応するWebDriver設定が追加されます。この設定エレメントをシナリオへ加えることで、以降はWebDriver Pluginでは該当ブラウザが使用されるようになります。

f:id:ka-ka_xyz:20160913001813p:plain


また、「サンプラー」としてWebDriver Samplerが追加されます。

f:id:ka-ka_xyz:20160913001825p:plain

このSamplerにはスクリプトが記述できます。デフォルトではjavascript

WDS.sampleResult.sampleStart()
WDS.browser.get('http://jmeter-plugins.org')
WDS.sampleResult.sampleEnd()

のようなテンプレが入力されています。
このテンプレでは、測定開始後にjmeter-plugins.orgへアクセスし、測定を完了するという処理を行っています。

新規にシナリオを作成し、スレッドグループ配下にWebDriver設定、WebDriverサンプラー、リスナー「結果を表で表示」を追加してシナリオを保存します。この状態でシナリオを実行するとブラウザが立ち上がり、jmeter-plugins.orgへアクセスした後にブラウザウィンドウが閉じられます。実行結果はリスナーに出力されます。


WebDriverサンプラーの詳細については以下を参照。

www.blazemeter.com
www.blazemeter.com

複数のWebDriverサンプラー間でコードを再利用

各WebDriverサンプラーjavascript(あるいは選択可能な他のスクリプト)でコードを書くことが出来ますが、変数のスコープは各サンプラーの中に限定されます。ただ、それでは不便なので、サンプラー間でコードを共有してみます*1

シナリオに「設定エレメント」-「ユーザー定義変数」を追加し、ユーザー定義変数"lib"を定義します。
そして、"lib"を選択した状態で"Detail"ボタンをクリックすると、ユーザー定義変数を複数行編集出来るようになります。

サンプルとして、以下の内容を入力します。

var mylib = mylib || {};

mylib.pkg = JavaImporter(org.openqa.selenium);
mylib.conditions = org.openqa.selenium.support.ui.ExpectedConditions;
mylib.wait = new org.openqa.selenium.support.ui.WebDriverWait(WDS.browser, 10);
mylib.vars = org.apache.jmeter.threads.JMeterContextService.getContext().getVariables();

mylib.google = (function() {
  var googleUrl = 'https://www.google.co.jp';

  var access = function() {
    WDS.browser.get(googleUrl);
  };

  var search = function(keyword) {
  	var inputQuery = 'input#lst-ib';
  	mylib.wait.until(mylib.conditions.presenceOfElementLocated(mylib.pkg.By.cssSelector(inputQuery)));
    var inputElem = WDS.browser.findElement(mylib.pkg.By.cssSelector(inputQuery));
    inputElem.sendKeys(mylib.pkg.Keys.chord(mylib.pkg.Keys.CONTROL, 'a'), keyword, mylib.pkg.Keys.ENTER);
  };

  var waitForResultsShown = function() {
    var resultListQuery = '#search';
    mylib.wait.until(mylib.conditions.presenceOfElementLocated(mylib.pkg.By.cssSelector(resultListQuery)));
  };

  return {
    access: access,
    search: search,
    waitForResultsShown: waitForResultsShown
  };
})();

名前空間 mylib に、googleの検索フォームへの入力を行う関数やユーテリティ類を定義しています。
そして、サンプラー側で以下のスクリプトを記述します。


サンプラー1

//load mylib
eval(org.apache.jmeter.threads.JMeterContextService.getContext().getVariables().get('lib'));

WDS.sampleResult.sampleStart();
mylib.google.access();
mylib.google.search('jMeter');
mylib.google.waitForResultsShown();


WDS.sampleResult.sampleEnd();

サンプラー2

//load mylib
eval(org.apache.jmeter.threads.JMeterContextService.getContext().getVariables().get('lib'));

WDS.sampleResult.sampleStart();
mylib.google.access();
mylib.google.search('Selenium WebDriver');
mylib.google.waitForResultsShown();

WDS.sampleResult.sampleEnd();

ユーザー定義変数"lib"を文字列として読み出し、evalすることで変数"lib"中に書かれた"mylib"定義が読み出されます。

eval を使っている事に嫌悪感があるかもしれませんが、JMeter WebDriver Pluginの制約上しょうが無いかなと。"Eval is Evil"は原則として正しいとは思いますが、今回は外部から入力された文字列をevalしている訳ではなく、あくまでJMeterのシナリオで変数として定義された文字列をしようしているので、危険は無いはず(おねがいみのがして)。

f:id:ka-ka_xyz:20160913004600p:plain

このシナリオを実行すると

1. google.co.jpへアクセス
2. キーワード"jMeter"で検索
3. 検索結果が表示されるまで待つ
4. google.co.jpへアクセス
5. キーワード"Selenium WebDriver"で検索
6. 検索結果が表示されるまで待つ
7. ブラウザを閉じる

というシナリオが実行されます。


jmxファイルの内容は以下のとおり

<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="2.9" jmeter="3.0 r1743807">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="テスト計画" enabled="true">
      <stringProp name="TestPlan.comments"></stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="ユーザー定義変数" enabled="true">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
      <stringProp name="TestPlan.user_define_classpath"></stringProp>
    </TestPlan>
    <hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="スレッドグループ" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="ループコントローラ" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <stringProp name="LoopController.loops">1</stringProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">1</stringProp>
        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
        <longProp name="ThreadGroup.start_time">1473680458000</longProp>
        <longProp name="ThreadGroup.end_time">1473680458000</longProp>
        <boolProp name="ThreadGroup.scheduler">false</boolProp>
        <stringProp name="ThreadGroup.duration"></stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
      </ThreadGroup>
      <hashTree>
        <com.googlecode.jmeter.plugins.webdriver.config.FirefoxDriverConfig guiclass="com.googlecode.jmeter.plugins.webdriver.config.gui.FirefoxDriverConfigGui" testclass="com.googlecode.jmeter.plugins.webdriver.config.FirefoxDriverConfig" testname="jp@gc - Firefox Driver Config" enabled="true">
          <stringProp name="WebDriverConfig.proxy_type">SYSTEM</stringProp>
          <stringProp name="WebDriverConfig.proxy_pac_url"></stringProp>
          <stringProp name="WebDriverConfig.http_host"></stringProp>
          <intProp name="WebDriverConfig.http_port">8080</intProp>
          <boolProp name="WebDriverConfig.use_http_for_all_protocols">true</boolProp>
          <stringProp name="WebDriverConfig.https_host"></stringProp>
          <intProp name="WebDriverConfig.https_port">8080</intProp>
          <stringProp name="WebDriverConfig.ftp_host"></stringProp>
          <intProp name="WebDriverConfig.ftp_port">8080</intProp>
          <stringProp name="WebDriverConfig.socks_host"></stringProp>
          <intProp name="WebDriverConfig.socks_port">8080</intProp>
          <stringProp name="WebDriverConfig.no_proxy">localhost</stringProp>
          <boolProp name="WebDriverConfig.maximize_browser">true</boolProp>
          <boolProp name="WebDriverConfig.reset_per_iteration">false</boolProp>
          <boolProp name="WebDriverConfig.dev_mode">false</boolProp>
          <boolProp name="FirefoxDriverConfig.general.useragent.override.enabled">false</boolProp>
          <boolProp name="FirefoxDriverConfig.network.negotiate-auth.allow-insecure-ntlm-v1">false</boolProp>
          <collectionProp name="FirefoxDriverConfig.general.extensions"/>
          <collectionProp name="FirefoxDriverConfig.general.preferences"/>
        </com.googlecode.jmeter.plugins.webdriver.config.FirefoxDriverConfig>
        <hashTree/>
        <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="ユーザー定義変数" enabled="true">
          <collectionProp name="Arguments.arguments">
            <elementProp name="lib" elementType="Argument">
              <stringProp name="Argument.name">lib</stringProp>
              <stringProp name="Argument.value">var mylib = mylib || {};

mylib.pkg = JavaImporter(org.openqa.selenium);
mylib.conditions = org.openqa.selenium.support.ui.ExpectedConditions;
mylib.wait = new org.openqa.selenium.support.ui.WebDriverWait(WDS.browser, 10);
mylib.vars = org.apache.jmeter.threads.JMeterContextService.getContext().getVariables();

mylib.google = (function() {
  var googleUrl = &apos;https://www.google.co.jp&apos;;

  var access = function() {
    WDS.browser.get(googleUrl);
  };

  var search = function(keyword) {
  	var inputQuery = &apos;input#lst-ib&apos;;
  	mylib.wait.until(mylib.conditions.presenceOfElementLocated(mylib.pkg.By.cssSelector(inputQuery)));
    var inputElem = WDS.browser.findElement(mylib.pkg.By.cssSelector(inputQuery));
    inputElem.sendKeys(mylib.pkg.Keys.chord(mylib.pkg.Keys.CONTROL, &apos;a&apos;), keyword, mylib.pkg.Keys.ENTER);
  };

  var waitForResultsShown = function() {
    var resultListQuery = &apos;#search&apos;;
    mylib.wait.until(mylib.conditions.presenceOfElementLocated(mylib.pkg.By.cssSelector(resultListQuery)));
  };

  return {
    access: access,
    search: search,
    waitForResultsShown: waitForResultsShown
  };
})();
</stringProp>
              <stringProp name="Argument.metadata">=</stringProp>
            </elementProp>
          </collectionProp>
        </Arguments>
        <hashTree/>
        <com.googlecode.jmeter.plugins.webdriver.sampler.WebDriverSampler guiclass="com.googlecode.jmeter.plugins.webdriver.sampler.gui.WebDriverSamplerGui" testclass="com.googlecode.jmeter.plugins.webdriver.sampler.WebDriverSampler" testname="WebDriver Sampler1" enabled="true">
          <stringProp name="WebDriverSampler.script">//load mylib
eval(org.apache.jmeter.threads.JMeterContextService.getContext().getVariables().get(&apos;lib&apos;));

WDS.sampleResult.sampleStart();
mylib.google.access();
mylib.google.search(&apos;jMeter&apos;);
mylib.google.waitForResultsShown();


WDS.sampleResult.sampleEnd();</stringProp>
          <stringProp name="WebDriverSampler.parameters"></stringProp>
          <stringProp name="WebDriverSampler.language">javascript</stringProp>
        </com.googlecode.jmeter.plugins.webdriver.sampler.WebDriverSampler>
        <hashTree/>
        <com.googlecode.jmeter.plugins.webdriver.sampler.WebDriverSampler guiclass="com.googlecode.jmeter.plugins.webdriver.sampler.gui.WebDriverSamplerGui" testclass="com.googlecode.jmeter.plugins.webdriver.sampler.WebDriverSampler" testname="WebDriver Sampler2" enabled="true">
          <stringProp name="WebDriverSampler.script">//load mylib
eval(org.apache.jmeter.threads.JMeterContextService.getContext().getVariables().get(&apos;lib&apos;));

WDS.sampleResult.sampleStart();
mylib.google.access();
mylib.google.search(&apos;Selenium WebDriver&apos;);
mylib.google.waitForResultsShown();

WDS.sampleResult.sampleEnd();
</stringProp>
          <stringProp name="WebDriverSampler.parameters"></stringProp>
          <stringProp name="WebDriverSampler.language">javascript</stringProp>
        </com.googlecode.jmeter.plugins.webdriver.sampler.WebDriverSampler>
        <hashTree/>
        <ResultCollector guiclass="TableVisualizer" testclass="ResultCollector" testname="結果を表で表示" enabled="true">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>false</xml>
              <fieldNames>true</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
              <threadCounts>true</threadCounts>
              <idleTime>true</idleTime>
            </value>
          </objProp>
          <stringProp name="filename"></stringProp>
        </ResultCollector>
        <hashTree/>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

*1:ただし、今回紹介する方法だと共有できるのはあくまでコードだけで、状態は共有されないので注意

益川敏英『科学者は戦争で何をしたか』感想


正直な所、「微妙だ」という感想はあんまり力を入れて書きたくない。ただ、


を読んで、絶賛だけで終わるのはちょい違う気がするなあと思うのでちょっとネガティブな感想をまとめておきます。
この本を読んでいて一番引っかかったのは、科学者の戦争協力についてのこの文章

私がまだ学生だった頃、量子電磁力学の分野でノーベル賞を受賞した朝永振一郎博士が戦時下に書かれた論文を読んで、いたく感銘を受けたことがあります。朝永先生は戦時中、電波兵器の研究に動員されていました。「私はそんな研究に加担したくない」などと、戦時下での動員に抵抗すれば、たちまち非国民として投獄されてしまいます。
朝永先生も強制的にそうした研究に従事させられたわけですが、私は先生の論文を読んでいて、はたと膝を叩きたい思いに駆られました。量子力学を知っていればわりと簡単に見抜けることなのですが、電波の出力の関係を解析する部分を、限りなく一般的なところでまとめ、核心部分をうまくごまかしていたのです

以下、この文章についてTwitterでつぶやいていた感想のまとめ・補足



第二次大戦中の日本だと、自分の親族や隣近所の顔見知りな人たちが徴兵され、「敵」に殺されて死亡通知すら戻ってこないのが当たり前な状況でした。さらに1944年以降だと戦争に直接かかわらない市民が戦略爆撃で殺されていた訳です。そういう状況下で、本書の表現を借りれば"科学者である前に人間として"の観点から「自分の協力によって顔見知りの死や同胞の死を少しでも減らせるかもしれない」という発想が出て来るのはヒューマニストとして極自然なものではないでしょうか。
仮に上で引用したエピソードが事実であり、実際にサボタージュが行われていたとしたら、それはすさまじい苦悩の末の判断だったと思います。しかし………本書ではそのような苦悩が有ったのではないかということに触れられず、単純に素晴らしいサボタージュであり「本来の科学者の知恵」であると絶賛されています。


この本で一番危なさを感じるのはこの辺の話で、「自分の戦争協力によって顔見知りの死や同胞の死を少しでも減らせるかもしれない」という状況に追い込まれるかもしれないという可能性を考えていないように見えるところです。世論や状況が変わり、民間人の犠牲者が出た時には真っ先に戦争協力するんじゃないか的な気がします。それが良い・悪いという判断についてはここでは置いておくとして。


ファインマンがロスアラモスでの原爆開発に関わっていたことは本人が自伝で(物凄いノリノリに)書いていて有名ですが、彼も別に狂信的な国粋主義者でも無い訳で、戦時の高揚ってそういうものだと思うのです。ただ、この本では愛国心の甘美さとか戦時の高揚感、周りみんなが傷つく中で戦争に協力しないことへの罪悪感といったあたりのことについてはほぼ無視されていて危うげな気がします。

あー、あと蛇足ですがフリッツ・ハーバーが毒ガス開発に邁進した盲目的愛国者としてやり玉に上げられているあたりは

という背景について無視されてるのもなんだかなーという気がします。この辺は

毒ガス開発の父ハーバー 愛国心を裏切られた科学者 (朝日選書 834)

毒ガス開発の父ハーバー 愛国心を裏切られた科学者 (朝日選書 834)

に詳しいのでお勧め。

『撤退するアメリカと「無秩序」の世紀』についてメモ

米大統領選の大荒れ模様を見てて思い出したので軽くメモ。

撤退するアメリカと「無秩序」の世紀ーーそして世界の警察はいなくなった

撤退するアメリカと「無秩序」の世紀ーーそして世界の警察はいなくなった


まあ要約するなら「アメリカは"世界の警察"を捨てて内向き路線になりつつあるけれど、引きこもっても問題は解決しないししっぺ返しを食らうだけなので積極介入路線に戻ろうぜ」という本。で、何が気になったかというと、アメリカで中流層がどんどん貧しくなりつつあるから外征やめて引きこもろうという主張に対して

「高等教育のレベルは高いし、イノベイティブな起業家も一杯いるから大丈夫」

程度の軽い切り返しだけで終わらせている所。
例えば『誰が中流を殺すのか アメリカが第三世界に堕ちる日』に描かれているような、中流層が感じている不安の厳しさとの落差が気になってた。

誰が中流を殺すのか アメリカが第三世界に堕ちる日

誰が中流を殺すのか アメリカが第三世界に堕ちる日

なんというか、ビッグイシューを語る上でそんなの気にしないで問題無いという意識が透けて見えるというか、国際問題を語る上で「追い詰められた中流層」の話は一種のタブーになってるんじゃないかとも思うぐらいあっけらかんとした扱いで、最初に『撤退するアメリカと「無秩序」の世紀』を読んだ時には強烈な違和感があった。

で、米大統領選の大荒れって結局はこの辺の話になって、国内問題から目をそむけてきたエスタブリッシュメント層の傲慢さへの反発については色々と分析記事が出てるけれど、確かに自分が読んできた本を見てもそんな感じがするよなあというメモ。