Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ The namespace tests are integration tests, plus focused generator diagnostics:
- `cargo test -p spacetimedb-testing --test standalone_integration_test namespace_csharp`:
independent dependency publication, root export selection, and cross-namespace
helper/HTTP calls, and runtime name conversion compared with host validation.
The nested fixture additionally checks repeated assembly instances, public
child promotion, deep writable/read-only access, per-instance dispatch and
transactions, and HTTP warnings at publication before any function calls.
- `cargo test -p spacetimedb-testing --test environment namespace_csharp_environment_security`:
root/public environment access, denial of namespaced host calls, and default
namespace case conversion when `Name` is omitted.
Expand All @@ -119,11 +122,26 @@ The namespace tests are integration tests, plus focused generator diagnostics:
`Accessor = "MyAuth", Name = "auth_data"`. The existing
`sdks/csharp/tools~/run-regression-tests.sh 8 10` harness includes it in the
.NET 10 module pass while retaining .NET 8 regressions.
- The [nested client regression](sdks/csharp/examples~/regression-tests/nested-namespaces/README.md)
is included in the same harness. It runs C# 9 bindings on both .NET 8 and
.NET 10 against the .NET 10 nested module, including three-level canonical SQL,
independent caches, reducer/procedure results and errors, views, joins, and
unsubscribe cleanup.

Use the local package setup in [DEVELOP.md](sdks/csharp/DEVELOP.md), not stale
published NuGet packages, when exercising changed query/runtime code.
Library-defined RLS inside named namespaces and cross-language module composition
are unsupported, not integration cases awaiting a passing assertion.
The nested host fixture also executes instance-aware module query views, including
deep paths, filters, and both semijoin directions. Immediate reducer/procedure
scheduling targets repeated instances through selected contexts and uses their
canonical paths. Context-selection assertions in the same fixture cover all
context families, shared state, and cross-scope transaction rollback. See the
[module limitations](crates/bindings-csharp/README.md#nested-assemblies).

For module performance comparisons, use the
[prebuilt-artifact benchmark](crates/bench/README.md#comparing-c-artifacts).
The circles integration test is a correctness smoke test, not a throughput result.

## Schema parity tests

Expand Down
24 changes: 23 additions & 1 deletion crates/bench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,28 @@ cargo test --release --package spacetimedb-testing --test standalone_integration
cargo test --release --package spacetimedb-testing --test standalone_integration_test test_calling_bench_db_circles -- --exact --nocapture
```

## Comparing C# artifacts

The `csharp_artifact` example accepts an already-built `benchmarks-cs` WASM module.
It measures empty reducer dispatch, a 10,000-row string-table scan, and the existing
circles cross-join workload (`initial_load = 100`). It uses the
same in-process host and excludes compilation and publication from measurements.

Build each revision with the same .NET SDK, NativeAOT toolchain, and release CLI
settings in separate worktrees. Use the optimized `StdbModule.opt.wasm` artifacts
from the `native` output directory. Build the runner once, then run it directly:

```sh
cargo build --release -p spacetimedb-bench --example csharp_artifact
CSHARP_BENCH_WASM=/absolute/path/to/before.wasm target/release/examples/csharp_artifact --bench --save-baseline before
CSHARP_BENCH_WASM=/absolute/path/to/after.wasm target/release/examples/csharp_artifact --bench --baseline before
```

Defaults are 30 samples, 3 seconds warmup, and 20 seconds measurement per workload.
Criterion options can override these. Do not run builds or other tests during
measurement; repeat in reverse order before attributing a small change to code.
This checks existing flat modules, not nested-instance startup cost or memory use.

## Pretty report
To generate a nicely formatted markdown report, you can use the "summarize" binary.
This is used on CI (see [`../../.github/workflows/benchmarks.yml`](../../.github/workflows/benchmarks.yml)).
Expand Down Expand Up @@ -220,4 +242,4 @@ You can generate flamegraphs using `flamegraph.sh` script:
```bash
# ./flamegraph.sh BENCH_EXECUTABLE FILTER SVG_PATH
./flamegraph.sh generic stdb_raw/mem/insert_bulk/location/multi_index/load=0/count=100 result.svg"
```
```
53 changes: 53 additions & 0 deletions crates/bench/examples/csharp_artifact.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//! Compare prebuilt C# benchmark modules without rebuilding or changing the host.
use criterion::{async_executor::AsyncExecutor, Criterion};
use spacetimedb_bench::{
database::BenchDatabase,
schemas::{create_sequential, u32_u64_str, IndexStrategy},
spacetime_module::SpacetimeModule,
};
use spacetimedb_lib::sats::product;
use spacetimedb_testing::modules::{CompiledModule, ModuleLanguage};
use std::{path::PathBuf, sync::OnceLock, time::Duration};

struct Artifact;

impl ModuleLanguage for Artifact {
const NAME: &'static str = "csharp-artifact";

fn get_module() -> &'static CompiledModule {
static MODULE: OnceLock<CompiledModule> = OnceLock::new();
MODULE.get_or_init(|| {
let path = PathBuf::from(std::env::var_os("CSHARP_BENCH_WASM").expect("set CSHARP_BENCH_WASM"));
CompiledModule::from_artifact("benchmarks-cs", spacetimedb::messages::control_db::HostType::Wasm, path)
})
}
}

fn main() {
let mut criterion = Criterion::default()
.sample_size(30)
.warm_up_time(Duration::from_secs(3))
.measurement_time(Duration::from_secs(20))
.configure_from_args();
let mut db = SpacetimeModule::<Artifact>::build(true).unwrap();
let table = db.create_table::<u32_u64_str>(IndexStrategy::Unique0).unwrap();
db.insert_bulk(&table, create_sequential::<u32_u64_str>(0xdeadbeef, 10_000, 100))
.unwrap();
criterion.bench_function("csharp-artifact/empty", |b| b.iter(|| db.empty_transaction().unwrap()));
criterion.bench_function("csharp-artifact/scan-10000", |b| b.iter(|| db.iterate(&table).unwrap()));
(&db).block_on(async {
db.module
.call_reducer_binary("init_game_circles", &product![100u32])
.await
.unwrap();
});
criterion.bench_function("csharp-artifact/circles-100", |b| {
b.to_async(&db).iter(|| async {
db.module
.call_reducer_binary("run_game_circles", &product![100u32])
.await
.unwrap()
});
});
criterion.final_summary();
}
3 changes: 3 additions & 0 deletions crates/bindings-csharp/BSATN.Codegen/Type.cs
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,9 @@ public override string ToString() =>

extensions.Contents.Append(
$$"""
#if NET10_0_OR_GREATER
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
#endif
public void ReadFields(System.IO.BinaryReader reader) {
{{string.Join(
"\n",
Expand Down
24 changes: 21 additions & 3 deletions crates/bindings-csharp/BSATN.Runtime/QueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,47 @@ namespace SpacetimeDB;

using System;
using System.Globalization;
using System.Linq;

/// <summary>A table identifier with a separately quoted optional namespace and local name.</summary>
/// <summary>A table identifier with separately quoted namespace segments and local name.</summary>
public readonly struct SqlTableName
{
private readonly string? quotedNamespace;

/// <summary>The namespace path, without SQL quoting.</summary>
public string? Namespace { get; }
public string LocalName { get; }

public SqlTableName(string localName)
{
LocalName = localName;
Namespace = null;
quotedNamespace = null;
}

public SqlTableName(string @namespace, string localName)
{
Namespace = @namespace ?? throw new ArgumentNullException(nameof(@namespace));
LocalName = localName;
quotedNamespace = SqlFormat.QuoteIdent(@namespace);
}

public SqlTableName(string[] namespaceSegments, string localName)
{
namespaceSegments =
namespaceSegments ?? throw new ArgumentNullException(nameof(namespaceSegments));
Namespace = namespaceSegments.Length == 0 ? null : string.Join(".", namespaceSegments);
LocalName = localName;
quotedNamespace =
namespaceSegments.Length == 0
? null
: string.Join(".", namespaceSegments.Select(SqlFormat.QuoteIdent));
}

public override string ToString() =>
Namespace is null
quotedNamespace is null
? SqlFormat.QuoteIdent(LocalName)
: SqlFormat.QuoteIdent(Namespace) + "." + SqlFormat.QuoteIdent(LocalName);
: quotedNamespace + "." + SqlFormat.QuoteIdent(LocalName);
}

public readonly struct SqlLiteral<T>
Expand Down
Loading
Loading