Skip to content

Add commits index and sync benchmarks#2254

Merged
myieye merged 9 commits into
developfrom
sync-perf-benchmarks
May 19, 2026
Merged

Add commits index and sync benchmarks#2254
myieye merged 9 commits into
developfrom
sync-perf-benchmarks

Conversation

@myieye
Copy link
Copy Markdown
Collaborator

@myieye myieye commented May 5, 2026

Adds various sync benchmark tests.
The motivation for this is that I was exploring performance optimizations and having a benchmark in place lets me/us actually measure those optimizations.
This PR includes the first and smallest optimization: a DB index on the commit order columns.
So, now there are 2 Comment lines per benchmark documenting the approximate speedup that the index resulted in.

Hopefully future PRs will add more radical speedups.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d9dfad21-6122-43cb-86ff-fc508f72c04c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces comprehensive BenchmarkDotNet-based performance testing for CRDT sync operations. It adds benchmark infrastructure (logging, configuration), refactors test fixtures to use xUnit collections, implements two benchmark suites (first-sync and mutation-sync with five mutation profiles), and includes a database index optimization.

Changes

Benchmark Infrastructure and Performance Testing

Layer / File(s) Summary
Package dependencies and CI workflow setup
backend/Directory.Packages.props, backend/FwLite/FwLiteProjectSync.Tests/FwLiteProjectSync.Tests.csproj, .github/workflows/fw-lite.yaml
BenchmarkDotNet version 0.15.8 is added to central package management and referenced in the test project. The fw-lite.yaml workflow separates regular tests (filtered Category!=Benchmark) and benchmark tests (filtered Category=Benchmark) into distinct jobs, with the benchmark job building in Release mode and uploading BenchmarkDotNet artifacts.
Benchmark support infrastructure
backend/FwLite/FwLiteProjectSync.Tests/XUnitBenchmarkLogger.cs, backend/FwLite/FwLiteProjectSync.Tests/BenchmarkSupport.cs
XUnitBenchmarkLogger bridges BenchmarkDotNet logging to xUnit's ITestOutputHelper by buffering fragmented writes and flushing to output. BenchmarkSupport provides ConfigFor to build standardized benchmark configurations (in-process toolchain, 30-minute timeout, cold-start, 5 iterations, JSON exporter) and AssertRunWasSuccessful to validate benchmark run reports.
Test fixture and collection refactoring
backend/FwLite/FwLiteProjectSync.Tests/Fixtures/Sena3Collection.cs, backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs
Sena3Collection defines an xUnit collection that groups tests sharing the Sena3Fixture. Sena3SyncTests now uses [Collection(Sena3Collection.Name)] and no longer declares IClassFixture<Sena3Fixture>, enabling collection-level fixture sharing.
First sync benchmark
backend/FwLite/FwLiteProjectSync.Tests/SyncBenchmark.cs
SyncBenchmark runs FirstSyncBench as an xUnit Fact. In DEBUG, executes a single coverage run; in RELEASE, runs BenchmarkDotNet and validates mean execution time is below a fixed ThresholdSeconds (55.0). FirstSyncBench provisions a test project, saves an empty snapshot to force sync logic, and measures CrdtFwdataProjectSyncService.Sync(...).
Mutation sync benchmark with profile strategies
backend/FwLite/FwLiteProjectSync.Tests/SyncMutationBenchmark.cs
SyncMutationBenchmark parameterizes MutationSyncBench across five profiles (delete-heavy, patch-heavy, reorder-heavy, component-heavy, mixed-realistic). Each iteration applies mutations to a fully-imported Sena-3 project and measures sync performance. Delete removes entries; patch appends "_mut" to lexeme/citation forms; reorder moves first sense to end; component-heavy creates complex-form component links with retry logic and deduplication; mixed-realistic combines all strategies across quarter-sized entry slices. RELEASE mode asserts each profile's mean execution time stays below its configured threshold.
Commits table index migration
backend/FwLite/LcmCrdt/Migrations/20260506150734_AddCommitsOrderIndex.cs, backend/FwLite/LcmCrdt/Migrations/20260506150734_AddCommitsOrderIndex.Designer.cs
New EF Core migration creates a descending composite index IX_Commits_DateTime_Counter_Id on the Commits table to optimize query performance. The designer file contains the auto-generated target model with entity mappings, relationships, and delete behaviors across LcmCrdt, MiniLcm, and SIL.Harmony entities.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • sillsdev/languageforge-lexbox#1558: PR's addition of BenchmarkDotNet to backend/Directory.Packages.props builds on the retrieved PR's work introducing central package management.
  • sillsdev/languageforge-lexbox#1557: Both PRs modify .github/workflows/fw-lite.yaml's test invocation; main PR refactors and splits test execution for benchmarks while retrieved PR adds Android build flag changes.

Suggested reviewers

  • rmunn
  • jasonleenaylor

Poem

🐰 Benchmarks hop through sync and mutation,
Five profiles test every operation.
From first sync's leap to complex mutation's dance,
Performance metrics get their chance.
With BenchmarkDotNet's precise measure,
Speed becomes a quantified treasure!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add commits index and sync benchmarks' directly and clearly summarizes the main changes: adding a database index on commits and introducing new benchmark tests for synchronization performance measurement.
Description check ✅ Passed The description is clearly related to the changeset, explaining the motivation for adding benchmarks, the first optimization (DB index), and future plans for performance improvements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sync-perf-benchmarks

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the 💻 FW Lite issues related to the fw lite application, not miniLcm or crdt related label May 5, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

C# Unit Tests

165 tests   146 ✅  20s ⏱️
 23 suites    0 💤
  1 files     19 ❌

For more details on these failures, see this check.

Results for commit 53d6f8e.

♻️ This comment has been updated with latest results.

@argos-ci
Copy link
Copy Markdown

argos-ci Bot commented May 5, 2026

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) ✅ No changes detected - May 19, 2026, 11:17 AM

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

UI unit Tests

  1 files  ±0   59 suites  ±0   28s ⏱️ -4s
176 tests ±0  176 ✅ ±0  0 💤 ±0  0 ❌ ±0 
245 runs  ±0  245 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 53d6f8e. ± Comparison against base commit 79c015a.

♻️ This comment has been updated with latest results.

@myieye myieye force-pushed the sync-perf-benchmarks branch 4 times, most recently from 403f0a7 to f29fa44 Compare May 7, 2026 15:58
@myieye myieye marked this pull request as ready for review May 8, 2026 08:35
@myieye
Copy link
Copy Markdown
Collaborator Author

myieye commented May 8, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

✅ Actions performed

Full review triggered.

@myieye myieye changed the title Add sync benchmarks Add commits index and sync benchmarks May 11, 2026
@rmunn
Copy link
Copy Markdown
Contributor

rmunn commented May 11, 2026

@myieye - I merged #2262, so you might want to rebase on top of develop. They maybe you can use the NuGet package you were talking about so you don't have to create the index manually.

Copy link
Copy Markdown
Contributor

@rmunn rmunn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor change needed: dotnet-version 9.x in the GHA workflow should be 10.x now, and I want to ask about the NuGet package version being outdated. Other than that this looks good.

Comment thread .github/workflows/fw-lite.yaml Outdated
Comment thread backend/FwLite/FwLiteProjectSync.Tests/Fixtures/Sena3Collection.cs
Comment thread backend/FwLite/FwLiteProjectSync.Tests/BenchmarkSupport.cs Outdated
Comment thread backend/Directory.Packages.props Outdated
@myieye myieye force-pushed the sync-perf-benchmarks branch from f29fa44 to da3929a Compare May 18, 2026 15:55
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
backend/FwLite/LcmCrdt/Migrations/20260506150734_AddCommitsOrderIndex.cs (1)

8-10: ⚡ Quick win

Consider using EFCore.ComplexIndexes now that .NET 10 is in place.

The comment mentions moving to EFCore.ComplexIndexes "when we're on .NET 10," and that condition is now met. The package (version 2.0.2+) is available and explicitly supports EF Core 10. This would allow replacing the hand-written index with a fluent API approach in the DbContext, making the code more maintainable and aligned with the current framework capabilities. There's also an EFCore.ComplexIndexes.PostgreSQL variant if provider-specific index types are needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/LcmCrdt/Migrations/20260506150734_AddCommitsOrderIndex.cs`
around lines 8 - 10, The migration comment in AddCommitsOrderIndex.cs notes a
hand‑written index because EFCore lacked complex index support; since .NET 10 is
now used, replace the manual migration approach by removing the handwritten
index and instead define the index via EFCore.ComplexIndexes in the DbContext
(use the ComplexProperty mapping for the ComplexProperty + regular property
combination), adding the EFCore.ComplexIndexes (and
EFCore.ComplexIndexes.PostgreSQL if needed) package (v2.0.2+) and updating the
model configuration where Commit ordering is configured so the fluent API
creates the same index instead of the custom migration.
backend/FwLite/FwLiteProjectSync.Tests/SyncMutationBenchmark.cs (1)

96-97: ⚡ Quick win

Make the mutation workload deterministic per profile.

Each iteration reshuffles entries and re-samples component pairs from a shared RNG, so the same threshold is gating a different workload on every run. That noise is already visible in the high-variance profile comments. Seed the RNG per profile/iteration, or precompute a fixed mutation plan and replay it.

Also applies to: 136-138, 221-238

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/FwLiteProjectSync.Tests/SyncMutationBenchmark.cs` around lines
96 - 97, The benchmark uses a shared RNG causing non-deterministic reshuffles
and resampling; make each profile's workload deterministic by creating a seeded
Random (e.g., seed = Hash(profileName) ^ iterationIndex) inside the
SyncMutationBenchmark class and use that RNG instead of the shared one for all
shuffles and component-pair sampling (replace calls in the regions around lines
referenced, e.g., the shuffling/sampling code at 136-138 and 221-238);
alternatively precompute a mutationPlan List for each profile/iteration using
AutoFaker and the seeded RNG and replay that plan so the same threshold always
gates the same mutation sequence.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/FwLite/FwLiteProjectSync.Tests/SyncBenchmark.cs`:
- Around line 50-83: The SyncFromEmpty benchmark synchronously blocks on an
async call via .Result; change the method to an async benchmark (e.g. make
SyncFromEmpty async Task or async Task<SyncResult> and await
_syncService.Sync(...)) so it uses await instead of .Result; update the
corresponding benchmark in MutationSyncBench the same way; leave
IterationSetup's GetAwaiter().GetResult() as-is unless you opt to upgrade
BenchmarkDotNet to v0.16.0+ to allow async [IterationSetup].

In `@backend/FwLite/FwLiteProjectSync.Tests/SyncMutationBenchmark.cs`:
- Around line 231-245: The bare catch in the block around
api.CreateComplexFormComponent(...) hides all failures; change it to catch only
InvalidOperationException so only validation-related skips are swallowed (the
ones from the .Single() after AddComplexFormComponent), allowing other
exceptions to propagate; locate the try block that calls
api.CreateComplexFormComponent(new ComplexFormComponent { ... }) in
SyncMutationBenchmark.cs and replace the generic catch with catch
(InvalidOperationException) to preserve visibility into infrastructure or API
errors.

---

Nitpick comments:
In `@backend/FwLite/FwLiteProjectSync.Tests/SyncMutationBenchmark.cs`:
- Around line 96-97: The benchmark uses a shared RNG causing non-deterministic
reshuffles and resampling; make each profile's workload deterministic by
creating a seeded Random (e.g., seed = Hash(profileName) ^ iterationIndex)
inside the SyncMutationBenchmark class and use that RNG instead of the shared
one for all shuffles and component-pair sampling (replace calls in the regions
around lines referenced, e.g., the shuffling/sampling code at 136-138 and
221-238); alternatively precompute a mutationPlan List for each
profile/iteration using AutoFaker and the seeded RNG and replay that plan so the
same threshold always gates the same mutation sequence.

In `@backend/FwLite/LcmCrdt/Migrations/20260506150734_AddCommitsOrderIndex.cs`:
- Around line 8-10: The migration comment in AddCommitsOrderIndex.cs notes a
hand‑written index because EFCore lacked complex index support; since .NET 10 is
now used, replace the manual migration approach by removing the handwritten
index and instead define the index via EFCore.ComplexIndexes in the DbContext
(use the ComplexProperty mapping for the ComplexProperty + regular property
combination), adding the EFCore.ComplexIndexes (and
EFCore.ComplexIndexes.PostgreSQL if needed) package (v2.0.2+) and updating the
model configuration where Commit ordering is configured so the fluent API
creates the same index instead of the custom migration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ea880447-924c-44a4-9c7e-5a138ac9e061

📥 Commits

Reviewing files that changed from the base of the PR and between 52ae636 and da3929a.

📒 Files selected for processing (11)
  • .github/workflows/fw-lite.yaml
  • backend/Directory.Packages.props
  • backend/FwLite/FwLiteProjectSync.Tests/BenchmarkSupport.cs
  • backend/FwLite/FwLiteProjectSync.Tests/Fixtures/Sena3Collection.cs
  • backend/FwLite/FwLiteProjectSync.Tests/FwLiteProjectSync.Tests.csproj
  • backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs
  • backend/FwLite/FwLiteProjectSync.Tests/SyncBenchmark.cs
  • backend/FwLite/FwLiteProjectSync.Tests/SyncMutationBenchmark.cs
  • backend/FwLite/FwLiteProjectSync.Tests/XUnitBenchmarkLogger.cs
  • backend/FwLite/LcmCrdt/Migrations/20260506150734_AddCommitsOrderIndex.Designer.cs
  • backend/FwLite/LcmCrdt/Migrations/20260506150734_AddCommitsOrderIndex.cs

Comment on lines +50 to +83
// BenchmarkDotNet has no async support. .Result is intentional and will not deadlock.
#pragma warning disable VSTHRD002

public class FirstSyncBench
{
// CI initial result: mean 49.5s, StdDev 2.4s (medium variance) => 57s (~3σ above mean)
// CI with commits order index: mean 44.5s, StdDev 3.5s (medium variance) => 55s (~3σ above mean)
public const double ThresholdSeconds = 55.0;

internal static Sena3Fixture Fixture = null!;

private TestProject _project = null!;
private ProjectSnapshot _projectSnapshot = null!;
private CrdtFwdataProjectSyncService _syncService = null!;

[IterationSetup]
public void IterationSetup()
{
_project = Fixture.SetupProjects().GetAwaiter().GetResult();
_syncService = _project.Services.GetRequiredService<CrdtFwdataProjectSyncService>();

// Save an empty snapshot so the first sync runs as Sync, not Import.
ProjectSnapshotService.SaveProjectSnapshot(_project.FwDataProject, ProjectSnapshot.Empty)
.GetAwaiter().GetResult();
_projectSnapshot = _project.Services.GetRequiredService<ProjectSnapshotService>()
.GetProjectSnapshot(_project.FwDataProject).GetAwaiter().GetResult()
?? throw new InvalidOperationException("Expected snapshot to exist after saving");
}

[Benchmark]
public SyncResult SyncFromEmpty()
{
return _syncService.Sync(_project.CrdtApi, _project.FwDataApi, _projectSnapshot).Result;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does BenchmarkDotNet 0.15.8 support async Taskbenchmark methods and async[IterationSetup]/[IterationCleanup] methods?

💡 Result:

Yes, BenchmarkDotNet 0.15.8 supports async Task benchmark methods, as well as async [IterationSetup] and [IterationCleanup] methods [1][2][3]. BenchmarkDotNet has long supported benchmarking asynchronous methods that return Task or ValueTask [2]. Regarding setup and cleanup methods: 1. Global Setup/Cleanup: Support for async [GlobalSetup] and [GlobalCleanup] methods has been available for several versions [4]. 2. Iteration Setup/Cleanup: Support for async [IterationSetup] and [IterationCleanup] methods was recently added and is included in version 0.16.0 [3]. While version 0.15.8 does not include this specific feature (as it was tracked for v0.16.0) [3][5][6], the library generally handles asynchronous operations by awaiting them correctly in most standard execution scenarios [2]. If you encounter issues with async methods not being awaited properly in specific configurations (such as the InProcess toolchain), it is often recommended to ensure you are using the latest version or, as a workaround, to synchronously wait on the task using task.GetAwaiter().GetResult() [7][8].

Citations:


Convert the SyncFromEmpty() benchmark method to async Task — BenchmarkDotNet 0.15.8 supports async Task benchmarks.

Line 82 uses .Result to synchronously block an async API, violating the backend/**/*.cs guideline "Use async/await for asynchronous operations, not .Result or .Wait()". BenchmarkDotNet 0.15.8 supports async Task benchmark methods natively, so the benchmark can be refactored to public async Task SyncFromEmpty() without requiring the blocking call.

The IterationSetup method (lines 68–75) uses GetAwaiter().GetResult() due to a limitation in BenchmarkDotNet 0.15.8: async [IterationSetup] was not added until v0.16.0. Either upgrade the package to v0.16.0+ to enable async setup, or keep the synchronous workaround if a version upgrade is not feasible.

Apply the same fix to the matching benchmark in MutationSyncBench.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/FwLiteProjectSync.Tests/SyncBenchmark.cs` around lines 50 -
83, The SyncFromEmpty benchmark synchronously blocks on an async call via
.Result; change the method to an async benchmark (e.g. make SyncFromEmpty async
Task or async Task<SyncResult> and await _syncService.Sync(...)) so it uses
await instead of .Result; update the corresponding benchmark in
MutationSyncBench the same way; leave IterationSetup's GetAwaiter().GetResult()
as-is unless you opt to upgrade BenchmarkDotNet to v0.16.0+ to allow async
[IterationSetup].

Comment thread backend/FwLite/FwLiteProjectSync.Tests/SyncMutationBenchmark.cs
@myieye myieye force-pushed the sync-perf-benchmarks branch from 6f28cea to 0d0a59b Compare May 19, 2026 09:11
Monitoring strategy with WarmupCount=1, IterationCount=4 — same total
work as before, but iteration 1's JIT/EF-model-build/cold-cache cost is
discarded instead of folded into the measurement, so StdDev tightens and
thresholds can come down to genuine 3σ above the with-index mean.

Both comment lines (baseline + with-index) are re-measured under this
config so the speedup comparison is apples-to-apples.
@myieye myieye force-pushed the sync-perf-benchmarks branch from 0d0a59b to 53d6f8e Compare May 19, 2026 11:14
@myieye myieye merged commit 53d6f8e into develop May 19, 2026
23 of 25 checks passed
@myieye myieye deleted the sync-perf-benchmarks branch May 19, 2026 11:47
@myieye myieye mentioned this pull request May 19, 2026
@myieye
Copy link
Copy Markdown
Collaborator Author

myieye commented May 19, 2026

The sync button in VS Code merged this into develop 🤦.
Reopened here: #2276

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FW Lite issues related to the fw lite application, not miniLcm or crdt related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants