Blog

QMK firmware の開発メモ

QMKファームウェアを開発するには、どうするべきか

brew install qmk/qmk/qmk とすると、qmk がインストールできる。これは、めちゃくちゃ時間かかる 。数時間かかるので注意。

qmk のビルドは、ln してやる。make SKIP_GIT=true keyball/keyball44:tokuhirom とかする。

qmk flash ... とすると、書き込むことができる。キーボードのリセットボタンを押すと自動的に書き込みが開始される。最初は pro micro web writer で書いていたが、自分でファームウェア書くならCLIで書いた方がいい。

debug の方法

qmk console とすると、コンソールをモニタリングできる。

rules.mk に CONSOLE_ENABLE = yes に書く。

void keyboard_post_init_user(void) {
  // Customise these values to desired behaviour
  debug_enable=true;
  debug_matrix=true;
  //debug_keyboard=true;
  //debug_mouse=true;
}

のように書いて、デバッグ機能を有効化する。

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
  // コンソールが有効化されている場合、マトリックス上の位置とキー押下状態を出力します
#ifdef CONSOLE_ENABLE
    uprintf("KL: kc: %u, col: %u, row: %u, pressed: %u\n", keycode, record->event.key.col, record->event.key.row, record->event.pressed);
#endif 
  return true;
}

のようにして、printf debug できる。

https://github.com/samhocevar-forks/qmk-firmware/blob/master/docs/ja/newbs_testing_debugging.md

QMK で増井さんの Dynamic Macro を実装してみた

https://github.com/tokuhirom/qmk-onemoretime

Keyboard のファームウェアとして、QMK が有名なわけだけど、その上で増井さんの Dynamic Macro を実装してみた。1ファイルのCライブラリとして実装してるので、自分のファームウェアに簡単に組み込めると思う。

DynamicMacro というのは、

Dynamic Macroの原理は非常に単純で、

    「同じ編集操作を2回繰り返したあとで[CTRL]+[t]を押すと繰り返された操作が再実行される」

というものです。「二度あることは三度ある」と言うように、同じことが二度あればもう一度あるのは世の中でごく普通のことです。二度実行した操作をもう一度実行することもよくあることですので、この方法はたいへん効果的です。

というようなやつ。

なんで作ったの?

なんかできそうなのでやってみただけです。 動いてみるとなんか楽しいです。

どう作ったの?

キーが押されたときに、リングバッファに押したキーを保存しておく。

任意のショートカットキーが押されたタイミングで、繰り返しコマンドになっている区間を検出して、処理する。

難しかったことは?

モディファイヤーキーの処理が難しい。キー送出している間はモディファイヤーキーを押してることにしちゃいけないので、状態を変えてもどしたりした。

改善点は?

メモリの要素をナイーブに舐めてるけど、ちゃんと memcmp とかにしたほうがエコかもしれない。が、ちょっとめんどくさいのでナイーブに実装した。

なんでこんな名前なの?

DynamicMacroという名前は QMK そのもので使われているから。Staticにハードコードされてない、キーボード側でマクロ記録開始!とかマクロ再生!!とかできるようなやつが DynamicMacro なのである。

もう一回実行するというところが本質かなと思うのでこういうネーミングとしてみた。

まとめ

気軽に組み込めると思うので、お気軽に試してみてね。

screencapturekit-rs がpanic!するのを直した

https://github.com/svtlabs/screencapturekit-rs/pull/14

何故か panic! していたところがあったので、ちゃんと Result を返すように修正した。

screencapturekit-rs に音声キャプチャ機能をつけた話

https://github.com/svtlabs/screencapturekit-rs/pull/13 pull-request を出しただけで、まだマージされてないんだけど。

最近のMacでは、ScreenCaptureKit というのがあって、任意のウィンドウの画面をキャプチャしたり、音声をキャプチャしたりできる。昔は Mac では BlackHole とか SoundFlower とか Loopback とかを入れないと、アプリの音声をキャプチャすることはできなかったのだが、これを使うと、簡単にキャプチャできるし、リアルタイムでの音声ミックスを BlackHole とかでやってると coreaudiod とかでCPU食いまくるってのもあるんで、そういうのを避けたいというのもある。

実際、ScreenCaptureKit 使うと CPU 負荷めっちゃ低くて良い。OBS でも ScreenCaptureKit を処理することで CPU 負荷を減らしたという話がある。 https://applech2.com/archives/20220613-obs-studio-for-mac-next-level-with-macos-13-ventura-screencapturekit.html

ScreenCaptureKit は mac の機能なので、これを使ったアプリを作るなら、 Swift を使うのが王道だとは思いつつ、Swift に慣れてないのと、XCode に慣れてないので、可能なら Rust とかでやりたいなと思っていると、そのものずばりの crate が存在している。 https://crates.io/crates/screencapturekit

よしこれで、音声ファイルをキャプチャするぞ、と思ったのだが、音声を取得する機能がそもそも実装されていないことに気づいた。

(この時点で、xcode で書く方向性も今一度考えたが、ちょっと試してみたらうまく動かなくて諦めたのであった。。結局、XCode で Swift で書いてても、権限周りとかではまるのと、XCode に慣れてなさすぎることと、そもそも rust だろうが swift だろうが、ScreenCaptureKit は新参なので情報が薄いというのもあって、RustRover で Rust で頑張ったほうが楽という結論に。。)

ScreenCaptureKit で、CMSamleBuffer に音声データが取れるので、これをそのまま raw PCM として保存したら、wave に変換して音声ファイルになるやろ、っておもってやってみたら全然そうならなかった。なんかノイズがすごい、ちょっと高い音のデータができた。このバイナリデータは、AudioBufferList という構造体になっていて、サイズ情報とかが混じってるので、ちゃんと AudioBufferList としてデシリアライズしてやらないと使えない。 AudioBufferList には、チャネルごとにバイナリデータが入ってるので、これをうまいこと取り出せば使える。 https://developer.apple.com/documentation/coremedia/cmsamplebuffer-u71

            let mut buffer_size = 0;
            CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(
                self,
                &mut buffer_size,
                null_mut(),
                0,
                null_mut(),
                null_mut(),
                0,
                &mut null_mut(),
            );

とかして、必要なバッファサイズを計測する。

            let mut block_buffer_ref = CMSampleBufferGetDataBuffer(self);
            let layout = alloc::Layout::from_size_align(buffer_size, 16).unwrap();
            let audio_buffer_list_ptr = alloc::alloc(layout);

            let result = CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(
                self,
                null_mut(),
                audio_buffer_list_ptr as _,
                buffer_size,
                null_mut(),
                null_mut(),
                kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment,
                &mut block_buffer_ref,
            );
            CFRelease(block_buffer_ref as _);
            if result != 0 {
                panic!()
            }

            let audio_buffer_list_ptr = audio_buffer_list_ptr as *mut AudioBufferList;

あとは、こんなふうにして、CMSmpleBufferGetDataBuffer で、データバッファを取り出して、maloc した領域にデータを取り出す。

        let audio_buffer_list = unsafe { *audio_buffer_list_ptr };
        let number_buffers = audio_buffer_list.number_buffers;
        let mut buffers = Vec::new();
        for i in 0..audio_buffer_list.number_buffers {
            let audio_buffer = audio_buffer_list.buffers[i as usize];
            buffers.push(CopiedAudioBuffer {
                number_channels: number_buffers,
                data: unsafe {
                    std::slice::from_raw_parts(audio_buffer.data, audio_buffer.data_bytes_size as usize)
                }.to_vec(),
            });
        }
        buffers

データをコピーするとちょっとオーバーヘッドあるけど、、そんなにシビアな用途でもないので、コピーしちゃう。

あとは、この PCM データをチャネルごとに、データを普通にファイルに書いていけばいい。 複数チャネルを一気にファイルに書いていくのは、今回の僕の用途では必要なかったので、、、raw PCM をゴリゴリ書いていき、sox コマンドで、wave ヘッダをつける。 このへん、rust のライブラリもありそうだが、結局 sox コマンド使ったほうが情報多くて楽そうだった。メモリたくさん使う、オーディオ関連の処理は、別プロセスでやったほうが、メモリリークの心配とかがなくて楽というのもある。

そんなこんなで、raw pcm の取得部分まで実装し、サンプルコードを書いて pull-request を出した。 ScreenCaptureKit には夢が詰まってる気がしていて、ScreenCaptureKit で適当に音声をキャプチャした上で、whisper.cpp 使って音声認識するとかすると結構遊べるなと思っている。 初心者にも書きやすい rust でかければ、誰でも簡単に ScreenCaptureKit で遊べるので、オススメ。

Java 21 からは `new URL(String)` は deprecated

https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/net/URL.html#%3Cinit%3E(java.lang.String)

今後は URI.toURL を利用せよとのこと。

Kotlin で GUI アプリを作るならやっぱり jetpack compose for desktop

Jetpack Compose for desktop によるアプリケーション開発

Kotlin で GUI アプリを開発する場合、Swing, awt と JavaFX と Jetpack compose とかぐらい。 今更 awt, Swing などを直接使うのはちょっと大変なので、まぁ kotlin ネイティブな Jetpack compose かなぁ、という感じ。Jetpack compose for desktop でそこそこの規模のアプリを作ったって報告はあまり見かけず、ちょっと不安な感じではありつつ。

現状としては、Jetpack compose 自体は Android で広く使われていることもあって、安定して動くし、情報も多い(ただし Android 用の情報)。 desktop として使う場合には、widget がちょっと物足りなかったりする。商用製品作るぞ!みたいな気持ちだと、ちょっと頼りないかも。エンジニアが自分用ツールを作って楽しむ分には十分すぎるぐらいの機能がすでにある。

JVM で動くので、Java のライブラリがそのまま動く点は魅力。 一方で、JVM で動く故の懸念もある。メモリの使用量。ちょっとメモリ使用量は native でちゃんと丁寧に書かれたアプリより多めかも。

kotlin native 対応が jetpack compose for desktop に入ってくれる未来が来ればそのへんよくなるかもなぁ、と個人的には思っていて、海外でもそういう意見がチラホラ。しかし、現状の jetpack compose for desktop は swing ベースらしいので、その道程は果てしなく遠いのかも。

まぁ、でも普段サーバーサイド用に書いてるプログラミング言語で GUI アプリが割と普通に作れるのって便利だもんで、jetpack compose for desktop もっと流行ったら嬉しいなぁ。

で、具体何つくってるかというと、なんか ChatGPT のモデルとか切り替えながらサクサク使えるやつを作ってて、普通に便利に毎日使っていたりする。

https://github.com/tokuhirom/reflect-ai

Kotlin で Mac の window list を得る

方法を探したが、結局 ProcessBuilder プロセスで AppleScript を実行するのが正攻法っぽい。

import java.io.BufferedReader
import java.io.InputStreamReader

class WindowNameCollector {
    data class WindowState(val processName: String, val processId: String, val bundleId: String, val windowName: String)

    fun getWindowStateList(): List<WindowState> {
        val windowListString = getWindowListString()

        return parseWindowState(windowListString)
    }

    private fun parseWindowState(input: String): List<WindowState> {
        // Regex pattern for matching the input string.
        val pattern = """Process: '(.+?)', PID: '(.+?)', Bundle ID: '(.+?)', Window: '(.+?)'""".toRegex(RegexOption.DOT_MATCHES_ALL)

        // Try to find a match in the input string.
        val matchResults = pattern.findAll(input)
        return matchResults.map { matchResult ->
            val (processName, processId, bundleId, windowName) = matchResult.destructured
            WindowState(processName, processId, bundleId, windowName)
        }.toList()
    }

    private fun getWindowListString(): String {
        val script = """
tell application "System Events"
    set procs to processes
    set results to ""
    repeat with proc in procs
        if exists (window 1 of proc) then
            repeat with w in windows of proc
                set results to results & "Process: '" & name of proc & "', PID: '" & unix id of proc & "', Bundle ID: '" & bundle identifier of proc & "', Window: '" & name of w & "'\n"
            end repeat
        end if
    end repeat
    return results
end tell
        """.trimIndent()

        val pb = ProcessBuilder("osascript", "-e", script)
        val p = pb.start()

        val lines = BufferedReader(InputStreamReader(p.inputStream)).use { reader ->
            reader.readText()
        }

        val exitStatus = p.waitFor() // Wait for the process to finish.
        if (exitStatus != 0) {
            throw RuntimeException("Failed to execute osascript. Exit status: $exitStatus")
        }

        return lines
    }
}

Kotlin multiplatform で生成するコードでは、application block にすべてのコードが入っているので注意。

Jetpack compose for desktop でアプリケーションを書く場合、 https://kmp.jetbrains.com/ で最初のコードを生成すると思うのだが、こいつが生成するコードには少し癖がある。

fun main() = application {
        Window(onCloseRequest = ::exitApplication, state = rememberWindowState()) {
            mainApp.App()
        }
}

のようになっている。

application block は、Compose 側の都合で何回か呼ばれるので、普通の Kotlin app の感覚で、適当に中で Thread { }.start() とか雑に発行してると、スレッドが何個も作られて死ぬので注意。

Jetpack Compose for Desktop で Caused by: java.lang.ClassNotFoundException: javax.naming.NamingException って言われるとき

アプリが完成したなぁ、とおもっていざ ./gradlew packageDmg したとき。

Caused by: java.lang.ClassNotFoundException: javax.naming.NamingException

というエラーがでて起動しなかったりする。

これは、javax.naming などのモジュールが参照できてないので、参照先を追加していくしかない。

compose.desktop {
    application {
        nativeDistributions {
            modules("java.naming")
            // alternatively: includeAllModules = true
        }
    }
}

のように追加していけば OK

Kotlin で数字を3桁区切りで区切ってコンマを入れる

整数値を3桁区切りでコンマを入れるためには、NumberFormatクラスを使用することができます。以下は、Kotlinでの例です。

import java.text.NumberFormat
import java.util.Locale

fun main() {
    val number = 1000000
    val formattedNumber = NumberFormat.getNumberInstance(Locale.getDefault()).format(number)
    println(formattedNumber)  // 結果: 1,000,000
}

ローラーマウスモバイルの COPY/PASTEをリマップする

[ローラーマウスモバイル]のCOPY/PASTEボタン、COPY/PASTE に使いたい感じは全くしないので、[Karabiner Elements]でリマップする。

~/.config/karabiner/assets/complex_modifications/roller_mouse.json に以下のように書く。

{
  "title": "ローラーマウスモバイル用設定",
  "rules": [
    {
      "description": "C-c を option-shift-↓にマッピング。C-v を option-shift-↑にマッピング (v4)",
      "manipulators": [
        {
          "type": "basic",
          "from": {
            "key_code": "c",
            "modifiers": {
              "mandatory": ["control"]
            }
          },
          "to": [
            {
              "key_code": "down_arrow",
              "modifiers": ["left_option", "left_shift"]
            }
          ],
          "conditions": [
            {
              "type": "device_if",
              "identifiers": [
                {
                  "vendor_id": 2867,
                  "product_id": 12288
                }
              ]
            }
          ]
        },
        {
          "type": "basic",
          "from": {
            "key_code": "v",
            "modifiers": {
              "mandatory": ["control"]
            }
          },
          "to": [
            {
              "key_code": "up_arrow",
              "modifiers": ["left_option", "left_shift"]
            }
          ],
          "conditions": [
            {
              "type": "device_if",
              "identifiers": [
                {
                  "vendor_id": 2867,
                  "product_id": 12288
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

これで、Slack の「未読のメッセージにジャンプする」が出来て便利。

Hello, world レベルの JVM を作った

Java エンジニアなら JVM を実装してみたほうがいいなぁ、ということで簡単に。

の2ステップで進めていけば OK.

class file の読み込み

バイナリファイルなので、DataInputStream とか使って読んでいけば OK。

https://qiita.com/mima_ita/items/a42f3f016a411627bd7a#constant_methodref

クラスファイルの中身は

実行フェーズ

普通の VM を実装していけば OK。

stack, program counter とかを持った普通の VM を作る。 Java は普通の VM と違って、stdout に書く命令が VM レベルで実装されていないんで、そのへんもランタイムとしてロードしておく必要がある。

実装

https://github.com/tokuhirom/picojvm

というわけで、Hello, world がなんとなく動くところまで実装したのがこちらです。 だいたいここまでで2人日ぐらい。

libs.versions.toml に記載されているが利用されていないバージョン番号を探す

長年システムを運用していると、libs.versions.toml に記載があるが存在しないシステムが発生することがある。これを検出したい。

import toml
import os
import re
from collections import defaultdict

def scan_files(root_dir):
    usage_counts = defaultdict(int)
    for subdir, _, files in os.walk(root_dir):
        for file in files:
            if file.endswith('.gradle.kts'):
                with open(os.path.join(subdir, file), 'r') as f:
                    content = f.read()
                    for match in re.finditer(r'libs\.([a-zA-Z0-9._-]+)', content):
                        library = match.group(1).replace('.', '-')
                        usage_counts[library] += 1
    return usage_counts

def main():
    # TOML ファイルを読み込む
    with open('gradle/libraries.versions.toml', 'r') as f:
        toml_data = toml.load(f)
    
    libraries = toml_data['libraries']
    usage_counts = scan_files('.')
    
    # すべてのライブラリに対して使用状況をチェックする
    for library in libraries.keys():
        usage_count = usage_counts[library]
        unused_indicator = ' (*)' if usage_count == 0 else ''
        print(f'{usage_count} {library}{unused_indicator}')

if __name__ == "__main__":
    main()

ナベアツは数字がでかくなるほどアホになる割合がアップする

https://twitter.com/jagarikin/status/1711855799184785732?s=20

Let's plot を練習するのにちょうどいいなと思ったので、久々に let's plot してみる。

%use lets-plot

val max = 1000000
val rates = mutableListOf<Double>()
var nabeatsu = 0
for (i in 0..max) {
    if (i % 3 == 0 || i.toString().contains('3')) {
        nabeatsu += 1
    }
    val r = nabeatsu.toDouble() / i.toDouble()
    if (r.isInfinite()) {
        rates.add(1.0)
    } else {
        rates.add(r)
    }
}

val data = mapOf(
    "x" to 0..max,
    "nabeatsu-rate" to rates
)

var p = letsPlot(data)
p += geomLine() { x = "x"; y = "nabeatsu-rate" }
p + ggsize(700, 350)

DataLore 使うとこういうの簡単に試せて便利。

2つのディレクトリの jar ファイルを比較するスクリプト

ChatGPT、こういうのの生成はやたら得意なので、ChatGPT で生成した。 こういう単純なスクリプトは手で書くより早いよねぇ。

import os
import sys
import subprocess
import difflib
import zipfile
import subprocess
import tempfile
from pathlib import Path

def get_git_commit_hash(directory):
    """Return the latest git commit hash of the specified directory."""
    try:
        commit_hash = subprocess.check_output(['git', '-C', directory, 'rev-parse', 'HEAD']).decode('utf-8').strip()
        return commit_hash
    except subprocess.CalledProcessError:
        print(f"Error: Failed to retrieve git commit hash for {directory}")
        sys.exit(1)

def get_jar_files(directory):
    """Return a list of jar files in the specified directory."""
    basedir = Path(directory)
    return [f.relative_to(basedir) for f in basedir.rglob("*.jar") if f.is_file()]

def are_jars_identical(jar1_path, jar2_path):
    """Check if the contents of the two JAR files are identical and display unified diff for .html files if they're different."""

    def is_binary(content):
        """Determine if the given content is binary."""
        return b'\x00' in content

    def get_javap_output(class_file_path):
        """Get the bytecode dump using javap."""
        try:
            output = subprocess.check_output(['javap', '-verbose', '-c', class_file_path], stderr=subprocess.STDOUT)
            return output.decode('utf-8')
        except subprocess.CalledProcessError as e:
            return str(e)

    with zipfile.ZipFile(jar1_path, 'r') as jar1, zipfile.ZipFile(jar2_path, 'r') as jar2:
        jar1_files = set(jar1.namelist())
        jar2_files = set(jar2.namelist())

        if jar1_files != jar2_files:
            return False, "File lists are different"

        for file_name in sorted(jar1_files):
            with jar1.open(file_name) as file1, jar2.open(file_name) as file2:
                file1_contents = file1.read()
                file2_contents = file2.read()

                if file1_contents != file2_contents:
                    # If the files are .html and not binary, display the unified diff
                    if file_name.endswith('.class'):
                        # If the files are .class files, get the javap output and compare
                        with tempfile.NamedTemporaryFile(suffix='.class', delete=True) as tmp1, tempfile.NamedTemporaryFile(suffix='.class', delete=True) as tmp2:
                            tmp1.write(file1_contents)
                            tmp2.write(file2_contents)
                            tmp1.flush()
                            tmp2.flush()

                            javap_output1 = get_javap_output(tmp1.name)
                            javap_output2 = get_javap_output(tmp2.name)

                            diff = difflib.unified_diff(
                                javap_output1.splitlines(),
                                javap_output2.splitlines(),
                                fromfile=f"{jar1_path}/{file_name}",
                                tofile=f"{jar2_path}/{file_name}"
                            )
                            return False, file_name + "\n" + "\n".join(diff)
                    elif file_name.endswith('.html') and not is_binary(file1_contents) and not is_binary(file2_contents):
                        diff = difflib.unified_diff(
                            file1_contents.decode().splitlines(),
                            file2_contents.decode().splitlines(),
                            fromfile=f"{jar1_path}/{file_name}",
                            tofile=f"{jar2_path}/{file_name}"
                        )
                        return False, "\n".join(diff)
                    return False, f"File contents are different: {file_name}"

    return True, ""

def main():
    if len(sys.argv) != 3:
        print("Usage: script_name directory1 directory2")
        sys.exit(1)

    dir1, dir2 = sys.argv[1], sys.argv[2]

    # Display git commit hashes for both directories
    commit_hash_dir1 = get_git_commit_hash(dir1)
    commit_hash_dir2 = get_git_commit_hash(dir2)

    print(f"Git commit hash for directory 1: {dir1} {commit_hash_dir1}")
    print(f"Git commit hash for directory 2: {dir2} {commit_hash_dir2}\n\n\n")

    jar_files_dir1 = set(get_jar_files(dir1))
    jar_files_dir2 = set(get_jar_files(dir2))

    common_jar_files = jar_files_dir1.intersection(jar_files_dir2)

    for jar_file in common_jar_files:
        if "-sources.jar" in str(jar_file):
            continue
        file1_path = os.path.join(dir1, jar_file)
        file2_path = os.path.join(dir2, jar_file)

        is_same, reason = are_jars_identical(file1_path, file2_path)
        if not is_same:
            print(f"## {jar_file} has different contents.")
            print(reason)
            print("\n\n")
        else:
            #print(f"{jar_file} has the same contents.")
            pass

    # Report files only in directory 1
    unique_files_dir1 = jar_files_dir1 - common_jar_files
    for jar_file in unique_files_dir1:
        print(f"{jar_file} exists only in directory 1.")

    # Report files only in directory 2
    unique_files_dir2 = jar_files_dir2 - common_jar_files
    for jar_file in unique_files_dir2:
        print(f"{jar_file} exists only in directory 2.")

if __name__ == "__main__":
    main()

Pebble という Java のテンプレートエンジンが Jinja2 みたいで良い

https://pebbletemplates.io/

最近、コードをテンプレートで生成するのに mustache を使っていたのだが、表現力がしょぼすぎて辛かったので pebble にしてみた。Jinja2 みたいな感じで使いやすい。

mustache は else すら書けないので冗長になりがちでイヤン。

StarField 一周目クリア

Bethesdaゲームを順当に宇宙に移したな〜という感想。 いつも通りシステムに対する説明がたりてなかったりするけど十分面白い。

gradle が遅いなと思ったら gradle-profiler を使おう

https://github.com/gradle/gradle-profiler

gradle の処理がなんだか遅いなってときがある。

サッとググッて出てくる情報だと、--profile つけろとか、--scan 使えとか書いてある。 --profile は情報がざっくりしすぎていてよくわからないし、--scan は gradle.org に情報がアップロードされちゃうのがちょっと。。 --profile って結局は、「どのタスクが遅いか」はわかるんだけど、どのプラグインが遅いか、とかはよくわからなかったりするんだよね。

gradle-profiler を使うと、どのプラグインが時間食ってるか、みたいなことをサッとしれて便利。 何回か動かしたベンチマークの結果をレポートにまとめてくれるので、gradle の速度チューニングした結果の評価にも便利。

benchmark の取り方

$ gradle-profiler --benchmark --project-dir . assemble

とかやって動かす。

数回試行された結果が以下のような HTML で生成される。

profiling の仕方

jfr や async-profiler をかけながらgradleを動かせるのも便利すぎる。

なんだか最近gradle遅いな、と思ったらgradle-profilerかけると良いと思う。

gradle-profiler --profile async-profiler --project-dir . assemble

async-profiler 以外にも jfr や YouProfiler などを使うことも可能。

以下のような見慣れた flamegraph を得ることができる。

これをやった結果、spring の dependencyManagement に個別の dependency をいっぱい指定すると遅いから platform project 作ったほうが良さそうだなぁ、とかそういうことがわかったりする。

let's plot kotlin で X 軸を YearMonth にしたときにソートされないよって場合

https://github.com/JetBrains/lets-plot-kotlin/issues/48

epoch millis にして scaleXDateTime するのが良さそう

JDK の JIT の様子を眺める方法 6選

JDK の JIT の様子を眺める方法について考える。

実際には 現代のサーバーサイドエンジニアリングでは JDK の JIT がパフォーマンスイシューの原因になっているケースは極めて少ない が、JIT がどんなふうなタイミングで発生しているかを把握しておくことは人生にとって有益だと思う。

ログを出すと若干のオーバーヘッドがあると思うけど、JIT のログは起動直後にちょっと出るだけで量も大したことないので気にしなくていいレベルな気がする。

JFR で見る

王道っぽい。

JFR Event Streaming を使って収集する

Java 14 以後で利用可能な JEP 349: JFR Event Streaming を使って micrometer などに出力する。この方法だと、JFR が動く前の JIT イベントが取れない気がする。 https://matsumana.info/blog/2020/09/20/jvm-jit-compilation-metrics/

Unified Logging に JIT log を出す

https://matsumana.info/blog/2020/09/22/jvm-jit-compilation-metrics-with-mtail/

-Xlog:jit+compilation=debug というオプションを指定することで、JIT のログを取れる。Unified JVM Logging 形式なので取り回しがわりとしやすいかも。

ref. https://www.slideshare.net/slideshow/embed_code/key/BLJsjK56iDL0y

-XX:+PrintCompilation

-XX:+PrintCompilation をつけると、JIT コンパイルログが stdout に出力される。軽く見るにはいいかも。Unified Logging でとれる今となってはあえてこれを使う理由はないかも。

ref. https://www.slideshare.net/nttdata-tech/jit-compiler-jjugccc-2020-fall-nttdata-sakata?slide=42

-XX:+LogCompilation

-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation つけるとファイルにログが出力される。ファイル名は current directory の hotspot_pid$$.log となる。XML で出力されるので処理しやすいかもしれない。

JitWatch で眺めると楽しい。

ref. https://yuya-hirooka.hatenablog.com/entry/2021/04/30/234227

https://github.com/AdoptOpenJDK/jitwatch/releases/tag/1.4.7 このへんからダウンロードできるので使うとよい。

wget https://github.com/AdoptOpenJDK/jitwatch/releases/download/1.4.7/jitwatch-ui-1.4.7-shaded-mac-m1.jar
java -jar jitwatch-ui-1.4.7-shaded-mac-m1.jar