AVL木と赤黒木をPythonで実装して比較する

はじめに 動かしながらゼロから学ぶLinuxカーネルの教科書 第2版 上記の技術書を読んでいてLinuxスケジューラの話が出てきた。CFSもEEVDFも赤黒木を使っている。赤黒木とは何かを調べていくうちにAVL木との比較が面白かったので、両方Pythonで実装してベンチマークを取った。 実装コードはClaude (Anthropic) により生成。リポジトリは以下。 https://github.com/wasuken/avl-rb-tree 自己平衡二分探索木とは まず前提として、ただの二分探索木には問題がある。 挿入順: 1, 2, 3, 4, 5 1 \ 2 \ 3 \ 4 ← 一直線になる 挿入順によっては木が一直線になり、検索がO(n)に劣化する。これを解決するのが自己平衡二分探索木で、挿入・削除のたびに自動でバランスを取り直す。AVL木と赤黒木はどちらもこのカテゴリに属する。 AVL木 1962年にソ連の数学者Adelson-VelskyとLandis(AVLの名前の由来)が考案した世界初の自己平衡二分探索木。 バランスの管理方法 各ノードに高さ(height)を持たせ、左右の高さの差(バランス係数)が常に1以内になるよう管理する。 def _balance_factor(self, node): return self._height(node.left) - self._height(node.right) 差が2以上になったら回転で修正する。 回転 回転は「親子関係を1段入れ替えるだけ」の操作。二分探索木の順序を壊さずに形だけ変える。 rotate_right(y): y x / \ / \ x C → A y / \ / \ A t2 t2 C t2 は回転で行き場を失う孫ノード。二分探索木の順序的に x < t2 < y が保証されているので、yの左に付け替えるだけでよい。 バランス崩れのパターンは4つ(LL, RR, LR, RL)で、それぞれ1〜2回の回転で解消できる。 挿入・削除 挿入・削除のたびに _rebalance が呼ばれ、高さの更新とバランスチェックが走る。 ...

March 1, 2026 · 2 min

LinuxのCFSとEEVDFを整理する - スケジューラはなぜ赤黒木を使うのか

はじめに 動かしながらゼロから学ぶLinuxカーネルの教科書 第2版 上記の技術書を読んでいてスケジューラ周りの理解が曖昧だったので、生成AIや公式ドキュメントを使って整理した。 CFS (Completely Fair Scheduler) とは Linux 2.6.23から導入されたプロセススケジューラ。「全プロセスに公平にCPU時間を与える」という思想で設計されている。 vruntime(仮想実行時間) vruntimeは「実際の実行時間をNICE値で補正した値」で、CFSの核心となる指標。 vruntime += 実際のCPU時間 × (1024 / プロセスの重み) NICE値が低い(優先度高)→ 重みが大きい → vruntimeの増加が遅い → より長くCPUを使える NICE値が高い(優先度低)→ 重みが小さい → vruntimeの増加が速い → すぐ交代させられる CFSは「vruntimeが最も小さいプロセスを次に実行する」というルールで動く。後ろ向きの指標(過去の使用量の累積)であることがEEVDFとの本質的な差になる。 NICE値と重み NICE値は -20(最高優先度)〜 +19(最低優先度)の範囲で、内部的に重みに変換される。 NICE 0 → weight 1024 NICE -1 → weight 1277(約1.25倍) NICE +1 → weight 820(約0.8倍) NICE -20 → weight 88761 NICE +19 → weight 15 1段階変わるごとに約10%のCPU時間が変化する設計になっている。 タイムスライスとスケジューリングレイテンシ スケジューリングレイテンシは「全プロセスが最低1回実行されるべき目標周期」。デフォルト約6〜24ms(プロセス数による)。 タイムスライスはその比例配分: タイムスライス = スケジューリングレイテンシ × (タスクの重み / キュー内の全タスクの重みの合計) 具体例: ...

March 1, 2026 · 2 min

Linuxの起動フローを整理する - UEFI/BIOSからinitまで

はじめに [https://info.nikkeibp.co.jp/media/LIN/atcl/books/070900046/:embed:cite] 上記の技術書を読んでいて、ブートローダとLinuxの初期スタート時の役割とか順番がいまいち掴めなかったので生成AIや他の記事など別軸から調べ直してまとめた。 起動フロー全体像 UEFI/BIOS ↓ POST(ハードウェア初期化)、ブートデバイス選択 ブートローダー(GRUB等) ↓ /boot/vmlinuz(カーネルイメージ)をメモリに展開 ↓ /boot/initramfs をメモリに展開 カーネル起動 ↓ initramfsを一時的な / としてマウント ↓ ドライバ読み込み、本物のrootデバイスを認識 ↓ 本物のroot FSをマウント(switch_root) /sbin/init(systemd)に移譲 各フェーズの詳細 1. UEFI/BIOS 起動の最初はUEFI(または旧来のBIOS)が担う。 POST(Power-On Self Test): メモリ、CPU、周辺デバイスの初期化 ブートデバイスの選択(NVMe, SSD, PXEなど) UEFIの場合はEFIパーティション(ESP)から .efi ファイルを直接実行できる UEFIとBIOSの大きな違いとして、UEFIはGPTディスクのネイティブサポートや、セキュアブートの仕組みを持つ。 2. ブートローダー(GRUB2等) UEFI/BIOSからブートローダーに制御が渡る。 代表的なものはGRUB2で、設定ファイルは /boot/grub/grub.cfg にある。 ブートローダーの役割はシンプルで、以下の2点だけ: カーネルイメージ(vmlinuz)をメモリに展開する initramfs(initramfs-*.img)をメモリに展開する # /boot 以下の典型的な構成 $ ls /boot/ grub/ initramfs-6.1.0-28-amd64.img vmlinuz-6.1.0-28-amd64 ブートローダー自身はルートFSのマウントをしない。あくまでカーネルとinitramfsをメモリに置いて制御を渡すだけ。 3. カーネル起動とinitramfs ここが一番誤解されやすいフェーズ。 カーネルが起動すると、まず**initramfs(Initial RAM Filesystem)**を一時的なルート(/)としてマウントする。 なぜinitramfsが必要か? カーネル本体はコンパクトに保つ設計になっており、NVMeやLVMやLUKS(暗号化)といった本物のディスクにアクセスするためのドライバを、起動時に動的にロードする必要がある。 initramfsはそのためのミニマルな環境を提供する。 initramfs の中身(概略) /init → 起動スクリプト /lib/modules → カーネルモジュール(ドライバ) /bin, /sbin → busybox等の最低限のコマンド群 処理の流れ: ...

February 28, 2026 · 1 min

歴史地図アプリを雑にk3sへデプロイした

歴史地図アプリの構成 React + TypeScript + Vite + MapLibre GL のSPA。歴史的国境データ(GeoJSON)を表示するアプリ。 データはpublic/data/以下にGeoJSONを置く構成で、.gitignoreに含めているためリポジトリには入っていない。 ちなみに、生成するスクリプトはあるが、GEMINIを利用しないといけない。 しかし、APIキーのレート制限が入ってしまったので、ローカルで生成済みのデータを持ち込むことにした。 インフラ構成 自宅のProxmox上にLXCコンテナとしてk3sクラスタを構築している。マスター1台+ノード1台の最小構成。 外部公開はNginx Proxy Manager(NPM)でポートフォワーディングしており、DuckDNSのドメインにSSL終端している。 インターネット ↓ Nginx Proxy Manager(SSL終端) ↓ k3s NodePort ↓ Pod 問題:データファイルをどう持ち込むか public/data/がgitignoreされているため、コンテナ内でgit cloneしてもデータが存在しない。 選択肢はいくつかあったが、今回はk3sのhostPathボリュームでマウントする方針にした。 だるいファイル転送 データファイルをk3sノードに転送するのが一番面倒だった。 Proxmoxのファイルアップロード → UIの制限でNG ngrok経由 → Tailscale環境のためlocalhostの名前解決失敗 結局TailscaleのIPでProxmoxホストに転送 → pct pushでLXCコンテナへ # ProxmoxホストからLXCへ pct push <CTID> /path/to/data.tar.gz /tmp/data.tar.gz # k3sマスターで解凍 mkdir -p /opt/history-map-data tar -xzf /tmp/data.tar.gz -C /opt/history-map-data 融通の効かないViteとふわふわClaude君の罠 npm run previewはデフォルトで許可ホストを制限する。Nginx Proxy Manager経由でアクセスするとBlocked requestが出る。 環境変数で全許可とかできたらよかったけど、結論だけ言うとできなかった。少なくともClaude君の指示では何をどうしても駄目だったので、最終的にvite.config.tsをデプロイ時に動的に書き換えることで回避した。 cat > /app/vite.config.ts << 'EOF' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], preview: { allowedHosts: ['your-domain.example.com'], }, }) EOF 最終的なYAML apiVersion: apps/v1 kind: Deployment metadata: name: history-map spec: replicas: 1 selector: matchLabels: app: history-map template: metadata: labels: app: history-map spec: nodeName: k3s-master containers: - name: history-map image: node:20-alpine workingDir: /app command: ["sh", "-c"] args: - | apk add --no-cache git git clone https://github.com/wasuken/history-map-app.git /app --depth=1 cat > /app/vite.config.ts << 'EOF' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], preview: { allowedHosts: ['your-domain.example.com'], }, }) EOF mkdir -p /app/public/data cp -r /data/historical /app/public/data/historical cp -r /data/modern /app/public/data/modern cp /data/translation-cache.json /app/public/data/translation-cache.json npm install npx vite build npm run preview -- --host 0.0.0.0 --port 3000 ports: - containerPort: 3000 volumeMounts: - name: map-data mountPath: /data volumes: - name: map-data hostPath: path: /opt/history-map-data type: Directory --- apiVersion: v1 kind: Service metadata: name: history-map-service spec: selector: app: history-map ports: - port: 80 targetPort: 3000 nodePort: 30080 type: NodePort nodeName: k3s-masterを指定しているのはhostPathがPodの動くノード上に存在する必要があるため。ProxmoxのNPM(Nginx Proxy Manager)から このNodePortに向けてプロキシを設定している。 ...

February 25, 2026 · 2 min

WSL2でExpo + E2Eテスト(MaestroとDetox)を試みて完全に詰んだ話

TL;DR WSL2環境でExpo(React Native)のE2EテストをMaestroとDetoxで試みたが、どちらもWSL2とWindowsエミュレータの構造的な問題で動かなかった。 かなり過言ではあるが、あえて感情的になるならば、Mobile開発においてMac以外は人権がない。というかあまりにもMac環境以外がだるすぎる。 環境 OS: Windows + WSL2(Ubuntu) Expo SDK 54 / React Native 0.81.5 New Architecture有効 Androidエミュレータ: Windows側で動作(Medium Phone API 36) ADB: Windows側のものをWSL2から参照 Maestroを試みる インストール curl -Ls "https://get.maestro.mobile.dev" | bash export PATH="$HOME/.maestro/bin:$PATH" ここで最初の罠。maestro --helpを叩くとAI系の全く別のCLIツールが応答した。同名の別アプリが先にPATHに入っていたため。$HOME/.maestro/binをPATHの先頭に置くことで解決。 フローの準備 # .maestro/add_and_complete_task.yml appId: com.example.myapp --- - launchApp - tapOn: text: "追加" - inputText: "テストタスク" - tapOn: text: "追加する" - assertVisible: text: "NOW" 実行して即死 You have 0 devices connected, which is not enough to run 1 shards. エミュレータはWindows側で動いており、adb devicesにはemulator-5554が見えている。しかしMaestroはWSL2側でデバイスを探すため認識できない。 --udid=emulator-5554を指定しても: Device emulator-5554 was requested, but it is not connected. maestro start-device --platform=androidを試みると: ...

February 23, 2026 · 2 min

react-native-pdf 6.7.7のiOS表示問題をpatch-packageで解決する

はじめに 業務でreact-native-pdfを使用した際、AndroidではPDFが正常に表示されるのにiOSでは表示されないという問題に遭遇しました。 この記事では、GitHubのissueで共有された解決策であるpatch-packageを使ったパッチ適用方法について解説します。 問題の概要 環境 { "react-native-pdf": "^6.7.7", "react-native": "0.80.1", "react-native-blob-util": "^0.22.2" } 症状 Android: PDF表示が正常に動作 iOS: PDFが表示されない この問題は、React Native 0.80以降でreact-native-pdfを使用した際に発生することが確認されています。 参考: pdf is not displayed,Android is working fine, but there are problems with iOS #966 解決策: patch-packageを使う GitHubのissueで@anhnguyen123さんが共有してくれたパッチファイルを適用することで、この問題を解決できます。 1. patch-packageのインストール まず、patch-packageとpostinstall-postinstallをdevDependenciesとしてインストールします。 # npmの場合 npm install --save-dev patch-package # yarnの場合 yarn add --dev patch-package postinstall-postinstall 参考: patch-package - npm 2. package.jsonにpostinstallスクリプトを追加 package.jsonのscriptsセクションに、postinstallスクリプトを追加します。 { "scripts": { "postinstall": "patch-package" } } このスクリプトにより、npm installまたはyarn installを実行するたびに、自動的にパッチが適用されます。 3. パッチファイルの配置 GitHubのissueからパッチファイルreact-native-pdf+6.7.7.patchをダウンロードし、プロジェクトルートにpatchesディレクトリを作成してそこに配置します。 ...

February 17, 2026 · 2 min

歴史地図アプリに日本語検索を実装: GeoJSONデータの効率的な翻訳手法

はじめに 歴史的国境を可視化する地図アプリを作っていたら、「日本語で国名検索ができない」という問題に直面した。外部のGeoJSONデータは英語のみで、日本語プロパティがない。 そこで、Gemini APIを使って効率的にデータを翻訳し、日本語検索を実装した手法を紹介する。 問題: 外部GeoJSONデータには日本語がない 使用したデータソース: 現代国境: Natural Earth (約200カ国) 歴史的国境: aourednik/historical-basemaps (18ファイル、紀元前2000年〜1920年) { "type": "Feature", "properties": { "NAME": "France", "NAME_JA": null // ← 日本語プロパティがない! }, "geometry": { ... } } このままでは「フランス」で検索できない。 解決策: 翻訳キャッシュを使った効率的なデータ拡張 アプローチ1: 愚直な方法 (非効率) 各ファイルごとに全データをLLMに投げる: // ❌ 非効率: 同じ国名を何度も翻訳 for (const file of geoJsonFiles) { const data = await fetch(file); const translated = await translateAll(data); // Franceを18回翻訳... await save(translated); } 問題点: 同じ国名が複数ファイルに登場 → 重複翻訳 トークン消費が膨大 処理時間が長い アプローチ2: 翻訳キャッシュ方式 (効率的) ✅ 全ファイル共通の翻訳キャッシュを使い回す: // ✅ 効率的: 一度翻訳した国名は二度と翻訳しない const translationCache = {}; // { "France": "フランス", ... } for (const file of geoJsonFiles) { const data = await fetch(file); // 未翻訳の国名のみ抽出 const newNames = extractUntranslatedNames(data, translationCache); // 新規の国名だけ翻訳 if (newNames.length > 0) { const translations = await translate(newNames); Object.assign(translationCache, translations); } // キャッシュを使って適用 applyTranslations(data, translationCache); await save(data); } 実装: Node.jsスクリプト 完全なコード この手法をNode.jsスクリプトとして実装した。Gemini 2.5 Flash Liteを使用している。この程度の翻訳ならこれで十分。 ...

February 15, 2026 · 4 min

PostgreSQLのCTEが現場で少ない理由を実務経験から考える

はじめに バッチ処理で大量のデータ変換を行う際、PostgreSQLのCTE(Common Table Expression、WITH句)を多用していた時期がありました。複雑な変換処理を段階的に分割できて、コードの見通しも良くなる便利な機能です。 しかし、実際の現場でCTEを使っているコードは意外と少ない。サブクエリや一時テーブルが使われているケースの方が圧倒的に多い印象です。 この記事では、実務でCTEを使って感じた強み・弱みと、「なぜ現場ではCTEが少ないのか」を考察します。特にPostgreSQL 12で大きく改善された最適化の仕組みについても解説します。 CTEの基本おさらい CTEはWITH句を使って一時的な結果セットを定義し、メインクエリから参照できる機能です。 WITH regional_sales AS ( SELECT region, SUM(amount) AS total_sales FROM orders GROUP BY region ) SELECT region, total_sales FROM regional_sales WHERE total_sales > 10000; サブクエリと似ていますが、名前をつけて再利用できる点が特徴です。変数のように扱えて、複雑なクエリを段階的に構築できます。 CTEの強みと弱み 強み 1. 可読性・保守性の向上 ネストしたサブクエリ地獄を回避できます。処理を論理的なステップに分割して、各ステップに名前をつけられるため、コードレビューやメンテナンスが格段に楽になります。 2. 再帰クエリが書ける WITH RECURSIVEを使えば、階層構造(組織図、カテゴリツリー)を扱えます。これはCTE独自の強みで、サブクエリでは実現できません。 3. 複数箇所から参照できる 同じCTEを複数回参照できます(ただし最適化の観点で注意が必要、後述)。サブクエリだと同じ処理を重複して書く必要があります。 4. デバッグしやすい 各CTEを個別に実行して中間結果を確認できます。サブクエリだと抜き出して実行するのが面倒です。 5. 変換処理の分離 SELECT句での複雑な計算を先にCTEで処理しておけます。WHERE句で使いたいけど計算が複雑な場合に便利です。 弱み 1. 親クエリのパラメータを参照できない サブクエリなら外側の列を参照できる(相関サブクエリ)のに対し、CTEは独立しているため参照できません。 -- サブクエリなら可能 SELECT * FROM orders o WHERE amount > (SELECT AVG(amount) FROM orders WHERE region = o.region); -- CTEでは不可能(外側のo.regionを参照できない) 2. 大量データ・長時間処理には不向き メモリ上に保持されるため、巨大データだと辛い。一時テーブルならインデックスを作成したり統計情報を活用できます。 実際、バッチ処理で数百万行のデータを扱う際、CTEよりも一時テーブルの方がパフォーマンスが良いケースが多かったです。 3. PostgreSQL 11以前は「最適化バリア」になる これが最大の問題でした。次のセクションで詳しく解説します。 ...

February 12, 2026 · 3 min

AsyncStorageって裏側何やってんの? - 2.0と3.0の実装の違いを調べてみた

私は普段React NativeでExpo触ってるので、AsyncStorageはよく使うんだけど、「そういえばAsyncStorageって裏側何やってんだろう?」って疑問が湧いてきたので調べてみることにした。 AsyncStorageの裏側 AsyncStorageのバージョンによって実装が少し違う。 AsyncStorage 2.0の実装 iOS/Androidのみ調査。 公式ドキュメント: Where your data is stored - Async Storage iOS (2.0) manifest.jsonファイルに保存される JSONファイル形式 パス: Documents/RCTAsyncLocalStorage_V1/manifest.json 詳細: 1024文字以下のデータはmanifest.jsonに、それより大きいデータは個別ファイル(MD5ハッシュ名)に保存される Android (2.0) SQLiteデータベースに保存される データベース名: RKStorage パス: /data/data/{package_name}/databases/RKStorage AsyncStorage 3.0 (next)の実装 公式ドキュメント: https://react-native-async-storage.github.io/3.0-next/ 対応プラットフォーム Android (SQLite) iOS (SQLite) ✨ macOS (SQLite) visionOS (legacy fallback, single database only) Web (IndexedDB backend) Windows (legacy fallback, single database only) iOS (3.0) SQLiteデータベースに変更された Androidと同じ実装に統一 パフォーマンスと安定性が向上 Android (3.0) 引き続きSQLite より洗練された実装 3.0からはiOSもAndroidも両方SQLiteになって、実装が統一されるそうだ。 互換性 React Native 0.76以降が必要(iOS/Android) Kotlin 2.1.0 iOS minimum target: 13 Android minimum SDK: 24 なぜiOSでmanifest.jsonからSQLiteに変更したのか あくまでも推測ではあるがやってみた。 ...

February 8, 2026 · 2 min

React NativeのTodoアプリで実装する相対時間ベースのプリセット機能

はじめに Todoアプリを使っていると、毎日・毎週繰り返す定型タスクの登録が面倒に感じることはありませんか? 「毎朝のルーチン」「週次ミーティングの準備タスク」など、同じタスクセットを何度も手入力するのは非効率です。この記事では、相対時間を使ったプリセット機能の実装方法を紹介します。 実装したアプリのソースコード: https://github.com/your-repo (適宜修正してください) 問題:絶対時間で期限を保存すると使い回せない 一般的なTodoアプリでプリセット機能を実装する場合、以下のような設計になりがちです: // ❌ よくある実装(絶対時間) interface PresetTask { text: string; dueDate: Date; // 2026-02-08 09:00:00 } この設計の問題点: プリセット作成時の日時が保存される 翌日読み込むと「昨日の9時」が期限になってしまう 毎回手動で期限を修正する必要がある 解決策:相対時間(dueHoursOffset)で管理する 代わりに、「今から何時間後」という相対的な時間で期限を管理します: // ✅ 相対時間ベースの設計 export interface PresetTask { id: string; text: string; priority?: Priority; dueHoursOffset?: number; // 現在時刻からの相対時間(時間単位) checklist?: string[]; } export interface Preset { id: string; name: string; tasks: PresetTask[]; createdAt: Date; } 公式ドキュメント: date-fns addHours: https://date-fns.org/v4.1.0/docs/addHours 実装の全体像 1. プリセット作成時の実装 プリセット編集画面では、期限を「現在時刻から何時間後」として入力します: // screens/PresetEditScreen.tsx const TaskInputRow = ({ item, index, onTaskTextChange, onDueHoursOffsetChange, // ... }: { item: PresetTask; index: number; onTaskTextChange: (index: number, text: string) => void; onDueHoursOffsetChange: (index: number, value: string) => void; // ... }) => { return ( <Card style={styles.taskCard}> <View style={styles.taskInputRow}> <TextInput label={`タスク ${index + 1}`} value={item.text} onChangeText={text => onTaskTextChange(index, text)} mode="outlined" style={styles.taskTextInput} autoComplete="off" autoCorrect={false} /> <TextInput label="期限(時間)" value={item.dueHoursOffset?.toString() || ''} onChangeText={value => { // 数字以外を除去 const filteredValue = value.replace(/[^0-9]/g, ''); onDueHoursOffsetChange(index, filteredValue); }} keyboardType="numeric" mode="outlined" style={styles.dueOffsetInput} /> </View> {/* ... */} </Card> ); }; ポイント: ...

February 8, 2026 · 4 min