スタック付きの進行状況インジケーター

私の仕事では、進行状況インジケーターなしでは実行できない長いプロセスを実装することがよくあります。 問題はプロセスが複雑になりすぎたときに始まりましたが、同時に、プロセス全体に対して1つの継続的な進行状況インジケーターが必要でした。 たとえば、プロセスはAsub、Bsub、およびCsub関数の呼び出しで構成できますが、それぞれの呼び出しにはかなりの時間がかかります(合計時間の約10%、20%、70%など)。 Asubに2つのループを、Bsubに複数のネストされたループを、Csubに1つのループを、ただしこのループの途中でAsubを呼び出します。 問題を正面から解くと、すべての行の3分の1が現在のパーセンテージを計算し、UIでそれを更新する時間かどうかを判断する状態にコードをもたらし、Asub関数は表示するパーセンテージ範囲(0から10まで)を決定する追加パラメーターを取得しますメインプロセスから呼び出された場合、またはCsub内から呼び出された場合は他の何か)。 その結果、コードの可読性が失われ、コードの維持がより困難になります。 そして、Bsubを別の場所で再利用したいが、途中ではなく、全体のプロセスの最後にBsubを再利用したいので、10分から30%の割合で表示されないように、楽しい時間を待っています。 私はこれで何かをする必要があるという結論に達しました。



次の要件を設定します。 既存のコードに進行状況を追加することはできません:

  1. 既存の関数とメソッドのプロトタイプを変更します。
  2. 関数内に新しい変数を追加します。
  3. 現在の進行状況の自明でない計算を含めるには(たとえば、 100 * $i / $n



    すでに自明ではないと見なされます)。
  4. この高価な操作に時間を浪費するために、進行状況インジケーターを更新する必要があるかどうかを理解するために、タイマーを調整するか、反復をカウントします。
進行状況インジケータの表示については説明しません。お気に入りのウィンドウシステムでウィジェットやコントロールを使用したり、お気に入りのWebSocketを介してWebフロントエンドに転送したり、STDOUTに「12%」の行を表示したりできます。 レンダラー-現在の進行状況をパーセンテージで受け入れる出力関数と、オプションでプロセスまたはそのステージを説明するテキストメッセージがあるとします。



プロセスをサブプロセスに分割する



簡単な例は次のようになります。

init_progress ;

#

do_first_half ;

update_progress 50 ;

#

do_last_half ;

update_progress 100 ;





ここで、それぞれの半分が、進行状況情報を提供できる長い関数への挑戦であると仮定します。 ただし、どのコンテキストで呼び出されたか、およびその実装に割り当てられた一般的な進捗インジケータの範囲はわかりません。 自然な実装は次のようになります。

sub do_first_half ( ) {

#

update_progress 33 ;

#

update_progress 66 ;

#

update_progress 100 ;

}





つまり、進行状況に関する情報を報告し、希望する範囲(この場合は0〜50%)で誰かに表示させます。 ここで、3次元座標のアフィン変換が4×4マトリックスで記述され、変換のシーケンスがスタックに配置されるOpenGLマトリックススタックとの類推を思い付きました。特定のオブジェクトの頂点を指定する場合は、計算なしで特定の数値を示します。 OpenGL自体が座標を変換し、特定のマトリックスを乗算します。 ここでは、実際には、進行状況インジケーターの座標もあり、1次元のみです。 アフィン変換は、転送とスケーリングの2つの数値で記述されます。 変換をスタックに配置し、 update_progress



関数update_progress



必要な変換update_progress



実行し、既に変換された座標をレンダラーに渡します。

# [, ]

my @stack = ( [ 1 , 0 ] ) ;

sub update_progress ( $ ) {

my $percent = shift ;

$percent = $stack [ - 1 ] [ 0 ] * $percent + $stack [ - 1 ] [ 1 ] ;

renderer ( $percent ) ;

}





次にpush_progress



およびpop_progress



追加しpop_progress



。 使いやすさのために、スケーリングとpush_progress



に転送するのではなく、後続のパーセンテージが表示される範囲を転送します。 もちろん、何らかの種類の変換が既に有効になっている場合は、 push_progress



パラメーターも変換するpush_progress



あります。

sub push_progress ( $$ ) {

#

my ( $s , $e ) = @_ ;

#

( $s , $e ) = map { $stack [ - 1 ] [ 0 ] * $_ + $stack [ - 1 ] [ 1 ] } ( $s , $e ) ;

#

push @stack , [ ( $e - $s ) / 100 , $s ] ;

}



sub pop_progress ( ) {

pop @stack ;

}




今では、関数呼び出しdo_first_half



do_last_half



を角かっこpush_progress/pop_progress



ラップするだけpush_progress/pop_progress





push_progress 0 , 50 ;

do_first_half ;

pop_progress ;

push_progress 50 , 100 ;

do_last_half ;

pop_progress ;





すでに悪くない。 残念ながら、各push_progress



がペアのpop_progress



対応することを確認する必要があります。 ただし、 push_progress



pop_progress



間のコードフラグメントをブロックにラップして、 sub_progress



関数に渡すことがsub_progress



ます。

sub sub_progress ( & $$ ) {

my ( $code , $s , $e ) = @_ ;

push_progress $s , $e ;

my @retval = & { $code } ( ) ;

update_progress 100 ;

pop_progress ;

return @retval ;

}





次に、メインコードが簡略化されます。

sub_progress { do_first_half } 0 , 50 ;

sub_progress { do_last_half } 50 , 100 ;





pop_progress



前に、ブロックがこれを行うのを忘れた場合に備えて、 update_progress(100)



を呼び出しました。 これで、 $s



パラメーターが不要であることが明らかになりました。代わりに、進行状況インジケーターの最後に表示された値を使用できます。



サイクル



次に、ループで何ができるかを見てみましょう。 サイクルのすべての反復がほぼ同じ時間であり、反復回数がわかっていると仮定します。 これはfor ( $i = 1 ; $i < = 1024 ; $i*= 2 )



のようなループでは動作しませんが、任意のforeach



ループで動作します(ところで、上記のループはforeach



for ( map { 2 **$_ } 0. .10 )



))。 for_progress



は、反復ごとにこのアクションチェーンを実行します。スタックの範囲[ $i / $n * 100 , ( $i + 1 ) / $n * 100 ]



for_progress



します。ここで、$ iは反復数、$ nは要素の数ですリスト、現在の要素を$ _にロード、コードブロックを実行、 update_progress(100)



呼び出し、スタックから最後の要素を抽出します。 次に、既存のループでfor



for_progress



に置き換えfor



、リストを最後までドラッグし( map



)、別の変数を使用した場合は変数に$ _を割り当てます。 内部for_progress



for



for_progress



レギュラーであるfor



next



last



引き続き機能します( for_progress



ますが)。 最も簡単なテストは次のようになります。

init_progress ;

for_progress { sleep ( 1 ) } 1. .10 ;





update_progress



はブロックの最後で自動的に呼び出されるため、ループからまったくupdate_progress



できます。 ただし、各反復が長い場合は、現在の反復の完了率を示すことで使用できます。 もちろん、 sub_progress



内でfor_progress



を使用してネストされたループが機能し、その逆も同様です。 以下に簡単な例を示します。

sub A {

for_progress {

sleep ( 1 ) ;

} 1. .4 ;

}



sub B {

sleep ( 1 ) ;

update_progress 10 ;

sub_progress { A } 50 ;

sleep ( 1 ) ;

update_progress 60 ;

sleep ( 2 ) ;

update_progress 80 ;

sleep ( 2 ) ;

}



init_progress ;

sub_progress { A } 25 ;

sub_progress { A } 50 ;

sub_progress { B } 100 ;





現代のプログラミングは、言葉のmap



reduce



なしでは想像しにくいです。 map_progress



およびreduce_progress



ラッパーもそれらに書き込まれます。

init_progress ;

print " \n Sum of cubes from 1 to 1000000 = " .

reduce_progress { $a + $b * $b * $b } 1. .1000000 ;





ここでは、もちろん、生産性の問題が発生します。反復が短すぎ、毎回進行状況インジケーターを更新するための呼び出しにより、プロセスが大幅に遅くなります。 update_progress



はこれを考慮し、毎回レンダラーを呼び出しませんが、それが必要であると考えた場合のみ:パーセンテージが100に達した場合、最後の更新からかなりの時間が経過したか、十分な時間が経過した(すべてがinit_progress



パラメーターで構成されます) さらに、追加の最適化が行われました。その結果、 reduce_progress



を使用した私の例は、 List::Util::reduce



場合よりも4.5倍遅いだけです。 非常に短い反復では、慎重に使用してください。



入手先



Progress::Stack



モジュールの最初のバージョンをCPANに配置しました。 これまで、名前空間のアプリケーションは承認されていませんが、パッケージはCPANのWebサイトからダウンロードできます。 ここで説明した機能に加えて、オブジェクトインターフェイス(特に必要ではありません)や、 while ( <FH> ) { }



に似たテキストファイルを処理するためのfile_progress



関数など、 file_progress



ものがあります。 ドキュメントには、詳細な説明と例があります。



コメントや提案を歓迎します:-)



All Articles