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

feat: Queue render state updates on a thread #182

Merged
merged 18 commits into from
Jan 19, 2024

Conversation

mofojed
Copy link
Member

@mofojed mofojed commented Dec 20, 2023

import deephaven.ui as ui
from deephaven.ui import use_state


@ui.component
def foo():
    a, set_a = use_state(0)
    b, set_b = use_state(0)

    print(f"Render with a {a} and b {b}")

    def handle_press():
        set_a(a + 1)
        set_b(b + 1)

    return ui.action_button(
        f"a is {a} and b is {b}", on_press=handle_press
    )


f = foo()
  • Ensured that the print out was only printed once per press of the button, and both values stayed the same
  • Allow a callable to be passed into the use_state setter function that takes the old value as a parameter. Tested using the following component:
import deephaven.ui as ui
from deephaven.ui import use_state


@ui.component
def bar():
    x, set_x = use_state(0)

    print(f"Render with x {x}")

    def handle_press():
        # Call set_x twice in the same method, using a callable. This should result in x increasing by 2 each time the button is pressed
        set_x(lambda old_x: old_x + 1)
        set_x(lambda old_x: old_x + 1)

    return ui.action_button(
        f"x is {x}", on_press=handle_press
    )


b = bar()
  • Tested that trying to update state from the wrong thread throws an error:
import deephaven.ui as ui
import threading
import time
from deephaven.ui import use_state


@ui.component
def foo():
    a, set_a = use_state(0)
    b, set_b = use_state(0)

    print(f"Render with a {a} and b {b}")

    def handle_press():
        def update_state():
            time.sleep(1)
            set_a(a + 1)
            set_b(b + 1)
        # Not using the correct thread
        threading.Thread(target=update_state).start()

    return ui.action_button(
        f"a is {a} and b is {b}", on_press=handle_press
    )


f = foo()

@mofojed mofojed requested a review from jnumainville December 20, 2023 23:58
@mofojed mofojed self-assigned this Dec 20, 2023
@mofojed mofojed requested a review from jnumainville December 22, 2023 18:29
jnumainville
jnumainville previously approved these changes Dec 22, 2023
@mofojed
Copy link
Member Author

mofojed commented Dec 27, 2023

Should use a ThreadPool so that one users deephaven.ui components doesn't block another users deephaven.ui components.

@mofojed
Copy link
Member Author

mofojed commented Jan 3, 2024

Thread pools now exposed to Python: deephaven/deephaven-core#4949

@mofojed mofojed force-pushed the 116-queue-state-updates branch from b162ed4 to aecfbcf Compare January 10, 2024 15:53
plugins/ui/src/deephaven/ui/_internal/RenderContext.py Outdated Show resolved Hide resolved
def use_state(initial_value: T) -> (T, Callable[[T], None]):
def use_state(
initial_value: T,
) -> tuple[T, Callable[[StateValue[T]], None]]:
Copy link
Member

Choose a reason for hiding this comment

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

As above, this seems to imply the following:

foo, set_foo = ui.use_state(None)

#... much later
set_foo(lambda old: do_something_new(old, other_vars))

Did I just assign a function that foo will now be equal to the next time my component renders? Or did I just pass a function that should be called with the old value as a parameter?

Copy link
Member Author

Choose a reason for hiding this comment

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

Passed a function that should be called with the old value. If you do want to set a function in a state variable, you need to have it be the return value of an updater function, e.g.

foo, set_foo = ui.use_state(None)

# Need to pass the function back as a result of the updater function
set_foo(lambda old: lambda : print("hello"))

Generally storing a function in a state variable is not a common use case.

@mofojed mofojed requested a review from niloc132 January 11, 2024 19:19
- Still need to do rendering on a specific thread, and group set_state calls to a thread as well
- Now updates are all queued on the same thread
- Fixes deephaven#116
- Build a CSV loader to show multi-threading example, add use_render_queue hook
- Add doc strings
- Callable takes the old value of the state
- Tested using the following component:
```
import deephaven.ui as ui
from deephaven.ui import use_state

@ui.component
def bar():
    x, set_x = use_state(0)

    print(f"Render with x {x}")

    def handle_press():
        # Call set_x twice in the same method, using a callable. This should result in x increasing by 2 each time the button is pressed
        set_x(lambda old_x: old_x + 1)
        set_x(lambda old_x: old_x + 1)

    return ui.action_button(
        f"x is {x}", on_press=handle_press
    )

b = bar()
```
- Each ElementMessageStream has it's own render queue
- Render queue is processed by calling `submit_task` and queuing a render
- Forgot to set the context to the threads local data
- Add some missing typing in RenderContext
- Split up the UpdaterFunction and InitializerFunction for setting state
- Was kind of weird how the initializer function was being handled, this makes it a little more clear
@mofojed mofojed force-pushed the 116-queue-state-updates branch from 2883d6a to bdbfca6 Compare January 12, 2024 15:23
- Also clean up the typing in use_memo a bit
- Fixes annotations in 3.8
@overload
def use_state(
initial_state: T | InitializerFunction[T] | None = None,
) -> tuple[T | None, Callable[[T | UpdaterFunction[T]], None]]:
Copy link
Member

Choose a reason for hiding this comment

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

This definition looks the same as below, why do you need it here also?

The T definitions here in the second one also need to union in None, so that None can be safely passed as a possible value.

Copy link
Member Author

Choose a reason for hiding this comment

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

I was using @overload incorrectly - I should only have had to define the two @overload and then not define typing in the actual implementation: https://docs.python.org/3/library/typing.html#typing.overload
But when I do that, pylance complains about no typings in the use_state implementation.
In any case, I think I need Python 3.12 to describe the typings correctly using the type parameter syntax on the use_state functions. I'll just get rid of the overload for now and always allow None

Copy link
Member Author

Choose a reason for hiding this comment

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

Also see discussion here about default parameters in generic functions: python/mypy#3737 (comment)
Without type parameter syntax to allow for doing something like x, set_x = use_state[int](), which would result in x: int | None, I think the best we can do for now is use type T when an initial_value is passed in, then allow Any when no initial_value is passed in.

# Initial value provided, only accepts the type that was initially provided
x, set_x = use_state(4)

set_x(5)     # This is valid
set_x("foo") # ERROR
set_x(None)  # ERROR

# No initial_value provided, accepts being set to any value
y, set_y = use_state()

set_y(5)     # valid
set_y("foo") # valid
set_y(None)  # valid

def foo(val: int | None):
    # Initial value provided, only accepts the type that was initially provided, including `None`
    z, set_z = use_state(val)

    set_z(5)     # valid
    set_z("foo") # ERROR
    set_z(None)  # valid

    print(z)

At least this way we get proper type checking when you start with an initial_value, and we don't block you from assigning a value if you don't declare an initial_value.

Filed a ticket to improve typing in 3.12: #208

Copy link
Member

Choose a reason for hiding this comment

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

Thanks, this makes more sense.

- Allow `Any` if no initial value is passed in
@mofojed mofojed requested a review from niloc132 January 17, 2024 19:15
Copy link
Member

@niloc132 niloc132 left a comment

Choose a reason for hiding this comment

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

Approved api details, would prefer if @jnumainville gives it a +1 too.

@mofojed mofojed merged commit 79a1002 into deephaven:main Jan 19, 2024
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Queue state updates to trigger re-render after current update cycle
3 participants