ZOZOTOWN Androidチームにおけるコードメトリクスとビルド時間計測の取り組み

ZOZOTOWN Androidチームにおけるコードメトリクスとビルド時間計測の取り組み

はじめに

ZOZOTOWN開発本部 ZOZOTOWNアプリ部 Android2ブロックの高橋です。

ZOZOTOWN Androidチームでは、リファクタリングやビルド速度改善の取り組みを継続的に行なっています。本記事では、それらの取り組みの効果を可視化するために導入した、コードメトリクスやビルド時間計測の方法について紹介します。

ZOZOTOWN Androidチームにおけるリファクタリングやビルド速度改善の取り組み

ZOZOTOWN Androidチームでは以前から、長いビルド時間や保守性の低いコードによって、チームの生産性が低下していることが問題となっていました。

そこで、保守性の高いコードを実現するためのリファクタリングや、ビルドの高速化に取り組んできました。しかし、それらの取り組みと並行して新機能の実装や既存機能の改修なども行なわれていたため、実際の改善度合いを把握することが難しい状況になっていました。

上記のような状況を鑑み、リファクタリングの効果・進捗とビルド時間を計測できる仕組みを検討し、導入しました。

コードメトリクスの計測

リファクタリングの効果・進捗を管理するための方法として、コードメトリクス計測の仕組みを導入しました。

メトリクス

計測するメトリクスは下記の目的に対応するものを選定しました。

  • リファクタリングの効果が高いファイルの検出
  • リファクタリングの進捗管理
  • チームのリファクタリングへの意識向上
  • 属人化しているコードの把握

コードメトリクス計測の導入目的に対応するメトリクスを検討した結果、下記のメトリクスを計測することになりました。

メトリクス 説明
Cyclomatic Complexity(循環的複雑度) メソッド単位でのコードの複雑度
LOC ファイルの行数
Author数 ファイルに対して変更を加えたメンバーの数

メトリクスの検討段階では、上記の他にも「構造複雑度」や「他ファイルからの被参照数」なども有効なメトリクスとして候補に挙がりました。しかし、それらのメトリクスは既存のツールでの計測が難しい、あるいはメトリクスそのものの理解が難しいなどの問題がありました。そこで、比較的スムーズに導入可能かつ理解が容易な「Cyclomatic Complexity」「LOC」「Author数」から計測を始めました。

Cyclomatic Complexity(循環的複雑度)

Cyclomatic Complexityは、メソッドの複雑度を示すメトリクスです。大まかにはif文やfor文などの分岐やループによって数値が増えます。数値の目安には決められたものはありませんが、一般的には下表のように言われています。

数値 複雑度とバグの混入リスク
〜10 シンプルな構造でバグの混入のリスクは低い
11〜20 やや複雑で中程度のバグの混入リスクがある
21〜50 複雑でバグの混入リスクが高い
51〜 テスト不可能な状態でバグの混入リスクが非常に高い

Cyclomatic Complexityを計測することで、バグの混入リスクが高いメソッドを検出できます。以上から、Cyclomatic Complexityは効果的なリファクタリングやリファクタリングの進捗管理に利用できると考え、計測対象としました。また、継続的にメトリクスを監視することで、チームのリファクタリングへの意識向上にも役立つと考えました。

LOC(ファイルのコード行数)

LOCは1ファイルあたりのコード行数を示すメトリクスです。LOCはいくつかの種類があります。

名称 説明
physical LOC(物理LOC) 空行やコメントの行数を含む、テキストファイルとしての行数
logical LOC(論理LOC) 空行やコメントの行数を含まない、実際の処理が記述されている行数

ZOZOTOWN Androidチームでは、空行やコメントを除いた実際の処理部分のリファクタリングにメトリクスを活用するため、logical LOCを計測対象のメトリクスとしました。

LOCを定期的に計測することで、削除予定となっているファイルや既に巨大になっているファイルに対する変更(追加)を把握できます。以上から、LOCはCyclomatic Complexityと同様に効果的なリファクタリングやリファクタリングの進捗管理に利用できると考え、計測対象としました。また、チームのリファクタリングへの意識向上についてもCyclomatic Complexityと同様に、継続的なメトリクスの監視によって達成できると考えました。

Author数

Author数は、ファイルに変更を加えた人数を示すメトリクスです。ZOZOTOWN Androidチームでは全てのコードの変更に対してコードレビューを実施しています。しかし、Author数が1の場合、該当ファイルを直接変更した人が1人しかおらずコードが属人化している状態である可能性が示唆されます。

Author数を計測することで、属人化したコードの内、特に重要な処理が記述されたコードの詳細をチームで共有できます。コードの属人化を解消することで、チームメンバーの仕様・実装理解が促進され、生産性の向上が期待できると考えました。Author数は、コードではなくGitのコミットログを解析して計測するため、一般的なコードメトリクスの文脈とは異なります。しかし、Author数はコードメトリクス計測の目的である「属人化しているコードの把握」に対応する指標であるため、計測することを決定しました。

計測方法

各メトリクスはそれぞれ異なるツールを使用して計測しました。いずれのツールも、継続的なメトリクス計測を目的として、GitHub ActionsのWorkflowに組み込みました。

ここでは、各メトリクスの計測方法とGitHub Actionsへの組み込みについて紹介します。

Cyclomatic Complexityの計測方法

Cyclomatic Complexityの計測には、KotlinとJavaで異なるツールを使用しました。

Java

Javaで記述されたコードのCyclomatic ComplexityはJava用の静的コード解析ツールであるcheckstyle/checkstyleを使用して計測しました。

checkstyleではCyclomatic Complexityのthreshold(許容最大値)がデフォルトでは3になっています。そこで、全てのメソッドのCyclomatic Complexityを検出するため、設定ファイルでthresholdを0に変更しました。

GitHub Actionsでcheckstyleを実行するJobは下記のようになります。

java-complexity:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v3
    - name: Setup Java
      uses: actions/setup-java@v2
      with:
        distribution: 'zulu'
        java-version: '11'
    - name: Install checkstyle
      run: curl -sSLO https://github.com/checkstyle/checkstyle/releases/download/checkstyle-10.1/checkstyle-10.1-all.jar
    - name: Run checkstyle
      run: find . -name "*.java" | xargs java -jar ./checkstyle-10.1-all.jar -f xml -c .github/checkstyle_rule.xml -o checkstyle_result.xml || true
    - name: Archive
      uses: actions/upload-artifact@v2
      with:
        name: result
        path: checkstyle_result.xml

このJobでは、checkstyleを実行し、出力結果を保存します。checkstyleは静的解析によって発見されたエラーの数がexitコードとなります。stepを正常終了させるため、ここではcheckstyleのexitコードを無視しています。出力結果はGitHub ActionsのArtifactsとして保存します。

レポートファイルは下記のようなXMLで出力されます。

<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="10.1">
<file name="/path/to/File.java">
<error line="18" column="5" severity="error" message="Cyclomatic Complexity is 1 (max allowed is 0)." source="com.puppycrawl.tools.checkstyle.checks.metrics.CyclomaticComplexityCheck"/>
<error line="25" column="5" severity="error" message="Cyclomatic Complexity is 1 (max allowed is 0)." source="com.puppycrawl.tools.checkstyle.checks.metrics.CyclomaticComplexityCheck"/>
</file>
...
</checkstyle>

fileタグのnameerrorタグのlinecolumnmessageを見ることで、計測対象のファイルに含まれるメソッドのCyclomatic Complexityを確認できます。

Kotlin

Kotlinで記述されたコードのCyclomatic Complexityは、Kotlin用の静的コード解析ツールであるdetekt/detektというツールを使用して計測しました。detektはコマンドラインツールやGradle Pluginとして利用できます。

detektではCyclomatic Complexityのthresholdがデフォルトでは15になっています。そこで、checkstyleと同様に全てのメソッドのCyclomatic Complexityを検出するため、設定ファイルでthresholdを0に変更しました。

complexity:
  ComplexMethod:
    active: true
    threshold: 0

GitHub Actionsでdetektを実行するJobは下記のようになります。

kotlin-complexity:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v3
    - name: Install detekt
      run: |
        curl -sSLO https://github.com/detekt/detekt/releases/download/v1.21.0/detekt-cli-1.21.0.zip
        unzip detekt-cli-1.21.0.zip
    - name: Run detekt
      run: ./detekt-cli-1.21.0/bin/detekt-cli -c .github/detekt-config.yml -r xml:detekt_result.xml || true
    - name: Archive
      uses: actions/upload-artifact@v2
      with:
        name: result
        path: detekt_result.xml

このJobでは、detektを実行し、出力結果を保存します。checkstyleと同様に、stepを正常終了させるため、exitコードを無視しています。出力結果はGitHub ActionsのArtifactsとして保存します。

レポートファイルは下記のようなXMLで出力されます。

<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="4.3">
<file name="/path/to/File1.kt">
    <error line="30" column="9" severity="warning" message="The function foo appears to be too complex (1). Defined complexity threshold for methods is set to &apos;0&apos;" source="detekt.ComplexMethod" />
</file>
<file name="/path/to/File2.kt">
    <error line="9" column="27" severity="warning" message="The function bar appears to be too complex (2). Defined complexity threshold for methods is set to &apos;0&apos;" source="detekt.ComplexMethod" />
</file>
...
</checkstyle>

detektもcheckstyleと同様に、fileタグとerrorタグを見ることで、計測対象のファイルに含まれるメソッドのCyclomatic Complexityを確認できます。

LOCの計測方法

LOCの計測はさまざまなプログラミング言語に対応したLOC計測ツールである、AlDanial/clocを使用しました。

GitHub Actionsでclocを実行するJobは下記のようになります。

lines-of-code:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v3
    - name: Setup cloc
      run: sudo apt install cloc
    - name: Run cloc
      run: cloc ./ --by-file --exclude-dir=build --include-ext=java,kt --xml --out=cloc_result.xml
    - uses: actions/upload-artifact@v2
      name: Archive
      with:
        name: result
        path: cloc_result.xml

このJobでは、clocを実行し、出力結果を保存します。JavaとKotlin以外のファイルや、ビルド時に生成されるファイルは解析対象から除外するため、exclude-dirinclude-extを設定しています。出力結果はLOCと同様に、GitHub ActionsのArtifactsとして保存します。

レポートファイルは下記のようなXMLで出力されます。

<?xml version="1.0" encoding="UTF-8"?><results>
<header>
  <cloc_url>github.com/AlDanial/cloc</cloc_url>
  <cloc_version>1.82</cloc_version>
  ...
</header>
<files>
  <file name="path/to/File1.java" blank="493" comment="275" code="2315"  language="Java" />
  <file name="path/to/File2.java" blank="262" comment="213" code="1841"  language="Java" />
  <file name="path/to/File3.java" blank="210" comment="117" code="1646"  language="Java" />
  ...
</files>

clocでは、fileタグを見ることで、空行の数とコメント行数、logical LOCを個別に確認できます。

Author数の計測方法

Author数の計測はiwata-n/git-analyzeをベースとし、カスタマイズしたものを使用しました。git-analyzeはファイル毎のコミット数やAuthor数を計測するリポジトリマイニングのツールです。git-analyzeは対象となるプロジェクトの全てのファイルに対して計測処理が実行されます。そこで、計測対象のファイル拡張子を指定できるようカスタマイズしたものを作成し、JavaとKotlinファイルのみを計測対象としました。

GitHub Actionsでgit-analyzeを実行するJobは下記のようになります。

number-of-authors:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 0
    - name: Run git-analyze
      run: |
        chmod u+x .github/git-analyze
        .github/git-analyze -branch=$TARGET_BRANCH -parse-file=git_analyze_result.json -ext=kt,java
    - name: Archive
      uses: actions/upload-artifact@v2
      with:
        name: result
        path: git_analyze_result.json

このJobでは、git-analyzeを実行し、出力結果を保存します。出力結果は、GitHub ActionsのArtifactsとして保存します。

レポートファイルは下記のようなJSONで出力されます。

[
  {
    "Path": "path/to/File.kt",
    "Authors": [
      "Metrics Taro",
      "Metrics Hanako"
    ],
    "CommitHash": [
      "f068ff6311893bdbae010c9c43b25ee65f1ccb06",
      "ab70bc1da76f067a3f9eea97159280750d998941"
    ],
    "CreateBy": "Metrics Taro"
  },
  {
    "Path": "path/to/File.kt",
    "Authors": [
      "Metrics Taro",
    ],
    "CommitHash": [
      "f068ff6311893bdbae010c9c43b25ee65f1ccb06",
    ],
    "CreateBy": "Metrics Taro"
  }
]

任意のファイルのAuthor数はAuthors配列のサイズを調べることで確認できます。

ビルド時間の計測

ビルド速度改善の効果計測と予期しないビルド時間の悪化を検知するため、ビルド時間計測の仕組みを導入しました。

計測方法

コードメトリクスの計測と同様に、ビルド時間を計測する仕組みもGitHub ActionsのWorkflowに組み込みました。ただし、コードメトリクスとは異なる頻度で計測するため、コードメトリクス計測とは別のWorkflowを用意しました。

ビルド時間の計測には、Square社が公開しているビルド時間計測に関する記事を参考に、gradle/gradle-profilerを使用しました。Gradle ProfilerはGradleを使用しているプロジェクトのビルドパフォーマンスを計測するツールです。Scenarioと呼ばれる設定を記述することで、ビルド時間やAndroid StudioのSync時間など、さまざまなパフォーマンスを計測できます。

Scenarioの設定はSquare社の記事とAndroid Developersを参考に、下記のようにしました。

build {
    tasks = [":app:assembleDebug"]
    gradle-args = ["--offline", "--no-build-cache"]
    show-build-cache-size = true
    warm-ups = 4
}

このScenarioでは、ビルドキャッシュを使用しなかった場合のビルド時間を計測します。gradle-argsには、プロジェクトが依存しているライブラリの取得時間が計測結果に影響を及ぼすことを防ぐため、--offlineを設定しています。また、ビルドキャッシュによるビルド時間計測への影響を防ぐため、--no-build-cacheも設定しています。

GitHub ActionsでGradle Profilerを実行するJobは下記のようになります。

measure-build-time:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v3
    - name: Copy CI gradle.properties
      run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
    - name: Set up JDK 11
      uses: actions/setup-java@v2
      with:
        distribution: 'zulu'
        java-version: '11'
    - name: Prefetch Gradle Dependencies
      run: ./gradlew --no-daemon assembleDebug
    - name: Run gradle-profiler
      run: |
        curl -s "https://get.sdkman.io" | bash
        source "$HOME/.sdkman/bin/sdkman-init.sh"
        sdk install gradleprofiler 0.18.0
        gradle-profiler --benchmark --scenario-file .github/performance.scenario build --gradle-user-home $HOME/.gradle
    - uses: actions/upload-artifact@v2
      name: Archive
      with:
        name: result
        path: profile-out

このJobではGradle Profilerのインストールと実行し出力結果の保存します。出力結果は、GitHub ActionsのArtifactsとして保存します。

レポートファイルはCSV形式で出力されます。また、後述するHTML形式のレポートも出力されます。

scenario  build
version Gradle 7.0.2
tasks   :app:assembleDebug
value   total execution time
warm-up build #1    267090
warm-up build #2    148813
warm-up build #3    135940
warm-up build #4    131386
measured build #1   131454
measured build #2   135238
measured build #3   141732
measured build #4   136364
measured build #5   139061
measured build #6   135794
measured build #7   138930
measured build #8   142453
measured build #9   141535
measured build #10  143400

各イテレーションでのビルド時間は、total execution time列で確認できます。

計測結果の可視化

コードメトリクスの計測結果

コードメトリクスは複数のツールを組み合わせて計測しているため、結果の一覧性がありません。そこで、計測結果を一覧で確認できるダッシュボードを作成しました。また、計測結果をBigQueryに保存し、GoogleデータポータルなどのBIツールでメトリクスの推移を継続的に監視できる仕組みを導入しました。

計測結果のパース

ダッシュボードの作成とBigQueryへの計測結果の保存に際して、メトリクス計測ツールが出力するXMLやJSONファイルを1つのJSON Linesファイルに統合するスクリプトを作成しました。このスクリプトは各メトリクスの計測後にGitHub Actions上で実行されます。出力されるJSON Linesファイルは、GitHub ActionsのArtifactsとして保存されます。

作成したスクリプトによって出力されるJSONは下記のようになります。実際はJSON Linesで出力されますが、ここでは見やすさのためフォーマットしています。

{
    "path": "path/to/File.kt",
    "language": "Kotlin",
    "loc": {
        "blank": 12,
        "comment": 4,
        "code": 39
    },
    "methods": [
        {
            "line": 17,
            "complexity": 1
        },
        {
            "line": 24,
            "complexity": 1
        },
    ],
    "numberOfCommits": 4,
    "numberOfAuthors": 3,
    "branch": "code_metrics",
    "commitHash": "cca154177ed75807f716bc9594fa16cc9a8405da",
    "date": "2022-08-19 20:51:11 Asia/Tokyo"
}
{
    "path": "path/to/File2.java",
    "language": "Java",
    "loc": {
        "blank": 12,
        "comment": 4,
        "code": 39
    },
    "methods": [
        {
            "line": 9,
            "complexity": 1
        },
        {
            "line": 18,
            "complexity": 10
        }
    ],
    "numberOfCommits": 6,
    "numberOfAuthors": 2,
    "branch": "code_metrics",
    "commitHash": "cca154177ed75807f716bc9594fa16cc9a8405da",
    "date": "2022-08-19 20:51:11 Asia/Tokyo"
}
...

各Keyの説明は下表の通りです。

Key 説明 値の取得に使用するツール
path 対象ファイルのパス cloc, checkstyle, detekt, git-analyze
language 言語 計測結果のパースをするスクリプト
loc LOC cloc
loc.blank 空行の数 cloc
loc.comment コメントの行数 cloc
loc.code logical LOC cloc
methods メソッドの情報を格納する配列 checkstyle, detekt
methods.line 対象のメソッドが存在する行番号 checkstyle, detekt
methods.complexity 対象のメソッドのCyclomatic Complexity checkstyle, detekt
numberOfCommits コミット数 git-analyze
numberOfAuthors Author数 git-analyze
branch 計測を実施したブランチ名 GitHub Actions
commitHash 計測時点のコミットのハッシュ値 GitHub Actions
date メトリクス計測を実施した日付 計測結果のパースをするスクリプト

内製ダッシュボードでの表示

スクリプトによって1つのファイルに統合された計測データは、社内にホスティングされたダッシュボードで確認できます。統合された計測データをダッシュボードにアップロードすると、1回分の計測結果を一覧で確認できます。このダッシュボードによって、計測結果をBigQueryに保存しない場合でもコードメトリクスの確認が可能になります。

コードメトリクスの内製ダッシュボード

ダッシュボードには、各ファイルのパス、言語、LOCと各ファイルに含まれるメソッドの最大Cyclomatic Complexity、Author数、コミット数が表示されます。任意のファイルのコードメトリクスは、言語やフリーワード入力のフィルターによってアクセスできます。また、各メトリクスでのソートも可能です。

データポータルでの表示

コードメトリクスの計測は、GitHub Actionsのscheduleイベントトリガーによって定期的に実行できます。scheduleイベントトリガーによって計測されたコードメトリクスは、BigQueryに保存することで、その推移を確認できます。

BigQueryに保存されたコードメトリクスは、データポータルで可視化できます。下図はZOZOTOWN Androidチームで継続的にリファクタリングを行なっているファイルの、ある期間にリリースされたバージョン毎のlogical LOCの推移を表しています。

コードメトリクスのデータポータル

この図からは、1750行以上あったlogical LOCがリファクタリングの取り組みによって100行以上減ったことがわかります。

このように、コードメトリクスの計測結果の保存・表示にBigQueryとデータポータルを利用することで、リファクタリング状況の継続的な監視が可能になります。

ビルド時間の計測結果

ビルド時間の計測結果は、Gradle Profilerが出力するHTMLで確認できます。また、コードメトリクスの計測結果と同様にビルド時間の推移を確認するための仕組みとして、BigQueryとデータポータルを導入しました。

計測結果のパース

ビルド時間の計測結果についても、計測結果をBigQueryに保存するため、Gradle Profilerが出力するCSVファイルをJSON Linesファイルへ変換するスクリプトを作成しました。

作成したスクリプトによって出力されるJSONは下記のようになります。

{
    "times": [
        153358,
        148786,
        155962,
        168292,
        162758,
        173117,
        162664,
        160480,
        162743,
        166319
    ],
    "mean": 161447.9,
    "median": 162703.5,
    "min": 148786,
    "max": 173117,
    "branch": "build_time",
    "commitHash": "cca154177ed75807f716bc9594fa16cc9a8405da",
    "date": "2022-08-24 01:30:52 Asia/Tokyo"
}

各Keyの説明は下表の通りです。

Key 説明 値の取得に使用するツール
times ビルド時間の計測結果(ms)の配列 Gradle Profiler
mean 計測結果の平均値(ms) Gradle Profiler
median 計測結果の中央値(ms) Gradle Profiler
min 計測結果の最小値(ms) Gradle Profiler
max 計測結果の最大値(ms) Gradle Profiler
branch 計測を実施したブランチ名 GitHub Actions
commitHash 計測時点のコミットのハッシュ値 GitHub Actions
date ビルド時間計測を実施した日付 計測結果のパースをするスクリプト

Gradle Profilerが出力するHTML

Gradle Profileが出力するHTMLには、イテレーション毎のビルド時間の計測結果や、その平均値などが表示されます。

gradle-profilerが出力するHTML

このHTMLファイルで計測されたビルド時間の詳細を確認できます。

データポータルでの表示

ビルド時間の計測もコードメトリクスの計測と同様に、GitHub Actionsのscheduleイベントトリガーによって定期的に実行できます。scheduleイベントトリガーによって計測されたビルド時間は、BigQueryに保存することで、その推移を確認できます。

データポータルで可視化した、ある期間にリリースされたバージョン毎のビルド時間の推移は下図の通りです。

ビルド時間のデータポータル

この図からは、特定のバージョンからビルド時間が大幅に増加したことがわかります。このように、指標の推移を可視化することで、ある時点からの指標の大幅な変化を検知できます。上図の例では、データポータルでの計測結果の確認後、Pull Request単位でのビルド時間の変化を計測し、ビルド時間の悪化原因が含まれるPull Requestを特定できました。

まとめ

本投稿では、ZOZOTOWN Androidにおけるコードメトリクスとビルド時間計測の取り組みを紹介しました。コードメトリクスによって示される数値は、必ずしも実際のコードの良し悪しを表していません。しかし、効率的なリファクタリングやリファクタリングの進捗管理に利用できます。今後は、効果的なリファクタリングのための、より有効なコードメトリクスの導入を進めていきたいと考えています。また、チームのリファクタリングへの意識向上のための、効果的なコードメトリクスの運用方法についても検討しようと考えています。ビルド時間計測については、増分ビルドの時間計測などのより開発時に近いシナリオでの計測を進めていきます。

最後に

ZOZOではAndroidエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください。

hrmos.co

カテゴリー