Skip to content

Commit

Permalink
Merge pull request #715 from seadowg/repeated-expr-2
Browse files Browse the repository at this point in the history
Use lazy indexing repeated expression caching to optimize predicates
  • Loading branch information
lognaturel authored May 17, 2023
2 parents 4297022 + f08c2b3 commit 34f4cbd
Show file tree
Hide file tree
Showing 17 changed files with 1,037 additions and 153 deletions.
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);
}
}

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

0 comments on commit 34f4cbd

Please sign in to comment.