]> Cypherpunks.ru repositories - gostls13.git/blobdiff - src/runtime/mgcpacer.go
runtime: refactor runtime->tracer API to appear more like a lock
[gostls13.git] / src / runtime / mgcpacer.go
index 503b8110b3b80711c07ac37a5babcac051491ba0..716e3efcccebba33414183211a6784a3bb2c5ef9 100644 (file)
@@ -6,14 +6,19 @@ package runtime
 
 import (
        "internal/cpu"
+       "internal/goexperiment"
        "runtime/internal/atomic"
-       "unsafe"
+       _ "unsafe" // for go:linkname
 )
 
 const (
        // gcGoalUtilization is the goal CPU utilization for
        // marking as a fraction of GOMAXPROCS.
-       gcGoalUtilization = 0.30
+       //
+       // Increasing the goal utilization will shorten GC cycles as the GC
+       // has more resources behind it, lessening costs from the write barrier,
+       // but comes at the cost of increasing mutator latency.
+       gcGoalUtilization = gcBackgroundUtilization
 
        // gcBackgroundUtilization is the fixed CPU utilization for background
        // marking. It must be <= gcGoalUtilization. The difference between
@@ -21,15 +26,18 @@ const (
        // mark assists. The scheduler will aim to use within 50% of this
        // goal.
        //
-       // Setting this to < gcGoalUtilization avoids saturating the trigger
-       // feedback controller when there are no assists, which allows it to
-       // better control CPU and heap growth. However, the larger the gap,
-       // the more mutator assists are expected to happen, which impact
-       // mutator latency.
+       // As a general rule, there's little reason to set gcBackgroundUtilization
+       // < gcGoalUtilization. One reason might be in mostly idle applications,
+       // where goroutines are unlikely to assist at all, so the actual
+       // utilization will be lower than the goal. But this is moot point
+       // because the idle mark workers already soak up idle CPU resources.
+       // These two values are still kept separate however because they are
+       // distinct conceptually, and in previous iterations of the pacer the
+       // distinction was more important.
        gcBackgroundUtilization = 0.25
 
        // gcCreditSlack is the amount of scan work credit that can
-       // accumulate locally before updating gcController.scanWork and,
+       // accumulate locally before updating gcController.heapScanWork and,
        // optionally, gcController.bgScanCredit. Lower values give a more
        // accurate assist ratio and make it more likely that assists will
        // successfully steal background credit. Higher values reduce memory
@@ -46,40 +54,52 @@ const (
        gcOverAssistWork = 64 << 10
 
        // defaultHeapMinimum is the value of heapMinimum for GOGC==100.
-       defaultHeapMinimum = 4 << 20
+       defaultHeapMinimum = (goexperiment.HeapMinimum512KiBInt)*(512<<10) +
+               (1-goexperiment.HeapMinimum512KiBInt)*(4<<20)
+
+       // maxStackScanSlack is the bytes of stack space allocated or freed
+       // that can accumulate on a P before updating gcController.stackSize.
+       maxStackScanSlack = 8 << 10
+
+       // memoryLimitMinHeapGoalHeadroom is the minimum amount of headroom the
+       // pacer gives to the heap goal when operating in the memory-limited regime.
+       // That is, it'll reduce the heap goal by this many extra bytes off of the
+       // base calculation, at minimum.
+       memoryLimitMinHeapGoalHeadroom = 1 << 20
+
+       // memoryLimitHeapGoalHeadroomPercent is how headroom the memory-limit-based
+       // heap goal should have as a percent of the maximum possible heap goal allowed
+       // to maintain the memory limit.
+       memoryLimitHeapGoalHeadroomPercent = 3
 )
 
-func init() {
-       if offset := unsafe.Offsetof(gcController.heapLive); offset%8 != 0 {
-               println(offset)
-               throw("gcController.heapLive not aligned to 8 bytes")
-       }
-}
-
 // gcController implements the GC pacing controller that determines
 // when to trigger concurrent garbage collection and how much marking
 // work to do in mutator assists and background marking.
 //
-// It uses a feedback control algorithm to adjust the gcController.trigger
-// trigger based on the heap growth and GC CPU utilization each cycle.
-// This algorithm optimizes for heap growth to match GOGC and for CPU
-// utilization between assist and background marking to be 25% of
+// It calculates the ratio between the allocation rate (in terms of CPU
+// time) and the GC scan throughput to determine the heap size at which to
+// trigger a GC cycle such that no GC assists are required to finish on time.
+// This algorithm thus optimizes GC CPU utilization to the dedicated background
+// mark utilization of 25% of GOMAXPROCS by minimizing GC assists.
 // GOMAXPROCS. The high-level design of this algorithm is documented
-// at https://golang.org/s/go15gcpacing.
-//
-// All fields of gcController are used only during a single mark
-// cycle.
+// at https://github.com/golang/proposal/blob/master/design/44167-gc-pacer-redesign.md.
+// See https://golang.org/s/go15gcpacing for additional historical context.
 var gcController gcControllerState
 
 type gcControllerState struct {
-       // Initialized from $GOGC. GOGC=off means no GC.
-       //
-       // Updated atomically with mheap_.lock held or during a STW.
-       // Safe to read atomically at any time, or non-atomically with
-       // mheap_.lock or STW.
-       gcPercent int32
+       // Initialized from GOGC. GOGC=off means no GC.
+       gcPercent atomic.Int32
 
-       _ uint32 // padding so following 64-bit values are 8-byte aligned
+       // memoryLimit is the soft memory limit in bytes.
+       //
+       // Initialized from GOMEMLIMIT. GOMEMLIMIT=off is equivalent to MaxInt64
+       // which means no soft memory limit in practice.
+       //
+       // This is an int64 instead of a uint64 to more easily maintain parity with
+       // the SetMemoryLimit API, which sets a maximum at MaxInt64. This value
+       // should never be negative.
+       memoryLimit atomic.Int64
 
        // heapMinimum is the minimum heap size at which to trigger GC.
        // For small heaps, this overrides the usual GOGC*live set rule.
@@ -95,37 +115,58 @@ type gcControllerState struct {
        // debugging.
        heapMinimum uint64
 
-       // triggerRatio is the heap growth ratio that triggers marking.
+       // runway is the amount of runway in heap bytes allocated by the
+       // application that we want to give the GC once it starts.
        //
-       // E.g., if this is 0.6, then GC should start when the live
-       // heap has reached 1.6 times the heap size marked by the
-       // previous cycle. This should be ≤ GOGC/100 so the trigger
-       // heap size is less than the goal heap size. This is set
-       // during mark termination for the next cycle's trigger.
-       //
-       // Protected by mheap_.lock or a STW.
-       triggerRatio float64
+       // This is computed from consMark during mark termination.
+       runway atomic.Uint64
 
-       // trigger is the heap size that triggers marking.
+       // consMark is the estimated per-CPU consMark ratio for the application.
        //
-       // When heapLive ≥ trigger, the mark phase will start.
-       // This is also the heap size by which proportional sweeping
-       // must be complete.
+       // It represents the ratio between the application's allocation
+       // rate, as bytes allocated per CPU-time, and the GC's scan rate,
+       // as bytes scanned per CPU-time.
+       // The units of this ratio are (B / cpu-ns) / (B / cpu-ns).
        //
-       // This is computed from triggerRatio during mark termination
-       // for the next cycle's trigger.
+       // At a high level, this value is computed as the bytes of memory
+       // allocated (cons) per unit of scan work completed (mark) in a GC
+       // cycle, divided by the CPU time spent on each activity.
        //
-       // Protected by mheap_.lock or a STW.
-       trigger uint64
+       // Updated at the end of each GC cycle, in endCycle.
+       consMark float64
+
+       // lastConsMark is the computed cons/mark value for the previous 4 GC
+       // cycles. Note that this is *not* the last value of consMark, but the
+       // measured cons/mark value in endCycle.
+       lastConsMark [4]float64
 
-       // heapGoal is the goal heapLive for when next GC ends.
-       // Set to ^uint64(0) if disabled.
+       // gcPercentHeapGoal is the goal heapLive for when next GC ends derived
+       // from gcPercent.
        //
-       // Read and written atomically, unless the world is stopped.
-       heapGoal uint64
+       // Set to ^uint64(0) if gcPercent is disabled.
+       gcPercentHeapGoal atomic.Uint64
 
-       // lastHeapGoal is the value of heapGoal for the previous GC.
-       // Note that this is distinct from the last value heapGoal had,
+       // sweepDistMinTrigger is the minimum trigger to ensure a minimum
+       // sweep distance.
+       //
+       // This bound is also special because it applies to both the trigger
+       // *and* the goal (all other trigger bounds must be based *on* the goal).
+       //
+       // It is computed ahead of time, at commit time. The theory is that,
+       // absent a sudden change to a parameter like gcPercent, the trigger
+       // will be chosen to always give the sweeper enough headroom. However,
+       // such a change might dramatically and suddenly move up the trigger,
+       // in which case we need to ensure the sweeper still has enough headroom.
+       sweepDistMinTrigger atomic.Uint64
+
+       // triggered is the point at which the current GC cycle actually triggered.
+       // Only valid during the mark phase of a GC cycle, otherwise set to ^uint64(0).
+       //
+       // Updated while the world is stopped.
+       triggered uint64
+
+       // lastHeapGoal is the value of heapGoal at the moment the last GC
+       // ended. Note that this is distinct from the last value heapGoal had,
        // because it could change if e.g. gcPercent changes.
        //
        // Read and written with the world stopped or with mheap_.lock held.
@@ -133,38 +174,57 @@ type gcControllerState struct {
 
        // heapLive is the number of bytes considered live by the GC.
        // That is: retained by the most recent GC plus allocated
-       // since then. heapLive ≤ memstats.heapAlloc, since heapAlloc includes
-       // unmarked objects that have not yet been swept (and hence goes up as we
-       // allocate and down as we sweep) while heapLive excludes these
-       // objects (and hence only goes up between GCs).
-       //
-       // This is updated atomically without locking. To reduce
-       // contention, this is updated only when obtaining a span from
-       // an mcentral and at this point it counts all of the
-       // unallocated slots in that span (which will be allocated
-       // before that mcache obtains another span from that
-       // mcentral). Hence, it slightly overestimates the "true" live
-       // heap size. It's better to overestimate than to
-       // underestimate because 1) this triggers the GC earlier than
-       // necessary rather than potentially too late and 2) this
-       // leads to a conservative GC rate rather than a GC rate that
-       // is potentially too low.
-       //
-       // Reads should likewise be atomic (or during STW).
+       // since then. heapLive ≤ memstats.totalAlloc-memstats.totalFree, since
+       // heapAlloc includes unmarked objects that have not yet been swept (and
+       // hence goes up as we allocate and down as we sweep) while heapLive
+       // excludes these objects (and hence only goes up between GCs).
+       //
+       // To reduce contention, this is updated only when obtaining a span
+       // from an mcentral and at this point it counts all of the unallocated
+       // slots in that span (which will be allocated before that mcache
+       // obtains another span from that mcentral). Hence, it slightly
+       // overestimates the "true" live heap size. It's better to overestimate
+       // than to underestimate because 1) this triggers the GC earlier than
+       // necessary rather than potentially too late and 2) this leads to a
+       // conservative GC rate rather than a GC rate that is potentially too
+       // low.
        //
        // Whenever this is updated, call traceHeapAlloc() and
        // this gcControllerState's revise() method.
-       heapLive uint64
+       heapLive atomic.Uint64
 
-       // heapScan is the number of bytes of "scannable" heap. This
-       // is the live heap (as counted by heapLive), but omitting
-       // no-scan objects and no-scan tails of objects.
+       // heapScan is the number of bytes of "scannable" heap. This is the
+       // live heap (as counted by heapLive), but omitting no-scan objects and
+       // no-scan tails of objects.
        //
-       // Whenever this is updated, call this gcControllerState's
-       // revise() method.
+       // This value is fixed at the start of a GC cycle. It represents the
+       // maximum scannable heap.
+       heapScan atomic.Uint64
+
+       // lastHeapScan is the number of bytes of heap that were scanned
+       // last GC cycle. It is the same as heapMarked, but only
+       // includes the "scannable" parts of objects.
+       //
+       // Updated when the world is stopped.
+       lastHeapScan uint64
+
+       // lastStackScan is the number of bytes of stack that were scanned
+       // last GC cycle.
+       lastStackScan atomic.Uint64
+
+       // maxStackScan is the amount of allocated goroutine stack space in
+       // use by goroutines.
        //
-       // Read and written atomically or with the world stopped.
-       heapScan uint64
+       // This number tracks allocated goroutine stack space rather than used
+       // goroutine stack space (i.e. what is actually scanned) because used
+       // goroutine stack space is much harder to measure cheaply. By using
+       // allocated space, we make an overestimate; this is OK, it's better
+       // to conservatively overcount than undercount.
+       maxStackScan atomic.Uint64
+
+       // globalsScan is the total amount of global variable space
+       // that is scannable.
+       globalsScan atomic.Uint64
 
        // heapMarked is the number of bytes marked by the previous
        // GC. After mark termination, heapLive == heapMarked, but
@@ -172,55 +232,94 @@ type gcControllerState struct {
        // next mark termination.
        heapMarked uint64
 
-       // scanWork is the total scan work performed this cycle. This
-       // is updated atomically during the cycle. Updates occur in
-       // bounded batches, since it is both written and read
-       // throughout the cycle. At the end of the cycle, this is how
+       // heapScanWork is the total heap scan work performed this cycle.
+       // stackScanWork is the total stack scan work performed this cycle.
+       // globalsScanWork is the total globals scan work performed this cycle.
+       //
+       // These are updated atomically during the cycle. Updates occur in
+       // bounded batches, since they are both written and read
+       // throughout the cycle. At the end of the cycle, heapScanWork is how
        // much of the retained heap is scannable.
        //
-       // Currently this is the bytes of heap scanned. For most uses,
-       // this is an opaque unit of work, but for estimation the
-       // definition is important.
-       scanWork int64
-
-       // bgScanCredit is the scan work credit accumulated by the
-       // concurrent background scan. This credit is accumulated by
-       // the background scan and stolen by mutator assists. This is
-       // updated atomically. Updates occur in bounded batches, since
-       // it is both written and read throughout the cycle.
-       bgScanCredit int64
+       // Currently these are measured in bytes. For most uses, this is an
+       // opaque unit of work, but for estimation the definition is important.
+       //
+       // Note that stackScanWork includes only stack space scanned, not all
+       // of the allocated stack.
+       heapScanWork    atomic.Int64
+       stackScanWork   atomic.Int64
+       globalsScanWork atomic.Int64
+
+       // bgScanCredit is the scan work credit accumulated by the concurrent
+       // background scan. This credit is accumulated by the background scan
+       // and stolen by mutator assists.  Updates occur in bounded batches,
+       // since it is both written and read throughout the cycle.
+       bgScanCredit atomic.Int64
 
        // assistTime is the nanoseconds spent in mutator assists
-       // during this cycle. This is updated atomically. Updates
-       // occur in bounded batches, since it is both written and read
-       // throughout the cycle.
-       assistTime int64
-
-       // dedicatedMarkTime is the nanoseconds spent in dedicated
-       // mark workers during this cycle. This is updated atomically
-       // at the end of the concurrent mark phase.
-       dedicatedMarkTime int64
-
-       // fractionalMarkTime is the nanoseconds spent in the
-       // fractional mark worker during this cycle. This is updated
-       // atomically throughout the cycle and will be up-to-date if
-       // the fractional mark worker is not currently running.
-       fractionalMarkTime int64
-
-       // idleMarkTime is the nanoseconds spent in idle marking
-       // during this cycle. This is updated atomically throughout
-       // the cycle.
-       idleMarkTime int64
+       // during this cycle. This is updated atomically, and must also
+       // be updated atomically even during a STW, because it is read
+       // by sysmon. Updates occur in bounded batches, since it is both
+       // written and read throughout the cycle.
+       assistTime atomic.Int64
+
+       // dedicatedMarkTime is the nanoseconds spent in dedicated mark workers
+       // during this cycle. This is updated at the end of the concurrent mark
+       // phase.
+       dedicatedMarkTime atomic.Int64
+
+       // fractionalMarkTime is the nanoseconds spent in the fractional mark
+       // worker during this cycle. This is updated throughout the cycle and
+       // will be up-to-date if the fractional mark worker is not currently
+       // running.
+       fractionalMarkTime atomic.Int64
+
+       // idleMarkTime is the nanoseconds spent in idle marking during this
+       // cycle. This is updated throughout the cycle.
+       idleMarkTime atomic.Int64
 
        // markStartTime is the absolute start time in nanoseconds
        // that assists and background mark workers started.
        markStartTime int64
 
-       // dedicatedMarkWorkersNeeded is the number of dedicated mark
-       // workers that need to be started. This is computed at the
-       // beginning of each cycle and decremented atomically as
-       // dedicated mark workers get started.
-       dedicatedMarkWorkersNeeded int64
+       // dedicatedMarkWorkersNeeded is the number of dedicated mark workers
+       // that need to be started. This is computed at the beginning of each
+       // cycle and decremented as dedicated mark workers get started.
+       dedicatedMarkWorkersNeeded atomic.Int64
+
+       // idleMarkWorkers is two packed int32 values in a single uint64.
+       // These two values are always updated simultaneously.
+       //
+       // The bottom int32 is the current number of idle mark workers executing.
+       //
+       // The top int32 is the maximum number of idle mark workers allowed to
+       // execute concurrently. Normally, this number is just gomaxprocs. However,
+       // during periodic GC cycles it is set to 0 because the system is idle
+       // anyway; there's no need to go full blast on all of GOMAXPROCS.
+       //
+       // The maximum number of idle mark workers is used to prevent new workers
+       // from starting, but it is not a hard maximum. It is possible (but
+       // exceedingly rare) for the current number of idle mark workers to
+       // transiently exceed the maximum. This could happen if the maximum changes
+       // just after a GC ends, and an M with no P.
+       //
+       // Note that if we have no dedicated mark workers, we set this value to
+       // 1 in this case we only have fractional GC workers which aren't scheduled
+       // strictly enough to ensure GC progress. As a result, idle-priority mark
+       // workers are vital to GC progress in these situations.
+       //
+       // For example, consider a situation in which goroutines block on the GC
+       // (such as via runtime.GOMAXPROCS) and only fractional mark workers are
+       // scheduled (e.g. GOMAXPROCS=1). Without idle-priority mark workers, the
+       // last running M might skip scheduling a fractional mark worker if its
+       // utilization goal is met, such that once it goes to sleep (because there's
+       // nothing to do), there will be nothing else to spin up a new M for the
+       // fractional worker in the future, stalling GC progress and causing a
+       // deadlock. However, idle-priority workers will *always* run when there is
+       // nothing left to do, ensuring the GC makes progress.
+       //
+       // See github.com/golang/go/issues/44163 for more details.
+       idleMarkWorkers atomic.Uint64
 
        // assistWorkPerByte is the ratio of scan work to allocated
        // bytes that should be performed by mutator assists. This is
@@ -247,73 +346,79 @@ type gcControllerState struct {
        // If this is zero, no fractional workers are needed.
        fractionalUtilizationGoal float64
 
+       // These memory stats are effectively duplicates of fields from
+       // memstats.heapStats but are updated atomically or with the world
+       // stopped and don't provide the same consistency guarantees.
+       //
+       // Because the runtime is responsible for managing a memory limit, it's
+       // useful to couple these stats more tightly to the gcController, which
+       // is intimately connected to how that memory limit is maintained.
+       heapInUse    sysMemStat    // bytes in mSpanInUse spans
+       heapReleased sysMemStat    // bytes released to the OS
+       heapFree     sysMemStat    // bytes not in any span, but not released to the OS
+       totalAlloc   atomic.Uint64 // total bytes allocated
+       totalFree    atomic.Uint64 // total bytes freed
+       mappedReady  atomic.Uint64 // total virtual memory in the Ready state (see mem.go).
+
+       // test indicates that this is a test-only copy of gcControllerState.
+       test bool
+
        _ cpu.CacheLinePad
 }
 
-func (c *gcControllerState) init(gcPercent int32) {
+func (c *gcControllerState) init(gcPercent int32, memoryLimit int64) {
        c.heapMinimum = defaultHeapMinimum
-
-       // Set a reasonable initial GC trigger.
-       c.triggerRatio = 7 / 8.0
-
-       // Fake a heapMarked value so it looks like a trigger at
-       // heapMinimum is the appropriate growth from heapMarked.
-       // This will go into computing the initial GC goal.
-       c.heapMarked = uint64(float64(c.heapMinimum) / (1 + c.triggerRatio))
-
-       // This will also compute and set the GC trigger and goal.
+       c.triggered = ^uint64(0)
        c.setGCPercent(gcPercent)
+       c.setMemoryLimit(memoryLimit)
+       c.commit(true) // No sweep phase in the first GC cycle.
+       // N.B. Don't bother calling traceHeapGoal. Tracing is never enabled at
+       // initialization time.
+       // N.B. No need to call revise; there's no GC enabled during
+       // initialization.
 }
 
 // startCycle resets the GC controller's state and computes estimates
 // for a new GC cycle. The caller must hold worldsema and the world
 // must be stopped.
-func (c *gcControllerState) startCycle(markStartTime int64) {
-       c.scanWork = 0
-       c.bgScanCredit = 0
-       c.assistTime = 0
-       c.dedicatedMarkTime = 0
-       c.fractionalMarkTime = 0
-       c.idleMarkTime = 0
+func (c *gcControllerState) startCycle(markStartTime int64, procs int, trigger gcTrigger) {
+       c.heapScanWork.Store(0)
+       c.stackScanWork.Store(0)
+       c.globalsScanWork.Store(0)
+       c.bgScanCredit.Store(0)
+       c.assistTime.Store(0)
+       c.dedicatedMarkTime.Store(0)
+       c.fractionalMarkTime.Store(0)
+       c.idleMarkTime.Store(0)
        c.markStartTime = markStartTime
-
-       // Ensure that the heap goal is at least a little larger than
-       // the current live heap size. This may not be the case if GC
-       // start is delayed or if the allocation that pushed gcController.heapLive
-       // over trigger is large or if the trigger is really close to
-       // GOGC. Assist is proportional to this distance, so enforce a
-       // minimum distance, even if it means going over the GOGC goal
-       // by a tiny bit.
-       if c.heapGoal < c.heapLive+1024*1024 {
-               c.heapGoal = c.heapLive + 1024*1024
-       }
+       c.triggered = c.heapLive.Load()
 
        // Compute the background mark utilization goal. In general,
        // this may not come out exactly. We round the number of
        // dedicated workers so that the utilization is closest to
        // 25%. For small GOMAXPROCS, this would introduce too much
        // error, so we add fractional workers in that case.
-       totalUtilizationGoal := float64(gomaxprocs) * gcBackgroundUtilization
-       c.dedicatedMarkWorkersNeeded = int64(totalUtilizationGoal + 0.5)
-       utilError := float64(c.dedicatedMarkWorkersNeeded)/totalUtilizationGoal - 1
+       totalUtilizationGoal := float64(procs) * gcBackgroundUtilization
+       dedicatedMarkWorkersNeeded := int64(totalUtilizationGoal + 0.5)
+       utilError := float64(dedicatedMarkWorkersNeeded)/totalUtilizationGoal - 1
        const maxUtilError = 0.3
        if utilError < -maxUtilError || utilError > maxUtilError {
                // Rounding put us more than 30% off our goal. With
                // gcBackgroundUtilization of 25%, this happens for
                // GOMAXPROCS<=3 or GOMAXPROCS=6. Enable fractional
                // workers to compensate.
-               if float64(c.dedicatedMarkWorkersNeeded) > totalUtilizationGoal {
+               if float64(dedicatedMarkWorkersNeeded) > totalUtilizationGoal {
                        // Too many dedicated workers.
-                       c.dedicatedMarkWorkersNeeded--
+                       dedicatedMarkWorkersNeeded--
                }
-               c.fractionalUtilizationGoal = (totalUtilizationGoal - float64(c.dedicatedMarkWorkersNeeded)) / float64(gomaxprocs)
+               c.fractionalUtilizationGoal = (totalUtilizationGoal - float64(dedicatedMarkWorkersNeeded)) / float64(procs)
        } else {
                c.fractionalUtilizationGoal = 0
        }
 
        // In STW mode, we just want dedicated workers.
        if debug.gcstoptheworld > 0 {
-               c.dedicatedMarkWorkersNeeded = int64(gomaxprocs)
+               dedicatedMarkWorkersNeeded = int64(procs)
                c.fractionalUtilizationGoal = 0
        }
 
@@ -323,25 +428,48 @@ func (c *gcControllerState) startCycle(markStartTime int64) {
                p.gcFractionalMarkTime = 0
        }
 
+       if trigger.kind == gcTriggerTime {
+               // During a periodic GC cycle, reduce the number of idle mark workers
+               // required. However, we need at least one dedicated mark worker or
+               // idle GC worker to ensure GC progress in some scenarios (see comment
+               // on maxIdleMarkWorkers).
+               if dedicatedMarkWorkersNeeded > 0 {
+                       c.setMaxIdleMarkWorkers(0)
+               } else {
+                       // TODO(mknyszek): The fundamental reason why we need this is because
+                       // we can't count on the fractional mark worker to get scheduled.
+                       // Fix that by ensuring it gets scheduled according to its quota even
+                       // if the rest of the application is idle.
+                       c.setMaxIdleMarkWorkers(1)
+               }
+       } else {
+               // N.B. gomaxprocs and dedicatedMarkWorkersNeeded are guaranteed not to
+               // change during a GC cycle.
+               c.setMaxIdleMarkWorkers(int32(procs) - int32(dedicatedMarkWorkersNeeded))
+       }
+
        // Compute initial values for controls that are updated
        // throughout the cycle.
+       c.dedicatedMarkWorkersNeeded.Store(dedicatedMarkWorkersNeeded)
        c.revise()
 
        if debug.gcpacertrace > 0 {
+               heapGoal := c.heapGoal()
                assistRatio := c.assistWorkPerByte.Load()
                print("pacer: assist ratio=", assistRatio,
-                       " (scan ", gcController.heapScan>>20, " MB in ",
+                       " (scan ", gcController.heapScan.Load()>>20, " MB in ",
                        work.initialHeapLive>>20, "->",
-                       c.heapGoal>>20, " MB)",
-                       " workers=", c.dedicatedMarkWorkersNeeded,
+                       heapGoal>>20, " MB)",
+                       " workers=", dedicatedMarkWorkersNeeded,
                        "+", c.fractionalUtilizationGoal, "\n")
        }
 }
 
 // revise updates the assist ratio during the GC cycle to account for
 // improved estimates. This should be called whenever gcController.heapScan,
-// gcController.heapLive, or gcController.heapGoal is updated. It is safe to
-// call concurrently, but it may race with other calls to revise.
+// gcController.heapLive, or if any inputs to gcController.heapGoal are
+// updated. It is safe to call concurrently, but it may race with other
+// calls to revise.
 //
 // The result of this race is that the two assist ratio values may not line
 // up or may be stale. In practice this is OK because the assist ratio
@@ -360,40 +488,66 @@ func (c *gcControllerState) startCycle(markStartTime int64) {
 // is when assists are enabled and the necessary statistics are
 // available).
 func (c *gcControllerState) revise() {
-       gcPercent := atomic.Loadint32(&c.gcPercent)
+       gcPercent := c.gcPercent.Load()
        if gcPercent < 0 {
                // If GC is disabled but we're running a forced GC,
                // act like GOGC is huge for the below calculations.
                gcPercent = 100000
        }
-       live := atomic.Load64(&c.heapLive)
-       scan := atomic.Load64(&c.heapScan)
-       work := atomic.Loadint64(&c.scanWork)
+       live := c.heapLive.Load()
+       scan := c.heapScan.Load()
+       work := c.heapScanWork.Load() + c.stackScanWork.Load() + c.globalsScanWork.Load()
 
        // Assume we're under the soft goal. Pace GC to complete at
        // heapGoal assuming the heap is in steady-state.
-       heapGoal := int64(atomic.Load64(&c.heapGoal))
-
-       // Compute the expected scan work remaining.
-       //
-       // This is estimated based on the expected
-       // steady-state scannable heap. For example, with
-       // GOGC=100, only half of the scannable heap is
-       // expected to be live, so that's what we target.
-       //
-       // (This is a float calculation to avoid overflowing on
-       // 100*heapScan.)
-       scanWorkExpected := int64(float64(scan) * 100 / float64(100+gcPercent))
-
-       if int64(live) > heapGoal || work > scanWorkExpected {
-               // We're past the soft goal, or we've already done more scan
-               // work than we expected. Pace GC so that in the worst case it
-               // will complete by the hard goal.
+       heapGoal := int64(c.heapGoal())
+
+       // The expected scan work is computed as the amount of bytes scanned last
+       // GC cycle (both heap and stack), plus our estimate of globals work for this cycle.
+       scanWorkExpected := int64(c.lastHeapScan + c.lastStackScan.Load() + c.globalsScan.Load())
+
+       // maxScanWork is a worst-case estimate of the amount of scan work that
+       // needs to be performed in this GC cycle. Specifically, it represents
+       // the case where *all* scannable memory turns out to be live, and
+       // *all* allocated stack space is scannable.
+       maxStackScan := c.maxStackScan.Load()
+       maxScanWork := int64(scan + maxStackScan + c.globalsScan.Load())
+       if work > scanWorkExpected {
+               // We've already done more scan work than expected. Because our expectation
+               // is based on a steady-state scannable heap size, we assume this means our
+               // heap is growing. Compute a new heap goal that takes our existing runway
+               // computed for scanWorkExpected and extrapolates it to maxScanWork, the worst-case
+               // scan work. This keeps our assist ratio stable if the heap continues to grow.
+               //
+               // The effect of this mechanism is that assists stay flat in the face of heap
+               // growths. It's OK to use more memory this cycle to scan all the live heap,
+               // because the next GC cycle is inevitably going to use *at least* that much
+               // memory anyway.
+               extHeapGoal := int64(float64(heapGoal-int64(c.triggered))/float64(scanWorkExpected)*float64(maxScanWork)) + int64(c.triggered)
+               scanWorkExpected = maxScanWork
+
+               // hardGoal is a hard limit on the amount that we're willing to push back the
+               // heap goal, and that's twice the heap goal (i.e. if GOGC=100 and the heap and/or
+               // stacks and/or globals grow to twice their size, this limits the current GC cycle's
+               // growth to 4x the original live heap's size).
+               //
+               // This maintains the invariant that we use no more memory than the next GC cycle
+               // will anyway.
+               hardGoal := int64((1.0 + float64(gcPercent)/100.0) * float64(heapGoal))
+               if extHeapGoal > hardGoal {
+                       extHeapGoal = hardGoal
+               }
+               heapGoal = extHeapGoal
+       }
+       if int64(live) > heapGoal {
+               // We're already past our heap goal, even the extrapolated one.
+               // Leave ourselves some extra runway, so in the worst case we
+               // finish by that point.
                const maxOvershoot = 1.1
                heapGoal = int64(float64(heapGoal) * maxOvershoot)
 
                // Compute the upper bound on the scan work remaining.
-               scanWorkExpected = int64(scan)
+               scanWorkExpected = maxScanWork
        }
 
        // Compute the remaining scan work estimate.
@@ -438,81 +592,95 @@ func (c *gcControllerState) revise() {
        c.assistBytesPerWork.Store(assistBytesPerWork)
 }
 
-// endCycle computes the trigger ratio for the next cycle.
+// endCycle computes the consMark estimate for the next cycle.
 // userForced indicates whether the current GC cycle was forced
 // by the application.
-func (c *gcControllerState) endCycle(userForced bool) float64 {
+func (c *gcControllerState) endCycle(now int64, procs int, userForced bool) {
        // Record last heap goal for the scavenger.
        // We'll be updating the heap goal soon.
-       gcController.lastHeapGoal = gcController.heapGoal
-
-       if userForced {
-               // Forced GC means this cycle didn't start at the
-               // trigger, so where it finished isn't good
-               // information about how to adjust the trigger.
-               // Just leave it where it is.
-               return c.triggerRatio
-       }
-
-       // Proportional response gain for the trigger controller. Must
-       // be in [0, 1]. Lower values smooth out transient effects but
-       // take longer to respond to phase changes. Higher values
-       // react to phase changes quickly, but are more affected by
-       // transient changes. Values near 1 may be unstable.
-       const triggerGain = 0.5
-
-       // Compute next cycle trigger ratio. First, this computes the
-       // "error" for this cycle; that is, how far off the trigger
-       // was from what it should have been, accounting for both heap
-       // growth and GC CPU utilization. We compute the actual heap
-       // growth during this cycle and scale that by how far off from
-       // the goal CPU utilization we were (to estimate the heap
-       // growth if we had the desired CPU utilization). The
-       // difference between this estimate and the GOGC-based goal
-       // heap growth is the error.
-       goalGrowthRatio := c.effectiveGrowthRatio()
-       actualGrowthRatio := float64(c.heapLive)/float64(c.heapMarked) - 1
-       assistDuration := nanotime() - c.markStartTime
+       gcController.lastHeapGoal = c.heapGoal()
+
+       // Compute the duration of time for which assists were turned on.
+       assistDuration := now - c.markStartTime
 
        // Assume background mark hit its utilization goal.
        utilization := gcBackgroundUtilization
        // Add assist utilization; avoid divide by zero.
        if assistDuration > 0 {
-               utilization += float64(c.assistTime) / float64(assistDuration*int64(gomaxprocs))
+               utilization += float64(c.assistTime.Load()) / float64(assistDuration*int64(procs))
        }
 
-       triggerError := goalGrowthRatio - c.triggerRatio - utilization/gcGoalUtilization*(actualGrowthRatio-c.triggerRatio)
-
-       // Finally, we adjust the trigger for next time by this error,
-       // damped by the proportional gain.
-       triggerRatio := c.triggerRatio + triggerGain*triggerError
+       if c.heapLive.Load() <= c.triggered {
+               // Shouldn't happen, but let's be very safe about this in case the
+               // GC is somehow extremely short.
+               //
+               // In this case though, the only reasonable value for c.heapLive-c.triggered
+               // would be 0, which isn't really all that useful, i.e. the GC was so short
+               // that it didn't matter.
+               //
+               // Ignore this case and don't update anything.
+               return
+       }
+       idleUtilization := 0.0
+       if assistDuration > 0 {
+               idleUtilization = float64(c.idleMarkTime.Load()) / float64(assistDuration*int64(procs))
+       }
+       // Determine the cons/mark ratio.
+       //
+       // The units we want for the numerator and denominator are both B / cpu-ns.
+       // We get this by taking the bytes allocated or scanned, and divide by the amount of
+       // CPU time it took for those operations. For allocations, that CPU time is
+       //
+       //    assistDuration * procs * (1 - utilization)
+       //
+       // Where utilization includes just background GC workers and assists. It does *not*
+       // include idle GC work time, because in theory the mutator is free to take that at
+       // any point.
+       //
+       // For scanning, that CPU time is
+       //
+       //    assistDuration * procs * (utilization + idleUtilization)
+       //
+       // In this case, we *include* idle utilization, because that is additional CPU time that
+       // the GC had available to it.
+       //
+       // In effect, idle GC time is sort of double-counted here, but it's very weird compared
+       // to other kinds of GC work, because of how fluid it is. Namely, because the mutator is
+       // *always* free to take it.
+       //
+       // So this calculation is really:
+       //     (heapLive-trigger) / (assistDuration * procs * (1-utilization)) /
+       //         (scanWork) / (assistDuration * procs * (utilization+idleUtilization))
+       //
+       // Note that because we only care about the ratio, assistDuration and procs cancel out.
+       scanWork := c.heapScanWork.Load() + c.stackScanWork.Load() + c.globalsScanWork.Load()
+       currentConsMark := (float64(c.heapLive.Load()-c.triggered) * (utilization + idleUtilization)) /
+               (float64(scanWork) * (1 - utilization))
+
+       // Update our cons/mark estimate. This is the maximum of the value we just computed and the last
+       // 4 cons/mark values we measured. The reason we take the maximum here is to bias a noisy
+       // cons/mark measurement toward fewer assists at the expense of additional GC cycles (starting
+       // earlier).
+       oldConsMark := c.consMark
+       c.consMark = currentConsMark
+       for i := range c.lastConsMark {
+               if c.lastConsMark[i] > c.consMark {
+                       c.consMark = c.lastConsMark[i]
+               }
+       }
+       copy(c.lastConsMark[:], c.lastConsMark[1:])
+       c.lastConsMark[len(c.lastConsMark)-1] = currentConsMark
 
        if debug.gcpacertrace > 0 {
-               // Print controller state in terms of the design
-               // document.
-               H_m_prev := c.heapMarked
-               h_t := c.triggerRatio
-               H_T := c.trigger
-               h_a := actualGrowthRatio
-               H_a := c.heapLive
-               h_g := goalGrowthRatio
-               H_g := int64(float64(H_m_prev) * (1 + h_g))
-               u_a := utilization
-               u_g := gcGoalUtilization
-               W_a := c.scanWork
-               print("pacer: H_m_prev=", H_m_prev,
-                       " h_t=", h_t, " H_T=", H_T,
-                       " h_a=", h_a, " H_a=", H_a,
-                       " h_g=", h_g, " H_g=", H_g,
-                       " u_a=", u_a, " u_g=", u_g,
-                       " W_a=", W_a,
-                       " goalΔ=", goalGrowthRatio-h_t,
-                       " actualΔ=", h_a-h_t,
-                       " u_a/u_g=", u_a/u_g,
-                       "\n")
-       }
-
-       return triggerRatio
+               printlock()
+               goal := gcGoalUtilization * 100
+               print("pacer: ", int(utilization*100), "% CPU (", int(goal), " exp.) for ")
+               print(c.heapScanWork.Load(), "+", c.stackScanWork.Load(), "+", c.globalsScanWork.Load(), " B work (", c.lastHeapScan+c.lastStackScan.Load()+c.globalsScan.Load(), " B exp.) ")
+               live := c.heapLive.Load()
+               print("in ", c.triggered, " B -> ", live, " B (∆goal ", int64(live)-int64(c.lastHeapGoal), ", cons/mark ", oldConsMark, ")")
+               println()
+               printunlock()
+       }
 }
 
 // enlistWorker encourages another dedicated mark worker to start on
@@ -524,14 +692,14 @@ func (c *gcControllerState) enlistWorker() {
        // If there are idle Ps, wake one so it will run an idle worker.
        // NOTE: This is suspected of causing deadlocks. See golang.org/issue/19112.
        //
-       //      if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 {
+       //      if sched.npidle.Load() != 0 && sched.nmspinning.Load() == 0 {
        //              wakep()
        //              return
        //      }
 
        // There are no idle Ps. If we need more dedicated workers,
        // try to preempt a running P so it will switch to a worker.
-       if c.dedicatedMarkWorkersNeeded <= 0 {
+       if c.dedicatedMarkWorkersNeeded.Load() <= 0 {
                return
        }
        // Pick a random other P to preempt.
@@ -558,19 +726,30 @@ func (c *gcControllerState) enlistWorker() {
        }
 }
 
-// findRunnableGCWorker returns a background mark worker for _p_ if it
+// findRunnableGCWorker returns a background mark worker for pp if it
 // should be run. This must only be called when gcBlackenEnabled != 0.
-func (c *gcControllerState) findRunnableGCWorker(_p_ *p) *g {
+func (c *gcControllerState) findRunnableGCWorker(pp *p, now int64) (*g, int64) {
        if gcBlackenEnabled == 0 {
                throw("gcControllerState.findRunnable: blackening not enabled")
        }
 
-       if !gcMarkWorkAvailable(_p_) {
+       // Since we have the current time, check if the GC CPU limiter
+       // hasn't had an update in a while. This check is necessary in
+       // case the limiter is on but hasn't been checked in a while and
+       // so may have left sufficient headroom to turn off again.
+       if now == 0 {
+               now = nanotime()
+       }
+       if gcCPULimiter.needUpdate(now) {
+               gcCPULimiter.update(now)
+       }
+
+       if !gcMarkWorkAvailable(pp) {
                // No work to be done right now. This can happen at
                // the end of the mark phase when there are still
                // assists tapering off. Don't bother running a worker
                // now because it'll just return immediately.
-               return nil
+               return nil, now
        }
 
        // Grab a worker before we commit to running below.
@@ -587,17 +766,17 @@ func (c *gcControllerState) findRunnableGCWorker(_p_ *p) *g {
                // it will always do so with queued global work. Thus, that P
                // will be immediately eligible to re-run the worker G it was
                // just using, ensuring work can complete.
-               return nil
+               return nil, now
        }
 
-       decIfPositive := func(ptr *int64) bool {
+       decIfPositive := func(val *atomic.Int64) bool {
                for {
-                       v := atomic.Loadint64(ptr)
+                       v := val.Load()
                        if v <= 0 {
                                return false
                        }
 
-                       if atomic.Casint64(ptr, v, v-1) {
+                       if val.CompareAndSwap(v, v-1) {
                                return true
                        }
                }
@@ -606,33 +785,35 @@ func (c *gcControllerState) findRunnableGCWorker(_p_ *p) *g {
        if decIfPositive(&c.dedicatedMarkWorkersNeeded) {
                // This P is now dedicated to marking until the end of
                // the concurrent mark phase.
-               _p_.gcMarkWorkerMode = gcMarkWorkerDedicatedMode
+               pp.gcMarkWorkerMode = gcMarkWorkerDedicatedMode
        } else if c.fractionalUtilizationGoal == 0 {
                // No need for fractional workers.
                gcBgMarkWorkerPool.push(&node.node)
-               return nil
+               return nil, now
        } else {
                // Is this P behind on the fractional utilization
                // goal?
                //
                // This should be kept in sync with pollFractionalWorkerExit.
-               delta := nanotime() - c.markStartTime
-               if delta > 0 && float64(_p_.gcFractionalMarkTime)/float64(delta) > c.fractionalUtilizationGoal {
+               delta := now - c.markStartTime
+               if delta > 0 && float64(pp.gcFractionalMarkTime)/float64(delta) > c.fractionalUtilizationGoal {
                        // Nope. No need to run a fractional worker.
                        gcBgMarkWorkerPool.push(&node.node)
-                       return nil
+                       return nil, now
                }
                // Run a fractional worker.
-               _p_.gcMarkWorkerMode = gcMarkWorkerFractionalMode
+               pp.gcMarkWorkerMode = gcMarkWorkerFractionalMode
        }
 
        // Run the background mark worker.
        gp := node.gp.ptr()
+       trace := traceAcquire()
        casgstatus(gp, _Gwaiting, _Grunnable)
-       if trace.enabled {
-               traceGoUnpark(gp, 0)
+       if trace.ok() {
+               trace.GoUnpark(gp, 0)
+               traceRelease(trace)
        }
-       return gp
+       return gp, now
 }
 
 // resetLive sets up the controller state for the next mark phase after the end
@@ -642,178 +823,434 @@ func (c *gcControllerState) findRunnableGCWorker(_p_ *p) *g {
 // The world must be stopped.
 func (c *gcControllerState) resetLive(bytesMarked uint64) {
        c.heapMarked = bytesMarked
-       c.heapLive = bytesMarked
-       c.heapScan = uint64(c.scanWork)
+       c.heapLive.Store(bytesMarked)
+       c.heapScan.Store(uint64(c.heapScanWork.Load()))
+       c.lastHeapScan = uint64(c.heapScanWork.Load())
+       c.lastStackScan.Store(uint64(c.stackScanWork.Load()))
+       c.triggered = ^uint64(0) // Reset triggered.
 
        // heapLive was updated, so emit a trace event.
-       if trace.enabled {
-               traceHeapAlloc()
+       trace := traceAcquire()
+       if trace.ok() {
+               trace.HeapAlloc(bytesMarked)
+               traceRelease(trace)
        }
 }
 
-// logWorkTime updates mark work accounting in the controller by a duration of
-// work in nanoseconds.
+// markWorkerStop must be called whenever a mark worker stops executing.
+//
+// It updates mark work accounting in the controller by a duration of
+// work in nanoseconds and other bookkeeping.
 //
 // Safe to execute at any time.
-func (c *gcControllerState) logWorkTime(mode gcMarkWorkerMode, duration int64) {
+func (c *gcControllerState) markWorkerStop(mode gcMarkWorkerMode, duration int64) {
        switch mode {
        case gcMarkWorkerDedicatedMode:
-               atomic.Xaddint64(&c.dedicatedMarkTime, duration)
-               atomic.Xaddint64(&c.dedicatedMarkWorkersNeeded, 1)
+               c.dedicatedMarkTime.Add(duration)
+               c.dedicatedMarkWorkersNeeded.Add(1)
        case gcMarkWorkerFractionalMode:
-               atomic.Xaddint64(&c.fractionalMarkTime, duration)
+               c.fractionalMarkTime.Add(duration)
        case gcMarkWorkerIdleMode:
-               atomic.Xaddint64(&c.idleMarkTime, duration)
+               c.idleMarkTime.Add(duration)
+               c.removeIdleMarkWorker()
        default:
-               throw("logWorkTime: unknown mark worker mode")
+               throw("markWorkerStop: unknown mark worker mode")
        }
 }
 
-// commit sets the trigger ratio and updates everything
-// derived from it: the absolute trigger, the heap goal, mark pacing,
-// and sweep pacing.
-//
-// This can be called any time. If GC is the in the middle of a
-// concurrent phase, it will adjust the pacing of that phase.
-//
-// This depends on gcPercent, gcController.heapMarked, and
-// gcController.heapLive. These must be up to date.
-//
-// mheap_.lock must be held or the world must be stopped.
-func (c *gcControllerState) commit(triggerRatio float64) {
-       assertWorldStoppedOrLockHeld(&mheap_.lock)
-
-       // Compute the next GC goal, which is when the allocated heap
-       // has grown by GOGC/100 over the heap marked by the last
-       // cycle.
-       goal := ^uint64(0)
-       if c.gcPercent >= 0 {
-               goal = c.heapMarked + c.heapMarked*uint64(c.gcPercent)/100
-       }
-
-       // Set the trigger ratio, capped to reasonable bounds.
-       if c.gcPercent >= 0 {
-               scalingFactor := float64(c.gcPercent) / 100
-               // Ensure there's always a little margin so that the
-               // mutator assist ratio isn't infinity.
-               maxTriggerRatio := 0.95 * scalingFactor
-               if triggerRatio > maxTriggerRatio {
-                       triggerRatio = maxTriggerRatio
-               }
-
-               // If we let triggerRatio go too low, then if the application
-               // is allocating very rapidly we might end up in a situation
-               // where we're allocating black during a nearly always-on GC.
-               // The result of this is a growing heap and ultimately an
-               // increase in RSS. By capping us at a point >0, we're essentially
-               // saying that we're OK using more CPU during the GC to prevent
-               // this growth in RSS.
-               //
-               // The current constant was chosen empirically: given a sufficiently
-               // fast/scalable allocator with 48 Ps that could drive the trigger ratio
-               // to <0.05, this constant causes applications to retain the same peak
-               // RSS compared to not having this allocator.
-               minTriggerRatio := 0.6 * scalingFactor
-               if triggerRatio < minTriggerRatio {
-                       triggerRatio = minTriggerRatio
+func (c *gcControllerState) update(dHeapLive, dHeapScan int64) {
+       if dHeapLive != 0 {
+               trace := traceAcquire()
+               live := gcController.heapLive.Add(dHeapLive)
+               if trace.ok() {
+                       // gcController.heapLive changed.
+                       trace.HeapAlloc(live)
+                       traceRelease(trace)
                }
-       } else if triggerRatio < 0 {
-               // gcPercent < 0, so just make sure we're not getting a negative
-               // triggerRatio. This case isn't expected to happen in practice,
-               // and doesn't really matter because if gcPercent < 0 then we won't
-               // ever consume triggerRatio further on in this function, but let's
-               // just be defensive here; the triggerRatio being negative is almost
-               // certainly undesirable.
-               triggerRatio = 0
-       }
-       c.triggerRatio = triggerRatio
-
-       // Compute the absolute GC trigger from the trigger ratio.
-       //
-       // We trigger the next GC cycle when the allocated heap has
-       // grown by the trigger ratio over the marked heap size.
-       trigger := ^uint64(0)
-       if c.gcPercent >= 0 {
-               trigger = uint64(float64(c.heapMarked) * (1 + triggerRatio))
-               // Don't trigger below the minimum heap size.
-               minTrigger := c.heapMinimum
-               if !isSweepDone() {
-                       // Concurrent sweep happens in the heap growth
-                       // from gcController.heapLive to trigger, so ensure
-                       // that concurrent sweep has some heap growth
-                       // in which to perform sweeping before we
-                       // start the next GC cycle.
-                       sweepMin := atomic.Load64(&c.heapLive) + sweepMinHeapDistance
-                       if sweepMin > minTrigger {
-                               minTrigger = sweepMin
-                       }
+       }
+       if gcBlackenEnabled == 0 {
+               // Update heapScan when we're not in a current GC. It is fixed
+               // at the beginning of a cycle.
+               if dHeapScan != 0 {
+                       gcController.heapScan.Add(dHeapScan)
                }
-               if trigger < minTrigger {
-                       trigger = minTrigger
+       } else {
+               // gcController.heapLive changed.
+               c.revise()
+       }
+}
+
+func (c *gcControllerState) addScannableStack(pp *p, amount int64) {
+       if pp == nil {
+               c.maxStackScan.Add(amount)
+               return
+       }
+       pp.maxStackScanDelta += amount
+       if pp.maxStackScanDelta >= maxStackScanSlack || pp.maxStackScanDelta <= -maxStackScanSlack {
+               c.maxStackScan.Add(pp.maxStackScanDelta)
+               pp.maxStackScanDelta = 0
+       }
+}
+
+func (c *gcControllerState) addGlobals(amount int64) {
+       c.globalsScan.Add(amount)
+}
+
+// heapGoal returns the current heap goal.
+func (c *gcControllerState) heapGoal() uint64 {
+       goal, _ := c.heapGoalInternal()
+       return goal
+}
+
+// heapGoalInternal is the implementation of heapGoal which returns additional
+// information that is necessary for computing the trigger.
+//
+// The returned minTrigger is always <= goal.
+func (c *gcControllerState) heapGoalInternal() (goal, minTrigger uint64) {
+       // Start with the goal calculated for gcPercent.
+       goal = c.gcPercentHeapGoal.Load()
+
+       // Check if the memory-limit-based goal is smaller, and if so, pick that.
+       if newGoal := c.memoryLimitHeapGoal(); newGoal < goal {
+               goal = newGoal
+       } else {
+               // We're not limited by the memory limit goal, so perform a series of
+               // adjustments that might move the goal forward in a variety of circumstances.
+
+               sweepDistTrigger := c.sweepDistMinTrigger.Load()
+               if sweepDistTrigger > goal {
+                       // Set the goal to maintain a minimum sweep distance since
+                       // the last call to commit. Note that we never want to do this
+                       // if we're in the memory limit regime, because it could push
+                       // the goal up.
+                       goal = sweepDistTrigger
                }
-               if int64(trigger) < 0 {
-                       print("runtime: heapGoal=", c.heapGoal, " heapMarked=", c.heapMarked, " gcController.heapLive=", c.heapLive, " initialHeapLive=", work.initialHeapLive, "triggerRatio=", triggerRatio, " minTrigger=", minTrigger, "\n")
-                       throw("trigger underflow")
+               // Since we ignore the sweep distance trigger in the memory
+               // limit regime, we need to ensure we don't propagate it to
+               // the trigger, because it could cause a violation of the
+               // invariant that the trigger < goal.
+               minTrigger = sweepDistTrigger
+
+               // Ensure that the heap goal is at least a little larger than
+               // the point at which we triggered. This may not be the case if GC
+               // start is delayed or if the allocation that pushed gcController.heapLive
+               // over trigger is large or if the trigger is really close to
+               // GOGC. Assist is proportional to this distance, so enforce a
+               // minimum distance, even if it means going over the GOGC goal
+               // by a tiny bit.
+               //
+               // Ignore this if we're in the memory limit regime: we'd prefer to
+               // have the GC respond hard about how close we are to the goal than to
+               // push the goal back in such a manner that it could cause us to exceed
+               // the memory limit.
+               const minRunway = 64 << 10
+               if c.triggered != ^uint64(0) && goal < c.triggered+minRunway {
+                       goal = c.triggered + minRunway
                }
-               if trigger > goal {
-                       // The trigger ratio is always less than GOGC/100, but
-                       // other bounds on the trigger may have raised it.
-                       // Push up the goal, too.
-                       goal = trigger
+       }
+       return
+}
+
+// memoryLimitHeapGoal returns a heap goal derived from memoryLimit.
+func (c *gcControllerState) memoryLimitHeapGoal() uint64 {
+       // Start by pulling out some values we'll need. Be careful about overflow.
+       var heapFree, heapAlloc, mappedReady uint64
+       for {
+               heapFree = c.heapFree.load()                         // Free and unscavenged memory.
+               heapAlloc = c.totalAlloc.Load() - c.totalFree.Load() // Heap object bytes in use.
+               mappedReady = c.mappedReady.Load()                   // Total unreleased mapped memory.
+               if heapFree+heapAlloc <= mappedReady {
+                       break
                }
+               // It is impossible for total unreleased mapped memory to exceed heap memory, but
+               // because these stats are updated independently, we may observe a partial update
+               // including only some values. Thus, we appear to break the invariant. However,
+               // this condition is necessarily transient, so just try again. In the case of a
+               // persistent accounting error, we'll deadlock here.
        }
 
-       // Commit to the trigger and goal.
-       c.trigger = trigger
-       atomic.Store64(&c.heapGoal, goal)
-       if trace.enabled {
-               traceHeapGoal()
+       // Below we compute a goal from memoryLimit. There are a few things to be aware of.
+       // Firstly, the memoryLimit does not easily compare to the heap goal: the former
+       // is total mapped memory by the runtime that hasn't been released, while the latter is
+       // only heap object memory. Intuitively, the way we convert from one to the other is to
+       // subtract everything from memoryLimit that both contributes to the memory limit (so,
+       // ignore scavenged memory) and doesn't contain heap objects. This isn't quite what
+       // lines up with reality, but it's a good starting point.
+       //
+       // In practice this computation looks like the following:
+       //
+       //    goal := memoryLimit - ((mappedReady - heapFree - heapAlloc) + max(mappedReady - memoryLimit, 0))
+       //                    ^1                                    ^2
+       //    goal -= goal / 100 * memoryLimitHeapGoalHeadroomPercent
+       //    ^3
+       //
+       // Let's break this down.
+       //
+       // The first term (marker 1) is everything that contributes to the memory limit and isn't
+       // or couldn't become heap objects. It represents, broadly speaking, non-heap overheads.
+       // One oddity you may have noticed is that we also subtract out heapFree, i.e. unscavenged
+       // memory that may contain heap objects in the future.
+       //
+       // Let's take a step back. In an ideal world, this term would look something like just
+       // the heap goal. That is, we "reserve" enough space for the heap to grow to the heap
+       // goal, and subtract out everything else. This is of course impossible; the definition
+       // is circular! However, this impossible definition contains a key insight: the amount
+       // we're *going* to use matters just as much as whatever we're currently using.
+       //
+       // Consider if the heap shrinks to 1/10th its size, leaving behind lots of free and
+       // unscavenged memory. mappedReady - heapAlloc will be quite large, because of that free
+       // and unscavenged memory, pushing the goal down significantly.
+       //
+       // heapFree is also safe to exclude from the memory limit because in the steady-state, it's
+       // just a pool of memory for future heap allocations, and making new allocations from heapFree
+       // memory doesn't increase overall memory use. In transient states, the scavenger and the
+       // allocator actively manage the pool of heapFree memory to maintain the memory limit.
+       //
+       // The second term (marker 2) is the amount of memory we've exceeded the limit by, and is
+       // intended to help recover from such a situation. By pushing the heap goal down, we also
+       // push the trigger down, triggering and finishing a GC sooner in order to make room for
+       // other memory sources. Note that since we're effectively reducing the heap goal by X bytes,
+       // we're actually giving more than X bytes of headroom back, because the heap goal is in
+       // terms of heap objects, but it takes more than X bytes (e.g. due to fragmentation) to store
+       // X bytes worth of objects.
+       //
+       // The final adjustment (marker 3) reduces the maximum possible memory limit heap goal by
+       // memoryLimitHeapGoalPercent. As the name implies, this is to provide additional headroom in
+       // the face of pacing inaccuracies, and also to leave a buffer of unscavenged memory so the
+       // allocator isn't constantly scavenging. The reduction amount also has a fixed minimum
+       // (memoryLimitMinHeapGoalHeadroom, not pictured) because the aforementioned pacing inaccuracies
+       // disproportionately affect small heaps: as heaps get smaller, the pacer's inputs get fuzzier.
+       // Shorter GC cycles and less GC work means noisy external factors like the OS scheduler have a
+       // greater impact.
+
+       memoryLimit := uint64(c.memoryLimit.Load())
+
+       // Compute term 1.
+       nonHeapMemory := mappedReady - heapFree - heapAlloc
+
+       // Compute term 2.
+       var overage uint64
+       if mappedReady > memoryLimit {
+               overage = mappedReady - memoryLimit
        }
 
-       // Update mark pacing.
-       if gcphase != _GCoff {
-               c.revise()
+       if nonHeapMemory+overage >= memoryLimit {
+               // We're at a point where non-heap memory exceeds the memory limit on its own.
+               // There's honestly not much we can do here but just trigger GCs continuously
+               // and let the CPU limiter reign that in. Something has to give at this point.
+               // Set it to heapMarked, the lowest possible goal.
+               return c.heapMarked
+       }
+
+       // Compute the goal.
+       goal := memoryLimit - (nonHeapMemory + overage)
+
+       // Apply some headroom to the goal to account for pacing inaccuracies and to reduce
+       // the impact of scavenging at allocation time in response to a high allocation rate
+       // when GOGC=off. See issue #57069. Also, be careful about small limits.
+       headroom := goal / 100 * memoryLimitHeapGoalHeadroomPercent
+       if headroom < memoryLimitMinHeapGoalHeadroom {
+               // Set a fixed minimum to deal with the particularly large effect pacing inaccuracies
+               // have for smaller heaps.
+               headroom = memoryLimitMinHeapGoalHeadroom
+       }
+       if goal < headroom || goal-headroom < headroom {
+               goal = headroom
+       } else {
+               goal = goal - headroom
+       }
+       // Don't let us go below the live heap. A heap goal below the live heap doesn't make sense.
+       if goal < c.heapMarked {
+               goal = c.heapMarked
+       }
+       return goal
+}
+
+const (
+       // These constants determine the bounds on the GC trigger as a fraction
+       // of heap bytes allocated between the start of a GC (heapLive == heapMarked)
+       // and the end of a GC (heapLive == heapGoal).
+       //
+       // The constants are obscured in this way for efficiency. The denominator
+       // of the fraction is always a power-of-two for a quick division, so that
+       // the numerator is a single constant integer multiplication.
+       triggerRatioDen = 64
+
+       // The minimum trigger constant was chosen empirically: given a sufficiently
+       // fast/scalable allocator with 48 Ps that could drive the trigger ratio
+       // to <0.05, this constant causes applications to retain the same peak
+       // RSS compared to not having this allocator.
+       minTriggerRatioNum = 45 // ~0.7
+
+       // The maximum trigger constant is chosen somewhat arbitrarily, but the
+       // current constant has served us well over the years.
+       maxTriggerRatioNum = 61 // ~0.95
+)
+
+// trigger returns the current point at which a GC should trigger along with
+// the heap goal.
+//
+// The returned value may be compared against heapLive to determine whether
+// the GC should trigger. Thus, the GC trigger condition should be (but may
+// not be, in the case of small movements for efficiency) checked whenever
+// the heap goal may change.
+func (c *gcControllerState) trigger() (uint64, uint64) {
+       goal, minTrigger := c.heapGoalInternal()
+
+       // Invariant: the trigger must always be less than the heap goal.
+       //
+       // Note that the memory limit sets a hard maximum on our heap goal,
+       // but the live heap may grow beyond it.
+
+       if c.heapMarked >= goal {
+               // The goal should never be smaller than heapMarked, but let's be
+               // defensive about it. The only reasonable trigger here is one that
+               // causes a continuous GC cycle at heapMarked, but respect the goal
+               // if it came out as smaller than that.
+               return goal, goal
+       }
+
+       // Below this point, c.heapMarked < goal.
+
+       // heapMarked is our absolute minimum, and it's possible the trigger
+       // bound we get from heapGoalinternal is less than that.
+       if minTrigger < c.heapMarked {
+               minTrigger = c.heapMarked
+       }
+
+       // If we let the trigger go too low, then if the application
+       // is allocating very rapidly we might end up in a situation
+       // where we're allocating black during a nearly always-on GC.
+       // The result of this is a growing heap and ultimately an
+       // increase in RSS. By capping us at a point >0, we're essentially
+       // saying that we're OK using more CPU during the GC to prevent
+       // this growth in RSS.
+       triggerLowerBound := ((goal-c.heapMarked)/triggerRatioDen)*minTriggerRatioNum + c.heapMarked
+       if minTrigger < triggerLowerBound {
+               minTrigger = triggerLowerBound
+       }
+
+       // For small heaps, set the max trigger point at maxTriggerRatio of the way
+       // from the live heap to the heap goal. This ensures we always have *some*
+       // headroom when the GC actually starts. For larger heaps, set the max trigger
+       // point at the goal, minus the minimum heap size.
+       //
+       // This choice follows from the fact that the minimum heap size is chosen
+       // to reflect the costs of a GC with no work to do. With a large heap but
+       // very little scan work to perform, this gives us exactly as much runway
+       // as we would need, in the worst case.
+       maxTrigger := ((goal-c.heapMarked)/triggerRatioDen)*maxTriggerRatioNum + c.heapMarked
+       if goal > defaultHeapMinimum && goal-defaultHeapMinimum > maxTrigger {
+               maxTrigger = goal - defaultHeapMinimum
+       }
+       maxTrigger = max(maxTrigger, minTrigger)
+
+       // Compute the trigger from our bounds and the runway stored by commit.
+       var trigger uint64
+       runway := c.runway.Load()
+       if runway > goal {
+               trigger = minTrigger
+       } else {
+               trigger = goal - runway
        }
+       trigger = max(trigger, minTrigger)
+       trigger = min(trigger, maxTrigger)
+       if trigger > goal {
+               print("trigger=", trigger, " heapGoal=", goal, "\n")
+               print("minTrigger=", minTrigger, " maxTrigger=", maxTrigger, "\n")
+               throw("produced a trigger greater than the heap goal")
+       }
+       return trigger, goal
 }
 
-// effectiveGrowthRatio returns the current effective heap growth
-// ratio (GOGC/100) based on heapMarked from the previous GC and
-// heapGoal for the current GC.
+// commit recomputes all pacing parameters needed to derive the
+// trigger and the heap goal. Namely, the gcPercent-based heap goal,
+// and the amount of runway we want to give the GC this cycle.
+//
+// This can be called any time. If GC is the in the middle of a
+// concurrent phase, it will adjust the pacing of that phase.
 //
-// This may differ from gcPercent/100 because of various upper and
-// lower bounds on gcPercent. For example, if the heap is smaller than
-// heapMinimum, this can be higher than gcPercent/100.
+// isSweepDone should be the result of calling isSweepDone(),
+// unless we're testing or we know we're executing during a GC cycle.
+//
+// This depends on gcPercent, gcController.heapMarked, and
+// gcController.heapLive. These must be up to date.
+//
+// Callers must call gcControllerState.revise after calling this
+// function if the GC is enabled.
 //
 // mheap_.lock must be held or the world must be stopped.
-func (c *gcControllerState) effectiveGrowthRatio() float64 {
-       assertWorldStoppedOrLockHeld(&mheap_.lock)
+func (c *gcControllerState) commit(isSweepDone bool) {
+       if !c.test {
+               assertWorldStoppedOrLockHeld(&mheap_.lock)
+       }
+
+       if isSweepDone {
+               // The sweep is done, so there aren't any restrictions on the trigger
+               // we need to think about.
+               c.sweepDistMinTrigger.Store(0)
+       } else {
+               // Concurrent sweep happens in the heap growth
+               // from gcController.heapLive to trigger. Make sure we
+               // give the sweeper some runway if it doesn't have enough.
+               c.sweepDistMinTrigger.Store(c.heapLive.Load() + sweepMinHeapDistance)
+       }
 
-       egogc := float64(atomic.Load64(&c.heapGoal)-c.heapMarked) / float64(c.heapMarked)
-       if egogc < 0 {
-               // Shouldn't happen, but just in case.
-               egogc = 0
+       // Compute the next GC goal, which is when the allocated heap
+       // has grown by GOGC/100 over where it started the last cycle,
+       // plus additional runway for non-heap sources of GC work.
+       gcPercentHeapGoal := ^uint64(0)
+       if gcPercent := c.gcPercent.Load(); gcPercent >= 0 {
+               gcPercentHeapGoal = c.heapMarked + (c.heapMarked+c.lastStackScan.Load()+c.globalsScan.Load())*uint64(gcPercent)/100
+       }
+       // Apply the minimum heap size here. It's defined in terms of gcPercent
+       // and is only updated by functions that call commit.
+       if gcPercentHeapGoal < c.heapMinimum {
+               gcPercentHeapGoal = c.heapMinimum
        }
-       return egogc
+       c.gcPercentHeapGoal.Store(gcPercentHeapGoal)
+
+       // Compute the amount of runway we want the GC to have by using our
+       // estimate of the cons/mark ratio.
+       //
+       // The idea is to take our expected scan work, and multiply it by
+       // the cons/mark ratio to determine how long it'll take to complete
+       // that scan work in terms of bytes allocated. This gives us our GC's
+       // runway.
+       //
+       // However, the cons/mark ratio is a ratio of rates per CPU-second, but
+       // here we care about the relative rates for some division of CPU
+       // resources among the mutator and the GC.
+       //
+       // To summarize, we have B / cpu-ns, and we want B / ns. We get that
+       // by multiplying by our desired division of CPU resources. We choose
+       // to express CPU resources as GOMAPROCS*fraction. Note that because
+       // we're working with a ratio here, we can omit the number of CPU cores,
+       // because they'll appear in the numerator and denominator and cancel out.
+       // As a result, this is basically just "weighing" the cons/mark ratio by
+       // our desired division of resources.
+       //
+       // Furthermore, by setting the runway so that CPU resources are divided
+       // this way, assuming that the cons/mark ratio is correct, we make that
+       // division a reality.
+       c.runway.Store(uint64((c.consMark * (1 - gcGoalUtilization) / (gcGoalUtilization)) * float64(c.lastHeapScan+c.lastStackScan.Load()+c.globalsScan.Load())))
 }
 
-// setGCPercent updates gcPercent and all related pacer state.
+// setGCPercent updates gcPercent. commit must be called after.
 // Returns the old value of gcPercent.
 //
-// Calls gcControllerState.commit.
-//
 // The world must be stopped, or mheap_.lock must be held.
 func (c *gcControllerState) setGCPercent(in int32) int32 {
-       assertWorldStoppedOrLockHeld(&mheap_.lock)
+       if !c.test {
+               assertWorldStoppedOrLockHeld(&mheap_.lock)
+       }
 
-       out := c.gcPercent
+       out := c.gcPercent.Load()
        if in < 0 {
                in = -1
        }
-       // Write it atomically so readers like revise() can read it safely.
-       atomic.Storeint32(&c.gcPercent, in)
-       c.heapMinimum = defaultHeapMinimum * uint64(c.gcPercent) / 100
-       // Update pacing in response to gcPercent change.
-       c.commit(c.triggerRatio)
+       c.heapMinimum = defaultHeapMinimum * uint64(in) / 100
+       c.gcPercent.Store(in)
 
        return out
 }
@@ -824,15 +1261,14 @@ func setGCPercent(in int32) (out int32) {
        systemstack(func() {
                lock(&mheap_.lock)
                out = gcController.setGCPercent(in)
-               gcPaceSweeper(gcController.trigger)
-               gcPaceScavenger(gcController.heapGoal, gcController.lastHeapGoal)
+               gcControllerCommit()
                unlock(&mheap_.lock)
        })
 
        // If we just disabled GC, wait for any concurrent GC mark to
        // finish so we always return with no GC running.
        if in < 0 {
-               gcWaitOnMark(atomic.Load(&work.cycles))
+               gcWaitOnMark(work.cycles.Load())
        }
 
        return out
@@ -848,3 +1284,163 @@ func readGOGC() int32 {
        }
        return 100
 }
+
+// setMemoryLimit updates memoryLimit. commit must be called after
+// Returns the old value of memoryLimit.
+//
+// The world must be stopped, or mheap_.lock must be held.
+func (c *gcControllerState) setMemoryLimit(in int64) int64 {
+       if !c.test {
+               assertWorldStoppedOrLockHeld(&mheap_.lock)
+       }
+
+       out := c.memoryLimit.Load()
+       if in >= 0 {
+               c.memoryLimit.Store(in)
+       }
+
+       return out
+}
+
+//go:linkname setMemoryLimit runtime/debug.setMemoryLimit
+func setMemoryLimit(in int64) (out int64) {
+       // Run on the system stack since we grab the heap lock.
+       systemstack(func() {
+               lock(&mheap_.lock)
+               out = gcController.setMemoryLimit(in)
+               if in < 0 || out == in {
+                       // If we're just checking the value or not changing
+                       // it, there's no point in doing the rest.
+                       unlock(&mheap_.lock)
+                       return
+               }
+               gcControllerCommit()
+               unlock(&mheap_.lock)
+       })
+       return out
+}
+
+func readGOMEMLIMIT() int64 {
+       p := gogetenv("GOMEMLIMIT")
+       if p == "" || p == "off" {
+               return maxInt64
+       }
+       n, ok := parseByteCount(p)
+       if !ok {
+               print("GOMEMLIMIT=", p, "\n")
+               throw("malformed GOMEMLIMIT; see `go doc runtime/debug.SetMemoryLimit`")
+       }
+       return n
+}
+
+// addIdleMarkWorker attempts to add a new idle mark worker.
+//
+// If this returns true, the caller must become an idle mark worker unless
+// there's no background mark worker goroutines in the pool. This case is
+// harmless because there are already background mark workers running.
+// If this returns false, the caller must NOT become an idle mark worker.
+//
+// nosplit because it may be called without a P.
+//
+//go:nosplit
+func (c *gcControllerState) addIdleMarkWorker() bool {
+       for {
+               old := c.idleMarkWorkers.Load()
+               n, max := int32(old&uint64(^uint32(0))), int32(old>>32)
+               if n >= max {
+                       // See the comment on idleMarkWorkers for why
+                       // n > max is tolerated.
+                       return false
+               }
+               if n < 0 {
+                       print("n=", n, " max=", max, "\n")
+                       throw("negative idle mark workers")
+               }
+               new := uint64(uint32(n+1)) | (uint64(max) << 32)
+               if c.idleMarkWorkers.CompareAndSwap(old, new) {
+                       return true
+               }
+       }
+}
+
+// needIdleMarkWorker is a hint as to whether another idle mark worker is needed.
+//
+// The caller must still call addIdleMarkWorker to become one. This is mainly
+// useful for a quick check before an expensive operation.
+//
+// nosplit because it may be called without a P.
+//
+//go:nosplit
+func (c *gcControllerState) needIdleMarkWorker() bool {
+       p := c.idleMarkWorkers.Load()
+       n, max := int32(p&uint64(^uint32(0))), int32(p>>32)
+       return n < max
+}
+
+// removeIdleMarkWorker must be called when an new idle mark worker stops executing.
+func (c *gcControllerState) removeIdleMarkWorker() {
+       for {
+               old := c.idleMarkWorkers.Load()
+               n, max := int32(old&uint64(^uint32(0))), int32(old>>32)
+               if n-1 < 0 {
+                       print("n=", n, " max=", max, "\n")
+                       throw("negative idle mark workers")
+               }
+               new := uint64(uint32(n-1)) | (uint64(max) << 32)
+               if c.idleMarkWorkers.CompareAndSwap(old, new) {
+                       return
+               }
+       }
+}
+
+// setMaxIdleMarkWorkers sets the maximum number of idle mark workers allowed.
+//
+// This method is optimistic in that it does not wait for the number of
+// idle mark workers to reduce to max before returning; it assumes the workers
+// will deschedule themselves.
+func (c *gcControllerState) setMaxIdleMarkWorkers(max int32) {
+       for {
+               old := c.idleMarkWorkers.Load()
+               n := int32(old & uint64(^uint32(0)))
+               if n < 0 {
+                       print("n=", n, " max=", max, "\n")
+                       throw("negative idle mark workers")
+               }
+               new := uint64(uint32(n)) | (uint64(max) << 32)
+               if c.idleMarkWorkers.CompareAndSwap(old, new) {
+                       return
+               }
+       }
+}
+
+// gcControllerCommit is gcController.commit, but passes arguments from live
+// (non-test) data. It also updates any consumers of the GC pacing, such as
+// sweep pacing and the background scavenger.
+//
+// Calls gcController.commit.
+//
+// The heap lock must be held, so this must be executed on the system stack.
+//
+//go:systemstack
+func gcControllerCommit() {
+       assertWorldStoppedOrLockHeld(&mheap_.lock)
+
+       gcController.commit(isSweepDone())
+
+       // Update mark pacing.
+       if gcphase != _GCoff {
+               gcController.revise()
+       }
+
+       // TODO(mknyszek): This isn't really accurate any longer because the heap
+       // goal is computed dynamically. Still useful to snapshot, but not as useful.
+       trace := traceAcquire()
+       if trace.ok() {
+               trace.HeapGoal()
+               traceRelease(trace)
+       }
+
+       trigger, heapGoal := gcController.trigger()
+       gcPaceSweeper(trigger)
+       gcPaceScavenger(gcController.memoryLimit.Load(), heapGoal, gcController.lastHeapGoal)
+}