$shibayu36->blog;

株式会社アンドパッドでエンジニアをしています。プログラミングや読書のことなどについて書いています。

Re: チームのベロシティを上げる vs. 安定させる

yigarashi.hatenablog.com

これが良い話だなと思ったので感想メモ。

「安定させる」のは良い。安定しないならチームの開発フローに問題が起こっていることが多く、「なんでブレちゃうんだっけ?」という議論からチームの課題が見つかることが多い。安定させるという考えなら変なハックが起こらない傾向にある印象

「上げる」は難しい。これまで、「上げよう」とした時に、ベロシティは基準によって変わる相対的指標なので、上げようと意識するとどれだけ気をつけても無意識にハックしてしまう経験がある。例えば

  • 完了条件には完全に明文化されていないような地味かつ大切なポイントが終わっていない(例えばコードのメンテナンス品質が思ったより低い)時も、上げる意識だと終わりにしてしまいたくなる
  • 考慮もれを見つけた時に、そのタスクで終わらせる意識ではなく、新タスクを作ってそっちで倒す意識になりがち
  • 何となく、悲観方向で見積もりをつけてしまう

そのため自分は、「安定させた上で、その後は価値提供の量および価値提供のスピード(フロー効率)を意識する」ようにしている。

ただし、以下のコメントもめちゃくちゃ分かる。

チームのベロシティを上げる vs. 安定させる - yigarashiのブログ

安定を優先すべきと頭ではわかっているが、数値的に上がっているのを見てモチベーションにつなげたいときがある

2022/04/26 11:28
b.hatena.ne.jp

この観点に関しては、以下のように考えている。

  • 成功体験を積むために、一番気軽な「ベロシティが上がった!」という体験をしたいということがある。つまりチームが理解しやすくするために敢えてベロシティに着目する。
  • その場合も、少なくとも先導する側は、「安定してユーザーに価値提供ができるチームを作る」「最速で価値検証ができるチーム」など、最終的な目標への意識を忘れないようにしたい
  • かつ、ベロシティに着目することを徐々にやめられるように、少しずつアジャイルの知識をチームに浸透させることが必要だと思う
  • なんだけど、「ベロシティあげよう」使ってしまいがちなんだよな〜〜〜〜〜〜〜〜(自戒)

コード変更で抜け漏れやミスを少なくするための習慣

自分はこれまでの仕事で、バグ修正や機能追加でPullRequestを送るときに、考慮の抜けもれやケアレスミスが非常に少ない方であると思っている。振り返ってみて、これは自分に課している習慣が大いに効いていると思っているので、メモしておく。

  • 毎回のcommit時
    • 必要な部分だけをgit addし、git diff --cachedによって差分をセルフコードレビューした上でcommitする
  • PullRequest作成時
    • filesの内容をセルフコードレビューし、直した方が良い部分は直す + わかりにくい部分にGitHub上でラインコメントを行う + コード上にコメントを残した方がいいならコメントする
    • ユーザーの導線に影響するコードなら、必ず自動テストだけでなく、自分自身でユーザーの行動をトレースし、違和感がある部分が存在しないかチェックする。チェックした流れについては、PullRequest上に「確認したこと」としてまとめる。必要があればgifアニメを添付しておく

GitHub ActionsでRuboCopのキャッシュを利用する

RuboCopはキャッシュファイルを作成し、2度目以降の実行を高速化するのですが、GitHub Actionsでキャッシュを利用するために工夫が必要だったのでメモしておきます。

小規模なプロジェクトの場合

小規模なプロジェクトでRuboCopの実行時間がある程度ありキャッシュを利用したい場合は、キャッシュのvalidityチェックは完全にRuboCopにお任せし、すべてのGitHub Actionsでキャッシュを作ってしまうやり方がオススメです。理由は、小規模な場合キャッシュサイズが問題になるケースは少なく、全部キャッシュした方がシンプルかつメンテナンス性も高いためです。

参考の設定はこちら。

    steps:
      ...
      - name: Cache rubocop
        uses: actions/cache@v3
        env:
          cache-name: cache-rubocop-v1
        with:
          path: ~/.cache/rubocop_cache
          key: ${{ env.cache-name }}-${{ github.head_ref }}
          restore-keys: ${{ env.cache-name }}-
      - run: bundle exec rubocop --parallel
      ...

古いキャッシュが残り続け、キャッシュサイズが肥大化する場合は、https://docs.rubocop.org/rubocop/usage/caching.html を参考に、AllCops: MaxFilesInCache を調整すると良いと思います。

大規模でキャッシュサイズが問題になる場合

大規模なプロジェクト場合、一度のRuboCopのキャッシュが500KB〜1MBに到達するケースがあります。かつ毎回のGitHub Actions実行でキャッシュを保持してしまうと、GitHub Actionsのキャッシュ上限に達してしまう場合があります。その場合はキャッシュの保持の仕方を工夫しなければなりません。ただし保持の仕方を間違えると以下の問題に遭遇します。

  • cache-rubocop-hashhhhdummmmmmy1のようなキャッシュが作られているとする
  • RuboCopのバージョンを更新すると、これまでのすべてのキャッシュが無効になる
  • すべてのキャッシュが使えないため、RuboCopのチェックがフルで実行される
  • 新しくできたキャッシュをActions側のキャッシュに入れようとするが、そちらのキャッシュキーが変わっていないとキャッシュ保存がスキップされてしまう
  • 結果、古いキャッシュを使い続け、ずっとフル実行のままになってしまう

この問題にならないためには、RuboCopで全キャッシュが無効になるタイミングでは、必ずGitHub Actionsのキャッシュキーも変えておく必要があります。https://docs.rubocop.org/rubocop/usage/caching.html#cache-validity を参考にすると、Rubyのバージョンを上げる・RuboCopのバージョンを上げる・RuboCopの設定を変える・rubocopコマンドへ渡すオプションを変えるケースだとすべてのキャッシュが無効になりそうです。

これらを考慮した参考例は次の通り。RuboCopのキャッシュが無効にならないケースでもGitHub Actions側のキャッシュキーが変わる時もありますが、キャッシュサイズの制御のためなので厳密なチェックはしていません。

    steps:
      ...
      - name: Cache rubocop
        uses: actions/cache@v3
        env:
          cache-name: cache-rubocop-v1
        with:
          path: ~/.cache/rubocop_cache
          # RuboCopのcacheのvalidityに合わせてキャッシュキーを決める
          # * RuboCopの設定変更の時にキャッシュを分けるため、.rubocop.ymlと.rubocop_todo.ymlを見る
          # * RuboCopのバージョン更新の時にキャッシュを分けるため、Gemfile.lockを見る
          # * Rubyのバージョンが変更された時にキャッシュを分けるため、.ruby-versionを見る
          key: ${{ env.cache-name }}-${{ hashFiles('.rubocop.yml', '.rubocop_todo.yml', 'Gemfile.lock', '.ruby-version') }}
          restore-keys: ${{ env.cache-name }}-
      - run: bundle exec rubocop --parallel
      ...

このチェックではrubocopコマンドに渡すオプションを変えたときにキャッシュキーを変えていないため、必要があればhashFilesに関係するファイルを入れておくと良さそうです。

またこのやり方の場合、hashFilesに指定したファイルをずっと更新しなかった場合に、徐々にキャッシュが古くなっていき実行が遅くなるという問題があります。しかし、大規模な場合、Gemfile.lockは1~2週に一度は更新されるであろうと考え、あまり気にしていません。様子を見て、もし問題になってきた場合は対策を考える予定です。例えばhttps://jpdebug.com/p/2821562のように、数日に一度はキャッシュがクリアされるように日付をキャッシュキーに入れるやり方を取ると良さそうです。

まとめ

今回はGitHub ActionsでRuboCopのキャッシュを保持する設定について書きました。大規模なプロジェクトの場合、工夫が必要なので難しいですね。

調査メモ: RuboCopのキャッシュ構造について

RuboCopは以下のようなキャッシュ構造になっている。

{cache_root}/{rubocop_checksum}/{context_checksum}/{file_checksum}

たとえば ~/.cache/rubocop_cache/48495939d6d5ca59d7f0a191fd9c11432a988b9d/eb1638cf9f9405f1dfcb03f4d43f86ec4b3f6af5/bab558f367464a4c3a65c3c112fae2e3168613cd のようなファイルに、どんな違反があったかの情報が書かれている。現在RuboCopチェックをかけようとしているファイルのキャッシュがあれば、違反内容は変わらないためキャッシュファイル内に記載された違反内容をそのまま返せば良い。

Rubyのバージョン変更、RuboCopの設定変更、RuboCopのオプション変更はrubocop_checksumやcontext_checksumへ影響を及ぼすので、全キャッシュが無効になるようだ。

まったく何も変更がない場合、checksumの計算 + RuboCopチェックをかける分のキャッシュ読み込みのみ行うため、ほぼ一瞬でRuboCopの結果が返ってくるようだ。