lost-and-found.scm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. ;; This file is part of scheme-GNUnet, a partial Scheme port of GNUnet.
  2. ;; Copyright © 2022 GNUnet e.V.
  3. ;;
  4. ;; Scheme-GNUnet is free software: you can redistribute it and/or modify it
  5. ;; under the terms of the GNU Affero General Public License as published
  6. ;; by the Free Software Foundation, either version 3 of the License,
  7. ;; or (at your option) any later version.
  8. ;;
  9. ;; Scheme-GNUnet is distributed in the hope that it will be useful, but
  10. ;; WITHOUT ANY WARRANTY; without even the implied warranty of
  11. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. ;; Affero General Public License for more details.
  13. ;;
  14. ;; You should have received a copy of the GNU Affero General Public License
  15. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. ;;
  17. ;; SPDX-License-Identifier: AGPL-3.0-or-later
  18. ;; Author: Maxime Devos
  19. (define-module (test-lost-and-found))
  20. (import (ice-9 match)
  21. (srfi srfi-1)
  22. (gnu gnunet concurrency lost-and-found)
  23. (rnrs base)
  24. (srfi srfi-64)
  25. (fibers conditions)
  26. (fibers operations)
  27. (fibers channels)
  28. (rnrs records syntactic)
  29. (fibers)
  30. (fibers timers)) ; sleep
  31. (test-begin "lost-and-found")
  32. (define-record-type (<losable+datum> make-losable+datum losable+datum?)
  33. (parent <losable>)
  34. (fields (immutable datum losable-datum))
  35. ;; TODO: why is this necessary?
  36. (protocol (lambda (%make)
  37. (lambda (lost-and-found foo)
  38. ((%make lost-and-found) foo)))))
  39. (define (lose lost-and-found start to/exclusive)
  40. "Lose integers from the range [start to/exclusive)."
  41. (when (< start to/exclusive)
  42. (make-losable+datum lost-and-found start)
  43. (lose lost-and-found (+ 1 start) to/exclusive)))
  44. (define (collect-operation lost-and-found)
  45. "Make an operation returning the list of found integers
  46. (make sure to gc before performing the operation!)."
  47. (wrap-operation
  48. (collect-lost-and-found-operation lost-and-found)
  49. (lambda (list)
  50. (map losable-datum list))))
  51. (define (collect lost-and-found)
  52. "Return a list of found integers (make sure to gc first!)."
  53. (perform-operation (collect-operation lost-and-found)))
  54. (define (verify collected from to/exclusive)
  55. (define count (- to/exclusive from))
  56. (define present (make-bitvector count #false))
  57. (for-each (lambda (i)
  58. (assert (not (bitvector-bit-set? present (- i from))))
  59. (bitvector-set-bit! present i))
  60. collected)
  61. ;; Presumably due to boehmgc being conservative, this number
  62. ;; of elements collected tends can be off by one or two.
  63. ;; Allow being 5 elements off.
  64. (define fraction (/ (bitvector-count present) (- count 5.)))
  65. (pk 'f (+ 0.0 fraction))
  66. (assert (>= fraction 1))
  67. #true
  68. #;
  69. (receive (collected\expected ∩)
  70. (lset-diff+intersection! = collected (iota count from))
  71. (assert (null? collected\expected))
  72. ;; Presumably due to boehmgc being conservative, this number
  73. ;; of elements collected tends can be off by one or two.
  74. ;; Allow being 5 elements off.
  75. (let ((fraction (/ (length ∩) (- count 5))))
  76. (pk 'f (+ 0.0 fraction))
  77. (assert (>= fraction 1))
  78. #true)))
  79. (define %count 1000)
  80. (test-assert "unreachable + gc -> moved into lost and found"
  81. (let ((lost-and-found (make-lost-and-found)))
  82. (lose lost-and-found 0 %count)
  83. (gc)
  84. (verify (collect lost-and-found) 0 %count)))
  85. (test-assert "new lost between making the operation and performing it (1)"
  86. (let ((lost-and-found (make-lost-and-found)))
  87. (lose lost-and-found 0 %count)
  88. (gc)
  89. (define operation (collect-operation lost-and-found))
  90. (lose lost-and-found %count (* 2 %count))
  91. (gc)
  92. (verify (perform-operation operation) 0 (* 2 %count))))
  93. (test-assert "new lost between making the operation and performing it (2)"
  94. (let ((lost-and-found (make-lost-and-found)))
  95. (lose lost-and-found 0 %count)
  96. ;; <- no gc!
  97. (define operation (collect-operation lost-and-found))
  98. (lose lost-and-found %count (* 2 %count))
  99. (gc)
  100. (verify (perform-operation operation) 0 (* 2 %count))))
  101. (test-assert "concurrent collecting (light)"
  102. (let ((lost-and-found (make-lost-and-found)))
  103. (lose lost-and-found 0 %count)
  104. (gc)
  105. (define operation1 (collect-operation lost-and-found))
  106. (define operation2 (collect-operation lost-and-found))
  107. (define result1 (perform-operation operation1))
  108. ;; Technically, this is allowed to hang (since everything is
  109. ;; collected by result1), but due to implementation details,
  110. ;; it doesn't.
  111. (define result2 (perform-operation operation2))
  112. (verify result1 0 %count)
  113. (verify (append result1 result2) 0 %count)))
  114. ;; TODO: copied from (tests update)
  115. ;; TODO: 1e-4 is not sufficient here, 1e-3 is required to make tests
  116. ;; fail (CPU-dependent?).
  117. (define expected-blocking-operation
  118. (wrap-operation (sleep-operation 1e-3) (lambda () 'blocking)))
  119. (test-eq "block while nothing to collect"
  120. 'blocking
  121. (perform-operation
  122. (choice-operation (collect-operation (make-lost-and-found))
  123. expected-blocking-operation)))
  124. (test-assert "delaying performing the operation, some concurrency"
  125. (let* ((lost-and-found (make-lost-and-found))
  126. ;; 'lost-and-found' currently has a condition, so the
  127. ;; (if (condition? old) ...) case should happen here
  128. (operation (collect-operation lost-and-found)))
  129. ;; Trigger and replace the original condition.
  130. (lose lost-and-found 0 %count)
  131. (gc)
  132. (collect lost-and-found)
  133. ;; Run the original operation.
  134. (define result
  135. (perform-operation
  136. (choice-operation operation expected-blocking-operation)))
  137. ;; The lost objects were already collected, so blocking is fine.
  138. ;; There's a form of concurrency, so a spurious empty list is
  139. ;; also allowed.
  140. (memq result '(blocking ()))))
  141. (define add-found! #{ add-found!}#)
  142. ;; There is no rule against the GC hook being called from within the GC hook,
  143. ;; or the GC hook being called in parallel from another thread running the
  144. ;; GC hook, in case a lot of garbage was generated before the original
  145. ;; invocation of the GC hook was able to finish.
  146. ;;
  147. ;; This seems a bit difficult to reliably trigger, so cheat by manually adding
  148. ;; running 'add-found!' concurrently.
  149. (define (lose* lost-and-found start to/exclusive)
  150. "Lose integers from the range [start to/exclusive), bypassing the GC and not
  151. wrap things in a <losable+datum>."
  152. (when (< start to/exclusive)
  153. (add-found! lost-and-found start)
  154. (lose* lost-and-found (+ 1 start) to/exclusive)))
  155. (define (collect* lost-and-found)
  156. "Return a list of found integers (no need to GC, since the GC and guardian was
  157. bypassed by calling @code{add-found!} directly)."
  158. (perform-operation (collect-lost-and-found-operation lost-and-found)))
  159. ;; In the current implementation of Guile, while to a degree GC is parellelised,
  160. ;; gc hooks are serialised (or maybe not, since ‘this hook is run
  161. (test-assert "concurrent losing"
  162. (run-fibers
  163. (lambda ()
  164. (define %count/fiber 100000)
  165. (define fibers 8)
  166. (define start (make-condition))
  167. (define done-channel (make-channel))
  168. (define lost-and-found (make-lost-and-found))
  169. (define (lose/async from to/exclusive)
  170. (spawn-fiber
  171. (lambda ()
  172. (wait start)
  173. (lose* lost-and-found from to/exclusive)
  174. (put-message done-channel 'done))))
  175. (let loop ((i 0))
  176. (when (< i fibers)
  177. (lose/async (* i %count/fiber) (* (+ 1 i) %count/fiber))
  178. (loop (+ i 1))))
  179. (signal-condition! start)
  180. (let loop ((i 0))
  181. (when (< i fibers)
  182. (get-message done-channel)
  183. (loop (+ i 1))))
  184. (verify (collect* lost-and-found) 0 (* %count/fiber fibers)))
  185. #:install-suspendable-ports? #false ; unnecessary
  186. #:hz 10000))
  187. (test-assert "losing and collecting concurrently"
  188. (run-fibers
  189. (lambda ()
  190. ;; 100000 does not suffice for testing the first
  191. ;; '(loop new-old)' in 'add-found!'.
  192. (define %count/loser 1000000)
  193. (define %losers 8)
  194. (define %collectors 8)
  195. (define start (make-condition))
  196. (define done-losing (make-condition))
  197. (define done-channel/losers (make-channel))
  198. (define done-channel/collectors (make-channel))
  199. (define done-losing-operation
  200. (wrap-operation
  201. (wait-operation done-losing)
  202. (lambda () 'done)))
  203. (define lost-and-found (make-lost-and-found))
  204. (define (lose/async from to/exclusive)
  205. (spawn-fiber
  206. (lambda ()
  207. (wait start)
  208. (lose* lost-and-found from to/exclusive)
  209. (put-message done-channel/losers 'done))))
  210. ;; vector of list of list of collected objects
  211. (define collected (make-vector %collectors))
  212. (define (collect/async i)
  213. (spawn-fiber
  214. (lambda ()
  215. (wait start)
  216. (let loop ((list-of-list-of-results '()))
  217. (define r
  218. (perform-operation
  219. (choice-operation
  220. (collect-lost-and-found-operation lost-and-found)
  221. done-losing-operation)))
  222. (if (eq? r 'done)
  223. (begin
  224. (vector-set! collected i list-of-list-of-results)
  225. (put-message done-channel/collectors 'done))
  226. (loop (cons r list-of-list-of-results)))))))
  227. ;; Start fibers collecting integers.
  228. (let loop ((i 0))
  229. (when (< i %collectors)
  230. (collect/async i)
  231. (loop (+ i 1))))
  232. ;; Start fibers losing integers
  233. (let loop ((i 0))
  234. (when (< i %losers)
  235. (lose/async (* i %count/loser) (* (+ 1 i) %count/loser))
  236. (loop (+ i 1))))
  237. ;; Try starting the collectors and losers start at the same time, to
  238. ;; maximise concurrency.
  239. (signal-condition! start)
  240. (let loop ((i 0))
  241. (when (< i %losers)
  242. (get-message done-channel/losers)
  243. (loop (+ i 1))))
  244. (signal-condition! done-losing)
  245. ;; Wait for 'collected' to be initialised.
  246. (let loop ((i 0))
  247. (when (< i %collectors)
  248. (get-message done-channel/collectors)
  249. (loop (+ i 1))))
  250. ;; Do like 'verify' does, without the - 5 because the GC
  251. ;; was bypassed.
  252. (define results (make-bitvector (* %count/loser %losers)))
  253. (define (register-result! i)
  254. (assert (not (bitvector-bit-set? results i)))
  255. (bitvector-set-bit! results i))
  256. (let loop ((i 0))
  257. (when (< i %collectors)
  258. (for-each
  259. (lambda (list)
  260. (for-each register-result! list))
  261. (vector-ref collected i))
  262. (loop (+ i 1))))
  263. (define fraction (/ (bitvector-count results) (bitvector-length results)))
  264. (pk 'f (+ 0.0 fraction))
  265. (assert (>= fraction 1)))
  266. #:install-suspendable-ports? #false ; unnecessary
  267. #:hz 10000))
  268. (test-assert "lost-and-found as a string (empty)"
  269. (let* ((l (make-lost-and-found))
  270. (expected (format #f "#<lost-and-found ~x empty>"
  271. (object-address l)))
  272. (found (object->string l)))
  273. (string=? expected found)))
  274. ;; It is important to _not_ print the objects inside the lost-and-found,
  275. ;; because <losable> objects keep a lost-and-found in their fields and hence
  276. ;; printing these objects would lead to infinite recursion.
  277. (test-assert "lost-and-found as a string (non-empty)"
  278. (let* ((l (make-lost-and-found))
  279. (expected (format #f "#<lost-and-found ~x non-empty>"
  280. (object-address l))))
  281. (add-found! l (make-losable l))
  282. (define found (object->string l))
  283. (string=? expected found)))
  284. ;; The exception is better raised during the construction of the
  285. ;; <losable> than during the after-gc hook.
  286. (test-error "make-losable without lost-and-found" (make-losable 'bogus))
  287. (test-end "lost-and-found")