mirror of
https://github.com/nim-lang/Nim.git
synced 2026-08-16 03:48:19 +00:00
179 lines
6.7 KiB
Nim
179 lines
6.7 KiB
Nim
discard """
|
|
cmd: "nim c --mm:yrc -d:useMalloc --threads:on $file"
|
|
output: "ok"
|
|
disabled: "windows"
|
|
disabled: "freebsd"
|
|
disabled: "openbsd"
|
|
"""
|
|
|
|
# Generational epoch stamps, young -> old.
|
|
#
|
|
# Each thread keeps a long-lived cyclic web and promotes it past
|
|
# YrcPromoteAge with a few seeded partial collects. After that, every
|
|
# iteration allocates die-young cyclic rings that reference the web -- the
|
|
# classic new-refers-to-old pattern. Capture then prunes at the stamp
|
|
# boundary and commit `trialDec`s the young -> web edges WITHOUT re-rooting
|
|
# the web, so from that point on the web's liveness no longer rests on being
|
|
# traced: it rests on the deferred machinery (the per-thread suspect buffer
|
|
# and the pruned-target list) keeping those cells examinable and alive until
|
|
# the epoch advances.
|
|
#
|
|
# Two properties are asserted. The surviving web is walked in full at the
|
|
# end, so a web that was collected or partially collected out from under the
|
|
# deferred machinery shows up as a nil edge, a corrupted id or a short node
|
|
# count. And after every web is dropped, a full collect must reclaim all of
|
|
# it -- deferring reclamation to the epoch boundary must not turn into never
|
|
# reclaiming.
|
|
#
|
|
# Sized so the shared epoch clock (YrcEpochLen collections) turns over
|
|
# repeatedly mid-run: the steady-state suspect flush is on the path under
|
|
# test, not just the one forced by the final GC_fullCollect. Measured on the
|
|
# current collector this run remembers ~200 suspects across ~80 flushes.
|
|
#
|
|
# NOTE: this is a functional test of the generational path, not a regression
|
|
# test for the dangling-suspect use-after-free that path once had. It was
|
|
# tried in that role and does not reproduce it: the suspects it creates are
|
|
# nearly always flushed before they die, so the bad ordering never comes up.
|
|
# tests/async/tasyncawait.nim reproduces that one reliably.
|
|
|
|
const
|
|
NumThreads = 4
|
|
WebSize = 8_000 ## the web that survives to the integrity check
|
|
DoomedSize = 800 ## promoted, given young -> old edges, then dropped
|
|
WebDegree = 4
|
|
SeedProbes = 6 ## must exceed YrcPromoteAge (3) to promote a web
|
|
OuterIters = 40 ## with NumThreads, enough collections to cross epochs
|
|
YoungBatches = 8
|
|
YoungRing = 100
|
|
|
|
type
|
|
WebNode = ref object
|
|
id: int32
|
|
seen: int32 ## walk marker, plain data: no GC interaction
|
|
edges: array[WebDegree, WebNode]
|
|
|
|
Bridge = ref object
|
|
toWeb: WebNode
|
|
self: Bridge
|
|
|
|
YoungNode = ref object
|
|
next: YoungNode
|
|
hub: WebNode ## the young -> old edge under test
|
|
self: YoungNode
|
|
|
|
var probeSlot {.threadvar.}: Bridge
|
|
|
|
proc buildWebNodes(n: int): seq[WebNode] =
|
|
## Strongly connected mesh: one incoming edge pulls the whole web into any
|
|
## collector that does not prune at the stamp boundary.
|
|
result = newSeq[WebNode](n)
|
|
for i in 0 ..< n:
|
|
result[i] = WebNode(id: int32(i))
|
|
for i in 0 ..< n:
|
|
for d in 0 ..< WebDegree:
|
|
result[i].edges[d] = result[(i + 1 + d * 97) mod n]
|
|
|
|
proc buildWeb(n: int): WebNode = buildWebNodes(n)[0]
|
|
|
|
proc checkWeb(root: WebNode; n: int) =
|
|
## Every node reachable exactly once, every edge intact. A web that was
|
|
## collected out from under us fails here instead of faulting later.
|
|
var stack = @[root]
|
|
root.seen = 1
|
|
var count = 0
|
|
while stack.len > 0:
|
|
let x = stack.pop()
|
|
inc count
|
|
doAssert x.id >= 0'i32 and x.id < int32(n), "web node corrupted: id " & $x.id
|
|
for d in 0 ..< WebDegree:
|
|
let e = x.edges[d]
|
|
doAssert e != nil, "web edge nil'ed at node " & $x.id
|
|
if e.seen != 1:
|
|
e.seen = 1
|
|
stack.add e
|
|
doAssert count == n, "web lost nodes: " & $count & " of " & $n
|
|
|
|
proc paintYoung(hub: WebNode; n: int) =
|
|
## Ring of `n` self-referential nodes, each pointing at the web. When the
|
|
## seq drops, the ring is garbage whose only external edges go into the
|
|
## live (and by now stamp-pruned) web.
|
|
var nodes = newSeq[YoungNode](n)
|
|
for i in 0 ..< n:
|
|
nodes[i] = YoungNode(hub: hub)
|
|
for i in 0 ..< n:
|
|
nodes[i].next = nodes[(i + 1) mod n]
|
|
nodes[i].self = nodes[i]
|
|
|
|
proc paintYoungSpread(web: seq[WebNode]; n: int) =
|
|
## Same, but every young node targets a DIFFERENT old cell, so the commit
|
|
## deposits many distinct cells in the suspect buffer instead of just the
|
|
## web root. Breadth here is what makes the "suspect dies before the epoch
|
|
## flush" ordering likely rather than incidental.
|
|
var nodes = newSeq[YoungNode](n)
|
|
for i in 0 ..< n:
|
|
nodes[i] = YoungNode(hub: web[(i * 7) mod web.len])
|
|
for i in 0 ..< n:
|
|
nodes[i].next = nodes[(i + 1) mod n]
|
|
nodes[i].self = nodes[i]
|
|
|
|
proc probeBridge(b: Bridge) {.noinline.} =
|
|
## Seeded false alarm so a partial collect traces -- and stamps -- the web.
|
|
## The threadvar slot is deliberate: a stack temporary is not a reliable
|
|
## way to get the bridge registered as a candidate root.
|
|
probeSlot = b
|
|
probeSlot = nil
|
|
|
|
proc promote(b: Bridge) =
|
|
## Trace-and-stamp the bridge's web often enough that its cells pass
|
|
## YrcPromoteAge and captures start pruning at them.
|
|
for _ in 1 .. SeedProbes:
|
|
probeBridge(b)
|
|
# Deliberately not GC_fullCollect: that advances the epoch and wipes the
|
|
# stamps this test needs.
|
|
GC_partialCollect(0)
|
|
|
|
proc cycleDoomedWeb() =
|
|
## Promote a web, hand it young -> old edges so its cells land in the
|
|
## deferred suspect buffer, then drop it. Those cells are now garbage
|
|
## while still listed, and an ordinary collection reclaims them well
|
|
## before the epoch advance that flushes the buffer. THIS is the case a
|
|
## live-forever web never produces: the list has to not be holding
|
|
## pointers to cells anyone else was free to reclaim.
|
|
let doomedNodes = buildWebNodes(DoomedSize)
|
|
let b = Bridge(toWeb: doomedNodes[0])
|
|
b.self = b
|
|
promote(b)
|
|
for _ in 1 .. YoungBatches:
|
|
paintYoungSpread(doomedNodes, YoungRing)
|
|
GC_partialCollect(0)
|
|
# `doomedNodes` and `b` die with this scope: every cell that just landed
|
|
# in the suspect buffer is now garbage while still listed there.
|
|
|
|
proc threadWork() {.thread.} =
|
|
let web = buildWeb(WebSize)
|
|
let bridge = Bridge(toWeb: web)
|
|
bridge.self = bridge
|
|
promote(bridge)
|
|
|
|
for i in 1 .. OuterIters:
|
|
cycleDoomedWeb()
|
|
for _ in 1 .. YoungBatches:
|
|
paintYoung(web, YoungRing)
|
|
GC_partialCollect(0)
|
|
|
|
checkWeb(web, WebSize)
|
|
doAssert bridge.toWeb == web, "bridge lost its web"
|
|
|
|
var threads: array[NumThreads, Thread[void]]
|
|
for i in 0 ..< NumThreads:
|
|
createThread(threads[i], threadWork)
|
|
joinThreads(threads)
|
|
|
|
# Every web is unreachable now. The full collect advances the epoch, which
|
|
# flushes the suspect buffers into the root set -- the major-collection half
|
|
# of the scheme -- so all of it must come back.
|
|
GC_fullCollect()
|
|
doAssert getOccupiedMem() < 8 * 1024 * 1024,
|
|
"webs not reclaimed: " & $(getOccupiedMem() div 1024) & " KiB still occupied"
|
|
echo "ok"
|