diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolTableAddRemoveStrategy.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolTableAddRemoveStrategy.java new file mode 100644 index 0000000000..f6cbefa1e2 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolTableAddRemoveStrategy.java @@ -0,0 +1,85 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.symtable; + +import java.util.*; + +import docking.widgets.table.AddRemoveListItem; +import docking.widgets.table.threaded.TableAddRemoveStrategy; +import docking.widgets.table.threaded.TableData; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * This strategy attempts to optimize removal of db objects that have been deleted. The issue with + * deleted db objects is that they may no longer have their attributes, which means we cannot + * use any of those attributes that may have been used as the basis for sorting. We use the + * table's sort to perform a binary search of existing symbols for removal. If the binary search + * does not work, then removal operations will require slow list traversal. Additionally, + * some clients use proxy objects in add/remove list to signal which object needs to be removed, + * since the original object is no longer available to the client. Using these proxy objects + * in a binary search may lead to exceptions if the proxy has unsupported methods called when + * searching. + * + *

This strategy will has guilty knowledge of client proxy object usage. The proxy objects + * are coded such that the {@code hashCode()} and {@code equals()} methods will match those + * methods of the data's real objects. + * + * @param the row type + */ +public class SymbolTableAddRemoveStrategy implements TableAddRemoveStrategy { + + @Override + public void process(List> addRemoveList, TableData tableData, + TaskMonitor monitor) throws CancelledException { + + // + // Hash map the existing values so that we can use any object inside the add/remove list + // as a key into this map to get the matching existing value. + // + Map hashed = new HashMap<>(); + for (T t : tableData) { + hashed.put(t, t); + } + + int n = addRemoveList.size(); + monitor.setMessage("Adding/Removing " + n + " items..."); + monitor.initialize(n); + for (int i = 0; i < n; i++) { + AddRemoveListItem item = addRemoveList.get(i); + T value = item.getValue(); + if (item.isChange()) { + T toRemove = hashed.get(value); + if (toRemove != null) { + tableData.remove(toRemove); + } + tableData.insert(value); + } + else if (item.isRemove()) { + T toRemove = hashed.get(value); + if (toRemove != null) { + tableData.remove(toRemove); + } + } + else if (item.isAdd()) { + tableData.insert(value); + } + monitor.checkCanceled(); + monitor.setProgress(i); + } + monitor.setMessage("Done adding/removing"); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolTableModel.java index fb922b22c0..c20783ca38 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolTableModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolTableModel.java @@ -18,6 +18,7 @@ package ghidra.app.plugin.core.symtable; import java.util.*; import docking.widgets.table.*; +import docking.widgets.table.threaded.TableAddRemoveStrategy; import ghidra.app.cmd.function.DeleteFunctionCmd; import ghidra.app.cmd.label.DeleteLabelCmd; import ghidra.app.cmd.label.RenameLabelCmd; @@ -60,6 +61,8 @@ class SymbolTableModel extends AddressBasedTableModel { private ReferenceManager refMgr; private Symbol lastSymbol; private SymbolFilter filter; + private TableAddRemoveStrategy deletedDbObjectAddRemoveStrategy = + new SymbolTableAddRemoveStrategy<>(); SymbolTableModel(SymbolProvider provider, PluginTool tool) { super("Symbols", tool, null, null); @@ -88,6 +91,11 @@ class SymbolTableModel extends AddressBasedTableModel { return descriptor; } + @Override + protected TableAddRemoveStrategy getAddRemoveStrategy() { + return deletedDbObjectAddRemoveStrategy; + } + void setFilter(SymbolFilter filter) { this.filter = filter; reload(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/DefaultAddRemoveStrategy.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/DefaultAddRemoveStrategy.java new file mode 100644 index 0000000000..17c085b223 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/DefaultAddRemoveStrategy.java @@ -0,0 +1,61 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package docking.widgets.table.threaded; + +import java.util.List; + +import docking.widgets.table.AddRemoveListItem; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * A strategy that uses the table's sort state to perform a binary search of items to be added + * and removed. + * + * @param the row type + */ +public class DefaultAddRemoveStrategy implements TableAddRemoveStrategy { + + @Override + public void process(List> addRemoveList, TableData updatedData, + TaskMonitor monitor) throws CancelledException { + + int n = addRemoveList.size(); + monitor.setMessage("Adding/Removing " + n + " items..."); + monitor.initialize(n); + + // Note: this class does not directly perform a binary such, but instead relies on that + // work to be done by the call to TableData.remove() + for (int i = 0; i < n; i++) { + AddRemoveListItem item = addRemoveList.get(i); + T value = item.getValue(); + if (item.isChange()) { + updatedData.remove(value); + updatedData.insert(value); + } + else if (item.isRemove()) { + updatedData.remove(value); + } + else if (item.isAdd()) { + updatedData.insert(value); + } + monitor.checkCanceled(); + monitor.setProgress(i); + } + monitor.setMessage("Done adding/removing"); + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableAddRemoveStrategy.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableAddRemoveStrategy.java new file mode 100644 index 0000000000..3bb91bb580 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableAddRemoveStrategy.java @@ -0,0 +1,40 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package docking.widgets.table.threaded; + +import java.util.List; + +import docking.widgets.table.AddRemoveListItem; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * A strategy to perform table add and remove updates + * + * @param the row type + */ +public interface TableAddRemoveStrategy { + + /** + * Adds to and removes from the table data those items in the given add/remove list + * @param addRemoveList the items to add/remove + * @param tableData the table's data + * @param monitor the monitor + * @throws CancelledException if the monitor is cancelled + */ + public void process(List> addRemoveList, TableData tableData, + TaskMonitor monitor) throws CancelledException; +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableData.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableData.java index 72b24ff65d..e59de08958 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableData.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableData.java @@ -137,7 +137,7 @@ public class TableData implements Iterable { * @param t the item * @return the index */ - int indexOf(ROW_OBJECT t) { + public int indexOf(ROW_OBJECT t) { if (!sortContext.isUnsorted()) { Comparator comparator = sortContext.getComparator(); return Collections.binarySearch(data, t, comparator); @@ -153,7 +153,7 @@ public class TableData implements Iterable { return -1; } - boolean remove(ROW_OBJECT t) { + public boolean remove(ROW_OBJECT t) { if (source != null) { source.remove(t); } @@ -186,7 +186,7 @@ public class TableData implements Iterable { * * @param value the row Object to insert */ - void insert(ROW_OBJECT value) { + public void insert(ROW_OBJECT value) { if (source != null) { // always update the master data diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableUpdateJob.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableUpdateJob.java index 08f09b1ab7..94df233f11 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableUpdateJob.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableUpdateJob.java @@ -553,31 +553,14 @@ public class TableUpdateJob { */ private void doProcessAddRemoves() throws CancelledException { - int n = addRemoveList.size(); - monitor.setMessage("Adding/Removing " + n + " items..."); - monitor.initialize(n); - initializeSortCache(); - - for (int i = 0; i < n; i++) { - AddRemoveListItem item = addRemoveList.get(i); - T value = item.getValue(); - if (item.isChange()) { - updatedData.remove(value); - updatedData.insert(value); - } - else if (item.isRemove()) { - updatedData.remove(value); - } - else if (item.isAdd()) { - updatedData.insert(value); - } - monitor.checkCanceled(); - monitor.setProgress(i); + try { + TableAddRemoveStrategy strategy = model.getAddRemoveStrategy(); + strategy.process(addRemoveList, updatedData, monitor); + } + finally { + clearSortCache(); } - monitor.setMessage("Done adding/removing"); - - clearSortCache(); } /** When sorting we cache column value lookups to increase speed. */ diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/ThreadedTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/ThreadedTableModel.java index 6439ef4edd..67ac2cc585 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/ThreadedTableModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/ThreadedTableModel.java @@ -90,6 +90,8 @@ public abstract class ThreadedTableModel private volatile Worker worker; // only created as needed (if we are incremental) private int minUpdateDelayMillis; private int maxUpdateDelayMillis; + private TableAddRemoveStrategy binarySearchAddRemoveStrategy = + new DefaultAddRemoveStrategy<>(); protected ThreadedTableModel(String modelName, ServiceProvider serviceProvider) { this(modelName, serviceProvider, null); @@ -510,6 +512,17 @@ public abstract class ThreadedTableModel /** * Removes the specified object from this model and schedules an update. + * + *

Note: for this method to function correctly, the given object must compare as + * {@link #equals(Object)} and have the same {@link #hashCode()} as the object to be removed + * from the table data. This allows clients to create proxy objects to pass into this method, + * as long as they honor those requirements. + * + *

If this model's data is sorted, then a binary search will be used to locate the item + * to be removed. However, for this to work, all field used to sort the data must still be + * available from the original object and must be the same values. If this is not true, then + * the binary search will not work and a brute force search will be used. + * * @param obj the object to remove */ public void removeObject(ROW_OBJECT obj) { @@ -786,6 +799,23 @@ public abstract class ThreadedTableModel updateManager.setTaskMonitor(monitor); } + /** + * Returns the strategy to use for performing adds and removes to this table. Subclasses can + * override this method to customize this process for their particular type of data. See + * the implementations of {@link TableAddRemoveStrategy} for details. + * + *

Note: The default add/remove strategy assumes that objects to be removed will be the + * same instance that is in the list of this model. This allows the {@link #equals(Object)} + * and {@link #hashCode()} to be used when removing the object from the list. If you model + * does not pass the same instance into {@link #removeObject(Object)}, then you will need to + * update your add/remove strategy accordingly. + * + * @return the strategy + */ + protected TableAddRemoveStrategy getAddRemoveStrategy() { + return binarySearchAddRemoveStrategy; + } + public void setIncrementalTaskMonitor(TaskMonitor monitor) { SystemUtilities.assertTrue(loadIncrementally, "Cannot set an incremental task monitor " + "on a table that was not constructed to load incrementally");