Skip to content

Commit

Permalink
Rust SDK: no more reducer args structs (#2036)
Browse files Browse the repository at this point in the history
  • Loading branch information
gefjon authored Dec 4, 2024
1 parent 3902f75 commit d9de1e3
Show file tree
Hide file tree
Showing 214 changed files with 6,948 additions and 2,712 deletions.
202 changes: 149 additions & 53 deletions crates/cli/src/subcommands/generate/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Requested namespace: {namespace}",
AlgebraicTypeDef::Product(product) => {
gen_and_print_imports(module, out, &product.elements, &[typ.ty]);
out.newline();
define_struct_for_product(module, out, &type_name, &product.elements);
define_struct_for_product(module, out, &type_name, &product.elements, "pub");
}
AlgebraicTypeDef::Sum(sum) => {
gen_and_print_imports(module, out, &sum.variants, &[typ.ty]);
Expand Down Expand Up @@ -130,7 +130,7 @@ Requested namespace: {namespace}",
let table_handle = table_name_pascalcase.clone() + "TableHandle";
let insert_callback_id = table_name_pascalcase.clone() + "InsertCallbackId";
let delete_callback_id = table_name_pascalcase.clone() + "DeleteCallbackId";
let accessor_trait = table_name_pascalcase.clone() + "TableAccess";
let accessor_trait = table_access_trait_name(&table.name);
let accessor_method = table_method_name(&table.name);

write!(
Expand Down Expand Up @@ -353,14 +353,31 @@ Requested namespace: {namespace}",

let reducer_name = reducer.name.deref();
let func_name = reducer_function_name(reducer);
let set_reducer_flags_trait = format!("set_flags_for_{func_name}");
let set_reducer_flags_trait = reducer_flags_trait_name(reducer);
let args_type = reducer_args_type_name(&reducer.name);

define_struct_for_product(module, out, &args_type, &reducer.params_for_generate.elements);
let enum_variant_name = reducer_variant_name(&reducer.name);

// Define an "args struct" for the reducer.
// This is not user-facing (note the `pub(super)` visibility);
// it is an internal helper for serialization and deserialization.
// We actually want to ser/de instances of `enum Reducer`, but:
// - `Reducer` will have struct-like variants, which SATS ser/de does not support.
// - The WS format does not contain a BSATN-serialized `Reducer` instance;
// it holds the reducer name or ID separately from the argument bytes.
// We could work up some magic with `DeserializeSeed`
// and/or custom `Serializer` and `Deserializer` types
// to account for this, but it's much easier to just use an intermediate struct per reducer.
define_struct_for_product(
module,
out,
&args_type,
&reducer.params_for_generate.elements,
"pub(super)",
);

out.newline();

let callback_id = args_type.clone() + "CallbackId";
let callback_id = reducer_callback_id_name(&reducer.name);

// The reducer arguments as `ident: ty, ident: ty, ident: ty,`,
// like an argument list.
Expand All @@ -373,10 +390,6 @@ Requested namespace: {namespace}",
// The reducer argument names as `ident, ident, ident`,
// for passing to function call and struct literal expressions.
let mut arg_names_list = String::new();
// The reducer argument names as `&args.ident, &args.ident, &args.ident`,
// for extracting from a structure named `args` by reference
// and passing to a function call.
let mut unboxed_arg_refs = String::new();
for (arg_ident, arg_ty) in &reducer.params_for_generate.elements[..] {
arg_types_ref_list += "&";
write_type(module, &mut arg_types_ref_list, arg_ty).unwrap();
Expand All @@ -385,12 +398,40 @@ Requested namespace: {namespace}",
let arg_name = arg_ident.deref().to_case(Case::Snake);
arg_names_list += &arg_name;
arg_names_list += ", ";

unboxed_arg_refs += "&args.";
unboxed_arg_refs += &arg_name;
unboxed_arg_refs += ", ";
}

write!(out, "impl From<{args_type}> for super::Reducer ");
out.delimited_block(
"{",
|out| {
write!(out, "fn from(args: {args_type}) -> Self ");
out.delimited_block(
"{",
|out| {
write!(out, "Self::{enum_variant_name}");
if !reducer.params_for_generate.elements.is_empty() {
// We generate "struct variants" for reducers with arguments,
// but "unit variants" for reducers of no arguments.
// These use different constructor syntax.
out.delimited_block(
" {",
|out| {
for (arg_ident, _ty) in &reducer.params_for_generate.elements[..] {
let arg_name = arg_ident.deref().to_case(Case::Snake);
writeln!(out, "{arg_name}: args.{arg_name},");
}
},
"}",
);
}
out.newline();
},
"}\n",
);
},
"}\n",
);

// TODO: check for lifecycle reducers and do not generate the invoke method.

writeln!(
Expand Down Expand Up @@ -437,13 +478,24 @@ impl {func_name} for super::RemoteReducers {{
&self,
mut callback: impl FnMut(&super::EventContext, {arg_types_ref_list}) + Send + 'static,
) -> {callback_id} {{
{callback_id}(self.imp.on_reducer::<{args_type}>(
{callback_id}(self.imp.on_reducer(
{reducer_name:?},
Box::new(move |ctx: &super::EventContext, args: &{args_type}| callback(ctx, {unboxed_arg_refs})),
Box::new(move |ctx: &super::EventContext| {{
let super::EventContext {{
event: __sdk::Event::Reducer(__sdk::ReducerEvent {{
reducer: super::Reducer::{enum_variant_name} {{
{arg_names_list}
}},
..
}}),
..
}} = ctx else {{ unreachable!() }};
callback(ctx, {arg_names_list})
}}),
))
}}
fn remove_on_{func_name}(&self, callback: {callback_id}) {{
self.imp.remove_on_reducer::<{args_type}>({reducer_name:?}, callback.0)
self.imp.remove_on_reducer({reducer_name:?}, callback.0)
}}
}}
Expand Down Expand Up @@ -714,10 +766,11 @@ fn define_struct_for_product(
out: &mut Indenter,
name: &str,
elements: &[(Identifier, AlgebraicTypeUse)],
vis: &str,
) {
print_struct_derives(out);

write!(out, "pub struct {name} ");
write!(out, "{vis} struct {name} ");

// TODO: if elements is empty, define a unit struct with no brace-delimited list of fields.
write_struct_type_fields_in_braces(
Expand All @@ -744,14 +797,22 @@ fn table_method_name(table_name: &Identifier) -> String {
table_name.deref().to_case(Case::Snake)
}

fn table_access_trait_name(table_name: &Identifier) -> String {
table_name.deref().to_case(Case::Pascal) + "TableAccess"
}

fn reducer_args_type_name(reducer_name: &Identifier) -> String {
reducer_name.deref().to_case(Case::Pascal)
reducer_name.deref().to_case(Case::Pascal) + "Args"
}

fn reducer_variant_name(reducer_name: &Identifier) -> String {
reducer_name.deref().to_case(Case::Pascal)
}

fn reducer_callback_id_name(reducer_name: &Identifier) -> String {
reducer_name.deref().to_case(Case::Pascal) + "CallbackId"
}

fn reducer_module_name(reducer_name: &Identifier) -> String {
reducer_name.deref().to_case(Case::Snake) + "_reducer"
}
Expand All @@ -760,6 +821,10 @@ fn reducer_function_name(reducer: &ReducerDef) -> String {
reducer.name.deref().to_case(Case::Snake)
}

fn reducer_flags_trait_name(reducer: &ReducerDef) -> String {
format!("set_flags_for_{}", reducer_function_name(reducer))
}

/// Iterate over all of the Rust `mod`s for types, reducers and tables in the `module`.
fn iter_module_names(module: &ModuleDef) -> impl Iterator<Item = String> + '_ {
itertools::chain!(
Expand All @@ -776,10 +841,31 @@ fn print_module_decls(module: &ModuleDef, out: &mut Indenter) {
}
}

/// Print `pub use *` declarations for all the files that will be generated for `items`.
/// Print appropriate reexports for all the files that will be generated for `items`.
fn print_module_reexports(module: &ModuleDef, out: &mut Indenter) {
for module_name in iter_module_names(module) {
writeln!(out, "pub use {module_name}::*;");
for ty in module.types().sorted_by_key(|ty| &ty.name) {
let mod_name = type_module_name(&ty.name);
let type_name = collect_case(Case::Pascal, ty.name.name_segments());
writeln!(out, "pub use {mod_name}::{type_name};")
}
for table in iter_tables(module) {
let mod_name = table_module_name(&table.name);
// TODO: More precise reexport: we want:
// - The trait name.
// - The insert, delete and possibly update callback ids.
// We do not want:
// - The table handle.
writeln!(out, "pub use {mod_name}::*;");
}
for reducer in iter_reducers(module) {
let mod_name = reducer_module_name(&reducer.name);
let reducer_trait_name = reducer_function_name(reducer);
let flags_trait_name = reducer_flags_trait_name(reducer);
let callback_id_name = reducer_callback_id_name(&reducer.name);
writeln!(
out,
"pub use {mod_name}::{{{reducer_trait_name}, {flags_trait_name}, {callback_id_name}}};"
);
}
}

Expand Down Expand Up @@ -814,7 +900,9 @@ fn iter_unique_cols<'a>(
}

fn print_reducer_enum_defn(module: &ModuleDef, out: &mut Indenter) {
print_enum_derives(out);
// Don't derive ser/de on this enum;
// it's not a proper SATS enum and the derive will fail.
writeln!(out, "#[derive(Clone, PartialEq, Debug)]");
writeln!(
out,
"
Expand All @@ -828,13 +916,15 @@ fn print_reducer_enum_defn(module: &ModuleDef, out: &mut Indenter) {
"pub enum Reducer {",
|out| {
for reducer in iter_reducers(module) {
writeln!(
out,
"{}({}::{}),",
reducer_variant_name(&reducer.name),
reducer_module_name(&reducer.name),
reducer_args_type_name(&reducer.name),
);
write!(out, "{} ", reducer_variant_name(&reducer.name));
if !reducer.params_for_generate.elements.is_empty() {
// If the reducer has any arguments, generate a "struct variant,"
// like `Foo { bar: Baz, }`.
// If it doesn't, generate a "unit variant" instead,
// like `Foo,`.
write_struct_type_fields_in_braces(module, out, &reducer.params_for_generate.elements, false);
}
writeln!(out, ",");
}
},
"}\n",
Expand All @@ -859,27 +949,17 @@ impl __sdk::InModule for Reducer {{
"match self {",
|out| {
for reducer in iter_reducers(module) {
writeln!(
out,
"Reducer::{}(_) => {:?},",
reducer_variant_name(&reducer.name),
reducer.name.deref(),
);
}
},
"}\n",
);
},
"}\n",
);
out.delimited_block(
"fn reducer_args(&self) -> &dyn std::any::Any {",
|out| {
out.delimited_block(
"match self {",
|out| {
for reducer in iter_reducers(module) {
writeln!(out, "Reducer::{}(args) => args,", reducer_variant_name(&reducer.name));
write!(out, "Reducer::{}", reducer_variant_name(&reducer.name));
if !reducer.params_for_generate.elements.is_empty() {
// Because we're emitting unit variants when the payload is empty,
// we will emit different patterns for empty vs non-empty variants.
// This is not strictly required;
// Rust allows matching a struct-like pattern
// against a unit-like enum variant,
// but we prefer the clarity of not including the braces for unit variants.
write!(out, " {{ .. }}");
}
writeln!(out, " => {:?},", reducer.name.deref());
}
},
"}\n",
Expand All @@ -895,6 +975,21 @@ impl __sdk::InModule for Reducer {{
"impl TryFrom<__ws::ReducerCallInfo<__ws::BsatnFormat>> for Reducer {",
|out| {
writeln!(out, "type Error = __anyhow::Error;");
// We define an "args struct" for each reducer in `generate_reducer`.
// This is not user-facing, and is not exported past the "root" `mod.rs`;
// it is an internal helper for serialization and deserialization.
// We actually want to ser/de instances of `enum Reducer`, but:
//
// - `Reducer` will have struct-like variants, which SATS ser/de does not support.
// - The WS format does not contain a BSATN-serialized `Reducer` instance;
// it holds the reducer name or ID separately from the argument bytes.
// We could work up some magic with `DeserializeSeed`
// and/or custom `Serializer` and `Deserializer` types
// to account for this, but it's much easier to just use an intermediate struct per reducer.
//
// As such, we deserialize from the `value.args` bytes into that "args struct,"
// then convert it into a `Reducer` variant via `Into::into`,
// which we also implement in `generate_reducer`.
out.delimited_block(
"fn try_from(value: __ws::ReducerCallInfo<__ws::BsatnFormat>) -> __anyhow::Result<Self> {",
|out| {
Expand All @@ -904,9 +999,10 @@ impl __sdk::InModule for Reducer {{
for reducer in iter_reducers(module) {
writeln!(
out,
"{:?} => Ok(Reducer::{}(__sdk::parse_reducer_args({:?}, &value.args)?)),",
"{:?} => Ok(__sdk::parse_reducer_args::<{}::{}>({:?}, &value.args)?.into()),",
reducer.name.deref(),
reducer_variant_name(&reducer.name),
reducer_module_name(&reducer.name),
reducer_args_type_name(&reducer.name),
reducer.name.deref(),
);
}
Expand Down
Loading

2 comments on commit d9de1e3

@github-actions
Copy link

@github-actions github-actions bot commented on d9de1e3 Dec 4, 2024

Choose a reason for hiding this comment

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

Benchmarking failed. Please check the workflow run for details.

@github-actions
Copy link

@github-actions github-actions bot commented on d9de1e3 Dec 4, 2024

Choose a reason for hiding this comment

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

Callgrind benchmark results

Callgrind Benchmark Report

These benchmarks were run using callgrind,
an instruction-level profiler. They allow comparisons between sqlite (sqlite), SpacetimeDB running through a module (stdb_module), and the underlying SpacetimeDB data storage engine (stdb_raw). Callgrind emulates a CPU to collect the below estimates.

Measurement changes larger than five percent are in bold.

In-memory benchmarks

callgrind: empty transaction

db total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw 6396 6396 0.00% 6532 6532 0.00%
sqlite 5609 5609 0.00% 6015 6015 0.00%

callgrind: filter

db schema indices count preload _column data_type total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str no_index 64 128 1 u64 74672 74672 0.00% 75420 75396 0.03%
stdb_raw u32_u64_str no_index 64 128 2 string 116914 116914 0.00% 117940 117940 0.00%
stdb_raw u32_u64_str btree_each_column 64 128 2 string 25081 25080 0.00% 25771 25750 0.08%
stdb_raw u32_u64_str btree_each_column 64 128 1 u64 24048 24048 0.00% 24776 24760 0.06%
sqlite u32_u64_str no_index 64 128 2 string 144415 144415 0.00% 145837 145841 -0.00%
sqlite u32_u64_str no_index 64 128 1 u64 123763 123781 -0.01% 124907 124929 -0.02%
sqlite u32_u64_str btree_each_column 64 128 1 u64 131080 131080 0.00% 132504 132508 -0.00%
sqlite u32_u64_str btree_each_column 64 128 2 string 134222 134222 0.00% 135832 135832 0.00%

callgrind: insert bulk

db schema indices count preload total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str unique_0 64 128 881594 880318 0.14% 927476 926372 0.12%
stdb_raw u32_u64_str btree_each_column 64 128 1029361 1031689 -0.23% 1053101 1055601 -0.24%
sqlite u32_u64_str unique_0 64 128 399366 399360 0.00% 418334 418328 0.00%
sqlite u32_u64_str btree_each_column 64 128 984611 984611 0.00% 1023405 1023425 -0.00%

callgrind: iterate

db schema indices count total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str unique_0 1024 138372 138372 0.00% 138482 138482 0.00%
stdb_raw u32_u64_str unique_0 64 15797 15797 0.00% 15915 15887 0.18%
sqlite u32_u64_str unique_0 1024 1042718 1042718 0.00% 1046038 1046038 0.00%
sqlite u32_u64_str unique_0 64 74710 74710 0.00% 75806 75806 0.00%

callgrind: serialize_product_value

count format total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
64 json 47528 47528 0.00% 50282 50282 0.00%
64 bsatn 25509 25509 0.00% 27719 27719 0.00%
16 bsatn 8200 8200 0.00% 9560 9560 0.00%
16 json 12188 12188 0.00% 14160 14160 0.00%

callgrind: update bulk

db schema indices count preload total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str unique_0 1024 1024 20130299 20546233 -2.02% 20588343 21058751 -2.23%
stdb_raw u32_u64_str unique_0 64 128 1287061 1286934 0.01% 1320957 1320806 0.01%
sqlite u32_u64_str unique_0 1024 1024 1802137 1802137 0.00% 1811273 1811269 0.00%
sqlite u32_u64_str unique_0 64 128 128540 128540 0.00% 131336 131336 0.00%
On-disk benchmarks

callgrind: empty transaction

db total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw 6401 6401 0.00% 6549 6549 0.00%
sqlite 5651 5651 0.00% 6091 6091 0.00%

callgrind: filter

db schema indices count preload _column data_type total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str no_index 64 128 1 u64 74677 74677 0.00% 75349 75333 0.02%
stdb_raw u32_u64_str no_index 64 128 2 string 116919 116919 0.00% 117861 117837 0.02%
stdb_raw u32_u64_str btree_each_column 64 128 2 string 25085 25086 -0.00% 25743 25720 0.09%
stdb_raw u32_u64_str btree_each_column 64 128 1 u64 24053 24053 0.00% 24709 24729 -0.08%
sqlite u32_u64_str no_index 64 128 1 u64 125684 125684 0.00% 127200 127204 -0.00%
sqlite u32_u64_str no_index 64 128 2 string 146336 146336 0.00% 148026 148034 -0.01%
sqlite u32_u64_str btree_each_column 64 128 2 string 136418 136418 0.00% 138444 138440 0.00%
sqlite u32_u64_str btree_each_column 64 128 1 u64 133176 133176 0.00% 134988 134972 0.01%

callgrind: insert bulk

db schema indices count preload total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str unique_0 64 128 830143 829664 0.06% 875167 843998 3.69%
stdb_raw u32_u64_str btree_each_column 64 128 980922 977602 0.34% 1037274 1002446 3.47%
sqlite u32_u64_str unique_0 64 128 416914 416914 0.00% 435364 435364 0.00%
sqlite u32_u64_str btree_each_column 64 128 1023158 1023158 0.00% 1062324 1062324 0.00%

callgrind: iterate

db schema indices count total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str unique_0 1024 138377 138377 0.00% 138467 138463 0.00%
stdb_raw u32_u64_str unique_0 64 15802 15802 0.00% 15892 15888 0.03%
sqlite u32_u64_str unique_0 1024 1045796 1045796 0.00% 1049528 1049528 0.00%
sqlite u32_u64_str unique_0 64 76486 76492 -0.01% 77830 77836 -0.01%

callgrind: serialize_product_value

count format total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
64 json 47528 47528 0.00% 50282 50282 0.00%
64 bsatn 25509 25509 0.00% 27719 27719 0.00%
16 bsatn 8200 8200 0.00% 9560 9560 0.00%
16 json 12188 12188 0.00% 14160 14160 0.00%

callgrind: update bulk

db schema indices count preload total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str unique_0 1024 1024 19059120 19055734 0.02% 19627540 19617292 0.05%
stdb_raw u32_u64_str unique_0 64 128 1240513 1240826 -0.03% 1306115 1306520 -0.03%
sqlite u32_u64_str unique_0 1024 1024 1809791 1809785 0.00% 1818339 1818333 0.00%
sqlite u32_u64_str unique_0 64 128 132687 132687 0.00% 135643 135643 0.00%

Please sign in to comment.