package org.commcare.engine.cases; import android.util.Log; import org.commcare.android.database.user.models.ACase; import org.commcare.cases.instance.CaseInstanceTreeElement; import org.commcare.cases.model.Case; import org.commcare.cases.query.IndexedSetMemberLookup; import org.commcare.cases.query.IndexedValueLookup; import org.commcare.cases.query.PredicateProfile; import org.commcare.cases.query.QueryContext; import org.commcare.cases.query.QueryPlanner; import org.commcare.cases.query.handlers.ModelQueryLookupHandler; import org.commcare.cases.query.queryset.CaseModelQuerySetMatcher; import org.commcare.models.database.SqlStorage; import org.commcare.models.database.user.models.AndroidCaseIndexTable; import org.commcare.modern.engine.cases.CaseGroupResultCache; import org.commcare.modern.engine.cases.CaseIndexQuerySetTransform; import org.commcare.modern.engine.cases.query.CaseIndexPrefetchHandler; import org.javarosa.core.model.instance.AbstractTreeElement; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.model.trace.EvaluationTrace; import org.javarosa.core.model.utils.CacheHost; import org.javarosa.core.services.storage.IStorageIterator; import org.javarosa.core.services.storage.IStorageUtilityIndexed; import org.javarosa.core.util.DataUtil; import java.util.Collection; import java.util.Hashtable; import java.util.LinkedHashSet; import java.util.Vector; /** * @author ctsims */ public class AndroidCaseInstanceTreeElement extends CaseInstanceTreeElement implements CacheHost { private static final String TAG = AndroidCaseInstanceTreeElement.class.getSimpleName(); private final AndroidCaseIndexTable mCaseIndexTable; private final Hashtable<Integer, Integer> multiplicityIdMapping = new Hashtable<>(); //We're storing this here for now because this is a safe lifecycle object that must represent //a single snapshot of the case database, but it could be generalized later. private Hashtable<String, LinkedHashSet<Integer>> mIndexCache = new Hashtable<>(); //TODO: Document private Hashtable<String, LinkedHashSet<Integer>> mPairedIndexCache = new Hashtable<>(); private String[][] mMostRecentBatchFetch = null; public AndroidCaseInstanceTreeElement(AbstractTreeElement instanceRoot, SqlStorage<ACase> storage) { this(instanceRoot, storage, new AndroidCaseIndexTable()); } public AndroidCaseInstanceTreeElement(AbstractTreeElement instanceRoot, SqlStorage<ACase> storage, AndroidCaseIndexTable caseIndexTable) { super(instanceRoot, storage); mCaseIndexTable = caseIndexTable; } @Override protected void initBasicQueryHandlers(QueryPlanner queryPlanner) { super.initBasicQueryHandlers(queryPlanner); queryPlanner.addQueryHandler(new CaseIndexPrefetchHandler(mCaseIndexTable)); CaseModelQuerySetMatcher matcher = new CaseModelQuerySetMatcher(multiplicityIdMapping); matcher.addQuerySetTransform(new CaseIndexQuerySetTransform(mCaseIndexTable)); queryPlanner.addQueryHandler(new ModelQueryLookupHandler(matcher)); } @Override protected synchronized void loadElements() { if (elements != null) { return; } elements = new Vector<>(); Log.d(TAG, "Getting Cases!"); long timeInMillis = System.currentTimeMillis(); int mult = 0; for (IStorageIterator i = ((SqlStorage<ACase>)storage).iterate(false); i.hasMore(); ) { int id = i.nextID(); elements.add(buildElement(this, id, null, mult)); objectIdMapping.put(DataUtil.integer(id), DataUtil.integer(mult)); multiplicityIdMapping.put(DataUtil.integer(mult), DataUtil.integer(id)); mult++; } long value = System.currentTimeMillis() - timeInMillis; Log.d(TAG, "Case iterate took: " + value + "ms"); } @Override protected Collection<Integer> getNextIndexMatch(Vector<PredicateProfile> profiles, IStorageUtilityIndexed<?> storage, QueryContext currentQueryContext) { //If the index object starts with "case-in-" it's actually a case index query and we need to run //this over the case index table String firstKey = profiles.elementAt(0).getKey(); if (firstKey.startsWith(Case.INDEX_CASE_INDEX_PRE)) { return performCaseIndexQuery(firstKey, profiles); } //Otherwise see how many of these we can bulk process int numKeys; for (numKeys = 0; numKeys < profiles.size(); ++numKeys) { //If the current key is an index fetch, we actually can't do it in bulk, //so we need to stop if (profiles.elementAt(numKeys).getKey().startsWith(Case.INDEX_CASE_INDEX_PRE) || !(profiles.elementAt(numKeys) instanceof IndexedValueLookup)) { break; } //otherwise, it's now in our queue } SqlStorage<ACase> sqlStorage = ((SqlStorage<ACase>)storage); String[] namesToMatch = new String[numKeys]; String[] valuesToMatch = new String[numKeys]; String cacheKey = ""; String keyDescription =""; for (int i = numKeys - 1; i >= 0; i--) { namesToMatch[i] = profiles.elementAt(i).getKey(); valuesToMatch[i] = (String) (((IndexedValueLookup)profiles.elementAt(i)).value); cacheKey += "|" + namesToMatch[i] + "=" + valuesToMatch[i]; keyDescription += namesToMatch[i] + "|"; } mMostRecentBatchFetch = new String[2][]; mMostRecentBatchFetch[0] = namesToMatch; mMostRecentBatchFetch[1] = valuesToMatch; LinkedHashSet<Integer> ids; if(mPairedIndexCache.containsKey(cacheKey)) { ids = mPairedIndexCache.get(cacheKey); } else { EvaluationTrace trace = new EvaluationTrace("Case Storage Lookup" + "["+keyDescription + "]"); ids = new LinkedHashSet<>(); sqlStorage.getIDsForValues(namesToMatch, valuesToMatch, ids); trace.setOutcome("Results: " + ids.size()); currentQueryContext.reportTrace(trace); mPairedIndexCache.put(cacheKey, ids); } if(ids.size() > 50 && ids.size() < CaseGroupResultCache.MAX_PREFETCH_CASE_BLOCK) { CaseGroupResultCache cue = currentQueryContext.getQueryCache(CaseGroupResultCache.class); cue.reportBulkCaseBody(cacheKey, ids); } //Ok, we matched! Remove all of the keys that we matched for (int i = 0; i < numKeys; ++i) { profiles.removeElementAt(0); } return ids; } private LinkedHashSet<Integer> performCaseIndexQuery(String firstKey, Vector<PredicateProfile> optimizations) { //CTS - March 9, 2015 - Introduced a small cache for child index queries here because they //are a frequent target of bulk operations like graphing which do multiple requests across the //same query. PredicateProfile op = optimizations.elementAt(0); //TODO: This should likely be generalized for a number of other queries with bulk/nodeset //returns String indexName = firstKey.substring(Case.INDEX_CASE_INDEX_PRE.length()); String indexCacheKey = null; LinkedHashSet<Integer> matchingCases = null; if (op instanceof IndexedValueLookup) { IndexedValueLookup iop = (IndexedValueLookup)op; String value = (String)iop.value; //TODO: Evaluate whether our indices could contain "|" but I don't imagine how they could. indexCacheKey = indexName + "|" + value; //Check whether we've got a cache of this index. if (mIndexCache.containsKey(indexCacheKey)) { //remove the match from the inputs optimizations.removeElementAt(0); ; return mIndexCache.get(indexCacheKey); } matchingCases = mCaseIndexTable.getCasesMatchingIndex(indexName, value); } if (op instanceof IndexedSetMemberLookup) { IndexedSetMemberLookup sop = (IndexedSetMemberLookup)op; matchingCases = mCaseIndexTable.getCasesMatchingValueSet(indexName, sop.valueSet); } //Clear the most recent index and wipe it, because there is no way it is going to be useful //after this mMostRecentBatchFetch = new String[2][]; //remove the match from the inputs optimizations.removeElementAt(0); if (indexCacheKey != null) { //For now we're only going to run this on very small data sets because we don't //want to manage this too explicitly until we generalize. Almost all results here //will be very very small either way (~O(10's of cases)), so given that this only //exists across one session that won't get out of hand if (matchingCases.size() < 50) { //Should never hit this, but don't wanna have any runaway memory if we do. if (mIndexCache.size() > 100) { mIndexCache.clear(); } mIndexCache.put(indexCacheKey, matchingCases); } } return matchingCases; } @Override public String getCacheIndex(TreeReference ref) { //NOTE: there's no evaluation here as to whether the ref is suitable //we only follow one pattern for now and it's evaluated below. loadElements(); //Testing - Don't bother actually seeing whether this fits int i = ref.getMultiplicity(1); if (i != -1) { Integer val = this.multiplicityIdMapping.get(DataUtil.integer(i)); if (val == null) { return null; } else { return val.toString(); } } return null; } @Override public boolean isReferencePatternCachable(TreeReference ref) { //we only support one pattern here, a raw, qualified //reference to an element at the case level with no //predicate support. The ref basically has to be a raw //pointer to one of this instance's children if (!ref.isAbsolute()) { return false; } if (ref.hasPredicates()) { return false; } if (ref.size() != 2) { return false; } if (!"casedb".equalsIgnoreCase(ref.getName(0))) { return false; } if (!"case".equalsIgnoreCase(ref.getName(1))) { return false; } return ref.getMultiplicity(1) >= 0; } @Override public String[][] getCachePrimeGuess() { return mMostRecentBatchFetch; } @Override protected Case getElement(int recordId, QueryContext context) { if(context == null) { return super.getElement(recordId, context); } CaseGroupResultCache caseGroupCache = context.getQueryCacheOrNull(CaseGroupResultCache.class); if(caseGroupCache != null && caseGroupCache.hasMatchingCaseSet(recordId)) { if(!caseGroupCache.isLoaded(recordId)) { EvaluationTrace loadTrace = new EvaluationTrace("Bulk Case Load"); SqlStorage<ACase> sqlStorage = ((SqlStorage<ACase>)storage); LinkedHashSet<Integer> body = caseGroupCache.getTranche(recordId); sqlStorage.bulkRead(body, caseGroupCache.getLoadedCaseMap()); loadTrace.setOutcome("Loaded: " + body.size()); context.reportTrace(loadTrace); } return caseGroupCache.getLoadedCase(recordId); } return super.getElement(recordId, context); } }