diff --git a/Lucene.Net.sln b/Lucene.Net.sln index 1561cf9d94..bb4ee7b6fb 100644 --- a/Lucene.Net.sln +++ b/Lucene.Net.sln @@ -190,6 +190,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{4D0ED7D9 src\dotnet\Lucene.Net.CodeAnalysis\tools\install.ps1 = src\dotnet\Lucene.Net.CodeAnalysis\tools\install.ps1 src\dotnet\Lucene.Net.CodeAnalysis\tools\uninstall.ps1 = src\dotnet\Lucene.Net.CodeAnalysis\tools\uninstall.ps1 EndProjectSection +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lucene.Net.Tests.BenchmarkDotNet", "src\Lucene.Net.Tests.BenchmarkDotNet\Lucene.Net.Tests.BenchmarkDotNet.csproj", "{0C476146-411E-4C94-8A59-726A5F982A89}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lucene.Net.Tests.AllProjects", "src\Lucene.Net.Tests.AllProjects\Lucene.Net.Tests.AllProjects.csproj", "{9880B87D-8D14-476B-B093-9C3AA0DA8B24}" EndProject @@ -491,6 +492,12 @@ Global {E71152A0-48CC-4334-981F-F5FBFFA50891}.Debug|Any CPU.Build.0 = Debug|Any CPU {E71152A0-48CC-4334-981F-F5FBFFA50891}.Release|Any CPU.ActiveCfg = Release|Any CPU {E71152A0-48CC-4334-981F-F5FBFFA50891}.Release|Any CPU.Build.0 = Release|Any CPU + {8988CDA4-8420-4BEE-869A-66825055EED2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8988CDA4-8420-4BEE-869A-66825055EED2}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {0C476146-411E-4C94-8A59-726A5F982A89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C476146-411E-4C94-8A59-726A5F982A89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C476146-411E-4C94-8A59-726A5F982A89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C476146-411E-4C94-8A59-726A5F982A89}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Lucene.Net.Tests.BenchmarkDotNet/BuildConfigurations.cs b/src/Lucene.Net.Tests.BenchmarkDotNet/BuildConfigurations.cs new file mode 100644 index 0000000000..693c236f53 --- /dev/null +++ b/src/Lucene.Net.Tests.BenchmarkDotNet/BuildConfigurations.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; + +namespace Lucene.Net.Tests.BenchmarkDotNet +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + public static class BuildConfigurations + { + public static IList Configs = new List + { + //new BuildConfiguration { PackageVersion = "4.8.0-beta00005" }, + //new BuildConfiguration { PackageVersion = "4.8.0-beta00006" }, + //new BuildConfiguration { PackageVersion = "4.8.0-beta00007" }, + //new BuildConfiguration { PackageVersion = "4.8.0-beta00008" }, + //new BuildConfiguration { PackageVersion = "4.8.0-beta00009" }, + //new BuildConfiguration { PackageVersion = "4.8.0-beta00010" }, + //new BuildConfiguration { PackageVersion = "4.8.0-beta00011" }, + //new BuildConfiguration { PackageVersion = "4.8.0-beta00012" }, + //new BuildConfiguration { PackageVersion = "4.8.0-beta00013" }, + new BuildConfiguration { PackageVersion = "4.8.0-beta00014" }, + new BuildConfiguration { PackageVersion = "4.8.0-beta00015" }, + new BuildConfiguration { PackageVersion = "4.8.0-beta00016" }, + new BuildConfiguration { PackageVersion = "4.8.0-beta00017" }, + //new BuildConfiguration { CustomConfigurationName = "LocalBuild", Id = "LocalBuild" }, // NOTE: This functions, but for some reason is less performant than testing a NuGet package + }; + } + + public class BuildConfiguration + { + private string id; + + /// + /// NuGet package version. May be on a NuGet feed or a local directory configured as a feed. + /// + public string PackageVersion { get; set; } + + public string CustomConfigurationName { get; set; } + + public string Id + { + get + { + if (id is null) + return PackageVersion; + return id; + } + set => id = value; + } + } +} diff --git a/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsAssociationsBenchmarks.cs b/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsAssociationsBenchmarks.cs new file mode 100644 index 0000000000..0de3df198f --- /dev/null +++ b/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsAssociationsBenchmarks.cs @@ -0,0 +1,61 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using Lucene.Net.Demo.Facet; + +namespace Lucene.Net.Tests.BenchmarkDotNet +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + [MemoryDiagnoser] + [Config(typeof(Config))] + public class FacetsAssociationsBenchmarks + { + private class Config : ManualConfig + { + public Config() + { + var baseJob = Job.MediumRun; + + for (int i = 0; i < BuildConfigurations.Configs.Count; i++) + { + var config = BuildConfigurations.Configs[i]; + if (string.IsNullOrEmpty(config.CustomConfigurationName)) + { + AddJob(baseJob.WithNuGet("Lucene.Net.Analysis.Common", config.PackageVersion) + .WithNuGet("Lucene.Net.Facet", config.PackageVersion) + .WithId($"{i:000}-{config.Id}")); + } + else + { + AddJob(baseJob.WithCustomBuildConfiguration(config.CustomConfigurationName) + .WithId($"{i:000}-{config.Id}")); + } + } + } + } + + public static readonly AssociationsFacetsExample example = new AssociationsFacetsExample(); + + [Benchmark] + public void RunSumAssociations() => example.RunSumAssociations(); + + [Benchmark] + public void RunDrillDown() => example.RunDrillDown(); + } +} diff --git a/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsDistanceBenchmarks.cs b/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsDistanceBenchmarks.cs new file mode 100644 index 0000000000..6fbf6f069f --- /dev/null +++ b/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsDistanceBenchmarks.cs @@ -0,0 +1,68 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using Lucene.Net.Demo.Facet; + +namespace Lucene.Net.Tests.BenchmarkDotNet +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + [MemoryDiagnoser] + [Config(typeof(Config))] + public class FacetsDistanceBenchmarks + { + private class Config : ManualConfig + { + public Config() + { + var baseJob = Job.MediumRun; + + for (int i = 0; i < BuildConfigurations.Configs.Count; i++) + { + var config = BuildConfigurations.Configs[i]; + if (string.IsNullOrEmpty(config.CustomConfigurationName)) + { + AddJob(baseJob.WithNuGet("Lucene.Net.Analysis.Common", config.PackageVersion) + .WithNuGet("Lucene.Net.Expressions", config.PackageVersion) + .WithNuGet("Lucene.Net.Facet", config.PackageVersion) + .WithId($"{i:000}-{config.Id}")); + } + else + { + AddJob(baseJob.WithCustomBuildConfiguration(config.CustomConfigurationName) + .WithId($"{i:000}-{config.Id}")); + } + } + } + } + + public static readonly DistanceFacetsExample example = new DistanceFacetsExample(); + + [GlobalSetup] + public void GlobalSetup() => example.Index(); + + [GlobalCleanup] + public void GlobalTearDown() => example.Dispose(); + + [Benchmark] + public void Search() => example.Search(); + + [Benchmark] + public void DrillDown() => example.DrillDown(DistanceFacetsExample.TWO_KM); + } +} diff --git a/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsExpressionAggregationBenchmarks.cs b/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsExpressionAggregationBenchmarks.cs new file mode 100644 index 0000000000..eaed15772b --- /dev/null +++ b/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsExpressionAggregationBenchmarks.cs @@ -0,0 +1,59 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using Lucene.Net.Demo.Facet; + +namespace Lucene.Net.Tests.BenchmarkDotNet +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + [MemoryDiagnoser] + [Config(typeof(Config))] + public class FacetsExpressionAggregationBenchmarks + { + private class Config : ManualConfig + { + public Config() + { + var baseJob = Job.MediumRun; + + for (int i = 0; i < BuildConfigurations.Configs.Count; i++) + { + var config = BuildConfigurations.Configs[i]; + if (string.IsNullOrEmpty(config.CustomConfigurationName)) + { + AddJob(baseJob.WithNuGet("Lucene.Net.Analysis.Common", config.PackageVersion) + .WithNuGet("Lucene.Net.Expressions", config.PackageVersion) + .WithNuGet("Lucene.Net.Facet", config.PackageVersion) + .WithId($"{i:000}-{config.Id}")); + } + else + { + AddJob(baseJob.WithCustomBuildConfiguration(config.CustomConfigurationName) + .WithId($"{i:000}-{config.Id}")); + } + } + } + } + + public static readonly ExpressionAggregationFacetsExample example = new ExpressionAggregationFacetsExample(); + + [Benchmark] + public void RunSearch() => example.RunSearch(); + } +} diff --git a/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsMultiCategoryListsBenchmarks.cs b/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsMultiCategoryListsBenchmarks.cs new file mode 100644 index 0000000000..3f7e8a78d7 --- /dev/null +++ b/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsMultiCategoryListsBenchmarks.cs @@ -0,0 +1,58 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using Lucene.Net.Demo.Facet; + +namespace Lucene.Net.Tests.BenchmarkDotNet +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + [MemoryDiagnoser] + [Config(typeof(Config))] + public class FacetsMultiCategoryListsBenchmarks + { + private class Config : ManualConfig + { + public Config() + { + var baseJob = Job.MediumRun; + + for (int i = 0; i < BuildConfigurations.Configs.Count; i++) + { + var config = BuildConfigurations.Configs[i]; + if (string.IsNullOrEmpty(config.CustomConfigurationName)) + { + AddJob(baseJob.WithNuGet("Lucene.Net.Analysis.Common", config.PackageVersion) + .WithNuGet("Lucene.Net.Facet", config.PackageVersion) + .WithId($"{i:000}-{config.Id}")); + } + else + { + AddJob(baseJob.WithCustomBuildConfiguration(config.CustomConfigurationName) + .WithId($"{i:000}-{config.Id}")); + } + } + } + } + + public static readonly MultiCategoryListsFacetsExample example = new MultiCategoryListsFacetsExample(); + + [Benchmark] + public void RunSearch() => example.RunSearch(); + } +} diff --git a/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsRangeBenchmarks.cs b/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsRangeBenchmarks.cs new file mode 100644 index 0000000000..8e5be3a999 --- /dev/null +++ b/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsRangeBenchmarks.cs @@ -0,0 +1,72 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using Lucene.Net.Demo.Facet; + +namespace Lucene.Net.Tests.BenchmarkDotNet +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + [MemoryDiagnoser] + [Config(typeof(Config))] + public class FacetsRangeBenchmarks + { + private class Config : ManualConfig + { + public Config() + { + var baseJob = Job.MediumRun; + + for (int i = 0; i < BuildConfigurations.Configs.Count; i++) + { + var config = BuildConfigurations.Configs[i]; + if (string.IsNullOrEmpty(config.CustomConfigurationName)) + { + AddJob(baseJob.WithNuGet("Lucene.Net.Analysis.Common", config.PackageVersion) + .WithNuGet("Lucene.Net.Facet", config.PackageVersion) + .WithId($"{i:000}-{config.Id}")); + } + else + { + AddJob(baseJob.WithCustomBuildConfiguration(config.CustomConfigurationName) + .WithId($"{i:000}-{config.Id}")); + } + } + } + } + + public static readonly RangeFacetsExample example = new RangeFacetsExample(); + + [GlobalCleanup] + public void GlobalTearDown() => example.Dispose(); + + [Benchmark] + public void Search() + { + example.Index(); + example.Search(); + } + + [Benchmark] + public void DrillDown() + { + example.Index(); + example.DrillDown(example.PAST_SIX_HOURS); + } + } +} diff --git a/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsSimpleBenchmarks.cs b/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsSimpleBenchmarks.cs new file mode 100644 index 0000000000..981a65a2c2 --- /dev/null +++ b/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsSimpleBenchmarks.cs @@ -0,0 +1,67 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using Lucene.Net.Demo.Facet; + +namespace Lucene.Net.Tests.BenchmarkDotNet +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + [MemoryDiagnoser] + [Config(typeof(Config))] + public class FacetsSimpleBenchmarks + { + private class Config : ManualConfig + { + public Config() + { + var baseJob = Job.MediumRun; + + for (int i = 0; i < BuildConfigurations.Configs.Count; i++) + { + var config = BuildConfigurations.Configs[i]; + if (string.IsNullOrEmpty(config.CustomConfigurationName)) + { + AddJob(baseJob.WithNuGet("Lucene.Net.Analysis.Common", config.PackageVersion) + .WithNuGet("Lucene.Net.Facet", config.PackageVersion) + .WithId($"{i:000}-{config.Id}")); + } + else + { + AddJob(baseJob.WithCustomBuildConfiguration(config.CustomConfigurationName) + .WithId($"{i:000}-{config.Id}")); + } + } + } + } + + public static readonly SimpleFacetsExample example = new SimpleFacetsExample(); + + [Benchmark] + public void RunFacetOnly() => example.RunFacetOnly(); + + [Benchmark] + public void RunSearch() => example.RunSearch(); + + [Benchmark] + public void RunDrillDown() => example.RunDrillDown(); + + [Benchmark] + public void RunDrillSideways() => example.RunDrillSideways(); + } +} diff --git a/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsSimpleSortedSetBenchmarks.cs b/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsSimpleSortedSetBenchmarks.cs new file mode 100644 index 0000000000..591b09131e --- /dev/null +++ b/src/Lucene.Net.Tests.BenchmarkDotNet/FacetsSimpleSortedSetBenchmarks.cs @@ -0,0 +1,61 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using Lucene.Net.Demo.Facet; + +namespace Lucene.Net.Tests.BenchmarkDotNet +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + [MemoryDiagnoser] + [Config(typeof(Config))] + public class FacetsSimpleSortedSetBenchmarks + { + private class Config : ManualConfig + { + public Config() + { + var baseJob = Job.MediumRun; + + for (int i = 0; i < BuildConfigurations.Configs.Count; i++) + { + var config = BuildConfigurations.Configs[i]; + if (string.IsNullOrEmpty(config.CustomConfigurationName)) + { + AddJob(baseJob.WithNuGet("Lucene.Net.Analysis.Common", config.PackageVersion) + .WithNuGet("Lucene.Net.Facet", config.PackageVersion) + .WithId($"{i:000}-{config.Id}")); + } + else + { + AddJob(baseJob.WithCustomBuildConfiguration(config.CustomConfigurationName) + .WithId($"{i:000}-{config.Id}")); + } + } + } + } + + public static readonly SimpleSortedSetFacetsExample example = new SimpleSortedSetFacetsExample(); + + [Benchmark] + public void RunSearch() => example.RunSearch(); + + [Benchmark] + public void RunDrillDown() => example.RunDrillDown(); + } +} diff --git a/src/Lucene.Net.Tests.BenchmarkDotNet/HomePageScriptBenchmarks.cs b/src/Lucene.Net.Tests.BenchmarkDotNet/HomePageScriptBenchmarks.cs new file mode 100644 index 0000000000..ba11f56b94 --- /dev/null +++ b/src/Lucene.Net.Tests.BenchmarkDotNet/HomePageScriptBenchmarks.cs @@ -0,0 +1,123 @@ +using System; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using Lucene.Net.Util; +using Lucene.Net.Store; +using Lucene.Net.Analysis.Standard; +using Lucene.Net.Index; +using Lucene.Net.Documents; +using Lucene.Net.Search; +using System.Diagnostics; + +namespace Lucene.Net.Tests.BenchmarkDotNet +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + [MemoryDiagnoser] + [Config(typeof(Config))] + public class HomePageScriptBenchmarks + { + private class Config : ManualConfig + { + public Config() + { + var baseJob = Job.MediumRun; + + for (int i = 0; i < BuildConfigurations.Configs.Count; i++) + { + var config = BuildConfigurations.Configs[i]; + if (string.IsNullOrEmpty(config.CustomConfigurationName)) + { + AddJob(baseJob.WithNuGet("Lucene.Net.Analysis.Common", config.PackageVersion) + .WithId($"{i:000}-{config.Id}")); + } + else + { + AddJob(baseJob.WithCustomBuildConfiguration(config.CustomConfigurationName) + .WithId($"{i:000}-{config.Id}")); + } + } + } + } + + private const int _directoryWriterIterations = 10; + private const int _indexSearchIterations = 25; + + [Benchmark] + public void HomePageScript() + { + // Ensures index backwards compatibility + var AppLuceneVersion = LuceneVersion.LUCENE_48; + + for (int d = 0; d < _directoryWriterIterations; d++) + { + using var dir = new RAMDirectory(); + + //create an analyzer to process the text + var analyzer = new StandardAnalyzer(AppLuceneVersion); + + //create an index writer + var indexConfig = new IndexWriterConfig(AppLuceneVersion, analyzer); + using var writer = new IndexWriter(dir, indexConfig); + + for (int i = 0; i < _indexSearchIterations; i++) + { + var source = new + { + Name = $"Kermit{i} the Frog{i}", + FavoritePhrase = $"The quick{i} brown{i} fox{i} jumps{i} over{i} the lazy{i} dog{i} " + }; + Document doc = new Document + { + // StringField indexes but doesn't tokenize + new StringField("name", source.Name, Field.Store.YES), + new TextField("favoritePhrase", source.FavoritePhrase, Field.Store.YES) + }; + + writer.AddDocument(doc); + writer.Flush(triggerMerge: false, applyAllDeletes: false); + } + + for (int i = 0; i < _indexSearchIterations; i++) + { + // search with a phrase + var phrase = new MultiPhraseQuery + { + new Term("favoritePhrase", $"brown{i}"), + new Term("favoritePhrase", $"fox{i}") + }; + + // re-use the writer to get real-time updates + using var reader = writer.GetReader(applyAllDeletes: true); + var searcher = new IndexSearcher(reader); + var hits = searcher.Search(phrase, 20 /* top 20 */).ScoreDocs; + Debug.Assert(hits.Length > 0); + foreach (var hit in hits) + { + var foundDoc = searcher.Doc(hit.Doc); + var score = hit.Score; + var name = foundDoc.Get("name"); + var favoritePhrase = foundDoc.Get("favoritePhrase"); + } + } + } + } + + } +} diff --git a/src/Lucene.Net.Tests.BenchmarkDotNet/IndexFilesBenchmarks.cs b/src/Lucene.Net.Tests.BenchmarkDotNet/IndexFilesBenchmarks.cs new file mode 100644 index 0000000000..8656a1b458 --- /dev/null +++ b/src/Lucene.Net.Tests.BenchmarkDotNet/IndexFilesBenchmarks.cs @@ -0,0 +1,238 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using Lucene.Net.Analysis; +using Lucene.Net.Analysis.Standard; +using Lucene.Net.Documents; +using Lucene.Net.Index; +using Lucene.Net.Store; +using Lucene.Net.BenchmarkDotNet.Util; +using Lucene.Net.Util; +using System; +using System.IO; +using System.Text; + +namespace Lucene.Net.Tests.BenchmarkDotNet +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + [MemoryDiagnoser] + [Config(typeof(Config))] + public class IndexFilesBenchmarks + { + private class Config : ManualConfig + { + public Config() + { + var baseJob = Job.MediumRun; + + for (int i = 0; i < BuildConfigurations.Configs.Count; i++) + { + var config = BuildConfigurations.Configs[i]; + if (string.IsNullOrEmpty(config.CustomConfigurationName)) + { + AddJob(baseJob.WithNuGet("Lucene.Net.Analysis.Common", config.PackageVersion) + .WithId($"{i:000}-{config.Id}")); + } + else + { + AddJob(baseJob.WithCustomBuildConfiguration(config.CustomConfigurationName) + .WithId($"{i:000}-{config.Id}")); + } + } + } + } + + private static DirectoryInfo sourceDirectory; + private static DirectoryInfo indexDirectory; + + [GlobalSetup] + public void GlobalSetUp() + { + sourceDirectory = PathUtil.CreateTempDir("sourceFiles"); + int seed = 2342; + ContentGenerator.GenerateFiles(new Random(seed), sourceDirectory.FullName, 250); + } + + [GlobalCleanup] + public void GlobalTearDown() + { + try + { + if (System.IO.Directory.Exists(sourceDirectory.FullName)) + System.IO.Directory.Delete(sourceDirectory.FullName, recursive: true); + } + catch { } + } + + [IterationSetup] + public void IterationSetUp() + { + indexDirectory = PathUtil.CreateTempDir("indexFiles"); + } + + [IterationCleanup] + public void IterationTearDown() + { + try + { + if (System.IO.Directory.Exists(indexDirectory.FullName)) + System.IO.Directory.Delete(indexDirectory.FullName, recursive: true); + } + catch { } + + } + + /// Index all text files under a directory. + [Benchmark] + public void IndexFiles() => IndexFiles(sourceDirectory, indexDirectory); + + /// Index all text files under a directory. + public static void IndexFiles(DirectoryInfo sourceDirectory, DirectoryInfo indexDirectory) + { + string indexPath = indexDirectory.FullName; + + bool create = true; + + Store.Directory dir = FSDirectory.Open(indexPath); + // :Post-Release-Update-Version.LUCENE_XY: + Analyzer analyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48); + IndexWriterConfig iwc = new IndexWriterConfig(LuceneVersion.LUCENE_48, analyzer); + + if (create) + { + // Create a new index in the directory, removing any + // previously indexed documents: + iwc.OpenMode = OpenMode.CREATE; + } + else + { + // Add new documents to an existing index: + iwc.OpenMode = OpenMode.CREATE_OR_APPEND; + } + + // Optional: for better indexing performance, if you + // are indexing many documents, increase the RAM + // buffer. + // + // iwc.RAMBufferSizeMB = 256.0; + + using (IndexWriter writer = new IndexWriter(dir, iwc)) + { + IndexDocs(writer, sourceDirectory); + + // NOTE: if you want to maximize search performance, + // you can optionally call forceMerge here. This can be + // a terribly costly operation, so generally it's only + // worth it when your index is relatively static (ie + // you're done adding documents to it): + // + // writer.ForceMerge(1); + } + } + + /// + /// Recurses over files and directories found under the + /// given directory and indexes each file. + /// + /// NOTE: This method indexes one document per input file. + /// This is slow. For good throughput, put multiple documents + /// into your input file(s). + /// + /// + /// to the index where the given + /// file/dir info will be stored + /// + /// + /// The directory to recurse into to find files to index. + /// + /// + /// If there is a low-level I/O error. + /// + internal static void IndexDocs(IndexWriter writer, DirectoryInfo directoryInfo) + { + foreach (var dirInfo in directoryInfo.GetDirectories()) + { + IndexDocs(writer, dirInfo); + } + foreach (var fileInfo in directoryInfo.GetFiles()) + { + IndexDocs(writer, fileInfo); + } + } + + /// + /// Indexes the given file using the given writer. + /// + /// + /// to the index where the given + /// file info will be stored. + /// + /// + /// The file to index. + /// + /// + /// If there is a low-level I/O error. + /// + internal static void IndexDocs(IndexWriter writer, FileInfo file) + { + using (FileStream fs = new FileStream(file.FullName, FileMode.Open, FileAccess.Read)) + { + // make a new, empty document + Document doc = new Document(); + + // Add the path of the file as a field named "path". Use a + // field that is indexed (i.e. searchable), but don't tokenize + // the field into separate words and don't index term frequency + // or positional information: + Field pathField = new StringField("path", file.FullName, Field.Store.YES); + doc.Add(pathField); + + // Add the last modified date of the file a field named "modified". + // Use a LongField that is indexed (i.e. efficiently filterable with + // NumericRangeFilter). This indexes to milli-second resolution, which + // is often too fine. You could instead create a number based on + // year/month/day/hour/minutes/seconds, down the resolution you require. + // For example the long value 2011021714 would mean + // February 17, 2011, 2-3 PM. + doc.Add(new Int64Field("modified", file.LastWriteTimeUtc.Ticks, Field.Store.NO)); + + // Add the contents of the file to a field named "contents". Specify a Reader, + // so that the text of the file is tokenized and indexed, but not stored. + // Note that FileReader expects the file to be in UTF-8 encoding. + // If that's not the case searching for special characters will fail. + doc.Add(new TextField("contents", new StreamReader(fs, Encoding.UTF8))); + + if (writer.Config.OpenMode == OpenMode.CREATE) + { + // New index, so we just add the document (no old document can be there): + //Console.WriteLine("adding " + file); + writer.AddDocument(doc); + } + else + { + // Existing index (an old copy of this document may have been indexed) so + // we use updateDocument instead to replace the old one matching the exact + // path, if present: + //Console.WriteLine("updating " + file); + writer.UpdateDocument(new Term("path", file.FullName), doc); + } + } + } + } +} diff --git a/src/Lucene.Net.Tests.BenchmarkDotNet/Lucene.Net.Tests.BenchmarkDotNet.csproj b/src/Lucene.Net.Tests.BenchmarkDotNet/Lucene.Net.Tests.BenchmarkDotNet.csproj new file mode 100644 index 0000000000..e3e54dfd77 --- /dev/null +++ b/src/Lucene.Net.Tests.BenchmarkDotNet/Lucene.Net.Tests.BenchmarkDotNet.csproj @@ -0,0 +1,48 @@ + + + + Exe + net8.0 + Lucene.Net.Tests.BenchmarkDotNet.Program + + + false + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Lucene.Net.Tests.BenchmarkDotNet/Program.cs b/src/Lucene.Net.Tests.BenchmarkDotNet/Program.cs new file mode 100644 index 0000000000..c65ec225c1 --- /dev/null +++ b/src/Lucene.Net.Tests.BenchmarkDotNet/Program.cs @@ -0,0 +1,26 @@ +using BenchmarkDotNet.Running; + +namespace Lucene.Net.Tests.BenchmarkDotNet +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + class Program + { + private static void Main(string[] args) => new BenchmarkSwitcher(typeof(Program).Assembly).Run(args); + } +} diff --git a/src/Lucene.Net.Tests.BenchmarkDotNet/SearchFilesBenchmarks.cs b/src/Lucene.Net.Tests.BenchmarkDotNet/SearchFilesBenchmarks.cs new file mode 100644 index 0000000000..9e5361e0c7 --- /dev/null +++ b/src/Lucene.Net.Tests.BenchmarkDotNet/SearchFilesBenchmarks.cs @@ -0,0 +1,136 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using Lucene.Net.Analysis; +using Lucene.Net.Analysis.Standard; +using Lucene.Net.Index; +using Lucene.Net.QueryParsers.Classic; +using Lucene.Net.Search; +using Lucene.Net.Store; +using Lucene.Net.BenchmarkDotNet.Util; +using Lucene.Net.Util; +using System; +using System.IO; + +namespace Lucene.Net.Tests.BenchmarkDotNet +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + [MemoryDiagnoser] + [Config(typeof(Config))] + public class SearchFilesBenchmarks + { + private class Config : ManualConfig + { + public Config() + { + var baseJob = Job.MediumRun; + + for (int i = 0; i < BuildConfigurations.Configs.Count; i++) + { + var config = BuildConfigurations.Configs[i]; + if (string.IsNullOrEmpty(config.CustomConfigurationName)) + { + AddJob(baseJob.WithNuGet("Lucene.Net.Analysis.Common", config.PackageVersion) + .WithNuGet("Lucene.Net.QueryParser", config.PackageVersion) + .WithId($"{i:000}-{config.Id}")); + } + else + { + AddJob(baseJob.WithCustomBuildConfiguration(config.CustomConfigurationName) + .WithId($"{i:000}-{config.Id}")); + } + } + } + } + + private const string QueryString = "settings"; + private static DirectoryInfo indexDirectory; + + [GlobalSetup] + public void GlobalSetUp() + { + var sourceDirectory = PathUtil.CreateTempDir("sourceFiles"); + + // Generate content to index (including our string that we will search for) + int seed = 2342; + ContentGenerator.GenerateFiles(new Random(seed), sourceDirectory.FullName, 1000, QueryString); + + + // Index the content + indexDirectory = PathUtil.CreateTempDir("indexFiles"); + IndexFilesBenchmarks.IndexFiles(sourceDirectory, indexDirectory); + + // Cleanup our source files, they are no longer needed + try + { + if (System.IO.Directory.Exists(sourceDirectory.FullName)) + System.IO.Directory.Delete(sourceDirectory.FullName, recursive: true); + } + catch { } + } + + [GlobalCleanup] + public void GlobalTearDown() + { + try + { + if (System.IO.Directory.Exists(indexDirectory.FullName)) + System.IO.Directory.Delete(indexDirectory.FullName, recursive: true); + } + catch { } + } + + [Benchmark] + public void SearchFiles() + { + + string index = indexDirectory.FullName; + string field = "contents"; + //string queries = null; + int repeat = 2000; + //bool raw = false; + string queryString = QueryString; + //int hitsPerPage = 10; + + using (IndexReader reader = DirectoryReader.Open(FSDirectory.Open(index))) + { + IndexSearcher searcher = new IndexSearcher(reader); + // :Post-Release-Update-Version.LUCENE_XY: + Analyzer analyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48); + + // :Post-Release-Update-Version.LUCENE_XY: + QueryParser parser = new QueryParser(LuceneVersion.LUCENE_48, field, analyzer); + + Query query = parser.Parse(queryString.Trim()); + //Console.WriteLine("Searching for: " + query.ToString(field)); + + // repeat & time as benchmark + { + //DateTime start = DateTime.UtcNow; + for (int i = 0; i < repeat; i++) + { + searcher.Search(query, null, 100); + } + //DateTime end = DateTime.UtcNow; + //Console.WriteLine("Time: " + (end - start).TotalMilliseconds + "ms"); + } + } // Disposes reader + } + } +} diff --git a/src/Lucene.Net.Tests.BenchmarkDotNet/Util/ContentGenerator.cs b/src/Lucene.Net.Tests.BenchmarkDotNet/Util/ContentGenerator.cs new file mode 100644 index 0000000000..e81c03cc8c --- /dev/null +++ b/src/Lucene.Net.Tests.BenchmarkDotNet/Util/ContentGenerator.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Lucene.Net.BenchmarkDotNet.Util +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + public static class ContentGenerator + { + public static void GenerateFiles(Random random, string directory, int numberOfFiles, params string[] stringsToQuery) + { + var subdirectories = new HashSet(); + for (int i = 0; i < numberOfFiles; i++) + { + bool root = random.Next(1, 100) > 50; + + if (root) + { + GenerateFile(random, directory, stringsToQuery); + } + else + { + string subdirectory; + if (subdirectories.Count > 0 && random.Next(1, 100) > 30) + { + subdirectory = RandomPicks.RandomFrom(random, subdirectories); + } + else + { + subdirectory = RandomSimpleString(random, 5, 20); + subdirectories.Add(subdirectory); + } + GenerateFile(random, Path.Combine(directory, subdirectory), stringsToQuery); + } + } + } + + private static void GenerateFile(Random random, string directory, ICollection stringsToQuery) + { + if (!System.IO.Directory.Exists(directory)) + System.IO.Directory.CreateDirectory(directory); + + string fileName = RandomSimpleString(random, 5, 25) + ".txt"; + int paragraphs = random.Next(5, 25); + + using (var writer = new StreamWriter(Path.Combine(directory, fileName), append: false, encoding: Encoding.UTF8)) + { + for (int i = 0; i < paragraphs; i++) + { + WriteParagraph(random, writer, stringsToQuery); + } + } + } + + private static void WriteParagraph(Random random, TextWriter writer, ICollection stringsToQuery) + { + int words = random.Next(50, 100); + bool addStringsToQuery = stringsToQuery != null && stringsToQuery.Count > 0; + + for (int i = 0; i < words; i++) + { + if (addStringsToQuery && random.Next(1, 1500) == 668) + writer.Write(RandomPicks.RandomFrom(random, stringsToQuery)); + else + writer.Write(RandomSimpleString(random, 1, 8)); + + if (i + 1 < words) + writer.Write(" "); + } + writer.WriteLine("."); + writer.WriteLine(); + } + + /// + /// Returns a random string consisting only of lowercase characters 'a' through 'z'. + /// + public static string RandomSimpleString(Random r, int minLength, int maxLength) + { + int end = RandomNumbers.RandomInt32Between(r, minLength, maxLength); + if (end == 0) + { + // allow 0 length + return ""; + } + char[] buffer = new char[end]; + for (int i = 0; i < end; i++) + { + buffer[i] = (char)RandomNumbers.RandomInt32Between(r, 'a', 'z'); + } + return new string(buffer, 0, end); + } + } +} diff --git a/src/Lucene.Net.Tests.BenchmarkDotNet/Util/NamespacePatch.cs b/src/Lucene.Net.Tests.BenchmarkDotNet/Util/NamespacePatch.cs new file mode 100644 index 0000000000..16b2a07dc3 --- /dev/null +++ b/src/Lucene.Net.Tests.BenchmarkDotNet/Util/NamespacePatch.cs @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Didn't exist in early versions of Lucene.NET - this is just to patch those cases so using directives will compile +namespace Lucene.Net.Documents.Extensions +{ +} diff --git a/src/Lucene.Net.Tests.BenchmarkDotNet/Util/PathUtil.cs b/src/Lucene.Net.Tests.BenchmarkDotNet/Util/PathUtil.cs new file mode 100644 index 0000000000..014e13c4af --- /dev/null +++ b/src/Lucene.Net.Tests.BenchmarkDotNet/Util/PathUtil.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; + +namespace Lucene.Net.BenchmarkDotNet.Util +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + public static class PathUtil + { + private const int TEMP_NAME_RETRY_THRESHOLD = 9999; + + public static DirectoryInfo CreateTempDir(string prefix) + { + //DirectoryInfo @base = BaseTempDirForTestClass(); + + int attempt = 0; + DirectoryInfo f; + bool iterate = true; + do + { + if (attempt++ >= TEMP_NAME_RETRY_THRESHOLD) + { + throw new Exception("Failed to get a temporary name too many times, check your temp directory and consider manually cleaning it: " + System.IO.Path.GetTempPath()); + } + // LUCENENET specific - need to use a random file name instead of a sequential one or two threads may attempt to do + // two operations on a file at the same time. + //f = new DirectoryInfo(Path.Combine(System.IO.Path.GetTempPath(), "LuceneTemp", prefix + "-" + attempt)); + f = new DirectoryInfo(Path.Combine(System.IO.Path.GetTempPath(), "LuceneTemp", prefix + "-" + Path.GetFileNameWithoutExtension(Path.GetRandomFileName()))); + + try + { + if (!System.IO.Directory.Exists(f.FullName)) + { + f.Create(); + iterate = false; + } + } +#pragma warning disable 168 + catch (IOException exc) +#pragma warning restore 168 + { + iterate = true; + } + } while (iterate); + + return f; + } + } +} diff --git a/src/Lucene.Net.Tests.BenchmarkDotNet/Util/RandomNumbers.cs b/src/Lucene.Net.Tests.BenchmarkDotNet/Util/RandomNumbers.cs new file mode 100644 index 0000000000..149adbcb9f --- /dev/null +++ b/src/Lucene.Net.Tests.BenchmarkDotNet/Util/RandomNumbers.cs @@ -0,0 +1,161 @@ +using J2N.Numerics; +using System; +using System.Diagnostics; +using System.Numerics; + +namespace Lucene.Net.BenchmarkDotNet.Util +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + /// + /// Utility classes for selecting random numbers from within a range or the + /// numeric domain for a given type. + /// + /// + public static class RandomNumbers + { + /// + /// Returns a random from (inclusive) to (inclusive). + /// + /// A instance. + /// The inclusive start of the range. + /// The inclusive end of the range. + /// A random from (inclusive) to (inclusive). + /// is greater than . + /// is null. + public static int RandomInt32Between(Random random, int minValue, int maxValue) + { + if (random is null) + throw new ArgumentNullException(nameof(random)); + if (minValue > maxValue) + throw new ArgumentException($"{nameof(minValue)} must be less than or equal to {nameof(maxValue)}. {nameof(minValue)}: {minValue}, {nameof(maxValue)}: {maxValue}"); + var range = maxValue - minValue; + if (range < int.MaxValue) + return minValue + random.Next(1 + range); + + return minValue + (int)Math.Round(random.NextDouble() * range); + } + + /// + /// Returns a random from to (inclusive). + /// + /// A instance. + /// The inclusive start of the range. + /// The inclusive end of the range. + /// A random from to (inclusive). + /// is greater than . + /// is null. + public static long RandomInt64Between(Random random, long minValue, long maxValue) + { + if (random is null) + throw new ArgumentNullException(nameof(random)); + if (minValue > maxValue) + throw new ArgumentException($"{nameof(minValue)} must be less than or equal to {nameof(maxValue)}. {nameof(minValue)}: {minValue}, {nameof(maxValue)}: {maxValue}"); + + BigInteger range = (BigInteger)maxValue + (BigInteger)1 - (BigInteger)minValue; + if (range.CompareTo((BigInteger)int.MaxValue) <= 0) + { + return minValue + random.Next((int)range); + } + else + { + // probably not evenly distributed when range is large, but OK for tests + //BigInteger augend = BigInteger.Multiply(range, new BigInteger(r.NextDouble())); + //long result = start + (long)augend; + + // NOTE: Using BigInteger/Decimal doesn't work because r.NextDouble() is always + // rounded down to 0, which makes the result always the same as start. This alternative solution was + // snagged from https://stackoverflow.com/a/13095144. All we really care about here is that we get + // a pretty good random distribution of values between start and end. + + //Working with ulong so that modulo works correctly with values > long.MaxValue + ulong uRange = (ulong)unchecked(maxValue - minValue); + + //Prevent a modolo bias; see https://stackoverflow.com/a/10984975/238419 + //for more information. + //In the worst case, the expected number of calls is 2 (though usually it's + //much closer to 1) so this loop doesn't really hurt performance at all. + ulong ulongRand; + do + { + byte[] buf = new byte[8]; + random.NextBytes(buf); + ulongRand = (ulong)BitConverter.ToInt64(buf, 0); + } while (ulongRand > ulong.MaxValue - ((ulong.MaxValue % uRange) + 1) % uRange); + + long result = (long)(ulongRand % uRange) + minValue + random.Next(0, 1); // Randomly decide whether to increment by 1 to make the second parameter "inclusive" + + Debug.Assert(result >= minValue); + Debug.Assert(result <= maxValue); + return result; + } + } + + /// + /// Similar to , but returns a between + /// 0 (inclusive) and (exclusive). + /// + /// A instance. + /// The bound on the random number to be returned. Must be positive. + /// A random between 0 and - 1. + /// is less than 1. + /// is null. + public static long NextInt64(Random random, long maxValue) + { + if (random is null) + throw new ArgumentNullException(nameof(random)); + if (maxValue <= 0) + throw new ArgumentOutOfRangeException(nameof(maxValue), maxValue, $"{nameof(maxValue)} must be greater than or equal to 0"); + + long value = random.NextInt64(); + long range = maxValue - 1; + if ((maxValue & range) == 0L) + { + value &= range; + } + else + { + for (long u = value.TripleShift(1); u + range - (value = u % maxValue) < 0L;) + { + u = random.NextInt64().TripleShift(1); + } + } + return value; + } + } + + internal static class RandomExtensions + { + /// + /// Generates a random . + /// + /// This . + /// A random . + /// is null. + // http://stackoverflow.com/a/6651656 + public static long NextInt64(this Random random) // .NET specific to cover missing member from Java + { + if (random is null) + throw new ArgumentNullException(nameof(random)); + + byte[] buffer = new byte[8]; + random.NextBytes(buffer); + return BitConverter.ToInt64(buffer, 0); + } + } +} diff --git a/src/Lucene.Net.Tests.BenchmarkDotNet/Util/RandomPicks.cs b/src/Lucene.Net.Tests.BenchmarkDotNet/Util/RandomPicks.cs new file mode 100644 index 0000000000..e968f842d2 --- /dev/null +++ b/src/Lucene.Net.Tests.BenchmarkDotNet/Util/RandomPicks.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Lucene.Net.BenchmarkDotNet.Util +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + /// + /// Random selections of objects. + /// + public class RandomPicks + { + /// + /// Pick a random element from the (which may be an array). + /// + /// contains no items. + /// or is null. + public static T RandomFrom(Random random, IList list) + { + if (random is null) + throw new ArgumentNullException(nameof(random)); + if (list is null) + throw new ArgumentNullException(nameof(list)); + + if (list.Count == 0) + { + throw new ArgumentException("Can't pick a random object from an empty list."); + } + return list[random.Next(0, list.Count)]; + } + + /// + /// Pick a random element from the . + /// + /// contains no items. + /// or is null. + public static T RandomFrom(Random random, ICollection collection) + { + if (random is null) + throw new ArgumentNullException(nameof(random)); + if (collection is null) + throw new ArgumentNullException(nameof(collection)); + + if (collection.Count == 0) + { + throw new ArgumentException("Can't pick a random object from an empty collection."); + } + return collection.ElementAt(random.Next(0, collection.Count)); + } + } +}