-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
Using keys to preserve values between reloads #8056
Conversation
🪼 branch checks and previews
Install Gradio from this PR pip install https://gradio-builds.s3.amazonaws.com/fe9003186dce675bcf9fd94a892b9bcfb1fae9f2/gradio-4.27.0-py3-none-any.whl Install Gradio Python Client from this PR pip install "gradio-client @ git+https://github.com/gradio-app/gradio@fe9003186dce675bcf9fd94a892b9bcfb1fae9f2#subdirectory=client/python" |
🦄 change detectedThis Pull Request includes changes to the following packages.
With the following changelog entry.
Maintainers or the PR author can modify the PR title to modify this entry.
|
if self.dev_mode: | ||
for block in self.blocks.values(): | ||
if block.key is None: | ||
block.key = f"__{block._id}__" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make the id a hash of the component's config? That way it will be unique across re-renders unless something about the component changed and we don't need to do the reassign_keys
logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what you're suggesting is that we use the hash of the config to decide what components are considered identical across re-renders. While we could use that logic in utils.reassign_keys
to consider what components are equivalent, it's gonna be problematic because it's very likely two components have the exact same config, e.g.
with gr.Blocks():
a = gr.Textbox()
b = gr.Textbox()
I think it makes more sense to rely on the position and type of components to assign identity. The logic in utils.reassign_keys
can always be improved but keep in mind this is only for dev mode refreshes - in actual render blocks, we will require users to explicitly assign keys to maintain identity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your point about duplicate keys but is that a concern for reload mode? Even if auto-generated ids collide, the component fetched in the frontend's restored_keyed_values
would be correct since the config should determine the state of the component.
The problem with using the position to determine equivalence is that if you insert a component "in between" or "before" already existing components, then the front-end state would get wiped.
This PR is already an improvement of what we currently do but wondering if we can create a better heuristic for equivalence from the get-go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if auto-generated ids collide, the component fetched in the frontend's restored_keyed_values would be correct since the config should determine the state of the component.
I don't think this is the case. If initially I have:
with gr.Blocks():
boxA = gr.Textbox()
boxB = gr.Textbox()
and I type something into boxA, and then I change boxA to:
with gr.Blocks():
boxA = gr.Textbox(label="A")
boxB = gr.Textbox()
then the value that was in A would get moved to B, since that's the best match by config logic.
This PR is already an improvement of what we currently do but wondering if we can create a better heuristic for equivalence from the get-go.
Sure, I think we can iterate on reassign_keys
logic in other PRs, but since its only used in dev mode I think this can be a decent starting point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok sounds good!
js/app/src/Blocks.svelte
Outdated
const restore_keyed_values = (): void => { | ||
let component_values_by_key: Record<string | number, ComponentMeta> = {}; | ||
old_components.forEach((component) => { | ||
if (component.key) { | ||
component_values_by_key[component.key] = component; | ||
} | ||
}); | ||
components.forEach((component) => { | ||
if (component.key) { | ||
const old_component = component_values_by_key[component.key]; | ||
if (old_component) { | ||
component.props.value = old_component.props.value; | ||
} | ||
} | ||
}); | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think it would be possible to do all of this in the init
module? I think we could expose an update_components
function where we could pass in components, and the old / new merging could be handled inside that module with all of the other component updates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure, moved to init.ts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I didn't mean just move the function but rather the old component state could be tracked in the init logic.
We already have the previous component state. If we added an update
method that took the new component / deps / layout, we could easily perform the logic all inside of the update method without needing to track the previous state outside of that module.
This way we could also perhaps avoid some work on every component change and instead only do what is necessary (this may be minimal if anything).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha, makes sense! Implemented this in #8110 which targets this PR.
@@ -155,7 +148,11 @@ def alert_change(self): | |||
self.change_event.set() | |||
|
|||
def swap_blocks(self, demo: Blocks): | |||
old_blocks = self.running_app.blocks | |||
super().swap_blocks(demo) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to take care of adding max_file_size
from the old blocks here as well:
demo.max_file_size = old_blocks.max_file_size
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, handled in BaseReloader.swap_blocks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the great PR @aliabid94 !
One remaining issue I noticed is that this breaks custom component reload mode. I created a new component in js/preview/test
(needs to be here until the next release because of #8066) and ran dev mode. Changes in the python file were not reflected in the UI. Confirmed it does work on main though.
I can also reproduce what @freddyaboulton is seeing. |
Okay I believe I fixed the maximum 6 refreshes bug and also the bug @freddyaboulton mentioned by simply closing the heartbeat connection on refreshes. Should be ready! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tested and works well @aliabid94 🔥 Nice
pnpm-lock.yaml
Outdated
@@ -1478,6 +1478,25 @@ importers: | |||
specifier: ^3.28.0 | |||
version: 3.28.0 | |||
|
|||
js/preview/test/mycomponent/frontend: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Revert this change please
Didn't do a deep review but it looks like the issues that I noticed are fixed. Nice!! As discussed previously, would be cool to talk about how values are preserved: |
Added the concept of a key to a Block, such that any Block that has the same key between re-renders has its value preserved between renders. Will be needed specifically for render PR.
Implemented in dev mode by automatically assigning every Block a generated key and giving similarly positioned blocks have the same key across renders. See below how re-renders do not change the value of the components.
BEFORE:
AFTER: