Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use lazy indexing repeated expression caching to optimize predicates #715

Merged
merged 47 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
68799cb
Remove potentially unused code
seadowg Apr 11, 2023
5c35208
Extract predicate filtering to a method
seadowg Apr 11, 2023
d7c37b2
Use chained approach for caching implemenation
seadowg Apr 11, 2023
5b61f24
Simplify chain with recursive implementation
seadowg Apr 11, 2023
e88f90d
Allow chain to built up rather than set
seadowg Apr 11, 2023
beb837f
Use lambda instead of anonymous
seadowg Apr 11, 2023
a9cf53f
Implement limited between answer predicate caching
seadowg Apr 11, 2023
dbe239c
Add failing test for case where relative expression is on right hand …
seadowg Apr 11, 2023
b069f4c
Only allow expressions where the left side is relative
seadowg Apr 11, 2023
c8f0d1f
Rename param
seadowg Apr 12, 2023
a8dc7bd
Correct test form
seadowg Apr 12, 2023
890454d
Add stub for potentially failing case
seadowg Apr 12, 2023
8447ba3
Add failing test for cmatching predicates that come after different ones
seadowg Apr 13, 2023
5884aeb
Add check for potential regression
seadowg Apr 13, 2023
d50cbfc
Disable predicate caching for multiple predicates for the moment
seadowg Apr 13, 2023
de743ff
Add stub failing tests for other scenarios we want to make sure are o…
seadowg Apr 13, 2023
fbb0a14
Rewrite tests with Scenario DSL
seadowg Apr 13, 2023
4816c1b
Add passing test for comparison case
seadowg Apr 13, 2023
ab7226f
Add test for failing comp case
seadowg Apr 13, 2023
fc9b983
Support caching for cmp predicates
seadowg Apr 13, 2023
ae070b5
Add test for func case
seadowg Apr 17, 2023
b543f38
Evaluate eq predicates with index
seadowg Apr 17, 2023
3399e34
Split indexing caching out
seadowg Apr 17, 2023
aa9bcf2
Rename classes
seadowg Apr 17, 2023
e559d3e
Pull out expression parsing logic
seadowg Apr 17, 2023
6785e5a
Rename class
seadowg Apr 17, 2023
68c9667
Add failing tests for supporting more expressions
seadowg Apr 17, 2023
140f7cd
Support expressions regardless of direction
seadowg Apr 17, 2023
4fe85ae
Support numeric and string literals for index caching
seadowg Apr 17, 2023
a0c4568
Don't try and index nested nodesets
seadowg Apr 17, 2023
e8cdc1b
Rename class
seadowg Apr 19, 2023
aa21452
Move evaluation logic into class
seadowg May 4, 2023
59cb3f9
Fix different types of eq being confused
seadowg May 5, 2023
2bb898f
Prefer path expressions in regression tests where it's possible
seadowg May 5, 2023
fa1a824
Correct name of test
seadowg May 5, 2023
2753251
Don't apply caching to expressions on main instance
seadowg May 9, 2023
7ded84f
Add caching for first predicate in chain
seadowg May 9, 2023
b496772
Pull indexing data structure code out of predicate filter
seadowg May 9, 2023
3cc5c76
Correct Measure implementation
seadowg May 10, 2023
adc1463
Remove unused method
seadowg May 10, 2023
31fbd2f
Nest interface
seadowg May 12, 2023
fcf5703
Create default filter chain
seadowg May 12, 2023
a30fade
Remove interface that's only used in one class
seadowg May 12, 2023
44d944a
Add ability to add predicate filter
seadowg May 12, 2023
93a6d6c
Improve interface for clients
seadowg May 12, 2023
2dc034d
Move nesting check to EvaluationContext
seadowg May 12, 2023
f08c2b3
Add test to check we aren't increasing load time when indexing
seadowg May 12, 2023
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.javarosa.core.model;

import kotlin.Pair;
import org.javarosa.core.model.condition.EvaluationContext;
import org.javarosa.core.model.instance.DataInstance;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.xpath.expr.XPathCmpExpr;
import org.javarosa.xpath.expr.XPathEqExpr;
import org.javarosa.xpath.expr.XPathExpression;
import org.javarosa.xpath.expr.XPathNumericLiteral;
import org.javarosa.xpath.expr.XPathPathExpr;
import org.javarosa.xpath.expr.XPathStringLiteral;
import org.jetbrains.annotations.Nullable;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;

public class CompareChildToAbsoluteExpression {

private final XPathPathExpr relativeSide;
private final XPathExpression absoluteSide;
private final XPathExpression original;

public CompareChildToAbsoluteExpression(XPathPathExpr relativeSide, XPathExpression absoluteSide, XPathExpression original) {
this.relativeSide = relativeSide;
this.absoluteSide = absoluteSide;
this.original = original;
}

public Object evalRelative(DataInstance sourceInstance, EvaluationContext evaluationContext, TreeReference child, int childIndex) {
EvaluationContext rescopedContext = evaluationContext.rescope(child, childIndex);
return getRelativeSide().eval(sourceInstance, rescopedContext).unpack();
}

public Object evalAbsolute(DataInstance sourceInstance, EvaluationContext evaluationContext) {
if (absoluteSide instanceof XPathPathExpr) {
return ((XPathPathExpr) getAbsoluteSide()).eval(sourceInstance, evaluationContext).unpack();
} else {
return absoluteSide.eval(sourceInstance, evaluationContext);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This accounts for literals? Yes, confirmed through debugging tests.

}
}

public XPathPathExpr getRelativeSide() {
return relativeSide;
}

public XPathExpression getAbsoluteSide() {
return absoluteSide;
}

public XPathExpression getOriginal() {
return original;
}

@Nullable
public static CompareChildToAbsoluteExpression parse(XPathExpression expression) {
XPathExpression a = null;
XPathExpression b = null;

if (expression instanceof XPathCmpExpr) {
a = ((XPathCmpExpr) expression).a;
b = ((XPathCmpExpr) expression).b;
} else if (expression instanceof XPathEqExpr) {
a = ((XPathEqExpr) expression).a;
b = ((XPathEqExpr) expression).b;
}

Pair<XPathPathExpr, XPathExpression> relativeAndAbsolute = getRelativeAndAbsolute(a, b);
if (relativeAndAbsolute != null) {
return new CompareChildToAbsoluteExpression(relativeAndAbsolute.getFirst(), relativeAndAbsolute.getSecond(), expression);
} else {
return null;
}
}

private static Pair<XPathPathExpr, XPathExpression> getRelativeAndAbsolute(XPathExpression a, XPathExpression b) {
XPathPathExpr relative = null;
XPathExpression absolute = null;

Queue<XPathExpression> subExpressions = new LinkedList<>(Arrays.asList(a, b));
while (!subExpressions.isEmpty()) {
XPathExpression subExpression = subExpressions.poll();
if (subExpression instanceof XPathPathExpr && ((XPathPathExpr) subExpression).init_context == XPathPathExpr.INIT_CONTEXT_RELATIVE)
relative = (XPathPathExpr) subExpression;
else if (subExpression instanceof XPathPathExpr && ((XPathPathExpr) subExpression).init_context == XPathPathExpr.INIT_CONTEXT_ROOT) {
absolute = subExpression;
} else if (subExpression instanceof XPathNumericLiteral || subExpression instanceof XPathStringLiteral) {
absolute = subExpression;
}
}

if (relative != null && absolute != null) {
return new Pair<>(relative, absolute);
} else {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.javarosa.core.model;

import org.javarosa.core.model.condition.EvaluationContext;
import org.javarosa.core.model.condition.PredicateFilter;
import org.javarosa.core.model.instance.DataInstance;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.xpath.expr.XPathCmpExpr;
import org.javarosa.xpath.expr.XPathEqExpr;
import org.javarosa.xpath.expr.XPathExpression;
import org.jetbrains.annotations.NotNull;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

/**
* Caches down stream evaluations (in the {@link PredicateFilter} chain) for supported expressions - currently just
* {@link XPathCmpExpr} and {@link XPathEqExpr}. Repeated evaluations are fetched in O(1) time.
*/
public class CompareChildToAbsoluteExpressionFilter implements PredicateFilter {

private final Map<String, List<TreeReference>> cachedEvaluations = new HashMap<>();

@NotNull
@Override
public List<TreeReference> filter(@NotNull DataInstance sourceInstance, @NotNull TreeReference nodeSet, @NotNull XPathExpression predicate, @NotNull List<TreeReference> children, @NotNull EvaluationContext evaluationContext, @NotNull Supplier<List<TreeReference>> next) {
if (sourceInstance.getInstanceId() == null) {
return next.get();
}

CompareChildToAbsoluteExpression candidate = CompareChildToAbsoluteExpression.parse(predicate);
if (candidate != null) {
Object absoluteValue = candidate.evalAbsolute(sourceInstance, evaluationContext);
String key = nodeSet.toString() + predicate + candidate.getRelativeSide() + absoluteValue.toString();

if (cachedEvaluations.containsKey(key)) {
return cachedEvaluations.get(key);
} else {
List<TreeReference> filtered = next.get();
cachedEvaluations.put(key, filtered);
return filtered;
}
} else {
return next.get();
}
}

}
5 changes: 5 additions & 0 deletions src/main/java/org/javarosa/core/model/FormDef.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.javarosa.core.model.condition.EvaluationContext;
import org.javarosa.core.model.condition.IConditionExpr;
import org.javarosa.core.model.condition.IFunctionHandler;
import org.javarosa.core.model.condition.PredicateFilter;
import org.javarosa.core.model.condition.Triggerable;
import org.javarosa.core.model.data.IAnswerData;
import org.javarosa.core.model.data.MultipleItemsData;
Expand Down Expand Up @@ -1707,4 +1708,8 @@ public Extras<Externalizable> getExtras() {
public void disablePredicateCaching() {
dagImpl.disablePredicateCaching();
}

public void addPredicateFilter(PredicateFilter predicateFilter) {
dagImpl.addPredicateFilter(predicateFilter);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.javarosa.core.model;

import org.javarosa.core.model.condition.EvaluationContext;
import org.javarosa.core.model.condition.PredicateFilter;
import org.javarosa.core.model.instance.DataInstance;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.xpath.expr.XPathExpression;
import org.jetbrains.annotations.NotNull;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

/**
* Caches down stream evaluations (in the {@link PredicateFilter} chain) for "idempotent" (with respect to current form
* state) predicates. Can only be used for static instances or in cases where form state won't change - will cause
* clashes otherwise. Repeated evaluations are fetched in O(1) time.
*/
public class IdempotentPredicateCache implements PredicateFilter {

private final Map<String, List<TreeReference>> cachedEvaluations = new HashMap<>();

@NotNull
@Override
public List<TreeReference> filter(@NotNull DataInstance sourceInstance, @NotNull TreeReference nodeSet, @NotNull XPathExpression predicate, @NotNull List<TreeReference> children, @NotNull EvaluationContext evaluationContext, @NotNull Supplier<List<TreeReference>> next) {
String key = getKey(nodeSet, predicate);

if (cachedEvaluations.containsKey(key)) {
return cachedEvaluations.get(key);
} else {
List<TreeReference> filtered = next.get();
if (isCacheable(predicate)) {
cachedEvaluations.put(key, filtered);
}

return filtered;
}
}

private String getKey(TreeReference nodeSet, XPathExpression predicate) {
return nodeSet.toString() + predicate.toString();
}

private boolean isCacheable(XPathExpression predicate) {
return predicate.isIdempotent();
}
}
94 changes: 94 additions & 0 deletions src/main/java/org/javarosa/core/model/IndexPredicateFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.javarosa.core.model;

import org.javarosa.core.model.condition.EvaluationContext;
import org.javarosa.core.model.condition.PredicateFilter;
import org.javarosa.core.model.instance.DataInstance;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.measure.Measure;
import org.javarosa.xpath.expr.XPathEqExpr;
import org.javarosa.xpath.expr.XPathExpression;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

import static java.util.Collections.emptyList;

/**
* Uses a (lazily constructed) index to evaluate a predicate for supported expressions - currently just
* {@link XPathEqExpr} where one side is relative to the instance child being filtered. Evaluations are fetched in
* O(1) time with O(n) expression evaluations only being required the first time a relative side is evaluated.
*/
public class IndexPredicateFilter implements PredicateFilter {

private final InMemTreeReferenceIndex index = new InMemTreeReferenceIndex();

@NotNull
@Override
public List<TreeReference> filter(@NotNull DataInstance sourceInstance, @NotNull TreeReference nodeSet, @NotNull XPathExpression predicate, @NotNull List<TreeReference> children, @NotNull EvaluationContext evaluationContext, @NotNull Supplier<List<TreeReference>> next) {
if (sourceInstance.getInstanceId() == null || !(predicate instanceof XPathEqExpr)) {
return next.get();
}

CompareChildToAbsoluteExpression candidate = CompareChildToAbsoluteExpression.parse(predicate);
if (candidate != null) {
XPathEqExpr original = (XPathEqExpr) candidate.getOriginal();
if (original.isEqual()) {
String section = sourceInstance.getInstanceId() + nodeSet + candidate.getRelativeSide().toString();
if (!index.contains(section)) {
buildIndex(sourceInstance, candidate, children, evaluationContext, section);
}

Object absoluteValue = candidate.evalAbsolute(sourceInstance, evaluationContext);
return index.lookup(section, absoluteValue.toString());
} else {
return next.get();
}
} else {
return next.get();
}
}

private void buildIndex(DataInstance sourceInstance, CompareChildToAbsoluteExpression predicate, List<TreeReference> children, EvaluationContext evaluationContext, String section) {
for (int i = 0; i < children.size(); i++) {
TreeReference child = children.get(i);

Measure.log("IndexEvaluation");
String relativeValue = predicate.evalRelative(sourceInstance, evaluationContext, child, i).toString();
index.add(section, relativeValue, child);
}
}

private static class InMemTreeReferenceIndex {

private final Map<String, Map<String, List<TreeReference>>> map = new HashMap<>();

public boolean contains(String section) {
return map.containsKey(section);
}

public void add(String section, String key, TreeReference reference) {
if (!map.containsKey(section)) {
map.put(section, new HashMap<>());
}

Map<String, List<TreeReference>> sectionMap = map.get(section);
if (!sectionMap.containsKey(key)) {
sectionMap.put(key, new ArrayList<>());
}

sectionMap.get(key).add(reference);
}

public List<TreeReference> lookup(String section, String key) {
if (map.containsKey(section) && map.get(section).containsKey(key)) {
return map.get(section).get(key);
} else {
return emptyList();
}
}
}
}
Loading