How to deal with blocking JS code (using ports)?


#1

I am a beginner to both Elm and JavaScript. On the personal project that I am trying to make, there are calculations in JS that could take a long time (~ 30 secs or longer), and I am using Elm for the UI.

The calculation will start as soon as the page is loaded, and there will be a “waiting…” message rendered by Elm. How to ensure that the calculation will start after Elm rendered the message?

I know that one possible solution is to wrap all the calculation inside app.ports.infoForOutside.subscribe (in the style of Murphy’s talk) and use elm to decide when the function runs. But I still have two questions regarding that:

  1. How do I know when Elm finishes rendering?
  2. How can I use ports to implement a progress-bar like feature?

Regarding the second question, my calculation is roughly like

for (i = 0; i < 100000; i++) { 
    status = some_calculation(status);
    if (debug_condition_met(status)){
        ask elm to render the progress bar and wait for it to render
    }
}

Thank you!


#2

I’d be looking at something along the lines of: (note I haven’t run this at all but the approach should be clear)

    module Main exposing (..)


    type alias Model =
        { calculation : CalculationState }


    type CalculationState
        = InProgress Int
        | Complete Float


    port calculate : () -> Cmd msg


    port calculationProgress : (Int -> msg) -> Sub msg


    port calculationResult : (Float -> msg) -> Sub msg


    init : ( Model, Cmd Msg )
    init =
        ( { calculation = InProgress 0 }
        , calculate ()
        )


    update msg model =
        case msg of
            CalculationProgressReceived progress ->
                ( { model | calculation = InProgress progress }, Cmd.none )

            CalculationResultReceived result ->
                ( { model | calculation = Complete result }, Cmd.none )


    view : Model -> Html Msg
    view model =
        case model.calculation of
            InProgress progress ->
                inProgressView progress

            Complete result ->
                resultView result


    inProgressView : Int -> Html Msg
    inProgressView progress =
        div
            []
            [ h1 "In Progress"
            , text <| toString <| progress
            ]


    resultView : Float -> Html Msg
    resultView result =
        div
            [ h1 "Result" ]
            [ text <| toString <| result ]


    type Msg
        = CalculationProgressReceived Int
        | CalculationResultReceived Float


    subscriptions =
        Sub.batch
            [ calculationProgress CalculationProgressReceived
            , calculationResult CalculationResultReceived
            ]

#3

Basically it is very similar to my code and it can be done with one port.
But how will Javascript know if the rendering is done?

If this code is applied directly and due to the async nature of Javascript, the Javascript scheduler may start the calculation first and the user will receive a blank screen for 30 seconds. It happened on my side in Firefox, hence I want to make sure the order to execution.

How can it be done with ports?


#4

It’s maybe a bit overkill but you could attach a subscription to animation frame using this. Example pseudo code:

import AnimationFrame

type alias Model =
    { started : Bool
    }

type Msg
    = FirstFrame

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        FirstFrame ->
            ( { model | started = True }, computeInJS )

port computeInJS : () -> Cmd msg

subscriptions : Model -> Sub Msg
subscriptions model =
    if model.started then
        Sub.none
    else
        AnimationFrame.times (always FirstFrame)

#5

I am also working with long running computations that can block the UI thread, although in my case I am doing it purely in Elm.

The way I handle the long running process is to split it into smaller parts. So instead of running all 100000 as in your example, I might run 1000 batches of 100 steps each. At the end of each batch, I return control to Elm runtime (by returning from my update function). I think you could use a similar approach?

There are sopme things I have found with this approach which are worth sharing with you:

  1. Splitting into small batches causes a loss of efficiency. I don’t really want to run the Elm runtime loop during my calculation loop - I’d much rather have a CPU core run my calculation loop undisturbed.
  2. Making the batches bigger is more efficient, but makes the UI much less responsive. You would expect the UI to become more unresponsive as more CPU calculation work is done, however the responsiveness changes quite dramatically. My current thinking is that garbage collection pressure is really impacting the UI performance beyond some threshold.
  3. This is explicit time-slicing multi-tasking by the application code. Its complicated and not really code that you want to be writing.

I am thinking of trying to move the calculatiopn code into a separate Elm process running under a web worker thread, and having the main UI thread and the web worker communicate over ports. I have not tried this web worker thread approach yet, but I am hoping it will perform a lot better.

I am interested in what you write above. If you have code like this, what actually happens?

for (i = 0; i < 100000; i++) { 
    status = some_calculation(status);
    if (debug_condition_met(status)){
        elmApp.ports.calculationProgress.send(i);
    }
}

Does the Elm runtime enqueue a message on the progress port, but the loop continues to run? And only once the calculation is complete does the Elm runtime become unblocked and able to process the messages?


#6

I know it’s a silly question but have you thought about using a webworker, that way you wouldn’t block the UI thread? When I last checked the other day I was quite surprised to see that these are available pretty much everywhere nowadays :slight_smile:


#7

I think your overthinking it. Data sent over ports is non-blocking by default - there is no response to wait for, so your JS calculation can do what it wants without affecting Elm.

Why do you need to know?

I use ports and Ajax to upload files with a progress bar in Elm.

The data sent from JS to Elm over the port is fast, so you don’t need to wait for it to render inside your JS calculation before doing anything.

You could just do something like this inside your calculate function:

elmapp.ports.calculationProgress.send(i)

Then in Elm your subscriptions could include a line like:

calculationProgress (\ progress -> ProgressMsg progress)

Your Msg definition could be something like:

type Msg
    = ...
    | ProgessMsg Int
    ...

And then you can update the progress in your model, and your view will render with the updated progress.


#8

Sorry I was caught up on work in the last few months. Thanks for the suggestion.

Does the Elm runtime enqueue a message on the progress port, but the loop continues to run? And only once the calculation is complete does the Elm runtime become unblocked and able to process the messages?

Yeah, this was exactly what happened.


#9

Sorry I was caught up on work in the last few months.
It seems that your case is quite different on mine. File uploading is disk and network bounded, but calculation is pure CPU computation.

I started with something like this but it keeps calculating in JS before Elm can process anything.