Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Changed
- Storage files are now written on a shared background thread. Changes are still serialized on the calling thread, but `Set` and auto-save no longer wait for the disk. `Save()` and `Dispose()` still block until the data has reached the disk.
- Auto-save no longer serializes the whole storage on every change. While the previous snapshot is still on its way to the disk, a change only marks the storage as changed; the storage is serialized once more when the writer becomes free, on the thread that built it. A burst of 200 `Set` calls now costs one serialization instead of 200. A storage built on a thread without a `SynchronizationContext` keeps serializing on every change, as before.

### Added
- `SaveOnBackgroundThread(bool)` on the builder. Pass `false` to write the file before every change returns, as before.
Expand Down
189 changes: 96 additions & 93 deletions src/Runtime/BinaryStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ public partial class BinaryStorage : IDisposable, IBinaryStorage
private readonly string _storageFilePath;
private readonly StoragePersistence _persistence;
private readonly IReadOnlyList<BinarySection> _supportedTypes;
private readonly Dictionary<Type, int> _sectionIndexByType;
private readonly Dictionary<string, Record> _data = new();
private readonly Dictionary<IReactiveCollection, string> _collections = new();
private readonly Action _decreaseCounter;
private int _changeScopeCounter;
private bool _hasUnsavedChanges;

Expand All @@ -37,6 +39,8 @@ public bool SaveJsonCopyForDebug
/// <summary> Gets a value indicating whether the storage has been disposed. </summary>
public bool IsDisposed { get; private set; }

internal int SerializeCount => _persistence.SerializeCount;

/// <summary> Initializes a new instance of the <see cref="BinaryStorage"/> class. </summary>
/// <param name="storageFilePath">The file path for storing data.</param>
/// <param name="supportedTypes">The list of supported types for storage.</param>
Expand All @@ -45,7 +49,13 @@ internal BinaryStorage(string storageFilePath, IReadOnlyList<BinarySection> supp
{
_storageFilePath = storageFilePath;
_supportedTypes = supportedTypes;
_persistence = new StoragePersistence(storageFilePath, supportedTypes, saveOnBackgroundThread);
_sectionIndexByType = new Dictionary<Type, int>(supportedTypes.Count);
for (var i = 0; i < supportedTypes.Count; i++)
{
_sectionIndexByType[supportedTypes[i].Type] = i;
}
_decreaseCounter = DecreaseCounter;
_persistence = new StoragePersistence(storageFilePath, supportedTypes, saveOnBackgroundThread, SaveDeferredChanges);
}

#region Events
Expand Down Expand Up @@ -104,7 +114,7 @@ public virtual bool SetRaw(string key, object value, TypeMismatchBehaviour? over
}

var valueType = value.GetType();
if (valueType.IsCollection())
if (CollectionTypeCache.IsCollection(valueType))
{
throw new IncorrectUsageOfCollectionException(nameof(SetRaw), valueType);
}
Expand All @@ -128,23 +138,17 @@ public virtual bool SetRaw(string key, object value, TypeMismatchBehaviour? over
return true;
}

var mismatchBehaviour = overrideTypeMismatchBehaviour ?? TypeMismatchBehaviour;
switch (mismatchBehaviour)
{
case TypeMismatchBehaviour.OverrideValueAndType:
using (new ChangeScope(this))
{
RemoveRecord(key);
AddRawRecord(key, value, valueType);
}
return true;
case TypeMismatchBehaviour.ThrowException:
throw new UnexpectedTypeException(key, nameof(SetRaw), record.Type, valueType);
case TypeMismatchBehaviour.Ignore:
return false;
default:
throw new UnexpectedEnumException(typeof(TypeMismatchBehaviour), mismatchBehaviour);
if (!ShouldReplaceMismatchedRecord(key, record, valueType, overrideTypeMismatchBehaviour))
{
return false;
}

using (new ChangeScope(this))
{
RemoveRecord(key);
AddRawRecord(key, value, valueType);
}
return true;
}

/// <summary> Determines whether the specified key exists in the storage. </summary>
Expand Down Expand Up @@ -236,23 +240,17 @@ public virtual bool Set<T>(string key, T value, TypeMismatchBehaviour? overrideT
return ChangeRecord(key, typedRecord, value);
}

var mismatchBehaviour = overrideTypeMismatchBehaviour ?? TypeMismatchBehaviour;
switch (mismatchBehaviour)
{
case TypeMismatchBehaviour.OverrideValueAndType:
using (new ChangeScope(this))
{
RemoveRecord(key);
AddRecord(key, value);
}
return true;
case TypeMismatchBehaviour.ThrowException:
throw new UnexpectedTypeException(key, nameof(Set), record.Type, typeof(T));
case TypeMismatchBehaviour.Ignore:
return false;
default:
throw new UnexpectedEnumException(typeof(TypeMismatchBehaviour), mismatchBehaviour);
if (!ShouldReplaceMismatchedRecord(key, record, typeof(T), overrideTypeMismatchBehaviour))
{
return false;
}

using (new ChangeScope(this))
{
RemoveRecord(key);
AddRecord(key, value);
}
return true;
}

/// <summary>
Expand Down Expand Up @@ -320,7 +318,7 @@ public IDisposable MultipleChangeScope()
{
ThrowIfDisposed();
_changeScopeCounter++;
return new DisposableScope(DecreaseCounter);
return new DisposableScope(_decreaseCounter);
}

#region Collections
Expand Down Expand Up @@ -448,12 +446,7 @@ private Record<T> AddRecord<T>(string key, T value)
var record = new Record<T>(value, typeIndex);
section.Count++;
_data.Add(key, record);
var rc = record.AsReactiveCollection();
if (rc != null)
{
_collections.Add(rc, key);
rc.OnChanged += ReactiveCollectionChanged;
}
TrackCollectionOf(record, key);
MarkChanged();
OnKeyAdded?.Invoke(key);
return record;
Expand All @@ -466,15 +459,7 @@ private Record<T> AddRecord<T>(string key, T value)
/// <exception cref="UnregisteredTypeException">Thrown if the type is not registered.</exception>
private void AddRawRecord(string key, object value, Type valueType)
{
var typeIndex = -1;
for (var i = 0; i < _supportedTypes.Count; i++)
{
if (_supportedTypes[i].Type == valueType)
{
typeIndex = i;
break;
}
}
var typeIndex = IndexOfSection(valueType);
if (typeIndex == -1)
{
throw new UnregisteredTypeException(valueType);
Expand Down Expand Up @@ -518,13 +503,7 @@ private bool RemoveRecord(string key)
{
return false;
}
var rc = value.AsReactiveCollection();
if (rc != null)
{
rc.OnChanged -= ReactiveCollectionChanged;
rc.Dispose();
_collections.Remove(rc);
}
UntrackCollectionOf(value);
_supportedTypes[value.TypeIndex].Count--;
_data.Remove(key);
MarkChanged();
Expand All @@ -540,14 +519,7 @@ private void RemoveAllRecords()
{
foreach (var record in _data.Values)
{
var rc = record.AsReactiveCollection();
if (rc == null)
{
continue;
}
rc.OnChanged -= ReactiveCollectionChanged;
rc.Dispose();
_collections.Remove(rc);
UntrackCollectionOf(record);
}
_data.Clear();
for (var i = 0; i < _supportedTypes.Count; i++)
Expand All @@ -573,14 +545,50 @@ private Record GetRecord(string key)

private int IndexOfSection<T>()
{
for (var i = 0; i < _supportedTypes.Count; i++)
return IndexOfSection(typeof(T));
}

private int IndexOfSection(Type type)
{
return _sectionIndexByType.TryGetValue(type, out var index) ? index : -1;
}

/// <summary> Decides whether a record whose stored type differs from the type being written has to be replaced. </summary>
/// <returns>True if the record has to be replaced; false if the write has to be ignored.</returns>
/// <exception cref="UnexpectedTypeException">Thrown if the mismatch behavior is to throw.</exception>
private bool ShouldReplaceMismatchedRecord(string key, Record record, Type valueType, TypeMismatchBehaviour? overrideTypeMismatchBehaviour, [CallerMemberName] string action = null)
{
var mismatchBehaviour = overrideTypeMismatchBehaviour ?? TypeMismatchBehaviour;
return mismatchBehaviour switch
{
if (_supportedTypes[i] is TypedBinarySection<T>)
{
return i;
}
TypeMismatchBehaviour.OverrideValueAndType => true,
TypeMismatchBehaviour.Ignore => false,
TypeMismatchBehaviour.ThrowException => throw new UnexpectedTypeException(key, action, record.Type, valueType),
_ => throw new UnexpectedEnumException(typeof(TypeMismatchBehaviour), mismatchBehaviour)
};
}

private void TrackCollectionOf(Record record, string key)
{
var collection = record.AsReactiveCollection();
if (collection == null)
{
return;
}
return -1;
_collections.Add(collection, key);
collection.OnChanged += ReactiveCollectionChanged;
}

private void UntrackCollectionOf(Record record)
{
var collection = record.AsReactiveCollection();
if (collection == null)
{
return;
}
collection.OnChanged -= ReactiveCollectionChanged;
collection.Dispose();
_collections.Remove(collection);
}

/// <summary>
Expand All @@ -594,14 +602,7 @@ private void DecreaseCounter()
return;
}
_changeScopeCounter--;
if (IsDisposed)
{
return;
}
if (_changeScopeCounter == 0 && _hasUnsavedChanges && AutoSave)
{
SaveDataOnDisk(false);
}
SaveDeferredChanges();
}

/// <summary> Reacts to a change in a reactive collection. </summary>
Expand Down Expand Up @@ -689,14 +690,7 @@ private void Dispose(bool disposing)
// Always dispose IReactiveCollection instances
foreach (var record in _data.Values)
{
var rc = record.AsReactiveCollection();
if (rc == null)
{
continue;
}
rc.OnChanged -= ReactiveCollectionChanged;
rc.Dispose();
_collections.Remove(rc);
UntrackCollectionOf(record);
}

OnKeyAdded = null;
Expand Down Expand Up @@ -731,12 +725,7 @@ private void LoadDataFromDisk(KeyLoadFailedBehaviour keyLoadFailedBehaviour)
_persistence.Load(_data, keyLoadFailedBehaviour);
foreach (var pair in _data)
{
var rc = pair.Value.AsReactiveCollection();
if (rc != null)
{
_collections.Add(rc, pair.Key);
rc.OnChanged += ReactiveCollectionChanged;
}
TrackCollectionOf(pair.Value, pair.Key);
}
}

Expand All @@ -747,10 +736,24 @@ private void LoadDataFromDisk(KeyLoadFailedBehaviour keyLoadFailedBehaviour)
private void SaveDataOnDisk(bool waitForDisk)
{
ThrowIfDisposed();
if (!waitForDisk && _persistence.TryDeferSave())
{
_hasUnsavedChanges = true;
return;
}
_persistence.Save(_data, waitForDisk);
_hasUnsavedChanges = false;
}

private void SaveDeferredChanges()
{
if (IsDisposed || !_hasUnsavedChanges)
{
return;
}
MarkChanged();
}

#endregion
}
}
38 changes: 38 additions & 0 deletions src/Runtime/Collections/ReactiveCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;

namespace Appegy.Storage
{
internal abstract class ReactiveCollection : IReactiveCollection
{
public bool IsDisposed { get; private set; }

public event Action<IReactiveCollection> OnChanged;

protected abstract string ObjectName { get; }

public abstract void Clear();

public void Dispose()
{
if (IsDisposed)
{
return;
}
Clear();
IsDisposed = true;
}

protected void SetDirty()
{
OnChanged?.Invoke(this);
}

protected void ThrowIfDisposed()
{
if (IsDisposed)
{
throw new ObjectDisposedException(ObjectName);
}
}
}
}
2 changes: 2 additions & 0 deletions src/Runtime/Collections/ReactiveCollection.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading