Parallel Fragments #2
Draft
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
Make it possible for
st.fragment
s to run in a parallel thread.Problem statement
Dashboards are one of the most common classes of apps in Streamlit. In a dashboard, data is typically loaded, then transformed (sometimes after some user input), then finally displayed as charts and other widgets.
It's very common for the load-transform code paths of any given chart to be completely distinct from the code paths of other charts. However, these code paths are typically executed sequentially, which leads to a slow loading pattern for the app, where one section will only load once the previous has done so.
Toy example:
In this app, each step runs sequentially after the previous one is done, so the whole thing takes 6s to draw:
Question 1: Given that these code paths are so different, it would make a lot more sense to load them in parallel instead. What would be the a simple, Streamlity API that is powerful enough to cover the more common patterns for this?
Question 2: When a user moves the sliders, the entire app reloads. How can we make sure only the fragments that depend on that slider reload instead? For now we'll leave this unanswered, as it will be the subject of a separate StEP. But you should have this question in mind as you think through this StEP since we don't want the solution to Question 1 to preclude a great solution for Question 2.
Goals
@st.fragment
s in a separate thread.Non-goals
Proposed solution
To address question 1, let's extend the fragments primitive to support parallel execution, so the example above looks more like this:
(NOTE: Ignore the exact API right now)
With parallel fragments, the app takes 2s to load, and its execution flow looks like this:
API
How should we declare that a given fragment can be executed in a parallel thread?
Option 1: New keyword argument
Signature
Usage
Pros
Cons
Naming
Option 2: New decorator
Signature
Usage
Pros
Cons
Introduces a new flow control primitive in Streamlit.
People tend to be confused by the primitives we already support (
cache_resource
,cache_data
,fragment
,form
), so I'd rather not make things more complicated for them.?
Naming:
Option 3: Async def ✅ CURRENT FAVORITE
The idea Option 3 is that you declare a parallel fragment using
async def
instead ofdef
.Signature
With this option, there would be no change to the
@st.fragment
signature:Usage
Pros
Cons
async
in PythonDesign
This is a Python-only feature. No impact on design.
Behavior
The return value of an async fragment is ignored.
Another option would be to return a
Future
or to somehow stuff the return value into Session State, but it's unclear that any of this is needed. So let's leave this feature out for now and see if there's a need. We can always add this later.Other solutions considered
Just use threads
Today, if you use a Thread in Streamlit you need to do some magic with the script run context. We
plan on fixing that soon. Once we fix it, you'll be able to solve Question 1 with pure Python
as shown below. So why add another Streamlit primitive?
Pros
Cons
st.command
are better at nudgingdevelopers to actually use them. But it's possible that this is just a matter of documentation.
Major difference
In the end, the thing that's inserted in the app is not a fragment. Which means that when the
user interacts with widgets inside that block they cause a full rerun of the a script. This may
be desired in some situations, but my hypothesis is that in most cases it would be better to
rerun just that "block" of the app.
In this scenario, you could turn on fragment behavior by using
@st.fragment
:Note: I don't know if this would actually work! Needs to be verified.
Metrics
Impact on metrics:
The hope is that this would make a certain class of apps faster. However, it may be hard to measure this since we'd need to look at performance metrics from before and after the change.
Requires new metrics:
If going with Option 3, we'll need to add some telemetry logic to be able to tell how much usage this feature is getting.
Otherwise, Options 1 and 2 should get automatically tracked with the current telemetry logic.
Implementation
Once there's a prototype implementation, we'll link the Github branch for it here.