Skip to content

refactor: use gstd::message_loop only for async ctors and service methods #939

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: master
Choose a base branch
from

Conversation

techraed
Copy link
Member

@techraed techraed commented May 28, 2025

Resolves #63

The PR suggests not using async runtime from the gstd by default for any message sent to program. The separation between calls that require async runtime and sync execution was benched (see comments below) and it was concluded that the separation saves 100-200kk of gas. The changes in code generation are the following:

  1. InvocationIo trait now has ASYNC constant. service and program macros define the ASYNC value for each constructor and service method that implements InvocationIo.
  2. Service exposure now implements check_asyncness method that accepts encoded service method route and params and returns whether the method is async. Basically, returns ASYNC const of the InvocationIo implementor.
  3. service macro now generates 2 try_handle methods: try_handle and try_handle_async. The former invokes sync method, and the latter - async ones. Methods also call try_handle* methods of base services.
  4. Commands and Queries has ASYNC const in their impl items. By default, the ASYNC is false. If there are any commands and queries, their InvocationIo::ASYNC values will be used.
  5. ServiceMeta trait now also has ASYNC constant. The value is generated from Commands::ASYNC, Queries::ASYNC and ASYNC values of base services, which also implement the ServiceMeta trait.
  6. ConstructorsMeta now also has ASYNC constant in its impl item. The value of the constant is defined by InvocationIo::ASYNC of constructors (params structs).
  7. program macro never uses gstd::async_init or gstd::async_main. Instead generates plain init, handle, handle_reply and handle_signal.
  8. init method invokes constructors. If a constructor is an async, then gstd::message_loop will be used. Otherwise a simple sync constructor call will be generated.
  9. ProgramMeta trait now also has ASYNC constant. It's value is defined from ConstructorsMeta::ASYNC and ServiceMeta::ASYNC of programs services. All in all it's a logical operation where each operand is an ASYNC value for each constructor and service method (params struct)
  10. handle_signal is always generated except for cases when ethexe feature is on
  11. handle_reply is always generated. Both in handle_reply and handle_signal hooks from gstd are called only if ProgramMeta::ASYNC value is true. The latter means that there's somewhere in program gstd::message_loop is used for async methods execution. So due to the fact that async methods are from gstd and the wait/wake logic is already handled by hooks in gstd, these hooks must be used in handle_reply and handle_signal to implement proper async/await experience for developers.
  12. For ethexe feature there are several corresponding changes:
    • try_handle_solidity and try_handle_solidity_async are also implemented pretty same as try_handle*
    • match_ctor_solidity is now a sync function that in case the constructor is async, invokes it inside gstd::message_loop.

TODO:

  1. C̶h̶e̶c̶k̶ ̶c̶r̶o̶s̶s̶-̶p̶r̶o̶g̶r̶a̶m̶ ̶c̶o̶m̶m̶u̶n̶i̶c̶a̶t̶i̶o̶n̶ ̶i̶n̶ ̶̶e̶x̶a̶m̶p̶l̶e̶s̶̶ ̶(̶a̶n̶d̶ ̶̶e̶t̶h̶e̶x̶e̶̶)̶ (UPD: done)

@techraed techraed self-assigned this May 28, 2025
@techraed
Copy link
Member Author

By the b3eb14 it was agreed to implement sync try_handle, because it gives about 200-300 million gas surplus. This was measured both using computational heavy/light tasks

@techraed techraed changed the title refactor: use sync version of try_handle if service has no async methods refactor: use gstd::message_loop only for async ctors and service methods Jun 11, 2025
syn = { workspace = true, features = ["full", "extra-traits"] }
tokio = { workspace = true, features = ["full"] }
trybuild = "1.0"
Copy link
Member

Choose a reason for hiding this comment

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

move to workspace deps, pls

@@ -84,6 +84,16 @@ pub fn unknown_input_panic(message: &str, input: &[u8]) -> ! {
pub trait InvocationIo {
const ROUTE: &'static [u8];
type Params: Decode;
const ASYNC: bool;

fn check_route(payload: impl AsRef<[u8]>) -> Result<()> {
Copy link
Member

Choose a reason for hiding this comment

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

better is_acync or check_asyncness returns Result<bool>

fn is_async(payload: impl AsRef<[u8]>) -> Result<bool> {
    let value = payload.as_ref();
    if !value.starts_with(Self::ROUTE) {
        return Err(Error::Rtl(RtlError::InvocationPrefixMismatches));
    }

    Ok(Self::ASYNC)
}

quote! {
<#path_wo_lifetimes as #sails_path::meta::ServiceMeta>::ASYNC
}
}));
Copy link
Member

Choose a reason for hiding this comment

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

we do not need to generate ASYNC in impl CommandsMeta, QueriesMeta
here we know exactly if we have any async fn
so, if yes - just set true
else - check base services

let is_any_async = self
    .service_handlers
    .iter()
    .any(|fn_builder| fn_builder.is_async());

let service_meta_asyncness = if is_any_async {
    quote!(true)
} else {
    if self.base_types.is_empty() {
        quote!(false)
    } else {
        let base_asyncness = self.base_types.iter().map(|base_type| {
            let path_wo_lifetimes = shared::remove_lifetimes(base_type);
            quote! {
                <#path_wo_lifetimes as #sails_path::meta::ServiceMeta>::ASYNC
            }
        });
        quote!(#( #base_asyncness )||*)
    }
};
const ASYNC: bool = #service_meta_asyncness;

@@ -49,10 +62,14 @@ impl ServiceBuilder<'_> {
(fn_builder.is_query()).then_some(fn_builder.handler_meta_variant())
});

let commands_asyncness = self.service_handler_asyncness(false);
let queries_asyncness = self.service_handler_asyncness(true);
Copy link
Member

Choose a reason for hiding this comment

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

not needed

@@ -62,12 +79,20 @@ impl ServiceBuilder<'_> {
#(#commands_meta_variants),*
}

impl CommandsMeta {
pub const ASYNC: bool = #commands_asyncness;
}
Copy link
Member

Choose a reason for hiding this comment

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

not needed

@@ -76,4 +101,23 @@ impl ServiceBuilder<'_> {
}
}
}

fn service_handler_asyncness(&self, is_query: bool) -> TokenStream {
Copy link
Member

Choose a reason for hiding this comment

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

not needed


impl ConstructorsMeta {
pub const ASYNC: bool = #( #ctors_asyncness )||*;
}
Copy link
Member

Choose a reason for hiding this comment

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

not needed

let mut invocation_dispatches = Vec::new();
let mut routes = BTreeMap::new();
// only used for ethexe
#[allow(unused_mut)]
let mut solidity_dispatchers: Vec<TokenStream2> = Vec::new();

meta_asyncness.push(quote!(meta_in_program::ConstructorsMeta::ASYNC));
Copy link
Member

Choose a reason for hiding this comment

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

here we know exactly if we have any async ctor
so, if yes - just set true
else - set from services

});

quote! {
pub fn check_asyncness(&self, #input_ident : &[u8]) -> Option<bool> {
Copy link
Member

Choose a reason for hiding this comment

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

To think about, maybe first check cosnt
<Service as ServiceMeta>::ASYNC
for the compiler to optimize the condition.

gstd::unknown_input_panic("Unknown request", input)
let Some(is_async) = service.check_asyncness(&input[#route_ident .len()..]) else {
gstd::unknown_input_panic("Unknown call", &input[#route_ident .len()..])
};
Copy link
Member

Choose a reason for hiding this comment

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

let is_async = service.check_asyncness(&input[#route_ident .len()..]).unwrap_or_else(|| gstd::unknown_input_panic("Unknown call", &input[#route_ident .len()..]));

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

Successfully merging this pull request may close these issues.

macros: Avoid generating async methods when there are no ones in service impl
2 participants