48 Abstractions over reflected vectors in a database
Currently a good deal of the OT functionality for deserialising and applying a delta remains in the cxOperation library, not the cxOT library. This has some big disadvantages:
- The code in cxOperation which deals with CSpaces, PSpaces, persistence, reflected types etc raises complexity.
- We can't unit test OT on vectors properly
- We can't measure and optimise performance of the underlying algorithms on transient data structures
- Changing over to recording the insertions/deletions next to the field is difficult
To solve this we should look for ways to abstract away the details of reflected vector fields in the database.
Abstracting a reflected vector
Xcpp and cxObject provides a mechanism for supporting reflected xvectors, using an underlying xvector of octets, plus a ReflectionByteCode which describes the element type.
Manipulation of a reflected vector is cumbersome, as seen by how reflection of the element type is used in numerous places in VectorDeltaReader.cpp:
- The byte code can be FT_VECTOR, FT_STRING8 or FT_STRING16
- ReflectionByteCode::IsPOD() affects whether an optimisation to avoid constructing/destructing elements is possible
- ConstructReflectedVariable(rbc, p) is used to construct an element
- DeserialiseReflectedVariable(ar,rbc,p) is used to deserialise an element from an archive
- DestructReflectedArrayVariable(rbc,n,data) is used to destruct an array of elements
- rbc.GetTypeSizeOnUnderlyingType() is used to get the element size
In addition we have cumbersome code to deal with access to the field in the database using a FieldId:
- Deserialising a class name and looking up the ReflectedClass
- TryBindObjectGivenOid(oid) to bind to an IPersistable object given an oid
- SetTouched(po) to help avoid the IPersistable object from being evicted
- po->GetReflectedClass() to get at the reflected class of the object
- Using ReflectedClass to create an instance and register in the CSpace
- Ensuring the CSpace is locked
- addr = NavigatePathToField(po,path,rbc,true) to navigate to the vector field of an object using the path
- IndepFieldWriteBarrier(po, path) to indicate field changed in the DGS
- MarkPersistableAsDirtyWithoutTrace(pspace,po) to mark object as dirty when the field is updated
- Issue the operation notifications with calls to IssueOnVectorInsert() and IssueOnVectorErase()
Let's focus on the requirements for being able to deserialised a delta, transform it using OT and apply it to a reflected vector field in a database.
That means we need to abstract over mutation to a vector field.
If the element type is not a POD type, then it may contain pointers to allocated memory. For performance we assume the element type is movable. That allows for efficient insertion operations - by moving elements deserialised from the stream into the field in the database.
A complicating issue is that both database and temporary vector fields are created/accessed/destroyed by the generic vector OT functions. A wrapper over a vector is different for database and temporary vector fields. This suggests we have generic algorithms in cxOT which can be polymorphic over types which either wrap database or temporary vectors.
Let Vec stand for an abstract vector field type. The following functions are needed:
struct VectorType
{
using SrcVec;
using DstVec;
// Create/destroy a vector used as a temporary
SrcVec Create();
//void Destroy(SrcVec& src);
// Erase elements [i1,i2) in v
void EraseRange(DstVec& v, int i1, i2);
// Move elements [s1,s2) from src to dst at position d
void MoveRange(DstVec& dst, int d, SrcVec& src, s1, s2);
};
class DatabaseVectorField
{
public:
DatabaseVectorField(const ReflectedClass& rc, const FieldId& fid);
bool Init();
using SrcVec = xvector<octet_t>;
void DeserialiseVec(DeltaReader& dr, InputArchive& ar, SrcVec& B2);
void DestructVec(SrcVec& B2);
ssize_t ElementSize() const { return m_elementSize; }
void EraseRange(ssize_t p1, ssize_t p2);
const octet_t* InsertFrom(ssize_t d, const octet_t* src, ssize_t count);
void SetChanged();
private:
const ReflectedClass& m_rc;
FieldId m_fid;
ptr<IPersistable> m_obj;
ReflectionByteCode m_rbcElement;
VectorOfByte* m_vf;
ssize_t m_elementSize;
bool m_isPOD;
};
Consider a class that manages access to a mutable xvector field.
struct WrappedVector
{
erase_range(int i, int count);
move_range(int dstPos, WrappedVector& src, srcPos, int count);
};
The temporary xvector
There is a temporary xvector B2 which is deserialised as follows:
xvector<octet_t> B2;
dvf.DeserialiseVec(dr, ar, B2);
This is destroyed as follows:
dvf.DestructVec(B2);
B2 is factorised as follows:
xvector<octet_t> Boutside;
{
xvector<octet_t> Binside;
TakeRFactor(vCommon,I2,D2, dvf.ElementSize(), B2,Binside,Boutside);
dvf.DestructVec(Binside);
}
This doesn't seem the most efficient approach, it copies B2 to Boutside in the common case where the factorisation produces an empty LFactor.
This also illustrates the leaky abstraction currently defined by DatabaseVectorField for working with a temporary xvector.
vCommon is calculated before B2 is deserialised. Can we combine the deserialisation of B2 with taking the RFactor with respect to vCommon? That would provide better performance - by avoiding the need to create/destroy Binside.