それが僕には楽しかったんです。

僕と MySQL と時々 MariaDB

JIT コンパイラのコンパイラスレッド、コード最適化について

はじめに

今回は前回の内容に引き続きJIT コンパイラについての話。割と薄くなる予定。
コンパイラスレッドとか最適化とかそのあたり。
Java パフォーマンスには、 JVM エンジニアが JVM の振る舞いを検証するために利用したり、コンパイルに興味を持った人が読むと良いとある。

コンパイラのスレッド

前回の記事で、コンパイルには閾値があると書いたがそれに関連している。
JVM ではコンパイル対象になったメソッドやループはキューに配置され、バックグラウンドで動く一つ以上のスレッドによって非同期プロセスとしてコンパイルが行われる。
そのためコードを実行中であってもコンパイルすることが可能となる。
またその時に利用されるキューは FIFO ではなく、呼び出しカウンタの値が大きいメソッドが優先的に処理される。

デフォルトでは C1 コンパイラはスレッドが一つで、C2 コンパイラはスレッドを二つ利用する。
この値は -XX:CICompilerCount=N で合計数を指定することでスレッド数を制御できる。

コンパイラのスレッドは単一 CPU マシン上であれば、一つにしてしまい競合状態を減らす方がパフォーマンス的には良いとされているがそれはウォームアップ時に大きなメリットがあるだけで多くのホットスポットコンパイルされたあとではメリットが少ない。

また階層型コンパイルを利用している環境下ではスレッド数を多くするとコンパイルスレッドも増えシステム全体に影響を及ぼす場合がある。
この時はスレッド数を減らした方がウォームアップの期間が長くなるというデメリットはあるものの全体のスループットを向上させることができる。

あまり使わないとは思うけど。

コードの最適化

インライン化

以下のコードを考える。

public class Point {
    private int x, y;
    public int getX() { return x; }
    public void setX(int i) { x = i; }
}
Point p = getPoint();
p.setX(p.getX() * 2);

このように呼び出されるコードがある場合、次のようにインライン化される。

Point p = getPoint();
p.x = p.x * 2;

これによりメソッド呼び出しが省略されるため、処理速度が向上する。
これまでのコードキャッシュやスレッドとは異なり、インライン化が行われる場合の振る舞いを見る術がない。
しかし、JVMソースコードビルドするのであれば -XX:+PrintInlining というフラグを利用可能にすることもできる。

インライン化するかどうかの判断基準はメソッドがよく利用されるかどうかとそれ自体のサイズに大きく影響される。
そのため、 -XX:MaxFreqInlineSIze, -XX:MaxInlineSize でインライン化するメソッドのサイズを調整することもできる。

エスケープ分析

次はエスケープ分析について。
これはデフォルトで有効になっていて、これが利用できる場合は積極的に最適化を行う。
最適化を行う項目はいろいろとあるらしいが、書籍に書かれているものであればループ内でのみ特定のオブジェクトが利用されている場合
同期ロックを取らないようにしたり、フィールドの値をメモリに持つ必要がなければレジスタに配置したり、オブジェクト自体が各フィールドだけ管理するようになったりと様々。

これはシンプルなコードだけでなく複雑なコードに対しても同様に適用される。
最適化がうまくいかない場合は、JVM のバグであることが多いそうだが、一番簡単で効果のある対処法として対象のコードをシンプルにすることが有効らしい。

おわりに

JIT コンパイルの話はいったんここまで。
次は GC とか。

JIT コンパイラのコードキャッシュ

はじめに

前回は JIT コンパイラの基礎について触れたので、今回は JIT コンパイラのコードキャッシュとそのチューニングついて。

基本的なものだと、どちらのコンパイラを選択するか、階層コンパイルを利用するかどうかがあるらしいが
もう少し踏み込んだ内容について書いていく。

今回も例によって Java パフォーマンスを参考文献にしている。

コードキャッシュとそのチューニング

コードキャッシュとは

コードキャッシュとは JVM がコードをコンパイルすることによって生成されたネイティブコードを持っている領域のことで Java 9 以降ではコードヒープと呼ばれている。
この領域を圧迫すると、一部のホットスポットだけコンパイルが適用され、その他の部分はインタープリターで実行される or コンパイル適用を一旦破棄するなど性能に大きな影響を及ぼす。

コードキャッシュの確認

  • XX:+PrintCodeCache オプションをつけることで確認できる。
❯ java -XX:+PrintCodeCache -version
openjdk version "11.0.8" 2020-07-14 LTS
OpenJDK Runtime Environment Corretto-11.0.8.10.1 (build 11.0.8+10-LTS)
OpenJDK 64-Bit Server VM Corretto-11.0.8.10.1 (build 11.0.8+10-LTS, mixed mode)
CodeHeap 'non-profiled nmethods': size=120032Kb used=15Kb max_used=15Kb free=120016Kb
 bounds [0x0000000126cd2000, 0x0000000126f42000, 0x000000012e20a000]
CodeHeap 'profiled nmethods': size=120028Kb used=97Kb max_used=97Kb free=119931Kb
 bounds [0x000000011f79b000, 0x000000011fa0b000, 0x0000000126cd2000]
CodeHeap 'non-nmethods': size=5700Kb used=972Kb max_used=977Kb free=4728Kb
 bounds [0x000000011f20a000, 0x000000011f47a000, 0x000000011f79b000]
 total_blobs=299 nmethods=71 adapters=141
 compilation: enabled
              stopped_count=0, restarted_count=0
 full_count=0

CodeHeap の種類は 3 つあり

  • non-profiled nmethods
  • profiled nmethods
  • non-nmethods

と分かれている*1
これらを合計しておおよそ 240MB の領域が割り当てられている。

コードキャッシュが圧迫された場合の挙動

このコードキャッシュ(コードヒープ)の挙動で重要となるのは、コードキャッシュが圧迫された場合の挙動で、コンパイルしたネイティブコードをコードキャッシュに配置できなくなると JIT コンパイルを停止する。この時、コードキャッシュがいっぱいになったというログメッセージが出力される。
コードキャッシュにネイティブコードを配置できなくなると JVM は一度最適化したネイティブコードを破棄し、Java のプログラムを実行する際に設定されている最低量のコードキャッシュを確保しようとする。*2
それらの処理が完了した段階で JIT コンパイルを再開する。

単純なチューニング

これらからわかるようにコードキャッシュのことを考慮した場合、重要となるのはコードキャッシュの上限サイズとなる。
上限サイズをどのように設定すればよいといった定量的な基準はなく場当たり的に設定する必要が出てくる。*3

コンパイル時の基準

コードキャッシュ自体のチューニングと言うとサイズを調整するぐらいしか見当たらないが、JIT コンパイルの特性を知る事で間接的にパフォーマンスに影響を与えることができる。
そのうちの一つがどのタイミングでコンパイルが行われるかということを明示的に指定する方法。
前回は JIT コンパイルで C1, C2 コンパイラについてまとめたが、それらがどう言った基準で適用されているかについて知り、それを設定することでコードキャッシュを有効活用できる場合がある。
JIT コンパイル時は呼び出しカウンタとバックエッジカウンタの二つを JVM が記録しておりこの総和が閾値を超えた段階で JIT コンパイルが適用される。
呼び出しカウンタはそのままの意味だが、バックエッジカウンタとはメソッド内のループ内で処理が完了した回数を示している。
やたら複雑で巨大なメソッドを実行する場合 JVM はメソッドの終了を待つ事なくコンパイルを実行するケースがある。その時はバッグエッジカウンタが個別の閾値を超えるとメソッド全体ではなく該当するループ処理がコンパイル対象となる。
こういったコンパイルを OSR *4*5 と呼ぶ。

ここまで長々とまとめたがカウンタについては -XX:CompileThreshold でどれだけその値を変更することができる。デフォルトでは C1 コンパイラが 1500, C2 コンパイラが 10000 と設定されている。

コンパイルプロセスを理解する

コードキャッシュと密接な関わりがある JIT コンパイルを理解することはチューニングとは直接関係しないが大事なことなのでまとめる。
前回も行った -XX:PrintCompilation フラグを利用して JIT コンパイルの挙動を知るという操作。これについてもう少し解説する。
次のログを参考にする。

    460  902       3       com.sun.tools.javac.comp.Flow$BaseAnalyzer::scan (27 bytes)
    460  903       3       com.sun.tools.javac.code.Types::isSignaturePolymorphic (111 bytes)
    462  904       3       com.sun.tools.javac.tree.JCTree::pos (2 bytes)
    462  906       3       com.sun.tools.javac.tree.JCTree::hasTag (14 bytes)
    462  907       3       com.sun.tools.javac.code.Type$ClassType::accept (9 bytes)
    462  905       1       com.sun.tools.javac.code.Type$JCPrimitiveType::isPrimitive (2 bytes)
    463  225   !   3       jdk.internal.jimage.BasicImageReader::slice (32 bytes)   made not entrant
    464  663       4       java.util.HashMap::put (13 bytes)
    464  908       3       java.lang.invoke.DirectMethodHandle::make (263 bytes)
    464   96       3       java.util.HashMap::put (13 bytes)   made not entrant
    465  736       4       java.lang.String::toString (2 bytes)
    465  472       3       java.lang.String::toString (2 bytes)   made not entrant
    465  910       3       java.lang.invoke.MethodHandles$Lookup::<init> (15 bytes)
    465  909       3       java.lang.invoke.MemberName::isField (7 bytes)
    465  911       3       java.util.ArrayList::isEmpty (13 bytes)
    466  912       3       com.sun.tools.javac.jvm.Code::typecode (141 bytes)
    467  913       3       com.sun.tools.javac.jvm.Code::width (42 bytes)
    467  914       3       com.sun.tools.javac.jvm.Code::width (16 bytes)
    467  915       3       com.sun.tools.javac.code.Type::typeNoMetadata (19 bytes)
    468  713       4       java.lang.StringLatin1::indexOf (121 bytes)   made not entrant
    470  916 %     3       SampleAlgorithm::main @ 15 (65 bytes)
    470  917       3       SampleAlgorithm::main (65 bytes)
    470  918 %     4       SampleAlgorithm::main @ 15 (65 bytes)
    472  916 %     3       SampleAlgorithm::main @ 15 (65 bytes)   made not entrant
    672  918 %     4       SampleAlgorithm::main @ 15 (65 bytes)   made not entrant

まず重要なこととしてこのログは以下のフォーマットを取る。

タイムスタンプ コンパイルID 属性 階層型コンパイルレベル メソッド名 サイズ 非最適化

コンパイルレベルに関しては前回の通りなので省略。

タイムスタンプは JVM が起動してからコンパイルが完了するまでの相対的な時間を示している。
コンパイルID は内部的な ID で単調増加するが C2 コンパイラや、非同期処理が利用されると必ずしも昇順になるとは限らない。
属性はコンパイルされるコードの属性を示していて以下の通り

残りで重要なものは非最適化についての記述。
これには made not entrant, made zombie があるがここではまとめない。次の記事でまとめる。

ここにはないがコンパイラログにはコードキャッシュがいっぱいになったことや、コンパイル中に対象クラスが変更されたため再コンパイルが行われることなどが出力される場合もある。
これらログから想定されたコンパイルが行われているかなどの確認が可能なので、性能に思うところがあればみる事がおすすめされる。

非最適化について

PrintCompilation で非最適化される場合の挙動について。
非最適化とはすでに行われて完了している最適化をもとの状態に戻すことで、次の二つのパターンで非最適化が行われる。

entrant ではないコード

entrant *6ではないコードは非最適化の対象となる。
これが発生する原因はクラスとインターフェースの仕組みに起因するものと、階層型コンパイルに起因するものがある。

まずインターフェースに起因する場合。
次のような実装を考える。

HogeRepository repository;
String param = request.getParameter("hoge");
if (param != null && param.equals("hoge")) {
     repository = new HogeRepositoryImpl();
} else {
    repository = new HogeLogger();
}

このように特定のパラメータか何かに強く依存し、インターフェースのインスタンスが異なる場合 param が hoge を取り続け C2 コンパイルが適用されると repository の型が HogeRepositoryImpl だと判断される場合がある。
そうなった場合、 else の処理が何らかの原因で呼ばれ続けると最適化した型情報が変わるため entrant なコードとして非最適化する必要が出てくる。


次に階層型コンパイルが起因となる場合について。
階層型コンパイルでは C1 コンパイルが適用され、その後 C2 コンパイルが行われる場合がある。
この時 C2 コンパイルが行われ利用できる状態になったら JVM は C1 コンパイルの結果であるコードを置き換える必要があり、置き換える対象となるクラスに印的なものとして made not entrant をつける。

zombie コード

この zombie とは entrant ではない古いコードが破棄されたことを示すもの。
entrant になっても対象のオブジェクトは残り続けどこかのタイミングで GC によって開放される。
ゾンビ化したコードはコードキャッシュから削除することができるため、それによって JVM のスペースに新たなコードを置くことができる。
ただし、ゾンビ化したコードが再度ロードされ、頻繁に呼び出された場合は再度最適化を行う必要が出てきてしまう。

おわりに

次はコンパイラスレッドとかについてまとめる。

参考文献

*1:nmethods とは JIT コンパイルによってコンパイルされたメソッドの事

*2:-XX:CodeCacheMinimumFreeSpace で設定できる

*3:一説によると、デフォルトの 2, 4 倍するのが一般的らしい

*4:On-Stack Replacement

*5:これもチューニング可能だが、ここではまとめない

*6:中に入れないという意味らしい

Java のJIT コンパイルについて

はじめに

今回は JavaJIT コンパイルについて。
Java パフォーマンスと各種信頼できるドキュメントを参照しつつまとめていく。

JIT コンパイルとは

Java のコードは javac コマンドでコンパイルすると中間バイトコードに変換され、それを JVM が解釈することによってアプリケーションが実行される。
中間バイトコードJVM が解釈できる形になるため、Javaインタープリター言語のようなプラットフォームにおける独立性を確保している。
この後 JVMインタープリターのように中間バイトコードを解釈するが、その時に JVM を実行しているプラットフォーム毎にバイナリにコンパイルされる場合がある。
これを Just-In-Time コンパイルという。
f:id:RabbitFoot141:20201121152307p:plain

JIT コンパイルによる効果を確かめる

エラトステネスの篩を使って素数を求める以下のコードを利用する。

public class SampleAlgorithm {
	
	private static final int LIMIT = 1000000;
	
	public static void main(String... args) {
		int primeCount = 2;
		boolean isPrime = false;
		for (int i = 5; i <= LIMIT; i++) {
			for (int j = 2; j * j <= i; j++) {
				if (i % j == 0) {
					isPrime = false;
					break;
				}
				isPrime = true;
			}
			if (isPrime) {
				primeCount += 1;
			}
		}
		System.out.println(primeCount);
	}
}

このコードを -Xint オプションをつけた場合、つけなかった場合で処理時間を比較する。

❯ time java -Xint SampleAlgorithm.java
78498
java -Xint SampleAlgorithm.java  1.80s user 0.05s system 98% cpu 1.876 total
❯ time java SampleAlgorithm.java 
78498
java SampleAlgorithm.java  1.14s user 0.07s system 160% cpu 0.753 total

1000000 までの素数であれば微々たる差だが、桁を増やすと差が明確になっていく。
1億以下の素数を計算すると以下のようになる。

❯ time java -Xint SampleAlgorithm.java 
664579
java -Xint SampleAlgorithm.java  21.35s user 0.13s system 99% cpu 21.590 total
❯ time java SampleAlgorithm.java 
664579
java SampleAlgorithm.java  6.18s user 0.08s system 108% cpu 5.769 total

JIT コンパイルの動作

実行時のオプションに -XX:+PrintCompilation をつけることで JIT コンパイルが何を行っているかわかる。
このフォーマットには列ごとに意味があるが、それは次の JIT コンパイラのチューニング part1 でまとめる。

❯ java -XX:+PrintCompilation SampleAlgorithm.java
   .....
   530  958       3       com.sun.tools.javac.tree.JCTree::hasTag (14 bytes)
    530  957       1       com.sun.tools.javac.code.Type$JCPrimitiveType::isPrimitive (2 bytes)
    530  959       3       com.sun.tools.javac.code.Type$ClassType::accept (9 bytes)
    532  960       3       java.lang.invoke.DirectMethodHandle::make (263 bytes)
    533  961       3       java.lang.invoke.MemberName::isField (7 bytes)
    533  962       3       java.util.ArrayList::isEmpty (13 bytes)
    534  963       3       com.sun.tools.javac.jvm.Code::typecode (141 bytes)
    534  964       3       com.sun.tools.javac.jvm.Code::width (42 bytes)
    534  965       3       com.sun.tools.javac.jvm.Code::width (16 bytes)
    535  966       3       com.sun.tools.javac.code.Type::typeNoMetadata (19 bytes)
    536  734       4       java.lang.StringLatin1::indexOf (121 bytes)   made not entrant
    537  309       3       jdk.internal.jimage.ImageStringsReader::stringFromByteBuffer (28 bytes)   made not entrant
    537  828       4       java.lang.ref.Reference::reachabilityFence (1 bytes)
    537  169       3       java.lang.ref.Reference::reachabilityFence (1 bytes)   made not entrant
    537  814       4       com.sun.tools.javac.util.ListBuffer::toList (10 bytes)
    537  635       3       com.sun.tools.javac.util.ListBuffer::toList (10 bytes)   made not entrant
    537  755       4       java.lang.String::toString (2 bytes)
    537  480       3       java.lang.String::toString (2 bytes)   made not entrant
    537  967 %     3       SampleAlgorithm::main @ 15 (65 bytes)
    538  968       3       SampleAlgorithm::main (65 bytes)
    538  969 %     4       SampleAlgorithm::main @ 15 (65 bytes)
    539  967 %     3       SampleAlgorithm::main @ 15 (65 bytes)   made not entrant
    745  969 %     4       SampleAlgorithm::main @ 15 (65 bytes)   made not entrant
  • XX:+LogCompilation をつけるとより詳細な以下のようなログを確認することができる。
<<<一部抜粋>>>
<nmethod compile_id='1' compiler='c1' level='3' entry='0x000000011d1911c0' size='1056' address='0x000000011d191010' relocation_offset='376' insts_offset='432' stub_offset='816' scopes_data_offset='872' scopes_pcs_offset='936' dependencies_offset='1032' nul_chk_table_offset='1040' metadata_offset='864' method='java.lang.StringLatin1 hashCode ([B)I' bytes='42' count='101' backedge_count='2096' iicount='101' stamp='0.054'/>
<writer thread='9731'/>
<task_queued compile_id='2' method='java.util.concurrent.ConcurrentHashMap tabAt ([Ljava/util/concurrent/ConcurrentHashMap$Node;I)Ljava/util/concurrent/ConcurrentHashMap$Node;' bytes='22' count='256' iicount='256' level='3' stamp='0.054' comment='tiered' hot_count='256'/>
<task_queued compile_id='3' method='jdk.internal.misc.Unsafe getObjectAcquire (Ljava/lang/Object;J)Ljava/lang/Object;' bytes='7' count='256' iicount='256' level='3' stamp='0.054' comment='tiered' hot_count='256'/>
<task_queued compile_id='4' method='java.lang.String coder ()B' bytes='15' count='256' iicount='256' level='3' stamp='0.054' comment='tiered' hot_count='256'/>
<writer thread='42755'/>
<nmethod compile_id='2' compiler='c1' level='3' entry='0x000000011d191660' size='1376' address='0x000000011d191490' relocation_offset='376' insts_offset='464' stub_offset='1168' scopes_data_offset='1240' scopes_pcs_offset='1288' dependencies_offset='1368' oops_offset='1216' metadata_offset='1224' method='java.util.concurrent.ConcurrentHashMap tabAt ([Ljava/util/concurrent/ConcurrentHashMap$Node;I)Ljava/util/concurrent/ConcurrentHashMap$Node;' bytes='22' count='260' iicount='260' stamp='0.055'/>
<writer thread='9731'/>
<task_queued compile_id='5' method='java.lang.String isLatin1 ()Z' bytes='19' count='256' iicount='256' level='3' stamp='0.055' comment='tiered' hot_count='256'/>
<writer thread='42755'/>
<nmethod compile_id='3' compiler='c1' level='3' entry='0x000000011d191bc0' size='856' address='0x000000011d191a10' relocation_offset='376' insts_offset='432' stub_offset='720' scopes_data_offset='776' scopes_pcs_offset='800' dependencies_offset='848' metadata_offset='768' method='jdk.internal.misc.Unsafe getObjectAcquire (Ljava/lang/Object;J)Ljava/lang/Object;' bytes='7' count='263' iicount='263' stamp='0.055'/>
<nmethod compile_id='5' compiler='c1' level='3' entry='0x000000011d191f40' size='880' address='0x000000011d191d90' relocation_offset='376' insts_offset='432' stub_offset='752' scopes_data_offset='808' scopes_pcs_offset='824' dependencies_offset='872' metadata_offset='800' method='java.lang.String isLatin1 ()Z' bytes='19' count='259' iicount='259' stamp='0.055'/>
<nmethod compile_id='4' compiler='c1' level='3' entry='0x000000011d1922c0' size='816' address='0x000000011d192110' relocation_offset='376' insts_offset='432' stub_offset='688' scopes_data_offset='744' scopes_pcs_offset='760' dependencies_offset='808' metadata_offset='736' method='java.lang.String coder ()B' bytes='15' count='262' iicount='262' stamp='0.055'/>
<writer thread='9731'/>
<task_queued compile_id='6' method='java.lang.Object &lt;init&gt; ()V' bytes='1' count='512' iicount='512' level='3' stamp='0.057' comment='tiered' hot_count='512'/>
<writer thread='42755'/>

この時に重要になるのが level の値。これは階層型コンパイルが行われ、その際にどのコンパイルが適用されたかを示している。
レベルは以下の通り。

クライアントコンパイルとサーバコンパイル

最近の JVMJIT でクライアントコンパイルとサーバコンパイル*1を併用する。
クライアントコンパイルとサーバコンパイルの違いはコンパイルをどの程度積極的に行うかにあり、クライアントコンパイラは可能な限り該当コードをコンパイルし、サーバコンパイラインタープリター的に実行しつつ実行しているコードについての情報を用いて最適化したコンパイルを行う。
JVM によっては -client, -server, -d64 オプションをつけることで片方を明示的に指定できる。(あまり使うことはないと思うが)
最近の JVM だとこの辺りがベンダー、プラットフォーム依存になっていて -client を指定してもサーバコンパイルが適用される場合もある。

トレードオフの存在

クライアントコンパイルは、最適化を施したコンパイルを行わないため少ないコストでネイティブコードを吐けるという性能上の特徴があり、反対にサーバコンパイルは最適化も含めてネイティブコードにするのでネイティブコードになるまでにコストはかかるがその分実行時パフォーマンスを向上させやすいという特徴がある。
またインタープリターと JIT コンパイルでは、繰り返し利用されないメソッドなどはインタープリターで実行した方がコンパイルにかかるコストを削減でき、反対によく利用される箇所はコストをかけてコンパイルした方が実行時のパフォーマンスが向上するなどそれぞれトレードオフが存在している。

階層型コンパイル

階層型コンパイルはクライアントコンパイルとサーバコンパイルを併用する。アプリケーションが起動する最初の頃はクライアントコンパイルが適用されて、繰り返し利用されているコードなどを検出した場合はサーバコンパイルが適用される。
この階層型コンパイルJava 8 以降ではデフォルトとなっている。

*1:それぞれ C1, C2 コンパイラと呼ばれる場合がある

CLion を使って MySQL のビルド&デバッグを行う

はじめに

どうも、4連休のほとんどを MySQLデバッグで溶かしそうなけんつです。

MySQL を触っていると、SQL の実行結果が謎な時とか稀によくあると思っているのだがそのとき気軽に MySQLデバッグしたくないですか?したいですよね。という気持ちでこの記事を書いています。
というわけでやります。

対象バージョンは MySQL 8.0.21。
Clang, CMake, make, lldb などが入っている前提で話を進めます。
macOS Catalina も前提。おそらく Linux とかなら Clang ではなく gcc になるはずだが、CLion がよしなにやってくれるはず。

MySQL のビルド&デバッグをする

ソースコードを持ってきて展開する

まずはここから、github からでも wget でもどちらでもよい。
boost が同梱しているか、していないかあたりで cmake のオプションが変わってくるので注意。
そのあたりを考えるのが面倒なのでいつも boost 同梱版を wget で持ってきている。

$ wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-boost-8.0.21.tar.gz
$ tar xzvf mysql-boost-8.0.21.tar.gz

CLion で CMake を実行する

ここからは CLion 上の話になっていく。
まずは mysql-server をビルドするところから。気軽に Build から選択しても CMake Project がないと言われるので
Preferences -> Build, Execution, Deployment -> CMake からビルド時の設定を変更する。
以下の二つを設定した段階で、Apply を押すと自動的に CMake が走ってくれる。

CMake Options

CMake options を以下のように修正している。

-DWITH_DEBUG=1 -DWITH_BOOST=./boost -DFORCE_INSOURCE_BUILD=1

ここで大事なのは WITH_DEBUG オプションが有効になっていること。
これがないとデバッグモードで mysqld を起動できない。

Generation path

これはカレントディレクトリを指定しておく。

ビルド

ここは少し面倒で、初回起動時に初期化する必要があるのでターミナルで make を実行する。

mysql-8.0.20 $ make -j7

これがまたややしばらくかかるので待つ。

これが終わったら一度 mysqld を初期化する

mysql-8.0.20 $ ./bin/mysqld --initialize

このとき、標準I/O の最後の方に root ユーザのパスワードが書かれているので忘れずに確認する。

デバッグ

Preferences -> Build, Execution, Deployment -> Toolchains から CMake, LLDB などのバージョンを必要に応じて確認しておく。
バンドルされているっぽいのでこのあたりは自動的に設定されている場合が多い。

デバッグ設定の変更

Run -> EditConfigurations -> CMake Application を選び、そこから mysqld を選択して Apply する。

ブレークポイントを設定

sql/sql_parse.cc の 1178 行目にある do_command に設定すると、大体の処理はここを通るのでわかりやすい。

実行する

Run -> Debug を実行する。
このとき make が走り、コードに修正を加えたなら再度ビルドされる。

ビルド完了後に mysqld が実行されるが、Console が真っ赤に染まっても正常なログなので無視する。

mysql クライアントから接続する

すでに mysql コマンドがある場合はそれでも行けるが、なんらかの理由で接続できない場合は bin/mysql を利用することで接続できる。
そして SELECT 1; とか実行すればブレークポイントで止まるはず。

おわりに

これで MySQL の内部処理がみやすくなる。(はず
ビルドをターミナルから行うところが少しイケてない。

余談

VSCode でも可能で、こちらの記事が参考になる。
labs.gree.jp

OSS にちょっと技術的な貢献するまでの流れ

はじめに

どうも、最近 Hulu でナルトが全話配信されていることに気がつき週末を溶かしたけんつです。
以前、MySQL の公式 Docker イメージにプルリクを投げたらようやくマージされたので、何をやったのか流れをまとめようかなと思って書いている。

よく「OSS に貢献してみよう」と言われるとドキュメントの翻訳や修正などが手頃で OSS 貢献の入門には丁度良いと言われる。
もう少し技術的なプルリクを出したい時にどうすればいいかは情報がなかったので、それらに意欲がある人に向けて少しでも役に立てばというモチベーションで書いている。

やったこと

github.com

MySQL の公式 Docker イメージに設定することが非推奨になっているオプションが明示的に設定されている箇所を見つけたので、該当するオプションを削除するプルリクを出した。

プルリクエストを投げるまでの流れ

問題が見つかる

MySQL の勉強をしようと思って Docker イメージを起動してログを見ていたら素のイメージでも Warning がいくつか発生していることに気がつき全部原因を調べればそれなりに勉強になると思って原因を調べることにしたのだが
そのなかで、どうやら非推奨オプションを明示的に指定しているとわかったので余計なログを抑制する名目で修正することにした。


Warning が発生している場合は非推奨の何かを使っていることがあり、それらは将来的に修正することがあったりする。
なのでそういう場合は積極的に修正しても良いし、何よりバグ見つけて貢献より圧倒的にハードルが低いがドキュメント修正よりは技術的な内容になりがちなのでおすすめしたい。

信頼できる情報をまとめておく

今回は該当するオプションが非推奨であることが MySQL 8.0 の公式ドキュメントとリリースノートに記載されていたのでそれを根拠に修正を進める。
後々、プルリクの概要欄などに使えるのでどこかにメモしておくと良い。

Symbolic link support as described here, along with the --symbolic-links option that controls it, is deprecated and will be removed in a future version of MySQL. In addition, the option is disabled by default.
https://dev.mysql.com/doc/refman/8.0/en/symbolic-links-to-tables.html:

Symbolic link support as described at Using Symbolic Links for MyISAM Tables on Unix, along with the --symbolic-links option that controls it, is now deprecated and will be removed in a future MySQL version. In addition, the option is now disabled by default. The related have_symlink system variable also is deprecated and will be removed in a future MySQL version.
https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-2.html:

リポジトリを fork する

対象のリポジトリを fork する。
今回 fork したのは以下の対象となる以下のリポジトリ
github.com


fork したやつがこれ。このリポジトリに修正を加えていく。
github.com

README を確認する。

README の Contribute みたいな項目を設けている場合は、プルリクにどういう要件があるのか明記されていることが多いので確認する。
それに従わないと修正してもマージされないことがあるので確認する。

今回は特になかったが、例えば以前ドキュメントにプルリクを出した thephpleague/oauth2-client では以下のようにコントリビュートの要件がかなり明確に決められている。

Contributing

Contributions are welcome and will be fully credited.

We accept contributions via Pull Requests on Github.

Pull Requests

  • PSR-2 Coding Standard - The easiest way to apply the conventions is to install PHP Code Sniffer.
  • Add tests! - Your patch won't be accepted if it doesn't have tests.
  • Document any change in behaviour - Make sure the README and any other relevant documentation are kept up-to-date.
  • Consider our release cycle - We try to follow SemVer. Randomly breaking public APIs is not an option.
  • Create topic branches - Don't ask us to pull from your master branch.
  • One pull request per feature - If you want to do more than one thing, send multiple pull requests.
  • Send coherent history - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting.
  • Ensure tests pass! - Please run the tests (see below) before submitting your pull request, and make sure they pass. We won't accept a patch until all tests pass.
  • Ensure no coding standards violations - Please run PHP Code Sniffer using the PSR-2 standard (see below) before submitting your pull request. A violation will cause the build to fail, so please make sure there are no violations. We can't accept a patch if the build fails.

Testing

The following tests must pass for a build to be considered successful. If contributing, please ensure these pass before submitting a pull request.

$ ./vendor/bin/parallel-lint src test
$ ./vendor/bin/phpunit --coverage-text
$ ./vendor/bin/phpcs src --standard=psr2 -sp

Happy coding!

https://github.com/thephpleague/oauth2-client/blob/master/CONTRIBUTING.md

過去にマージされた修正から傾向を掴む

もし明確なコントリビュートの要件がない場合は、過去にマージされたプルリクから何があるとレビュワーにとって嬉しいのかを判断できる場合があるので見てみると良い。
ブランチの命名規則や、プルリクの概要、コミットの粒度など。

ある程度みたら何がクローズされているかも見れば、修正は正しいがマージされないケースを知ることができることがある。
master ブランチにプッシュしているなど。

問題箇所を修正する

ブランチを切る

コントリビュートの要件にしたがってブランチを切る。
今回は特になく、fork したリポジトリの master ブランチからプルリクを飛ばしても良いみたいだったのでそうしている。

修正する

問題の箇所を修正する。

今回は以下の設定を削除している。
github.com

ビルド、テスト、実行

ビルドが通り、テストをパスし、実行しても不具合がないことの確認をする。

今回は Dockerfile からイメージをビルドして、docker run している。
その段階でログを確認して、対象の Warning が出力されないことを確認している。
CI を利用していたので、正しく動作していることが確認できたらプッシュして、CI でテストがパスされればここのフェーズは完了。
テストが落ちた場合は、適宜修正する。

プルリクを作成する

プッシュしたら Github の Web UI からプルリクを作成できるので実行する。
その段階でプルリクの概要を記載する必要がある。

概要を記載する

ここは空でもいい場合があるが、自分は空でいい場合でも書く派なので書く。
何を書くかというと以下の3点を意識すると良いと思う。

  • プルリクで何を修正、追加したのか(この時判断材料となるドキュメントがあれば載せる)
  • 予期する結果がどういうものか(ログや実行結果など)
  • 必要となる設定、変更後の利用方法(ビルド方法など、利用方法に変更がある場合)

ここまで書いたらプルリクの作成を完了する。

マージされることを天に祈る

レビューが返ってきたら、適宜修正する。


やることをやり切ったらあとは祈るだけ。

まとめ

  • Warning はバグ潰しよりも遭遇し、非推奨の何かが絡んでくることが多く修正しやすい
  • コントリビュートの要件は遵守する
  • 概要は要点を抑えてしっかり書く

余談

書いていて思ったがバグに遭遇した場合も検証したことはないが、今までみた OSS のプルリクをみる限りこれでいけると思う。

MySQL 8.0.15 の前後で変わった文字列と DATE 型の比較について

はじめに

そういえば、最近この手の記事を書いてないし何ならインプットもしてない事を思い出し、今日雑にテストしてたらたまたまハマった面白い挙動があったのでまとめる。
久々に MySQL と格闘した。

前提条件

こんなテーブルを作っておく。

create table hoge(t DATE);
insert into hoge(t) values("2020-01-01");
mysql> desc hoge;
+-------+------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+------+------+-----+---------+-------+
| t     | date | YES  |     | NULL    |       |
+-------+------+------+-----+---------+-------+
1 row in set (0.01 sec)

mysql> select * from hoge;
+------------+
| t          |
+------------+
| 2020-01-01 |
+------------+
1 row in set (0.01 sec)

こうなればおっけー。

謎現象を再現する

8.0.15 以下

mysql> select * from hoge where t > "2020";     
+------------+                                                                                
| t          |                                                                                
+------------+                                                                                
| 2020-01-01 |                                                                                
+------------+                                                                                
1 row in set, 1 warning (0.00 sec)

mysql> show warnings;
+---------+------+------------------------------------------------------+
| Level   | Code | Message                                              |
+---------+------+------------------------------------------------------+
| Warning | 1292 | Incorrect date value: '2020' for column 't' at row 1 |
+---------+------+------------------------------------------------------+
1 row in set (0.00 sec)                                

これも個人的には??という感じだったが、まぁとりあえず放置。

8.0.16 以上

mysql> select * from hoge where t > "2020";
ERROR 1525 (HY000): Incorrect DATE value: '2020'

エラーになる。

原理

まず、MySQL 8.0.15 以下では文字列と DATE 型を比較する場合、以下の手順を踏む。
1. 文字列を DATE (or DATETIME) に変換して比較する
2. 1.の変換ができない場合は逆に DATE を文字列に変換して比較する


これが MySQL 8.0.16 以上では次のような手順に変更された。
1. 文字列を DATE (or DATETIME) に変換して比較する
2. 1.が失敗した段階でエラーとなる


これ、MySQL 8.0.16 以上を触っている人からすると普通にやべぇなって思うかもしれないが
MySQL 5.7.x から MySQL 8.0.16 以上にアップグレードしてからこれがわかるとまぁびっくりすることこの上ない。
しかも、ガチで文字列になっているみたいで DATE 型の値を文字列に直して評価したときと同じ結果になる。
断じて内部的に 2020 という文字列が "2020-01-01" みたいに修正されているわけではない。
ガチで DATE が文字列として評価されているみたい。

文献

MySQL 8.0.16 の Release Note に書いてあった。

When comparing DATE values with constant strings, MySQL first tries to convert the string to a DATE and then to perform the comparison. When the conversion failed, MySQL executed the comparison treating the DATE as a string, which could lead to unpredictable behavior. Now in such cases, if the conversion of the string to a DATE fails, the comparison fails with ER_WRONG_VALUE. (Bug #29025656)
(DATE値を定数文字列と比較する場合、MySQLは最初に文字列をDATEに変換してから比較を実行しようとします。変換に失敗した場合、MySQLはDATEを文字列として扱って比較を実行するため、予測できない動作が発生する可能性がありました。このような場合、文字列のDATEへの変換に失敗すると、ER_WRONG_VALUEで比較が失敗するようになりました。(バグ#29025656))

MySQL :: MySQL 8.0 Release Notes :: Changes in MySQL 8.0.16 (2019-04-25, General Availability)

いや、これ Bug 扱いかよとは思ったが一応そういうことらしい。

ちなみに 8.0.15 で Bug Report が出されていて、コメントがついていた。

Documented fix as follows in the MySQL 8.0.16 changelog:

When comparing DATE values with constant strings, MySQL first
tries to convert the string to a DATE and then to perform the
comparison. When the conversion failed, MySQL executed the
comparison treating the DATE as a string. Now in such cases, if
the conversion of the string to a DATE fails, the comparison
fails with ER_WRONG_VALUE.

Closed.

(一部抜粋)
( DATE値を定数文字列と比較する際に、MySQLが文字列をDATEに変換してから比較を実行しようとする。変換に失敗した場合、MySQL は DATE を文字列として扱い比較を実行していました。このような場合、文字列のDATEへの変換に失敗すると、ER_WRONG_VALUEで比較に失敗します。)

MySQL Bugs: #93513: Unexpected behaviour for date comparison with constant strings

おわりに

今日はこれに4時間溶かしました。

MySQL の docker コンテナを起動した時の warning, error を調べつくす

はじめに

ふと MySQL の docker コンテナのログを見たら、なにやら warning や error を吐いていたから原因を調べてみた。
基本的な設定は password 周りと port 33060, MYSQL_ROOT_PASSWORD のみ設定している

問題のログ

master_1  | 2020-05-24 14:14:47+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.18-1debian9 started.
master_1  | 2020-05-24 14:14:47+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
master_1  | 2020-05-24 14:14:47+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.18-1debian9 started.
master_1  | 2020-05-24 14:14:47+00:00 [Note] [Entrypoint]: Initializing database files
master_1  | 2020-05-24T14:14:47.489542Z 0 [Warning] [MY-011070] [Server] 'Disabling symbolic links using --skip-symbolic-links (or equivalent) is the default. Consider not using this option as it' is deprecated and will be removed in a future release.
master_1  | 2020-05-24T14:14:47.490218Z 0 [System] [MY-013169] [Server] /usr/sbin/mysqld (mysqld 8.0.18) initializing of server in progress as process 48
master_1  | 2020-05-24T14:14:51.079329Z 5 [Warning] [MY-010453] [Server] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.
master_1  | 2020-05-24 14:14:54+00:00 [Note] [Entrypoint]: Database files initialized
master_1  | 2020-05-24 14:14:54+00:00 [Note] [Entrypoint]: Starting temporary server
master_1  | 2020-05-24T14:14:54.738243Z 0 [Warning] [MY-011070] [Server] 'Disabling symbolic links using --skip-symbolic-links (or equivalent) is the default. Consider not using this option as it' is deprecated and will be removed in a future release.
master_1  | 2020-05-24T14:14:54.739005Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.18) starting as process 97
master_1  | 2020-05-24T14:14:55.555469Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
master_1  | 2020-05-24T14:14:55.566293Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
master_1  | 2020-05-24T14:14:55.608994Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.18'  socket: '/var/run/mysqld/mysqld.sock'  port: 0  MySQL Community Server - GPL.
master_1  | 2020-05-24 14:14:55+00:00 [Note] [Entrypoint]: Temporary server started.
master_1  | 2020-05-24T14:14:55.863681Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Socket: '/var/run/mysqld/mysqlx.sock'
master_1  | Warning: Unable to load '/usr/share/zoneinfo/iso3166.tab' as time zone. Skipping it.
master_1  | Warning: Unable to load '/usr/share/zoneinfo/leap-seconds.list' as time zone. Skipping it.
master_1  | Warning: Unable to load '/usr/share/zoneinfo/zone.tab' as time zone. Skipping it.
master_1  | Warning: Unable to load '/usr/share/zoneinfo/zone1970.tab' as time zone. Skipping it.
master_1  | 2020-05-24 14:14:59+00:00 [Note] [Entrypoint]: Creating database sample
master_1  | 
master_1  | 2020-05-24 14:14:59+00:00 [Note] [Entrypoint]: Stopping temporary server
master_1  | 2020-05-24T14:14:59.745593Z 11 [System] [MY-013172] [Server] Received SHUTDOWN from user root. Shutting down mysqld (Version: 8.0.18).
master_1  | 2020-05-24T14:15:00.689149Z 0 [System] [MY-010910] [Server] /usr/sbin/mysqld: Shutdown complete (mysqld 8.0.18)  MySQL Community Server - GPL.
master_1  | 2020-05-24 14:15:00+00:00 [Note] [Entrypoint]: Temporary server stopped
master_1  | 
master_1  | 2020-05-24 14:15:00+00:00 [Note] [Entrypoint]: MySQL init process done. Ready for start up.
master_1  | 
master_1  | 2020-05-24T14:15:01.036210Z 0 [Warning] [MY-011070] [Server] 'Disabling symbolic links using --skip-symbolic-links (or equivalent) is the default. Consider not using this option as it' is deprecated and will be removed in a future release.
master_1  | 2020-05-24T14:15:01.036868Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.18) starting as process 1
master_1  | 2020-05-24T14:15:01.733035Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
master_1  | 2020-05-24T14:15:01.743247Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
master_1  | 2020-05-24T14:15:01.783752Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.18'  socket: '/var/run/mysqld/mysqld.sock'  port: 33060  MySQL Community Server - GPL.
master_1  | 2020-05-24T14:15:02.882639Z 0 [ERROR] [MY-011300] [Server] Plugin mysqlx reported: 'Setup of bind-address: '*' port: 33060 failed, `bind()` failed with error: Address already in use (98). Do you already have another mysqld server running with Mysqlx ?'
master_1  | 2020-05-24T14:15:02.882761Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Socket: '/var/run/mysqld/mysqlx.sock' 

symbolic-links=0 が元のコンテナイメージ側にある Dockerfile で設定されているために発生するエラー。
おそらく MySQL 8.0.2 以降で非推奨になっている?
dev.mysql.com

あと、デフォルトで無効化されているため設定する必要がない。
dev.mysql.com

これはプルリクだした。マージされてないが LGTM が帰ってきている。
github.com


[Warning] [MY-010453] [Server] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.

起動時に datadir/mysql が存在しなければ、--initialize-insecure が無条件でつけられる。それで、このオプションをつけると必ず発生するのがこの Warning。
パスワードなしで root が生成されるが、その後にパスワードが設定されることが想定される場合はやっているみたい。
dev.mysql.com

実際 entrypoint.sh で --initialize-insecure 付きで実行された後に root アカウントに対してなにやらパスワードが設定されている部分を見つけた。
github.com

[Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.

MySQL 8 で auto_generate_certs が有効になっている場合(デフォルトで有効になっている) datadir に証明書がなければ自己署名証明書が生成される。
証明書が自己署名である場合にこのエラーは発生する。

[Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.

これは権限周りがあれしてあれになっている。
MySQL 8.0.18 の docker コンテナを割と最小構成で起動した時のログ。default_native_password, port=33060, MYSQL_ROOT_PASSWORD だけ設定している · GitHub

詳しくは yoku さんが解説していて pid ファイルそのものや pid ファイルを置いているディレクトリが o+w になっていると発生するエラーらしい。

Warning: Unable to load '/usr/share/zoneinfo/iso3166.tab' as time zone. Skipping it.

これはタイムゾーンのロードしている部分なのだが、 zoneinfo を全て見ているっぽくて timezone data 以外のファイルをロードしようとするとこの手のエラーを吐く。

[ERROR] [MY-011300] [Server] Plugin mysqlx reported: 'Setup of bind-address: '*' port: 33060 failed, `bind()` failed with error: Address already in use (98). Do you already have another mysqld server running with Mysqlx ?'

MySQL 側の port を 33060 にしているため mysqlx のものとかぶってしまっているために発生しているエラー。

おわりに

プルリク一個出せたので満足。