第15回 redmine.tokyo勉強会で発表してきました

第15回 redmine.tokyo にて、View customize のバージョン1.2.0について発表してきました。

資料はこちら。

www.slideshare.net

発表中に、View cutomizeを知っている人に手を上げてもらったのですが、7、8割くらいの人が手をあげてくれました。うれしくて、ちょっと涙がでました。

他の方の発表も、いろいろ盛りだくさんで面白かったです。また、Twitterなどでやり取りしていた方と会うことが出来たのも良かったです。(ご挨拶しようと思っていて、タイミング逃してしまった方もいるので、また次回に、、)

声をかけていただいて、こうやって参加して発表できて、とても良い1日になりました。スタッフの皆様、ありがとうございました。

大きなリストを検索するときにArrayListではなくHashSetを使う (Java)

社内でも同じこと書いたのですが、こっちでも。

大きなリスト(例えば10万件を超えるようなもの)から一致するものを探す場合、ArrayList#containsを使うと、パフォーマンス的に問題になる場合があります。ArrayList#containsは、先頭からシーケンシャルに見ていくためです。

で、大きなリストを検索する場合には、検索に向いたデータ形式、例えばHashSetに格納することによって、パフォーマンスが大きく改善する場合があります。HashSetはハッシュテーブルを使って値を格納するので、シーケンシャルに見ていくより高速です。

ということで、試したコードです。 10万件の2つのリストを使って、「リスト1からリスト2に含まれるもののみに絞り込む」といったことをやっています。

package com.example;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.junit.Test;

public class LargeListFilterTest {

    private static final int SIZE = 100000;

    private static final List<String> SOURCE;
    private static final List<String> CANDIDATE;

    static {

        SOURCE = IntStream.rangeClosed(1, SIZE)
                .mapToObj(String::valueOf)
                .collect(Collectors.toList());

        CANDIDATE = IntStream.rangeClosed(1, SIZE)
                .map(x -> x * 2) // 偶数のみに
                .mapToObj(String::valueOf)
                .collect(Collectors.toList());
    }

    @Test
    public void filterWithSetのテスト() {

        List<String> filterd = filterWithSet(SOURCE, CANDIDATE);

        assertThat(filterd)
                .hasSize(SIZE / 2);
    }

    @Test
    public void filterWithListのテスト() {

        List<String> filterd = filterWithList(SOURCE, CANDIDATE);

        assertThat(filterd)
                .hasSize(SIZE / 2);
    }

    private <T> List<T> filterWithSet(List<T> source, List<T> candidate) {

        Set<T> candidateSet = new HashSet<>(candidate);

        return source.stream()
                .filter(candidateSet::contains)
                .collect(Collectors.toList());
    }

    private <T> List<T> filterWithList(List<T> source, List<T> candidate) {

        return source.stream()
                .filter(candidate::contains)
                .collect(Collectors.toList());
    }
}

パフォーマンスの差はあきらかで、下記のような結果になりました。(あくまで自分のPC上でのパフォーマンスですが、差が大きいのは明確です)

  • ArrayListの場合: 26.046秒
  • HashSetの場合: 0.084秒

f:id:onozaty:20181103211322p:plain

なお、小さいリストだったら、いちいちHashSetに変える必要はないです。逆にHashSetにすることによって遅くなる場合もあります。 (小さければシーケンシャルに見たほうが早い場合も多いです。そもそも小さいと気にするレベルの差にもならないですが…)

このようなボトルネックは、プロファイリングかければすぐに見つかるので、パフォーマンスを厳しく求められたり、思った通りのパフォーマンスが出ていないときは、まずはプロファイリングにかけてみると良いです。 JavaだとFlight Recorderが簡単に使えて便利なので、とても助けられています。(それでこういった問題に気がついたことも多いです)

Redmine: View customize plugin の v1.2.0 をリリースしました

View customize plugin の v1.2.0 をリリースしました。2年ぶりの新バージョンになります。

リリース内容

今回のリリースには、2つの新規機能と細々とした変更がいくつか入っています。

  1. コードの挿入位置が選択できるように
  2. ユーザやプロジェクトの情報にJavaScriptでアクセス可能に
  3. ローカライズ対応 (日本語リソースの追加)
  4. 一覧をソート可能に
  5. コメント欄を追加し、一覧表示の際にコメントがあればそちらを表示するように (コメントが無かった時にコードを)
  6. View customizeのアイコンが表示されていなかった問題対処

以降はリリース内容の詳細です。

1. コードの挿入位置が選択できるように

今までView customizeでは、ヘッダ部分にコードを挿入していました。 それが1.2.0からは、下記の3パターンから選べるようになります。

  • Head of all pages (全てのページのヘッダ) ※v1.2.0より前と同じ
  • Bottom of issue form (チケット入力欄の下)
  • Bottom of issue detail (チケット詳細の下)

「Bottom of issue form」は、今まで嵌る人が多かった「チケット入力欄がトラッカーやステータスを変えた際に再構築されてしまい、View customizeでの変更内容がクリアされてしまう」といった問題に対応するためのものです。これを使うと、入力フォームが再構築される際にもコードが再度埋め込まれて実行されるので、今までのような考慮が不要になります。

なお、埋め込み箇所が存在しないページでは埋め込まれないことになるので、Path patternとの組み合わせにご注意ください。 たとえば、「Bottom of issue detail」はチケット詳細でしか埋め込まれないので、Path patternとして.*で全ページを指定していてもチケット詳細画面でのみ実行されるようになります。(Path patternで正規表現を細かく考えなくて済むので、それはそれで便利かと)

2. ユーザやプロジェクトの情報にJavaScriptでアクセス可能に

View customizeでは、画面にある情報以外にアクセスするのは大変でした。まったく参照するのが不可な情報も多いですし、出来たとしてもREST API叩いたり、スクレイピングしたり、、と面倒でした。

私自身も、ロールやグループの情報を使って適用する人を区別したいといったことがあり、それら情報にアクセスする手段として、ViewCustomize.contextといったオブジェクトを用意しました。

ユーザに紐づく情報として、ID、氏名以外にも、APIキー、グループ、ロール、カスタムフィールドといった情報に参照することができます。プロジェクトの情報としても識別子やロール、またチケットの情報としてIDを参照できるようにしました。 プロジェクトの識別子やチケットのIDは、今までも画面に存在する情報から取得可能でしたが、利用頻度が多いため、より直感的に書けるように、、ということで加えました。

オブジェクト全体のイメージは下記の通りです。

ViewCustomize = {
  "context": {
    "user": {
      "id": 1,
      "login": "admin",
      "admin": true,
      "firstname": "Redmine",
      "lastname": "Admin",
      "groups": [
        {"id": 5, "name": "Group1"}
      ],
      "apiKey": "3dd35b5ad8456d90d21ef882f7aea651d367a9d8",
      "customFields": [
        {"id": 1, "name": "[Custom field] Text", "value": "text"},
        {"id": 2, "name": "[Custom field] List", "value": ["B", "A"]},
        {"id": 3, "name": "[Custom field] Boolean", "value": "1"}
      ]
    },
    "project": {
      "identifier": "project-a",
      "name": "Project A",
      "roles": [
        {"id": 6, "name": "RoleX"}
      ]
    },
    "issue": {
      "id": 1
    }
  }
}

追記@2018-10-20 user.roles は、Redmineのバージョンによってはエラーとなってしまっていたため、1.2.2にて削除しています。上記にも反映しました。

例えばユーザのAPIキーにアクセスするにはViewCustomize.context.user.apiKeyとなります。

3. ローカライズ対応 (日本語リソースの追加)

今回日本語リソースを追加し、ロケールとして日本語を選択しているユーザに対しては、日本語で表示されるようになりました。ずっと英語だったので、今まで使っていた方々には違和感を感じる人も多いかもしれませんが、きっと慣れてもらえると思います。(私も最初は違和感ありましたが、やっと慣れました)

母国語じゃない、、ってだけで、敷居をあげてしまうということは実際あるので、いつかやらなきゃな、、と思っていたのですが、今回Pull requestをもらったのもあって、入れることにしました。今後は他の言語も入れて行きたいと思っています。

その他

一覧系に関することで、下記の2点の修正を行っています。

  • 一覧をソート可能に
  • コメント欄を追加し、一覧表示の際にコメントがあればそちらを表示するように (コメントが無かった時にコードを)

コメント欄ですが、View customizeの設定が多くなってくると、内容を判断するための情報がコードの先頭部分だけだと厳しくなってきたため、コメント欄として別途設けることにしました。コメントが入力されていなかった場合には、今までどおりコードの先頭部分が表示されます。

f:id:onozaty:20181001000424p:plain

他は細かいBugfixなので、詳細は省略します。

最後に

今回の新しい機能の使用例を、次回のredmine.tokyoでお話させていただく予定です。

あと、もうすぐリリースされそうなRedime4に対する対応は、Redmine4がリリースされたらすぐに出す予定です。(対応方法は既に確認済みですので、大丈夫かと、、)

Redmineの検索欄に素早く移動する (ShortcutKey2URL)

人から聞いたチケット番号を元にチケットを開いたり、何かキーワードで検索したり、ってのが良くあるのですが、その際にすぐにRedmineの検索欄に飛べるように、ShortcutKey2URLという拡張機能を使っています。

ShortcutKey2URL

ShortcutKey2URLは、ショートカットキーを使用してURLを開いたり、移動したり、JavaScriptを実行できる拡張機能です。Chrome版とFirefox版があります。

スタートアップキーであらかじめ設定しておいた動作の一覧を表示し、次のキーでその動作を実行するような動きとなっています。

キーとして使用できる文字は1文字に限定されません。複数文字として設定しておくことが可能です。ShrotcutKey2URLは、キーとして連続して入力された文字から、対象が1つに絞り込まれた時点でその動作を実行します。

動作として設定できるものには、下記のようなものがあります。

  • URLへの移動。既に開いているURLの場合は、そのタブへ移動し、開かれていない場合には、新しいタブとして開く。
  • 新規タブとしてURLを開く。
  • 現在のタブにURLを開く。
  • 現在のタブで指定したJavaScriptを実行する。
  • 新規タブとしてURLを開いて、その後に指定したJavaScriptを実行する。

Redmineの検索欄に素早く移動する

ということで、Redmineの検索欄に素早く移動できるようにショートカットキーを設定します。

新しいタブでRedmineのページを開いて、その後にJavaScriptで検索欄にフォーカスを当てます。

document.getElementById('q').focus();

こんな感じの設定になります。(http://192.168.33.11/ がRedmineのURL)

f:id:onozaty:20180905005706p:plain

動作イメージは以下のような感じです。

f:id:onozaty:20180905010958g:plain

ShortcutKey2URLは他にもいろいろ使える場面があると思うので、ぜひご利用ください!!

RUANN - RUANN TOUR 2018 SUMMER "LOVE & HOPE" (2018年8月26日@新宿ReNY)

ずっと行きたかったRUANNのライブにやっといけました。嫁さんと一緒での参戦でした。

半分ぐらいの曲がギターでの弾き語りで、他の曲はダンスしながらと、とても元気なパフォーマンスを見せてくれました。

洋楽アーティストのライブみたいな感じをイメージしていたのですが、みんなで歌うって感じじゃなくて、みんな聴き入っている、、っていうのが多かったです。実際ギターの弾き語りはすごくよくて、自分も聴き入ってしまいました。

オリジナル曲の歌詞は、若い子向けかなってのもあって、ちょっと自分の世代(40代..)だと共感というより、娘はこんな感じなのかな(長女とRUANNは1歳違い)、、と思いながら聴く曲もあります。アンコールのときのMCは、実際に娘のことを考えながら聞いてしまい、ちょっと涙ぐんでしまいました。ほんと頑張って欲しい。応援してる!

洋楽のカバーもいくつか入れてきましたが、Lady Gaga の「Born This Way」は特に良かったです。英語の歌詞で力強く歌う声がすごくいい。RUANNのことを好きになったのも、この力強い声と、ギター1本でのパフォーマンスでした。

メジャーデビューも決まっているので、これからいろんな人がRUANNの存在に気がつくと思います。若いのに、、とかじゃなく、年齢関係なくすごいアーティストなので、これからのパフォーマンスがとても楽しみです。

セットリスト

  1. I Want You Back (Jackson 5)
  2. The Beautiful girl is about u
  3. Born This Way (Lady Gaga)
  4. Wherever you are (ONE OK ROCK)
  5. I AM STANDING
  6. TT (TWICE)
  7. 頑張る君へ
  8. instagram (DEAN)
  9. Perfect life
  10. Pinky World
  11. GET THE GLORY
  12. LOVE & HOPE
  13. (アンコール) I'm just walking without you
  14. (アンコール) The Beautiful girl is about u -TeddyLoid ver.-

ShortcutKey2URL(Firefox版)の 4.0.1 をリリースしました(クリップボードへの書き込みを許可)

4.0.1 を先ほどリリースしました。クリップボードへの書き込みを許可するようにしただけとなります。(Chrome版はもともと出来たのですが、Firefox版のほうだとPermissionが異なっていて、デフォルトだと許可されていませんでした)

現在開いているタブのタイトルとURLをクリップボードに書き込むスクリプトは下記のように書きます。(Action: Execute script として設定)

var copyUrl = (e) => {
  e.clipboardData.setData('text/plain', document.title + ' ' + location.href);
  e.preventDefault();
  document.removeEventListener('copy', copyUrl);
}

document.addEventListener('copy', copyUrl);
document.execCommand('copy');

copyイベントを発生させて、そのイベントハンドラ内でクリップボードの内容を書き換える、、といった書き方になります。(直接クリップボードを触りたいけど、許可されていないのでやもえない、、)

PostgreSQLのデータフォルダ(PGDATA)をtmpfsに置いてみる(Vagrant)

ユニットテスト用のPostgreSQLを立ち上げる際に、データフォルダ(PGDATA)をtmpfsに置けたらI/Oが早くなって、テスト時間短縮になるかなっておもって試してみました。テスト用ならば、シャットダウンでデータが消えてしまっても問題ないので。

Windows上で動かしたかったので、今回はVagrantでやりました。各種セットアップはAnsibleでやっています。

上記フォルダで、vagrant upとすると、CentOS7.3でPostgreSQL10のインストールを行い、tmpfsとしてマウントした/mnt/ram配下にデータフォルダを作成します。 また、testdbというDBを作成し、user1というユーザを作成しています。

pgbenchによるパフォーマンス測定

実際tmpfsにして早くなっているのか見るために、pgbenchを流してみます。

postgresql-contribをインストールします。

[root@localhost ~]# yum install postgresql10-contrib

テスト用のテーブルおよびデータを作成します。

[root@localhost ~]# /usr/pgsql-10/bin/pgbench -i testdb -U user1
Password:
NOTICE:  table "pgbench_history" does not exist, skipping
NOTICE:  table "pgbench_tellers" does not exist, skipping
NOTICE:  table "pgbench_accounts" does not exist, skipping
NOTICE:  table "pgbench_branches" does not exist, skipping
creating tables...
100000 of 100000 tuples (100%) done (elapsed 0.12 s, remaining 0.00 s)
vacuum...
set primary keys...
done.

10クライアントから1000回のトランザクションを発行します。

まずはtmpfsでは無い場合を試してみます。 (https://github.com/onozaty/sandbox/blob/master/postgresql/tmpfs/vars.ymlpostgresql_data_dirを削除すると、デフォルトのデータフォルダになります)

[root@localhost ~]# /usr/pgsql-10/bin/pgbench -c 10 -t 1000 testdb -U user1
Password:
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 10
number of threads: 1
number of transactions per client: 1000
number of transactions actually processed: 10000/10000
latency average = 10.335 ms
tps = 967.558734 (including connections establishing)
tps = 967.906273 (excluding connections establishing)
[root@localhost ~]# /usr/pgsql-10/bin/pgbench -c 10 -t 1000 testdb -U user1
Password:
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 10
number of threads: 1
number of transactions per client: 1000
number of transactions actually processed: 10000/10000
latency average = 10.013 ms
tps = 998.726630 (including connections establishing)
tps = 999.077213 (excluding connections establishing)

2回計りました。tps(1秒間に実行できたトランザクションの数)は900後半でした。

続いてtmpfsの場合。こちらも2回計ってみます。

[root@localhost ~]# /usr/pgsql-10/bin/pgbench -c 10 -t 1000 testdb -U user1
Password:
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 10
number of threads: 1
number of transactions per client: 1000
number of transactions actually processed: 10000/10000
latency average = 7.276 ms
tps = 1374.364210 (including connections establishing)
tps = 1374.912095 (excluding connections establishing)
[root@localhost ~]# /usr/pgsql-10/bin/pgbench -c 10 -t 1000 testdb -U user1
Password:
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 10
number of threads: 1
number of transactions per client: 1000
number of transactions actually processed: 10000/10000
latency average = 7.619 ms
tps = 1312.520754 (including connections establishing)
tps = 1313.168002 (excluding connections establishing)

tpsは1300台でした。tmpfsにすることによって、1.3~1.4倍くらいの性能が出ています。ちなみにホストのストレージはSSDなので、これがもっと遅いストレージだと、もう少し差が大きくなるかもしれません。

おわりに

tmpfsにすることによってpgbenchでのパフォーマンスはあがりました。

ただ、tmpfsにしたために毎回起動時にinitdbから始めているので、その分余計な時間がかかることになります。トータルとして時間短縮の効果があるのかを判断しながら、使い分けてみようと思います。