DBRaw.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.Concurrent;
  4. using System.Data;
  5. using System.Data.Common;
  6. using System.Linq;
  7. using System.Runtime.Versioning;
  8. using System.Text;
  9. using System.Threading.Tasks;
  10. using Microsoft.Extensions.Caching.Memory;
  11. using Npgsql;
  12. using System.Runtime.CompilerServices;
  13. namespace PlatformBenchmarks
  14. {
  15. public sealed class RawDb
  16. {
  17. private readonly ConcurrentRandom _random;
  18. private readonly MemoryCache _cache
  19. = new(new MemoryCacheOptions { ExpirationScanFrequency = TimeSpan.FromMinutes(60) });
  20. private static DbProviderFactory _dbProviderFactory => Npgsql.NpgsqlFactory.Instance;
  21. private readonly string _connectionString;
  22. public RawDb(ConcurrentRandom random, string connectionString)
  23. {
  24. _random = random;
  25. _connectionString = connectionString;
  26. }
  27. public async Task<World> LoadSingleQueryRow()
  28. {
  29. using (var connection = (NpgsqlConnection)_dbProviderFactory.CreateConnection())
  30. {
  31. connection.ConnectionString = _connectionString;
  32. await connection.OpenAsync();
  33. var (cmd, _) = CreateReadCommand(connection);
  34. using (var command = cmd)
  35. {
  36. return await ReadSingleRow(cmd);
  37. }
  38. }
  39. }
  40. public Task<CachedWorld[]> LoadCachedQueries(int count)
  41. {
  42. var result = new CachedWorld[count];
  43. var cacheKeys = _cacheKeys;
  44. var cache = _cache;
  45. var random = _random;
  46. for (var i = 0; i < result.Length; i++)
  47. {
  48. var id = random.Next(1, 10001);
  49. var key = cacheKeys[id];
  50. if (cache.TryGetValue(key, out var cached))
  51. {
  52. result[i] = (CachedWorld)cached;
  53. }
  54. else
  55. {
  56. return LoadUncachedQueries(_connectionString, id, i, count, this, result);
  57. }
  58. }
  59. return Task.FromResult(result);
  60. static async Task<CachedWorld[]> LoadUncachedQueries(string conn, int id, int i, int count, RawDb rawdb, CachedWorld[] result)
  61. {
  62. using (var connection = (NpgsqlConnection)_dbProviderFactory.CreateConnection())
  63. {
  64. connection.ConnectionString = conn;
  65. await connection.OpenAsync();
  66. var (cmd, idParameter) = rawdb.CreateReadCommand(connection);
  67. using var command = cmd;
  68. async Task<CachedWorld> create(ICacheEntry _) => await ReadSingleRow(cmd);
  69. var cacheKeys = _cacheKeys;
  70. var key = cacheKeys[id];
  71. idParameter.TypedValue = id;
  72. for (; i < result.Length; i++)
  73. {
  74. result[i] = await rawdb._cache.GetOrCreateAsync(key, create);
  75. id = rawdb._random.Next(1, 10001);
  76. idParameter.TypedValue = id;
  77. key = cacheKeys[id];
  78. }
  79. }
  80. return result;
  81. }
  82. }
  83. public async Task PopulateCache()
  84. {
  85. using (var connection = (NpgsqlConnection)_dbProviderFactory.CreateConnection())
  86. {
  87. connection.ConnectionString = _connectionString;
  88. await connection.OpenAsync();
  89. var (cmd, idParameter) = CreateReadCommand(connection);
  90. using var command = cmd;
  91. var cacheKeys = _cacheKeys;
  92. var cache = _cache;
  93. for (var i = 1; i < 10001; i++)
  94. {
  95. idParameter.TypedValue = i;
  96. cache.Set<CachedWorld>(cacheKeys[i], await ReadSingleRow(cmd));
  97. }
  98. }
  99. Console.WriteLine("Caching Populated");
  100. }
  101. public async Task<World[]> LoadMultipleQueriesRows(int count)
  102. {
  103. var results = new World[count];
  104. using (var connection = (NpgsqlConnection)_dbProviderFactory.CreateConnection())
  105. {
  106. connection.ConnectionString = _connectionString;
  107. await connection.OpenAsync();
  108. using var batch = new NpgsqlBatch(connection)
  109. {
  110. // Inserts a PG Sync message between each statement in the batch, required for compliance with
  111. // TechEmpower general test requirement 7
  112. // https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview
  113. EnableErrorBarriers = true
  114. };
  115. for (var i = 0; i < count; i++)
  116. {
  117. batch.BatchCommands.Add(new()
  118. {
  119. CommandText = "SELECT id, randomnumber FROM world WHERE id = $1",
  120. Parameters = { new NpgsqlParameter<int> { TypedValue = _random.Next(1, 10001) } }
  121. });
  122. }
  123. using var reader = await batch.ExecuteReaderAsync();
  124. for (var i = 0; i < count; i++)
  125. {
  126. await reader.ReadAsync();
  127. results[i] = new World { Id = reader.GetInt32(0), RandomNumber = reader.GetInt32(1) };
  128. await reader.NextResultAsync();
  129. }
  130. }
  131. return results;
  132. }
  133. public async Task<World[]> LoadMultipleUpdatesRows(int count)
  134. {
  135. var results = new World[count];
  136. using (var connection = (NpgsqlConnection)_dbProviderFactory.CreateConnection())
  137. {
  138. connection.ConnectionString = _connectionString;
  139. await connection.OpenAsync();
  140. var (queryCmd, queryParameter) = CreateReadCommand(connection);
  141. using (queryCmd)
  142. {
  143. for (var i = 0; i < results.Length; i++)
  144. {
  145. results[i] = await ReadSingleRow(queryCmd);
  146. queryParameter.TypedValue = _random.Next(1, 10001);
  147. }
  148. }
  149. using (var updateCmd = new NpgsqlCommand(BatchUpdateString.Query(count), connection))
  150. {
  151. for (var i = 0; i < results.Length; i++)
  152. {
  153. var randomNumber = _random.Next(1, 10001);
  154. updateCmd.Parameters.Add(new NpgsqlParameter<int> { TypedValue = results[i].Id });
  155. updateCmd.Parameters.Add(new NpgsqlParameter<int> { TypedValue = randomNumber });
  156. results[i].RandomNumber = randomNumber;
  157. }
  158. await updateCmd.ExecuteNonQueryAsync();
  159. }
  160. }
  161. return results;
  162. }
  163. public async Task<List<Fortune>> LoadFortunesRows()
  164. {
  165. // Benchmark requirements explicitly prohibit pre-initializing the list size
  166. var result = new List<Fortune>();
  167. using (var connection = (NpgsqlConnection)_dbProviderFactory.CreateConnection())
  168. {
  169. connection.ConnectionString = _connectionString;
  170. await connection.OpenAsync();
  171. using (var cmd = new NpgsqlCommand("SELECT id, message FROM fortune", connection))
  172. {
  173. using (var rdr = await cmd.ExecuteReaderAsync())
  174. {
  175. while (await rdr.ReadAsync())
  176. {
  177. result.Add(new Fortune
  178. {
  179. Id = rdr.GetInt32(0),
  180. Message = rdr.GetString(1)
  181. });
  182. }
  183. }
  184. }
  185. }
  186. result.Add(new Fortune { Message = "Additional fortune added at request time." });
  187. result.Sort();
  188. return result;
  189. }
  190. private (NpgsqlCommand readCmd, NpgsqlParameter<int> idParameter) CreateReadCommand(NpgsqlConnection connection)
  191. {
  192. var cmd = new NpgsqlCommand("SELECT id, randomnumber FROM world WHERE id = $1", connection);
  193. var parameter = new NpgsqlParameter<int> { TypedValue = _random.Next(1, 10001) };
  194. cmd.Parameters.Add(parameter);
  195. return (cmd, parameter);
  196. }
  197. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  198. private static async Task<World> ReadSingleRow(NpgsqlCommand cmd)
  199. {
  200. using var rdr = await cmd.ExecuteReaderAsync(System.Data.CommandBehavior.SingleRow);
  201. await rdr.ReadAsync();
  202. return new World
  203. {
  204. Id = rdr.GetInt32(0),
  205. RandomNumber = rdr.GetInt32(1)
  206. };
  207. }
  208. private static readonly object[] _cacheKeys = Enumerable.Range(0, 10001).Select((i) => new CacheKey(i)).ToArray();
  209. public sealed class CacheKey : IEquatable<CacheKey>
  210. {
  211. private readonly int _value;
  212. public CacheKey(int value)
  213. => _value = value;
  214. public bool Equals(CacheKey key)
  215. => key._value == _value;
  216. public override bool Equals(object obj)
  217. => ReferenceEquals(obj, this);
  218. public override int GetHashCode()
  219. => _value;
  220. public override string ToString()
  221. => _value.ToString();
  222. }
  223. }
  224. internal sealed class BatchUpdateString
  225. {
  226. private const int MaxBatch = 500;
  227. internal static readonly string[] ParamNames = Enumerable.Range(0, MaxBatch * 2).Select(i => $"@p{i}").ToArray();
  228. private static string[] _queries = new string[MaxBatch + 1];
  229. public static string Query(int batchSize)
  230. => _queries[batchSize] is null
  231. ? CreateBatch(batchSize)
  232. : _queries[batchSize];
  233. private static string CreateBatch(int batchSize)
  234. {
  235. var sb = StringBuilderCache.Acquire();
  236. sb.Append("UPDATE world SET randomNumber = temp.randomNumber FROM (VALUES ");
  237. var c = 1;
  238. for (var i = 0; i < batchSize; i++)
  239. {
  240. if (i > 0)
  241. sb.Append(", ");
  242. sb.Append($"(${c++}, ${c++})");
  243. }
  244. sb.Append(" ORDER BY 1) AS temp(id, randomNumber) WHERE temp.id = world.id");
  245. return _queries[batchSize] = StringBuilderCache.GetStringAndRelease(sb);
  246. }
  247. }
  248. internal static class StringBuilderCache
  249. {
  250. private const int DefaultCapacity = 1386;
  251. private const int MaxBuilderSize = DefaultCapacity * 3;
  252. [ThreadStatic]
  253. private static StringBuilder t_cachedInstance;
  254. /// <summary>Get a StringBuilder for the specified capacity.</summary>
  255. /// <remarks>If a StringBuilder of an appropriate size is cached, it will be returned and the cache emptied.</remarks>
  256. public static StringBuilder Acquire(int capacity = DefaultCapacity)
  257. {
  258. if (capacity <= MaxBuilderSize)
  259. {
  260. StringBuilder sb = t_cachedInstance;
  261. if (capacity < DefaultCapacity)
  262. {
  263. capacity = DefaultCapacity;
  264. }
  265. if (sb != null)
  266. {
  267. // Avoid stringbuilder block fragmentation by getting a new StringBuilder
  268. // when the requested size is larger than the current capacity
  269. if (capacity <= sb.Capacity)
  270. {
  271. t_cachedInstance = null;
  272. sb.Clear();
  273. return sb;
  274. }
  275. }
  276. }
  277. return new StringBuilder(capacity);
  278. }
  279. public static void Release(StringBuilder sb)
  280. {
  281. if (sb.Capacity <= MaxBuilderSize)
  282. {
  283. t_cachedInstance = sb;
  284. }
  285. }
  286. public static string GetStringAndRelease(StringBuilder sb)
  287. {
  288. string result = sb.ToString();
  289. Release(sb);
  290. return result;
  291. }
  292. }
  293. }