Skip to content

Commit

Permalink
CultureInfo Refactoring
Browse files Browse the repository at this point in the history
The goal of these refactoring is to unify some of our API and respect the given value of CultureInfo.
The implementation is still not perfect, but it's an important first step.
Highlights:

* Unify ToString APIs
  All of the ToStr(), ToTimeStr(), ToSizeStr() methods were removed.
  Initially, they were introduced as hacked shortcuts to formatting purposes, but today they become misleading in some cases.
  Instead of them, now we always should use ToString().
  Where it makes sense, the proper overloads like ToString(CultureInfo, format) are available.
* Introduce CultureInfo property in Config
  Now it's possible to set the CultureInfo, the given value will be used everywhere including exporters (Fix #1295)
* Remove MultiEncodingString and Encodings in Configs
  The original goal of these classes was providing a way to enable Unicode support in exporter (see #487 for details).
  Unfortunately, it made many APIs overcomplicated because we had to pass Encoding everywhere.
  If we think carefully, we will understand that the only problem that we actually have relates to the terminal that doesn't support Unicode
    (in other places, we can use Unicode symbols without any problems).
  I decided to delete the MultiEncodingString class, use Unicode symbols by default, and patch them only in ConsoleLogger
    (see AsciiHelper for details).
  Now, users can turn on Unicode support in console output via [UnicodeConsoleLogger]
    (see IntroUnicode for details).
  All other exporters are always able to use Unicode.
  Some of them may patch Unicode symbols to achieve better portability (e.g., see HtmlExporter.HtmlLoggerWrapper.Escape)
* Introduce ILogger.Id and ILogger.Priority
  These APIs allow overriding existing loggers by custom.
  For each Id, only the logger with the highest priority will be chosen.
  With the help of this feature, we can override ConsoleLogger.Ascii with ConsoleLogger.Unicode
* Introduce SizeValue
  This structure is a wrapper for a long value that helps to operate with size values (bytes, kilobytes, etc.)
* Rename TimeInterval->TimeValue
  The original name was confusing because an interval typically has start and end.
  In our situation, it similar to TimeSpan but has its own features and use cases.
  I decided to rename it to TimeValue (to make consistent with SizeValue).
* Introduce CultureInfo.GetActualListSeparator()
  TextInfo.ListSeparator shouldn't be used anymore because it returns incorrect value on .NET Core+Unix
    (see dotnet/runtime#536 for details)
  • Loading branch information
AndreyAkinshin committed Dec 5, 2019
1 parent 8486e16 commit f7053ae
Show file tree
Hide file tree
Showing 141 changed files with 1,214 additions and 905 deletions.
2 changes: 1 addition & 1 deletion docs/_changelog/header/v0.11.0.md
Expand Up @@ -332,7 +332,7 @@ dotnet run -c Release -- --runtimes clr core

* **Unicode support:**
now you can enable support of Unicode symbols like `μ` or `±` with `[EncodingAttribute.Unicode]`,
an example: @BenchmarkDotNet.Samples.IntroEncoding
an example: BenchmarkDotNet.Samples.IntroEncoding
(see [#735](https://github.com/dotnet/BenchmarkDotNet/pull/735))
* **Better benchmark validation**
(see [#693](https://github.com/dotnet/BenchmarkDotNet/pull/693), [#737](https://github.com/dotnet/BenchmarkDotNet/pull/737))
Expand Down
10 changes: 0 additions & 10 deletions docs/articles/configs/encoding.md

This file was deleted.

2 changes: 0 additions & 2 deletions docs/articles/configs/toc.yml
Expand Up @@ -20,7 +20,5 @@
href: filters.md
- name: Orderers
href: orderers.md
- name: Encoding
href: encoding.md
- name: ConfigOptions
href: configoptions.md
@@ -1,23 +1,23 @@
---
uid: BenchmarkDotNet.Samples.IntroEncoding
uid: BenchmarkDotNet.Samples.IntroUnicode
---

## Sample: IntroEncoding
## Sample: IntroUnicode

BenchmarkDotNet currently supports two encodings for output - `ASCII` and `Unicode`.
By default `ASCII` is set.
`Unicode` allows to use special characters, like `μ` and `±`.
*Encoding* allows you to set encoding in your benchmark.
Some of the BenchmarkDotNet exporters use Unicode symbols that are not ASCII-compatible (e.g., `μ` or `±`).
Unfortunately, some terminals are not supported such symbols.
That's why BenchmarkDotNet prints only ASCII characters by default (`μ` will be replaced by `u`).
If you want to display Unicode symbols in your terminal, you should use `[UnicodeConsoleLoggerAttribute]` (see usage examples below).

> [!WARNING]
> You should be sure that your terminal/text editor supports Unicode.
> To use these feature, you should be sure that your terminal/text editor supports Unicode.
> On Windows, you may have some troubles with Unicode symbols
> if system default code page configured as non-English
> (in Control Panel + Regional and Language Options, Language for Non-Unicode Programs).
### Source code

[!code-csharp[IntroEncoding.cs](../../../samples/BenchmarkDotNet.Samples/IntroEncoding.cs)]
[!code-csharp[IntroUnicode.cs](../../../samples/BenchmarkDotNet.Samples/IntroUnicode.cs)]

### Output

Expand All @@ -40,9 +40,7 @@ Skewness = 0.12, Kurtosis = 1.56, MValue = 2

### Links

* @docs.encoding
* @BenchmarkDotNet.Attributes.EncodingAttribute
* @BenchmarkDotNet.Helpers.MultiEncodingString
* The permanent link to this sample: @BenchmarkDotNet.Samples.IntroEncoding
* @BenchmarkDotNet.Attributes.UnicodeConsoleLoggerAttribute
* The permanent link to this sample: @BenchmarkDotNet.Samples.IntroUnicode

---
6 changes: 3 additions & 3 deletions docs/articles/samples/toc.yml
Expand Up @@ -32,8 +32,6 @@
href: IntroDisassemblyDry.md
- name: IntroDisassemblyRyuJit
href: IntroDisassemblyRyuJit.md
- name: IntroEncoding
href: IntroEncoding.md
- name: IntroEnvVars
href: IntroEnvVars.md
- name: IntroExport
Expand Down Expand Up @@ -107,4 +105,6 @@
- name: IntroTagColumn
href: IntroTagColumn.md
- name: IntroTailcall
href: IntroTailcall.md
href: IntroTailcall.md
- name: IntroUnicode
href: IntroUnicode.md
24 changes: 24 additions & 0 deletions samples/BenchmarkDotNet.Samples/IntroCultureInfo.cs
@@ -0,0 +1,24 @@
using System.Globalization;
using System.Threading;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;

namespace BenchmarkDotNet.Samples
{
[Config(typeof(Config))]
[ShortRunJob]
public class IntroCultureInfo
{
private class Config : ManualConfig
{
public Config()
{
CultureInfo = (CultureInfo) CultureInfo.InvariantCulture.Clone();
CultureInfo.NumberFormat.NumberDecimalSeparator = "@";
}
}

[Benchmark]
public void Foo() => Thread.Sleep(100);
}
}
@@ -1,64 +1,58 @@
using System.Diagnostics;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using System.Text;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Running;

namespace BenchmarkDotNet.Samples
{
// *** Attribute Style ***

[EncodingAttribute.Unicode]
public class IntroEncoding
[UnicodeConsoleLogger]
public class IntroUnicode
{
[Benchmark]
public long Foo()
{
long waitUntil = Stopwatch.GetTimestamp() + 1000;
while (Stopwatch.GetTimestamp() < waitUntil) { }

return waitUntil;
}
}

// *** Object Style ***

[Config(typeof(Config))]
public class IntroEncodingObjectStyle
public class IntroUnicodeObjectStyle
{
private class Config : ManualConfig
{
public Config() => Encoding = Encoding.Unicode;
public Config() => Add(ConsoleLogger.Unicode);
}

[Benchmark]
public long Foo()
{
long waitUntil = Stopwatch.GetTimestamp() + 1000;
while (Stopwatch.GetTimestamp() < waitUntil) { }

return waitUntil;
}
}

// *** Fluent Config ***

public class IntroEncodingFluentConfig
public class IntroUnicodeFluentConfig
{
public static void Run()
{
BenchmarkRunner.Run<IntroEncodingFluentConfig>(
BenchmarkRunner.Run<IntroUnicodeFluentConfig>(
ManualConfig
.Create(DefaultConfig.Instance)
.With(Encoding.Unicode));
.With(ConsoleLogger.Unicode));
}

[Benchmark]
public long Foo()
{
long waitUntil = Stopwatch.GetTimestamp() + 1000;
while (Stopwatch.GetTimestamp() < waitUntil) { }

return waitUntil;
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/BenchmarkDotNet.Diagnostics.Windows/LogCapture.cs
Expand Up @@ -9,6 +9,9 @@ public class LogCapture : ILogger

private readonly List<OutputLine> capturedOutput = new List<OutputLine>(100);

public string Id => nameof(LogCapture);
public int Priority => 0;

public void Write(LogKind logKind, string text)
{
capturedOutput.Add(new OutputLine
Expand Down
@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;
Expand Down Expand Up @@ -184,12 +184,12 @@ public IEnumerable<Metric> Parse()
var memoryAllocatedPerOperation = totalAllocation / totalOperation;
var memoryLeakPerOperation = nativeLeakSize / totalOperation;

logger.WriteLine($"Native memory allocated per single operation: {memoryAllocatedPerOperation.ToSizeStr(SizeUnit.B)}");
logger.WriteLine($"Native memory allocated per single operation: {SizeValue.FromBytes(memoryAllocatedPerOperation).ToString(SizeUnit.B, DefaultCultureInfo.Instance)}");
logger.WriteLine($"Count of allocated object: {countOfAllocatedObject / totalOperation}");

if (nativeLeakSize != 0)
{
logger.WriteLine($"Native memory leak per single operation: {memoryLeakPerOperation.ToSizeStr(SizeUnit.B)}");
logger.WriteLine($"Native memory leak per single operation: {SizeValue.FromBytes(memoryLeakPerOperation).ToString(SizeUnit.B, DefaultCultureInfo.Instance)}");
}

var heapInfoList = heaps.Select(h => new { Address = h.Key, h.Value.Count, types = h.Value.Values });
Expand Down
4 changes: 2 additions & 2 deletions src/BenchmarkDotNet/Analysers/MinIterationTimeAnalyser.cs
Expand Up @@ -9,7 +9,7 @@ namespace BenchmarkDotNet.Analysers
{
public class MinIterationTimeAnalyser : AnalyserBase
{
private static readonly TimeInterval MinSufficientIterationTime = 100 * TimeInterval.Millisecond;
private static readonly TimeValue MinSufficientIterationTime = 100 * TimeValue.Millisecond;

public override string Id => "MinIterationTime";
public static readonly IAnalyser Default = new MinIterationTimeAnalyser();
Expand All @@ -23,7 +23,7 @@ protected override IEnumerable<Conclusion> AnalyseReport(BenchmarkReport report,
var target = report.AllMeasurements.Where(m => m.Is(IterationMode.Workload, IterationStage.Actual)).ToArray();
if (target.IsEmpty())
yield break;
var minActualIterationTime = TimeInterval.FromNanoseconds(target.Min(m => m.Nanoseconds));
var minActualIterationTime = TimeValue.FromNanoseconds(target.Min(m => m.Nanoseconds));
if (minActualIterationTime < MinSufficientIterationTime)
yield return CreateWarning($"The minimum observed iteration time is {minActualIterationTime} which is very small. It's recommended to increase it.", report);
}
Expand Down
11 changes: 6 additions & 5 deletions src/BenchmarkDotNet/Analysers/MultimodalDistributionAnalyzer.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Globalization;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Mathematics;
Expand All @@ -23,15 +24,15 @@ protected override IEnumerable<Conclusion> AnalyseReport(BenchmarkReport report,

double mValue = MathHelper.CalculateMValue(statistics);
if (mValue > 4.2)
yield return Create("is multimodal", mValue, report);
yield return Create("is multimodal", mValue, report, summary.Style.CultureInfo);
else if (mValue > 3.2)
yield return Create("is bimodal", mValue, report);
yield return Create("is bimodal", mValue, report, summary.Style.CultureInfo);
else if (mValue > 2.8)
yield return Create("can have several modes", mValue, report);
yield return Create("can have several modes", mValue, report, summary.Style.CultureInfo);
}

[NotNull]
private Conclusion Create([NotNull] string kind, double mValue, [CanBeNull] BenchmarkReport report)
=> CreateWarning($"It seems that the distribution {kind} (mValue = {mValue.ToStr()})", report);
private Conclusion Create([NotNull] string kind, double mValue, [CanBeNull] BenchmarkReport report, CultureInfo cultureInfo)
=> CreateWarning($"It seems that the distribution {kind} (mValue = {mValue.ToString("0.##", cultureInfo)})", report);
}
}
19 changes: 12 additions & 7 deletions src/BenchmarkDotNet/Analysers/OutliersAnalyser.cs
@@ -1,7 +1,9 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Horology;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Reports;
using JetBrains.Annotations;
Expand Down Expand Up @@ -39,8 +41,9 @@ protected override IEnumerable<Conclusion> AnalyseReport(BenchmarkReport report,
yield break;
}

var cultureInfo = summary.GetCultureInfo();
if (allOutliers.Any())
yield return CreateHint(GetMessage(actualOutliers, allOutliers, statistics.LowerOutliers, statistics.UpperOutliers), report);
yield return CreateHint(GetMessage(actualOutliers, allOutliers, statistics.LowerOutliers, statistics.UpperOutliers, cultureInfo), report);
}

/// <summary>
Expand All @@ -52,7 +55,7 @@ protected override IEnumerable<Conclusion> AnalyseReport(BenchmarkReport report,
/// <param name="upperOutliers">All upper outliers</param>
/// <returns>The message</returns>
[PublicAPI, NotNull, Pure]
public static string GetMessage(double[] actualOutliers, double[] allOutliers, double[] lowerOutliers, double[] upperOutliers)
public static string GetMessage(double[] actualOutliers, double[] allOutliers, double[] lowerOutliers, double[] upperOutliers, CultureInfo cultureInfo)
{
if (allOutliers.Length == 0)
return string.Empty;
Expand All @@ -63,7 +66,7 @@ string Format(int n, string verb)
return $"{n} {words} {verb}";
}

var rangeMessages = new List<string> { GetRangeMessage(lowerOutliers), GetRangeMessage(upperOutliers) };
var rangeMessages = new List<string> { GetRangeMessage(lowerOutliers, cultureInfo), GetRangeMessage(upperOutliers, cultureInfo) };
rangeMessages.RemoveAll(string.IsNullOrEmpty);
string rangeMessage = rangeMessages.Any()
? " (" + string.Join(", ", rangeMessages) + ")"
Expand All @@ -77,17 +80,19 @@ string Format(int n, string verb)
}

[CanBeNull]
private static string GetRangeMessage([NotNull] double[] values)
private static string GetRangeMessage([NotNull] double[] values, CultureInfo cultureInfo)
{
string Format(double value) => TimeValue.FromNanoseconds(value).ToString(cultureInfo, "N2");

switch (values.Length) {
case 0:
return null;
case 1:
return values.First().ToTimeStr(format: "N2");
return Format(values.First());
case 2:
return values.Min().ToTimeStr(format: "N2") + ", " + values.Max().ToTimeStr(format: "N2");
return Format(values.Min()) + ", " + Format(values.Max());
default:
return values.Min().ToTimeStr(format: "N2") + ".." + values.Max().ToTimeStr(format: "N2");
return Format(values.Min()) + ".." + Format(values.Max());
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/BenchmarkDotNet/Analysers/ZeroMeasurementAnalyser.cs
Expand Up @@ -13,7 +13,7 @@ public class ZeroMeasurementAnalyser : AnalyserBase

public static readonly IAnalyser Default = new ZeroMeasurementAnalyser();

private static readonly TimeInterval FallbackCpuResolutionValue = TimeInterval.FromNanoseconds(0.2d);
private static readonly TimeValue FallbackCpuResolutionValue = TimeValue.FromNanoseconds(0.2d);

private ZeroMeasurementAnalyser() { }

Expand Down
15 changes: 11 additions & 4 deletions src/BenchmarkDotNet/Attributes/EncodingAttribute.cs
@@ -1,26 +1,33 @@
using System;
using System.Text;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Loggers;
using JetBrains.Annotations;

#pragma warning disable CS3015 // no public ctor with CLS-compliant arguments
namespace BenchmarkDotNet.Attributes
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = true)]
public class EncodingAttribute: Attribute, IConfigSource
[Obsolete]
public class EncodingAttribute : Attribute, IConfigSource
{
public IConfig Config { get; }

private EncodingAttribute(Encoding encoding) => Config = ManualConfig.CreateEmpty().With(encoding);
private EncodingAttribute(Encoding encoding)
{
Config = Equals(encoding, Encoding.Unicode)
? ManualConfig.CreateEmpty().With(ConsoleLogger.Unicode)
: ManualConfig.CreateEmpty();
}

[PublicAPI]
public class Unicode: EncodingAttribute
public class Unicode : EncodingAttribute
{
public Unicode() : base(Encoding.Unicode) { }
}

[PublicAPI]
public class ASCII: EncodingAttribute
public class ASCII : EncodingAttribute
{
public ASCII() : base(Encoding.ASCII) { }
}
Expand Down
Expand Up @@ -11,7 +11,7 @@ namespace BenchmarkDotNet.Attributes
[PublicAPI]
public class IterationTimeAttribute : JobMutatorConfigBaseAttribute
{
public IterationTimeAttribute(double milliseconds) : base(Job.Default.WithIterationTime(TimeInterval.FromMilliseconds(milliseconds)))
public IterationTimeAttribute(double milliseconds) : base(Job.Default.WithIterationTime(TimeValue.FromMilliseconds(milliseconds)))
{
}
}
Expand Down

0 comments on commit f7053ae

Please sign in to comment.