Skip to content
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

Invoke all Enso benchmarks via JMH #7101

Merged
merged 77 commits into from
Aug 7, 2023
Merged

Conversation

Akirathan
Copy link
Member

@Akirathan Akirathan commented Jun 22, 2023

Closes #7323

Pull Request Description

Motivation

There are a lot of Enso-only benchmarks in test/Benchmarks project. These benchmarks are not run on the CI (they are, but only in dry-run). We want to run these benchmarks and collect the results, such that we will see them in engine-benchmark-results along with all the other Engine bench results. These benchmarks use our Enso benchmarking infrastrcture, which basically consist of just one method - Bench.measure.

In order to do that, we have two choices:

  • Try to invoke everything via JMH, so that JMH will provide all the data processing and data collection.
  • Implement result collector to our benchmarking infrastructure.

Let's invoke every benchmark via JMH. Not only it collects the results for us, but it also does a lot of additional work like JVM preparation, forking, warmup, etc. We don't want to lose that ability.

Ideal properties of the solution:

  • We don't want to manually rewrite all the benchmark sources.
    • Ideally, there is no need to any modification to any of these sources.
  • We still want to be able to invoke the benchmarks as standalone scripts, just as we can do right now.

How to use it

This PR adds two new SBT projects - bench-processor in libs/scala/bench-processor directory, and std-benchmarks in std-bits/benchmarks directory. std-benchmarks has just one Java class with the main method and with @GenerateBenchSources annotation.

To run one benchmark:

std-benchmarks/benchOnly <bench-name-regex>

To force the annotation processor to rediscover new benchmarks and to regenerate the JMH sources:

std-benchmarks/clean; std-benchmarks/Bench/clean; std-benchmarks/Bench/compile

To provide additional cmdline arguments that are supported by JMH runner:

std-benchmarks/Bench/run -h

More info in docs/infrastructure/benchmarks.md

Important Notes

The Plot

The Revelation

This PR has the potential to fix it all!

  • It designs new Bench API ready for non-batch execution
  • It allows for single benchmark in a dedicated JVM execution
  • It provides a simple way to wrap such an Enso benchmark as a Java benchmark
  • thus the results of Enso and Java benchmarks are now unified

Long live single benchmarking infrastructure for Java and Enso!

Checklist

Please ensure that the following checklist has been satisfied before submitting the PR:

  • The documentation has been updated, if necessary.
  • Screenshots/screencasts have been attached, if there are any visual changes. For interactive or animated visual changes, a screencast is preferred.
  • All code follows the
    Scala,
    Java,
    and
    Rust
    style guides. In case you are using a language not listed above, follow the Rust style guide.
  • All code has been tested:
    • Unit tests have been written where possible.
    • If GUI codebase was changed, the GUI was tested when built using ./run ide build.

@hubertp
Copy link
Contributor

hubertp commented Jun 23, 2023

@Akirathan could we also combine this with #6554?

@Akirathan
Copy link
Member Author

@Akirathan could we also combine this with #6554?

Well, this kind of supersedes #6554 - if this is integrated, there will be no more need for Enso benchmarks test/Benchmarks to provide any sophisticated output because both Engine benchmarks and Enso benchmarks will be run via JMH.

@Akirathan Akirathan force-pushed the wip/akirathan/enso-bench-jmh branch from 39fc4c9 to 188d465 Compare June 26, 2023 17:01
@Akirathan
Copy link
Member Author

Current idea of the solution (sketched in f6dd423):

  • There will be one class LibBenchRunner with main method that:
    • Collects all the benchmark specifications from test/Benchmarks project
    • Parses JMH specific cmdline arguments and merges them with some custom arguments
      • Inspired by Main.java from JMH project that parses all the arguments.
    • Delegates to a proper benchmark
  • One class LibBench with a single method annotated with @Benchmark.
    • Needed in order for the JMH runner to recognize it as a benchmark.
    • This method takes parameters and extracts the benchmark name from it and delegates to an appropriate benchmark

Another alternative is to generate many Java sources with many methods annotated with @Benchmark based on discovered benchmarks from test/Benchmarks, but that is too complicated.

IR manipulation is out of the question - better to refactor test/Benchmarks than manipulate with IR.

This idea assumes that all the benchmarks in test/Benchmarks are migrated to the builder pattern.

@JaroslavTulach JaroslavTulach added the CI: No changelog needed Do not require a changelog entry for this PR. label Jul 11, 2023
@radeusgd radeusgd linked an issue Jul 13, 2023 that may be closed by this pull request
@Akirathan Akirathan marked this pull request as ready for review August 3, 2023 13:22
Copy link
Member

@JaroslavTulach JaroslavTulach left a comment

Choose a reason for hiding this comment

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

I've just used this support to execute Greg's benchmarks and see the results in IGV. I think the support is good enough.

docs/CONTRIBUTING.md Outdated Show resolved Hide resolved
Comment on lines +39 to +41
- Use `env JAVA_OPTS=-Dpolyglot.inspect.Path=enso_debug` to set the chrome to
use a fixed URL. In this case the URL is
`devtools://devtools/bundled/js_app.html?ws=127.0.0.1:9229/enso_debug`
Copy link
Member

Choose a reason for hiding this comment

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

Wow I did not know that, so cool!


The `std-benchmarks` SBT project supports `bench` and `benchOnly` commands, that
work the same as in the `runtime` project, with the exception that the benchmark
name does not have to be specified as a fully qualified name, but as a regular
Copy link
Member

Choose a reason for hiding this comment

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

Does this not work in runtime?

Copy link
Member

Choose a reason for hiding this comment

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

(using regexp to filter benchmarks)

Copy link
Member Author

Choose a reason for hiding this comment

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

Not yet, I will look into that in one of the follow-up PRs and unify the functionality even more.

*/
public interface BenchSpec {
String name();
Value code();
Copy link
Member

Choose a reason for hiding this comment

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

Is it really the code or more like an executable thunk?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's exactly the same as code field in Bench.Spec, as mentioned in the javadoc. It's just a polyglot org.graalvm.polyglot.Value that can be executed. I guess that technically it is a thunk, but what difference does it make here?

Comment on lines +235 to +242
if (debugAnotProcessorOpt) {
log.info(
s"Frgaal compiler is about to be launched with $debugArg, which means that" +
" it will wait for a debugger to attach. The output from the compiler is by default" +
" redirected, therefore \"Listening to the debugger\" message will not be displayed." +
" You should attach the debugger now."
)
}
Copy link
Member

Choose a reason for hiding this comment

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

Wow, thanks for adding this message!

Copy link
Member Author

Choose a reason for hiding this comment

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

Without that message, you would have no idea that the java process is waiting for a debugger, because the output is redirected.

Comment on lines +10 to +13
random_vec = Utils.make_random_vec 100000
uniform_vec = Vector.fill 100000 1
random_text_vec = random_vec.map .to_text
uniform_text_vec = random_vec.map .to_text
Copy link
Member

Choose a reason for hiding this comment

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

This is not good.

Enso has a subtle difference between top-level and scoped definitions, that plays a very crucial role here.

A scoped definition is computed (unless it's a block...) and stored. A top-level definition is re-computed on every access.

Essentially you have turned values computed once at initialization of the suite into 0-argument methods that are recomputed on each access.

This changes the semantics of these benchmarks.

We used to be just computing the time needed to compute the distinct operation. Now we are computing the total time it takes to both generate the vector and compute distinct.

I don't think that's right.

I know that we don't have the 'setup' pattern yet. For now I'd just run this inside of the group. It's not perfect and will slow down gathering benchmarks, so maybe for future we need a better solution.

On the other hand, I'm not 100% convinced that changing semantics is bad here. I don't think it is good but feel free to convince me otherwise.

Copy link
Member Author

Choose a reason for hiding this comment

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

A scoped definition is computed (unless it's a block...) and stored. A top-level definition is re-computed on every access.

I have not realized that. That might be problematic here - we don't want a different random_vec in each iteration. I will try to revert that. My goal here was just to demonstrate the usage of the builder pattern that is syntactically a bit nicer than the usage in Operations.enso.

Copy link
Member Author

Choose a reason for hiding this comment

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

On second thought, let me take care of this in follow-up PRs, I have now added it to a task list in #7489.

@@ -39,7 +41,8 @@ public BenchmarkItem run(String label) throws RunnerException, JAXBException {
if (Boolean.getBoolean("bench.compileOnly")) {
builder
.measurementIterations(1)
.warmupIterations(0);
.warmupIterations(0)
.forks(0);
Copy link
Member Author

Choose a reason for hiding this comment

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

Setting forks to 0 when bench.CompileOnly = true seems to have a negative side effect when all the benchmarks are run on the CI - it seems to cause StackOverflow errors, like this one

Copy link
Member Author

Choose a reason for hiding this comment

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

I have reverted 11e0a12 as a workaround for now. In the upcoming PRs, I will try to unify and improve the debugging experience of the benchmarks. Tracking this in #7489.

@mergify mergify bot merged commit 8e49255 into develop Aug 7, 2023
23 of 24 checks passed
@mergify mergify bot deleted the wip/akirathan/enso-bench-jmh branch August 7, 2023 12:39
@@ -29,6 +29,9 @@ object FrgaalJavaCompiler {
val frgaal = "org.frgaal" % "compiler" % "19.0.1" % "provided"
val sourceLevel = "19"

val debugArg =
"-J-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=localhost:8000"
Copy link
Member

Choose a reason for hiding this comment

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

I believe this option shall be unified with WithDebugCommand setup... I wanted to mention that before, but maybe I haven't done so...

Copy link
Member Author

Choose a reason for hiding this comment

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

You have mentioned that already. And the answer is buried somewhere within dozens of comments in this PR. The brief answer is that we need the javac process (more specifically, in this case, java -jar frgaal-compiler.jar process) to wait for the debugger and not to start without it. That is because the java -jar frgaal-compiler.jar process's output is piped, so the compiler process can finish even before the user notices that they could attach a debugger.

Moreover, using WithDebugCommand.DEBUG_ARG requires some more refactoring - for that, we would need to remove some files in project directory from org.enso.build package.

TL;DR; it would be unnecessarily complicated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CI: No changelog needed Do not require a changelog entry for this PR. CI: Ready to merge This PR is eligible for automatic merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Execute (and analyze) single Bench.measure stdlib benchmarks should produce reports in a unified format
6 participants