「bash time の min, avg, max」なスクリプト

連日 Unix「的」なネタな。

これ、ワタシ的にはかなり「イマサラ」なネタで、話の性格としてはこのネタと完全に同じ。「出来合いのソフトウェアを探し回って疲れ果てるくらいなら作ってしまった方が早い、と考えるタイプの、なおかつ、毎度短時間で書いてしまって整理しないので何度も書くハメになる」ヤツね。

「Python スニペットの性能を計測する」のには、timeit が手っ取り早くて便利。けど今やりたいのはそれじゃなくて、「プロセス(コマンド)全体の処理時間」の計測。

Unix には伝統的に「time」がある。で、見出しであえて「bash」を冠した見出しを付けてるのは、「伝統的な」ものとは必ずしも同じじゃないから。「Unix ユーザ」として「time を使う」と思った場合、実際は「time コマンド」と「bash 組み込みの time」など複数のものがあって、それらには微妙な差異がある。ユーザとしてみた場合に一番顕著に違いを感じるパターンはおそらく「linux ユーザか BSD ユーザか」の差。linux ユーザは「問答無用で bash」な環境に慣れてるのでたぶん「ほかの time」を知らない。そして FreeBSD あたりを使ってみてビビることになる。

ともあれ今扱うのは「bash の time」ね。たとえばこんな感じ:

1 [me@host: ~]$ time ./aaa.sh
2    ...
3 
4 real    33m0.499s
5 user    0m0.031s
6 sys     0m0.061s

こいつの出力は標準エラーに行く。real, user, sys の各々の意味についてはググってくれ。懇切丁寧な説明はきっと見つかる。

で、この「time」、単独のコマンドラインだけを一回測るだけのつもりなら、それだけでいいんだけれど、「何度も繰り返して平均を取りたい」みたいなことをしたいなら、「自力で」やるしかない。てな話。あとなんだかんだ「標準エラーに行く」のが煩わしさの種になる場合もある。特に「全情報が標準エラーに行く」てなダメな設計の ffmpeg とかね。(つーか ffmpeg はいいんだが、なんで ffprobe の出力が標準エラーなんだ、てことだ。) そんなヤツ相手だと、シンプルにリダイレクトしたり tee したりですら「ウザい」コマンドラインになる。

てわけで。

このネタと同じく「きっと探せばちゃんとしたものはあるんだろうなぁ」と思いつつ 1時間以内で書いた:

「bashtimeit.py」みたいな名前として
 1 #! /bin/env python
 2 # -*- coding: utf-8 -*-
 3 from __future__ import unicode_literals
 4 
 5 import sys
 6 import re
 7 import subprocess
 8 
 9 
10 def _filter_args(*cmd):
11     if hasattr("", "decode"):  # python 2
12         def _encode(s):
13             return s.encode(sys.getfilesystemencoding())
14     else:
15         def _encode(s):
16             return s
17     return list(map(_encode, filter(None, *cmd)))
18 
19 
20 def _bashtime_one(cmdline):
21     p = subprocess.Popen(
22         _filter_args(["bash", "-c", "time {}".format(cmdline)]),
23         stderr=subprocess.PIPE, stdout=subprocess.PIPE)
24     tres = []
25     for line in p.stderr.readlines():
26         tres.append(line.rstrip().decode())
27         if len(tres) > 3:
28             tres = tres[-3:]
29     rgx = re.compile(r"(.+)\t(.+)m(.*)s")
30     return {
31         nm: 60 * int(m) + float(s)
32         for nm, m, s in [
33             rgx.match(t).group(1, 2, 3)
34             for t in tres]}
35 
36 
37 if __name__ == '__main__':
38     import argparse
39     parser = argparse.ArgumentParser()
40     parser.add_argument("command")
41     parser.add_argument("--repeat", type=int, default=10)
42     args = parser.parse_args()
43 
44     tmp_res = {"real": [], "sys": [], "user": []}
45     for i in range(args.repeat):
46         for k, v in _bashtime_one(args.command).items():
47             tmp_res[k].append(v)
48     for k, v in tmp_res.items():
49         print("{:4s}: min={:.3f}s, avg={:.3f}s, max={:.3f}s".format(
50                 k, min(v), sum(v) / len(v), max(v)))

使い方はたとえば:

1 [me@host: ~]$ python bashtimeit.py --r=100 ./aaa.sh
2 real: min=0.073s, avg=0.080s, max=0.142s
3 sys : min=0.000s, avg=0.026s, max=0.061s
4 user: min=0.000s, avg=0.010s, max=0.045s

とか。