Skip to content

Commit 4f09d0a

Browse files
authored
Merge pull request eXist-db#6264 from joewiz/feature/performance-optimizations
[optimize] XQuery local-variable resolution and dead cache removal
2 parents 1de148b + 49cb746 commit 4f09d0a

4 files changed

Lines changed: 65 additions & 42 deletions

File tree

exist-core/src/main/java/org/exist/xquery/LocalVariable.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,22 @@ public class LocalVariable extends VariableImpl {
3535
protected LocalVariable before = null;
3636
protected LocalVariable after = null;
3737

38+
/**
39+
* For O(1) variable resolution: the previous LocalVariable with the same
40+
* {@link #getQName()} that was the topmost binding before this one was
41+
* declared. Used by {@link XQueryContext#popLocalVariables} to restore the
42+
* shadowed binding when this variable goes out of scope.
43+
*/
44+
LocalVariable prevSameName = null;
45+
46+
/**
47+
* The {@link #contextStack} marker that was on top when this variable was
48+
* declared. A variable is visible in the current scope iff
49+
* {@code markedUnder == contextStack.peek()}; the previous linked-list
50+
* walk used to express this by stopping at {@code contextStack.peek()}.
51+
*/
52+
LocalVariable markedUnder = null;
53+
3854
public LocalVariable(QName qname) {
3955
super(qname);
4056
}

exist-core/src/main/java/org/exist/xquery/LocationStep.java

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,6 @@ public class LocationStep extends Step {
5757
protected UpdateListener listener = null;
5858
protected Expression parent = null;
5959

60-
// Fields for caching the last result
61-
protected CachedResult cached = null;
62-
6360
//private int parentDeps = Dependency.UNKNOWN_DEPENDENCY;
6461
private boolean preloadedData = false;
6562
protected boolean optimized = false;
@@ -350,30 +347,6 @@ public Sequence eval(Sequence contextSequence, final Item contextItem)
350347
if (contextItem != null) {
351348
contextSequence = contextItem.toSequence();
352349
}
353-
/*
354-
* if(contextSequence == null) //Commented because this the high level
355-
* result nodeset is *really* null result = NodeSet.EMPTY_SET; //Try to
356-
* return cached results else
357-
*/
358-
// TODO: disabled cache for now as it may cause concurrency issues
359-
// better use compile-time inspection and maybe a pragma to mark those
360-
// sections in the query that can be safely cached
361-
// if (cached != null && cached.isValid(contextSequence, contextItem)) {
362-
//
363-
// // WARNING : commented since predicates are *also* applied below !
364-
// // -pb
365-
// /*
366-
// * if (predicates.size() > 0) { applyPredicate(contextSequence,
367-
// * cached.getResult()); } else {
368-
// */
369-
// result = cached.getResult();
370-
// if (context.getProfiler().isEnabled()) {
371-
// LOG.debug("Using cached results");
372-
// }
373-
// context.getProfiler().message(this, Profiler.OPTIMIZATIONS,
374-
// "Using cached results", result);
375-
//
376-
// // }
377350

378351
Sequence result;
379352
if (needsComputation()) {
@@ -455,11 +428,8 @@ public Sequence eval(Sequence contextSequence, final Item contextItem)
455428
} else {
456429
result = NodeSet.EMPTY_SET;
457430
}
458-
// Caches the result
459431
if (axis != Constants.SELF_AXIS && contextSequence != null
460432
&& contextSequence.isCacheable()) {
461-
// TODO : cache *after* removing duplicates ? -pb
462-
cached = new CachedResult(contextSequence, contextItem, result);
463433
registerUpdateListener();
464434
}
465435
// Remove duplicate nodes
@@ -1214,7 +1184,6 @@ protected void registerUpdateListener() {
12141184
listener = new UpdateListener() {
12151185
@Override
12161186
public void documentUpdated(final DocumentImpl document, final int event) {
1217-
cached = null;
12181187
if (document == null || event == UpdateListener.ADD || event == UpdateListener.REMOVE) {
12191188
// clear all
12201189
currentDocs = null;
@@ -1272,7 +1241,6 @@ public void resetState(final boolean postOptimization) {
12721241
currentSet = null;
12731242
currentDocs = null;
12741243
optimized = false;
1275-
cached = null;
12761244
listener = null;
12771245
}
12781246
}

exist-core/src/main/java/org/exist/xquery/UserDefinedFunction.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,17 @@
2828
import org.exist.xquery.value.Sequence;
2929

3030
import java.util.ArrayList;
31+
import java.util.HashSet;
3132
import java.util.List;
33+
import java.util.Set;
3234

3335
/**
3436
* @author wolf
3537
*/
3638
public class UserDefinedFunction extends Function implements Cloneable {
3739

3840
private final List<QName> parameters = new ArrayList<>(5);
41+
private final Set<QName> parameterNames = new HashSet<>();
3942
protected boolean visited = false;
4043
private Expression body;
4144
private Sequence[] currentArguments = null;
@@ -67,7 +70,7 @@ public void addVariable(final String varName) throws XPathException {
6770
}
6871

6972
public void addVariable(QName varName) throws XPathException {
70-
if (parameters.contains(varName)) {
73+
if (!parameterNames.add(varName)) {
7174
throw new XPathException(this, ErrorCodes.XQST0039, "function " + getName() + " already has a parameter with the name " + varName);
7275
}
7376

exist-core/src/main/java/org/exist/xquery/XQueryContext.java

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,17 @@ public class XQueryContext implements BinaryValueManager, Context {
187187
// The last element in the linked list of local in-scope variables
188188
private LocalVariable lastVar = null;
189189

190+
/**
191+
* O(1) lookup table from QName to the most-recently-declared LocalVariable
192+
* with that name. Maintained alongside the {@link #lastVar} linked list:
193+
* declareVariableBinding adds, popLocalVariables restores the prevSameName
194+
* chain. Visibility is enforced in resolveLocalVariable by comparing
195+
* {@link LocalVariable#markedUnder} to {@code contextStack.peek()}.
196+
*/
197+
// Shared by reference with copies of this context (see copyFields,
198+
// updateContext) just like {@link #contextStack} and {@link #lastVar}.
199+
private Map<QName, LocalVariable> localVariableLookup = new HashMap<>();
200+
190201
private Deque<LocalVariable> contextStack = new ArrayDeque<>();
191202

192203
private final Deque<FunctionSignature> callStack = new ArrayDeque<>();
@@ -657,6 +668,7 @@ public void updateContext(final XQueryContext from) {
657668
this.watchdog = from.watchdog;
658669
this.lastVar = from.lastVar;
659670
this.contextStack = from.contextStack;
671+
this.localVariableLookup = from.localVariableLookup;
660672
this.inScopeNamespaces = from.inScopeNamespaces;
661673
this.inScopePrefixes = from.inScopePrefixes;
662674
this.inheritedInScopeNamespaces = from.inheritedInScopeNamespaces;
@@ -727,6 +739,7 @@ protected void copyFields(final XQueryContext ctx) {
727739
ctx.lastVar = this.lastVar;
728740
ctx.variableStackSize = getCurrentStackSize();
729741
ctx.contextStack = this.contextStack;
742+
ctx.localVariableLookup = this.localVariableLookup;
730743
ctx.staticNamespaces = new HashMap<>(this.staticNamespaces);
731744
ctx.staticPrefixes = new HashMap<>(this.staticPrefixes);
732745

@@ -1459,6 +1472,7 @@ public void reset(final boolean keepGlobals) {
14591472

14601473
if (!isShared) {
14611474
lastVar = null;
1475+
localVariableLookup.clear();
14621476
}
14631477

14641478
// clear inline functions using closures
@@ -1924,6 +1938,8 @@ public LocalVariable declareVariableBinding(final LocalVariable var) throws XPat
19241938
}
19251939
lastVar = var;
19261940
var.setStackPosition(getCurrentStackSize());
1941+
var.markedUnder = contextStack.peek();
1942+
var.prevSameName = localVariableLookup.put(var.getQName(), var);
19271943
return var;
19281944
}
19291945

@@ -2057,16 +2073,19 @@ Variable resolveGlobalVariable(final QName qname) {
20572073
}
20582074

20592075
protected Variable resolveLocalVariable(final QName qname) throws XPathException {
2060-
final LocalVariable end = contextStack.peek();
2061-
for (LocalVariable var = lastVar; var != null; var = var.before) {
2062-
if (var == end) {
2063-
return null;
2064-
}
2065-
if (qname.equals(var.getQName())) {
2066-
return var;
2067-
}
2076+
// O(1) fast path. The linked-list walk previously here is O(N) per
2077+
// call and O(N²) when a body of N variables is analyzed.
2078+
final LocalVariable var = localVariableLookup.get(qname);
2079+
if (var == null) {
2080+
return null;
20682081
}
2069-
return null;
2082+
// Visibility: var is visible if it was declared under the current
2083+
// contextStack mark — the same boundary the linked-list walk used to
2084+
// express by stopping at {@code contextStack.peek()}.
2085+
if (var.markedUnder != contextStack.peek()) {
2086+
return null;
2087+
}
2088+
return var;
20702089
}
20712090

20722091
/**
@@ -2513,10 +2532,27 @@ public void popLocalVariables(@Nullable final LocalVariable var) {
25132532
/**
25142533
* Restore the local variable stack to the position marked by variable var.
25152534
*
2535+
* <p>Walks {@link #lastVar} backward to {@code var} (or to the start when
2536+
* {@code var} is {@code null}), unwinding each variable's
2537+
* {@code prevSameName} chain into {@link #localVariableLookup} in
2538+
* REVERSE-of-declaration order so that names with multiple bindings in the
2539+
* popped scope settle on the still-visible binding, not on a popped one.
2540+
*
25162541
* @param var only clear variables after this variable, or null
25172542
* @param resultSeq the result sequence
25182543
*/
25192544
public void popLocalVariables(@Nullable final LocalVariable var, @Nullable final Sequence resultSeq) {
2545+
for (LocalVariable cursor = lastVar; cursor != null && cursor != var; cursor = cursor.before) {
2546+
if (localVariableLookup.get(cursor.getQName()) == cursor) {
2547+
if (cursor.prevSameName != null) {
2548+
localVariableLookup.put(cursor.getQName(), cursor.prevSameName);
2549+
} else {
2550+
localVariableLookup.remove(cursor.getQName());
2551+
}
2552+
}
2553+
cursor.prevSameName = null;
2554+
}
2555+
25202556
if (var != null) {
25212557
// clear all variables registered after var. they should be out of scope.
25222558
LocalVariable outOfScope = var.after;

0 commit comments

Comments
 (0)