From c2b7efcda75bff76c95c3eaa6a01c7cc9b415e24 Mon Sep 17 00:00:00 2001 From: krymtkts Date: Sun, 19 Jan 2025 20:17:51 +0900 Subject: [PATCH 1/6] Move mock `RawUI` implementation to `Mock` module for testing purposes. --- src/pocof.Test/Mock.fs | 137 +++++++++++++++++++++++++++++++ src/pocof.Test/Pocof.fs | 2 +- src/pocof.Test/Screen.fs | 132 +---------------------------- src/pocof.Test/pocof.Test.fsproj | 1 + 4 files changed, 140 insertions(+), 132 deletions(-) create mode 100644 src/pocof.Test/Mock.fs diff --git a/src/pocof.Test/Mock.fs b/src/pocof.Test/Mock.fs new file mode 100644 index 0000000..39bd5f5 --- /dev/null +++ b/src/pocof.Test/Mock.fs @@ -0,0 +1,137 @@ +namespace Pocof.Test + +open System + +open Pocof.LanguageExtension +open Pocof.Screen + +[] +module Mock = + let generateLine x y = + List.replicate y <| String.replicate x " " + + type MockRawUI = + val caAsInput: bool + val mutable height: int + val mutable width: int + val mutable x: int + val mutable y: int + val mutable screen: string list + val mutable keys: ConsoleKeyInfo option list + val mutable forceCancel: bool + static member xx = 50 + static member yy = 30 + + new() = + // NOTE: accessing Console.TreatControlCAsInput will raise System.IO.IOException when running on GitHub Actions windows runner. + { caAsInput = true + x = MockRawUI.xx + y = MockRawUI.yy + width = MockRawUI.xx + height = MockRawUI.yy + screen = generateLine MockRawUI.xx MockRawUI.yy + keys = [ MockRawUI.ConsoleKey '\000' ConsoleKey.Enter ] + forceCancel = false } + + new(x: int, y: int) = + // NOTE: accessing Console.TreatControlCAsInput will raise System.IO.IOException when running on GitHub Actions windows runner. + { caAsInput = true + x = x + y = y + width = x + height = y + screen = generateLine x y + keys = [ MockRawUI.ConsoleKey '\000' ConsoleKey.Enter ] + forceCancel = false } + + new(x: int, y: int, keys: ConsoleKeyInfo option list) = + // NOTE: accessing Console.TreatControlCAsInput will raise System.IO.IOException when running on GitHub Actions windows runner. + { caAsInput = true + x = x + y = y + width = x + height = y + screen = generateLine x y + keys = keys + forceCancel = false } + + new(x: int, y: int, keys: ConsoleKeyInfo option list, forceCancel: bool) = + // NOTE: accessing Console.TreatControlCAsInput will raise System.IO.IOException when running on GitHub Actions windows runner. + { caAsInput = true + x = x + y = y + width = x + height = y + screen = generateLine x y + keys = keys + forceCancel = forceCancel } + + interface IRawUI with + member __.GetCursorPosition() = __.x, __.y + + member __.SetCursorPosition (x: int) (y: int) = + __.x <- x + __.y <- y + + member __.GetLengthInBufferCells(s: string) = + let isFullWidth c = // NOTE: simple full-width character detection for test. + let code = int c + code >= 0xFF00 && code <= 0xFF60 + + s |> Seq.cast |> Seq.sumBy (fun c -> if isFullWidth c then 2 else 1) + + member __.GetWindowWidth() = __.width + member __.GetWindowHeight() = __.height + + member __.Write x y s = + __.screen <- + __.screen + |> List.mapi (fun i ss -> + match i with + | ii when ii = y -> ss |> String.upToIndex x |> (+) s + | _ -> + let l = (__ :> IRawUI).GetLengthInBufferCells ss + + match l <= __.width with + | true -> ss + String.replicate (__.width - l) " " + | _ -> ss) + + member __.ReadKey(_) = + match __.keys with + | [] -> failwith "key sequence is empty. check your test key sequence." + | k :: ks -> + match k with + | None -> failwith "key is none. check your test key sequence." + | Some k -> + __.keys <- ks + k + + member __.KeyAvailable() = + match __.keys with + | [] -> + if __.forceCancel then + __.keys <- [ MockRawUI.ConsoleKey '\000' ConsoleKey.Escape ] + __.forceCancel <- false + + false + | k :: ks -> + match k with + | None -> + __.keys <- ks + false + | Some _ -> true + + member __.HideCursorWhileRendering() = + { new IDisposable with + member _.Dispose() = () } + + interface IDisposable with + member __.Dispose() = () + + static member ConsoleKey keyChar key = + new ConsoleKeyInfo(keyChar, key, false, false, false) |> Some + + member __.Check() = + match __.keys with + | [] -> () + | _ -> failwith "keys remains. probably test is broken." diff --git a/src/pocof.Test/Pocof.fs b/src/pocof.Test/Pocof.fs index 81928c0..e69f27b 100644 --- a/src/pocof.Test/Pocof.fs +++ b/src/pocof.Test/Pocof.fs @@ -8,7 +8,7 @@ open FsUnitTyped open Pocof open Pocof.Data -open Screen.Mock +open Pocof.Test let toObj x = x |> (PSObject.AsPSObject >> Entry.Obj) diff --git a/src/pocof.Test/Screen.fs b/src/pocof.Test/Screen.fs index 7e97be8..dc72969 100644 --- a/src/pocof.Test/Screen.fs +++ b/src/pocof.Test/Screen.fs @@ -8,137 +8,7 @@ open FsUnitTyped open Pocof.Data open Pocof.LanguageExtension open Pocof.Screen - -[] -module Mock = - let generateLine x y = - List.replicate y <| String.replicate x " " - - type MockRawUI = - val caAsInput: bool - val mutable height: int - val mutable width: int - val mutable x: int - val mutable y: int - val mutable screen: string list - val mutable keys: ConsoleKeyInfo option list - val mutable forceCancel: bool - static member xx = 50 - static member yy = 30 - - new() = - // NOTE: accessing Console.TreatControlCAsInput will raise System.IO.IOException when running on GitHub Actions windows runner. - { caAsInput = true - x = MockRawUI.xx - y = MockRawUI.yy - width = MockRawUI.xx - height = MockRawUI.yy - screen = generateLine MockRawUI.xx MockRawUI.yy - keys = [ MockRawUI.ConsoleKey '\000' ConsoleKey.Enter ] - forceCancel = false } - - new(x: int, y: int) = - // NOTE: accessing Console.TreatControlCAsInput will raise System.IO.IOException when running on GitHub Actions windows runner. - { caAsInput = true - x = x - y = y - width = x - height = y - screen = generateLine x y - keys = [ MockRawUI.ConsoleKey '\000' ConsoleKey.Enter ] - forceCancel = false } - - new(x: int, y: int, keys: ConsoleKeyInfo option list) = - // NOTE: accessing Console.TreatControlCAsInput will raise System.IO.IOException when running on GitHub Actions windows runner. - { caAsInput = true - x = x - y = y - width = x - height = y - screen = generateLine x y - keys = keys - forceCancel = false } - - new(x: int, y: int, keys: ConsoleKeyInfo option list, forceCancel: bool) = - // NOTE: accessing Console.TreatControlCAsInput will raise System.IO.IOException when running on GitHub Actions windows runner. - { caAsInput = true - x = x - y = y - width = x - height = y - screen = generateLine x y - keys = keys - forceCancel = forceCancel } - - interface IRawUI with - member __.GetCursorPosition() = __.x, __.y - - member __.SetCursorPosition (x: int) (y: int) = - __.x <- x - __.y <- y - - member __.GetLengthInBufferCells(s: string) = - let isFullWidth c = // NOTE: simple full-width character detection for test. - let code = int c - code >= 0xFF00 && code <= 0xFF60 - - s |> Seq.cast |> Seq.sumBy (fun c -> if isFullWidth c then 2 else 1) - - member __.GetWindowWidth() = __.width - member __.GetWindowHeight() = __.height - - member __.Write x y s = - __.screen <- - __.screen - |> List.mapi (fun i ss -> - match i with - | ii when ii = y -> ss |> String.upToIndex x |> (+) s - | _ -> - let l = (__ :> IRawUI).GetLengthInBufferCells ss - - match l <= __.width with - | true -> ss + String.replicate (__.width - l) " " - | _ -> ss) - - member __.ReadKey(_) = - match __.keys with - | [] -> failwith "key sequence is empty. check your test key sequence." - | k :: ks -> - match k with - | None -> failwith "key is none. check your test key sequence." - | Some k -> - __.keys <- ks - k - - member __.KeyAvailable() = - match __.keys with - | [] -> - if __.forceCancel then - __.keys <- [ MockRawUI.ConsoleKey '\000' ConsoleKey.Escape ] - __.forceCancel <- false - - false - | k :: ks -> - match k with - | None -> - __.keys <- ks - false - | Some _ -> true - - member __.HideCursorWhileRendering() = - { new IDisposable with - member _.Dispose() = () } - - interface IDisposable with - member __.Dispose() = () - - static member ConsoleKey keyChar key = - new ConsoleKeyInfo(keyChar, key, false, false, false) |> Some - - member __.Check() = - match __.keys with - | [] -> () - | _ -> failwith "keys remains. probably test is broken." +open Pocof.Test module ``Buff writeScreen`` = open System.Collections diff --git a/src/pocof.Test/pocof.Test.fsproj b/src/pocof.Test/pocof.Test.fsproj index b03bfeb..8cdfabf 100644 --- a/src/pocof.Test/pocof.Test.fsproj +++ b/src/pocof.Test/pocof.Test.fsproj @@ -11,6 +11,7 @@ + From 8549fed83325d6d2907dee25029483cddf4a47ef Mon Sep 17 00:00:00 2001 From: krymtkts Date: Sun, 19 Jan 2025 22:36:31 +0900 Subject: [PATCH 2/6] Add project reference for `pocof.Test` in benchmark project. --- src/pocof.Benchmark/pocof.Benchmark.fsproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pocof.Benchmark/pocof.Benchmark.fsproj b/src/pocof.Benchmark/pocof.Benchmark.fsproj index 68881f1..298c91e 100644 --- a/src/pocof.Benchmark/pocof.Benchmark.fsproj +++ b/src/pocof.Benchmark/pocof.Benchmark.fsproj @@ -33,6 +33,7 @@ + From b27b1fbd1e20aae7d2e7c63e2730da9cebcbdcda Mon Sep 17 00:00:00 2001 From: krymtkts Date: Sun, 19 Jan 2025 22:37:45 +0900 Subject: [PATCH 3/6] Add a simple benchmark for the pocof event loop. --- src/pocof.Benchmark/Benchmarks.fs | 74 +++++++++++++++++++++++++++++++ src/pocof.Benchmark/Program.fs | 9 +--- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/src/pocof.Benchmark/Benchmarks.fs b/src/pocof.Benchmark/Benchmarks.fs index 5e93550..66f5191 100644 --- a/src/pocof.Benchmark/Benchmarks.fs +++ b/src/pocof.Benchmark/Benchmarks.fs @@ -1,6 +1,7 @@ module pocof.Benchmark open BenchmarkDotNet.Attributes +open BenchmarkDotNet.Engines open System open System.Collections @@ -8,6 +9,7 @@ open System.Management.Automation open Pocof open Pocof.Data +open Pocof.Test [] type PocofBenchmarks() = @@ -210,3 +212,75 @@ type QueryBenchmarks() = [] member __.run_dict_property() = Query.run __.PropertyContext __.Dicts props + +[] +type PocofBenchmarks2() = + let prompt = ">" + + let state: InternalState = + { QueryState = + { Query = "foo" + Cursor = 3 + WindowBeginningCursor = 0 + WindowWidth = 0 + InputMode = InputMode.Input } + QueryCondition = + { Matcher = Matcher.Match + Operator = Operator.And + CaseSensitive = true + Invert = false } + PropertySearch = PropertySearch.NoSearch + SuppressProperties = false + Refresh = Refresh.Required } + + let publishEvent _ = () + + let consumer = new Consumer() + + [] + member val EntryCount = 0 with get, set + + // [] + // member val QueryCount = 0 with get, set + + member val Objects: Entry pseq = PSeq.empty with get, set + member val Dicts: Entry pseq = PSeq.empty with get, set + + [] + member __.GlobalSetup() = + __.Objects <- + seq { 1 .. __.EntryCount } + |> Seq.map (string >> PSObject.AsPSObject >> Entry.Obj) + |> PSeq.ofSeq + + __.Dicts <- + seq { 1 .. __.EntryCount } + |> Seq.map (fun x -> ("key", x) |> DictionaryEntry |> Entry.Dict) + |> PSeq.ofSeq + + [] + member __.interact_obj() = + let rui = + new MockRawUI( + 60, + 30, + [ MockRawUI.ConsoleKey 'a' ConsoleKey.A + MockRawUI.ConsoleKey ' ' ConsoleKey.Spacebar + MockRawUI.ConsoleKey 'd' ConsoleKey.D + None + MockRawUI.ConsoleKey '\000' ConsoleKey.Enter ] + ) + + let config: InternalConfig = + { NotInteractive = true + Layout = Layout.TopDown + Keymaps = Keys.defaultKeymap + WordDelimiters = ";:,.[]{}()/\\|!?^&*-=+'\"–—―" + Prompt = prompt + PromptLength = prompt |> String.length + Properties = [] + PropertiesMap = Map [] } + + use buff = Screen.init (fun _ -> rui) (fun _ -> Seq.empty) config.Layout prompt + // TODO: is Consumer.Consume the right way for this benchmark? + Pocof.interact config state buff publishEvent __.Objects |> consumer.Consume diff --git a/src/pocof.Benchmark/Program.fs b/src/pocof.Benchmark/Program.fs index a80f93f..aef1991 100644 --- a/src/pocof.Benchmark/Program.fs +++ b/src/pocof.Benchmark/Program.fs @@ -3,14 +3,7 @@ open pocof.Benchmark [] let main argv = - BenchmarkSwitcher - .FromTypes( - [| typeof - typeof - typeof - typeof |] - ) - .Run(argv) + BenchmarkSwitcher.FromAssembly(typeof.Assembly).Run(argv) |> ignore 0 // return an integer exit code From 742330d2159bacbafc0fc2b6fdf90d60f5a44afb Mon Sep 17 00:00:00 2001 From: krymtkts Date: Thu, 23 Jan 2025 23:37:13 +0900 Subject: [PATCH 4/6] Update benchmark for pocof event loop to add dynamic key generation. --- src/pocof.Benchmark/Benchmarks.fs | 32 +++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/pocof.Benchmark/Benchmarks.fs b/src/pocof.Benchmark/Benchmarks.fs index 66f5191..ef62aa0 100644 --- a/src/pocof.Benchmark/Benchmarks.fs +++ b/src/pocof.Benchmark/Benchmarks.fs @@ -240,11 +240,12 @@ type PocofBenchmarks2() = [] member val EntryCount = 0 with get, set - // [] - // member val QueryCount = 0 with get, set + [] + member val QueryCount = 0 with get, set member val Objects: Entry pseq = PSeq.empty with get, set member val Dicts: Entry pseq = PSeq.empty with get, set + member val Keys: ConsoleKeyInfo option list = [] with get, set [] member __.GlobalSetup() = @@ -258,18 +259,25 @@ type PocofBenchmarks2() = |> Seq.map (fun x -> ("key", x) |> DictionaryEntry |> Entry.Dict) |> PSeq.ofSeq + __.Keys <- + [ __.QueryCount .. 1 ] + |> List.collect (fun x -> + [ match x with + | 1 + | 3 + | 5 + | 7 + | 9 -> + let c = x |> (+) 48 |> char + MockRawUI.ConsoleKey c (Enum.Parse(typeof, c.ToString()) :?> ConsoleKey) + | _ -> None + MockRawUI.ConsoleKey ' ' ConsoleKey.Spacebar ]) + |> List.append [ MockRawUI.ConsoleKey '\000' ConsoleKey.Enter ] + |> List.rev + [] member __.interact_obj() = - let rui = - new MockRawUI( - 60, - 30, - [ MockRawUI.ConsoleKey 'a' ConsoleKey.A - MockRawUI.ConsoleKey ' ' ConsoleKey.Spacebar - MockRawUI.ConsoleKey 'd' ConsoleKey.D - None - MockRawUI.ConsoleKey '\000' ConsoleKey.Enter ] - ) + let rui = new MockRawUI(60, 30, __.Keys) let config: InternalConfig = { NotInteractive = true From c584e36cd4e2ee947a584927be5159863d8943b1 Mon Sep 17 00:00:00 2001 From: krymtkts Date: Thu, 23 Jan 2025 23:39:41 +0900 Subject: [PATCH 5/6] Add benchmark for pocof event loop using dictionary input. --- src/pocof.Benchmark/Benchmarks.fs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/pocof.Benchmark/Benchmarks.fs b/src/pocof.Benchmark/Benchmarks.fs index ef62aa0..579486c 100644 --- a/src/pocof.Benchmark/Benchmarks.fs +++ b/src/pocof.Benchmark/Benchmarks.fs @@ -292,3 +292,21 @@ type PocofBenchmarks2() = use buff = Screen.init (fun _ -> rui) (fun _ -> Seq.empty) config.Layout prompt // TODO: is Consumer.Consume the right way for this benchmark? Pocof.interact config state buff publishEvent __.Objects |> consumer.Consume + + [] + member __.interact_dict() = + let rui = new MockRawUI(60, 30, __.Keys) + + let config: InternalConfig = + { NotInteractive = true + Layout = Layout.TopDown + Keymaps = Keys.defaultKeymap + WordDelimiters = ";:,.[]{}()/\\|!?^&*-=+'\"–—―" + Prompt = prompt + PromptLength = prompt |> String.length + Properties = [] + PropertiesMap = Map [] } + + use buff = Screen.init (fun _ -> rui) (fun _ -> Seq.empty) config.Layout prompt + // TODO: is Consumer.Consume the right way for this benchmark? + Pocof.interact config state buff publishEvent __.Dicts |> consumer.Consume From 9c4416d580d5d7ac7b37878f8915a92c2daa3d20 Mon Sep 17 00:00:00 2001 From: krymtkts Date: Sat, 25 Jan 2025 09:56:12 +0900 Subject: [PATCH 6/6] Refine. --- src/pocof.Benchmark/Benchmarks.fs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/pocof.Benchmark/Benchmarks.fs b/src/pocof.Benchmark/Benchmarks.fs index 579486c..a45542f 100644 --- a/src/pocof.Benchmark/Benchmarks.fs +++ b/src/pocof.Benchmark/Benchmarks.fs @@ -214,7 +214,7 @@ type QueryBenchmarks() = Query.run __.PropertyContext __.Dicts props [] -type PocofBenchmarks2() = +type PocofInteractBenchmarks() = let prompt = ">" let state: InternalState = @@ -235,8 +235,6 @@ type PocofBenchmarks2() = let publishEvent _ = () - let consumer = new Consumer() - [] member val EntryCount = 0 with get, set @@ -290,8 +288,8 @@ type PocofBenchmarks2() = PropertiesMap = Map [] } use buff = Screen.init (fun _ -> rui) (fun _ -> Seq.empty) config.Layout prompt - // TODO: is Consumer.Consume the right way for this benchmark? - Pocof.interact config state buff publishEvent __.Objects |> consumer.Consume + // NOTE: use Seq.length to force strict evaluation of the sequence + Pocof.interact config state buff publishEvent __.Objects |> Seq.length |> ignore [] member __.interact_dict() = @@ -308,5 +306,5 @@ type PocofBenchmarks2() = PropertiesMap = Map [] } use buff = Screen.init (fun _ -> rui) (fun _ -> Seq.empty) config.Layout prompt - // TODO: is Consumer.Consume the right way for this benchmark? - Pocof.interact config state buff publishEvent __.Dicts |> consumer.Consume + // NOTE: use Seq.length to force strict evaluation of the sequence + Pocof.interact config state buff publishEvent __.Dicts |> Seq.length |> ignore