Skip to content
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

Hot reloading updating wrong part inside a conditional expression #1995

Closed
3 tasks
ochrons opened this issue Mar 1, 2024 · 14 comments · Fixed by #2055
Closed
3 tasks

Hot reloading updating wrong part inside a conditional expression #1995

ochrons opened this issue Mar 1, 2024 · 14 comments · Fixed by #2055
Assignees
Labels
bug Something isn't working cli Related to the dioxus-cli program hot-reload Related to the hot reload crate

Comments

@ochrons
Copy link

ochrons commented Mar 1, 2024

Problem

When making changes in a component that has a match expression, hot reload only ever updates the last expression even when changes are made in other parts.

Steps To Reproduce

I have a following component

#[component]
pub fn RecipeView(id: String) -> Element {
    let recipe = use_server_future(move || {
        get_recipe(id.clone())
    })?;

    match recipe.read().as_ref() {
        Some(Ok(recipe)) => {
            rsx! {
                div { class: "flex flex-col justify-center overflow-hidden py-6",
                    div { class: "mx-auto max-w-2xl",
                        h1 { class: "text-5xl text-center font-extrabold py-3", "{recipe.name}" }
                        p { class: "text-4xl py-3", "{recipe.description}" }
                        p { "Another static text with more data" }
                    }
                }
            }
        }
        Some(Err(_)) => {
            rsx! {"Error loading recipe"}
        }
        None => {
            rsx! {"Loading..."}
        }
    }
}

Serving the app with dx serve --hot-reload

Make a change in one of the class definitions or in static text.

Monitor the messages in the hot_reload websocket.

See that it always sends a message to update the "Loading..." text, regardless of what was actually changed.

{"name":"src/components/recipe.rs:31:13:0","roots":[{"type":"Text","text":"Loading..."}],"node_paths":[],"attr_paths":[]}

Expected behavior

It should send a message to update the affected part of the page.

Screenshots

If applicable, add screenshots to help explain your problem.

Environment:

  • Dioxus version: 0.5.0-alpha.0
  • Rust version: 1.76
  • OS info: Windows 11, WSL2
  • App platform: fullstack web

Questionnaire

  • I'm interested in fixing this myself but don't know where to start
  • I would like to fix and I have a solution
  • I don't have time to fix this right now, but maybe later
@ochrons
Copy link
Author

ochrons commented Mar 1, 2024

To be more exact the version I'm using is

dioxus = { git = "https://github.com/ealmloff/dioxus.git", branch = "fix-fullstack-history"

since routing was broken in 0.5.0 alpha.

@ochrons
Copy link
Author

ochrons commented Mar 1, 2024

Also would be great to have more debug logging in Dioxus, to better see what's going on under the hood when something like this happens.

@ochrons
Copy link
Author

ochrons commented Mar 1, 2024

It's always the last rsx expression that gets sent over the websocket when anything changes. So changing the order of match clauses will change the behavior.

@ealmloff ealmloff added bug Something isn't working cli Related to the dioxus-cli program hot-reload Related to the hot reload crate labels Mar 1, 2024
@ealmloff
Copy link
Member

ealmloff commented Mar 1, 2024

If you set DIOXUS_LOG="trace", the CLI will log some information about what it thought changed between the new and old versions of the code.

Some part of the code diffing rsx here may be broken:

(syn::Expr::Match(new_expr), syn::Expr::Match(old_expr)) => {
if find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls) {
return true;
}
for (new_arm, old_arm) in new_expr.arms.iter().zip(old_expr.arms.iter()) {
match (&new_arm.guard, &old_arm.guard) {
(Some((new_tok, new_expr)), Some((old_tok, old_expr))) => {
if find_rsx_expr(new_expr, old_expr, rsx_calls) || new_tok != old_tok {
return true;
}
}
(None, None) => (),
_ => return true,
}
if find_rsx_expr(&new_arm.body, &old_arm.body, rsx_calls)
|| new_arm.attrs != old_arm.attrs
|| new_arm.pat != old_arm.pat
|| new_arm.fat_arrow_token != old_arm.fat_arrow_token
|| new_arm.comma != old_arm.comma
{
return true;
}
}
new_expr.attrs != old_expr.attrs
|| new_expr.match_token != old_expr.match_token
|| new_expr.brace_token != old_expr.brace_token
}

I was unable to reproduce this issue with the CLI version installed from the latest commit on the main branch (9ae3d14) and the fix-fullstack-history branch of dioxus on either the web platform or the fullstack platform on MacOs with this code:

use dioxus::prelude::*;

fn main() {
    launch(app);
}

fn app() -> Element {
    rsx! {
        RecipeView { id: "1".to_string() }
    }
}

#[component]
pub fn RecipeView(id: String) -> Element {
    let recipe: Option<Result<usize, usize>> = Some(Ok(1));
    match recipe {
        Some(Ok(recipe)) => {
            rsx! {
                div { class: "flex flex-col justify-center overflow-hidden py-6",
                    div { class: "mx-auto max-w-2xl",
                        h1 { class: "text-5xl text-center font-extrabold py-3", "{recipe}" }
                        p { class: "text-4xl py-3", "{recipe}" }
                        p { "Another static text with more data" }
                    }
                }
            }
        }
        Some(Err(_)) => {
            rsx! {"Error loading recipe"}
        }
        None => {
            rsx! {"Loading..."}
        }
    }
}

@ochrons
Copy link
Author

ochrons commented Mar 1, 2024

Maybe in your code example the compiler optimizes the other cases away, since you have a static value 😊

If I try with DIOXUS_LOG in trace level, I run into another problem which is the hot_reload websocket getting closed early. Also I get this flood of continuous log messages in the terminal that never stop. Keeps the CPU busy

[TRACE] dfs_in_order: visit_instr(Store(Store { memory: Id { idx: 0 }, kind: I32 { atomic: false }, arg: MemArg { align: 4, offset: 4 } }))
[TRACE] dfs_in_order: (Store(Store { memory: Id { idx: 0 }, kind: I32 { atomic: false }, arg: MemArg { align: 4, offset: 4 } })).visit(..)
[TRACE] dfs_in_order: visit_instr(LocalGet(LocalGet { local: Id { idx: 312841 } }))
[TRACE] dfs_in_order: (LocalGet(LocalGet { local: Id { idx: 312841 } })).visit(..)
[TRACE] dfs_in_order: visit_instr(Load(Load { memory: Id { idx: 0 }, kind: I32 { atomic: false }, arg: MemArg { align: 4, offset: 4 } }))
[TRACE] dfs_in_order: (Load(Load { memory: Id { idx: 0 }, kind: I32 { atomic: false }, arg: MemArg { align: 4, offset: 4 } })).visit(..)
[TRACE] dfs_in_order: visit_instr(LocalSet(LocalSet { local: Id { idx: 312856 } }))
[TRACE] dfs_in_order: (LocalSet(LocalSet { local: Id { idx: 312856 } })).visit(..)
[TRACE] dfs_in_order: visit_instr(Const(Const { value: I32(32) }))
[TRACE] dfs_in_order: (Const(Const { value: I32(32) })).visit(..)

@ochrons
Copy link
Author

ochrons commented Mar 1, 2024

Actually it seems like those two cases don't ever get to produce a template. When I force a long delay on the server call, I can see in the browser inspector that the component contents for None case is never inserted into the DOM. It just has some <pre hidden></pre> placeholder there.

@ochrons
Copy link
Author

ochrons commented Mar 1, 2024

Another weird thing I'm seeing in the logs is the execution of part of the server function multiple times.

[INFO] dioxus_fullstack::render - Suspense resolved
get_recipe: done
[INFO] dioxus_core::diff::node - creating template self=VNode { vnode: VNodeInner { key: None, template: Cell { value: Template { name: "src/components/recipe.rs:19:13:2484", roots: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "flex flex-col justify-center overflow-hidden py-6", namespace: None }], children: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "mx-auto max-w-2xl", namespace: None }], children: [Element { tag: "h1", namespace: None, attrs: [Static { name: "class", value: "text-5xl text-center font-extrabold py-3", namespace: None }], children: [DynamicText { id: 0 }] }, Element { tag: "p", namespace: None, attrs: [Static { name: "class", value: "text-xl py-3", namespace: None }], children: [DynamicText { id: 1 }] }, Element { tag: "p", namespace: None, attrs: [], children: [Text { text: "Another static text with more datas" }] }] }] }], node_paths: [[0, 0, 0, 0], [0, 0, 1, 0]], attr_paths: [] } }, dynamic_nodes: [Text(VText { value: "Test Recipe" }), Text(VText { value: "This is a test recipe" })], dynamic_attrs: [] }, mount: Cell { value: MountId(4) } } mount=MountId(4)
[INFO] dioxus_fullstack::render - Suspense resolved
spinning up hot reloading
hot reloading ready
🔥 Hot Reload WebSocket connected
Connected to hot reloading 🚀
🔮 Finding updates since last compile...
finished
get_recipe: done
[INFO] dioxus_core::diff::node - creating template self=VNode { vnode: VNodeInner { key: None, template: Cell { value: Template { name: "src/components/recipe.rs:19:13:2484", roots: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "flex flex-col justify-center overflow-hidden py-6", namespace: None }], children: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "mx-auto max-w-2xl", namespace: None }], children: [Element { tag: "h1", namespace: None, attrs: [Static { name: "class", value: "text-5xl text-center font-extrabold py-3", namespace: None }], children: [DynamicText { id: 0 }] }, Element { tag: "p", namespace: None, attrs: [Static { name: "class", value: "text-xl py-3", namespace: None }], children: [DynamicText { id: 1 }] }, Element { tag: "p", namespace: None, attrs: [], children: [Text { text: "Another static text with more datas" }] }] }] }], node_paths: [[0, 0, 0, 0], [0, 0, 1, 0]], attr_paths: [] } }, dynamic_nodes: [Text(VText { value: "Test Recipe" }), Text(VText { value: "This is a test recipe" })], dynamic_attrs: [] }, mount: Cell { value: MountId(4) } } mount=MountId(4)
[INFO] dioxus_fullstack::render - Suspense resolved
get_recipe: done
[INFO] dioxus_core::diff::node - creating template self=VNode { vnode: VNodeInner { key: None, template: Cell { value: Template { name: "src/components/recipe.rs:19:13:2484", roots: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "flex flex-col justify-center overflow-hidden py-6", namespace: None }], children: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "mx-auto max-w-2xl", namespace: None }], children: [Element { tag: "h1", namespace: None, attrs: [Static { name: "class", value: "text-5xl text-center font-extrabold py-3", namespace: None }], children: [DynamicText { id: 0 }] }, Element { tag: "p", namespace: None, attrs: [Static { name: "class", value: "text-xl py-3", namespace: None }], children: [DynamicText { id: 1 }] }, Element { tag: "p", namespace: None, attrs: [], children: [Text { text: "Another static text with more datas" }] }] }] }], node_paths: [[0, 0, 0, 0], [0, 0, 1, 0]], attr_paths: [] } }, dynamic_nodes: [Text(VText { value: "Test Recipe" }), Text(VText { value: "This is a test recipe" })], dynamic_attrs: [] }, mount: Cell { value: MountId(4) } } mount=MountId(4)
[INFO] dioxus_fullstack::render - Suspense resolved
get_recipe: done
[INFO] dioxus_core::diff::node - creating template self=VNode { vnode: VNodeInner { key: None, template: Cell { value: Template { name: "src/components/recipe.rs:19:13:2484", roots: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "flex flex-col justify-center overflow-hidden py-6", namespace: None }], children: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "mx-auto max-w-2xl", namespace: None }], children: [Element { tag: "h1", namespace: None, attrs: [Static { name: "class", value: "text-5xl text-center font-extrabold py-3", namespace: None }], children: [DynamicText { id: 0 }] }, Element { tag: "p", namespace: None, attrs: [Static { name: "class", value: "text-xl py-3", namespace: None }], children: [DynamicText { id: 1 }] }, Element { tag: "p", namespace: None, attrs: [], children: [Text { text: "Another static text with more datas" }] }] }] }], node_paths: [[0, 0, 0, 0], [0, 0, 1, 0]], attr_paths: [] } }, dynamic_nodes: [Text(VText { value: "Test Recipe" }), Text(VText { value: "This is a test recipe" })], dynamic_attrs: [] }, mount: Cell { value: MountId(4) } } mount=MountId(4)
[INFO] dioxus_fullstack::render - Suspense resolved
get_recipe: done
[INFO] dioxus_core::diff::node - creating template self=VNode { vnode: VNodeInner { key: None, template: Cell { value: Template { name: "src/components/recipe.rs:19:13:2484", roots: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "flex flex-col justify-center overflow-hidden py-6", namespace: None }], children: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "mx-auto max-w-2xl", namespace: None }], children: [Element { tag: "h1", namespace: None, attrs: [Static { name: "class", value: "text-5xl text-center font-extrabold py-3", namespace: None }], children: [DynamicText { id: 0 }] }, Element { tag: "p", namespace: None, attrs: [Static { name: "class", value: "text-xl py-3", namespace: None }], children: [DynamicText { id: 1 }] }, Element { tag: "p", namespace: None, attrs: [], children: [Text { text: "Another static text with more datas" }] }] }] }], node_paths: [[0, 0, 0, 0], [0, 0, 1, 0]], attr_paths: [] } }, dynamic_nodes: [Text(VText { value: "Test Recipe" }), Text(VText { value: "This is a test recipe" })], dynamic_attrs: [] }, mount: Cell { value: MountId(4) } } mount=MountId(4)
[INFO] dioxus_fullstack::render - Suspense resolved

So the code after the sleep.await gets called five times, seems like once a second when observing.

#[server]
pub async fn get_recipe(id: String) -> Result<Recipe, ServerFnError> {
    println!("get_recipe: {}", id);
    use tokio::time::{sleep, Duration};
    sleep(Duration::from_millis(5000)).await;
    println!("get_recipe: done");
    Ok(Recipe {
        id,
        name: "Test Recipe".to_string(),
        description: "This is a test recipe".to_string(),
        ingredients: vec![],
        steps: vec![],
    })
}

@ochrons
Copy link
Author

ochrons commented Mar 1, 2024

BTW didn't see those repeating get_recipe: done with the CLI version (9ae3d14) but still the same hot reloading behavior.

@ochrons
Copy link
Author

ochrons commented Mar 1, 2024

Actually it seems like those two cases don't ever get to produce a template. When I force a long delay on the server call, I can see in the browser inspector that the component contents for None case is never inserted into the DOM. It just has some <pre hidden>\</pre\> placeholder there.

The reason for this was the use_server_future. With use_resource it works as expected. Although IMO the behavior should not be like that with the server future when dynamically switching components, instead of doing a full SSR of the page on initial load.

@ealmloff
Copy link
Member

ealmloff commented Mar 1, 2024

use_server_future suspends the component here:

let recipe = use_server_future(move || {
    get_recipe(id.clone())
})?;

The ? will return None (and render a placeholder) when the future is resolving. We don't do a full reload of the page because it generally won't make the future resolve any faster and it would reset the state in your page.

Suspense boundaries are not implemented yet, but once they are you will be able to handle rendering a placeholder higher up in your component.

@ealmloff
Copy link
Member

ealmloff commented Mar 1, 2024

Another weird thing I'm seeing in the logs is the execution of part of the server function multiple times.

[INFO] dioxus_fullstack::render - Suspense resolved
get_recipe: done
[INFO] dioxus_core::diff::node - creating template self=VNode { vnode: VNodeInner { key: None, template: Cell { value: Template { name: "src/components/recipe.rs:19:13:2484", roots: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "flex flex-col justify-center overflow-hidden py-6", namespace: None }], children: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "mx-auto max-w-2xl", namespace: None }], children: [Element { tag: "h1", namespace: None, attrs: [Static { name: "class", value: "text-5xl text-center font-extrabold py-3", namespace: None }], children: [DynamicText { id: 0 }] }, Element { tag: "p", namespace: None, attrs: [Static { name: "class", value: "text-xl py-3", namespace: None }], children: [DynamicText { id: 1 }] }, Element { tag: "p", namespace: None, attrs: [], children: [Text { text: "Another static text with more datas" }] }] }] }], node_paths: [[0, 0, 0, 0], [0, 0, 1, 0]], attr_paths: [] } }, dynamic_nodes: [Text(VText { value: "Test Recipe" }), Text(VText { value: "This is a test recipe" })], dynamic_attrs: [] }, mount: Cell { value: MountId(4) } } mount=MountId(4)
[INFO] dioxus_fullstack::render - Suspense resolved
spinning up hot reloading
hot reloading ready
🔥 Hot Reload WebSocket connected
Connected to hot reloading 🚀
🔮 Finding updates since last compile...
finished
get_recipe: done
[INFO] dioxus_core::diff::node - creating template self=VNode { vnode: VNodeInner { key: None, template: Cell { value: Template { name: "src/components/recipe.rs:19:13:2484", roots: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "flex flex-col justify-center overflow-hidden py-6", namespace: None }], children: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "mx-auto max-w-2xl", namespace: None }], children: [Element { tag: "h1", namespace: None, attrs: [Static { name: "class", value: "text-5xl text-center font-extrabold py-3", namespace: None }], children: [DynamicText { id: 0 }] }, Element { tag: "p", namespace: None, attrs: [Static { name: "class", value: "text-xl py-3", namespace: None }], children: [DynamicText { id: 1 }] }, Element { tag: "p", namespace: None, attrs: [], children: [Text { text: "Another static text with more datas" }] }] }] }], node_paths: [[0, 0, 0, 0], [0, 0, 1, 0]], attr_paths: [] } }, dynamic_nodes: [Text(VText { value: "Test Recipe" }), Text(VText { value: "This is a test recipe" })], dynamic_attrs: [] }, mount: Cell { value: MountId(4) } } mount=MountId(4)
[INFO] dioxus_fullstack::render - Suspense resolved
get_recipe: done
[INFO] dioxus_core::diff::node - creating template self=VNode { vnode: VNodeInner { key: None, template: Cell { value: Template { name: "src/components/recipe.rs:19:13:2484", roots: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "flex flex-col justify-center overflow-hidden py-6", namespace: None }], children: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "mx-auto max-w-2xl", namespace: None }], children: [Element { tag: "h1", namespace: None, attrs: [Static { name: "class", value: "text-5xl text-center font-extrabold py-3", namespace: None }], children: [DynamicText { id: 0 }] }, Element { tag: "p", namespace: None, attrs: [Static { name: "class", value: "text-xl py-3", namespace: None }], children: [DynamicText { id: 1 }] }, Element { tag: "p", namespace: None, attrs: [], children: [Text { text: "Another static text with more datas" }] }] }] }], node_paths: [[0, 0, 0, 0], [0, 0, 1, 0]], attr_paths: [] } }, dynamic_nodes: [Text(VText { value: "Test Recipe" }), Text(VText { value: "This is a test recipe" })], dynamic_attrs: [] }, mount: Cell { value: MountId(4) } } mount=MountId(4)
[INFO] dioxus_fullstack::render - Suspense resolved
get_recipe: done
[INFO] dioxus_core::diff::node - creating template self=VNode { vnode: VNodeInner { key: None, template: Cell { value: Template { name: "src/components/recipe.rs:19:13:2484", roots: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "flex flex-col justify-center overflow-hidden py-6", namespace: None }], children: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "mx-auto max-w-2xl", namespace: None }], children: [Element { tag: "h1", namespace: None, attrs: [Static { name: "class", value: "text-5xl text-center font-extrabold py-3", namespace: None }], children: [DynamicText { id: 0 }] }, Element { tag: "p", namespace: None, attrs: [Static { name: "class", value: "text-xl py-3", namespace: None }], children: [DynamicText { id: 1 }] }, Element { tag: "p", namespace: None, attrs: [], children: [Text { text: "Another static text with more datas" }] }] }] }], node_paths: [[0, 0, 0, 0], [0, 0, 1, 0]], attr_paths: [] } }, dynamic_nodes: [Text(VText { value: "Test Recipe" }), Text(VText { value: "This is a test recipe" })], dynamic_attrs: [] }, mount: Cell { value: MountId(4) } } mount=MountId(4)
[INFO] dioxus_fullstack::render - Suspense resolved
get_recipe: done
[INFO] dioxus_core::diff::node - creating template self=VNode { vnode: VNodeInner { key: None, template: Cell { value: Template { name: "src/components/recipe.rs:19:13:2484", roots: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "flex flex-col justify-center overflow-hidden py-6", namespace: None }], children: [Element { tag: "div", namespace: None, attrs: [Static { name: "class", value: "mx-auto max-w-2xl", namespace: None }], children: [Element { tag: "h1", namespace: None, attrs: [Static { name: "class", value: "text-5xl text-center font-extrabold py-3", namespace: None }], children: [DynamicText { id: 0 }] }, Element { tag: "p", namespace: None, attrs: [Static { name: "class", value: "text-xl py-3", namespace: None }], children: [DynamicText { id: 1 }] }, Element { tag: "p", namespace: None, attrs: [], children: [Text { text: "Another static text with more datas" }] }] }] }], node_paths: [[0, 0, 0, 0], [0, 0, 1, 0]], attr_paths: [] } }, dynamic_nodes: [Text(VText { value: "Test Recipe" }), Text(VText { value: "This is a test recipe" })], dynamic_attrs: [] }, mount: Cell { value: MountId(4) } } mount=MountId(4)
[INFO] dioxus_fullstack::render - Suspense resolved

So the code after the sleep.await gets called five times, seems like once a second when observing.

#[server]
pub async fn get_recipe(id: String) -> Result<Recipe, ServerFnError> {
    println!("get_recipe: {}", id);
    use tokio::time::{sleep, Duration};
    sleep(Duration::from_millis(5000)).await;
    println!("get_recipe: done");
    Ok(Recipe {
        id,
        name: "Test Recipe".to_string(),
        description: "This is a test recipe".to_string(),
        ingredients: vec![],
        steps: vec![],
    })
}

Those logs will happen whenever a client makes a request to your server and the server renders the page. If you reload your page 5 times, you should see 5 logs. You may also more logs if an asset is trying to load that doesn't exist and you are rendering a page on a 404 route

@ochrons
Copy link
Author

ochrons commented Mar 1, 2024

But I see in the logs only the get_recipe: done message, not the first get_recipe: 1 message, which only occurs once. Couldn't really get this to happen again, so might have been some fluke as well, or some nasty timing issue.

@jkelleyrtp jkelleyrtp added this to the 0.5.0: Signals milestone Mar 7, 2024
@jkelleyrtp
Copy link
Member

I think this is a race condition on how we register templates. Templates are given a template ID but that ID assignment can vary depending on the order in which we process them. If the template doesn't exist on the client when we handle its edits, we have nothing to direct those edits to. Hence why you only see edits for the actually loaded block of rsx.

This needs to be solved with more resilient handling of edits queued for nonexistent templates.

@jkelleyrtp
Copy link
Member

Tracked it down - our implementation was only storing the most recent change and a max of 1 template per file. This is being fixed in #2055 along with a few other issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working cli Related to the dioxus-cli program hot-reload Related to the hot reload crate
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants