Release retired document sources outside the cache lock
This commit is contained in:
1 parent
b4896a2eb4
commit
228271b43b
3 files changed
+124
-13
No files matched your search
+28
-13
@@ -489,11 +489,30 @@ func (adapter *Adapter) retainedDocumentRoot(id ObjectID) ObjectID {
|
|||||||
// calls this on the GLib thread once the capture root is known, before
|
// calls this on the GLib thread once the capture root is known, before
|
||||||
// preparing the incremental-refresh cache.
|
// preparing the incremental-refresh cache.
|
||||||
func (adapter *Adapter) rotateDocumentSlots(root ObjectID) {
|
func (adapter *Adapter) rotateDocumentSlots(root ObjectID) {
|
||||||
|
adapter.rotateDocumentSlotsAndRelease(root, releaseDocumentSources)
|
||||||
|
}
|
||||||
|
|
||||||
|
func releaseDocumentSources(document stashedDocument) {
|
||||||
|
for _, source := range document.sources {
|
||||||
|
C.g_object_unref(C.gpointer(source))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// libatspi disposal synchronously emits defunct events. Publish the new slots
|
||||||
|
// and unlock before releasing references, so reentrant invalidation sees a
|
||||||
|
// consistent cache and can acquire treeMutex.
|
||||||
|
func (adapter *Adapter) rotateDocumentSlotsAndRelease(root ObjectID, release func(stashedDocument)) {
|
||||||
if !root.Valid() {
|
if !root.Valid() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
var retired []stashedDocument
|
||||||
adapter.treeMutex.Lock()
|
adapter.treeMutex.Lock()
|
||||||
defer adapter.treeMutex.Unlock()
|
defer func() {
|
||||||
|
adapter.treeMutex.Unlock()
|
||||||
|
for _, document := range retired {
|
||||||
|
release(document)
|
||||||
|
}
|
||||||
|
}()
|
||||||
if root == adapter.documentSlotRoot {
|
if root == adapter.documentSlotRoot {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -509,14 +528,12 @@ func (adapter *Adapter) rotateDocumentSlots(root ObjectID) {
|
|||||||
}
|
}
|
||||||
if adapter.documentSlotRoot.Valid() &&
|
if adapter.documentSlotRoot.Valid() &&
|
||||||
(adapter.documentTree != nil || adapter.documentTextTree != nil) {
|
(adapter.documentTree != nil || adapter.documentTextTree != nil) {
|
||||||
adapter.stashDocumentLocked(adapter.documentSlotRoot, stashedDocument{
|
retired = adapter.stashDocumentLocked(adapter.documentSlotRoot, stashedDocument{
|
||||||
tree: adapter.documentTree, textTree: adapter.documentTextTree,
|
tree: adapter.documentTree, textTree: adapter.documentTextTree,
|
||||||
sources: adapter.nativeDocument,
|
sources: adapter.nativeDocument,
|
||||||
})
|
})
|
||||||
} else {
|
} else if len(adapter.nativeDocument) != 0 {
|
||||||
for _, source := range adapter.nativeDocument {
|
retired = append(retired, stashedDocument{sources: adapter.nativeDocument})
|
||||||
C.g_object_unref(C.gpointer(source))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
adapter.documentTree = stashed.tree
|
adapter.documentTree = stashed.tree
|
||||||
adapter.documentTextTree = stashed.textTree
|
adapter.documentTextTree = stashed.textTree
|
||||||
@@ -524,14 +541,13 @@ func (adapter *Adapter) rotateDocumentSlots(root ObjectID) {
|
|||||||
adapter.documentSlotRoot = root
|
adapter.documentSlotRoot = root
|
||||||
}
|
}
|
||||||
|
|
||||||
func (adapter *Adapter) stashDocumentLocked(root ObjectID, document stashedDocument) {
|
func (adapter *Adapter) stashDocumentLocked(root ObjectID, document stashedDocument) []stashedDocument {
|
||||||
|
var retired []stashedDocument
|
||||||
if adapter.stashedDocuments == nil {
|
if adapter.stashedDocuments == nil {
|
||||||
adapter.stashedDocuments = make(map[ObjectID]stashedDocument)
|
adapter.stashedDocuments = make(map[ObjectID]stashedDocument)
|
||||||
}
|
}
|
||||||
if old, ok := adapter.stashedDocuments[root]; ok {
|
if old, ok := adapter.stashedDocuments[root]; ok {
|
||||||
for _, source := range old.sources {
|
retired = append(retired, old)
|
||||||
C.g_object_unref(C.gpointer(source))
|
|
||||||
}
|
|
||||||
for index, id := range adapter.stashedDocumentIDs {
|
for index, id := range adapter.stashedDocumentIDs {
|
||||||
if id == root {
|
if id == root {
|
||||||
adapter.stashedDocumentIDs = append(adapter.stashedDocumentIDs[:index], adapter.stashedDocumentIDs[index+1:]...)
|
adapter.stashedDocumentIDs = append(adapter.stashedDocumentIDs[:index], adapter.stashedDocumentIDs[index+1:]...)
|
||||||
@@ -543,12 +559,11 @@ func (adapter *Adapter) stashDocumentLocked(root ObjectID, document stashedDocum
|
|||||||
adapter.stashedDocumentIDs = append(adapter.stashedDocumentIDs, root)
|
adapter.stashedDocumentIDs = append(adapter.stashedDocumentIDs, root)
|
||||||
for len(adapter.stashedDocumentIDs) > maxStashedDocuments {
|
for len(adapter.stashedDocumentIDs) > maxStashedDocuments {
|
||||||
oldest := adapter.stashedDocumentIDs[0]
|
oldest := adapter.stashedDocumentIDs[0]
|
||||||
for _, source := range adapter.stashedDocuments[oldest].sources {
|
retired = append(retired, adapter.stashedDocuments[oldest])
|
||||||
C.g_object_unref(C.gpointer(source))
|
|
||||||
}
|
|
||||||
delete(adapter.stashedDocuments, oldest)
|
delete(adapter.stashedDocuments, oldest)
|
||||||
adapter.stashedDocumentIDs = adapter.stashedDocumentIDs[1:]
|
adapter.stashedDocumentIDs = adapter.stashedDocumentIDs[1:]
|
||||||
}
|
}
|
||||||
|
return retired
|
||||||
}
|
}
|
||||||
|
|
||||||
// clearStashedDocuments releases every stashed document's native sources.
|
// clearStashedDocuments releases every stashed document's native sources.
|
||||||
|
|||||||
@@ -683,3 +683,67 @@ func TestCaptureFailsFastWhileProviderHung(t *testing.T) {
|
|||||||
t.Fatalf("hung placement error = %v, want ErrProviderHung", err)
|
t.Fatalf("hung placement error = %v, want ErrProviderHung", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDocumentSlotReleaseAllowsReentrantInvalidation(t *testing.T) {
|
||||||
|
for _, orphaned := range []bool{false, true} {
|
||||||
|
t.Run(fmt.Sprintf("orphaned=%v", orphaned), func(t *testing.T) {
|
||||||
|
adapter := New()
|
||||||
|
oldRoot := testID("/old")
|
||||||
|
newRoot := testID("/new")
|
||||||
|
oldTree, err := NewAccessibleTree(oldRoot, []TreeNode{{Snapshot: Snapshot{BusName: oldRoot.BusName, Path: oldRoot.Path}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if orphaned {
|
||||||
|
adapter.nativeDocument = documentSourceKeys(adapter.nativeDocument, oldRoot)
|
||||||
|
} else {
|
||||||
|
adapter.documentSlotRoot = testID("/active")
|
||||||
|
adapter.documentTree = oldTree
|
||||||
|
adapter.stashedDocumentIDs = []ObjectID{oldRoot, testID("/second"), testID("/third")}
|
||||||
|
adapter.stashedDocuments = map[ObjectID]stashedDocument{oldRoot: {tree: oldTree}}
|
||||||
|
}
|
||||||
|
releases := 0
|
||||||
|
adapter.rotateDocumentSlotsAndRelease(newRoot, func(retired stashedDocument) {
|
||||||
|
releases++
|
||||||
|
if !adapter.treeMutex.TryLock() {
|
||||||
|
t.Fatal("native disposal would deadlock reentrant defunct invalidation")
|
||||||
|
}
|
||||||
|
adapter.treeMutex.Unlock()
|
||||||
|
if adapter.documentSlotRoot != newRoot || adapter.documentTree != nil {
|
||||||
|
t.Fatal("disposal observed partially rotated slots")
|
||||||
|
}
|
||||||
|
if _, retained := adapter.stashedDocuments[oldRoot]; retained {
|
||||||
|
t.Fatal("disposed document still retained")
|
||||||
|
}
|
||||||
|
adapter.invalidateTreeEvent(DefunctStateEvent, 1, oldRoot, ObjectID{})
|
||||||
|
})
|
||||||
|
if releases != 1 {
|
||||||
|
t.Fatalf("released %d documents, want 1", releases)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDocumentSlotEviction(b *testing.B) {
|
||||||
|
adapter := New()
|
||||||
|
roots := make([]ObjectID, maxStashedDocuments+2)
|
||||||
|
trees := make([]*AccessibleTree, len(roots))
|
||||||
|
for i := range roots {
|
||||||
|
roots[i] = testID(fmt.Sprintf("/doc%d", i))
|
||||||
|
var err error
|
||||||
|
trees[i], err = NewAccessibleTree(roots[i], []TreeNode{{Snapshot: Snapshot{BusName: roots[i].BusName, Path: roots[i].Path}}})
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
index := i % len(roots)
|
||||||
|
adapter.rotateDocumentSlots(roots[index])
|
||||||
|
if adapter.documentTree != nil {
|
||||||
|
b.Fatal("expected evicted document to need recapture")
|
||||||
|
}
|
||||||
|
adapter.documentTree = trees[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -390,6 +390,7 @@ waitReload:
|
|||||||
if !activated {
|
if !activated {
|
||||||
t.Fatal("captured link action did not update the page")
|
t.Fatal("captured link action did not update the page")
|
||||||
}
|
}
|
||||||
|
testNativeBrowserDocumentEviction(t, adapter)
|
||||||
testNativeBrowserBackgroundLoad(t, adapter)
|
testNativeBrowserBackgroundLoad(t, adapter)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -481,3 +482,34 @@ func testNativeBrowserBackgroundLoad(t *testing.T, adapter *Adapter) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// More loads than the retained-slot limit force disposal of old native objects.
|
||||||
|
// libatspi can synchronously deliver defunct callbacks during that disposal.
|
||||||
|
func testNativeBrowserDocumentEviction(t *testing.T, adapter *Adapter) {
|
||||||
|
t.Helper()
|
||||||
|
for index := 0; index < maxStashedDocuments+2; index++ {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
if out, err := exec.CommandContext(ctx, "xdotool", "key", "--clearmodifiers", "ctrl+r").CombinedOutput(); err != nil {
|
||||||
|
cancel()
|
||||||
|
t.Fatalf("reload %d: %v %s", index, err, out)
|
||||||
|
}
|
||||||
|
var loaded ObjectID
|
||||||
|
for !loaded.Valid() {
|
||||||
|
select {
|
||||||
|
case event := <-adapter.Events():
|
||||||
|
if event.Type == DocumentLoadCompleteEvent && event.Source.Name == "Cthulhu Browser Probe" {
|
||||||
|
loaded = event.Source.ID()
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
cancel()
|
||||||
|
t.Fatalf("reload %d: load completion missing", index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tree, err := adapter.CaptureDocument(ctx, loaded)
|
||||||
|
cancel()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("capture after reload %d: %v", index, err)
|
||||||
|
}
|
||||||
|
findNamedReference(t, tree, "Browser foundation")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user