Skip to content

Commit 34b9748

Browse files
committed
Optimize WorkspaceTreeNode getOrInsert behavior
The flag for-each child matcher now is done via binary search since paths are comparable.
1 parent bb5b719 commit 34b9748

File tree

5 files changed

+92
-32
lines changed

5 files changed

+92
-32
lines changed

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ dex-translator = "1.1.1"
1212
diffutils = "4.16"
1313
docking = "0.11.1"
1414
downgrader = "1.3.3"
15-
extra-collections = "1.6.0"
15+
extra-collections = "1.7.0"
1616
extra-observables = "1.3.0"
1717
gson = "2.13.1"
1818
ikonli = "12.4.0"

recaf-core/src/main/java/software/coley/recaf/path/DirectoryPathNode.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import software.coley.recaf.info.FileInfo;
88
import software.coley.recaf.workspace.model.bundle.Bundle;
99

10+
import java.util.Objects;
1011
import java.util.Set;
1112

1213
/**
@@ -129,4 +130,17 @@ public int localCompare(PathNode<?> o) {
129130
}
130131
return 0;
131132
}
133+
134+
@Override
135+
public boolean equals(Object o) {
136+
// If the directories are the same and the parent paths are also equal, then this path points to the same location.
137+
if (o instanceof DirectoryPathNode otherPath) {
138+
String dir = getValue();
139+
String otherDir = otherPath.getValue();
140+
return dir.hashCode() == otherDir.hashCode() // Hash check first which is very fast, and the result is cached.
141+
&& dir.equals(otherDir) // Sanity check for matching items to prevent hash collisions.
142+
&& Objects.equals(getParent(), otherPath.getParent()); // Parents must also match.
143+
}
144+
return false;
145+
}
132146
}

recaf-core/src/main/java/software/coley/recaf/path/FilePathNode.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import software.coley.recaf.info.FileInfo;
77
import software.coley.recaf.workspace.model.bundle.FileBundle;
88

9+
import java.util.Objects;
910
import java.util.Set;
1011

1112
/**
@@ -104,4 +105,17 @@ public int localCompare(PathNode<?> o) {
104105
}
105106
return 0;
106107
}
108+
109+
@Override
110+
public boolean equals(Object o) {
111+
// If the file names are the same and the parent paths are also equal, then this path points to the same location.
112+
if (o instanceof FilePathNode otherPath) {
113+
String name = getValue().getName();
114+
String otherName = otherPath.getValue().getName();
115+
return name.hashCode() == otherName.hashCode() // Hash check first which is very fast, and the result is cached.
116+
&& name.equals(otherName) // Sanity check for matching items to prevent hash collisions.
117+
&& Objects.equals(getParent(), otherPath.getParent()); // Parents must also match.
118+
}
119+
return false;
120+
}
107121
}

recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/FilterableTreeItem.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,28 @@ public void addAndSortChild(@Nonnull TreeItem<T> item) {
176176
}
177177

178178
/**
179-
* Add an unfiltered unsorted child to this item.
179+
* Add an unfiltered sorted child to this item.
180+
* It is assumed the index provided will maintain sorted order when used with {@link List#add(int, Object)}.
181+
*
182+
* @param item
183+
* Child item to add.
184+
* @param index
185+
* Index to add the child at, assumed to maintain sorted order.
186+
*/
187+
public void addPreSortedChild(@Nonnull TreeItem<T> item, int index) {
188+
synchronized (sourceChildren) {
189+
if (sourceChildren.isEmpty())
190+
sourceChildren.add(item);
191+
else
192+
sourceChildren.add(index, item);
193+
if (item instanceof FilterableTreeItem<?> filterableItem)
194+
filterableItem.sourceParent.set(Unchecked.cast(this));
195+
}
196+
}
197+
198+
/**
199+
* Add an unfiltered sorted child to this item.
200+
* It is assumed this method is used exclusively and all paths are added in sorted order.
180201
*
181202
* @param item
182203
* Child item to add.

recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTreeNode.java

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import software.coley.recaf.path.PathNode;
1111
import software.coley.recaf.util.StringUtil;
1212

13+
import java.util.List;
14+
1315
/**
1416
* Tree item subtype for more convenience tree building operations.
1517
*
@@ -201,7 +203,6 @@ public static WorkspaceTreeNode getOrInsertIntoTree(@Nonnull WorkspaceTreeNode n
201203
* @return Inserted node.
202204
*/
203205
@Nonnull
204-
@SuppressWarnings("deprecation")
205206
public static WorkspaceTreeNode getOrInsertIntoTree(@Nonnull WorkspaceTreeNode node, @Nonnull PathNode<?> path, boolean sorted) {
206207
// Edge case handling for directory nodes.
207208
if (path instanceof DirectoryPathNode directoryPath) {
@@ -234,24 +235,17 @@ public static WorkspaceTreeNode getOrInsertIntoTree(@Nonnull WorkspaceTreeNode n
234235
DirectoryPathNode localPathNode = directoryPath.withDirectory(directoryName);
235236

236237
// Get existing tree node, or create child if non-existent
237-
WorkspaceTreeNode childNode = null;
238-
ObservableList<TreeItem<PathNode<?>>> children;
239-
if (node instanceof FilterableTreeItem<?> filterableNode)
240-
children = Unchecked.cast(filterableNode.getSourceChildren());
241-
else
242-
children = node.getChildren();
243-
for (TreeItem<PathNode<?>> child : children)
244-
if (child.getValue().equals(localPathNode)) {
245-
childNode = (WorkspaceTreeNode) child;
246-
break;
247-
}
248-
if (childNode == null) {
238+
WorkspaceTreeNode childNode;
239+
ObservableList<WorkspaceTreeNode> children = Unchecked.cast(node.getSourceChildren());
240+
int index = binaryUnboxingSearch(children, localPathNode);
241+
if (index >= 0)
242+
childNode = children.get(index);
243+
else {
249244
childNode = new WorkspaceTreeNode(localPathNode);
250-
if (sorted) {
245+
if (sorted)
251246
node.addPreSortedChild(childNode);
252-
} else {
253-
node.addAndSortChild(childNode);
254-
}
247+
else
248+
node.addPreSortedChild(childNode, -(index + 1));
255249
}
256250

257251
// Prepare for next directory path entry.
@@ -272,22 +266,39 @@ else if (path.typeIdMatch(node.getValue())) {
272266
}
273267

274268
// Check if already inserted.
275-
ObservableList<TreeItem<PathNode<?>>> children;
276-
if (node instanceof FilterableTreeItem<?> filterableNode)
277-
children = Unchecked.cast(filterableNode.getSourceChildren());
278-
else
279-
children = node.getChildren();
280-
for (TreeItem<PathNode<?>> child : children)
281-
if (path.equals(child.getValue()))
282-
return (WorkspaceTreeNode) child;
269+
ObservableList<WorkspaceTreeNode> children = Unchecked.cast(node.getSourceChildren());
270+
WorkspaceTreeNode inserted;//= new WorkspaceTreeNode(path);
271+
int index = binaryUnboxingSearch(children, path);
272+
if (index >= 0)
273+
return children.get(index);
274+
inserted = new WorkspaceTreeNode(path);
283275

284276
// Not already inserted, create a new node and insert it.
285-
WorkspaceTreeNode inserted = new WorkspaceTreeNode(path);
286-
if (sorted) {
277+
if (sorted)
287278
node.addPreSortedChild(inserted);
288-
} else {
289-
node.addAndSortChild(inserted);
290-
}
279+
else
280+
node.addPreSortedChild(inserted, -(index + 1));
281+
291282
return inserted;
292283
}
284+
285+
/**
286+
* Binary search but using {@link PathNode#localCompare(software.coley.recaf.path.PathNode)}
287+
* rather than the standard {@link java.lang.Comparable#compareTo(java.lang.Object)}.
288+
*/
289+
private static int binaryUnboxingSearch(@Nonnull List<WorkspaceTreeNode> items, @Nonnull PathNode<?> target) {
290+
int first = 0;
291+
int last = items.size() - 1;
292+
while (first <= last) {
293+
int middle = (first + last) >>> 1;
294+
int compResult = items.get(middle).getValue().localCompare(target);
295+
if (compResult < 0)
296+
first = middle + 1;
297+
else if (compResult > 0)
298+
last = middle - 1;
299+
else
300+
return middle;
301+
}
302+
return -(first + 1);
303+
}
293304
}

0 commit comments

Comments
 (0)