jbangを使って複雑なビルドプロセスをなしで jmh/jcstress を実行する

出落ちで申し訳ないけど jbangで、Javaベンチマークツールであるjmhを実行出来る記事があるのでそれのリンクを貼っておく。 しかし、後述の話に由来するのだが、以下の記事のprofilerの実行はあまり使わないほうが良いと思われる。 jbangによって、jmhが実行するのが簡単になったことに着目してほしい。

Benchmarks, Async profiler and JBang - @maxandersen

従来のjmhの実行方法としては、mavenプロジェクトを用意して、コンパイルして、uberjarを作って実行する、みたいなのが多かったが jbangを使うことで、mavenプロジェクトもuberjarも必要ない。pom.xmlともおさらばできる。.java ファイルだけで良いのは最高だ!

これだけで終わってしまうとアレなので、以下の話を少しする。 jmh標準のprofile機能の話とjcstressの話を書いておくことにする。

  • jmhには 標準でprofilerの機能がついている
  • jmhの標準のprofilerで async-profilerを使ってみる
  • jcstress とは
  • jbangで jcstressを実行してみる

jmhには 標準でprofilerの機能がついている

jmhには標準で様々なツール依存・非依存のprofilingが可能である。 jmhの起動時に -lprof を実行すると、以下のprofilerの一覧が確認できる。

  • cl
  • comp
  • gc
  • jfr
  • pauses
  • perfc2c
  • safepoints
  • stack
  • async
  • perf
  • perfasm
  • perfnorm
  • xperfasm

実際に -lprof を付与して実行するとこんな出力が出る。実行環境や引数によっては使えないprofilerについても表示してくれる。 Apple SiliconのMacOSで実行したので、環境によって出力が変わる可能性があることを付記しておく。

jbang TestBenchmark.java -lprof
Supported profilers:
          cl: Classloader profiling via standard MBeans 
        comp: JIT compiler profiling via standard MBeans 
          gc: GC profiling via standard MBeans 
         jfr: Java Flight Recorder profiler 
      pauses: Pauses profiler 
     perfc2c: Linux perf c2c profiler 
  safepoints: Safepoints profiler 
       stack: Simple and naive Java stack profiler 

Unsupported profilers:
       async: <none> 
Unable to load async-profiler. Ensure asyncProfiler library is on LD_LIBRARY_PATH (Linux), DYLD_LIBRARY_PATH (Mac OS), or -Djava.library.path. Alternatively, point to explicit library location with -prof async:libPath=<path>.
   dtraceasm: <none> 
[sudo: a password is required
]
        perf: <none> 
[Cannot run program "perf": error=2, No such file or directory]
     perfasm: <none> 
[Cannot run program "perf": error=2, No such file or directory]
    perfnorm: <none> 
[Cannot run program "perf": error=2, No such file or directory]
    xperfasm: <none> 
Unable to start the profiler. Try running JMH as Administrator, and ensure that previous profiling session is stopped. Use 'xperf -stop' to stop the active profiling session.: [Cannot run program "xperf": error=2, No such file or directory]

jmhの標準のprofilerで async-profilerを使ってみる

自分が普段お世話になっているのは、async-profilerなので、async-profilerを試してみる。 以下のコマンドで、ベンチマークごとに、flamegraphが出力される。

jbang TestBenchmark.java -prof async:libPath=$PWD/lib/libasyncProfiler.dylib\;output=flamegraph\;dir=profile-results

なお、一番最初に紹介したの記事の中では、javaagentでasync-profilerを適用しているようだが、jmhの実行全体のprofileが取られてそうだった。 そのため、jmhでベンチマークのprofileを取りたくなったら、jmh標準の機能を使うのが良いと思われる。 実際に試したテストケースが悪い可能性はあるが、全ベンチマークの実行全体のプロファイルは別にほしくない。 jmh標準の機能では、ベンチマークごとのprofileが取得できる。これがほしかった。

jcstress とは

こちらはベンチマークのためのツールではなく、JVMやクラスライブラリ及びハードウェアによる並行性の問題を調査することを目的としたテストハーネスとテストスイートのようだ。 jcstressを使えば並行処理でrace conditionが発生しないかどうかを検査することが可能になるが、よっぽど特殊な事情がない限り使わないと思う。

アプリケーションを書くうえで、そこまで頑健にrace conditionの検査をする必要性がそこまで発生しないことが多いと思うのと jcstressを使わないと調査出来ないわけではないからだ。

まぁ気が向いたら触ってみて、並行性で疑わしく再現が難しい問題に直面したときにシュッと出せるツールとして使いたい。

jbangで jcstressを実行してみる

というわけで実行してみる。

以下のソースコードを用意する。 jcstress-samples から持ってきたファイルに jbangのコメントヘッダを付与して、依存解決とJavaのバージョンを指定している。

//JAVA 19
//DEPS org.openjdk.jcstress:jcstress-core:0.16

package com.github.wreulicke.test;
// 追記したのはここから↑

/*
 * Copyright (c) 2021, Red Hat, Inc. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
import org.openjdk.jcstress.annotations.Actor;
import org.openjdk.jcstress.annotations.JCStressTest;
import org.openjdk.jcstress.annotations.Outcome;
import org.openjdk.jcstress.annotations.State;
import org.openjdk.jcstress.infra.results.L_Result;

import static org.openjdk.jcstress.annotations.Expect.*;

@JCStressTest
@Outcome(id = "null",                   expect = ACCEPTABLE, desc = "Not seeing the object yet")
@Outcome(id = "class java.lang.Object", expect = ACCEPTABLE, desc = "Seeing the object, valid class")
@Outcome(                               expect = FORBIDDEN,  desc = "Other cases are illegal")
@State
public class TestJcstress {

  Object o;

  @Actor
  public void writer() {
      o = new Object();
  }

  @Actor
  public void reader(L_Result r) {
      Object lo = o;
      if (lo != null) {
          try {
              r.r1 = lo.getClass();
          } catch (NullPointerException npe) {
              r.r1 = npe;
          }
      } else {
          r.r1 = null;
      }
  }

    public static void main(String[] args) throws Exception {
        org.openjdk.jcstress.Main.main(args);
    }
}

実行方法は以下の通りだ。

jbang TestJcstress.java

なんか色々出力が出て、こんなhtmlが出力される。

以前触った記憶だと、並行性の問題で壊れたりすると、表示が変わった記憶があるが jbang を通した実行方法のみ整理しておきたいので、この記事では取り扱わない。

↑の画面から、テストのクラスをクリックすると、以下のような表示があり、compilation modeで網羅的に検査してくれていることが分かる。便利。

まとめ

この記事で、jbangを使って jmh/jcstressを実行する方法を紹介した。jmhは外部の記事のリンクを貼っただけだが。 また、jmh標準のprofile機能の紹介と、jcstressの紹介およびjbangを通した実行方法を紹介した。

jbangは、存在は知っていたのだが、標準でJavaファイルをスクリプト的に実行出来るようになってしまったので あまり興味が沸かず使ったことがなかった。 しかし、最初のリンクの記事を見ると、jbangでは、依存関係をファイルに記述することができ、groovyのGrapeのような仕組みが実装されていることに気付き、これは便利だなぁと思っている。

好きあらばどこかで使いたいが、JavaScriptとかGoで良い説がある。 終わり。

com.palantir.baseline:baseline-error-prone の検査の一覧和訳 part 2/2

part 1の続きです。

progret.hatenadiary.com

和訳しておきます。

com/palantir/baseline/errorprone/ImplicitPublicBuilderConstructor

カプセル化クラス(?)のstatic factory methodを持つビルダーは、private コンストラクタを持つべきです。 不必要なpublic APIを小さくすることで、将来のAPIの変更から利用者への影響を防ぎます。

com/palantir/baseline/errorprone/IncubatingMethod

可能であれば、incubatingのメソッドを使うのは避けるべきです、それらはとても弱い安定性の保証なので。 ケースバイケースで、このチェックを明示的に無効化することができます。

com/palantir/baseline/errorprone/InvocationHandlerDelegation

他のオブジェクトに移譲されているInvocationHandlersは、InvocationTargetException を catchして、unwrapするべきです。 そうしなければ、移譲先が例外をthrowするたびに UndeclaredThrowableException をthrowされます。 このチェックは助言を意図しており、特定のケースでは抑制してもよいですが、通常はおすすめされません。

com/palantir/baseline/errorprone/InvocationTargetExceptionGetTargetException

InvocationTargetException.getTargetException() は一般的な用途の例外の連鎖機能よりも前から存在します。 現在この情報を取得するには Throwable.getCause() メソッドが推奨される手段です。 ソース: https://docs.oracle.com/en/java/javase/17/docs/api//java.base/java/lang/reflect/InvocationTargetException.html#getTargetException()

com/palantir/baseline/errorprone/JUnit5RuleUsage

JUnit5 のテストで、Rule/ClassRules を使っても、静かに実行されません。

com/palantir/baseline/errorprone/JUnit5SuiteMisuse

JUnit4のスイートからJUnit5のテストを参照しても、静かに動きません。

com/palantir/baseline/errorprone/JavaTimeSystemDefaultTimeZone

システム依存の挙動のため、システムのデフォルトのタイムゾーンは使うべきではありません。 代わりに UTCを必ず使うべきです。

com/palantir/baseline/errorprone/JooqBatchWithoutBindArgs

jOOQのバッチメソッドで引数の束縛をしない実行は、パフォーマンスの問題を引き起こす可能性があります。

com/palantir/baseline/errorprone/JooqResultStreamLeak

jOOQのResultQueryにおいて、autocloseableなリソースを返却するメソッドは、try-with-resourcesでcloseされるべきです。 そうしなければ、データーベースのリソースリークを引き起こし、例外がスローされたり、closeの呼び出しに失敗する可能性があります。

com/palantir/baseline/errorprone/LambdaMethodReference

このラムダは、メソッド参照であるべきです。

com/palantir/baseline/errorprone/LogSafePreconditionsMessageFormat

訳者注:

palantirで使われているsafe-loggingの話っぽいので飛ばす。

com/palantir/baseline/errorprone/LoggerEnclosingClass

getLogger(Class<?>) を使って作られたLoggerは、それを囲うクラスで参照するべきです。

訳者注:

テストコード見たほうが早い。 https://github.com/palantir/gradle-baseline/blob/5e773859eee2463ca8b262f8730efff13cdb32db/baseline-error-prone/src/test/java/com/palantir/baseline/errorprone/LoggerEnclosingClassTest.java

com/palantir/baseline/errorprone/LoggerInterpolationConsumesThrowable

意訳: ロガーのinterpolationにthrowableが指定されている。

com/palantir/baseline/errorprone/LoggingDoNotLog

訳者注:

palantirで使われているsafe-loggingの話っぽいので飛ばす。

com/palantir/baseline/errorprone/LogsafeArgName

訳者注:

palantirで使われているsafe-loggingの話っぽいので飛ばす。

com/palantir/baseline/errorprone/LogsafeThrowableArgument

訳者注:

palantirで使われているsafe-loggingの話っぽいので飛ばす。

com/palantir/baseline/errorprone/NonComparableStreamSort

Stream.sorted() は Comparableな型のStreamでのみ呼び出しされるべきです。

com/palantir/baseline/errorprone/ObjectsHashCodeUnnecessaryVarargs

可変長配列のアロケーションを避けるために Objects.hash(non-varargs)java.util.Objects.hashCode(value) で置き換えられるべきです。

com/palantir/baseline/errorprone/OptionalFlatMapOfNullable

Optional.map は 安全に空の結果を生成するために、nullを返す場合があります。

com/palantir/baseline/errorprone/OptionalOrElseGetValue

Optional#orElseGet に渡されたラムダ式が simple expressionなら、代わりに Optional#orElse を使ってください。

com/palantir/baseline/errorprone/OptionalOrElseMethodInvocation

Optional#orElse に渡された式がメソッドを呼び出しているなら、代わりに Optional#orElseGet を使ってください。

com/palantir/baseline/errorprone/OptionalOrElseThrowThrows

Optional#orElseThrow の引数はexceptionを返すべきです。throwするのではありません。

com/palantir/baseline/errorprone/PreconditionsConstantMessage

Preconditions.checkX() では、定数のメッセージのみ許可されています。

訳者注:

Preconditionsはguavaのクラス

com/palantir/baseline/errorprone/PreferBuiltInConcurrentKeySet

Guava の ConcurrentHashSetよりも、Javaの組み込みのConcurrent Setを好みます。 これは間接的な処理(?)を減らしてGuavaに依存しないためです。

com/palantir/baseline/errorprone/PreferCollectionConstructors

Guavaのstatic factory methodよりも、Java 7以降では、標準コレクションコンストラクタを使うべきです。

com/palantir/baseline/errorprone/PreferCollectionTransform

訳者注:

訳者は、Guava使わないので、飛ばす。読むの疲れてきた。

com/palantir/baseline/errorprone/PreferCommonAnnotations

コピーよりも、commonのアノテーションを使います。

訳者注:

guavaに入っているアノテーションのこと。

com/palantir/baseline/errorprone/PreferImmutableStreamExCollections

訳者注:

streamexの話なので飛ばす。

com/palantir/baseline/errorprone/PreferInputStreamTransferTo

ライブラリのutilityメソッドより、JDKInputStream.transferTo(OutputStream) を好みます。 (ex. com.google.common.io.ByteStreams.copy(InputStream, OutputStream), org.apache.commons.io.IOUtils.copy(InputStream, OutputStream), org.apache.commons.io.IOUtils.copyLong(InputStream, OutputStream)) 参考: https://github.com/palantir/gradle-baseline/issues/2615

com/palantir/baseline/errorprone/PreferListsPartition

訳者注:

訳者は、Guava使わないので、飛ばす。

com/palantir/baseline/errorprone/PreferSafeLoggableExceptions

訳者注:

palantirで使われているsafe-loggingの話っぽいので飛ばす。

com/palantir/baseline/errorprone/PreferSafeLogger

訳者注:

palantirで使われているsafe-loggingの話っぽいので飛ばす。

com/palantir/baseline/errorprone/PreferSafeLoggingPreconditions

訳者注:

palantirで使われているsafe-loggingの話っぽいので飛ばす。

com/palantir/baseline/errorprone/PreferStaticLoggers

訳者注:

palantirで使われているsafe-loggingの話っぽいので飛ばす。

com/palantir/baseline/errorprone/PreventTokenLogging

訳者注:

palantirで使われている認証トークンの型をログ出力していると怒られるルール

com/palantir/baseline/errorprone/ProxyNonConstantType

Proxyインスタンスは、native-imageの動作がhotspotと一致するように、コンパイル時に既知の定数の型を使って作られるべきです。 任意のクラスインスタンスの代わりに、proxyをビルドするメソッドは、Function<InvocationHandler, ? extends T> 関数を受け取るべきです。 このチェックは、SuppressWarningsを使って、レガシーコードの中では安全に抑制することが可能です。 proxy annotation processor はこの処理をより簡単にします。ref: https://github.com/palantir/proxy-processor 参照: https://www.graalvm.org/reference-manual/native-image/DynamicProxy/#automatic-detection

com/palantir/baseline/errorprone/RawTypes

Rawタイプを避けてください。可能であれば、実際の型パラメータを追加してください。 これは 必要に応じて、SuppressWarningsで 抑制が可能です, より古いライブラリのコードとやりとりする時など。

com/palantir/baseline/errorprone/ReadReturnValueIgnored

readの呼び出しの結果は、EOFに到達したか、期待している消費するバイトの数かどうかをチェックすべきです。

com/palantir/baseline/errorprone/RedundantMethodReference

同じ型への冗長なメソッド参照

com/palantir/baseline/errorprone/RedundantModifier

冗長な modifierを使うことを避ける

com/palantir/baseline/errorprone/ReverseDnsLookup

address.getHostName を呼び出すと、ネットワーク呼び出しであるDNSの逆引きにつながる可能性があり、その呼出しは、環境によっては、期待しているよりも大幅に高価です。このチェックは、助言を意図しています。特定のケースでは、抑制しても良いですが、通常おすすめできません。

com/palantir/baseline/errorprone/SafeLoggingExceptionMessageFormat

訳者注:

palantirで使われているsafe-loggingの話っぽいので飛ばす。

com/palantir/baseline/errorprone/SafeLoggingPropagation

訳者注:

palantirで使われているsafe-loggingの話っぽいので飛ばす。

com/palantir/baseline/errorprone/ShutdownHook

Runtime#addShutdownHook を直接使うのではなく、webserverの管理リソースの機能を使うべきです。

com/palantir/baseline/errorprone/Slf4jConstantLogMessage

slf4jのログメッセージは、コンパイル時定数のみ許可されています。

com/palantir/baseline/errorprone/Slf4jLevelCheck

slf4jの log.is[Level]Enabled は最も厳しいログ文に一致するべきです。

com/palantir/baseline/errorprone/Slf4jLogsafeArgs

訳者注:

palantirで使われているsafe-loggingの話っぽいので飛ばす。

com/palantir/baseline/errorprone/Slf4jThrowable

slf4jのロガーは、throwableを最後のパラメータに要求します。さもなければ、スタックトレースは生成されません。 ドキュメントはこちらです。 http://www.slf4j.org/faq.html#paramException

com/palantir/baseline/errorprone/SortedStreamFirstElement

Stream::min は 一番小さい要素を探すために、線形探索を実行します。

com/palantir/baseline/errorprone/StreamOfEmpty

不必要な可変長配列のアロケーションを避けるために Stream.of() (実引数なし) は Stream.emptyで置き換えられるべきです。

com/palantir/baseline/errorprone/StrictCollectionIncompatibleType

Objectを受け取るコレクションのメソッドに引数として適合しない型を使っているため、プログラムエラーに見受けられる。

com/palantir/baseline/errorprone/StrictUnusedVariable

使っていない。

com/palantir/baseline/errorprone/StringBuilderConstantParameters

決まった数の引数でStringBuilderを使うことは、単純な結合に置き換えられるべきです。

com/palantir/baseline/errorprone/ThrowError

Error よりも、RuntimeException を投げることを好んでください。エラーはしばしばライブラリによって処理されず、結果として予期しない挙動やリソースリークに繋がります。 cathc(Exception e) は Errorをキャッチしないことは明確ではありません。エラーは、通常JVMによって投げられ、システムが、単にアプリケーションではなく、良くない状態になっているときです。例えば、適合しない型に遭遇したとき、LinkageErrorJVMによって投げられ、また NoClassDefFoundError は、クラスが見つからなかった時に投げられます。これらは、一般的ではなく、アプリケーションの失敗とは異なる処理するべきです。このチェックは助言を意図しています。 特定のケースでは、抑制することは問題ありませんが、通常はおすすめされません。あなたがAssertionErrorを投げるテストライブラリを書いてないなら。

com/palantir/baseline/errorprone/ThrowSpecificity

ExceptionやThrowableではなく、もっとより具体的なthrowする型を定義することを好みます。 新しいチェック例外をthrowするためにメソッドを更新したときに、呼び出し元は失敗した型を明示的に処理することを期待するいます。 広い型をthrowすることは、型システムが無効になります。可能な限り、より具体的な型をthrowすることで、既存のコンパイラの機能を活用して到達しないコードを検査します。 Note: チェック例外は、コンパイラによってのみ検査され、実行時に非標準のバイトコードによって投げられる可能性があります。例えば、JavaからgroovyやScalaが生成したバイトコードを呼び出した時、チェック例外は宣言に内にも関わらず、投げられることがあります。このシナリオのために、私達はこの検査を抑制し、理由を記載したコメントすることをおすすめします。残りのインスタンスは error-proneのsuggested fixで自動的に修正が可能です。

com/palantir/baseline/errorprone/TooManyArguments

たくさんよりも少数の引数を取るインターフェースが好ましい。

com/palantir/baseline/errorprone/UnnecessarilyQualified

importされているのであれば、正規型にすべきではない。

com/palantir/baseline/errorprone/UnnecessaryLambdaArgumentParentheses

単一引数のラムダは、カッコは必要ありません。

com/palantir/baseline/errorprone/UnsafeGaugeRegistration

TaggedMetricRegistry.gaugeを使うことは、map.putIfAbsentと等価です。そして、分かりづらいリソースリークを引き起こす可能性があります。 既存のgaugeの値を置き換えることが好ましい。これは、自動で修正が可能です。

com/palantir/baseline/errorprone/ValidateConstantMessage

Validate.X() のメソッドは定数メッセージのみ許可されています。

訳者注:

org.apache.commons.lang3.Validate のメソッドの話。

com/palantir/baseline/errorprone/VarUsage

var キーワードは、多くのケースで判読不能になり、使うべきではありません。

com/palantir/baseline/errorprone/VisibleForTestingPackagePrivate

VisibleForTestingのメンバーは、package-privateの可視性であるべきです。

com/palantir/baseline/errorprone/ZeroWarmupRateLimiter

zero warmup durationの RateLimitersは rate limitしません。 https://github.com/google/guava/issues/2730 でトラックされています。

訳者注:

guavaの話らしい。

com/palantir/baseline/errorprone/ZoneIdConstant

ZoneIdの定数を好みます。

終わり

com.palantir.baseline:baseline-error-proneのルールを一通り和訳してみました。 大体の検査は一般的に便利そうな検査が多い感じですね。

一方で、guavaやその他の使わなさそうなライブラリのチェックなども入っているようです。 必要に応じて無効化したいですね。

終わり。

gradle向け error-proneでsuggestedFixを適用する方法

以下のプラグインを使っている前提で書きます。

github.com

こうです。 これではうまく動かないので追記を参照

tasks.withType(JavaCompile).configureEach {
    options.errorprone {
        // error-proneのsuggested fixesを適用する
        // usage: ./gradlew compileJava compileTestJava -PerrorProneApply
        if (project.hasProperty("errorProneApply")) {
            errorproneArgs.addAll("-XepPatchLocation:IN_PLACE")
        }
    }
}

2024/10/16追記 動かないケースがあったので直した

上だとうまく動かないケースがあったのでこうした。 もうちょっと楽な方法があったら教えて下さい。

ext {
    errorProneApply = project.hasProperty("errorProneApply")
}

tasks.register('errorProneApply').configure {
    errorProneApply = true
    // error-proneはコンパイル時に動くのでcleanの後に実行する
    dependsOn('clean', tasks.withType(JavaCompile))
}

class ErrorProneApply implements CommandLineArgumentProvider {

    private final Project project

    ErrorProneApply(Project project) {
        this.project = project
    }

    @Override
    Iterable<String> asArguments() {
        if (!project.hasProperty("errorProneApply") || !project.property("errorProneApply")) {
            return []
        }

        return ["-XepPatchLocation:IN_PLACE"]
    }
}

tasks.withType(JavaCompile).configureEach {
    options.errorprone {
        // custom taskでpropertyの上書きをするために、errorproneArgumentProvidersを追加する
        errorproneArgumentProviders.add(new ErrorProneApply(project))
    }
}

終わり。調べても出てこんので、メモ程度に書いておいた。