[ANN] Js_of_ocaml 5.1

I’m pleased to announce the release of js_of_ocaml 5.1. It should soon be available in opam.

Js_of_ocaml is a compiler from OCaml bytecode to JavaScript. It makes it
possible to run pure OCaml programs in JavaScript environment like browsers and
Node.js.

This release includes many significant changes. Here are some of the notable ones:

  • Js_of_ocaml now understands most es6 features (but import and export)
  • JavaScript files generated by js_of_ocaml are now UTF-8 encoded.
  • Change the memory representation of OCaml strings to use JS ones. String still represent sequences of bytes and only contains codepoints in the range [0-255].
  • Improved support for OCaml 5 effects, performing partial CPS transformation to significantly improve perfs (see http://ocsigen.org/js_of_ocaml/5.1.0/manual/performances)
  • Various improvements to make the compiler faster.
  • Separate compilation only link compilation units that are needed, similar to ocamlc, and generate much smaller js files

See the Changelog for other changes.

29 Likes

This is a great release, UTF8 encoded and OCaml strings being JavaScript ones is going to get stuff much easy.

Thanks @hhugo

2 Likes

So is this line from the performance page still correct?

String operations are also slow (splay), as OCaml strings cannot be mapped directly to Javascript immutable UTF-16 strings.

Also, now that CPS transformation is partial, when effects are enabled, are all functions still tail call optimized with effects enabled?

This sounds like a great change! It should help a lot with bundle size. I have a couple of questions though:

  • What does “compilation unit” mean in this context? Are we talking about individual .cmo files or something more/less granular?
  • Is there any way to get a list of what does end up getting linked? This should be helpful for bundle size analysis which is something I’ve been looking for a tool for a while.

What does “compilation unit” mean in this context? Are we talking about individual .cmo files or something more/less granular?

Yes, it means individual cmo. It works exactly like ocamlc.

Is there any way to get a list of what does end up getting linked? This should be helpful for bundle size analysis which is something I’ve been looking for a tool for a while.

You can use --debug link when calling js_of_ocaml link.

Calling ocamlobjinfo on the bytecode executable should give you the list as well.

2 Likes

Thanks!

Is this in opam yet? I’m not sure if I’m doing everything right:

pam  update; opam list | grep js_of_ocaml

<><> Updating package repositories ><><><><><><><><><><><><><><><><><><><><><><>
[default] no changes from https://opam.ocaml.org

<><> Synchronising development packages <><><><><><><><><><><><><><><><><><><><>
[ojs.1.1.2] synchronised (no changes)
[gen_js_api.1.1.2] synchronised (no changes)
js_of_ocaml                 5.0.1               Compiler from OCaml bytecode to JavaScript
js_of_ocaml-compiler        5.0.1               Compiler from OCaml bytecode to JavaScript
js_of_ocaml-ppx             5.0.1               Compiler from OCaml bytecode to JavaScript
js_of_ocaml-toplevel        5.0.1               Compiler from OCaml bytecode to JavaScript

or is the current way to install an opam pin js_of_ocaml ...#5.1 ?

Yes, It is already in opam, You should run opam update, not pam update

My typo. In the meanwhile, I tried this:


 opam pin add js_of_ocaml https://github.com/ocsigen/js_of_ocaml.git#5.1.0 ; js_of_ocaml --version
[NOTE] Package js_of_ocaml is already pinned to git+https://github.com/ocsigen/js_of_ocaml.git#5.1.0 (version
       5.0.1).
[js_of_ocaml.5.0.1] synchronised (no changes)
[WARNING] Failed checks on js_of_ocaml package definition from source at
          git+https://github.com/ocsigen/js_of_ocaml.git#5.1.0:
  warning 62: License doesn't adhere to the SPDX standard, see https://spdx.org/licenses/: "GPL-2.0-or-later AND
              LGPL-2.1-or-later WITH OCaml-LGPL-linking-exception"
js_of_ocaml is now pinned to git+https://github.com/ocsigen/js_of_ocaml.git#5.1.0 (version 5.0.1)

[NOTE] External dependency handling not supported for OS family 'nixos'.
       You can disable this check using 'opam option --global depext=false'
No package build needed.
Nothing to do.
5.0.1

It’s not clear why I am getting 5.0.1

 opam update ; opam list | grep js_of_ocaml

<><> Updating package repositories ><><><><><><><><><><><><><><><><><><><><><><>
[default] no changes from https://opam.ocaml.org

<><> Synchronising development packages <><><><><><><><><><><><><><><><><><><><>
[ojs.1.1.2] synchronised (no changes)
[gen_js_api.1.1.2] synchronised (no changes)
[js_of_ocaml.5.0.1] synchronised (no changes)
js_of_ocaml                 5.0.1               pinned to version 5.0.1 at git+https://github.com/ocsigen/js_of_ocaml.git#5.1.0
js_of_ocaml-compiler        5.0.1               Compiler from OCaml bytecode to JavaScript
js_of_ocaml-ppx             5.0.1               Compiler from OCaml bytecode to JavaScript
js_of_ocaml-toplevel        5.0.1               Compiler from OCaml bytecode to JavaScript

In retrospect, I think I typed the right command of “opam”, not “pam” (based on output), but lost 1 character when I did copy-paste.

The short answer is no.

I don’t know what was wrong with my opam setup, but doing a rm -rf ~/.opam and reinstalling everything fixed it. Now running js_of_ocaml 5.1.0 :slight_smile:

I think this is still correct to some degree. But one would need to rerun the benchmark. String operation are still slow. Doing String.concat still has to go through a Bytes.t instead of simply concatenating js strings.

Regarding --debug link and bundle size, is there a way to know which files contribute the most to the size of the resulting JS file? I currently have a 50MB JS generated file by jsoo and I’d like to know which code I should optimize spacewise.

2 Likes

You might want to have a look at GitHub - Drup/modulectomy: Dissect OCaml compiled programs, and weight their content (never tried it myself)

Is the 50MB JS file the result of separate compilation or whole program compilation ? Do you happen to use ocaml -linkall ? js_of_ocaml --toplevel or --enable effects flags ?

Separate compilation with dune will emit pretty printed js which will be larger.

The file is actually 16MB when using dune build --profile=release.
We don’t use -linkall, nor --toplevel, nor --enable-effects.

modulectomy seems nice but it’s 4 years old, requires patch versions to jsoo and other libs,
and is not available in OPAM, so seems pretty difficult to test.

Then I don’t have an easy way to do it

Building with separate compilation and looking at the size of individual cmo.js or cma.js can give you some hints. You probably want to disable sourcemap when doing the experiment.
The list of files can be found in the arguments of the js_of_ocaml link comand in the logs of dune build --verbose

To get the size of individual unit inside an a cma, you can follow the step from RFC: ecmascript modules as primary build artifact · Issue #1161 · ocsigen/js_of_ocaml · GitHub

Feel free to open a feature wish on the jsoo tracker

@aryx, quick hack using this branch of jsoo GitHub - ocsigen/js_of_ocaml at size-from.
Used it on the jsoo toplevel:

(cd _build/default/toplevel/examples/lwt_toplevel && ../../../../install/default/bin/js_of_ocaml link ... ... ....  --debug link) 2>&1 | grep "bytes from " | sort -uhr
10633234 bytes from of Js_of_ocaml__Dom_html
3663300 bytes from of Js_of_ocaml_lwt__Lwt_js_events
3591840 bytes from of Js_of_ocaml_tyxml__Tyxml_js
3486066 bytes from of Js_of_ocaml__Dom_svg
2210868 bytes from of Html_f
2062895 bytes from of Ppxlib_ast__Ast
1731952 bytes from of Js_of_ocaml_compiler__Flow_lexer
1668692 bytes from of Dynlink_compilerlibs
1519059 bytes from of Ppxlib_ast__Versions
999805 bytes from of Yojson
994043 bytes from of Parser
903526 bytes from of Svg_f
799511 bytes from of Ppxlib
754869 bytes from of Ctype
708102 bytes from of Typecore
....
282030 bytes from of Pprintast
269338 bytes from of Matching
257545 bytes from of MenhirLib
255533 bytes from of CamlinternalMenhirLib
240519 bytes from of Typeclass
237466 bytes from of Misc
235979 bytes from of Typedecl
235418 bytes from of Js_of_ocaml_compiler__Js_output
....
26752 bytes from of Printpat
26474 bytes from of Js_of_ocaml_compiler__Config
26362 bytes from of Js_of_ocaml_compiler__Unit_info
26315 bytes from of Primitive
26306 bytes from of Stdlib__Random
26041 bytes from of Stdlib__Lexing
25922 bytes from of Lwt_result
25806 bytes from of Compile_common
25699 bytes from of Load_path
25065 bytes from of Js_of_ocaml_compiler__Pseudo_fs
24863 bytes from of Js_of_ocaml_compiler__Partial_cps_analysis
24819 bytes from of Datarepr
....
11128 bytes from of Stdlib__Either
11096 bytes from of Js_of_ocaml_compiler__Mlvalue
11055 bytes from of Js_of_ocaml__PerformanceObserver
11003 bytes from of Js_of_ocaml_compiler_dynlink
10690 bytes from of Xml_stream
10673 bytes from of Stdlib__Digest
10508 bytes from of Stdlib__Int
10465 bytes from of Stdlib__Option
10362 bytes from of Typedecl_immediacy
10325 bytes from of Ppxlib__Caller_id
10145 bytes from of Stdlib__Stack
10118 bytes from of Js_of_ocaml_compiler__Ocaml_version
9922 bytes from of Ppxlib__Merlin_helpers
9582 bytes from of Astlib__
9518 bytes from of Dune__exe__Indent
9068 bytes from of Ppxlib__File_path
9040 bytes from of GenM_intf
8864 bytes from of Ppxlib__Loc
8844 bytes from of Stdlib__Semaphore
8809 bytes from of Stdlib__Complex
....
1192 bytes from of Ppx_js__
1171 bytes from of Ppxlib__Keyword
1034 bytes from of Js_of_ocaml_toplevel
824 bytes from of Std_exit
788 bytes from of GenShims_
233 bytes from of Dynlink_platform_intf

Note that some numbers are big because some modules embed there own cmis.

wow, super nice! can’t wait for 5.1.2 :slight_smile: