Optimize serializable file writes

This commit is contained in:
Łukasz Domeradzki
2025-03-30 21:06:33 +02:00
parent cbe0502154
commit eb983843c8
4 changed files with 73 additions and 41 deletions

View File

@@ -100,9 +100,7 @@ public sealed class ConcurrentHashSet<T> : IReadOnlySet<T>, ISet<T> where T : no
public void ExceptWith(IEnumerable<T> other) {
ArgumentNullException.ThrowIfNull(other);
foreach (T item in other) {
Remove(item);
}
RemoveRange(other);
}
[MustDisposeResource]
@@ -113,8 +111,14 @@ public sealed class ConcurrentHashSet<T> : IReadOnlySet<T>, ISet<T> where T : no
IReadOnlySet<T> otherSet = other as IReadOnlySet<T> ?? other.ToHashSet();
foreach (T item in this.Where(item => !otherSet.Contains(item))) {
Remove(item);
bool modified = false;
foreach (T _ in this.Where(item => !otherSet.Contains(item) && BackingCollection.TryRemove(item, out _))) {
modified = true;
}
if (modified) {
OnModified?.Invoke(this, EventArgs.Empty);
}
}
@@ -182,24 +186,24 @@ public sealed class ConcurrentHashSet<T> : IReadOnlySet<T>, ISet<T> where T : no
ArgumentNullException.ThrowIfNull(other);
IReadOnlySet<T> otherSet = other as IReadOnlySet<T> ?? other.ToHashSet();
HashSet<T> removed = [];
foreach (T item in otherSet.Where(Contains)) {
removed.Add(item);
Remove(item);
HashSet<T> removed = otherSet.Where(item => Contains(item) && BackingCollection.TryRemove(item, out _)).ToHashSet();
bool modified = removed.Count > 0;
foreach (T _ in otherSet.Where(item => !removed.Contains(item) && BackingCollection.TryAdd(item, true))) {
modified = true;
}
foreach (T item in otherSet.Where(item => !removed.Contains(item))) {
Add(item);
if (modified) {
OnModified?.Invoke(this, EventArgs.Empty);
}
}
public void UnionWith(IEnumerable<T> other) {
ArgumentNullException.ThrowIfNull(other);
foreach (T otherElement in other) {
Add(otherElement);
}
AddRange(other);
}
void ICollection<T>.Add(T item) {
@@ -215,44 +219,66 @@ public sealed class ConcurrentHashSet<T> : IReadOnlySet<T>, ISet<T> where T : no
public bool AddRange(IEnumerable<T> items) {
ArgumentNullException.ThrowIfNull(items);
bool result = false;
bool modified = false;
foreach (T _ in items.Where(Add)) {
result = true;
foreach (T _ in items.Where(item => BackingCollection.TryAdd(item, true))) {
modified = true;
}
return result;
if (modified) {
OnModified?.Invoke(this, EventArgs.Empty);
}
return modified;
}
[PublicAPI]
public bool RemoveRange(IEnumerable<T> items) {
ArgumentNullException.ThrowIfNull(items);
bool result = false;
bool modified = false;
foreach (T _ in items.Where(Remove)) {
result = true;
foreach (T _ in items.Where(item => BackingCollection.TryRemove(item, out _))) {
modified = true;
}
return result;
if (modified) {
OnModified?.Invoke(this, EventArgs.Empty);
}
return modified;
}
[PublicAPI]
public int RemoveWhere(Predicate<T> match) {
ArgumentNullException.ThrowIfNull(match);
return BackingCollection.Keys.Where(match.Invoke).Count(key => BackingCollection.TryRemove(key, out _));
int count = BackingCollection.Keys.Where(match.Invoke).Count(key => BackingCollection.TryRemove(key, out _));
if (count > 0) {
OnModified?.Invoke(this, EventArgs.Empty);
}
return count;
}
[PublicAPI]
public bool ReplaceIfNeededWith(IReadOnlyCollection<T> other) {
public bool ReplaceIfNeededWith(IEnumerable<T> other) {
ArgumentNullException.ThrowIfNull(other);
if (SetEquals(other)) {
ICollection<T> otherCollection = other as ICollection<T> ?? other.ToHashSet();
if (SetEquals(otherCollection)) {
return false;
}
ReplaceWith(other);
BackingCollection.Clear();
foreach (T item in otherCollection) {
BackingCollection.TryAdd(item, true);
}
OnModified?.Invoke(this, EventArgs.Empty);
return true;
}
@@ -261,7 +287,6 @@ public sealed class ConcurrentHashSet<T> : IReadOnlySet<T>, ISet<T> where T : no
public void ReplaceWith(IEnumerable<T> other) {
ArgumentNullException.ThrowIfNull(other);
Clear();
UnionWith(other);
ReplaceIfNeededWith(other);
}
}

View File

@@ -24,6 +24,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using Nito.AsyncEx;
@@ -60,6 +61,12 @@ public sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
ArgumentOutOfRangeException.ThrowIfNegative(index);
using (Lock.WriterLock()) {
T oldValue = BackingCollection[index];
if (EqualityComparer<T>.Default.Equals(oldValue, value)) {
return;
}
BackingCollection[index] = value;
}
@@ -86,6 +93,10 @@ public sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
public void Clear() {
using (Lock.WriterLock()) {
if (BackingCollection.Count == 0) {
return;
}
BackingCollection.Clear();
}
@@ -155,9 +166,15 @@ public sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
public void ReplaceWith(IEnumerable<T> collection) {
ArgumentNullException.ThrowIfNull(collection);
ICollection<T> newCollection = collection as ICollection<T> ?? collection.ToList();
using (Lock.WriterLock()) {
if (BackingCollection.SequenceEqual(newCollection)) {
return;
}
BackingCollection.Clear();
BackingCollection.AddRange(collection);
BackingCollection.AddRange(newCollection);
}
OnModified?.Invoke(this, EventArgs.Empty);

View File

@@ -65,6 +65,7 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
}
BackingDictionary[key] = value;
OnModified?.Invoke(this, EventArgs.Empty);
}
}
@@ -111,6 +112,7 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
}
BackingDictionary.Clear();
OnModified?.Invoke(this, EventArgs.Empty);
}

View File

@@ -105,12 +105,6 @@ public abstract class SerializableFile : IDisposable {
string newFilePath = $"{serializableFile.FilePath}.new";
if (File.Exists(serializableFile.FilePath)) {
string currentJson = await File.ReadAllTextAsync(serializableFile.FilePath).ConfigureAwait(false);
if (json == currentJson) {
return;
}
await File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false);
File.Replace(newFilePath, serializableFile.FilePath, null);
@@ -156,12 +150,6 @@ public abstract class SerializableFile : IDisposable {
// We always want to write entire content to temporary file first, in order to never load corrupted data, also when target file doesn't exist
#pragma warning disable CA3003 // Ignored due to caller's intent
if (File.Exists(filePath)) {
string currentJson = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
if (json == currentJson) {
return true;
}
await File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false);
File.Replace(newFilePath, filePath, null);