From 3e5aec9e94d6858bee83993ccff23e2bedf5116f Mon Sep 17 00:00:00 2001 From: Sam Gammon Date: Mon, 13 Jun 2022 00:52:00 -0700 Subject: [PATCH] Proposal: Support for GraalVM Native This changeset proposes support for invocation of Closure Compiler on the command line, via a GraalVM Native Image binary. Using the [`rules_graal`][1] package, there is now a target for the compiler in native binary form. Downstream, projects can load a bzl file to download a binary distribution for their platform, and run Closure in their own projects without needing to build. Included is a small benchmark tool which compares a fresh native vs. JVM copy on your own system. In preliminary tests (in `SIMPLE` mode only, for now), macOS M1 sees about 70% faster compile time versus execution through a JVM. [1]: https://github.com/andyscott/rules_graal --- BUILD.bazel | 9 ++ WORKSPACE.bazel | 20 +++++ build_test_native.sh | 24 +++++ defs/BUILD.bazel | 0 defs/closure_bindist.bzl | 105 ++++++++++++++++++++++ native/BUILD.bazel | 21 +++++ native/bench/.gitignore | 2 + native/bench/Makefile | 40 +++++++++ native/bench/bench.py | 101 +++++++++++++++++++++ native/reflection.json | 185 +++++++++++++++++++++++++++++++++++++++ 10 files changed, 507 insertions(+) create mode 100755 build_test_native.sh create mode 100644 defs/BUILD.bazel create mode 100644 defs/closure_bindist.bzl create mode 100644 native/BUILD.bazel create mode 100644 native/bench/.gitignore create mode 100644 native/bench/Makefile create mode 100644 native/bench/bench.py create mode 100644 native/reflection.json diff --git a/BUILD.bazel b/BUILD.bazel index 4c9ed2a4088..d54a9495abd 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -18,6 +18,7 @@ load("//:bazel/sonatype_artifact_bundle.bzl", "sonatype_artifact_bundle") load("//:bazel/typedast.bzl", "typedast") load("@com_github_johnynek_bazel_jar_jar//:jar_jar.bzl", "jar_jar") load("@google_bazel_common//testing:test_defs.bzl", "gen_java_tests") +load("@rules_graal//graal:graal.bzl", "graal_binary") package(licenses = ["notice"]) @@ -81,6 +82,14 @@ java_binary( runtime_deps = [":compiler_lib"], ) +graal_binary( + name = "compiler_native", + deps = [":compiler_shaded"], + main_class = "com.google.javascript.jscomp.CommandLineRunner", + reflection_configuration = "@//native:reflection.json", + include_resources = ".*", +) + java_binary( name = "linter", main_class = "com.google.javascript.jscomp.LinterMain", diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index e6dfcc5fec3..1ccbe9977cc 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -149,3 +149,23 @@ http_archive( load("@com_github_johnynek_bazel_jar_jar//:jar_jar.bzl", "jar_jar_repositories") jar_jar_repositories() + +# GraalVM is a tool from Oracle (https://graalvm.org) which includes support for +# building native binaries from Java applications. Closure Compiler can be build with +# the bundled `native-image` tool and installed on a target system to execute natively. +http_archive( + name = "rules_graal", + sha256 = "8fa2a40ef37704a6cd2d2ca5c8e2b845f9b207e77141014877dceac6cd40f321", + strip_prefix = "rules_graal-9fd38761df4ac293f952d10379c0c3520dd9ceed", + urls = [ + "https://github.com/andyscott/rules_graal/archive/9fd38761df4ac293f952d10379c0c3520dd9ceed.tar.gz", + ], +) + +load("@rules_graal//graal:graal_bindist.bzl", "graal_bindist_repository") + +graal_bindist_repository( + name = "graal", + java_version = "11", + version = "22.1.0", +) diff --git a/build_test_native.sh b/build_test_native.sh new file mode 100755 index 00000000000..b81e137a4ad --- /dev/null +++ b/build_test_native.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Copyright 2020 Google Inc. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Script that can be used by CI server for testing JsCompiler builds. +set -e + +source build_test.sh + +bazel build //:compiler_native +make -C native/bench clean reports + +# TODO: Run other tests needed for open source verification diff --git a/defs/BUILD.bazel b/defs/BUILD.bazel new file mode 100644 index 00000000000..e69de29bb2d diff --git a/defs/closure_bindist.bzl b/defs/closure_bindist.bzl new file mode 100644 index 00000000000..ec3026096d1 --- /dev/null +++ b/defs/closure_bindist.bzl @@ -0,0 +1,105 @@ +"""Bazel declarations to run a native binary copy of Closure Compiler in downstream Bazel projects.""" + + +# sample link (for `_bindist("1.0.0", "darwin", "arm64")`): +# https://github.com/sgammon/closure-compiler/releases/download/v1.0.0/compiler_native_1.0.0_darwin_arm64.tar.gz +def _bindist(version, os, arch): + return "https://github.com/sgammon/closure-compiler/releases/download/v%s/compiler_native_v%s_%s_%s.tar.gz" % ( + version, + version, + os, + arch + ) + +# sample bundle (for `_bindist_bundle("1.0.0", "linux", archs = ["arm64", "s390x"])`): +# "linux": { +# "arm64": _bindist("1.0.0", "linux", "arm64"), +# "s390x": _bindist("1.0.0", "linux", "s390x"), +# }, +def _bindist_bundle(version, os, archs = []): + return dict([ + (arch, _bindist(version, os, arch)) + for arch in archs + ]) + +# sample version: (for `_bindist_bundle("1.0.0", bundles = {"darwin": ["arm64"], "linux": ["arm64"]})`) +# "1.8.6": { +# "linux": { +# "arm64": _bindist("1.0.0", "linux", "arm64"), +# }, +# "darwin": { +# "arm64": _bindist("1.0.0", "darwin", "arm64"), +# }, +# }, +def _bindist_version(version, bundles = {}): + return dict([ + (os, _bindist_bundle(version, os, archs)) + for os, archs in bundles.items() + ]) + + +# version checksums (static) +_compiler_version_checksums = { + "v20220612_darwin_arm64": "1a787ec3a242e19589b041586dd487c13daa4dc0c19afb846cb31128f7606d87", + "v20220612_linux_amd64": "71d9488a6e3bf536e80b9cf74d353c8c42b7883d9d54de742152908390cbba0b", +} + +# version configs (static) +_compiler_version_configs = { + "v20220612": _bindist_version( + version = "20220612", + bundles = { + "darwin": ["arm64"], + }, + ), +} + +_compiler_latest_version = "v20220612" + +def _get_platform(ctx): + res = ctx.execute(["uname", "-p"]) + arch = "amd64" + if res.return_code == 0: + uname = res.stdout.strip() + if uname == "arm": + arch = "arm64" + elif uname == "aarch64": + arch = "aarch64" + + if ctx.os.name == "linux": + return ("linux", arch) + elif ctx.os.name == "mac os x": + if arch == "arm64" or arch == "aarch64": + return ("darwin", "arm64") + return ("darwin", "x86_64") + else: + fail("Unsupported operating system: " + ctx.os.name) + +def _compiler_bindist_repository_impl(ctx): + platform = _get_platform(ctx) + version = ctx.attr.version + + # resolve dist + config = _compiler_version_configs[version] + link = config[platform[0]][platform[1]] + sha = _compiler_version_checksums["%s_%s_%s" % (version, platform[0], platform[1])] + + urls = [link] + ctx.download_and_extract( + url = urls, + sha256 = sha, + ) + + ctx.file("BUILD", """exports_files(glob(["**/*"]))""") + ctx.file("WORKSPACE", "workspace(name = \"{name}\")".format(name = ctx.name)) + + +closure_compiler_bindist_repository = repository_rule( + attrs = { + "version": attr.string( + mandatory = True, + default = _compiler_latest_version, + ), + }, + implementation = _compiler_bindist_repository_impl, +) diff --git a/native/BUILD.bazel b/native/BUILD.bazel new file mode 100644 index 00000000000..b8078665525 --- /dev/null +++ b/native/BUILD.bazel @@ -0,0 +1,21 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +package( + default_visibility = ["//visibility:public"], +) + +exports_files([ + "reflection.json", +]) diff --git a/native/bench/.gitignore b/native/bench/.gitignore new file mode 100644 index 00000000000..9a80a91472c --- /dev/null +++ b/native/bench/.gitignore @@ -0,0 +1,2 @@ +subjects +reports diff --git a/native/bench/Makefile b/native/bench/Makefile new file mode 100644 index 00000000000..77f7640e374 --- /dev/null +++ b/native/bench/Makefile @@ -0,0 +1,40 @@ +# +# Benchmark suite +# + +all: subjects reports + + +SUBJECTS ?= https://unpkg.com/react@18.1.0/cjs/react.development.js \ + https://unpkg.com/lodash@4.17.21/lodash.js \ + https://unpkg.com/jquery@3.6.0/dist/jquery.js + +TARGETS ?= react.development \ + lodash \ + jquery + +REPORTS = $(patsubst %,%.jvm.min.js,$(TARGETS)) $(patsubst %,%.native.min.js,$(TARGETS)) + +WGET_ARGS ?= -q --progress=dot + + +reports: subjects ## Generate benchmark reports. + @echo "Benchmarking Closure Compiler (JVM vs. Native)..." + @for target in $(TARGETS); do python3 bench.py "$$target"; done + +subjects: ## Download subject test files. + @echo "Downloading test bundles..." + @mkdir subjects + @cd subjects && for subj in $(SUBJECTS) ; do \ + echo "- Downloading '$$subj'..." && \ + wget $(WGET_ARGS) $$subj; \ + done + +clean: ## Clean benchmark reports and subject test files. + @echo "Cleaning subjects..." + @rm -fr subjects + @echo "Cleaning reports..." + @rm -fr reports + +.PHONY: clean + diff --git a/native/bench/bench.py b/native/bench/bench.py new file mode 100644 index 00000000000..8cfe0f90f88 --- /dev/null +++ b/native/bench/bench.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 + +import sys, os, time + + +defaultIterations = 1 + +COMMAND_JVM_TARGET = "//:compiler_unshaded" +COMMAND_NATIVE_TARGET = "//:compiler_native" + +COMMAND_CLOSURE_SIMPLE = [ + "bazel", + "run", + "--ui_event_filters=-info,-stdout,-stderr", + "--noshow_progress", + "%binary%", + "--", + "--compilation_level=SIMPLE", + "--js=%input%", + "--js_output_file=%output%", + "2>", + "/dev/null", +] + + +def render_command_segment(seg, target, bundle, target_label): + return ( + seg.replace("%binary%", target) + .replace("%input%", "$(bazel info workspace)/native/bench/subjects/%s.js" % bundle) + .replace("%output%", "$(bazel info workspace)/native/bench/reports/%s.%s.min.js" % (bundle, target_label)) + ) + + +def compile_command(target, target_label, bundle, args = []): + base_args = [i for i in map(lambda i: render_command_segment(i, target, bundle, target_label), COMMAND_CLOSURE_SIMPLE)] + base_args += args + return base_args + +def run_report(bundle, jvm, native): + print(""" + +| Bundle: %s +| +| Build times: +| - JVM: %s +| - Native: %s +| ------------------- +| Diff: %s (%s) + + """ % ( + bundle, + str(round(jvm)) + "ms", + str(round(native)) + "ms", + str(round(jvm - native)) + "ms", + ((native < jvm) and "+" or "-") + str(round(100 - ((native/jvm) * 100))) + "%", + )) + +def run_compile(target, target_label, bundle, args = [], iterations = defaultIterations): + command = compile_command(target, target_label, bundle, args) + joined = " ".join(command) + print("- Running %s build for '%s' (iterations: %s)..." % (target_label, bundle, iterations)) + all_measurements = [] + for i in range(0, iterations): + start = round(time.time() * 1000) + os.system(" ".join(command)) + end = round(time.time() * 1000) + all_measurements.append(end - start) + return sum(all_measurements) / iterations + +def run_benchmark(bundle, variant = "SIMPLE", args = []): + print("\nBenchmarking bundle '%s' (variant: '%s')..." % (bundle, variant)) + jvm_time = run_compile( + COMMAND_JVM_TARGET, + "jvm", + bundle, + args + ) + native_time = run_compile( + COMMAND_NATIVE_TARGET, + "native", + bundle, + args + ) + run_report( + bundle, + jvm_time, + native_time, + ) + +def run_bench(bundle): + """Run a JVM copy of the Closure Compiler and compare it with a Native copy built with GraalVM.""" + run_benchmark( + bundle, + ) + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Please provide bundle name to test") + sys.exit(2) + run_bench(sys.argv[1]) + diff --git a/native/reflection.json b/native/reflection.json new file mode 100644 index 00000000000..718193bda9c --- /dev/null +++ b/native/reflection.json @@ -0,0 +1,185 @@ +[ + { + "name" : "com.google.javascript.jscomp.CommandLineRunner$Flags", + "queryAllDeclaredMethods" : true, + "allDeclaredClasses": true, + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allPublicFields": true + }, + { + "name" : "com.google.javascript.jscomp.CommandLineRunner$Flags$BooleanOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allDeclaredClasses": true, + "allPublicMethods": true + }, + { + "name" : "com.google.javascript.jscomp.CommandLineRunner$Flags$WarningGuardErrorOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name" : "com.google.javascript.jscomp.CommandLineRunner$Flags$WarningGuardWarningOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name" : "com.google.javascript.jscomp.CommandLineRunner$Flags$WarningGuardOffOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name" : "com.google.javascript.jscomp.CommandLineRunner$Flags$JsOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name" : "com.google.javascript.jscomp.CommandLineRunner$Flags$JsZipOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.BooleanOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.FileOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.URLOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.URIOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.IntOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.DoubleOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.StringOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.ByteOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.CharOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.EnumOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.ExplicitBooleanOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.FloatOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.LongOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.MapOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.MultiFileOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.MultiPathOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.ShortOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.SubCommandHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.StringArrayOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.InetAddressOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.PathOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.MacAddressOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + }, + { + "name": "com.google.javascript.jscomp.jarjar.org.kohsuke.args4j.spi.PatternOptionHandler", + "queryAllDeclaredConstructors" : true, + "allDeclaredConstructors": true, + "allPublicMethods": true + } +] \ No newline at end of file