EfCacheL2 tested.

This commit is contained in:
ngfrolov 2022-06-01 12:18:10 +05:00
parent fa38c145e7
commit cb4eb3341c
3 changed files with 295 additions and 153 deletions

View File

@ -1,6 +1,6 @@
using System; using Microsoft.EntityFrameworkCore;
using System;
using System.Collections; using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@ -9,44 +9,67 @@ using System.Threading.Tasks;
namespace AsbCloudInfrastructure.Services.Cache namespace AsbCloudInfrastructure.Services.Cache
{ {
#nullable enable #nullable enable
/// <summary>
/// Кеширование запросов EF.
/// </summary>
public static class EfCacheL2 public static class EfCacheL2
{ {
public static int RequestsToDb = 0; /// <summary>
/// Кол-во обращений к БД.
static readonly ConcurrentDictionary<string, Lazy<CacheItem>> caches = new(1, 16); /// </summary>
public static int CountOfRequestsToDB = 0;
private const int semaphoreTimeout = 25_000; private static readonly Dictionary<string, CacheItem> caches = new(16);
private static readonly TimeSpan semaphoreTimeout = TimeSpan.FromSeconds(25);
private static readonly SemaphoreSlim semaphore = new(1); private static readonly SemaphoreSlim semaphore = new(1);
private static readonly TimeSpan minCacheTime = TimeSpan.FromSeconds(2);
private struct CacheItem private class CacheItem
{ {
public readonly IEnumerable Data; internal IEnumerable? Data;
public readonly DateTime DateObsolete; internal DateTime DateObsolete;
internal DateTime DateObsoleteTotal;
public CacheItem(IEnumerable data, TimeSpan obsolescence) internal readonly SemaphoreSlim semaphore = new(1);
{
DateObsolete = DateTime.Now + obsolescence;
Data = data;
}
} }
private static CacheItem GetOrAddCache(string tag, Func<CacheItem> valueFactory) private static CacheItem GetOrAddCache(string tag, Func<IEnumerable> valueFactory, TimeSpan obsolete)
{ {
Lazy<CacheItem>? lazyCache; CacheItem cache;
while (!caches.TryGetValue(tag, out lazyCache)) while (!caches.ContainsKey(tag))
{ {
if (semaphore.Wait(0)) if (semaphore.Wait(0))
{ {
lazyCache = new Lazy<CacheItem>(valueFactory); try {
caches.TryAdd(tag, lazyCache); cache = new CacheItem();
_ = lazyCache.Value;
semaphore.Release(); var dateObsolete = DateTime.Now + obsolete;
var dateQueryStart = DateTime.Now;
var data = valueFactory();
var queryTime = DateTime.Now - dateQueryStart;
if (dateObsolete - DateTime.Now < minCacheTime)
dateObsolete = DateTime.Now + minCacheTime;
cache.Data = data;
cache.DateObsolete = dateObsolete;
cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime;
caches.Add(tag, cache);
}
catch
{
throw;
}
finally
{
semaphore.Release();
}
break; break;
} }
else else
{ {
if(semaphore.Wait(semaphoreTimeout)) if (semaphore.Wait(semaphoreTimeout))
{
semaphore.Release(); semaphore.Release();
}
else else
{ {
semaphore.Release(); semaphore.Release();
@ -55,55 +78,91 @@ namespace AsbCloudInfrastructure.Services.Cache
} }
} }
if (lazyCache.Value.DateObsolete < DateTime.Now) cache = caches[tag];
if (cache.DateObsolete < DateTime.Now)
{ {
var isUpdated = false; if (cache.semaphore.Wait(0))
if (semaphore.Wait(0))
{ {
lazyCache = new Lazy<CacheItem>(valueFactory); try
caches.Remove(tag, out _); {
caches.TryAdd(tag, lazyCache); var dateObsolete = DateTime.Now + obsolete;
_ = lazyCache.Value; var dateQueryStart = DateTime.Now;
isUpdated = true; var data = valueFactory();
semaphore.Release(); var queryTime = DateTime.Now - dateQueryStart;
if (dateObsolete - DateTime.Now < minCacheTime)
dateObsolete = DateTime.Now + minCacheTime;
cache.Data = data;
cache.DateObsolete = dateObsolete;
cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime;
}
catch
{
throw;
}
finally
{
cache.semaphore.Release();
}
} }
else else if(cache.DateObsoleteTotal < DateTime.Now)
{ {
if (semaphore.Wait(semaphoreTimeout)) if (cache.semaphore.Wait(semaphoreTimeout))
semaphore.Release(); {
cache.semaphore.Release();
}
else else
{ {
semaphore.Release(); cache.semaphore.Release();
throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache"); throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache");
} }
} }
if (isUpdated || caches.TryGetValue(tag, out lazyCache))
return lazyCache.Value;
throw new Exception("EfCacheL2.GetOrAddCache it should never happens");
} }
else return cache;
return lazyCache.Value;
} }
private static async Task<CacheItem> GetOrAddCacheAsync(string tag, Func<CacheItem> valueFactory, CancellationToken token) private static async Task<CacheItem> GetOrAddCacheAsync(string tag, Func<CancellationToken, Task<IEnumerable>> valueFactoryAsync, TimeSpan obsolete, CancellationToken token)
{ {
Lazy<CacheItem>? lazyCache; CacheItem cache;
while (!caches.TryGetValue(tag, out lazyCache)) while (!caches.ContainsKey(tag))
{ {
if (semaphore.Wait(0, CancellationToken.None)) if (semaphore.Wait(0))
{ {
lazyCache = new Lazy<CacheItem>(valueFactory); try
caches.TryAdd(tag, lazyCache); {
_ = lazyCache.Value; cache = new CacheItem();
semaphore.Release();
var dateObsolete = DateTime.Now + obsolete;
var dateQueryStart = DateTime.Now;
var data = await valueFactoryAsync(token);
var queryTime = DateTime.Now - dateQueryStart;
if (dateObsolete - DateTime.Now < minCacheTime)
dateObsolete = DateTime.Now + minCacheTime;
cache.Data = data;
cache.DateObsolete = dateObsolete;
cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime;
caches.Add(tag, cache);
}
catch
{
throw;
}
finally
{
semaphore.Release();
}
break; break;
} }
else else
{ {
if (await semaphore.WaitAsync(semaphoreTimeout, token)) if (await semaphore.WaitAsync(semaphoreTimeout, token))
{
semaphore.Release(); semaphore.Release();
}
else else
{ {
semaphore.Release(); semaphore.Release();
@ -112,39 +171,52 @@ namespace AsbCloudInfrastructure.Services.Cache
} }
} }
if (lazyCache.Value.DateObsolete < DateTime.Now) cache = caches[tag];
if (cache.DateObsolete < DateTime.Now)
{ {
var isUpdated = false; if (cache.semaphore.Wait(0))
if (semaphore.Wait(0, CancellationToken.None))
{ {
lazyCache = new Lazy<CacheItem>(valueFactory); try
caches.Remove(tag, out _);
caches.TryAdd(tag, lazyCache);
_ = lazyCache.Value;
isUpdated = true;
semaphore.Release();
}
else
{
if (await semaphore.WaitAsync(semaphoreTimeout, token))
semaphore.Release();
else
{ {
semaphore.Release(); var dateObsolete = DateTime.Now + obsolete;
throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache"); var dateQueryStart = DateTime.Now;
var data = await valueFactoryAsync(token);
var queryTime = DateTime.Now - dateQueryStart;
if (dateObsolete - DateTime.Now < minCacheTime)
dateObsolete = DateTime.Now + minCacheTime;
cache.Data = data;
cache.DateObsolete = dateObsolete;
cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime;
}
catch
{
throw;
}
finally
{
cache.semaphore.Release();
}
}
else if (cache.DateObsoleteTotal < DateTime.Now)
{
if (await cache.semaphore.WaitAsync(semaphoreTimeout, token))
{
cache.semaphore.Release();
}
else
{
cache.semaphore.Release();
throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting updated cache");
} }
} }
if (isUpdated || caches.TryGetValue(tag, out lazyCache))
return lazyCache.Value;
throw new Exception("EfCacheL2.GetOrAddCache it should never happens");
} }
else return cache;
return lazyCache.Value;
} }
private static IEnumerable<T> ConvertToIEnumerable<T>(IEnumerable data) private static IEnumerable<T> ConvertToIEnumerable<T>(IEnumerable? data)
{ {
if (data is IEnumerable<T> list) if (data is IEnumerable<T> list)
return list; return list;
@ -157,84 +229,177 @@ namespace AsbCloudInfrastructure.Services.Cache
throw new NotSupportedException("cache.Data has wrong type."); throw new NotSupportedException("cache.Data has wrong type.");
} }
private static Dictionary<TKey, T> ConvertToDictionary<TKey, T>(IEnumerable data, Func<T, TKey> keySelector) private static Dictionary<TKey, T> ConvertToDictionary<TKey, T>(IEnumerable? data, Func<T, TKey> keySelector)
where TKey : notnull where TKey : notnull
{ {
if (data is Dictionary<TKey, T> dictionary) if (data is Dictionary<TKey, T> dictionary)
return dictionary; return dictionary;
else else if (data is IEnumerable<T> enumerable)
{ {
System.Diagnostics.Trace.TraceWarning($"ConvertToDictionary. Use keyed method on keyless cache. Type: {typeof(T).Name};"); System.Diagnostics.Trace.TraceWarning($"ConvertToDictionary. Use keyed method on keyless cache. Type: {typeof(T).Name};");
return ((IEnumerable<T>)data).ToDictionary(keySelector); return enumerable.ToDictionary(keySelector);
} }
else
throw new NotSupportedException("cache.Data has wrong type.");
} }
private static Func<CacheItem> MakeValueListFactory<T>(IQueryable<T> query, TimeSpan obsolescence) /// <summary>
{ /// Кешировать запрос в List\<typeparamref name="T"\>.
CacheItem ValueFactory() /// Выборки по PK будут работать медленнее, чем при кешировании в виде словаря.
{ /// </summary>
var list = query.ToList(); /// <typeparam name="T"></typeparam>
RequestsToDb++; /// <param name="query"></param>
return new CacheItem(list, obsolescence); /// <param name="tag">Метка кеша</param>
} /// <param name="obsolescence">Период устаревания данных</param>
return ValueFactory; /// <returns></returns>
}
private static Func<CacheItem> MakeValueDictionaryFactory<TKey, T>(IQueryable<T> query, TimeSpan obsolescence, Func<T, TKey> keySelector)
where TKey : notnull
{
CacheItem ValueFactory()
{
var dictionary = query.ToDictionary(keySelector);
RequestsToDb++;
return new CacheItem(dictionary, obsolescence);
};
return ValueFactory;
}
public static IEnumerable<T> FromCache<T>(this IQueryable<T> query, string tag, TimeSpan obsolescence) public static IEnumerable<T> FromCache<T>(this IQueryable<T> query, string tag, TimeSpan obsolescence)
{ {
var factory = MakeValueListFactory(query, obsolescence); IEnumerable factory ()
var cache = GetOrAddCache(tag, factory); {
CountOfRequestsToDB++;
return query.ToList();
}
var cache = GetOrAddCache(tag, factory, obsolescence);
return ConvertToIEnumerable<T>(cache.Data); return ConvertToIEnumerable<T>(cache.Data);
} }
/// <summary>
/// Асинхронно кешировать запрос в List\<typeparamref name="T"\>.
/// Выборки по PK будут работать медленнее, чем при кешировании в виде словаря.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="query"></param>
/// <param name="tag">Метка кеша</param>
/// <param name="obsolescence">Период устаревания данных</param>
/// <param name="token"></param>
/// <returns></returns>
public static async Task<IEnumerable<T>> FromCacheAsync<T>(this IQueryable<T> query, string tag, TimeSpan obsolescence, CancellationToken token = default) public static async Task<IEnumerable<T>> FromCacheAsync<T>(this IQueryable<T> query, string tag, TimeSpan obsolescence, CancellationToken token = default)
{ {
var factory = MakeValueListFactory(query, obsolescence); async Task<IEnumerable> factory(CancellationToken token)
var cache = await GetOrAddCacheAsync(tag, factory, token); {
CountOfRequestsToDB++;
return await query.ToListAsync(token);
}
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
return ConvertToIEnumerable<T>(cache.Data); return ConvertToIEnumerable<T>(cache.Data);
} }
/// <summary>
/// Кешировать запрос в Dictionary\<typeparamref name="TKey", typeparamref name="T"\>.
/// </summary>
/// <typeparam name="TKey">тип ключа</typeparam>
/// <typeparam name="T">тип значения</typeparam>
/// <param name="query"></param>
/// <param name="tag">Метка кеша</param>
/// <param name="obsolescence">Период устаревания данных</param>
/// <param name="keySelector">Делегат получения ключа из записи</param>
/// <returns></returns>
/// <example>
///
/// </example>
public static Dictionary<TKey, T> FromCache<TKey, T>(this IQueryable<T> query, string tag, TimeSpan obsolescence, Func<T, TKey> keySelector) public static Dictionary<TKey, T> FromCache<TKey, T>(this IQueryable<T> query, string tag, TimeSpan obsolescence, Func<T, TKey> keySelector)
where TKey: notnull where TKey: notnull
{ {
var factory = MakeValueDictionaryFactory(query, obsolescence, keySelector); IEnumerable factory ()
var cache = GetOrAddCache(tag, factory); {
CountOfRequestsToDB++;
return query.ToDictionary(keySelector);
}
var cache = GetOrAddCache(tag, factory, obsolescence);
return ConvertToDictionary(cache.Data, keySelector); return ConvertToDictionary(cache.Data, keySelector);
} }
/// <summary>
/// Асинхронно кешировать запрос в Dictionary\<typeparamref name="TKey", typeparamref name="T"\>.
/// </summary>
/// <typeparam name="TKey">тип ключа</typeparam>
/// <typeparam name="T">тип значения</typeparam>
/// <param name="query"></param>
/// <param name="tag">Метка кеша</param>
/// <param name="obsolescence">Период устаревания данных</param>
/// <param name="keySelector">Делегат получения ключа из записи</param>
/// <param name="token"></param>
/// <returns></returns>
public static async Task<Dictionary<TKey, T>> FromCacheAsync<TKey, T>(this IQueryable<T> query, string tag, TimeSpan obsolescence, Func<T, TKey> keySelector, CancellationToken token = default) public static async Task<Dictionary<TKey, T>> FromCacheAsync<TKey, T>(this IQueryable<T> query, string tag, TimeSpan obsolescence, Func<T, TKey> keySelector, CancellationToken token = default)
where TKey : notnull where TKey : notnull
{ {
var factory = MakeValueDictionaryFactory(query, obsolescence, keySelector); async Task<IEnumerable> factory(CancellationToken token)
var cache = await GetOrAddCacheAsync(tag, factory, token); {
CountOfRequestsToDB++;
return await query.ToDictionaryAsync(keySelector, token);
}
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
return ConvertToDictionary(cache.Data, keySelector); return ConvertToDictionary(cache.Data, keySelector);
} }
/// <summary>
/// Получить запись из кеша по ключу.
/// При отсутствии кеша создаст его для всех записей из query.
/// </summary>
/// <typeparam name="TKey">тип ключа</typeparam>
/// <typeparam name="T">тип значения</typeparam>
/// <param name="query"></param>
/// <param name="tag">Метка кеша</param>
/// <param name="obsolescence">Период устаревания данных</param>
/// <param name="keySelector">Делегат получения ключа из записи</param>
/// <param name="key"></param>
/// <returns></returns>
/// <exception cref="NotSupportedException">if cache contains trash</exception>
public static T? FromCacheGetValueOrDefault<TKey, T>(this IQueryable<T> query, string tag, TimeSpan obsolescence, Func<T, TKey> keySelector, TKey key) public static T? FromCacheGetValueOrDefault<TKey, T>(this IQueryable<T> query, string tag, TimeSpan obsolescence, Func<T, TKey> keySelector, TKey key)
where TKey : notnull where TKey : notnull
{ {
var factory = MakeValueDictionaryFactory(query, obsolescence, keySelector); IEnumerable factory()
var cache = GetOrAddCache(tag, factory); {
CountOfRequestsToDB++;
if (cache.Data is Dictionary<TKey, T> dictionary) return query.ToDictionary(keySelector);
}
var cache = GetOrAddCache(tag, factory, obsolescence);
var data = cache.Data;
if (data is Dictionary<TKey, T> dictionary)
return dictionary.GetValueOrDefault(key); return dictionary.GetValueOrDefault(key);
else else if (data is IEnumerable<T> enumerable)
{ {
System.Diagnostics.Trace.TraceWarning($"Use keyed method on keyless cache. Tag: {tag}, type: {typeof(T).Name};"); System.Diagnostics.Trace.TraceWarning($"Use keyed method on keyless cache. Tag: {tag}, type: {typeof(T).Name};");
return ((IEnumerable<T>)cache.Data).FirstOrDefault(v => keySelector(v).Equals(key)); return enumerable.FirstOrDefault(v => keySelector(v).Equals(key));
} }
else
throw new NotSupportedException("cache.Data has wrong type.");
}
/// <summary>
/// Асинхронно получить запись из кеша по ключу.
/// При отсутствии кеша создаст его для всех записей из query.
/// </summary>
/// <typeparam name="TKey">тип ключа</typeparam>
/// <typeparam name="T">тип значения</typeparam>
/// <param name="query"></param>
/// <param name="tag">Метка кеша</param>
/// <param name="obsolescence">Период устаревания данных</param>
/// <param name="keySelector">Делегат получения ключа из записи</param>
/// <param name="key"></param>
/// <param name="token"></param>
/// <returns></returns>
/// <exception cref="NotSupportedException">if cache contains trash</exception>
public static async Task<T?> FromCacheGetValueOrDefaultAsync<TKey, T>(this IQueryable<T> query, string tag, TimeSpan obsolescence, Func<T, TKey> keySelector, TKey key, CancellationToken token = default)
where TKey : notnull
{
async Task<IEnumerable> factory(CancellationToken token)
{
CountOfRequestsToDB++;
return await query.ToDictionaryAsync(keySelector, token);
}
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
var data = cache.Data;
if (data is Dictionary<TKey, T> dictionary)
return dictionary.GetValueOrDefault(key);
else if (data is IEnumerable<T> enumerable)
{
System.Diagnostics.Trace.TraceWarning($"Use keyed method on keyless cache. Tag: {tag}, type: {typeof(T).Name};");
return enumerable.FirstOrDefault(v => keySelector(v).Equals(key));
}
else
throw new NotSupportedException("cache.Data has wrong type.");
} }
public static void DropCache<T>(this IQueryable<T> query, string tag) public static void DropCache<T>(this IQueryable<T> query, string tag)

View File

@ -14,8 +14,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" /> <PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="Google.Apis.Drive.v3" Version="1.55.0.2502" /> <PackageReference Include="Google.Apis.Drive.v3" Version="1.57.0.2684" />
<PackageReference Include="Mapster" Version="7.2.0" /> <PackageReference Include="Mapster" Version="7.3.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Net.Http" Version="4.3.4" /> <PackageReference Include="System.Net.Http" Version="4.3.4" />
</ItemGroup> </ItemGroup>

View File

@ -1,7 +1,6 @@
using AsbCloudApp.Data; using AsbCloudApp.Data;
using AsbCloudDb.Model; using AsbCloudDb.Model;
using AsbCloudInfrastructure.Services.Cache; using AsbCloudInfrastructure.Services.Cache;
//using AsbCloudInfrastructure.Services.Cache;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -14,40 +13,18 @@ namespace ConsoleApp1
class Program class Program
{ {
// use ServiceFactory to make services
static void Main(/*string[] args*/) static void Main(/*string[] args*/)
{ {
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<DateTimeOffset, DateTime>()
.MapWith((source) => source.DateTime);
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<DateTime, DateTimeOffset>()
.MapWith((source) => source == default ? new DateTime(0, DateTimeKind.Utc) : source);
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<TimeDto, TimeOnly>()
.MapWith((source) => source == default? default: source.MakeTimeOnly());
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<TimeOnly, TimeDto>()
.MapWith((source) => new (source));
var sh = new ScheduleDto{
ShiftStart = new TimeDto { Hour = 11, Minute = 30, }
};
var en = sh.Adapt<Schedule>();
var aa = en.Adapt<ScheduleDto>();
// use ServiceFactory to make services
Console.WriteLine("hit keyboard to start"); Console.WriteLine("hit keyboard to start");
Console.ReadLine(); Console.ReadLine();
for (int i = 0; i < 24; i++) for (int i = 0; i < 24; i++)
{ {
//Thread.Sleep(3000);
var t = new Thread(_ => { var t = new Thread(_ => {
for (int j = 0; j < 64; j++) for (int j = 0; j < 64; j++)
//Task.Run(GetClastersAsync).Wait(); Task.Run(GetClastersAsync).Wait();
GetClasters(); //GetClasters();
}); });
t.Start(); t.Start();
} }
@ -65,14 +42,14 @@ namespace ConsoleApp1
.Where(t => t.IdTelemetry == 135) .Where(t => t.IdTelemetry == 135)
.OrderBy(t => t.DateTime) .OrderBy(t => t.DateTime)
.Take(100_000) .Take(100_000)
.FromCache("tds", obso, r=>(r.IdTelemetry, r.DateTime)) .FromCache("tds", obso)
.ToList(); .ToList();
sw.Stop(); sw.Stop();
Console.WriteLine($"{DateTime.Now}\tth: {Thread.CurrentThread.ManagedThreadId}\trequests {EfCacheL2.RequestsToDb}\ttime {sw.ElapsedMilliseconds}\tcount {cs.Count}"); //Console.WriteLine($"{DateTime.Now}\tth: {Thread.CurrentThread.ManagedThreadId}\trequests {EfCacheL2rev4.RequestsToDb}\ttime {sw.ElapsedMilliseconds}\tcount {cs.Count}");
//Console.WriteLine($"{DateTime.Now}\tth: {Thread.CurrentThread.ManagedThreadId}\ttime {sw.ElapsedMilliseconds}\tcount {cs.Count}"); Console.WriteLine($"{DateTime.Now}\tth: {Thread.CurrentThread.ManagedThreadId}\ttime {sw.ElapsedMilliseconds}\tcount {cs.Count}");
GC.Collect(); GC.Collect();
Thread.Sleep(100); Thread.Sleep(10);
return (cs.Count, sw.ElapsedMilliseconds); return (cs.Count, sw.ElapsedMilliseconds);
} }
@ -87,11 +64,11 @@ namespace ConsoleApp1
.FromCacheAsync("tds", obso, r => (r.IdTelemetry, r.DateTime))) .FromCacheAsync("tds", obso, r => (r.IdTelemetry, r.DateTime)))
.ToList(); .ToList();
sw.Stop(); sw.Stop();
Console.WriteLine($"{DateTime.Now}\tth: {Thread.CurrentThread.ManagedThreadId}\trequests {EfCacheL2.RequestsToDb}\ttime {sw.ElapsedMilliseconds}\tcount {cs.Count}"); //Console.WriteLine($"{DateTime.Now}\tth: {Thread.CurrentThread.ManagedThreadId}\trequests {EfCacheL2rev4.RequestsToDb}\ttime {sw.ElapsedMilliseconds}\tcount {cs.Count}");
//Console.WriteLine($"{DateTime.Now}\tth: {Thread.CurrentThread.ManagedThreadId}\ttime {sw.ElapsedMilliseconds}\tcount {cs.Count}"); Console.WriteLine($"{DateTime.Now}\tth: {Thread.CurrentThread.ManagedThreadId}\ttime {sw.ElapsedMilliseconds}\tcount {cs.Count}");
GC.Collect(); GC.Collect();
Thread.Sleep(100); Thread.Sleep(10);
return (cs.Count, sw.ElapsedMilliseconds); return (cs.Count, sw.ElapsedMilliseconds);
} }
} }