You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

277 lines
12 KiB

  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. // See the LICENSE file in the project root for more information.
  4. using System;
  5. using BenchmarkDotNet.Attributes;
  6. using BenchmarkDotNet.Jobs;
  7. namespace MQTTnet.Benchmarks
  8. {
  9. [SimpleJob(RuntimeMoniker.NetCoreApp50)]
  10. [RPlotExporter]
  11. [MemoryDiagnoser]
  12. public class TopicFilterComparerBenchmark
  13. {
  14. static readonly char[] TopicLevelSeparator = { '/' };
  15. readonly string _longTopic =
  16. "AAAAAAAAAAAAAssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC";
  17. [Benchmark]
  18. public void MqttTopicFilterComparer_10000_LoopMethod()
  19. {
  20. for (var i = 0; i < 100000; i++)
  21. {
  22. MqttTopicFilterComparer.Compare("sport/tennis/player1", "sport/#");
  23. MqttTopicFilterComparer.Compare("sport/tennis/player1/ranking", "sport/#/ranking");
  24. MqttTopicFilterComparer.Compare("sport/tennis/player1/score/wimbledon", "sport/+/player1/#");
  25. MqttTopicFilterComparer.Compare("sport/tennis/player1", "sport/tennis/+");
  26. MqttTopicFilterComparer.Compare("/finance", "+/+");
  27. MqttTopicFilterComparer.Compare("/finance", "/+");
  28. MqttTopicFilterComparer.Compare("/finance", "+");
  29. MqttTopicFilterComparer.Compare(_longTopic, _longTopic);
  30. }
  31. }
  32. [Benchmark]
  33. public void MqttTopicFilterComparer_10000_LoopMethod_Without_Pointer()
  34. {
  35. for (var i = 0; i < 100000; i++)
  36. {
  37. MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1", "sport/#");
  38. MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1/ranking", "sport/#/ranking");
  39. MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1/score/wimbledon", "sport/+/player1/#");
  40. MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1", "sport/tennis/+");
  41. MqttTopicFilterComparerWithoutPointer.Compare("/finance", "+/+");
  42. MqttTopicFilterComparerWithoutPointer.Compare("/finance", "/+");
  43. MqttTopicFilterComparerWithoutPointer.Compare("/finance", "+");
  44. MqttTopicFilterComparerWithoutPointer.Compare(_longTopic, _longTopic);
  45. }
  46. }
  47. [Benchmark]
  48. public void MqttTopicFilterComparer_10000_StringSplitMethod()
  49. {
  50. for (var i = 0; i < 100000; i++)
  51. {
  52. LegacyMethodByStringSplit("sport/tennis/player1", "sport/#");
  53. LegacyMethodByStringSplit("sport/tennis/player1/ranking", "sport/#/ranking");
  54. LegacyMethodByStringSplit("sport/tennis/player1/score/wimbledon", "sport/+/player1/#");
  55. LegacyMethodByStringSplit("sport/tennis/player1", "sport/tennis/+");
  56. LegacyMethodByStringSplit("/finance", "+/+");
  57. LegacyMethodByStringSplit("/finance", "/+");
  58. LegacyMethodByStringSplit("/finance", "+");
  59. MqttTopicFilterComparer.Compare(_longTopic, _longTopic);
  60. }
  61. }
  62. [GlobalSetup]
  63. public void Setup()
  64. {
  65. }
  66. static bool LegacyMethodByStringSplit(string topic, string filter)
  67. {
  68. if (topic == null)
  69. {
  70. throw new ArgumentNullException(nameof(topic));
  71. }
  72. if (filter == null)
  73. {
  74. throw new ArgumentNullException(nameof(filter));
  75. }
  76. if (string.Equals(topic, filter, StringComparison.Ordinal))
  77. {
  78. return true;
  79. }
  80. var fragmentsTopic = topic.Split(TopicLevelSeparator, StringSplitOptions.None);
  81. var fragmentsFilter = filter.Split(TopicLevelSeparator, StringSplitOptions.None);
  82. // # > In either case it MUST be the last character specified in the Topic Filter [MQTT-4.7.1-2].
  83. for (var i = 0; i < fragmentsFilter.Length; i++)
  84. {
  85. if (fragmentsFilter[i] == "+")
  86. {
  87. continue;
  88. }
  89. if (fragmentsFilter[i] == "#")
  90. {
  91. return true;
  92. }
  93. if (i >= fragmentsTopic.Length)
  94. {
  95. return false;
  96. }
  97. if (!string.Equals(fragmentsFilter[i], fragmentsTopic[i], StringComparison.Ordinal))
  98. {
  99. return false;
  100. }
  101. }
  102. return fragmentsTopic.Length == fragmentsFilter.Length;
  103. }
  104. public static class MqttTopicFilterComparerWithoutPointer
  105. {
  106. public const char LevelSeparator = '/';
  107. public const char MultiLevelWildcard = '#';
  108. public const char SingleLevelWildcard = '+';
  109. public const char ReservedTopicPrefix = '$';
  110. public static MqttTopicFilterCompareResult Compare(string topic, string filter)
  111. {
  112. if (string.IsNullOrEmpty(topic))
  113. {
  114. return MqttTopicFilterCompareResult.TopicInvalid;
  115. }
  116. if (string.IsNullOrEmpty(filter))
  117. {
  118. return MqttTopicFilterCompareResult.FilterInvalid;
  119. }
  120. var filterOffset = 0;
  121. var filterLength = filter.Length;
  122. var topicOffset = 0;
  123. var topicLength = topic.Length;
  124. var topicPointer = topic;
  125. var filterPointer = filter;
  126. var isMultiLevelFilter = filterPointer[filterLength - 1] == MultiLevelWildcard;
  127. var isReservedTopic = topicPointer[0] == ReservedTopicPrefix;
  128. if (isReservedTopic && filterLength == 1 && isMultiLevelFilter)
  129. {
  130. // It is not allowed to receive i.e. '$foo/bar' with filter '#'.
  131. return MqttTopicFilterCompareResult.NoMatch;
  132. }
  133. if (isReservedTopic && filterPointer[0] == SingleLevelWildcard)
  134. {
  135. // It is not allowed to receive i.e. '$SYS/monitor/Clients' with filter '+/monitor/Clients'.
  136. return MqttTopicFilterCompareResult.NoMatch;
  137. }
  138. if (filterLength == 1 && isMultiLevelFilter)
  139. {
  140. // Filter '#' matches basically everything.
  141. return MqttTopicFilterCompareResult.IsMatch;
  142. }
  143. // Go through the filter char by char.
  144. while (filterOffset < filterLength && topicOffset < topicLength)
  145. {
  146. // Check if the current char is a multi level wildcard. The char is only allowed
  147. // at the very las position.
  148. if (filterPointer[filterOffset] == MultiLevelWildcard && filterOffset != filterLength - 1)
  149. {
  150. return MqttTopicFilterCompareResult.FilterInvalid;
  151. }
  152. if (filterPointer[filterOffset] == topicPointer[topicOffset])
  153. {
  154. if (topicOffset == topicLength - 1)
  155. {
  156. // Check for e.g. "foo" matching "foo/#"
  157. if (filterOffset == filterLength - 3 && filterPointer[filterOffset + 1] == LevelSeparator && isMultiLevelFilter)
  158. {
  159. return MqttTopicFilterCompareResult.IsMatch;
  160. }
  161. // Check for e.g. "foo/" matching "foo/#"
  162. if (filterOffset == filterLength - 2 && filterPointer[filterOffset] == LevelSeparator && isMultiLevelFilter)
  163. {
  164. return MqttTopicFilterCompareResult.IsMatch;
  165. }
  166. }
  167. filterOffset++;
  168. topicOffset++;
  169. // Check if the end was reached and i.e. "foo/bar" matches "foo/bar"
  170. if (filterOffset == filterLength && topicOffset == topicLength)
  171. {
  172. return MqttTopicFilterCompareResult.IsMatch;
  173. }
  174. var endOfTopic = topicOffset == topicLength;
  175. if (endOfTopic && filterOffset == filterLength - 1 && filterPointer[filterOffset] == SingleLevelWildcard)
  176. {
  177. if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator)
  178. {
  179. return MqttTopicFilterCompareResult.FilterInvalid;
  180. }
  181. return MqttTopicFilterCompareResult.IsMatch;
  182. }
  183. }
  184. else
  185. {
  186. if (filterPointer[filterOffset] == SingleLevelWildcard)
  187. {
  188. // Check for invalid "+foo" or "a/+foo" subscription
  189. if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator)
  190. {
  191. return MqttTopicFilterCompareResult.FilterInvalid;
  192. }
  193. // Check for bad "foo+" or "foo+/a" subscription
  194. if (filterOffset < filterLength - 1 && filterPointer[filterOffset + 1] != LevelSeparator)
  195. {
  196. return MqttTopicFilterCompareResult.FilterInvalid;
  197. }
  198. filterOffset++;
  199. while (topicOffset < topicLength && topicPointer[topicOffset] != LevelSeparator)
  200. {
  201. topicOffset++;
  202. }
  203. if (topicOffset == topicLength && filterOffset == filterLength)
  204. {
  205. return MqttTopicFilterCompareResult.IsMatch;
  206. }
  207. }
  208. else if (filterPointer[filterOffset] == MultiLevelWildcard)
  209. {
  210. if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator)
  211. {
  212. return MqttTopicFilterCompareResult.FilterInvalid;
  213. }
  214. if (filterOffset + 1 != filterLength)
  215. {
  216. return MqttTopicFilterCompareResult.FilterInvalid;
  217. }
  218. return MqttTopicFilterCompareResult.IsMatch;
  219. }
  220. else
  221. {
  222. // Check for e.g. "foo/bar" matching "foo/+/#".
  223. if (filterOffset > 0 && filterOffset + 2 == filterLength && topicOffset == topicLength && filterPointer[filterOffset - 1] == SingleLevelWildcard &&
  224. filterPointer[filterOffset] == LevelSeparator && isMultiLevelFilter)
  225. {
  226. return MqttTopicFilterCompareResult.IsMatch;
  227. }
  228. return MqttTopicFilterCompareResult.NoMatch;
  229. }
  230. }
  231. }
  232. return MqttTopicFilterCompareResult.NoMatch;
  233. }
  234. }
  235. }
  236. }