Skip to content

Commit

Permalink
Merge 14d45f0 into f26c6e4
Browse files Browse the repository at this point in the history
  • Loading branch information
axunonb committed Jun 20, 2021
2 parents f26c6e4 + 14d45f0 commit bf6b6b3
Show file tree
Hide file tree
Showing 9 changed files with 501 additions and 26 deletions.
54 changes: 53 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,63 @@
Latest Changes
====

v3.0.0-alpha-14
v3.0.0-alpha-15
===

### Current changes merged into the `version/v3.0` branch:

#### Added `StringSource` as another `ISource`

`StringSource` adds the following selector names, which have before been implemented with `ReflectionSource`:
* Length
* ToUpper
* ToUpperInvariant
* ToLower
* ToLowerInvariant
* Trim
* TrimStart
* TrimEnd
* ToCharArray

Additionally, the following selector names are implemented:
* Capitalize
* CapitalizeWords
* FromBase64
* ToBase64

All these selector names may linked. Example with indexed placeholders:
```CSharp
Smart.Format("{0.ToLower.TrimStart.TrimEnd.ToBase64}", " ABCDE ");
// result: "YWJjZGU="
```
This also works for named placeholders.

**Note**: `ReflectionSource` does not evaluate `string`s any more.

The new formatter privates additional funcionality and with with reflection caching, performance is 13% better.

```
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
AMD Ryzen 9 3900X, 1 CPU, 24 logical and 12 physical cores
.NET Core SDK=5.0.202
[Host] : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT
.NET Core 5.0 : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT
Job=.NET Core 5.0 Runtime=.NET Core 5.0
| Method | N | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|------------------- |------ |------------:|----------:|----------:|----------:|------:|------:|------------:|
| DirectMemberAccess | 1000 | 261.5 us | 5.13 us | 8.71 us | 20.9961 | - | - | 171.88 KB |
| SfStringSource | 1000 | 2,727.5 us | 10.42 us | 9.75 us | 207.03 | - | - | 1695.31 KB |
| SfCacheReflection | 1000 | 3,712.0 us | 67.06 us | 62.73 us | 214.8438 | - | - | 1757.81 KB |
|SfNoCacheReflection | 1000 | 13,091.9 us | 129.38 us | 121.02 us | 781.2500 | - | - | 6468.75 KB |
| | | | | | | | | |
| DirectMemberAccess | 10000 | 2,519.2 us | 49.85 us | 53.34 us | 207.0313 | - | - | 1718.75 KB |
| SfStringSource | 10000 | 27,612.5 us | 68.50 us | 64.08 us | 2062.5000 | - | - | 16953.13 KB |
| SfCacheReflection | 10000 | 36,312.6 us | 438.96 us | 389.12us | 2142.8571 | - | - | 17578.13 KB |
|SfNoCacheReflection | 10000 |130,049.2 us |1,231.06us |1,027.99us | 7750.0000 | - | - | 64687.81 KB |
```

#### JSON Source ([#177](https://github.com/axuno/SmartFormat/pull/177))

Separation of `JsonSource` into 2 `ISource` extensions:
Expand Down
177 changes: 177 additions & 0 deletions src/SmartFormat.Performance/ReflectionVsStringSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using Newtonsoft.Json.Linq;
using SmartFormat.Core.Formatting;
using SmartFormat.Extensions;

namespace SmartFormat.Performance
{
/*
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
AMD Ryzen 9 3900X, 1 CPU, 24 logical and 12 physical cores
.NET Core SDK=5.0.202
[Host] : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT
.NET Core 5.0 : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT
Job=.NET Core 5.0 Runtime=.NET Core 5.0
| Method | N | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|------------------- |------ |------------:|----------:|----------:|----------:|------:|------:|------------:|
| DirectMemberAccess | 1000 | 261.5 us | 5.13 us | 8.71 us | 20.9961 | - | - | 171.88 KB |
| SfStringSource | 1000 | 2,727.5 us | 10.42 us | 9.75 us | 207.03 | - | - | 1695.31 KB |
| SfCacheReflection | 1000 | 3,712.0 us | 67.06 us | 62.73 us | 214.8438 | - | - | 1757.81 KB |
|SfNoCacheReflection | 1000 | 13,091.9 us | 129.38 us | 121.02 us | 781.2500 | - | - | 6468.75 KB |
| | | | | | | | | |
| DirectMemberAccess | 10000 | 2,519.2 us | 49.85 us | 53.34 us | 207.0313 | - | - | 1718.75 KB |
| SfStringSource | 10000 | 27,612.5 us | 68.50 us | 64.08 us | 2062.5000 | - | - | 16953.13 KB |
| SfCacheReflection | 10000 | 36,312.6 us | 438.96 us | 389.12us | 2142.8571 | - | - | 17578.13 KB |
|SfNoCacheReflection | 10000 |130,049.2 us |1,231.06us |1,027.99us | 7750.0000 | - | - | 64687.81 KB |
* Legends *
N : Value of the 'N' parameter
Mean : Arithmetic mean of all measurements
Error : Half of 99.9% confidence interval
StdDev : Standard deviation of all measurements
Ratio : Mean of the ratio distribution ([Current]/[Baseline])
RatioSD : Standard deviation of the ratio distribution ([Current]/[Baseline])
Gen 0 : GC Generation 0 collects per 1000 operations
Gen 1 : GC Generation 1 collects per 1000 operations
Gen 2 : GC Generation 2 collects per 1000 operations
Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
1 us : 1 Microsecond (0.000001 sec)
*/

[SimpleJob(RuntimeMoniker.NetCoreApp50)]
[MemoryDiagnoser]
// [RPlotExporter]
public class ReflectionVsStringSource
{
private const string _formatString = "Address: {0.ToUpper} {1.ToLower}, {2.Trim}";

private readonly SmartFormatter _reflectionSourceFormatter;
private readonly SmartFormatter _stringSourceFormatter;

private readonly Address _address = new Address();

private FormatCache _formatCacheLiteral;

public ReflectionVsStringSource()
{
_reflectionSourceFormatter = new SmartFormatter();
_reflectionSourceFormatter.AddExtensions(
new ReflectionSource(_reflectionSourceFormatter),
new DefaultSource(_reflectionSourceFormatter)
);
_reflectionSourceFormatter.AddExtensions(
new DefaultFormatter()
);

_stringSourceFormatter = new SmartFormatter();
_stringSourceFormatter.AddExtensions(
new StringSource(_stringSourceFormatter),
new DefaultSource(_stringSourceFormatter)
);
_stringSourceFormatter.AddExtensions(
new DefaultFormatter()
);

var parsedFormat = _stringSourceFormatter.Parser.ParseFormat(_formatString);
_formatCache = new FormatCache(parsedFormat);

}

[Params(1000, 10000)]
public int N;

private readonly FormatCache _formatCache;

[GlobalSetup]
public void Setup()
{
}

[Benchmark]
public void DirectMemberAccess()
{
for (var i = 0; i < N; i++)
{
_ = string.Format("Address: {0} {1}, {2}", _address.City.ZipCode.ToUpper(),
_address.City.Name.ToLower(), _address.City.AreaCode.Trim());
}
}

[Benchmark]
public void SfCacheReflectionSource()
{
for (var i = 0; i < N; i++)
{
_ = _reflectionSourceFormatter.FormatWithCache(ref _formatCacheLiteral,"Address: {0} {1}, {2}", _address.City.ZipCode,
_address.City.Name, _address.City.AreaCode);
}
}

[Benchmark]
public void SfWithStringSource()
{
for (var i = 0; i < N; i++)
{
_ = _stringSourceFormatter.FormatWithCache(ref _formatCacheLiteral,"Address: {0} {1}, {2}", _address.City.ZipCode,
_address.City.Name, _address.City.AreaCode);
}
}

public class Address
{
public CityDetails City { get; set; } = new CityDetails();
public PersonDetails Person { get; set; } = new PersonDetails();

public Dictionary<string, object> ToDictionary()
{
var d = new Dictionary<string, object>
{
{ nameof(City), City.ToDictionary() },
{ nameof(Person), Person.ToDictionary() }
};
return d;
}

public JObject ToJson()
{
return JObject.Parse(Newtonsoft.Json.JsonConvert.SerializeObject(this));
}

public class CityDetails
{
public string Name { get; set; } = "New York";
public string ZipCode { get; set; } = "00501";
public string AreaCode { get; set; } = "631";

public Dictionary<string, string> ToDictionary()
{
return new()
{
{nameof(Name), Name},
{nameof(ZipCode), ZipCode},
{nameof(AreaCode), AreaCode}
};
}
}

public class PersonDetails
{
public string FirstName { get; set; } = "John";
public string LastName { get; set; } = "Doe";
public Dictionary<string, string> ToDictionary()
{
return new()
{
{nameof(FirstName), FirstName},
{nameof(LastName), LastName}
};
}
}
}
}
}
2 changes: 1 addition & 1 deletion src/SmartFormat.Tests/Extensions/ListFormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ public void Objects_Not_Implementing_IList_Are_Not_Processed()
}

[TestCase("{0:{} = {Index}|, }", "A = 0, B = 1, C = 2, D = 3, E = 4")] // Index holds the current index of the iteration
[TestCase("{1:{Index}: {ToCharArray:{} = {Index}|, }|; }", "0: O = 0, n = 1, e = 2; 1: T = 0, w = 1, o = 2; 2: T = 0, h = 1, r = 2, e = 3, e = 4; 3: F = 0, o = 1, u = 2, r = 3; 4: F = 0, i = 1, v = 2, e = 3")] // Index can be nested
[TestCase("{1:{Index}: {ToCharArray:{} = {Index}|, }|; }", "0: O = 0, n = 1, e = 2; 1: T = 0, w = 1, o = 2; 2: T = 0, h = 1, r = 2, e = 3, e = 4; 3: F = 0, o = 1, u = 2, r = 3; 4: F = 0, i = 1, v = 2, e = 3")] // Index can be nested, ToCharArray() requires StringSource or ReflectionSource
[TestCase("{0:{} = {1.Index}|, }", "A = One, B = Two, C = Three, D = Four, E = Five")] // Index is used to synchronize 2 lists
[TestCase("{Index}", "-1")] // Index can be used out-of-context, but should always be -1
public void TestIndex(string format, string expected)
Expand Down
84 changes: 64 additions & 20 deletions src/SmartFormat.Tests/Extensions/ReflectionFormatterTests.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using SmartFormat.Core.Extensions;
using SmartFormat.Core.Formatting;
using SmartFormat.Core.Output;
using SmartFormat.Core.Parsing;
using SmartFormat.Core.Settings;
using SmartFormat.Extensions;
using SmartFormat.Tests.TestUtils;
Expand All @@ -26,14 +30,15 @@ public object[] GetArgs()
[Test]
public void Test_Properties()
{
var formats = new string[] {
// Length property for a string comes from StringSource
var formats = new[] {
"{0} {0.Length} {Length}",
"{2.Year} {2.Month:00}-{2.Day:00}",
"{3.Value} {3.Anon}",
"Chained: {4.FirstName} {4.FirstName.Length} {4.Address.City} {4.Address.State}",
"Nested: {4:{FirstName:{} {Length} }{Address:{City} {State}}}"
};
var expected = new string[] {
var expected = new[] {
"Zero 4 4",
"2222 02-02",
"3 True",
Expand All @@ -50,47 +55,85 @@ public void Test_Properties_CaseInsensitive()
var formatter = Smart.CreateDefaultSmartFormat();
formatter.Settings.CaseSensitivity = CaseSensitivityType.CaseInsensitive;

var formats = new string[]
// Length property for a string comes from StringSource
var formats = new []
{
"{0} {0.lenGth} {length}", "{2.YEar} {2.MoNth:00}-{2.daY:00}", "{3.Value} {3.AnoN}",
"Chained: {4.fIrstName} {4.Firstname.Length} {4.Address.City} {4.aDdress.StAte} ",
"Nested: {4:{FirstName:{} {Length} }{Address:{City} {StaTe} } }",
"Chained: {4.fIrstName} {4.Firstname.Length} {4.Address.City} {4.aDdress.StAte}",
"Nested: {4:{FirstName:{} {Length} }{Address:{City} {StaTe}}}",
// Due to double-brace escaping, the spacing in this nested format is irregular
};
var expected = new string[]
var expected = new []
{
"Zero 4 4", "2222 02-02", "3 True", "Chained: Michael 7 Scranton Pennsylvania ",
"Nested: Michael 7 Scranton Pennsylvania ",
"Zero 4 4", "2222 02-02", "3 True", "Chained: Michael 7 Scranton Pennsylvania",
"Nested: Michael 7 Scranton Pennsylvania",
};
var args = GetArgs();
formatter.Test(formats, args, expected);
}

/// <summary>
/// system.string methods are processed by <see cref="StringSource"/> since v3.0
/// </summary>
[Test]
public void Test_Methods()
{
var formats = new string[] {
"{0} {0.ToLower} {ToLower} {ToUpper}",
};
var expected = new string[] {
"Zero zero zero ZERO",
};
var format = "{0} {0.ToLower} {ToLower} {ToUpper}";
//var expected = "Zero zero zero ZERO";

var smart = new SmartFormatter();
smart.SourceExtensions.AddRange(new ISource[]{new ReflectionSource(smart), new DefaultSource(smart)});
smart.FormatterExtensions.Add(new DefaultFormatter());

var args = GetArgs();
Smart.Default.Test(formats, args, expected);
Assert.That(() => smart.Format(format, args), Throws.Exception.TypeOf(typeof(FormattingException)).And.Message.Contains("ToLower"));
}

/// <summary>
/// system.string methods are processed by <see cref="StringSource"/> since v3.0
/// </summary>
[Test]
public void Test_Methods_CaseInsensitive()
{
var formatter = Smart.CreateDefaultSmartFormat();
formatter.Settings.CaseSensitivity = CaseSensitivityType.CaseInsensitive;
var smart = new SmartFormatter();
smart.SourceExtensions.AddRange(new ISource[]{new ReflectionSource(smart)});
smart.FormatterExtensions.Add(new DefaultFormatter());
smart.Settings.CaseSensitivity = CaseSensitivityType.CaseInsensitive;

var formats = new string[] { "{0} {0.ToLower} {toloWer} {touPPer}", };
var expected = new string[] { "Zero zero zero ZERO", };
var format = "{0} {0.ToLower} {toloWer} {touPPer}";
//var expected = "Zero zero zero ZERO";
var args = GetArgs();
formatter.Test(formats, args, expected);
Assert.That(() => smart.Format(format, args), Throws.Exception.TypeOf(typeof(FormattingException)).And.Message.Contains("ToLower"));
}

[Test]
public void Void_Methods_Should_Just_Be_Ignored()
{
var smart = new SmartFormatter();
smart.SourceExtensions.AddRange(new ISource[]{new ReflectionSource(smart), new DefaultSource(smart)});
smart.FormatterExtensions.Add(new DefaultFormatter());
Assert.That(() => smart.Format("{0.Clear}", smart.SourceExtensions), Throws.Exception.TypeOf(typeof(FormattingException)).And.Message.Contains("Clear"));
}

[Test]
public void Methods_With_Parameter_Should_Just_Be_Ignored()
{
var smart = new SmartFormatter();
smart.SourceExtensions.AddRange(new ISource[]{new ReflectionSource(smart), new DefaultSource(smart)});
smart.FormatterExtensions.Add(new DefaultFormatter());
Assert.That(() => smart.Format("{0.Add}", smart.SourceExtensions), Throws.Exception.TypeOf(typeof(FormattingException)).And.Message.Contains("Add"));
}

[Test]
public void Properties_With_No_Getter_Should_Just_Be_Ignored()
{
var smart = new SmartFormatter();
smart.SourceExtensions.AddRange(new ISource[]{new ReflectionSource(smart), new DefaultSource(smart)});
smart.FormatterExtensions.Add(new DefaultFormatter());
Assert.That(() => smart.Format("{Misc.OnlySetterProperty}", new { Misc = new MiscObject() }), Throws.Exception.TypeOf(typeof(FormattingException)).And.Message.Contains("OnlySetterProperty"));
}


[Test]
public void Test_Fields()
{
Expand Down Expand Up @@ -198,6 +241,7 @@ public MiscObject()
MethodReturnValue = "Method";
}
public string Field;
public string OnlySetterProperty { set { } }
public string ReadonlyProperty { get; private set; }
public virtual string Property { get; set; } = "Property";
public string Method()
Expand Down
Loading

0 comments on commit bf6b6b3

Please sign in to comment.