-
Notifications
You must be signed in to change notification settings - Fork 145
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
Add mount state reporting #152
Conversation
…DomBuilder also clean up some code around the immediate dom builder, and add some doc comments compiles but needs testing
…s and fix the instance for ImmediateDomBuilderT use the compiler, luke. *shakes fist at cabal*
also export MonadWidgetConstraints so it shows up in haddock
-- | ||
-- ___Note:___ once the current scope is replaced, any 'performEvent's in the scope will be cancelled and so if you want to observe the 'Unmounted' status | ||
-- you have to plumb the mount state dynamic back out of the current scope so the parent scope can react to it. | ||
class Monad m => MonadMountStatus t m | m -> t where |
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.
Let's see if we can avoid using the word "Monad" in the name. I've been trying to move away from that (MonadAdjust, etc., ought to be renamed as well, I think, but that's for another time).
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.
hmm I liked it if only to distinguish between the type class which lets you fetch it and the MountState type. class MountStatus
with data MountState
could work though seems easily confusable. do you have a suggested name?
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.
ReadMountState?
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.
GetMountStatus
to match the method it provides?
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.
Perhaps HasMountStatus is a good fit; we also have, e.g., HasDocument.
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 like consistency. I'll go with that.
class DomBuilder t m => MountableDomBuilder t m where | ||
type DomFragment m :: * | ||
buildDomFragment :: m a -> m (DomFragment m, a) | ||
mountDomFragment :: DomFragment m -> Event t (DomFragment m) -> m () | ||
|
||
-} | ||
|
||
-- |'MonadMountStatus' represents an action that can be aware of whether the corresponding DOM built by the action is present within the document yet or not. |
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.
Would it make sense to call it a 'widget' here, instead of an 'action'? 'MonadAdjust' does kind of imply that any sort of action may be adjusted, and that's deeply connected to this, but I'm not quite clear on what it'd mean outside the contexts of widgets anyhow.
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, if widget is the widely accepted term for "any unit of dom building"
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.
corrected.
-- ^DOM structures are not yet installed in the document. | ||
| Mounted | ||
-- ^DOM structures are now in the document. | ||
deriving (Eq, Ord, Show) |
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.
Would there be any value added if we, and/or would it be possible to, add an "Unmounting" state?
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 had it originally and removed it because I wasn't sure how to wire it in... I think it'd need to go to Unmounting when runWithReplace is about to remove it but before the DOM updates happen?
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.
Yes, I think that would make sense. However, I'm OK with leaving it out for now to avoid adding more complexity to this PR.
@@ -56,7 +57,6 @@ module Reflex.Dom.Builder.Immediate | |||
#ifndef USE_TEMPLATE_HASKELL | |||
, phantom2 | |||
#endif | |||
, drawChildUpdate |
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.
This is intentional, because drawChildUpdate ought to actually be internal, right?
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.
yes, and the type(s) it deals in (ChildReadyState
) are not exported
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.
Yep, makes sense!
-- ^Action to take when the unready children all become ready, usually to install the document fragment. | ||
, _immediateDomBuilderEnv_mountState :: Dynamic t MountState | ||
-- ^'Dynamic' representing the current state of DOM nodes within the parent node with respect to the document as a whole. See 'MountState' for more. | ||
} |
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 adding docs and fixing the style! Much better!
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.
👍
@@ -652,6 +684,7 @@ instance (Reflex t, MonadAdjust t m, MonadJSM m, MonadHold t m, MonadFix m, Prim | |||
result <- runReaderT (unImmediateDomBuilderT f) $ initialEnv | |||
{ _immediateDomBuilderEnv_unreadyChildren = unreadyChildren | |||
, _immediateDomBuilderEnv_commitAction = myCommitAction | |||
, _immediateDomBuilderEnv_mountState = zipDynWith min parentMountState firstLocalMountState |
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.
Nice
@@ -112,6 +113,7 @@ type MonadWidgetConstraints t m = | |||
, MonadSample t (Performable m) | |||
, MonadReflexCreateTrigger t m | |||
, PostBuild t m | |||
, MonadMountStatus t m |
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'm not sure this should go in here. MonadWidget
is a bit of an ad-hoc collection of stuff, which ideally I'd like to deprecate in favor of something that makes a bit more sense. We might, e.g., write a class Widget
, which is explicitly designed to be beginner-friendly, and then move from there.
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.
my understanding of MonadWidget
is that it's basically everything you can do while running live in a page, and corresponds to all the powers of Widget
(from Reflex.Dom.Main
). I can remove it, though I'm not sure what value that has until there's a better solution to all the powers.
can remove it if you want, though. it's trivial to add it as a requirement to use sites (and their callers)
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.
Alright, I think that makes sense; we can keep MonadWidget
going as a kind of "kitchen sink class" for the time being.
@@ -15,6 +15,8 @@ module Reflex.Dom.Main where | |||
|
|||
import Prelude hiding (concat, mapM, mapM_, sequence, sequence_) | |||
|
|||
import Reflex.Class (holdDyn) | |||
import Reflex.Dom.Builder.Class (MountState(Mounted, Mounting)) |
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.
The whitespace style is just slightly different here. Running stylish-haskell
from the root of this project should work - but in this particular case, it's just that MountState
should get a space after it.
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.
ah yeah I've never used that tool so didn't notice. I'll fix
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.
corrected. I couldn't get stylish-haskell to run in a way convenient to check its complaints as it seems to want to be a pass-through filter only? running it from the root just had it hanging waiting for stdin.
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 run stylish-haskell as stylish-haskell -i **/*.hs
, after I've committed everything to git, and then I check the git diff. It's not ideal, but it works pretty well and makes it easy to revert.
liftIO $ writeIORef placeholders $! placeholders0 | ||
_ <- DMap.traverseWithKey (\_ (Compose (df, _, _, _)) -> Constant () <$ append df) children0 | ||
let result0 = DMap.map (_child_result . getCompose) children0 | ||
childInstallations0 = weakenDMapWith (_child_installation . getCompose) children0 |
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 there may be a strictness issue here? Previously, we were casing on the (,,,), but now we're using a record selector. If my understanding is correct, this could potentially lead to the creation of a thunk that would still include the _child_result.
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.
nice catch, I think you're correct since the target map is a lazy one.
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.
corrected.
@@ -822,7 +913,7 @@ hoistTraverseWithKeyWithAdjust base mapPatch updateChildUnreadiness applyDomUpda | |||
when (DMap.null newUnready) $ do | |||
applyDomUpdate p | |||
(children0, children') <- ImmediateDomBuilderT $ lift $ base (\k v -> drawChildUpdate initialEnv markChildReady $ f k v) dm0 dm' | |||
let processChild k (Compose (_, _, sRef, _)) = ComposeMaybe <$> do | |||
let processChild k (_child_readyState . getCompose -> sRef) = ComposeMaybe <$> do |
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.
Is this the only use of ViewPatterns
in the file? Although I don't have a strong preference against ViewPatterns
, I think in this particular case it might be just as good to do something like Compose (Child { _child_readyState = sRef })
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.
yeah I had more uses but then backed off from them. I'll clean out this case to not have a one-off
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.
corrected.
Overall, looks like a huge improvement, with just a few minor tweaks needed. One big point remaining: what do we need to do to convince ourselves that this won't negatively impact performance? |
… and replace with TODO to possibly remove it
…ns, use pattern matching to be as strict as the previous version
okay, fixed all the low-hanging fruit. open issues I need some feedback on:
regarding the performance angle, it will undeniably add work to every DOM build though without a lot more digging I can't be sure how much. What work I know it adds:
|
The perf impact seems a bit hard to estimate. It might make sense for you to try it out on a couple key use cases in your own codebase, and if it has an impact, we can extract that into benchmarks that hit those use cases. |
I'll try it out on our two reflex projects (one which doesn't care about mount state and one which does) and report back if anything goes poorly. That said, neither is particularly performance heavy so may not be the best test. |
As long as you have a couple of metrics that seem relevant, that'll be a good start! The top concern is that we can't create a regression in straight-line dom building speed (i.e. dom building without MonadAdjust in it). |
I would be very surprised if it creates a regression there, as the only time it shows up is at the top level and in uses of MonadAdjust. In my testing using ConferHealth/reflex-mount-test I didn't see spurious mount events firing at the top level (or in MonadAdjust uses). |
How's this coming along? |
@3noch in my experience it works fine. @ryantrinkle is concerned about performance impacts though so will not be merging it into 0.5. If someone can provide some benchmarks that show its performance impact that would help move it along into mergetown. Unfortunately right now I'm underwater with work so I haven't gotten time to shepherd it further. https://github.com/ConferHealth/reflex-platform's confer branch is a reflex-platform with this branch of reflex-dom if you want to use it prior to being merged. The performance should be no worse for non-dynamic DOM building, but it definitely will impact dynamic DOM as it uses trigger events and dynamics to keep track of the mount state. |
this branch had drifted w/r/t develop, and so I've merged develop into it and made sure it builds. there was one minor merge conflict around ChildReadyState, which I resolved |
I've got the branch compiling against origin/develop, now working through testing it again. the upstream changes were pretty finicky as were the PR changes, so there may be some fallout I'll have to clean up |
okay, tried out the basic tests at ConferHealth/reflex-mount-test using ConferHealth/reflex-platform@116fba2 which points at ConferHealth@c4dfbe3 and it seems to trivially work |
Any more progress on this? I am likely going to need it soon. |
Hey everyone, @alexfmpe and I just got the full benchmark measurement suite to run, and unfortunately it looks like there's a bit of work to do here. Here are the results I got:
It looks like perhaps we need to try to cut down on anything being done in the critical rendering path here. Since the benchmarks don't use mount state reporting, perhaps we can somehow avoid having them pay for it? |
Would it be possible to have a combinator like |
@3noch Perhaps! It also might be possible to do it as a separate monad transformer. However, this code is pretty hair, so it's not obvious to me what the best way to go is. |
I should also note that I resolved conflicts and pushed the result here: https://github.com/reflex-frp/reflex-dom/tree/pr-152 |
Oh yeah, also, the script for generating that table (given two "results" directories) is here: https://github.com/reflex-frp/reflex-platform/blob/develop/benchmarking/compareBenchmarkResults.hs |
Another monad transformer would be fine. This seems like it gets deep into the spider/renderer though. Would that be possible without disrupting the rest of the code? |
it doesn't get too close to spider, but it is pretty deep in the renderer. my thought for expediency if not most elegant was to add another hook or two to the ImmediateDomBuilderEnv, like commitHook, which gets called on mount and unmount, along with exposing some interfaces to tack another action onto the hook. then you could add the monad transformer to get you the dynamics by hooking |
That makes sense to me! Ugly, but sounds like it will get the job done.
…On Sat, Nov 11, 2017 at 6:19 PM, Ross MacLeod ***@***.***> wrote:
it doesn't get too close to spider, but it is pretty deep in the renderer.
my thought for expediency if not most elegant was to add another hook or
two to the ImmediateDomBuilderEnv, like commitHook, which gets called on
mount and unmount, along with exposing some interfaces to tack another
action onto the hook. then you could add the monad transformer to get you
the dynamics by hooking
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#152 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABGlYOE1xms9FMtzsplRQktMTLuPIUNbks5s1it0gaJpZM4Nh34b>
.
|
@ryantrinkle Thanks so much for getting the ball rolling on those perf benchmarks, BTW. |
similar idea but no. that hook runs on each DOM update, regardless of what sort of DOM update it is. The thing the mount state reporting is trying to work with is that ImmediateDomBuilderT might wait indefinitely to put things in the DOM; it waits for all of its children to be ready. Usually that's not very long as it's only |
I did some analysis of the code with respect to the slowdown in BMs. There is about 25-30% slowdown in I incrementally applied the changes of the PR and found that the slowdown is only due to creation of Dynamic values for each node with holdDyn, and then using zipDynWith. (The In the current design we are providing a (This code is also capturing the Possible solution / alternate design to avoid the slowdown: Each node maintains a I think that the user will be interested in mainly this Now here the tricky part can be determining which commit action actually puts the child node in the DOM. |
@dfordivam thanks a lot for looking into this! I figured the Dynamic was probably the issue. I was imagining reducing the inbuilt support from the DomBuilder to just an Event for commit and Event for uninstall. I think the uninstall event is pretty important to allow external JS libraries to release resources associated with DOM, though the mounted state is definitely the more important. Once those Events were exposed the Dynamic could be built up from them using a monad transformer or something. As far as when the mounting occurs, the patch should incorporate all the myriad places where that occurs. It's fairly complicated because it's when initial nodes get installed iff the parent is ready, or when the parent becomes ready and its parent is already ready, etc. I can help try to answer questions about the patch if you have them. |
|
||
{-# INLINE hoistTraverseIntMapWithKeyWithAdjust #-} |
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.
This here is the culprit of the whole slowdown.
So it turns out the whole slowdown was due to I observed about 15% slowdown in the refactor PR #271 , and figured this out. After fixing this the latest numbers are like this, we have about 2-3% change in the big
|
Also my implementation of Event based approach in #272 has this result
|
Closing in favor of #273 |
As discussed in #110, there's no robust way currently to do something when an element goes into the document, as unready children in the same scope will prevent the whole scope from being installed. Often you can work around it using
delay 0 =<< getPostBuild
, but that would only work if the children became ready in the current frame and all ancestors of the current scope are already mounted in the document.This PR adds a
getMountState :: m (Dynamic t MountState)
function by way of theMonadMountStatus
class whichImmediateDomBuilderT
implements.MountState
starts atMounting
, goes toMounted
as soon as (but no earlier than) the time the corresponding DOM nodes get mounted in the document, and goes toUnmounted
once they are removed.This is implemented by creating a trigger event and holding it with a Dynamic at each scope, known as the local mount state, and zipping that with the parent scope's mount state. The root scope is handled specially by
mainWidget
,mainWidgetWithHead
, andattachWidget'
, since it can never beUnmounted
(as far as I know anyway :) ).The majority of the changes are in
ImmediateDomBuilderT
, and while I was there I did several separate cleanups:((,,,) DOM.DocumentFragment DOM.Text (IORef (ChildReadyState k)))
with a dedicated data structureChild
Caveat involving
Unmounted
: it looks like anyperformEvent
s inside a scope get suppressed after they get replaced withrunWithReplace
, so the natural code one might write to catchUnmounted
might be like:won't actually catch the unmount. Plumbing the dynamic out to the parent scope makes it possible to observe though, so I put a note on the doc comment. I'm not sure if there's a more elegant solution here.
I tested workingness of the
MonadAdjust
members for both regress by futzing around with https://github.com/ConferHealth/reflex-mount-testIf there are any scary edge cases that test doesn't help with, I didn't test them. I also didn't check performance, as I was unsure how best to go about that.
@ryantrinkle