Skip to content

Commit ace87e0

Browse files
Benchmarks: two ODX Map(Func) paths in the IN6 probe, with and without the async-read opt-in (#143)
The IN6 probe measured the ODX Map<T>() paths and the hand-written provider paths, but not the Map(Func<IDataRecord,T>) selector path that Default-mode callers use. Two paths are added, selector+Read (Map(selector).ToListAsync(), rows read with Read) and selector+EnableAsyncRead (the same after EnableAsyncRead(), rows read with ReadAsync), so the selector path has an ODX-on-both-sides allocation and timing pair. They run in S2 only, with the existing hand-written 20-column selector carried on the AsyncPlan, and are checked field by field against Map<T>() on the first 5 rows before measuring; --selftest-in6 covers them on the synthetic provider. The default --paths set is unchanged. Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent c320e3e commit ace87e0

2 files changed

Lines changed: 40 additions & 4 deletions

File tree

‎Benchmarks/Open.Database.Extensions.Benchmarks/In6AsyncPaths.cs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ public override void ReadSync(DbDataReader reader, TRow row, ref long getterTick
5555
/// <summary>How to read one scenario's rows with the async getters: how to make a row and the columns in ordinal order.</summary>
5656
public sealed class AsyncPlan<TRow>(Func<TRow> create, AsyncColumn<TRow>[] columns)
5757
{
58+
/// <summary>The hand-written Map(Func&lt;IDataRecord, TRow&gt;) selector for the selector+Read and selector+EnableAsyncRead paths (S2 only), or null.</summary>
59+
public Func<IDataRecord, TRow>? Selector { get; init; }
60+
5861
/// <summary>Reads the rows (at most maxRows) with ExecuteReaderAsync, ReadAsync and the async getters; drains the other result sets when it read them all.</summary>
5962
public async Task<List<TRow>> ReadAllAsync(DbConnection connection, string sql, CommandBehavior behavior, int maxRows = int.MaxValue)
6063
{
@@ -162,7 +165,7 @@ _ when typeof(TValue) == typeof(byte[]) => Of<byte[]>(static (r, o) => (byte[])r
162165

163166
/// <summary>The catalog row (SynRow) as SQL Server delivers it: the first width columns.</summary>
164167
public static AsyncPlan<SynRow> SynRowPlan(int width)
165-
=> new(static () => new SynRow(), [.. SynRowColumns().Take(width)]);
168+
=> new(static () => new SynRow(), [.. SynRowColumns().Take(width)]) { Selector = width == 20 ? SyntheticVariants.Hand(20) : null };
166169

167170
static AsyncColumn<SynRow>[] SynRowColumns() =>
168171
[

‎Benchmarks/Open.Database.Extensions.Benchmarks/In6Probe.cs‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public static class In6Probe
9696
--pressure auto|N|off|both auto or N: K busy thread-pool items for the "pressure on" cells (default auto = processor count x 2); off: no pressure cells for any scenario; both (default): pressure off and on
9797
--rows 10000 --warmup 5 --ops 20 rows per scenario, warm-up and measured operations per cell (per path)
9898
--scenarios S1,S2,S3 which SQL Server scenarios run (default all three; S1 width 5, S2 width 20, S3 about 8 KB per row)
99-
--paths <list> which read paths run (default sync,async+Read,EnableAsyncRead; also P4 = hand-written async getters, P5 = the same with SequentialAccess, P6 = raw provider: ReadAsync + sync getters, no ODX, no wrapper, timed inline), e.g. --paths sync,P4,P5
99+
--paths <list> which read paths run (default sync,async+Read,EnableAsyncRead; also P4 = hand-written async getters, P5 = the same with SequentialAccess, P6 = raw provider: ReadAsync + sync getters, no ODX, no wrapper, timed inline; selector+Read and selector+EnableAsyncRead = ODX Map(Func<IDataRecord,T>).ToListAsync() with the hand-written selector, rows read with Read or, with the opt-in, with ReadAsync; S2 only), e.g. --paths sync,P4,P5
100100
--ops-s3 N measured operations for S3 only (default: the value of --ops)
101101
--pressure-s3 both|off with off, S3 runs only its pressure-off cells (S1 and S2 still run both); default both
102102
--sql-tcp <host,port> run the SQL Server part against a server over TCP (for example 127.0.0.1,14333) as user sa, TrustServerCertificate, default packet size, instead of LocalDB;
@@ -307,6 +307,8 @@ static void DryRun(In6Settings s)
307307
if (s.Paths.Contains("P4")) Console.WriteLine("P4 (hand-written, not ODX): ExecuteReaderAsync with the default behavior, ReadAsync, and EVERY cell read with await GetFieldValueAsync<T>(i); IsDBNullAsync only for the columns that can be NULL; builds the same row objects (checked on the first 5 rows before measuring)");
308308
if (s.Paths.Contains("P6")) Console.WriteLine("P6 (raw, not ODX, NO timing wrapper): the provider's own connection and command, ExecuteReaderAsync (default behavior), await ReadAsync per row, then SYNC typed getters (the same cells and row objects); timed INLINE in the probe loop: 'inline await ReadAsync ms sum/max' = the time from before each ReadAsync call to after its await (a truly async completion and the continuation's wait for a thread are inside it), 'inline getters ms sum' = the time in the typed getters, the ReadAsync incomplete count = calls whose task was not complete when checked, 'wait ms' = the await time of those calls (not the wait until completion), 'wait rows' = the rows read so far at those calls; reader-open is timed inline too");
309309
if (s.Paths.Contains("P5")) Console.WriteLine("P5 (hand-written, not ODX): the same as P4 with CommandBehavior.SequentialAccess, cells read strictly in ordinal order");
310+
if (s.Paths.Contains("selector+Read")) Console.WriteLine("selector+Read (ODX): Map(hand-written Func<IDataRecord,T> over all 20 columns, typed getters).ToListAsync(), async execute + Read; S2 only (checked against Map<T>() on the first 5 rows before measuring)");
311+
if (s.Paths.Contains("selector+EnableAsyncRead")) Console.WriteLine("selector+EnableAsyncRead (ODX): the same with EnableAsyncRead() first, async execute + ReadAsync; S2 only (checked the same way)");
310312
if (s.Tcp)
311313
{
312314
string[] hostPort = s.SqlTcp!.Split(',');
@@ -454,7 +456,7 @@ internal static In6Summary Measure(string path, In6Stats stats, Func<int> operat
454456
}
455457

456458
// The three paths of one scenario over one (wrapped) connection. T is the row type.
457-
internal static readonly string[] AllPaths = ["sync", "async+Read", "EnableAsyncRead", "P4", "P5", "P6"];
459+
internal static readonly string[] AllPaths = ["sync", "async+Read", "EnableAsyncRead", "P4", "P5", "P6", "selector+Read", "selector+EnableAsyncRead"];
458460
internal static readonly string[] DefaultPaths = ["sync", "async+Read", "EnableAsyncRead"];
459461

460462
// The paths of one scenario over one (wrapped) connection: the three ODX paths, and P4 and P5 (hand-written, async getters) when a plan is given. T is the row type.
@@ -469,6 +471,8 @@ internal static List<In6Summary> RunPaths<T>(TimedConnection timed, In6Stats sta
469471
("P4", () => RequirePlan(plan).ReadAllAsync(timed, sql, CommandBehavior.Default).GetAwaiter().GetResult().Count),
470472
("P5", () => RequirePlan(plan).ReadAllAsync(timed, sql, CommandBehavior.SequentialAccess).GetAwaiter().GetResult().Count),
471473
("P6", () => RequirePlan(plan).ReadAllRawAsync(timed.Inner, sql, stats).GetAwaiter().GetResult().Count),
474+
("selector+Read", () => timed.Command(sql).Map(RequireSelector(plan)).ToListAsync().AsTask().GetAwaiter().GetResult().Count),
475+
("selector+EnableAsyncRead", () => timed.Command(sql).EnableAsyncRead().Map(RequireSelector(plan)).ToListAsync().AsTask().GetAwaiter().GetResult().Count),
472476
};
473477

474478
only ??= DefaultPaths;
@@ -492,6 +496,8 @@ internal static List<In6Summary> RunPaths<T>(TimedConnection timed, In6Stats sta
492496

493497
static AsyncPlan<T> RequirePlan<T>(AsyncPlan<T>? plan) => plan ?? throw new InvalidOperationException("this scenario has no hand-written plan for the async getter paths");
494498

499+
static Func<IDataRecord, T> RequireSelector<T>(AsyncPlan<T>? plan) => plan?.Selector ?? throw new InvalidOperationException("this scenario has no hand-written selector (S2 only)");
500+
495501
static string Ms(double us) => (us / 1000d).ToString("F3", CultureInfo.InvariantCulture);
496502
static string Kb(double bytes) => (bytes / 1024d).ToString("F0", CultureInfo.InvariantCulture);
497503
static string N(double value) => value.ToString("0.#", CultureInfo.InvariantCulture);
@@ -517,6 +523,11 @@ static void VerifyAsyncPaths<T>(TimedConnection timed, string sql, AsyncPlan<T>
517523
if (paths.Contains("P4")) CheckSame("P4", expected, plan.ReadAllAsync(timed, sql, CommandBehavior.Default, 5).GetAwaiter().GetResult());
518524
if (paths.Contains("P5")) CheckSame("P5", expected, plan.ReadAllAsync(timed, sql, CommandBehavior.SequentialAccess, 5).GetAwaiter().GetResult());
519525
if (paths.Contains("P6")) CheckSame("P6", expected, plan.ReadAllRawAsync(timed.Inner, sql, new In6Stats(), 5).GetAwaiter().GetResult());
526+
if (plan.Selector is { } selector)
527+
{
528+
if (paths.Contains("selector+Read")) CheckSame("selector+Read", expected, timed.Command(sql).Map(selector).Take(5).ToListAsync().AsTask().GetAwaiter().GetResult());
529+
if (paths.Contains("selector+EnableAsyncRead")) CheckSame("selector+EnableAsyncRead", expected, timed.Command(sql).EnableAsyncRead().Map(selector).Take(5).ToListAsync().AsTask().GetAwaiter().GetResult());
530+
}
520531
}
521532

522533
static void CheckSame<T>(string path, List<T> expected, List<T> actual)
@@ -531,7 +542,7 @@ static void RunCell<T>(In6Settings s, string provider, string scenario, DbConnec
531542
{
532543
var stats = new In6Stats();
533544
var timed = new TimedConnection(connection, stats);
534-
if (plan is not null && (s.Paths.Contains("P4") || s.Paths.Contains("P5") || s.Paths.Contains("P6"))) VerifyAsyncPaths(timed, sql, plan, s.Paths);
545+
if (plan is not null && (s.Paths.Contains("P4") || s.Paths.Contains("P5") || s.Paths.Contains("P6") || s.Paths.Contains("selector+Read") || s.Paths.Contains("selector+EnableAsyncRead"))) VerifyAsyncPaths(timed, sql, plan, s.Paths);
535546
foreach (int k in pressures)
536547
{
537548
string label = k == 0 ? "off" : "on K=" + k;
@@ -1081,6 +1092,28 @@ void Check(bool ok, string what)
10811092
Check(shapeOk, $"{r.Path}: getter wait rows are not 100, 200, ..., 900 each seen {GetterOps} times: {spots?.Format()}");
10821093
}
10831094

1095+
// The two ODX selector paths (width 20, the hand-written selector): checked on the first rows against Map<T>(), then run; Read for the first, ReadAsync for the second.
1096+
var wideTimed = new TimedConnection(new SyntheticConnection(SyntheticTable.Create(20, Rows), new SyntheticOptions()), stats);
1097+
var widePlan = In6Plans.SynRowPlan(20);
1098+
string[] selectorPaths = ["selector+Read", "selector+EnableAsyncRead"];
1099+
try
1100+
{
1101+
VerifyAsyncPaths(wideTimed, SyntheticVariants.Sql, widePlan, selectorPaths);
1102+
}
1103+
catch (InvalidOperationException ex)
1104+
{
1105+
Check(false, "the selector paths read different values than Map<T>(): " + ex.Message);
1106+
}
1107+
1108+
var selectorResults = RunPaths<SynRow>(wideTimed, stats, SyntheticVariants.Sql, Rows, 1, GetterOps, null, selectorPaths, widePlan);
1109+
Check(selectorResults.Count == 2, $"{selectorResults.Count} selector paths measured, expected 2");
1110+
foreach (var r in selectorResults)
1111+
{
1112+
Console.WriteLine(" " + Line("synthetic", "-", "selector", r));
1113+
if (r.Path == "selector+EnableAsyncRead") Check(r.ReadAsyncCalls == Rows + 1 && r.ReadCalls == 0, $"{r.Path}: {r.ReadAsyncCalls} ReadAsync and {r.ReadCalls} Read calls per operation, expected {Rows + 1} and 0");
1114+
else Check(r.ReadAsyncCalls == 0 && r.ReadCalls == Rows + 1, $"{r.Path}: {r.ReadAsyncCalls} ReadAsync and {r.ReadCalls} Read calls per operation, expected 0 and {Rows + 1}");
1115+
}
1116+
10841117
// The recorder inside the synthetic reader and the wrappers around it time the same readers: the wrapper's window contains the recorder's.
10851118
long readers = recorder.Readers;
10861119
Check(readers == 3L * (Warmup + Ops), $"the synthetic recorder saw {readers} readers, expected {3 * (Warmup + Ops)}");

0 commit comments

Comments
 (0)