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

私の仕事では、進行状況インジケーターなしでは実行できない長いプロセスを実装することがよくあります。 問題はプロセスが複雑になりすぎたときに始まりましたが、同時に、プロセス全体に対して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_halfdo_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_progresspop_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ループで動作します(ところで、上記のループはforeachfor ( map { 2 **$_ } 0. .10 ) ))。 for_progressは、反復ごとにこのアクションチェーンを実行します。スタックの範囲[ $i / $n * 100 , ( $i + 1 ) / $n * 100 ]for_progressします。ここで、$ iは反復数、$ nは要素の数ですリスト、現在の要素を$ _にロード、コードブロックを実行、 update_progress(100)呼び出し、スタックから最後の要素を抽出します。 次に、既存のループでforfor_progressに置き換えfor 、リストを最後までドラッグし( map )、別の変数を使用した場合は変数に$ _を割り当てます。 内部for_progress for for_progressレギュラーであるfornextlast引き続き機能します( 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 ;
現代のプログラミングは、言葉のmapreduceなしでは想像しにくいです。 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ものがあります。 ドキュメントには、詳細な説明と例があります。

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

Source: https://habr.com/ru/post/J81765/


All Articles