続: 結局は psutil の方がより救世主なの (psutil.Process の nice)

psutil に触れたその時点でもう言わずもがなな人相手には言わずもがな、な内容なので、「ワタシが書くことか?」とも思うようなそんな。

Unix ユーザと Windows ユーザの違いの一つとして、「タスク(あるいは「ジョブ」、「プロセス」)」についての「優先度」が身近なのかどうか、てのがある。

命名の妙だよな、と思うのだよね。「Unix の教科書」的なものを開けばかなり最初の方に登場しそうなのが「やったねあんたはえらいnice値」で、なんでそう教科書的かというと、「Unix文化」をよく表した命名だから、なの。

しょっぱなから「パーソナルなコンピュータ」としてのスタートだった MSDOS, Windows, Mac OS らと違って、もともと Unix は「多重ログイン上等」のもとに誕生した OS である。つまり、「たくさんのユーザが一台の Unix 機にログインして使うので、「譲り合いの心が必要である」」と。そうして「プロセスの優先度を調整するプログラム」に「nice」という名前がついた。そう、「譲ってあげるオレ、ナイスでしょ?」てこと。ゆえ、Unix ユーザは、(少なくとも昔は)初心者であっても「プロセスの優先度を変更できる」ことを常識として知っていた。

一方で、Windows にはそんな文化はない上に、マイクロソフトが「そういうことはあなたは意識すべきではない」として、原則巧妙に隠しているので、実は Windows でも同じことが出来ることを、おそらく一般ユーザの多くが知らない。Unix 経験者のほうが気づきやすいとは思うが、そうでない場合でこれを知っているのは、特に Windows サービスなどのシステムプログラミング経験者で、一般のデスクトップアプリケーション開発しか経験がないと、技術者であっても結構知らないんではないかと想像する。出来るよ:

問題は、今の Windows 10 タスクマネージャでこの「優先度の設定」メニューを「どうやって出すか」だ。わからない人はわからない。気づかない人は多分一生気づかない。

そんなわけで、Windows 文化において、マイクロソフト自身以外でこの「優先度」を活用していると思われるのは「Windows を何かしらのサーバに採用した(奇特な)組織」くらいではないのかと思う。以外では、個々のアプリケーションが自身の優先度を変えるために使う例がわずかばかりある程度である(たとえば 7-zip などのアーカイバや、ビデオ編集ソフトなどの重いやつら)。

てわけで、Windows で Unix と同じようなノリで優先度を変えようとするのは「タスクマネージャがあぁなので」鬱陶しくて、普通はそんなに日常にはしてないだろう。そんなあなたに?:

suspend, resume ネタで書いたコードとほとんど同じである
 1 # -*- coding: utf-8 -*-
 2 from __future__ import print_function
 3 from __future__ import unicode_literals
 4 
 5 import os
 6 import sys
 7 import pipes
 8 import re
 9 import psutil
10 
11 
12 def _str(s):
13     return s.encode(sys.getfilesystemencoding(), errors="xmlcharrefreplace").decode(
14         sys.getfilesystemencoding())
15 
16 
17 _prio_nmap = {
18     "1": ("ABOVE_NORMAL_PRIORITY_CLASS", psutil.ABOVE_NORMAL_PRIORITY_CLASS),
19     "2": ("BELOW_NORMAL_PRIORITY_CLASS", psutil.BELOW_NORMAL_PRIORITY_CLASS),
20     "3": ("HIGH_PRIORITY_CLASS", psutil.HIGH_PRIORITY_CLASS),
21     "4": ("IDLE_PRIORITY_CLASS", psutil.IDLE_PRIORITY_CLASS),
22     "5": ("NORMAL_PRIORITY_CLASS", psutil.NORMAL_PRIORITY_CLASS),
23     "6": ("REALTIME_PRIORITY_CLASS", psutil.REALTIME_PRIORITY_CLASS),
24 }
25 
26 
27 if __name__ == '__main__':
28     import argparse
29     ap = argparse.ArgumentParser()
30     ap.add_argument("--search_cmdline_regexp", default=".*")
31     ap.add_argument("--search_cmd_regexp", default=".*")
32     args = ap.parse_args()
33     def _catcmdl(cmdl):
34         return " ".join([pipes.quote(a) for a in cmdl])
35     ma = {
36         "cmd": re.compile(args.search_cmd_regexp),
37         "cmdline": re.compile(args.search_cmdline_regexp),
38         }
39     for p in psutil.process_iter():
40         cmdl = ""
41         try:
42             cmdl = p.cmdline()
43         except Exception as e:
44             print(e, file=sys.stderr)
45         if not cmdl:
46             continue
47         tcmdl = _catcmdl(cmdl)
48         if ma["cmd"].search(cmdl[0]) and ma["cmdline"].search(tcmdl):
49             orig = p.nice()
50             print(_str(tcmdl), "|", p.nice())
51             q = input("{}? ".format(" / ".join(
52                 ["{}: {}".format(n, _prio_nmap[n][0]) for n in sorted(_prio_nmap.keys())])))
53             if q in _prio_nmap:
54                 p.nice(_prio_nmap[q][1])
55                 print(orig, "=>", p.nice())

どういうときに使いたいかって、たぶんだけど「優先度を上げたい」時ではない気がする。まさに ffmpeg なんだけど、「どうせ何時間もかかるんなら 1時間も2時間も同じろ」として優先度を下げてノンビリ処理してもらおうぜ、みたいなことをね、たぶんしたいことが多いと思うんだ。他のあらゆる作業ができなくなるほど凄まじく CPU 時間を使ってしまうことも多いからね。


2021-04-11追記:
思った以上に日常使い出来ると感じ始めてる。ゆえに、ちと整理して Gist 管理してみることにする:

まぁこういうの、欲張りだせばまだまだ色々あるんだけどさ。

なお、「WM_CLOSE を SendMessage」みたいなことも出来たらいいなぁと思ったが、やっぱりノリが Unix なんで、これは psutil はまかなってくれないみたい。これをやりたい場合、どこまでさかのぼらなければならんだろうか? ウィンドウハンドルが必要だろうから…、C まで戻らんと出来ないか? うーん、そのうち考えたいなぁ。


2021-10-11追記:
ほんとは別ネタとして独立して書いてもいいような話なんだけれど、どうもこのページに割と人が集まってるみたいなんで、ここに。

「より救世主」の意味するところが果たして伝わったのかいな、てのがね、書いた後ずっと気になってはいたのよね。話の半分以上を nice の話に費やしてしまっていたけれど、もともとの話は「glances のような汎用を目指した完成品が得てして痒いところに手が届かなくて困るので psutil そのものがうれしんだぜぃ」てハナシ。あと細かい話としては、ワタシは Windows ユーザとしての嬉しさばかり強調しがちだけれど、実際はほぼ Unix 系しか使わない人にとっても「ユニバーサルなプログラミング」が出来て嬉しい、てことにはなる。生粋の Unix ユーザほどポータビリティ至上主義みたいなところがあって、「Unix でしか使えない」ものを思ったよりは嫌う、少なくともワタシはそう。てなことどもがまぁ「より救世主」の意味だよ。

で、この補足説明だけなら追記しようとは思わないわけよ。「痒いところに手」のいい例があったんで、それをちょっと紹介しとこうかと。

(翻訳のヒドさで有名な)「Unix 原典」などを読むとわかることだったりするのだけれど、もともと「ウィンドウシステム」は、「(マルチユーザ・)マルチタスクOSであることをフルに活かすために不可欠」なものとして考えられた。今の現代的な OS ばかりをみているとなかなかこのことはわかりにくくなってしまっているけれど、Windows ユーザであれば「DOS窓フルスクリーンモード」を想像すると良い。これがいわゆる「ウィンドウシステムのない世界」が想像出来る、現代に残る遺跡だ。ウィンドウシステムがあれば、「たくさんのDOS窓を同時に動かせる」てぇことになるわけだ、「マルチタスクさまさまっ」。そう、「プロセスが同時並行で複数動かせる」だけでは人間にとっての有用性としては不十分で、「対人間インターフェイス部分の多重化」が必要だった、てこと。

この、「対人間インターフェイス部分の多重化」なんだけどね、確かに「複数ウィンドウを開くことで、複数のプロセスを同時進行出来る」のではあるけれど、「GUI」として設計されていないプロセスの多くが「人間が途中で介入できない」ものとして設計されることがほとんどなわけよ、特に自分で作るスクリプトなんぞでは「終わるまでは終わらないよ」、要は「作業Aが終わってから作業Bを実行する」というのを「後から思った」場合にかなり悲しいことになる:

実行してしまったら最後、この ffmpeg が終わるまでこのコンソールウィンドウは使えない
1 [me@host: ~]$ ffmpeg -y -i huge_video.mp4 -c:v libx265 huge_video.mkv
2    ... (とてつもなく時間がかかる、とか。数時間、的な) ...
常にこういう↓賢いスケジューリングで作業出来るとは限らない
1 [me@host: ~]$ ffmpeg -y -i huge_video.mp4 -c:v libx265 huge_video.mkv && \
2 > ffmpeg -y -i huge_video2.mp4 -c:v libx265 huge_video2.mkv 
3    ... (とてつもなく時間がかかる、とか。数時間、的な)...
4    ... (だけれども最初のが終われば2つ目は即座に実行される) ...

後者の例のようなのがいわゆる「賢いシェル使い」だとはいえ、常にこういうことを思い描けるわけではないし、作業の都合でこういうことが出来ないこともある。たとえば huge_video.mp4 処理開始時点で huge_video2.mp4 がまだない、とかね。

つまり「&&」みたいな、シェルによる連鎖実行という手段を取らなかった場合・取れなかった場合に、別ウィンドウ内でプロセスAの終了検出をしたい、て話ね、たとえば。すなわち「プロセス群の探索」、すわ、psutil の出番、てことだ:

 1 #! py -3
 2 # -*- coding: utf-8 -*-
 3 from __future__ import print_function
 4 from __future__ import unicode_literals
 5 
 6 import os
 7 import sys
 8 import re
 9 import time
10 import psutil  # pip install psutil
11 
12 
13 def _exists(det):
14     myproc = psutil.Process(os.getpid())
15     for p in psutil.process_iter():
16         if (p.pid == myproc.pid) or (p.pid == myproc.ppid()):
17             continue
18         cmdl = []
19         if args.search_mode in ("CMD",):
20             try:
21                 cmdl = p.cmdline()
22             except Exception as e:
23                 #print("cannot get commandline:", e, file=sys.stderr)
24                 pass
25             if not cmdl:
26                 continue
27         if any([cf(cmdl, p.pid) for cf in det]):
28             print(p, file=sys.stderr)
29             break
30     else:
31         return False
32     return True
33 
34 
35 if __name__ == '__main__':
36     import argparse
37     ap = argparse.ArgumentParser()
38     ap.add_argument("search_target", nargs="+")
39     ap.add_argument("--search_mode", choices=["PID", "CMD"], default="CMD")
40     args = ap.parse_args()
41     #
42     if args.search_mode == "CMD":
43         det = [lambda cmdl, pid: re.search(p, cmdl[0]) for p in args.search_target]
44     else:
45         try:
46             det = [lambda cmdl, pid: int(p) == pid for p in args.search_target]
47         except ValueError as e:
48             ap.error(e)
49     #
50     while _exists(det):
51         time.sleep(30)
52         if not _exists(det):
53             time.sleep(30)

たとえばこれを ps_sleepuntil.py という名前のスクリプトにするとして(なおかつ実行可能にするとして):

1 [me@host: ~]$ ps_sleepuntil.py ffmpeg && ffmpeg -y -i huge_video2.mp4 -c:v libx265 huge_video2.mkv

「検出対象が見つからなかった」のあとの判定がいい加減なので実用にはまだあまりならんけれど、言いたいことは伝わる、よね? こういうことを psutil を使えば、割と簡単に出来る、て話。これが「glances のような完成品としての価値よりはインフラとしての psutil が救世主」の一つの姿。


2021-10-20追記:
「2021-10-11追記」で、肝心なことを書き忘れていたことに気付いた。書き始める前は意識してるんだけどね、書き始めると書こうと思ってたことを書き忘れるなんてまぁ、よくある話。

実は「世界には Unix しかない」なら、「ps_sleepuntil.py」的な道具はいらない。問題の本質は、仮想端末一個まるまるを「フォアグラウンドプロセスが専有してしまう」(ので他のことが出来ない)ということなのだけれど、「フォアグラウンドで始めてしまった」ことを中断出来ることは実は利用できる。Unix で普通使われているシェルであれば、ジョブコントロールはかなり柔軟で、「サスペンド後、レジュームしたプロセス終了を待つ」ことが出来る:

 1 [me@host: ~]$ python
 2    ...
 3 >>> import os, time
 4 >>> while True:
 5 ...     if os.path.exists("result.txt"):
 6 ...         break
 7 ...     time.sleep(30)
 8 >>>
 9    ... Ctrl-Z でサスペンド…
10 [me@host: ~]$ fg %1 && echo "owata..."

「Ctrl-Z で…」とか「fg」が各々のシェルの頑張りで実現されているジョブコントロール機能なので、シェルが違えば違う可能性はあるが、まぁおおむねほとんどがこれ。これで出来るのであれば、「あ、しまった、これ、終わらないと終わらないや」とはならない。

問題はこれが Windows では「出来たり出来なかったりする」こと。「Unix もどき環境」の場合であっても、たとえば cygwin 系の場合「cygwin な DLL依存 or DIE」となってて、cygwin のお仲間プログラム以外はこれが出来ない。上の python の例だと、多くの Windows ユーザが選んでいる公式 CPython は cygwin ではないので、Ctrl-Z での中断が出来ない。あとね、上で gist 管理してる pscmdlines.py の suspend/resume を実際に活用し続けてみればわかるんだけれど、Windows の suspend/resume って、ちょっと安定しないんだよね。resume できなくなることが結構ある。のであまり日常使いは出来ない。

そんなこんなで、ようやっと「ps_sleepuntil.py」的なものが嬉しい、救世主だ、となる。ここまでの内容を「2021-10-11追記」を書く前にはここまで書こうと思ってたんだよ。うん、書くべきことをメモする癖をつけた方がいいよなぁ…。


2022-04-05追記:
「psutil を日常生活に役立てる」というカテゴリに入るならどんなに些末なものでも書き留めとくぜ、って気分になってるの。なぜって、個々のしょーもないニーズが、案外「汎用化出来ない」からなの。

今回のは「ffmpeg って、重いけれども並列化に向かない」の件。もちろん「32コア CPU」みたいな怪物マシンを使ってるなら別だけれど、家電量販店で買うようなごくごく一般的な PC、具体的には Intel i3 クラスの「ローエンド」な PC ね、こういう PC だと、ffmpeg は並列実行すればするほど遅くなる。理由はあんましわかってないけど、とにかくそうなる。

それでもなお、てことなわけよ。実際たとえば「一つのビデオを処理するのに一時間かかる」のだとすれば、この「一連のビデオたち10個」を処理するのに「一つのビデオの処理が終わってから次のビデオを処理するスクリプトを起動する」なんてことはしたくないわけ。だって一時間後には「ワレは寝たいんじゃぁ」かもしれんじゃないか、だからスケジュールだけはしときたいわけよ、「起動のために終了待ちをする」のをリアルではやりたくないわけ。つまり「一連のビデオたち10個」のためのスクリプトは10個全部起動だけはしときたい、てことね。

この場合、「10個全部動いてたら個々の処理速度は悶絶するほど遅いので、この10個の ffmpeg の一つだけ通常プライオリティにし、残りはアイドルにする」としたい、てことね。たとえばこういうこと:

nice の仕様の関係で、これは Windows 専用
 1 # -*- coding: utf-8 -*-
 2 from __future__ import print_function
 3 from __future__ import unicode_literals
 4 
 5 import os
 6 import sys
 7 import pipes
 8 import re
 9 import time
10 import psutil  # pip install psutil
11 
12 
13 _spcodes = {
14     "reset": "\x1b[39;49;00m",
15     "pwd": "\x1b[44m",
16     "argv0": "\x1b[102m;\x1b[30m",
17 }
18 if sys.platform == "win32":
19     try:
20         import colorama
21         colorama.init()
22     except ImportError as e:
23         _spcodes = {k: "" for k, _ in _spcodes.items()}
24 
25 
26 if __name__ == '__main__':
27     import argparse
28     ap = argparse.ArgumentParser()
29     ap.add_argument("sleeptime", type=float, default=30)
30     ap.add_argument("-V", "--verbose", action="store_true", help="print warning")
31     args = ap.parse_args()
32     def _catcmdl(cmdl):
33         cl = [pipes.quote(a) for a in cmdl]
34         cl[0] = "{}{}{}".format(
35             _spcodes["argv0"], cl[0], _spcodes["reset"])
36         return " ".join(cl)
37     def _warn(*a, **kw):
38         if not args.verbose:
39             return
40         print(*a, **kw, file=sys.stderr)
41     myproc = psutil.Process(os.getpid())
42     while True:
43         targets = []
44         for p in psutil.process_iter():
45             if p.pid == myproc.pid:
46                 continue
47             cmdl = []
48             try:
49                 cmdl = p.cmdline()
50             except Exception as e:
51                 _warn("cannot get commandline:", e)
52             if not cmdl:
53                 continue
54             if os.path.splitext(os.path.basename(cmdl[0]))[0] != "ffmpeg":
55                 continue
56             targets.append((p, cmdl))
57         if len(targets) == 1:
58             (p, cmdl) = targets[0]
59             if p.nice() in (
60                     psutil.BELOW_NORMAL_PRIORITY_CLASS,
61                     psutil.IDLE_PRIORITY_CLASS,
62             ):
63                 p.nice(psutil.NORMAL_PRIORITY_CLASS)
64                 print(_catcmdl(cmdl), p.nice())
65         else:
66             nxt = 0
67             for i, (p, cmdl) in enumerate(targets):
68                 if p.nice() in (
69                         psutil.ABOVE_NORMAL_PRIORITY_CLASS,
70                         psutil.HIGH_PRIORITY_CLASS,
71                         psutil.NORMAL_PRIORITY_CLASS,
72                         psutil.REALTIME_PRIORITY_CLASS,
73                 ):
74                     nxt = (i + 1) % len(targets)
75                     break
76             for i, (p, cmdl) in enumerate(targets):
77                 if i == nxt:
78                     p.nice(psutil.NORMAL_PRIORITY_CLASS)
79                     print(_catcmdl(cmdl), p.nice())
80                 else:
81                     p.nice(psutil.IDLE_PRIORITY_CLASS)
82         time.sleep(args.sleeptime)

x秒ごとにプライオリティを変える、てことね。


翌朝追記(「2022-04-05追記」の続き):
昨日の、ちょっと説明不足だったかなと思うところは後述。ひとまず「起こすヤツは NORMAL_PRIORITY_CLASS より優先度上げたいかも」版:

  1 # -*- coding: utf-8 -*-
  2 from __future__ import print_function
  3 from __future__ import unicode_literals
  4 
  5 import os
  6 import sys
  7 import pipes
  8 import re
  9 import time
 10 import psutil  # pip install psutil
 11 
 12 
 13 _spcodes = {
 14     "reset": "\x1b[39;49;00m",
 15     "pwd": "\x1b[44m",
 16     "argv0": "\x1b[102m;\x1b[30m",
 17 }
 18 if sys.platform == "win32":
 19     try:
 20         import colorama
 21         colorama.init()
 22     except ImportError as e:
 23         _spcodes = {k: "" for k, _ in _spcodes.items()}
 24 
 25 
 26 if __name__ == '__main__':
 27     import argparse
 28     ap = argparse.ArgumentParser()
 29     ap.add_argument("sleeptime", type=float, default=30)
 30     ap.add_argument("-A", "--above_normal", action="store_true")
 31     ap.add_argument("-V", "--verbose", action="store_true", help="print warning")
 32     args = ap.parse_args()
 33     p_u, p_d = [
 34         psutil.NORMAL_PRIORITY_CLASS,
 35         psutil.ABOVE_NORMAL_PRIORITY_CLASS,
 36         psutil.HIGH_PRIORITY_CLASS,
 37         psutil.REALTIME_PRIORITY_CLASS,
 38     ], [
 39         psutil.BELOW_NORMAL_PRIORITY_CLASS,
 40         psutil.IDLE_PRIORITY_CLASS,
 41     ]
 42     np = psutil.NORMAL_PRIORITY_CLASS
 43     if args.above_normal:
 44         np = psutil.ABOVE_NORMAL_PRIORITY_CLASS
 45         p_u.remove(psutil.NORMAL_PRIORITY_CLASS)
 46         p_d.append(psutil.NORMAL_PRIORITY_CLASS)
 47     def _catcmdl(cmdl):
 48         cl = [pipes.quote(a) for a in cmdl]
 49         cl[0] = "{}{}{}".format(
 50             _spcodes["argv0"], cl[0], _spcodes["reset"])
 51         return " ".join(cl)
 52     def _warn(*a, **kw):
 53         if not args.verbose:
 54             return
 55         print(*a, **kw, file=sys.stderr)
 56     def _getcmdl(p):
 57         try:
 58             cmdl = p.cmdline()
 59             if os.path.splitext(os.path.basename(cmdl[0]))[0] == "ffmpeg":
 60                 return cmdl
 61         except Exception as e:
 62             _warn("cannot get commandline:", e)
 63     def _nice(p):
 64         try:
 65             return p.nice()
 66         except Exception as e:
 67             _warn(e)
 68             return None
 69     def _renice(cmdl, p, val):
 70         try:
 71             p.nice(val)
 72             if val != psutil.IDLE_PRIORITY_CLASS:
 73                 print(_catcmdl(cmdl), _nice(p))
 74             return True
 75         except Exception as e:
 76             _warn(e)
 77             return False
 78     myproc = psutil.Process(os.getpid())
 79     while True:
 80         targets = []
 81         for p in psutil.process_iter():
 82             if p.pid == myproc.pid:
 83                 continue
 84             cmdl = _getcmdl(p)
 85             if not cmdl:
 86                 continue
 87             targets.append((p, cmdl))
 88         if len(targets) == 1:
 89             (p, cmdl) = targets[0]
 90             if _nice(p) in p_d:
 91                 _renice(cmdl, p, psutil.NORMAL_PRIORITY_CLASS)
 92         else:
 93             nxt = 0
 94             for i, (p, cmdl) in enumerate(targets):
 95                 if _nice(p) in p_u:
 96                     nxt = (i + 1) % len(targets)
 97                     break
 98             for i, (p, cmdl) in enumerate(targets):
 99                 if i == nxt:
100                     if not _renice(cmdl, p, np):
101                         nxt = min((i + 1), len(targets) - 1)
102                 else:
103                     _renice(cmdl, p, psutil.IDLE_PRIORITY_CLASS)
104         time.sleep(args.sleeptime)

闇雲に ABOVE_NORMAL_PRIORITY_CLASS しないでね、ほかに重量級タスクを動かしてる場合は、無難に NORMAL のまま動かすべし。

「後述」の件。「ffmpeg と並列化の相性」の話をこれまでワタシはわかっていながら避けてきてたのよ。「2021-10-11追記」でも「ffmpeg を並列で使う」話をしてないでしょう? 暗に同時には動かさない前提のネタとして書いてる。今回も「ffmpeg は並列実行すればするほど遅くなる。理由はあんましわかってないけど、とにかくそうなる」と書いたが、まぁこれこそがこのネタを書くのを避けてる理由。並列実行して効果が出る・出ないは、多くの場合は少なくともザックリとは理由を説明出来るんだけれど、ffmpeg のケースはほんとにワタシはよくわからんのよ。ffmpeg 自身がスレッド並列してるのは知ってるので、それが理由でマルチコア並列が不得意だ、とかなのかしらん、とは少し思うけど、まぁわからんよ。

「説明不足」と書いたのはこの「理由の究明」の話ではない。「実際どういう具合に遅くなるのか、そして psutil.Process#nice がどう解決になるのか」の説明が少なすぎたなと。

「遅くなる」については、たとえば ffmpeg が同時に 5つ動いていたとして、これが「各々 1/5 の処理速度になる」のなら何ら問題はないのだし、ワタシのスクリプトも「何もしてない」わけね。そうじゃないんだわ。実際に試みてみるのはオススメは出来ないけれど、現実には最悪のシナリオだと「各々 1/10000 の処理速度になる」。というかまぁ「PC がほとんどフリーズに近い状態に陥る」確率のほうが高いかと思う。ワタシのここで挙げたスクリプトというのは「各々 1/5 の処理速度にする」ためのものだと言えばわかるかな?

もちろんプロセス自体は全てオンメモリで動作している状態。なので、メモリ使用が逼迫してると PC が危ない状態になる可能性はあるんだけれど、CPU 使用については ffmpeg 一つだけ動かしてる状態と同じ。なので、ワタシのスクリプトを動かしてる状態なら ffmpeg を 10個動かしてる状態でも「平時とあまり変わらない状態で PC を使える」。事実今その状態だけど、こうしてブログの編集を出来てる。

「並列実行」というとどうしても「実行時間を短くする」ことばかりが目的としてクローズアップされがちなんだけれど、つまり今のこの ffmpeg の例に関しては、昨晩言った「起動のための終了待ちをしないようにするための並列」というだけなの。実行時間はこの場合直列実行と変わらない。個々に 1時間かかるなら、10個で「並列しても10時間」ね。


2022-04-07追記(さらに続き):
『個々のしょーもないニーズが、案外「汎用化出来ない」』と言った。こういう諦めとか割り切りそのものは「年の功」的なところもあって、若気て至れれば汎用化を頑張ってしまう(ぶっちゃけその方が美しく感じるしカッコイイから)けれど、経験を積めば「汎用化と同じくらい特化も尊い」こともわかってくるし、バランス感覚も磨かれてくるので汎用ばかりこだわることも減ってくる、ということが根本にはあるんだけれど、ワタシ、これに似た主張をほかの場所でもしてきた記憶があるんだけど、「どういうパターンが汎用化しづらい」みたいな話って、そういえばしたことないなぁと思った。

psutil の件に限らず「案外「汎用化出来ない」」になりがちなのは、『「完全な自由と手作業」vs「完全なカスタム自動化」』にニーズが二極化するケースだと思うよ。glances は「汎用ツール」だけれど、これは「全部出来るが全部手動操作」。対して、ワタシがこのページで作ってきたものは、「オレだけのためのカスタム自動化」ということ。

で、「ffmpeg 多重実行の直列化」の話の続き。「カスタム」っぷりをさらに。

「プライオリティを均等に均したい」んではないんだよね、実際のところ。エコ贔屓したいパターンがあるの。「とにかく実行開始だけはしときたい」というニーズとともに、やはり「同時実行数そのものは少なくなる方がいいので、とっとと終わるものを優先したい」というのが一つ。あと一歩で処理が終わるのに低いプライオリティで処理機会がずっと与えられないのでは、いつまで経っても同時実行数が減らない、それはヤダ、てことね。もう一つが、ffmpeg については、matroska のように中途生成でも再生出来るものの場合の「開始してすぐはフラッシュされないので再生出来ない」という時間を短くしたい、てのと。つまり、既に処理時間を多く費やしているものと始めたばかりのものの二種類を「贔屓したい」てこと。

てわけでこんな感じにしてみた:

  1 # -*- coding: utf-8 -*-
  2 from __future__ import print_function
  3 from __future__ import unicode_literals
  4 
  5 import os
  6 import sys
  7 import pipes
  8 import re
  9 import time
 10 import itertools
 11 import psutil  # pip install psutil
 12 
 13 
 14 _spcodes = {
 15     "reset": "\x1b[39;49;00m",
 16     "pwd": "\x1b[44m",
 17     "argv0": "\x1b[102m;\x1b[30m",
 18 }
 19 if sys.platform == "win32":
 20     try:
 21         import colorama
 22         colorama.init()
 23     except ImportError as e:
 24         _spcodes = {k: "" for k, _ in _spcodes.items()}
 25 
 26 
 27 if __name__ == '__main__':
 28     import argparse
 29     ap = argparse.ArgumentParser()
 30     ap.add_argument("minsleeptime", type=float, default=30)
 31     ap.add_argument("-W", "--weight_factor", type=float, default=2)
 32     ap.add_argument("-A", "--above_normal", action="store_true")
 33     ap.add_argument("-V", "--verbose", action="store_true", help="print warning")
 34     args = ap.parse_args()
 35     p_u, p_d = [
 36         psutil.NORMAL_PRIORITY_CLASS,
 37         psutil.ABOVE_NORMAL_PRIORITY_CLASS,
 38         psutil.HIGH_PRIORITY_CLASS,
 39         psutil.REALTIME_PRIORITY_CLASS,
 40     ], [
 41         psutil.BELOW_NORMAL_PRIORITY_CLASS,
 42         psutil.IDLE_PRIORITY_CLASS,
 43     ]
 44     np = psutil.NORMAL_PRIORITY_CLASS
 45     if args.above_normal:
 46         np = psutil.ABOVE_NORMAL_PRIORITY_CLASS
 47         p_u.remove(psutil.NORMAL_PRIORITY_CLASS)
 48         p_d.append(psutil.NORMAL_PRIORITY_CLASS)
 49     def _catcmdl(cmdl):
 50         cl = [pipes.quote(a) for a in cmdl]
 51         cl[0] = "{}{}{}".format(
 52             _spcodes["argv0"], cl[0], _spcodes["reset"])
 53         return " ".join(cl)
 54     def _warn(*a, **kw):
 55         if not args.verbose:
 56             return
 57         print(*a, **kw, file=sys.stderr)
 58     def _getcmdl(p):
 59         try:
 60             cmdl = p.cmdline()
 61             if os.path.splitext(os.path.basename(cmdl[0]))[0] == "ffmpeg":
 62                 return cmdl
 63         except Exception as e:
 64             _warn("cannot get commandline:", e)
 65     def _p_meth(p, meth, fallback, *a, **kw):
 66         try:
 67             return getattr(p, meth)(*a, **kw)
 68         except Exception as e:
 69             _warn(e)
 70             return fallback
 71     def _renice(cmdl, p, val):
 72         try:
 73             p.nice(val)
 74             if val != psutil.IDLE_PRIORITY_CLASS:
 75                 print(_catcmdl(cmdl), _p_meth(p, "nice", None))
 76             return True
 77         except Exception as e:
 78             _warn(e)
 79             return False
 80     myproc = psutil.Process(os.getpid())
 81     while True:
 82         targets = []
 83         for p in psutil.process_iter():
 84             if p.pid == myproc.pid:
 85                 continue
 86             cmdl = _getcmdl(p)
 87             if not cmdl:
 88                 continue
 89             ut = _p_meth(p, "cpu_times", [0])[0]
 90             targets.append((p, cmdl, ut))
 91         if not targets:
 92             continue
 93         #
 94         nxt = 0
 95         for i, (p, cmdl, ut) in enumerate(targets):
 96             if _p_meth(p, "nice", None) in p_u:
 97                 nxt = (i + 1) % len(targets)
 98                 break
 99         tut = targets[0][2]
100         for i, (p, cmdl, ut) in enumerate(targets):
101             if i == nxt:
102                 if not _renice(cmdl, p, np):
103                     nxt = min((i + 1), len(targets) - 1)
104                 else:
105                     tut = ut
106             else:
107                 _renice(cmdl, p, psutil.IDLE_PRIORITY_CLASS)
108         #
109         allut = list(sorted([ut for _, _, ut in targets]))
110         allut = list(
111             filter(
112                 lambda x: x is not None,
113                 itertools.chain(
114                     *itertools.zip_longest(
115                         allut[:len(allut) // 2][::-1],
116                         allut[len(allut) // 2:]
117                     ))))
118         w = (allut.index(tut) + 1)
119         slt = args.minsleeptime + w * args.weight_factor
120         print("### sleep ({} + {} = ){}...".format(
121             args.minsleeptime, w * args.weight_factor, slt))
122         time.sleep(slt)

itertools を駆使して「かっちょよく」やってる…のはまぁどうでもよくて、cpu_times から得られる usertime が一番大きいものと一番小さいものに最も処理機会を多く与えるようにした、てことだよ。