プリプロセッサを利用したプログレスインジケータの試み

ユーザを待たせる長い処理を行う場合には、進行状況をユーザに知らせるプログレスインジケータを出すのが作法である。Perlベストプラクティス では、ユーティリティサブルーチンを使う方法 (10.16 節) と Smart::Comments を用いる方法 (10.17 節) を紹介している。以前関わったプロジェクトでは、次のような条件があり、どちらも使えなかった

  • CPAN モジュールをインストールしずらい環境
  • 適当な間隔で '.' を表示するのは通常のプログレスインジケータと同じだが、10 回に一回は '*' を表示

なので、次のような解決策を用いた。

  • コード中に特殊なコメントの形でプログレスインジケータを挿入する場所を示す
  • コードを本番用に変換するフィルタを作り、それを通してから実行する

このほど、似たような状況になったのだが、ふと、Perl には C 言語のプリプロセッサを通す -P コマンドがあったことを思い出した。ので、プリプロセッサを利用したプログレスインジケータを作ってみた。
処理の始まりのところに配置する _BEGIN_PHASE には、標準エラー出力に表示する処理の名前 name と、後に示す _CONTINUE_PHASE を何回実行したら '.' を表示するかを表す interval を指定する。処理を繰り返すところには _CONTINUE_PHASE を置く。処理終了の場所には _END_PHASE を置き、処理終了を表示させるようにする。

use strict;
use warnings;

#define _BEGIN_PHASE(name,interval) my ($_c1,$_c2,$_int)=(0,0,interval); print {*STDERR} name
#define _CONTINUE_PHASE $_c1++;if($_c1>=$_int){$_c2++;if($_c2>=10){print {*STDERR} '*';$_c2=0;}else{print {*STDERR} '.'}$_c1 = 0;}
#define _END_PHASE print {*STDERR} "done\n"

sub heavy_process {
    use Time::HiRes qw/usleep/;
    usleep(1 + rand(10));
}

_BEGIN_PHASE('testing',100);
for my $i (0..100_000) {
    _CONTINUE_PHASE;
    heavy_process;
}
_END_PHASE;

実行するときには -P オプションをつける。

[takeyuki@sunya ~]$ perl -P a.pl
test.........*.........*.........*..()...*done

複数回使うためには、別のブロックに入れなければならない、などセンシティブな面も多く、実行時のコストもかかるが、ちょっと凝ったプログレスインジケータが必要な場合などに応用が効くのではないだろうか。