Wrap caching FFI functions in Android Java library.

`CacheDirection` enum is used only on the Android side to provide a usable interface. FFI calls are more explicit.

Tests ensure that a cached query is faster than the uncached one.
This commit is contained in:
Emily Toop 2018-04-27 11:26:56 +01:00
parent 8add073001
commit 35467c1b24
6 changed files with 159 additions and 16 deletions

View file

@ -1030,11 +1030,6 @@ pub unsafe extern "C" fn tx_report_entity_for_temp_id(tx_report: *mut TxReport,
/// Adds an attribute to the cache. /// Adds an attribute to the cache.
/// `store_cache_attribute_forward` caches values for an attribute keyed by entity /// `store_cache_attribute_forward` caches values for an attribute keyed by entity
/// (i.e. find values and entities that have this attribute, or find values of attribute for an entity) /// (i.e. find values and entities that have this attribute, or find values of attribute for an entity)
///
/// # Safety
///
/// Callers must ensure that the pointer to the `Store` is not dangling and that
/// the C string provided to `attribute` is valid.
#[no_mangle] #[no_mangle]
pub extern "C" fn store_cache_attribute_forward(store: *mut Store, attribute: *const c_char) -> *mut ExternResult { pub extern "C" fn store_cache_attribute_forward(store: *mut Store, attribute: *const c_char) -> *mut ExternResult {
let store = unsafe { &mut *store }; let store = unsafe { &mut *store };
@ -1045,11 +1040,6 @@ pub extern "C" fn store_cache_attribute_forward(store: *mut Store, attribute: *c
/// Adds an attribute to the cache. /// Adds an attribute to the cache.
/// `store_cache_attribute_reverse` caches entities for an attribute keyed by value. /// `store_cache_attribute_reverse` caches entities for an attribute keyed by value.
/// (i.e. find entities that have a particular value for an attribute). /// (i.e. find entities that have a particular value for an attribute).
///
/// # Safety
///
/// Callers must ensure that the pointer to the `Store` is not dangling and that
/// the C string provided to `attribute` is valid.
#[no_mangle] #[no_mangle]
pub extern "C" fn store_cache_attribute_reverse(store: *mut Store, attribute: *const c_char) -> *mut ExternResult { pub extern "C" fn store_cache_attribute_reverse(store: *mut Store, attribute: *const c_char) -> *mut ExternResult {
let store = unsafe { &mut *store }; let store = unsafe { &mut *store };
@ -1065,11 +1055,6 @@ pub extern "C" fn store_cache_attribute_reverse(store: *mut Store, attribute: *c
/// ///
/// `Reverse` caches entities for an attribute keyed by value. /// `Reverse` caches entities for an attribute keyed by value.
/// (i.e. find entities that have a particular value for an attribute). /// (i.e. find entities that have a particular value for an attribute).
///
/// # Safety
///
/// Callers must ensure that the pointer to the `Store` is not dangling and that
/// the C string provided to `attribute` is valid.
#[no_mangle] #[no_mangle]
pub extern "C" fn store_cache_attribute_bi_directional(store: *mut Store, attribute: *const c_char) -> *mut ExternResult { pub extern "C" fn store_cache_attribute_bi_directional(store: *mut Store, attribute: *const c_char) -> *mut ExternResult {
let store = unsafe { &mut *store }; let store = unsafe { &mut *store };

View file

@ -14,6 +14,7 @@ import android.content.Context;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.support.test.InstrumentationRegistry; import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import android.util.Log;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -1149,6 +1150,7 @@ public class FFIIntegrationTest {
synchronized (expectation1) { synchronized (expectation1) {
expectation1.wait(1000); expectation1.wait(1000);
} }
assertTrue(expectation1.isFulfilled);
EntityBuilder builder = mentat.entityBuilder(bEntid); EntityBuilder builder = mentat.entityBuilder(bEntid);
builder.retract(":foo/boolean", false); builder.retract(":foo/boolean", false);
@ -1173,6 +1175,7 @@ public class FFIIntegrationTest {
synchronized (expectation2) { synchronized (expectation2) {
expectation2.wait(1000); expectation2.wait(1000);
} }
assertTrue(expectation2.isFulfilled);
} }
@Test @Test
@ -1217,6 +1220,7 @@ public class FFIIntegrationTest {
synchronized (expectation1) { synchronized (expectation1) {
expectation1.wait(1000); expectation1.wait(1000);
} }
assertTrue(expectation1.isFulfilled);
InProgressBuilder builder = mentat.entityBuilder(); InProgressBuilder builder = mentat.entityBuilder();
builder.retract(bEntid, ":foo/boolean", false); builder.retract(bEntid, ":foo/boolean", false);
@ -1241,5 +1245,77 @@ public class FFIIntegrationTest {
synchronized (expectation2) { synchronized (expectation2) {
expectation2.wait(1000); expectation2.wait(1000);
} }
assertTrue(expectation2.isFulfilled);
}
@Test
public void testCaching() throws InterruptedException {
String query = "[:find ?district :where\n" +
" [?neighborhood :neighborhood/name \"Beacon Hill\"]\n" +
" [?neighborhood :neighborhood/district ?d]\n" +
" [?d :district/name ?district]]";
Mentat mentat = openAndInitializeCitiesStore();
final Expectation expectation1 = new Expectation();
final QueryTimer uncachedTimer = new QueryTimer();
uncachedTimer.start();
mentat.query(query).run(new RelResultHandler() {
@Override
public void handleRows(RelResult rows) {
uncachedTimer.end();
assertNotNull(rows);
expectation1.fulfill();
}
});
synchronized (expectation1) {
expectation1.wait(1000);
}
assertTrue(expectation1.isFulfilled);
mentat.cache(":neighborhood/name", CacheDirection.REVERSE);
mentat.cache(":neighborhood/district", CacheDirection.FORWARD);
final Expectation expectation2 = new Expectation();
final QueryTimer cachedTimer = new QueryTimer();
cachedTimer.start();
mentat.query(query).run(new RelResultHandler() {
@Override
public void handleRows(RelResult rows) {
cachedTimer.end();
assertNotNull(rows);
expectation2.fulfill();
}
});
synchronized (expectation2) {
expectation2.wait(1000);
}
assertTrue(expectation2.isFulfilled);
long timingDifference = uncachedTimer.duration() - cachedTimer.duration();
Log.d("testCaching", "Cached query is "+ timingDifference +" nanoseconds faster than the uncached query");
assert cachedTimer.duration() < uncachedTimer.duration();
}
}
class QueryTimer {
private long startTime = 0;
private long endTime = 0;
public void start() {
this.startTime = System.nanoTime();
}
public void end() {
this.endTime = System.nanoTime();
}
public long duration() {
return this.endTime - this.startTime;
} }
} }

View file

@ -0,0 +1,15 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
public enum CacheDirection {
FORWARD, REVERSE, BOTH
}

View file

@ -41,6 +41,11 @@ public interface JNA extends Library {
void in_progress_builder_destroy(Pointer obj); void in_progress_builder_destroy(Pointer obj);
void entity_builder_destroy(Pointer obj); void entity_builder_destroy(Pointer obj);
// caching
RustResult store_cache_attribute_forward(Pointer store, String attribute);
RustResult store_cache_attribute_reverse(Pointer store, String attribute);
RustResult store_cache_attribute_bi_directional(Pointer store, String attribute);
// transact // transact
RustResult store_transact(Pointer store, String transaction); RustResult store_transact(Pointer store, String transaction);
Pointer tx_report_entity_for_temp_id(Pointer report, String tempid); Pointer tx_report_entity_for_temp_id(Pointer report, String tempid);

View file

@ -48,6 +48,35 @@ public class Mentat extends RustObject {
*/ */
public Mentat(Pointer rawPointer) { this.rawPointer = rawPointer; } public Mentat(Pointer rawPointer) { this.rawPointer = rawPointer; }
/**
* Add an attribute to the cache. The {@link CacheDirection} determines how that attribute can be
* looked up.
*
* TODO: Throw an exception if cache action fails
*
* @param attribute The attribute to cache
* @param direction The direction the attribute should be keyed.
* FORWARD caches values for an attribute keyed by entity
* (i.e. find values and entities that have this attribute, or find values of attribute for an entity)
* REVERSE caches entities for an attribute keyed by value.
* (i.e. find entities that have a particular value for an attribute).
* BOTH adds an attribute such that it is cached in both directions.
*/
public void cache(String attribute, CacheDirection direction) {
RustResult result = null;
switch (direction) {
case FORWARD:
result = JNA.INSTANCE.store_cache_attribute_forward(this.rawPointer, attribute);
case REVERSE:
result = JNA.INSTANCE.store_cache_attribute_reverse(this.rawPointer, attribute);
case BOTH:
result = JNA.INSTANCE.store_cache_attribute_bi_directional(this.rawPointer, attribute);
}
if (result.isFailure()) {
Log.e("Mentat", result.err);
}
}
/** /**
* Simple transact of an EDN string. * Simple transact of an EDN string.
* TODO: Throw an exception if the transact fails * TODO: Throw an exception if the transact fails
@ -88,7 +117,7 @@ public class Mentat extends RustObject {
/** /**
* Retrieve a single value of an attribute for an Entity * Retrieve a single value of an attribute for an Entity
* TODO: Throw an exception if there is no the result contains an error. * TODO: Throw an exception if the result contains an error.
* @param attribute The string the attribute whose value is to be returned. The string is represented as `:namespace/name`. * @param attribute The string the attribute whose value is to be returned. The string is represented as `:namespace/name`.
* @param entid The `Entid` of the entity we want the value from. * @param entid The `Entid` of the entity we want the value from.
* @return The {@link TypedValue} containing the value of the attribute for the entity. * @return The {@link TypedValue} containing the value of the attribute for the entity.
@ -139,6 +168,13 @@ public class Mentat extends RustObject {
} }
/**
* Start a new transaction
*
* TODO: Throw an exception if the result contains an error.
*
* @return The {@link InProgress} used to manage the transaction
*/
public InProgress beginTransaction() { public InProgress beginTransaction() {
RustResult result = JNA.INSTANCE.store_begin_transaction(this.rawPointer); RustResult result = JNA.INSTANCE.store_begin_transaction(this.rawPointer);
if (result.isSuccess()) { if (result.isSuccess()) {
@ -152,6 +188,14 @@ public class Mentat extends RustObject {
return null; return null;
} }
/**
* Creates a new transaction ({@link InProgress}) and returns an {@link InProgressBuilder} for
* that transaction.
*
* TODO: Throw an exception if the result contains an error.
*
* @return an {@link InProgressBuilder} for a new transaction.
*/
public InProgressBuilder entityBuilder() { public InProgressBuilder entityBuilder() {
RustResult result = JNA.INSTANCE.store_in_progress_builder(this.rawPointer); RustResult result = JNA.INSTANCE.store_in_progress_builder(this.rawPointer);
if (result.isSuccess()) { if (result.isSuccess()) {
@ -165,6 +209,15 @@ public class Mentat extends RustObject {
return null; return null;
} }
/**
* Creates a new transaction ({@link InProgress}) and returns an {@link EntityBuilder} for the
* entity with `entid` for that transaction.
*
* TODO: Throw an exception if the result contains an error.
*
* @param entid The `Entid` for this entity.
* @return an {@link EntityBuilder} for a new transaction.
*/
public EntityBuilder entityBuilder(long entid) { public EntityBuilder entityBuilder(long entid) {
RustResult result = JNA.INSTANCE.store_entity_builder_from_entid(this.rawPointer, entid); RustResult result = JNA.INSTANCE.store_entity_builder_from_entid(this.rawPointer, entid);
if (result.isSuccess()) { if (result.isSuccess()) {
@ -178,6 +231,15 @@ public class Mentat extends RustObject {
return null; return null;
} }
/**
* Creates a new transaction ({@link InProgress}) and returns an {@link EntityBuilder} for a new
* entity with `tempId` for that transaction.
*
* TODO: Throw an exception if the result contains an error.
*
* @param tempId The temporary identifier for this entity.
* @return an {@link EntityBuilder} for a new transaction.
*/
public EntityBuilder entityBuilder(String tempId) { public EntityBuilder entityBuilder(String tempId) {
RustResult result = JNA.INSTANCE.store_entity_builder_from_temp_id(this.rawPointer, tempId); RustResult result = JNA.INSTANCE.store_entity_builder_from_temp_id(this.rawPointer, tempId);
if (result.isSuccess()) { if (result.isSuccess()) {