From d54484870ee00f3630a0dc84960fede0caba5447 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Sat, 10 Jun 2023 13:14:14 +0200 Subject: [PATCH] Rewrite in Rust (obviously) - Refactored code for more readability (e.g. use UnsafeCell instead of AtomicU32) - Fix "ghost-pixels" caused by wrong parsing (striping some commands) - Improve performance - Add ffmpeg sink --- Cargo.lock | 997 +++++++++++++++++++++--- Cargo.toml | 39 +- README.md | 115 ++- benches/benchmarks.rs | 122 +++ src/args.rs | 37 +- src/framebuffer.rs | 62 +- src/lib.rs | 5 +- src/main.rs | 134 +++- src/network.rs | 613 +++++++-------- src/parser.rs | 360 +++++++++ src/prometheus_exporter.rs | 93 +++ src/sinks/ffmpeg.rs | 132 ++++ src/sinks/mod.rs | 3 + src/{ => sinks}/vnc.rs | 172 ++-- src/statistics.rs | 481 ++++-------- src/test/helpers/dev_null_tcp_stream.rs | 40 + src/test/helpers/mock_tcp_stream.rs | 85 ++ src/test/helpers/mod.rs | 7 + src/test/helpers/pixelflut_commands.rs | 23 + src/test/mod.rs | 1 + tests/common/mod.rs | 57 -- tests/drawing.rs | 174 ----- 22 files changed, 2522 insertions(+), 1230 deletions(-) create mode 100644 benches/benchmarks.rs create mode 100644 src/parser.rs create mode 100644 src/prometheus_exporter.rs create mode 100644 src/sinks/ffmpeg.rs create mode 100644 src/sinks/mod.rs rename src/{ => sinks}/vnc.rs (51%) create mode 100644 src/test/helpers/dev_null_tcp_stream.rs create mode 100644 src/test/helpers/mock_tcp_stream.rs create mode 100644 src/test/helpers/mod.rs create mode 100644 src/test/helpers/pixelflut_commands.rs create mode 100644 src/test/mod.rs delete mode 100644 tests/common/mod.rs delete mode 100644 tests/drawing.rs diff --git a/Cargo.lock b/Cargo.lock index f8e7836..0245850 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,13 +10,34 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "ansi_term" version = "0.12.1" @@ -26,6 +47,55 @@ dependencies = [ "winapi", ] +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "ascii" version = "1.1.0" @@ -38,7 +108,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -59,7 +129,7 @@ dependencies = [ "cexpr", "clang-sys", "clap 2.34.0", - "env_logger", + "env_logger 0.8.4", "lazy_static", "lazycell", "log", @@ -80,20 +150,44 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "breakwater" -version = "0.0.1" +version = "0.1.0" dependencies = [ - "clap 3.2.25", + "chrono", + "clap 4.3.3", + "criterion", + "env_logger 0.10.0", "lazy_static", + "log", "number_prefix", - "prometheus", "prometheus_exporter", "rstest", "rusttype", "serde", "serde_json", + "simple_moving_average", + "thread-priority", + "tokio", "vncserver", ] +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.0.79" @@ -115,12 +209,54 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + [[package]] name = "chunked_transfer" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cca491388666e04d7248af3f60f0c40cfb0991c72205595d7c396e3510207d1a" +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clang-sys" version = "1.6.1" @@ -142,50 +278,152 @@ dependencies = [ "atty", "bitflags", "strsim 0.8.0", - "textwrap 0.11.0", + "textwrap", "unicode-width", "vec_map", ] [[package]] name = "clap" -version = "3.2.25" +version = "4.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +checksum = "ca8f255e4b8027970e78db75e78831229c9815fdbfa67eb1a1b777a62e24b4a0" dependencies = [ - "atty", - "bitflags", + "clap_builder", "clap_derive", - "clap_lex", - "indexmap", "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acd4f3c17c83b0ba34ffbc4f8bbd74f079413f747f84a6f89292f138057e36ab" +dependencies = [ + "anstream", + "anstyle", + "bitflags", + "clap_lex", "strsim 0.10.0", - "termcolor", - "textwrap 0.16.0", ] [[package]] name = "clap_derive" -version = "3.2.25" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" dependencies = [ "heck", - "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.18", ] [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" dependencies = [ - "os_str_bytes", + "anes", + "cast", + "ciborium", + "clap 4.3.3", + "criterion-plot", + "futures", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "tokio", + "walkdir", ] +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + [[package]] name = "env_logger" version = "0.8.4" @@ -199,6 +437,40 @@ dependencies = [ "termcolor", ] +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "fnv" version = "1.0.7" @@ -207,13 +479,108 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "glob" version = "0.3.1" @@ -221,10 +588,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] -name = "hashbrown" -version = "0.12.3" +name = "half" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" [[package]] name = "heck" @@ -241,30 +608,90 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] -name = "indexmap" -version = "1.9.3" +name = "io-lifetimes" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "autocfg", - "hashbrown", + "hermit-abi 0.3.1", + "libc", + "windows-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", ] [[package]] @@ -273,6 +700,15 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +[[package]] +name = "js-sys" +version = "0.3.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -287,9 +723,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.146" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" [[package]] name = "libloading" @@ -301,11 +737,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -313,12 +755,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" [[package]] name = "memchr" @@ -326,6 +765,26 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + [[package]] name = "nom" version = "5.1.3" @@ -336,6 +795,25 @@ dependencies = [ "version_check", ] +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + [[package]] name = "number_prefix" version = "0.4.0" @@ -344,15 +822,15 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] -name = "os_str_bytes" -version = "6.5.0" +name = "oorandom" +version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "owned_ttf_parser" @@ -375,15 +853,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-targets", ] [[package]] @@ -394,9 +872,21 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" @@ -405,34 +895,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] -name = "proc-macro-error" -version = "1.0.4" +name = "plotters" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "proc-macro-error-attr" -version = "1.0.4" +name = "plotters-backend" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" + +[[package]] +name = "plotters-svg" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f" dependencies = [ - "proc-macro2", - "quote", - "version_check", + "plotters-backend", ] [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" dependencies = [ "unicode-ident", ] @@ -448,7 +942,6 @@ dependencies = [ "lazy_static", "memchr", "parking_lot", - "protobuf", "thiserror", ] @@ -467,34 +960,50 @@ dependencies = [ ] [[package]] -name = "protobuf" -version = "2.28.0" +name = "quote" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] [[package]] -name = "quote" -version = "1.0.27" +name = "rayon-core" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" dependencies = [ - "proc-macro2", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", ] [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.8.1" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", @@ -503,21 +1012,34 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "rstest" -version = "0.12.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d912f35156a3f99a66ee3e11ac2e0b3f34ac85a07e05263d05a7e2c8810d616f" +checksum = "de1bb486a691878cd320c2f0d319ba91eeaa2e894066d8b5f8f117c000e9d962" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290ca1a1c8ca7edb7c3283bd44dc35dd54fdec6253a3912e201ba1072018fca8" dependencies = [ "cfg-if", "proc-macro2", "quote", "rustc_version", "syn 1.0.109", + "unicode-ident", ] [[package]] @@ -535,6 +1057,20 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.37.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rusttype" version = "0.9.3" @@ -545,12 +1081,27 @@ dependencies = [ "owned_ttf_parser", ] +[[package]] +name = "rustversion" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" + [[package]] name = "ryu" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -565,22 +1116,22 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.163" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -600,12 +1151,49 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simple_moving_average" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd19d3808aad2604c824399fd270260d634678b010328c9d96851bb0fb63121" +dependencies = [ + "num-traits", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "strsim" version = "0.8.0" @@ -631,9 +1219,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" dependencies = [ "proc-macro2", "quote", @@ -658,12 +1246,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - [[package]] name = "thiserror" version = "1.0.40" @@ -681,14 +1263,39 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", +] + +[[package]] +name = "thread-priority" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c56ce92f1285eaaa11fc1a3201e25de97898c50e87caa4c2aee836fe05288de" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "log", + "rustversion", + "winapi", ] [[package]] name = "time" -version = "0.3.21" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" dependencies = [ "itoa", "serde", @@ -720,10 +1327,20 @@ dependencies = [ "ascii", "chunked_transfer", "log", - "time", + "time 0.3.22", "url", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -739,6 +1356,35 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" +dependencies = [ + "autocfg", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + [[package]] name = "ttf-parser" version = "0.15.2" @@ -753,9 +1399,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-normalization" @@ -774,15 +1420,21 @@ checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "url" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "vec_map" version = "0.8.2" @@ -806,6 +1458,92 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.18", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" + +[[package]] +name = "web-sys" +version = "0.3.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "which" version = "3.1.1" @@ -846,20 +1584,29 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" -version = "0.45.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" -version = "0.42.2" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -872,42 +1619,42 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.2" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" -version = "0.42.2" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" -version = "0.42.2" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" -version = "0.42.2" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" -version = "0.42.2" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.2" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" -version = "0.42.2" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/Cargo.toml b/Cargo.toml index 458a1d7..b108d38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,27 +1,46 @@ [package] name = "breakwater" -version = "0.0.1" +version = "0.1.0" edition = "2021" [dependencies] -clap = { version = "3.2", features = ["derive"] } +clap = { version = "4.3", features = ["derive"] } rusttype = "0.9" number_prefix = "0.4" -prometheus = "0.13" +env_logger = "0.10" +lazy_static = "1.4" +log = "0.4" prometheus_exporter = "0.8" -serde_json = "1.0" +rstest = "0.17" serde = { version = "1.0", features = ["derive"] } -vncserver = "0.2" +serde_json = "1.0" +simple_moving_average = "0.1" +thread-priority = "0.13" +tokio = { version = "1.28", features = ["fs", "rt-multi-thread", "net", "io-util", "macros", "process", "signal", "sync", "time"] } +vncserver = { version ="0.2", optional = true} +chrono = "0.4.26" [dev-dependencies] -rstest = "0.12" -lazy_static = "1.4" +criterion = {version = "0.5", features = ["async_tokio"]} + +[features] +default = ["vnc"] +vnc = ["dep:vncserver"] + +[lib] +name = "breakwater" +path = "src/lib.rs" + +[[bin]] +name = "breakwater" +path = "src/main.rs" + +[[bench]] +name = "benchmarks" +harness = false [profile.dev] opt-level = 3 [profile.release] opt-level = 3 - -[features] -count_pixels = [] diff --git a/README.md b/README.md index a97c3eb..2948de5 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ # breakwater breakwater is a very fast [Pixelflut](https://wiki.cccgoe.de/wiki/Pixelflut) server written in Rust. It is heavily inspired by [Shoreline](https://github.com/TobleMiner/shoreline). -It claims to be **the fastest Pixelflut server in existence** - at least at the time of writing 02/2022. +It claims to be the fastest Pixelflut server in existence - at least at the time of writing 02/2022. ![breakwater logo](docs/images/breakwater.png) # Features 1. Accepts Pixelflut commands -2. Provides VNC server so that everybody can watch -3. Exposes Prometheus metrics -4. IPv6 and legacy IP support +2. Can provide a VNC server so that everybody can watch +3. As an alternative it can stream to a RTMP sink, so that you can e.g. directly live-stream into Twitch or YouTube +4. Exposes Prometheus metrics +5. IPv6 and legacy IP support # Available Pixelflut commands Commands must be sent newline-separated, for more details see [Pixelflut](https://wiki.cccgoe.de/wiki/Pixelflut) @@ -45,47 +46,41 @@ cargo run --release -- --help ```bash cargo run --release -- --help - Finished release [optimized] target(s) in 0.03s + Finished release [optimized] target(s) in 0.04s Running `target/release/breakwater --help` -breakwater 0.0.1 - -USAGE: - breakwater [OPTIONS] - -OPTIONS: - -f, --fps - Frames per second the VNC server should aim for [default: 30] - - --font - The font used to render the text on the screen. Should be a ttf file [default: - Arial.ttf] - - -h, --height - Height of the drawing surface [default: 720] - - --help - Print help information - - -l, --listen-address - Listen address to bind to. The default value will listen on all interfaces for IPv4 and - v6 packets [default: [::]:1234] - - -p, --prometheus-listen-address - Listen address zhe prometheus exporter should listen om. The default value will listen - on all interfaces for IPv4 and v6 packets [default: [::]:9100] - - -t, --text - Text to display on the screen. The text will be followed by "on " - [default: "Breakwater Pixelflut server"] - - -v, --vnc-port - Port of the VNC server [default: 5900] - - -V, --version - Print version information - - -w, --width - Width of the drawing surface [default: 1280] +Usage: breakwater [OPTIONS] + +Options: + -l, --listen-address + Listen address to bind to. The default value will listen on all interfaces for IPv4 and IPv6 packets [default: [::]:1234] + --width + Width of the drawing surface [default: 1280] + --height + Height of the drawing surface [default: 720] + -f, --fps + Frames per second the server should aim for [default: 30] + -t, --text + Text to display on the screen. The text will be followed by "on " [default: "Pixelflut server (breakwater)"] + --font + The font used to render the text on the screen. Should be a ttf file. If you use the default value a copy that ships with breakwater will be used - no need to download and provide the font [default: Arial.ttf] + -p, --prometheus-listen-address + Listen address the prometheus exporter should listen on [default: [::]:9100] + --statistics-save-file + Save file where statistics are periodically saved. The save file will be read during startup and statistics are restored. To reset the statistics simply remove the file [default: statistics.json] + --statistics-save-interval-s + Interval (in seconds) in which the statistics save file should be updated [default: 10] + --disable-statistics-save-file + Disable periodical saving of statistics into save file + --rtmp-address + Enable rtmp streaming to configured address, e.g. `rtmp://127.0.0.1:1935/live/test` + --save-video-to-file + Enable dump of video stream into file. File name will be `pixelflut_dump_{timestamp}.mp4 + -v, --vnc-port + Port of the VNC server [default: 5900] + -h, --help + Print help + -V, --version + Print version ``` @@ -94,10 +89,14 @@ You can also build the binary with `cargo build --release`. The binary will be p ## Compile time features Breakwater also has some compile-time features for performance reasons. You can get the list of available features by looking at the [Cargo.toml](Cargo.toml). -To e.g. count the actual pixels colored by every IP enable the future `count_pixels` as follows. -Please note that this will have a very larger performance penality. +As of writing the following features are supported + +* `vnc` (enabled by default): Starts a VNC server, where users can connect to. Needs `libvncserver-dev` to be installed. Please note that the VNC server offers basically no latency, but consumes quite some CPU. + +To e.g. turn the VNS server off, build with + ```bash -cargo run --release --features count_pixels +cargo run --release --no-default-features # --features feature-to-enable ``` # Run in docker container @@ -105,7 +104,7 @@ This command will start the Pixelflut server in a docker container ```bash docker run --rm -p 1234:1234 -p 5900:5900 -p 9100:9100 sbernauer/breakwater # --help ``` -The following command stops the server again (if there are some problems with SIGINT) +The following command stops the server again (if there are some problems with `SIGINT`) ```bash docker stop $(docker ps -q --filter ancestor=sbernauer/breakwater) ``` @@ -154,23 +153,5 @@ The servers were connected with two 40G and one 10G links, through which traffic | [Shoreline](https://github.com/TobleMiner/shoreline) | C | 34 Gbit/s | | [Breakwater](https://github.com/sbernauer/breakwater) | Rust | 52 Gbit/s | -## Usage of [Tokio](https://crates.io/crates/tokio) -You can find a prototype with Tokio in the `tokio` branch. -Performance measurements have shown that the usage of Tokio decreased the average performance from 22.9 to 21.6 Gbit/s. -
- Used benchmark - -```bash -for i in $(seq 1 20); do - for branch in master tokio; do - git checkout $branch - cargo run --release >/dev/null 2>/dev/null & sleep 2; ../sturmflut/sturmflut 127.0.0.1 ../sturmflut/cat.jpg -t 24 >/dev/null 2>/dev/null & sleep 10; bmon -b -p lo -o ascii:quitafter=3 | tail -n 1 | awk '{ print $2 }' | tee -a "perf/$branch"; killall sturmflut; killall breakwater - sleep 1 - done -done -``` -
- - # TODOs -* Implement Alpha channel feature. For performance reasons there should be a compile-time switch (similar to `#ifdef` in C) +* Implement Alpha channel feature. For performance reasons there should be a compile-time switch for it diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs new file mode 100644 index 0000000..af01b67 --- /dev/null +++ b/benches/benchmarks.rs @@ -0,0 +1,122 @@ +use breakwater::{ + framebuffer::FrameBuffer, + parser::{from_hex_char_lookup, from_hex_char_map, parse_pixelflut_commands, ParserState}, + test::helpers::{get_commands_to_draw_rect, DevNullTcpStream}, +}; +use criterion::{ + BenchmarkId, Criterion, {criterion_group, criterion_main}, +}; +use std::{sync::Arc, time::Duration}; + +const FRAMEBUFFER_WIDTH: usize = 1920; +const FRAMEBUFFER_HEIGHT: usize = 1080; + +async fn invoke_parse_pixelflut_commands( + input: &[u8], + fb: &Arc, + parser_state: ParserState, +) { + let mut stream = DevNullTcpStream::default(); + parse_pixelflut_commands(input, fb, &mut stream, parser_state).await; +} + +#[allow(unused)] // Benchmarks are commented out by default +fn invoke_from_hex_char_map() -> u8 { + // So that we actually compute something + let mut result = 0; + for char in b'0'..=b'9' { + result |= from_hex_char_map(char); + } + for char in b'a'..=b'f' { + result |= from_hex_char_map(char); + } + for char in b'A'..=b'F' { + result |= from_hex_char_map(char); + } + result |= from_hex_char_map(b'\n'); + result |= from_hex_char_map(b' '); + result |= from_hex_char_map(b';'); + result |= from_hex_char_map(b'%'); + result +} + +#[allow(unused)] // Benchmarks are commented out by default +fn invoke_from_hex_char_lookup() -> u8 { + // So that we actually compute something + let mut result = 0; + for char in b'0'..=b'9' { + result |= from_hex_char_lookup(char); + } + for char in b'a'..=b'f' { + result |= from_hex_char_lookup(char); + } + for char in b'A'..=b'F' { + result |= from_hex_char_lookup(char); + } + result |= from_hex_char_lookup(b'\n'); + result |= from_hex_char_lookup(b' '); + result |= from_hex_char_lookup(b';'); + result |= from_hex_char_lookup(b'%'); + result +} + +fn from_elem(c: &mut Criterion) { + let draw_commands = get_commands_to_draw_rect(FRAMEBUFFER_WIDTH, FRAMEBUFFER_HEIGHT, 0x123456); + let draw_commands = draw_commands.as_bytes(); + + c.bench_with_input( + BenchmarkId::new( + "parse_draw_commands", + format!("{FRAMEBUFFER_WIDTH} x {FRAMEBUFFER_HEIGHT}"), + ), + &draw_commands, + |b, input| { + let fb = Arc::new(FrameBuffer::new(FRAMEBUFFER_WIDTH, FRAMEBUFFER_HEIGHT)); + let parser_state = ParserState::default(); + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| invoke_parse_pixelflut_commands(input, &fb, parser_state.clone())); + }, + ); + + // let read_commands = get_commands_to_read_rect(FRAMEBUFFER_WIDTH, FRAMEBUFFER_HEIGHT); + // let read_commands = read_commands.as_bytes(); + + // c.bench_with_input( + // BenchmarkId::new( + // "parse_read_commands", + // format!("{FRAMEBUFFER_WIDTH} x {FRAMEBUFFER_HEIGHT}"), + // ), + // &read_commands, + // |b, input| { + // let fb = Arc::new(FrameBuffer::new(FRAMEBUFFER_WIDTH, FRAMEBUFFER_HEIGHT)); + // let parser_state = ParserState::default(); + // b.to_async(tokio::runtime::Runtime::new().unwrap()) + // .iter(|| invoke_parse_pixelflut_commands(input, &fb, parser_state.clone())); + // }, + // ); + + // c.bench_function("from_hex_char_map", |b: &mut criterion::Bencher| { + // b.iter(invoke_from_hex_char_map) + // }); + // c.bench_function("from_hex_char_lookup", |b: &mut criterion::Bencher| { + // b.iter(invoke_from_hex_char_lookup) + // }); +} + +criterion_group!( + name = benches; + config = Criterion::default().warm_up_time(Duration::from_secs(10)).measurement_time(Duration::from_secs(30)); + targets = from_elem +); +criterion_main!(benches); + +// Performance numbers + +// Starting point (while loop) 17.967 ms (does not count) 26.289 ms 26.725 ms +// Loop {} instead of while 19.118 ms (does not count) 26.563 ms 28.125 ms +// => Change not worth it. Use while loop for better readability + +// Starting point (checking for command by indexing [u8] buffer) 27.332 ms 27.175 ms 27.848 ms +// Check for command by reading u32 25.590 ms 25.327 ms 23.174 ms +// => Accepted the change :) So far we have only read changed the parsing for "PX " logic, lets also change the other parsing logics +// Check for command by reading u32 everywhere 24.465 ms 23.435 ms 22.087 ms diff --git a/src/args.rs b/src/args.rs index fa664d7..54274de 100644 --- a/src/args.rs +++ b/src/args.rs @@ -4,28 +4,24 @@ use clap::Parser; #[clap(author, version, about, long_about = None)] pub struct Args { /// Listen address to bind to. - /// The default value will listen on all interfaces for IPv4 and v6 packets. + /// The default value will listen on all interfaces for IPv4 and IPv6 packets. #[clap(short, long, default_value = "[::]:1234")] pub listen_address: String, - /// Port of the VNC server. - #[clap(short, long, default_value_t = 5900)] - pub vnc_port: u32, - - /// Width of the drawing surface - #[clap(short, long, default_value_t = 1280)] + /// Width of the drawing surface. + #[clap(long, default_value_t = 1280)] pub width: usize, - /// Height of the drawing surface - #[clap(short, long, default_value_t = 720)] + /// Height of the drawing surface. + #[clap(long, default_value_t = 720)] pub height: usize, - /// Frames per second the VNC server should aim for + /// Frames per second the server should aim for. #[clap(short, long, default_value_t = 30)] pub fps: u32, /// Text to display on the screen. - /// The text will be followed by "on " + /// The text will be followed by "on ". #[clap(short, long, default_value = "Pixelflut server (breakwater)")] pub text: String, @@ -35,8 +31,7 @@ pub struct Args { #[clap(long, default_value = "Arial.ttf")] pub font: String, - /// Listen address zhe prometheus exporter should listen om. - /// The default value will listen on all interfaces for IPv4 and v6 packets. + /// Listen address the prometheus exporter should listen on. #[clap(short, long, default_value = "[::]:9100")] pub prometheus_listen_address: String, @@ -46,11 +41,25 @@ pub struct Args { #[clap(long, default_value = "statistics.json")] pub statistics_save_file: String, - /// Interval (in seconds) in which the save file should be updated. + /// Interval (in seconds) in which the statistics save file should be updated. #[clap(long, default_value = "10")] pub statistics_save_interval_s: u64, /// Disable periodical saving of statistics into save file. #[clap(long)] pub disable_statistics_save_file: bool, + + /// Enable rtmp streaming to configured address, e.g. `rtmp://127.0.0.1:1935/live/test` + #[clap(long)] + pub rtmp_address: Option, + + /// Enable dump of video stream into file. File name will be `pixelflut_dump_{timestamp}.mp4 + #[clap(long)] + pub save_video_to_file: bool, + + /// Port of the VNC server. + // #[cfg_attr(feature = "vnc", clap(short, long, default_value_t = 5900))] + #[cfg(feature = "vnc")] + #[clap(short, long, default_value_t = 5900)] + pub vnc_port: u32, } diff --git a/src/framebuffer.rs b/src/framebuffer.rs index 979c453..b8302e0 100644 --- a/src/framebuffer.rs +++ b/src/framebuffer.rs @@ -1,41 +1,63 @@ -use std::sync::atomic::AtomicU32; -use std::sync::atomic::Ordering::Relaxed; +use std::{cell::UnsafeCell, slice}; pub struct FrameBuffer { - pub width: usize, - pub height: usize, - _vec: Vec, // Every pixel is represented by 32 bits: aabbggrr - slice: &'static mut [AtomicU32], + width: usize, + height: usize, + buffer: UnsafeCell>, } +// FIXME Nothing to see here, I don't know what I'm doing ¯\_(ツ)_/¯ +unsafe impl Sync for FrameBuffer {} + impl FrameBuffer { pub fn new(width: usize, height: usize) -> Self { - let mut vec = Vec::with_capacity(width * height); - vec.resize_with(width * height, || AtomicU32::new(0)); - let ptr = vec.as_mut_ptr(); - unsafe { - FrameBuffer { - width, - height, - _vec: vec, - slice: std::slice::from_raw_parts_mut(ptr, width * height), - } + let mut buffer = Vec::with_capacity(width * height); + buffer.resize_with(width * height, || 0); + FrameBuffer { + width, + height, + buffer: UnsafeCell::from(buffer), } } + pub fn get_width(&self) -> usize { + self.width + } + + pub fn get_height(&self) -> usize { + self.height + } + + pub fn get_size(&self) -> usize { + self.width * self.height + } + #[inline(always)] - pub fn get(&self, x: usize, y: usize) -> u32 { + pub fn get(&self, x: usize, y: usize) -> Option { if x < self.width && y < self.height { - self.slice[x + y * self.width].load(Relaxed) // Using Relaxed here for best performance + unsafe { Some((*self.buffer.get())[x + y * self.width]) } } else { - 0 + None } } #[inline(always)] pub fn set(&self, x: usize, y: usize, rgba: u32) { + // TODO: If we make the FrameBuffer large enough (e.g. 10_000 x 10_000) we don't need to check the bounds here (x and y are max 4 digit numbers). + // (flamegraph has shown 5.21% of runtime in this bound check O.o) if x < self.width && y < self.height { - self.slice[x + y * self.width].store(rgba, Relaxed); // Using Relaxed here for best performance + unsafe { (*self.buffer.get())[x + y * self.width] = rgba } } } + + pub fn get_buffer(&self) -> *mut Vec { + self.buffer.get() + } + + pub fn as_bytes(&self) -> &[u8] { + let buffer = self.buffer.get(); + let len_in_bytes: usize = unsafe { (*buffer).len() } * 4; + + unsafe { slice::from_raw_parts((*buffer).as_ptr() as *const u8, len_in_bytes) } + } } diff --git a/src/lib.rs b/src/lib.rs index 1fc6ccb..6df9305 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,8 @@ pub mod args; pub mod framebuffer; pub mod network; +pub mod parser; +pub mod prometheus_exporter; +pub mod sinks; pub mod statistics; -pub mod vnc; +pub mod test; diff --git a/src/main.rs b/src/main.rs index eb9f5aa..f9c406b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,50 +1,108 @@ -use std::sync::Arc; -use std::thread; - +use breakwater::{ + args::Args, + framebuffer::FrameBuffer, + network::Network, + prometheus_exporter::PrometheusExporter, + sinks::ffmpeg::FfmpegSink, + statistics::{Statistics, StatisticsEvent, StatisticsInformationEvent, StatisticsSaveMode}, +}; use clap::Parser; +use env_logger::Env; +use std::sync::Arc; +use tokio::sync::{broadcast, mpsc}; +#[cfg(feature = "vnc")] +use { + breakwater::sinks::vnc::VncServer, + thread_priority::{ThreadBuilderExt, ThreadPriority}, +}; -use breakwater::args::Args; -use breakwater::framebuffer::FrameBuffer; -use breakwater::network::Network; -use breakwater::statistics::{self, Statistics}; -use breakwater::vnc::VncServer; - -fn main() { +#[tokio::main] +async fn main() -> Result<(), Box> { + env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); let args = Args::parse(); let fb = Arc::new(FrameBuffer::new(args.width, args.height)); - let statistics = if args.disable_statistics_save_file { - Arc::new(Statistics::new(None)) + + // If we make the channel to big, stats will start to lag behind + // TODO: Check performance impact in real-world scenario. Maybe the statistics thread blocks the other threads + let (statistics_tx, statistics_rx) = mpsc::channel::(100); + let (statistics_information_tx, statistics_information_rx_for_prometheus_exporter) = + broadcast::channel::(2); + #[cfg(feature = "vnc")] + let statistics_information_rx_for_vnc_server = statistics_information_tx.subscribe(); + + let statistics_save_mode = if args.disable_statistics_save_file { + StatisticsSaveMode::Disabled } else { - Arc::new(Statistics::from_save_file_or_new( - &args.statistics_save_file, - )) + StatisticsSaveMode::Enabled { + save_file: args.statistics_save_file.clone(), + interval_s: args.statistics_save_interval_s, + } }; - statistics::start_loop(Arc::clone(&statistics), args.statistics_save_interval_s); - statistics::start_prometheus_server(args.prometheus_listen_address.as_str()); - - let network_listen_address = args.listen_address.clone(); - let network_fb = Arc::clone(&fb); - let network_statistics = Arc::clone(&statistics); - let network_thread = thread::spawn(move || { - let network = Network::new(&network_listen_address, network_fb, network_statistics); - network.listen(); + let mut statistics = Statistics::new( + statistics_rx, + statistics_information_tx, + statistics_save_mode, + )?; + + let network = Network::new(&args.listen_address, Arc::clone(&fb), statistics_tx.clone()); + let network_listener_thread = tokio::spawn(async move { + network.listen().await.unwrap(); }); - thread::spawn(move || { - let vnc_text = args.text; - let vnc_server = VncServer::new( - &fb, - args.vnc_port, - args.fps, - &vnc_text, - &statistics, - &args.font, - ); - vnc_server.run(); + let ffmpeg_sink = FfmpegSink::new(&args, Arc::clone(&fb)); + let ffmpeg_thread = + ffmpeg_sink.map(|sink| tokio::spawn(async move { sink.run().await.unwrap() })); + + #[cfg(feature = "vnc")] + let vnc_server_thread = { + let fb_for_vnc_server = Arc::clone(&fb); + // TODO Use tokio::spawn instead of std::thread::spawn + // I was not able to get to work with async closure + // We than also need to think about setting a priority + std::thread::Builder::new() + .name("breakwater vnc server thread".to_owned()) + .spawn_with_priority( + ThreadPriority::Crossplatform(70.try_into().expect("Failed to get cross-platform ThreadPriority. Please report this error message together with your operating system.")), + move |_| { + let mut vnc_server = VncServer::new( + fb_for_vnc_server, + args.vnc_port, + args.fps, + statistics_tx, + statistics_information_rx_for_vnc_server, + &args.text, + &args.font, + ); + vnc_server.run(); + }, + ) + .unwrap() + }; + + let statistics_thread = + tokio::spawn(async move { statistics.start().await.expect("Statistics thread failed") }); + + let mut prometheus_exporter = PrometheusExporter::new( + &args.prometheus_listen_address, + statistics_information_rx_for_prometheus_exporter, + ); + let prometheus_exporter_thread = tokio::spawn(async move { + prometheus_exporter.run().await; }); - network_thread - .join() - .expect("Failed to join network thread"); + prometheus_exporter_thread.await?; + network_listener_thread.await?; + if let Some(ffmpeg_thread) = ffmpeg_thread { + ffmpeg_thread.await?; + } + statistics_thread.await?; + #[cfg(feature = "vnc")] + { + vnc_server_thread + .join() + .expect("Failed to join VNC server thread"); + } + + Ok(()) } diff --git a/src/network.rs b/src/network.rs index 2d95315..55fabcb 100644 --- a/src/network.rs +++ b/src/network.rs @@ -1,357 +1,168 @@ -use std::io::prelude::*; -use std::net::{IpAddr, Ipv4Addr, TcpListener}; -use std::str; -use std::sync::Arc; -use std::thread; - -use crate::framebuffer::FrameBuffer; -use crate::statistics::Statistics; +use crate::{ + framebuffer::FrameBuffer, + parser::{parse_pixelflut_commands, ParserState, PARSER_LOOKAHEAD}, + statistics::StatisticsEvent, +}; +use log::{debug, info}; +use std::{ + cmp::min, + net::{IpAddr, Ipv4Addr}, + sync::Arc, + time::Duration, +}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpListener, + sync::mpsc::Sender, + time::Instant, +}; const NETWORK_BUFFER_SIZE: usize = 256_000; -pub const HELP_TEXT: &[u8] = "\ -Pixelflut server powered by breakwater https://github.com/sbernauer/breakwater -Available commands: -HELP: Show this help -PX x y rrggbb: Color the pixel (x,y) with the given hexadecimal color -PX x y rrggbbaa: Color the pixel (x,y) with the given hexadecimal color rrggbb (alpha channel is ignored for now) -PX x y: Get the color value of the pixel (x,y) -SIZE: Get the size of the drawing surface, e.g. `SIZE 1920 1080` -OFFSET x y: Apply offset (x,y) to all further pixel draws on this connection -".as_bytes(); -const LOOP_LOOKAHEAD: usize = "PX 1234 1234 rrggbbaa\n".len(); - -pub struct Network<'a> { - listen_address: &'a str, +// Every client connection spawns a new thread, so we need to limit the number of stat events we send +const STATISTICS_REPORT_INTERVAL: Duration = Duration::from_millis(250); + +pub struct Network { + listen_address: String, fb: Arc, - statistics: Arc, + statistics_tx: Sender, } -impl<'a> Network<'a> { - pub fn new(listen_address: &'a str, fb: Arc, statistics: Arc) -> Self { +impl Network { + pub fn new( + listen_address: &str, + fb: Arc, + statistics_tx: Sender, + ) -> Self { Network { - listen_address, + listen_address: listen_address.to_string(), fb, - statistics, + statistics_tx, } } - pub fn listen(&self) { - let listener = TcpListener::bind(self.listen_address) - .unwrap_or_else(|err| panic!("Failed to listen on {}: {}", self.listen_address, err)); - println!( - "Listening for Pixelflut connections on {}", - self.listen_address - ); - - for stream in listener.incoming() { - let stream = stream.expect("Failed to get tcp stream from listener"); - let ip = stream - .peer_addr() - .expect("Failed to get peer address from tcp connection") - .ip(); + pub async fn listen(&self) -> tokio::io::Result<()> { + let listener = TcpListener::bind(&self.listen_address).await?; + info!("Started Pixelflut server on {}", self.listen_address); + + loop { + let (socket, socket_addr) = listener.accept().await?; // If you connect via IPv4 you often show up as embedded inside an IPv6 address // Extracting the embedded information here, so we get the real (TM) address - let ip = ip_to_canonical(ip); - - self.statistics.inc_connections(ip); + let ip = ip_to_canonical(socket_addr.ip()); - let fb = Arc::clone(&self.fb); - let statistics = Arc::clone(&self.statistics); - thread::spawn(move || { - handle_connection(stream, ip, fb, statistics); + let fb_for_thread = Arc::clone(&self.fb); + let statistics_tx_for_thread = self.statistics_tx.clone(); + tokio::spawn(async move { + handle_connection(socket, ip, fb_for_thread, statistics_tx_for_thread).await; }); } } } -pub fn handle_connection( - mut stream: impl Read + Write + Unpin, +pub async fn handle_connection( + mut stream: impl AsyncReadExt + AsyncWriteExt + Unpin, ip: IpAddr, fb: Arc, - statistics: Arc, + statistics_tx: Sender, ) { + debug!("Handling connection from {ip}"); + + statistics_tx + .send(StatisticsEvent::ConnectionCreated { ip }) + .await + .expect("Statistics channel disconnected"); + + // TODO: Try performance of Vec<> on heap instead of stack. Also bigger buffer let mut buffer = [0u8; NETWORK_BUFFER_SIZE]; // Number bytes left over **on the first bytes of the buffer** from the previous loop iteration let mut leftover_bytes_in_buffer = 0; - let mut x: usize; - let mut y: usize; - let mut x_offset = 0; - let mut y_offset = 0; + // We have to keep the some things - such as connection offset - for the whole connection lifetime, so let's define them here + let mut parser_state = ParserState::default(); + + // If we send e.g. an StatisticsEvent::BytesRead for every time we read something from the socket the statistics thread would go crazy. + // Instead we bulk the statistics and send them pre-aggregated. + let mut last_statistics = Instant::now(); + let mut statistics_bytes_read: u64 = 0; loop { // Fill the buffer up with new data from the socket // If there are any bytes left over from the previous loop iteration leave them as is and but the new data behind - let bytes = match stream.read(&mut buffer[leftover_bytes_in_buffer..]) { - Ok(bytes) => bytes, + let bytes_read = match stream + .read(&mut buffer[leftover_bytes_in_buffer..NETWORK_BUFFER_SIZE - PARSER_LOOKAHEAD]) + .await + { + Ok(bytes_read) => bytes_read, Err(_) => { - statistics.dec_connections(ip); break; } }; - statistics.inc_bytes(ip, bytes as u64); + statistics_bytes_read += bytes_read as u64; + if last_statistics.elapsed() > STATISTICS_REPORT_INTERVAL { + statistics_tx + // We use a blocking call here as we want to process the stats. + // Otherwise the stats will lag behind resulting in weird spikes in bytes/s statistics. + // As the statistics calculation should be trivial let's wait for it + .send(StatisticsEvent::BytesRead { + ip, + bytes: statistics_bytes_read, + }) + .await + .expect("Statistics channel disconnected"); + last_statistics = Instant::now(); + statistics_bytes_read = 0; + } - let mut loop_end = leftover_bytes_in_buffer + bytes; - if bytes == 0 { + let data_end = leftover_bytes_in_buffer + bytes_read; + if bytes_read == 0 { if leftover_bytes_in_buffer == 0 { // We read no data and the previous loop did consume all data // Nothing to do here, closing connection - statistics.dec_connections(ip); break; } // No new data from socket, read to the end and everything should be fine leftover_bytes_in_buffer = 0; } else { - // Read some data, process it - if loop_end >= NETWORK_BUFFER_SIZE { - leftover_bytes_in_buffer = LOOP_LOOKAHEAD; - loop_end -= leftover_bytes_in_buffer; - } else { - leftover_bytes_in_buffer = 0; - } - } + // We have read some data, process it - let mut i = 0; // We can't use a for loop here because Rust don't lets use skip characters by incrementing i - while i < loop_end { - if buffer[i] == b'P' { - i += 1; - if buffer[i] == b'X' { - i += 1; - if buffer[i] == b' ' { - i += 1; - // Parse first x coordinate char - if buffer[i] >= b'0' && buffer[i] <= b'9' { - x = (buffer[i] - b'0') as usize; - i += 1; - - // Parse optional second x coordinate char - if buffer[i] >= b'0' && buffer[i] <= b'9' { - x = 10 * x + (buffer[i] - b'0') as usize; - i += 1; - - // Parse optional third x coordinate char - if buffer[i] >= b'0' && buffer[i] <= b'9' { - x = 10 * x + (buffer[i] - b'0') as usize; - i += 1; - - // Parse optional forth x coordinate char - if buffer[i] >= b'0' && buffer[i] <= b'9' { - x = 10 * x + (buffer[i] - b'0') as usize; - i += 1; - } - } - } - - // Separator between x and y - if buffer[i] == b' ' { - i += 1; - - // Parse first y coordinate char - if buffer[i] >= b'0' && buffer[i] <= b'9' { - y = (buffer[i] - b'0') as usize; - i += 1; - - // Parse optional second y coordinate char - if buffer[i] >= b'0' && buffer[i] <= b'9' { - y = 10 * y + (buffer[i] - b'0') as usize; - i += 1; - - // Parse optional third y coordinate char - if buffer[i] >= b'0' && buffer[i] <= b'9' { - y = 10 * y + (buffer[i] - b'0') as usize; - i += 1; - - // Parse optional forth y coordinate char - if buffer[i] >= b'0' && buffer[i] <= b'9' { - y = 10 * y + (buffer[i] - b'0') as usize; - i += 1; - } - } - } - - x += x_offset; - y += y_offset; - - // Separator between coordinates and color - if buffer[i] == b' ' { - i += 1; - - // Must be followed by 6 bytes RGB and newline or ... - if buffer[i + 6] == b'\n' { - i += 7; // We can advance one byte more than normal as we use continue and therefore not get incremented at the end of the loop - - let rgba: u32 = (from_hex_char(buffer[i - 3]) as u32) - << 20 - | (from_hex_char(buffer[i - 2]) as u32) << 16 - | (from_hex_char(buffer[i - 5]) as u32) << 12 - | (from_hex_char(buffer[i - 4]) as u32) << 8 - | (from_hex_char(buffer[i - 7]) as u32) << 4 - | (from_hex_char(buffer[i - 6]) as u32); - - fb.set(x, y, rgba); - if cfg!(feature = "count_pixels") { - statistics.inc_pixels(ip); - } - continue; - } - - // ... or must be followed by 8 bytes RGBA and newline - if buffer[i + 8] == b'\n' { - i += 9; // We can advance one byte more than normal as we use continue and therefore not get incremented at the end of the loop - - let rgba: u32 = (from_hex_char(buffer[i - 5]) as u32) - << 20 - | (from_hex_char(buffer[i - 4]) as u32) << 16 - | (from_hex_char(buffer[i - 7]) as u32) << 12 - | (from_hex_char(buffer[i - 6]) as u32) << 8 - | (from_hex_char(buffer[i - 9]) as u32) << 4 - | (from_hex_char(buffer[i - 8]) as u32); - - fb.set(x, y, rgba); - if cfg!(feature = "count_pixels") { - statistics.inc_pixels(ip); - } - continue; - } - } - - // End of command to read Pixel value - if buffer[i] == b'\n' && x < fb.width && y < fb.height { - match stream.write_all( - format!( - "PX {} {} {:06x}\n", - // We don't want to return the actual (absolute) coordinates, the client should also get the result offseted - x - x_offset, - y - y_offset, - fb.get(x, y).to_be() >> 8 - ) - .as_bytes(), - ) { - Ok(_) => (), - Err(_) => continue, - } - } - } - } - } - } - } - } else if buffer[i] == b'S' { - i += 1; - if buffer[i] == b'I' { - i += 1; - if buffer[i] == b'Z' { - i += 1; - if buffer[i] == b'E' { - stream - .write_all(format!("SIZE {} {}\n", fb.width, fb.height).as_bytes()) - .expect("Failed to write bytes to tcp socket"); - } - } - } - } else if buffer[i] == b'H' { - i += 1; - if buffer[i] == b'E' { - i += 1; - if buffer[i] == b'L' { - i += 1; - if buffer[i] == b'P' { - stream - .write_all(HELP_TEXT) - .expect("Failed to write bytes to tcp socket"); - } - } - } - } else if buffer[i] == b'O' - && buffer[i + 1] == b'F' - && buffer[i + 2] == b'F' - && buffer[i + 3] == b'S' - && buffer[i + 4] == b'E' - && buffer[i + 5] == b'T' - { - i += 6; - if buffer[i] == b' ' { - i += 1; - // Parse first x coordinate char - if buffer[i] >= b'0' && buffer[i] <= b'9' { - x = (buffer[i] - b'0') as usize; - i += 1; - - // Parse optional second x coordinate char - if buffer[i] >= b'0' && buffer[i] <= b'9' { - x = 10 * x + (buffer[i] - b'0') as usize; - i += 1; - - // Parse optional third x coordinate char - if buffer[i] >= b'0' && buffer[i] <= b'9' { - x = 10 * x + (buffer[i] - b'0') as usize; - i += 1; - - // Parse optional forth x coordinate char - if buffer[i] >= b'0' && buffer[i] <= b'9' { - x = 10 * x + (buffer[i] - b'0') as usize; - i += 1; - } - } - } - - // Separator between x and y - if buffer[i] == b' ' { - i += 1; - - // Parse first y coordinate char - if buffer[i] >= b'0' && buffer[i] <= b'9' { - y = (buffer[i] - b'0') as usize; - i += 1; - - // Parse optional second y coordinate char - if buffer[i] >= b'0' && buffer[i] <= b'9' { - y = 10 * y + (buffer[i] - b'0') as usize; - i += 1; - - // Parse optional third y coordinate char - if buffer[i] >= b'0' && buffer[i] <= b'9' { - y = 10 * y + (buffer[i] - b'0') as usize; - i += 1; - - // Parse optional forth y coordinate char - if buffer[i] >= b'0' && buffer[i] <= b'9' { - y = 10 * y + (buffer[i] - b'0') as usize; - i += 1; - } - } - } - - // End of command to set offset - if buffer[i] == b'\n' { - x_offset = x; - y_offset = y; - } - } - } - } - } + // We need to zero the PARSER_LOOKAHEAD bytes, so the parser does not detect any command left over from a previous loop iteration + for i in &mut buffer[data_end..data_end + PARSER_LOOKAHEAD] { + *i = 0; } - i += 1; + parser_state = parse_pixelflut_commands( + &buffer[..data_end + PARSER_LOOKAHEAD], + &fb, + &mut stream, + parser_state, + ) + .await; + + // IMPORTANT: We have to subtract 1 here, as e.g. we have "PX 0 0\n" data_end is 7 and parser_state.last_byte_parsed is 6. + // This happens, because last_byte_parsed is an index starting at 0, so index 6 is from an array of length 7 + leftover_bytes_in_buffer = data_end - parser_state.last_byte_parsed() - 1; + + // There is no need to leave anything longer than a command can take + // This prevents malicious clients from sending gibberish and the buffer not getting drained + leftover_bytes_in_buffer = min(leftover_bytes_in_buffer, PARSER_LOOKAHEAD); } if leftover_bytes_in_buffer > 0 { // We need to move the leftover bytes to the beginning of the buffer so that the next loop iteration con work on them - buffer.copy_within(NETWORK_BUFFER_SIZE - leftover_bytes_in_buffer.., 0); + buffer.copy_within( + parser_state.last_byte_parsed() + 1 + ..parser_state.last_byte_parsed() + 1 + leftover_bytes_in_buffer, + 0, + ); } } -} -fn from_hex_char(char: u8) -> u8 { - match char { - b'0'..=b'9' => char - b'0', - b'a'..=b'f' => char - b'a' + 10, - b'A'..=b'F' => char - b'A' + 10, - _ => 0, - } + statistics_tx + .send(StatisticsEvent::ConnectionClosed { ip }) + .await + .expect("Statistics channel disconnected"); } /// TODO: Switch to official ip.to_canonical() method when it is stable. **If** it gets stable sometime ;) @@ -369,22 +180,196 @@ fn ip_to_canonical(ip: IpAddr) -> IpAddr { } #[cfg(test)] -mod tests { - use crate::network::*; - use rstest::rstest; - use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +mod test { + use super::*; + use crate::test::helpers::MockTcpStream; + use rstest::{fixture, rstest}; + use std::time::Duration; + use tokio::sync::mpsc::{self, Receiver}; + + #[fixture] + fn ip() -> IpAddr { + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)) + } + + #[fixture] + fn fb() -> Arc { + Arc::new(FrameBuffer::new(1920, 1080)) + } + + #[fixture] + fn statistics_channel() -> (Sender, Receiver) { + mpsc::channel(10000) + } + + #[rstest] + #[timeout(Duration::from_secs(1))] + #[case("", "")] + #[case("\n", "")] + #[case("not a pixelflut command", "")] + #[case("not a pixelflut command with newline\n", "")] + #[case("SIZE", "SIZE 1920 1080\n")] + #[case("SIZE\n", "SIZE 1920 1080\n")] + #[case("SIZE\nSIZE\n", "SIZE 1920 1080\nSIZE 1920 1080\n")] + #[case("SIZE", "SIZE 1920 1080\n")] + #[case("HELP", std::str::from_utf8(crate::parser::HELP_TEXT).unwrap())] + #[case("HELP\n", std::str::from_utf8(crate::parser::HELP_TEXT).unwrap())] + #[case("bla bla bla\nSIZE\nblub\nbla", "SIZE 1920 1080\n")] + #[tokio::test] + async fn test_correct_responses_to_general_commands( + #[case] input: &str, + #[case] expected: &str, + ip: IpAddr, + fb: Arc, + statistics_channel: (Sender, Receiver), + ) { + let mut stream = MockTcpStream::from_input(input); + handle_connection(&mut stream, ip, fb, statistics_channel.0).await; + + assert_eq!(expected, stream.get_output()); + } #[rstest] - #[case(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), "0.0.0.0")] - #[case(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), "127.0.0.1")] - #[case(IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0)), "fe80::")] + // Without alpha + #[case("PX 0 0 ffffff\nPX 0 0\n", "PX 0 0 ffffff\n")] + #[case("PX 0 0 abcdef\nPX 0 0\n", "PX 0 0 abcdef\n")] + #[case("PX 0 42 abcdef\nPX 0 42\n", "PX 0 42 abcdef\n")] + #[case("PX 42 0 abcdef\nPX 42 0\n", "PX 42 0 abcdef\n")] + // With alpha + // TODO: At the moment alpha channel is not supported and silently ignored (pixels are painted with 0% transparency) + #[case("PX 0 0 ffffffaa\nPX 0 0\n", "PX 0 0 ffffff\n")] + #[case("PX 0 0 abcdefaa\nPX 0 0\n", "PX 0 0 abcdef\n")] + #[case("PX 0 1 abcdefaa\nPX 0 1\n", "PX 0 1 abcdef\n")] + #[case("PX 1 0 abcdefaa\nPX 1 0\n", "PX 1 0 abcdef\n")] + // Tests invalid bounds + #[case("PX 9999 0 abcdef\nPX 9999 0\n", "")] // Parsable but outside screen size + #[case("PX 0 9999 abcdef\nPX 9999 0\n", "")] + #[case("PX 9999 9999 abcdef\nPX 9999 9999\n", "")] + #[case("PX 99999 0 abcdef\nPX 0 99999\n", "")] // Not even parsable because to many digits + #[case("PX 0 99999 abcdef\nPX 0 99999\n", "")] + #[case("PX 99999 99999 abcdef\nPX 99999 99999\n", "")] + // Test invalid inputs + #[case("PX 0 abcdef\nPX 0 0\n", "PX 0 0 000000\n")] + #[case("PX 0 1 2 abcdef\nPX 0 0\n", "PX 0 0 000000\n")] + #[case("PX -1 0 abcdef\nPX 0 0\n", "PX 0 0 000000\n")] + #[case("bla bla bla\nPX 0 0\n", "PX 0 0 000000\n")] + // Test offset #[case( - IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0x85a3, 0x0000, 0x0000, 0x8a2e, 0x0370, 0x7334)), - "2001:db8:85a3::8a2e:370:7334" - )] - #[case(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0xFFFF, 0, 1)), "0.0.0.1")] - #[case(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0xFFFF, 127 << 8, 1)), "127.0.0.1")] - fn test_ip_to_string_respect_mapped_v4(#[case] input: IpAddr, #[case] expected: String) { - assert_eq!(expected, ip_to_canonical(input).to_string()); + "OFFSET 10 10\nPX 0 0 ffffff\nPX 0 0\nPX 42 42\n", + "PX 0 0 ffffff\nPX 42 42 000000\n" + )] // The get pixel result is also offseted + #[case("OFFSET 0 0\nPX 0 42 abcdef\nPX 0 42\n", "PX 0 42 abcdef\n")] + #[tokio::test] + async fn test_setting_pixel( + #[case] input: &str, + #[case] expected: &str, + ip: IpAddr, + fb: Arc, + statistics_channel: (Sender, Receiver), + ) { + let mut stream = MockTcpStream::from_input(input); + handle_connection(&mut stream, ip, fb, statistics_channel.0).await; + + assert_eq!(expected, stream.get_output()); + } + + #[rstest] + #[case(5, 5, 0, 0)] + #[case(6, 6, 0, 0)] + #[case(7, 7, 0, 0)] + #[case(8, 8, 0, 0)] + #[case(9, 9, 0, 0)] + #[case(10, 10, 0, 0)] + #[case(10, 10, 100, 200)] + #[case(10, 10, 510, 520)] + #[case(100, 100, 0, 0)] + #[case(100, 100, 300, 400)] + #[case(479, 361, 721, 391)] + #[case(500, 500, 0, 0)] + #[case(500, 500, 300, 400)] + #[case(fb().get_width(), fb().get_height(), 0, 0)] + #[case(fb().get_width() - 1, fb().get_height() - 1, 1, 1)] + #[tokio::test] + async fn test_drawing_rect( + #[case] width: usize, + #[case] height: usize, + #[case] offset_x: usize, + #[case] offset_y: usize, + ip: IpAddr, + fb: Arc, + statistics_channel: (Sender, Receiver), + ) { + let mut color: u32 = 0; + let mut fill_commands = String::new(); + let mut read_commands = String::new(); + let mut combined_commands = String::new(); + let mut combined_commands_expected = String::new(); + let mut read_other_pixels_commands = String::new(); + let mut read_other_pixels_commands_expected = String::new(); + + for x in 0..fb.get_width() { + for y in 0..height { + // Inside the rect + if x >= offset_x && x <= offset_x + width && y >= offset_y && y <= offset_y + height + { + fill_commands += &format!("PX {x} {y} {color:06x}\n"); + read_commands += &format!("PX {x} {y}\n"); + + color += 1; // Use another color for the next test case + combined_commands += &format!("PX {x} {y} {color:06x}\nPX {x} {y}\n"); + combined_commands_expected += &format!("PX {x} {y} {color:06x}\n"); + + color += 1; + } else { + // Non touched pixels must remain black + read_other_pixels_commands += &format!("PX {x} {y}\n"); + read_other_pixels_commands_expected += &format!("PX {x} {y} 000000\n"); + } + } + } + + // Color the pixels + let mut stream = MockTcpStream::from_input(&fill_commands); + handle_connection( + &mut stream, + ip, + Arc::clone(&fb), + statistics_channel.0.clone(), + ) + .await; + assert_eq!("", stream.get_output()); + + // Read the pixels again + let mut stream = MockTcpStream::from_input(&read_commands); + handle_connection( + &mut stream, + ip, + Arc::clone(&fb), + statistics_channel.0.clone(), + ) + .await; + assert_eq!(fill_commands, stream.get_output()); + + // We can also do coloring and reading in a single connection + let mut stream = MockTcpStream::from_input(&combined_commands); + handle_connection( + &mut stream, + ip, + Arc::clone(&fb), + statistics_channel.0.clone(), + ) + .await; + assert_eq!(combined_commands_expected, stream.get_output()); + + // Check that nothing else was colored + let mut stream = MockTcpStream::from_input(&read_other_pixels_commands); + handle_connection( + &mut stream, + ip, + Arc::clone(&fb), + statistics_channel.0.clone(), + ) + .await; + assert_eq!(read_other_pixels_commands_expected, stream.get_output()); } } diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..4ce0928 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,360 @@ +use std::sync::Arc; + +use tokio::io::AsyncWriteExt; + +use crate::framebuffer::FrameBuffer; + +pub const PARSER_LOOKAHEAD: usize = "PX 1234 1234 rrggbbaa\n".len(); // Longest possible command +pub const HELP_TEXT: &[u8] = "\ +Pixelflut server powered by breakwater https://github.com/sbernauer/breakwater +Available commands: +HELP: Show this help +PX x y rrggbb: Color the pixel (x,y) with the given hexadecimal color +PX x y rrggbbaa: Color the pixel (x,y) with the given hexadecimal color rrggbb (alpha channel is ignored for now) +PX x y: Get the color value of the pixel (x,y) +SIZE: Get the size of the drawing surface, e.g. `SIZE 1920 1080` +OFFSET x y: Apply offset (x,y) to all further pixel draws on this connection +".as_bytes(); + +#[derive(Clone, Default, Debug)] +pub struct ParserState { + connection_x_offset: usize, + connection_y_offset: usize, + last_byte_parsed: usize, +} + +impl ParserState { + pub fn last_byte_parsed(&self) -> usize { + self.last_byte_parsed + } +} + +/// Returns the offset (think of index in [u8]) of the last bytes of the last fully parsed command. +/// +/// TODO: Implement support for 16K (15360 × 8640). +/// Currently the parser only can read up to 4 digits of x or y coordinates. +/// If you buy me a big enough screen I will kindly implement this feature. +pub async fn parse_pixelflut_commands( + buffer: &[u8], + fb: &Arc, + mut stream: impl AsyncWriteExt + Unpin, + // We don't pass this as mutual reference but instead hand it around - I guess on the stack? + // I don't know what I'm doing, hoping for best performance anyway ;) + parser_state: ParserState, +) -> ParserState { + let mut last_byte_parsed = 0; + let mut connection_x_offset = parser_state.connection_x_offset; + let mut connection_y_offset = parser_state.connection_y_offset; + + let mut x: usize; + let mut y: usize; + + let mut i = 0; // We can't use a for loop here because Rust don't lets use skip characters by incrementing i + let loop_end = buffer.len().saturating_sub(PARSER_LOOKAHEAD); // Let's extract the .len() call and the subtraction into it's own variable so we only compute it once + + while i < loop_end { + // Check for buffer[i] = "PX " + if unsafe { (buffer.as_ptr().add(i) as *const u32).read_unaligned() } & 0x00ff_ffff + == 0x50582000_u32.swap_bytes() + { + i += 3; + // Parse first x coordinate char + if buffer[i] >= b'0' && buffer[i] <= b'9' { + x = (buffer[i] - b'0') as usize; + i += 1; + + // Parse optional second x coordinate char + if buffer[i] >= b'0' && buffer[i] <= b'9' { + // TODO: Test bitshifts and add instead of multiplication + // i = (i << 3) + (i << 1); + // i = (i * 8) + (i * 2); + // i = 8i + 2i + // i = 10i + x = 10 * x + (buffer[i] - b'0') as usize; + i += 1; + + // Parse optional third x coordinate char + if buffer[i] >= b'0' && buffer[i] <= b'9' { + x = 10 * x + (buffer[i] - b'0') as usize; + i += 1; + + // Parse optional forth x coordinate char + if buffer[i] >= b'0' && buffer[i] <= b'9' { + x = 10 * x + (buffer[i] - b'0') as usize; + i += 1; + } + } + } + + // Separator between x and y + if buffer[i] == b' ' { + i += 1; + + // Parse first y coordinate char + if buffer[i] >= b'0' && buffer[i] <= b'9' { + y = (buffer[i] - b'0') as usize; + i += 1; + + // Parse optional second y coordinate char + if buffer[i] >= b'0' && buffer[i] <= b'9' { + y = 10 * y + (buffer[i] - b'0') as usize; + i += 1; + + // Parse optional third y coordinate char + if buffer[i] >= b'0' && buffer[i] <= b'9' { + y = 10 * y + (buffer[i] - b'0') as usize; + i += 1; + + // Parse optional forth y coordinate char + if buffer[i] >= b'0' && buffer[i] <= b'9' { + y = 10 * y + (buffer[i] - b'0') as usize; + i += 1; + } + } + } + + x += connection_x_offset; + y += connection_y_offset; + + // Separator between coordinates and color + if buffer[i] == b' ' { + i += 1; + + // TODO: Determine what clients use more: RGB or RGBA. + // If RGBA is used more often move the RGB code below the RGBA code + + // Must be followed by 6 bytes RGB and newline or ... + if buffer[i + 6] == b'\n' { + last_byte_parsed = i + 6; + i += 7; // We can advance one byte more than normal as we use continue and therefore not get incremented at the end of the loop + + // 30% slower (38,334 ms vs 29,385 ms) + // let str = unsafe { + // std::str::from_utf8_unchecked(&buffer[i - 7..i - 2]) + // }; + // let rgba = u32::from_str_radix(str, 16).unwrap(); + + let rgba: u32 = + (ASCII_HEXADECIMAL_VALUES[buffer[i - 3] as usize] as u32) << 20 + | (ASCII_HEXADECIMAL_VALUES[buffer[i - 2] as usize] as u32) + << 16 + | (ASCII_HEXADECIMAL_VALUES[buffer[i - 5] as usize] as u32) + << 12 + | (ASCII_HEXADECIMAL_VALUES[buffer[i - 4] as usize] as u32) + << 8 + | (ASCII_HEXADECIMAL_VALUES[buffer[i - 7] as usize] as u32) + << 4 + | (ASCII_HEXADECIMAL_VALUES[buffer[i - 6] as usize] as u32); + + fb.set(x, y, rgba); + if cfg!(feature = "count_pixels") { + // statistics.inc_pixels(ip); + } + continue; + } + + // ... or must be followed by 8 bytes RGBA and newline + if buffer[i + 8] == b'\n' { + last_byte_parsed = i + 8; + i += 9; // We can advance one byte more than normal as we use continue and therefore not get incremented at the end of the loop + + let rgba: u32 = + (ASCII_HEXADECIMAL_VALUES[buffer[i - 5] as usize] as u32) << 20 + | (ASCII_HEXADECIMAL_VALUES[buffer[i - 4] as usize] as u32) + << 16 + | (ASCII_HEXADECIMAL_VALUES[buffer[i - 7] as usize] as u32) + << 12 + | (ASCII_HEXADECIMAL_VALUES[buffer[i - 6] as usize] as u32) + << 8 + | (ASCII_HEXADECIMAL_VALUES[buffer[i - 9] as usize] as u32) + << 4 + | (ASCII_HEXADECIMAL_VALUES[buffer[i - 8] as usize] as u32); + + fb.set(x, y, rgba); + if cfg!(feature = "count_pixels") { + // statistics.inc_pixels(ip); + } + + continue; + } + } + + // End of command to read Pixel value + if buffer[i] == b'\n' { + last_byte_parsed = i; + i += 1; + if let Some(rgb) = fb.get(x, y) { + match stream + .write_all( + format!( + "PX {} {} {:06x}\n", + // We don't want to return the actual (absolute) coordinates, the client should also get the result offseted + x - connection_x_offset, + y - connection_y_offset, + rgb.to_be() >> 8 + ) + .as_bytes(), + ) + .await + { + Ok(_) => (), + Err(_) => continue, + } + } + continue; + } + } + } + } + // Check for buffer[i] = "SIZE" + } else if unsafe { (buffer.as_ptr().add(i) as *const u32).read_unaligned() } + == 0x53495a45_u32.swap_bytes() + { + i += 4; + last_byte_parsed = i - 1; + + stream + .write_all(format!("SIZE {} {}\n", fb.get_width(), fb.get_height()).as_bytes()) + .await + .expect("Failed to write bytes to tcp socket"); + continue; + // Check for buffer[i] = "HELP" + } else if unsafe { (buffer.as_ptr().add(i) as *const u32).read_unaligned() } + == 0x48454c50_u32.swap_bytes() + { + i += 4; + last_byte_parsed = i - 1; + + stream + .write_all(HELP_TEXT) + .await + .expect("Failed to write bytes to tcp socket"); + continue; + // Check for buffer[i] = "OFFSET " + } else if unsafe { (buffer.as_ptr().add(i) as *const u64).read_unaligned() } + & 0x0000_ffff_ffff_ffff + == 0x4f464653455420_u64.swap_bytes() + { + i += 7; + // Parse first x coordinate char + if buffer[i] >= b'0' && buffer[i] <= b'9' { + x = (buffer[i] - b'0') as usize; + i += 1; + + // Parse optional second x coordinate char + if buffer[i] >= b'0' && buffer[i] <= b'9' { + x = 10 * x + (buffer[i] - b'0') as usize; + i += 1; + + // Parse optional third x coordinate char + if buffer[i] >= b'0' && buffer[i] <= b'9' { + x = 10 * x + (buffer[i] - b'0') as usize; + i += 1; + + // Parse optional forth x coordinate char + if buffer[i] >= b'0' && buffer[i] <= b'9' { + x = 10 * x + (buffer[i] - b'0') as usize; + i += 1; + } + } + } + + // Separator between x and y + if buffer[i] == b' ' { + i += 1; + + // Parse first y coordinate char + if buffer[i] >= b'0' && buffer[i] <= b'9' { + y = (buffer[i] - b'0') as usize; + i += 1; + + // Parse optional second y coordinate char + if buffer[i] >= b'0' && buffer[i] <= b'9' { + y = 10 * y + (buffer[i] - b'0') as usize; + i += 1; + + // Parse optional third y coordinate char + if buffer[i] >= b'0' && buffer[i] <= b'9' { + y = 10 * y + (buffer[i] - b'0') as usize; + i += 1; + + // Parse optional forth y coordinate char + if buffer[i] >= b'0' && buffer[i] <= b'9' { + y = 10 * y + (buffer[i] - b'0') as usize; + i += 1; + } + } + } + + // End of command to set offset + if buffer[i] == b'\n' { + last_byte_parsed = i; + connection_x_offset = x; + connection_y_offset = y; + continue; + } + } + } + } + } + + i += 1; + } + + ParserState { + connection_x_offset, + connection_y_offset, + last_byte_parsed, + } +} + +#[inline(always)] +pub fn from_hex_char_map(char: u8) -> u8 { + match char { + b'0'..=b'9' => char - b'0', + b'a'..=b'f' => char - b'a' + 10, + b'A'..=b'F' => char - b'A' + 10, + _ => 0, + } +} + +// fn main() { +// let numbers = (0..=255) +// .map(|char| match char { +// b'0'..=b'9' => char - b'0', +// b'a'..=b'f' => char - b'a' + 10, +// b'A'..=b'F' => char - b'A' + 10, +// _ => 0, +// }) +// .map(|number| number.to_string()) +// .collect::>(); +// println!("{}", numbers.join(", ")); +// } +const ASCII_HEXADECIMAL_VALUES: [u8; 256] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, + 0, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, +]; + +#[inline(always)] +pub fn from_hex_char_lookup(char: u8) -> u8 { + ASCII_HEXADECIMAL_VALUES[char as usize] +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_from_hex_char() { + for c in 0..=255 { + assert_eq!(from_hex_char_map(c), from_hex_char_map(c)); + } + } +} diff --git a/src/prometheus_exporter.rs b/src/prometheus_exporter.rs new file mode 100644 index 0000000..5ae8809 --- /dev/null +++ b/src/prometheus_exporter.rs @@ -0,0 +1,93 @@ +use std::net::SocketAddr; + +use prometheus_exporter::{ + self, + prometheus::{register_int_gauge, register_int_gauge_vec, IntGauge, IntGaugeVec}, +}; +use tokio::sync::broadcast; + +use crate::statistics::StatisticsInformationEvent; + +pub struct PrometheusExporter { + listen_addr: SocketAddr, + statistics_information_rx: broadcast::Receiver, + + // Prometheus metrics + metric_ips: IntGauge, + metric_legacy_ips: IntGauge, + metric_frame: IntGauge, + metric_statistic_events: IntGauge, + + metric_connections_for_ip: IntGaugeVec, + metric_bytes_for_ip: IntGaugeVec, +} + +impl PrometheusExporter { + pub fn new( + listen_addr: &str, + statistics_information_rx: broadcast::Receiver, + ) -> Self { + let listen_addr = listen_addr.parse().unwrap_or_else(|_| { + panic!("Failed to parse prometheus listen address: {listen_addr}",) + }); + PrometheusExporter { + listen_addr, + statistics_information_rx, + metric_ips: register_int_gauge!("breakwater_ips", "Total number of IPs connected") + .unwrap(), + metric_legacy_ips: register_int_gauge!( + "breakwater_legacy_ips", + "Total number of legacy (v4) IPs connected" + ) + .unwrap(), + metric_frame: register_int_gauge!("breakwater_frame", "Frame number of the VNC server") + .unwrap(), + metric_statistic_events: register_int_gauge!( + "breakwater_statistic_events", + "Number of statistics events send internally" + ) + .unwrap(), + metric_connections_for_ip: register_int_gauge_vec!( + "breakwater_connections", + "Number of client connections per IP address", + &["ip"] + ) + .unwrap(), + metric_bytes_for_ip: register_int_gauge_vec!( + "breakwater_bytes", + "Number of bytes received", + &["ip"] + ) + .unwrap(), + } + } + + pub async fn run(&mut self) { + prometheus_exporter::start(self.listen_addr).expect("Failed to start prometheus exporter"); + while let Ok(event) = self.statistics_information_rx.recv().await { + self.metric_ips.set(event.ips as i64); + self.metric_legacy_ips.set(event.legacy_ips as i64); + self.metric_frame.set(event.frame as i64); + self.metric_statistic_events + .set(event.statistic_events as i64); + + // When clients drop a connection the item will be missing in `event.connections_for_ip, + // but would stay forever in the Prometheus metric + self.metric_connections_for_ip.reset(); + event + .connections_for_ip + .iter() + .for_each(|(ip, connections)| { + self.metric_connections_for_ip + .with_label_values(&[&ip.to_string()]) + .set(*connections as i64) + }); + self.metric_bytes_for_ip.reset(); + event.bytes_for_ip.iter().for_each(|(ip, bytes)| { + self.metric_bytes_for_ip + .with_label_values(&[&ip.to_string()]) + .set(*bytes as i64) + }); + } + } +} diff --git a/src/sinks/ffmpeg.rs b/src/sinks/ffmpeg.rs new file mode 100644 index 0000000..46c6b41 --- /dev/null +++ b/src/sinks/ffmpeg.rs @@ -0,0 +1,132 @@ +use std::{process::Stdio, sync::Arc, time::Duration}; + +use chrono::Local; +use tokio::{io::AsyncWriteExt, process::Command, time}; + +use crate::{args::Args, framebuffer::FrameBuffer}; + +pub struct FfmpegSink { + fb: Arc, + rtmp_address: Option, + save_video_to_file: bool, + fps: u32, +} + +impl FfmpegSink { + pub fn new(args: &Args, fb: Arc) -> Option { + if args.rtmp_address.is_some() || args.save_video_to_file { + Some(FfmpegSink { + fb, + rtmp_address: args.rtmp_address.clone(), + save_video_to_file: args.save_video_to_file, + fps: args.fps, + }) + } else { + None + } + } + + pub async fn run(&self) -> tokio::io::Result<()> { + let mut ffmpeg_args: Vec = self + .ffmpeg_input_args() + .into_iter() + .flat_map(|(arg, value)| [format!("-{arg}"), value]) + .collect(); + + let video_file = format!( + "pixelflut_dump_{}.mp4", + Local::now().format("%Y-%m-%d_%H-%M-%S") + ); + match &self.rtmp_address { + Some(rtmp_address) => { + if self.save_video_to_file { + ffmpeg_args.extend( + self.ffmpeg_rtmp_sink_args() + .into_iter() + .flat_map(|(arg, value)| [format!("-{arg}"), value]) + .collect::>(), + ); + ffmpeg_args.extend([ + "-f".to_string(), + "tee".to_string(), + "-map".to_string(), + "0:v".to_string(), + "-map".to_string(), + "1:a".to_string(), + format!( + "{video_file}|[f=flv]{rtmp_address}", + rtmp_address = rtmp_address.clone(), + ), + ]); + todo!("Writing to file and rtmp sink simultaneously currently not supported"); + } else { + ffmpeg_args.extend( + self.ffmpeg_rtmp_sink_args() + .into_iter() + .flat_map(|(arg, value)| [format!("-{arg}"), value]) + .collect::>(), + ); + ffmpeg_args.extend(["-f".to_string(), "flv".to_string(), rtmp_address.clone()]) + } + } + None => { + if self.save_video_to_file { + ffmpeg_args.extend([video_file]) + } else { + unreachable!("FfmpegSink can only be created when either rtmp or video file is activated") + } + } + } + + log::info!("ffmpeg {}", ffmpeg_args.join(" ")); + let mut command = Command::new("ffmpeg") + .args(ffmpeg_args) + .stdin(Stdio::piped()) + .spawn() + .unwrap(); + + let mut stdin = command + .stdin + .take() + .expect("child did not have a handle to stdin"); + + let mut interval = time::interval(Duration::from_micros(1_000_000 / 30)); + loop { + let bytes = self.fb.as_bytes(); + stdin.write_all(bytes).await?; + interval.tick().await; + } + } + + fn ffmpeg_input_args(&self) -> Vec<(String, String)> { + let video_size: String = format!("{}x{}", self.fb.get_width(), self.fb.get_height()); + [ + ("f", "rawvideo"), + ("pixel_format", "rgb0"), + ("video_size", video_size.as_str()), + ("i", "-"), + ("f", "lavfi"), + ("i", "anullsrc=channel_layout=stereo:sample_rate=44100"), + ] + .map(|(s1, s2)| (s1.to_string(), s2.to_string())) + .into() + } + + fn ffmpeg_rtmp_sink_args(&self) -> Vec<(String, String)> { + [ + ("vcodec", "libx264"), + ("acodec", "aac"), + ("pix_fmt", "yuv420p"), + ("preset", "veryfast"), + ("r", self.fps.to_string().as_str()), + ("g", (self.fps * 2).to_string().as_str()), + ("ar", "44100"), + ("b:v", "4500k"), + ("b:a", "128k"), + ("threads", "4"), + // ("f", "flv"), + ] + .map(|(s1, s2)| (s1.to_string(), s2.to_string())) + .into() + } +} diff --git a/src/sinks/mod.rs b/src/sinks/mod.rs new file mode 100644 index 0000000..c4838e4 --- /dev/null +++ b/src/sinks/mod.rs @@ -0,0 +1,3 @@ +pub mod ffmpeg; +#[cfg(feature = "vnc")] +pub mod vnc; diff --git a/src/vnc.rs b/src/sinks/vnc.rs similarity index 51% rename from src/vnc.rs rename to src/sinks/vnc.rs index 2fc9072..cc6a351 100644 --- a/src/vnc.rs +++ b/src/sinks/vnc.rs @@ -1,37 +1,42 @@ +use crate::framebuffer::FrameBuffer; +use crate::statistics::{StatisticsEvent, StatisticsInformationEvent}; use core::slice; -use std::fs; -use std::sync::atomic::Ordering::{AcqRel, Acquire, SeqCst}; -use std::thread; -use std::time::{Duration, Instant}; - use number_prefix::NumberPrefix; use rusttype::{point, Font, Scale}; -use vncserver::*; - -use crate::framebuffer::FrameBuffer; -use crate::statistics::Statistics; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::broadcast; +use tokio::sync::mpsc::Sender; +use vncserver::{ + rfb_framebuffer_malloc, rfb_get_screen, rfb_init_server, rfb_mark_rect_as_modified, + rfb_run_event_loop, RfbScreenInfoPtr, +}; const STATS_HEIGHT: usize = 35; pub struct VncServer<'a> { - fb: &'a FrameBuffer, + fb: Arc, screen: RfbScreenInfoPtr, - fps: u32, - font: Font<'a>, + target_fps: u32, + + statistics_tx: Sender, + statistics_information_rx: broadcast::Receiver, + text: &'a str, - statistics: &'a Statistics, + font: Font<'a>, } impl<'a> VncServer<'a> { pub fn new( - fb: &'a FrameBuffer, + fb: Arc, port: u32, - fps: u32, + target_fps: u32, + statistics_tx: Sender, + statistics_information_rx: broadcast::Receiver, text: &'a str, - statistics: &'a Statistics, font: &'a str, ) -> Self { - let screen = rfb_get_screen(fb.width as i32, fb.height as i32, 8, 3, 4); + let screen = rfb_get_screen(fb.get_width() as i32, fb.get_height() as i32, 8, 3, 4); unsafe { // We need to set bitsPerPixel and depth to the correct values, // otherwise some VNC clients (like gstreamer) won't work @@ -44,22 +49,19 @@ impl<'a> VncServer<'a> { (*screen).ipv6port = port as i32; } - rfb_framebuffer_malloc( - screen, - (fb.width * fb.height * 4/* bytes per pixel */) as u64, - ); + rfb_framebuffer_malloc(screen, (fb.get_size() * 4/* bytes per pixel */) as u64); rfb_init_server(screen); rfb_run_event_loop(screen, 1, 1); let font = match font { // We ship our own copy of Arial.ttf, so that users don't need to download and provide it "Arial.ttf" => { - let font_bytes = include_bytes!("../Arial.ttf"); + let font_bytes = include_bytes!("../../Arial.ttf"); Font::try_from_bytes(font_bytes) .unwrap_or_else(|| panic!("Failed to construct Font from Arial.ttf")) } _ => { - let font_bytes = fs::read(font) + let font_bytes = std::fs::read(font) .unwrap_or_else(|err| panic!("Failed to read font file {font}: {err}")); Font::try_from_vec(font_bytes) .unwrap_or_else(|| panic!("Failed to construct Font from font file {font}")) @@ -69,95 +71,74 @@ impl<'a> VncServer<'a> { VncServer { fb, screen, - fps, - font, + target_fps, + statistics_tx, + statistics_information_rx, text, - statistics, + font, } } - pub fn run(&self) { - let desired_loop_time_ms = (1_000 / self.fps) as u128; + pub fn run(&mut self) { + let target_loop_duration = Duration::from_micros(1_000_000 / self.target_fps as u64); + + let fb = &self.fb; + let vnc_fb_slice: &mut [u32] = unsafe { + slice::from_raw_parts_mut((*self.screen).frameBuffer as *mut u32, fb.get_size()) + }; + let fb_slice = unsafe { &*fb.get_buffer() }; + // A line less because the (height - STATS_SURFACE_HEIGHT) belongs to the stats and gets refreshed by them + let height_up_to_stats_text = self.fb.get_height() - STATS_HEIGHT - 1; + let fb_size_up_to_stats_text = fb.get_width() * height_up_to_stats_text; + loop { - let start = Instant::now(); + let start = std::time::Instant::now(); + vnc_fb_slice[0..fb_size_up_to_stats_text] + .copy_from_slice(&fb_slice[0..fb_size_up_to_stats_text]); - for x in 0..self.fb.width { - for y in 0..(self.fb.height - STATS_HEIGHT) { - self.set_pixel_unchecked(x, y, self.fb.get(x, y)); - } - } // Only refresh the drawing surface, not the stats surface rfb_mark_rect_as_modified( self.screen, 0, 0, - self.fb.width as i32, - // A line less because the (height - STATS_SURFACE_HEIGHT) belongs to the stats and get refreshed by them - (self.fb.height - STATS_HEIGHT - 1) as i32, + self.fb.get_width() as i32, + height_up_to_stats_text as i32, ); - - // If the statistics thread has produced new stats it flags this for us so that we can re-draw the stats *once*. - // If we draw them every frame we get a flickering and produce unnecessary VNC updates. - if !self.statistics.stats_on_screen_are_up_to_date.load(SeqCst) { - self.statistics - .stats_on_screen_are_up_to_date - .store(true, SeqCst); - self.update_stats(); - } - - self.statistics.frame.fetch_add(1, AcqRel); - - let duration_ms = start.elapsed().as_millis(); - if duration_ms < desired_loop_time_ms { - thread::sleep(Duration::from_millis( - (desired_loop_time_ms - duration_ms) as u64, - )); + self.statistics_tx + .blocking_send(StatisticsEvent::FrameRendered) + .unwrap(); + + if !self.statistics_information_rx.is_empty() { + let statistics_information_event = + self.statistics_information_rx.try_recv().unwrap(); + self.display_stats(statistics_information_event); } - } - } - /// Don't check for bounds as input is assumed to be safe for performance reasons - fn set_pixel_unchecked(&self, x: usize, y: usize, rgba: u32) { - unsafe { - let addr = (*self.screen).frameBuffer as *mut u32; - let slice: &mut [u32] = slice::from_raw_parts_mut(addr, self.fb.width * self.fb.height); - slice[x + self.fb.width * y] = rgba; + std::thread::sleep(target_loop_duration.saturating_sub(start.elapsed())); } } - /// Check for bounds. If out of bound do nothing. - fn set_pixel_checked(&self, x: usize, y: usize, rgba: u32) { - if x < self.fb.width && y < self.fb.height { - unsafe { - let addr = (*self.screen).frameBuffer as *mut u32; - let slice: &mut [u32] = - slice::from_raw_parts_mut(addr, self.fb.width * self.fb.height); - slice[x + self.fb.width * y] = rgba; - } - } - } - - pub fn update_stats(&self) { + fn display_stats(&mut self, stats: StatisticsInformationEvent) { self.draw_rect( 0, - self.fb.height - STATS_HEIGHT, - self.fb.width, - self.fb.height, - 0x0000_0000, + self.fb.get_height() - STATS_HEIGHT, + self.fb.get_width(), + self.fb.get_height(), + 0, ); self.draw_text( 20, - self.fb.height - STATS_HEIGHT + 2, + self.fb.get_height() - STATS_HEIGHT + 2, 27_f32, 0x00ff_ffff, format!( "{}. {} Bit/s ({}B total) by {} connections from {} IPs ({} legacy)", self.text, - format_per_s(self.statistics.bytes_per_s.load(Acquire) as f64 * 8.0), - format(self.statistics.current_bytes.load(Acquire) as f64), - self.statistics.current_connections.load(Acquire), - self.statistics.current_ips.load(Acquire), - self.statistics.current_legacy_ips.load(Acquire), + format_per_s(stats.bytes_per_s as f64 * 8.0), + format(stats.bytes as f64), + stats.connections, + stats.ips, + stats.legacy_ips, ) .as_str(), ); @@ -166,13 +147,13 @@ impl<'a> VncServer<'a> { rfb_mark_rect_as_modified( self.screen, 0, - (self.fb.height - STATS_HEIGHT) as i32, - self.fb.width as i32, - self.fb.height as i32, + (self.fb.get_height() - STATS_HEIGHT) as i32, + self.fb.get_width() as i32, + self.fb.get_height() as i32, ); } - fn draw_text(&self, x: usize, y: usize, scale: f32, text_rgba: u32, text: &str) { + fn draw_text(&mut self, x: usize, y: usize, scale: f32, text_rgba: u32, text: &str) { let scale = Scale::uniform(scale); let v_metrics = self.font.v_metrics(scale); @@ -197,13 +178,24 @@ impl<'a> VncServer<'a> { } } - fn draw_rect(&self, start_x: usize, start_y: usize, end_x: usize, end_y: usize, rgba: u32) { + fn draw_rect(&mut self, start_x: usize, start_y: usize, end_x: usize, end_y: usize, rgba: u32) { for x in start_x..end_x { for y in start_y..end_y { self.set_pixel_checked(x, y, rgba); } } } + + /// Check for bounds. If out of bound do nothing. + fn set_pixel_checked(&mut self, x: usize, y: usize, rgba: u32) { + if x < self.fb.get_width() && y < self.fb.get_height() { + unsafe { + let addr = (*self.screen).frameBuffer as *mut u32; + let slice: &mut [u32] = slice::from_raw_parts_mut(addr, self.fb.get_size()); + slice[x + self.fb.get_width() * y] = rgba; + } + } + } } fn format_per_s(value: f64) -> String { diff --git a/src/statistics.rs b/src/statistics.rs index 32a8e9d..1984736 100644 --- a/src/statistics.rs +++ b/src/statistics.rs @@ -1,351 +1,192 @@ -use std::collections::HashMap; -use std::fs::File; -use std::net::IpAddr; -use std::sync::atomic::Ordering::{AcqRel, Acquire, Release}; -use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; -use std::sync::{Arc, Mutex}; -use std::thread; -use std::time::Duration; - -use prometheus::core::{AtomicI64, GenericGauge, GenericGaugeVec}; -use prometheus::{register_int_gauge, register_int_gauge_vec}; use serde::{Deserialize, Serialize}; +use simple_moving_average::{SingleSumSMA, SMA}; +use std::{ + cmp::max, + collections::{hash_map::Entry, HashMap}, + fs::File, + net::IpAddr, + time::{Duration, Instant}, +}; +use tokio::sync::{broadcast, mpsc::Receiver}; + +pub const STATS_REPORT_INTERVAL: Duration = Duration::from_millis(1000); +pub const STATS_SLIDING_WINDOW_SIZE: usize = 5; + +#[derive(Debug)] +pub enum StatisticsEvent { + ConnectionCreated { ip: IpAddr }, + ConnectionClosed { ip: IpAddr }, + BytesRead { ip: IpAddr, bytes: u64 }, + FrameRendered, +} -#[derive(Serialize)] -pub struct Statistics { - #[serde(skip)] - pub save_file: Option, - - // These statistics are always up to date - #[serde(skip)] - connections_for_ip: Mutex>, - bytes_for_ip: Mutex>, - pixels_for_ip: Mutex>, - #[serde(skip)] - pub frame: AtomicU64, - - // Whether the current stats have been printed to the screen. - // With this we can only draw the stats one - directly after updating them - and not every frame - // By doing so we avoid flickering stats and don't need to mark the rect as modified every frame besides nothing having changed - #[serde(skip)] - pub stats_on_screen_are_up_to_date: AtomicBool, - - // Variables to hold the statistics at the last time gathered - #[serde(skip)] - pub current_connections: AtomicU32, - #[serde(skip)] - pub current_ips: AtomicU32, - #[serde(skip)] - pub current_legacy_ips: AtomicU32, - #[serde(skip)] - pub current_bytes: AtomicU64, - #[serde(skip)] - pub current_pixels: AtomicU64, - #[serde(skip)] - pub current_frame: AtomicU64, - - #[serde(skip)] - pub bytes_per_s: AtomicU64, - #[serde(skip)] - pub pixels_per_s: AtomicU64, - #[serde(skip)] - pub fps: AtomicU64, - - // Prometheus metrics - #[serde(skip)] - metric_connections: GenericGaugeVec, - #[serde(skip)] - metric_ips: GenericGauge, - #[serde(skip)] - metric_legacy_ips: GenericGauge, - #[serde(skip)] - metric_bytes: GenericGaugeVec, - #[serde(skip)] - metric_pixels: GenericGaugeVec, - #[serde(skip)] - metric_fps: GenericGauge, +pub enum StatisticsSaveMode { + Disabled, + Enabled { save_file: String, interval_s: u64 }, } -impl Statistics { - pub fn new(save_file: Option<&str>) -> Self { - Statistics { - save_file: save_file.map(str::to_string), +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct StatisticsInformationEvent { + pub frame: u64, + pub connections: u32, + pub ips: u32, + pub legacy_ips: u32, + pub bytes: u64, + pub fps: u64, + pub bytes_per_s: u64, - connections_for_ip: Mutex::new(HashMap::new()), - bytes_for_ip: Mutex::new(HashMap::new()), - pixels_for_ip: Mutex::new(HashMap::new()), - frame: AtomicU64::new(0), + pub connections_for_ip: HashMap, + pub bytes_for_ip: HashMap, - stats_on_screen_are_up_to_date: AtomicBool::new(false), + pub statistic_events: u64, +} - current_connections: AtomicU32::new(0), - current_ips: AtomicU32::new(0), - current_legacy_ips: AtomicU32::new(0), - current_bytes: AtomicU64::new(0), - current_pixels: AtomicU64::new(0), - current_frame: AtomicU64::new(0), +pub struct Statistics { + statistics_rx: Receiver, + statistics_information_tx: broadcast::Sender, + statistic_events: u64, - bytes_per_s: AtomicU64::new(0), - pixels_per_s: AtomicU64::new(0), - fps: AtomicU64::new(0), + frame: u64, + connections_for_ip: HashMap, + bytes_for_ip: HashMap, - metric_connections: register_int_gauge_vec!( - "breakwater_connections", - "Number of client connections", - &["ip"] - ) - .unwrap(), - metric_ips: register_int_gauge!("breakwater_ips", "Number of IPs connected").unwrap(), - metric_legacy_ips: register_int_gauge!( - "breakwater_legacy_ips", - "Number of legacy (v4) IPs connected" - ) - .unwrap(), - metric_bytes: register_int_gauge_vec!( - "breakwater_bytes", - "Number of bytes received", - &["ip"] - ) - .unwrap(), - metric_pixels: register_int_gauge_vec!( - "breakwater_pixels", - "Number of Pixels set", - &["ip"] - ) - .unwrap(), - metric_fps: register_int_gauge!( - "breakwater_fps", - "Frames per second of the VNC server" - ) - .unwrap(), - } - } + bytes_per_s_window: SingleSumSMA, + fps_window: SingleSumSMA, - pub fn from_save_file_or_new(save_file: &str) -> Self { - let mut statistics = Statistics::new(Some(save_file)); + statistics_save_mode: StatisticsSaveMode, +} - if let Ok(save_point) = StatisticsSavePoint::load(save_file) { - statistics.bytes_for_ip = Mutex::new(save_point.bytes_for_ip); - statistics.pixels_for_ip = Mutex::new(save_point.pixels_for_ip); - } +impl StatisticsInformationEvent { + fn save_to_file(&self, file_name: &str) -> std::io::Result<()> { + // TODO Check if we can use tokio's File here. This needs some integration with serde_json though + // This operation is also called very infrequently + let file = File::create(file_name)?; + serde_json::to_writer(file, &self)?; - statistics + Ok(()) } - pub fn inc_connections(&self, ip: IpAddr) { - self.connections_for_ip - .lock() - .unwrap() - .entry(ip) - .or_insert(AtomicU32::new(0)) - .fetch_add(1, AcqRel); + fn load_from_file(file_name: &str) -> std::io::Result { + let file = File::open(file_name)?; + Ok(serde_json::from_reader(file)?) } +} - pub fn dec_connections(&self, ip: IpAddr) { - let mut connections_for_ip = self.connections_for_ip.lock().unwrap(); - match connections_for_ip.get(&ip) { - None => {} - Some(connections) => { - let previous_connections_for_ip = connections.fetch_sub(1, AcqRel); - if previous_connections_for_ip <= 1 { - connections_for_ip.remove(&ip); - self.metric_connections - .remove_label_values(&[&ip.to_string()]) - .ok(); - } +impl Statistics { + pub fn new( + statistics_rx: Receiver, + statistics_information_tx: broadcast::Sender, + statistics_save_mode: StatisticsSaveMode, + ) -> std::io::Result { + let mut statistics = Statistics { + statistics_rx, + statistics_information_tx, + statistic_events: 0, + frame: 0, + connections_for_ip: HashMap::new(), + bytes_for_ip: HashMap::new(), + bytes_per_s_window: SingleSumSMA::new(), + fps_window: SingleSumSMA::new(), + statistics_save_mode, + }; + + if let StatisticsSaveMode::Enabled { save_file, .. } = &statistics.statistics_save_mode { + if let Ok(save_point) = StatisticsInformationEvent::load_from_file(save_file) { + statistics.statistic_events = save_point.statistic_events; + statistics.frame = save_point.frame; + statistics.bytes_for_ip = save_point.bytes_for_ip; } } - } - - fn get_connections(&self) -> u32 { - self.connections_for_ip - .lock() - .unwrap() - .values() - .map(|i| i.load(Acquire)) - .sum() - } - - fn get_ip_count(&self) -> u32 { - self.connections_for_ip.lock().unwrap().len() as u32 - } - - fn get_ip_count_legacy(&self) -> u32 { - self.connections_for_ip - .lock() - .unwrap() - .keys() - .filter(|ip| ip.is_ipv4()) - .count() as u32 - } - - #[inline(always)] - pub fn inc_bytes(&self, ip: IpAddr, bytes: u64) { - self.bytes_for_ip - .lock() - .unwrap() - .entry(ip) - .or_insert(AtomicU64::new(0)) - .fetch_add(bytes, AcqRel); - } - - /// Expensive! - /// Should only be called when feature `count_pixels` is enabled - #[inline(always)] - pub fn inc_pixels(&self, ip: IpAddr) { - self.pixels_for_ip - .lock() - .unwrap() - .entry(ip) - .or_insert(AtomicU64::new(0)) - .fetch_add(1, AcqRel); - } - - fn get_bytes(&self) -> u64 { - self.bytes_for_ip - .lock() - .unwrap() - .values() - .map(|i| i.load(Acquire)) - .sum() - } - fn get_pixels(&self) -> u64 { - self.pixels_for_ip - .lock() - .unwrap() - .values() - .map(|i| i.load(Acquire)) - .sum() + Ok(statistics) } - fn update(&self) { - // Calculate statistics - self.current_connections - .store(self.get_connections(), Release); - self.current_ips.store(self.get_ip_count(), Release); - self.current_legacy_ips - .store(self.get_ip_count_legacy(), Release); - - let new_bytes = self.get_bytes(); - self.bytes_per_s - .store(new_bytes - self.current_bytes.load(Acquire), Release); - self.current_bytes.store(new_bytes, Release); - - if cfg!(not(feature = "count_pixels")) { - // Do a crude estimation if actual pixel count is not available. Average Pixel is about |PX XXX YYY rrggbb\n| = 18 bytes - self.bytes_for_ip - .lock() - .unwrap() - .iter() - .for_each(|(ip, bytes)| { - self.pixels_for_ip - .lock() - .unwrap() - .entry(*ip) - .or_insert(AtomicU64::new(0)) - .store(bytes.load(Acquire) / 18, Release) - }); - } - let new_pixels = self.get_pixels(); - self.pixels_per_s - .store(new_pixels - self.current_pixels.load(Acquire), Release); - self.current_pixels.store(new_pixels, Release); + pub async fn start(&mut self) -> std::io::Result<()> { + let mut last_stat_report = Instant::now(); + let mut last_save_file_written = Instant::now(); + let mut statistics_information_event = StatisticsInformationEvent::default(); - let new_frame = self.frame.load(Acquire); - self.fps - .store(new_frame - self.current_frame.load(Acquire), Release); - self.current_frame.store(new_frame, Release); - - // Put statistics into Prometheus metrics - self.connections_for_ip - .lock() - .unwrap() - .iter() - .for_each(|(ip, connections)| { - self.metric_connections - .with_label_values(&[&ip.to_string()]) - .set(connections.load(Acquire) as i64) - }); - self.metric_ips.set(self.current_ips.load(Acquire) as i64); - self.metric_legacy_ips - .set(self.current_legacy_ips.load(Acquire) as i64); - self.bytes_for_ip - .lock() - .unwrap() - .iter() - .for_each(|(ip, bytes)| { - self.metric_bytes - .with_label_values(&[&ip.to_string()]) - .set(bytes.load(Acquire) as i64) - }); - self.pixels_for_ip - .lock() - .unwrap() - .iter() - .for_each(|(ip, pixels)| { - self.metric_pixels - .with_label_values(&[&ip.to_string()]) - .set(pixels.load(Acquire) as i64) - }); - self.metric_fps.set(self.fps.load(Acquire) as i64); - - // Force re-draw of stats on screen - self.stats_on_screen_are_up_to_date - .store(false, Ordering::SeqCst); - } + while let Some(statistics_update) = self.statistics_rx.recv().await { + self.statistic_events += 1; + match statistics_update { + StatisticsEvent::ConnectionCreated { ip } => { + *self.connections_for_ip.entry(ip).or_insert(0) += 1; + } + StatisticsEvent::ConnectionClosed { ip } => { + if let Entry::Occupied(mut o) = self.connections_for_ip.entry(ip) { + let connections = o.get_mut(); + *connections -= 1; + if *connections == 0 { + o.remove_entry(); + } + } + } + StatisticsEvent::BytesRead { ip, bytes } => { + *self.bytes_for_ip.entry(ip).or_insert(0) += bytes; + } + StatisticsEvent::FrameRendered => self.frame += 1, + } - /// Saves the statistics to the save-file if enabled - pub fn save_to_save_file(&self) { - if let Some(save_file) = &self.save_file { - match File::create(save_file) { - Ok(file) => match serde_json::to_writer(file, &self) { - Ok(()) => (), - Err(err) => { - println!("Failed to write to statistics save file {save_file}: {err}") + // As there is an event for every frame we are guaranteed to land here every second + let last_stat_report_elapsed = last_stat_report.elapsed(); + if last_stat_report_elapsed > STATS_REPORT_INTERVAL { + last_stat_report = Instant::now(); + statistics_information_event = self.calculate_statistics_information_event( + &statistics_information_event, + last_stat_report_elapsed, + ); + self.statistics_information_tx + .send(statistics_information_event.clone()) + .expect("Statistics information channel full (or disconnected)"); + + if let StatisticsSaveMode::Enabled { + save_file, + interval_s, + } = &self.statistics_save_mode + { + if last_save_file_written.elapsed() > Duration::from_secs(*interval_s) { + last_save_file_written = Instant::now(); + statistics_information_event.save_to_file(save_file)?; } - }, - Err(err) => println!("Failed to create statistics save file {save_file}: {err}"), + } } } - } -} -#[derive(Serialize, Deserialize)] -pub struct StatisticsSavePoint { - pub bytes_for_ip: HashMap, - pub pixels_for_ip: HashMap, -} - -impl StatisticsSavePoint { - pub fn load(save_file: &str) -> std::io::Result { - let file = File::open(save_file)?; - Ok(serde_json::from_reader(file)?) + Ok(()) } -} - -pub fn start_loop(statistics: Arc, save_interval_s: u64) { - let statistics_1 = Arc::clone(&statistics); - thread::spawn(move || loop { - thread::sleep(Duration::from_secs(1)); - statistics_1.update(); - }); - if statistics.save_file.is_some() { - thread::spawn(move || loop { - thread::sleep(Duration::from_secs(save_interval_s)); - statistics.save_to_save_file(); - }); + fn calculate_statistics_information_event( + &mut self, + prev: &StatisticsInformationEvent, + elapsed: Duration, + ) -> StatisticsInformationEvent { + let elapsed_ms = max(1, elapsed.as_millis()) as u64; + let frame = self.frame; + let connections = self.connections_for_ip.values().sum(); + let ips = self.connections_for_ip.len() as u32; + let legacy_ips = self + .connections_for_ip + .keys() + .filter(|ip| ip.is_ipv4()) + .count() as u32; + let bytes = self.bytes_for_ip.values().sum(); + self.bytes_per_s_window + .add_sample((bytes - prev.bytes) * 1000 / elapsed_ms); + self.fps_window + .add_sample((frame - prev.frame) * 1000 / elapsed_ms); + let statistic_events = self.statistic_events; + + StatisticsInformationEvent { + frame, + connections, + ips, + legacy_ips, + bytes, + fps: self.fps_window.get_average(), + bytes_per_s: self.bytes_per_s_window.get_average(), + connections_for_ip: self.connections_for_ip.clone(), + bytes_for_ip: self.bytes_for_ip.clone(), + statistic_events, + } } } - -pub fn start_prometheus_server(prometheus_listen_address: &str) { - prometheus_exporter::start(prometheus_listen_address.parse().unwrap_or_else(|_| { - panic!( - "Failed to parse prometheus listen address: {}", - prometheus_listen_address - ) - })) - .expect("Failed to start prometheus exporter"); - println!("Started Prometheus Exporter on {prometheus_listen_address}"); -} diff --git a/src/test/helpers/dev_null_tcp_stream.rs b/src/test/helpers/dev_null_tcp_stream.rs new file mode 100644 index 0000000..9f1dbe3 --- /dev/null +++ b/src/test/helpers/dev_null_tcp_stream.rs @@ -0,0 +1,40 @@ +use std::{io::Write, task::Poll}; + +use tokio::io::AsyncWrite; + +#[derive(Debug, Default)] +pub struct DevNullTcpStream {} + +impl Write for DevNullTcpStream { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +impl AsyncWrite for DevNullTcpStream { + fn poll_write( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + Poll::Ready(Ok(buf.len())) + } + + fn poll_flush( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } +} diff --git a/src/test/helpers/mock_tcp_stream.rs b/src/test/helpers/mock_tcp_stream.rs new file mode 100644 index 0000000..cbf06ee9 --- /dev/null +++ b/src/test/helpers/mock_tcp_stream.rs @@ -0,0 +1,85 @@ +use std::{ + cmp::min, + io::{Read, Write}, + task::Poll, +}; + +use tokio::io::{AsyncRead, AsyncWrite}; + +#[derive(Debug, Default)] +pub struct MockTcpStream { + read_data: Vec, + write_data: Vec, +} + +impl MockTcpStream { + pub fn from_input(input: &str) -> Self { + MockTcpStream { + read_data: input.as_bytes().to_vec(), + write_data: Vec::new(), + } + } + + pub fn get_output(self) -> String { + String::from_utf8(self.write_data).unwrap() + } +} + +impl Read for MockTcpStream { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let size: usize = min(self.read_data.len(), buf.len()); + buf[..size].copy_from_slice(&self.read_data[..size]); + + self.read_data.drain(..size); + Ok(size) + } +} + +impl Write for MockTcpStream { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.write_data.extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +impl AsyncRead for MockTcpStream { + fn poll_read( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + let size: usize = min(self.read_data.len(), buf.remaining()); + buf.put_slice(&self.read_data[..size]); + self.get_mut().read_data.drain(..size); + std::task::Poll::Ready(Ok(())) + } +} + +impl AsyncWrite for MockTcpStream { + fn poll_write( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + self.get_mut().write_data.extend_from_slice(buf); + Poll::Ready(Ok(buf.len())) + } + + fn poll_flush( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } +} diff --git a/src/test/helpers/mod.rs b/src/test/helpers/mod.rs new file mode 100644 index 0000000..8f68a04 --- /dev/null +++ b/src/test/helpers/mod.rs @@ -0,0 +1,7 @@ +mod dev_null_tcp_stream; +mod mock_tcp_stream; +mod pixelflut_commands; + +pub use dev_null_tcp_stream::*; +pub use mock_tcp_stream::*; +pub use pixelflut_commands::*; diff --git a/src/test/helpers/pixelflut_commands.rs b/src/test/helpers/pixelflut_commands.rs new file mode 100644 index 0000000..cb54bd8 --- /dev/null +++ b/src/test/helpers/pixelflut_commands.rs @@ -0,0 +1,23 @@ +pub fn get_commands_to_draw_rect(width: usize, height: usize, color: u32) -> String { + let mut draw_commands = String::new(); + + for x in 0..width { + for y in 0..height { + draw_commands += &format!("PX {x} {y} {color:06x}\n"); + } + } + + draw_commands +} + +pub fn get_commands_to_read_rect(width: usize, height: usize) -> String { + let mut read_commands = String::new(); + + for x in 0..width { + for y in 0..height { + read_commands += &format!("PX {x} {y}\n"); + } + } + + read_commands +} diff --git a/src/test/mod.rs b/src/test/mod.rs new file mode 100644 index 0000000..1630fab --- /dev/null +++ b/src/test/mod.rs @@ -0,0 +1 @@ +pub mod helpers; diff --git a/tests/common/mod.rs b/tests/common/mod.rs deleted file mode 100644 index ab40597..0000000 --- a/tests/common/mod.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::{ - cmp::min, - io::{Read, Write}, -}; - -#[derive(Debug)] -pub struct MockTcpStream { - read_data: Vec, - write_data: Vec, -} - -impl MockTcpStream { - pub fn new() -> Self { - MockTcpStream { - read_data: Vec::new(), - write_data: Vec::new(), - } - } - - pub fn from_input(input: &str) -> Self { - MockTcpStream { - read_data: input.as_bytes().to_vec(), - write_data: Vec::new(), - } - } - - pub fn get_output(self) -> String { - String::from_utf8(self.write_data).unwrap() - } -} - -impl Default for MockTcpStream { - fn default() -> Self { - Self::new() - } -} - -impl Read for MockTcpStream { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - let size: usize = min(self.read_data.len(), buf.len()); - buf[..size].copy_from_slice(&self.read_data[..size]); - - self.read_data.drain(..size); - Ok(size) - } -} - -impl Write for MockTcpStream { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - self.write_data.extend_from_slice(buf); - Ok(buf.len()) - } - - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } -} diff --git a/tests/drawing.rs b/tests/drawing.rs deleted file mode 100644 index faeea3d..0000000 --- a/tests/drawing.rs +++ /dev/null @@ -1,174 +0,0 @@ -mod common; - -use breakwater::framebuffer::FrameBuffer; -use breakwater::network; -use breakwater::statistics::Statistics; -use common::MockTcpStream; -use lazy_static::lazy_static; -use rstest::{fixture, rstest}; -use std::net::{IpAddr, Ipv4Addr}; -use std::str; -use std::string::String; -use std::sync::Arc; - -lazy_static! { - pub static ref STATISTICS: Arc = Arc::new(Statistics::new(None)); -} - -#[fixture] -fn fb() -> Arc { - Arc::new(FrameBuffer::new(1920, 1080)) -} - -#[fixture] -fn statistics() -> Arc { - // We need a single statistics object as otherwise it tries to register the same Prometheus metric multiple times - Arc::clone(&STATISTICS) -} - -#[fixture] -fn ip() -> IpAddr { - IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)) -} - -#[rstest] -#[case("", "")] -#[case("\n", "")] -#[case("not a pixelflut command", "")] -#[case("not a pixelflut command with newline\n", "")] -#[case("SIZE", "SIZE 1920 1080\n")] -#[case("SIZE\n", "SIZE 1920 1080\n")] -#[case("SIZE\nSIZE\n", "SIZE 1920 1080\nSIZE 1920 1080\n")] -#[case("SIZE", "SIZE 1920 1080\n")] -#[case("HELP", str::from_utf8(breakwater::network::HELP_TEXT).unwrap())] -#[case("HELP\n", str::from_utf8(breakwater::network::HELP_TEXT).unwrap())] -#[case("bla bla bla\nSIZE\nblub\nbla", "SIZE 1920 1080\n")] -fn test_correct_responses_to_general_commands( - #[case] input: &str, - #[case] expected: &str, - ip: IpAddr, - fb: Arc, - statistics: Arc, -) { - let mut stream = MockTcpStream::from_input(input); - network::handle_connection(&mut stream, ip, fb, statistics); - - assert_eq!(expected, stream.get_output()); -} - -#[rstest] -// Without alpha -#[case("PX 0 0 ffffff\nPX 0 0\n", "PX 0 0 ffffff\n")] -#[case("PX 0 0 abcdef\nPX 0 0\n", "PX 0 0 abcdef\n")] -#[case("PX 0 42 abcdef\nPX 0 42\n", "PX 0 42 abcdef\n")] -#[case("PX 42 0 abcdef\nPX 42 0\n", "PX 42 0 abcdef\n")] -// With alpha -// TODO: At the moment alpha channel is not supported and silently ignored (pixels are painted with 0% transparency) -#[case("PX 0 0 ffffffaa\nPX 0 0\n", "PX 0 0 ffffff\n")] -#[case("PX 0 0 abcdefaa\nPX 0 0\n", "PX 0 0 abcdef\n")] -#[case("PX 0 1 abcdefaa\nPX 0 1\n", "PX 0 1 abcdef\n")] -#[case("PX 1 0 abcdefaa\nPX 1 0\n", "PX 1 0 abcdef\n")] -// Tests invalid bounds -#[case("PX 9999 0 abcdef\nPX 9999 0\n", "")] // Parsable but outside screen size -#[case("PX 0 9999 abcdef\nPX 9999 0\n", "")] -#[case("PX 9999 9999 abcdef\nPX 9999 9999\n", "")] -#[case("PX 99999 0 abcdef\nPX 0 99999\n", "")] // Not even parsable because to many digits -#[case("PX 0 99999 abcdef\nPX 0 99999\n", "")] -#[case("PX 99999 99999 abcdef\nPX 99999 99999\n", "")] -// Test invalid inputs -#[case("PX 0 abcdef\nPX 0 0\n", "PX 0 0 000000\n")] -#[case("PX 0 1 2 abcdef\nPX 0 0\n", "PX 0 0 000000\n")] -#[case("PX -1 0 abcdef\nPX 0 0\n", "PX 0 0 000000\n")] -#[case("bla bla bla\nPX 0 0\n", "PX 0 0 000000\n")] -// Test offset -#[case( - "OFFSET 10 10\nPX 0 0 ffffff\nPX 0 0\nPX 42 42\n", - "PX 0 0 ffffff\nPX 42 42 000000\n" -)] // The get pixel result is also offseted -#[case("OFFSET 0 0\nPX 0 42 abcdef\nPX 0 42\n", "PX 0 42 abcdef\n")] -fn test_setting_pixel( - #[case] input: &str, - #[case] expected: &str, - ip: IpAddr, - fb: Arc, - statistics: Arc, -) { - let mut stream = MockTcpStream::from_input(input); - network::handle_connection(&mut stream, ip, fb, statistics); - - assert_eq!(expected, stream.get_output()); -} - -#[rstest] -#[case(5, 5, 0, 0)] -#[case(6, 6, 0, 0)] -#[case(7, 7, 0, 0)] -#[case(8, 8, 0, 0)] -#[case(9, 9, 0, 0)] -#[case(10, 10, 0, 0)] -#[case(10, 10, 100, 200)] -#[case(10, 10, 510, 520)] -#[case(100, 100, 0, 0)] -#[case(100, 100, 300, 400)] -#[case(479, 361, 721, 391)] -#[case(500, 500, 0, 0)] -#[case(500, 500, 300, 400)] -#[case(fb().width, fb().height, 0, 0)] -#[case(fb().width - 1, fb().height - 1, 1, 1)] -fn test_drawing_rect( - #[case] width: usize, - #[case] height: usize, - #[case] offset_x: usize, - #[case] offset_y: usize, - ip: IpAddr, - fb: Arc, - statistics: Arc, -) { - let mut color: u32 = 0; - let mut fill_commands = String::new(); - let mut read_commands = String::new(); - let mut combined_commands = String::new(); - let mut combined_commands_expected = String::new(); - let mut read_other_pixels_commands = String::new(); - let mut read_other_pixels_commands_expected = String::new(); - - for x in 0..fb.width { - for y in 0..height { - // Inside the rect - if x >= offset_x && x <= offset_x + width && y >= offset_y && y <= offset_y + height { - fill_commands += &format!("PX {x} {y} {color:06x}\n"); - read_commands += &format!("PX {x} {y}\n"); - - color += 1; // Use another color for the next test case - combined_commands += &format!("PX {x} {y} {color:06x}\nPX {x} {y}\n"); - combined_commands_expected += &format!("PX {x} {y} {color:06x}\n"); - - color += 1; - } else { - // Non touched pixels must remain black - read_other_pixels_commands += &format!("PX {x} {y}\n"); - read_other_pixels_commands_expected += &format!("PX {x} {y} 000000\n"); - } - } - } - - // Color the pixels - let mut stream = MockTcpStream::from_input(&fill_commands); - network::handle_connection(&mut stream, ip, Arc::clone(&fb), Arc::clone(&statistics)); - assert_eq!("", stream.get_output()); - - // Read the pixels again - let mut stream = MockTcpStream::from_input(&read_commands); - network::handle_connection(&mut stream, ip, Arc::clone(&fb), Arc::clone(&statistics)); - assert_eq!(fill_commands, stream.get_output()); - - // We can also do coloring and reading in a single connection - let mut stream = MockTcpStream::from_input(&combined_commands); - network::handle_connection(&mut stream, ip, Arc::clone(&fb), Arc::clone(&statistics)); - assert_eq!(combined_commands_expected, stream.get_output()); - - // Check that nothing else was colored - let mut stream = MockTcpStream::from_input(&read_other_pixels_commands); - network::handle_connection(&mut stream, ip, Arc::clone(&fb), Arc::clone(&statistics)); - assert_eq!(read_other_pixels_commands_expected, stream.get_output()); -}