diff --git a/.gitignore b/.gitignore index 9cf6a78..149aac7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ xctrace build cmake-* .vscode +.worktrees/ release benchmarks* Makefile diff --git a/CMakeLists.txt b/CMakeLists.txt index a34102b..bcba5db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -177,6 +177,14 @@ target_include_directories(test_pgas_controller_counters PRIVATE include src ${s target_link_libraries(test_pgas_controller_counters cxlmemsim spdlog::spdlog_header_only ${RT_LIB} ${ATOMIC_LIB}) add_test(NAME test_pgas_controller_counters COMMAND test_pgas_controller_counters) +add_executable(test_pgas_poll_policy tests/test_pgas_poll_policy.cpp) +target_include_directories(test_pgas_poll_policy PRIVATE include) +add_test(NAME test_pgas_poll_policy COMMAND test_pgas_poll_policy) + +add_executable(test_cxl_memsim_wait_policy tests/test_cxl_memsim_wait_policy.c) +target_include_directories(test_cxl_memsim_wait_policy PRIVATE lib/qemu/include) +add_test(NAME test_cxl_memsim_wait_policy COMMAND test_cxl_memsim_wait_policy) + add_executable(test_coherence_protocol_v2 tests/test_coherence_protocol_v2.cpp) target_include_directories(test_coherence_protocol_v2 PRIVATE include) target_link_libraries(test_coherence_protocol_v2 cxlmemsim) diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/README.md b/artifact/pgas_fast_poll/2026-08-20T2255Z/README.md new file mode 100644 index 0000000..75e0575 --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/README.md @@ -0,0 +1,82 @@ +# PGAS fast-poll live evidence + +## Configuration + +The server ran the synchronous PGAS shared-memory protocol with one polling +worker and transport-only accounting: + +```text +./build/cxlmemsim_server --comm-mode=pgas-shm \ + --pgas-shm-name=/cxlmemsim_pgas --capacity=256 \ + --pgas-workers=1 --pgas-spin-us=1000 --pgas-yield-count=10 \ + --pgas-idle-sleep-us=100 --pgas-record-accesses=false +``` + +`record-accesses=false` skips the controller's linear per-address occupation +history. It does not skip address validation, data reads/writes, synchronous +responses, or protocol counters. + +The guest allocator mapped the working buffer `rw-s` from `/dev/dax0.0`, and +the device remained bound to `/sys/bus/dax/drivers/device_dax`. + +## Adaptive-client result + +The exact guest command was: + +```text +LD_PRELOAD=target/debug/libcxlalloc_preload.so \ + /root/lmbench/bin/x86_64-linux-gnu/lat_mem_rd -t -N 4 128 64 +``` + +The adaptive QEMU client completed all 115 numeric rows with no zero values and +exit status 0. Its final row was: + +```text +128.00000 5009.636 +``` + +QEMU reported a 497.7 ns average successful server-response wait after +771,000,000 operations. It also reported one isolated 100 millisecond response +timeout; this is retained as a limitation even though the benchmark completed. + +## Original-wait mediated control + +The same QEMU integration checkout was rebuilt with the adaptive wait reverted. +To avoid repeating the full logarithmic sweep, only the 128 MiB endpoint was +selected while retaining `-N 4`: + +```text +LD_PRELOAD=target/debug/libcxlalloc_preload.so \ + /root/lmbench/bin/x86_64-linux-gnu/lat_mem_rd -t -N 4 \ + -s 134217728 -e 134217728 128 64 +``` + +That control exited 0 at: + +```text +128.00000 31963.106 +``` + +Server reads advanced from 404,900,000 to 430,000,000 and writes from +370,898,371 to 400,260,303 during this endpoint, proving mediation. Relative +to this control, adaptive polling reduced endpoint latency by 84.3%. + +## Rejected direct control + +An installed-QEMU run completed at 4062.530 ns/load, but the server log and +counters did not advance during that run. It is retained as a non-mediated +devdax lower-bound reference and is not used as evidence of server-path +performance. + +## Correctness boundary + +- Adaptive run: 115 numeric rows, zero zero-valued rows, exit status 0. +- Original-wait endpoint: one numeric row, exit status 0. +- `/dev/dax0.0` remained in `devdax` mode. +- No invalid-opcode trap or `lat_mem_rd` segfault appeared in either accepted + mediated run. + +The remaining approximately 5 microseconds includes KVM MMIO exit, QEMU +dispatch, and a synchronous memory-server round trip. The approximately 4 +microsecond non-mediated result is not an achievable server-path target without +changing the per-load mediation requirement. diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/acceptance-oldclient.after b/artifact/pgas_fast_poll/2026-08-20T2255Z/acceptance-oldclient.after new file mode 100644 index 0000000..8420a96 --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/acceptance-oldclient.after @@ -0,0 +1,5 @@ +boot_id=e1794343-43fe-41f0-bc19-bc1c84486528 +dmesg_lines=675 +invalid_opcode=0 +segfaults=0 +driver=/sys/bus/dax/drivers/device_dax diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/acceptance-oldclient.before b/artifact/pgas_fast_poll/2026-08-20T2255Z/acceptance-oldclient.before new file mode 100644 index 0000000..8420a96 --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/acceptance-oldclient.before @@ -0,0 +1,5 @@ +boot_id=e1794343-43fe-41f0-bc19-bc1c84486528 +dmesg_lines=675 +invalid_opcode=0 +segfaults=0 +driver=/sys/bus/dax/drivers/device_dax diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/acceptance.after b/artifact/pgas_fast_poll/2026-08-20T2255Z/acceptance.after new file mode 100644 index 0000000..166eb37 --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/acceptance.after @@ -0,0 +1,14 @@ +2026-08-21T00:39:00Z +bc9911b6-4ca8-4da6-bd9f-97d1e35daeed +675 +0 +0 +[ + { + "chardev":"dax0.0", + "size":262144000, + "target_node":1, + "align":2097152, + "mode":"devdax" + } +] diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/acceptance.before b/artifact/pgas_fast_poll/2026-08-20T2255Z/acceptance.before new file mode 100644 index 0000000..ba8eefc --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/acceptance.before @@ -0,0 +1,5 @@ +2026-08-20T23:38:14Z +bc9911b6-4ca8-4da6-bd9f-97d1e35daeed +675 +0 +0 diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/acceptance.summary b/artifact/pgas_fast_poll/2026-08-20T2255Z/acceptance.summary new file mode 100644 index 0000000..c7a097e --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/acceptance.summary @@ -0,0 +1,6 @@ +numeric_rows=115 +zero_rows=0 +final=128.00000 5009.636 +exit_status=0 +qemu_timeout_lines=1 +qemu_last_wait=qemu-system-x86_64: info: CXL Type3: PGAS response waits @ 771000000 ops: avg=497.7 ns, max=36712215 ns diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/devdax-transport.txt b/artifact/pgas_fast_poll/2026-08-20T2255Z/devdax-transport.txt new file mode 100644 index 0000000..8f84338 --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/devdax-transport.txt @@ -0,0 +1,29 @@ +{ + "region":"region0", + "resource":45365592064, + "size":268435456, + "type":"pmem", + "interleave_ways":1, + "interleave_granularity":256, + "decode_state":"commit", + "mappings":[ + { + "position":0, + "memdev":"mem0", + "decoder":"decoder2.0" + } + ], + "qos_class_mismatch":true +} +crw------- 1 root root 250, 0 Aug 20 23:37 /dev/dax0.0 +[ + { + "chardev":"dax0.0", + "size":262144000, + "target_node":1, + "align":2097152, + "mode":"devdax" + } +] +driver=device_dax +trap_count=0 diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/lat_mem_rd-128m-endpoint-reverted.status b/artifact/pgas_fast_poll/2026-08-20T2255Z/lat_mem_rd-128m-endpoint-reverted.status new file mode 100644 index 0000000..573541a --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/lat_mem_rd-128m-endpoint-reverted.status @@ -0,0 +1 @@ +0 diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/lat_mem_rd-128m-endpoint-reverted.txt b/artifact/pgas_fast_poll/2026-08-20T2255Z/lat_mem_rd-128m-endpoint-reverted.txt new file mode 100644 index 0000000..e012330 --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/lat_mem_rd-128m-endpoint-reverted.txt @@ -0,0 +1,2 @@ +"stride=64 +128.00000 31963.106 diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/lat_mem_rd-128m-oldclient.status b/artifact/pgas_fast_poll/2026-08-20T2255Z/lat_mem_rd-128m-oldclient.status new file mode 100644 index 0000000..573541a --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/lat_mem_rd-128m-oldclient.status @@ -0,0 +1 @@ +0 diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/lat_mem_rd-128m-oldclient.txt b/artifact/pgas_fast_poll/2026-08-20T2255Z/lat_mem_rd-128m-oldclient.txt new file mode 100644 index 0000000..0943ca7 --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/lat_mem_rd-128m-oldclient.txt @@ -0,0 +1,116 @@ +"stride=64 +0.00049 3880.099 +0.00098 3850.937 +0.00195 3812.414 +0.00293 3816.505 +0.00391 3831.690 +0.00586 3809.867 +0.00781 3997.977 +0.00977 3961.080 +0.01172 3948.675 +0.01367 3941.966 +0.01562 3967.314 +0.01758 3915.214 +0.01953 3920.662 +0.02148 3944.414 +0.02344 3937.980 +0.02539 3925.585 +0.02734 3911.098 +0.02930 3938.489 +0.03125 3949.359 +0.03516 3949.021 +0.03906 3940.404 +0.04297 3907.563 +0.04688 3924.560 +0.05078 3932.792 +0.05469 3927.114 +0.05859 3930.498 +0.06250 3925.270 +0.07031 3922.500 +0.07812 3944.494 +0.08594 3935.692 +0.09375 3943.227 +0.10156 3930.114 +0.10938 3931.193 +0.11719 3939.480 +0.12500 3986.726 +0.14062 3935.157 +0.15625 3932.020 +0.17188 3942.709 +0.18750 4020.261 +0.20312 3908.714 +0.21875 3939.343 +0.23438 3948.240 +0.25000 3934.413 +0.28125 3944.317 +0.31250 3964.635 +0.34375 3944.335 +0.37500 3955.657 +0.40625 4002.744 +0.43750 3925.687 +0.46875 3922.028 +0.50000 3936.220 +0.56250 4015.515 +0.62500 3981.464 +0.68750 3926.495 +0.75000 3950.716 +0.81250 3906.167 +0.87500 3935.968 +0.93750 3910.107 +1.00000 3937.218 +1.12500 3926.483 +1.25000 3926.360 +1.37500 3935.962 +1.50000 3977.177 +1.62500 3930.438 +1.75000 3928.246 +1.87500 3935.483 +2.00000 3930.229 +2.25000 3898.459 +2.50000 3907.599 +2.75000 3915.876 +3.00000 3908.618 +3.25000 3910.582 +3.50000 3923.836 +3.75000 3905.502 +4.00000 3914.360 +4.50000 3928.022 +5.00000 3910.570 +5.50000 3940.843 +6.00000 3927.730 +6.50000 3922.701 +7.00000 3944.560 +7.50000 3945.148 +8.00000 3940.061 +9.00000 3908.915 +10.00000 3942.309 +11.00000 3954.706 +12.00000 3962.717 +13.00000 4025.502 +14.00000 3937.633 +15.00000 3929.874 +16.00000 3955.542 +18.00000 3997.153 +20.00000 3980.909 +22.00000 3942.205 +24.00000 4005.660 +26.00000 4003.155 +28.00000 4031.798 +30.00000 4014.516 +32.00000 4026.946 +36.00000 4004.409 +40.00000 4025.850 +44.00000 4061.681 +48.00000 4105.995 +52.00000 4065.266 +56.00000 4066.803 +60.00000 4077.514 +64.00000 4138.188 +72.00000 4067.699 +80.00000 4081.586 +88.00000 4088.746 +96.00000 4052.300 +104.00000 4060.704 +112.00000 4135.704 +120.00000 4090.074 +128.00000 4062.530 diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/lat_mem_rd-128m-transport.status b/artifact/pgas_fast_poll/2026-08-20T2255Z/lat_mem_rd-128m-transport.status new file mode 100644 index 0000000..573541a --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/lat_mem_rd-128m-transport.status @@ -0,0 +1 @@ +0 diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/lat_mem_rd-128m-transport.txt b/artifact/pgas_fast_poll/2026-08-20T2255Z/lat_mem_rd-128m-transport.txt new file mode 100644 index 0000000..65000f3 --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/lat_mem_rd-128m-transport.txt @@ -0,0 +1,116 @@ +"stride=64 +0.00049 4440.858 +0.00098 4443.889 +0.00195 4431.989 +0.00293 4425.790 +0.00391 4444.177 +0.00586 4448.615 +0.00781 4563.788 +0.00977 4572.056 +0.01172 4534.737 +0.01367 4506.853 +0.01562 4492.708 +0.01758 4525.879 +0.01953 4546.959 +0.02148 4516.999 +0.02344 4552.653 +0.02539 4532.718 +0.02734 4530.434 +0.02930 4547.559 +0.03125 4550.159 +0.03516 4523.746 +0.03906 4549.302 +0.04297 4521.595 +0.04688 4550.533 +0.05078 4530.243 +0.05469 4521.947 +0.05859 4522.386 +0.06250 4551.218 +0.07031 4557.129 +0.07812 4553.698 +0.08594 4533.507 +0.09375 4574.072 +0.10156 4527.403 +0.10938 4530.303 +0.11719 4526.095 +0.12500 4537.256 +0.14062 4596.209 +0.15625 4552.720 +0.17188 4541.992 +0.18750 4550.214 +0.20312 4554.457 +0.21875 4548.660 +0.23438 4544.498 +0.25000 4577.276 +0.28125 4546.876 +0.31250 4569.106 +0.34375 4565.888 +0.37500 4489.341 +0.40625 4568.141 +0.43750 4519.413 +0.46875 4586.126 +0.50000 4530.867 +0.56250 4566.102 +0.62500 4597.820 +0.68750 4656.624 +0.75000 4575.578 +0.81250 4590.228 +0.87500 4638.047 +0.93750 4614.922 +1.00000 4659.817 +1.12500 4671.869 +1.25000 4704.084 +1.37500 4693.128 +1.50000 4729.837 +1.62500 4744.916 +1.75000 4679.142 +1.87500 4675.724 +2.00000 4731.934 +2.25000 4743.257 +2.50000 4745.951 +2.75000 4715.335 +3.00000 4771.146 +3.25000 4755.769 +3.50000 4723.471 +3.75000 4790.667 +4.00000 4735.762 +4.50000 4780.637 +5.00000 4789.650 +5.50000 4795.920 +6.00000 4676.050 +6.50000 4781.779 +7.00000 4763.971 +7.50000 4785.102 +8.00000 4665.629 +9.00000 4713.112 +10.00000 4779.979 +11.00000 4735.546 +12.00000 4738.589 +13.00000 4828.244 +14.00000 4826.805 +15.00000 4817.103 +16.00000 4859.592 +18.00000 4918.390 +20.00000 4796.219 +22.00000 4866.840 +24.00000 4875.451 +26.00000 4964.831 +28.00000 4918.446 +30.00000 4920.092 +32.00000 4939.744 +36.00000 4998.071 +40.00000 4976.051 +44.00000 4987.955 +48.00000 4980.045 +52.00000 5047.854 +56.00000 5060.460 +60.00000 4996.011 +64.00000 5082.557 +72.00000 5031.294 +80.00000 5062.016 +88.00000 5082.833 +96.00000 5058.460 +104.00000 5050.569 +112.00000 5050.542 +120.00000 5036.568 +128.00000 5009.636 diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/maps-oldclient.snapshot b/artifact/pgas_fast_poll/2026-08-20T2255Z/maps-oldclient.snapshot new file mode 100644 index 0000000..845bc13 --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/maps-oldclient.snapshot @@ -0,0 +1,3 @@ +pid=630 +75d095a00000-75d0a5400000 rw-s 00000000 00:06 438 /dev/dax0.0 +/sys/bus/dax/drivers/device_dax diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/maps-transport.snapshot b/artifact/pgas_fast_poll/2026-08-20T2255Z/maps-transport.snapshot new file mode 100644 index 0000000..3a618dc --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/maps-transport.snapshot @@ -0,0 +1,6 @@ +PID=589 +/root/lmbench/bin/x86_64-linux-gnu/lat_mem_rd -t -N 4 128 64 +7fc085a00000-7fc095400000 rw-s 00000000 00:06 438 /dev/dax0.0 +PID=605 +/root/lmbench/bin/x86_64-linux-gnu/lat_mem_rd -t -N 4 128 64 +7fc085a00000-7fc095400000 rw-s 00000000 00:06 438 /dev/dax0.0 diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/qemu-transport-diagnostics.txt b/artifact/pgas_fast_poll/2026-08-20T2255Z/qemu-transport-diagnostics.txt new file mode 100644 index 0000000..a666369 --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/qemu-transport-diagnostics.txt @@ -0,0 +1,2 @@ +qemu-system-x86_64: CXL Type3: SHM response timeout +qemu-system-x86_64: info: CXL Type3: PGAS response waits @ 771000000 ops: avg=497.7 ns, max=36712215 ns diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/reverted-128m-counter.after b/artifact/pgas_fast_poll/2026-08-20T2255Z/reverted-128m-counter.after new file mode 100644 index 0000000..19cbb50 --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/reverted-128m-counter.after @@ -0,0 +1 @@ +[2026-08-21 02:22:01.740] [info] [main_server.cc:233] Reads: 430000000, Writes: 400260303 diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/reverted-128m-counter.before b/artifact/pgas_fast_poll/2026-08-20T2255Z/reverted-128m-counter.before new file mode 100644 index 0000000..09399ff --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/reverted-128m-counter.before @@ -0,0 +1 @@ +[2026-08-21 01:51:56.688] [info] [main_server.cc:233] Reads: 404900000, Writes: 370898371 diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/reverted-128m-map.txt b/artifact/pgas_fast_poll/2026-08-20T2255Z/reverted-128m-map.txt new file mode 100644 index 0000000..561ae69 --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/reverted-128m-map.txt @@ -0,0 +1,3 @@ +pid=1083 +70333e400000-70334de00000 rw-s 00000000 00:06 438 /dev/dax0.0 +invalid_opcode=0 diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/reverted-128m.after b/artifact/pgas_fast_poll/2026-08-20T2255Z/reverted-128m.after new file mode 100644 index 0000000..a877f40 --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/reverted-128m.after @@ -0,0 +1,5 @@ +boot_id=c2b882dc-d738-4944-ba75-4154c730b392 +dmesg_lines=672 +invalid_opcode=0 +segfaults=0 +driver=/sys/bus/dax/drivers/device_dax diff --git a/artifact/pgas_fast_poll/2026-08-20T2255Z/server-transport-config.txt b/artifact/pgas_fast_poll/2026-08-20T2255Z/server-transport-config.txt new file mode 100644 index 0000000..1cbcc02 --- /dev/null +++ b/artifact/pgas_fast_poll/2026-08-20T2255Z/server-transport-config.txt @@ -0,0 +1,6 @@ +[2026-08-20 23:34:28.086] Communication Mode: PGAS Shared Memory (cxl_backend.h) +[2026-08-20 23:34:28.086] PGAS SHM Name: /cxlmemsim_pgas +[2026-08-20 23:34:28.086] PGAS polling: workers=1, spin=1000 us, yields=10, idle sleep=100 us, record accesses=false +[2026-08-20 23:34:28.086] Capacity: 256 MB +[2026-08-20 23:34:28.086] Backing Mode: POSIX shared memory +[2026-08-20 23:34:28.086] Default latency: 100 ns diff --git a/docs/superpowers/plans/2026-08-20-pgas-fast-poll.md b/docs/superpowers/plans/2026-08-20-pgas-fast-poll.md new file mode 100644 index 0000000..9a34db6 --- /dev/null +++ b/docs/superpowers/plans/2026-08-20-pgas-fast-poll.md @@ -0,0 +1,190 @@ +# PGAS SHM Fast-Poll Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Reduce synchronous PGAS SHM request wakeup overhead while keeping every guest CXL load mediated by the memory server. + +**Architecture:** A testable server-side policy selects spin, yield, or sleep +from recent activity. QEMU spins during the expected response window and +sleeps only on cold or failed paths; the slot protocol and timeout remain +authoritative. + +For transport-only latency proof, an explicit server switch skips the +controller's unbounded per-address occupation bookkeeping while retaining the +synchronous PGAS request, data operation, response, and protocol counters. The +compatibility default continues to record controller accesses. + +**Tech Stack:** C++20, CMake/CTest, POSIX shared memory, QEMU, Linux KVM. + +**Spec:** `docs/superpowers/specs/2026-08-20-pgas-fast-poll-design.md` + +## Global Constraints + +- Preserve a synchronous memory-server request and response for every guest CXL access. +- Do not add direct-mapped, asynchronous-accounting, or request-batching paths. +- Keep bounded client timeouts and cold-path sleeps. +- Validate with the exact 128 MiB `lat_mem_rd` command and `/dev/dax0.0` mapping. + +--- + +### Task 1: Adaptive memory-server polling + +**Files:** +- Create: `include/pgas_poll_policy.h` +- Create: `tests/test_pgas_poll_policy.cpp` +- Modify: `src/main_server.cc` +- Modify: `CMakeLists.txt` + +**Interfaces:** +- Produces: `PgasPollPolicy::next(bool processed, std::uint64_t now_ns) -> PgasPollAction`. +- Produces: server options `pgas_workers`, `pgas_spin_us`, + `pgas_yield_count`, `pgas_idle_sleep_us`, and `pgas_record_accesses`. + +- [x] **Step 1: Write the failing policy test** + +```cpp +PgasPollPolicy policy(50'000, 2); +assert(policy.next(true, 1'000'000) == PgasPollAction::Spin); +assert(policy.next(false, 1'049'999) == PgasPollAction::Spin); +assert(policy.next(false, 1'050'000) == PgasPollAction::Yield); +assert(policy.next(false, 1'050'001) == PgasPollAction::Yield); +assert(policy.next(false, 1'050'002) == PgasPollAction::Sleep); +``` + +- [x] **Step 2: Run the focused build and verify RED** + +Run: `cmake --build build --target test_pgas_poll_policy -j$(nproc)` + +Expected: fail because the target and `pgas_poll_policy.h` do not exist. + +- [x] **Step 3: Implement the policy and server options** + +Implement a header-only policy storing the last-active timestamp and yield +count. Parse and validate the four server options, log their resolved values, +and apply the selected action in each PGAS worker loop. + +- [x] **Step 4: Run focused and full tests** + +Run: `cmake --build build --target test_pgas_poll_policy cxlmemsim_server -j$(nproc) && ctest --test-dir build --output-on-failure` + +Expected: the policy test and all existing CTest cases pass. + +- [x] **Step 5: Commit** + +```bash +git add CMakeLists.txt include/pgas_poll_policy.h tests/test_pgas_poll_policy.cpp src/main_server.cc +git commit -m "server: add adaptive PGAS polling" +``` + +### Task 2: Adaptive QEMU PGAS response wait + +**Files:** +- Create: `lib/qemu/include/hw/cxl/cxl_memsim_wait.h` +- Modify: `lib/qemu/hw/mem/cxl_type3.c` +- Create: `tests/test_cxl_memsim_wait_policy.c` +- Modify: `CMakeLists.txt` + +**Outcome:** Built and retained after a valid same-checkout mediated A/B. + +- [x] **Step 1: Write the failing QEMU wait-policy test** + +```c +assert(cxl_memsim_wait_action(49'999, 50'000) == CXL_MEMSIM_WAIT_SPIN); +assert(cxl_memsim_wait_action(50'000, 50'000) == CXL_MEMSIM_WAIT_SLEEP); +``` + +Use ordinary decimal literals in the C source (`49999` and `50000`). + +- [x] **Step 2: Run the focused build and verify RED** + +Run: `cmake --build build --target test_cxl_memsim_wait_policy -j$(nproc)` + +Expected: fail because the target and helper header do not exist. + +- [x] **Step 3: Implement and integrate the wait helper** + +Implement the pure-C boundary helper, parse the two environment settings during +CXLMemSim initialization, and replace the slot-free and response fixed-sleep +loops with monotonic deadline loops using `cpu_relax()` during the hot window. +Accumulate response-wait count, total nanoseconds, and maximum nanoseconds and +log them every 100,000 successful requests. + +- [x] **Step 4: Run focused tests and build QEMU** + +Run: `cmake --build build --target test_cxl_memsim_wait_policy -j$(nproc)` + +Run: `cmake --build build -j$(nproc)` + +Run the existing QEMU configure/build command discovered from the active build, +targeting the isolated QEMU checkout. + +Expected: both wait-policy tests and both projects compile successfully. + +- [x] **Step 5: Validate the client change with server-counter proof** + +The adaptive client completed the exact 128 MiB sweep at 5009.636 ns/load. The +same-checkout original-wait client completed an isolated 128 MiB endpoint at +31963.106 ns/load while server reads advanced by 25.1 million. The apparent +4062.530 ns/load installed-QEMU control was rejected because its server +counters did not advance. + +### Task 3: Live latency validation + +**Files:** +- Create: `artifact/pgas_fast_poll//server-command.txt` +- Create: `artifact/pgas_fast_poll//qemu-environment.txt` +- Create: `artifact/pgas_fast_poll//lat_mem_rd-128m.txt` +- Create: `artifact/pgas_fast_poll//dax-mapping.txt` +- Create: `artifact/pgas_fast_poll//kernel-traps.txt` +- Create: `artifact/pgas_fast_poll//summary.md` + +**Interfaces:** +- Consumes: isolated server and QEMU builds from Tasks 1 and 2. +- Produces: reproducible before/after wall-clock latency evidence. + +- [x] **Step 1: Capture and stop the current launch cleanly** + +Record the active server command, QEMU command/environment, guest process state, +and current kernel trap tail before stopping only the identified server and QEMU +processes. + +- [x] **Step 2: Launch the optimized synchronous path** + +Start the isolated server with: + +```bash +./build/cxlmemsim_server --comm-mode=pgas-shm \ + --pgas-shm-name=/cxlmemsim_pgas --capacity=256 \ + --pgas-workers=1 --pgas-spin-us=1000 --pgas-yield-count=10 \ + --pgas-idle-sleep-us=100 --pgas-record-accesses=false +``` + +Launch the isolated QEMU client with `CXL_PGAS_CLIENT_SPIN_US=50` and +`CXL_PGAS_CLIENT_SLEEP_US=10`, preserving all other launch arguments. + +- [x] **Step 3: Run the exact guest benchmark** + +Run: + +```bash +LD_PRELOAD=target/debug/libcxlalloc_preload.so \ + ~/lmbench/bin/x86_64-linux-gnu/lat_mem_rd -t -N 4 128 64 +``` + +Capture the complete output, exit status, live `/proc//maps` entry for +`/dev/dax0.0`, and kernel trap lines. + +- [x] **Step 4: Compare and verify** + +Compare the 128 MiB result with a same-checkout original-wait control whose +server counters increase. Treat the approximately 4130 ns/load devdax result +without counter proof only as a non-mediated lower bound. Verify that the +command exits zero, the DAX mapping is present, the server request counter +increases, and no new invalid-opcode trap is recorded. + +- [x] **Step 5: Commit evidence** + +```bash +git add artifact/pgas_fast_poll +git commit -m "bench: record PGAS fast-poll latency" +``` diff --git a/docs/superpowers/specs/2026-08-20-pgas-fast-poll-design.md b/docs/superpowers/specs/2026-08-20-pgas-fast-poll-design.md new file mode 100644 index 0000000..8bbc75c --- /dev/null +++ b/docs/superpowers/specs/2026-08-20-pgas-fast-poll-design.md @@ -0,0 +1,119 @@ +# PGAS SHM Fast-Poll Design + +## Goal + +Reduce the measured `lat_mem_rd` dependent-load latency of the synchronous +QEMU-to-CXLMemSim PGAS shared-memory path while preserving one memory-server +request and response for every guest CXL load. + +## Current bottleneck + +Each guest load exits KVM through the CXL MMIO handler, publishes a request to +a PGAS shared-memory slot, waits for the server, and returns through QEMU. The +KVM exit is unavoidable in this design. Server workers also slept for 100 +microseconds when idle. + +Live profiling found a more serious server-side problem: detailed controller +accounting performs a linear search through the per-address `occupation` +vector for every request. Region creation populated that vector with more than +one million entries, so server response time grew with cumulative request +count instead of remaining constant. + +The reported server latency remains the modeled CXL latency. Wall-clock +`lat_mem_rd` latency includes KVM exit, QEMU dispatch, server wakeup and request +processing, so the two values must not be presented as equivalent. + +## Server design + +The PGAS server uses an adaptive idle policy with three states: + +1. After processing a request, repeatedly poll without sleeping for a bounded + active window. +2. After the active window expires, yield for a bounded number of iterations. +3. Once fully idle, sleep for the configured idle interval. + +The server exposes these command-line controls: + +- `--pgas-workers `: worker count, default 4, valid range 1 through the + number of PGAS slots. +- `--pgas-spin-us `: active busy-poll window, default 50 microseconds. +- `--pgas-yield-count `: yield iterations before sleeping, default 10. +- `--pgas-idle-sleep-us `: fully-idle sleep interval, default 100 + microseconds. +- `--pgas-record-accesses[=bool]`: retain detailed controller occupation and + policy bookkeeping, default true. A transport-latency run may set this false + to keep protocol operation counters while excluding the controller's + per-address simulation bookkeeping from the synchronous response path. + +For the single outstanding request generated by `lat_mem_rd`, the benchmark +launch uses one worker to avoid four workers contending while scanning the same +slot. Multi-client runs can retain or increase the default worker count. + +## QEMU client policy + +The QEMU PGAS client replaces fixed sleeps in the slot-free and response wait +loops with a deadline-based adaptive wait: + +1. Spin with `cpu_relax()` for the configured hot window. +2. Sleep in short intervals only after the hot window expires. +3. Fail when the monotonic timeout expires, retaining the existing error paths. + +The hot window is controlled by `CXL_PGAS_CLIENT_SPIN_US`, defaulting to 50 +microseconds. `CXL_PGAS_CLIENT_SLEEP_US` controls cold-path sleeps and defaults +to 10 microseconds. QEMU reports cumulative and maximum PGAS response-wait time +every 100,000 successful requests. + +A same-checkout live A/B validated this change. With true server mediation and +the same lean server, the original fixed-sleep client measured 31963.106 +ns/load at 128 MiB. The adaptive client completed the full exact sweep at +5009.636 ns/load, an 84.3% reduction. A separate 4062.530 ns/load result from +the installed QEMU was rejected as a control because the server counters did +not advance during that run. + +## Correctness and compatibility + +- Every guest load still waits for a memory-server response. +- Disabling detailed controller recording does not bypass the server data copy, + validation, response, or total read/write counters. +- No direct-mapped or asynchronous accounting path is introduced. +- No request batching is used because `lat_mem_rd` is a dependent pointer + chase; the next address is unavailable until the prior load completes. +- QEMU retains a bounded 100 millisecond deadline and sleeps after the hot + polling window. +- Server options reject zero workers and worker counts larger than the PGAS + slot count. + +## Validation + +Automated tests cover server adaptive state transitions, option validation, +the safe default for detailed access recording, and the QEMU wait-policy +boundary. Release and Debug builds and the full CTest suite must pass. + +The live acceptance test uses the same guest command and `/dev/dax0.0` mapping: + +```bash +LD_PRELOAD=target/debug/libcxlalloc_preload.so \ + ~/lmbench/bin/x86_64-linux-gnu/lat_mem_rd -t -N 4 128 64 +``` + +Evidence includes the server command line, complete benchmark output, process +mapping showing `/dev/dax0.0`, exit status, server-counter movement, and +before/after kernel boundaries. The accepted adaptive-client run completed all +115 rows with no zero values at 5009.636 ns/load. The original-wait control +used the same integration checkout and server and measured 31963.106 ns/load +at the isolated 128 MiB endpoint; server reads increased from 404900000 to +430000000 during that control. Both paths retained `device_dax` and had zero +invalid-opcode traps and zero `lat_mem_rd` segfaults. + +The adaptive full run recorded one isolated 100 millisecond response timeout +among approximately 771 million successful operations. That event is retained +in the evidence and is not hidden by the successful benchmark exit. + +## Expected limit + +Adaptive polling and transport-only accounting remove most avoidable scheduler +and unbounded bookkeeping delay but do not remove the KVM MMIO exit, QEMU +dispatch, or synchronous server round trip. The optimized true-mediated result +is therefore approximately 5 microseconds, while the approximately 4 +microsecond devdax result without proven counter movement is only a lower-bound +reference, not a server-mediated result. diff --git a/include/pgas_poll_policy.h b/include/pgas_poll_policy.h new file mode 100644 index 0000000..b0af286 --- /dev/null +++ b/include/pgas_poll_policy.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include + +enum class PgasPollAction { + Spin, + Yield, + Sleep, +}; + +struct PgasPollConfig { + std::size_t workers = 4; + std::uint64_t spin_us = 50; + std::size_t yield_count = 10; + std::uint64_t idle_sleep_us = 100; + bool record_accesses = true; +}; + +inline bool isValidPgasPollConfig(const PgasPollConfig &config, std::size_t max_workers) noexcept { + return config.workers > 0 && config.workers <= max_workers && + config.spin_us <= std::numeric_limits::max() / 1000; +} + +class PgasPollPolicy { +public: + PgasPollPolicy(std::uint64_t active_spin_ns, std::size_t yield_count) noexcept + : active_spin_ns_(active_spin_ns), max_yields_(yield_count) {} + + PgasPollAction next(bool processed, std::uint64_t now_ns) noexcept { + if (processed) { + last_active_ns_ = now_ns; + idle_yields_ = 0; + return PgasPollAction::Spin; + } + + if (last_active_ns_ == 0) { + return PgasPollAction::Sleep; + } + + if (now_ns < last_active_ns_ || now_ns - last_active_ns_ < active_spin_ns_) { + return PgasPollAction::Spin; + } + + if (idle_yields_ < max_yields_) { + ++idle_yields_; + return PgasPollAction::Yield; + } + + return PgasPollAction::Sleep; + } + +private: + std::uint64_t active_spin_ns_; + std::size_t max_yields_; + std::uint64_t last_active_ns_ = 0; + std::size_t idle_yields_ = 0; +}; diff --git a/lib/qemu b/lib/qemu index 904f0a3..d986b2f 160000 --- a/lib/qemu +++ b/lib/qemu @@ -1 +1 @@ -Subproject commit 904f0a3cb2a56ca58a66f2c5507a0b2b9eb67b10 +Subproject commit d986b2f591c00cbf4e676863ba8199523ceda3f0 diff --git a/src/main_server.cc b/src/main_server.cc index d2b0771..aa378e9 100644 --- a/src/main_server.cc +++ b/src/main_server.cc @@ -17,6 +17,7 @@ #include "cxlcontroller.h" #include "cxlendpoint.h" #include "distributed_server.h" +#include "pgas_poll_policy.h" #include "policy.h" #include "shm_communication.h" #include @@ -151,6 +152,7 @@ class ThreadPerConnectionServer { cxl_shm_header_t *pgas_shm_header_; void *pgas_memory_; size_t pgas_memory_size_; + PgasPollConfig pgas_poll_config_; // Shared memory manager for real memory allocation std::unique_ptr shm_manager; @@ -243,14 +245,16 @@ class ThreadPerConnectionServer { ThreadPerConnectionServer( int port, CXLController *ctrl, size_t capacity_mb, const std::string &backing_file = "", CommMode mode = CommMode::TCP, const std::string &pgas_shm_name = "/cxlmemsim_pgas", + const PgasPollConfig &pgas_poll_config = {}, SharedMemoryManager::BackingMode backing_mode = SharedMemoryManager::BackingMode::SharedMemory, const SharedMemoryManager::SsdStreamingConfig &ssd_config = {}, bool coherence_v2_enabled = false, std::chrono::milliseconds coherence_v2_snoop_timeout = std::chrono::milliseconds(1000), std::string coherence_v2_shm_name = "/cxlmemsim_coherence_v2") : server_fd(-1), port(port), controller(ctrl), running(true), next_thread_id(0), comm_mode(mode), pgas_shm_name_(pgas_shm_name), pgas_shm_fd_(-1), pgas_shm_header_(nullptr), pgas_memory_(nullptr), - pgas_memory_size_(0), backing_file_(backing_file), backing_mode_(backing_mode), ssd_config_(ssd_config), - coherence_v2_enabled_(coherence_v2_enabled), coherence_v2_snoop_timeout_(coherence_v2_snoop_timeout), + pgas_memory_size_(0), pgas_poll_config_(pgas_poll_config), backing_file_(backing_file), + backing_mode_(backing_mode), ssd_config_(ssd_config), coherence_v2_enabled_(coherence_v2_enabled), + coherence_v2_snoop_timeout_(coherence_v2_snoop_timeout), coherence_v2_shm_name_(std::move(coherence_v2_shm_name)) { congestion_info.active_requests = 0; congestion_info.total_bandwidth_used = 0; @@ -373,6 +377,7 @@ struct ServerOptions { bool ssd_odirect = true; std::string comm_mode_str = "tcp"; std::string pgas_shm_name = "/cxlmemsim_pgas"; + PgasPollConfig pgas_poll; uint32_t node_id = 0; std::string dist_shm_name = "/cxlmemsim_dist"; std::string coordinator_shm; @@ -415,6 +420,11 @@ static void print_server_help(const char *program) { << " --ssd-odirect[=true|false] Use O_DIRECT when backend supports it\n" << " --comm-mode tcp, shm, pgas-shm, or distributed\n" << " --pgas-shm-name PGAS shared memory name\n" + << " --pgas-workers PGAS worker threads (default: 4)\n" + << " --pgas-spin-us Active PGAS busy-poll window (default: 50)\n" + << " --pgas-yield-count PGAS yields before idle sleep (default: 10)\n" + << " --pgas-idle-sleep-us PGAS fully-idle sleep (default: 100)\n" + << " --pgas-record-accesses[=bool] Run detailed controller accounting (default: true)\n" << " --node-id Distributed node ID\n" << " --dist-shm-name Distributed shared memory name\n" << " --coordinator-shm Coordinator shared memory to join\n" @@ -594,6 +604,16 @@ static bool parse_server_options(int argc, char *argv[], ServerOptions &opts, st opts.comm_mode_str = get_value(key); } else if (key == "pgas-shm-name") { opts.pgas_shm_name = get_value(key); + } else if (key == "pgas-workers") { + opts.pgas_poll.workers = std::stoull(get_value(key)); + } else if (key == "pgas-spin-us") { + opts.pgas_poll.spin_us = std::stoull(get_value(key)); + } else if (key == "pgas-yield-count") { + opts.pgas_poll.yield_count = std::stoull(get_value(key)); + } else if (key == "pgas-idle-sleep-us") { + opts.pgas_poll.idle_sleep_us = std::stoull(get_value(key)); + } else if (key == "pgas-record-accesses") { + opts.pgas_poll.record_accesses = parse_optional_bool_option(argc, argv, i, value, has_inline_value); } else if (key == "node-id") { opts.node_id = static_cast(std::stoul(get_value(key))); } else if (key == "dist-shm-name") { @@ -776,6 +796,10 @@ int main(int argc, char *argv[]) { SPDLOG_ERROR("--coherence-v2-snoop-timeout-ms must be non-zero"); return 1; } + if (!isValidPgasPollConfig(opts.pgas_poll, CXL_SHM_MAX_SLOTS)) { + SPDLOG_ERROR("PGAS polling requires 1..{} workers and a representable spin interval", CXL_SHM_MAX_SLOTS); + return 1; + } // Initialize policies std::array policies = {new AllocationPolicy(), new MigrationPolicy(), new PagingPolicy(), @@ -856,6 +880,9 @@ int main(int argc, char *argv[]) { } if (comm_mode == CommMode::PGAS_SHM) { SPDLOG_INFO(" PGAS SHM Name: {}", pgas_shm_name); + SPDLOG_INFO(" PGAS polling: workers={}, spin={} us, yields={}, idle sleep={} us, record accesses={}", + opts.pgas_poll.workers, opts.pgas_poll.spin_us, opts.pgas_poll.yield_count, + opts.pgas_poll.idle_sleep_us, opts.pgas_poll.record_accesses); } if (comm_mode == CommMode::DISTRIBUTED) { SPDLOG_INFO(" Node ID: {}", node_id); @@ -1043,8 +1070,8 @@ int main(int argc, char *argv[]) { try { ThreadPerConnectionServer server(port, controller, capacity, backing_file, comm_mode, pgas_shm_name, - backing_mode, ssd_config, coherence_v2, coherence_v2_snoop_timeout, - coherence_v2_shm_name); + opts.pgas_poll, backing_mode, ssd_config, coherence_v2, + coherence_v2_snoop_timeout, coherence_v2_shm_name); if (!server.start()) { SPDLOG_ERROR("Failed to start server"); @@ -2491,16 +2518,26 @@ void ThreadPerConnectionServer::run_pgas_shm_mode() { __atomic_store_n(&pgas_shm_header_->server_ready, 1, __ATOMIC_RELEASE); // Create worker threads for handling PGAS SHM requests - const int num_workers = 4; std::vector workers; + workers.reserve(pgas_poll_config_.workers); - for (int i = 0; i < num_workers; i++) { + for (std::size_t i = 0; i < pgas_poll_config_.workers; i++) { workers.emplace_back([this]() { + PgasPollPolicy policy(pgas_poll_config_.spin_us * 1000, pgas_poll_config_.yield_count); while (running && !shutdown_requested()) { int processed = poll_pgas_shm_requests(); - if (processed == 0) { - // No requests - sleep briefly to reduce CPU usage - usleep(100); // 100us + const auto now_ns = static_cast(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count()); + switch (policy.next(processed > 0, now_ns)) { + case PgasPollAction::Spin: + break; + case PgasPollAction::Yield: + std::this_thread::yield(); + break; + case PgasPollAction::Sleep: + std::this_thread::sleep_for(std::chrono::microseconds(pgas_poll_config_.idle_sleep_us)); + break; } } }); @@ -2612,7 +2649,9 @@ int ThreadPerConnectionServer::poll_pgas_shm_requests() { total_reads++; - controller->record_cxl_access(request_ts, static_cast(i), addr, false); + if (pgas_poll_config_.record_accesses) { + controller->record_cxl_access(request_ts, static_cast(i), addr, false); + } log_periodic_stats("PGAS_READ", total_reads.load()); __atomic_thread_fence(__ATOMIC_RELEASE); @@ -2654,7 +2693,9 @@ int ThreadPerConnectionServer::poll_pgas_shm_requests() { slot->latency_ns = (uint64_t)(base_latency + fabric_latency_ns); total_writes++; - controller->record_cxl_access(request_ts, static_cast(i), addr, true); + if (pgas_poll_config_.record_accesses) { + controller->record_cxl_access(request_ts, static_cast(i), addr, true); + } log_periodic_stats("PGAS_WRITE", total_writes.load()); __atomic_thread_fence(__ATOMIC_RELEASE); @@ -2698,7 +2739,9 @@ int ThreadPerConnectionServer::poll_pgas_shm_requests() { __atomic_thread_fence(__ATOMIC_RELEASE); slot->resp_status = CXL_SHM_RESP_OK; total_atomic_faa++; - controller->record_cxl_access(request_ts, static_cast(i), addr, true); + if (pgas_poll_config_.record_accesses) { + controller->record_cxl_access(request_ts, static_cast(i), addr, true); + } log_periodic_stats("PGAS_FAA", total_atomic_faa.load()); } else { slot->resp_status = CXL_SHM_RESP_ERROR; @@ -2742,7 +2785,9 @@ int ThreadPerConnectionServer::poll_pgas_shm_requests() { __atomic_thread_fence(__ATOMIC_RELEASE); slot->resp_status = CXL_SHM_RESP_OK; total_atomic_cas++; - controller->record_cxl_access(request_ts, static_cast(i), addr, true); + if (pgas_poll_config_.record_accesses) { + controller->record_cxl_access(request_ts, static_cast(i), addr, true); + } log_periodic_stats("PGAS_CAS", total_atomic_cas.load()); } else { slot->resp_status = CXL_SHM_RESP_ERROR; diff --git a/tests/test_cxl_memsim_wait_policy.c b/tests/test_cxl_memsim_wait_policy.c new file mode 100644 index 0000000..aaad4a1 --- /dev/null +++ b/tests/test_cxl_memsim_wait_policy.c @@ -0,0 +1,30 @@ +#include "hw/cxl/cxl_memsim_wait.h" + +#include + +static int failures; + +static void expect_action(CXLMemSimWaitAction actual, CXLMemSimWaitAction expected, const char *message) { + if (actual != expected) { + fprintf(stderr, "FAIL: %s\n", message); + failures++; + } +} + +static void testElapsedTimeInsideHotWindowSpins(void) { + expect_action(cxl_memsim_wait_action(0, 50000), CXL_MEMSIM_WAIT_SPIN, "start of hot window must spin"); + expect_action(cxl_memsim_wait_action(49999, 50000), CXL_MEMSIM_WAIT_SPIN, + "last nanosecond of hot window must spin"); +} + +static void testElapsedTimeAtBoundarySleeps(void) { + expect_action(cxl_memsim_wait_action(50000, 50000), CXL_MEMSIM_WAIT_SLEEP, "hot-window boundary must sleep"); + expect_action(cxl_memsim_wait_action(50001, 50000), CXL_MEMSIM_WAIT_SLEEP, "elapsed cold path must sleep"); + expect_action(cxl_memsim_wait_action(0, 0), CXL_MEMSIM_WAIT_SLEEP, "zero spin interval must sleep immediately"); +} + +int main(void) { + testElapsedTimeInsideHotWindowSpins(); + testElapsedTimeAtBoundarySleeps(); + return failures == 0 ? 0 : 1; +} diff --git a/tests/test_pgas_poll_policy.cpp b/tests/test_pgas_poll_policy.cpp new file mode 100644 index 0000000..56fcbbd --- /dev/null +++ b/tests/test_pgas_poll_policy.cpp @@ -0,0 +1,73 @@ +#include "pgas_poll_policy.h" + +#include +#include + +static int failures; + +static void expectAction(PgasPollAction actual, PgasPollAction expected, const char *message) { + if (actual != expected) { + std::cerr << "FAIL: " << message << '\n'; + ++failures; + } +} + +static void expectCondition(bool condition, const char *message) { + if (!condition) { + std::cerr << "FAIL: " << message << '\n'; + ++failures; + } +} + +static void testRecentActivityKeepsWorkerSpinning() { + PgasPollPolicy policy(50'000, 2); + + expectAction(policy.next(true, 1'000'000), PgasPollAction::Spin, "activity must select spin"); + expectAction(policy.next(false, 1'049'999), PgasPollAction::Spin, "worker must spin inside the active window"); +} + +static void testExpiredActiveWindowYieldsThenSleeps() { + PgasPollPolicy policy(50'000, 2); + + expectAction(policy.next(true, 1'000'000), PgasPollAction::Spin, "activity must select spin"); + expectAction(policy.next(false, 1'050'000), PgasPollAction::Yield, "active-window boundary must start yielding"); + expectAction(policy.next(false, 1'050'001), PgasPollAction::Yield, "configured second yield must be honored"); + expectAction(policy.next(false, 1'050'002), PgasPollAction::Sleep, "worker must sleep after configured yields"); +} + +static void testNewActivityResetsIdleBackoff() { + PgasPollPolicy policy(50'000, 1); + + expectAction(policy.next(false, 1'000'000), PgasPollAction::Sleep, "never-active worker must remain asleep"); + expectAction(policy.next(true, 2'000'000), PgasPollAction::Spin, "new activity must select spin"); + expectAction(policy.next(false, 2'050'000), PgasPollAction::Yield, "first idle transition must yield"); + expectAction(policy.next(true, 3'000'000), PgasPollAction::Spin, "later activity must reset idle backoff"); + expectAction(policy.next(false, 3'050'000), PgasPollAction::Yield, + "reset backoff must yield again before sleeping"); +} + +static void testWorkerCountValidationProtectsSlotOwnership() { + PgasPollConfig config; + + expectCondition(config.record_accesses, "detailed controller accounting must remain enabled by default"); + + config.workers = 0; + expectCondition(!isValidPgasPollConfig(config, 256), "zero workers must be rejected"); + + config.workers = 257; + expectCondition(!isValidPgasPollConfig(config, 256), "workers beyond slot count must be rejected"); + + config.workers = 1; + expectCondition(isValidPgasPollConfig(config, 256), "one worker must be accepted"); + + config.workers = 256; + expectCondition(isValidPgasPollConfig(config, 256), "one worker per slot must be accepted"); +} + +int main() { + testRecentActivityKeepsWorkerSpinning(); + testExpiredActiveWindowYieldsThenSleeps(); + testNewActivityResetsIdleBackoff(); + testWorkerCountValidationProtectsSlotOwnership(); + return failures == 0 ? 0 : 1; +}