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

Compose in the Runtime Take 3 #824

Closed

Conversation

steve-the-edwards
Copy link
Contributor

@steve-the-edwards steve-the-edwards commented Jul 4, 2022

To address #665

@steve-the-edwards steve-the-edwards force-pushed the sedwards/compose-with-return-rendering branch from 6e53c87 to 5fe9107 Compare July 6, 2022 16:03
@steve-the-edwards steve-the-edwards changed the base branch from sedwards/compose-take-2 to main July 6, 2022 16:03
@steve-the-edwards steve-the-edwards changed the title Return rendering instead of Unit Compose in the Runtime Take 3 Jul 6, 2022
@steve-the-edwards steve-the-edwards force-pushed the sedwards/compose-with-return-rendering branch from 5fe9107 to ce5557a Compare July 6, 2022 16:04
Comment on lines 74 to 76
setRendering = {
renderingState.value = it as R
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

We usually discourage this, but since this is framework code that is not public API and will be run a lot, it might be more efficient to just pass in the MutableState itself so Compose doesn't have to memoize this lambda for you.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

props: PropsT,
setRendering: (RenderingT) -> Unit
): Unit {
key(props) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If you're comparing props anyway, I don't see the reason for putting this in a key function (and if it is necessary, it should probably be an implementation detail of UpdatePropsAndState).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed.

setRendering: (RenderingT) -> Unit
): Unit {
key(props) {
UpdatePropsAndState(workflow, props)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: Compose conventions discourage naming unit-returning composable functions as verbs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Worthwhile to break convention here I think.

) {
if (newProps != lastProps) {
val newState = interceptor.intercept(workflow, this)
.onPropsChanged(lastProps, newProps, state.value)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I hope everyone heeded our advice to not perform side effects in this function 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we can protect against that anyway like this:

    val newState = produceState(initialValue = state.value, newProps) {
      if (newProps != lastProps) {
        interceptor.intercept(workflow, this@WorkflowNode)
          .onPropsChanged(lastProps, newProps, state.value)
      }
    }
    state.value = newState.value

Copy link
Contributor Author

Choose a reason for hiding this comment

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

NVM. that does not work because there is nothing actually reproducing the state!

This works though:

key (newProps) {
      if (newProps != lastProps) {
        state.value = interceptor.intercept(workflow, this@WorkflowNode)
          .onPropsChanged(lastProps, newProps, state.value)
      }
    }

@@ -52,7 +56,7 @@ internal class WorkflowRunner<PropsT, OutputT, RenderingT>(
private val rootNode = WorkflowNode(
id = workflow.id(),
workflow = workflow,
initialProps = currentProps,
initialProps = currentProps.value,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: I don't see you using the delegate syntax for MutableStates very much, it would make a lot of the code in this PR more idiomatic and a teeny bit cleaner.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The delegate syntax continues to confuse me and I don't necessarily like the syntactic sugar obfuscating what is happening.

In this case currentProps is a class property. How would I use the delegate syntax here as I thought that's only when using remember? And remember is only for within a @Composable?

Copy link
Collaborator

@zach-klippenstein zach-klippenstein Jul 6, 2022

Choose a reason for hiding this comment

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

Remember has nothing to do with delegation. Property delegation is a well-documented Kotlin language feature. It's supported for MutableState because it has getValue and setValue extension methods. More info on how it works here.

Copy link
Contributor Author

@steve-the-edwards steve-the-edwards Jul 6, 2022

Choose a reason for hiding this comment

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

Ah I was getting confused by all the examples including the remember in the delegate. I didn't realize that even outside of that it was actually MutableState that had the getValue/setValue so it could be used with delegation.

I see now that you can just use:

val currentProps: PropsT by mutableStateOf(props.value)

that makes a lot more sense.

handler: (ChildOutputT) -> WorkflowAction<PropsT, StateT, OutputT>
): ChildRenderingT {
val stagedChild = remember (child, props, key, handler) {
prepareStagedChild(
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't believe this is safe to call from composition since it modifies non-snapshot state in this class.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

given that SideEffect only occurs after Composition though and we need this stagedChild to call the Rendering waht are the options here?

I thought remember would effectively avoid calling this multiple times across recomposition?

Do you mean that the StagingList needs to be State?

Copy link
Collaborator

@zach-klippenstein zach-klippenstein Jul 6, 2022

Choose a reason for hiding this comment

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

Remember will prevent calling it again on subsequent compositions, but it will still be called if the initial composition is discarded and never applied. It is probably enough to make the staged child list a SnapshotStateList.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So if I am understanding you correctly, we would make a @Composable StagedChild() that would have a SideEffect that would create the child node and add it to the staging list.

That way that would only be called if StagedChild was recomposed successfully, but we could still rely on its side effect by the time the Rendering is composed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think i'm just going to have to re-write or modify ActiveStagingList in a Snapshot-State-friendly way

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  /**
   * Prepare the staged child while only modifying [children] in a SideEffect. This will ensure
   * that we do not inappropriately modify non-snapshot state.
   */
  @Composable
  private fun <ChildPropsT, ChildOutputT, ChildRenderingT> StagedChild(
    child: Workflow<ChildPropsT, ChildOutputT, ChildRenderingT>,
    props: ChildPropsT,
    key: String,
    handler: (ChildOutputT) -> WorkflowAction<PropsT, StateT, OutputT>
  ): WorkflowChildNode<*, *, *, *, *> {
    val childState = remember (child, key, props, handler) {
      children.forEachStaging {
        require(!(it.matches(child, key))) {
          "Expected keys to be unique for ${child.identifier}: key=\"$key\""
        }
      }
      mutableStateOf(children.firstActiveMatchingOrNull {
        it.matches(child, key)
      } ?: createChildNode(child, props, key, handler))
    }

    SideEffect {
      children.swapOrStage(
        predicate = {it.matches(child, key)},
        child = childState.value)
    }
    return childState.value
  }

@steve-the-edwards steve-the-edwards force-pushed the sedwards/compose-with-return-rendering branch 3 times, most recently from 410fbd1 to 320bdc5 Compare July 8, 2022 21:57
Still todo on the Snapshotting and figure out how we want to 'freeze' to guard against out of lifecycle calls.
@steve-the-edwards steve-the-edwards force-pushed the sedwards/compose-with-return-rendering branch from f132ca8 to dcec792 Compare July 13, 2022 21:24
@steve-the-edwards
Copy link
Contributor Author

OK so I have updated this with a recent commit that isolates all of teh Compose optimizations as a plugin module

@steve-the-edwards
Copy link
Contributor Author

  1. Trying to call into the Compose runtime directly (without it being a Composable) was a dead end for now as I worked on it for a bit with Zach and we got past his knowledge and idea of where to go next, so letting that go. The impact of this is we need a StatefulWorkflow/StatelessWorkflow extension to add the @composable Rendering - but I think i did that relatively nicely.
  2. This update is passing all the tests locally and is behind a warning annotation.
  3. You would add the workflow-core-compose module and plugin the optimization to the runtime when calling renderWorkflowIn, as I do in performance poetry (and it shows the same rendering oprtimizations on the tests).
  4. I like where this is at for pausing as its all isolated and I could likely rebase this without too much difficulty. (Obviously it doesn't have any compose runtime specific tests yet so I would want to add that when picking it back up).
  5. Unfortunately I had to pull all of our runtime.internal classes into public land because we (still I think?) don't have good 'friend path' support for kotlin modules - and since I'm extending them all in the compose-core module I need access.
  6. What I would like to do with this is turn off the publishing of these and the explicit API mode. So they can still be public for the other module but won't be part of the published API (haven't done this yet). Unless someone has a good idea for package-protected in kotlin?
  7. Some of the polymorphism got a bit messier than at other times - would really appreciate ideas as to how it could be cleaner at points. WorkflowNode initialization is really the ugliest one as I had to add a startSession so that the initialization happens after the child class is instantiated and not just after the parent class.

@steve-the-edwards
Copy link
Contributor Author

Superseded by #886

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.

4 participants