I am still working on my web application and I want to try and implement time travel debugging on the front end similar to this elm project here https://github.com/rtfeldman/elm-spa-example (the demo with time travel debugging, directly here http://rtfeldman.github.io/elm-spa-example-with-debug/). The ultimate end goal will be to have the program record all of the actions that the ui fires and give the user the option to look at them or even play them back (hence the time travel). a stretch goal if I can implement all that would be to allow for the user to export and import these logs and have the application run through them as if the user were hitting the buttons or performing the actions themselves.
Ive been looking through the Incr_dom documentation (which is what Im using on the front end) and I noticed that Action has a built in val should_log: t -> bool. I thought to myself im golden becuase it has to be what im looking for but now after fiddling with it for a while im not totally sure its the silver bullet i was looking for. Ive looked through much of the rest of the incr_dom documentation and some of the vdom docs in case and havent found anything that looks like it is going to perform the actions I need.
I was wondering if anyone had done this before or had resources they could point me to to help me progress, I have been reading a lot of blog posts and repo notes about doing this sort of thing in Elm in the hopes of finding some references to ocaml (and while I did find some none really panned out as they all tended to refer to ocamldebug and its time travel features which dont help me … I think …) at this point im not even sure what to google because “time travel debugging ocaml” just leads me to the ocamldebug which I feel is not the exact resource im looking for. there are plenty of elm resources for this so that is reassuring it definitely makes me think that this is possible in ocaml!
Thank you all for your help.
I’m glad you’re thinking about this, and I do think that this would be fairly natural to do within Incr_dom. I don’t think you the should_log call is really relevant here, but the basic structure of an Incr_dom app fits the time-travel idea quite well. (and yes, ocamldebug will not help you here.)
I think you can add time-travel to an app in a quite generic way without modifying the incr_dom itself. My thought would be to write a functor that can take something that matches, say, Incr_dom.App_intf.S_simple, and produces a new module that matches Incr_dom.App_intf.S_simple, but that does the time-travel work.
The key thing you need to do for time-travel is to capture and remember the initial state, as well as the full sequence of Action.t’s. It then needs to add some kind of overlay to the UI that allows you to do the actual time-travel work, e.g., by letting you change the model to be what it was at some point in the execution of the system, by replaying the actions from the beginning. (You could also keep occasional snapshots if you want to speed up replay.)
One limitation is that this won’t do anything to recover the imperative part of the state, so this makes the most sense for apps that have very limited imperative state. Hard to see how one would really do much better, since things like open websocket connections are not going to react well to time-travel…
so I have spent the last couple days mulling over your suggestions and trying some things out and i have a couple more questions.
why would i want to make a functor on the incr_dom.app_intf.s_simple, i thought it would be enough to just track the actions and the initial model that way i can just execute any actions that happen between the beginning and the current time on the initial version of the model and it will show the changes up to that point? Would it give me some advantage to work on the whole app_intf level instead of just the proverbial “actions applied to the model” level?
Where do i want this functor to live, I’ve tried putting it in the model but that doesnt seem to work for a whole slew of reasons most of which boil down to i am unsure about how to make a seperate module part of the type of the model, i have played with it living as part of the action module but that makes it hard to get out of the module to operate on it in the model to display it later on, and I thought about making it global and passing it around from there which seemed like the most practicable plan but its 1) not terribly (or at all) functional and 2) i dont know how to run the functor if its just hanging out. i figured i could just always update the action_list in the apply_action function but then i would need to pass the action list or the functor into the apply_action which I cannot since apply_action is from incr_dom and has a signature it needs to adhere to
also i was thinking that im going to run into an issue with calls to the database on the back end wont i, since any changes that occur on the back end have nothing to do with changes to the model, i.e. the model reflects the database not the other way around.
Functors are a really powerful feature of OCaml. In this particular scenario you can use them to extend existing app_intf module with new functionalities. In simple terms functors are functions that work on a module and return a new module. So you could imagine a functor that works on a
App_intf.s_simple and returns an enhanced module that contains your implementation of the work needed to support the time travel debugging. Note that you could potentially implement the same as part of your specific application, but then that implementation is tied down to your particular application and isn’t generic to work for any other incr_dom app.
I’d definitely recommend reading: https://dev.realworldocaml.org/functors.html to learn more about what’s possible to achieve using Functors.
As for how to use it, you would use it with the
Start_app just like a regular module that satisfies the App_intf interface. What changes is that the functor enhances the module before you pass it along to the
Like Yaron said, recovering imperative state wouldn’t work here. In my opinion this is where its important to spend some time to think about the scenarios where this style of debugging should be used. Personally for me, it adds value in situations where you care about getting a sequence of steps that can be used to find out how you reached a particular place in the application. But I don’t see it being useful to debug issues in components that are outside the scope of the UI application (HTTP Calls, websocket connections, database state etc).
I started to take a look at this today.
I’m still not super confident with my knowledge about Functors but this is where I am right now. I don’t know of any better way to do this but as a first attempt I was just trying to avoid duplicating the signature from
App_intf.S_simple if possible.
I am not worrying about the application making network calls, performing URL changes or anything that causes a change outside the application. What I was thinking was to block any user interaction on the application once someone starts using the history to move through the sequence, and have them manually end the debug session to resume their usual work.
For my first attempt I am able to keep track of the sequence of actions and the the model changes along with that. I still have the problem that I’m not able to actually restore a previous state of the model, as I don’t have access to anything that can trigger an
apply_action to update the model.
I wanted to somehow be able to wrap the child components’
Action module so I could reset the model with the value from a particular point in the history list. This seems to work at a first glance, but I’m not sure if this is the best way to go about it.
I think you’ve got some steps in the right direction here, but a few thoughts:
- the serialization and deserialization is unnecessary. You’re keeping everything in memory, after all, so there’s no need to convert it to strings. If you wanted to push the state out over a websocket or something, you’d need that, but that doesn’t seem necessary here.
- A simple form of time-travel that you should perhaps consider as a first attempt is just a reset-to-the-beginning action. You then just need to remember the initial value of the model, and the “restart” action needs to contain no extra information.
- The next improvement is to add a
Reset_to of int action. You can implement this by going to the very first model, and replaying the first n actions in order.
- If that’s too slow for your purposes, as an optimization, you can keep periodic snapshots, and then implement
Reset_to n by going to the biggest snapshot whose index is smaller than
n, and replaying from there.
- Another interesting implementation strategy is to keep an indexed pointer to every state you’ve ever been at. Because these are updated functionally and so share memory with each other for bits that weren’t modified, the amount of space used might be tolerable.
Thanks for the feeback! The serialization was intially just a way for me to display something on the page that tells me it works
Just like you said I’ll be removing the model list in the history, as the actions are sufficient to compute them.
I should have an update soon!
As a side note: Its been really fun playing around with
Incr_dom. Its the first time i’ve tried
js_of_ocaml and its proving to be a lot of fun.
I’m glad you’re enjoying it! I should warn you that there’s a significant revamp of the incr_dom APIs coming in the next release. The good news is that if you never understood how the S_derived interface worked, you’ll never have to, because it’s going away. We found a simpler way to express things that lets us do the optimization we need to do without the extra complexity of a “derived model”.
The downside is that you’ll need to rejigger the top-level of your app a bit to fit the new API. That said, it shouldn’t be a hard adjustment.
Thanks for the heads up!
I have been looking at the changes on github after version
0.11.0 and so far the changes look like they address some pain points I had when I first started with