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

Cache repeated predicate evaluations during triggers #713

Merged
merged 19 commits into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ if (!project.hasProperty("android")) {
// Be sure to update version in pom.xml to match
// snapshot release = x.x.x-SNAPSHOT
// production release = x.x.x
version = '4.1.0'
version = '4.2.0-SNAPSHOT'
archiveName = baseName + '-' + version + '.jar'

manifest {
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<!-- Be sure to update version in build.gradle to match -->
<!-- snapshot release = x.x.x-SNAPSHOT -->
<!-- production release = x.x.x -->
<version>4.1.0</version>
<version>4.2.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>javarosa</name>
<description>A Java library for rendering forms that are compliant with ODK XForms spec</description>
Expand Down
18 changes: 10 additions & 8 deletions src/main/java/org/javarosa/core/model/FormDef.java
Original file line number Diff line number Diff line change
Expand Up @@ -745,20 +745,16 @@ public boolean evaluateConstraint(TreeReference ref, IAnswerData data) {
}

private void resetEvaluationContext() {
EvaluationContext ec = new EvaluationContext(null);
ec = new EvaluationContext(mainInstance, getFormInstances(), ec);
initEvalContext(ec);
this.exprEvalContext = ec;
this.exprEvalContext = initEvalContext();
}

public EvaluationContext getEvaluationContext() {
return this.exprEvalContext;
}

/**
* @param ec The new Evaluation Context
*/
private void initEvalContext(EvaluationContext ec) {
private EvaluationContext initEvalContext() {
EvaluationContext ec = new EvaluationContext(mainInstance, getFormInstances(), new EvaluationContext(null));

if (!ec.getFunctionHandlers().containsKey("jr:itext")) {
final FormDef f = this;
ec.addFunctionHandler(new IFunctionHandler() {
Expand Down Expand Up @@ -917,6 +913,8 @@ public boolean realTime() {
}
});
}

return ec;
}

public String fillTemplateString(String template, TreeReference contextRef) {
Expand Down Expand Up @@ -1705,4 +1703,8 @@ public HashMap<String, DataInstance> getFormInstances() {
public Extras<Externalizable> getExtras() {
return extras;
}

public void disablePredicateCaching() {
dagImpl.disablePredicateCaching();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.javarosa.core.model;

import org.javarosa.core.model.condition.PredicateCache;
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;

/**
* In memory implementation of a {@link PredicateCache}. Cannot cache predicate evaluations that contain a
* non-idempotent function.
*/
public class IdempotentInMemPredicateCache implements PredicateCache {

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

@Override
@NotNull
public List<TreeReference> get(TreeReference nodeSet, XPathExpression predicate, Supplier<List<TreeReference>> onMiss) {
String key = getKey(nodeSet, predicate);

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

return references;
}
}

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

private boolean isCacheable(XPathExpression predicate) {
return predicate.isIdempotent();
}
}
42 changes: 27 additions & 15 deletions src/main/java/org/javarosa/core/model/TriggerableDag.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,6 @@

package org.javarosa.core.model;

import static java.util.Collections.emptySet;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.javarosa.core.model.condition.Condition;
import org.javarosa.core.model.condition.EvaluationContext;
import org.javarosa.core.model.condition.Recalculate;
Expand All @@ -49,6 +35,21 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static java.util.Collections.emptySet;

public class TriggerableDag {
private static final Logger logger = LoggerFactory.getLogger(TriggerableDag.class);

Expand Down Expand Up @@ -95,6 +96,8 @@ public interface EventNotifierAccessor {
*/
private Map<TreeReference, QuickTriggerable> relevancePerRepeat = new HashMap<>();

private boolean predicateCaching = true;

TriggerableDag(EventNotifierAccessor accessor) {
this.accessor = accessor;
}
Expand Down Expand Up @@ -513,12 +516,18 @@ private Set<QuickTriggerable> getAllToTrigger(Set<QuickTriggerable> cascadeRoots
private Set<QuickTriggerable> doEvaluateTriggerables(FormInstance mainInstance, EvaluationContext evalContext, Set<QuickTriggerable> toTrigger,
TreeReference changedRef, Set<QuickTriggerable> affectAllRepeatInstances, Set<QuickTriggerable> alreadyEvaluated) {
Set<QuickTriggerable> evaluated = new HashSet<>();
EvaluationContext context;
if (predicateCaching) {
context = new EvaluationContext(evalContext, new IdempotentInMemPredicateCache());
} else {
context = evalContext;
}

// Evaluate the provided set of triggerables in the order they appear
// in the sorted DAG to ensure the correct sequence of evaluations
for (QuickTriggerable qt : triggerablesDAG)
if (toTrigger.contains(qt) && !alreadyEvaluated.contains(qt)) {
evaluateTriggerable(mainInstance, evalContext, qt, affectAllRepeatInstances.contains(qt), changedRef);
evaluateTriggerable(mainInstance, context, qt, affectAllRepeatInstances.contains(qt), changedRef);

evaluated.add(qt);
}
Expand Down Expand Up @@ -725,4 +734,7 @@ private List<Recalculate> getRecalculates() {

// endregion

public void disablePredicateCaching() {
this.predicateCaching = false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,26 @@

package org.javarosa.core.model.condition;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import org.javarosa.core.model.data.IAnswerData;
import org.javarosa.core.model.instance.AbstractTreeElement;
import org.javarosa.core.model.instance.DataInstance;
import org.javarosa.core.model.instance.TreeElement;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.measure.Measure;
import org.javarosa.xpath.IExprDataType;
import org.javarosa.xpath.expr.XPathExpression;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;

/**
* A collection of objects that affect the evaluation of an expression, like
* function handlers and (not supported) variable bindings.
*/
public class EvaluationContext {

/**
* Unambiguous anchor reference for relative paths
*/
Expand Down Expand Up @@ -60,6 +63,9 @@ public class EvaluationContext {
private DataInstance instance;
private int[] predicateEvaluationProgress;

private PredicateCache predicateCache = ((reference, predicate, onMiss) -> onMiss.get());


/**
* Copy Constructor
**/
Expand All @@ -84,6 +90,13 @@ private EvaluationContext(EvaluationContext base) {
//and is fixed on the context. Anything that changes the context should
//invalidate this
currentContextPosition = base.currentContextPosition;

predicateCache = base.predicateCache;
}

public EvaluationContext(EvaluationContext base, PredicateCache predicateCache) {
this(base);
this.predicateCache = predicateCache;
}

public EvaluationContext(EvaluationContext base, TreeReference context) {
Expand Down Expand Up @@ -309,23 +322,36 @@ private void expandReferenceAccumulator(TreeReference sourceRef, DataInstance so
}

if (predicates != null) {
TreeReference nodeSetRef = workingRef.clone();
nodeSetRef.add(name, -1);

boolean firstTime = true;
List<TreeReference> passed = new ArrayList<TreeReference>(treeReferences.size());
for (XPathExpression xpe : predicates) {
for (int i = 0; i < treeReferences.size(); ++i) {
//if there are predicates then we need to see if e.nextElement meets the standard of the predicate
TreeReference treeRef = treeReferences.get(i);

//test the predicate on the treeElement
EvaluationContext evalContext = rescope(treeRef, (firstTime ? treeRef.getMultLast() : i));
Object o = xpe.eval(sourceInstance, evalContext);
if (o instanceof Boolean) {
boolean testOutcome = (Boolean) o;
if (testOutcome) {
passed.add(treeRef);
boolean firstTimeCapture = firstTime;
lognaturel marked this conversation as resolved.
Show resolved Hide resolved
passed.addAll(predicateCache.get(nodeSetRef, xpe, () -> {
List<TreeReference> predicatePassed = new ArrayList<>(treeReferences.size());
for (int i = 0; i < treeReferences.size(); ++i) {
//if there are predicates then we need to see if e.nextElement meets the standard of the predicate
TreeReference treeRef = treeReferences.get(i);

//test the predicate on the treeElement
EvaluationContext evalContext = rescope(treeRef, (firstTimeCapture ? treeRef.getMultLast() : i));

Measure.log("PredicateEvaluations");
Object o = xpe.eval(sourceInstance, evalContext);

if (o instanceof Boolean) {
boolean testOutcome = (Boolean) o;
if (testOutcome) {
predicatePassed.add(treeRef);
}
}
}
}

return predicatePassed;
}));

firstTime = false;
treeReferences.clear();
treeReferences.addAll(passed);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.javarosa.core.model.condition;

import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.xpath.expr.XPathExpression;
import org.jetbrains.annotations.NotNull;

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

/**
* Allows the result of predicate evaluations (references for matching nodes) to be cached. The cache doesn't know
* anything about the values that might be referenced in a predicate (the "triggerables"), so can only be used in cases
* where the predicates are "static".
*/
public interface PredicateCache {

@NotNull
List<TreeReference> get(TreeReference nodeSet, XPathExpression predicate, Supplier<List<TreeReference>> onMiss);
}
4 changes: 4 additions & 0 deletions src/main/java/org/javarosa/form/api/FormEntryController.java
Original file line number Diff line number Diff line change
Expand Up @@ -345,4 +345,8 @@ private static FormIndex getRepeatGroupIndex(FormIndex index, FormDef formDef) {
}
}
}

public void disablePredicateCaching() {
Copy link
Member Author

Choose a reason for hiding this comment

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

This will let Collect (or any other client) present this as an opt-in feature.

model.getForm().disablePredicateCaching();
}
}
47 changes: 47 additions & 0 deletions src/main/java/org/javarosa/measure/Measure.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.javarosa.measure;

import java.util.HashMap;
import java.util.Map;

public class Measure {

private static final Map<String, Integer> counts = new HashMap<>();
private static boolean measuring;

private Measure() {

}

public static int withMeasure(String event, Runnable work) {
start();
work.run();
int count = getCount(event);
start();

return count;
}

public static void log(String event) {
if (!measuring) return;

if (!counts.containsKey(event)) {
counts.put(event, 0);
}

counts.put(event, counts.get(event) + 1);
}

private static void start() {
counts.clear();
measuring = true;
}

private static void stop() {
counts.clear();
measuring = false;
}

private static int getCount(String event) {
return counts.get(event);
}
}
5 changes: 5 additions & 0 deletions src/main/java/org/javarosa/xpath/expr/XPathArithExpr.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public Object eval (DataInstance model, EvaluationContext evalContext) {
return new Double(result);
}

@Override
public boolean isIdempotent() {
return a.isIdempotent() && b.isIdempotent();
}

public String toString () {
String sOp = null;

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/javarosa/xpath/expr/XPathBoolExpr.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public Object eval (DataInstance model, EvaluationContext evalContext) {
return new Boolean(result);
}

@Override
public boolean isIdempotent() {
return a.isIdempotent() && b.isIdempotent();
}

public String toString () {
String sOp = null;

Expand Down
Loading