#chiroito ’s blog

Java を中心とした趣味の技術について

Terraform を使って Cloud上に OpenJDK の開発環境を整えてみた

OpenJDK のビルドやテストは時間が非常に掛かるので、クラウド上で大量の CPU リソースを使って短時間化できないかを試してみました。

今回作った環境に自分で書いたパッチを置いて Terraform を実行すると、あとはクラウド上で大量のリソースを使ってビルドとテストが終わるようになりました。試しに 48 vCPU の環境を作ったところ、ビルドは4~5分、fastdebug での Tier1 のテストは40分程度で完了しました。

ソースコードはこちら(sandbox/Terraform/developOpenJDK at master · chiroito/sandbox · GitHub

クラウドを使うと言えど毎回この全行程を実行するのは時間(=料金)がかかるので、できる限り再利用したいと思います。 今回は、再利用できる部分と毎回処理する部分に分けて環境を構築してみました。再利用できる部分は環境構築時に作成して Block Storage として保管しておきます。Block Storage は 2 つ作ってあり、1つはOSや作業スクリプトなど一回作ったらしばらく変らない部分を Boot イメージとして作成し、もう1つは、OpenJDK のソースコードなどわりと頻繁に変る部分を作業用のボリュームとして作成しました。

今回は Terraform を使って Oracle Cloud 上に環境を構築していきます。OpenJDK 関連の作業はシェル化してあるので他のクラウドサービス上でも環境構築できると思います。

f:id:chiroito:20190106155351p:plain

OpenJDK の環境を整えてテストを流すために必要な作業は以下のようになります。

  1. ビルドやテストに必要なパッケージのインストール
  2. OpenJDK ソースコードのダウンロード
  3. テストツールであるJTREG ソースコードのダウンロード
  4. 自分で書いたパッチを適用
  5. OpenJDK と JTREG のビルド
  6. JTREG を使って OpenJDK のテストを実行

今回は、再利用できる部分として1~3 を済ませた Block Storage を作成し、テスト実行時に 4 以降の作業をするようにします。1~3の部分はダウンロード時間がほとんどを占めるため、CPU の量が少ないインスタンスを使えます。

OpenJDK のスクリプト

  1. ビルドやテストに必要なパッケージのインストール
  2. OpenJDK ソースコードのダウンロード
  3. テストツールであるJTREG ソースコードのダウンロード
SRC_BASE_PATH=${1}
sudo yum groupinstall -y "Development Tools"
sudo yum install -y freetype-devel cups-devel libXtst-devel libXt-devel libXrender-devel libXrandr-devel libXi-devel alsa-lib-devel libffi-devel autoconf java-1.8.0-openjdk-devel fontconfig-devel java-11-openjdk-devel mercurial
mkdir -p ${SRC_BASE_PATH}
cd ${SRC_BASE_PATH}
hg clone http://hg.openjdk.java.net/jdk/jdk
hg clone http://hg.openjdk.java.net/code-tools/jtreg

4 . 自分で書いたパッチを適用

5 . OpenJDK と JTREG のビルド

SRC_BASE_PATH=${1}
cd ${SRC_BASE_PATH}/jtreg
hg pull -u
bash make/build-all.sh /usr/lib/jvm/java-1.8.0

cd ${SRC_BASE_PATH}/jdk
hg pull -u
patch -p1 < /home/opc/myPatch
bash configure --enable-debug --with-native-debug-symbols=internal --with-boot-jdk=/usr/lib/jvm/java-11 --with-jtreg=${SRC_BASE_PATH}/jtreg/build/images/jtreg
time make images

6 . JTREG を使って OpenJDK のテストを実行

SRC_BASE_PATH=${1}
cd ${SRC_BASE_PATH}/jdk
time make test-tier1

Terraform のスクリプト

環境構築時に実行されて、以後再利用される処理

resource "null_resource" "preparing_task" {
  depends_on = ["oci_core_instance.prepare_instance", "oci_core_volume_attachment.prepare_volume_attach"]
  connection {
    agent       = false
    timeout     = "300m"
    host        = "${oci_core_instance.prepare_instance.public_ip}"
    user        = "opc"
    private_key = "${file("${var.ssh_private_key}")}"
  }
  provisioner "file" {
    source = "script"
    destination = "/home/opc/"
  }
  provisioner "remote-exec" {
    inline = [ <<EOS
sh /home/opc/script/prepareForOpenJDK.sh /src
EOS
    ]
  }
}

ビルドとテストのたびに実行される処理

resource "null_resource" "build_and_test_task" {
  depends_on = ["oci_core_instance.worker_instance", "oci_core_volume_attachment.worker_volume_attach"]
  connection {
    agent       = false
    timeout     = "120m"
    host        = "${oci_core_instance.worker_instance.public_ip}"
    user        = "opc"
    private_key = "${file("${var.ssh_private_key}")}"
  }
  provisioner "file" {
    source = "script"
    destination = "/home/opc/"
  }
  provisioner "file" {
    source = "userdata/myPatch"
    destination = "/home/opc/"
  }
  provisioner "remote-exec" {
    inline = [ <<EOS
sh /home/opc/script/buildOpenJDK.sh /src
sh /home/opc/script/testOpenJDK.sh /src
EOS
    ]
  }
}

全体のソースコードはこちら(sandbox/Terraform/developOpenJDK at master · chiroito/sandbox · GitHub

OpenJDK のテストをデバッグ実行するためいろいろ試みてみた

Java のコードをデバッグ実行するには-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:12345みたいな感じのを付けます。これを使って OpenJDK の Java 部分のデバッグができるだろうなと思って試してみました。

jtreg で実行するテストコードにこれを付けたところ、アタッチするまでに気が遠くなるぐらい長い時間がかかり、やっと繋がったと思ったら jtreg のタイムアウトにやられるため実用に耐えられなかったです。

なので、もっと気軽にアタッチできる方法は無いか試してみました。

make で jtreg を使うには configure するときに --with-jtreg=/src/jtreg/build/images/jtreg を付けないといけません。自分がどのオプションで実行したかは build/linux-x86_64-server-fastdebug/configure.log で確認できます。linux-x86_64fastdebug の部分は環境や指定したオプションによって変ります。

jtreg で実行したテストへアタッチ

jtreg でテストを実行する JVM へVM引数を渡すには JTREG="VM_OPTIONS=*****" のような形式でmake の実行時に指定します。***** の部分がVM引数としてテストを実行する JVM へ渡されます。

今回は JTREG="VM_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:12345" としています。

> make test TEST="test/jdk/jdk/jfr/startupargs/TestRepositoryPath.java" JTREG="VM_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:12345"

Building target 'test' in configuration 'linux-x86_64-server-fastdebug'
Skip building of Graal unit tests because 3rd party libraries directory is not specified
Skip building of Graal unit tests because 3rd party libraries directory is not specified
Running tests using JTREG control variable 'VM_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:12345'
Test selection 'test/jdk/jdk/jfr/startupargs/TestRepositoryPath.java', will run:
* jtreg:test/jdk/jdk/jfr/startupargs/TestRepositoryPath.java

Running test 'jtreg:test/jdk/jdk/jfr/startupargs/TestRepositoryPath.java'
[2019-01-02 11:48:15,081] Agent[0]: stdout: Listening for transport dt_socket at address: 12345
[2019-01-02 11:48:32,392] Agent[0]: stderr: openjdk version "13-internal" 2019-09-17
[2019-01-02 11:48:32,397] Agent[0]: stderr: OpenJDK Runtime Environment (fastdebug build 13-internal+0-adhoc.chito.jdk)
[2019-01-02 11:48:32,397] Agent[0]: stderr: OpenJDK 64-Bit Server VM (fastdebug build 13-internal+0-adhoc.chito.jdk, mixed mode)

実行してみると分かりますが、恐ろしく長い時間が掛かりますし、いつになったらアタッチできるのか分かりません。ポートは開くのですがアタッチされる準備は出来ていないようなので、Connection Refused されてしまい「接続できない??」と思うほどです。

jtreg を使わずにテストを実行してみる

これでは OpenJDK を開発する中で軽い気持ちで動作を確認できないので、方法を模索してみました。

各種 IDE 上でテストコードをコンパイルして実行してみようと思ったのですが、IDE ちからがなくて断念。

面倒臭い部分は全て jtreg にやってもらって、実行だけできないかと思い、jtreg が生成するクラスファイルなどを使用することにしました。

make test すると build/linux-x86_64-server-fastdebug/test-support/ ディレクトリ以下にいろいろ作ってくれます。その中にはクラスファイルも含まれるためこちらを使用することとしました。

以下 5 つの変数を使ってテストを実行したいと思います。

  • JAVA_EXE はテストしたい環境の java ファイルを指定します。
  • JAVA_OPTIONS は自分が追加したい VM 引数を指定します。
  • TEST_CLASSPATH はjtregに作成されたクラスファイル群を指定します。
  • TEST_OPTIONS はテストのファイルに記載されているオプションを指定します。
  • TEST_CLASS は実行したいテストクラスを指定します。
> export JAVA_EXE=build/linux-x86_64-server-fastdebug/images/jdk/bin/java
> export JAVA_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:12345

> export TEST_CLASSPATH=build/linux-x86_64-server-fastdebug/test-support/jtreg_test_jdk_jdk_jfr_startupargs_TestRepositoryPath_java/classes/jdk/jfr/startupargs/TestRepositoryPath.d
> export TEST_OPTIONS=-XX:StartFlightRecording=name=TestStartRecording,settings=profile -XX:FlightRecorderOptions=repository=./repo
> export TEST_CLASS=jdk.jfr.startupargs.TestRepositoryPath

> ${JAVA_EXE} ${JAVA_OPTIONS} -cp  ${TEST_CLASSPATH} ${TEST_OPTIONS} ${TEST_CLASS}

Listening for transport dt_socket at address: 12345

この方法だと、すぐにアタッチできるようになりタイムアウトもなくなったので、OpenJDK のデバッグライフがより快適になるのではないかと思います。

JShell 自体をデバッグしてみた

Java Bug System を見ていたら JShell 自体のデバッグを見つけたので、パッチを書こうと思いJShellについて調べてみた。

使い方や 2 つの JVM が動くぐらいは知っていたけど、具体的には知らないので調べてみました。

試しにjshellコマンドを実行してからjmcjcmdを確認するとjdk.internal.jshell.tool.JShellToolProviderjdk.jshell.execution.RemoteExecutionControlが起動するのが確認できます。

人間がふれ合うクラスはjdk.internal.jshell.tool.JShellToolProviderです。

jdk.internal.jshell.tool.JShellToolProviderの実行時引数を確認したところ以下のような感じでした。

java -Dapplication.home=C:\Oracle\Java\jdk-11 -Xms8m -Djdk.module.main=jdk.jshell jdk.internal.jshell.tool.JShellToolProvider

これに-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:12345を付けてデバッグできるようにします。

全部付けるとこんな感じ。

java -Dapplication.home=C:\Oracle\Java\jdk-11 -Xms8m -Djdk.module.main=jdk.jshell -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:12345jdk.internal.jshell.tool.JShellToolProvider

デバッグが出来るようになりましたが、気になるのはユーザからの入力を待つ場所あたりかと。 これは、jdk.jshellモジュールの中ではjdk.internal.jshell.tool.ConsoleIOContextクラスのpublic String readLine(String prompt, String prefix) throws IOException, InputInterruptedExceptionメソッドでユーザの入力を待ちます。

@Override
public String readLine(String prompt, String prefix) throws IOException, InputInterruptedException {
(略)
  try {
    return in.readLine(prompt);
  } catch (UserInterruptException ex) {
(略)
}

inのオブジェクトはjdk.internal.leモジュールのjdk.internal.jline.console.ConsoleReaderです。 jdk.internal.leモジュールでは、jdk.internal.jline.internal.InputStreamReaderクラスのpublic int read(char[] buf, int offset, int length) throws IOExceptionメソッドでユーザの入力を待ちます。

@Override
public int read(char[] buf, int offset, int length) throws IOException {
(略)
  int off = bytes.arrayOffset() + bytes.limit();
  int was_red = in.read(bytes.array(), off, to_read);

  if (was_red == -1) {
(略)
}