Skip to content

Commit 6283440

Browse files
author
fabien.menager
committed
Add support for IncludeGraph option in benchmarks
1 parent 0410e40 commit 6283440

3 files changed

Lines changed: 148 additions & 26 deletions

File tree

tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/LibComparator.cs

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,6 @@ public abstract partial class LibComparator
1919
[Params(500_000/*, 1_000_000/*, 10_000_000*/)]
2020
public int N;
2121

22-
/// <summary>
23-
/// Set to true to benchmark with IncludeGraph option enabled.
24-
/// When true, each entity will have 2 child entities for graph insertion benchmarking.
25-
/// Default is false, which runs the benchmark exactly as before (flat entities only).
26-
/// </summary>
27-
private const bool UseIncludeGraph = false;
2822

2923
private IList<TestEntity> data = [];
3024
protected TestDbContext DbContext { get; set; } = null!;
@@ -42,15 +36,14 @@ public void IterationSetup()
4236
NumericEnumValue = (NumericEnum)(i % 2),
4337
};
4438

45-
// When UseIncludeGraph is true, add child entities for graph insertion benchmarking
46-
if (UseIncludeGraph)
47-
{
39+
// When BENCHMARK_INCLUDE_GRAPH is set, add child entities for graph insertion benchmarking
40+
#if BENCHMARK_INCLUDE_GRAPH
4841
entity.Children = new List<TestEntityChild>
4942
{
5043
new TestEntityChild { Description = $"Child1 of Entity{i}", Quantity = i },
5144
new TestEntityChild { Description = $"Child2 of Entity{i}", Quantity = i * 2 },
5245
};
53-
}
46+
#endif
5447

5548
return entity;
5649
}).ToList();
@@ -82,7 +75,9 @@ public async Task PhenX_EntityFrameworkCore_BulkInsert()
8275
{
8376
await DbContext.ExecuteBulkInsertAsync(data, options =>
8477
{
85-
options.IncludeGraph = UseIncludeGraph;
78+
#if BENCHMARK_INCLUDE_GRAPH
79+
options.IncludeGraph = true;
80+
#endif
8681
});
8782
}
8883
//
@@ -92,15 +87,10 @@ await DbContext.ExecuteBulkInsertAsync(data, options =>
9287
// DbContext.ExecuteBulkInsert(data);
9388
// }
9489

90+
#if !BENCHMARK_INCLUDE_GRAPH
9591
[Benchmark]
9692
public void RawInsert()
9793
{
98-
if (UseIncludeGraph)
99-
{
100-
// Raw insert doesn't support graph insertion - skip when UseIncludeGraph is true
101-
return;
102-
}
103-
10494
if (DbContext.Database.ProviderName!.Contains("SqlServer", StringComparison.InvariantCultureIgnoreCase))
10595
{
10696
// Use SqlBulkCopy for SQL Server
@@ -133,22 +123,22 @@ public void RawInsert()
133123
[Benchmark]
134124
public async Task Linq2Db()
135125
{
136-
if (UseIncludeGraph)
137-
{
138-
// Linq2Db doesn't support graph insertion - skip when UseIncludeGraph is true
139-
return;
140-
}
141-
142126
await DbContext.BulkCopyAsync(new BulkCopyOptions
143127
{
144128
BulkCopyType = BulkCopyType.ProviderSpecific,
145129
}, data);
146130
}
131+
#endif
147132

148133
[Benchmark]
149134
public async Task Z_EntityFramework_Extensions_EFCore()
150135
{
151-
await DbContext.BulkInsertOptimizedAsync(data, options => options.IncludeGraph = UseIncludeGraph);
136+
await DbContext.BulkInsertOptimizedAsync(data, options =>
137+
{
138+
#if BENCHMARK_INCLUDE_GRAPH
139+
options.IncludeGraph = true;
140+
#endif
141+
});
152142
}
153143

154144
// [Benchmark]
@@ -162,8 +152,10 @@ public async Task EFCore_BulkExtensions()
162152
{
163153
await DbContext.BulkInsertAsync(data, options =>
164154
{
165-
options.IncludeGraph = UseIncludeGraph;
166-
options.PreserveInsertOrder = UseIncludeGraph; // Required for graph insertion
155+
#if BENCHMARK_INCLUDE_GRAPH
156+
options.IncludeGraph = true;
157+
options.PreserveInsertOrder = true; // Required for graph insertion
158+
#endif
167159
});
168160
}
169161

@@ -177,10 +169,12 @@ await DbContext.BulkInsertAsync(data, options =>
177169
// });
178170
// }
179171

172+
#if !BENCHMARK_INCLUDE_GRAPH
180173
[Benchmark]
181174
public async Task EFCore_SaveChanges()
182175
{
183176
DbContext.AddRange(data);
184177
await DbContext.SaveChangesAsync();
185178
}
179+
#endif
186180
}

tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/PhenX.EntityFrameworkCore.BulkInsert.Benchmark.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
<OutputType>Exe</OutputType>
55
<IsPackable>false</IsPackable>
66
<IsTestProject>false</IsTestProject>
7+
<!-- Uncomment the line below to enable IncludeGraph in the benchmark. -->
8+
<!-- <DefineConstants>$(DefineConstants);BENCHMARK_INCLUDE_GRAPH</DefineConstants>-->
79
</PropertyGroup>
810

911
<ItemGroup>

tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Graph/GraphTestsBase.cs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,4 +588,130 @@ await _context.ExecuteBulkInsertAsync(blogs, options =>
588588
dbPosts.Select(p => p.Id).Should().Contain(post2.Id);
589589
dbPosts.Select(p => p.Id).Should().Contain(post3.Id);
590590
}
591+
592+
[SkippableFact]
593+
public async Task InsertGraph_LargeScale()
594+
{
595+
// Arrange - Create many blogs with many children each (Posts, Tags, BlogSettings)
596+
// This tests performance and correctness at scale
597+
const int blogCount = 5000;
598+
const int postsPerBlog = 100;
599+
const int tagsPerPost = 10;
600+
601+
var blogs = new List<Blog>();
602+
var allTags = new List<Tag>();
603+
604+
// Pre-create a pool of tags that will be shared across posts
605+
for (var i = 0; i < 50; i++)
606+
{
607+
allTags.Add(new Tag
608+
{
609+
TestRun = _run,
610+
Name = $"{_run}_SharedTag_{i}"
611+
});
612+
}
613+
614+
for (var blogIndex = 0; blogIndex < blogCount; blogIndex++)
615+
{
616+
var posts = new List<Post>();
617+
618+
// Create posts for this blog
619+
for (var postIndex = 0; postIndex < postsPerBlog; postIndex++)
620+
{
621+
var post = new Post
622+
{
623+
TestRun = _run,
624+
Title = $"{_run}_Blog{blogIndex}_Post{postIndex}"
625+
};
626+
627+
// Add some tags to this post (from the shared pool)
628+
var postTags = new List<Tag>();
629+
for (var tagIndex = 0; tagIndex < tagsPerPost; tagIndex++)
630+
{
631+
var tagPoolIndex = (blogIndex * postsPerBlog + postIndex + tagIndex) % allTags.Count;
632+
postTags.Add(allTags[tagPoolIndex]);
633+
}
634+
post.Tags = postTags;
635+
636+
posts.Add(post);
637+
}
638+
639+
// Create the blog with its children
640+
var blog = new Blog
641+
{
642+
TestRun = _run,
643+
Name = $"{_run}_LargeScaleBlog_{blogIndex}",
644+
Posts = posts,
645+
Settings = new BlogSettings
646+
{
647+
TestRun = _run,
648+
EnableComments = blogIndex % 2 == 0
649+
}
650+
};
651+
652+
blogs.Add(blog);
653+
}
654+
655+
// Act - Insert all 1000 blogs with their children
656+
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
657+
await _context.ExecuteBulkInsertAsync(blogs, options =>
658+
{
659+
options.IncludeGraph = true;
660+
});
661+
stopwatch.Stop();
662+
663+
// Assert - Verify all entities were inserted correctly
664+
var insertedBlogs = _context.Blogs.Where(b => b.TestRun == _run).ToList();
665+
insertedBlogs.Should().HaveCount(blogCount, "All blogs should be inserted");
666+
insertedBlogs.Should().AllSatisfy(b => b.Id.Should().BeGreaterThan(0), "All blogs should have generated IDs");
667+
668+
var insertedPosts = _context.Posts.Where(p => p.TestRun == _run).ToList();
669+
insertedPosts.Should().HaveCount(blogCount * postsPerBlog, "All posts should be inserted");
670+
insertedPosts.Should().AllSatisfy(p =>
671+
{
672+
p.Id.Should().BeGreaterThan(0, "Post should have generated ID");
673+
p.BlogId.Should().BeGreaterThan(0, "Post should have valid BlogId FK");
674+
});
675+
676+
var insertedSettings = _context.BlogSettings.Where(s => s.TestRun == _run).ToList();
677+
insertedSettings.Should().HaveCount(blogCount, "All blog settings should be inserted");
678+
insertedSettings.Should().AllSatisfy(s =>
679+
{
680+
s.Id.Should().BeGreaterThan(0, "Settings should have generated ID");
681+
s.BlogId.Should().BeGreaterThan(0, "Settings should have valid BlogId FK");
682+
});
683+
684+
var insertedTags = _context.Tags.Where(t => t.TestRun == _run).ToList();
685+
insertedTags.Should().HaveCount(allTags.Count, "All unique tags should be inserted");
686+
insertedTags.Should().AllSatisfy(t => t.Id.Should().BeGreaterThan(0), "All tags should have generated IDs");
687+
688+
// Verify original entities have been updated with generated IDs
689+
blogs.Should().AllSatisfy(b =>
690+
{
691+
b.Id.Should().BeGreaterThan(0, "Original blog should have ID populated");
692+
b.Posts.Should().AllSatisfy(p =>
693+
{
694+
p.Id.Should().BeGreaterThan(0, "Original post should have ID populated");
695+
p.BlogId.Should().Be(b.Id, "Original post FK should reference its blog");
696+
});
697+
b.Settings.Should().NotBeNull();
698+
b.Settings!.Id.Should().BeGreaterThan(0, "Original settings should have ID populated");
699+
b.Settings.BlogId.Should().Be(b.Id, "Original settings FK should reference its blog");
700+
});
701+
702+
allTags.Should().AllSatisfy(t =>
703+
{
704+
t.Id.Should().BeGreaterThan(0, "Original tag should have ID populated");
705+
});
706+
707+
// Report performance metrics
708+
var totalEntities = blogCount + (blogCount * postsPerBlog) + blogCount + allTags.Count;
709+
var entitiesPerSecond = totalEntities / stopwatch.Elapsed.TotalSeconds;
710+
711+
// Note: This is informational, not an assertion
712+
// Output is visible in test logs
713+
_context.GetType().Name.Should().NotBeNullOrEmpty(
714+
$"Inserted {totalEntities:N0} entities in {stopwatch.Elapsed.TotalSeconds:F2}s " +
715+
$"({entitiesPerSecond:F0} entities/sec) using {_context.GetType().Name}");
716+
}
591717
}

0 commit comments

Comments
 (0)