Skip to content

Improved profiler (support for "Cpuprofile" and "Folded" formats) - #2956

Open
sungshik wants to merge 9 commits into
mainfrom
improved-profiler
Open

sungshik wants to merge 9 commits into
mainfrom
improved-profiler

Conversation

@sungshik

@sungshik sungshik commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR adds support to the profiler to write profiles in formats that can be visualized as flame charts/graphs by other tools. Two formats are currently supported:

  • The cpuprofile format originates from the Chrome Dev Tools (CDT) protocol. Files written in this format can be opened, for instance, in VS Code using the "Flame Chart Visualizer for JavaScript Profiles" extension, or in Chrome using the built-in developer tools. Example:
    profile
    Note: The video demonstrates the flame chart for a typecheck of bird-core. I cut a few frames in the middle of the video, though, to skip 45 seconds of typechecking time.

  • The folded format originates from Brendan Gregg's original flamegraph.pl script. Files written in this format can be post-processed by that script to SVG. Example:
    profile-latest

Anecdotally, the profiler adds 10% runtime overhead.

Design/implementation notes

  • Intuitively, each profiling run consists of two phases: (a) while the evaluator is running, the sampler produces a stream of samples, each of which contains the call stack and a timestamp; (b) after the evaluator has run, the formatter consumes the stream of samples and generates the formatted output.

  • Following these two phases, new package org.rascalmpl.interpreter.utils.profiler consists of new interfaces Sampler and Formatter, as well as their implementations. Through these interfaces, sampling and formatting are decoupled as much as possible.

  • To keep memory pressure low, samples are temporarily written to a file in the sampling phase, and lazily read from that file in the formatting phase, so samples don't need to be kept in memory all at the same time.

  • IO with the temporary file, as well as de/serialization (using base64-encoded IValues to avoid the need to pretty-print/parse ISourceLocations), are done in a separate auxiliary thread to not block the main thread.

  • Following the existing naming conventions, there are two new configuration properties:

    • rascal.profileFmt controls the format of the profile. Possible values: "none", "folded", and "cpuprofile" (default).
    • rascal.profileLoc controls the directory to which formatted profiles can be written. Possible values: any location (default: |cwd:///|).

    Like other configuration properties, the new ones can be set when the REPL process is started (e.g., '-Drascal.profileFmt="folded"' and -Drascal.profileDir=|home:///Desktop|) or when the REPL process is running (e.g., :set profileFmt "folded" and :set profileDir |home:///Desktop|).

  • To support strings and locations as arguments of -Drascal.xyz=... and :set, the existing code for configuration properties was adapted a bit (minor changes).

TODO

  • Add license headers
  • Update documentation on the website

@codecov

codecov Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.27824% with 97 lines in your changes missing coverage. Please review.
✅ Project coverage is 46%. Comparing base (e2aeef0) to head (6b74c45).

Files with missing lines Patch % Lines
src/org/rascalmpl/interpreter/Configuration.java 38% 22 Missing and 5 partials ⚠️
src/org/rascalmpl/interpreter/utils/Profiler.java 60% 14 Missing and 4 partials ⚠️
.../org/rascalmpl/semantics/dynamic/ShellCommand.java 30% 15 Missing and 1 partial ⚠️
...nterpreter/utils/profiler/CpuprofileFormatter.java 88% 7 Missing and 6 partials ⚠️
...pl/interpreter/utils/profiler/TempFileSampler.java 83% 8 Missing and 3 partials ⚠️
.../rascalmpl/interpreter/utils/profiler/Sampler.java 83% 2 Missing and 5 partials ⚠️
...mpl/interpreter/utils/profiler/DummyFormatter.java 50% 2 Missing ⚠️
...ascalmpl/interpreter/utils/profiler/Formatter.java 66% 2 Missing ⚠️
...pl/interpreter/utils/profiler/FoldedFormatter.java 93% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##              main   #2956    +/-   ##
========================================
  Coverage       45%     46%            
- Complexity    6815    6885    +70     
========================================
  Files          844     851     +7     
  Lines        68826   69174   +348     
  Branches     10040   10075    +35     
========================================
+ Hits         31434   31993   +559     
+ Misses       34995   34756   -239     
- Partials      2397    2425    +28     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sungshik
sungshik marked this pull request as ready for review October 29, 2026 11:50
@sonarqubecloud

Copy link
Copy Markdown

@DavyLandman DavyLandman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks nice, I think my biggest question is about the raw JSON output in the CpuProfileFormatter and the base64 in json overhead for the tempfile.

@@ -0,0 +1,243 @@
package org.rascalmpl.interpreter.utils.profiler;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all the new files need a license header (see other new files in for example the maven code for one that's okay)

Comment on lines +50 to +54
public final Set<ProfileNode> nodes; // CDT protocol
public final long startTime; // CDT protocol
public final long endTime; // CDT protocol
public final List<Integer> samples; // CDT protocol
public final List<Long> timeDeltas; // CDT protocol

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the reason all these fields (and the one of the other class) are all commented that they are "CDT protocol"?

this.timeDeltas = timeDeltas;
}

public void write(Writer w, int tabs) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the indentation needed?

appendlnUnchecked(w, indent(tabs, " \"nodes\": ["));

var iterator = nodes.iterator();
while (iterator.hasNext()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not use a JSON writer? We already have gson dependencies, and we there is a gson stream writer that would allow us to abstract away from the json serialization?

Or is there a specific reason we need to be this frugal?

this.children = new ArrayList<>();
}

public ProfileNode addChildIfAbsent(ProfileNode child) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this function should be renamed, because externally it behaves more like a "lookup"/memo/identity?

Comment on lines +48 to +50
// Configure the handle to store samples on disk as base 64-encoded `IValue`s
decoded -> GsonUtils.base64Encode(decoded.toIValue(evaluator.getValueFactory())),
encoded -> Sample.of(GsonUtils.<IValue> base64Decode(encoded, typeStore))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we using gson and base64 to store IValue? the underlying IValueOutputStream should be a lot faster.

If we want to use it in a streaming way (like we should), we have to encode some kind of header for every chunk we serialize. which can just be a single byte that marks a new chunk of samples coming up.

doing that would make buffer a IListWriter and we just do a .done() before sending it to the serializer.


public TempFile(Function<T, String> encode, Function<String, T> decode) throws IOException {
this.executor = Executors.newSingleThreadExecutor(Executors.defaultThreadFactory());
this.path = Files.createTempFile("", ""); // Prefix and suffix don't matter

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's still nice to give it a name, when a user finds some random files on their disk, they know what is the reason they exist.

@Override
public void run(){
while(running) {
sampler.produceSample();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while we're sampling, can we not skip the below capturing of the current AST etc? or alternativly reuse the data that is calcualted here? as it too collects some information about the current state (al be it not the call stack).

out.flush();
}

private void reportSamples() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if this method is called when the sampling was disabled?

// Print to out
var out = eval.getOutPrinter();
out.printf("FORMATTED PROFILE: %s\n", message);
out.flush();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason this cannot use URIResolverRegistry and instead has to be limited to local files only?

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants