Speedscope export#49
Conversation
Adds `format=speedscope` to `OCAML_LANDMARKS`, which writes a sampled flame-graph profile openable at https://www.speedscope.app. Implementation: - `src/speedscope_fmt.atd` — ATD schema for the Speedscope file format (source of truth; regenerate with `atdml speedscope_fmt.atd`) - `src/speedscope_fmt.ml[i]` — generated types + Yojson serialisers, tracked in git so the build has no atdml dependency - `src/graph.ml` — `Graph.Speedscope` submodule: DFS over the call graph producing one sampled stack per node with positive self-time; uses `sys_time` (seconds) when collected, CPU cycles (`unit: none`) otherwise - `src/landmark.ml[i]` — new `Speedscope` variant of `profile_format`, env-var parsing, at-exit hook wiring - `src/dune` / `dune-project` — add `yojson` dependency - `tests/speedscope/` — hand-instrumented example with pre-generated `profile.json` checked in for read-without-running convenience Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move Speedscope export to a standalone Speedscope module (src/speedscope.ml[i]) instead of a submodule of Graph; expose it as Landmark.Speedscope - Regenerate speedscope_fmt.ml[i] using atdml (fixed ATD syntax: annotations need '=', 'shared' is a reserved ATD keyword, 'None' shadows Stdlib.None) - Rename ATD fields per review: unit_ → unit, dollar_schema → schema, shared → profile_shared; use None_ for the "none" value_unit variant - Add link to the TypeScript spec in the ATD file header - Wrap Speedscope doc comment in landmark.mli to ≤80 columns - Rewrite example using PPX [@landmark] decorators with a 3-level call tree (main → prepare/run/summarise, run → phase_a/phase_b) - Add deterministic test (test.ml builds a fixed Graph and exports it; test.out.expected is checked in and verified by dune runtest) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All applicable field and type comments from file-format-spec.ts are now expressed as ATD doc annotations and flow through into the generated speedscope_fmt.mli as OCaml doc comments. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move long URLs in the file header onto their own indented lines - Condense <doc text="..."> strings that exceeded the column limit - Regenerate speedscope_fmt.ml[i] Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Do you think that a lot of infrastructure is missing so that new export formats could be implemented as a separate library (we can still keep it inside this dune project)? In that case, using |
|
Sorry for the late reply [GitHub did not email me because I have a tab open. Maybe I fixed it now?]
Yes, I think so. I'll update the PR accordingly. There will be a new Here's my reasoning: At first glance, the main issue is here: let () = match Sys.getenv "OCAML_LANDMARKS" with
| exception Not_found -> ()
| str ->
try start_profiling ~profiling_options:(parse_env_options str) ()
with Exit -> ()This allows custom exporters (via something like I can think of the following solutions:
I think the cleanest solution is (1) but it breaks backward compatibility. As a workaround, what we could do is:
This is a little contrived but doesn't add much code and allows arbitrary exporters to be registered by the user. To recap, the behavior of the proposed solution is:
I'll amend the current PR to implement this. Update: the solution actually is much simpler since the core library knows the list of possible exporters. The initialization done with |
Landmark.init() is called both at module-load time (via 'let () = init ()') and again by Landmarks_exports.init(), which caused start_profiling to be called a second time while profiling was already running. Fix: guard init() with 'if not !profiling_ref' so it is idempotent — the second call (after the exporter is registered) becomes a no-op, which is correct because profiling options were already applied from OCAML_LANDMARKS at startup. Also add a runtest rule for the example under (package landmarks-exports): runs the example with format=speedscope and verifies exit code 0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
I think I'm done with this revision. It includes:
Naming issue: the main module of the |
|
If we want to move toward an explicit
Existing users of the The main advantage of this setup is that users could register various custom exporters without the Possible refinement to avoid runtime conflicts
This would be useful if an external library is instrumented with This may be overkill, though. |
Hello @mlasson,
On @nojb's suggestion, I added Speedscope export to Landmarks. Here's a PR that I created with AI assistance. It's ready for review. Please let me know what you think.
Supported format
Among the various formats supported by Speedscope, three of them are documented as generic, and one of them is Speedscope's own format. I decided to target it and it seems to work fine. I don't have big programs to test it on, though.
JSON support
The original implementation uses it own tiny implementation for exporting to JSON. I'm not sure why but if I assumed it's a matter of reducing external dependencies. Keeping this in mind, here's what I did:
atdmlis not an Opam dependency of thelandmarkspackage. A call toatdml speedscope_fmt.atdwill regeneratespeedscope_fmt.mliandspeedscope_fmt.mlwhen the need arises. These two files are kept alongside source code insrc/.Example
Download
tests/speedscope/profile.jsonand then upload it at https://www.speedscope.app/.Code quality
I paid reasonable attention to the interfaces and to the tests. I did not review
src/speedscope.mlclosely. Trying it out on non-trivial example would be great.