Unverified Commit 0b168789 authored by Nick Craver's avatar Nick Craver Committed by GitHub

Benchmarks: updating libs and overall runner (#1123)

This is prepping a run we can drop into the README easily. And more info on allocations and such along with it. It's finishing off the benchmark port getting it into a good output state (something we can copy/paste). It removes Simple.Data because the package was debug and causes a lot of issues.

This doesn't output 100% like we want yet (fastest to slowest). BenchmarkDotNet joins summary results after ordering...we need to post-order that. I'm asking the maintainers how to do so.

Also note Belgrade was not parameterizing before, giving it a huge unfair advantage. This has been fixed to be apples:apples.
parent 6e277406
......@@ -18,4 +18,5 @@ Test.DB.*
TestResults/
Dapper.Tests/*.sdf
Dapper.Tests/SqlServerTypes/
.dotnet/*
\ No newline at end of file
.dotnet/*
BenchmarkDotNet.Artifacts/
\ No newline at end of file
using BenchmarkDotNet.Attributes;
using Belgrade.SqlClient.SqlDb;
using System.Threading.Tasks;
using Belgrade.SqlClient;
namespace Dapper.Tests.Performance
{
......@@ -20,7 +21,7 @@ public Post ExecuteReader()
{
Step();
var post = new Post();
_mapper.ExecuteReader("SELECT TOP 1 * FROM Posts WHERE Id = " + i,
_mapper.Sql("SELECT TOP 1 * FROM Posts WHERE Id = @Id").Param("Id", i).Map(
reader =>
{
post.Id = reader.GetInt32(0);
......
......@@ -47,7 +47,7 @@ public void Setup()
#endif
}
[Benchmark(Description = "SqlCommand", Baseline = true)]
[Benchmark(Description = "SqlCommand")]
public Post SqlCommand()
{
Step();
......@@ -56,21 +56,23 @@ public Post SqlCommand()
using (var reader = _postCommand.ExecuteReader())
{
reader.Read();
var post = new Post();
post.Id = reader.GetInt32(0);
post.Text = reader.GetNullableString(1);
post.CreationDate = reader.GetDateTime(2);
post.LastChangeDate = reader.GetDateTime(3);
var post = new Post
{
Id = reader.GetInt32(0),
Text = reader.GetNullableString(1),
CreationDate = reader.GetDateTime(2),
LastChangeDate = reader.GetDateTime(3),
post.Counter1 = reader.GetNullableValue<int>(4);
post.Counter2 = reader.GetNullableValue<int>(5);
post.Counter3 = reader.GetNullableValue<int>(6);
post.Counter4 = reader.GetNullableValue<int>(7);
post.Counter5 = reader.GetNullableValue<int>(8);
post.Counter6 = reader.GetNullableValue<int>(9);
post.Counter7 = reader.GetNullableValue<int>(10);
post.Counter8 = reader.GetNullableValue<int>(11);
post.Counter9 = reader.GetNullableValue<int>(12);
Counter1 = reader.IsDBNull(4) ? null : (int?)reader.GetInt32(4),
Counter2 = reader.IsDBNull(5) ? null : (int?)reader.GetInt32(5),
Counter3 = reader.IsDBNull(6) ? null : (int?)reader.GetInt32(6),
Counter4 = reader.IsDBNull(7) ? null : (int?)reader.GetInt32(7),
Counter5 = reader.IsDBNull(8) ? null : (int?)reader.GetInt32(8),
Counter6 = reader.IsDBNull(9) ? null : (int?)reader.GetInt32(9),
Counter7 = reader.IsDBNull(10) ? null : (int?)reader.GetInt32(10),
Counter8 = reader.IsDBNull(11) ? null : (int?)reader.GetInt32(11),
Counter9 = reader.IsDBNull(12) ? null : (int?)reader.GetInt32(12)
};
return post;
}
}
......
using BenchmarkDotNet.Attributes;
namespace Dapper.Tests.Performance
{
public class SomaBenchmarks : BenchmarkBase
{
private dynamic _sdb;
[GlobalSetup]
public void Setup()
{
BaseSetup();
_sdb = Simple.Data.Database.OpenConnection(ConnectionString);
}
[Benchmark(Description = "FindById")]
public dynamic QueryDynamic()
{
Step();
return _sdb.Posts.FindById(i).FirstOrDefault();
}
}
}
\ No newline at end of file
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Attributes.Columns;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Horology;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Order;
using Dapper.Tests.Performance.Helpers;
using System;
using System.Configuration;
using System.Data.SqlClient;
namespace Dapper.Tests.Performance
{
[OrderProvider(SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
[Config(typeof(Config))]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[BenchmarkCategory("ORM")]
public abstract class BenchmarkBase
{
public const int Iterations = 50;
protected static readonly Random _rand = new Random();
protected SqlConnection _connection;
public static string ConnectionString { get; } = ConfigurationManager.ConnectionStrings["Main"].ConnectionString;
......@@ -37,22 +28,4 @@ protected void Step()
if (i > 5000) i = 1;
}
}
public class Config : ManualConfig
{
public Config()
{
Add(new MemoryDiagnoser());
Add(new ORMColum());
Add(new ReturnColum());
Add(Job.Default
.WithUnrollFactor(BenchmarkBase.Iterations)
//.WithIterationTime(new TimeInterval(500, TimeUnit.Millisecond))
.WithLaunchCount(1)
.WithWarmupCount(0)
.WithTargetCount(5)
.WithRemoveOutliers(true)
);
}
}
}
\ No newline at end of file
}
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Exporters.Csv;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Order;
using Dapper.Tests.Performance.Helpers;
namespace Dapper.Tests.Performance
{
public class Config : ManualConfig
{
public const int Iterations = 5000;
public Config()
{
Add(ConsoleLogger.Default);
Add(CsvExporter.Default);
Add(MarkdownExporter.GitHub);
Add(HtmlExporter.Default);
var md = new MemoryDiagnoser();
Add(md);
Add(new ORMColum());
Add(TargetMethodColumn.Method);
Add(new ReturnColum());
Add(StatisticColumn.Mean);
Add(StatisticColumn.StdDev);
Add(StatisticColumn.Error);
Add(BaselineScaledColumn.Scaled);
Add(md.GetColumnProvider());
Add(Job.Dry
.WithLaunchCount(1)
.WithWarmupCount(1)
.WithInvocationCount(Iterations)
.WithIterationCount(10)
);
Set(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest));
SummaryPerType = false;
}
}
}
......@@ -11,27 +11,24 @@
<ProjectReference Include="..\Dapper\Dapper.csproj" />
<ProjectReference Include="..\Dapper.Contrib\Dapper.Contrib.csproj" />
<ProjectReference Include="..\Dapper.EntityFramework\Dapper.EntityFramework.csproj" />
<PackageReference Include="Belgrade.Sql.Client" Version="0.7.0" />
<PackageReference Include="BenchmarkDotNet" Version="0.10.9" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.10.9" />
<PackageReference Include="Belgrade.Sql.Client" Version="1.1.4" />
<PackageReference Include="BenchmarkDotNet" Version="0.11.1" />
<!--<PackageReference Include="BLToolkit" Version="4.3.6" />-->
<PackageReference Include="EntityFramework" Version="6.1.3" />
<PackageReference Include="FirebirdSql.Data.FirebirdClient" Version="5.9.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="1.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="1.1.2" />
<PackageReference Include="EntityFramework" Version="6.2.0" />
<PackageReference Include="FirebirdSql.Data.FirebirdClient" Version="6.3.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.1.2" />
<PackageReference Include="Microsoft.SqlServer.Types" Version="14.0.314.76" />
<PackageReference Include="MySqlConnector" Version="0.44.1" />
<PackageReference Include="NHibernate" Version="4.1.1.4000" />
<PackageReference Include="Iesi.Collections" Version="4.0.2" />
<PackageReference Include="Npgsql" Version="3.2.5" />
<PackageReference Include="PetaPoco" Version="5.1.259" />
<PackageReference Include="ServiceStack.OrmLite.SqlServer.Signed" Version="4.5.12" />
<PackageReference Include="Simple.Data.SqlServer" Version="2.0.0-alpha1" />
<PackageReference Include="NHibernate" Version="5.1.3" />
<PackageReference Include="Iesi.Collections" Version="4.0.4" />
<PackageReference Include="Npgsql" Version="4.0.3" />
<PackageReference Include="PetaPoco" Version="5.1.306" />
<PackageReference Include="ServiceStack.OrmLite.SqlServer.Signed" Version="4.5.14" />
<PackageReference Include="Soma" Version="1.9.0.1" />
<PackageReference Include="SubSonic" Version="3.0.0.4" />
<PackageReference Include="Susanoo.SqlServer" Version="1.2.4.2" />
<PackageReference Include="System.Data.SQLite" Version="1.0.105.2" />
<PackageReference Include="System.Data.SqlClient" Version="4.4.0" />
<PackageReference Include="System.Data.SQLite" Version="1.0.109.1" />
<PackageReference Include="System.Data.SqlClient" Version="4.5.1" />
<Reference Include="System.Configuration" />
<Reference Include="System.Data" />
<Reference Include="System.Data.Linq" />
......
......@@ -10,9 +10,9 @@ public class ORMColum : IColumn
public string ColumnName { get; } = "ORM";
public string Legend => "The object/relational mapper being tested";
public bool IsDefault(Summary summary, Benchmark benchmark) => false;
public string GetValue(Summary summary, Benchmark benchmark) => benchmark.Target.Method.DeclaringType.Name.Replace("Benchmarks", string.Empty);
public string GetValue(Summary summary, Benchmark benchmark, ISummaryStyle style) => benchmark.Target.Method.DeclaringType.Name.Replace("Benchmarks", string.Empty);
public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false;
public string GetValue(Summary summary, BenchmarkCase benchmarkCase) => benchmarkCase.Descriptor.WorkloadMethod.DeclaringType.Name.Replace("Benchmarks", string.Empty);
public string GetValue(Summary summary, BenchmarkCase benchmarkCase, ISummaryStyle style) => GetValue(summary, benchmarkCase);
public bool IsAvailable(Summary summary) => true;
public bool AlwaysShow => true;
......
......@@ -10,9 +10,14 @@ public class ReturnColum : IColumn
public string ColumnName { get; } = "Return";
public string Legend => "The return type of the method";
public bool IsDefault(Summary summary, Benchmark benchmark) => false;
public string GetValue(Summary summary, Benchmark benchmark) => benchmark.Target.Method.ReturnType.Name;
public string GetValue(Summary summary, Benchmark benchmark, ISummaryStyle style) => benchmark.Target.Method.ReturnType.Name;
public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false;
public string GetValue(Summary summary, BenchmarkCase benchmarkCase)
{
var type = benchmarkCase.Descriptor.WorkloadMethod.ReturnType;
return type == typeof(object) ? "dynamic" : type.Name;
}
public string GetValue(Summary summary, BenchmarkCase benchmarkCase, ISummaryStyle style) => GetValue(summary, benchmarkCase);
public bool IsAvailable(Summary summary) => true;
public bool AlwaysShow => true;
......
......@@ -21,6 +21,7 @@
using System.Threading.Tasks;
using Dapper.Tests.Performance.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Belgrade.SqlClient;
namespace Dapper.Tests.Performance
{
......@@ -81,12 +82,16 @@ public async Task RunAsync(int iterations)
}
}
Console.WriteLine("|Time|Framework|");
foreach (var test in this.OrderBy(t => t.Watch.ElapsedMilliseconds))
{
var ms = test.Watch.ElapsedMilliseconds.ToString();
Console.Write("|");
Console.Write(ms);
Program.WriteColor("ms ".PadRight(8 - ms.Length), ConsoleColor.DarkGray);
Console.WriteLine(test.Name);
Console.Write("|");
Console.Write(test.Name);
Console.WriteLine("|");
}
}
}
......@@ -233,18 +238,11 @@ public async Task RunAsync(int iterations)
tests.Add(id => nhSession5.Get<Post>(id), "NHibernate: Session.Get");
}, "NHibernate");
// Simple.Data
Try(() =>
{
var sdb = Simple.Data.Database.OpenConnection(ConnectionString);
tests.Add(id => sdb.Posts.FindById(id).FirstOrDefault(), "Simple.Data");
}, "Simple.Data");
// Belgrade
Try(() =>
{
var query = new Belgrade.SqlClient.SqlDb.QueryMapper(ConnectionString);
tests.AsyncAdd(id => query.ExecuteReader("SELECT TOP 1 * FROM Posts WHERE Id = " + id,
tests.AsyncAdd(id => query.Sql("SELECT TOP 1 * FROM Posts WHERE Id = @Id").Param("Id", id).Map(
reader =>
{
var post = new Post();
......
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Running;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Reflection;
using static System.Console;
namespace Dapper.Tests.Performance
......@@ -25,7 +25,7 @@ public static void Main(string[] args)
if (args.Length == 0)
{
WriteLine("Optional arguments:");
WriteColor(" --all", ConsoleColor.Blue);
WriteColor(" (no args)", ConsoleColor.Blue);
WriteLine(": run all benchmarks");
WriteColor(" --legacy", ConsoleColor.Blue);
WriteLine(": run the legacy benchmark suite/format", ConsoleColor.Gray);
......@@ -35,19 +35,7 @@ public static void Main(string[] args)
EnsureDBSetup();
WriteLine("Database setup complete.");
if (args.Any(a => a == "--all"))
{
WriteLine("Iterations: " + BenchmarkBase.Iterations);
var benchmarks = new List<Benchmark>();
var benchTypes = Assembly.GetEntryAssembly().DefinedTypes.Where(t => t.IsSubclassOf(typeof(BenchmarkBase)));
WriteLineColor("Running full benchmarks suite", ConsoleColor.Green);
foreach (var b in benchTypes)
{
benchmarks.AddRange(BenchmarkConverter.TypeToBenchmarks(b));
}
BenchmarkRunner.Run(benchmarks.ToArray(), null);
}
else if (args.Any(a => a == "--legacy"))
if (args.Any(a => a == "--legacy"))
{
var test = new LegacyTests();
const int iterations = 500;
......@@ -58,8 +46,8 @@ public static void Main(string[] args)
}
else
{
WriteLine("Iterations: " + BenchmarkBase.Iterations);
BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args);
WriteLine("Iterations: " + Config.Iterations);
new BenchmarkSwitcher(typeof(BenchmarkBase).Assembly).Run(args, new Config());
}
}
......
......@@ -112,49 +112,55 @@ This works for any parameter that implements IEnumerable<T> for some T.
Performance
-----------
A key feature of Dapper is performance. The following metrics show how long it takes to execute 500 `SELECT` statements against a DB and map the data returned to objects.
The performance tests are broken in to 3 lists:
- POCO serialization for frameworks that support pulling static typed objects from the DB. Using raw SQL.
- Dynamic serialization for frameworks that support returning dynamic lists of objects.
- Typical framework usage. Often typical framework usage differs from the optimal usage performance wise. Often it will not involve writing SQL.
### Performance of SELECT mapping over 500 iterations - POCO serialization
| Method | Duration | Remarks |
| --------------------------------------------------- | -------- | ------- |
| Hand coded (using a `SqlDataReader`) | 47ms |
| Dapper `ExecuteMapperQuery` | 49ms |
| [ServiceStack.OrmLite](https://github.com/ServiceStack/ServiceStack.OrmLite) (QueryById) | 50ms |
| [PetaPoco](https://github.com/CollaboratingPlatypus/PetaPoco) | 52ms | [Can be faster](https://web.archive.org/web/20170921124755/http://www.toptensoftware.com/blog/posts/94-PetaPoco-More-Speed) |
| BLToolkit | 80ms |
| SubSonic CodingHorror | 107ms |
| NHibernate SQL | 104ms |
| Linq 2 SQL `ExecuteQuery` | 181ms |
| Entity framework `ExecuteStoreQuery` | 631ms |
### Performance of SELECT mapping over 500 iterations - dynamic serialization
| Method | Duration | Remarks |
| -------------------------------------------------------- | -------- | ------- |
| Dapper `ExecuteMapperQuery` (dynamic) | 48ms |
| [Massive](https://github.com/FransBouma/Massive) | 52ms |
| [Simple.Data](https://github.com/markrendle/Simple.Data) | 95ms |
### Performance of SELECT mapping over 500 iterations - typical usage
| Method | Duration | Remarks |
| ------------------------------------- | -------- | ------- |
| Linq 2 SQL CompiledQuery | 81ms | Not super typical involves complex code |
| NHibernate HQL | 118ms |
| Linq 2 SQL | 559ms |
| Entity framework | 859ms |
| SubSonic ActiveRecord.SingleOrDefault | 3619ms |
A key feature of Dapper is performance. The following metrics show how long it takes to execute a `SELECT` statement against a DB (in various config, each labeled) and map the data returned to objects.
The benchmarks can be found in [Dapper.Tests.Performance](https://github.com/StackExchange/Dapper/tree/master/Dapper.Tests.Performance) (contributions welcome!) and can be run once compiled via:
```
Dapper.Tests.Performance.exe -f * --join
```
Output from the latest run is:
``` ini
BenchmarkDotNet=v0.11.1, OS=Windows 10.0.17134.254 (1803/April2018Update/Redstone4)
Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
Frequency=2742188 Hz, Resolution=364.6723 ns, Timer=TSC
[Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3163.0
Dry : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3163.0
Performance benchmarks are available [here](https://github.com/StackExchange/Dapper/tree/master/Dapper.Tests.Performance).
```
| ORM | Method | Return | Mean | StdDev | Error | Gen 0 | Gen 1 | Allocated |
|------------- |------------------------------ |-------- |------------:|-----------:|-----------:|--------:|-------:|----------:|
| Belgrade | ExecuteReader | Post | 93.20 us | 17.628 us | 26.652 us | 3.6000 | 1.0000 | 11.28 KB |
| PetaPoco | &#39;Fetch&lt;Post&gt; (Fast)&#39; | Post | 95.47 us | 2.608 us | 3.943 us | 4.4000 | - | 13.65 KB |
| Dapper | QueryFirstOrDefault&lt;dynamic&gt; | dynamic | 99.27 us | 6.661 us | 10.070 us | 4.2000 | - | 13.51 KB |
| Dapper | &#39;Query&lt;T&gt; (buffered)&#39; | Post | 99.37 us | 6.892 us | 10.420 us | 4.4000 | - | 13.79 KB |
| Massive | &#39;Query (dynamic)&#39; | dynamic | 100.11 us | 2.543 us | 3.845 us | 4.6000 | - | 14.21 KB |
| Dapper | &#39;Query&lt;dynamic&gt; (buffered)&#39; | dynamic | 100.30 us | 4.362 us | 6.595 us | 4.4000 | - | 13.88 KB |
| HandCoded | SqlCommand | Post | 102.95 us | 1.909 us | 2.886 us | 3.8000 | - | 12.24 KB |
| HandCoded | DataTable | dynamic | 105.04 us | 4.730 us | 7.151 us | 2.2000 | 0.6000 | 12.45 KB |
| Susanoo | &#39;Mapping Static (dynamic)&#39; | dynamic | 105.10 us | 8.457 us | 12.786 us | 4.8000 | - | 14.97 KB |
| Dapper | &#39;Contrib Get&lt;T&gt;&#39; | Post | 107.35 us | 9.207 us | 13.920 us | 4.6000 | - | 14.45 KB |
| Susanoo | &#39;Mapping Static&#39; | Post | 111.39 us | 7.716 us | 11.666 us | 4.8000 | - | 14.99 KB |
| Dapper | QueryFirstOrDefault&lt;T&gt; | Post | 112.32 us | 5.053 us | 7.639 us | 4.2000 | - | 13.47 KB |
| PetaPoco | Fetch&lt;Post&gt; | Post | 114.62 us | 3.273 us | 4.948 us | 4.6000 | - | 14.59 KB |
| Susanoo | &#39;Mapping Cache (dynamic)&#39; | dynamic | 124.43 us | 3.182 us | 4.811 us | 6.6000 | - | 20.41 KB |
| Dapper | &#39;Query&lt;dynamic&gt; (unbuffered)&#39; | dynamic | 124.43 us | 4.195 us | 6.342 us | 4.4000 | - | 13.87 KB |
| Linq2Sql | Compiled | Post | 125.92 us | 6.187 us | 9.354 us | 3.0000 | - | 9.82 KB |
| Susanoo | &#39;Mapping Cache&#39; | Post | 128.99 us | 10.511 us | 15.891 us | 6.8000 | - | 20.9 KB |
| ServiceStack | SingleById | Post | 130.70 us | 5.525 us | 8.354 us | 5.6000 | - | 17.53 KB |
| Dapper | &#39;Query&lt;T&gt; (unbuffered)&#39; | Post | 146.41 us | 12.281 us | 18.568 us | 4.4000 | - | 13.84 KB |
| EF6 | SqlQuery | Post | 197.36 us | 139.733 us | 211.257 us | 9.0000 | - | 27.86 KB |
| NHibernate | Get&lt;T&gt; | Post | 201.49 us | 7.650 us | 11.565 us | 10.4000 | - | 32.5 KB |
| NHibernate | HQL | Post | 231.44 us | 31.127 us | 47.060 us | 11.2000 | - | 35 KB |
| EFCore | Normal | Post | 244.87 us | 69.894 us | 105.670 us | 6.4000 | - | 20.25 KB |
| EFCore | &#39;No Tracking&#39; | Post | 253.52 us | 61.048 us | 92.296 us | 6.8000 | - | 21.36 KB |
| Linq2Sql | ExecuteQuery | Post | 264.58 us | 4.516 us | 6.828 us | 13.6000 | - | 42.34 KB |
| EFCore | SqlQuery | Post | 273.44 us | 71.265 us | 107.742 us | 6.6000 | - | 20.75 KB |
| NHibernate | Criteria | Post | 274.41 us | 11.087 us | 16.762 us | 21.2000 | - | 65.37 KB |
| EF6 | Normal | Post | 317.09 us | 155.828 us | 235.589 us | 15.6000 | - | 48.29 KB |
| EF6 | &#39;No Tracking&#39; | Post | 343.42 us | 150.279 us | 227.201 us | 17.8000 | - | 55.1 KB |
| NHibernate | SQL | Post | 355.03 us | 17.416 us | 26.330 us | 32.8000 | - | 101.06 KB |
| Linq2Sql | Normal | Post | 388.27 us | 260.226 us | 393.424 us | 4.6000 | 1.4000 | 14.68 KB |
| NHibernate | LINQ | Post | 1,156.97 us | 35.166 us | 53.167 us | 20.2000 | - | 62.13 KB |
Feel free to submit patches that include other ORMs - when running benchmarks, be sure to compile in Release and not attach a debugger (<kbd>Ctrl</kbd>+<kbd>F5</kbd>).
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment