Wrap caching FFI functions in Swift library.

`CacheDirection` enum is used only on the Swift 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:24 +01:00
parent b4b558e196
commit 8add073001
4 changed files with 107 additions and 2 deletions

View file

@ -29,6 +29,12 @@ protocol Observable {
func unregister(key: String) func unregister(key: String)
} }
enum CacheDirection {
case forward;
case reverse;
case both;
}
/** /**
The primary class for accessing Mentat's API. The primary class for accessing Mentat's API.
This class provides all of the basic API that can be found in Mentat's Store struct. This class provides all of the basic API that can be found in Mentat's Store struct.
@ -56,11 +62,36 @@ class Mentat: RustObject {
self.init(raw: store_open(storeURI)) self.init(raw: store_open(storeURI))
} }
/**
Add an attribute to the cache. The {@link CacheDirection} determines how that attribute can be
looked up.
- Parameter attribute: The attribute to cache
- Parameter 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.
- Throws: `ResultError.error` if an error occured while trying to cache the attribute.
*/
func cache(attribute: String, direction: CacheDirection) throws {
switch direction {
case .forward:
try store_cache_attribute_forward(self.raw, attribute).pointee.tryUnwrap()
case .reverse:
try store_cache_attribute_reverse(self.raw, attribute).pointee.tryUnwrap()
case .both:
try store_cache_attribute_bi_directional(self.raw, attribute).pointee.tryUnwrap()
}
}
/** /**
Simple transact of an EDN string. Simple transact of an EDN string.
- Parameter transaction: The string, as EDN, to be transacted - Parameter transaction: The string, as EDN, to be transacted
- Throws: `MentatError` if the an error occured during the transaction, or the TxReport is nil. - Throws: `ResultError.error` if the an error occured during the transaction, or the TxReport is nil.
- Returns: The `TxReport` of the completed transaction - Returns: The `TxReport` of the completed transaction
*/ */
@ -99,6 +130,8 @@ class Mentat: RustObject {
Creates a new transaction (`InProgress`) and returns an `EntityBuilder` for the entity with `entid` Creates a new transaction (`InProgress`) and returns an `EntityBuilder` for the entity with `entid`
for that transaction. for that transaction.
- Parameter entid: The `Entid` for this entity.
- Throws: `ResultError.error` if the creation of the transaction fails. - Throws: `ResultError.error` if the creation of the transaction fails.
- Throws: `ResultError.empty` if no `EntityBuilder` is created. - Throws: `ResultError.empty` if no `EntityBuilder` is created.
@ -113,6 +146,8 @@ class Mentat: RustObject {
Creates a new transaction (`InProgress`) and returns an `EntityBuilder` for a new entity with `tempId` Creates a new transaction (`InProgress`) and returns an `EntityBuilder` for a new entity with `tempId`
for that transaction. for that transaction.
- Parameter tempId: The temporary identifier for this entity.
- Throws: `ResultError.error` if the creation of the transaction fails. - Throws: `ResultError.error` if the creation of the transaction fails.
- Throws: `ResultError.empty` if no `EntityBuilder` is created. - Throws: `ResultError.empty` if no `EntityBuilder` is created.

View file

@ -98,7 +98,7 @@ class InProgress: OptionalRustObject {
/** /**
Creates an `EntityBuilder` using this `InProgress` for the entity with `entid`. Creates an `EntityBuilder` using this `InProgress` for the entity with `entid`.
- Parameter tempId: The `Entid` for this entity. - Parameter entid: The `Entid` for this entity.
- Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the `InProgress` - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the `InProgress`
has already been committed, or converted into a Builder. has already been committed, or converted into a Builder.

View file

@ -114,6 +114,11 @@ void in_progress_destroy(struct InProgress* _Nullable obj);
void in_progress_builder_destroy(struct InProgressBuilder* _Nullable obj); void in_progress_builder_destroy(struct InProgressBuilder* _Nullable obj);
void entity_builder_destroy(struct EntityBuilder* _Nullable obj); void entity_builder_destroy(struct EntityBuilder* _Nullable obj);
// caching
struct Result*_Nonnull store_cache_attribute_forward(struct Store*_Nonnull store, const char* _Nonnull attribute);
struct Result*_Nonnull store_cache_attribute_reverse(struct Store*_Nonnull store, const char* _Nonnull attribute);
struct Result*_Nonnull store_cache_attribute_bi_directional(struct Store*_Nonnull store, const char* _Nonnull attribute);
// transact // transact
struct Result*_Nonnull store_transact(struct Store*_Nonnull store, const char* _Nonnull transaction); struct Result*_Nonnull store_transact(struct Store*_Nonnull store, const char* _Nonnull transaction);
const int64_t* _Nullable tx_report_entity_for_temp_id(const struct TxReport* _Nonnull report, const char* _Nonnull tempid); const int64_t* _Nullable tx_report_entity_for_temp_id(const struct TxReport* _Nonnull report, const char* _Nonnull tempid);

View file

@ -1175,5 +1175,70 @@ class MentatTests: XCTestCase {
}) })
} }
func testCaching() {
let query = """
[:find ?district :where
[?neighborhood :neighborhood/name \"Beacon Hill\"]
[?neighborhood :neighborhood/district ?d]
[?d :district/name ?district]]
"""
let mentat = openAndInitializeCitiesStore()
struct QueryTimer {
private var _start: UInt64
private var _end: UInt64
init() {
self._start = 0
self._end = 0
}
private func currentTimeNanos() -> UInt64 {
var info = mach_timebase_info()
guard mach_timebase_info(&info) == KERN_SUCCESS else { return 0 }
let currentTime = mach_absolute_time()
return currentTime * UInt64(info.numer) / UInt64(info.denom)
}
mutating func start() {
self._start = self.currentTimeNanos()
}
mutating func end() {
self._end = self.currentTimeNanos()
}
func duration() -> UInt64 {
return self._end - self._start
}
}
var uncachedTimer = QueryTimer()
uncachedTimer.start()
XCTAssertNoThrow(try mentat.query(query: query).run { (result) in
uncachedTimer.end()
XCTAssertNotNil(result)
})
XCTAssertNoThrow(try mentat.cache(attribute: ":neighborhood/name", direction: CacheDirection.reverse))
XCTAssertNoThrow(try mentat.cache(attribute: ":neighborhood/district", direction: CacheDirection.forward))
var cachedTimer = QueryTimer()
cachedTimer.start()
XCTAssertNoThrow(try mentat.query(query: query).run { (result) in
cachedTimer.end()
XCTAssertNotNil(result)
})
let timingDifference = uncachedTimer.duration() - cachedTimer.duration()
print("Cached query is \(timingDifference) nanoseconds faster than the uncached query")
XCTAssertLessThan(cachedTimer.duration(), uncachedTimer.duration())
}
// TODO: Add tests for transaction observation // TODO: Add tests for transaction observation
} }