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/lazy-workspace-regather.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Editing a document no longer recomputes workspace-wide gathers inside the edit; they are recomputed on the first diagnostics or query after it, so an edit itself is as cheap as before those gathers existed.
40 changes: 40 additions & 0 deletions internal/workspace/model/lazy_regather_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package model

import (
"reflect"
"testing"
)

// TestWorkspacePendingGathersSettleOnRead: an edit queues its gathers instead of
// replaying them, so several edits coalesce into one settle the next read runs —
// and what that read reports is what a fresh analysis of the same text says.
func TestWorkspacePendingGathersSettleOnRead(t *testing.T) {
const notDerived = "oosem-requirement-not-derived"
sat := []byte("package S { private import OOSEM::*; #systemRequirement requirement sys; }")

ws := NewWorkspace()
ws.Open("hub.sysml", []byte("package M { private import OOSEM::*; #stakeholderNeed requirement need; }"), 1)
ws.Open("sat.sysml", sat, 1)
ws.Diagnostics("sat.sysml")

// Two edits with no read between them queue together: the hub drops its
// stakeholder need for a mission requirement, the satellite's verdict
// follows it when the next diagnostics settle the pending regathers.
ws.Update("hub.sysml", []byte("package M { private import OOSEM::*; #missionRequirement requirement mission; }"), 2)
ws.Update("sat.sysml", []byte("package S { private import OOSEM::*; #systemRequirement requirement sys; requirement extra; }"), 2)
gotHub := codesOf(ws.Diagnostics("hub.sysml"))
gotSat := codesOf(ws.Diagnostics("sat.sysml"))
if gotSat[notDerived] != 1 {
t.Fatalf("sat.sysml under a mission requirement added while pending: %v, want one %s", gotSat, notDerived)
}

fresh := NewWorkspace()
fresh.Open("hub.sysml", ws.Document("hub.sysml").Content, 1)
fresh.Open("sat.sysml", ws.Document("sat.sysml").Content, 1)
if want := codesOf(fresh.Diagnostics("hub.sysml")); !reflect.DeepEqual(gotHub, want) {
t.Errorf("hub.sysml: incremental %v, fresh %v", gotHub, want)
}
if want := codesOf(fresh.Diagnostics("sat.sysml")); !reflect.DeepEqual(gotSat, want) {
t.Errorf("sat.sysml: incremental %v, fresh %v", gotSat, want)
}
}
1 change: 1 addition & 0 deletions internal/workspace/model/refindex.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func (x *refIndex) add(doc *Document, ref resolve.Reference, part int, element,
// document, in document then position order, building the table of each
// document a change has dropped. Caller holds the write lock.
func (w *Workspace) referencesLocked(key symbols.ElementKey) []refEntry {
w.settleGathersLocked()
if w.refs == nil {
w.refs = newRefIndex()
}
Expand Down
57 changes: 43 additions & 14 deletions internal/workspace/model/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ type Workspace struct {
resolver *resolve.Resolver
model *semantics.Model
gathers *passes.Gathers
// regatherPending queues the documents whose gathers an invalidate dropped,
// replayed on the next read; settling bars a settle re-entering itself.
regatherPending map[string]bool
settling bool
// analysis is the options every document of this workspace is analyzed under,
// so one session asks one question of all its files.
analysis passes.Options
Expand Down Expand Up @@ -350,20 +354,35 @@ func (w *Workspace) invalidateLocked(name string) {
dropped := w.resolver.Invalidate(ch)
delete(w.diagCache, name)
w.refs.drop(name)
// The gathers the drop took go again, and what they now say differently
// drops the judgments that read it, until nothing more moves.
regather := ch.Docs
for {
for _, doc := range dropped {
delete(w.diagCache, doc)
w.refs.drop(doc)
if gathered, ok := resolve.GatheredDoc(doc); ok {
regather[gathered] = true
}
}
if len(regather) == 0 {
return
// The gathers the drop took are replayed by the next read that needs them
// (settleGathersLocked), not inside the edit.
if w.regatherPending == nil {
w.regatherPending = map[string]bool{}
}
for doc := range ch.Docs {
w.regatherPending[doc] = true
}
for _, doc := range dropped {
delete(w.diagCache, doc)
w.refs.drop(doc)
if gathered, ok := resolve.GatheredDoc(doc); ok {
w.regatherPending[gathered] = true
}
}
}

// settleGathersLocked runs the regather cascade the invalidations queued: what
// a regathered union now says differently drops the judgments that read it,
// until nothing more moves. Caller holds the write lock.
func (w *Workspace) settleGathersLocked() {
if w.settling || w.resolver == nil || len(w.regatherPending) == 0 {
return
}
w.settling = true
defer func() { w.settling = false }()
regather := w.regatherPending
w.regatherPending = nil
for len(regather) > 0 {
changed := w.gathers.Regather(w.contextLocked(), regather)
if len(changed) == 0 {
return
Expand All @@ -372,8 +391,15 @@ func (w *Workspace) invalidateLocked(name string) {
for _, n := range changed {
names[n] = true
}
dropped = w.resolver.Invalidate(symbols.Changes{Names: names})
dropped := w.resolver.Invalidate(symbols.Changes{Names: names})
regather = map[string]bool{}
for _, doc := range dropped {
delete(w.diagCache, doc)
w.refs.drop(doc)
if gathered, ok := resolve.GatheredDoc(doc); ok {
regather[gathered] = true
}
}
}
}

Expand All @@ -390,6 +416,7 @@ func (w *Workspace) contextLocked() *passes.Context {
func (w *Workspace) invalidateAllLocked() {
w.diagCache = map[string][]diag.Diagnostic{}
w.refs = nil
w.regatherPending = nil
w.generation++
if w.resolver != nil {
w.resolver.InvalidateAll()
Expand Down Expand Up @@ -425,6 +452,7 @@ func (w *Workspace) AnalyzedContent(name string) ([]byte, []diag.Diagnostic, boo

// diagnosticsLocked analyzes doc, caching the result. Caller holds the lock.
func (w *Workspace) diagnosticsLocked(name string, doc *Document) []diag.Diagnostic {
w.settleGathersLocked()
if cached, ok := w.diagCache[name]; ok {
return cached
}
Expand Down Expand Up @@ -540,6 +568,7 @@ func (w *Workspace) semanticsLocked() (*resolve.Resolver, *semantics.Model) {
resolver.Track()
w.resolver, w.model, w.gathers = resolver, sem, passes.NewGathers()
}
w.settleGathersLocked()
return w.resolver, w.model
}

Expand Down
Loading