This commit is contained in:
2026-06-15 18:18:16 +08:00
parent 97c9fba14e
commit 2b9f134e5f
4164 changed files with 386922 additions and 79 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 566c5447b0e061e4c90deb0de9a596e5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,260 @@
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.PlatformSupport.Threading;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
namespace Best.HTTP.Shared.Databases
{
public sealed class FolderAndFileOptions
{
public string FolderName = "Best.HTTP.Shared.Databases";
public string DatabaseFolderName = "Databases";
public string MetadataExtension = "metadata";
public string DatabaseExtension = "db";
public string DatabaseFreeListExtension = "freelist";
public string HashExtension = "hash";
}
public abstract class Database<ContentType, MetadataType, IndexingServiceType, MetadataServiceType> : IDisposable, IHeartbeat
where MetadataType : Metadata, new()
where IndexingServiceType : IndexingService<ContentType, MetadataType>
where MetadataServiceType : MetadataService<MetadataType, ContentType>
{
public static FolderAndFileOptions FolderAndFileOptions = new FolderAndFileOptions();
public string SaveDir { get; private set; }
public string Name { get { return this.Options.Name; } }
public string MetadataFileName { get { return Path.ChangeExtension(Path.Combine(this.SaveDir, this.Name), FolderAndFileOptions.MetadataExtension); } }
public string DatabaseFileName { get { return Path.ChangeExtension(Path.Combine(this.SaveDir, this.Name), FolderAndFileOptions.DatabaseExtension); } }
public string DatabaseFreeListFileName { get { return Path.ChangeExtension(Path.Combine(this.SaveDir, this.Name), FolderAndFileOptions.DatabaseFreeListExtension); } }
public string HashFileName { get { return Path.ChangeExtension(Path.Combine(this.SaveDir, this.Name), FolderAndFileOptions.HashExtension); } }
public MetadataServiceType MetadataService { get; private set; }
protected DatabaseOptions Options { get; private set; }
protected IndexingServiceType IndexingService { get; private set; }
protected DiskManager<ContentType> DiskManager { get; private set; }
protected int isDirty = 0;
protected ReaderWriterLockSlim rwlock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
public Database(string directory,
DatabaseOptions options,
IndexingServiceType indexingService,
IDiskContentParser<ContentType> diskContentParser,
MetadataServiceType metadataService)
{
this.SaveDir = directory;
this.Options = options;
this.IndexingService = indexingService;
this.MetadataService = metadataService;
var dir = Path.GetDirectoryName(this.DatabaseFileName);
if (!HTTPManager.IOService.DirectoryExists(dir))
HTTPManager.IOService.DirectoryCreate(dir);
this.DiskManager = new DiskManager<ContentType>(
HTTPManager.IOService.CreateFileStream(this.DatabaseFileName, Best.HTTP.Shared.PlatformSupport.FileSystem.FileStreamModes.OpenReadWrite),
HTTPManager.IOService.CreateFileStream(this.DatabaseFreeListFileName, Best.HTTP.Shared.PlatformSupport.FileSystem.FileStreamModes.OpenReadWrite),
diskContentParser,
options.DiskManager);
using (var fileStream = HTTPManager.IOService.CreateFileStream(this.MetadataFileName, Best.HTTP.Shared.PlatformSupport.FileSystem.FileStreamModes.OpenReadWrite))
using (var stream = new BufferedStream(fileStream))
this.MetadataService.LoadFrom(stream);
}
public int Clear()
{
using (new WriteLock(this.rwlock))
{
int count = this.MetadataService.Metadatas.Count;
this.IndexingService.Clear();
this.DiskManager.Clear();
this.MetadataService.Clear();
FlagDirty(1);
return count;
}
}
public int Delete(IEnumerable<MetadataType> metadatas)
{
if (metadatas == null)
return 0;
using (new WriteLock(this.rwlock))
{
int deletedCount = 0;
foreach (var metadata in metadatas)
if (DeleteMetadata(metadata))
deletedCount++;
FlagDirty(deletedCount);
return deletedCount;
}
}
public int Delete(IEnumerable<int> metadataIndexes)
{
if (metadataIndexes == null)
return 0;
using (new WriteLock(this.rwlock))
{
int deletedCount = 0;
foreach (int idx in metadataIndexes)
{
var metadata = this.MetadataService.Metadatas[idx];
if (DeleteMetadata(metadata))
deletedCount++;
}
FlagDirty(deletedCount);
return deletedCount;
}
}
protected bool DeleteMetadata(MetadataType metadata)
{
if (metadata.Length > 0)
this.DiskManager.Delete(metadata);
this.MetadataService.Remove(metadata);
FlagDirty(1);
return true;
}
/// <summary>
/// Loads the first content from the metadata indexes.
/// </summary>
public ContentType FromFirstMetadataIndex(IEnumerable<int> metadataIndexes)
{
if (metadataIndexes == null)
return default;
var index = metadataIndexes.DefaultIfEmpty(-1).First();
if (index < 0)
return default;
return FromMetadataIndex(index);
}
/// <summary>
/// Loads the content from the metadata index.
/// </summary>
public ContentType FromMetadataIndex(int metadataIndex)
{
if (metadataIndex < 0 || metadataIndex >= this.MetadataService.Metadatas.Count)
return default;
//using (new ReadLock(this.rwlock))
{
var metadata = this.MetadataService.Metadatas[metadataIndex];
return this.DiskManager.Load(metadata);
}
}
public ContentType FromMetadata(MetadataType metadata) => this.DiskManager.Load(metadata);
/// <summary>
/// Loads all content from the metadatas.
/// </summary>
public IEnumerable<ContentType> FromMetadatas(IEnumerable<MetadataType> metadatas) => FromMetadataIndexes(from m in metadatas select m.Index);
/// <summary>
/// Loads all content from the metadata indexes.
/// </summary>
public IEnumerable<ContentType> FromMetadataIndexes(IEnumerable<int> metadataIndexes)
{
if (metadataIndexes == null)
yield break;
//using (new ReadLock(this.rwlock))
{
foreach (int metadataIndex in metadataIndexes)
{
var metadata = this.MetadataService.Metadatas[metadataIndex];
var content = this.DiskManager.Load(metadata);
//result.Add(content);
yield return content;
}
}
}
protected void FlagDirty(int dirty)
{
if (dirty != 0 && Interlocked.CompareExchange(ref this.isDirty, dirty, 0) == 0)
HTTPManager.Heartbeats.Subscribe(this);
}
public bool Save()
{
if (!this.rwlock.TryEnterWriteLock(TimeSpan.FromMilliseconds(0)))
return false;
try
{
int itWasDirty = Interlocked.CompareExchange(ref this.isDirty, 0, 1);
if (itWasDirty == 0)
return true;
using (var fileStream = HTTPManager.IOService.CreateFileStream(this.MetadataFileName, Best.HTTP.Shared.PlatformSupport.FileSystem.FileStreamModes.Create))
using (var stream = new BufferedStream(fileStream))
this.MetadataService.SaveTo(stream);
if (this.Options.UseHashFile)
{
using (var hashStream = HTTPManager.IOService.CreateFileStream(this.HashFileName, Best.HTTP.Shared.PlatformSupport.FileSystem.FileStreamModes.Create))
{
var hash = this.DiskManager.CalculateHash();
hashStream.Write(hash.Data, 0, hash.Count);
BufferPool.Release(hash);
}
}
this.DiskManager.Save();
Interlocked.Exchange(ref this.isDirty, 0);
return true;
}
finally
{
this.rwlock.ExitWriteLock();
}
}
void IHeartbeat.OnHeartbeatUpdate(DateTime now, TimeSpan dif)
{
if (this.Save())
HTTPManager.Heartbeats.Unsubscribe(this);
}
public void Dispose()
{
Save();
HTTPManager.Heartbeats.Unsubscribe(this);
this.DiskManager.Dispose();
this.DiskManager = null;
this.rwlock.Dispose();
this.rwlock = null;
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8a166a5458783404a94c362f12b90e88
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,15 @@
namespace Best.HTTP.Shared.Databases
{
public class DatabaseOptions
{
public string Name;
public bool UseHashFile;
public DiskManagerOptions DiskManager = new DiskManagerOptions();
public DatabaseOptions(string dbName)
{
this.Name = dbName;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2a5df3217523b8e468cf636c4b5ee3fa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,295 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.PlatformSupport.Threading;
using Best.HTTP.Shared.Streams;
namespace Best.HTTP.Shared.Databases
{
public sealed class DiskManagerOptions
{
// avg. size of certificates is 1390 (calculated from 2621 intermediate certificate)
public int MaxCacheSizeInBytes = 5 * 1024;
public string HashDigest = "SHA256";
}
public interface IDiskContentParser<T>
{
T Parse(Stream stream, int length);
void Encode(Stream stream, T content);
}
public sealed class DiskManager<T> : IDisposable
{
// TODO: store usage date/count and delete the oldest/least used?
struct CachePointer<CacheType>
{
public static readonly CachePointer<CacheType> Empty = new CachePointer<CacheType> { Position = -1, Length = -1, Content = default(CacheType) };
public int Position;
public int Length;
public CacheType Content;
public override string ToString()
{
return $"[CachePointer<{this.Content.GetType().Name}>({Position}, {Length}, {Content})]";
}
}
/// <summary>
/// Sum size of the cached contents
/// </summary>
public int CacheSize { get; private set; }
private Stream stream;
private List<CachePointer<T>> cache = new List<CachePointer<T>>();
private IDiskContentParser<T> diskContentParser;
private DiskManagerOptions options;
private FreeListManager freeListManager;
private ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
public DiskManager(Stream stream, Stream freeListStream, IDiskContentParser<T> contentParser, DiskManagerOptions options)
{
if (!stream.CanSeek)
throw new ArgumentException("DiskManager - stream can't seek!");
this.stream = stream;
this.freeListManager = new FreeListManager(freeListStream);
this.diskContentParser = contentParser;
this.options = options;
}
public (int, int) Append(T content)
{
using (new WriteLock(this.rwLock))
{
int pos = -1;
int length = -1;
using (var buffer = new BufferPoolMemoryStream())
{
diskContentParser.Encode(buffer, content);
length = (int)buffer.Length;
var idx = this.freeListManager.FindFreeIndex(length);
if (idx >= 0)
pos = this.freeListManager.Occupy(idx, length);
else
pos = (int)this.stream.Length;
this.stream.Seek(pos, SeekOrigin.Begin);
buffer.Seek(0, SeekOrigin.Begin);
buffer.CopyTo(this.stream);
}
this.stream.Flush();
return (pos, length);
}
}
public void SaveChanged(Metadata metadata, T content)
{
using var _ = new WriteLock(this.rwLock);
int pos = -1;
int length = -1;
using (var buffer = new BufferPoolMemoryStream())
{
diskContentParser.Encode(buffer, content);
length = (int)buffer.Length;
this.freeListManager.Add(metadata.FilePosition, metadata.Length);
var idx = this.freeListManager.FindFreeIndex(length);
if (idx >= 0)
pos = this.freeListManager.Occupy(idx, length);
else
pos = (int)this.stream.Length;
this.stream.Seek(pos, SeekOrigin.Begin);
buffer.Seek(0, SeekOrigin.Begin);
buffer.CopyTo(this.stream);
}
this.stream.Flush();
metadata.FilePosition = pos;
metadata.Length = length;
}
public void Delete(Metadata metadata)
{
using (new WriteLock(this.rwLock))
{
this.freeListManager.Add(metadata.FilePosition, metadata.Length);
this.stream.Seek(metadata.FilePosition, SeekOrigin.Begin);
var buffer = BufferPool.Get(BufferPool.MIN_BUFFER_SIZE, true);
Array.Clear(buffer, 0, (int)BufferPool.MIN_BUFFER_SIZE);
int length = metadata.Length;
int iterationCount = length / (int)BufferPool.MIN_BUFFER_SIZE;
for (int i = 0; i < iterationCount; ++i)
{
this.stream.Write(buffer, 0, (int)BufferPool.MIN_BUFFER_SIZE);
length -= (int)BufferPool.MIN_BUFFER_SIZE;
}
this.stream.Write(buffer, 0, length);
this.stream.Flush();
BufferPool.Release(buffer);
}
}
public T Load(Metadata metadata)
{
using (new WriteLock(this.rwLock))
{
T parsedContent = default(T);
var cachePointer = GetCached(metadata.FilePosition);
if (cachePointer.Position != -1 && cachePointer.Content != null)
return cachePointer.Content;
this.stream.Seek(metadata.FilePosition, SeekOrigin.Begin);
parsedContent = diskContentParser.Parse(this.stream, metadata.Length);
AddToCache(parsedContent, metadata.FilePosition, metadata.Length);
return parsedContent;
}
}
public List<KeyValuePair<Meta, T>> LoadAll<Meta>(List<Meta> metadatas) where Meta : Metadata
{
using (new WriteLock(this.rwLock))
{
if (metadatas == null || metadatas.Count == 0)
return null;
metadatas.Sort((m1, m2) => m1.FilePosition.CompareTo(m2.FilePosition));
List<KeyValuePair<Meta, T>> result = new List<KeyValuePair<Meta, T>>(metadatas.Count);
for (int i = 0; i < metadatas.Count; ++i)
{
var metadata = metadatas[i];
result.Add(new KeyValuePair<Meta, T>(metadata, Load(metadata)));
}
return result;
}
}
public void Clear()
{
using (new WriteLock(this.rwLock))
{
this.freeListManager.Clear();
this.stream.SetLength(0);
this.stream.Flush();
this.cache.Clear();
this.CacheSize = 0;
}
}
private CachePointer<T> GetCached(int position)
{
for (int i = 0; i < this.cache.Count; ++i)
{
var cache = this.cache[i];
if (cache.Position == position)
return cache;
}
return CachePointer<T>.Empty;
}
private void AddToCache(T parsedContent, int pos, int length)
{
if (this.options.MaxCacheSizeInBytes >= length)
{
this.cache.Insert(0, new CachePointer<T>
{
Position = pos,
Length = length,
Content = parsedContent
});
this.CacheSize += length;
}
while (this.CacheSize > this.options.MaxCacheSizeInBytes && this.cache.Count > 0)
{
var removingCache = this.cache[this.cache.Count - 1];
this.cache.RemoveAt(this.cache.Count - 1);
this.CacheSize -= removingCache.Length;
}
}
public BufferSegment CalculateHash()
{
using (new WriteLock(this.rwLock))
{
this.stream.Seek(0, SeekOrigin.Begin);
#if UNITY_WEBGL && !UNITY_EDITOR
var hash = System.Security.Cryptography.HashAlgorithm.Create(this.options.HashDigest);
var result = hash.ComputeHash(this.stream);
return new BufferSegment(result, 0, result.Length);
#elif !BESTHTTP_DISABLE_ALTERNATE_SSL
var digest = Best.HTTP.SecureProtocol.Org.BouncyCastle.Security.DigestUtilities.GetDigest(this.options.HashDigest);
byte[] buffer = BufferPool.Get(4 * 1024, true);
int readCount = 0;
while ((readCount = this.stream.Read(buffer, 0, buffer.Length)) > 0)
{
digest.BlockUpdate(buffer, 0, readCount);
}
BufferPool.Release(buffer);
byte[] result = BufferPool.Get(digest.GetDigestSize(), true);
int length = digest.DoFinal(result, 0);
return new BufferSegment(result, 0, length);
#else
throw new NotImplementedException(nameof(CalculateHash));
#endif
}
}
public void Save()
{
this.stream.Flush();
this.freeListManager.Save();
}
public void Dispose()
{
this.freeListManager.Dispose();
this.stream.Flush();
this.stream.Close();
this.stream = null;
this.rwLock.Dispose();
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 07c2801d01675b0479bc135ee8327fd1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,181 @@
using System;
using System.Collections.Generic;
using System.IO;
using Best.HTTP.Shared.Databases.Utils;
namespace Best.HTTP.Shared.Databases
{
public sealed class FreeListManager : IDisposable
{
struct FreeSpot
{
public int pos;
public int length;
}
private Stream stream;
private List<FreeSpot> freeList = new List<FreeSpot>();
public FreeListManager(Stream stream)
{
this.stream = stream;
Load();
}
private void Load()
{
this.freeList.Clear();
this.stream.Seek(0, SeekOrigin.Begin);
if (this.stream.Length == 0)
return;
try
{
uint count = (uint)stream.DecodeUnsignedVariableByteInteger();
for (int i = 0; i < count; ++i)
{
int pos = (int)stream.DecodeUnsignedVariableByteInteger();
int length = (int)stream.DecodeUnsignedVariableByteInteger();
this.freeList.Add(new FreeSpot { pos = pos, length = length });
}
}
catch
{
this.freeList.Clear();
this.stream.SetLength(0);
}
}
public void Save()
{
if (this.freeList.Count == 0)
{
this.stream.SetLength(0);
return;
}
int count = this.freeList.Count;
this.stream.Seek(0, SeekOrigin.Begin);
stream.EncodeUnsignedVariableByteInteger((uint)count);
for (int i = 0; i < count; ++i)
{
FreeSpot spot = this.freeList[i];
stream.EncodeUnsignedVariableByteInteger((uint)spot.pos);
stream.EncodeUnsignedVariableByteInteger((uint)spot.length);
}
this.stream.Flush();
}
public int FindFreeIndex(int length)
{
for (int i = 0; i < this.freeList.Count; ++i)
{
FreeSpot spot = this.freeList[i];
if (spot.length >= length)
return i;
}
return -1;
}
public int Occupy(int idx, int length)
{
FreeSpot spot = this.freeList[idx];
int position = spot.pos;
if (spot.length < length)
throw new Exception($"Can't Occupy a free spot with smaller space ({spot.length} < {length})!");
if (spot.length > length)
{
spot.pos += length;
spot.length -= length;
this.freeList[idx] = spot;
}
else
this.freeList.RemoveAt(idx);
return position;
}
public void Add(int pos, int length)
{
int insertToIdx = 0;
while (insertToIdx < this.freeList.Count && this.freeList[insertToIdx].pos < pos)
insertToIdx++;
if (insertToIdx > this.freeList.Count)
throw new Exception($"Couldn't find free spot with position '{pos}'!");
bool merged = false;
FreeSpot spot = new FreeSpot { pos = pos, length = length };
if (insertToIdx > 0)
{
var prev = this.freeList[insertToIdx - 1];
// Merge with previous
if (prev.pos + prev.length == pos)
{
prev.length += length;
this.freeList[insertToIdx - 1] = prev;
spot = prev;
merged = true;
}
}
if (insertToIdx < this.freeList.Count)
{
var next = this.freeList[insertToIdx];
// merge with next?
if (spot.pos + spot.length == next.pos)
{
spot.length += next.length;
if (!merged)
{
// Not already merged, extend the one in place
this.freeList[insertToIdx] = spot;
merged = true;
}
else
{
// Already merged. Further extend the previous, and remove the next.
this.freeList[insertToIdx - 1] = spot;
this.freeList.RemoveAt(insertToIdx);
}
}
}
if (!merged)
this.freeList.Insert(insertToIdx, spot);
}
public void Clear()
{
this.freeList.Clear();
}
public void Dispose()
{
if (this.stream != null)
this.stream.Close();
this.stream = null;
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b0aa37cc0a1f5054f8ffb64d2cef5c5a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fc04de29f38ec7c48b0517fc0ba4d8b2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,941 @@
using System;
using System.Collections.Generic;
namespace Best.HTTP.Shared.Databases.Indexing
{
public enum Side
{
Left,
Right
}
/// <summary>
/// Implements most common list functions. With best case (no or only one item) it doesn't do any allocation.
/// </summary>
public struct NoAllocList<T>
{
private T _value;
private bool _hasValue;
private List<T> _values;
public NoAllocList(T value)
{
this._value = value;
this._hasValue = true;
this._values = null;
}
public T this[int index] {
get => this._hasValue ? this._value : this._values[index];
set
{
if (index < 0 || (this._values == null && index > 0))
throw new IndexOutOfRangeException(index.ToString());
if (this._values != null)
this._values[index] = value;
else
{
this._value = value;
this._hasValue = true;
}
}
}
public int Count { get => this._values != null ? this._values.Count : (this._hasValue ? 1 : 0); }
public void Add(T item)
{
if (this._values != null)
this._values.Add(item);
else if (this._hasValue)
{
this._values = new List<T> { this._value, item };
this._value = default(T);
this._hasValue = false;
}
else
{
this._value = item;
this._hasValue = true;
}
}
public void Clear()
{
this._values?.Clear();
this._values = null;
this._value = default(T);
this._hasValue = false;
}
public bool Contains(T item)
{
if (this._values != null)
return this._values.Contains(item);
// This can thrown a NullRefException if _value is null!
return this._hasValue ? this._value.Equals(item) : false;
}
public bool Remove(T item)
{
if (this._values != null)
return this._values.Remove(item);
else if (this._hasValue && this._value.Equals(item))
{
this._value = default(T);
this._hasValue = false;
return true;
}
return false;
}
public void RemoveAt(int index)
{
if (index < 0 || (this._values == null && index > 0))
throw new IndexOutOfRangeException(index.ToString());
if (this._values != null)
this._values.RemoveAt(index);
else
{
this._value = default(T);
this._hasValue = false;
}
}
}
public sealed class Node<KeyT, ValueT>
{
public Node<KeyT, ValueT> Parent, Left, Right;
public KeyT Key { get; private set; }
/// <summary>
/// Depth of the node.
/// </summary>
public int Depth;
/// <summary>
/// Difference between LeftDepth and RightDepth.
/// </summary>
public int BalanceFactor { get { return this.LeftDepth - this.RightDepth; } }
/// <summary>
/// Left node's Depth, or -1 if it's null.
/// </summary>
public int LeftDepth { get { return this.Left == null ? -1 : this.Left.Depth; } }
/// <summary>
/// Right node's Depth, or -1 if it's null.
/// </summary>
public int RightDepth { get { return this.Right == null ? -1 : this.Right.Depth; } }
public bool IsRoot { get { return this.Parent == null; } }
public int ChildCount { get { return (this.Left == null ? 0 : 1) + (this.Right == null ? 0 : 1); } }
// Stored values aren't public as modifing them requires special care.
private NoAllocList<ValueT> _item;
public Node(Node<KeyT, ValueT> parent, KeyT key, ValueT value)
{
this.Parent = parent;
this.Key = key;
this._item = new NoAllocList<ValueT>(value);
// Depth is 0 by default, as it has no child
this.Depth = 0;
}
public void BubbleUpDepthChange()
{
var current = this;
while (current != null)
{
var oldDepth = current.Depth;
current.Depth = Math.Max(current.LeftDepth, current.RightDepth) + 1;
if (oldDepth != current.Depth)
current = current.Parent;
else
break;
}
}
public ValueT this[int index] { get => this._item[index]; }
public int Count { get => this._item.Count; }
public void Clear() => this._item = new NoAllocList<ValueT>();
public void Add(ValueT value)
{
var tmp = this._item;
tmp.Add(value);
this._item = tmp;
}
public bool Remove(ValueT value)
{
var tmp = this._item;
var result = tmp.Remove(value);
if (result)
this._item = tmp;
return result;
}
public List<ValueT> ToList()
{
var list = new List<ValueT>(this._item.Count);
for (int i = 0; i < this._item.Count; ++i)
list.Add(this._item[i]);
return list;
}
public override string ToString()
{
return $"{this.Left?.Key.ToString()} <- {this.Key.ToString()} -> {this.Right?.Key.ToString()}";
}
}
// https://www.codesdope.com/course/data-structures-avl-trees/
public sealed class AVLTree<Key, Value>
{
public int ElemCount { get; private set; }
public int NodeCount { get; private set; }
public IComparer<Key> Comparer;
public Node<Key, Value> RootNode { get; private set; } = null;
public AVLTree(IComparer<Key> comparer)
{
this.Comparer = comparer;
}
public void Add(Key key, Value item, bool clearValues = false)
{
if (this.RootNode == null) {
this.NodeCount++;
this.ElemCount++;
this.RootNode = new Node<Key, Value>(null, key, item);
return;
}
var current = this.RootNode;
do
{
// +--------------------+-----------------------+
// | Value | Meaning |
// +--------------------+-----------------------+
// | Less than zero | x is less than y. |
// | Zero | x equals y. |
// | Greater than zero | x is greater than y. |
// +--------------------------------------------+
int comp = this.Comparer.Compare(/*x: */ current.Key, /*y: */ key);
// equals
if (comp == 0)
{
if (clearValues)
{
this.ElemCount -= current.Count;
current.Clear();
}
current.Add(item);
break;
}
// current's key > key
if (comp > 0)
{
// insert new node
if (current.Left == null)
{
current.Left = new Node<Key, Value>(current, key, item);
current.BubbleUpDepthChange(/*Side.Left, 1*/);
current = current.Left;
this.NodeCount++;
break;
}
else
{
current = current.Left;
continue;
}
}
// current's key < key
if (comp < 0)
{
// insert new node
if (current.Right == null)
{
current.Right = new Node<Key, Value>(current, key, item);
current.BubbleUpDepthChange(/*Side.Right, 1*/);
current = current.Right;
this.NodeCount++;
break;
}
else
{
current = current.Right;
continue;
}
}
} while (true);
this.ElemCount++;
while (RebalanceFrom(current) != null)
;
//TestBalance(this.root);
}
public bool TestBalance() => TestBalance(this.RootNode);
private bool TestBalance(Node<Key, Value> node)
{
if (node == null)
return true;
if (Math.Abs(node.BalanceFactor) > 1)
{
//UnityEngine.Debug.Break();
return false;
}
return TestBalance(node.Left) && TestBalance(node.Right);
}
List<Side> path = new List<Side>(2);
private Node<Key, Value> RebalanceFrom(Node<Key, Value> newNode)
{
if (newNode.IsRoot || newNode.Parent.IsRoot)
return null;
path.Clear();
// find first unbalanced node or exit when found the root node (root still can be unbalanced!)
var current = newNode;
var balanceFactor = current.BalanceFactor;
while (!current.IsRoot && Math.Abs(balanceFactor) <= 1)
{
if (current.Parent.Left == current)
path.Add(Side.Left);
else
path.Add(Side.Right);
current = current.Parent;
balanceFactor = current.BalanceFactor;
}
// it's a balanced tree
if (Math.Abs(balanceFactor) <= 1)
return null;
Side last = path[path.Count - 1];// path[path.StartIdx];
Side prev = path[path.Count - 2];// path[path.EndIdx];
if (last == Side.Left && prev == Side.Left)
{
// insertion to a left child of a left child
RotateRight(current)
.BubbleUpDepthChange();
current.BubbleUpDepthChange();
}
else if (last == Side.Right && prev == Side.Right)
{
// insertion to a right child of a right child
RotateLeft(current)
.BubbleUpDepthChange();
current.BubbleUpDepthChange();
}
else if (last == Side.Right && prev == Side.Left)
{
// insertion to a left child of a right child
var current_right = current.Right;
RotateRight(current.Right);
RotateLeft(current);
current_right.BubbleUpDepthChange();
current.BubbleUpDepthChange();
}
else if (last == Side.Left && prev == Side.Right)
{
// insertion to a right child of a left child
var current_left = current.Left;
RotateLeft(current.Left);
RotateRight(current);
current_left.BubbleUpDepthChange();
current.BubbleUpDepthChange();
}
return current;
}
public void Clear()
{
this.RootNode = null;
this.ElemCount = 0;
this.NodeCount = 0;
}
public List<Value> Remove(Key key)
{
if (this.RootNode == null)
return null;
var current = this.RootNode;
do
{
int comp = this.Comparer.Compare(current.Key, key);
// equals
if (comp == 0)
{
this.NodeCount--;
this.ElemCount -= current.Count;
// remove current node from the tree
RemoveNode(current);
return current.ToList();
}
// current's key > key
if (comp > 0)
{
if (current.Left == null)
return null;
else
{
current = current.Left;
continue;
}
}
// current's key < key
if (comp < 0)
{
if (current.Right == null)
return null;
else
{
current = current.Right;
continue;
}
}
} while (true);
}
public void Remove(Key key, Value value)
{
if (this.RootNode == null)
return;
var current = this.RootNode;
do
{
int comp = this.Comparer.Compare(current.Key, key);
// equals
if (comp == 0)
{
if (current.Remove(value))
this.ElemCount--;
if (current.Count == 0)
{
// remove current node from the tree
RemoveNode(current);
this.NodeCount--;
}
return;
}
// current's key > key
if (comp > 0)
{
if (current.Left == null)
return ;
else
{
current = current.Left;
continue;
}
}
// current's key < key
if (comp < 0)
{
if (current.Right == null)
return ;
else
{
current = current.Right;
continue;
}
}
} while (true);
}
/// <summary>
/// Removes node and reparent any child it has.
/// </summary>
private void RemoveNode(Node<Key, Value> node)
{
var parent = node.Parent;
Side side = parent?.Left == node ? Side.Left : Side.Right;
int childCount = node.ChildCount;
var testForRebalanceNode = parent;
switch(childCount)
{
case 0:
// node has no child
if (parent == null)
{
this.RootNode = null;
}
else
{
if (parent.Left == node)
parent.Left = null;
else
parent.Right = null;
parent.BubbleUpDepthChange();
}
node.Parent = null;
break;
case 1:
// re-parent the only child
// Example: Removing node 25 will replace it with 30
//
// 20
// 15 25
// 30
//
var child = node.Left ?? node.Right;
if (parent == null)
{
this.RootNode = child;
this.RootNode.Parent = null;
}
else
{
if (parent.Left == node)
parent.Left = child;
else
parent.Right = child;
child.Parent = parent;
parent.BubbleUpDepthChange();
}
break;
default:
// two child
// 1: Replace 20 with 25
//
// 20 20
// 15 25 15 25
// 30
//
// 2: Replace 20 with 22
//
// 20
// 15 25
// 22
// 3: Re-parent 23 for 25, replace 20 with 22
//
// 20
// 15 25
// 22
// 23
// Cases 1 and 3 are the same, both 25 and 22 has a right child. But in case 3, 22 isn't first child of 20!
// Find node with the least Key, that's a node without a left node so we have to deal only with its right node.
var nodeToReplaceWith = FindMin(node.Right);
testForRebalanceNode = nodeToReplaceWith;
side = Side.Right;
// re-parent 23 in case 3:
if (nodeToReplaceWith.Parent != node)
{
testForRebalanceNode = nodeToReplaceWith.Parent;
if (nodeToReplaceWith.Parent.Left == nodeToReplaceWith)
{
nodeToReplaceWith.Parent.Left = nodeToReplaceWith.Right;
side = Side.Left;
}
else
{
nodeToReplaceWith.Parent.Right = nodeToReplaceWith.Right;
side = Side.Right;
}
if (nodeToReplaceWith.Right != null)
nodeToReplaceWith.Right.Parent = nodeToReplaceWith.Parent;
}
if (parent == null)
this.RootNode = nodeToReplaceWith;
else
{
if (parent.Left == node)
parent.Left = nodeToReplaceWith;
else
parent.Right = nodeToReplaceWith;
}
nodeToReplaceWith.Parent = parent;
// Reparent node's left
nodeToReplaceWith.Left = node.Left;
node.Left.Parent = nodeToReplaceWith;
// Reparent node's right node, if it's not the one we replaceing it with
if (node.Right != nodeToReplaceWith)
{
nodeToReplaceWith.Right = node.Right;
node.Right.Parent = nodeToReplaceWith;
}
//else
// nodeToReplaceWith.Right = null;
if (testForRebalanceNode != nodeToReplaceWith)
testForRebalanceNode.BubbleUpDepthChange();
nodeToReplaceWith.BubbleUpDepthChange();
break;
}
while (RebalanceForRemoval(testForRebalanceNode, side) != null)
;
//TestBalance(this.root);
}
private Node<Key, Value> RebalanceForRemoval(Node<Key, Value> removedParentNode, Side side)
{
if (removedParentNode == null)
return null;
path.Clear();
path.Add(side);
// find first unbalanced node or exit when found the root node (root still can be unbalanced!)
var current = removedParentNode;
while (!current.IsRoot && Math.Abs(current.BalanceFactor) <= 1)
{
if (current.Parent.Left == current)
path.Add(Side.Left);
else
path.Add(Side.Right);
current = current.Parent;
}
// it's a balanced tree
if (Math.Abs(current.BalanceFactor) <= 1)
return null;
// from what direction we came from
Side fromDirection = path[path.Count - 1];
// check weather it's an inside or outside case
switch (fromDirection)
{
case Side.Right:
{
bool isOutside = current.Left.LeftDepth >= current.Left.RightDepth;
if (isOutside)
{
RotateRight(current)
.BubbleUpDepthChange();
current.BubbleUpDepthChange();
}
else
{
var current_left = current.Left;
RotateLeft(current.Left);
RotateRight(current);
current_left.BubbleUpDepthChange();
current.BubbleUpDepthChange();
}
}
break;
case Side.Left:
{
bool isOutside = current.Right.RightDepth >= current.Right.LeftDepth;
if (isOutside)
{
// Example: Removing node 14 result in a disbalance in node 20
//
// 20
// 15 25
// (14) 22 26
// 27
var current_right = current.Right; // node 25
RotateLeft(current);
// After RotateLeft(current: node 20):
// 25
// 20 26
// 15 22 27
current.BubbleUpDepthChange(); // node 20
current_right.BubbleUpDepthChange(); // node 25
}
else
{
// Example: Removing node 14 results in a disbalance in node 20.
//
// 20
// 15 25
// (14) 22 26
// 23
var current_right = current.Right;
RotateRight(current.Right);
// After RotateRight(current.Right: node 22):
// 20
// 15 22
// 25
// 23 26
RotateLeft(current);
// After RotateLeft(current: node 20):
// 22
// 20 25
// 15 23 26
current.BubbleUpDepthChange();
current_right.BubbleUpDepthChange();
}
}
break;
}
return current;
}
private Node<Key, Value> FindMin(Node<Key, Value> node)
{
var current = node;
while (current.Left != null)
current = current.Left;
return current;
}
private Node<Key, Value> FindMax(Node<Key, Value> node)
{
var current = node;
while (current.Right != null)
current = current.Right;
return current;
}
public List<Value> Find(Key key) {
if (this.RootNode == null)
return null;
var current = this.RootNode;
do
{
int comp = this.Comparer.Compare(current.Key, key);
// equals
if (comp == 0)
return current.ToList();
// current's key > key
if (comp > 0)
{
if (current.Left == null)
return null;
else
{
current = current.Left;
continue;
}
}
// current's key < key
if (comp < 0)
{
if (current.Right == null)
return null;
else
{
current = current.Right;
continue;
}
}
} while (true);
}
public IEnumerable<Value> WalkHorizontal()
{
if (this.RootNode == null)
yield break;
Queue<Node<Key, Value>> toWalk = new Queue<Node<Key, Value>>();
toWalk.Enqueue(this.RootNode);
while (toWalk.Count > 0)
{
var current = toWalk.Dequeue();
if (current.Left != null)
toWalk.Enqueue(current.Left);
if (current.Right != null)
toWalk.Enqueue(current.Right);
for (int i = 0; i < current.Count; i++)
yield return current[i];
}
}
public bool ContainsKey(Key key)
{
if (this.RootNode == null)
return false;
var current = this.RootNode;
do
{
int comp = this.Comparer.Compare(current.Key, key);
// equals
if (comp == 0)
return true;
// current's key > key
if (comp > 0)
{
if (current.Left == null)
return false;
else
{
current = current.Left;
continue;
}
}
// current's key < key
if (comp < 0)
{
if (current.Right == null)
return false;
else
{
current = current.Right;
continue;
}
}
} while (true);
}
private Node<Key, Value> RotateRight(Node<Key, Value> current)
{
// Current\
// 20 15
// 15 10 20
// 10 ? ?
var parent = current.Parent;
var leftChild = current.Left;
// re-parent left child
if (parent != null)
{
if (parent.Left == current)
parent.Left = leftChild;
else
parent.Right = leftChild;
}
else
this.RootNode = leftChild;
leftChild.Parent = parent;
// re-parent left child's right child
if (leftChild.Right != null)
leftChild.Right.Parent = current;
current.Left = leftChild.Right;
// re-parent current
current.Parent = leftChild;
leftChild.Right = current;
// return with the node that took the place of current
return leftChild;
}
private Node<Key, Value> RotateLeft(Node<Key, Value> current)
{
// /Current
// 20 15
// 15 20 10
// ? 10 ?
var parent = current.Parent;
var rightChild = current.Right;
// re-parent right child
if (parent != null)
{
if (parent.Left == current)
parent.Left = rightChild;
else
parent.Right = rightChild;
}
else
this.RootNode = rightChild;
rightChild.Parent = parent;
// re-parent right child's left child
if (rightChild.Left != null)
rightChild.Left.Parent = current;
current.Right = rightChild.Left;
// re-parent current
current.Parent = rightChild;
rightChild.Left = current;
return rightChild;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7cc4a6484b91bfd489fab519ef4f61eb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1cefb9c5ecaff6a44b77039a56050aac
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
namespace Best.HTTP.Shared.Databases.Indexing.Comparers
{
public sealed class ByteArrayComparer : IComparer<byte[]>
{
public int Compare(byte[] x, byte[] y)
{
int result = x.Length.CompareTo(y.Length);
if (result != 0)
return result;
for (int i = 0; i < x.Length; ++i)
{
result = x[i].CompareTo(y[i]);
if (result != 0)
return result;
}
return 0;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e92d1b70cb4146d47b88bfb3f43c49f8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
namespace Best.HTTP.Shared.Databases.Indexing.Comparers
{
public sealed class DateTimeComparer : IComparer<DateTime>
{
public int Compare(DateTime x, DateTime y)
{
return x.CompareTo(y);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4c788ada3967bfe4dac4ce9e7522dc8c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
namespace Best.HTTP.Shared.Databases.Indexing.Comparers
{
public sealed class Hash128Comparer : IComparer<UnityEngine.Hash128>
{
public int Compare(UnityEngine.Hash128 x, UnityEngine.Hash128 y)
{
return x.CompareTo(y);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0d8da4c912810a742a1af50bb90fb26e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace Best.HTTP.Shared.Databases.Indexing.Comparers
{
public sealed class StringComparer : IComparer<string>
{
public int Compare(string x, string y)
{
return x.CompareTo(y);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c484a2e93b3a84746939c4d2d5cdf8d9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
namespace Best.HTTP.Shared.Databases.Indexing.Comparers
{
public sealed class UInt16Comparer : IComparer<UInt16>
{
public int Compare(ushort x, ushort y)
{
return x.CompareTo(y);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bedd5a1e08a99404fbc747e79b92f17c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
namespace Best.HTTP.Shared.Databases.Indexing.Comparers
{
public sealed class UInt32Comparer : IComparer<UInt32>
{
public int Compare(UInt32 x, UInt32 y)
{
return x.CompareTo(y);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7b03ac89356268b40b90d8a46678c699
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
namespace Best.HTTP.Shared.Databases
{
public abstract class IndexingService<ContentType, MetadataType> where MetadataType : Metadata
{
/// <summary>
/// Index newly added metadata
/// </summary>
public virtual void Index(MetadataType metadata) { }
/// <summary>
/// Remove metadata from all indexes.
/// </summary>
public virtual void Remove(MetadataType metadata) { }
/// <summary>
/// Clear all indexes
/// </summary>
public virtual void Clear() { }
/// <summary>
/// Get indexes in an optimized order. This is usually one of the indexes' WalkHorizontal() call.
/// </summary>
public virtual IEnumerable<int> GetOptimizedIndexes() => null;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 161054408a1ecbe4383c76ea478332ed
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,42 @@
using System;
using System.IO;
using Best.HTTP.Shared.Databases.Utils;
namespace Best.HTTP.Shared.Databases
{
public abstract class Metadata
{
public int Index;
public int FilePosition;
public int Length;
public bool IsDeleted => this.FilePosition == -1 && this.Length == -1;
public void MarkForDelete()
{
this.FilePosition = -1;
this.Length = -1;
}
public virtual void SaveTo(Stream stream)
{
if (this.IsDeleted)
throw new Exception($"Trying to save a deleted metadata({this.ToString()})!");
stream.EncodeUnsignedVariableByteInteger((uint)this.FilePosition);
stream.EncodeUnsignedVariableByteInteger((uint)this.Length);
}
public virtual void LoadFrom(Stream stream)
{
this.FilePosition = (int)stream.DecodeUnsignedVariableByteInteger();
this.Length = (int)stream.DecodeUnsignedVariableByteInteger();
}
public override string ToString()
{
return $"[Metadata Idx: {Index}, Pos: {FilePosition}, Length: {Length}, IsDeleted: {IsDeleted}]";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dc2cf4b60fcca484e9aafe612ba9ac51
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0bb24b6286ceff24abd558107641cd8b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
namespace Best.HTTP.Shared.Databases.MetadataIndexFinders
{
public sealed class DefaultEmptyMetadataIndexFinder<MetadataType> : IEmptyMetadataIndexFinder<MetadataType> where MetadataType : Metadata
{
public int FindFreeIndex(List<MetadataType> metadatas)
{
return metadatas.Count;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e3a9d8df83b70404782d3d67884dc1a9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
namespace Best.HTTP.Shared.Databases.MetadataIndexFinders
{
public sealed class FindDeletedMetadataIndexFinder<MetadataType> : IEmptyMetadataIndexFinder<MetadataType> where MetadataType : Metadata
{
public int FindFreeIndex(List<MetadataType> metadatas)
{
for (int i = 0; i < metadatas.Count; ++i)
if (metadatas[i].IsDeleted)
return i;
return metadatas.Count;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8a63cdf33c00b6443bcbd6aa81aa6562
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
namespace Best.HTTP.Shared.Databases.MetadataIndexFinders
{
public interface IEmptyMetadataIndexFinder<MetadataType> where MetadataType : Metadata
{
int FindFreeIndex(List<MetadataType> metadatas);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e825306d81906834b84135765d5c3174
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.IO;
using Best.HTTP.Shared.Databases.MetadataIndexFinders;
namespace Best.HTTP.Shared.Databases
{
public struct MetadataStats
{
public int min;
public int max;
public int count;
public int sum;
}
public abstract class MetadataService<MetadataType, ContentType> where MetadataType : Metadata, new()
{
public List<MetadataType> Metadatas { get; protected set; }
public IndexingService<ContentType, MetadataType> IndexingService { get; protected set; }
public IEmptyMetadataIndexFinder<MetadataType> EmptyMetadataIndexFinder { get; protected set; }
protected MetadataService(IndexingService<ContentType, MetadataType> indexingService, IEmptyMetadataIndexFinder<MetadataType> emptyMetadataIndexFinder)
{
this.IndexingService = indexingService;
this.EmptyMetadataIndexFinder = emptyMetadataIndexFinder;
this.Metadatas = new List<MetadataType>();
}
/// <summary>
/// Called when metadata loaded from file
/// </summary>
public virtual MetadataType CreateFrom(Stream stream)
{
var metadata = new MetadataType();
metadata.Index = this.Metadatas.Count;
metadata.LoadFrom(stream);
this.Metadatas.Add(metadata);
this.IndexingService.Index(metadata);
return metadata;
}
/// <summary>
/// Called by a concrete MetadataService implementation to create a new metadata
/// </summary>
protected MetadataType CreateDefault(ContentType content, int filePos, int length, Action<ContentType, MetadataType> setupCallback)
{
var metadata = new MetadataType();
metadata.FilePosition = filePos;
metadata.Length = length;
metadata.Index = this.EmptyMetadataIndexFinder.FindFreeIndex(this.Metadatas);
if (setupCallback != null)
setupCallback(content, metadata);
if (metadata.Index == this.Metadatas.Count)
this.Metadatas.Add(metadata);
else
this.Metadatas[metadata.Index] = metadata;
this.IndexingService.Index(metadata);
return metadata;
}
public void Remove(MetadataType metadata)
{
this.IndexingService.Remove(metadata);
// Mark metadata for deletion. Next time we save, it's not going to written out to disk.
metadata.MarkForDelete();
}
public void SaveTo(Stream stream)
{
stream.SetLength(0);
var optimizedIndexes = this.IndexingService.GetOptimizedIndexes();
if (optimizedIndexes != null)
{
foreach (var index in optimizedIndexes)
{
var metadata = this.Metadatas[index];
if (metadata.IsDeleted)
continue;
metadata.SaveTo(stream);
}
}
else
{
for (int i = 0; i < this.Metadatas.Count; ++i)
{
var metadata = this.Metadatas[i];
if (metadata.IsDeleted)
continue;
metadata.SaveTo(stream);
}
}
}
public void LoadFrom(Stream stream)
{
while (stream.Position < stream.Length)
CreateFrom(stream);
}
public virtual void Clear()
{
this.Metadatas.Clear();
this.IndexingService.Clear();
}
public MetadataStats GetStats()
{
int min = int.MaxValue;
int max = 0;
int sum = 0;
int count = 0;
foreach (var metadata in this.Metadatas)
{
if (metadata.IsDeleted)
continue;
if (metadata.Length > max)
max = metadata.Length;
if (metadata.Length < min)
min = metadata.Length;
sum += metadata.Length;
count++;
}
return new MetadataStats
{
count = count,
min = min,
max = max,
sum = sum
};
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 36cd9f102ecc80746bc7ff850f782e32
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ea59aadbd0882aa46a9b5c86d137d70a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,133 @@
using System;
using System.IO;
using System.Text;
using Best.HTTP.Shared.PlatformSupport.Memory;
namespace Best.HTTP.Shared.Databases.Utils
{
public static class StreamUtil
{
public static void WriteLengthPrefixedString(this Stream stream, string str)
{
if (str != null)
{
var byteCount = Encoding.UTF8.GetByteCount(str);
if (byteCount >= 1 << 16)
throw new ArgumentException($"byteCount({byteCount})");
stream.EncodeUnsignedVariableByteInteger((ulong)byteCount);
byte[] tmp = BufferPool.Get(byteCount, true);
Encoding.UTF8.GetBytes(str, 0, str.Length, tmp, 0);
stream.Write(tmp, 0, byteCount);
BufferPool.Release(tmp);
}
else
{
stream.WriteByte(0);
}
}
public static string ReadLengthPrefixedString(this Stream stream)
{
int strLength = (int)stream.DecodeUnsignedVariableByteInteger();
string result = null;
if (strLength != 0)
{
byte[] buffer = BufferPool.Get(strLength, true);
stream.Read(buffer, 0, strLength);
result = System.Text.Encoding.UTF8.GetString(buffer, 0, strLength);
BufferPool.Release(buffer);
}
return result;
}
public static void EncodeUnsignedVariableByteInteger(this Stream encodeTo, ulong value)
{
if (value < 0)
throw new NotSupportedException($"Can't encode negative value({value:N0})!");
byte encodedByte;
do
{
encodedByte = (byte)(value % 128);
value /= 128;
// if there are more data to encode, set the top bit of this byte
if (value > 0)
encodedByte = (byte)(encodedByte | 128);
encodeTo.WriteByte(encodedByte);
}
while (value > 0);
}
public static ulong DecodeUnsignedVariableByteInteger(this Stream decodeFrom)
{
ulong multiplier = 1;
ulong value = 0;
byte encodedByte = 0;
do
{
encodedByte = (byte)decodeFrom.ReadByte();
value += (ulong)((ulong)(encodedByte & 127) * multiplier);
multiplier *= 128;
} while ((encodedByte & 128) != 0);
return value;
}
public static void EncodeSignedVariableByteInteger(this Stream encodeTo, long value)
{
bool more = true;
while (more)
{
byte chunk = (byte)(value & 0x7fL); // extract a 7-bit chunk
value >>= 7;
bool signBitSet = (chunk & 0x40) != 0; // sign bit is the msb of a 7-bit byte, so 0x40
more = !((value == 0 && !signBitSet) || (value == -1 && signBitSet));
if (more) { chunk |= 0x80; } // set msb marker that more bytes are coming
encodeTo.WriteByte(chunk);
};
}
public static long DecodeSignedVariableByteInteger(this Stream stream)
{
long value = 0;
int shift = 0;
bool more = true;
bool signBitSet = false;
while (more)
{
var next = stream.ReadByte();
if (next < 0)
throw new InvalidOperationException("Unexpected end of stream");
byte b = (byte)next;
more = (b & 0x80) != 0; // extract msb
signBitSet = (b & 0x40) != 0; // sign bit is the msb of a 7-bit byte, so 0x40
long chunk = b & 0x7fL; // extract lower 7 bits
value |= chunk << shift;
shift += 7;
};
// extend the sign of shorter negative numbers
if (shift < (sizeof(long) * 8) && signBitSet) { value |= -1L << shift; }
return value;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 031b34e0ebacb3649a074863a87dd5ed
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b8a2c2eff9e98f84cb925568bfb30c8c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,76 @@
using System;
namespace Best.HTTP.Shared.Extensions
{
public sealed class CircularBuffer<T>
{
public int Capacity { get; private set; }
public int Count { get; private set; }
public int StartIdx { get { return this.startIdx; } }
public int EndIdx { get { return this.endIdx; } }
public T this[int idx]
{
get
{
int realIdx = (this.startIdx + idx) % this.Capacity;
return this.buffer[realIdx];
}
set
{
int realIdx = (this.startIdx + idx) % this.Capacity;
this.buffer[realIdx] = value;
}
}
private T[] buffer;
private int startIdx;
private int endIdx;
public CircularBuffer(int capacity)
{
this.Capacity = capacity;
}
public void Add(T element)
{
if (this.buffer == null)
this.buffer = new T[this.Capacity];
this.buffer[this.endIdx] = element;
this.endIdx = (this.endIdx + 1) % this.Capacity;
if (this.endIdx == this.startIdx)
this.startIdx = (this.startIdx + 1) % this.Capacity;
this.Count = Math.Min(this.Count + 1, this.Capacity);
}
public void Clear()
{
this.Count = this.startIdx = this.endIdx = 0;
}
public override string ToString()
{
var sb = PlatformSupport.Text.StringBuilderPool.Get(2);
sb.Append("[");
int idx = this.startIdx;
while (idx != this.endIdx)
{
sb.Append(this.buffer[idx].ToString());
idx = (idx + 1) % this.Capacity;
if (idx != this.endIdx)
sb.Append("; ");
}
sb.Append("]");
return sb.ToString();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d7e67aa2e8ef9344cb04e85bfff0aed5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,593 @@
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.PlatformSupport.Text;
using Best.HTTP.Shared.Streams;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using static Best.HTTP.Hosts.Connections.HTTP1.Constants;
using Cryptography = System.Security.Cryptography;
namespace Best.HTTP.Shared.Extensions
{
public static class Extensions
{
#region ASCII Encoding (These are required because Windows Phone doesn't supports the Encoding.ASCII class.)
/// <summary>
/// On WP8 platform there are no ASCII encoding.
/// </summary>
public static string AsciiToString(this byte[] bytes)
{
StringBuilder sb = StringBuilderPool.Get(bytes.Length); //new StringBuilder(bytes.Length);
foreach (byte b in bytes)
sb.Append(b <= 0x7f ? (char)b : '?');
return StringBuilderPool.ReleaseAndGrab(sb);
}
/// <summary>
/// On WP8 platform there are no ASCII encoding.
/// </summary>
public static BufferSegment GetASCIIBytes(this string str)
{
byte[] result = BufferPool.Get(str.Length, true);
for (int i = 0; i < str.Length; ++i)
{
char ch = str[i];
result[i] = (byte)((ch < (char)0x80) ? ch : '?');
}
return new BufferSegment(result, 0, str.Length);
}
public static void SendAsASCII(this BinaryWriter stream, string str)
{
for (int i = 0; i < str.Length; ++i)
{
char ch = str[i];
stream.Write((byte)((ch < (char)0x80) ? ch : '?'));
}
}
#endregion
#region Headers
public static Dictionary<string, List<string>> AddHeader(this Dictionary<string, List<string>> headers, string name, string value)
{
if (headers == null)
headers = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
List<string> values;
if (!headers.TryGetValue(name, out values))
headers.Add(name, values = new List<string>(1));
values.Add(value);
return headers;
}
public static Dictionary<string, List<string>> SetHeader(this Dictionary<string, List<string>> headers, string name, string value)
{
if (headers == null)
headers = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
List<string> values;
if (!headers.TryGetValue(name, out values))
headers.Add(name, values = new List<string>(1));
values.Clear();
values.Add(value);
return headers;
}
public static bool RemoveHeader(this Dictionary<string, List<string>> headers, string name) => headers != null && headers.Remove(name);
public static void RemoveHeaders(this Dictionary<string, List<string>> headers) => headers?.Clear();
public static List<string> GetHeaderValues(this Dictionary<string, List<string>> headers, string name)
{
if (headers == null)
return null;
List<string> values;
if (!headers.TryGetValue(name, out values) || values.Count == 0)
return null;
return values;
}
public static string GetFirstHeaderValue(this Dictionary<string, List<string>> headers, string name)
{
if (headers == null)
return null;
List<string> values;
if (!headers.TryGetValue(name, out values) || values.Count == 0)
return null;
return values[0];
}
public static bool HasHeaderWithValue(this Dictionary<string, List<string>> headers, string headerName, string value)
{
var values = headers.GetHeaderValues(headerName);
if (values == null)
return false;
for (int i = 0; i < values.Count; ++i)
if (string.Compare(values[i], value, StringComparison.OrdinalIgnoreCase) == 0)
return true;
return false;
}
public static bool HasHeader(this Dictionary<string, List<string>> headers, string headerName)
=> headers != null && headers.ContainsKey(headerName);
#endregion
#region FileSystem WriteLine function support
public static void WriteString(this Stream fs, string value)
{
int count = System.Text.Encoding.UTF8.GetByteCount(value);
var buffer = BufferPool.Get(count, true);
try
{
System.Text.Encoding.UTF8.GetBytes(value, 0, value.Length, buffer, 0);
fs.Write(buffer, 0, count);
}
finally
{
BufferPool.Release(buffer);
}
}
public static void WriteLine(this Stream fs)
{
fs.Write(EOL, 0, 2);
}
public static void WriteLine(this Stream fs, string line)
{
var buff = line.GetASCIIBytes();
try
{
fs.Write(buff.Data, buff.Offset, buff.Count);
fs.WriteLine();
}
finally
{
BufferPool.Release(buff);
}
}
public static void WriteLine(this Stream fs, string format, params object[] values)
{
var buff = string.Format(format, values).GetASCIIBytes();
try
{
fs.Write(buff.Data, buff.Offset, buff.Count);
fs.WriteLine();
}
finally
{
BufferPool.Release(buff);
}
}
#endregion
#region Other Extensions
public static AutoReleaseBuffer AsAutoRelease(this byte[] buffer) => new AutoReleaseBuffer(buffer);
public static BufferSegment AsBuffer(this byte[] bytes)
{
return new BufferSegment(bytes, 0, bytes.Length);
}
public static BufferSegment AsBuffer(this byte[] bytes, int length)
{
return new BufferSegment(bytes, 0, length);
}
public static BufferSegment AsBuffer(this byte[] bytes, int offset, int length)
{
return new BufferSegment(bytes, offset, length);
}
public static BufferSegment CopyAsBuffer(this byte[] bytes, int offset, int length)
{
var newBuff = BufferPool.Get(length, true);
Array.Copy(bytes, offset, newBuff, 0, length);
return newBuff.AsBuffer(0, length);
}
public static string GetRequestPathAndQueryURL(this Uri uri)
{
string requestPathAndQuery = uri.GetComponents(UriComponents.PathAndQuery, UriFormat.UriEscaped);
// http://forum.unity3d.com/threads/best-http-released.200006/page-26#post-2723250
if (string.IsNullOrEmpty(requestPathAndQuery))
requestPathAndQuery = "/";
return requestPathAndQuery;
}
public static string[] FindOption(this string str, string option)
{
//s-maxage=2678400, must-revalidate, max-age=0
string[] options = str.ToLowerInvariant().Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
option = option.ToLowerInvariant();
for (int i = 0; i < options.Length; ++i)
if (options[i].Contains(option))
return options[i].Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
return null;
}
public static string[] FindOption(this string[] options, string option)
{
for (int i = 0; i < options.Length; ++i)
if (options[i].Contains(option))
return options[i].Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
return null;
}
public static void WriteArray(this Stream stream, byte[] array)
{
stream.Write(array, 0, array.Length);
}
public static void WriteBufferSegment(this Stream stream, BufferSegment buffer)
{
stream.Write(buffer.Data, buffer.Offset, buffer.Count);
}
/// <summary>
/// Returns true if the Uri's host is a valid IPv4 or IPv6 address.
/// </summary>
public static bool IsHostIsAnIPAddress(this Uri uri)
{
if (uri == null)
return false;
return IsIpV4AddressValid(uri.Host) || IsIpV6AddressValid(uri.Host);
}
// Original idea from: https://www.code4copy.com/csharp/c-validate-ip-address-string/
// Working regex: https://www.regular-expressions.info/ip.html
private static readonly System.Text.RegularExpressions.Regex validIpV4AddressRegex = new System.Text.RegularExpressions.Regex("\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
/// <summary>
/// Validates an IPv4 address.
/// </summary>
public static bool IsIpV4AddressValid(string address)
{
if (!string.IsNullOrEmpty(address))
return validIpV4AddressRegex.IsMatch(address.Trim());
return false;
}
/// <summary>
/// Validates an IPv6 address.
/// </summary>
public static bool IsIpV6AddressValid(string address)
{
if (!string.IsNullOrEmpty(address))
{
System.Net.IPAddress ip;
if (System.Net.IPAddress.TryParse(address, out ip))
return ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6;
}
return false;
}
#endregion
#region String Conversions
public static int ToInt32(this string str, int defaultValue = default(int)) => int.TryParse(str, out var value) ? value : defaultValue;
public static uint ToUInt32(this string str, uint defaultValue = default) => uint.TryParse(str, out var value) ? value : defaultValue;
public static long ToInt64(this string str, long defaultValue = default(long)) => long.TryParse(str, out var value) ? value : defaultValue;
public static ulong ToUInt64(this string str, ulong defaultValue = default(ulong)) => ulong.TryParse(str, out var value) ? value : defaultValue;
public static DateTime ToDateTime(this string str, DateTime defaultValue = default(DateTime))
{
if (str == null)
return defaultValue;
if (DateTime.TryParse(str, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var value))
return value;
return defaultValue;
}
public static string ToStrOrEmpty(this string str)
{
if (str == null)
return String.Empty;
return str;
}
public static string ToStr(this string str, string defaultVale)
{
if (str == null)
return defaultVale;
return str;
}
public static string ToBinaryStr(this byte value)
{
return Convert.ToString(value, 2).PadLeft(8, '0');
}
#endregion
#region MD5 Hashing
public static string CalculateMD5Hash(this string input)
{
var asciiBuff = input.GetASCIIBytes();
var hash = asciiBuff.CalculateMD5Hash();
BufferPool.Release(asciiBuff);
return hash;
}
public static string CalculateMD5Hash(this BufferSegment input)
{
using (var md5 = Cryptography.MD5.Create())
{
var hash = md5.ComputeHash(input.Data, input.Offset, input.Count);
var sb = StringBuilderPool.Get(hash.Length); //new StringBuilder(hash.Length);
for (int i = 0; i < hash.Length; ++i)
sb.Append(hash[i].ToString("x2"));
BufferPool.Release(hash);
return StringBuilderPool.ReleaseAndGrab(sb);
}
}
#endregion
#region Efficient String Parsing Helpers
internal static string Read(this string str, ref int pos, char block, bool needResult = true)
{
return str.Read(ref pos, (ch) => ch != block, needResult);
}
internal static string Read(this string str, ref int pos, Func<char, bool> block, bool needResult = true)
{
if (pos >= str.Length)
return string.Empty;
str.SkipWhiteSpace(ref pos);
int startPos = pos;
while (pos < str.Length && block(str[pos]))
pos++;
string result = needResult ? str.Substring(startPos, pos - startPos) : null;
// set position to the next char
pos++;
return result;
}
internal static string ReadPossibleQuotedText(this string str, ref int pos)
{
string result = string.Empty;
if (str == null)
return result;
// It's a quoted text?
if (str[pos] == '\"')
{
// Skip the starting quote
str.Read(ref pos, '\"', false);
// Read the text until the ending quote
result = str.Read(ref pos, '\"');
// Next option
str.Read(ref pos, (ch) => ch != ',' && ch != ';', false);
}
else
// It's not a quoted text, so we will read until the next option
result = str.Read(ref pos, (ch) => ch != ',' && ch != ';');
return result;
}
internal static void SkipWhiteSpace(this string str, ref int pos)
{
if (pos >= str.Length)
return;
while (pos < str.Length && char.IsWhiteSpace(str[pos]))
pos++;
}
internal static string TrimAndLower(this string str)
{
if (str == null)
return null;
char[] buffer = new char[str.Length];
int length = 0;
for (int i = 0; i < str.Length; ++i)
{
char ch = str[i];
if (!char.IsWhiteSpace(ch) && !char.IsControl(ch))
buffer[length++] = char.ToLowerInvariant(ch);
}
return new string(buffer, 0, length);
}
internal static char? Peek(this string str, int pos)
{
if (pos < 0 || pos >= str.Length)
return null;
return str[pos];
}
#endregion
#region Specialized String Parsers
//public, max-age=2592000
internal static List<HeaderValue> ParseOptionalHeader(this string str)
{
List<HeaderValue> result = new List<HeaderValue>();
if (str == null)
return result;
int idx = 0;
// process the rest of the text
while (idx < str.Length)
{
// Read key
string key = str.Read(ref idx, (ch) => ch != '=' && ch != ',').TrimAndLower();
HeaderValue qp = new HeaderValue(key);
if (str[idx - 1] == '=')
qp.Value = str.ReadPossibleQuotedText(ref idx);
result.Add(qp);
}
return result;
}
//deflate, gzip, x-gzip, identity, *;q=0
internal static List<HeaderValue> ParseQualityParams(this string str)
{
List<HeaderValue> result = new List<HeaderValue>();
if (str == null)
return result;
int idx = 0;
while (idx < str.Length)
{
string key = str.Read(ref idx, (ch) => ch != ',' && ch != ';').TrimAndLower();
HeaderValue qp = new HeaderValue(key);
if (str[idx - 1] == ';')
{
str.Read(ref idx, '=', false);
qp.Value = str.Read(ref idx, ',');
}
result.Add(qp);
}
return result;
}
#endregion
#region Buffer Filling
/// <summary>
/// Will fill the entire buffer from the stream. Will throw an exception when the underlying stream is closed.
/// </summary>
public static void ReadBuffer(this Stream stream, byte[] buffer)
{
int count = 0;
do
{
int read = stream.Read(buffer, count, buffer.Length - count);
if (read <= 0)
throw ExceptionHelper.ServerClosedTCPStream();
count += read;
} while (count < buffer.Length);
}
public static void ReadBuffer(this Stream stream, byte[] buffer, int length)
{
int count = 0;
do
{
int read = stream.Read(buffer, count, length - count);
if (read <= 0)
throw ExceptionHelper.ServerClosedTCPStream();
count += read;
} while (count < length);
}
#endregion
#region BufferPoolMemoryStream
public static void WriteString(this BufferPoolMemoryStream ms, string str)
{
var byteCount = Encoding.UTF8.GetByteCount(str);
byte[] buffer = BufferPool.Get(byteCount, true);
Encoding.UTF8.GetBytes(str, 0, str.Length, buffer, 0);
ms.Write(buffer, 0, byteCount);
BufferPool.Release(buffer);
}
public static void WriteLine(this BufferPoolMemoryStream ms)
{
ms.Write(EOL, 0, EOL.Length);
}
public static void WriteLine(this BufferPoolMemoryStream ms, string str)
{
ms.WriteString(str);
ms.Write(EOL, 0, EOL.Length);
}
#endregion
#if NET_STANDARD_2_0 || NET_4_6
public static void Clear<T>(this System.Collections.Concurrent.ConcurrentQueue<T> queue)
{
T result;
while (queue.TryDequeue(out result))
;
}
#endif
}
public static class ExceptionHelper
{
public static Exception ServerClosedTCPStream()
{
return new Exception("TCP Stream closed unexpectedly by the remote server");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d1436ef67cdb4ca409385774293711b2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,399 @@
// Based on https://github.com/nickgravelyn/UnityToolbag/blob/master/Future/Future.cs
/*
* The MIT License (MIT)
Copyright (c) 2017, Nick Gravelyn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
* */
using System;
using System.Collections.Generic;
namespace Best.HTTP.Futures
{
/// <summary>
/// Describes the state of a future.
/// </summary>
public enum FutureState
{
/// <summary>
/// The future hasn't begun to resolve a value.
/// </summary>
Pending,
/// <summary>
/// The future is working on resolving a value.
/// </summary>
Processing,
/// <summary>
/// The future has a value ready.
/// </summary>
Success,
/// <summary>
/// The future failed to resolve a value.
/// </summary>
Error
}
/// <summary>
/// Defines the interface of an object that can be used to track a future value.
/// </summary>
/// <typeparam name="T">The type of object being retrieved.</typeparam>
public interface IFuture<T>
{
/// <summary>
/// Gets the state of the future.
/// </summary>
FutureState state { get; }
/// <summary>
/// Gets the value if the State is Success.
/// </summary>
T value { get; }
/// <summary>
/// Gets the failure exception if the State is Error.
/// </summary>
Exception error { get; }
/// <summary>
/// Adds a new callback to invoke when an intermediate result is known.
/// </summary>
/// <param name="callback">The callback to invoke.</param>
/// <returns>The future so additional calls can be chained together.</returns>
IFuture<T> OnItem(FutureValueCallback<T> callback);
/// <summary>
/// Adds a new callback to invoke if the future value is retrieved successfully.
/// </summary>
/// <param name="callback">The callback to invoke.</param>
/// <returns>The future so additional calls can be chained together.</returns>
IFuture<T> OnSuccess(FutureValueCallback<T> callback);
/// <summary>
/// Adds a new callback to invoke if the future has an error.
/// </summary>
/// <param name="callback">The callback to invoke.</param>
/// <returns>The future so additional calls can be chained together.</returns>
IFuture<T> OnError(FutureErrorCallback callback);
/// <summary>
/// Adds a new callback to invoke if the future value is retrieved successfully or has an error.
/// </summary>
/// <param name="callback">The callback to invoke.</param>
/// <returns>The future so additional calls can be chained together.</returns>
IFuture<T> OnComplete(FutureCallback<T> callback);
}
/// <summary>
/// Defines the signature for callbacks used by the future.
/// </summary>
/// <param name="future">The future.</param>
public delegate void FutureCallback<T>(IFuture<T> future);
public delegate void FutureValueCallback<T>(T value);
public delegate void FutureErrorCallback(Exception error);
/// <summary>
/// An implementation of <see cref="IFuture{T}"/> that can be used internally by methods that return futures.
/// </summary>
/// <remarks>
/// Methods should always return the <see cref="IFuture{T}"/> interface when calling code requests a future.
/// This class is intended to be constructed internally in the method to provide a simple implementation of
/// the interface. By returning the interface instead of the class it ensures the implementation can change
/// later on if requirements change, without affecting the calling code.
/// </remarks>
/// <typeparam name="T">The type of object being retrieved.</typeparam>
public class Future<T> : IFuture<T>
{
private volatile FutureState _state;
private T _value;
private Exception _error;
private Func<T> _processFunc;
private readonly List<FutureValueCallback<T>> _itemCallbacks = new List<FutureValueCallback<T>>();
private readonly List<FutureValueCallback<T>> _successCallbacks = new List<FutureValueCallback<T>>();
private readonly List<FutureErrorCallback> _errorCallbacks = new List<FutureErrorCallback>();
private readonly List<FutureCallback<T>> _complationCallbacks = new List<FutureCallback<T>>();
/// <summary>
/// Gets the state of the future.
/// </summary>
public FutureState state { get { return _state; } }
/// <summary>
/// Gets the value if the State is Success.
/// </summary>
public T value
{
get
{
if (_state != FutureState.Success && _state != FutureState.Processing)
{
throw new InvalidOperationException("value is not available unless state is Success or Processing.");
}
return _value;
}
}
/// <summary>
/// Gets the failure exception if the State is Error.
/// </summary>
public Exception error
{
get
{
if (_state != FutureState.Error)
{
throw new InvalidOperationException("error is not available unless state is Error.");
}
return _error;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="Future{T}"/> class.
/// </summary>
public Future()
{
_state = FutureState.Pending;
}
public IFuture<T> OnItem(FutureValueCallback<T> callback)
{
if (_state < FutureState.Success && !_itemCallbacks.Contains(callback))
_itemCallbacks.Add(callback);
return this;
}
/// <summary>
/// Adds a new callback to invoke if the future value is retrieved successfully.
/// </summary>
/// <param name="callback">The callback to invoke.</param>
/// <returns>The future so additional calls can be chained together.</returns>
public IFuture<T> OnSuccess(FutureValueCallback<T> callback)
{
if (_state == FutureState.Success)
{
callback(this.value);
}
else if (_state != FutureState.Error && !_successCallbacks.Contains(callback))
{
_successCallbacks.Add(callback);
}
return this;
}
/// <summary>
/// Adds a new callback to invoke if the future has an error.
/// </summary>
/// <param name="callback">The callback to invoke.</param>
/// <returns>The future so additional calls can be chained together.</returns>
public IFuture<T> OnError(FutureErrorCallback callback)
{
if (_state == FutureState.Error)
{
callback(this.error);
}
else if (_state != FutureState.Success && !_errorCallbacks.Contains(callback))
{
_errorCallbacks.Add(callback);
}
return this;
}
/// <summary>
/// Adds a new callback to invoke if the future value is retrieved successfully or has an error.
/// </summary>
/// <param name="callback">The callback to invoke.</param>
/// <returns>The future so additional calls can be chained together.</returns>
public IFuture<T> OnComplete(FutureCallback<T> callback)
{
if (_state == FutureState.Success || _state == FutureState.Error)
{
callback(this);
}
else
{
if (!_complationCallbacks.Contains(callback))
_complationCallbacks.Add(callback);
}
return this;
}
#pragma warning disable 1998
/// <summary>
/// Begins running a given function on a background thread to resolve the future's value, as long
/// as it is still in the Pending state.
/// </summary>
/// <param name="func">The function that will retrieve the desired value.</param>
public IFuture<T> Process(Func<T> func)
{
if (_state != FutureState.Pending)
{
throw new InvalidOperationException("Cannot process a future that isn't in the Pending state.");
}
BeginProcess();
_processFunc = func;
System.Threading.ThreadPool.QueueUserWorkItem(ThreadFunc);
return this;
}
private void ThreadFunc(object param)
{
try
{
// Directly call the Impl version to avoid the state validation of the public method
AssignImpl(_processFunc());
}
catch (Exception e)
{
// Directly call the Impl version to avoid the state validation of the public method
FailImpl(e);
}
finally
{
_processFunc = null;
}
}
#pragma warning restore 1998
/// <summary>
/// Allows manually assigning a value to a future, as long as it is still in the pending state.
/// </summary>
/// <remarks>
/// There are times where you may not need to do background processing for a value. For example,
/// you may have a cache of values and can just hand one out. In those cases you still want to
/// return a future for the method signature, but can just call this method to fill in the future.
/// </remarks>
/// <param name="value">The value to assign the future.</param>
public void Assign(T value)
{
if (_state != FutureState.Pending && _state != FutureState.Processing)
{
throw new InvalidOperationException("Cannot assign a value to a future that isn't in the Pending or Processing state.");
}
AssignImpl(value);
}
public void BeginProcess(T initialItem = default(T))
{
_state = FutureState.Processing;
_value = initialItem;
}
public void AssignItem(T value)
{
_value = value;
_error = null;
foreach (var callback in _itemCallbacks)
callback(this.value);
}
public void Finish()
{
_state = FutureState.Success;
FlushSuccessCallbacks();
}
/// <summary>
/// Allows manually failing a future, as long as it is still in the pending state.
/// </summary>
/// <remarks>
/// As with the Assign method, there are times where you may know a future value is a failure without
/// doing any background work. In those cases you can simply fail the future manually and return it.
/// </remarks>
/// <param name="error">The exception to use to fail the future.</param>
public void Fail(Exception error)
{
if (_state != FutureState.Pending && _state != FutureState.Processing)
{
throw new InvalidOperationException("Cannot fail future that isn't in the Pending or Processing state.");
}
FailImpl(error);
}
private void AssignImpl(T value)
{
_value = value;
_error = null;
_state = FutureState.Success;
FlushSuccessCallbacks();
}
private void FailImpl(Exception error)
{
_value = default(T);
_error = error;
_state = FutureState.Error;
FlushErrorCallbacks();
}
private void FlushSuccessCallbacks()
{
foreach (var callback in _successCallbacks)
callback(this.value);
FlushComplationCallbacks();
}
private void FlushErrorCallbacks()
{
foreach (var callback in _errorCallbacks)
callback(this.error);
FlushComplationCallbacks();
}
private void FlushComplationCallbacks()
{
foreach (var callback in _complationCallbacks)
callback(this);
ClearCallbacks();
}
private void ClearCallbacks()
{
_itemCallbacks.Clear();
_successCallbacks.Clear();
_errorCallbacks.Clear();
_complationCallbacks.Clear();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 356c46c35b6c6df45abc494c61a7039a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
namespace Best.HTTP.Shared.Extensions
{
/// <summary>
/// Will parse a comma-separeted header value
/// </summary>
public sealed class HeaderParser : KeyValuePairList
{
public HeaderParser(string headerStr)
{
base.Values = Parse(headerStr);
}
private List<HeaderValue> Parse(string headerStr)
{
List<HeaderValue> result = new List<HeaderValue>();
int pos = 0;
try
{
while (pos < headerStr.Length)
{
HeaderValue current = new HeaderValue();
current.Parse(headerStr, ref pos);
result.Add(current);
}
}
catch(System.Exception ex)
{
HTTPManager.Logger.Exception("HeaderParser - Parse", headerStr, ex);
}
return result;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 35bb5ccf89a1dc545a7be69cc867d08d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Best.HTTP.Shared.PlatformSupport.Text;
namespace Best.HTTP.Shared.Extensions
{
/// <summary>
/// Used in string parsers. Its Value is optional.
/// </summary>
public sealed class HeaderValue
{
#region Public Properties
public string Key { get; set; }
public string Value { get; set; }
public List<HeaderValue> Options { get; set; }
public bool HasValue { get { return !string.IsNullOrEmpty(this.Value); } }
#endregion
#region Constructors
public HeaderValue()
{ }
public HeaderValue(string key)
{
this.Key = key;
}
#endregion
#region Public Helper Functions
public void Parse(string headerStr, ref int pos)
{
ParseImplementation(headerStr, ref pos, true);
}
public bool TryGetOption(string key, out HeaderValue option)
{
option = null;
if (Options == null || Options.Count == 0)
return false;
for (int i = 0; i < Options.Count; ++i)
if (String.Equals(Options[i].Key, key, StringComparison.OrdinalIgnoreCase))
{
option = Options[i];
return true;
}
return false;
}
#endregion
#region Private Helper Functions
private void ParseImplementation(string headerStr, ref int pos, bool isOptionIsAnOption)
{
// According to https://tools.ietf.org/html/rfc7234#section-5.2.1.1
// Max-Age has a form "max-age=5", but some (imgur.com specifically) sends it as "max-age:5"
string key = headerStr.Read(ref pos, (ch) => ch != ';' && ch != '=' && ch != ':' && ch != ',', true);
this.Key = key;
char? skippedChar = headerStr.Peek(pos - 1);
bool isValue = skippedChar == '=' || skippedChar == ':';
bool isOption = isOptionIsAnOption && skippedChar == ';';
while ((skippedChar != null && isValue || isOption) && pos < headerStr.Length)
{
if (isValue)
{
string value = headerStr.ReadPossibleQuotedText(ref pos);
this.Value = value;
}
else if (isOption)
{
HeaderValue option = new HeaderValue();
option.ParseImplementation(headerStr, ref pos, false);
if (this.Options == null)
this.Options = new List<HeaderValue>();
this.Options.Add(option);
}
if (!isOptionIsAnOption)
return;
skippedChar = headerStr.Peek(pos - 1);
isValue = skippedChar == '=';
isOption = isOptionIsAnOption && skippedChar == ';';
}
}
#endregion
#region Overrides
public override string ToString()
{
if (this.Options != null && this.Options.Count > 0)
{
StringBuilder sb = StringBuilderPool.Get(4); //new StringBuilder(4);
sb.Append(Key);
sb.Append("=");
sb.Append(Value);
foreach(var option in Options)
{
sb.Append(";");
sb.Append(option.ToString());
}
return StringBuilderPool.ReleaseAndGrab(sb);
}
else if (!string.IsNullOrEmpty(Value))
return Key + '=' + Value;
else
return Key;
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 01b6bb7a29a6e7447baf8768ab665143
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,124 @@
using Best.HTTP.Shared.Logger;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
namespace Best.HTTP.Shared.Extensions
{
public sealed class RunOnceOnMainThread : IHeartbeat
{
private Action _action;
private int _subscribed;
private LoggingContext _context;
public RunOnceOnMainThread(Action action, LoggingContext context)
{
this._action = action;
this._context = context;
}
public void Subscribe()
{
if (Interlocked.CompareExchange(ref this._subscribed, 1, 0) == 0)
HTTPManager.Heartbeats.Subscribe(this);
}
public void OnHeartbeatUpdate(DateTime now, TimeSpan dif)
{
try
{
this._action?.Invoke();
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(RunOnceOnMainThread.OnHeartbeatUpdate), $"{nameof(_action)}", ex, this._context);
}
finally
{
HTTPManager.Heartbeats.Unsubscribe(this);
}
}
}
public interface IHeartbeat
{
void OnHeartbeatUpdate(DateTime utcNow, TimeSpan dif);
}
enum HeartbeatUpdateEventType
{
Subscribe,
Unsubscribe,
Clear
}
struct HeartbeatUpdateEvent
{
public HeartbeatUpdateEventType Event;
public IHeartbeat Heartbeat;
}
/// <summary>
/// A manager class that can handle subscribing and unsubscribeing in the same update.
/// </summary>
public sealed class HeartbeatManager
{
private List<IHeartbeat> _heartbeats = new List<IHeartbeat>();
private DateTime _lastUpdate = DateTime.MinValue;
private ConcurrentQueue<HeartbeatUpdateEvent> _updates = new();
public void Subscribe(IHeartbeat heartbeat)
{
this._updates.Enqueue(new HeartbeatUpdateEvent { Event = HeartbeatUpdateEventType.Subscribe, Heartbeat = heartbeat });
}
public void Unsubscribe(IHeartbeat heartbeat)
{
this._updates.Enqueue(new HeartbeatUpdateEvent { Event = HeartbeatUpdateEventType.Unsubscribe, Heartbeat = heartbeat });
}
public void Update()
{
var now = HTTPManager.CurrentFrameDateTime;
if (_lastUpdate == DateTime.MinValue)
_lastUpdate = now;
else
{
TimeSpan dif = now - _lastUpdate;
_lastUpdate = now;
while (this._updates.TryDequeue(out var updateEvent))
{
switch (updateEvent.Event)
{
case HeartbeatUpdateEventType.Subscribe: this._heartbeats.Add(updateEvent.Heartbeat); break;
case HeartbeatUpdateEventType.Unsubscribe: this._heartbeats.Remove(updateEvent.Heartbeat); break;
case HeartbeatUpdateEventType.Clear: this._heartbeats.Clear(); break;
}
}
for (int i = 0; i < this._heartbeats.Count; ++i)
{
var heartbeat = this._heartbeats[i];
try
{
heartbeat.OnHeartbeatUpdate(now, dif);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("HeartbeatManager", heartbeat.GetType().Name, ex, null);
}
}
}
}
public void Clear()
{
this._updates.Enqueue(new HeartbeatUpdateEvent { Event = HeartbeatUpdateEventType.Clear, Heartbeat = null });
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 169261907c72c004eae68f0a8ae6a8a8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Best.HTTP.Shared.Extensions
{
/// <summary>
/// Base class for specialized parsers
/// </summary>
public class KeyValuePairList
{
public List<HeaderValue> Values { get; protected set; }
public bool TryGet(string valueKeyName, out HeaderValue @param)
{
@param = null;
for (int i = 0; i < Values.Count; ++i)
if (string.CompareOrdinal(Values[i].Key, valueKeyName) == 0)
{
@param = Values[i];
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4ccc301b8038cb64b9e489bafa17d2bc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Best.HTTP.Shared;
using Best.HTTP.Shared.PlatformSupport.Threading;
namespace Best.HTTP.Shared.Extensions
{
public readonly struct TimerData
{
public readonly DateTime Created;
public readonly TimeSpan Interval;
public readonly object Context;
public readonly Func<DateTime, object, bool> OnTimer;
public bool IsOnTime(DateTime now)
{
return now >= this.Created + this.Interval;
}
public TimerData(TimeSpan interval, object context, Func<DateTime, object, bool> onTimer)
{
this.Created = DateTime.UtcNow;
this.Interval = interval;
this.Context = context;
this.OnTimer = onTimer;
}
/// <summary>
/// Create a new TimerData but the Created field will be set to the current time.
/// </summary>
public TimerData CreateNew()
{
return new TimerData(this.Interval, this.Context, this.OnTimer);
}
public override string ToString()
{
return string.Format("[TimerData Created: {0}, Interval: {1}, IsOnTime: {2}]", this.Created.ToString(System.Globalization.CultureInfo.InvariantCulture), this.Interval, this.IsOnTime(DateTime.UtcNow));
}
}
public static class Timer
{
private static List<TimerData> _timers = new List<TimerData>(1);
private static ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
private static int _isSubscribed;
#if UNITY_EDITOR
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)]
public static void ResetSetup()
{
HTTPManager.Logger.Information(nameof(Timer), "Reset called!");
Interlocked.Exchange(ref _isSubscribed, 0);
}
#endif
public static void Add(TimerData timer)
{
using (var _ = new WriteLock(_lock))
_timers.Add(timer);
if (Interlocked.CompareExchange(ref _isSubscribed, 1, 0) == 0)
{
HTTPManager.Logger.Information(nameof(Timer), "Subscribing timer to heartbeats!");
HTTPManager.Heartbeats.Subscribe(new TimerImplementation());
}
}
private sealed class TimerImplementation : IHeartbeat
{
public void OnHeartbeatUpdate(DateTime now, TimeSpan dif)
{
using var __ = new Unity.Profiling.ProfilerMarker(nameof(Timer)).Auto();
using (var _ = new WriteLock(_lock))
{
if (_timers.Count == 0)
{
HTTPManager.Heartbeats.Unsubscribe(this);
Interlocked.Exchange(ref _isSubscribed, 0);
HTTPManager.Logger.Information(nameof(Timer), "Unsubscribing timer from heartbeats!");
return;
}
for (int i = 0; i < _timers.Count; ++i)
{
TimerData timer = _timers[i];
if (timer.IsOnTime(now))
{
try
{
bool repeat = timer.OnTimer(now, timer.Context);
if (repeat)
_timers[i] = timer.CreateNew();
else
_timers.RemoveAt(i--);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(Timer), "OnTimer", ex);
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 70785d1a06f961348acdef24b845ba0c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,394 @@
using Best.HTTP.Caching;
using Best.HTTP.Cookies;
using Best.HTTP.Hosts.Connections;
using Best.HTTP.Hosts.Settings;
using Best.HTTP.HostSetting;
using Best.HTTP.Request.Authentication;
using Best.HTTP.Request.Timings;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.PlatformSupport.Text;
using Best.HTTP.Shared.PlatformSupport.Threading;
using System;
using System.IO;
namespace Best.HTTP.Shared
{
public enum ShutdownTypes
{
Running,
Gentle,
Immediate
}
public delegate void OnSetupFinishedDelegate();
/// <summary>
/// Global entry point to access and manage main services of the plugin.
/// </summary>
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstructionAttribute]
public static partial class HTTPManager
{
/// <summary>
/// Static constructor. Setup default values.
/// </summary>
static HTTPManager()
{
PerHostSettings.Add("*", new HostSettings());
// Set the default logger mechanism
logger = new Best.HTTP.Shared.Logger.ThreadedLogger();
IOService = new Best.HTTP.Shared.PlatformSupport.FileSystem.DefaultIOService();
#if !UNITY_WEBGL || UNITY_EDITOR
ProxyDetector = new HTTP.Proxies.Autodetect.ProxyDetector();
#endif
UserAgent = $"com.Tivadar.Best.HTTP v{typeof(HTTPManager)?.Assembly?.GetName()?.Version}/Unity {UnityEngine.Application.unityVersion}";
}
/// <summary>
/// Delegate for the setup finished event.
/// </summary>
public static OnSetupFinishedDelegate OnSetupFinished;
/// <summary>
/// Instance of the per-host settings manager.
/// </summary>
public static HostSettingsManager PerHostSettings { get; private set; } = new HostSettingsManager();
/// <summary>
/// Cached DateTime value for cases where high resolution isn't needed.
/// </summary>
/// <remarks>Warning!! It must be used only on the main update thread!</remarks>
public static DateTime CurrentFrameDateTime { get; private set; } = DateTime.UtcNow;
/// <summary>
/// By default the plugin will save all cache and cookie data under the path returned by Application.persistentDataPath.
/// You can assign a function to this delegate to return a custom root path to define a new path.
/// <remarks>This delegate will be called on a non Unity thread!</remarks>
/// </summary>
public static Func<string> RootSaveFolderProvider { get; set; }
#if !UNITY_WEBGL || UNITY_EDITOR
public static HTTP.Proxies.Autodetect.ProxyDetector ProxyDetector {
get => _proxyDetector;
set {
_proxyDetector?.Detach();
_proxyDetector = value;
}
}
private static HTTP.Proxies.Autodetect.ProxyDetector _proxyDetector;
#endif
/// <summary>
/// The global, default proxy for all HTTPRequests. The HTTPRequest's Proxy still can be changed per-request. Default value is null.
/// </summary>
public static HTTP.Proxies.Proxy Proxy { get; set; }
/// <summary>
/// Heartbeat manager to use less threads in the plugin. The heartbeat updates are called from the OnUpdate function.
/// </summary>
public static HeartbeatManager Heartbeats
{
get
{
if (heartbeats == null)
heartbeats = new HeartbeatManager();
return heartbeats;
}
}
private static HeartbeatManager heartbeats;
/// <summary>
/// A basic Best.HTTP.Logger.ILogger implementation to be able to log intelligently additional informations about the plugin's internal mechanism.
/// </summary>
public static Best.HTTP.Shared.Logger.ILogger Logger
{
get
{
// Make sure that it has a valid logger instance.
if (logger == null)
{
logger = new ThreadedLogger();
logger.Level = Loglevels.None;
}
return logger;
}
set { logger = value; }
}
private static Best.HTTP.Shared.Logger.ILogger logger;
/// <summary>
/// An IIOService implementation to handle filesystem operations.
/// </summary>
public static Best.HTTP.Shared.PlatformSupport.FileSystem.IIOService IOService;
/// <summary>
/// User-agent string that will be sent with each requests.
/// </summary>
public static string UserAgent;
/// <summary>
/// It's true if the application is quitting and the plugin is shutting down itself.
/// </summary>
public static bool IsQuitting { get { return _isQuitting; } private set { _isQuitting = value; } }
private static volatile bool _isQuitting;
public static string RootFolderName = "com.Tivadar.Best.HTTP.v3";
/// <summary>
/// The local content cache, maintained by the plugin. When set to a non-null value, Maintain called immediately on the cache.
/// </summary>
public static HTTPCache LocalCache
{
get => _httpCache;
set
{
_httpCache?.Dispose();
(_httpCache = value)?.Maintain(contentLength: 0, deleteLockedEntries: true, context: null);
}
}
private static HTTPCache _httpCache;
private static bool IsSetupCalled;
/// <summary>
/// Initializes the HTTPManager with default settings. This method should be called on Unity's main thread before using the HTTP plugin. By default it gets called by <see cref="HTTPUpdateDelegator"/>.
/// </summary>
public static void Setup()
{
if (IsSetupCalled)
return;
IsSetupCalled = true;
IsQuitting = false;
HTTPManager.Logger.Information("HTTPManager", "Setup called! UserAgent: " + UserAgent);
HTTPUpdateDelegator.CheckInstance();
if (LocalCache == null)
{
// this will trigger a maintain call too.
LocalCache = new HTTPCache(new HTTPCacheOptions());
}
CookieJar.SetupFolder();
CookieJar.Load();
try
{
OnSetupFinished?.Invoke();
}
catch(Exception ex)
{
HTTPManager.logger.Exception(nameof(HTTPManager), "OnSetupFinished", ex, null);
}
}
internal static HTTPRequest SendRequest(HTTPRequest request)
{
if (!IsSetupCalled)
Setup();
if (request.IsCancellationRequested || IsQuitting)
return request;
RequestEventHelper.RegisterRequest(request);
if (!request.DownloadSettings.DisableCache)
{
#if !UNITY_WEBGL || UNITY_EDITOR
ThreadedRunner.RunShortLiving<HTTPRequest>((request) =>
{
#endif
var hash = HTTPCache.CalculateHash(request.MethodType, request.CurrentUri);
if (LocalCache.CanServeWithoutValidation(hash, ErrorTypeForValidation.None, request.Context))
LocalCache.Redirect(request, hash);
//RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(request, HTTPRequestStates.Queued, null));
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(request, RequestEvents.QueuedResend));
#if !UNITY_WEBGL || UNITY_EDITOR
}, request);
#endif
}
else
{
//RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(request, HTTPRequestStates.Queued, null));
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(request, RequestEvents.QueuedResend));
}
return request;
}
/// <summary>
/// Will return where the various caches should be saved.
/// </summary>
public static string GetRootSaveFolder()
{
try
{
if (RootSaveFolderProvider != null)
return Path.Combine(RootSaveFolderProvider(), RootFolderName);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(HTTPManager), nameof(GetRootSaveFolder), ex);
}
#if UNITY_SWITCH && !UNITY_EDITOR
throw new NotSupportedException(UnityEngine.Application.platform.ToString());
#else
return Path.Combine(UnityEngine.Application.persistentDataPath, RootFolderName);
#endif
}
#if UNITY_EDITOR
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)]
public static void ResetSetup()
{
IsSetupCalled = false;
Profiler.Network.NetworkStatsCollector.ResetNetworkStats();
#if !UNITY_WEBGL || UNITY_EDITOR
PlatformSupport.Network.DNS.Cache.DNSCache.Clear();
#endif
DigestStore.Clear();
PerHostSettings.Clear();
PerHostSettings.Add("*", new HostSettings());
HostManager.Clear();
RequestEventHelper.Clear();
RequestEventHelper.RemoveAllRegisteredRequests();
LocalCache = null;
HTTPManager.Logger.Information("HTTPManager", "Reset called!");
}
#endif
/// <summary>
/// Updates the HTTPManager. This method should be called regularly from a Unity event (e.g., Update, LateUpdate).
/// It processes various events and callbacks and manages internal tasks.
/// </summary>
public static void OnUpdate()
{
try
{
CurrentFrameDateTime = DateTime.UtcNow;
using (new Unity.Profiling.ProfilerMarker(nameof(RequestEventHelper)).Auto())
RequestEventHelper.ProcessQueue();
using (new Unity.Profiling.ProfilerMarker(nameof(ConnectionEventHelper)).Auto())
ConnectionEventHelper.ProcessQueue();
if (heartbeats != null)
{
using (new Unity.Profiling.ProfilerMarker(nameof(HeartbeatManager)).Auto())
heartbeats.Update();
}
using (new Unity.Profiling.ProfilerMarker(nameof(BufferPool)).Auto())
BufferPool.Maintain();
using (new Unity.Profiling.ProfilerMarker(nameof(StringBuilderPool)).Auto())
StringBuilderPool.Maintain();
#if BESTHTTP_PROFILE && UNITY_2021_2_OR_NEWER
using (new Unity.Profiling.ProfilerMarker("Profile").Auto())
{
// Sent
{
long newNetworkBytesSent = Profiler.Network.NetworkStatsCollector.TotalNetworkBytesSent;
var diff = newNetworkBytesSent - _lastNetworkBytesSent;
_lastNetworkBytesSent = newNetworkBytesSent;
Profiler.Network.NetworkStats.SentSinceLastFrame.Value = diff;
Profiler.Network.NetworkStats.SentTotal.Value = newNetworkBytesSent;
Profiler.Network.NetworkStats.BufferedToSend.Value = Profiler.Network.NetworkStatsCollector.BufferedToSend;
}
// Received
{
long newNetworkBytesReceived = Profiler.Network.NetworkStatsCollector.TotalNetworkBytesReceived;
var diff = newNetworkBytesReceived - _lastNetworkBytesReceived;
_lastNetworkBytesReceived = newNetworkBytesReceived;
Profiler.Network.NetworkStats.ReceivedSinceLastFrame.Value = diff;
Profiler.Network.NetworkStats.ReceivedTotal.Value = newNetworkBytesReceived;
Profiler.Network.NetworkStats.ReceivedAndUnprocessed.Value = Profiler.Network.NetworkStatsCollector.ReceivedAndUnprocessed;
}
// Open/Total connections
Profiler.Network.NetworkStats.OpenConnectionsCounter.Value = Profiler.Network.NetworkStatsCollector.OpenConnections;
Profiler.Network.NetworkStats.TotalConnectionsCounter.Value = Profiler.Network.NetworkStatsCollector.TotalConnections;
// Memory stats
BufferPool.GetStatistics(ref bufferPoolStats);
Profiler.Memory.MemoryStats.Borrowed.Value = bufferPoolStats.Borrowed;
Profiler.Memory.MemoryStats.Pooled.Value = bufferPoolStats.PoolSize;
Profiler.Memory.MemoryStats.CacheHits.Value = bufferPoolStats.GetBuffers;
Profiler.Memory.MemoryStats.ArrayAllocations.Value = bufferPoolStats.ArrayAllocations;
}
#endif
}
catch (Exception ex)
{
HTTPManager.logger.Exception(nameof(HTTPManager), nameof(OnUpdate), ex);
}
}
#if BESTHTTP_PROFILE && UNITY_2021_2_OR_NEWER
private static long _lastNetworkBytesSent = 0;
private static long _lastNetworkBytesReceived = 0;
private static BufferPoolStats bufferPoolStats = default;
#endif
/// <summary>
/// Shuts down the HTTPManager and performs cleanup operations. This method should be called when the application is quitting.
/// </summary>
public static void OnQuit()
{
HTTPManager.Logger.Information("HTTPManager", "OnQuit called!");
IsQuitting = true;
AbortAll();
CookieJar.Persist();
OnUpdate();
HostManager.Clear();
Heartbeats.Clear();
DigestStore.Clear();
}
/// <summary>
/// Aborts all ongoing HTTP requests and performs an immediate shutdown of the HTTPManager.
/// </summary>
public static void AbortAll()
{
HTTPManager.Logger.Information("HTTPManager", "AbortAll called!");
// This is an immediate shutdown request!
RequestEventHelper.AbortAllRequests();
RequestEventHelper.RemoveAllRegisteredRequests();
RequestEventHelper.Clear();
ConnectionEventHelper.Clear();
HostManager.Shutdown();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8302c34d0db94b447a0969d738d37543
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,367 @@
using Best.HTTP.Shared.PlatformSupport.Threading;
using System.Threading;
using UnityEngine;
namespace Best.HTTP.Shared
{
/// <summary>
/// Threading mode the plugin will use to call HTTPManager.OnUpdate().
/// </summary>
public enum ThreadingMode : int
{
/// <summary>
/// HTTPManager.OnUpdate() is called from the HTTPUpdateDelegator's Update functions (Unity's main thread).
/// </summary>
UnityUpdate,
/// <summary>
/// The plugin starts a dedicated thread to call HTTPManager.OnUpdate() periodically.
/// </summary>
Threaded,
/// <summary>
/// HTTPManager.OnUpdate() will not be called automatically.
/// </summary>
None
}
/// <summary>
/// Will route some U3D calls to the HTTPManager.
/// </summary>
[ExecuteInEditMode]
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstructionAttribute]
public sealed class HTTPUpdateDelegator : MonoBehaviour
{
#region Public Properties
/// <summary>
/// The singleton instance of the HTTPUpdateDelegator
/// </summary>
public static HTTPUpdateDelegator Instance { get { return CheckInstance(); } }
private volatile static HTTPUpdateDelegator instance;
/// <summary>
/// True, if the Instance property should hold a valid value.
/// </summary>
public static bool IsCreated { get; private set; }
/// <summary>
/// It's true if the dispatch thread running.
/// </summary>
public static bool IsThreadRunning { get; private set; }
/// <summary>
/// The current threading mode the plugin is in.
/// </summary>
public ThreadingMode CurrentThreadingMode { get { return _currentThreadingMode; } set { SetThreadingMode(value); } }
private ThreadingMode _currentThreadingMode = ThreadingMode.UnityUpdate;
/// <summary>
/// How much time the plugin should wait between two update call. Its default value 100 ms.
/// </summary>
public static int ThreadFrequencyInMS { get; set; }
/// <summary>
/// Called in the OnApplicationQuit function. If this function returns False, the plugin will not start to
/// shut down itself.
/// </summary>
public static System.Func<bool> OnBeforeApplicationQuit;
/// <summary>
/// Called when the Unity application's foreground state changed.
/// </summary>
public static System.Action<bool> OnApplicationForegroundStateChanged;
public int MainThreadId { get => this.mainThreadId; }
#endregion
private static bool isSetupCalled;
private int isHTTPManagerOnUpdateRunning;
private AutoResetEvent pingEvent = new AutoResetEvent(false);
private int updateThreadCount = 0;
private int mainThreadId;
#if UNITY_EDITOR
/// <summary>
/// Called after scene loaded to support Configurable Enter Play Mode (https://docs.unity3d.com/2019.3/Documentation/Manual/ConfigurableEnterPlayMode.html)
/// </summary>
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void ResetSetup()
{
isSetupCalled = false;
instance?.SetThreadingMode(ThreadingMode.UnityUpdate);
HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "Reset called!");
}
#endif
static HTTPUpdateDelegator()
{
ThreadFrequencyInMS = 100;
}
/// <summary>
/// Will create the HTTPUpdateDelegator instance and set it up.
/// </summary>
public static HTTPUpdateDelegator CheckInstance()
{
try
{
if (!IsCreated)
{
GameObject go = GameObject.Find("HTTP Update Delegator");
if (go != null)
instance = go.GetComponent<HTTPUpdateDelegator>();
if (instance == null)
{
go = new GameObject("HTTP Update Delegator");
go.hideFlags = HideFlags.HideAndDontSave;
instance = go.AddComponent<HTTPUpdateDelegator>();
}
IsCreated = true;
#if UNITY_EDITOR
if (!UnityEditor.EditorApplication.isPlaying)
{
UnityEditor.EditorApplication.update -= instance.Update;
UnityEditor.EditorApplication.update += instance.Update;
}
UnityEditor.EditorApplication.playModeStateChanged -= instance.OnPlayModeStateChanged;
UnityEditor.EditorApplication.playModeStateChanged += instance.OnPlayModeStateChanged;
#endif
// https://docs.unity3d.com/ScriptReference/Application-wantsToQuit.html
Application.wantsToQuit -= UnityApplication_WantsToQuit;
Application.wantsToQuit += UnityApplication_WantsToQuit;
HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "Instance Created!");
}
}
catch
{
HTTPManager.Logger.Error(nameof(HTTPUpdateDelegator), "Please call the Best.HTTP.Shared.HTTPManager.Setup() from one of Unity's event(eg. awake, start) before you send any request!");
}
return instance;
}
/// <summary>
/// Destroys the current HTTPUpdateDelegator instance.
/// </summary>
public static void DestroyInstance()
{
IsCreated = false;
IsThreadRunning = false;
instance?.PingUpdateThread();
if (instance)
{
GameObject.DestroyImmediate(instance.gameObject);
instance = null;
}
}
public void Setup()
{
if (isSetupCalled)
return;
isSetupCalled = true;
// Setup is expected to be called on the Unity main thread only.
mainThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), $"Setup called Threading Mode: {this._currentThreadingMode}, MainThreadId: {this.mainThreadId}");
HTTPManager.Setup();
SetThreadingMode(this._currentThreadingMode);
// Unity doesn't tolerate well if the DontDestroyOnLoad called when purely in editor mode. So, we will set the flag
// only when we are playing, or not in the editor.
if (!Application.isEditor || Application.isPlaying)
GameObject.DontDestroyOnLoad(this.gameObject);
HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "Setup done!");
}
/// <summary>
/// Return true if the call happens on the Unity main thread. Setup must be called before to save the thread id!
/// </summary>
public bool IsMainThread() => System.Threading.Thread.CurrentThread.ManagedThreadId == mainThreadId;
/// <summary>
/// Set directly the threading mode to use.
/// </summary>
public void SetThreadingMode(ThreadingMode mode)
{
if (_currentThreadingMode == mode)
return;
HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), $"SetThreadingMode({mode}, {isSetupCalled})");
_currentThreadingMode = mode;
if (!isSetupCalled)
Setup();
switch (_currentThreadingMode)
{
case ThreadingMode.UnityUpdate:
case ThreadingMode.None:
IsThreadRunning = false;
PingUpdateThread();
break;
case ThreadingMode.Threaded:
#if !UNITY_WEBGL || UNITY_EDITOR
ThreadedRunner.RunLongLiving(ThreadFunc);
#else
HTTPManager.Logger.Warning(nameof(HTTPUpdateDelegator), "Threading mode set to ThreadingMode.Threaded, but threads aren't supported under WebGL!");
#endif
break;
}
}
/// <summary>
/// Swaps threading mode between Unity's Update function or a distinct thread.
/// </summary>
public void SwapThreadingMode() => SetThreadingMode(_currentThreadingMode == ThreadingMode.Threaded ? ThreadingMode.UnityUpdate : ThreadingMode.Threaded);
/// <summary>
/// Pings the update thread to call HTTPManager.OnUpdate immediately.
/// </summary>
/// <remarks>Works only when the current threading mode is Threaded!</remarks>
public void PingUpdateThread() => pingEvent.Set();
void ThreadFunc()
{
HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "Update Thread Started");
ThreadedRunner.SetThreadName("Best.HTTP.Update Thread");
try
{
if (Interlocked.Increment(ref updateThreadCount) > 1)
{
HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "An update thread already started.");
return;
}
// Threading mode might be already changed, so set IsThreadRunning to IsThreaded's value.
IsThreadRunning = CurrentThreadingMode == ThreadingMode.Threaded;
while (IsThreadRunning)
{
CallOnUpdate();
pingEvent.WaitOne(ThreadFrequencyInMS);
}
}
finally
{
Interlocked.Decrement(ref updateThreadCount);
HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "Update Thread Ended");
}
}
void Update()
{
if (!isSetupCalled)
Setup();
if (CurrentThreadingMode == ThreadingMode.UnityUpdate)
CallOnUpdate();
}
private void CallOnUpdate()
{
// Prevent overlapping call of OnUpdate from unity's main thread and a separate thread
if (Interlocked.CompareExchange(ref isHTTPManagerOnUpdateRunning, 1, 0) == 0)
{
try
{
HTTPManager.OnUpdate();
}
finally
{
Interlocked.Exchange(ref isHTTPManagerOnUpdateRunning, 0);
}
}
}
#if UNITY_EDITOR
void OnPlayModeStateChanged(UnityEditor.PlayModeStateChange playMode)
{
if (playMode == UnityEditor.PlayModeStateChange.EnteredPlayMode)
{
UnityEditor.EditorApplication.update -= Update;
}
else if (playMode == UnityEditor.PlayModeStateChange.EnteredEditMode)
{
UnityEditor.EditorApplication.update -= Update;
UnityEditor.EditorApplication.update += Update;
HTTPUpdateDelegator.ResetSetup();
HTTPManager.ResetSetup();
}
}
#endif
void OnDisable()
{
HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "OnDisable Called!");
#if UNITY_EDITOR
if (UnityEditor.EditorApplication.isPlaying)
#endif
UnityApplication_WantsToQuit();
}
void OnApplicationPause(bool isPaused)
{
HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "OnApplicationPause isPaused: " + isPaused);
if (HTTPUpdateDelegator.OnApplicationForegroundStateChanged != null)
HTTPUpdateDelegator.OnApplicationForegroundStateChanged(isPaused);
}
private static bool UnityApplication_WantsToQuit()
{
HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "UnityApplication_WantsToQuit Called!");
if (OnBeforeApplicationQuit != null)
{
try
{
if (!OnBeforeApplicationQuit())
{
HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "OnBeforeApplicationQuit call returned false, postponing plugin and application shutdown.");
return false;
}
}
catch (System.Exception ex)
{
HTTPManager.Logger.Exception(nameof(HTTPUpdateDelegator), string.Empty, ex);
}
}
IsThreadRunning = false;
instance?.PingUpdateThread();
if (!IsCreated)
return true;
IsCreated = false;
HTTPManager.OnQuit();
return true;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4cd5cef435d748c4d9b0f7b5cf16341b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3516ff05842cc2b40a7fd4a79efc1fbd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,77 @@
using System;
using System.IO;
using Best.HTTP.Shared.PlatformSupport.FileSystem;
using Best.HTTP.Shared.PlatformSupport.Memory;
namespace Best.HTTP.Shared.Logger
{
/// <summary>
/// Provides an implementation of <see cref="ILogOutput"/> that writes log messages to a file.
/// </summary>
public sealed class FileOutput : ILogOutput
{
/// <summary>
/// Gets a value indicating whether this log output accepts color codes. Always returns <c>false</c>.
/// </summary>
public bool AcceptColor { get; } = false;
private Stream fileStream;
/// <summary>
/// Initializes a new instance of the FileOutput class with the specified file name.
/// </summary>
/// <param name="fileName">The name of the file to write log messages to.</param>
public FileOutput(string fileName)
{
// Create a buffered stream for writing log messages to the specified file.
this.fileStream = new BufferedStream(HTTPManager.IOService.CreateFileStream(fileName, FileStreamModes.Create), 512 * 1024);
}
/// <summary>
/// Writes a log message to the file.
/// </summary>
/// <param name="level">The log level of the message.</param>
/// <param name="logEntry">The log message to write.</param>
public void Write(Loglevels level, string logEntry)
{
if (this.fileStream != null && !string.IsNullOrEmpty(logEntry))
{
int count = System.Text.Encoding.UTF8.GetByteCount(logEntry) + 2;
var buffer = BufferPool.Get(count, true);
try
{
System.Text.Encoding.UTF8.GetBytes(logEntry, 0, logEntry.Length, buffer, 0);
buffer[count - 2] = (byte)'\r';
buffer[count - 1] = (byte)'\n';
this.fileStream.Write(buffer, 0, count);
}
finally
{
BufferPool.Release(buffer);
}
}
}
/// <summary>
/// Flushes any buffered log messages to the file.
/// </summary>
public void Flush() => this.fileStream?.Flush();
/// <summary>
/// Releases any resources used by the FileOutput instance.
/// </summary>
public void Dispose()
{
if (this.fileStream != null)
{
this.fileStream.Close();
this.fileStream = null;
}
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2ad11187a50e381439c128d7d791eb3a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,172 @@
using System;
namespace Best.HTTP.Shared.Logger
{
[HideFromDocumentation]
public sealed class HideFromDocumentation : Attribute
{
}
/// <summary>
/// Available logging levels.
/// </summary>
public enum Loglevels : int
{
/// <summary>
/// All message will be logged.
/// </summary>
All,
/// <summary>
/// Only Informations and above will be logged.
/// </summary>
Information,
/// <summary>
/// Only Warnings and above will be logged.
/// </summary>
Warning,
/// <summary>
/// Only Errors and above will be logged.
/// </summary>
Error,
/// <summary>
/// Only Exceptions will be logged.
/// </summary>
Exception,
/// <summary>
/// No logging will occur.
/// </summary>
None
}
/// <summary>
/// Represents an output target for log messages.
/// </summary>
/// <remarks>
/// <para>
/// This interface defines methods for writing log messages to an output target.
/// Implementations of this interface are used to configure where log messages
/// should be written.
/// </para>
/// <para>
/// Two of its out-of-the-box implementations are
/// <list type="bullet">
/// <item><description><see cref="UnityOutput">UnityOutput</see></description></item>
/// <item><description><see cref="FileOutput">FileOutput</see></description></item>
/// </list>
/// </para>
/// </remarks>
public interface ILogOutput : IDisposable
{
/// <summary>
/// Gets a value indicating whether the log output supports colored text.
/// </summary>
bool AcceptColor { get; }
/// <summary>
/// Writes a log entry to the output.
/// </summary>
/// <param name="level">The logging level of the entry.</param>
/// <param name="logEntry">The log message to write.</param>
void Write(Loglevels level, string logEntry);
/// <summary>
/// Flushes any buffered log entries to the output.
/// </summary>
void Flush();
}
/// <summary>
/// Represents a filter for further sort out what log entries to include in the final log output.
/// </summary>
public interface IFilter
{
/// <summary>
/// Return <c>true</c> if the division must be included in the output.
/// </summary>
bool Include(string division);
}
/// <summary>
/// Represents a logger for recording log messages.
/// </summary>
public interface ILogger
{
/// <summary>
/// Gets or sets the minimum severity level for logging.
/// </summary>
Loglevels Level { get; set; }
/// <summary>
/// Gets or sets the output target for log messages.
/// </summary>
/// <value>
/// The <see cref="ILogOutput"/> instance used to write log messages.
/// </value>
ILogOutput Output { get; set; }
/// <summary>
/// Gets or sets an output filter to decide what messages are included or not.
/// </summary>
/// <value>The <see cref="IFilter"/> instance used for filtering.</value>
IFilter Filter { get; set; }
/// <summary>
/// Property indicating whether the logger's internal queue is empty or not.
/// </summary>
bool IsEmpty { get; }
/// <summary>
/// Gets a value indicating whether diagnostic logging is enabled.
/// </summary>
/// <remarks>
/// Diagnostic logging is enabled when <see cref="Level"/> is set to <see cref="Loglevels.All"/>.
/// </remarks>
bool IsDiagnostic { get; }
/// <summary>
/// Logs a message with <see cref="Loglevels.All"/> level.
/// </summary>
/// <param name="division">The division or category of the log message.</param>
/// <param name="msg">The verbose log message.</param>
/// <param name="context">The optional <see cref="LoggingContext"/> for additional context.</param>
void Verbose(string division, string msg, LoggingContext context = null);
/// <summary>
/// Logs a message with <see cref="Loglevels.Information"/> level.
/// </summary>
/// <param name="division">The division or category of the log message.</param>
/// <param name="msg">The verbose log message.</param>
/// <param name="context">The optional <see cref="LoggingContext"/> for additional context.</param>
void Information(string division, string msg, LoggingContext context = null);
/// <summary>
/// Logs a message with <see cref="Loglevels.Warning"/> level.
/// </summary>
/// <param name="division">The division or category of the log message.</param>
/// <param name="msg">The verbose log message.</param>
/// <param name="context">The optional <see cref="LoggingContext"/> for additional context.</param>
void Warning(string division, string msg, LoggingContext context = null);
/// <summary>
/// Logs a message with <see cref="Loglevels.Error"/> level.
/// </summary>
/// <param name="division">The division or category of the log message.</param>
/// <param name="msg">The verbose log message.</param>
/// <param name="context">The optional <see cref="LoggingContext"/> for additional context.</param>
void Error(string division, string msg, LoggingContext context = null);
/// <summary>
/// Logs a message with <see cref="Loglevels.Exception"/> level.
/// </summary>
/// <param name="division">The division or category of the log message.</param>
/// <param name="msg">The verbose log message.</param>
/// <param name="context">The optional <see cref="LoggingContext"/> for additional context.</param>
void Exception(string division, string msg, Exception ex, LoggingContext context = null);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 81c5c1596e18e4d46beef29fa682d58d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,205 @@
using System.Collections.Generic;
using System.Linq;
namespace Best.HTTP.Shared.Logger
{
/// <summary>
/// Represents a logging context for categorizing and organizing log messages.
/// </summary>
/// <remarks>
/// The LoggingContext class is used to provide additional context information
/// to log messages, allowing for better categorization and organization of log output. It can be
/// associated with specific objects or situations to enrich log entries with context-specific data.
/// </remarks>
public sealed class LoggingContext
{
/// <summary>
/// Gets the unique hash value of this logging context.
/// </summary>
public string Hash { get; private set; }
public enum LoggingContextFieldType
{
Long,
Bool,
String,
AnotherContext
}
public struct LoggingContextField
{
public string key;
public long longValue;
public bool boolValue;
public string stringValue;
public LoggingContext loggingContextValue;
public LoggingContextFieldType fieldType;
public override string ToString()
{
object value = this.fieldType switch
{
LoggingContextFieldType.Bool => this.boolValue,
LoggingContextFieldType.Long => this.longValue,
LoggingContextFieldType.String => this.stringValue,
_ => this.loggingContextValue
};
return $"[{this.key} => '{value}']";
}
}
public List<LoggingContextField> fields = new List<LoggingContextField>(8);
/// <summary>
/// Initializes a new instance of the LoggingContext class associated with the specified object.
/// </summary>
/// <param name="boundto">The object to associate the context with.</param>
public LoggingContext(object boundto)
{
var name = boundto.GetType().Name;
Add("TypeName", name);
UnityEngine.Hash128 hash = new UnityEngine.Hash128();
hash.Append(name);
hash.Append(boundto.GetHashCode());
hash.Append(this.GetHashCode());
this.Hash = hash.ToString();
Add("Hash", this.Hash);
}
/// <summary>
/// Adds a <c>long</c> value to the logging context.
/// </summary>
/// <param name="key">The key to associate with the value.</param>
/// <param name="value">The <c>long</c> value to add.</param>
public void Add(string key, long value) => Add(new LoggingContextField { fieldType = LoggingContextFieldType.Long, key = key, longValue = value });
/// <summary>
/// Adds a <c>bool</c> value to the logging context.
/// </summary>
/// <param name="key">The key to associate with the value.</param>
/// <param name="value">The <c>bool</c> value to add.</param>
public void Add(string key, bool value) => Add(new LoggingContextField { fieldType = LoggingContextFieldType.Bool, key = key, boolValue = value });
/// <summary>
/// Adds a <c>string</c> value to the logging context.
/// </summary>
/// <param name="key">The key to associate with the value.</param>
/// <param name="value">The <c>string</c> value to add.</param>
public void Add(string key, string value) => Add(new LoggingContextField { fieldType = LoggingContextFieldType.String, key = key, stringValue = value });
/// <summary>
/// Adds a <c>LoggingContext</c> value to the logging context.
/// </summary>
/// <param name="key">The key to associate with the value.</param>
/// <param name="value">The <c>LoggingContext</c> value to add.</param>
public void Add(string key, LoggingContext value) => Add(new LoggingContextField { fieldType = LoggingContextFieldType.AnotherContext, key = key, loggingContextValue = value });
private void Add(LoggingContextField field)
{
Remove(field.key);
this.fields.Add(field);
}
/// <summary>
/// Gets the <c>string</c> field with the specified name from the logging context.
/// </summary>
/// <param name="fieldName">The name of the <c>string</c> field to retrieve.</param>
/// <returns>The value of the <c>string</c> field or <c>null</c> if not found.</returns>
public string GetStringField(string fieldName) => this.fields.FirstOrDefault(f => f.key == fieldName).stringValue;
/// <summary>
/// Removes a field from the logging context by its key.
/// </summary>
/// <param name="key">The key of the field to remove.</param>
public void Remove(string key)
{
for (int i = 0; i < this.fields.Count; i++)
{
var field = this.fields[i];
if (field.key.Equals(key, System.StringComparison.Ordinal))
this.fields.RemoveAt(i--);
}
}
/// <summary>
/// Converts the logging context and its associated fields to a JSON string representation.
/// </summary>
/// <param name="sb">A <see cref="System.Text.StringBuilder"/> instance to which the JSON string is appended.</param>
/// <remarks>
/// This method serializes the logging context and its associated fields into a JSON format
/// for structured logging purposes. The resulting JSON string represents the context and its fields, making it
/// suitable for inclusion in log entries for better analysis and debugging.
/// </remarks>
public void ToJson(System.Text.StringBuilder sb)
{
if (this.fields == null || this.fields.Count == 0)
{
sb.Append("null");
return;
}
sb.Append("{");
for (int i = 0; i < this.fields.Count; ++i)
{
var field = this.fields[i];
if (field.fieldType != LoggingContextFieldType.AnotherContext)
{
if (i > 0)
sb.Append(", ");
sb.AppendFormat("\"{0}\": ", field.key);
}
switch (field.fieldType)
{
case LoggingContextFieldType.Long:
sb.Append(field.longValue);
break;
case LoggingContextFieldType.Bool:
sb.Append(field.boolValue ? "true" : "false");
break;
case LoggingContextFieldType.String:
sb.AppendFormat("\"{0}\"", Escape(field.stringValue));
break;
}
}
sb.Append("}");
for (int i = 0; i < this.fields.Count; ++i)
{
var field = this.fields[i];
if (field.loggingContextValue == null)
continue;
switch (field.fieldType)
{
case LoggingContextFieldType.AnotherContext:
sb.Append(", ");
field.loggingContextValue.ToJson(sb);
break;
}
}
}
public static string Escape(string original)
{
return Best.HTTP.Shared.PlatformSupport.Text.StringBuilderPool.ReleaseAndGrab(Best.HTTP.Shared.PlatformSupport.Text.StringBuilderPool.Get(1)
.Append(original)
.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("/", "\\/")
.Replace("\b", "\\b")
.Replace("\f", "\\f")
.Replace("\n", "\\n")
.Replace("\r", "\\r")
.Replace("\t", "\\t"));
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 32f8edc3b27d9d24384f41757f618d15
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,409 @@
using System;
using System.Collections.Concurrent;
using System.Text;
using Best.HTTP.Shared.PlatformSupport.Text;
using Best.HTTP.Shared.PlatformSupport.Threading;
namespace Best.HTTP.Shared.Logger
{
/// <summary>
/// <see cref="IFilter"/> implementation to include only one division in the log output.
/// </summary>
public sealed class SingleDivisionFilter : IFilter
{
private string _division;
public SingleDivisionFilter(string division) => this._division = division;
public bool Include(string division) => this._division.Equals(division, StringComparison.Ordinal);
}
/// <summary>
/// <see cref="IFilter"/> implementation to allow filtering for multiple divisions.
/// </summary>
public sealed class MultiDivisionFilter : IFilter
{
private string[] _divisions;
public MultiDivisionFilter(string[] divisions)
{
this._divisions = divisions;
for (int i = 0; i < this._divisions.Length; ++i)
this._divisions[i] = this._divisions[i].Trim();
}
public bool Include(string division)
{
for (int i = 0; i < this._divisions.Length; ++i)
{
ref var div = ref this._divisions[i];
if (div.Equals(division, StringComparison.Ordinal))
return true;
}
return false;
}
}
public sealed class ThreadedLogger : Best.HTTP.Shared.Logger.ILogger, IDisposable
{
public Loglevels Level { get; set; }
public bool IsDiagnostic { get => this.Level == Loglevels.All; }
public ILogOutput Output { get { return this._output; }
set
{
if (this._output != value)
{
if (this._output != null)
this._output.Dispose();
this._output = value;
}
}
}
private ILogOutput _output;
public IFilter Filter { get; set; }
public bool IsEmpty
#if !UNITY_WEBGL || UNITY_EDITOR
=> this.jobs.IsEmpty;
#else
=> true;
#endif
public int InitialStringBufferCapacity = 256;
#if !UNITY_WEBGL || UNITY_EDITOR
public TimeSpan ExitThreadAfterInactivity = TimeSpan.FromMinutes(1);
public int QueuedJobs { get => this.jobs.Count; }
private ConcurrentQueue<LogJob> jobs = new ConcurrentQueue<LogJob>();
private System.Threading.AutoResetEvent newJobEvent = new System.Threading.AutoResetEvent(false);
private volatile int threadCreated;
private volatile bool isDisposed;
#endif
private StringBuilder sb = new StringBuilder(0);
public ThreadedLogger()
{
this.Level = UnityEngine.Debug.isDebugBuild ? Loglevels.Warning : Loglevels.Error;
this.Output = new UnityOutput();
}
public void Verbose(string division, string msg, LoggingContext context) {
AddJob(Loglevels.All, division, msg, null, context);
}
public void Information(string division, string msg, LoggingContext context) {
AddJob(Loglevels.Information, division, msg, null, context);
}
public void Warning(string division, string msg, LoggingContext context) {
AddJob(Loglevels.Warning, division, msg, null, context);
}
public void Error(string division, string msg, LoggingContext context) {
AddJob(Loglevels.Error, division, msg, null, context);
}
public void Exception(string division, string msg, Exception ex, LoggingContext context) {
AddJob(Loglevels.Exception, division, msg, ex, context);
}
private void AddJob(Loglevels level, string div, string msg, Exception ex, LoggingContext context)
{
if (this.Level > level)
return;
var filter = this.Filter;
if (filter != null && !filter.Include(div))
return;
sb.EnsureCapacity(InitialStringBufferCapacity);
#if !UNITY_WEBGL || UNITY_EDITOR
if (this.isDisposed)
return;
#endif
string json = null;
if (context != null)
{
var jsonBuilder = StringBuilderPool.Get(1);
context.ToJson(jsonBuilder);
json = StringBuilderPool.ReleaseAndGrab(jsonBuilder);
}
var job = new LogJob
{
level = level,
division = div,
msg = msg,
ex = ex,
time = DateTime.UtcNow,
threadId = System.Threading.Thread.CurrentThread.ManagedThreadId,
stackTrace = System.Environment.StackTrace,
context = json
};
#if !UNITY_WEBGL || UNITY_EDITOR
// Start the consumer thread before enqueuing to get up and running sooner
if (System.Threading.Interlocked.CompareExchange(ref this.threadCreated, 1, 0) == 0)
Best.HTTP.Shared.PlatformSupport.Threading.ThreadedRunner.RunLongLiving(ThreadFunc);
this.jobs.Enqueue(job);
try
{
this.newJobEvent.Set();
}
catch
{
try
{
this.Output.Write(job.level, job.ToJson(sb, this.Output.AcceptColor));
}
catch
{ }
return;
}
// newJobEvent might timed out between the previous threadCreated check and newJobEvent.Set() calls closing the thread.
// So, here we check threadCreated again and create a new thread if needed.
if (System.Threading.Interlocked.CompareExchange(ref this.threadCreated, 1, 0) == 0)
Best.HTTP.Shared.PlatformSupport.Threading.ThreadedRunner.RunLongLiving(ThreadFunc);
#else
this.Output.Write(job.level, job.ToJson(sb, this.Output.AcceptColor));
#endif
}
#if !UNITY_WEBGL || UNITY_EDITOR
private void ThreadFunc()
{
ThreadedRunner.SetThreadName("Best.HTTP.Logger");
try
{
LogJob job;
/*
LogJob job = new LogJob
{
level = Loglevels.Information,
division = "ThreadFunc",
msg = "Log thread starting!",
ex = null,
time = DateTime.UtcNow,
threadId = System.Threading.Thread.CurrentThread.ManagedThreadId,
stackTrace = null,
context1 = null,
context2 = null,
context3 = null
};
WriteJob(ref job);
*/
do
{
// Waiting for a new log-job timed out
if (!this.newJobEvent.WaitOne(this.ExitThreadAfterInactivity))
{
/*
job = new LogJob
{
level = Loglevels.Information,
division = "ThreadFunc",
msg = "Log thread quitting!",
ex = null,
time = DateTime.UtcNow,
threadId = System.Threading.Thread.CurrentThread.ManagedThreadId,
stackTrace = null,
context1 = null,
context2 = null,
context3 = null
};
WriteJob(ref job);
*/
// clear StringBuilder's inner cache and exit the thread
sb.Length = 0;
sb.Capacity = 0;
System.Threading.Interlocked.Exchange(ref this.threadCreated, 0);
return;
}
try
{
int count = 0;
while (this.jobs.TryDequeue(out job))
{
WriteJob(ref job);
if (++count >= 1000)
this.Output.Flush();
}
}
finally
{
this.Output.Flush();
}
} while (!HTTPManager.IsQuitting);
System.Threading.Interlocked.Exchange(ref this.threadCreated, 0);
// When HTTPManager.IsQuitting is true, there is still logging that will create a new thread after the last one quit
// and always writing a new entry about the exiting thread would be too much overhead.
// It would also hard to know what's the last log entry because some are generated on another thread non-deterministically.
//var lastLog = new LogJob
//{
// level = Loglevels.All,
// division = "ThreadedLogger",
// msg = "Log Processing Thread Quitting!",
// time = DateTime.UtcNow,
// threadId = System.Threading.Thread.CurrentThread.ManagedThreadId,
//};
//
//this.Output.WriteVerbose(lastLog.ToJson(sb));
}
catch
{
System.Threading.Interlocked.Exchange(ref this.threadCreated, 0);
}
}
void WriteJob(ref LogJob job)
{
try
{
this.Output.Write(job.level, job.ToJson(sb, this.Output.AcceptColor));
}
catch
{ }
}
#endif
public void Dispose()
{
#if !UNITY_WEBGL || UNITY_EDITOR
this.isDisposed = true;
if (this.newJobEvent != null)
{
this.newJobEvent.Close();
this.newJobEvent = null;
}
#endif
if (this.Output != null)
{
this.Output.Dispose();
this.Output = new UnityOutput();
}
GC.SuppressFinalize(this);
}
}
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstructionAttribute]
public struct LogJob
{
private static string[] LevelStrings = new string[] { "Verbose", "Information", "Warning", "Error", "Exception" };
public Loglevels level;
public string division;
public string msg;
public Exception ex;
public DateTime time;
public int threadId;
public string stackTrace;
public string context;
private static string WrapInColor(string str, string color, bool acceptColor)
{
#if UNITY_EDITOR
return acceptColor ? string.Format("<b><color={1}>{0}</color></b>", str, color) : str;
#else
return str;
#endif
}
public string ToJson(StringBuilder sb, bool acceptColor)
{
sb.Length = 0;
sb.AppendFormat("{{\"tid\":{0},\"div\":\"{1}\",\"msg\":\"{2}\"",
WrapInColor(this.threadId.ToString(), "yellow", acceptColor),
WrapInColor(this.division, "yellow", acceptColor),
WrapInColor(LoggingContext.Escape(this.msg), "yellow", acceptColor));
if (ex != null)
{
sb.Append(",\"ex\": [");
Exception exception = this.ex;
while (exception != null)
{
sb.Append("{\"msg\": \"");
sb.Append(LoggingContext.Escape(exception.Message));
sb.Append("\", \"stack\": \"");
sb.Append(LoggingContext.Escape(exception.StackTrace));
sb.Append("\"}");
exception = exception.InnerException;
if (exception != null)
sb.Append(",");
}
sb.Append("]");
}
if (this.stackTrace != null)
{
sb.Append(",\"stack\":\"");
ProcessStackTrace(sb);
sb.Append("\"");
}
else
sb.Append(",\"stack\":\"\"");
if (this.context != null)
{
sb.Append(",\"ctx\": [");
sb.Append(this.context);
sb.Append("]");
}
else
sb.Append(",\"ctxs\":[]");
sb.AppendFormat(",\"t\":{0},\"ll\":\"{1}\",",
this.time.Ticks.ToString(),
LevelStrings[(int)this.level]);
sb.Append("\"bh\":1}");
return sb.ToString();
}
private void ProcessStackTrace(StringBuilder sb)
{
if (string.IsNullOrEmpty(this.stackTrace))
return;
var lines = this.stackTrace.Split('\n');
// skip top 4 lines that would show the logger.
for (int i = 3; i < lines.Length; ++i)
sb.Append(LoggingContext.Escape(lines[i].Replace("Best.HTTP.", "")));
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 780d670e1b8a6a340a1441f844142390
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,50 @@
using System;
namespace Best.HTTP.Shared.Logger
{
/// <summary>
/// Provides an implementation of <see cref="ILogOutput"/> that writes log messages to the Unity Debug Console.
/// </summary>
public sealed class UnityOutput : ILogOutput
{
/// <summary>
/// Gets a value indicating whether this log output accepts color codes.
/// </summary>
/// <remarks>
/// This property returns <c>true</c> when running in the Unity Editor and <c>false</c> otherwise.
/// </remarks>
public bool AcceptColor { get; } = UnityEngine.Application.isEditor;
/// <summary>
/// Writes a log message to the Unity Debug Console based on the specified log level.
/// </summary>
/// <param name="level">The log level of the message.</param>
/// <param name="logEntry">The log message to write.</param>
public void Write(Loglevels level, string logEntry)
{
switch (level)
{
case Loglevels.All:
case Loglevels.Information:
UnityEngine.Debug.Log(logEntry);
break;
case Loglevels.Warning:
UnityEngine.Debug.LogWarning(logEntry);
break;
case Loglevels.Error:
case Loglevels.Exception:
UnityEngine.Debug.LogError(logEntry);
break;
}
}
/// <summary>
/// This implementation does nothing.
/// </summary>
void ILogOutput.Flush() {}
void IDisposable.Dispose() => GC.SuppressFinalize(this);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fd86ab6e6f8fab649b1b2a37452aa80f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d47c08ea57a56cd49b8c37790efced77
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 82bd7454309118f4e9fa14dd5afaae6d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5db467dd3130b1349ab57a28f34db155
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,259 @@
using System;
using System.Linq;
using System.ComponentModel;
using System.Collections.Generic;
using System.Collections;
using specialized = PlatformSupport.Collections.Specialized;
namespace PlatformSupport.Collections.ObjectModel
{
public class ObservableDictionary<TKey, TValue> : IDictionary<TKey, TValue>, specialized.INotifyCollectionChanged, INotifyPropertyChanged
{
private const string CountString = "Count";
private const string IndexerName = "Item[]";
private const string KeysName = "Keys";
private const string ValuesName = "Values";
private IDictionary<TKey, TValue> _Dictionary;
protected IDictionary<TKey, TValue> Dictionary
{
get { return _Dictionary; }
}
#region Constructors
public ObservableDictionary()
{
_Dictionary = new Dictionary<TKey, TValue>();
}
public ObservableDictionary(IDictionary<TKey, TValue> dictionary)
{
_Dictionary = new Dictionary<TKey, TValue>(dictionary);
}
public ObservableDictionary(IEqualityComparer<TKey> comparer)
{
_Dictionary = new Dictionary<TKey, TValue>(comparer);
}
public ObservableDictionary(int capacity)
{
_Dictionary = new Dictionary<TKey, TValue>(capacity);
}
public ObservableDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer)
{
_Dictionary = new Dictionary<TKey, TValue>(dictionary, comparer);
}
public ObservableDictionary(int capacity, IEqualityComparer<TKey> comparer)
{
_Dictionary = new Dictionary<TKey, TValue>(capacity, comparer);
}
#endregion
#region IDictionary<TKey,TValue> Members
public void Add(TKey key, TValue value)
{
Insert(key, value, true);
}
public bool ContainsKey(TKey key)
{
return Dictionary.ContainsKey(key);
}
public ICollection<TKey> Keys
{
get { return Dictionary.Keys; }
}
public bool Remove(TKey key)
{
if (key == null) throw new ArgumentNullException("key");
TValue value;
Dictionary.TryGetValue(key, out value);
var removed = Dictionary.Remove(key);
if (removed)
//OnCollectionChanged(NotifyCollectionChangedAction.Remove, new KeyValuePair<TKey, TValue>(key, value));
OnCollectionChanged();
return removed;
}
public bool TryGetValue(TKey key, out TValue value)
{
return Dictionary.TryGetValue(key, out value);
}
public ICollection<TValue> Values
{
get { return Dictionary.Values; }
}
public TValue this[TKey key]
{
get
{
return Dictionary[key];
}
set
{
Insert(key, value, false);
}
}
#endregion
#region ICollection<KeyValuePair<TKey,TValue>> Members
public void Add(KeyValuePair<TKey, TValue> item)
{
Insert(item.Key, item.Value, true);
}
public void Clear()
{
if (Dictionary.Count > 0)
{
Dictionary.Clear();
OnCollectionChanged();
}
}
public bool Contains(KeyValuePair<TKey, TValue> item)
{
return Dictionary.Contains(item);
}
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
{
Dictionary.CopyTo(array, arrayIndex);
}
public int Count
{
get { return Dictionary.Count; }
}
public bool IsReadOnly
{
get { return Dictionary.IsReadOnly; }
}
public bool Remove(KeyValuePair<TKey, TValue> item)
{
return Remove(item.Key);
}
#endregion
#region IEnumerable<KeyValuePair<TKey,TValue>> Members
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
return Dictionary.GetEnumerator();
}
#endregion
#region IEnumerable Members
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)Dictionary).GetEnumerator();
}
#endregion
#region INotifyCollectionChanged Members
public event specialized.NotifyCollectionChangedEventHandler CollectionChanged;
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
public void AddRange(IDictionary<TKey, TValue> items)
{
if (items == null) throw new ArgumentNullException("items");
if (items.Count > 0)
{
if (Dictionary.Count > 0)
{
if (items.Keys.Any((k) => Dictionary.ContainsKey(k)))
throw new ArgumentException("An item with the same key has already been added.");
else
foreach (var item in items) Dictionary.Add(item);
}
else
_Dictionary = new Dictionary<TKey, TValue>(items);
OnCollectionChanged(specialized.NotifyCollectionChangedAction.Add, items.ToArray());
}
}
private void Insert(TKey key, TValue value, bool add)
{
if (key == null) throw new ArgumentNullException("key");
TValue item;
if (Dictionary.TryGetValue(key, out item))
{
if (add) throw new ArgumentException("An item with the same key has already been added.");
if (Equals(item, value)) return;
Dictionary[key] = value;
OnCollectionChanged(specialized.NotifyCollectionChangedAction.Replace, new KeyValuePair<TKey, TValue>(key, value), new KeyValuePair<TKey, TValue>(key, item));
}
else
{
Dictionary[key] = value;
OnCollectionChanged(specialized.NotifyCollectionChangedAction.Add, new KeyValuePair<TKey, TValue>(key, value));
}
}
private void OnPropertyChanged()
{
OnPropertyChanged(CountString);
OnPropertyChanged(IndexerName);
OnPropertyChanged(KeysName);
OnPropertyChanged(ValuesName);
}
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private void OnCollectionChanged()
{
OnPropertyChanged();
if (CollectionChanged != null) CollectionChanged(this, new specialized.NotifyCollectionChangedEventArgs(specialized.NotifyCollectionChangedAction.Reset));
}
private void OnCollectionChanged(specialized.NotifyCollectionChangedAction action, KeyValuePair<TKey, TValue> changedItem)
{
OnPropertyChanged();
if (CollectionChanged != null) CollectionChanged(this, new specialized.NotifyCollectionChangedEventArgs(action, changedItem));
}
private void OnCollectionChanged(specialized.NotifyCollectionChangedAction action, KeyValuePair<TKey, TValue> newItem, KeyValuePair<TKey, TValue> oldItem)
{
OnPropertyChanged();
if (CollectionChanged != null) CollectionChanged(this, new specialized.NotifyCollectionChangedEventArgs(action, newItem, oldItem));
}
private void OnCollectionChanged(specialized.NotifyCollectionChangedAction action, IList newItems)
{
OnPropertyChanged();
if (CollectionChanged != null) CollectionChanged(this, new specialized.NotifyCollectionChangedEventArgs(action, newItems));
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 902ef36b7f3ace94a83e085968c22e44
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0bded87d86b03f344a35f9b83d8be3b0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,464 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections;
using System.Diagnostics;
namespace PlatformSupport.Collections.Specialized
{
public delegate void NotifyCollectionChangedEventHandler(object sender, PlatformSupport.Collections.Specialized.NotifyCollectionChangedEventArgs e);
public interface INotifyCollectionChanged
{
event NotifyCollectionChangedEventHandler CollectionChanged;
}
/// <summary>
/// This enum describes the action that caused a CollectionChanged event.
/// </summary>
public enum NotifyCollectionChangedAction
{
/// <summary> One or more items were added to the collection. </summary>
Add,
/// <summary> One or more items were removed from the collection. </summary>
Remove,
/// <summary> One or more items were replaced in the collection. </summary>
Replace,
/// <summary> One or more items were moved within the collection. </summary>
Move,
/// <summary> The contents of the collection changed dramatically. </summary>
Reset,
}
/// <summary>
/// Arguments for the CollectionChanged event.
/// A collection that supports INotifyCollectionChangedThis raises this event
/// whenever an item is added or removed, or when the contents of the collection
/// changes dramatically.
/// </summary>
public class NotifyCollectionChangedEventArgs : EventArgs
{
//------------------------------------------------------
//
// Constructors
//
//------------------------------------------------------
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a reset change.
/// </summary>
/// <param name="action">The action that caused the event (must be Reset).</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action)
{
if (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Reset)
throw new ArgumentException("action");
InitializeAdd(action, null, -1);
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a one-item change.
/// </summary>
/// <param name="action">The action that caused the event; can only be Reset, Add or Remove action.</param>
/// <param name="changedItem">The item affected by the change.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, object changedItem)
{
if ((action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Add) && (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Remove)
&& (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Reset))
throw new ArgumentException("action");
if (action == PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Reset)
{
if (changedItem != null)
throw new ArgumentException("action");
InitializeAdd(action, null, -1);
}
else
{
InitializeAddOrRemove(action, new object[] { changedItem }, -1);
}
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a one-item change.
/// </summary>
/// <param name="action">The action that caused the event.</param>
/// <param name="changedItem">The item affected by the change.</param>
/// <param name="index">The index where the change occurred.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, object changedItem, int index)
{
if ((action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Add) && (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Remove)
&& (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Reset))
throw new ArgumentException("action");
if (action == PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Reset)
{
if (changedItem != null)
throw new ArgumentException("action");
if (index != -1)
throw new ArgumentException("action");
InitializeAdd(action, null, -1);
}
else
{
InitializeAddOrRemove(action, new object[] { changedItem }, index);
}
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a multi-item change.
/// </summary>
/// <param name="action">The action that caused the event.</param>
/// <param name="changedItems">The items affected by the change.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList changedItems)
{
if ((action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Add) && (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Remove)
&& (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Reset))
throw new ArgumentException("action");
if (action == PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Reset)
{
if (changedItems != null)
throw new ArgumentException("action");
InitializeAdd(action, null, -1);
}
else
{
if (changedItems == null)
throw new ArgumentNullException("changedItems");
InitializeAddOrRemove(action, changedItems, -1);
}
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a multi-item change (or a reset).
/// </summary>
/// <param name="action">The action that caused the event.</param>
/// <param name="changedItems">The items affected by the change.</param>
/// <param name="startingIndex">The index where the change occurred.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList changedItems, int startingIndex)
{
if ((action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Add) && (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Remove)
&& (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Reset))
throw new ArgumentException("action");
if (action == PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Reset)
{
if (changedItems != null)
throw new ArgumentException("action");
if (startingIndex != -1)
throw new ArgumentException("action");
InitializeAdd(action, null, -1);
}
else
{
if (changedItems == null)
throw new ArgumentNullException("changedItems");
if (startingIndex < -1)
throw new ArgumentException("startingIndex");
InitializeAddOrRemove(action, changedItems, startingIndex);
}
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a one-item Replace event.
/// </summary>
/// <param name="action">Can only be a Replace action.</param>
/// <param name="newItem">The new item replacing the original item.</param>
/// <param name="oldItem">The original item that is replaced.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, object newItem, object oldItem)
{
if (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Replace)
throw new ArgumentException("action");
InitializeMoveOrReplace(action, new object[] { newItem }, new object[] { oldItem }, -1, -1);
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a one-item Replace event.
/// </summary>
/// <param name="action">Can only be a Replace action.</param>
/// <param name="newItem">The new item replacing the original item.</param>
/// <param name="oldItem">The original item that is replaced.</param>
/// <param name="index">The index of the item being replaced.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, object newItem, object oldItem, int index)
{
if (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Replace)
throw new ArgumentException("action");
InitializeMoveOrReplace(action, new object[] { newItem }, new object[] { oldItem }, index, index);
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a multi-item Replace event.
/// </summary>
/// <param name="action">Can only be a Replace action.</param>
/// <param name="newItems">The new items replacing the original items.</param>
/// <param name="oldItems">The original items that are replaced.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList newItems, IList oldItems)
{
if (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Replace)
throw new ArgumentException("action");
if (newItems == null)
throw new ArgumentNullException("newItems");
if (oldItems == null)
throw new ArgumentNullException("oldItems");
InitializeMoveOrReplace(action, newItems, oldItems, -1, -1);
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a multi-item Replace event.
/// </summary>
/// <param name="action">Can only be a Replace action.</param>
/// <param name="newItems">The new items replacing the original items.</param>
/// <param name="oldItems">The original items that are replaced.</param>
/// <param name="startingIndex">The starting index of the items being replaced.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList newItems, IList oldItems, int startingIndex)
{
if (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Replace)
throw new ArgumentException("action");
if (newItems == null)
throw new ArgumentNullException("newItems");
if (oldItems == null)
throw new ArgumentNullException("oldItems");
InitializeMoveOrReplace(action, newItems, oldItems, startingIndex, startingIndex);
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a one-item Move event.
/// </summary>
/// <param name="action">Can only be a Move action.</param>
/// <param name="changedItem">The item affected by the change.</param>
/// <param name="index">The new index for the changed item.</param>
/// <param name="oldIndex">The old index for the changed item.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, object changedItem, int index, int oldIndex)
{
if (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Move)
throw new ArgumentException("action");
if (index < 0)
throw new ArgumentException("index");
object[] changedItems = new object[] { changedItem };
InitializeMoveOrReplace(action, changedItems, changedItems, index, oldIndex);
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a multi-item Move event.
/// </summary>
/// <param name="action">The action that caused the event.</param>
/// <param name="changedItems">The items affected by the change.</param>
/// <param name="index">The new index for the changed items.</param>
/// <param name="oldIndex">The old index for the changed items.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList changedItems, int index, int oldIndex)
{
if (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Move)
throw new ArgumentException("action");
if (index < 0)
throw new ArgumentException("index");
InitializeMoveOrReplace(action, changedItems, changedItems, index, oldIndex);
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs with given fields (no validation). Used by WinRT marshaling.
/// </summary>
internal NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList newItems, IList oldItems, int newIndex, int oldIndex)
{
_action = action;
_newItems = (newItems == null) ? null : new ReadOnlyList(newItems);
_oldItems = (oldItems == null) ? null : new ReadOnlyList(oldItems);
_newStartingIndex = newIndex;
_oldStartingIndex = oldIndex;
}
private void InitializeAddOrRemove(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList changedItems, int startingIndex)
{
if (action == PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Add)
InitializeAdd(action, changedItems, startingIndex);
else if (action == PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Remove)
InitializeRemove(action, changedItems, startingIndex);
else
Debug.Assert(false, String.Format("Unsupported action: {0}", action.ToString()));
}
private void InitializeAdd(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList newItems, int newStartingIndex)
{
_action = action;
_newItems = (newItems == null) ? null : new ReadOnlyList(newItems);
_newStartingIndex = newStartingIndex;
}
private void InitializeRemove(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList oldItems, int oldStartingIndex)
{
_action = action;
_oldItems = (oldItems == null) ? null : new ReadOnlyList(oldItems);
_oldStartingIndex = oldStartingIndex;
}
private void InitializeMoveOrReplace(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList newItems, IList oldItems, int startingIndex, int oldStartingIndex)
{
InitializeAdd(action, newItems, startingIndex);
InitializeRemove(action, oldItems, oldStartingIndex);
}
//------------------------------------------------------
//
// Public Properties
//
//------------------------------------------------------
/// <summary>
/// The action that caused the event.
/// </summary>
public PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction Action
{
get { return _action; }
}
/// <summary>
/// The items affected by the change.
/// </summary>
public IList NewItems
{
get { return _newItems; }
}
/// <summary>
/// The old items affected by the change (for Replace events).
/// </summary>
public IList OldItems
{
get { return _oldItems; }
}
/// <summary>
/// The index where the change occurred.
/// </summary>
public int NewStartingIndex
{
get { return _newStartingIndex; }
}
/// <summary>
/// The old index where the change occurred (for Move events).
/// </summary>
public int OldStartingIndex
{
get { return _oldStartingIndex; }
}
//------------------------------------------------------
//
// Private Fields
//
//------------------------------------------------------
private PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction _action;
private IList _newItems, _oldItems;
private int _newStartingIndex = -1;
private int _oldStartingIndex = -1;
}
internal sealed class ReadOnlyList : IList
{
private readonly IList _list;
internal ReadOnlyList(IList list)
{
Debug.Assert(list != null);
_list = list;
}
public int Count
{
get { return _list.Count; }
}
public bool IsReadOnly
{
get { return true; }
}
public bool IsFixedSize
{
get { return true; }
}
public bool IsSynchronized
{
get { return _list.IsSynchronized; }
}
public object this[int index]
{
get
{
return _list[index];
}
set
{
throw new NotSupportedException();
}
}
public object SyncRoot
{
get { return _list.SyncRoot; }
}
public int Add(object value)
{
throw new NotSupportedException();
}
public void Clear()
{
throw new NotSupportedException();
}
public bool Contains(object value)
{
return _list.Contains(value);
}
public void CopyTo(Array array, int index)
{
_list.CopyTo(array, index);
}
public IEnumerator GetEnumerator()
{
return _list.GetEnumerator();
}
public int IndexOf(object value)
{
return _list.IndexOf(value);
}
public void Insert(int index, object value)
{
throw new NotSupportedException();
}
public void Remove(object value)
{
throw new NotSupportedException();
}
public void RemoveAt(int index)
{
throw new NotSupportedException();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9cb2ba99046317b4b8fb03f76d994f00
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fd7ddc2ed89fa8643bc32e214898f855
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,87 @@
#define _ENABLE_LOGGING
using System;
using System.IO;
namespace Best.HTTP.Shared.PlatformSupport.FileSystem
{
public sealed class DefaultIOService : IIOService
{
public Stream CreateFileStream(string path, FileStreamModes mode)
{
#if ENABLE_LOGGING
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DefaultIOService), $"{nameof(CreateFileStream)}('{path}', {mode})");
#endif
switch (mode)
{
case FileStreamModes.Create:
return new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read);
case FileStreamModes.OpenRead:
return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read /*, 64 * 1024, FileOptions.SequentialScan*/);
case FileStreamModes.OpenReadWrite:
return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
case FileStreamModes.Append:
return new FileStream(path, FileMode.Append);
}
throw new NotImplementedException($"{nameof(DefaultIOService)}.{nameof(CreateFileStream)} - '{mode}' not implemented!");
}
public void DirectoryCreate(string path)
{
#if ENABLE_LOGGING
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DefaultIOService), $"{nameof(DirectoryCreate)}('{path}')");
#endif
Directory.CreateDirectory(path);
}
public void DirectoryDelete(string path) => Directory.Delete(path, true);
public bool DirectoryExists(string path)
{
bool exists = Directory.Exists(path);
#if ENABLE_LOGGING
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DefaultIOService), $"{nameof(DirectoryExists)}('{path}', {exists})");
#endif
return exists;
}
public string[] GetFiles(string path)
{
var files = Directory.GetFiles(path);
#if ENABLE_LOGGING
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DefaultIOService), $"{nameof(GetFiles)}('{path}', {files?.Length})");
#endif
return files;
}
public void FileDelete(string path)
{
#if ENABLE_LOGGING
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DefaultIOService), $"{nameof(FileDelete)}('{path}')");
#endif
File.Delete(path);
}
public bool FileExists(string path)
{
bool exists = File.Exists(path);
#if ENABLE_LOGGING
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DefaultIOService), $"{nameof(FileExists)}('{path}', {exists})");
#endif
return exists;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 70d8cd7c804fca64ca518e3bb0210fb0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,74 @@
using System;
using System.IO;
namespace Best.HTTP.Shared.PlatformSupport.FileSystem
{
/// <summary>
/// These are the different modes that the plugin want's to use a filestream.
/// </summary>
public enum FileStreamModes
{
/// <summary>
/// Create a new file.
/// </summary>
Create,
/// <summary>
/// Open an existing file for reading.
/// </summary>
OpenRead,
/// <summary>
/// Open or create a file for read and write.
/// </summary>
OpenReadWrite,
/// <summary>
/// Open an existing file for writing to the end.
/// </summary>
Append
}
/// <summary>
/// Interface for file-system abstraction.
/// </summary>
public interface IIOService
{
/// <summary>
/// Create a directory for the given path.
/// </summary>
void DirectoryCreate(string path);
/// <summary>
/// Return true if the directory exists for the given path.
/// </summary>
bool DirectoryExists(string path);
/// <summary>
/// Delete the directory.
/// </summary>
void DirectoryDelete(string path);
/// <summary>
/// Return with the file names for the given path.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
string[] GetFiles(string path);
/// <summary>
/// Delete the file for the given path.
/// </summary>
void FileDelete(string path);
/// <summary>
/// Return true if the file exists on the given path.
/// </summary>
bool FileExists(string path);
/// <summary>
/// Create a stream that can read and/or write a file on the given path.
/// </summary>
Stream CreateFileStream(string path, FileStreamModes mode);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9e114e4d1a092534796049799a691aa8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 599e163332b86fa4ba9b1f6ba0819c04
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
using System;
namespace Best.HTTP.Shared.PlatformSupport.IL2CPP
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)]
public class Il2CppEagerStaticClassConstructionAttribute : Attribute
{
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f2fa59793a441f54da418bb0438edbc3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,75 @@
// https://forum.unity.com/threads/il2cpp-code-generation-options.367074/
using System;
namespace Best.HTTP.Shared.PlatformSupport.IL2CPP
{
/// <summary>
/// The code generation options available for IL to C++ conversion.
/// Enable or disabled these with caution.
/// </summary>
public enum Option
{
/// <summary>
/// Enable or disable code generation for null checks.
///
/// Global null check support is enabled by default when il2cpp.exe
/// is launched from the Unity editor.
///
/// Disabling this will prevent NullReferenceException exceptions from
/// being thrown in generated code. In *most* cases, code that dereferences
/// a null pointer will crash then. Sometimes the point where the crash
/// happens is later than the location where the null reference check would
/// have been emitted though.
/// </summary>
NullChecks = 1,
/// <summary>
/// Enable or disable code generation for array bounds checks.
///
/// Global array bounds check support is enabled by default when il2cpp.exe
/// is launched from the Unity editor.
///
/// Disabling this will prevent IndexOutOfRangeException exceptions from
/// being thrown in generated code. This will allow reading and writing to
/// memory outside of the bounds of an array without any runtime checks.
/// Disable this check with extreme caution.
/// </summary>
ArrayBoundsChecks = 2,
/// <summary>
/// Enable or disable code generation for divide by zero checks.
///
/// Global divide by zero check support is disabled by default when il2cpp.exe
/// is launched from the Unity editor.
///
/// Enabling this will cause DivideByZeroException exceptions to be
/// thrown in generated code. Most code doesn't need to handle this
/// exception, so it is probably safe to leave it disabled.
/// </summary>
DivideByZeroChecks = 3,
}
/// <summary>
/// Use this attribute on a class, method, or property to inform the IL2CPP code conversion utility to override the
/// global setting for one of a few different runtime checks.
///
/// Example:
///
/// [Il2CppSetOption(Option.NullChecks, false)]
/// public static string MethodWithNullChecksDisabled()
/// {
/// var tmp = new Object();
/// return tmp.ToString();
/// }
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
public class Il2CppSetOptionAttribute : Attribute
{
public Option Option { get; private set; }
public object Value { get; private set; }
public Il2CppSetOptionAttribute(Option option, object value)
{
Option = option;
Value = value;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 54d985a2d9f4dd540a34861291c3508e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,21 @@
using System;
namespace Best.HTTP.Shared.PlatformSupport.IL2CPP
{
/// <summary>
/// https://docs.unity3d.com/Manual/ManagedCodeStripping.html
/// </summary>
[AttributeUsage(
AttributeTargets.Assembly |
AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Delegate |
AttributeTargets.Enum |
AttributeTargets.Event |
AttributeTargets.Field |
AttributeTargets.Interface |
AttributeTargets.Method |
AttributeTargets.Property |
AttributeTargets.Struct)]
public sealed class PreserveAttribute : Attribute {}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 332261a86aa5bba4b823b2d2bf5f8a26
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3bab02f6ca42fa64b8cfb7d3e2c2d28b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,103 @@
using System;
using Best.HTTP.Shared.PlatformSupport.Text;
namespace Best.HTTP.Shared.PlatformSupport.Memory
{
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstructionAttribute]
public struct AutoReleaseBuffer : IDisposable
{
public static readonly AutoReleaseBuffer Empty = new AutoReleaseBuffer(null);
public byte[] Data;
public int Offset;
public int Count;
public AutoReleaseBuffer(byte[] data)
{
this.Data = data;
this.Offset = 0;
this.Count = data != null ? data.Length : 0;
}
public AutoReleaseBuffer(BufferSegment segment)
{
this.Data = segment.Data;
this.Offset = segment.Offset;
this.Count = segment.Count;
}
public AutoReleaseBuffer(byte[] data, int offset, int count)
{
this.Data = data;
this.Offset = offset;
this.Count = count;
}
public BufferSegment Slice(int newOffset) => new BufferSegment(this.Data, newOffset, this.Count - (newOffset - this.Offset));
public BufferSegment Slice(int offset, int count) => new BufferSegment(this.Data, offset, count);
public override bool Equals(object obj)
{
if (obj == null || !(obj is BufferSegment))
return false;
return Equals((BufferSegment)obj);
}
public bool Equals(BufferSegment other) => this.Data == other.Data && this.Offset == other.Offset && this.Count == other.Count;
public bool Equals(AutoReleaseBuffer other) => this.Data == other.Data && this.Offset == other.Offset && this.Count == other.Count;
public override int GetHashCode() => (this.Data != null ? this.Data.GetHashCode() : 0) * 21 + this.Offset + this.Count;
public static bool operator ==(AutoReleaseBuffer left, AutoReleaseBuffer right) => left.Equals(right);
public static bool operator !=(AutoReleaseBuffer left, AutoReleaseBuffer right) => !left.Equals(right);
public static bool operator ==(AutoReleaseBuffer left, BufferSegment right) => left.Equals(right);
public static bool operator !=(AutoReleaseBuffer left, BufferSegment right) => !left.Equals(right);
public static implicit operator byte[](AutoReleaseBuffer left) => left.Data;
public static implicit operator BufferSegment(AutoReleaseBuffer left) => new BufferSegment(left.Data, left.Offset, left.Count);
public override string ToString()
{
var sb = StringBuilderPool.Get(this.Count + 5);
sb.Append("[AutoReleaseBuffer ");
if (this.Count > 0)
{
sb.AppendFormat("Offset: {0:N0} ", this.Offset);
sb.AppendFormat("Count: {0:N0} ", this.Count);
sb.Append("Data: [");
if (this.Count > 0)
{
int dumpCount = Math.Min(this.Count, BufferSegment.ToStringMaxDumpLength);
sb.AppendFormat("{0:X2}", this.Data[this.Offset]);
for (int i = 1; i < dumpCount; ++i)
sb.AppendFormat(", {0:X2}", this.Data[this.Offset + i]);
if (this.Count > dumpCount)
sb.Append(", ...");
}
sb.Append("]]");
}
else
sb.Append(']');
return StringBuilderPool.ReleaseAndGrab(sb);
}
public void Dispose()
{
if (this.Data != null)
BufferPool.Release(this.Data);
this.Data = null;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 510c03648931e35459c6a7b1f09cb549
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,80 @@
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Threading;
namespace Best.HTTP.Shared.PlatformSupport.Memory
{
internal struct Bucket
{
#if UNITY_EDITOR
/// <summary>
/// Size the Bucket is associated with. Serves mostly debug purposes.
/// </summary>
public readonly int Size;
#endif
/// <summary>
/// What was Items' minimum Count between two checks.
/// </summary>
public int MinCount;
/// <summary>
/// Direct access to a buffer, without going throug ConcurrentStack's pop/push logic.
/// </summary>
public byte[] FastItem;
private int _count;
private readonly ConcurrentStack<byte[]> _items;
public Bucket(int size)
{
#if UNITY_EDITOR
this.Size = size;
#endif
this.FastItem = null;
this._items = new ConcurrentStack<byte[]>();
this.MinCount = int.MaxValue;
this._count = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryPop(out byte[] item)
{
if (this._items.TryPop(out item))
{
Interlocked.Decrement(ref this._count);
return true;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Push(byte[] item)
{
this._items.Push(item);
Interlocked.Increment(ref this._count);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Clear()
{
this._items.Clear();
Interlocked.Exchange(ref this._count, 0);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void UpdateMinCount(int fastItem)
{
int newMinCount = _count + fastItem;
int oldMinCount = MinCount;
while (newMinCount < oldMinCount && Interlocked.CompareExchange(ref MinCount, newMinCount, oldMinCount) != oldMinCount)
oldMinCount = MinCount;
}
#if UNITY_EDITOR
public override string ToString() => $"[{Size}, {(FastItem != null ? "y" : "n")}, {_count}, {MinCount}]";
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1cd00a5f117ec2c41b301e57bd825200
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,542 @@
#if BESTHTTP_ENABLE_BUFFERPOOL_DOUBLE_RELEASE_CHECKER
using Best.HTTP.Shared.Extensions;
#endif
using Best.HTTP.Shared.Logger;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Unity.Burst;
using static Unity.Burst.Intrinsics.X86.Bmi1;
using static System.Math;
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
using System.Linq;
#endif
namespace Best.HTTP.Shared.PlatformSupport.Memory
{
/// <summary>
/// The BufferPool is a foundational element of the Best HTTP package, aiming to reduce dynamic memory allocation overheads by reusing byte arrays. The concept is elegantly simple: rather than allocating and deallocating memory for every requirement, byte arrays can be "borrowed" and "returned" within this pool. Once returned, these arrays are retained for subsequent use, minimizing repetitive memory operations.
/// <para>While the BufferPool is housed within the Best HTTP package, its benefits are not limited to just HTTP operations. All protocols and packages integrated with or built upon the Best HTTP package utilize and benefit from the BufferPool. This ensures that memory is used efficiently and performance remains optimal across all integrated components.</para>
/// </summary>
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstructionAttribute]
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppSetOptionAttribute(Best.HTTP.Shared.PlatformSupport.IL2CPP.Option.NullChecks, false)]
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppSetOptionAttribute(Best.HTTP.Shared.PlatformSupport.IL2CPP.Option.ArrayBoundsChecks, false)]
public static class BufferPool
{
/// <summary>
/// Specifies the minimum buffer size that will be allocated. If a request is made for a size smaller than this and canBeLarger is <c>true</c>,
/// this size will be used.
/// </summary>
public const long MIN_BUFFER_SIZE = 512;
/// <summary>
/// Specifies the maximum size of a buffer that the system will consider storing back into the pool.
/// </summary>
public const long MAX_BUFFER_SIZE = 32 * 1024 * 1024;
/// <summary>
/// Represents an empty byte array that can be returned for zero-length requests.
/// </summary>
public static readonly byte[] NoData = Array.Empty<byte>();
/// <summary>
/// Gets or sets a value indicating whether the buffer pooling mechanism is enabled or disabled.
/// Disabling will also clear all stored entries.
/// </summary>
public static bool IsEnabled
{
get { return _isEnabled; }
set
{
_isEnabled = value;
// When set to non-enabled remove all stored entries
if (!_isEnabled)
Clear();
}
}
private static volatile bool _isEnabled = true;
/// <summary>
/// Specifies how frequently the maintenance cycle should run to manage old buffers.
/// </summary>
public static TimeSpan RunMaintenanceEvery = TimeSpan.FromSeconds(30);
/// <summary>
/// Specifies the maximum total size of all stored buffers. When the buffer reach this threshold, new releases will be declined.
/// </summary>
public static long MaxPoolSize = 100 * 1024 * 1024;
public static long MinBufferSize => MIN_BUFFER_SIZE;
#if BESTHTTP_ENABLE_BUFFERPOOL_BUFFER_STEALING
/// <summary>
/// Index threshold that getting a larger buffer is allowed in.
/// </summary>
public static byte MaxIndexThreshold = 3;
/// <summary>
/// The maximum size that the pool will try to steal from.
/// </summary>
/// <remarks>It must be power of <c>2</c>!</remarks>
public static uint StealLimit = 1 << 20;
#endif
private static DateTime lastMaintenance = DateTime.MinValue;
// Statistics
private static long PoolSize = 0;
private static long GetBuffers = 0;
private static long ReleaseBuffers = 0;
private static long Borrowed = 0;
private static long ArrayAllocations = 0;
private static readonly Bucket[] _buckets;
private static readonly int firstIdx;
private static readonly int lastIdx;
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
private static ConditionalWeakTable<byte[], Tracker> _trackers = new ConditionalWeakTable<byte[], Tracker>();
#endif
#if BESTHTTP_ENABLE_BUFFERPOOL_DOUBLE_RELEASE_CHECKER
private static ConcurrentDictionary<byte[], (byte[] buffer, string stackTrace)> _doubleReleaseTracker = new ConcurrentDictionary<byte[], (byte[] buffer, string stackTrace)>();
#endif
static BufferPool()
{
#if UNITY_ANDROID || UNITY_IOS
UnityEngine.Application.lowMemory -= OnLowMemory;
UnityEngine.Application.lowMemory += OnLowMemory;
#endif
firstIdx = GetIdx(BufferPool.MIN_BUFFER_SIZE);
lastIdx = GetIdx(BufferPool.MAX_BUFFER_SIZE);
_buckets = new Bucket[(lastIdx - firstIdx) + 1];
for (int i = 0; i < _buckets.Length; i++)
_buckets[i] = new Bucket((int)(MIN_BUFFER_SIZE << i));
}
#if UNITY_EDITOR
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)]
public static void ResetSetup()
{
HTTPManager.Logger.Information("BufferPool", "Reset called!");
PoolSize = 0;
GetBuffers = 0;
ReleaseBuffers = 0;
Borrowed = 0;
ArrayAllocations = 0;
for (int i = 0; i < _buckets.Length; i++)
_buckets[i] = new Bucket((int)(MIN_BUFFER_SIZE << i));
lastMaintenance = DateTime.MinValue;
#if UNITY_ANDROID || UNITY_IOS
UnityEngine.Application.lowMemory -= OnLowMemory;
UnityEngine.Application.lowMemory += OnLowMemory;
#endif
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
ClearTrackers();
#endif
#if BESTHTTP_ENABLE_BUFFERPOOL_DOUBLE_RELEASE_CHECKER
_doubleReleaseTracker.Clear();
#endif
}
#endif
#if UNITY_ANDROID || UNITY_IOS
private static void OnLowMemory()
{
HTTPManager.Logger.Warning(nameof(BufferPool), nameof(OnLowMemory));
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
ClearTrackers();
#endif
#if BESTHTTP_ENABLE_BUFFERPOOL_DOUBLE_RELEASE_CHECKER
_doubleReleaseTracker.Clear();
#endif
// Drop all buffers
Clear();
}
#endif
/// <summary>
/// Fetches a byte array from the pool.
/// </summary>
/// <remarks>Depending on the `canBeLarger` parameter, the returned buffer may be larger than the requested size!</remarks>
/// <param name="size">Requested size of the buffer.</param>
/// <param name="canBeLarger">If <c>true</c>, the returned buffer can be larger than the requested size.</param>
/// <param name="context">Optional context for logging purposes.</param>
/// <returns>A byte array from the pool or a newly allocated one if suitable size is not available.</returns>
public static byte[] Get(long size, bool canBeLarger, LoggingContext context = null)
{
if (!_isEnabled)
return new byte[size];
// Return a fix reference for 0 length requests. Any resize call (even Array.Resize) creates a new reference
// so we are safe to expose it to multiple callers.
if (size == 0)
return BufferPool.NoData;
if (size > BufferPool.MAX_BUFFER_SIZE)
return new byte[size];
if (size < BufferPool.MIN_BUFFER_SIZE)
size = BufferPool.MIN_BUFFER_SIZE;
else if (!BufferPool.IsPowerOf2(size))
size = BufferPool.NextPowerOf2(size);
int idx = GetIdx(size >> firstIdx);
var buckets = _buckets;
#if BESTHTTP_ENABLE_BUFFERPOOL_BUFFER_STEALING
int maxIdx = Max(idx, Min(idx + MaxIndexThreshold, GetIdx(StealLimit >> firstIdx)));
while (idx < buckets.Length && idx <= maxIdx)
#else
if (idx < buckets.Length)
#endif
{
ref Bucket bucket = ref buckets[idx];
var item = bucket.FastItem;
if ((item != null && Interlocked.CompareExchange(ref bucket.FastItem, null, item) == item) ||
bucket.TryPop(out item))
{
Interlocked.Increment(ref GetBuffers);
Interlocked.Add(ref PoolSize, -item.Length);
Interlocked.Add(ref Borrowed, item.Length);
bucket.UpdateMinCount(0);
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
_trackers.Add(item, new Tracker(context));
#endif
#if BESTHTTP_ENABLE_BUFFERPOOL_DOUBLE_RELEASE_CHECKER
_doubleReleaseTracker.Remove(item, out var _);
#endif
return item;
}
#if BESTHTTP_ENABLE_BUFFERPOOL_BUFFER_STEALING
idx++;
#endif
}
Interlocked.Increment(ref ArrayAllocations);
Interlocked.Add(ref Borrowed, size);
var result = new byte[size];
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
_trackers.Add(result, new Tracker(context));
#endif
return result;
}
/// <summary>
/// Releases a list of buffer segments back to the pool in a bulk operation.
/// </summary>
/// <param name="segments">List of buffer segments to release.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReleaseBulk(ConcurrentQueue<BufferSegment> segments)
{
if (!_isEnabled || segments == null)
return;
while (segments.TryDequeue(out var segment))
Release(segment);
}
/// <summary>
/// Releases a list of buffer segments back to the pool in a bulk operation.
/// </summary>
/// <param name="segments">List of buffer segments to release.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReleaseBulk(List<BufferSegment> segments)
{
if (!_isEnabled || segments == null)
return;
for (int i = 0; i < segments.Count; i++)
Release(segments[i]);
segments.Clear();
}
/// <summary>
/// Releases a byte array back to the pool.
/// </summary>
/// <param name="buffer">Buffer to be released back to the pool.</param>
public static void Release(byte[] buffer)
{
if (!_isEnabled || buffer == null)
return;
int size = buffer.Length;
if (size == 0 || size < MIN_BUFFER_SIZE || size > MAX_BUFFER_SIZE)
return;
if (!IsPowerOf2(size))
return;
#if BESTHTTP_ENABLE_BUFFERPOOL_DOUBLE_RELEASE_CHECKER
if (_doubleReleaseTracker.TryGetValue(buffer, out var entry))
{
HTTPManager.Logger.Error("BufferPool", $"Buffer already added to the pool! Previously added from: {ProcessStackTrace(entry.stackTrace)}. Buffer: {entry.buffer.AsBuffer()}");
return;
}
else
_doubleReleaseTracker.TryAdd(buffer, new(buffer, Environment.StackTrace));
#endif
Interlocked.Add(ref Borrowed, -size);
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
if (_trackers.TryGetValue(buffer, out var tracker))
{
_trackers.Remove(buffer);
tracker.Dispose();
}
#endif
var ps = Interlocked.Read(ref PoolSize);
if (ps + size > MaxPoolSize)
return;
Interlocked.Add(ref PoolSize, size);
Interlocked.Increment(ref ReleaseBuffers);
int idx = GetIdx(size >> firstIdx);
var buckets = _buckets;
ref var bucket = ref buckets[idx];
if (Interlocked.CompareExchange(ref bucket.FastItem, buffer, null) != null)
{
bucket.Push(buffer);
bucket.UpdateMinCount(0);
}
else
{
bucket.UpdateMinCount(1);
}
}
/// <summary>
/// Resizes a byte array by returning the old one to the pool and fetching (or creating) a new one of the specified size.
/// </summary>
/// <param name="buffer">Buffer to resize.</param>
/// <param name="newSize">New size for the buffer.</param>
/// <param name="canBeLarger">If <c>true</c>, the new buffer can be larger than the specified size.</param>
/// <param name="clear">If <c>true</c>, the new buffer will be cleared (set to all zeros).</param>
/// <param name="context">Optional context for logging purposes.</param>
/// <returns>A resized buffer.</returns>
public static byte[] Resize(ref byte[] buffer, int newSize, bool canBeLarger, bool clear, LoggingContext context = null)
{
if (!_isEnabled)
{
Array.Resize<byte>(ref buffer, newSize);
return buffer;
}
byte[] newBuf = BufferPool.Get(newSize, canBeLarger, context);
if (buffer != null)
{
if (!clear)
Array.Copy(buffer, 0, newBuf, 0, Min(newBuf.Length, buffer.Length));
BufferPool.Release(buffer);
}
if (clear)
Array.Clear(newBuf, 0, newSize);
return buffer = newBuf;
}
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
public static KeyValuePair<byte[], Tracker>[] GetBorrowedBuffers() => _trackers.ToArray();
#endif
#if BESTHTTP_PROFILE
public static void GetStatistics(ref BufferPoolStats stats)
{
//using (new ReadLock(rwLock))
stats.GetBuffers = Interlocked.Read(ref GetBuffers);
stats.ReleaseBuffers = Interlocked.Read(ref ReleaseBuffers);
stats.PoolSize = Interlocked.Read(ref PoolSize);
stats.MinBufferSize = MIN_BUFFER_SIZE;
stats.MaxBufferSize = MAX_BUFFER_SIZE;
stats.MaxPoolSize = Interlocked.Read(ref MaxPoolSize);
stats.Borrowed = Interlocked.Read(ref Borrowed);
stats.ArrayAllocations = Interlocked.Read(ref ArrayAllocations);
/*stats.FreeBufferCount = FreeBuffers.Count;
if (stats.FreeBufferStats == null)
stats.FreeBufferStats = new List<BufferStats>(FreeBuffers.Count);
else
stats.FreeBufferStats.Clear();
for (int i = 0; i < FreeBuffers.Count; ++i)
{
BufferStore store = FreeBuffers[i];
List<BufferDesc> buffers = store.buffers;
BufferStats bufferStats = new BufferStats();
bufferStats.Size = store.Size;
bufferStats.Count = buffers.Count;
stats.FreeBufferStats.Add(bufferStats);
}*/
stats.NextMaintenance = (lastMaintenance + RunMaintenanceEvery) - DateTime.UtcNow;
}
#endif
/// <summary>
/// Clears all stored entries in the buffer pool instantly, releasing memory.
/// </summary>
public static void Clear()
{
var buckets = _buckets;
for (int i = 0; i < _buckets.Length; ++i)
{
ref Bucket bucket = ref buckets[i];
bucket.Clear();
Interlocked.Exchange(ref bucket.FastItem, null);
}
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
ClearTrackers();
#endif
Interlocked.Exchange(ref PoolSize, 0);
}
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
private static void ClearTrackers()
{
var all = _trackers.ToArray();
foreach (var item in all)
_trackers.Remove(item.Key);
_trackers.Clear();
}
#endif
/// <summary>
/// Internal method called by the plugin to remove old, non-used buffers.
/// </summary>
internal static void Maintain()
{
DateTime now = DateTime.UtcNow;
if (!_isEnabled || lastMaintenance + RunMaintenanceEvery > now)
return;
lastMaintenance = now;
var buckets = _buckets;
for (int i = 0; i < buckets.Length; ++i)
{
ref Bucket bucket = ref buckets[i];
// Size of the bucket, already negated.
int bucketSize = -(int)(MIN_BUFFER_SIZE << i);
var remove = Interlocked.Exchange(ref bucket.MinCount, int.MaxValue);
// Remove half of the unused items, releasing them back to the GC in a few steps
if (remove != int.MaxValue && remove > 1)
remove >>= 1;
for (int counter = 0; counter < remove && bucket.TryPop(out var _); ++counter)
Interlocked.Add(ref PoolSize, bucketSize);
// Remove FastItem too, when it wasn't used in a full maintenance round
if (remove == int.MaxValue && Interlocked.Exchange(ref bucket.FastItem, null) != null)
Interlocked.Add(ref PoolSize, bucketSize);
}
}
#region Helper functions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsPowerOf2(long x) => (x & (x - 1)) == 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static long NextPowerOf2(long x)
{
long pow = 1;
while (pow <= x)
pow *= 2;
return pow;
}
[ThreadStatic]
private static System.Text.StringBuilder stacktraceBuilder;
public static string ProcessStackTrace(string stackTrace)
{
if (string.IsNullOrEmpty(stackTrace))
return string.Empty;
stacktraceBuilder ??= new System.Text.StringBuilder();
var lines = stackTrace.Split('\n');
for (int i = 0; i < lines.Length; ++i)
{
var line = lines[i];
if (!line.Contains("Shared.PlatformSupport.Memory.Tracker..ctor") &&
!line.Contains(".Memory.BufferPool") &&
!line.Contains("Environment") &&
!line.Contains("System.Threading"))
stacktraceBuilder.Append(line.Replace("Best.HTTP.", ""));
}
return stacktraceBuilder.ToString();
}
[BurstCompile]
private static int GetIdx(long po2)
{
if (IsBmi1Supported)
return (int)tzcnt_u64((ulong)(po2 >> firstIdx));
/*else if (IsNeonSupported)
{
v64 v = new v64((uint)po2, (uint)po2);
var result = vclz_s32(v);
return (int)result.UInt0;
}*/
ulong a = (ulong)po2;
ulong c = 64;
a &= (ulong)-(long)(a);
if (a != 0) c--;
if ((a & 0x00000000FFFFFFFF) != 0) c -= 32;
if ((a & 0x0000FFFF0000FFFF) != 0) c -= 16;
if ((a & 0x00FF00FF00FF00FF) != 0) c -= 8;
if ((a & 0x0F0F0F0F0F0F0F0F) != 0) c -= 4;
if ((a & 0x3333333333333333) != 0) c -= 2;
if ((a & 0x5555555555555555) != 0) c -= 1;
return (int)c;
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9d2975ff23d129949bbef756327c52fc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More