Summary
Safepoint polls in the regex path are being strided 1-in-64 as a performance measure. #10494 landed the first (the pre-search poll); a second is in flight for perex_replace_direct::replace. Each is individually sound and individually cheap. What nobody is tracking is the aggregate: the maximum number of iterations along any path between two executed polls.
This issue exists because the thing that will collide with it — concurrent GC — is an approved direction with no issue of its own, so there is currently nowhere durable for this to be recorded. A PR body is not that place; nobody re-reads one.
Today the debt is unobservable, and that is verified, not assumed
gc_runtime_safepoint_poll is two lines wrapping gc_runtime_safepoint_report(). perex_runtime::poll returns Ok(()) unconditionally, so it services no interrupt and can raise no cancellation.
- There is no cross-thread safepoint protocol — no
stop_the_world, no safepoint_request, no rendezvous or handshake in gc/. gc/roots/shadow_stack.rs states the actual model: "GC is stop-the-world relative to this TLS". The collector runs on the allocating thread.
- The one cross-thread mechanism,
GC_UNSAFE_ZONES (gc/policy.rs:4668), is an AtomicI32 suppression counter letting stdlib features block user-initiated gc() while workers hold live refs. Its own doc calls it "a global stop-the-collector". It is a flag, not a rendezvous anyone waits at.
So max-time-to-safepoint is unmeasurable today because nothing is waiting to observe it. That is a statement about the present, not about the design.
Two things that make this worth a tracking issue anyway
1. Strides compose, and each site is justified in isolation. Two sites at 1-in-64 in sequence along a path give up to 128 iterations between executed polls; nested, 4096. Every individual stride will look like "only 1-in-64" to its own reviewer. The quantity that matters to a concurrent collector is not any single stride but the max iteration count along any path between two consecutive executed polls, and nothing currently computes or bounds it. A third and fourth stride, each individually reasonable, is how this becomes a problem without anyone approving it.
2. The 64 is an unmeasured constant that has become precedent. As I understand #10494's own doc comment, removing that poll entirely left cycle_starts, completions and steps identical across 48,000,000 allocation-free calls interleaved with churn. That evidence argues for removal; it does not derive 64. The 64 is a safety margin someone chose — reasonably — and it is now being cited as precedent for a second site. That is how an unmeasured constant becomes load-bearing across a subsystem.
The stated reason for striding rather than removing is to retain "the option of servicing a due collection from a loop that allocates nothing, which is how a non-allocating mutator participates in an incremental cycle." If that is the purpose, then the basis should be justified against that purpose: how stale can a non-allocating mutator's participation get before the incremental cycle suffers? As far as I can tell that has never been measured, at 64 or at any other value.
What would close this
Either of:
Plus, if striding continues: something that bounds the aggregate — max iterations between executed polls along a path — rather than reviewing each site alone.
Note for whoever builds the concurrent collector
A concurrent or parallel collector needs every thread to reach a safepoint promptly. At that point these strides stop being free and the workload that exposes them is a larger input, not a different metric on the same one. There are two today. Please check how many there are then, and what the aggregate is, before assuming the regex path yields quickly.
Filed from the #10362 GC campaign. The #10494 doc-comment details above are as reported to me by the session that read them; the stop-the-world model, the absence of a cross-thread protocol, and the GC_UNSAFE_ZONES semantics I verified directly in the tree.
Summary
Safepoint polls in the regex path are being strided 1-in-64 as a performance measure. #10494 landed the first (the pre-search poll); a second is in flight for
perex_replace_direct::replace. Each is individually sound and individually cheap. What nobody is tracking is the aggregate: the maximum number of iterations along any path between two executed polls.This issue exists because the thing that will collide with it — concurrent GC — is an approved direction with no issue of its own, so there is currently nowhere durable for this to be recorded. A PR body is not that place; nobody re-reads one.
Today the debt is unobservable, and that is verified, not assumed
gc_runtime_safepoint_pollis two lines wrappinggc_runtime_safepoint_report().perex_runtime::pollreturnsOk(())unconditionally, so it services no interrupt and can raise no cancellation.stop_the_world, nosafepoint_request, no rendezvous or handshake ingc/.gc/roots/shadow_stack.rsstates the actual model: "GC is stop-the-world relative to this TLS". The collector runs on the allocating thread.GC_UNSAFE_ZONES(gc/policy.rs:4668), is anAtomicI32suppression counter letting stdlib features block user-initiatedgc()while workers hold live refs. Its own doc calls it "a global stop-the-collector". It is a flag, not a rendezvous anyone waits at.So max-time-to-safepoint is unmeasurable today because nothing is waiting to observe it. That is a statement about the present, not about the design.
Two things that make this worth a tracking issue anyway
1. Strides compose, and each site is justified in isolation. Two sites at 1-in-64 in sequence along a path give up to 128 iterations between executed polls; nested, 4096. Every individual stride will look like "only 1-in-64" to its own reviewer. The quantity that matters to a concurrent collector is not any single stride but the max iteration count along any path between two consecutive executed polls, and nothing currently computes or bounds it. A third and fourth stride, each individually reasonable, is how this becomes a problem without anyone approving it.
2. The 64 is an unmeasured constant that has become precedent. As I understand #10494's own doc comment, removing that poll entirely left
cycle_starts,completionsandstepsidentical across 48,000,000 allocation-free calls interleaved with churn. That evidence argues for removal; it does not derive 64. The 64 is a safety margin someone chose — reasonably — and it is now being cited as precedent for a second site. That is how an unmeasured constant becomes load-bearing across a subsystem.The stated reason for striding rather than removing is to retain "the option of servicing a due collection from a loop that allocates nothing, which is how a non-allocating mutator participates in an incremental cycle." If that is the purpose, then the basis should be justified against that purpose: how stale can a non-allocating mutator's participation get before the incremental cycle suffers? As far as I can tell that has never been measured, at 64 or at any other value.
What would close this
Either of:
Plus, if striding continues: something that bounds the aggregate — max iterations between executed polls along a path — rather than reviewing each site alone.
Note for whoever builds the concurrent collector
A concurrent or parallel collector needs every thread to reach a safepoint promptly. At that point these strides stop being free and the workload that exposes them is a larger input, not a different metric on the same one. There are two today. Please check how many there are then, and what the aggregate is, before assuming the regex path yields quickly.
Filed from the #10362 GC campaign. The
#10494doc-comment details above are as reported to me by the session that read them; the stop-the-world model, the absence of a cross-thread protocol, and theGC_UNSAFE_ZONESsemantics I verified directly in the tree.