oinume journal

Scratchpad of what I learned

go testを並列で動かして速くする

はじめに

アプリケーションが大きくなってくると、テストを並列で実行しないとどんどんgo testの実行時間が長くなってしまい、いわゆる「CI待ち」というものが発生してしまう。この記事は自分用のメモだが、テストを少しでも速くしたいという人のための記事。おそらく会社の誰かがもっと丁寧な説明のブログ記事を書いてくれるはず。

go testを速くする方法

方法としては以下がある。それぞれ細かく説明していく。

  • go test実行時に-pオプションを使う
  • Test関数にt.Parallelを入れる

go testの実行方法

go testコマンドでは大きく分けて以下の2つを行っている。

  1. .go, _test.goをコンパイルしてバイナリを生成
  2. 生成したバイナリの実行(=テストの実行)

この時 go test ./... のように実行するとパッケージごとにバイナリを生成してテストを実行する。-pフラグはこのパッケージごとのコンパイルとテスト実行の並列数を設定するもので、デフォルトではGOMAXPROCSの数になっている。

t.Parallel

上では-pによってパッケージごとのテストの実行を並列にすることができると説明したが、同じパッケージ内のTest関数は直列で実行されている。これを並列で実行したい場合はどうするのかというと、(*T).Parallelを使えば良い。これを入れることで「そのTest関数は並列実行できる」ということを伝えられる。

まとめ

  • パッケージごとのテストのコンパイルと実行を並列にしたい場合は-pを指定する(デフォルトはGOMAXPROCS(
  • t.Parallel()の呼び出しがあると、そのTest関数は並列で実行される

Touch BarありでIntelliJ IDEAのShift + F6などのショートカットキーが効かない場合の対処方法

TL;DR

  • Touch BarありのMacBookProで、IntelliJ IDEAでShift + F6などのFunction Keyと他のキーを組み合わせた場合のショートカットキーが効かないという問題があった
  • Karabinar Elementsを使っている場合は DevicesNo product name (No manufacturer name) にチェックを入れることで解決する
  • スクショ: https://github.com/pqrs-org/Karabiner-Elements/issues/535#issuecomment-350522019

問題の詳細

IntelliJ IDEAをTouch BarありのMacBookProを使っている場合、Shift + F6(Rename)やOpt + F7(Find Usages)のようなFunction Keyと組み合わせたショートカットキーが動作しないという問題に長年苦しめられてきた。特にIntelliJはFunction Keyとなにかを組み合わるショートカットが多くて、これはIntelliJ使いとしては死活問題だった。

この問題のせいで「MacBook ProもTouch Barついてるのか?じゃあ買うのやーめよ」と本気で考えるぐらい困っていた。そして実際Touch BarがないMacBook Airを買う寸前だった。

どうやって解決したのか?

MacBook Airを購入する寸前で「もしかしたら解決方法があるかもしれない」と思い、いろいろとググってみた。しかし検索しても引っかかるのはIntelliJのTouch Bar Supportの記事ばかり... しかし、IntelliJのフォーラムのコメントを熟読していたところ、以下のようなコメントがあった。

Ok, did som further debugging and tracked it down to Karabiner Elements (which I have installed) - quitting it fixed the problem. It seems that they have a number of conflicts with the touchbar

おっ、もしかしてKarabinar Elementsがなにか悪さしているのか?と思い、試しにKarabinar-Elementsを落としてShift + F6を試したところ Renameが動く。動くぞ...!!!

というわけで、 IntelliJ Touch Bar Karabinar Elements function keys というキーワードでググったところ、冒頭で紹介したKarabinar-ElementsのIssueにたどりついた。そして以下のスクショのようにNo product name (No manufacturer name)のチェックを入れて見事Shift + F6が効くようになりましたとさ。

というか、2017年に解決されていたのね、、、ググる力が足りなくて3年も我慢してしまっていた。

まとめ

Touch Bar + IntelliJ のファンクションキーが効くようになり人権を取り戻しました。

使ってみて便利だったGitHub Actions

今年の2月ぐらいからGitHub Actionsを仕事で使うようになったので、実際に使ってみて便利だったものを紹介する。

Slackへの通知を行うaction-slack

github.com

Slackへ通知するActionはいくつかあるけど、これが一番きめ細かく送る内容を設定できてかゆいところに手が届く感じだった。

branchにcommitがあったらSyncするPull requestを作るSync branches

github.com

たとえばmasterとdevelop branchがあるとして、masterにcommitがあった時にmaster -> develop に対して差分のpull requestを自動で作ってくれる。自分たちはそれを確認してマージすればよい。

特定のファイルに更新があった場合にラベルをつけるLabeler

github.com

設定ファイルを書くことで「このディレクトリ配下のファイルが更新されたら」とか「この拡張子のファイルが更新されたら」とかの条件でPull requestにラベルをつけることができる。難点なのは、以下のような時にラベルを自動で削除してくれないこと。

  1. 条件にマッチするファイルが更新される(差分がある)
  2. ラベルがつく
  3. 別のコミットによって1.のファイルの差分がなくなる

GoでLRU Cacheを実装する

LRU Cacheは何かをキャッシュする際によく使うデータ構造の一つだと思う。よく使う一方でその実装はやったことがなかったので、今回Goで実装してみたよ、という話。

LRUCacheとは?

Least Recently Used Cache のこと。一定のキャパシティを持つもので、キャパシティを超える場合使用された時間がもっとも古い要素から削除される。詳しくはWikipediaを見てもらうと良いと思う。

Goでの実装とその解説

まずは単純に実装してみた。テストも含めると上記の3つのファイルから構成されている。lru_cache.goには以下のようなGetとPutメソッドがinterfaceとして定義されており、その実装がdefault.goにある。

type LRUCache interface {
    // Get returns value for `key`. Returns -1 if not found
    Get(key int) int
    // Put sets value with key
    Put(key, value int)
}

使用例

example_test.go で使用例が書かれており、これを

   cache := lru_cache.NewDefault(2)
    cache.Put(1, 1)
    cache.Put(2, 2)

    fmt.Println(cache.Get(1)) // References key `1`

    cache.Put(3, 3) // This operation evicts key `2`
    // Output:
    // 1
    // -1
    fmt.Println(cache.Get(2))

上のコードにあるように、cache.Put(3, 3) によって一番参照が古いkey 2 がcacheから削除される。結果として、cache.Get(2)は-1を返す。これがLRU Cacheの基本的な動作である。

キャッシュのアイテム

キャッシュするアイテムはmapを使って管理する。この時、「いつ参照されたのか」を判断するために、mapのvalueには以下のようなitem構造体を保存する。

type item struct {
    value int
    age   int
}

なお、item.ageはそのアイテムがGetで参照された時またはPutで追加された時のageとなる。そのため、LRU Cacheの実装をしている defaultLRUCache ではGet/Putが呼ばれるたびにcurrentAgeというフィールドをインクリメントする。

type defaultLRUCache struct {
    capacity   int
    values     map[int]*item
    currentAge int
    mutex      *sync.Mutex
}

Get

Getの実装はシンプルで、以下を行うだけである

  • keyが存在しない場合: -1を返す
  • keyが存在する場合: 参照されたitemのageを更新し、currentAgeをインクリメントしてreturnする
func (c *defaultLRUCache) Get(key int) int {
    i, ok := c.values[key]
    if !ok {
        return -1
    }
    c.mutex.Lock()
    i.age = c.currentAge
    // `Get` also increment current age
    c.currentAge++
    c.mutex.Unlock()
    return i.value
}

Put

Getに比べるとPutはやや複雑である。

  • すでにitemがある場合: mapに存在するitemのageを更新する
  • itemがない場合
    • LRU Cacheのcapacityを超えている場合: mapの中で一番ageが小さいitemを見つけて削除する
    • 上記に加えて、mapにitemをセットする

という操作を行う。またそれぞれの操作で currentAgeをインクリメントする。

func (c *defaultLRUCache) Put(key int, value int) {
    c.mutex.Lock()
    defer c.mutex.Unlock()

    if i, ok := c.values[key]; ok {
        // If the key exists, update its value and increment its age for this key
        i.value = value
        i.age = c.currentAge
        c.currentAge++
    } else {
        if len(c.values) >= c.capacity {
            // Search key with least age when over capacity before setting key and value
            leastAge := math.MaxInt32
            leastAgeKey := 0
            for key, item := range c.values {
                if item.age < leastAge {
                    leastAge = item.age
                    leastAgeKey = key
                }
            }
            if leastAgeKey != 0 {
                // Evict least age key from cache
                delete(c.values, leastAgeKey)
            }
        }
        // Set key and value to cache
        c.values[key] = &item{
            value: value,
            age:   c.currentAge,
        }
        c.currentAge++
    }
}

この実装の問題点

このdefaultLRUCacheの実装には一つ大きな問題がある。というのは、capacityを超えている場合、ループで一番ageが小さいものを探しているため、O(n)のコストがかかる。これについては長くなるので別の記事でまとめようと思う。

まとめ

LRU Cacheのとてもシンプルな実装をやってみた。簡単なので面接の時の課題として出してみるのも面白いかなと思った。