2025-08-03

F# で Cmdlet を書いてる pt.68

WaitHandle を使う

今週はまるで開発の時間が取れなかったが、久しぶりに krymtkts/pocof 開発をした。 krymtkts/PSGameOfLife でいくつか高速化に関わる Tips を手に入れたので、それを輸入しようという魂胆だ。 ただ今のところは、それらをすぐに pocof で実現するのは難しそうだ、という結論になった。

ひとつは、 Console.OutStreamWriter.AutoFlush を無効にした StreamWriter で置き換える方法。 書き込み完了後に Flush することで Console の書き込み性能を高められる。 PSGameOfLife の CUI では非常に上手くいったが、 pocof の方は単純に導入できるものではなさそうだった。 Console.WriteLine を使わないと 1 行の改行が適切に出力されない1ため、狙った印字にならない。 pocof は初期カーソル位置下部に必要な数の行を出力して描画スクリーンを作っている関係で、 Console.WriteLine で自動で出力される改行と相性が悪いので、その調整が必要そう。 閾値の調整位で済めば導入も容易いのだけど、ややこしい部分なので億劫だ。

もう 1 つは async expressiontask expression に置き換える方法。 置き換えるだけなのでとても簡単なのだが、 pocof では F#らしく async expression と末尾再帰による無限 loop をしているので、 task だと末尾再帰を最適化できない。 つまり再帰をやめて while に変える必要がある。 それは性能のために F# らしさをやめるということで、 pocof ではどうするかというのが悩ましい。 速いは正義なのでそれでいいのだけど、もうちょっと悩んでもいいかなと思ってやらなかった。


そんな感じで打つ手がなくなってしまった。 仕方ないので、これらの代わりに pocof での main loop(UI Thread) に相当する箇所の効率化を試しているところ。 最早 PSGameOfLife から得たアイディアでもない。 外出中に pocof のコードを読んでて Thread.Sleep があるのを思い出したので、これは良くないなと GitHub Copilot に相談して出てきたアイディア。 再帰による無限 loop で Thread.Sleep を使っていたところを WaitHandle で置き換えることで、 CPU の無駄使いを取り除く。 #347

diff --git a/src/pocof/Pocof.fs b/src/pocof/Pocof.fs
index d31c9c9..b29993b 100644
--- a/src/pocof/Pocof.fs
+++ b/src/pocof/Pocof.fs
@@ -188,6 +188,8 @@ module Pocof =
let renderStack: RenderEvent Concurrent.ConcurrentStack =
Concurrent.ConcurrentStack()

+ let event = new AutoResetEvent(false)
+
[<TailCall>]
let rec getLatestEvent (h: RenderEvent) (es: RenderEvent list) =
match h with
@@ -205,9 +207,14 @@ module Pocof =
| [] -> RenderMessage.None
| h :: es -> getLatestEvent h es |> RenderMessage.Received

- member __.Publish = renderStack.Push
+ member __.Publish e =
+ renderStack.Push e
+ event.Set() |> ignore
+
+ member __.Receive(block: bool) =
+ if block then
+ event.WaitOne() |> ignore

- member __.Receive() =
let items =
match renderStack.Count with
// NOTE: case of 0 is required for .NET Framework forward compatibility.
@@ -222,9 +229,12 @@ module Pocof =

[<TailCall>]
let rec render (buff: Screen.Buff) (handler: RenderHandler) =
- match handler.Receive() with
+ match handler.Receive(block = true) with
| RenderMessage.None ->
- Thread.Sleep 10
+ // NOTE: for backward compatibility.
+#if DEBUG
+ Logger.LogFile [ "render received RenderMessage.None." ]
+#endif
render buff handler
| RenderMessage.Received RenderEvent.Quit -> ()
| RenderMessage.Received(RenderEvent.Render(state, entries, props)) ->
@@ -257,7 +267,7 @@ module Pocof =
| StopUpstreamCommands

let renderOnce (handler: RenderHandler) (buff: Screen.Buff) =
- match handler.Receive() with
+ match handler.Receive(block = false) with
| RenderMessage.None -> RenderProcess.Noop
| RenderMessage.Received RenderEvent.Quit -> RenderProcess.StopUpstreamCommands
| RenderMessage.Received(RenderEvent.Render(state, entries, props)) ->

元々 Thread.Sleep していた箇所は取り除き、 WaitOne で待ち受ける。 ProcessRecord から呼び出される描画処理を詰まらせないために non-blocking な option も提供する。

いい感じなのだが、これらの差分はまだ実装途中で merge もしてない。 というのも pocof は結構 coverage が厳しいので、 engine 部分以外はゆるい PSGameOfLife と違い下手は変更が許容されないのを忘れてた。 RenderMessage.None は発生することがないので消したいがまだ消せてない。おかげで coverage が低下している。なんとかせな。

自分が書いてる F# の中では poof は legacy な部類なので、この様な形でちょいちょい更新していけたら良いなと考えている。

多分続く。


  1. 確か改行コードや ANSI escape sequence でもダメだったと記憶だが今度再確認する