// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; namespace MQTTnet.Benchmarks { [SimpleJob(RuntimeMoniker.NetCoreApp50)] [RPlotExporter] [MemoryDiagnoser] public class TopicFilterComparerBenchmark { static readonly char[] TopicLevelSeparator = { '/' }; readonly string _longTopic = "AAAAAAAAAAAAAssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; [Benchmark] public void MqttTopicFilterComparer_10000_LoopMethod() { for (var i = 0; i < 100000; i++) { MqttTopicFilterComparer.Compare("sport/tennis/player1", "sport/#"); MqttTopicFilterComparer.Compare("sport/tennis/player1/ranking", "sport/#/ranking"); MqttTopicFilterComparer.Compare("sport/tennis/player1/score/wimbledon", "sport/+/player1/#"); MqttTopicFilterComparer.Compare("sport/tennis/player1", "sport/tennis/+"); MqttTopicFilterComparer.Compare("/finance", "+/+"); MqttTopicFilterComparer.Compare("/finance", "/+"); MqttTopicFilterComparer.Compare("/finance", "+"); MqttTopicFilterComparer.Compare(_longTopic, _longTopic); } } [Benchmark] public void MqttTopicFilterComparer_10000_LoopMethod_Without_Pointer() { for (var i = 0; i < 100000; i++) { MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1", "sport/#"); MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1/ranking", "sport/#/ranking"); MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1/score/wimbledon", "sport/+/player1/#"); MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1", "sport/tennis/+"); MqttTopicFilterComparerWithoutPointer.Compare("/finance", "+/+"); MqttTopicFilterComparerWithoutPointer.Compare("/finance", "/+"); MqttTopicFilterComparerWithoutPointer.Compare("/finance", "+"); MqttTopicFilterComparerWithoutPointer.Compare(_longTopic, _longTopic); } } [Benchmark] public void MqttTopicFilterComparer_10000_StringSplitMethod() { for (var i = 0; i < 100000; i++) { LegacyMethodByStringSplit("sport/tennis/player1", "sport/#"); LegacyMethodByStringSplit("sport/tennis/player1/ranking", "sport/#/ranking"); LegacyMethodByStringSplit("sport/tennis/player1/score/wimbledon", "sport/+/player1/#"); LegacyMethodByStringSplit("sport/tennis/player1", "sport/tennis/+"); LegacyMethodByStringSplit("/finance", "+/+"); LegacyMethodByStringSplit("/finance", "/+"); LegacyMethodByStringSplit("/finance", "+"); MqttTopicFilterComparer.Compare(_longTopic, _longTopic); } } [GlobalSetup] public void Setup() { } static bool LegacyMethodByStringSplit(string topic, string filter) { if (topic == null) { throw new ArgumentNullException(nameof(topic)); } if (filter == null) { throw new ArgumentNullException(nameof(filter)); } if (string.Equals(topic, filter, StringComparison.Ordinal)) { return true; } var fragmentsTopic = topic.Split(TopicLevelSeparator, StringSplitOptions.None); var fragmentsFilter = filter.Split(TopicLevelSeparator, StringSplitOptions.None); // # > In either case it MUST be the last character specified in the Topic Filter [MQTT-4.7.1-2]. for (var i = 0; i < fragmentsFilter.Length; i++) { if (fragmentsFilter[i] == "+") { continue; } if (fragmentsFilter[i] == "#") { return true; } if (i >= fragmentsTopic.Length) { return false; } if (!string.Equals(fragmentsFilter[i], fragmentsTopic[i], StringComparison.Ordinal)) { return false; } } return fragmentsTopic.Length == fragmentsFilter.Length; } public static class MqttTopicFilterComparerWithoutPointer { public const char LevelSeparator = '/'; public const char MultiLevelWildcard = '#'; public const char SingleLevelWildcard = '+'; public const char ReservedTopicPrefix = '$'; public static MqttTopicFilterCompareResult Compare(string topic, string filter) { if (string.IsNullOrEmpty(topic)) { return MqttTopicFilterCompareResult.TopicInvalid; } if (string.IsNullOrEmpty(filter)) { return MqttTopicFilterCompareResult.FilterInvalid; } var filterOffset = 0; var filterLength = filter.Length; var topicOffset = 0; var topicLength = topic.Length; var topicPointer = topic; var filterPointer = filter; var isMultiLevelFilter = filterPointer[filterLength - 1] == MultiLevelWildcard; var isReservedTopic = topicPointer[0] == ReservedTopicPrefix; if (isReservedTopic && filterLength == 1 && isMultiLevelFilter) { // It is not allowed to receive i.e. '$foo/bar' with filter '#'. return MqttTopicFilterCompareResult.NoMatch; } if (isReservedTopic && filterPointer[0] == SingleLevelWildcard) { // It is not allowed to receive i.e. '$SYS/monitor/Clients' with filter '+/monitor/Clients'. return MqttTopicFilterCompareResult.NoMatch; } if (filterLength == 1 && isMultiLevelFilter) { // Filter '#' matches basically everything. return MqttTopicFilterCompareResult.IsMatch; } // Go through the filter char by char. while (filterOffset < filterLength && topicOffset < topicLength) { // Check if the current char is a multi level wildcard. The char is only allowed // at the very las position. if (filterPointer[filterOffset] == MultiLevelWildcard && filterOffset != filterLength - 1) { return MqttTopicFilterCompareResult.FilterInvalid; } if (filterPointer[filterOffset] == topicPointer[topicOffset]) { if (topicOffset == topicLength - 1) { // Check for e.g. "foo" matching "foo/#" if (filterOffset == filterLength - 3 && filterPointer[filterOffset + 1] == LevelSeparator && isMultiLevelFilter) { return MqttTopicFilterCompareResult.IsMatch; } // Check for e.g. "foo/" matching "foo/#" if (filterOffset == filterLength - 2 && filterPointer[filterOffset] == LevelSeparator && isMultiLevelFilter) { return MqttTopicFilterCompareResult.IsMatch; } } filterOffset++; topicOffset++; // Check if the end was reached and i.e. "foo/bar" matches "foo/bar" if (filterOffset == filterLength && topicOffset == topicLength) { return MqttTopicFilterCompareResult.IsMatch; } var endOfTopic = topicOffset == topicLength; if (endOfTopic && filterOffset == filterLength - 1 && filterPointer[filterOffset] == SingleLevelWildcard) { if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator) { return MqttTopicFilterCompareResult.FilterInvalid; } return MqttTopicFilterCompareResult.IsMatch; } } else { if (filterPointer[filterOffset] == SingleLevelWildcard) { // Check for invalid "+foo" or "a/+foo" subscription if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator) { return MqttTopicFilterCompareResult.FilterInvalid; } // Check for bad "foo+" or "foo+/a" subscription if (filterOffset < filterLength - 1 && filterPointer[filterOffset + 1] != LevelSeparator) { return MqttTopicFilterCompareResult.FilterInvalid; } filterOffset++; while (topicOffset < topicLength && topicPointer[topicOffset] != LevelSeparator) { topicOffset++; } if (topicOffset == topicLength && filterOffset == filterLength) { return MqttTopicFilterCompareResult.IsMatch; } } else if (filterPointer[filterOffset] == MultiLevelWildcard) { if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator) { return MqttTopicFilterCompareResult.FilterInvalid; } if (filterOffset + 1 != filterLength) { return MqttTopicFilterCompareResult.FilterInvalid; } return MqttTopicFilterCompareResult.IsMatch; } else { // Check for e.g. "foo/bar" matching "foo/+/#". if (filterOffset > 0 && filterOffset + 2 == filterLength && topicOffset == topicLength && filterPointer[filterOffset - 1] == SingleLevelWildcard && filterPointer[filterOffset] == LevelSeparator && isMultiLevelFilter) { return MqttTopicFilterCompareResult.IsMatch; } return MqttTopicFilterCompareResult.NoMatch; } } } return MqttTopicFilterCompareResult.NoMatch; } } } }