iodide-project / iodide

Literate scientific computing and communication for the web
Mozilla Public License 2.0
1.49k stars 141 forks source link

iodide "print" and DOM manipulation challenges #587

Closed bcolloran closed 5 years ago

bcolloran commented 6 years ago

@hamilton @mdboom @wlach -- hey guys, so i'm maybe quite stuck on some functionality; i think my JS greenness let me wander dawn a garden path regarding the iodide.print functionality we've discussed a number of times, and i'm wondering whether any of you can see of any reasonable work arounds.

Basically: as i've understood our discussion around "print" functionality, we want to enable workflows similar to python-ish workflows, wherein you can call print("foo") an "foo" will immediately show up somewhere. This is available in python+jupyter, and among other things, is very useful for tracking progress during long running tasks. E.g., i do something like the following pattern (printing the iteration/epoch number for long loops) all the time:

t0 = Date.now()
for (i=0; i<10; i++){
  iodide.print("epoch: "+i)
  j=0
  iter = iodide.DOM.element("div")
  while (j<10000000){
    j++
    if (j%1000000===0){
      // some expensive computation here
      console.log(j)
      iter.innerHTML = j
    }
  }
}
Date.now()-t0

Hopefully this is pretty clear, but to add detail: iodide.print just writes a string to the middle "side effect" row (between editor and output) iodide.DOM.element appends an element to the side-effect row that you can target and manipulate with as you would with any other dom element.

My prototype for this works... but it doesn't work as i had expected. rather than immediately updating the DOM each time iodide.print or iter.innerHTML is called, the UI is updated all at once when the cell evaluation is finished (and this is with direct dom manipulation, no react/redux interference).

And apparently this is a known thing... https://stackoverflow.com/questions/7635453/javascript-not-updating-dom-immediately https://stackoverflow.com/questions/9490780/dom-update-followed-by-a-big-loop-doesnt-render-in-time https://stackoverflow.com/questions/8110905/javascript-a-loop-with-innerhtml-is-not-updating-during-loop-execution (ect ad infinitum) ... but i did not know it.

It seems that all of these DOM manipulation APIs are actually queuing tasks or something rather than actually immediately going out and tweaking the DOM right that instant, and all the solutions online involve seem to involve rewriting the loop to be like a recursive function in a setTimeout, which enable "handing control back to the browser" for DOM manipulations to occur. But this would be very ugly gymnastics to force our users to go through in order to operationalize the workflow/use case i described above, and i don't see a way to do this "handing control back to the browser" operation from within an eval call.

So the questions, i suppose, are: 1) does anyone see an easy way to do this? 1.1) is there even a way to do this at all, or is this another one of these fundamental and irreducible limitation of js+browser? 2) @mdboom you'd mentioned wanting print-like functionality; is it still worthwhile if what we can create end up acting more like a cell output (showing up in the UI all at once) rather than like the print python+jupyter (which shows up immediately when called)? 3) there might still be some value in being able to create DOM nodes and write text string from within an eval, even if they don't show up immediately / can't be updated in real time. For example, maybe iteratively outputting several strings, and certainly creating DOM element to be targeted for plots etc. So even though I would be bummed to not be able to provide an easy function function for python style printing, we could still do something here, but in that case i think it would be better to frame it as "output" or "show" or something rather than "print", which i think many folks would expect to be instant. does that make sense?

i have a branch here where you can see what i was trying: https://github.com/iodide-project/iodide/tree/iodide-print-dom-direct please feel free to take a look, try out other things, etc.

and here's a notebook to test that basic functionality https://gist.github.com/bcolloran/a7b1c7298d2f77250274847a8defaefb

mdboom commented 6 years ago

I think even this (where all the printing basically "shows up at once") gets me most of the way there to replicate some of the important Jupyter/Python idioms. Basically the use case is printing a handful of things rather than just the last value in the cell. Of course the other common use case -- showing progress of a long-running process -- won't be possible. But I think in the short term we should just document that shortcoming and not let it hold up the other uses.

This all comes back around to being another downside of running everything on the main thread, I think, though the good reasons for doing that have been discussed and investigated very thoroughly elsewhere.

mdboom commented 6 years ago

A possible solution (if the code transformation can be worked out in a generic way): https://gist.github.com/mdboom/33ba964e67a9e8a621fcfac4f478f567

bcolloran commented 6 years ago

yep, this is defintely a consequence of updating on the main thread. :-/ in the very first version of Iodide, we used https://github.com/NeilFraser/JS-Interpreter to do a version of code transformation (that thing also e.g. allowed us to abort running code). That particular thing also killed the performance of evaled code. We could experiment with approaches like that again, but obviously there may be no way to do that without introducing a lot of overhead and tanking performance. But even then, maybe there is some kind of UX that makes it palatable (perhaps you can select whether to run a cell in interpreter mode or bare metal mode).

@hamilton i've thought about this some more, and i think i'm not inclined to attempt to implement this within our normal react for the following reasons: 1) after having played around with the direct DOM manipulation version last night and read more about the event loop and stuff, i'm basically positive that for use cases like creating one or more elements to be targeted for other manipulation, like in this example --

x = [1, 2, 3, 4, 5]
for (i=0; i<10; i++){
  Plotly.plot( iodide.DOM.element("div"), [{
    x: x,
    y: x.map(() => Math.random()) }], {
    margin: { t: 0 } } );
}

-- that the DOM elements created will not reliably be available to be mutated by subsequent code in the same eval call, since Reacts internal task/render queue is not at all guaranteed to put element creation on the browser task queue in the order needed for user-evaled code 2) obvi React is designed around the idea that you use React to manipulate all of you DOM, so creating React components and handing refs to them back to users to manipulate at they wish might create weir behavior. Read about this some here -- https://reactjs.org/docs/integrating-with-other-libraries.html -- it is possible to use react with libraries that manipulate the DOM directly, but the examples given all show that the components to be manipulated are specially tailored to the library used. Since we want to offer up elements to be manipulated by arbitrary libraries (jquery, d3, etc), we can't tailor our components to be aware of those libraries and what they might do to the DOM, so it might get hairy 3) i think the conceptual distinction i made remains reasonable (if blurry) -- that this API / set of APIs is about handing a little piece of DOM over to the user, and so it's kind of "user DOM" not "iodide DOM" (it kind of comes from user eval-ed code, not Iodide code), and doesn't necessarily need to impinge upon global state and long we maintain that firewall around it.

anyhoo, i consider this branch a very rough draft, and i'm not wedded to any part of the design. I don't think it's ready for a PR, so please all feel very welcome to hack on / fork this branch and experiment with it.

@hamilton in particular -- perhaps you see a clear path to adding this to redux state that i'm just missing, in which case i'm very open to that. but i'm not seeing a way to do it that justifies how complicated it's looking to me ATM. also, please try this out with some async stuff, and/or give me some ideas about what those use cases (or example notebooks). also also: this small gist works fine with run all (but no fancy async stuff is used) https://gist.github.com/bcolloran/db37ddc20ca150ede51cffe4fa2081f8

bcolloran commented 6 years ago

perhaps creating a closure around eval along these lines would help us here:

https://stackoverflow.com/questions/9781285/specify-scope-for-eval-in-javascript

bcolloran commented 6 years ago

tried the closure idea -- it does not super much appear to work for our use case (not surprising)-- the inner scope prevents variables/functions defined in the inner eval from being available to in the global scope (and hence to other evals)

https://extremely-alpha.iodide.io/notebooks/6/

bcolloran commented 5 years ago

i think at this point these challenges are known, and we know there is no solution other than to document the weirdness and edge cases in our dom manipulation workflow, and to nudge people towards using html in MD and to explicitly target elements created in that way if they need fine grained control.