add all
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 566c5447b0e061e4c90deb0de9a596e5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8a166a5458783404a94c362f12b90e88
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a5df3217523b8e468cf636c4b5ee3fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07c2801d01675b0479bc135ee8327fd1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b0aa37cc0a1f5054f8ffb64d2cef5c5a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc04de29f38ec7c48b0517fc0ba4d8b2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7cc4a6484b91bfd489fab519ef4f61eb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1cefb9c5ecaff6a44b77039a56050aac
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e92d1b70cb4146d47b88bfb3f43c49f8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c788ada3967bfe4dac4ce9e7522dc8c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d8da4c912810a742a1af50bb90fb26e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c484a2e93b3a84746939c4d2d5cdf8d9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bedd5a1e08a99404fbc747e79b92f17c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b03ac89356268b40b90d8a46678c699
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 161054408a1ecbe4383c76ea478332ed
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc2cf4b60fcca484e9aafe612ba9ac51
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0bb24b6286ceff24abd558107641cd8b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e3a9d8df83b70404782d3d67884dc1a9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8a63cdf33c00b6443bcbd6aa81aa6562
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e825306d81906834b84135765d5c3174
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36cd9f102ecc80746bc7ff850f782e32
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea59aadbd0882aa46a9b5c86d137d70a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 031b34e0ebacb3649a074863a87dd5ed
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8a2c2eff9e98f84cb925568bfb30c8c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7e67aa2e8ef9344cb04e85bfff0aed5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1436ef67cdb4ca409385774293711b2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 356c46c35b6c6df45abc494c61a7039a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35bb5ccf89a1dc545a7be69cc867d08d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 01b6bb7a29a6e7447baf8768ab665143
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 169261907c72c004eae68f0a8ae6a8a8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ccc301b8038cb64b9e489bafa17d2bc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 70785d1a06f961348acdef24b845ba0c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
394
Packages/com.tivadar.best.http/Runtime/Shared/HTTPManager.cs
Normal file
394
Packages/com.tivadar.best.http/Runtime/Shared/HTTPManager.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8302c34d0db94b447a0969d738d37543
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4cd5cef435d748c4d9b0f7b5cf16341b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3516ff05842cc2b40a7fd4a79efc1fbd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2ad11187a50e381439c128d7d791eb3a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
172
Packages/com.tivadar.best.http/Runtime/Shared/Logger/ILogger.cs
Normal file
172
Packages/com.tivadar.best.http/Runtime/Shared/Logger/ILogger.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81c5c1596e18e4d46beef29fa682d58d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32f8edc3b27d9d24384f41757f618d15
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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.", "")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 780d670e1b8a6a340a1441f844142390
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd86ab6e6f8fab649b1b2a37452aa80f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d47c08ea57a56cd49b8c37790efced77
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82bd7454309118f4e9fa14dd5afaae6d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5db467dd3130b1349ab57a28f34db155
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 902ef36b7f3ace94a83e085968c22e44
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0bded87d86b03f344a35f9b83d8be3b0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9cb2ba99046317b4b8fb03f76d994f00
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd7ddc2ed89fa8643bc32e214898f855
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 70d8cd7c804fca64ca518e3bb0210fb0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e114e4d1a092534796049799a691aa8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 599e163332b86fa4ba9b1f6ba0819c04
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f2fa59793a441f54da418bb0438edbc3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54d985a2d9f4dd540a34861291c3508e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 332261a86aa5bba4b823b2d2bf5f8a26
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3bab02f6ca42fa64b8cfb7d3e2c2d28b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 510c03648931e35459c6a7b1f09cb549
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1cd00a5f117ec2c41b301e57bd825200
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user