< Summary

Class:DCL.CRDT.CRDTProtocol
Assembly:DCL.CRDTProtocol
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/CRDTProtocol/CRDTProtocol.cs
Covered lines:76
Uncovered lines:46
Coverable lines:122
Total lines:457
Line coverage:62.2% (76 of 122)
Covered branches:0
Total branches:0
Covered methods:9
Total methods:13
Method coverage:69.2% (9 of 13)

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
CRDTProtocol()0%110100%
CompareTo(...)0%6200%
CrdtState()0%110100%
ProcessMessage(...)0%15.2214081.58%
GetStateAsMessages()0%19.129050%
CreateLwwMessage(...)0%4.254075%
CreateSetMessage(...)0%6200%
GetState(...)0%110100%
UpdateSingleComponentState(...)0%330100%
TryAddSetComponentState(...)0%30500%
TryGetSingleComponentState(...)0%330100%
TryGetComponentSetState(...)0%12300%
CompareData(...)0%1915073.91%

File(s)

/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/CRDTProtocol/CRDTProtocol.cs

#LineLine coverage
 1using System;
 2using System.Collections;
 3using System.Collections.Generic;
 4
 5namespace DCL.CRDT
 6{
 7    public class CRDTProtocol
 8    {
 9        public enum ProcessMessageResultType
 10        {
 11            /**
 12           * Typical message and new state set.
 13           * @state CHANGE
 14           * @reason Incoming message has a timestamp greater
 15           */
 16            StateUpdatedTimestamp = 1,
 17
 18            /**
 19           * Typical message when it is considered old.
 20           * @state it does NOT CHANGE.
 21           * @reason incoming message has a timestamp lower.
 22           */
 23            StateOutdatedTimestamp = 2,
 24
 25            /**
 26           * Weird message, same timestamp and data.
 27           * @state it does NOT CHANGE.
 28           * @reason consistent state between peers.
 29           */
 30            NoChanges = 3,
 31
 32            /**
 33           * Less but typical message, same timestamp, resolution by data.
 34           * @state it does NOT CHANGE.
 35           * @reason incoming message has a LOWER data.
 36           */
 37            StateOutdatedData = 4,
 38
 39            /**
 40           * Less but typical message, same timestamp, resolution by data.
 41           * @state CHANGE
 42           * @reason incoming message has a GREATER data.
 43           */
 44            StateUpdatedData = 5,
 45
 46            /**
 47           * Entity was previously deleted.
 48           * @state it does NOT CHANGE.
 49           * @reason The message is considered old.
 50           */
 51            EntityWasDeleted = 6,
 52
 53            /**
 54           * Entity should be deleted.
 55           * @state CHANGE
 56           * @reason the state is storing old entities
 57           */
 58            EntityDeleted = 7,
 59
 60            /**
 61           * An APPEND with a success insert
 62           * @state CHANGE
 63           * @reason the element is not in set set yet
 64           */
 65            StateElementAddedToSet = 8,
 66        };
 67
 36768        private int MAX_ELEMENT_SET = 100;
 69
 70        internal class CrdtEntityComponentData
 71        {
 72            public long entityId;
 73            public int componentId;
 74            public int timestamp;
 75            public object data;
 76        }
 77
 78        internal class EntityComponentData : IComparable<EntityComponentData>
 79        {
 80            public int timestamp;
 81            public object data;
 82
 83            public int CompareTo(EntityComponentData other)
 84            {
 085                int timestampDiff = this.timestamp - other.timestamp;
 86
 087                if (timestampDiff == 0)
 88                {
 089                    return CompareData(this.data, other.data);
 90                }
 91                else
 92                {
 093                    return Math.Sign(timestampDiff);
 94                }
 95            }
 96        }
 97
 98        internal class CrdtState
 99        {
 393100            public readonly Dictionary<int, Dictionary<long, EntityComponentData>> singleComponents = new Dictionary<int
 393101            public readonly Dictionary<int, Dictionary<long, List<EntityComponentData>>> setComponents = new Dictionary<
 393102            public readonly Dictionary<int, int> deletedEntitiesSet = new Dictionary<int, int>();
 103        }
 104
 367105        internal CrdtState state = new CrdtState();
 106
 107        public ProcessMessageResultType ProcessMessage(CrdtMessage message)
 108        {
 124109            int entityId = (int)message.EntityId;
 124110            int entityNumber = entityId & 0xffff;
 124111            int entityVersion = (entityId >> 16) & 0xffff;
 124112            bool entityNumberWasDeleted = state.deletedEntitiesSet.TryGetValue(entityNumber, out int deletedVersion);
 113
 124114            if (entityNumberWasDeleted)
 115            {
 24116                if (deletedVersion >= entityVersion)
 117                {
 24118                    return ProcessMessageResultType.EntityWasDeleted;
 119                }
 120            }
 121
 100122            if (message.Type == CrdtMessageType.DELETE_ENTITY)
 123            {
 6124                if (entityNumberWasDeleted)
 125                {
 0126                    state.deletedEntitiesSet[entityNumber] = entityVersion;
 127                }
 128                else
 129                {
 6130                    state.deletedEntitiesSet.Add(entityNumber, entityVersion);
 131                }
 132
 84133                foreach (var component in state.singleComponents)
 134                {
 36135                    component.Value.Remove(entityId);
 136                }
 137
 12138                foreach (var componentSet in state.setComponents)
 139                {
 0140                    componentSet.Value.Remove(entityId);
 141                }
 142
 143                // TODO: clean the state with this entityId
 144
 6145                return ProcessMessageResultType.EntityDeleted;
 146            }
 147
 94148            if (message.Type == CrdtMessageType.APPEND_COMPONENT)
 149            {
 0150                bool elementAdded = TryAddSetComponentState(message.EntityId, message.ComponentId, message.Data, message
 151
 0152                if (elementAdded)
 153                {
 0154                    return ProcessMessageResultType.StateElementAddedToSet;
 155                }
 156                else
 157                {
 0158                    return ProcessMessageResultType.NoChanges;
 159                }
 160            }
 161
 94162            TryGetSingleComponentState(message.EntityId, message.ComponentId, out EntityComponentData storedData);
 163
 164            // The received message is > than our current value, update our state.components.
 94165            if (storedData == null || storedData.timestamp < message.Timestamp)
 166            {
 72167                UpdateSingleComponentState(message.EntityId, message.ComponentId, message.Data, message.Timestamp);
 72168                return ProcessMessageResultType.StateUpdatedTimestamp;
 169            }
 170
 171            // Outdated Message. Resend our state message through the wire.
 22172            if (storedData.timestamp > message.Timestamp)
 173            {
 2174                return ProcessMessageResultType.StateOutdatedTimestamp;
 175            }
 176
 20177            int currentDataGreater = CompareData(storedData.data, message.Data);
 178
 179            // Same data, same timestamp. Weirdo echo message.
 20180            if (currentDataGreater == 0)
 181            {
 1182                return ProcessMessageResultType.NoChanges;
 183            }
 184
 185            // Current data is greater
 19186            if (currentDataGreater > 0)
 187            {
 3188                return ProcessMessageResultType.StateOutdatedData;
 189            }
 190
 191            // Curent data is lower
 16192            UpdateSingleComponentState(message.EntityId, message.ComponentId, message.Data, message.Timestamp);
 16193            return ProcessMessageResultType.StateUpdatedData;
 194        }
 195
 196        public List<CrdtMessage> GetStateAsMessages()
 197        {
 2198            List<CrdtMessage> crdtMessagesList = new List<CrdtMessage>();
 199
 8200            foreach (var component in state.singleComponents)
 201            {
 8202                foreach (var entityComponentData in component.Value)
 203                {
 2204                    crdtMessagesList.Add(new CrdtMessage
 205                    (
 206                        type: entityComponentData.Value.data == null ? CrdtMessageType.DELETE_COMPONENT : CrdtMessageTyp
 207                        entityId: entityComponentData.Key,
 208                        componentId: component.Key,
 209                        timestamp: entityComponentData.Value.timestamp,
 210                        data: entityComponentData.Value.data
 211                    ));
 212                }
 213            }
 214
 4215            foreach (var component in state.setComponents)
 216            {
 0217                foreach (var set in component.Value)
 218                {
 0219                    foreach (var entityComponentData in set.Value)
 220                    {
 0221                        crdtMessagesList.Add(new CrdtMessage
 222                        (
 223                            type: CrdtMessageType.APPEND_COMPONENT,
 224                            entityId: set.Key,
 225                            componentId: component.Key,
 226                            timestamp: entityComponentData.timestamp,
 227                            data: entityComponentData.data
 228                        ));
 229                    }
 230                }
 231            }
 232
 4233            foreach (var entity in state.deletedEntitiesSet)
 234            {
 0235                long entityNumber = entity.Key;
 0236                long entityVersion = entity.Value;
 0237                long entityId = entityNumber | (entityVersion << 16);
 238
 0239                crdtMessagesList.Add(new CrdtMessage(
 240                    type: CrdtMessageType.DELETE_ENTITY,
 241                    entityId: entityId,
 242                    componentId: 0,
 243                    timestamp: 0,
 244                    data: null
 245                ));
 246            }
 247
 2248            return crdtMessagesList;
 249        }
 250
 251        public CrdtMessage CreateLwwMessage(int entityId, int componentId, byte[] data)
 252        {
 2253            int timeStamp = 0;
 254
 2255            if (TryGetSingleComponentState(entityId, componentId, out EntityComponentData storedMessage))
 256            {
 0257                timeStamp = storedMessage.timestamp + 1;
 258            }
 259
 2260            return new CrdtMessage
 261            (
 262                type: data == null ? CrdtMessageType.DELETE_COMPONENT : CrdtMessageType.PUT_COMPONENT,
 263                entityId: entityId,
 264                componentId: componentId,
 265                timestamp: timeStamp,
 266                data: data
 267            );
 268        }
 269
 270        public CrdtMessage CreateSetMessage(int entityId, int componentId, byte[] data)
 271        {
 0272            int timeStamp = 0;
 273
 0274            if (TryGetSingleComponentState(entityId, componentId, out EntityComponentData storedMessage))
 275            {
 0276                timeStamp = storedMessage.timestamp + 1;
 277            }
 278
 0279            return new CrdtMessage
 280            (
 281                type: CrdtMessageType.APPEND_COMPONENT,
 282                entityId: entityId,
 283                componentId: componentId,
 284                timestamp: timeStamp,
 285                data: data
 286            );
 287        }
 288
 289        internal EntityComponentData GetState(long entityId, int componentId)
 290        {
 3291            TryGetSingleComponentState(entityId, componentId, out EntityComponentData entityComponentData);
 3292            return entityComponentData;
 293        }
 294
 295        private void UpdateSingleComponentState(long entityId, int componentId, object data, int remoteTimestamp)
 296        {
 88297            bool stateExists = TryGetSingleComponentState(entityId, componentId, out EntityComponentData currentStateVal
 298
 88299            if (stateExists)
 300            {
 31301                currentStateValue.data = data;
 31302                currentStateValue.timestamp = Math.Max(remoteTimestamp, currentStateValue.timestamp);
 303            }
 304            else
 305            {
 57306                EntityComponentData newState = new EntityComponentData()
 307                {
 308                    timestamp = remoteTimestamp,
 309                    data = data
 310                };
 311
 57312                state.singleComponents.TryGetValue(componentId, out Dictionary<long, EntityComponentData> componentSet);
 313
 57314                if (componentSet != null)
 315                {
 1316                    componentSet.Add(entityId, newState);
 317                }
 318                else
 319                {
 56320                    state.singleComponents.Add(componentId, new Dictionary<long, EntityComponentData>());
 56321                    state.singleComponents[componentId].Add(entityId, newState);
 322                }
 323            }
 56324        }
 325
 326        /**
 327         * @returns true if the element is added or false if it already exists
 328         */
 329        private bool TryAddSetComponentState(long entityId, int componentId, object data, int remoteTimestamp)
 330        {
 0331            bool stateExists = TryGetComponentSetState(entityId, componentId, out List<EntityComponentData> currentSetSt
 332
 0333            EntityComponentData newState = new EntityComponentData()
 334            {
 335                timestamp = remoteTimestamp,
 336                data = data
 337            };
 338
 339            // The entity already has a Set
 0340            if (stateExists)
 341            {
 0342                int index = currentSetState.BinarySearch(newState);
 343
 0344                if (index < 0)
 345                {
 0346                    index = ~index;
 0347                    currentSetState.Insert(index, newState);
 348
 0349                    if (currentSetState.Count > MAX_ELEMENT_SET)
 350                    {
 0351                        currentSetState.RemoveRange(MAX_ELEMENT_SET, currentSetState.Count - MAX_ELEMENT_SET);
 352                    }
 353                }
 354                else
 355                {
 356                    // If the element already exist, we don't add it twice.
 0357                    return false;
 358                }
 359            }
 360            else
 361            {
 0362                state.setComponents.TryGetValue(componentId, out Dictionary<long, List<EntityComponentData>> componentSe
 363
 364                // The component is already in the dictionary, we have to create the new List for the Entity
 0365                if (componentSet != null)
 366                {
 0367                    componentSet.Add(entityId, new List<EntityComponentData>() { newState });
 368                }
 369                else
 370                {
 0371                    state.setComponents.Add(componentId, new Dictionary<long, List<EntityComponentData>>());
 0372                    state.setComponents[componentId].Add(entityId, new List<EntityComponentData>() { newState });
 373                }
 374            }
 375
 0376            return true;
 377        }
 378
 379        private bool TryGetSingleComponentState(long entityId, int componentId, out EntityComponentData entityComponentD
 380        {
 187381            if (state.singleComponents.TryGetValue(componentId, out Dictionary<long, EntityComponentData> innerDictionar
 382            {
 73383                if (innerDictionary.TryGetValue(entityId, out entityComponentData))
 384                {
 71385                    return true;
 386                }
 387            }
 388
 116389            entityComponentData = null;
 116390            return false;
 391        }
 392
 393        private bool TryGetComponentSetState(long entityId, int componentId, out List<EntityComponentData> entityCompone
 394        {
 0395            if (state.setComponents.TryGetValue(componentId, out Dictionary<long, List<EntityComponentData>> innerDictio
 396            {
 0397                if (innerDictionary.TryGetValue(entityId, out entityComponentSet))
 398                {
 0399                    return true;
 400                }
 401            }
 402
 0403            entityComponentSet = null;
 0404            return false;
 405        }
 406
 407        /**
 408         * Compare raw data.
 409         * @internal
 410         * @returns 0 if is the same data, 1 if a > b, -1 if b > a
 411         */
 412        public static int CompareData(object a, object b)
 413        {
 414            // At reference level
 53415            if (a == b) return 0;
 53416            if (a == null) return -1;
 53417            if (b == null) return 1;
 418
 53419            if (a is byte[] bytesA && b is byte[] bytesB)
 420            {
 3421                int lengthDifference = bytesA.Length - bytesB.Length;
 422
 3423                if (lengthDifference != 0)
 424                {
 0425                    return lengthDifference > 0 ? 1 : -1;
 426                }
 427
 246428                for (int i = 0; i < bytesA.Length; i++)
 429                {
 120430                    int res = bytesA[i] - bytesB[i];
 431
 120432                    if (res != 0)
 433                    {
 0434                        return res > 0 ? 1 : -1;
 435                    }
 436                }
 437
 438                // the data is exactly the same
 3439                return 0;
 440            }
 441
 50442            if (a is string strA && b is string strB)
 443            {
 50444                int lengthDifference = strA.Length - strB.Length;
 445
 50446                if (lengthDifference != 0)
 447                {
 2448                    return lengthDifference > 0 ? 1 : -1;
 449                }
 450
 48451                return string.Compare(strA, strB, StringComparison.InvariantCulture);
 452            }
 453
 0454            return Comparer.Default.Compare(a, b);
 455        }
 456    }
 457}