aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments/019-idle-links/quic
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-28 19:41:08 +0100
committereunhhu <[email protected]>2026-09-28 19:41:08 +0100
commit63cc9554f2cd93e46d0413a21cbce05014e2f2b3 (patch)
tree1479bb368b8fa4d2dcac3c43d73c9b34ffca41dc /research/experiments/019-idle-links/quic
parenta83d663404e030a8734fddc8827fb4afe309a1b9 (diff)
downloadpure-63cc9554f2cd93e46d0413a21cbce05014e2f2b3.tar.gz
pure-63cc9554f2cd93e46d0413a21cbce05014e2f2b3.zip
Measure a link at rest: 132 bytes against a QUIC connection's 38,106 (094)
M1 of design 011. A Pure node allows 1,000 to 1,000,000 senders; each sends one order and, once told it is stored, rests. At rest a link costs the node 132 bytes (the sender allowed, 73; its number heard, about 60) and no transport state; with a million the node runs in 139 MB. A quinn 0.11 service holding the same exchange's clients connected at rest costs 38,106 bytes a connection: 289 times as much, past the design's 1/100. A link at rest takes its next record in 0.56 ms to stored, one X25519 on the node included; a new QUIC connection takes 1.06 ms and 2.8 times the receiver's CPU, without storing. research/experiments/019-idle-links: links.c, many senders written from serve/link.h's packet format alone (a second implementation), and the quinn service and clients (quic/, built offline); run.py makes both measurements.
Diffstat (limited to 'research/experiments/019-idle-links/quic')
-rw-r--r--research/experiments/019-idle-links/quic/Cargo.lock634
-rw-r--r--research/experiments/019-idle-links/quic/Cargo.toml18
-rw-r--r--research/experiments/019-idle-links/quic/src/main.rs329
3 files changed, 981 insertions, 0 deletions
diff --git a/research/experiments/019-idle-links/quic/Cargo.lock b/research/experiments/019-idle-links/quic/Cargo.lock
new file mode 100644
index 0000000..f31be94
--- /dev/null
+++ b/research/experiments/019-idle-links/quic/Cargo.lock
@@ -0,0 +1,634 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+
+[[package]]
+name = "cc"
+version = "1.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
+
+[[package]]
+name = "chacha20"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "rand_core",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
+
+[[package]]
+name = "futures-core"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+
+[[package]]
+name = "futures-task"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
+
+[[package]]
+name = "futures-util"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi",
+ "rand_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "idle-quic"
+version = "0.1.0"
+dependencies = [
+ "quinn",
+ "ring",
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "mio"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quinn"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"
+dependencies = [
+ "bytes",
+ "getrandom 0.4.3",
+ "lru-slab",
+ "rand",
+ "rand_pcg",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.3",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "rand_pcg"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustls"
+version = "0.23.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "socket2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[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.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[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.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[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 = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
diff --git a/research/experiments/019-idle-links/quic/Cargo.toml b/research/experiments/019-idle-links/quic/Cargo.toml
new file mode 100644
index 0000000..cc0f527
--- /dev/null
+++ b/research/experiments/019-idle-links/quic/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "idle-quic"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+# The QUIC side of M1 (../run.py, docs/design/011-pure-transport.md): quinn
+# 0.11 with rustls and ring, as a Rust service would use it.
+[dependencies]
+quinn = { version = "0.11", default-features = false, features = ["rustls-ring", "runtime-tokio"] }
+rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
+tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "time", "net", "sync", "io-util", "io-std"] }
+ring = "0.17"
+
+[profile.release]
+debug = 1
+
+[workspace]
diff --git a/research/experiments/019-idle-links/quic/src/main.rs b/research/experiments/019-idle-links/quic/src/main.rs
new file mode 100644
index 0000000..cdb0376
--- /dev/null
+++ b/research/experiments/019-idle-links/quic/src/main.rs
@@ -0,0 +1,329 @@
+//! A QUIC service holding many clients at rest, for M1 of
+//! docs/design/011-pure-transport.md: the same exchange as a Pure link's
+//! (a client sends one order, the service stores it, says so, and the two
+//! stay connected, quiet), over quinn 0.11 with rustls (AES-128-GCM).
+//!
+//! idle-quic serve CERT.der KEY.der [PORT]
+//! a service on 127.0.0.1 (the port on stdout as JSON), one thread;
+//! a line of JSON on stdout at every 1,000th order stored. Its
+//! stateless reset key comes from KEY.der, so one started again on
+//! the same port resets the connections of the one before at once
+//! idle-quic connect PORT FIRST COUNT WINDOW
+//! clients FIRST.. each connect, send order 1 and wait until it is
+//! stored, WINDOW at a time over one endpoint; a line of JSON; the
+//! connections stay open until stdin closes; then COUNT orders more
+//! (number 2) over them, and another line
+//! idle-quic steady PORT FIRST COUNT PERIOD_MS SECONDS WINDOW
+//! clients FIRST.. connect (WINDOW at a time), then each makes an
+//! order every PERIOD_MS for SECONDS and sends it over its connection;
+//! one that fails (no answer in a second, or the connection lost)
+//! connects again and sends it again. Then a line of JSON: orders made
+//! and stored, connections made again, the orders stored in each
+//! 100 ms, the time from an order made to it stored
+use std::collections::HashMap;
+use std::net::SocketAddr;
+use std::sync::{Arc, Mutex};
+use std::time::{Duration, Instant};
+
+use quinn::crypto::rustls::QuicClientConfig;
+use quinn::{ClientConfig, Endpoint, IdleTimeout, ServerConfig, TransportConfig, VarInt};
+use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
+use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName, UnixTime};
+use rustls::{DigitallySignedStruct, SignatureScheme};
+use tokio::io::AsyncReadExt;
+
+fn transport() -> Arc<TransportConfig> {
+ let mut t = TransportConfig::default();
+ t.max_idle_timeout(Some(IdleTimeout::from(VarInt::from_u32(3_600_000)))); // an hour: connections at rest stay
+ t.keep_alive_interval(None);
+ Arc::new(t)
+}
+
+/// The order: the client's number and the order's.
+fn order(client: u64, seq: u64) -> [u8; 64] {
+ let mut bytes = [0u8; 64];
+ bytes[..8].copy_from_slice(&client.to_le_bytes());
+ bytes[8..16].copy_from_slice(&seq.to_le_bytes());
+ bytes[16..19].copy_from_slice(b"pen");
+ bytes
+}
+
+fn serve(cert: Vec<u8>, key: Vec<u8>, port: u16) {
+ let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
+ runtime.block_on(async move {
+ let reset = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, ring::digest::digest(&ring::digest::SHA256, &key).as_ref());
+ let mut config = ServerConfig::with_single_cert(
+ vec![CertificateDer::from(cert)],
+ PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key)),
+ )
+ .unwrap();
+ config.transport_config(transport());
+ let socket = std::net::UdpSocket::bind(SocketAddr::from(([127, 0, 0, 1], port))).unwrap();
+ let endpoint = Endpoint::new(quinn::EndpointConfig::new(Arc::new(reset)), Some(config), socket,
+ quinn::default_runtime().unwrap()).unwrap();
+ println!("{{\"port\":{}}}", endpoint.local_addr().unwrap().port());
+ // What the service keeps: each client's last order heard, and its connection.
+ let heard: Arc<Mutex<HashMap<u64, u64>>> = Arc::default();
+ let kept: Arc<Mutex<Vec<quinn::Connection>>> = Arc::default();
+ let stored = Arc::new(std::sync::atomic::AtomicU64::new(0));
+ while let Some(incoming) = endpoint.accept().await {
+ let (heard, kept, stored) = (heard.clone(), kept.clone(), stored.clone());
+ tokio::spawn(async move {
+ let Ok(connection) = incoming.await else { return };
+ kept.lock().unwrap().push(connection.clone());
+ while let Ok((mut send, mut recv)) = connection.accept_bi().await {
+ let Ok(bytes) = recv.read_to_end(1024).await else { return };
+ if bytes.len() < 16 {
+ return;
+ }
+ let client = u64::from_le_bytes(bytes[..8].try_into().unwrap());
+ let seq = u64::from_le_bytes(bytes[8..16].try_into().unwrap());
+ let fresh = {
+ let mut heard = heard.lock().unwrap();
+ let last = heard.entry(client).or_insert(0);
+ let fresh = seq > *last;
+ *last = (*last).max(seq);
+ fresh
+ };
+ let _ = send.write_all(&[1]).await;
+ let _ = send.finish();
+ if fresh {
+ let n = stored.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
+ if n % 1000 == 0 {
+ println!("{{\"stored\":{n}}}");
+ }
+ }
+ }
+ });
+ }
+ });
+}
+
+#[derive(Debug)]
+struct Anyone; // the test's own service: its certificate is not checked
+
+impl ServerCertVerifier for Anyone {
+ fn verify_server_cert(&self, _: &CertificateDer<'_>, _: &[CertificateDer<'_>], _: &ServerName<'_>, _: &[u8],
+ _: UnixTime) -> Result<ServerCertVerified, rustls::Error> {
+ Ok(ServerCertVerified::assertion())
+ }
+ fn verify_tls12_signature(&self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct)
+ -> Result<HandshakeSignatureValid, rustls::Error> {
+ rustls::crypto::verify_tls12_signature(message, cert, dss,
+ &rustls::crypto::ring::default_provider().signature_verification_algorithms)
+ }
+ fn verify_tls13_signature(&self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct)
+ -> Result<HandshakeSignatureValid, rustls::Error> {
+ rustls::crypto::verify_tls13_signature(message, cert, dss,
+ &rustls::crypto::ring::default_provider().signature_verification_algorithms)
+ }
+ fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
+ rustls::crypto::ring::default_provider().signature_verification_algorithms.supported_schemes()
+ }
+}
+
+fn percentile(values: &mut [f64], p: f64) -> f64 {
+ if values.is_empty() {
+ return 0.0;
+ }
+ values.sort_by(|a, b| a.partial_cmp(b).unwrap());
+ values[((values.len() as f64 * p) as usize).min(values.len() - 1)]
+}
+
+fn client_config() -> ClientConfig {
+ let crypto = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
+ .with_protocol_versions(&[&rustls::version::TLS13])
+ .unwrap()
+ .dangerous()
+ .with_custom_certificate_verifier(Arc::new(Anyone))
+ .with_no_client_auth();
+ let mut config = ClientConfig::new(Arc::new(QuicClientConfig::try_from(crypto).unwrap()));
+ config.transport_config(transport());
+ config
+}
+
+async fn exchange(connection: &quinn::Connection, client: u64, seq: u64) -> bool {
+ let Ok((mut send, mut recv)) = connection.open_bi().await else { return false };
+ if send.write_all(&order(client, seq)).await.is_err() || send.finish().is_err() {
+ return false;
+ }
+ matches!(recv.read_to_end(16).await, Ok(answer) if answer == [1])
+}
+
+fn connect(port: u16, first: u64, count: u64, window: usize) {
+ let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
+ runtime.block_on(async move {
+ let mut endpoint = Endpoint::client(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap();
+ endpoint.set_default_client_config(client_config());
+ let server = SocketAddr::from(([127, 0, 0, 1], port));
+ let window = Arc::new(tokio::sync::Semaphore::new(window));
+ let kept: Arc<Mutex<Vec<quinn::Connection>>> = Arc::default();
+ let waited: Arc<Mutex<Vec<f64>>> = Arc::default();
+ let began = Instant::now();
+ let mut tasks = Vec::new();
+ for client in first..first + count {
+ let permit = window.clone().acquire_owned().await.unwrap();
+ let (endpoint, kept, waited) = (endpoint.clone(), kept.clone(), waited.clone());
+ tasks.push(tokio::spawn(async move {
+ let started = Instant::now();
+ let Ok(connecting) = endpoint.connect(server, "localhost") else { return false };
+ let Ok(connection) = connecting.await else { return false };
+ let ok = exchange(&connection, client, 1).await;
+ waited.lock().unwrap().push(started.elapsed().as_secs_f64() * 1000.0);
+ kept.lock().unwrap().push(connection);
+ drop(permit);
+ ok
+ }));
+ }
+ let mut failed = 0;
+ for task in tasks {
+ if !task.await.unwrap_or(false) {
+ failed += 1;
+ }
+ }
+ let seconds = began.elapsed().as_secs_f64();
+ let mut w = waited.lock().unwrap().clone();
+ println!("{{\"clients\":{count},\"failed\":{failed},\"seconds\":{seconds:.3},\"per_second\":{:.0},\"stored_ms_p50\":{:.3},\"stored_ms_p99\":{:.3}}}",
+ count as f64 / seconds, percentile(&mut w, 0.5), percentile(&mut w, 0.99));
+ // At rest until stdin closes; then one more order each, over the same connections.
+ let mut rest = Vec::new();
+ let _ = tokio::io::stdin().read_to_end(&mut rest).await;
+ let connections = kept.lock().unwrap().clone();
+ let began = Instant::now();
+ let mut again = 0;
+ for (k, connection) in connections.iter().enumerate() {
+ if exchange(connection, first + k as u64, 2).await {
+ again += 1;
+ }
+ }
+ println!("{{\"again\":{again},\"seconds\":{:.3}}}", began.elapsed().as_secs_f64());
+ tokio::time::sleep(Duration::from_millis(100)).await;
+ });
+}
+
+/// Clients that keep sending, connecting again when their connection fails.
+fn steady(port: u16, first: u64, count: u64, period: Duration, seconds: u64, window: usize) {
+ let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
+ runtime.block_on(async move {
+ let mut endpoint = Endpoint::client(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap();
+ endpoint.set_default_client_config(client_config());
+ let server = SocketAddr::from(([127, 0, 0, 1], port));
+ let connecting = Arc::new(tokio::sync::Semaphore::new(window));
+ let began = Instant::now();
+ let end = began + Duration::from_secs(seconds) + Duration::from_secs(2);
+ let buckets = ((seconds + 70) * 10) as usize;
+ let timeline: Arc<Vec<std::sync::atomic::AtomicU32>> =
+ Arc::new((0..buckets).map(|_| std::sync::atomic::AtomicU32::new(0)).collect());
+ let waited: Arc<Mutex<Vec<f64>>> = Arc::default();
+ let (made, again) = (Arc::new(std::sync::atomic::AtomicU64::new(0)), Arc::new(std::sync::atomic::AtomicU64::new(0)));
+ let mut tasks = Vec::new();
+ for (k, client) in (first..first + count).enumerate() {
+ let (endpoint, connecting, timeline, waited, made, again) =
+ (endpoint.clone(), connecting.clone(), timeline.clone(), waited.clone(), made.clone(), again.clone());
+ let start = began + Duration::from_secs(2) + period.mul_f64(k as f64 / count as f64);
+ tasks.push(tokio::spawn(async move {
+ let mut connection: Option<quinn::Connection> = None;
+ let mut seq = 0u64;
+ let mut next = start;
+ loop {
+ if connection.is_none() {
+ let permit = connecting.acquire().await.unwrap();
+ if let Ok(c) = endpoint.connect(server, "localhost") {
+ if let Ok(Ok(c)) = tokio::time::timeout(Duration::from_secs(2), c).await {
+ connection = Some(c);
+ }
+ }
+ drop(permit);
+ if connection.is_none() {
+ tokio::time::sleep(Duration::from_millis(200)).await;
+ continue;
+ }
+ }
+ if Instant::now() >= end {
+ return;
+ }
+ tokio::time::sleep_until(next.into()).await;
+ seq += 1;
+ made.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
+ let order_made = Instant::now();
+ next += period;
+ loop {
+ let c = connection.clone().unwrap();
+ let done = tokio::time::timeout(Duration::from_secs(1), exchange(&c, client, seq)).await;
+ if matches!(done, Ok(true)) {
+ let now = Instant::now();
+ let bucket = ((now - began).as_millis() / 100) as usize;
+ if bucket < timeline.len() {
+ timeline[bucket].fetch_add(1, std::sync::atomic::Ordering::Relaxed);
+ }
+ waited.lock().unwrap().push((now - order_made).as_secs_f64() * 1000.0);
+ break;
+ }
+ // Lost: a new connection (a handshake), and the order again.
+ again.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
+ connection = None;
+ while connection.is_none() {
+ let permit = connecting.acquire().await.unwrap();
+ if let Ok(c) = endpoint.connect(server, "localhost") {
+ if let Ok(Ok(c)) = tokio::time::timeout(Duration::from_secs(2), c).await {
+ connection = Some(c);
+ }
+ }
+ drop(permit);
+ if connection.is_none() {
+ tokio::time::sleep(Duration::from_millis(200)).await;
+ }
+ }
+ }
+ if Instant::now() >= end {
+ return;
+ }
+ }
+ }));
+ }
+ for task in tasks {
+ let _ = task.await;
+ }
+ let mut w = waited.lock().unwrap().clone();
+ let stored = w.len();
+ let timeline: Vec<u32> = timeline.iter().map(|b| b.load(std::sync::atomic::Ordering::Relaxed)).collect();
+ let last = timeline.iter().rposition(|&b| b > 0).unwrap_or(0);
+ let (mut longest, mut gap, mut since) = (0usize, 0usize, None::<usize>);
+ for (b, &n) in timeline.iter().enumerate().take(last + 1) {
+ if n > 0 {
+ if let Some(s) = since {
+ if b - s - 1 > longest {
+ longest = b - s - 1;
+ gap = s + 1;
+ }
+ }
+ since = Some(b);
+ }
+ }
+ let series: Vec<String> = timeline[..=last].iter().map(|n| n.to_string()).collect();
+ println!("{{\"clients\":{count},\"made\":{},\"stored\":{stored},\"again\":{},\"stored_ms_p50\":{:.3},\"stored_ms_p99\":{:.3},\"stored_ms_max\":{:.3},\"longest_gap_ms\":{},\"gap_from_ms\":{},\"timeline_100ms\":[{}]}}",
+ made.load(std::sync::atomic::Ordering::Relaxed), again.load(std::sync::atomic::Ordering::Relaxed),
+ percentile(&mut w, 0.5), percentile(&mut w, 0.99), percentile(&mut w, 1.0), longest * 100, gap * 100,
+ series.join(","));
+ });
+}
+
+fn main() {
+ let args: Vec<String> = std::env::args().collect();
+ match args.get(1).map(String::as_str) {
+ Some("serve") if args.len() == 4 || args.len() == 5 => serve(std::fs::read(&args[2]).unwrap(),
+ std::fs::read(&args[3]).unwrap(),
+ args.get(4).map_or(0, |p| p.parse().unwrap())),
+ Some("steady") if args.len() == 8 => steady(args[2].parse().unwrap(), args[3].parse().unwrap(),
+ args[4].parse().unwrap(),
+ Duration::from_millis(args[5].parse().unwrap()),
+ args[6].parse().unwrap(), args[7].parse().unwrap()),
+ Some("connect") if args.len() == 6 => connect(args[2].parse().unwrap(), args[3].parse().unwrap(),
+ args[4].parse().unwrap(), args[5].parse().unwrap()),
+ _ => {
+ eprintln!("idle-quic serve CERT.der KEY.der [PORT] | idle-quic connect PORT FIRST COUNT WINDOW | \
+ idle-quic steady PORT FIRST COUNT PERIOD_MS SECONDS WINDOW");
+ std::process::exit(2);
+ }
+ }
+}