Blog

ローラーマウスモバイルの 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

Remnant 2 クリア

Remnant 2 をクリア。難易度はサバイバー。サバイバーでも十分すぎるぐらいに難しい。特にボス。ラスボスとラビリンスがキツかった。。 ソウル系のTPSというやつ。

かなりゲームバランス良くて、楽しめた。雰囲気もダクソっぽくて楽しい。

自動生成ダンジョンなのは野心的だが、結局のところ、ソウル系ってボスが命ってことになっちゃうので、ボスまわりは毎回同じなので、刺激は薄れちゃうかもなぁ。

ゲーム内がかなり説明が不親切で、例えば特性が65が上限なのにそれがゲーム内だとさっぱりわからんとかそういうのはある。。 が、基本的には楽しめた。

攻略につまった場合は野良の協力者を召喚すればうまくいくこともあるので楽しい。

自作ブログの構成更新

k8s を趣味でも使うか、、と思ってこのブログでも DigitalOcean の k8s を使うようにしてみたのだが、DigitalOcean への月々の支払いが $100/mo とかになってきて、さすがにたけーなと。円安だし。。

というわけで、もう少し安くすることを考えてみた。

DigitalOcean Apps というのがあったので、これを使うようにしてみる。これは AWS App Runner みたいなやつで、簡単に docker image をホスティングできるし、TLS 終端とかもいい感じにやってくれる。

といった感じのオペレーションをしたらとりあえず完了。

これでとりあえず $40/mo ぐらいにはなった。本当は、admin サイトと web サイトを統合して一つの app に入れるようにしたらもう少しコストを圧縮可能だが、、アプリのコードをそこそこいじらないと行けないと思うので、一旦ここまで。

kravis をやめて letsPlot に移行した

どちらも ggplot2 を意識したインターフェース。

というか kravis は ggplot2 の wrapper なので、ggplot2 の実装をそのまま使えて便利っちゃ便利。 一方で、ggplot2 をインストールしなきゃいけないので面倒なこともある。docker で動かすこともできるのだが、動作にちょっとコツが必要。

kravis は krangl と同じ作者なので、相性がよくて便利だったのだが、kotlinx-dataframe 時代になったのでその優位性は失われた。

letsPlot kotlin は kotlin で実装されてるのでインストールが楽。 昔は letsPlot は機能が少なすぎたが、最近は letsPlot は機能がだいぶ増えてきたので letsPlot に移行することにする。

    implementation("org.jetbrains.lets-plot:lets-plot-common:3.2.0")
    implementation("org.jetbrains.lets-plot:lets-plot-image-export:3.2.0")
    implementation("org.jetbrains.lets-plot:lets-plot-kotlin-jvm:4.4.1")

とかやる。

    private fun renderSummary(df: DataFrame<Any?>) {
        val p = letsPlot(df.toMap()) +
            geomBar(alpha = 0.3) { x = "year"; fill = "severity" } +
            facetWrap(facets = arrayOf("product")) +
            themeLight()

        ggsave(p, "./reports/summary.png", path = ".")
    }

みたいに実装する。ggsave() とか ggplot2 感がすごい。

LocalDate をそのまま入れておくと描画できないとか微妙に癖はあるが。。 datalore との相性も jetbrains がメンテしているだけあって良い気がするので、今後はこっちをつかっていきたい所存。

Spring の @Scheduled は 1つのスレッドで動く。

https://qiita.com/ksh-fthr/items/34126324cc71bfc4d469 https://tech.excite.co.jp/entry/2022/12/09/101008

Spring の @Scheduled はデフォルトでは1つのスレッドで動くので、必要ならば spring.task.scheduling.pool.size を設定してプールサイズを大きくしておく必要があります。

queue にどのぐらい積まれているかは actuator を prometheus で出している場合、 executor_queued_tasks で確認できるので、これを監視すると良い。

fsmonitor のソケットが原因で generateGitProperties がエラーになる

	> java.io.IOException: Cannot snapshot .git/fsmonitor--daemon.ipc: not a regular file

というエラーが generateGitProperties タスクで発生する。

rm -rf .git/**/fsmonitor-* をした上で、git config --global fsmonitor.socketDir "$HOME/.git-fsmonitor" としたほうが良いと思われる。

これでダメなら git config --global core.fsmonitor false か。

SparkSQL で暗黙の型変換を抑制する

automatic type coercing が起きると期待しない結果になることがある。

select if('-999999999999' < 0, true, false) as s, if(-999999999999 < 0, true, false) as i

↑の結果が、s は false, i は true となる。 数字をうっかり文字列のまま使ってる場合、int の範囲を超えた場合に false になる。

こういう暗黙の型変換は困るケースも多いので、spark.sql.ansi.enabled を有効に設定することを検討する。

spark.conf.set("spark.sql.ansi.enabled", "true")

この環境下では、上記のクエリーは invalid input syntax for type numeric: -999999999999 と型変換エラーになる。

https://spark.apache.org/docs/3.4.1/sql-ref-ansi-compliance.html

大規模言語モデルは新たな知能か――ChatGPTが変えた世界 (岩波科学ライブラリー)

https://amzn.to/44CzDFh

数式なども登場せず、LLM について過度な期待をもたせすぎず、それでいて未来への展望を冷静に語っている良書だと思います。 ChatGPT について学びたい人、特に文系の人にはオススメ。

OkHttp3 では GET request に content-body が送れない

https://github.com/square/okhttp/issues/3154

elasticsearch/opensearch 等では GET request に content-body をつける必要があるが、OkHttp3 では対応していない。強い気持ちがあるようだ。

diablo4 のストーリーをやった

ネクロマンサーでレベル50まで。 tier1でやった。

楽しかった。エクスプロージョンが使いこなせると楽しくなったかなー。

Java の ByteBuffer の endian は big endian がデフォルト

一般的なプログラミング言語に慣れていると「普通に考えたらデフォルトは machine native だろ」って考えがちだけど、Java の Byte buffer は big endian がデフォルトなので気をつけよう!! めっちゃ時間溶けた。

https://docs.oracle.com/javase/jp/6/api/java/nio/ByteBuffer.html