Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/unreleased/quiescent-behavior-scan.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Checking a constraint or writing a feature no longer rescans every object's behaviors when nothing has changed since the last scan found them all idle, so a batch of checks over many instantiated objects is linear again.
6 changes: 3 additions & 3 deletions internal/exec/runtime/advance.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,10 @@ func (ctx *Context) AdvanceUntil(duration float64, halted func() bool) (AdvanceR
if !ok || next > deadline {
break
}
ctx.clock.now = next
ctx.setClock(next)
progress.unsettle()
}
ctx.clock.now = deadline
ctx.setClock(deadline)
report.To = deadline
return report.counting(progress, ctx.run.notes[noted:]), ctx.advanceEnded()
}
Expand Down Expand Up @@ -155,7 +155,7 @@ func (ctx *Context) advanceToNextDue(progress *dueProgress) bool {
if !ok {
return false
}
ctx.clock.now = next
ctx.setClock(next)
progress.unsettle()
return true
}
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/runtime/check_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ func (r *invocationRun) advanceClock() bool {
if !ok || next <= r.ctx.clock.now {
return false
}
r.ctx.clock.now = next
r.ctx.setClock(next)
r.turn = nil
return true
}
Expand Down
87 changes: 80 additions & 7 deletions internal/exec/runtime/classifier_behavior.go
Original file line number Diff line number Diff line change
Expand Up @@ -662,30 +662,77 @@ func (ctx *Context) startBehaviorsOf(inst *Instance) error {
inst.behaviors = append(inst.behaviors, behavior)
ctx.pendingBehaviors = append(ctx.pendingBehaviors, behavior)
ctx.objectBehaviors = append(ctx.objectBehaviors, behavior)
ctx.workChanged()
}
}

return ctx.runAttachedBehaviors()
}

// workChanged counts a change that can leave an attached behavior holding work:
// a message posted, the clock moved, an event queued, an executor run or left.
func (ctx *Context) workChanged() { ctx.work++ }

// quiescence is the memo a full behavior scan leaves when it finds every
// attached behavior idle: the work and write marks it holds under, and whether
// the scan read the objects' data, so writes since then invalidate it.
type quiescence struct {
at uint64
writes uint64
readsData bool
}

// holds reports whether the memo still answers: taken, and nothing it depends
// on moved since, the objects' data counting only where the scan read it.
func (q quiescence) holds(ctx *Context) bool {
return q.at != 0 && q.at == ctx.work && (!q.readsData || q.writes == ctx.writes)
}

// setClock moves the shared clock, the work due on it moving with it.
func (ctx *Context) setClock(now float64) {
ctx.clock.now = now
ctx.workChanged()
}

// holdDrivenWork marks, at an outermost start, the behaviors already holding
// work: a driver put it in flight, so the start leaves it to that driver. Once
// the start returns, the behaviors it attached are as their start left them.
func (ctx *Context) holdDrivenWork() func() {
if ctx.behaviorRunDepth > 0 || ctx.heldBehaviors != nil {
if ctx.behaviorRunDepth > 0 || ctx.holdingDriven {
return func() { /* an outer start already holds them */ }
}
held := make(map[*ObjectBehavior]bool)
ctx.behaviorRunDepth++
for _, behavior := range ctx.objectBehaviors {
if behavior.hasPendingWork() {
held[behavior] = true
var held map[*ObjectBehavior]bool
if ctx.quiescent.holds(ctx) {
// Nothing woke a behavior since a full scan found them all idle.
} else if len(ctx.objectBehaviors) == 0 {
ctx.quiescent = quiescence{at: ctx.work, writes: ctx.writes}
} else {
memo := &pendingMemo{}
saved := ctx.polling
ctx.polling = memo
ctx.behaviorRunDepth++
for _, behavior := range ctx.objectBehaviors {
if behavior.hasPendingWork() {
if held == nil {
held = map[*ObjectBehavior]bool{}
}
held[behavior] = true
}
}
ctx.behaviorRunDepth--
ctx.polling = saved
if saved != nil && memo.readsData {
saved.readsData = true
}
if len(held) == 0 {
ctx.quiescent = quiescence{at: ctx.work, writes: ctx.writes, readsData: memo.readsData}
}
}
ctx.behaviorRunDepth--
ctx.heldBehaviors = held
ctx.holdingDriven = true
attached := len(ctx.objectBehaviors)
return func() {
ctx.holdingDriven = false
ctx.heldBehaviors = nil
for _, behavior := range ctx.objectBehaviors[min(attached, len(ctx.objectBehaviors)):] {
behavior.settle()
Expand Down Expand Up @@ -771,6 +818,7 @@ func (ctx *Context) forgetBehaviors(behaviors []*ObjectBehavior) {
}
ctx.objectBehaviors = behaviorsExcept(ctx.objectBehaviors, dropped)
ctx.pendingBehaviors = behaviorsExcept(ctx.pendingBehaviors, dropped)
ctx.workChanged()
}

// leaveClock releases the behavior's execution, ending the work it left paused
Expand Down Expand Up @@ -838,11 +886,36 @@ func (ctx *Context) nextRunnableBehavior() (*ObjectBehavior, bool) {
return behavior, true
}
}
// A context a full scan found idle, unchanged since, holds no runnable behavior.
if ctx.quiescent.holds(ctx) {
return nil, false
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
}
if attached >= len(ctx.objectBehaviors) {
// Every behavior pending is already held by a driver, or there are none.
if attached == 0 && len(ctx.heldBehaviors) == 0 {
ctx.quiescent = quiescence{at: ctx.work, writes: ctx.writes}
}
return nil, false
}
memo := &pendingMemo{}
saved := ctx.polling
ctx.polling = memo
for _, behavior := range ctx.objectBehaviors[attached:] {
if !ctx.heldBehaviors[behavior] && behavior.hasPendingWork() {
ctx.polling = saved
if saved != nil && memo.readsData {
saved.readsData = true
}
return behavior, true
}
}
ctx.polling = saved
if saved != nil && memo.readsData {
saved.readsData = true
}
if attached == 0 && len(ctx.heldBehaviors) == 0 {
ctx.quiescent = quiescence{at: ctx.work, writes: ctx.writes, readsData: memo.readsData}
}
return nil, false
}

Expand Down
20 changes: 14 additions & 6 deletions internal/exec/runtime/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ type Context struct {
// heldBehaviors are the behaviors already holding work when the outermost
// start under way began: a driver put it in flight, and dispatches it.
heldBehaviors map[*ObjectBehavior]bool
// holdingDriven marks that hold however it came out, nil map included; a
// nested start leaves the driving to the outermost one.
holdingDriven bool

// objectBehaviors are every behavior an object of this context runs, so a
// drain to quiescence can re-run one a sibling's send woke.
Expand Down Expand Up @@ -241,6 +244,10 @@ type Context struct {
// clockRun the run an advance of it draws its due-order choices from.
clock Clock
clockRun executorRun
// work counts the changes that can leave an attached behavior holding work;
// quiescent is the memo a full scan leaves when it finds them all idle.
work uint64
quiescent quiescence
// onStack lists the runs of the executors whose calls are under way, outermost first.
onStack []*executorRun

Expand Down Expand Up @@ -302,8 +309,7 @@ func NewContext(model *Model, maxSteps int64) *Context {
compileCalcs: CalcCompileFromEnv(),

run: &runState{
calcUsageRuns: make(map[int64]map[calcUsageKey]*calcRun),
extentCandidates: make(map[*symbols.Symbol]*extentCandidates),
calcUsageRuns: make(map[int64]map[calcUsageKey]*calcRun),
},
calcUsageRunning: make(map[calcUsageKey]*calcShape),

Expand Down Expand Up @@ -658,9 +664,8 @@ type runState struct {
// newRunState is the state a run starts with, under the schedule policy set now.
func (ctx *Context) newRunState() *runState {
return &runState{
scheduler: ctx.newScheduler(),
calcUsageRuns: make(map[int64]map[calcUsageKey]*calcRun),
extentCandidates: make(map[*symbols.Symbol]*extentCandidates),
scheduler: ctx.newScheduler(),
calcUsageRuns: make(map[int64]map[calcUsageKey]*calcRun),
}
}

Expand Down Expand Up @@ -746,6 +751,7 @@ func (ctx *Context) beginExecutorRun(run *executorRun) func() {
run.stir(1)
ctx.onStack = append(ctx.onStack, run)
leave := ctx.enterRun(run.state)
ctx.workChanged()
// A call into an executor whose performer ended in between finds its performance over.
if run.exec != nil && run.exec.performerEnded() {
run.exec.endTerminated()
Expand All @@ -754,6 +760,7 @@ func (ctx *Context) beginExecutorRun(run *executorRun) func() {
leave()
ctx.onStack = ctx.onStack[:len(ctx.onStack)-1]
run.stir(-1)
ctx.workChanged()
}
}

Expand Down Expand Up @@ -806,7 +813,8 @@ func (ctx *Context) previewExecutorRun(run *executorRun) func() {
} else {
ctx.run = ctx.newRunState()
}
return func() { ctx.run = saved }
ctx.workChanged()
return func() { ctx.run = saved; ctx.workChanged() }
}

// endExecutorRun brackets the release of a call-by-call driven run: its leftovers
Expand Down
3 changes: 3 additions & 0 deletions internal/exec/runtime/extent.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,9 @@ func (ctx *Context) extentCandidates(target *symbols.Symbol) []*symbols.Symbol {
}
}
found.judged = append(found.judged, target)
if ctx.run.extentCandidates == nil {
ctx.run.extentCandidates = map[*symbols.Symbol]*extentCandidates{}
}
ctx.run.extentCandidates[target] = found
}
for _, sym := range found.usages {
Expand Down
5 changes: 3 additions & 2 deletions internal/exec/runtime/held_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ func (mark materializeMark) rollBack(ctx *Context) {
ctx.ids.release(ctx, mark.nextID)
}
ctx.activations, ctx.runs = mark.activations, mark.runs
ctx.clock.now = mark.clock
ctx.setClock(mark.clock)
ctx.clockRun.state = mark.clockRun
ctx.lifetimes.dependents = mark.readLives
}
Expand Down Expand Up @@ -721,7 +721,7 @@ func (m *materializing) run() error {
dst.livesChanged()
dst.activations = max(dst.activations, img.activations)
dst.runs = max(dst.runs, img.runs)
dst.clock.now = img.clock
dst.setClock(img.clock)
for _, run := range img.runStates {
m.runs = append(m.runs, m.runState(run))
}
Expand All @@ -744,6 +744,7 @@ func (m *materializing) run() error {
// Nothing below fails: what names the objects made is installed once they all stand.
dst.messages = append(dst.messages, messages...)
dst.bus.posts += uint64(len(messages))
dst.workChanged()
for sym, ids := range img.occurrences {
dst.occurrences[sym] = slices.Clone(ids)
}
Expand Down
1 change: 1 addition & 0 deletions internal/exec/runtime/held_image_behavior.go
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ func (m *materializing) behavior(b imagedBehavior) error {
}
inst.behaviors = append(inst.behaviors, behavior)
dst.objectBehaviors = append(dst.objectBehaviors, behavior)
dst.workChanged()
return nil
}

Expand Down
Loading
Loading