Line of Code (LoC) の計測

LoCの計測で一番簡単だと思われるのは次のような方法である。

find ./src -type f -name '*.c' -o -name '*.h' | xargs wc -l

この例では ./src ディレクトリに含まれる C 言語のソースファイルとヘッダファイルについて全て wc(1) に食わせて行数を出力する,といったことを行なっている。

しかしこれでは,プロジェクトで利用する言語が増える度に find(1) のオプションを調整しなければならないし,コード内の空行やコメント行を全く考慮しない。

cloc

そこで,実は cloc という便利な LoC 計測ツールがあるらしい。https://github.com/AlDanial/cloc

このコマンドでは,単純にプロジェクトのディレクトリを引数として渡すと,どういう言語がどのくらい使われており,それぞれに含まれる空行とコメントはどのぐらいか,といったことを一覧表示してくれる。

しかし,私はLinux kernelにおける各サブシステムがコード全体のうちどのぐらいを占めているかを知りたかったため,次のように cloc を実行した。

cd /usr/src/linux/
for dir in $(find . -maxdepth 1 -type d ! \( -name '.*' -o -name 'Documentation' -o -name 'LICENSES' -o -name 'samples' -o -name 'scripts' -o -name 'tools' \)); do
    echo $dir
    cloc $dir
done > ~/linux_loc.txt
すると linux_loc.txt には次のような出力が得られる。
./arch
   16005 text files.
   15902 unique files.
    4299 files ignored.

github.com/AlDanial/cloc v 1.72  T=28.79 s (406.7 files/s, 90104.3 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
C                             4725         226114         234741        1106210
C/C++ Header                  5078          82383         114960         432840
Assembly                      1245          44995          98313         220827
make                           591           3298           3271          12514
Perl                             7           1187            959           7692
Bourne Shell                    55            398            621           2090
awk                              5             51             58            486
Python                           1              7             12             46
sed                              1              2              5              5
-------------------------------------------------------------------------------
SUM:                         11708         358435         452940        1782710
-------------------------------------------------------------------------------
./block
      94 text files.
      94 unique files.
       3 files ignored.
...

さて,欲しいのは各サブシステムのコードが全体に占める割合であるため,次のようにして各サブシステムのコード行数を表にしてしまう。

dirs=$(find . -maxdepth 1 -type d ! \( -name '.*' -o -name 'Documentation' -o -name 'LICENSES' -o -name 'samples' -o -name 'scripts' -o -name 'tools' \))
for i in $(grep SUM ~/linux_loc.txt | awk '{print $5}'); do
  echo -e "${dirs[$count]}\t$i"
  set count (expr $count + 1)
done | sort -r -n -k 2,2 > loc.txt

これで loc.txt には次のような出力が得られるはずである。

./drivers       13048806
./arch  1782710
./fs    967163
./sound 935637
./net   828886
./include       644501
./kernel        237713
./lib   131807
./mm    91805
./crypto        83988
./security      67041
./block 37711
./ipc   6689
./virt  5234
./init  3309
./usr   867
./certs 442

最後にこの TSV から集計と割合の計算を行う。そう,こういう時役立つのは awk 言語だ。

LOC_percentage=$(awk 'NR==FNR{sum=sum+$2;next}{percentage=$2/sum*100; print $0, "\t", percentage"%"}' loc.txt loc.txt)
LOC_total=$(awk '{sum+=$2} END {print "total\t",sum}' loc.txt)
echo "$LOC_percentage" > loc.txt
echo "$LOC_total" >> loc.txt

NR は行数だが,FNR はファイル単位の行数である。ここでは loc.txt を二回食わせているため,一度目では NRFNR が一致するが,二度目では FNR はまた 1 からカウントアップされるが NR は一度目の数を引き継ぐため,一致しなくなる。 そこで,一度目のループでのみ各ディレクトリのコード行数を足し合わせており,また,nextによってその次の {} で囲まれているコードは実行しない。

二度目のループでは二つ目の {} の中身のみを実行するため,一度目のループで取得した総和に対して各ディレクトリのコード行数の割合を計算する,といった具合になる。

最後に,Linux 5.8 に対してこれを行なって,得られた結果は次の通りとなる。

./drivers       13048806    69.1353%
./arch          1782710     9.44517%
./fs            967163      5.12423%
./sound         935637      4.9572%
./net           828886      4.39161%
./include       644501      3.4147%
./kernel        237713      1.25945%
./lib           131807      0.698341%
./mm            91805       0.486402%
./crypto        83988       0.444986%
./security      67041       0.355197%
./block         37711       0.199801%
./ipc           6689        0.0354397%
./virt          5234        0.0277308%
./init          3309        0.0175318%
./usr           867         0.00459355%
./certs         442         0.00234181%
total           l18874309

コードの実に 8 割が drivers/arch/ で占められ,次点に fs/sound/net/ ... と続く。

ちなみに同様のことを OSv に対して実行するとこのようになる。

./modules       421847   51.0683%
./bsd   251317   30.4241%
./musl  68332    8.27219%
./include       22400    2.71172%
./drivers       15663    1.89614%
./core  12595    1.52474%
./libc  11885    1.43878%
./fs    9164     1.10938%
./arch  9136     1.10599%
./      3166     0.383272%
./fastlz        526      0.0636769%
./compiler      14       0.00169482%
total   826045

最大の行数を誇るのは modules/ であるが,この部分は CLI や RESTful API server,Java VM のバルーニング実装に libz や ncurses,openssl といったものが並ぶ。通常の OS であればユーザーランドに置かれるものであり,OSv のビルドや実行にも必ずしも含まれると限らない。

bsd/ には主に FreeBSD 由来であるネットワークスタックと ZFS のコードを含み,カーネルとしては実質この部分が最大になる。次に musl-libc の移植コードである musl/ が続き,それから以降は OSv の独自実装が主となっていく。

また,./ となっているのは OSv の場合トップディレクトリにもいくつか重要なコードがあるためで,これは bootfs.S, dummy-shlib.cc, empty_bootfs.S, gen-ctype-data.cc, linux.cc, loader.cc, Makefile, runtime.cc の 8 つのファイルを集計したものとなる。

OSv は unikernel としては実装が大きいほうだが,それでも全体で 8 万行強であり,コア部分だけみるとたかだか 1 万行になっており,Linux に比べればカーネルの実装全体に目が通しやすい。