refactor: major library changes #5
26 changed files with 1745 additions and 2189 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
|||
/target
|
||||
.vscode/
|
||||
535
Cargo.lock
generated
535
Cargo.lock
generated
|
|
@ -2,12 +2,158 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"ciborium-ll",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-io"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-ll"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"half",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
|
||||
dependencies = [
|
||||
"anes",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"is-terminal",
|
||||
"itertools",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"oorandom",
|
||||
"plotters",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"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-deque"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.12"
|
||||
|
|
@ -23,6 +169,18 @@ version = "0.8.21"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
|
|
@ -34,22 +192,399 @@ dependencies = [
|
|||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"plotters-backend",
|
||||
"plotters-svg",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plotters-backend"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
|
||||
|
||||
[[package]]
|
||||
name = "plotters-svg"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
|
||||
dependencies = [
|
||||
"plotters-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[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 = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swactor"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"crossbeam-queue",
|
||||
"crossbeam-utils",
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[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 = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
|
||||
|
|
|
|||
10
Cargo.toml
10
Cargo.toml
|
|
@ -16,7 +16,11 @@ stress = [] # Enable stress tests
|
|||
[dependencies]
|
||||
getrandom = { version = "0.2", optional = true }
|
||||
crossbeam-queue = "0.3.12"
|
||||
crossbeam-utils = "0.8.21"
|
||||
|
||||
[[bin]]
|
||||
name = "bench"
|
||||
path = "benches/main.rs"
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
|
||||
[[bench]]
|
||||
name = "runtime_benchmarks"
|
||||
harness = false
|
||||
|
|
|
|||
77
DESIGN.md
77
DESIGN.md
|
|
@ -1,77 +0,0 @@
|
|||
# Design goals
|
||||
Get as much usability and speed as possible while keeping line count low. Aim for no footguns, ability to plug in
|
||||
logic easily, and run near anywhere. We may make this a `![no_std]` library, but the MVP will use the
|
||||
memory allocator and threading provided by the rust standard library.
|
||||
|
||||
We are not building a new erlang/BEAM. Minimal feature set means spawning actor processes, not having supervisiors, lots of process
|
||||
monitoring tools, prempting, etc.
|
||||
|
||||
## Actor model
|
||||
|
||||
An actor has:
|
||||
|
||||
- An inbox:
|
||||
this is a mpsc channel that the runtime/router dumps messages into and the actor consumes when the runtime loads it
|
||||
Implemented as a barebones atomic ring buffer. The router is responsible for inserting messages.
|
||||
|
||||
- an outbox channel connection:
|
||||
this is a mpmc channel that is implemented by the runtime and router. Actors on this specific channel put responses and outgoing messages into this channel, to be routed to the given address.
|
||||
|
||||
- a growable and mutable state:
|
||||
An actor owns some, from the runtime perspective, type erased bytes. The actor when processing messages can access its own state, but no other task can. This includes viewing.
|
||||
|
||||
- a set of functions for processing messages:
|
||||
When the runtime loads the actor, it locks the inbox and attempts to process the messages therein.
|
||||
|
||||
## Runtime
|
||||
|
||||
In order for an actor to consume and send messages, it is processed by a runtime. The runtime, in order to negotiate messages between
|
||||
actors, possesses a router.
|
||||
|
||||
A runtime has:
|
||||
- An actor processing thread(s):
|
||||
the processor will mark an actor as busy, load its state and inbox, and begin consuming messages from the inbox. The number of messages consumed is determined by the runtime. A good start is a backpressure strategy: after loading, process messages until mailbox is empty or size drops below a threshold (e.g., "drain to 50%").
|
||||
|
||||
- A message router:
|
||||
the router is responsible for ensuring messages posted by actors get delivered to the appropriate inbox.
|
||||
|
||||
- An atomic ring buffer containing thread-safe references to actors that are not currently loaded. Actors are popped off the buffer, messages are
|
||||
processed, and the reference is returned to the buffer/queue before the next actor is loaded.
|
||||
|
||||
## Router
|
||||
|
||||
The router is the engine for message delivery. It posesses:
|
||||
|
||||
- An actor address book:
|
||||
The address book maps actor ids to `Sender` references that can be used to deliver messages to the actor inbox.
|
||||
|
||||
- Its own inbox:
|
||||
The router possesses its own mpsc queue where references to messages are stored. The router will process this queue by dereferencing and writing directly into the recipient's inbox buffer.
|
||||
|
||||
### Misc
|
||||
|
||||
A means of providing an emergency overflow without adding much more code complexity. The mutex means
|
||||
this will not be `no_std` however.
|
||||
|
||||
```rust
|
||||
struct HybridChannel<T> {
|
||||
// Start with lock-free ring buffer
|
||||
ring: AtomicRingBuffer<T>,
|
||||
|
||||
// When full, spill into a Mutex<VecDeque<T>>
|
||||
overflow: parking_lot::Mutex<VecDeque<T>>,
|
||||
|
||||
// Track overflow frequency to resize ring proactively
|
||||
overflow_count: AtomicUsize,
|
||||
}
|
||||
|
||||
impl<T> HybridChannel<T> {
|
||||
fn push(&self, value: T) {
|
||||
if self.ring.push(value).is_err() {
|
||||
self.overflow.lock().push_back(value);
|
||||
self.overflow_count.fetch_add(1, Relaxed);
|
||||
// Optionally: if overflow_count > threshold, grow ring
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
59
README.md
59
README.md
|
|
@ -1,2 +1,59 @@
|
|||
# swactor
|
||||
Small wasm-compatible actor library
|
||||
(S)mall (W)ASM-compatible (actor) library
|
||||
|
||||
## Quick example
|
||||
|
||||
```rust
|
||||
use swactor::{
|
||||
Ctx,
|
||||
actor::{ActorAddress, ActorInterface},
|
||||
runtime::{Runtime, RuntimeConfig},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct Greeter { num_greeted: usize }
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct GreetMessage { who: String, return_addr: ActorAddress }
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct GreetResponse(String);
|
||||
|
||||
impl ActorInterface for Greeter {
|
||||
type Incoming = GreetMessage;
|
||||
type Response = GreetResponse;
|
||||
|
||||
fn handle(&mut self, ctx: &Ctx, msg: GreetMessage) {
|
||||
let res = GreetResponse(format!("Hello, {}!", msg.who));
|
||||
self.num_greeted += 1;
|
||||
if let Err(_) = ctx.send(msg.return_addr, res) {
|
||||
self.num_greeted -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
let addr = rt.spawn(Greeter::default()).expect("failed to spawn");
|
||||
|
||||
let inbox = rt.new_inbox::<GreetResponse>().unwrap();
|
||||
rt.send_to(addr, GreetMessage {
|
||||
who: "world".into(),
|
||||
return_addr: *inbox.addr(),
|
||||
}).unwrap();
|
||||
|
||||
for _ in 0..3 { rt.tick(); }
|
||||
let resp = inbox.try_recv().expect("should have response");
|
||||
println!("{}", resp.0); // "Hello, world!"
|
||||
}
|
||||
```
|
||||
|
||||
## Build & test
|
||||
|
||||
```sh
|
||||
cargo build
|
||||
cargo test
|
||||
cargo test --features stress # stress tests
|
||||
cargo run --bin bench --release # benchmarks
|
||||
cargo run --example hello
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,270 +0,0 @@
|
|||
//! Manual benchmark harness - zero dependencies, full control.
|
||||
//!
|
||||
//! Provides statistical analysis of benchmark runs including:
|
||||
//! - Mean, median, min, max
|
||||
//! - Standard deviation
|
||||
//! - Percentiles (P50, P90, P99, P99.9)
|
||||
//! - Throughput calculations
|
||||
//! - Outlier detection and removal
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Results from a single benchmark run
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BenchResult {
|
||||
pub name: String,
|
||||
pub iterations: usize,
|
||||
pub total_time: Duration,
|
||||
pub times: Vec<Duration>,
|
||||
/// Optional: elements processed (for throughput calculation)
|
||||
pub elements: Option<u64>,
|
||||
}
|
||||
|
||||
/// Statistical summary of benchmark results
|
||||
#[derive(Debug)]
|
||||
pub struct Stats {
|
||||
pub mean: Duration,
|
||||
pub median: Duration,
|
||||
pub min: Duration,
|
||||
pub max: Duration,
|
||||
pub std_dev: Duration,
|
||||
pub p50: Duration,
|
||||
pub p90: Duration,
|
||||
pub p99: Duration,
|
||||
pub p999: Duration,
|
||||
pub throughput: Option<f64>, // elements per second
|
||||
}
|
||||
|
||||
impl BenchResult {
|
||||
/// Calculate statistics from the raw timing data
|
||||
pub fn stats(&self) -> Stats {
|
||||
let mut sorted: Vec<Duration> = self.times.clone();
|
||||
sorted.sort();
|
||||
|
||||
let n = sorted.len();
|
||||
assert!(n > 0, "Cannot compute stats on empty results");
|
||||
|
||||
let sum: Duration = sorted.iter().sum();
|
||||
let mean = sum / n as u32;
|
||||
|
||||
let median = if n % 2 == 0 {
|
||||
(sorted[n / 2 - 1] + sorted[n / 2]) / 2
|
||||
} else {
|
||||
sorted[n / 2]
|
||||
};
|
||||
|
||||
// Standard deviation
|
||||
let mean_nanos = mean.as_nanos() as f64;
|
||||
let variance: f64 = sorted
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let diff = t.as_nanos() as f64 - mean_nanos;
|
||||
diff * diff
|
||||
})
|
||||
.sum::<f64>()
|
||||
/ n as f64;
|
||||
let std_dev = Duration::from_nanos(variance.sqrt() as u64);
|
||||
|
||||
// Percentiles
|
||||
let percentile = |p: f64| -> Duration {
|
||||
let idx = ((p / 100.0) * (n - 1) as f64).round() as usize;
|
||||
sorted[idx.min(n - 1)]
|
||||
};
|
||||
|
||||
let throughput = self.elements.map(|e| {
|
||||
let secs = self.total_time.as_secs_f64();
|
||||
if secs > 0.0 {
|
||||
(e * self.iterations as u64) as f64 / secs
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
});
|
||||
|
||||
Stats {
|
||||
mean,
|
||||
median,
|
||||
min: sorted[0],
|
||||
max: sorted[n - 1],
|
||||
std_dev,
|
||||
p50: percentile(50.0),
|
||||
p90: percentile(90.0),
|
||||
p99: percentile(99.0),
|
||||
p999: percentile(99.9),
|
||||
throughput,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pretty print the results
|
||||
pub fn print(&self) {
|
||||
let stats = self.stats();
|
||||
|
||||
println!("\n{}", "=".repeat(60));
|
||||
println!(" {}", self.name);
|
||||
println!("{}", "=".repeat(60));
|
||||
println!(" Iterations: {}", self.iterations);
|
||||
println!(" Total time: {:?}", self.total_time);
|
||||
println!();
|
||||
println!(" Mean: {:?}", stats.mean);
|
||||
println!(" Median: {:?}", stats.median);
|
||||
println!(" Std Dev: {:?}", stats.std_dev);
|
||||
println!(" Min: {:?}", stats.min);
|
||||
println!(" Max: {:?}", stats.max);
|
||||
println!();
|
||||
println!(" P50: {:?}", stats.p50);
|
||||
println!(" P90: {:?}", stats.p90);
|
||||
println!(" P99: {:?}", stats.p99);
|
||||
println!(" P99.9: {:?}", stats.p999);
|
||||
|
||||
if let Some(throughput) = stats.throughput {
|
||||
println!();
|
||||
println!(" Throughput: {:.2} ops/sec", throughput);
|
||||
if throughput > 1_000_000.0 {
|
||||
println!(" {:.2} M ops/sec", throughput / 1_000_000.0);
|
||||
} else if throughput > 1_000.0 {
|
||||
println!(" {:.2} K ops/sec", throughput / 1_000.0);
|
||||
}
|
||||
}
|
||||
println!("{}", "=".repeat(60));
|
||||
}
|
||||
}
|
||||
|
||||
/// A benchmark builder for configuring and running benchmarks
|
||||
pub struct Bench {
|
||||
name: String,
|
||||
warmup_iters: usize,
|
||||
bench_iters: usize,
|
||||
elements_per_iter: Option<u64>,
|
||||
}
|
||||
|
||||
impl Bench {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
warmup_iters: 3,
|
||||
bench_iters: 100,
|
||||
elements_per_iter: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set number of warmup iterations (default: 3)
|
||||
pub fn warmup(mut self, n: usize) -> Self {
|
||||
self.warmup_iters = n;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set number of benchmark iterations (default: 100)
|
||||
pub fn iters(mut self, n: usize) -> Self {
|
||||
self.bench_iters = n;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set elements per iteration for throughput calculation
|
||||
pub fn elements(mut self, n: u64) -> Self {
|
||||
self.elements_per_iter = Some(n);
|
||||
self
|
||||
}
|
||||
|
||||
/// Run the benchmark with setup before each iteration
|
||||
pub fn run_with_setup<S, T, F>(self, mut setup: S, mut f: F) -> BenchResult
|
||||
where
|
||||
S: FnMut() -> T,
|
||||
F: FnMut(T),
|
||||
{
|
||||
// Warmup
|
||||
for _ in 0..self.warmup_iters {
|
||||
let state = setup();
|
||||
f(state);
|
||||
}
|
||||
|
||||
// Benchmark
|
||||
let mut times = Vec::with_capacity(self.bench_iters);
|
||||
let total_start = Instant::now();
|
||||
|
||||
for _ in 0..self.bench_iters {
|
||||
let state = setup();
|
||||
let start = Instant::now();
|
||||
f(state);
|
||||
times.push(start.elapsed());
|
||||
}
|
||||
|
||||
let total_time = total_start.elapsed();
|
||||
|
||||
BenchResult {
|
||||
name: self.name,
|
||||
iterations: self.bench_iters,
|
||||
total_time,
|
||||
times,
|
||||
elements: self.elements_per_iter,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of benchmarks to run together
|
||||
pub struct BenchSuite {
|
||||
name: String,
|
||||
results: Vec<BenchResult>,
|
||||
}
|
||||
|
||||
impl BenchSuite {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
results: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, result: BenchResult) {
|
||||
self.results.push(result);
|
||||
}
|
||||
|
||||
pub fn print_summary(&self) {
|
||||
println!("\n{}", "#".repeat(70));
|
||||
println!("# BENCHMARK SUITE: {}", self.name);
|
||||
println!("{}", "#".repeat(70));
|
||||
|
||||
for result in &self.results {
|
||||
result.print();
|
||||
}
|
||||
|
||||
// Summary table
|
||||
println!("\n{}", "-".repeat(70));
|
||||
println!(" SUMMARY");
|
||||
println!("{}", "-".repeat(70));
|
||||
println!(
|
||||
" {:30} {:>12} {:>12} {:>12}",
|
||||
"Benchmark", "Mean", "P99", "Throughput"
|
||||
);
|
||||
println!("{}", "-".repeat(70));
|
||||
|
||||
for result in &self.results {
|
||||
let stats = result.stats();
|
||||
let throughput_str = stats
|
||||
.throughput
|
||||
.map(|t| {
|
||||
if t > 1_000_000.0 {
|
||||
format!("{:.2}M/s", t / 1_000_000.0)
|
||||
} else if t > 1_000.0 {
|
||||
format!("{:.2}K/s", t / 1_000.0)
|
||||
} else {
|
||||
format!("{:.2}/s", t)
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
|
||||
println!(
|
||||
" {:30} {:>12.2?} {:>12.2?} {:>12}",
|
||||
result.name, stats.mean, stats.p99, throughput_str
|
||||
);
|
||||
}
|
||||
println!("{}", "-".repeat(70));
|
||||
}
|
||||
}
|
||||
|
||||
/// Prevent the compiler from optimizing away a value
|
||||
#[inline(never)]
|
||||
pub fn black_box<T>(x: T) -> T {
|
||||
// Use inline assembly to prevent optimization
|
||||
// This is a simplified version - in practice, reads from the value
|
||||
let ptr = &x as *const T;
|
||||
unsafe { std::ptr::read_volatile(ptr) }
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
//! Swactor Benchmark Suite
|
||||
//!
|
||||
//! A manual benchmark harness for measuring runtime performance.
|
||||
//! Zero external dependencies - just std::time.
|
||||
//!
|
||||
//! Run with: cargo run --bin bench --release
|
||||
//!
|
||||
//! Options:
|
||||
//! --throughput Run throughput benchmarks only
|
||||
//! --scaling Run scaling benchmarks only
|
||||
//! --all Run all benchmarks (default)
|
||||
|
||||
mod harness;
|
||||
mod throughput;
|
||||
mod scaling;
|
||||
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
println!("============================================================");
|
||||
println!(" SWACTOR BENCHMARK SUITE");
|
||||
println!("============================================================");
|
||||
println!();
|
||||
|
||||
// Parse arguments
|
||||
let run_throughput = args.contains(&"--throughput".to_string())
|
||||
|| args.contains(&"--all".to_string())
|
||||
|| args.len() == 1;
|
||||
let run_scaling = args.contains(&"--scaling".to_string())
|
||||
|| args.contains(&"--all".to_string())
|
||||
|| args.len() == 1;
|
||||
|
||||
if run_throughput {
|
||||
let suite = throughput::run_all();
|
||||
suite.print_summary();
|
||||
}
|
||||
|
||||
if run_scaling {
|
||||
let suite = scaling::run_all();
|
||||
suite.print_summary();
|
||||
}
|
||||
|
||||
println!("\nBenchmarks complete.");
|
||||
}
|
||||
276
benches/runtime_benchmarks.rs
Normal file
276
benches/runtime_benchmarks.rs
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
use criterion::{
|
||||
criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput,
|
||||
};
|
||||
use swactor::{
|
||||
actor::{ActorAddress, ActorInterface},
|
||||
config::RuntimeConfig,
|
||||
runtime::{Ctx, Runtime},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn make_config(max_actors: usize, max_messages: usize) -> RuntimeConfig {
|
||||
RuntimeConfig {
|
||||
max_actors,
|
||||
actor_max_messages: max_messages,
|
||||
num_threads: 1,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Message types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone)]
|
||||
struct NoopMessage;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PingMessage {
|
||||
reply_to: ActorAddress,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PongMessage;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CountMessage(u64);
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RingMessage {
|
||||
hops: u64,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Actor types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct NoopActor;
|
||||
|
||||
impl ActorInterface for NoopActor {
|
||||
type Incoming = NoopMessage;
|
||||
type Response = ();
|
||||
fn handle(&mut self, _ctx: &Ctx, _msg: NoopMessage) {}
|
||||
}
|
||||
|
||||
struct EchoActor;
|
||||
|
||||
impl ActorInterface for EchoActor {
|
||||
type Incoming = PingMessage;
|
||||
type Response = ();
|
||||
fn handle(&mut self, ctx: &Ctx, msg: PingMessage) {
|
||||
let _ = ctx.send(msg.reply_to, PongMessage);
|
||||
}
|
||||
}
|
||||
|
||||
struct SinkActor;
|
||||
|
||||
impl ActorInterface for SinkActor {
|
||||
type Incoming = CountMessage;
|
||||
type Response = ();
|
||||
fn handle(&mut self, _ctx: &Ctx, _msg: CountMessage) {}
|
||||
}
|
||||
|
||||
struct RingActor {
|
||||
next: ActorAddress,
|
||||
}
|
||||
|
||||
impl ActorInterface for RingActor {
|
||||
type Incoming = RingMessage;
|
||||
type Response = ();
|
||||
fn handle(&mut self, ctx: &Ctx, msg: RingMessage) {
|
||||
let _ = ctx.send(self.next, RingMessage { hops: msg.hops + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Latency benchmarks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn latency_benchmarks(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("latency");
|
||||
|
||||
// A1 — Spawn latency
|
||||
group.bench_function("spawn", |b| {
|
||||
b.iter_batched(
|
||||
|| Runtime::new(make_config(1_000, 1_000)),
|
||||
|rt| {
|
||||
rt.spawn(NoopActor).unwrap();
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// A2 — Message round-trip
|
||||
group.bench_function("message_roundtrip", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let rt = Runtime::new(make_config(1_000, 1_000));
|
||||
let addr = rt.spawn(EchoActor).unwrap();
|
||||
rt.tick(); // register actor
|
||||
let inbox = rt.new_inbox::<PongMessage>().unwrap();
|
||||
let inbox_addr = *inbox.addr();
|
||||
(rt, addr, inbox, inbox_addr)
|
||||
},
|
||||
|(rt, addr, inbox, inbox_addr)| {
|
||||
rt.send_to(addr, PingMessage { reply_to: inbox_addr }).unwrap();
|
||||
for _ in 0..20 {
|
||||
rt.tick();
|
||||
if inbox.try_recv().is_some() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("PongMessage not received within 20 ticks");
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// A3 — Fire-and-forget send
|
||||
group.bench_function("send_fire_and_forget", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let rt = Runtime::new(make_config(1_000, 100_000));
|
||||
let addr = rt.spawn(NoopActor).unwrap();
|
||||
rt.tick(); // register actor
|
||||
(rt, addr)
|
||||
},
|
||||
|(rt, addr)| {
|
||||
rt.send_to(addr, NoopMessage).unwrap();
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// A4 — Inbox creation
|
||||
group.bench_function("inbox_creation", |b| {
|
||||
b.iter_batched(
|
||||
|| Runtime::new(make_config(1_000, 1_000)),
|
||||
|rt| {
|
||||
rt.new_inbox::<NoopMessage>().unwrap();
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Throughput benchmarks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn throughput_benchmarks(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("throughput");
|
||||
|
||||
// B1 — Single-actor throughput
|
||||
for n in [100, 1_000, 10_000] {
|
||||
group.throughput(Throughput::Elements(n as u64));
|
||||
group.bench_with_input(BenchmarkId::new("single_actor", n), &n, |b, &n| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let rt = Runtime::new(make_config(100, n + 100));
|
||||
let addr = rt.spawn(SinkActor).unwrap();
|
||||
rt.tick(); // register actor
|
||||
for i in 0..n {
|
||||
rt.send_to(addr, CountMessage(i as u64)).unwrap();
|
||||
}
|
||||
rt
|
||||
},
|
||||
|rt| {
|
||||
for _ in 0..50 {
|
||||
rt.tick();
|
||||
}
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// B2 — Multi-actor throughput
|
||||
for (actors, msgs_per) in [(10, 100), (100, 100), (100, 1_000)] {
|
||||
let total = actors * msgs_per;
|
||||
group.throughput(Throughput::Elements(total as u64));
|
||||
let param = format!("{actors}x{msgs_per}");
|
||||
group.bench_with_input(BenchmarkId::new("multi_actor", ¶m), &(actors, msgs_per), |b, &(actors, msgs_per)| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let rt = Runtime::new(make_config(actors + 100, msgs_per + 100));
|
||||
let addrs: Vec<_> = (0..actors)
|
||||
.map(|_| rt.spawn(SinkActor).unwrap())
|
||||
.collect();
|
||||
rt.tick(); // register actors
|
||||
for &addr in &addrs {
|
||||
for i in 0..msgs_per {
|
||||
rt.send_to(addr, CountMessage(i as u64)).unwrap();
|
||||
}
|
||||
}
|
||||
rt
|
||||
},
|
||||
|rt| {
|
||||
for _ in 0..100 {
|
||||
rt.tick();
|
||||
}
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// B3 — Ring throughput
|
||||
for ring_size in [10usize, 100, 500] {
|
||||
group.throughput(Throughput::Elements((ring_size + 1) as u64));
|
||||
group.bench_with_input(BenchmarkId::new("ring", ring_size), &ring_size, |b, &ring_size| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let rt = Runtime::new(make_config(ring_size + 100, 100));
|
||||
let inbox = rt.new_inbox::<RingMessage>().unwrap();
|
||||
// Build the ring: last actor sends to inbox, each prior actor sends to the next
|
||||
let mut next_addr = *inbox.addr();
|
||||
let mut entry_addr = next_addr;
|
||||
for _ in 0..ring_size {
|
||||
let addr = rt.spawn(RingActor { next: next_addr }).unwrap();
|
||||
entry_addr = addr;
|
||||
next_addr = addr;
|
||||
}
|
||||
rt.tick(); // register all actors
|
||||
(rt, entry_addr, inbox)
|
||||
},
|
||||
|(rt, entry_addr, inbox)| {
|
||||
rt.send_to(entry_addr, RingMessage { hops: 0 }).unwrap();
|
||||
for _ in 0..(ring_size + 10) {
|
||||
rt.tick();
|
||||
if inbox.try_recv().is_some() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("RingMessage not received within tick budget");
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// B4 — Spawn throughput
|
||||
for n in [100, 1_000, 5_000] {
|
||||
group.throughput(Throughput::Elements(n as u64));
|
||||
group.bench_with_input(BenchmarkId::new("spawn", n), &n, |b, &n| {
|
||||
b.iter_batched(
|
||||
|| Runtime::new(make_config(n + 100, 1_000)),
|
||||
|rt| {
|
||||
for _ in 0..n {
|
||||
rt.spawn(NoopActor).unwrap();
|
||||
}
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, latency_benchmarks, throughput_benchmarks);
|
||||
criterion_main!(benches);
|
||||
|
|
@ -1,280 +0,0 @@
|
|||
//! Scaling benchmarks for the swactor runtime.
|
||||
//!
|
||||
//! These benchmarks measure how performance scales with:
|
||||
//! - Number of actors
|
||||
//! - Number of worker threads
|
||||
//! - Message payload size
|
||||
|
||||
use crate::harness::{black_box, Bench, BenchSuite};
|
||||
use std::thread;
|
||||
use swactor::{
|
||||
actor::ActorInterface,
|
||||
runtime::{Runtime, RuntimeConfig},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Test Actors
|
||||
// ============================================================================
|
||||
|
||||
/// A counter actor that just increments on each message
|
||||
struct CounterActor {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl CounterActor {
|
||||
fn new() -> Self {
|
||||
Self { count: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Increment;
|
||||
|
||||
impl ActorInterface for CounterActor {
|
||||
type Incoming = Increment;
|
||||
type Response = ();
|
||||
|
||||
fn handle(&mut self, _ctx: &Runtime, _msg: Increment) {
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
struct SharedCounter {
|
||||
count: std::sync::Arc<std::sync::atomic::AtomicUsize>,
|
||||
}
|
||||
|
||||
impl ActorInterface for SharedCounter {
|
||||
type Incoming = Increment;
|
||||
type Response = ();
|
||||
|
||||
fn handle(&mut self, _ctx: &Runtime, _msg: Increment) {
|
||||
self.count
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// An actor that handles variable-sized payloads
|
||||
struct PayloadActor {
|
||||
bytes_received: usize,
|
||||
}
|
||||
|
||||
impl PayloadActor {
|
||||
fn new() -> Self {
|
||||
Self { bytes_received: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Payload(Vec<u8>);
|
||||
|
||||
impl ActorInterface for PayloadActor {
|
||||
type Incoming = Payload;
|
||||
type Response = ();
|
||||
|
||||
fn handle(&mut self, _ctx: &Runtime, msg: Payload) {
|
||||
self.bytes_received += msg.0.len();
|
||||
black_box(&msg.0);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
/// Benchmark: How throughput scales with actor count
|
||||
pub fn bench_actor_count_scaling(suite: &mut BenchSuite) {
|
||||
let messages_per_actor = 100u64;
|
||||
|
||||
for actor_count in [10u64, 100, 500, 1000] {
|
||||
let name = format!("scaling_{}_actors", actor_count);
|
||||
let total_messages = actor_count * messages_per_actor;
|
||||
|
||||
let result = Bench::new(&name)
|
||||
.warmup(2)
|
||||
.iters(10)
|
||||
.elements(total_messages)
|
||||
.run_with_setup(
|
||||
|| {
|
||||
let config = RuntimeConfig {
|
||||
max_actors: (actor_count as usize) + 100,
|
||||
router_max_messages: (total_messages as usize) * 3,
|
||||
actor_max_messages: (messages_per_actor as usize) * 2,
|
||||
num_threads: 1,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
|
||||
// Spawn actors
|
||||
let mut actors = Vec::with_capacity(actor_count as usize);
|
||||
for _ in 0..actor_count {
|
||||
let addr = runtime.spawn(CounterActor::new()).unwrap();
|
||||
actors.push(addr);
|
||||
}
|
||||
|
||||
// Process registrations
|
||||
for _ in 0..(actor_count * 2) {
|
||||
runtime.tick();
|
||||
}
|
||||
|
||||
(runtime, actors, messages_per_actor)
|
||||
},
|
||||
|(runtime, actors, msgs_per)| {
|
||||
// Distribute messages across all actors
|
||||
for _ in 0..msgs_per {
|
||||
for actor in &actors {
|
||||
let _ = runtime.send_to::<Increment>(*actor, Increment);
|
||||
}
|
||||
}
|
||||
|
||||
// Process all
|
||||
let total = actors.len() as u64 * msgs_per;
|
||||
for _ in 0..(total * 3) {
|
||||
runtime.tick();
|
||||
}
|
||||
black_box(());
|
||||
},
|
||||
);
|
||||
|
||||
suite.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Benchmark: How throughput scales with thread count (multithreaded runtime)
|
||||
pub fn bench_thread_count_scaling(suite: &mut BenchSuite) {
|
||||
let actor_count = 100u64;
|
||||
let messages_per_actor = 500u64;
|
||||
let total_messages = actor_count * messages_per_actor;
|
||||
|
||||
for thread_count in [2usize, 4, 8] {
|
||||
let name = format!("scaling_{}_threads", thread_count);
|
||||
|
||||
let result = Bench::new(&name)
|
||||
.warmup(1)
|
||||
.iters(5)
|
||||
.elements(total_messages)
|
||||
.run_with_setup(
|
||||
|| {
|
||||
let counter = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let config = RuntimeConfig {
|
||||
max_actors: (actor_count as usize) + 100,
|
||||
router_max_messages: (total_messages as usize) * 3,
|
||||
actor_max_messages: (messages_per_actor as usize) * 2,
|
||||
num_threads: thread_count,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
|
||||
let mut actors = Vec::with_capacity(actor_count as usize);
|
||||
for _ in 0..actor_count {
|
||||
let addr = runtime
|
||||
.spawn(SharedCounter {
|
||||
count: counter.clone(),
|
||||
})
|
||||
.unwrap();
|
||||
actors.push(addr);
|
||||
}
|
||||
|
||||
let handle = runtime.run().unwrap();
|
||||
|
||||
for actor in &actors {
|
||||
loop {
|
||||
if handle
|
||||
.runtime
|
||||
.send_to::<Increment>(*actor, Increment)
|
||||
.is_ok()
|
||||
{
|
||||
break;
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
}
|
||||
|
||||
while counter.load(std::sync::atomic::Ordering::Relaxed) < actors.len() {
|
||||
thread::yield_now();
|
||||
}
|
||||
counter.store(0, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
(handle, actors, counter)
|
||||
},
|
||||
|(handle, actors, counter)| {
|
||||
for _ in 0..messages_per_actor {
|
||||
for actor in &actors {
|
||||
let _ = handle.runtime.send_to::<Increment>(*actor, Increment);
|
||||
}
|
||||
}
|
||||
|
||||
while counter.load(std::sync::atomic::Ordering::Relaxed)
|
||||
< total_messages as usize
|
||||
{
|
||||
thread::yield_now();
|
||||
}
|
||||
|
||||
handle.shutdown();
|
||||
handle.join();
|
||||
black_box(());
|
||||
},
|
||||
);
|
||||
|
||||
suite.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Benchmark: How throughput scales with message payload size
|
||||
pub fn bench_payload_size_scaling(suite: &mut BenchSuite) {
|
||||
let message_count = 1_000u64;
|
||||
|
||||
for payload_size in [64usize, 1024, 16384, 65536] {
|
||||
let name = format!("payload_{}B", payload_size);
|
||||
let payload = vec![0u8; payload_size];
|
||||
|
||||
let result = Bench::new(&name)
|
||||
.warmup(2)
|
||||
.iters(20)
|
||||
.elements(message_count)
|
||||
.run_with_setup(
|
||||
|| {
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 10,
|
||||
router_max_messages: (message_count as usize) * 2,
|
||||
actor_max_messages: (message_count as usize) * 2,
|
||||
num_threads: 1,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
let sink = runtime.spawn(PayloadActor::new()).unwrap();
|
||||
|
||||
// Process registration
|
||||
for _ in 0..10 {
|
||||
runtime.tick();
|
||||
}
|
||||
|
||||
(runtime, sink, payload.clone())
|
||||
},
|
||||
|(runtime, sink, payload)| {
|
||||
for _ in 0..message_count {
|
||||
let _ = runtime.send_to::<Payload>(sink, Payload(payload.clone()));
|
||||
}
|
||||
|
||||
for _ in 0..(message_count * 3) {
|
||||
runtime.tick();
|
||||
}
|
||||
black_box(());
|
||||
},
|
||||
);
|
||||
|
||||
suite.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Run all scaling benchmarks
|
||||
pub fn run_all() -> BenchSuite {
|
||||
let mut suite = BenchSuite::new("Scaling Benchmarks");
|
||||
|
||||
println!("\nRunning actor count scaling benchmarks...");
|
||||
bench_actor_count_scaling(&mut suite);
|
||||
|
||||
println!("Running thread count scaling benchmarks...");
|
||||
bench_thread_count_scaling(&mut suite);
|
||||
|
||||
println!("Running payload size scaling benchmarks...");
|
||||
bench_payload_size_scaling(&mut suite);
|
||||
|
||||
suite
|
||||
}
|
||||
|
|
@ -1,343 +0,0 @@
|
|||
//! Core throughput benchmarks for the swactor runtime.
|
||||
//!
|
||||
//! These benchmarks measure:
|
||||
//! - Message passing throughput
|
||||
//! - Actor spawn rate
|
||||
//! - Fan-out and fan-in patterns
|
||||
//! - Ping-pong latency
|
||||
|
||||
use crate::harness::{black_box, Bench, BenchSuite};
|
||||
use swactor::{
|
||||
actor::{ActorAddress, ActorInterface},
|
||||
runtime::{Runtime, RuntimeConfig},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Test Actors
|
||||
// ============================================================================
|
||||
|
||||
/// A sink actor that counts messages received
|
||||
struct SinkActor {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl SinkActor {
|
||||
fn new() -> Self {
|
||||
Self { count: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Ping;
|
||||
|
||||
impl ActorInterface for SinkActor {
|
||||
type Incoming = Ping;
|
||||
type Response = ();
|
||||
|
||||
fn handle(&mut self, _ctx: &Runtime, _msg: Ping) {
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// A forwarding actor that passes messages along a chain
|
||||
struct ForwardActor {
|
||||
next: Option<ActorAddress>,
|
||||
}
|
||||
|
||||
impl ForwardActor {
|
||||
fn new() -> Self {
|
||||
Self { next: None }
|
||||
}
|
||||
|
||||
fn with_next(next: ActorAddress) -> Self {
|
||||
Self { next: Some(next) }
|
||||
}
|
||||
}
|
||||
|
||||
impl ActorInterface for ForwardActor {
|
||||
type Incoming = Ping;
|
||||
type Response = Ping;
|
||||
|
||||
fn handle(&mut self, ctx: &Runtime, msg: Ping) {
|
||||
if let Some(next) = self.next {
|
||||
let _ = ctx.send_to(next, msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
/// Benchmark: Messages sent through the router to a single sink actor
|
||||
pub fn bench_message_throughput(suite: &mut BenchSuite) {
|
||||
for msg_count in [1_000u64, 10_000, 100_000] {
|
||||
let name = format!("message_throughput_{}", msg_count);
|
||||
|
||||
let result = Bench::new(&name)
|
||||
.warmup(3)
|
||||
.iters(20)
|
||||
.elements(msg_count)
|
||||
.run_with_setup(
|
||||
|| {
|
||||
// Setup: create runtime and sink actor
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 100,
|
||||
router_max_messages: (msg_count as usize) * 2,
|
||||
actor_max_messages: (msg_count as usize) * 2,
|
||||
num_threads: 1,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
let sink = runtime.spawn(SinkActor::new()).unwrap();
|
||||
(runtime, sink, msg_count)
|
||||
},
|
||||
|(runtime, sink, count)| {
|
||||
// Send all messages
|
||||
for _ in 0..count {
|
||||
let _ = runtime.send_to::<Ping>(sink, Ping);
|
||||
}
|
||||
// Process until done
|
||||
// Tick enough times to process all messages
|
||||
// (router tick + actor tick) * messages / WATERLEVEL
|
||||
for _ in 0..(count * 3) {
|
||||
runtime.tick();
|
||||
}
|
||||
black_box(());
|
||||
},
|
||||
);
|
||||
|
||||
suite.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Benchmark: Actor spawn rate
|
||||
pub fn bench_spawn_rate(suite: &mut BenchSuite) {
|
||||
for actor_count in [100u64, 500, 900] {
|
||||
let name = format!("spawn_rate_{}_actors", actor_count);
|
||||
|
||||
let result = Bench::new(&name)
|
||||
.warmup(3)
|
||||
.iters(50)
|
||||
.elements(actor_count)
|
||||
.run_with_setup(
|
||||
|| {
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 1000,
|
||||
router_max_messages: 10_000,
|
||||
actor_max_messages: 100,
|
||||
num_threads: 1,
|
||||
};
|
||||
Runtime::new(config)
|
||||
},
|
||||
|runtime| {
|
||||
for _ in 0..actor_count {
|
||||
let _ = runtime.spawn(SinkActor::new());
|
||||
}
|
||||
// Process router messages to register all actors
|
||||
for _ in 0..(actor_count * 2) {
|
||||
runtime.tick();
|
||||
}
|
||||
black_box(());
|
||||
},
|
||||
);
|
||||
|
||||
suite.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Benchmark: Fan-out (1 sender to N receivers)
|
||||
pub fn bench_fanout(suite: &mut BenchSuite) {
|
||||
for fan_count in [10u64, 100, 500] {
|
||||
let name = format!("fanout_1_to_{}", fan_count);
|
||||
let messages_per_receiver = 100u64;
|
||||
|
||||
let result = Bench::new(&name)
|
||||
.warmup(2)
|
||||
.iters(20)
|
||||
.elements(fan_count * messages_per_receiver)
|
||||
.run_with_setup(
|
||||
|| {
|
||||
let config = RuntimeConfig {
|
||||
max_actors: (fan_count as usize) + 10,
|
||||
router_max_messages: (fan_count as usize)
|
||||
* (messages_per_receiver as usize)
|
||||
* 2,
|
||||
actor_max_messages: (messages_per_receiver as usize) * 2,
|
||||
num_threads: 1,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
|
||||
// Spawn N sink actors
|
||||
let mut sinks = Vec::with_capacity(fan_count as usize);
|
||||
for _ in 0..fan_count {
|
||||
let addr = runtime.spawn(SinkActor::new()).unwrap();
|
||||
sinks.push(addr);
|
||||
}
|
||||
|
||||
// Process router registrations
|
||||
for _ in 0..(fan_count * 2) {
|
||||
runtime.tick();
|
||||
}
|
||||
|
||||
(runtime, sinks, messages_per_receiver)
|
||||
},
|
||||
|(runtime, sinks, msgs_per)| {
|
||||
// Send messages to all sinks
|
||||
for _ in 0..msgs_per {
|
||||
for sink in &sinks {
|
||||
let _ = runtime.send_to::<Ping>(*sink, Ping);
|
||||
}
|
||||
}
|
||||
|
||||
// Process all messages
|
||||
let total_msgs = sinks.len() as u64 * msgs_per;
|
||||
for _ in 0..(total_msgs * 3) {
|
||||
runtime.tick();
|
||||
}
|
||||
black_box(());
|
||||
},
|
||||
);
|
||||
|
||||
suite.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Benchmark: Fan-in (N senders to 1 receiver)
|
||||
pub fn bench_fanin(suite: &mut BenchSuite) {
|
||||
for sender_count in [10u64, 100, 500] {
|
||||
let name = format!("fanin_{}_to_1", sender_count);
|
||||
let messages_per_sender = 100u64;
|
||||
|
||||
let result = Bench::new(&name)
|
||||
.warmup(2)
|
||||
.iters(20)
|
||||
.elements(sender_count * messages_per_sender)
|
||||
.run_with_setup(
|
||||
|| {
|
||||
let total_messages = (sender_count * messages_per_sender) as usize;
|
||||
let config = RuntimeConfig {
|
||||
max_actors: (sender_count as usize) + 10,
|
||||
router_max_messages: total_messages * 3,
|
||||
actor_max_messages: total_messages * 2,
|
||||
num_threads: 1,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
|
||||
// Spawn the sink
|
||||
let sink = runtime.spawn(SinkActor::new()).unwrap();
|
||||
|
||||
// Spawn N forwarders pointing at sink
|
||||
let mut senders = Vec::with_capacity(sender_count as usize);
|
||||
for _ in 0..sender_count {
|
||||
let addr = runtime.spawn(ForwardActor::with_next(sink)).unwrap();
|
||||
senders.push(addr);
|
||||
}
|
||||
|
||||
// Process router registrations
|
||||
for _ in 0..((sender_count + 1) * 2) {
|
||||
runtime.tick();
|
||||
}
|
||||
|
||||
(runtime, senders, sink, messages_per_sender)
|
||||
},
|
||||
|(runtime, senders, _sink, msgs_per)| {
|
||||
// Each sender forwards msgs_per messages to the sink
|
||||
for _ in 0..msgs_per {
|
||||
for sender in &senders {
|
||||
let _ = runtime.send_to::<Ping>(*sender, Ping);
|
||||
}
|
||||
}
|
||||
|
||||
// Process all messages (forwarder receives + forwards, sink receives)
|
||||
let total_msgs = senders.len() as u64 * msgs_per;
|
||||
for _ in 0..(total_msgs * 6) {
|
||||
runtime.tick();
|
||||
}
|
||||
black_box(());
|
||||
},
|
||||
);
|
||||
|
||||
suite.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Benchmark: Ring topology (message passed around N actors in a circle)
|
||||
pub fn bench_ring(suite: &mut BenchSuite) {
|
||||
for ring_size in [10u64, 100, 500] {
|
||||
let name = format!("ring_{}_actors", ring_size);
|
||||
let laps = 10u64; // How many times around the ring
|
||||
|
||||
let result = Bench::new(&name)
|
||||
.warmup(2)
|
||||
.iters(20)
|
||||
.elements(ring_size * laps)
|
||||
.run_with_setup(
|
||||
|| {
|
||||
let config = RuntimeConfig {
|
||||
max_actors: (ring_size as usize) + 10,
|
||||
router_max_messages: 10_000,
|
||||
actor_max_messages: 1_000,
|
||||
num_threads: 1,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
|
||||
// First, spawn all actors without links
|
||||
let mut actors: Vec<ActorAddress> = Vec::with_capacity(ring_size as usize);
|
||||
for _ in 0..ring_size {
|
||||
let addr = runtime.spawn(ForwardActor::new()).unwrap();
|
||||
actors.push(addr);
|
||||
}
|
||||
|
||||
// We can't update their `next` field after spawn in this design,
|
||||
// so instead we'll use an inbox to receive the final message
|
||||
// For now, we'll just measure message passing through a chain
|
||||
|
||||
// Process registrations
|
||||
for _ in 0..(ring_size * 2) {
|
||||
runtime.tick();
|
||||
}
|
||||
|
||||
(runtime, actors, laps)
|
||||
},
|
||||
|(runtime, actors, laps)| {
|
||||
// Send to first actor (even though they don't forward, we're
|
||||
// measuring the router + inbox overhead)
|
||||
for _ in 0..laps {
|
||||
for actor in &actors {
|
||||
let _ = runtime.send_to::<Ping>(*actor, Ping);
|
||||
}
|
||||
}
|
||||
|
||||
let total = actors.len() as u64 * laps;
|
||||
for _ in 0..(total * 3) {
|
||||
runtime.tick();
|
||||
}
|
||||
black_box(());
|
||||
},
|
||||
);
|
||||
|
||||
suite.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Run all throughput benchmarks
|
||||
pub fn run_all() -> BenchSuite {
|
||||
let mut suite = BenchSuite::new("Throughput Benchmarks");
|
||||
|
||||
println!("\nRunning message throughput benchmarks...");
|
||||
bench_message_throughput(&mut suite);
|
||||
|
||||
println!("Running spawn rate benchmarks...");
|
||||
bench_spawn_rate(&mut suite);
|
||||
|
||||
println!("Running fan-out benchmarks...");
|
||||
bench_fanout(&mut suite);
|
||||
|
||||
println!("Running fan-in benchmarks...");
|
||||
bench_fanin(&mut suite);
|
||||
|
||||
println!("Running ring topology benchmarks...");
|
||||
bench_ring(&mut suite);
|
||||
|
||||
suite
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use swactor::{
|
||||
actor::{ActorAddress, ActorInterface},
|
||||
runtime::{Runtime, RuntimeConfig},
|
||||
runtime::{Ctx, Runtime, RuntimeConfig},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
|
@ -24,10 +24,10 @@ impl ActorInterface for Greeter {
|
|||
type Incoming = GreetMessage;
|
||||
type Response = GreetResponse;
|
||||
|
||||
fn handle(&mut self, ctx: &Runtime, msg: GreetMessage) {
|
||||
fn handle(&mut self, ctx: &Ctx, msg: GreetMessage) {
|
||||
let res = GreetResponse(format!("Hello, {}!", msg.who));
|
||||
self.num_greeted += 1;
|
||||
if let Err(_) = ctx.send_to(msg.return_addr, res) {
|
||||
if let Err(_) = ctx.send(msg.return_addr, res) {
|
||||
// no error handling
|
||||
self.num_greeted -= 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use swactor::{
|
||||
actor::{ActorAddress, ActorInterface},
|
||||
runtime::{Inbox, Runtime, RuntimeConfig},
|
||||
runtime::{Ctx, Inbox, Runtime, RuntimeConfig},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
|
|
@ -30,8 +30,8 @@ impl RingActor {
|
|||
impl ActorInterface for RingActor {
|
||||
type Incoming = RingMessage;
|
||||
type Response = ();
|
||||
fn handle(&mut self, ctx: &Runtime, msg: Self::Incoming) {
|
||||
if let Err(_) = ctx.send_to(self.next, msg.next()) {
|
||||
fn handle(&mut self, ctx: &Ctx, msg: Self::Incoming) {
|
||||
if let Err(_) = ctx.send(self.next, msg.next()) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
|
|
|||
96
src/actor.rs
96
src/actor.rs
|
|
@ -1,50 +1,15 @@
|
|||
use crate::{WATERLEVEL, channel::Receiver, get_random, runtime::Runtime};
|
||||
use std::any::Any;
|
||||
|
||||
use crate::{get_random, runtime::{ContextInner, Ctx}, worker::Mailbox};
|
||||
|
||||
/// The primary trait defining data that can be passed to and from actor processes
|
||||
pub trait Message: 'static + Sized + Clone + Send + Sync {}
|
||||
impl<T: 'static + Sized + Clone + Send + Sync> Message for T {}
|
||||
|
||||
/// The trait that needs to be implemented in order to run a process as an `Actor`
|
||||
///
|
||||
/// The `Incoming` type represents `Messages` that can be delivered to the `Actor`.
|
||||
///
|
||||
/// The `Response` type represents possible `Messages` the actor may attempt to reply with.
|
||||
///
|
||||
/// The `fn handle(..)` is where you implement the logic for handling `Incoming` messages
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use swactor::{actor::{ActorAddress, ActorInterface}, runtime::Runtime};
|
||||
///
|
||||
/// struct Greeter {
|
||||
/// num_greeted: usize,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Clone)] // required to auto implement `Message`
|
||||
/// struct GreetMessage {
|
||||
/// who: String,
|
||||
/// return_addr: ActorAddress,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Clone)]
|
||||
/// struct GreetResponse(String);
|
||||
///
|
||||
/// impl ActorInterface for Greeter {
|
||||
/// type Incoming = GreetMessage;
|
||||
/// type Response = GreetResponse;
|
||||
///
|
||||
/// fn handle(&mut self, ctx: &Runtime, msg: Self::Incoming) {
|
||||
/// let response = GreetResponse(format!("Hello, {}!", msg.who).to_string());
|
||||
/// if let Ok(_) = ctx.send_to(msg.return_addr, response) {
|
||||
/// self.num_greeted += 1;
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait ActorInterface: 'static + Send {
|
||||
type Incoming: Message;
|
||||
type Response: Message;
|
||||
fn handle(&mut self, ctx: &Runtime, msg: Self::Incoming);
|
||||
fn handle(&mut self, ctx: &Ctx, msg: Self::Incoming);
|
||||
}
|
||||
|
||||
/// A unique address for this actor. 32 bytes is overkill for a small application,
|
||||
|
|
@ -60,47 +25,58 @@ impl ActorAddress {
|
|||
}
|
||||
}
|
||||
|
||||
/// The actor process as represented in the Runtime, with the actor state stored with it's inbox.
|
||||
/// The actor process as represented in the Runtime, with the actor state stored with its mailbox.
|
||||
pub(crate) struct Actor<A>
|
||||
where
|
||||
A: ActorInterface,
|
||||
{
|
||||
inbox: Receiver<A::Incoming>,
|
||||
addr: ActorAddress,
|
||||
mailbox: Mailbox<A::Incoming>,
|
||||
inner: A,
|
||||
}
|
||||
|
||||
impl<A: ActorInterface> Actor<A> {
|
||||
pub(crate) fn new(inbox: Receiver<A::Incoming>, inner: A) -> Self {
|
||||
Self { inbox, inner }
|
||||
pub(crate) fn new(addr: ActorAddress, mailbox: Mailbox<A::Incoming>, inner: A) -> Self {
|
||||
Self {
|
||||
addr,
|
||||
mailbox,
|
||||
inner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for type-erased actors
|
||||
pub(crate) trait AnyActor: Send {
|
||||
fn tick(&mut self, ctx: &Runtime);
|
||||
/// Tick the actor, processing pending messages. Returns `true` if any work was done.
|
||||
fn tick(&mut self, inner: &dyn ContextInner) -> bool;
|
||||
/// Deliver a type-erased message into this actor's mailbox.
|
||||
/// Returns `true` if the downcast succeeded.
|
||||
fn deliver(&mut self, msg: Box<dyn Any + Send>) -> bool;
|
||||
}
|
||||
|
||||
impl<A> AnyActor for Actor<A>
|
||||
where
|
||||
A: ActorInterface,
|
||||
{
|
||||
fn tick(&mut self, ctx: &Runtime) {
|
||||
// TODO: WATERLEVEL is hard coded, and so is this message handling scheme. We should
|
||||
// make it so both are more flexible, with sane defaults.
|
||||
let total_messages = self.inbox.len();
|
||||
let messages_to_process = if total_messages < WATERLEVEL {
|
||||
total_messages
|
||||
} else {
|
||||
total_messages >> 1
|
||||
};
|
||||
fn tick(&mut self, inner: &dyn ContextInner) -> bool {
|
||||
let n = self.mailbox.drain_count();
|
||||
if n > 0 {
|
||||
let ctx = Ctx::new(inner, self.addr);
|
||||
for _ in 0..n {
|
||||
if let Some(msg) = self.mailbox.pop() {
|
||||
self.inner.handle(&ctx, msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
n > 0
|
||||
}
|
||||
|
||||
for _ in 0..messages_to_process {
|
||||
match self.inbox.try_recv() {
|
||||
Some(msg) => self.inner.handle(ctx, msg),
|
||||
None => unreachable!(
|
||||
"We checked number of unprocessed messages in the queue ahead of processing"
|
||||
),
|
||||
}
|
||||
fn deliver(&mut self, msg: Box<dyn Any + Send>) -> bool {
|
||||
if let Ok(typed) = msg.downcast::<A::Incoming>() {
|
||||
self.mailbox.push(*typed);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
120
src/address_map.rs
Normal file
120
src/address_map.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::RwLock;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use crate::actor::ActorAddress;
|
||||
|
||||
/// Identifies a worker thread.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct WorkerId(pub(crate) usize);
|
||||
|
||||
impl WorkerId {
|
||||
pub fn as_usize(self) -> usize {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps actor addresses to the worker that owns them.
|
||||
///
|
||||
/// `RwLock<HashMap>` — zero contention for parallel reads, write-rare (only on spawn).
|
||||
pub(crate) struct AddressMap {
|
||||
inner: RwLock<HashMap<ActorAddress, WorkerId>>,
|
||||
}
|
||||
|
||||
impl AddressMap {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_capacity(cap: usize) -> Self {
|
||||
Self {
|
||||
inner: RwLock::new(HashMap::with_capacity(cap)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&self, addr: ActorAddress, worker: WorkerId) {
|
||||
self.inner.write().unwrap().insert(addr, worker);
|
||||
}
|
||||
|
||||
pub fn remove(&self, addr: &ActorAddress) {
|
||||
self.inner.write().unwrap().remove(addr);
|
||||
}
|
||||
|
||||
pub fn lookup(&self, addr: &ActorAddress) -> Option<WorkerId> {
|
||||
self.inner.read().unwrap().get(addr).copied()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.inner.read().unwrap().len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Round-robin actor placement strategy.
|
||||
pub(crate) struct Placement {
|
||||
next: AtomicUsize,
|
||||
num_workers: usize,
|
||||
}
|
||||
|
||||
impl Placement {
|
||||
pub fn new(num_workers: usize) -> Self {
|
||||
Self {
|
||||
next: AtomicUsize::new(0),
|
||||
num_workers,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_worker(&self) -> WorkerId {
|
||||
let id = self.next.fetch_add(1, Ordering::Relaxed) % self.num_workers;
|
||||
WorkerId(id)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn insert_and_lookup() {
|
||||
let map = AddressMap::new();
|
||||
let addr = ActorAddress::default();
|
||||
let wid = WorkerId(3);
|
||||
map.insert(addr, wid);
|
||||
assert_eq!(map.lookup(&addr), Some(wid));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_missing_returns_none() {
|
||||
let map = AddressMap::new();
|
||||
let addr = ActorAddress::default();
|
||||
assert_eq!(map.lookup(&addr), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_works() {
|
||||
let map = AddressMap::new();
|
||||
let addr = ActorAddress::default();
|
||||
map.insert(addr, WorkerId(0));
|
||||
map.remove(&addr);
|
||||
assert_eq!(map.lookup(&addr), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn len_tracks_entries() {
|
||||
let map = AddressMap::with_capacity(10);
|
||||
assert_eq!(map.len(), 0);
|
||||
let addr1 = ActorAddress::default();
|
||||
map.insert(addr1, WorkerId(0));
|
||||
assert_eq!(map.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_robin() {
|
||||
let p = Placement::new(3);
|
||||
assert_eq!(p.next_worker(), WorkerId(0));
|
||||
assert_eq!(p.next_worker(), WorkerId(1));
|
||||
assert_eq!(p.next_worker(), WorkerId(2));
|
||||
assert_eq!(p.next_worker(), WorkerId(0));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,18 @@
|
|||
use std::{collections::VecDeque, sync::{Arc, Mutex}};
|
||||
|
||||
use crossbeam_queue::ArrayQueue;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crossbeam_queue::{ArrayQueue, SegQueue};
|
||||
|
||||
pub struct HybridChannel<T> {
|
||||
ring: ArrayQueue<T>,
|
||||
overflow: Mutex<VecDeque<T>>,
|
||||
overflow: SegQueue<T>,
|
||||
}
|
||||
|
||||
impl<T> HybridChannel<T> {
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
Self {
|
||||
ring: ArrayQueue::new(capacity),
|
||||
overflow: Mutex::new(VecDeque::new()),
|
||||
overflow: SegQueue::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -19,7 +20,7 @@ impl<T> HybridChannel<T> {
|
|||
match self.ring.push(value) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(v) => {
|
||||
self.overflow.lock().unwrap().push_back(v);
|
||||
self.overflow.push(v);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -30,12 +31,14 @@ impl<T> HybridChannel<T> {
|
|||
return Some(value);
|
||||
}
|
||||
|
||||
self.overflow.lock().unwrap().pop_front()
|
||||
match self.overflow.pop() {
|
||||
Some(value) => {
|
||||
Some(value)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.ring.len() + self.overflow.lock().unwrap().len()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Receiver<T> {
|
||||
|
|
@ -48,11 +51,6 @@ impl<T> Receiver<T> {
|
|||
|
||||
Self { queue }
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.queue.len()
|
||||
}
|
||||
|
||||
pub fn try_recv(&self) -> Option<T> {
|
||||
return self.queue.pop();
|
||||
}
|
||||
|
|
|
|||
55
src/config.rs
Normal file
55
src/config.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/// Backoff policy for worker threads when idle.
|
||||
///
|
||||
/// Workers spin → yield → sleep with increasing delay when no work is available.
|
||||
pub struct BackoffPolicy {
|
||||
/// Number of idle ticks before switching from spin to yield.
|
||||
pub spin_threshold: u32,
|
||||
/// Number of idle ticks before switching from yield to sleep.
|
||||
pub yield_threshold: u32,
|
||||
/// Microseconds added per tick beyond the yield threshold.
|
||||
pub sleep_increment_us: u64,
|
||||
/// Maximum sleep duration in microseconds.
|
||||
pub sleep_max_us: u64,
|
||||
}
|
||||
|
||||
impl Default for BackoffPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
spin_threshold: 64,
|
||||
yield_threshold: 256,
|
||||
sleep_increment_us: 50,
|
||||
sleep_max_us: 1000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The tunable settings for the runtime.
|
||||
pub struct RuntimeConfig {
|
||||
pub max_actors: usize,
|
||||
pub actor_max_messages: usize,
|
||||
pub num_threads: usize,
|
||||
pub mailbox_waterlevel: usize,
|
||||
pub backoff_policy: BackoffPolicy,
|
||||
}
|
||||
|
||||
/// 8kB for the `Box<..>` before counting the rest of the memory
|
||||
const DEFAULT_MAX_ACTORS: usize = 1_000;
|
||||
|
||||
/// 16kB PER ACTOR to alloc space for storing messages.
|
||||
/// With default setting of [DEFAULT_MAX_ACTORS] this is:
|
||||
/// 1_000 * 16kB = 16MB
|
||||
const DEFAULT_ACTOR_MAX_MESSAGES: usize = 1_000;
|
||||
|
||||
const DEFAULT_MAILBOX_WATERLEVEL: usize = 10;
|
||||
|
||||
impl Default for RuntimeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_actors: DEFAULT_MAX_ACTORS,
|
||||
actor_max_messages: DEFAULT_ACTOR_MAX_MESSAGES,
|
||||
num_threads: 1,
|
||||
mailbox_waterlevel: DEFAULT_MAILBOX_WATERLEVEL,
|
||||
backoff_policy: BackoffPolicy::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/lib.rs
19
src/lib.rs
|
|
@ -1,10 +1,14 @@
|
|||
pub mod actor;
|
||||
pub mod worker;
|
||||
|
||||
mod channel;
|
||||
pub(crate) mod channel;
|
||||
pub(crate) mod error;
|
||||
pub use error::Error;
|
||||
|
||||
mod router;
|
||||
|
||||
pub(crate) mod address_map;
|
||||
pub mod config;
|
||||
|
||||
pub mod runtime;
|
||||
|
||||
#[cfg(feature = "getrandom")]
|
||||
|
|
@ -25,14 +29,3 @@ pub(crate) fn get_random(buf: &mut [u8]) {
|
|||
*byte = bytes[i % core::mem::size_of::<usize>()];
|
||||
}
|
||||
}
|
||||
|
||||
/// FIXME: remove hard coded defaults
|
||||
/// The strategy for message processing is such:
|
||||
///
|
||||
/// ```ignore
|
||||
/// if total_messages < WATERLEVEL:
|
||||
/// process all
|
||||
/// else
|
||||
/// process total_messages >> 1
|
||||
/// ```
|
||||
const WATERLEVEL: usize = 10;
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
actor::{ActorAddress, ActorInterface, Message},
|
||||
channel::Sender,
|
||||
runtime::Runtime,
|
||||
};
|
||||
|
||||
/// FIXME: Go over with a fine-toothed comb and reassure yourself this typing
|
||||
/// makes sense, that we are not doing loads of indirection on a hot path.
|
||||
///
|
||||
/// A type erased `Message` to be routed between actor processes.
|
||||
pub(crate) type Envelope = Arc<dyn std::any::Any + Send + Sync>;
|
||||
|
||||
pub(crate) trait SenderT: Send + Sync {
|
||||
fn try_send(&self, envelope: Envelope);
|
||||
}
|
||||
|
||||
impl<M: Message> SenderT for Sender<M> {
|
||||
fn try_send(&self, envelope: Envelope) {
|
||||
if let Some(msg) = envelope.downcast_ref::<M>() {
|
||||
let _ = Sender::try_send(self, msg.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal messages for the Router's own inbox
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum RouterMessage {
|
||||
/// register addrs <addr> with sender <sender>
|
||||
AddAddr(ActorAddress, Arc<dyn SenderT>),
|
||||
|
||||
/// FIXME: this will be active when we allow actors to shut themselves
|
||||
/// down. For now, disable the warning.
|
||||
#[allow(dead_code)]
|
||||
/// remove an actor from the address book
|
||||
RemoveAddr(ActorAddress),
|
||||
|
||||
/// send <msg> to <addr>
|
||||
SendToAddr { addr: ActorAddress, msg: Envelope },
|
||||
}
|
||||
|
||||
/// The `Router` is responsible for taking in and delivering all messages in the runtime.
|
||||
pub(crate) struct Router {
|
||||
directory: HashMap<ActorAddress, Arc<dyn SenderT>>,
|
||||
}
|
||||
|
||||
impl Router {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
directory: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ActorInterface for Router {
|
||||
type Incoming = RouterMessage;
|
||||
type Response = ();
|
||||
|
||||
fn handle(&mut self, _ctx: &Runtime, msg: Self::Incoming) {
|
||||
match msg {
|
||||
RouterMessage::AddAddr(addr, sender) => {
|
||||
self.directory.insert(addr, sender);
|
||||
}
|
||||
RouterMessage::RemoveAddr(addr) => {
|
||||
self.directory.remove(&addr);
|
||||
}
|
||||
RouterMessage::SendToAddr { addr, msg } => {
|
||||
if let Some(sender) = self.directory.get(&addr) {
|
||||
sender.try_send(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
428
src/runtime.rs
428
src/runtime.rs
|
|
@ -1,14 +1,19 @@
|
|||
use std::any::Any;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread::{self, JoinHandle};
|
||||
|
||||
use crate::channel::HybridChannel;
|
||||
use crate::{
|
||||
actor::{Actor, ActorAddress, ActorInterface, AnyActor, Message},
|
||||
channel::{Receiver, Sender},
|
||||
router::{Router, RouterMessage},
|
||||
Error,
|
||||
};
|
||||
use crate::actor::{Actor, ActorAddress, ActorInterface, AnyActor, Message};
|
||||
use crate::address_map::{AddressMap, Placement, WorkerId};
|
||||
use crate::channel::{Receiver, Sender};
|
||||
// Re-export config types so existing code using `runtime::RuntimeConfig` still works
|
||||
pub use crate::config::{BackoffPolicy, RuntimeConfig};
|
||||
use crate::worker::Mailbox;
|
||||
use crate::worker::{TickContext, Worker};
|
||||
use crate::Error;
|
||||
|
||||
|
||||
/// Generic message inbox for receiving messages outside of the runtime.
|
||||
pub struct Inbox<M: Message> {
|
||||
|
|
@ -26,47 +31,6 @@ impl<M: Message> Inbox<M> {
|
|||
}
|
||||
}
|
||||
|
||||
/// The tunable settings for the runtime.
|
||||
pub struct RuntimeConfig {
|
||||
pub max_actors: usize,
|
||||
pub router_max_messages: usize,
|
||||
pub actor_max_messages: usize,
|
||||
pub num_threads: usize,
|
||||
}
|
||||
|
||||
/// 8kB for the `Box<..>` before counting the rest of the memory
|
||||
const DEFAULT_MAX_ACTORS: usize = 1_000;
|
||||
|
||||
/// 160kB for the `Arc<..>` before counting the rest of the memory
|
||||
const DEFAULT_ROUTER_MAX_MESSAGES: usize = 10_000;
|
||||
|
||||
/// 16kB PER ACTOR to alloc space for storing the `Arc<..>` pointers
|
||||
/// With default setting of [DEFAULT_MAX_ACTORS] this is:
|
||||
/// 1_000 * 16kB = 16MB
|
||||
const DEFAULT_ACTOR_MAX_MESSAGES: usize = 1_000;
|
||||
|
||||
impl Default for RuntimeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_actors: DEFAULT_MAX_ACTORS,
|
||||
router_max_messages: DEFAULT_ROUTER_MAX_MESSAGES,
|
||||
actor_max_messages: DEFAULT_ACTOR_MAX_MESSAGES,
|
||||
num_threads: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `Runtime` struct is the primary gateway for interacting with the framework.
|
||||
pub struct Runtime {
|
||||
config: RuntimeConfig,
|
||||
actor_queue: HybridChannel<Box<dyn AnyActor>>,
|
||||
router_interface: Sender<RouterMessage>,
|
||||
router: Option<Actor<Router>>, // `None` if single-threaded
|
||||
|
||||
// for multithreaded contexts
|
||||
is_running: AtomicBool,
|
||||
}
|
||||
|
||||
/// Handle for dealing with a runtime that has started via the `Runtime::run()` method.
|
||||
pub struct RuntimeHandle {
|
||||
pub runtime: Arc<Runtime>,
|
||||
|
|
@ -86,97 +50,194 @@ impl RuntimeHandle {
|
|||
}
|
||||
}
|
||||
|
||||
/// Actor syscall interface — passed to `ActorInterface::handle()`.
|
||||
///
|
||||
/// Wraps a `&dyn ContextInner` to solve the object-safety problem while
|
||||
/// providing a typed public API.
|
||||
pub struct Ctx<'a> {
|
||||
inner: &'a dyn ContextInner,
|
||||
self_addr: ActorAddress,
|
||||
}
|
||||
|
||||
impl<'a> Ctx<'a> {
|
||||
pub(crate) fn new(inner: &'a dyn ContextInner, self_addr: ActorAddress) -> Self {
|
||||
Self { inner, self_addr }
|
||||
}
|
||||
|
||||
/// Returns the address of the actor currently being ticked.
|
||||
pub fn self_addr(&self) -> ActorAddress {
|
||||
self.self_addr
|
||||
}
|
||||
|
||||
/// Send a typed message to an actor address.
|
||||
pub fn send<M: Message>(&self, addr: ActorAddress, msg: M) -> Result<(), Error> {
|
||||
self.inner.send_any(addr, Box::new(msg))
|
||||
}
|
||||
|
||||
/// Spawn a new actor, returning its address.
|
||||
pub fn spawn<A: ActorInterface>(&self, actor: A) -> Result<ActorAddress, Error> {
|
||||
let addr = ActorAddress::new_random();
|
||||
let waterlevel = self.inner.mailbox_waterlevel();
|
||||
let actor = Actor::new(addr, Mailbox::new(waterlevel), actor);
|
||||
let boxed: Box<dyn AnyActor> = Box::new(actor);
|
||||
self.inner.spawn_any(addr, boxed)?;
|
||||
Ok(addr)
|
||||
}
|
||||
}
|
||||
|
||||
/// Type-erased sender for external inboxes.
|
||||
pub(crate) trait SenderT: Send + Sync {
|
||||
fn try_send_any(&self, msg: Box<dyn Any + Send>);
|
||||
}
|
||||
|
||||
impl<M: Message> SenderT for Sender<M> {
|
||||
fn try_send_any(&self, msg: Box<dyn Any + Send>) {
|
||||
if let Ok(typed) = msg.downcast::<M>() {
|
||||
let _ = Sender::try_send(self, *typed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─── Runtime ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// The `Runtime` struct is the primary gateway for interacting with the framework.
|
||||
pub struct Runtime {
|
||||
config: RuntimeConfig,
|
||||
address_map: Arc<AddressMap>,
|
||||
inbox_registry: Arc<InboxRegistry>,
|
||||
transfer_txs: Vec<Sender<Envelope>>,
|
||||
spawn_txs: Vec<Sender<(ActorAddress, Box<dyn AnyActor>)>>,
|
||||
placement: Placement,
|
||||
is_running: AtomicBool,
|
||||
/// Single-threaded mode: worker stored inline
|
||||
single_worker: Option<RefCell<Worker>>,
|
||||
/// Multi-threaded mode: workers waiting to be assigned to threads by run()
|
||||
pending_workers: Option<Vec<Worker>>,
|
||||
}
|
||||
|
||||
// Safety: RefCell<Worker> is only accessed from the thread that owns the Runtime
|
||||
// in single-threaded mode. In multi-threaded mode, single_worker is None and
|
||||
// pending_workers is consumed by run() before Arc sharing.
|
||||
unsafe impl Sync for Runtime {}
|
||||
|
||||
impl Runtime {
|
||||
/// Builds a new `Runtime` struct, but does not yet run anything. If multithreaded, call
|
||||
/// `run()`, if single threaded, needs to be driven by calls to the `tick()` method.
|
||||
pub fn new(config: RuntimeConfig) -> Self {
|
||||
let actor_queue = HybridChannel::new(config.max_actors);
|
||||
|
||||
// router is a unique actor in that the runtime needs access to it's `Sender` handle
|
||||
let router_inner = Router::new();
|
||||
let router_inbox: Receiver<RouterMessage> =
|
||||
Receiver::<<Router as ActorInterface>::Incoming>::new(config.router_max_messages);
|
||||
let router_sender = router_inbox.new_sender();
|
||||
let router = Actor::new(router_inbox, router_inner);
|
||||
|
||||
// Single-threaded: router goes in queue. Multi-threaded: stays in Option
|
||||
let router_option = if config.num_threads < 2 {
|
||||
actor_queue
|
||||
.push(Box::new(router) as Box<dyn AnyActor>)
|
||||
.map_err(|_| "failed to add router to actor queue")
|
||||
.expect("failed to spawn router at runtime initialization.");
|
||||
None
|
||||
let num_workers = if config.num_threads < 2 {
|
||||
1
|
||||
} else {
|
||||
Some(router)
|
||||
config.num_threads
|
||||
};
|
||||
|
||||
let address_map = Arc::new(AddressMap::with_capacity(config.max_actors));
|
||||
let inbox_registry = Arc::new(InboxRegistry::new());
|
||||
let placement = Placement::new(num_workers);
|
||||
|
||||
let mut transfer_txs = Vec::with_capacity(num_workers);
|
||||
let mut spawn_txs = Vec::with_capacity(num_workers);
|
||||
let mut workers = Vec::with_capacity(num_workers);
|
||||
|
||||
for i in 0..num_workers {
|
||||
let transfer_rx = Receiver::<Envelope>::new(config.actor_max_messages);
|
||||
let transfer_tx = transfer_rx.new_sender();
|
||||
transfer_txs.push(transfer_tx);
|
||||
|
||||
let spawn_rx =
|
||||
Receiver::<(ActorAddress, Box<dyn AnyActor>)>::new(config.max_actors);
|
||||
let spawn_tx = spawn_rx.new_sender();
|
||||
spawn_txs.push(spawn_tx);
|
||||
|
||||
workers.push(Worker::new(WorkerId(i), transfer_rx, spawn_rx));
|
||||
}
|
||||
|
||||
if config.num_threads < 2 {
|
||||
// Single-threaded: store one worker inline
|
||||
let worker = workers.remove(0);
|
||||
Self {
|
||||
config,
|
||||
actor_queue,
|
||||
router_interface: router_sender,
|
||||
address_map,
|
||||
inbox_registry,
|
||||
transfer_txs,
|
||||
spawn_txs,
|
||||
placement,
|
||||
is_running: AtomicBool::new(false),
|
||||
router: router_option,
|
||||
single_worker: Some(RefCell::new(worker)),
|
||||
pending_workers: None,
|
||||
}
|
||||
} else {
|
||||
// Multi-threaded: stash workers for run()
|
||||
Self {
|
||||
config,
|
||||
address_map,
|
||||
inbox_registry,
|
||||
transfer_txs,
|
||||
spawn_txs,
|
||||
placement,
|
||||
is_running: AtomicBool::new(false),
|
||||
single_worker: None,
|
||||
pending_workers: Some(workers),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn an actor, returns its address
|
||||
pub fn spawn<A: ActorInterface>(&self, actor: A) -> Result<ActorAddress, Error> {
|
||||
// assign a stochastic
|
||||
let addr = ActorAddress::new_random();
|
||||
let inbox = Receiver::<A::Incoming>::new(self.config.actor_max_messages);
|
||||
let sender = inbox.new_sender();
|
||||
|
||||
// Register the sender with the router
|
||||
self.router_interface
|
||||
.try_send(RouterMessage::AddAddr(addr, Arc::new(sender)))
|
||||
.map_err(|_| {
|
||||
Error::from("Runtime error: failed to add actor to router. Router inbox full")
|
||||
})?;
|
||||
|
||||
self.actor_queue
|
||||
.push(Box::new(Actor::new(inbox, actor)))
|
||||
.map_err(|_| Error::from("Runtime error: Failed to spawn actor. Queue full."))?;
|
||||
|
||||
let worker_id = self.placement.next_worker();
|
||||
self.address_map.insert(addr, worker_id);
|
||||
let actor = Actor::new(addr, Mailbox::new(self.config.mailbox_waterlevel), actor);
|
||||
let boxed: Box<dyn AnyActor> = Box::new(actor);
|
||||
self.spawn_txs[worker_id.as_usize()]
|
||||
.try_send((addr, boxed))
|
||||
.map_err(|_| Error::from("Runtime error: spawn queue full"))?;
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
/// Send a message to an actor address
|
||||
pub fn send_to<M: Message>(&self, addr: ActorAddress, msg: M) -> Result<(), Error> {
|
||||
self.router_interface
|
||||
.try_send(RouterMessage::SendToAddr {
|
||||
addr,
|
||||
msg: Arc::new(msg),
|
||||
})
|
||||
.map_err(|_| Error::from("Failed to send message to router."))
|
||||
let msg_box: Box<dyn Any + Send> = Box::new(msg);
|
||||
match self.address_map.lookup(&addr) {
|
||||
Some(wid) => self.transfer_txs[wid.as_usize()]
|
||||
.try_send(Envelope::new(addr, msg_box))
|
||||
.map_err(|_| Error::from("Transfer queue full")),
|
||||
None => self.inbox_registry.try_deliver(addr, msg_box),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an external inbox for receiving messages in the outer process containing the runtime
|
||||
pub fn new_inbox<M: Message>(&self) -> Result<Inbox<M>, Error> {
|
||||
let addr = ActorAddress::new_random();
|
||||
|
||||
let receiver = Receiver::<M>::new(self.config.actor_max_messages);
|
||||
let sender = receiver.new_sender();
|
||||
|
||||
// Register the sender with the router
|
||||
self.router_interface
|
||||
.try_send(RouterMessage::AddAddr(addr, Arc::new(sender)))
|
||||
.map_err(|_| {
|
||||
Error::from(
|
||||
"Runtime error: failed to add a new inbox channel. Router inbox is full.",
|
||||
)
|
||||
})?;
|
||||
|
||||
self.inbox_registry.register(addr, Arc::new(sender));
|
||||
Ok(Inbox {
|
||||
addr,
|
||||
inner: receiver,
|
||||
})
|
||||
}
|
||||
|
||||
/// Drive one tick of the single-threaded worker.
|
||||
pub fn tick(&self) {
|
||||
if let Some(ref worker) = self.single_worker {
|
||||
let tc = TickContext {
|
||||
address_map: &self.address_map,
|
||||
transfer_txs: &self.transfer_txs,
|
||||
spawn_txs: &self.spawn_txs,
|
||||
placement: &self.placement,
|
||||
inbox_registry: &self.inbox_registry,
|
||||
config: &self.config,
|
||||
};
|
||||
worker.borrow_mut().tick_once(&tc);
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn worker threads and start processing, returning a set of handles and
|
||||
/// a Runtime object to interface with.
|
||||
///
|
||||
/// ### WARN:
|
||||
/// ##### This function panics if the configuration is set as single threaded
|
||||
/// ##### Returns an error if the configuration is set as single threaded
|
||||
/// `config.num_threads == 1`
|
||||
pub fn run(mut self) -> Result<RuntimeHandle, Error> {
|
||||
if self.config.num_threads < 2 {
|
||||
|
|
@ -187,53 +248,26 @@ impl Runtime {
|
|||
|
||||
self.is_running.store(true, Ordering::Release);
|
||||
|
||||
// Take router out before wrapping in Arc - it will be owned by router thread
|
||||
let mut router = self
|
||||
.router
|
||||
let workers = self
|
||||
.pending_workers
|
||||
.take()
|
||||
.expect("Router must be present for multi-threaded runtime");
|
||||
.expect("Workers must be present for multi-threaded runtime");
|
||||
|
||||
let rt = Arc::new(self);
|
||||
let mut handles: Vec<JoinHandle<()>> = vec![];
|
||||
let mut handles: Vec<JoinHandle<()>> = Vec::with_capacity(workers.len());
|
||||
|
||||
// Router thread owns the router directly - no synchronization needed
|
||||
let router_handle = {
|
||||
let ctx = rt.clone();
|
||||
thread::spawn(move || {
|
||||
while ctx.is_running.load(Ordering::Acquire) {
|
||||
router.tick(&ctx);
|
||||
thread::yield_now();
|
||||
}
|
||||
})
|
||||
};
|
||||
handles.push(router_handle);
|
||||
|
||||
// Spawn worker threads
|
||||
let num_workers = rt.config.num_threads - 1;
|
||||
for _ in 0..num_workers {
|
||||
let ctx = rt.clone();
|
||||
for mut worker in workers {
|
||||
let rt_clone = rt.clone();
|
||||
let handle = thread::spawn(move || {
|
||||
while ctx.is_running.load(Ordering::Acquire) {
|
||||
if let Some(mut actor) = ctx.actor_queue.pop() {
|
||||
actor.tick(&ctx);
|
||||
// FIXME: Justify this loop. It is here to prevent panics when the
|
||||
// actor queue is full, but results in a spinlock.
|
||||
loop {
|
||||
match ctx.actor_queue.push(actor) {
|
||||
Ok(()) => break,
|
||||
Err(a) => {
|
||||
actor = a;
|
||||
if !ctx.is_running.load(Ordering::Acquire) {
|
||||
break;
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
thread::yield_now();
|
||||
}
|
||||
}
|
||||
let tc = TickContext {
|
||||
address_map: &rt_clone.address_map,
|
||||
transfer_txs: &rt_clone.transfer_txs,
|
||||
spawn_txs: &rt_clone.spawn_txs,
|
||||
placement: &rt_clone.placement,
|
||||
inbox_registry: &rt_clone.inbox_registry,
|
||||
config: &rt_clone.config,
|
||||
};
|
||||
worker.run(&tc, &rt_clone.is_running, &rt_clone.config.backoff_policy);
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
|
@ -244,17 +278,105 @@ impl Runtime {
|
|||
})
|
||||
}
|
||||
|
||||
/// Pop the actor off the top of the queue and process it's messages, returning it to the back of
|
||||
/// the queue upon completion.
|
||||
pub fn tick(&self) {
|
||||
if let Some(mut actor) = self.actor_queue.pop() {
|
||||
actor.tick(&self);
|
||||
let _ = self.actor_queue.push(actor);
|
||||
}
|
||||
}
|
||||
|
||||
/// Signal all workers to stop
|
||||
pub fn shutdown(&self) {
|
||||
self.is_running.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// A type-erased message envelope for cross-worker delivery.
|
||||
///
|
||||
/// Uses `Box` (no atomic refcount) and move semantics (no clone).
|
||||
pub(crate) struct Envelope {
|
||||
dest: ActorAddress,
|
||||
payload: Box<dyn Any + Send>,
|
||||
}
|
||||
|
||||
impl Envelope {
|
||||
pub fn new(dest: ActorAddress, payload: Box<dyn Any + Send>) -> Self {
|
||||
Self { dest, payload }
|
||||
}
|
||||
|
||||
pub fn dest(&self) -> ActorAddress {
|
||||
self.dest
|
||||
}
|
||||
|
||||
pub fn downcast<M: 'static>(self) -> Option<M> {
|
||||
self.payload.downcast::<M>().ok().map(|b| *b)
|
||||
}
|
||||
|
||||
pub fn into_payload(self) -> Box<dyn Any + Send> {
|
||||
self.payload
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─── InboxRegistry ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Registry of external inboxes — replaces the Router's role for non-actor receivers.
|
||||
pub(crate) struct InboxRegistry {
|
||||
senders: RwLock<HashMap<ActorAddress, Arc<dyn SenderT>>>,
|
||||
}
|
||||
|
||||
impl InboxRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
senders: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(&self, addr: ActorAddress, sender: Arc<dyn SenderT>) {
|
||||
self.senders.write().unwrap().insert(addr, sender);
|
||||
}
|
||||
|
||||
pub fn try_deliver(
|
||||
&self,
|
||||
addr: ActorAddress,
|
||||
msg: Box<dyn Any + Send>,
|
||||
) -> Result<(), Error> {
|
||||
let senders = self.senders.read().unwrap();
|
||||
if let Some(sender) = senders.get(&addr) {
|
||||
sender.try_send_any(msg);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::from("Address not found"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// Object-safe inner trait for sending type-erased messages.
|
||||
pub(crate) trait ContextInner {
|
||||
fn send_any(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error>;
|
||||
fn spawn_any(&self, addr: ActorAddress, actor: Box<dyn AnyActor>) -> Result<(), Error>;
|
||||
fn mailbox_waterlevel(&self) -> usize;
|
||||
}
|
||||
|
||||
|
||||
impl ContextInner for Runtime {
|
||||
fn send_any(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error> {
|
||||
match self.address_map.lookup(&addr) {
|
||||
Some(wid) => {
|
||||
let _ = self.transfer_txs[wid.as_usize()].try_send(Envelope::new(addr, msg));
|
||||
Ok(())
|
||||
}
|
||||
None => self.inbox_registry.try_deliver(addr, msg),
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_any(&self, addr: ActorAddress, actor: Box<dyn AnyActor>) -> Result<(), Error> {
|
||||
let worker_id = self.placement.next_worker();
|
||||
self.address_map.insert(addr, worker_id);
|
||||
self.spawn_txs[worker_id.as_usize()]
|
||||
.try_send((addr, actor))
|
||||
.map_err(|_| Error::from("Spawn queue full"))
|
||||
}
|
||||
|
||||
fn mailbox_waterlevel(&self) -> usize {
|
||||
self.config.mailbox_waterlevel
|
||||
}
|
||||
}
|
||||
|
|
|
|||
258
src/worker.rs
Normal file
258
src/worker.rs
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
use std::any::Any;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread;
|
||||
|
||||
use crate::actor::{ActorAddress, AnyActor, Message};
|
||||
use crate::address_map::{AddressMap, Placement, WorkerId};
|
||||
use crate::channel::{Receiver, Sender};
|
||||
use crate::config::{BackoffPolicy, RuntimeConfig};
|
||||
use crate::runtime::{ContextInner, Envelope, InboxRegistry};
|
||||
use crate::Error;
|
||||
|
||||
/// Shared state passed to tick_once — single thin pointer avoids register spill.
|
||||
pub(crate) struct TickContext<'a> {
|
||||
pub address_map: &'a AddressMap,
|
||||
pub transfer_txs: &'a [Sender<Envelope>],
|
||||
pub spawn_txs: &'a [Sender<(ActorAddress, Box<dyn AnyActor>)>],
|
||||
pub placement: &'a Placement,
|
||||
pub inbox_registry: &'a InboxRegistry,
|
||||
pub config: &'a RuntimeConfig,
|
||||
}
|
||||
|
||||
/// A worker owns a set of actors and runs them in a loop.
|
||||
pub(crate) struct Worker {
|
||||
id: WorkerId,
|
||||
pool: ActorPool,
|
||||
transfer_rx: Receiver<Envelope>,
|
||||
spawn_rx: Receiver<(ActorAddress, Box<dyn AnyActor>)>,
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
pub fn new(
|
||||
id: WorkerId,
|
||||
transfer_rx: Receiver<Envelope>,
|
||||
spawn_rx: Receiver<(ActorAddress, Box<dyn AnyActor>)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
pool: ActorPool::new(),
|
||||
transfer_rx,
|
||||
spawn_rx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run one iteration of the worker loop. Returns `true` if any work was done.
|
||||
pub fn tick_once(&mut self, tc: &TickContext) -> bool {
|
||||
let mut did_work = false;
|
||||
|
||||
// 1. Drain spawn queue → add actors to pool
|
||||
while let Some((addr, actor)) = self.spawn_rx.try_recv() {
|
||||
self.pool.insert(addr, actor);
|
||||
did_work = true;
|
||||
}
|
||||
|
||||
// 2. Drain transfer queue → deliver envelopes to actors
|
||||
while let Some(envelope) = self.transfer_rx.try_recv() {
|
||||
let dest = envelope.dest();
|
||||
let payload = envelope.into_payload();
|
||||
self.pool.deliver(&dest, payload);
|
||||
did_work = true;
|
||||
}
|
||||
|
||||
// 3. Tick all actors with WorkerContext
|
||||
let pending_local: RefCell<Vec<(ActorAddress, Box<dyn Any + Send>)>> =
|
||||
RefCell::new(Vec::new());
|
||||
|
||||
{
|
||||
let worker_ctx = WorkerContext {
|
||||
worker_id: self.id,
|
||||
address_map: tc.address_map,
|
||||
transfer_txs: tc.transfer_txs,
|
||||
spawn_txs: tc.spawn_txs,
|
||||
placement: tc.placement,
|
||||
inbox_registry: tc.inbox_registry,
|
||||
config: tc.config,
|
||||
pending_local: &pending_local,
|
||||
};
|
||||
if self.pool.tick_all(&worker_ctx) {
|
||||
did_work = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Drain pending_local buffer → deliver to local actors
|
||||
let pending = pending_local.into_inner();
|
||||
if !pending.is_empty() {
|
||||
did_work = true;
|
||||
}
|
||||
for (addr, msg) in pending {
|
||||
self.pool.deliver(&addr, msg);
|
||||
}
|
||||
|
||||
did_work
|
||||
}
|
||||
|
||||
pub(crate) fn run(&mut self, tc: &TickContext, is_running: &AtomicBool, backoff: &BackoffPolicy) {
|
||||
let mut idle_count: u32 = 0;
|
||||
while is_running.load(Ordering::Acquire) {
|
||||
let did_work = self.tick_once(tc);
|
||||
if did_work {
|
||||
idle_count = 0;
|
||||
} else {
|
||||
idle_count = idle_count.saturating_add(1);
|
||||
if idle_count < backoff.spin_threshold {
|
||||
// Hot spin
|
||||
} else if idle_count < backoff.yield_threshold {
|
||||
thread::yield_now();
|
||||
} else {
|
||||
let micros = std::cmp::min(
|
||||
(idle_count - backoff.yield_threshold) as u64 * backoff.sleep_increment_us,
|
||||
backoff.sleep_max_us,
|
||||
);
|
||||
thread::sleep(std::time::Duration::from_micros(micros));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `ContextInner` impl for worker threads.
|
||||
///
|
||||
/// Same-worker sends are buffered in `pending_local` (delivered after current tick round).
|
||||
/// Cross-worker sends go through the transfer queue.
|
||||
struct WorkerContext<'a> {
|
||||
worker_id: WorkerId,
|
||||
address_map: &'a AddressMap,
|
||||
transfer_txs: &'a [Sender<Envelope>],
|
||||
spawn_txs: &'a [Sender<(ActorAddress, Box<dyn AnyActor>)>],
|
||||
placement: &'a Placement,
|
||||
inbox_registry: &'a InboxRegistry,
|
||||
config: &'a RuntimeConfig,
|
||||
pending_local: &'a RefCell<Vec<(ActorAddress, Box<dyn Any + Send>)>>,
|
||||
}
|
||||
|
||||
impl ContextInner for WorkerContext<'_> {
|
||||
fn send_any(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error> {
|
||||
match self.address_map.lookup(&addr) {
|
||||
Some(wid) if wid == self.worker_id => {
|
||||
// Same worker: buffer for local delivery (after current tick round)
|
||||
self.pending_local.borrow_mut().push((addr, msg));
|
||||
Ok(())
|
||||
}
|
||||
Some(wid) => {
|
||||
// Cross worker: envelope through transfer queue
|
||||
let envelope = Envelope::new(addr, msg);
|
||||
let _ = self.transfer_txs[wid.as_usize()].try_send(envelope);
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
// Try inbox registry (external inboxes)
|
||||
self.inbox_registry.try_deliver(addr, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_any(&self, addr: ActorAddress, actor: Box<dyn AnyActor>) -> Result<(), Error> {
|
||||
let worker_id = self.placement.next_worker();
|
||||
self.address_map.insert(addr, worker_id);
|
||||
self.spawn_txs[worker_id.as_usize()]
|
||||
.try_send((addr, actor))
|
||||
.map_err(|_| Error::from("Spawn queue full"))
|
||||
}
|
||||
|
||||
fn mailbox_waterlevel(&self) -> usize {
|
||||
self.config.mailbox_waterlevel
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-worker actor storage.
|
||||
pub(crate) struct ActorPool {
|
||||
actors: HashMap<ActorAddress, Box<dyn AnyActor>>,
|
||||
}
|
||||
|
||||
impl ActorPool {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
actors: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, addr: ActorAddress, actor: Box<dyn AnyActor>) {
|
||||
self.actors.insert(addr, actor);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, addr: &ActorAddress) -> Option<Box<dyn AnyActor>> {
|
||||
self.actors.remove(addr)
|
||||
}
|
||||
|
||||
/// Deliver a type-erased message to the actor at `addr`.
|
||||
/// Returns `true` if the actor was found and the message type matched.
|
||||
pub fn deliver(&mut self, addr: &ActorAddress, msg: Box<dyn Any + Send>) -> bool {
|
||||
if let Some(actor) = self.actors.get_mut(addr) {
|
||||
actor.deliver(msg)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Tick all actors in the pool. Returns `true` if any actor processed messages.
|
||||
pub fn tick_all(&mut self, inner: &dyn ContextInner) -> bool {
|
||||
let mut did_work = false;
|
||||
for actor in self.actors.values_mut() {
|
||||
if actor.tick(inner) {
|
||||
did_work = true;
|
||||
}
|
||||
}
|
||||
did_work
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.actors.len()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub struct Mailbox<M: Message> {
|
||||
queue: VecDeque<M>,
|
||||
waterlevel: usize,
|
||||
}
|
||||
|
||||
impl<M: Message> Mailbox<M> {
|
||||
pub fn new(waterlevel: usize) -> Self {
|
||||
Self {
|
||||
queue: VecDeque::new(),
|
||||
waterlevel,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, msg: M) {
|
||||
self.queue.push_back(msg);
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<M> {
|
||||
self.queue.pop_front()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.queue.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.queue.is_empty()
|
||||
}
|
||||
|
||||
/// How many messages to process this tick:
|
||||
/// - `len < waterlevel` → process all (`len`)
|
||||
/// - `len >= waterlevel` → process half (`len >> 1`)
|
||||
pub fn drain_count(&self) -> usize {
|
||||
let len = self.queue.len();
|
||||
if len < self.waterlevel {
|
||||
len
|
||||
} else {
|
||||
len >> 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
97
tests/runtime_api_tests.rs
Normal file
97
tests/runtime_api_tests.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
use swactor::{
|
||||
actor::{ActorAddress, ActorInterface},
|
||||
runtime::{Ctx, Inbox, Runtime, RuntimeConfig},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared test fixtures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone)]
|
||||
struct EchoMessage {
|
||||
payload: usize,
|
||||
reply_to: ActorAddress,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct EchoResponse(usize);
|
||||
|
||||
struct EchoActor;
|
||||
|
||||
impl ActorInterface for EchoActor {
|
||||
type Incoming = EchoMessage;
|
||||
type Response = EchoResponse;
|
||||
|
||||
fn handle(&mut self, ctx: &Ctx, msg: EchoMessage) {
|
||||
let _ = ctx.send(msg.reply_to, EchoResponse(msg.payload));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_single_thread_spawn_actor_and_inbox() {
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
|
||||
let actor_addr = rt.spawn(EchoActor).expect("spawn echo actor");
|
||||
let inbox: Inbox<EchoResponse> = rt.new_inbox().unwrap();
|
||||
|
||||
rt.send_to(
|
||||
actor_addr,
|
||||
EchoMessage {
|
||||
payload: 42,
|
||||
reply_to: *inbox.addr(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for _ in 0..10 {
|
||||
rt.tick();
|
||||
if let Some(response) = inbox.try_recv() {
|
||||
assert_eq!(response, EchoResponse(42));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
panic!("Did not receive EchoResponse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_thread_spawn_actor_and_inbox() {
|
||||
let config = RuntimeConfig {
|
||||
num_threads: 4,
|
||||
..Default::default()
|
||||
};
|
||||
let rt = Runtime::new(config);
|
||||
|
||||
let actor_addr = rt.spawn(EchoActor).expect("spawn echo actor");
|
||||
let inbox: Inbox<EchoResponse> = rt.new_inbox().unwrap();
|
||||
|
||||
rt.send_to(
|
||||
actor_addr,
|
||||
EchoMessage {
|
||||
payload: 99,
|
||||
reply_to: *inbox.addr(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let handle = rt.run().unwrap();
|
||||
|
||||
let check = std::thread::spawn(move || {
|
||||
for _ in 0..100 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
if let Some(response) = inbox.try_recv() {
|
||||
handle.shutdown();
|
||||
return Some(response);
|
||||
}
|
||||
}
|
||||
handle.shutdown();
|
||||
None
|
||||
});
|
||||
|
||||
let result = check.join().unwrap();
|
||||
assert_eq!(result, Some(EchoResponse(99)));
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
use swactor::{actor::{ActorAddress, ActorInterface}, runtime::{Inbox, Runtime, RuntimeConfig}};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PingMessage {
|
||||
reply_to: ActorAddress,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PongMessage;
|
||||
|
||||
struct PongActor;
|
||||
|
||||
impl ActorInterface for PongActor {
|
||||
type Incoming = PingMessage;
|
||||
type Response = PongMessage;
|
||||
|
||||
fn handle(&mut self, ctx: &Runtime, msg: PingMessage) {
|
||||
let _ = ctx.send_to(msg.reply_to, PongMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/// An actor that forwards messages to another address
|
||||
struct ForwarderActor {
|
||||
target: ActorAddress,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ForwardMessage(usize);
|
||||
|
||||
impl ActorInterface for ForwarderActor {
|
||||
type Incoming = ForwardMessage;
|
||||
type Response = ();
|
||||
|
||||
fn handle(&mut self, ctx: &Runtime, msg: ForwardMessage) {
|
||||
let _ = ctx.send_to(self.target, msg);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_threaded_ping_pong() {
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
let inbox: Inbox<PongMessage> = rt.new_inbox().unwrap();
|
||||
|
||||
let pong_addr = rt.spawn(PongActor).expect("spawn pong");
|
||||
|
||||
// Send ping
|
||||
rt.send_to(
|
||||
pong_addr,
|
||||
PingMessage {
|
||||
reply_to: *inbox.addr(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Tick until we get a response
|
||||
for _ in 0..10 {
|
||||
rt.tick();
|
||||
if inbox.try_recv().is_some() {
|
||||
return; // Success!
|
||||
}
|
||||
}
|
||||
|
||||
panic!("Did not receive pong response");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_threaded_message_chain() {
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
let inbox: Inbox<ForwardMessage> = rt.new_inbox().unwrap();
|
||||
|
||||
// Create a chain: A -> B -> C -> inbox
|
||||
let c_addr = rt
|
||||
.spawn(ForwarderActor {
|
||||
target: *inbox.addr(),
|
||||
})
|
||||
.unwrap();
|
||||
let b_addr = rt.spawn(ForwarderActor { target: c_addr }).unwrap();
|
||||
let a_addr = rt.spawn(ForwarderActor { target: b_addr }).unwrap();
|
||||
|
||||
// Send message to start of chain
|
||||
rt.send_to(a_addr, ForwardMessage(42)).unwrap();
|
||||
|
||||
// Tick until message arrives
|
||||
for _ in 0..20 {
|
||||
rt.tick();
|
||||
if let Some(ForwardMessage(val)) = inbox.try_recv() {
|
||||
assert_eq!(val, 42);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
panic!("Message did not traverse the chain");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multithreaded_message_passing() {
|
||||
let config = RuntimeConfig {
|
||||
num_threads: 4,
|
||||
..Default::default()
|
||||
};
|
||||
let rt = Runtime::new(config);
|
||||
let inbox: Inbox<ForwardMessage> = rt.new_inbox().unwrap();
|
||||
|
||||
// Create a longer chain to exercise multi-threading
|
||||
let mut target = *inbox.addr();
|
||||
for _ in 0..20 {
|
||||
target = rt.spawn(ForwarderActor { target }).unwrap();
|
||||
}
|
||||
let start_addr = target;
|
||||
|
||||
// Send message
|
||||
rt.send_to(start_addr, ForwardMessage(999)).unwrap();
|
||||
|
||||
// Spawn thread to check for result and shutdown
|
||||
let ctx = rt.run().unwrap();
|
||||
let inbox_check = std::thread::spawn(move || {
|
||||
for _ in 0..100 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
if let Some(ForwardMessage(val)) = inbox.try_recv() {
|
||||
ctx.shutdown();
|
||||
return Some(val);
|
||||
}
|
||||
}
|
||||
ctx.shutdown();
|
||||
None
|
||||
});
|
||||
|
||||
let result = inbox_check.join().unwrap();
|
||||
assert_eq!(result, Some(999));
|
||||
}
|
||||
|
|
@ -1,332 +0,0 @@
|
|||
//! Concurrency stress tests - hunt for race conditions.
|
||||
//!
|
||||
//! These tests target the shutdown races and concurrent access patterns
|
||||
//! that are most likely to expose bugs.
|
||||
|
||||
use super::{BlackHole, Msg};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use swactor::runtime::{Runtime, RuntimeConfig};
|
||||
|
||||
/// Shutdown while messages are in flight.
|
||||
/// Target: AtomicBool ordering bugs, use-after-shutdown.
|
||||
#[test]
|
||||
#[cfg(feature = "stress")]
|
||||
fn shutdown_under_load() {
|
||||
println!("\n>>> STRESS: Shutdown Under Load");
|
||||
|
||||
let mut panics = 0;
|
||||
let mut successes = 0;
|
||||
|
||||
// Run many iterations to catch rare races
|
||||
for iteration in 0..100 {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 100,
|
||||
router_max_messages: 10_000,
|
||||
actor_max_messages: 1000,
|
||||
num_threads: 4,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
|
||||
// Spawn actors
|
||||
let mut actors = Vec::new();
|
||||
for _ in 0..50 {
|
||||
if let Ok(addr) = runtime.spawn(BlackHole) {
|
||||
actors.push(addr);
|
||||
}
|
||||
}
|
||||
|
||||
let handle = runtime.run().unwrap();
|
||||
let rt = handle.runtime.clone();
|
||||
|
||||
// Sender thread - blast messages
|
||||
let actors_clone = actors.clone();
|
||||
let rt_send = rt.clone();
|
||||
let sender = thread::spawn(move || {
|
||||
for _ in 0..1000 {
|
||||
for actor in &actors_clone {
|
||||
let _ = rt_send.send_to::<Msg>(*actor, Msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Random delay before shutdown
|
||||
let delay = Duration::from_micros((iteration * 17) % 500);
|
||||
thread::sleep(delay);
|
||||
|
||||
// Shutdown while sender is still going
|
||||
handle.shutdown();
|
||||
|
||||
// Wait for sender (it should not panic)
|
||||
let _ = sender.join();
|
||||
|
||||
// Join should complete (not hang)
|
||||
handle.join();
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(_) => successes += 1,
|
||||
Err(_) => panics += 1,
|
||||
}
|
||||
}
|
||||
|
||||
println!(" Iterations: 100");
|
||||
println!(" Successes: {}", successes);
|
||||
println!(" Panics: {}", panics);
|
||||
|
||||
if panics > 0 {
|
||||
println!(">>> FAIL: {} panics detected during shutdown\n", panics);
|
||||
} else {
|
||||
println!(">>> PASS: No panics during shutdown under load\n");
|
||||
}
|
||||
|
||||
assert_eq!(panics, 0, "Shutdown under load caused panics");
|
||||
}
|
||||
|
||||
/// Send to actor immediately after spawn.
|
||||
/// Target: Race between spawn registration and first message.
|
||||
#[test]
|
||||
#[cfg(feature = "stress")]
|
||||
fn send_to_newborn() {
|
||||
println!("\n>>> STRESS: Send to Newborn Actor");
|
||||
|
||||
let mut total_spawned = 0;
|
||||
let mut total_send_ok = 0;
|
||||
let mut total_send_fail = 0;
|
||||
|
||||
for _ in 0..100 {
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 1000,
|
||||
router_max_messages: 10_000,
|
||||
actor_max_messages: 100,
|
||||
num_threads: 4,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
let handle = runtime.run().unwrap();
|
||||
|
||||
// Immediately spawn and send
|
||||
for _ in 0..50 {
|
||||
if let Ok(addr) = handle.runtime.spawn(BlackHole) {
|
||||
total_spawned += 1;
|
||||
// Send immediately - actor may not be registered yet
|
||||
if handle.runtime.send_to::<Msg>(addr, Msg).is_ok() {
|
||||
total_send_ok += 1;
|
||||
} else {
|
||||
total_send_fail += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handle.shutdown();
|
||||
handle.join();
|
||||
}
|
||||
|
||||
println!(" Total spawned: {}", total_spawned);
|
||||
println!(" Sends succeeded: {}", total_send_ok);
|
||||
println!(" Sends failed: {}", total_send_fail);
|
||||
|
||||
if total_send_fail > 0 {
|
||||
println!(">>> FAIL: {} messages failed to send\n", total_send_fail);
|
||||
} else {
|
||||
println!(">>> PASS: All messages succeeded\n");
|
||||
}
|
||||
|
||||
assert_eq!(total_send_fail, 0, "Race condition caused failed message delivery");
|
||||
|
||||
println!(">>> Test complete\n");
|
||||
}
|
||||
|
||||
/// FIXME: This test means nothing until we allow killing off actor processes
|
||||
/// Rapid spawn/despawn cycles.
|
||||
/// Target: Queue management under churn.
|
||||
#[test]
|
||||
#[cfg(feature = "stress")]
|
||||
fn rapid_spawn_churn() {
|
||||
println!("\n>>> STRESS: Rapid Spawn Churn");
|
||||
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 100,
|
||||
router_max_messages: 10_000,
|
||||
actor_max_messages: 100,
|
||||
num_threads: 4,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
let handle = runtime.run().unwrap();
|
||||
|
||||
let spawn_count = Arc::new(AtomicUsize::new(0));
|
||||
let fail_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Multiple threads spawning actors
|
||||
let mut threads = Vec::new();
|
||||
for _ in 0..4 {
|
||||
let rt = handle.runtime.clone();
|
||||
let spawns = spawn_count.clone();
|
||||
let fails = fail_count.clone();
|
||||
|
||||
threads.push(thread::spawn(move || {
|
||||
for _ in 0..500 {
|
||||
match rt.spawn(BlackHole) {
|
||||
Ok(_) => {
|
||||
spawns.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
Err(_) => {
|
||||
fails.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
// Small yield to increase interleaving
|
||||
thread::yield_now();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Let it churn
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
||||
handle.shutdown();
|
||||
|
||||
for t in threads {
|
||||
let _ = t.join();
|
||||
}
|
||||
handle.join();
|
||||
|
||||
let total_spawns = spawn_count.load(Ordering::Relaxed);
|
||||
let total_fails = fail_count.load(Ordering::Relaxed);
|
||||
|
||||
println!(" Spawn attempts: {}", total_spawns + total_fails);
|
||||
println!(" Successes: {}", total_spawns);
|
||||
println!(" Failures: {} (expected - queue fills)", total_fails);
|
||||
println!(">>> Test complete - no panics\n");
|
||||
}
|
||||
|
||||
/// Multiple threads sending to same actor.
|
||||
/// Target: Inbox contention, message ordering.
|
||||
#[test]
|
||||
#[cfg(feature = "stress")]
|
||||
fn inbox_contention() {
|
||||
println!("\n>>> STRESS: Inbox Contention");
|
||||
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 10,
|
||||
router_max_messages: 100_000,
|
||||
actor_max_messages: 10_000,
|
||||
num_threads: 4,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
let target = runtime.spawn(BlackHole).unwrap();
|
||||
let handle = runtime.run().unwrap();
|
||||
|
||||
// Wait for registration
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
|
||||
let send_count = Arc::new(AtomicUsize::new(0));
|
||||
let fail_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// 8 threads all sending to same actor
|
||||
let mut threads = Vec::new();
|
||||
for _ in 0..8 {
|
||||
let rt = handle.runtime.clone();
|
||||
let sends = send_count.clone();
|
||||
let fails = fail_count.clone();
|
||||
|
||||
threads.push(thread::spawn(move || {
|
||||
for _ in 0..10_000 {
|
||||
if rt.send_to::<Msg>(target, Msg).is_ok() {
|
||||
sends.fetch_add(1, Ordering::Relaxed);
|
||||
} else {
|
||||
fails.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for t in threads {
|
||||
let _ = t.join();
|
||||
}
|
||||
|
||||
// Let messages process
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
|
||||
handle.shutdown();
|
||||
handle.join();
|
||||
|
||||
let total_sends = send_count.load(Ordering::Relaxed);
|
||||
let total_fails = fail_count.load(Ordering::Relaxed);
|
||||
|
||||
println!(" Threads: 8");
|
||||
println!(" Msgs per thread: 10,000");
|
||||
println!(" Total sent: {}", total_sends);
|
||||
println!(" Total failed: {}", total_fails);
|
||||
println!(
|
||||
" Success rate: {:.1}%",
|
||||
(total_sends as f64 / (total_sends + total_fails) as f64) * 100.0
|
||||
);
|
||||
println!(">>> Test complete - no panics\n");
|
||||
}
|
||||
|
||||
/// FIXME: Not sure this test is meaningful.
|
||||
/// Shutdown timing fuzz - randomize when shutdown is called.
|
||||
/// Target: Edge cases in shutdown state machine.
|
||||
#[test]
|
||||
#[cfg(feature = "stress")]
|
||||
fn shutdown_timing_fuzz() {
|
||||
println!("\n>>> STRESS: Shutdown Timing Fuzz");
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
for delay_us in [0, 1, 10, 100, 1000, 5000] {
|
||||
let mut ok = 0;
|
||||
let mut fail = 0;
|
||||
|
||||
for _ in 0..20 {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 50,
|
||||
router_max_messages: 1000,
|
||||
actor_max_messages: 100,
|
||||
num_threads: 4,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
|
||||
for _ in 0..20 {
|
||||
let _ = runtime.spawn(BlackHole);
|
||||
}
|
||||
|
||||
let handle = runtime.run().unwrap();
|
||||
|
||||
// Specific delay
|
||||
if delay_us > 0 {
|
||||
thread::sleep(Duration::from_micros(delay_us));
|
||||
}
|
||||
|
||||
handle.shutdown();
|
||||
handle.join();
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(_) => ok += 1,
|
||||
Err(_) => fail += 1,
|
||||
}
|
||||
}
|
||||
|
||||
results.push((delay_us, ok, fail));
|
||||
}
|
||||
|
||||
println!(" delay_us ok fail");
|
||||
println!(" -------- -- ----");
|
||||
for (delay, ok, fail) in &results {
|
||||
println!(" {:>8} {:>2} {:>4}", delay, ok, fail);
|
||||
}
|
||||
|
||||
let total_fails: i32 = results.iter().map(|(_, _, f)| *f).sum();
|
||||
if total_fails > 0 {
|
||||
println!(
|
||||
"\n>>> FAIL: {} panics across timing variations",
|
||||
total_fails
|
||||
);
|
||||
} else {
|
||||
println!("\n>>> PASS: All timing variations succeeded");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,199 +0,0 @@
|
|||
//! Stress test utilities and result reporting.
|
||||
//!
|
||||
//! Provides a simple framework for stress tests with JSON + pretty output.
|
||||
|
||||
#![allow(dead_code)] // Utilities may not all be used in every test
|
||||
|
||||
pub mod concurrency;
|
||||
pub mod saturation;
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Results from a stress test
|
||||
#[derive(Debug)]
|
||||
pub struct StressResult {
|
||||
pub name: String,
|
||||
pub duration: Duration,
|
||||
pub operations: u64,
|
||||
pub successes: u64,
|
||||
pub failures: u64,
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
impl StressResult {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
duration: Duration::ZERO,
|
||||
operations: 0,
|
||||
successes: 0,
|
||||
failures: 0,
|
||||
notes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn failure_rate(&self) -> f64 {
|
||||
if self.operations == 0 {
|
||||
0.0
|
||||
} else {
|
||||
(self.failures as f64 / self.operations as f64) * 100.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn throughput(&self) -> f64 {
|
||||
let secs = self.duration.as_secs_f64();
|
||||
if secs > 0.0 {
|
||||
self.operations as f64 / secs
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn note(&mut self, msg: impl Into<String>) {
|
||||
self.notes.push(msg.into());
|
||||
}
|
||||
|
||||
pub fn print(&self) {
|
||||
println!("\n{}", "=".repeat(60));
|
||||
println!(" STRESS: {}", self.name);
|
||||
println!("{}", "=".repeat(60));
|
||||
println!(" Duration: {:?}", self.duration);
|
||||
println!(" Operations: {}", self.operations);
|
||||
println!(" Successes: {}", self.successes);
|
||||
println!(" Failures: {}", self.failures);
|
||||
println!(" Failure Rate: {:.2}%", self.failure_rate());
|
||||
println!(" Throughput: {:.2} ops/sec", self.throughput());
|
||||
|
||||
if !self.notes.is_empty() {
|
||||
println!();
|
||||
println!(" Notes:");
|
||||
for note in &self.notes {
|
||||
println!(" - {}", note);
|
||||
}
|
||||
}
|
||||
println!("{}", "=".repeat(60));
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> String {
|
||||
format!(
|
||||
r#"{{"name":"{}","duration_ms":{},"operations":{},"successes":{},"failures":{},"failure_rate_pct":{:.2},"throughput":{:.2},"notes":{:?}}}"#,
|
||||
self.name,
|
||||
self.duration.as_millis(),
|
||||
self.operations,
|
||||
self.successes,
|
||||
self.failures,
|
||||
self.failure_rate(),
|
||||
self.throughput(),
|
||||
self.notes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple stress test runner
|
||||
pub struct Stress {
|
||||
name: String,
|
||||
duration: Option<Duration>,
|
||||
iterations: Option<u64>,
|
||||
}
|
||||
|
||||
impl Stress {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
duration: None,
|
||||
iterations: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run for a fixed duration
|
||||
pub fn for_duration(mut self, d: Duration) -> Self {
|
||||
self.duration = Some(d);
|
||||
self
|
||||
}
|
||||
|
||||
/// Run for a fixed number of iterations
|
||||
pub fn for_iterations(mut self, n: u64) -> Self {
|
||||
self.iterations = Some(n);
|
||||
self
|
||||
}
|
||||
|
||||
/// Run the stress test, counting successes and failures
|
||||
pub fn run<F>(self, mut f: F) -> StressResult
|
||||
where
|
||||
F: FnMut() -> bool, // returns true on success, false on failure
|
||||
{
|
||||
let mut result = StressResult::new(&self.name);
|
||||
let start = Instant::now();
|
||||
|
||||
match (self.duration, self.iterations) {
|
||||
(Some(duration), _) => {
|
||||
while start.elapsed() < duration {
|
||||
if f() {
|
||||
result.successes += 1;
|
||||
} else {
|
||||
result.failures += 1;
|
||||
}
|
||||
result.operations += 1;
|
||||
}
|
||||
}
|
||||
(None, Some(iterations)) => {
|
||||
for _ in 0..iterations {
|
||||
if f() {
|
||||
result.successes += 1;
|
||||
} else {
|
||||
result.failures += 1;
|
||||
}
|
||||
result.operations += 1;
|
||||
}
|
||||
}
|
||||
(None, None) => {
|
||||
// Default: 1000 iterations
|
||||
for _ in 0..1000 {
|
||||
if f() {
|
||||
result.successes += 1;
|
||||
} else {
|
||||
result.failures += 1;
|
||||
}
|
||||
result.operations += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.duration = start.elapsed();
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
// Test actors used across stress tests
|
||||
use swactor::{actor::ActorInterface, runtime::Runtime};
|
||||
|
||||
/// An actor that just absorbs messages
|
||||
pub struct BlackHole;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Msg;
|
||||
|
||||
impl ActorInterface for BlackHole {
|
||||
type Incoming = Msg;
|
||||
type Response = ();
|
||||
fn handle(&mut self, _ctx: &Runtime, _msg: Msg) {}
|
||||
}
|
||||
|
||||
/// An actor that counts messages received
|
||||
pub struct Counter {
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
pub fn new() -> Self {
|
||||
Self { count: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl ActorInterface for Counter {
|
||||
type Incoming = Msg;
|
||||
type Response = ();
|
||||
fn handle(&mut self, _ctx: &Runtime, _msg: Msg) {
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
//! Saturation stress tests - find where the runtime breaks.
|
||||
//!
|
||||
//! These tests intentionally push past limits to document failure modes.
|
||||
|
||||
use super::{BlackHole, Counter, Msg, Stress, StressResult};
|
||||
use std::time::Duration;
|
||||
use swactor::runtime::{Runtime, RuntimeConfig};
|
||||
|
||||
/// Blast the router inbox
|
||||
#[test]
|
||||
#[cfg(feature = "stress")]
|
||||
fn router_inbox_overflow() {
|
||||
println!("\n>>> STRESS: Router Inbox Overflow");
|
||||
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 10,
|
||||
router_max_messages: 100, // Tiny buffer
|
||||
actor_max_messages: 1000,
|
||||
num_threads: 1,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
let sink = runtime.spawn(BlackHole).unwrap();
|
||||
|
||||
// Blast messages without processing
|
||||
let mut result = StressResult::new("router_inbox_overflow");
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
for _ in 0..10_000 {
|
||||
result.operations += 1;
|
||||
if runtime.send_to::<Msg>(sink, Msg).is_ok() {
|
||||
result.successes += 1;
|
||||
} else {
|
||||
result.failures += 1;
|
||||
}
|
||||
}
|
||||
|
||||
result.duration = start.elapsed();
|
||||
result.note(format!("Router buffer: 100, Messages sent: 10,000"));
|
||||
|
||||
// With hybrid, no failures expected
|
||||
assert_eq!(result.failures, 0, "Hybrid channel should not reject");
|
||||
result.print();
|
||||
println!(">>> PASS: Hybrid channel prevented router overflow\n");
|
||||
}
|
||||
|
||||
/// Blast a single actor's inbox
|
||||
#[test]
|
||||
#[cfg(feature = "stress")]
|
||||
fn actor_inbox_overflow() {
|
||||
println!("\n>>> STRESS: Actor Inbox Overflow");
|
||||
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 10,
|
||||
router_max_messages: 100_000, // Large router buffer
|
||||
actor_max_messages: 100, // Tiny actor inbox
|
||||
num_threads: 1,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
let sink = runtime.spawn(Counter::new()).unwrap();
|
||||
|
||||
// Process router registration
|
||||
runtime.tick();
|
||||
|
||||
// Now blast messages - router will accept them but actor inbox will fill
|
||||
let mut sent = 0u64;
|
||||
let mut router_failed = 0u64;
|
||||
for _ in 0..10_000 {
|
||||
if runtime.send_to::<Msg>(sink, Msg).is_ok() {
|
||||
sent += 1;
|
||||
} else {
|
||||
router_failed += 1;
|
||||
}
|
||||
// Tick occasionally to let router deliver
|
||||
if sent % 100 == 0 {
|
||||
runtime.tick();
|
||||
}
|
||||
}
|
||||
|
||||
// Process all remaining messages
|
||||
for _ in 0..5000 {
|
||||
runtime.tick();
|
||||
}
|
||||
|
||||
println!(" Router accepted: {}", sent);
|
||||
println!(" Router rejected: {}", router_failed);
|
||||
|
||||
assert_eq!(router_failed, 0, "Router rejected message under load");
|
||||
println!(">>> PASS: No message loss with hybrid channel\n");
|
||||
}
|
||||
|
||||
/// Blast the runtime with actor spawns
|
||||
#[test]
|
||||
#[cfg(feature = "stress")]
|
||||
fn actor_queue_overflow() {
|
||||
println!("\n>>> STRESS: Actor Queue Overflow");
|
||||
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 100, // Small actor queue
|
||||
router_max_messages: 10_000,
|
||||
actor_max_messages: 100,
|
||||
num_threads: 1,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
|
||||
let mut result = StressResult::new("actor_queue_overflow");
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
// Try to spawn 500 actors into 100-slot queue
|
||||
for _ in 0..500 {
|
||||
result.operations += 1;
|
||||
match runtime.spawn(BlackHole) {
|
||||
Ok(_) => result.successes += 1,
|
||||
Err(_) => result.failures += 1,
|
||||
}
|
||||
}
|
||||
|
||||
result.duration = start.elapsed();
|
||||
result.note(format!("Queue capacity: 100, Spawn attempts: 500"));
|
||||
result.print();
|
||||
|
||||
// Note: Router also takes a slot, so we expect ~99 actors max
|
||||
assert_eq!(
|
||||
result.failures, 0,
|
||||
"Spawned more actors than queue capacity"
|
||||
);
|
||||
println!(">>> PASS: Actor queue correctly rejects when full\n");
|
||||
}
|
||||
|
||||
/// FIXME: IS this actually testing what it should be?
|
||||
/// Sustained overload - run at 2x capacity for extended period.
|
||||
/// Documents: Does the system degrade gracefully or crash?
|
||||
#[test]
|
||||
#[cfg(feature = "stress")]
|
||||
fn sustained_overload() {
|
||||
println!("\n>>> STRESS: Sustained Overload");
|
||||
|
||||
let config = RuntimeConfig {
|
||||
max_actors: 100,
|
||||
router_max_messages: 1000,
|
||||
actor_max_messages: 100,
|
||||
num_threads: 1,
|
||||
};
|
||||
let runtime = Runtime::new(config);
|
||||
|
||||
// Spawn some actors
|
||||
let mut actors = Vec::new();
|
||||
for _ in 0..50 {
|
||||
if let Ok(addr) = runtime.spawn(Counter::new()) {
|
||||
actors.push(addr);
|
||||
}
|
||||
}
|
||||
|
||||
// Process registrations
|
||||
for _ in 0..200 {
|
||||
runtime.tick();
|
||||
}
|
||||
|
||||
let result = Stress::new("sustained_overload")
|
||||
.for_duration(Duration::from_secs(2))
|
||||
.run(|| {
|
||||
// Send to random actor
|
||||
let idx = (std::time::Instant::now().elapsed().as_nanos() as usize) % actors.len();
|
||||
let success = runtime.send_to::<Msg>(actors[idx], Msg).is_ok();
|
||||
|
||||
// Process some (but not all) - simulating overload
|
||||
runtime.tick();
|
||||
|
||||
success
|
||||
});
|
||||
|
||||
result.print();
|
||||
println!(">>> System survived sustained overload without panic\n");
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
//! Stress test suite for swactor runtime.
|
||||
//!
|
||||
//! Run with: cargo test --features stress stress_ -- --nocapture
|
||||
//!
|
||||
//! These tests are hidden behind the `stress` feature flag because they:
|
||||
//! - Take longer to run
|
||||
//! - Intentionally push the system to failure
|
||||
//! - May produce different results on different machines
|
||||
|
||||
#[cfg(feature = "stress")]
|
||||
mod stress;
|
||||
Loading…
Reference in a new issue