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

Web Worker support #970

Closed
Tarmil opened this Issue Jun 27, 2018 · 10 comments

Comments

Projects
None yet
4 participants
@Tarmil
Member

Tarmil commented Jun 27, 2018

Here is what I think should be our plan for implementing Web Workers support, as requested notably in #641. @Jand42 Thoughts?

  1. Ensure that the compiler and libraries always reference the global scope as self rather than window. This is universally supported and WW-compatible, whereas window only works on the main thread.

    I think this point alone should already be helpful for users who want to compile a WebSharper bundle and use it as a web worker instantiated from plain JavaScript or TypeScript.

  2. Implement the WW APIs in WebSharper.JavaScript.

  3. Add to the macro API the capability to create a separate DCE'd bundle with the given entry point. This bundle needs to be:

    • Written next to the main bundle, if we're compiling in Bundle mode.

    • Embedded in the assembly, if we're compiling in Library mode.

    • Extracted next to the assembly's main file, if we're compiling in Site or Html mode.

    Ideally this bundle should be able to load external resources using importScripts(); although I think it would be acceptable to leave this feature aside in the initial implementation.

  4. Add a macro that uses the above API to create a web worker. Its use would look like this:

    let worker = new Worker(fun self ->
        self.Onmessage <- fun e ->
            self.PostMessage(e.Data)
    )
    
    worker.Onmessage <- fun e ->
        Console.Log("The worker replied", e.Data)
    
     // Prints "The worker replied", "Hello world!" to the console:
    worker.PostMessage("Hello world!")

    This would create a bundle whose entry point (and basically whole contents) would be the fun self lambda, and the new Worker(fun ...) F# code would be compiled to new Worker("url/to/this/bundle.js") in js.

Unresolved questions

  1. How to ensure the correct relative URL to the bundle is passed to the worker ctor? This might need another compiler option, eg wsconfig.json might need to be extended like this:

    {
      "project": "bundle",
      "outputDir": "wwwroot/Content",
      "scriptBaseUrl": "Content"
    }
  2. Regarding external dependencies, is the WW API's importScripts() enough for us? It should be enough for BaseResource dependencies (ie resources defined only by a URL or a set of URLs). It's not enough for arbitrary IResources (ie resources defined by arbitrary HTML to insert in the page), but those are quite rare; the only ones I can think of are Google Visualization and the form validation shim in UI, both of which wouldn't make sense in a WW anyway. So only supporting BaseResource is fine by me.

    Also, the relative URL question also stands here.

@cgravill

This comment has been minimized.

cgravill commented Jul 5, 2018

  1. is definitely good. It would remove workarounds we've had to use
  2. sounds good too, being able to access more of this in a type-safe way is always welcome
  3. & 4.

at the moment, the contents of our web workers are entirely self-contained. It's existing and maintained code written in F# and doesn't need to call into other JavaScript/importscripts (though we do patch in some WebAssembly implementations).

For us, we're putting all the F# into the web worker, so a module might have several entry points, if we can do the same sort of set-up as #899 that would be great.

For other projects we may want to use UI.Next on the front-end, and parts of that as a web worker. For now we handle the creation and communication with Web Workers in TypeScript. One reason being we have a pool of Web Workers to avoid costly set-up but also allow concurrency.

That also means that the scriptBaseUrl seems to be out of scope for us. The only request being for us to be able to call script ourselves. If more of this could be handled automatically that would be great but practically we may have to maintain control of this to fit in with our TypeScript.

@Tarmil

This comment has been minimized.

Member

Tarmil commented Jul 5, 2018

Just to make sure that I understand your use case properly: when you need some work to be done by a WW, you send a postMessage to an available WW in your pool with a message identifying which entrypoint to use and presumably some arguments. Is that correct?

I think you can still take advantage of 3+4 to not need a TypeScript bridge between your UI(.Next) frontend and your workers, while still being able to use the same worker script directly from TypeScript in another project. As it currently stands, you can give a name to a worker:

let worker = new Worker("my-worker-name", fun self ->
    // use self.Onmessage to dispatch work...
)

and the resulting script will be deterministically extracted as <outputDir>/<assemblyName>/<assemblyName>.my-worker-name.js, so you can reliably use this file from TypeScript too.

@cgravill

This comment has been minimized.

cgravill commented Jul 6, 2018

Yes, approximately. We have a set of messages, that get some processing and then call into some number of methods. It's not a trivial mapping though. For example, we reshape some exceptions to be more JavaScript compatible.

Sounds good on being able to name and target from TypeScript. Having a very explicit "access point" would be good as we're somewhat relying on WebSharper's implementation details which can cause some challenges.

How does the named worker interact with DCE? Can we somehow indicate a given worker relies on a full module - as on #899 - with something analogous to JavaScriptExport? How would we annotate which functions go to which web worker? Or would we need to have say an F# dummy function that touches each of the dependent functions to ensure it's extracted into the bundle for TypeScript? The set of functions used by WebSharper.UI and "TypeScript" UI may not fully intersect.

@Tarmil

This comment has been minimized.

Member

Tarmil commented Jul 6, 2018

Currently JavaScriptExport entries are not included in the worker bundle, but I can easily add the option to include them.

@FilippoPolo

This comment has been minimized.

FilippoPolo commented Jul 7, 2018

Hi Tarmil,

I'm one of Colin's colleagues, working on some of the same projects. We have a front-end that is designed to be able to use either a web worker or a web server as its back-end. Currently, we accomplish this with quite a lot of TS glue code on both sides, because we need to bridge differences between the two modes.

  1. As Colin explained, we have a non-trivial threading model. When running in web worker mode, we implement a threadpool of web workers to run a queue of concurrent tasks. Currently, this must be implemented in TS, so it's a fairly large chunk of our glue code. Your points 3 & 4 would allow us to move it from TS to F#, which would be very nice for us.

  2. In the model you're describing, would we be able to pass F# objects that contain non-serialisable fields (i.e. functions) between two web workers, if both web workers were built by WebSharper from the same assembly? Currently, if we want to run a task in a secondary worker, we have to gather its data in a form that's easily serialised, ship it, and then reconstruct the original object. That's not always easy.

(note that currently Chrome doesn't support nested workers, which is probably a blocker for this regardless of what WebSharper does - but they're working on it, and other browsers do support nested workers).

  1. Another major problem we have is handling differences in serialisation formats. This is relevant to web workers because it's the base of message passing. When running as web worker, we use WebSharper's JSON serialiser generators to create JSON objects for the front-end. When running as web server, we use the serialiser from Newtonsoft.Json. The two serialisers have significant differences, which we have to bridge on the front-end side; because some type information is lost in serialisation, this requires handmade code, significant maintenance, and frequently causes mysterious runtime errors.

We're not particularly attached to Newtonsoft.Json, but the WebSharper serialisers cannot be used in .NET; they are only generated for JS. It would be extremely helpful for us if we could be able to generate the same JSON string for an arbitrary (serialisable) F# object, both from WebSharper-generated JS code and from .NET. With this new push on web workers, would you consider a revamp of JSON serialisation with the aim of providing uniform behavior between F#-on-.NET and F#-on-WebSharper?

@Tarmil

This comment has been minimized.

Member

Tarmil commented Jul 9, 2018

In the model you're describing, would we be able to pass F# objects that contain non-serialisable fields (i.e. functions) between two web workers, if both web workers were built by WebSharper from the same assembly?

What you can do is use WebSharper.Json.Encode to convert an F# value to a plain JSON object, pass that as message, and decode it on the other side with WebSharper.Json.Decode. That will restore instance methods on the object. Unfortunately directly passing a function is not allowed by the Web Worker API, because objects passed as messages must be cloneable.

We're not particularly attached to Newtonsoft.Json, but the WebSharper serialisers cannot be used in .NET; they are only generated for JS.

That used to be the case, but it is not true anymore. WebSharper.Json.Serialize and WebSharper.Json.Deserialize are also implemented on the server side, so you can already handle this uniformly. In fact, if you have a Sitelet server side, Content.Json as well as [<Json>] endpoints use this same format.

@FilippoPolo

This comment has been minimized.

FilippoPolo commented Jul 9, 2018

What you can do is use WebSharper.Json.Encode to convert an F# value to a plain JSON object, pass that as message, and decode it on the other side with WebSharper.Json.Decode. That will restore instance methods on the object. Unfortunately directly passing a function is not allowed by the Web Worker API, because objects passed as messages must be cloneable.

That's fine. What if the F# object graph contains loops? Is that handled?

That used to be the case, but it is not true anymore. WebSharper.Json.Serialize and WebSharper.Json.Deserialize are also implemented on the server side, so you can already handle this uniformly. In fact, if you have a Sitelet server side, Content.Json as well as [] endpoints use this same format.

That's really good to know! We don't have a Sitelet, but access to WebSharper.Json should be enough.

@FilippoPolo

This comment has been minimized.

FilippoPolo commented Jul 9, 2018

Another message passing issue came to mind - what if the F# object contains actual function values (as opposed to methods)? Off the top of my head, you'd have to somehow create an identifier for the function, bundle that with the function closure, serialise that; then deserialise it, use the identifier in a lookup and unbundle the closure... that sounds rather tough. I suppose this means that function fields are not supported by serialisation?

@Tarmil

This comment has been minimized.

Member

Tarmil commented Jul 9, 2018

That's fine. What if the F# object graph contains loops? Is that handled?

Not currently, no. But we have been talking for a long time about providing an API to customize serialization for a given type, and it's high time we put forward a proposal for it.

@Tarmil

This comment has been minimized.

Member

Tarmil commented Jul 9, 2018

Indeed, function fields are not supported by WebSharper.Json.

@Tarmil Tarmil closed this Jul 11, 2018

@Tarmil Tarmil added the 4.4.0.280 label Jul 12, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment