Skip to content

Commit

Permalink
Proposal: Support for GraalVM Native
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sgammon committed Jun 13, 2022
1 parent 0751104 commit d651efc
Show file tree
Hide file tree
Showing 10 changed files with 506 additions and 0 deletions.
9 changes: 9 additions & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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"])

Expand Down Expand Up @@ -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",
Expand Down
20 changes: 20 additions & 0 deletions WORKSPACE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
24 changes: 24 additions & 0 deletions build_test_native.sh
Original file line number Diff line number Diff line change
@@ -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
Empty file added defs/BUILD.bazel
Empty file.
104 changes: 104 additions & 0 deletions defs/closure_bindist.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""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_%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",
}

# 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,
)
21 changes: 21 additions & 0 deletions native/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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",
])
2 changes: 2 additions & 0 deletions native/bench/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
subjects
reports
40 changes: 40 additions & 0 deletions native/bench/Makefile
Original file line number Diff line number Diff line change
@@ -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

101 changes: 101 additions & 0 deletions native/bench/bench.py
Original file line number Diff line number Diff line change
@@ -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])

Loading

0 comments on commit d651efc

Please sign in to comment.