株式会社クリアコード > ククログ

ククログ


macOS版FirefoxのIPCとドラッグ&ドロップ周りのバグを調査する

クリアコードの結城です。

先日、動画でバグ報告する方法の中で例として挙げていたmacOS版Firefoxのサイドバーの操作に関するバグですが、最終的には原因の特定とパッチの提出にまで至る事ができました。このエントリでは当該バグの調査の過程を辿りながら、macOS上のFirefoxにおいてマルチプロセスが絡む部分やドラッグ&ドロップに関わる部分のデバッグをどのように進めたかの一例をご紹介します。

問題の概要

当該バグはツリー型タブというアドオンに寄せられた報告に端を発しています。このアドオンはFirefoxのサイドバー内でタブバーの代替となるUIを提供するという物で、項目をクリックするとタブが切り替わり、ドラッグ&ドロップでタブを並べ替える事もできます。このサイドバーパネル上において「セッション復元後にまだ読み込まれていないタブをドラッグしようとするとドラッグできず、また、それ以後サイドバー内で一切のドラッグ操作が行えなくなる」という現象が発生する、というのが報告の要旨でした。

報告者の方と連絡を取りながら調査を進めた結果、この問題はmacOSでのみ発生するらしいという事と、アドオン側で可能と思われる対策を講じても現象を回避できない、Firefox自体のバグに起因する問題であるらしいという事が分かりました。当該アドオンの実装を参考に最小のテストケースを作成して検証した結果、確かに現象を再現できたため、Firefox自体のバグとしてBugzillaに報告しました。またその過程において、上記の再現手順の他に以下の事も事が分かりました。

  • サイドバーの内容(=アドオンが提供するコンテンツ)が別プロセスで動作している時だけ現象が再現する。
  • 既にセッションが完全に復元された状態のタブでは、現象は再現しない。
  • 現象発生時は、dragstartイベントは発生するが、その後に発生するはずのdragoverdragenterdragleavedragendの各イベントは発生しない。

調査前の時点での見立て

本格的に調査を始める前に、この時点で分かっていた情報から、Firefoxの内部で何が起こりどうしてこの問題が起こっているのかという事について、いくつかの仮説を立てました。

  • 仮説1:タブを切り替えてセッション復元処理が行われる時に、親プロセスと子プロセス群との間の「繋ぎ替え」が発生し、サイドバーのプロセスが「アクティブな子プロセス」でなくなるせいで、サイドバーのプロセスから親プロセスにドラッグ操作が伝わらなくなる。
  • 仮説2:タブを切り替えてセッション復元処理が行われる時に、ドラッグ&ドロップの処理に対して何らかの割り込みをかけるせいで、「何らかのフラグ」が内部で立ってしまい、以後ドラッグ&ドロップのイベントが発生しなくなる。

ただ、そもそも一連の処理に関わる実装がどのような設計になっているのか、「繋ぎ替え」や「フラグ」とここで呼んでいるような物が実際にあるのかどうかも、この時点では不明なままでした。これらはあくまで、過去の経験に基づく推測での見立てとなります。

また、筆者はmacOSのネイティブアプリ開発の知見を全く持っていないという問題もありました。筆者はWindows版のFirefoxの低レイヤ部分の調査は若干経験しているものの、macOSネイティブの開発経験は皆無です。今回のように特定プラットフォームでしか発生しない問題は、そのプラットフォーム向けのコード(WindowsではWin32 API、LinuxではGTK、macOSではCocoa APIを使う部分)を調査する必要が出てきます。調査対象の事を全く知らない状態での手探りの調査は泥沼化しがちですので、これは大きなリスクです。

そのリスクを回避するために「macOSアプリでのドラッグ&ドロップはどのように実装するのか」という事を先に勉強してから調査に臨むというやり方もあります。しかしながら、今回の調査に必要な知識がその知識体系のどのあたりに存在しているかは全く予想が付きませんし、そこまでガッツリとmacOSでの開発を学びたいという訳でもありません*1。そもそもこれは「解決できれば儲け物だが、自身がmacOSユーザーではないので究極的には直らなくても困らない問題」という性質の物でしたので、失敗に終わったとしてもそれほどダメージは無いと言えます。ですので、知見の不足には一旦目を瞑って、Mozilla製品でのクロスプラットフォームな開発の知識だけを元に調査していく事にしました。

調査の出発点を探す

全く手がかりがない所から調査を行うのは、Firefoxほどの規模のソフトウェアではほぼ不可能です。そこで、ここまでに分かっていた事からヒントになりそうな要素はないかを検討してみました。

成功ケースと失敗ケースの両方の再現手順がはっきりしているという事は、その両者で起こっている事を子細に比較していけば、現象が起こる原因が分かる可能性は高いと考えられます。一連の操作の中で「共通してこのモジュールのこのメソッドが呼ばれている」という部分が特定できれば、そこを手がかりにして調査範囲を広げていけるはずです。

(ある一点を起点にして、成功ケースにまで辿り着くルート上のどこかで、失敗ケースへと分岐するポイントがあるはず)

ここで着目したのは、正常な場合でも異常な場合でもDOMのdragstartイベントは発行されているという点でした。Firefoxの内部ではこういったDOMイベントの名前は定数で定義されていますので、その定数を伴ってイベントを初期化したり発行したりしていそうな所を特定できれば、そこを起点に調査を進められます。

"dragstart"という文字列でざっとソースコード内を検索してみた所、dragstartを定数として定義しているらしい箇所がすぐに見つかりました。ここで、このイベント名は他の箇所からeDragStartという定数名で参照されているらしいという事が分かりました。

次に、eDragStartという定数名でソースコード内を検索してみた所、自動テスト用に擬似的にイベントを生成しているらしい箇所に混ざって、EventStateManagerというモジュールの中でイベントを生成しているらしい箇所が1箇所だけ見つかりました。この処理が含まれているのはEventStateManager::GenerateDragGesture()というメソッドでしたので、メソッド名から見ても、どうやらドラッグ&ドロップの操作が行われた時には必ずここを通過すると考えて良さそうです。

同様にeDragOverという定数名でソースコード内を検索した結果、成功ケースにおける「dragoverイベントが発行されるのはここ」という場所も特定できました*2

ところで、今回の問題は厳密に言うと、以下の2つの現象が起こっています。

  • セッション復元後にまだ読み込まれていないタブをドラッグしようとするとドラッグできない。(最初の問題)
  • それ以後、サイドバー内で一切のドラッグ操作が行えなくなる。(2回目以降の問題)

2つの現象は「タブをドラッグできない」「どちらもdragstartイベントだけが発行される」という症状は共通していますが、原因が同じかどうかは分かりません。ただ、再現が容易なのは2つ目の現象の方(1つ目の現象が再現したら、以後は何度やっても2つ目の現象が起こる事になるので)なので、先にこちらの方から原因を調べる事にしました。こちらの原因が分かれば、今度はそれが「その原因となっている状況を引き起こした犯人=1つ目の現象の原因」を特定する手がかりとなります。

低レベルのログを収集する

Firefoxには環境変数を指定する事でモジュール単位の低レベルのログを収集する機能があるため、成功ケースと失敗ケースの両方についてこのモジュールのログを取得して比較すれば、何か分かるかも知れません。この方法であれば、子プロセスが出力したログもそれぞれ個別に収集する事ができます。

低レベルのログを収集するには、macOSではターミナルを使います。Firefoxのアプリケーションファイルはファイルシステム上はフォルダ(ディレクトリ)になっているので、以下のようにして環境変数を設定して実行します。

$ MOZ_LOG=timestamp,sync,nsCocoaWidget:5 MOZ_LOG_FILE=~/debug.log /Applications/Nightly.app/Contents/MacOS/firefox

環境変数MOZ_LOGには、ログを取得したいモジュール名とログレベルを:で繋げたものを、,区切りのリストとして指定します。このようにしてFirefoxを起動すると、メインプロセスのログがMOZ_LOG_FILEで指定した名前のファイルに出力され、子プロセスのログはdebug.log.child-1のような名前で同じ位置に出力されます。ログ出力が始まった状態から、ターミナルの新しいタブでtail -F debug.log*と実行すれば、それぞれのログに出力される内容をリアルタイムで見る事もできます。

しかし、先程特定した「成功時と失敗で共通して呼ばれているモジュール」であったEventStateManager.cppのソース内をlogで検索してみても、ログ出力を行っているらしきコードは見つかりませんでした。また、dragoverイベントが発行される契機になっている処理の方についてはnsCocoaWidgetというログモジュールでログを収集できましたが、こちらは成功ケースのログしか出力されず、しかも知りたい核心部分の処理が全て終わった後の時点のログでした。これではログを取っても調査の役には立ちません。

モジュール単位でのログが役に立たないとなると、何か別の方法で「成功ケースと失敗ケースのそれぞれで、内部的に何が起こっているのか」を調べる必要があります。

デバッグビルドとデバッガを使用した調査

このような場面で使えるツールの1つがデバッガです。問題が発生するケース・期待通りに動くケースで必ずこの行を通る、という事がはっきり分かっている場合には、デバッガ上でそこにブレークポイントを仕掛けておく事で、処理を一時停止して各変数の値を詳細に見る事ができます。また、そこから1行ずつ処理を進めて流れをじっくり追うという事(ステップ実行)もできます。

デバッグビルドの準備

ただ、デバッガを使うにはmacOS版Firefoxのデバッグ用ビルドが必要です。オフィシャルに公開されているバイナリは無い様子でしたので、「Firefox macOS debug」と検索して出てきたMDNのmacOSでのデバッグ手順の解説ビルド手順の解説を見ながら自分でデバッグビルドを作成することにしました。

  • macOS版FirefoxのビルドのためにはXcodeが必要です。しかし、調査に使える環境はmacOS 10.12 Sierraだったため、App Storeにある最新のmacOS向けのXcodeはインストールできませんでした。そこで「Xcode for old macOS」などのキーワードで検索して見つかった情報を参考に、開発者向けのページからXcodeの旧バージョンをダウンロードしてインストールしました。
  • Homebrewなどのツール類については、解説にある通りbootstrap.pyを使って一括インストールする事ができました。
    • 初期設定時に選択するビルド対象は、Artifact Buildではないデスクトップ版Firefoxとしました。Artifact Buildとは、ビルド済みバイナリをダウンロードしてきて使う事によって、ビルドに要する時間を短縮できるという特殊なビルドです。しかし、今回はバイナリ部分をデバッグ用にビルドしたいので、Artifact Buildは不適当という事になります。
  • 既にNightlyで現象を再現できていたので、cloneするリポジトリはNightlyに対応する https://hg.mozilla.org/mozilla-central/ を使いました。

解説の通りに進めてデバッグビルドができた時点で*3./mach runでテスト実行してみました。最適化がなされないためなのかデバッグビルドだからなのか、一挙手一投足がもたつく程に非常に低速ではありますが、一応動く事は動いているため、準備が整った事にして次の行程に進みます。

ブレークポイントの設定とステップ実行

macOSでのデバッグ手順解説に記載の通りに準備した上で、Xcodeの「Product」メニューから「Run」を選ぶと、デバッグビルドのFirefoxが起動します。

(Xcodeでブレークポイントを設定した様子)

ブレークポイントを設定するためには、まずXcodeの左ペインのフォルダーアイコンをクリックし、ソースコードのファイル一覧から「ここで処理を止めたい」という処理が含まれているファイル(今回は dom/events/EventStateManager.cpp )を選択します。 右上のペインにソースコードが表示されますので、止めたい処理の行番号をクリックします。すると、行番号の部分に青いマークが付きます。これをブレークポイントといい、Xcode経由で起動したデバッグビルドのFirefoxの内部で処理がこの行に到達すると、処理がその場所で止まってXcodeに制御が移るようになります。

ブレークポイントで処理が止まった後は、左のペインの内容が関数呼び出しのスタック表示に切り替わり、ソースコードが表示されているペインの左下ペインには停止位置での各変数の値が表示されます。

(ブレークポイントで処理が停止した様子)

この時には、ツールバー上の各ボタンで以下の操作を行えます。

  • Deactivate breakpoints:全てのブレークポイントを一時的に無効化する。もう一度クリックすると、ブレークポイントを有効化する。
  • Continue program execution:次のブレークポイントまで処理を一気に進める(ブレークポイントでの一時停止を解いて、通常の実行に戻す)
  • Step over:処理をソースコード上で1行分次に進める(ステップ実行)。
  • Step into:関数の呼び出し行において、関数の中に入る(呼ばれた関数の1行目からステップ実行を行う)。
  • Step out:関数の外に出る(現在ステップ実行中の処理を関数の最終行またはreturnに到達するまで進めて、呼び出し元の関数で元の関数が呼ばれている行からステップ実行を行う)。

変数の値を調べても有用な情報を見付けられなかった場合には、ステップ実行を繰り返したり、別の位置にブレークポイントを設定したり、ブレークポイントを削除*4したりして、解析を続けていきます。同じ箇所で「期待通りの結果が得られている時の内部状態」と「問題が起こっている時の内部状態」を詳細に比較していけば、「何が原因で、処理が期待と異なる方向に進んでいってしまったのか」を明らかにできるというわけです。

ただ、実際には各変数の値はツリー構造になっていて、奥の奥の方に原因が潜んでいるという場合も多々あるため、余程「ここをピンポイントで調べたい」という事が事前にはっきり分かっている場合でもない限りは、この方法で一発で原因を見付けるというのは難しいです。今回の調査でも、闇雲に調べるだけでは残念ながら決定打となる情報に辿り着く事はできませんでした。

期待外れだったのは、成功ケースで実際にdragoverイベントが発行されている場面の詳細を調べられなかったという点です。この処理はCocoaに対して登録しておくイベントハンドラのような関数の中にあり、Cocoaから通知されたイベントをトリガーに実行されるため、ここにブレークポイントを置いても(関数呼び出しのスタックが切れてしまっているので)デバッガでは処理の呼び出し元を辿る事ができないのです。「成功ケースと失敗ケースで明らかに異なる部分」が目の前にあるにも関わらず、そこからは何の情報も得られないという、非常に残念な結末でした。

ただ、ここまでの調査過程で以下の事は分かりました。

(プロセス間でのドラッグデータの受け渡しの様子の図)

  • 成功ケースでは親プロセス側で認識できたドラッグデータ(アドオンで定義したdragstartイベントのリスナにおいて、event.dataTransfer.setData()で追加したデータ)の個数が1以上になっているが、失敗ケースでは0になっている。子プロセス側で設定したはずのドラッグデータが親プロセスからは認識されていない。そのため、EventStateManager::DoDefaultDragStart()falseを返して終了する形となっており、ドラッグセッションを開始するnsDragService::InvokeDragSessionWithImage()が呼ばれていない。
  • このドラッグデータはEventStateManager::DetermineDragTargetAndDefaultData()nsContentAreaDragDrop::GetDragData()DragDataProducer::Produce()TabParent::AddInitialDnDDataTo()TabParentmInitialDataTransferItemsというメンバ変数の値を取得する形で初期化されている。
  • TabParentmInitialDataTransferItemsの値は、TabParent::RecvInvokeDragSession()で設定されている。このメソッドは、プロセス間通信でPBrowser::Msg_InvokeDragSession__IDというメッセージを受け取った時に実行されている
    • この時のIPC周りのコードは自動生成されているため、デバッガでブレークポイントを設定できない。
  • PBrowser::Msg_InvokeDragSession__IDというメッセージは、子プロセス側でnsDragServiceProxy::InvokeDragSessionImpl()が呼ばれた時に(IPC周りの自動生成されたコードを経由して)親プロセスに向けて送出される模様。

以上の通り、成功するケースでは子プロセス側のInvokeDragSessionと親プロセス側のInvokeDragSessionが両方とも期待通りに処理されるのに対し、失敗するケースでは親プロセス側のInvokeDragSessionに到達する前に(ドラッグデータが0個という事で)処理が終わってしまっている、という事が分かりました。という事は、失敗ケースでは「子プロセスが送ったメッセージを何らかの理由で親プロセスが受け取れていない」か、もしくは「子プロセスがそもそもそのメッセージを送出していない」かのどちらかであるという事が言えます。

標準出力・標準エラー出力に現れるメッセージの監視

状況の把握のためにPBrowser::Msg_InvokeDragSession__IDというメッセージやその他のメッセージを親プロセスが受け取れているかどうかを調べたいと思ってコードを見ていると、TabParent::RecvInvokeDragSession()呼び出し元箇所mozilla::ipc::LogMessageForProtocol("PBrowserParent",...というコードがある事に気付きました。関数の定義を調べてみた所、これはデバッグビルドの実行時にMOZ_IPC_MESSAGE_LOGという環境変数にPBrowserParentまたは1が設定されている時に、fputsで標準エラー出力にログメッセージを出力するという物であることが分かりました。このログメッセージが現れるかどうかを調べれば、上記のIPCのメッセージを親プロセスが受け取っているかどうかが分かります。

fputsで直接標準出力や標準エラー出力に出力されたメッセージは、MOZ_LOGで収集するログには出力されません。ではどこを見ればよいかというと、Xcodeの右下のペインです。標準出力や標準エラー出力に出力されたメッセージは、ここで確認する事ができます。

デバッグ実行時の環境変数は、Xcodeのメニューの「Product」→「Scheme」→「Edit Scheme」→「Run」→「Environment Variables」で設定できます。ここでMOZ_IPC_MESSAGE_LOGPBrowserParentを設定して再度デバッグ実行して確認した所、成功ケースでは上記のログメッセージが出るのに対し失敗ケースでは出なかったという事から、親プロセス側がPBrowser::Msg_InvokeDragSession__IDというメッセージを受け取れていないという事が分かりました。

そうなると今度は、子プロセス側がIPCのメッセージを送出しているにも関わらず親プロセスが受け取れていない(IPCの仕組みの中での問題)のか、それとも子プロセス側でnsBaseDragService::InvokeDragSession()が実行されていないかのどちらかという点が問題になります。

任意のログを任意のタイミングで出力させる

ここで子プロセス側の処理に対してデバッガでブレークポイントを設定できればよかったのですが、弊社調査環境では何故か、MDNのマルチプロセス有効時のデバッグに関する説明通りに設定しているにも関わらず、設定したブレークポイントで処理を止める事ができないという状況でした。処理の流れを追いたい肝心のモジュールは前述した通りMOZ_LOGでの指定でログを出力してくれないため、これでは調査のしようがありません。

そのため、ここで初めてカスタムログに頼る事にしました。

MOZ_LOGで出力されるログは既存の物以外に、全く新しくログを出力する事もできます。C++製コンポーネントの開発に関わる人向けにMOZ_LOGで出力可能なログをC++製のモジュール内で定義する手順の説明が用意されていますので、これを参考に、EventStateManager.cppの冒頭で以下のようなログモジュールを定義するようにしました。

using mozilla::LogLevel;
static mozilla::LazyLogModule sEventStateManagerLog("EventStateManager");

その上で、ここまでに調査した範囲の中で特定していたドラッグを開始するかどうかの判定を何段階も行っている関数において、returnする直前にMOZ_LOG(sEventStateManagerLog, LogLevel::Info, ("DoDefaultDragStart - no drag service"));のような行を追加して、ログを見ればどのreturnで関数が終了したのかを分かるようにしました。

このようにして収集したログを調べて分かったのは、2回目以降の失敗ケースにおいては「現在進行中のドラッグセッションがまだあるならば、新たなドラッグセッションは開始しない」という判断の結果親プロセス側でドラッグセッションが開始されていない、という事でした。

つまり、1回目の失敗ケースから2回目以降の失敗ケースにかけての間では以下の事が起こっていたと考えられます。

  • 成功ケースや1回目の失敗ケースでは、子プロセス側では通常通りにドラッグセッションが開始されている。
  • その処理が親プロセスに引き継がれるまでの処理のどこかに問題があって、「子プロセスはドラッグセッションが開始されているが、親プロセスはドラッグセッションが始まっていない(終了した)と認識している」状態が発生した。
  • 本来であれば子プロセス側のドラッグセッションを終了させるための処理が行われるはずなのに、この一連の処理がどこかの時点で中断されてしまったために、子プロセス側のドラッグセッショが終了処理が呼ばれず、ゾンビドラッグセッションが残留してしまっている。
  • このゾンビドラッグセッションがあるせいで、親プロセスでドラッグセッションが開始されなくなっている。

ここまでで分かった事をまとめて、1回目の失敗ケースの原因を探る

ここまでの調査で、2回目以降の失敗ケースは「1回目の失敗ケースにおいて、子プロセス側のドラッグセッションが開始されたにも関わらず、親プロセス側ではドラッグセッションが開始されなかったために、子プロセス側で残ってしまったゾンビドラッグセッション」が原因で発生している事が分かりました。

そうなると今度は、1回目の失敗ケースで何故ゾンビドラッグセッションができてしてしまうのか、何故親プロセス側でドラッグセッションを開始できなかったのか(処理が中断されたのか)、という事を明らかにしなくてはいけません。

現時点までで、1回目の失敗ケースでは子プロセスから親プロセスへドラッグデータがきちんと引き渡されているという事が分かっています。そうなると、「ドラッグデータは受け取れているが、ドラッグセッションを開始できない」という状況が発生する条件は、一体何なのでしょうか?

以上の事を念頭に置きながら関連モジュールのコードを眺めていると、各プラットフォームで共通のドラッグセッション開始のための処理であるnsBaseDragService::InvokeDragSession()から呼び出されるmacOS版固有のnsDragService::InvokeDragSessionImpl()の実装の冒頭に、何らかの条件が満たされなかった時にNS_ERROR_FAILUREというエラーコードを返却するというコードがある事に気がつきました。

さらにその先を読み進めていくとbeginDraggingSessionWithItemsというCocoaのAPIを呼んでいる箇所があります。調べてみると、beginDraggingSessionWithItemsはまさにドラッグセッションを開始するためのCocoaのAPIであると書かれています。

ここまで分かってやっと、失敗ケースでdragoverイベントを発行するためのコードが何故呼ばれないかの謎が解けました。このコードはCocoaでのドラッグセッションが進行中である場合に呼ばれるdraggingUpdateというイベントハンドラの中にありますが、ドラッグセッションが開始されていなければ当然これらのイベントハンドラも呼ばれないという訳です。つまり最初から、beginDraggingSessionWithItemsの呼び出しに至るまでのコードパスを調査すればよかったのでした。Cocoa APIの知識を持たない状態で調査を始めたために、ひどく遠回りをしてしまった事になります。

ともあれ、これで調査が必要な範囲はだいぶ絞り込めました。今度はbeginDraggingSessionWithItemsが呼ばれなくなるパターンの分岐に絞ってMOZ_LOGを仕込んで再びログを収集してみた所、1回目の失敗ケースはまさに先程見付けたNS_ERROR_FAILUREを返す分岐に入っている、という事が分かりました。

その分岐に入る条件はgLastDragViewというグローバル変数が空である事で、これは意味としては、「どのビューでドラッグが開始されたか分からなければ、ドラッグセッションを開始せずにエラーを返す」という事です。

そこでこの変数に値を代入している箇所を検索した所、変数の初期化時を除くと、Cocoa API用のmouseDraggedというイベントハンドラの中でのみ値を設定している事が分かりました。具体的には、4728行目で「そのビュー自身」を代入し、イベントの処理を挟んだわずか10行後にnilを再代入しているという状況でした。そこでこの前後に絞ってMOZ_LOGをさらに仕込んで調査した所、成功ケースと失敗ケースでは非同期処理の実行順が異なっており、そのせいで失敗ケースでは、ドラッグが開始されたビューが分かるより前にドラッグセッションを開始しようとしてドラッグセッションを開始できずにいるらしいという事が分かりました。

詳しい人の意見を聞いてみる

Cocoa APIまわりの非同期処理の話になると、ますますこちらに知見がないため、これ以上の調査は難しく思えます。そこで一旦、識者に意見を求めてみる事にしました。

調査対象にしているファイルのコミット履歴を見ると、複数人が関与している様子が窺えます。この時に注目するのは、コミットした人やパッチを書いた人ではなく、パッチのレビューを行った人です。

パッチが投入されるまでの流れを見ると分かりますが、パッチのコミット自体はcheckin-neededという目印に沿って作業担当者が行っているだけなので、コミットした人は必ずしもそのモジュールのエキスパートという訳ではありません。また、パッチを書いた人もたまたま関わっただけの協力者という立場である可能性があります。それらに比べると、レビューはある程度の知見がないとできないため、複数のパッチでレビュー担当者として指名されている人ほど、信頼できる識者である可能性が高くなります。

Bugzillaではコメントを追記する時に「needinfo」という情報を設定できます。これはバグの報告者など他の人に情報を求める、つまり質問するときに使われる機能です。「Need more information from...」というラベルのチェックボックスをONにして、質問先の種別を「other」にし、上記の方法であたりを付けた識者の人のメールアドレスを入力して投稿すれば、これで「質問されている」という事がその人に通知されます。

数日待った所、その方が反応して下さり、「それぞれのケースのスタックトレースを収集してみてはどうか」というようなコメントを頂けました。

スタックトレースはXcodeのメイン画面の左ペインに表示されますが、これをそのままコピーする事はできません。文字列としてコピーできる形でスタックトレースを取得するには、ブレークポイントで処理を止めた状態でXcodeのウィンドウ右下のペインのコンソールの「(lldb)」と表示されている箇所(これが実はプロンプトになっています)にbtというコマンドを入力して実行すると、コンソール内にスタックトレースの情報が出力されます。これを選択してコピー&ペーストすれば、スタックトレースを容易に収集できます。

この方法でスタックトレースを収集してじっくり比較してみた所、成功ケースではCocoa APIのmouseDraggedハンドラからそのまま呼び出しが続いているのに対し、失敗ケースではそうなっておらず、RefreshDriverTimer::Tickなどのメソッドの呼び出しが親となっていたという事が分かりました。

(1つのイベントループの中で処理が完結する場合は成功する) (1つのイベントループで処理が完結しなかった場合は失敗する)

成功ケースでも失敗ケースでもmouseDraggedから全ての処理が始まっているとばかり思い込んでいたため、これは盲点でした。

どうやら、何らかの条件に合致した場合には、ドラッグ時のイベントの処理が実際には後のイベントループに回されており、その場合は必要な情報であるgLastDragViewが既に失われてしまっているのでドラッグセッションを開始できない、という事がこの問題の根本的な発生原因である模様です。このような状況は、元々同期処理で書かれていた物を後から非同期処理の形に改修したというような場面でよく見られる物です。Firefoxは現在、体感的なパフォーマンスの向上のための改良が続いていますので、その中でそういった事が起こる事は十分にあり得そうです。

パッチの作成

ともかく、原因が分かったことでようやく問題を修正する段階に移れます。

ここまでの調査で分かった原因からは、以下のような改修案が考えられます。

  1. gLastDragViewをすぐにnilで破棄してしまわずに、ドラッグイベントの処理が終了するまで待ってから破棄するようにする。
  2. gLastDragViewに相当する情報をイベントの情報の一部として引き回す、または保持しておき、後のイベントループで続きが処理される時にその情報を参照するようにする。

1の方法は非常に単純なやり方です。確実にこの問題は直りますが、gLastDragViewがメモリ上に保持される期間が長くなるため、メモリリークや、ドラッグ開始操作として認識されて欲しくない物が誤ってドラッグ開始操作として扱われてしまうといった新しい別の問題の原因になる恐れがあります。

2は、そういった副作用の恐れがない安全な方法に思えます。一般的にはこちらの方針で改修を行う事が望ましいと言えるでしょう。ただ、今回はこの方針を取るのがためらわれる理由があります。それは、gLastDragViewを設定している箇所や参照している箇所がmacOS固有の実装の中であるのに対し、設定箇所から参照箇所までの間に通過する処理はほとんど全てクロスプラットフォームな実装であるという点です。クロスプラットフォームな実装の中に特定プラットフォーム向けのコードを入れるという変更はあまり行儀が良いとは言えず、また、その中には非同期処理の基盤的な実装も含まれていたため、変更の影響が想像以上に広い範囲に及ぶ恐れがあります。副作用の恐れがないはずの方法の方が、実際にはリスクが大きいという困った状況です。

以上の検討結果を踏まえ、1の方が調査が必要な範囲は狭い(macOS固有の実装だけに変更が閉じる)と考えられたため、まずはその前提で影響範囲を調査・検討しました。その結果、想定外のタイミングでドラッグセッションの開始処理が呼ばれてしまう事は理論上あり得ないという結論に至ったため、実際に1の方針でパッチを作成しました。しかし安全であると言い切れる確証は持てなかったため、前のコメントで紹介して頂けた別のこの件に詳しい方にneedinfoで意見を求めてみる事にしました。

時間はかかったものの無事にその方にパッチを見て頂けて、方針はこれで問題ないというコメントと、追加で修正が必要な箇所(コメント形式で書かれた説明文が実装と食い違うようになるため、それらも併せて更新する必要がある)の指摘を頂きました。現在は、その指摘に基づいて更新したパッチを再提出してレビューを待っているという状況です。この後特に問題がなければ、パッチが取り込まれるまでの流れなどで紹介ている通りに進行してマージに至ると期待されます。

まとめ

以上、プロセス間通信やmacOSでのドラッグ&ドロップといった箇所の実装を調査して、バグの原因を特定しパッチを提出するまでの一通りの経緯をご紹介しました。

最終的なパッチは実質的には1行削除・2行追加しただけの内容ですが、そこに辿り着くまでの調査にはかなりの時間を要する結果となってしまいました。要因の1つに、Cocoa APIを使ったmacOSネイティブアプリにおけるドラッグ&ドロップの実装の一般的な作法を知らないまま調査を進めた事がある事は否めず、その部分の調査をスキップする選択をしたという調査初期の判断ミスが悔やまれます。

この記事で述べた情報そのものが直接的に役に立つ場面はあまり無いと思われますが、未知の部分が大きい調査対象に取り組む際の様々なアプローチの仕方や、調査方針の見直しのタイミングなど、メタな部分で知見を得るための資料、あるいは「しくじり先生」的な反面教師として参考にしていただけると、この紆余曲折の記録も無駄にはならないのではないかと思っております。

OSSの利点の1つとして、今回のように、不可解なトラブルに遭遇した場合でも第三者の立場で詳細な調査を実施できる余地があるという点が挙げられます。本体の開発チームは通常のリリースに向けての作業に注力している事が多く、今回の問題のように希な条件下でしか発生しない・一般のユーザーに与える影響の小さい問題の解決はどうしても後回しになってしまいがちです*5。しかし、その問題に遭遇した当事者にとってはまさに今直面している問題で、ともすれば死活問題ともなり得ます。そのような場合でもただ待つだけ以上の事ができるというのは、OSSならではの事と言えるでしょう。

当社は、そのようにOSS開発により積極的に関われるようになりたい人を支援するOSS Gateという取り組みを支援しています。このエントリを見て「普段使っているOSSに自分でもフィードバックできるのか! 自分もやってみたい!」と新鮮な驚きを感じた方や、「そうそう、こんな感じで調査するんだよね。このやり方が分からなくて困ってる人を手助けできればいいんだけど……」とお考えの方は、ぜひワークショップへの参加をご検討下さい。

また、当社ではFirefoxやThunderbirdの他、Fluentd、Groonga等のOSSの法人利用において発生する様々なトラブルや不具合について有償でのサポートサービスを提供しており、このエントリに記載しているようなソースコードレベルでの調査も承っております。業務上でのOSSの利用でお困りの場合、メールフォームよりお問い合わせ下さい。

*1 そこで学ぶ知識を今後も活用していける目処があるなら話は別ですが……

*2 ただ、こちらはCocoa(macOSでGUIアプリを実装する際に使われるAPIセット)のイベントハンドラにあたる関数で、単にCocoaでの`draggingUpdate`というイベントを`dragover`というDOMイベントにマッピングするためのものでしかありません。後述しますが、この事が後の調査を難航させる1つの原因になりました。

*3 使用した検証機ではフルビルドに4〜5時間程を要しました。

*4 ブレークポイントを左クリックすると、そのブレークポイントだけ一時的に無効化できます。右クリックして「Delete Breakpoint」を選択すると、ブレークポイントを削除できます。

*5 とはいえ、今回取り組んだBugはpriorityがP3(中程度)と設定されており、それなりに重要な問題と認識されてはいたようです。

2018-10-17

Apache Arrow 0.11.0リリース

Apache Arrow 0.11.0のリリースマネージャーをした須藤です。

2018年10月8日にApache Arrow 0.11.0をリリースしました。

0.11.0の新機能

0.10.0のリリースから2ヶ月くらいしか経っていないのですが、今回のリリースはすごくアグレッシブです。まだ荒削りのものが多いのですが、新しく次の機能が入りました。

  • RPC機能
    • Apache Arrow Flightという名前がついている
  • Apache Parquet C++
    • 別リポジトリーで開発していたが密に連携しているのでApache Arrowのパッケージ含めることになった
  • Apache Parquet GLib
    • 別リポジトリーで開発していたがApache Parquet C++がApache Arrowに含まれることになったのでこれも含めることになった
  • LLVMを使った実行エンジン
    • Gandivaという名前がついている
  • CSVパーサー
    • CSVを高速に読み込んでApache Arrowのデータとして処理できるようになる
  • R実装
    • C++のバインディングとして実装
  • MATLAB実装
    • C++のバインディングとして実装

Apache Arrowの最新情報(2018年9月版)でまとめた内容のいくつかはすでに古いものになってしまいました。開発が活発ですね!リリース後に.NET実装も現れています。

開発者を増やしたい!

活発な開発をさらに活発にするために、12月8日(土)にApache Arrow東京ミートアップ2018という「開発者を増やすこと」が目的のイベントを開催します!

開発者を増やしたいプロジェクトはApache Arrowはもちろんですが、Apache Arrow以外のデータ処理関連ソフトウェアもです。たとえば、Apache SparkやApache Hadoop関連の開発者も増やしたいですし、R関連の開発者も増やしたいですし、Ruby用のデータ処理ツールの開発者も増やしたいです。

開発に参加したいけど踏み出せていなかったという人はこの機会をぜひ活用してください!開発に参加する人を増やすためのいろいろな仕掛け(?フォロー?)を準備しています。

「Apache Arrow東京ミートアップ2018」は今のところ以下のプロジェクトの開発者・関係者が協力しています。

  • Apache Arrow
  • Apache Spark
  • R
  • Ruby(Red Data Tools)

他のプロジェクトのみなさんにも協力して欲しいので、開発者が増えることに興味がある人はぜひ@ktouに連絡してください。たとえば、次のような界隈のプロジェクトです。

  • Python(pandasとか)
  • JavaScript
  • Julia
  • Go
  • Rust
  • MATLAB
  • GPU

Ruby用のデータ処理ツールを開発するRed Data Toolsプロジェクトもこのイベントに協力しています。

そんなRed Data Toolsは毎月開発イベントを開催しています。今月は来週の火曜日(2018年10月16日)開催です。「Apache Arrow東京ミートアップ2018」を待たずにApache Arrow本体やRuby用のデータ処理ツールの開発に参加したい人はぜひどうぞ。「Apache Arrow東京ミートアップ2018」後には翌週の火曜日(2018年12月11日)に開催します。

まとめ

Apache Arrow 0.11.0をリリースしたので自慢しました。

Apache Arrowを含むデータ処理関連プロジェクトの開発者を増やすためのイベントをすることを宣伝しました。

2018-10-10

グループポリシーでエンタープライズの証明書を配布した場合、IE(Edge)では閲覧できるページがFirefoxではエラーで閲覧できない場合があります

Firefox ESR52以降のバージョンは、security.enterprise_roots.enabledtrueに設定しておく事で、Active Directoryのグループポリシー機能を使って配布された証明書をFirefoxに自動的にインポートできます *1。また、Firefox ESR60ではグループポリシー経由である程度の設定の制御も行えるようになりました。以前のバージョンのFirefoxでは証明書のインポートや管理者による設定の制御にはCCK2や独自形式の設定ファイルなどを別途用意する必要がありましたが、Firefox ESR52以降では設定も証明書の管理もActive Directoryのグループポリシーに一元化できるようになったという事で、Internet Explorer(およびその後継ブラウザであるEdge)をメインで運用しつつFirefoxも併用するという使い方をしやすくなってきていると言えます。

ただ、そのために、「IEではこれで問題なく閲覧できるのに、Firefoxでは何故かエラーになる」という状況が顕在化しやすくなっています。組織内のWebサイトにSSL/TLSで接続できるようにするために証明書をグループポリシーで配布した場合に、IE(Edge)では期待通りにそのWebサイトを閲覧できるのにも関わらず、Firefoxでは何故か証明書のエラーになって閲覧できない場合がある、というのもその一つです。

この現象が発生する原因として典型的なのは、配布されている証明書自体に問題があるというものです。

証明書の安全性がFirefoxの求める要件を満たしていない場合

Firefoxをアップデートしたら急に、今まで問題なく閲覧できていたWebサイトで「安全でない接続」というエラーが表示されるようになり、閲覧できなくなってしまった。という場合、これはFirefoxの許容する証明書の安全性の水準が見直された事が原因となっている可能性があります

「この水準の安全性が確保されていれば妥当な証明書として認める」という基準は、IE/EdgeとFirefoxで異なります。また同じFirefoxであっても、バージョンによってその基準が見直される事があります。Firefoxのアップデート直後からSSL/TLSに関するトラブルが急増したという場合には、Firefoxの各バージョンのリリースノートや、開発者向けリリースノートの「セキュリティ」の見出し配下を確認し、問題が起こるようになったバージョンにおいて何らかのアナウンスがなされていないかを確認してみて下さい。

GoogleやYahooなどの一般的なWebサイトを閲覧しようとして「安全でない」というエラーが表示されるようになったというケースも、証明書の安全性が低い事が原因である場合が多いです。これは、以下の例のような「SSLロガー」や「SSLプロキシ」と言われるようなネットワーク機器やサービスを使用している場合によく見られるトラブルです。

この種の機器やサービスは、使用にあたっては原理上必ず、各クライアントに専用の認証局証明書をインポートする必要があります。この認証局証明書がFirefoxの求める安全性の水準を満たさなくなると、SSL/TLSを使用したあらゆるWebサイトで「安全でない接続」の問題が起こるようになります。 (いわゆるSSLプロキシがある場合の通信) 導入手順の一環として流されがちなためか、「証明書をインポートして使っている」という事実を忘れてしまいやすい模様ですので、殊更注意が必要です。

認証局証明書として本来は不正な証明書を、気付かずに使っている場合

証明書の安全性の水準に問題がなくても「安全でない接続」のエラーが発生する場面がもう1つあります。それは、認証局証明書(CA*2証明書、あるいはルート証明書)として使えないはずの証明書をエンタープライズの証明書として配布してしまっている場合です。

具体的には、実験や組織内での一時的な使用のために作られる事の多い、「サーバーの署名用」ではあるが「認証局証明所用」ではない自己署名証明書をエンタープライズの証明書として配布している場合に問題が起こります。実際に、各種の証明書エラーの様子を確認できるbadssl.comというサイトにおいて自己署名証明書を使ったサイトの例で使われている証明書の詳細な情報を見ると、

  • Subject(この証明書の識別子と証明する対象):C=US, ST=California, L=San Francisco, O=BadSSL, CN=*.badssl.com
  • Issuer(この証明書の署名者):C=US, ST=California, L=San Francisco, O=BadSSL, CN=*.badssl.com
  • X509v3 extensions(拡張属性), X509v3 Basic Constraints(基本の制約事項):CA:FALSE(認証局ではない

と、まさにそのような形式になっています。

このような証明書を使っているWebサイトを訪問した場合には、必ず「安全でない接続」のエラーが表示され、内容を閲覧するためにはそのサイトをセキュリティの例外として登録しなくてはなりません。しかしながら一般的に、セキュリティの例外はユーザー個々人が手動で操作して行う必要があります。組織内で多くのユーザーが共通して使用するWebサーバーでこの種の証明書を使っている場合*3、全ユーザーに手動で例外登録をさせるというのは非現実的です。そこで取られる事があるのが、この証明書自体をエンタープライズの証明書として配布するという方法です。

この証明書がエンタープライズの証明書として読み込まれているクライアントPCでは、IE/Edgeで当該Webサイトを訪問すると、

  1. そのWebサイトが送ってくる証明書の署名者を確認する。
  2. エンタープライズの証明書に登録されている証明書(Webサイトが送ってきた証明書と同じ物)が見つかる。
  3. 見つかった証明書が認証局証明書として使われ、Webサイトが送ってきた証明書が正しく認証局証明書によって発行された物であると判定される。

という形で検証され、安全な接続であるとしてWebサイトのコンテンツがそのまま表示される結果になります。

Firefoxでsecurity.enterprise_roots.enabledtrueに設定されていている場合も、当然IE/Edgeと同様の検証が行われて当該Webサイトの内容が警告無しに表示できるはず……と思ってしまう所ですが、実際にはそうはなりません。Firefoxでは、

  1. そのWebサイトが送ってくる証明書の署名者を確認する。
  2. エンタープライズの証明書に登録されている証明書(Webサイトが送ってきた証明書と同じ物)が見つかる。
  3. 見つかった証明書にCA:TRUE(認証局証明書であるというメタ情報)が無いため、認証局証明書ではないと判定される。
  4. 1の証明書の署名者の認証局証明書が見つからないため、証明書の妥当性を検証できず、エラーになる。

という形で検証が行われるため、安全な接続ではないと判定され、Webサイトのコンテンツの代わりにエラーページが表示される結果になります*4

ユーザーが手動操作でFirefoxの証明書マネージャから認証局証明書をインポートする場合、本来であれば、用途が認証局証明書でない証明書は「この証明書は認証局の証明書ではないため、認証局の一覧には追加できません。」というエラーメッセージが出て拒絶されるため、誤って認証局に登録してしまうという事も起こりません。しかしながら、グループポリシー経由でエンタープライズの証明書として配布された物を security.enterprise_roots.enabled の機能でインポートする場合には、このチェックが行われません。また、厄介な事に、そうしてエンタープライズの証明書として一度認識されてしまうと、この証明書を証明書マネージャから手動操作で認証局証明書として再インポートしようとした場合にも、用途のチェックがスキップされてインポートに成功してしまうという性質があります。

この問題がそもそも発生しないようにするためには、以下のどちらかの方法を取るしかありません。

  • サーバーの証明書を再発行する。その時に、認証局証明書としての用途も併せて設定することで、認証局証明書で、且つサーバーの証明書という状態にしておく。
  • 認証局証明書を新たに作成し、その認証局で署名された証明書としてサーバーの証明書を再発行する。エンタープライズの証明書としては、認証局証明書の方を配布する。

一般的な認証局で発行してもらう証明書には適切な用途情報が設定されていますが、組織内でのみ使用するために独自に証明書を発行する場合、この点のチェックがなおざりになりがちです。証明書の用途は適切に使い分けるように気をつけましょう。

まとめ

以上、Active Directoryのグループポリシー機能を使って配布したエンタープライズの証明書をIE/EdgeとFirefoxで共用する場合に起こりがちなトラブルについて解説しました。

当社では、Firefoxの法人での運用における不明点の調査、ご要望に合わせた適切な設定のご案内、本体に含まれない特別な運用を実現するアドオンの開発などを広く承っております。Firefoxの運用でお困りのシステム管理・運用ご担当者さまは、お問い合わせフォームよりお気軽にお問い合わせ下さい。

*1 なお、この機能はActive Directoryを運用していない環境でも、Windowsのレジストリを編集することで動作の検証が可能です

*2 Certificate Authority=認証局

*3 本来であればそのような運用を取るべきではありません。

*4 これはIE/EdgeとFirefoxで証明書の取り扱いのポリシーの厳格さに差があるためで、どちらかが正しくてどちらかが間違っているという事はありません。IE/Edgeは証明書の用途に関する検証がルーズなので、そちらを基準にして運用していると支障が生じる場合がある、という事です。

2018-10-02

db tech showcase Tokyo 2018 - MySQL・PostgreSQLだけで作る高速あいまい全文検索システム #dbts2018

db tech showcase Toyo 2018で話すことを事前に宣伝をしておきたかったけど間に合わなかった須藤です。

関連リンク:

内容

去年は「MySQL・PostgreSQLだけで作る高速でリッチな全文検索システム」というタイトルで話しました。去年はMySQL(Mroonga)・PostgreSQL(PGroonga)で次のことを実現するための具体的なSQLを紹介しました。

  • 全文検索
  • キーワードハイライト
  • 周辺テキスト表示
  • 入力補完
  • 同義語展開
  • 関連文書の表示
  • 構造化データ(オフィス文書・HTML・PDFなど)対応

今年は「MySQL・PostgreSQLだけで作る高速あいまい全文検索システム」というタイトルで話しました。今年も話の流れは同じにしました。あることを実現する具体的なSQLを紹介するというスタイルです。今年はMySQL(Mroonga)・PostgreSQL(PGroonga)で次のことを実現するための具体的なSQLを紹介しました。

  • ヨミガナ検索
  • 同義語展開
  • 電話番号検索
  • ワイン名検索
  • fuzzy検索

今年は「あいまい検索」の実現方法にフォーカスした機能を選びました。同義語展開は去年も紹介したのですが、「あいまい検索」というテーマでは入っていたほうがよさそうだと思ったので入れました。「近傍検索」と「quorumマッチ」は入れられませんでした。

「あいまい検索」というテーマにしたのは今年はこのあたりの機能を強化したからです。「ヨミガナ検索」は今回の発表のために間に合わせました。

まとめ

去年の内容と組み合わせると全文検索まわりのかなりのことをMySQL・PostgreSQLで実現できます。ぜひ、Mroonga・PGroongaを使ってMySQL・PostgreSQLをさらに活用してください。

Mroonga・PGroongaのサポートが必要な方は問い合わせフォームからご相談ください。

タグ: Groonga
2018-09-20

Markdownで書いたテキストをPDFに変換してドキュメントを作成する方法(テンプレート活用編)

はじめに

以前Markdownで書いたテキストをPDFに変換して納品用ドキュメントを作成する方法としてテキストから納品用のPDFを作成する方法を紹介しました。 今回は、前回のやりかたとはちょっと趣向を変えて、既存のテンプレートを活用してドキュメントを作成する方法を紹介します。

体裁を整える

体裁を整えるときの条件を以下のとおりと想定します。

  • 章や節を採番する(-Nを指定する)
  • 注釈を使う(footnotesを指定する)
  • 定義リストを使う(definition_listsを指定する)
  • フォントにはIPAexGothicを指定する(-V CJKmainfont=IPAexGothic
  • 表紙を作成する(-V titlepage=trueを指定する)
  • クロスリファレンスを使う(pandoc-crossrefが必要)
  • 目次を作成する(--table-of-contentsを指定する)
  • 目次のレベルを3にする(--toc-depth=3を指定する)
  • 既存のテンプレートを使う(--template eisvogelを指定する)
  • コードのシンタックスハイライトはtangoを使う(--highlight-style tangoを指定する)

pandocpandoc-crossrefはすでに導入済みであるものとします。

既存のテンプレートを指定するので、以前指定していた自前のクラスファイルmyltjsarticleははずします。 eisvogelというテンプレートは https://github.com/Wandmalfarbe/pandoc-latex-template から入手できます。

上記をまとめると、次のようなコマンドでPDFを作成できます。

pandoc -o sample.pdf \
	       -N \
	       -f markdown+ignore_line_breaks+footnotes+definition_lists \
	       -V CJKmainfont=IPAexGothic \
	       -V titlepage=true \
	       -V toc-own-page=true \
	       -F pandoc-crossref \
	       --table-of-contents \
	       --toc-depth=3 \
	       --pdf-engine=lualatex \
	       --template eisvogel \
	       --highlight-style tango \
	       sample.md

メタ情報を記述するには

ドキュメントにはタイトルやキーワードといったメタ情報を含めることができます。

---
title: ○○仕様書
subject: ○○に関する仕様書
date: 2018/09/12
author: 株式会社××
keywords: [△△, □□]
---

文書の冒頭に上記のようなメタ情報を記述しておくと、eisvogelテンプレートを使ったPDFの表紙として使われます。 表紙に含まれるのはtitleauthordateです。

メタ情報の詳細については、Usageを参照するとよいでしょう。

生成されたサンプルのPDF

実際にサンプルからPDFを生成してみましょう。以下のようなMarkdownのテキストをPDFに変換してみます。

---
title: ○○仕様書
subject: ○○に関する仕様書
date: 2018/09/12
author: 株式会社××
keywords: [△△, □□]
---

# 大項目1タイトル

サンプルのテキスト

## 中項目1タイトル

サンプルのテキスト

### 小項目1タイトル

サンプルのテキスト

![サンプルの図](sample.png){#fig:sample-image}

[@fig:sample-image] はサンプルの図です。

サンプルのテキスト [^footnote]

```ruby
puts "Hello, Ruby"
```

定義リスト

: 定義の説明

[^footnote]: サンプルのテキストの注釈

\appendix
\clearpage

# 付録Aタイトル

サンプルのテキスト

PDFに変換した結果は次のようになります。

生成したPDFの複数ページプレビュー画像

サンプルはシンプル過ぎるように感じるかもしれませんが、テンプレート変数を利用していろいろカスタマイズができます。 詳細については、次のページを参照してください。

pdffonts を使うと、実際に使われているフォントを確認できます。

% pdffonts sample.pdf
name                                 type              encoding         emb sub uni object ID
------------------------------------ ----------------- ---------------- --- --- --- ---------
SXTRUT+IPAexGothic                   CID TrueType      Identity-H       yes yes yes     24  0
OAJEHK+IPAexGothic                   CID TrueType      Identity-H       yes yes yes     25  0
MKLQPT+SourceSansPro-Regular         CID Type 0C       Identity-H       yes yes yes     26  0
CRVRHO+SourceSansPro-Bold            CID Type 0C       Identity-H       yes yes yes     37  0
JSIIWV+SourceCodePro-Regular         CID Type 0C       Identity-H       yes yes yes     51  0

指定したとおりにIPAexGothicが使われていることがわかります。

pdfinfoを使うとPDFファイルの情報を確認できます。

% pdfinfo sample.pdf
Title:          ○○仕様書
Subject:        ○○に関する仕様書
Keywords:       △△, □□
Author:         株式会社××
Creator:        LaTeX with hyperref package
Producer:       LuaTeX-1.7.0
CreationDate:   Wed Sep 12 18:26:42 2018 JST
ModDate:        Wed Sep 12 18:26:42 2018 JST
Tagged:         no
UserProperties: no
Suspects:       no
Form:           none
JavaScript:     no
Pages:          4
Encrypted:      no
Page size:      595.276 x 841.89 pts (A4)
Page rot:       0
File size:      236561 bytes
Optimized:      no
PDF version:    1.5

メタ情報として記述していたキーワードなども反映されていることがわかります。

まとめ

今回は、Pandocを使ってMarkdownで書いたテキストをPDFに変換してドキュメントを生成する方法(既存のテンプレートを使う)を紹介しました。 PDFの生成には少し時間がかかりますが、綺麗に出力できるので特にレイアウトに制限のないようなドキュメントをテキストエディタで書きたい人は検討してみてはいかがでしょうか。

2018-09-13

fcitx-mozcの入力モードを外部プロセスから制御する

はじめに

何年か前に、とある案件で「fcitx-mozcの入力モードを自分のアプリケーションから制御したい」というご要望をお客様から承りました。そのときに、fcitx-dbus-statusというFcitx用アドオンを実装して、このご要望にお応えしました。

今回はこのアドオンについて紹介します。

fcitx-dbus-statusとは

fcitx-dbus-statusはFcitxで動作している入力メソッドのステータスをD-Bus経由で取得したり、変更したりできるようにするためのFcitxアドオンです。dbus-sendコマンドやdbus-monitorコマンドを使えば、シェルスクリプトで入力メソッドのステータスを制御したり監視したりすることもできます。

主にMozcを想定して開発しましたが、他の入力メソッドでも使用することができます。

インストール方法

おそらくどのディストリビューションでもfcitx-dbus-statusのパッケージは用意されていないでしょうから、自分でビルドしてインストールする必要があります。

例えばUbuntu 18.04では以下のような手順でインストールすることができます。

$ sudo apt install g++ cmake fcitx-libs-dev libdbus-1-dev fcitx-module-dbus
$ git clone git@github.com:clear-code/fcitx-dbus-status.git
$ cd fcitx-dbus-status
$ mkdir build
$ cd build
$ cmake ..
$ make
$ sudo make install

インストールが完了したら、アドオンが確実に読み込まれるようにシステムを再起動した方が良いでしょう。

使用方法

scriptsディレクトリ以下にMozc用のサンプルスクリプトがあります。

たとえばMozcの入力モードを全角カタカナに変更したい場合には以下のコマンドを実行します。

$ ./scripts/set-mozc-composition-mode.sh katakana

現在のモードを取得したい場合は以下のコマンドを実行します。

$ ./scripts/get-mozc-composition-mode.sh

モードの変更をリアルタイムで検知したい場合は以下のコマンドを実行します。

$ ./scripts/monitor-status.sh

まとめ

fcitx-dbus-statusについて紹介しました。なお、ステータス変更のリアルタイム検知を実現するにはFcitx側の変更も必要だったので、事前にFcitxのメーリングリストで実装方針を作者の方に相談した上で、パッチを提案しています。この修正は既に取り込まれており、最近のLinuxディストリビューションではこの変更が含まれたバージョンを利用できるようです。

2018-09-06

Apache Arrowの最新情報(2018年9月版)

Apache ArrowPMC(Project Management Commitee、プロジェクト管理チームみたいな感じ)のメンバーの須藤です。

みなさんはApache Arrowを知っていますか?聞いたことがないとか名前は聞いたことがあるとかコンセプトは知っているあたりがほとんどで、触っている人はあまりいないのではないでしょうか。Apache Arrowは数年後にはデータ処理界隈で重要なコンポーネントになっているだろうプロジェクトです。データ処理界隈に興味がある人は知っておくと役に立つはずなので2018年9月現在の最新情報を紹介します。

私は、PMCの中では唯一の日本人で、コミット数は3番目に多いので、日本ではApache Arrowのことをだいぶ知っている方なはずです。日本語でのApache Arrowの情報があまりないので日本語で紹介します。ちなみに、英語ではいろいろ情報があります。有用な情報源はApache Arrowの公式ブログ公式メーリングリストやそれぞれの開発者のブログ・発表などです。この記事ではそれらの情報へのリンクも示しながら最新情報を紹介するので、ぜひ英語の情報も活用してください。

Apache Arrowが実現すること

Apache Arrowが目指していることはメモリー上でのデータ処理を効率化することです。

「効率化」には2つの観点があります。1つは「速度」です。速いほど効率的ということです。もう1つは「実装コスト」です。実装コストが低いほど効率的ということです。

速度

まず「速度」の方から説明します。

Apache Arrowは大量のデータを処理するケースを想定しています。そのため、速度のことを考えるときも大量のデータを処理する前提で考えます。

大量のデータを処理するときにネックになる部分を解消し、高速化できる部分を最適化していくことで速く処理できるようになります。

Apache Arrowが注目したネックになる部分はデータ交換の部分です。

Apache Arrowが注目した高速化できる部分は並列処理の部分です。

順番に説明します。

データ交換

ここではネックになる部分の解消について説明します。対象となる処理はデータ交換処理です。

大量のデータを扱う時は複数のシステムが連携してデータを処理します。たとえば、データを集めるシステム・データを永続化するシステム・データを前処理するシステム・データを集計するシステムなどです。それぞれのシステム間でデータを交換する必要がありますが、そのときのデータのシリアライズ・デシリアライズ処理にかかるコストが結構かかるのです。たとえば、JSONでデータ交換するとした場合、データをJSONにフォーマットするコストとJSONをパースしてプログラムで扱える形式にするコストが結構かかるということです。

そこで、Apache Arrowは各種システムで共通で使えるシリアライズ・デシリアライズコストがすごく低いデータフォーマットの仕様(Apache Arrowフォーマット)を作っています。基本的なアイディアは「パースしない」です。プログラムから直接扱えるフォーマットでデータを配置することでパースせずにそのままデータを扱えます。これはデータをシリアライズするときも効率的です。プログラムで扱っているデータをそのまま書き出せばよいのでシリアライズコストがほとんどありません。なお、パースしないデータフォーマットの実現にはFlatBuffersを使っています。

Apache Arrowフォーマットで一番うまみがでるのは、各システムすべてがApache Arrowフォーマットに対応しているときです。Apache Arrowフォーマットに対応していないシステムがあるとそのシステムとのデータ交換時にApache Arrowフォーマットを使えないからです。つまり、そのシステムとのデータ交換コストが高くなります。

データ交換コストを下げることによる速度向上の実現にはApache Arrowフォーマットを広く使える状況になっていなければいけません。そのため、より多くの言語でApache Arrowフォーマットを扱えるように開発を進めています。現在Apache Arrowフォーマットを読み書きできる言語は次の通り(アルファベット順)です。

  • C
  • C++
  • Go
  • Java
  • JavaScript
  • Lua
  • MATLAB
  • Python
  • R
  • Ruby
  • Rust

完成度が高いのはC++とJavaです。C++のバインディングとして実装されているC、Lua、Python、Rubyも完成度が高いです。MATLABとRもC++のバインディングですがまだ開発が始まったばかりで完成度はそれほどではありません。JavaScriptとGoとRustはバインディングではなくその言語で1から実装しています。これらの言語の実装は、主な型(たとえば数値型)はサポートしているがまだサポートしていない型もある、という状況です。Juliaの実装も進んでいます。各種言語での実装状況の詳細は後述します。

並列処理

ここでは高速化できる部分の最適化について説明します。対象となる処理は並列処理です。

大量のデータを高速に処理するには次の両方またはどちらかを実現します。

  • 1つのデータをより速く処理する
  • 同時に複数のデータを処理する(並列処理)

Apache Arrowのデータフォーマットはできるだけデータを局所化する配置になっています。局所化するとCPUのキャッシュメモリーを活用しやすくなり「1つのデータをより速く処理する」ことにつながります。局所化した上でデータの境界を64bitに揃えるとSIMDを有効活用しやすくなります。SIMDを活用できると1回の命令で複数のデータを処理できるため「同時に複数のデータを処理する」ことにつながります。

Apache ArrowのデータはOLTP(OnLine Transaction Processing)(ECサイトで購買するような処理)よりもOLAP(OnLine Analytical Processing)(データから探索的に知見を探し出すような処理)で性能がでるように設計されています。OLTPとOLAPでは処理の内容が違うので、データの配置の仕方で実現しやすさが変わってきます。RDBMSを知っている人向けな説明をすると、OLTPでは行単位の処理(行の追加・削除など)が多くて、OLAPでは列単位の処理(COUNTGROUP BYなど)が多いです。OLTPでは行単位でデータが固まっていた方が性能がでやすく、OLAPでは列単位でデータが固まっていた方が性能がでやすいです。Apache ArrowはOLAP向けの設計なので列単位でデータをまとめています。このようなデータの持ち方を「カラムナー」と言います。

「速度」のまとめ

Apache Arrowのデータフォーマットは速く大量のデータを処理するために次のことをうまくできるような仕様になっています。

  • データ交換
  • 並列処理

データ交換のためにシリアライズ・デシリアライズコストが小さくなるように設計されています。また、より多くの言語で読み書きできるようにライブラリーの整備を進めています。

並列処理のためにデータの配置方法を工夫しています。最近のCPU・GPUで高速に処理できる配置になっています。

実装コスト

Apache Arrowはメモリー上でのデータ処理を効率化することを目指しています。「効率化」のために「速度」と「実装コスト」に着目しています。ここまでで「速度」について説明したので次は「実装コスト」について説明します。

メモリー上でのデータ処理するソフトウェアはたくさんあります。速度が重要なソフトウェアも多いです。速度が必要なソフトウェアでは速くするためにいろいろな工夫を実装します。これにはそれなりの時間がかかります。

Apache Arrowは高速に大量のデータをメモリー上で処理するときに必要な機能をライブラリーとして提供しようとしています。Apache Arrowをインフラとして使うことで同じような工夫をしなくて済むようにしたい、多くのソフトウェアで重要な部分は協力してみんなでよいものを開発して共有したい、ということです。

最近では次のような処理をApache Arrowに取り込む動きがあります。

なんでもかんでも取り込むとメンテナンスしにくくなって開発が停滞する危険性もあるのですが、そのあたりはバランスを取りながら判断していくはずです。

Apache Arrowが実現することのまとめ

Apache Arrowは効率的に大量のデータをメモリー上で処理することを目指しています。そのためにしていることは次の通りです。

  1. データ交換・高速処理しやすいApache Arrowフォーマットの仕様を定義
  2. 各種言語用のApache Arrowフォーマットを読み書きするライブラリーを開発
  3. 大量のメモリー上のデータを高速処理するためライブラリーを開発

Apache Arrowの位置付けの説明はこのくらいにして、どのような用途に向いているか、現状はどうなっているか、今後どうなっていくかについて説明します。

Apache Arrowが向いている用途

Apache Arrowはどんな用途にでも向いているわけではありません。得意なこと不得意なことがあります。用途にあわせて使い分けましょう。

Apache Arrowは複数のシステムが協調して大量のデータを処理するユースケース向けに設計されています。そのため、次の用途に向いています。

  • 大量データの交換
  • メモリー上での大量データの分析処理

シリアライズ・デシリアライズコストの低いデータフォーマットな点が「大量データの交換」に向いています。

高速処理に向いたデータ配置・高速な処理をライブラリーで提供している点が「メモリー上での大量データの分析処理」に向いています。

もし、ファイルサイズが気にならないなら「処理結果の一時的なキャッシュ」にも有用です。ファイルサイズが気になるならApache Parquetフォーマットの方が向いています。

一方、永続化には向いていません。たとえば、ログをApache Arrowフォーマットで保存するというのは向いていませんし、データベースのデータをApache Arrowフォーマットで保存するというのにも向いていません。

永続化用のデータフォーマットではサイズが要重なポイントになるケースが多いです。サイズが小さい方がたくさんのデータを保存できますし、データを読み書きするときのI/Oが減るからです。Apache Arrowはシリアライズ・デシリアライズコストを低くすることを重視しているので、サイズを小さくすることに関してはそれほどがんばれません。たとえば、Zstandardなどで圧縮するとApache Arrowのメリットの一部が薄れてしまいます。そのメリットとは「ゼロコピー」です。

ゼロコピーはデータをコピーしないことです。Apache Arrowフォーマットではデータをコピーせずにそのまま扱えるように設計されています。具体的には次のように設計されています。

  • データをプログラムから直接効率よく扱える配置にしている
  • データは基本的にリードオンリーで扱う

プログラムから直接効率良く扱える配置にしてあるとそのままデータを扱えます。システムが提供するメモリーマップ機能を使えばゼロコピーでデータを扱えます。しかし、直接扱えるデータでも圧縮すると伸張しなければ使えません。つまり、「直接効率よく扱える配置」ではなくなってしまいます。そのため、新しい領域を用意して伸張したデータを格納する必要があります。コピーのような挙動です。大量データを処理する想定なのでこのような伸張するコストが無視できないことも多いです。ただ、ネットワーク越しにデータ交換する時のようにネットワーク上のデータ送受信コストの方が圧縮・伸張コストよりも高いことがあります。このような場合は圧縮した方が割に合うので、用途によって使い分けます。(まだ圧縮機能はサポートされていませんが、ZstandardやLZ4を扱うための部分は実装済みです。ARROW-300 [Format] Add buffer compression option to IPC file format参照。)

リードオンリーとゼロコピーについても説明します。データが変更される可能性があると、安全にデータを処理するためにはデータをコピーするか排他制御をして扱わないといけません。大量データを処理する想定なのでコピーするコストは無視できません。排他制御をすると並列度が下がりがちで速度が犠牲になります。そのため、Apache Arrowはリードオンリーでデータを扱う設計になっています。

このようにApache Arrowはゼロコピーを大事にした実装になっています。Apache Arrowではゼロコピーはよくでてくるキーワードなので覚えておいてください。

参考:

Apache Arrowが向いている用途は次の用途だということを説明しました。

  • 大量データの交換
  • メモリー上での大量データの分析処理

つづいて、現状はどうなっているか、今後どうなっていくかについて説明します。

Apache Arrowの現状

2018年9月現在、Apache Arrowのデータフォーマットの仕様と実装がどのようになっているかを説明します。

データフォーマットの仕様は今年中には固める方向で進んでいます。仕様が固まったらApache Arrow 1.0がリリースされる予定です。現時点での最新バージョンは0.10.0です。9月中に0.11.0がリリースされる予定です。

実装は、データ交換のための実装(Apache Arrowフォーマットを扱うための実装)がだいたい完成してきていて、Apache Arrowフォーマットのデータを高速に処理するための実装に比重が移ってきています。

それでは詳細を説明していきます。

扱えるデータ

まずはApache Arrowが扱えるデータを説明します。

現在Apache Arrowが扱えるデータは次の通りです。

  • データフレーム
  • 密な多次元配列

それぞれ説明します。

データフレーム

Apache Arrowはもともと「データフレーム」なデータを扱うために開発が始まりました。「データフレーム」は表形式のデータです。RDBMSのテーブルのようなものです。1つのテーブルには1つ以上のカラムがあり、それぞれのカラムは違う型にできます。

現在、Apache Arrowがサポートしている型は次の通りです。これらの型のデータを扱えるということです。

  • 真偽値(1bit)
  • 整数
    • 8bit非負整数(リトルエンディアン)
    • 8bit整数(リトルエンディアン)
    • 16bit非負整数(リトルエンディアン)
    • 16bit整数(リトルエンディアン)
    • 32bit非負整数(リトルエンディアン)
    • 32bit整数(リトルエンディアン)
    • 64bit非負整数(リトルエンディアン)
    • 64bit整数(リトルエンディアン)
  • 浮動小数点数
    • 16bit浮動小数点数
    • 32bit浮動小数点数
    • 64bit浮動小数点数
  • 小数(精度・スケールを指定)
  • 可変長UTF-8文字列
  • バイナリーデータ
    • 可変長バイナリーデータ
    • 固定長バイナリーデータ
  • 日付
    • UNIXエポックからの経過日数(32bit)
    • UNIXエポックからの経過ミリ秒数(64bit)
  • タイムスタンプ(64bit整数)
    • UNIXエポックからの経過秒数
    • UNIXエポックからの経過ミリ秒数
    • UNIXエポックからの経過マイクロ秒数
    • UNIXエポックからの経過ナノ秒数
  • 時刻
    • 深夜0時からの経過秒数(32bit整数)
    • 深夜0時からの経過ミリ秒数(32bit整数)
    • 深夜0時からの経過マイクロ秒数(64bit整数)
    • 深夜0時からの経過ナノ秒数(64bit整数)
  • リスト(0個以上の同じ型の値を持つ型)
  • 構造体(1個以上のフィールドを持つ型で、各フィールドは別の型にできる)
  • 共用体(1個以上のフィールドを持つ型で、各フィールドは別の型にでき、どれか1つのフィールドの値のみが設定されている)
  • 辞書
    • 統計っぽい説明:名義尺度なカテゴリーデータ
    • 実装よりの説明:各値に整数でIDを割り当て、数値で値を表現する型
    • scikit-learnを知っている人向けの説明:sklearn.preprocessing.LabelEncodertransform結果を値として使う型

なお、どの型でもnullを設定できます。

多次元配列

Apache Arrowはデータフレームを想定して開発が始まりましたが、多次元配列も扱えるとよさそうというフィードバックがありApache Arrow 0.3.0から多次元配列をサポートしました。

参考:ARROW-550 [Format] Add a TensorMessage type

データフレームと多次元配列の違いは次の通りです。

  • 次元数:
    • データフレームは2次元
    • 多次元配列はn次元
  • 型:
    • データフレームはカラム毎に違う型を使える
    • 多次元配列の要素の値はすべて同じ型

Pythonでいうと、pandasが実現しているのがデータフレームで、NumPyが実現しているのが多次元配列です。

多次元配列には密(dense)な多次元配列と疎(sparse)な多次元配列があります。密な多次元配列は要素の値をすべて持っている多次元配列で、疎な多次元配列は0以外の要素の値だけを持っている多次元配列です。

たとえば、[0, 0, 0, 1, 0]という多次元配列(1次元配列)があった場合、5要素全部の値を保持しているのが密な多次元配列で、「4番目の要素の値が1でそれ以外は0」という情報だけ保持しているのが疎な多次元配列です。

Apache Arrowは現在は密な多次元配列のみサポートしています。疎な多次元配列もサポートする予定があります。現在は妥当なデータフォーマットの仕様を検討する人を募集している状態です。疎な多次元配列の知見がある人はぜひ取り組んでみてください。

参考:ARROW-854 [C++] Support sparse tensor

データ処理部分の実装

Apache Arrowは各種データ処理ツールが共通で使える高速なデータ処理機能の開発も重視しています。これまでデータフォーマットの方に注力していたためまだあまり進んでいませんが、現時点できることを紹介します。

データ処理部分の実装に着手しているのはC++、JavaScript、Goでの実装だけなので、それぞれの言語での実装ごとに紹介します。

なお、データ処理部分が実装されていない言語あるいは実装が始まっている言語でも未実装の処理はできないかというとそうでもありません。読み込んだApache Arrowフォーマットのデータを他の既存のライブラリーで扱えるように変換することで処理できます。コピーやデータの変換が必要になるので多少非効率ですが、Apache Arrowフォーマットによりデータ交換部分のコストがすごく効率化されるので割に合うことが多いです。

たとえば、Apache Sparkではデータ交換部分でApache Arrowを使うことにより大きく速度向上しています。Apache Sparkには処理の一部をPythonで実装できるPySparkという機能があります。Apache Spark本体はScalaで実装されているので別プロセスで動いているPythonにデータを渡す必要があります。このときにソケット経由でApache Arrowデータを渡しています。従来はPythonに標準で含まれているpickleを使ってデータを転送していました。Apache Arrowフォーマットで受け取ったデータはpandasのオブジェクトに変換し、データ処理はpandasの機能を使います。ちなみに、Apache Arrowのデータからpandasのオブジェクトへの変換は、型によってはゼロコピーで実現できます。この場合はさらに効率的にデータ交換できます。

参考:Speeding up PySpark with Apache Arrow

それでは現時点で実装されているデータ処理機能を各言語毎に紹介します。

C++

C++で実装されている処理は次の通りです。

  • キャスト(型の変換)
    • 例:16bit整数を32bit整数へ変換
    • 辞書型への変換も実装済み
  • 要素ごとの論理否定
  • 要素ごとの論理積
  • 要素ごとの論理和
  • 要素ごとの排他的論理和

実装が予定されている処理のリストはApache Arrowのプロジェクト管理ツールであるJIRAを「kernel」で検索してみてください。実装してみたい処理があったらぜひ取り組んでみてください。

JavaScript

JavaScriptで実装されている処理は次の通りです。

  • 要素ごとの論理否定
  • 要素ごとの論理積
  • 要素ごとの論理和
  • 要素ごとの排他的論理和
  • 要素ごとの==比較
  • 要素ごとの<比較
  • 要素ごとの<=比較
  • 要素ごとの>比較
  • 要素ごとの>=比較
  • 値ごとの出現数のカウント
    • 辞書型のみサポート
Go

Goで実装されている処理は次の通りです。

  • 全要素の合計値の計算

まだ1つしか実装されていないんですが、SIMDを活用するために面白い実装になっているので少し説明します。

SIMDを使うためにはCPUがSIMDをサポートしていないと使えません。最近のC/C++コンパイラーはCPUがSIMDをサポートしているかどうかで自動的にSIMDを使ったコードを生成できるくらい賢くなっています。しかし、Goのコンパイラーはまだその最適化はできません。そこで、事前にClangを使ってSIMD対応のアセンブリを出力してそれをGoに組み込んでいます。ただ、この方法だとCPUが対応していないSIMDもGoに組み込まれています。そこで、使えるSIMDを実行時に検出して適切なSIMD実装の処理を呼び出すようにしています。詳細は以下の参考URLを参照してください。Go実装を書いたInfluxDataの人が書いたブログです。

参考:InfluxData Working on Go Implementation of Apache Arrow | InfluxData

ちなみに、InfluxDataは時系列データベースInfluxDBを開発している会社です。InfluxDBはGoで実装されているのでInfluxDBで活用するためにApache Arrowの開発に参加しています。

データ処理部分の実装のまとめ

データ処理部分はあまり実装されていなくてがっかりしたかもしれません。少し将来の話もしておきます。

Apache Arrowでは各要素の処理だけでなく、複雑な条件を高速に処理する実行エンジンの実装も視野に入れています。その実装として使えそうなモジュールが近いうちにApache Arrowに入る予定です。

このモジュールにはGandivaという名前がついています。Gandivaはどのように処理を実行すればよいかを実行時に計算して、LLVMを使って実行時にコンパイルしてから実行します。コンパイルしているので事前にコンパイルしたときと同じくらい高速になりますし、LLVMはSIMD用の最適化機能もあるので、SIMD対応の処理にもなります。詳細はGandivaの開発者が書いた以下のブログを参考にしてください。

参考:Introducing the Gandiva Initiative for Apache Arrow - Dremio

「近いうちにApache Arrowに入る予定」で今はどんなステータスなのかについても触れておきます。

外部のモジュールがApache Arrowに取り込まれるには次のステップを踏む必要があります。

  1. PMC(プロジェクト管理チームみたいなやつ)が承認する
  2. IPクリアランス(知的財産侵害を心配しなくても使えることを確認する)
  3. 取り込み

Gandivaは今は「PMCが承認する」を完了したステータスです。

参考:[RESULT] [VOTE] Accept donation of Gandiva to Apache Arrow

次はIPクリアランスなのですが、そのためにはApache Arrowのリポジトリーにpull requestを作る必要があるのですが、まだそれはできていません。

0.11.0のリリースが今月中の予定なので、それには間に合わなそうですが、数ヶ月以内には入るんじゃないかと私は思っています。

Plasma

Apache Arrowには同一マシン内でオブジェクトを共有する機能もあります。それがPlasmaという名前のモジュールです。Apache Arrowにはデータフレームなデータを低いシリアライズ・デシリアライズコストで交換するための仕組みがあります。それを活用して実装しています。Plasmaはデータフレームなデータを交換するための機能ではなく、メモリー上の生データを交換するためのもっと低レベルな機能であることに注意してください。

PlasmaはもともとRayの一部として開発していた機能ですが、広く有用そうだということでApache Arrowに移動しました。Rayは大規模な機械学習・強化学習用の高速な分散処理フレームワークです。カリフォルニア大学バークレイ校のRISELabが開発しています。

Plasmaを使うとCPU上のデータもGPU上のデータもゼロコピーで共有できます。Plasmaでデータを共有することで複数プロセスで分散処理できます。

Plasmaを使うためにはサーバープロセスを起動する必要があります。各プロセスがサーバープロセスに接続して、共有したいデータを置いたり、共有されているデータを参照したりして処理を進めます。

参考:Plasma In-Memory Object Store

各言語での実装の完成度

次は各言語での実装の完成度を説明します。

Apache Arrowフォーマットはより多くの環境で使えるほどメリットが大きくなります。そのため、Apache Arrowフォーマットを使える言語を増やすべく開発しています。現在は次の言語で使えます(再掲)が、その完成度は様々です。

  • C
  • C++
  • Go
  • Java
  • JavaScript
  • Lua
  • MATLAB
  • Python
  • R
  • Ruby
  • Rust

特に完成度が高いのがJava、C++、Python、C、Ruby実装です。

最初にJava、C++実装から開発が始まっているのでこれらの言語の実装は完成度が高いのです。Python、C、Ruby実装も完成度が高いのはこれらはC++実装のバインディング(C++実装の機能を他の言語でも使えるようにしている)からです。Lua実装も(間接的に)C++実装のバインディングなのですが、APIの使い勝手という面で少し完成度が落ちます。

MATLAB、R実装もC++実装のバインディングですが、まだ開発が始まったばかりで完成度はそれほどではありません。

Go、JavaScript、Rust実装は1から実装しているのでまだJava、C++実装ほどの完成度にはなっていません。ただ、対応しているデータフレームの型に関してはJavaScript実装の完成度は高いです。

まだApache Arrowのリポジトリーには入っていませんが、Juliaの実装も進んでいます。Julia実装は1から実装しています。

それではアルファベット順に各言語の実装の概要と完成度を紹介します。

C

C実装はC++実装のバインディングです。これは私が作りはじめたモジュールです。今は主に私と@shiro615が開発しています。

GLibというC言語用の便利ライブラリーを使っているのが特徴です。GLibを使った理由は「各種言語のバインディングを自動生成できる」からです。念のため補足しますが、「C++実装のバインディングを自動生成できる」のではなく、「C++実装のバインディングであるC実装のバインディングを自動生成できる」です。

この機能を使ってRubyとLuaのバインディングが動いています。GLibにはC言語でオブジェクト指向プログラミングをするためのライブラリーGObjectも含まれています。GObjectを使ってオブジェクト指向なAPIを実現しています。そのため、自動生成されたRuby・Lua用のバインディングもオブジェクト指向なAPIになっていて使いやすいです。通常、Cのライブラリーのバインディングを自動生成するアプローチは生のCの関数をそのまま使えるようなAPIになります。そうすると使いやすくするために手動で使いやすいAPIをラップするレイヤーを実装する必要があるのですが、C実装で使っている自動生成機能ではその手間は必要ありません。

なお、C実装で使っている自動生成する仕組みはGObject Introspectionです。

C実装ではPlasma以外のすべてのC++実装の機能を使えます。近いうちにPlasmaも使えるようになる予定です。

C++

C++実装は1から開発しています。主に@wesm@xhochy@pitrou@pcmoritz@cpcloudが開発しています。

C++実装はC++11を使っています。そのため、CentOS 6に標準で入っている古いg++ではビルドできません。CentOS 6でビルドしたい時はdevtoolset-6あるいはdevtoolset-7パッケージをインストールして新しいg++を用意します。

C++実装はデータフレームのすべての型も多次元配列もサポートしています。

PlasmaのサーバープロセスはC++で実装されています。もちろん、Plasmaクライアント機能も実装されています。

C++実装には他のフォーマットとの相互変換機能がいくつか含まれています。現時点で実装されているフォーマットは次の通りです。

  • Feather
    • PythonとR間でデータ交換するためのフォーマット。
    • Apache Arrowで置き換えるため現在は非推奨。
  • Apache ORC
    • Hadoop用のカラムナーなフォーマット。
    • 現時点では読み込みのみ。簡単な書き込み機能はサポート予定。
  • Apache Parquet
    • Hadoop用のカラムナーなフォーマット。
    • Apache ArrowのC++実装ではなく、Apache ParquetのC++実装の方に実装されている。

メモリー上ではなくGPU上にデータを置く機能もあります。まだあまり活用されていない機能ですが、今後より活用されていくはずです。というのは、Apache ArrowフォーマットのデータをGPU上で処理するライブラリーが開発されているからです。

たとえばlibgdfというライブラリーです。libgdfはC言語用のデータフレームライブラリーです。CUDAを使って実装しています。値の比較処理だけでなくにグループ化機能やジョイン機能といった高度な機能も実装されています。

libgdfはGoAi (GPU Open Analytics Initiative)という団体が開発しています。この団体のミッションは「GPUを活用してデータ分析できるプラットフォームを構築する」です。

他にもMapDというGPU上で動くデータベースがApache Arrowフォーマットに対応しています。MapDではSQLを使ってデータを処理できます。MapDを開発している会社もGoAiに参加しています。

Python連携用のモジュールもC++実装に含まれています。Python連携用のモジュールではC++のApache ArrowオブジェクトとPythonのpandas・NumPyオブジェクトを相互変換する機能を提供しています。Pythonバインディングの方に入れずにC++実装の方に入れているのはいろいろなライブラリーでこの機能を共有したいからです。当初の想定の1つのpandas 2.0ではまだ使っていませんがすでに活用している例があります。それはRed Arrow PyCallという同一プロセス内でRubyとPythonを両方動かしてRubyとPython間でApache Arrowデータを共有するためのライブラリーです。Apache ArrowデータとPythonオブジェクトを変換するためにPython連携用のモジュールを使っています。

参考:ARROW-341 [Python] Making libpyarrow available to third parties

2018年9月にApache ParquetのC++実装がApache Arrowのリポジトリーに移動しました。理由は現時点では両者がすごく密に連携をして開発を進めているので同じリポジトリーにあった方が開発を進めやすいからです。将来的に、APIが安定したらまた分離するかもしれません。

参考:

Go

Go実装は1から開発しています。InfluxDataの人たちが開発しました。Apache Arrowのリポジトリーに入ってからは@sbinet(InfluxDataの人ではない)が開発しています。

対応しているデータフレームの型は次の通りです。

  • 真偽値(1bit)
  • 整数
    • 8bit非負整数(リトルエンディアン)
    • 8bit整数(リトルエンディアン)
    • 16bit非負整数(リトルエンディアン)
    • 16bit整数(リトルエンディアン)
    • 32bit非負整数(リトルエンディアン)
    • 32bit整数(リトルエンディアン)
    • 64bit非負整数(リトルエンディアン)
    • 64bit整数(リトルエンディアン)
  • 浮動小数点数
    • 32bit浮動小数点数
    • 64bit浮動小数点数
  • バイナリーデータ
    • 可変長バイナリーデータ
  • タイムスタンプ(64bit整数)
    • UNIXエポックからの経過秒数
    • UNIXエポックからの経過ミリ秒数
    • UNIXエポックからの経過マイクロ秒数
    • UNIXエポックからの経過ナノ秒数
  • リスト(0個以上の同じ型の値を持つ型)
  • 構造体(1個以上のフィールドを持つ型で、各フィールドは別の型にできる)

多次元配列はまだサポートしていません。

Plasmaクライアント機能もまだサポートしていません。

他のフォーマットとの相互変換機能もまだサポートしていません。

Java

Java実装は1から開発しています。主に@julienledem@BryanCutler@StevenMPhillips@siddharthteotia@elahrvivaz@icexellossが開発しています。

Java実装はデータフレームの型をほとんどサポートしています。サポートしていないのは次の型だけです。

  • 浮動小数点数
    • 16bit浮動小数点数

多次元配列はまだサポートしていません。

Plasmaクライアント機能をサポートしています。

JDBCで取得したデータをApache Arrowオブジェクトで返す機能が実装されているので、簡単にRDBMSのデータをApache Arrowオブジェクトとして扱えます。

JavaScript

JavaScript実装は1から開発しています。主に@trxcllnt@TheNeuralBitが開発しています。

TypeScriptで開発しています。Webブラウザー上でもNode.js上でも動きます。

MapDで処理した結果をWebブラウザー上でビジュアライズするために活用できます。MapDはApache Arrowに対応しているのでWebブラウザー上のJavaScriptと低いコストでデータ交換できるのです。

参考:Supercharging Visualization with Apache Arrow

JavaScript実装はデータフレームのすべての型に対応しています。

多次元配列はまだサポートしていません。

Plasmaクライアント機能もまだサポートしていません。node-plasmaというNode.js用モジュールを作ってサポートしたいという構想があります。

参考:Connecting JS to modern GPU and ML frameworks: Update from Nvidia GTC 2018

Julia

公式のJulia実装はまだありませんが、@ExpandingManArrow.jlの実装を進めています。公式実装にするための議論も進んでいます。

参考:collaboration with Apache Arrow org · Issue #28 · ExpandingMan/Arrow.jl

Julia実装が対応しているデータフレームの型は次の通りです。

  • 真偽値(1bit)
  • 整数
    • 8bit非負整数(リトルエンディアン)
    • 8bit整数(リトルエンディアン)
    • 16bit非負整数(リトルエンディアン)
    • 16bit整数(リトルエンディアン)
    • 32bit非負整数(リトルエンディアン)
    • 32bit整数(リトルエンディアン)
    • 64bit非負整数(リトルエンディアン)
    • 64bit整数(リトルエンディアン)
  • 浮動小数点数
    • 32bit浮動小数点数
    • 64bit浮動小数点数
  • 可変長UTF-8文字列
  • 日付
    • UNIXエポックからの経過日数(32bit)
  • タイムスタンプ(64bit整数)
    • UNIXエポックからの経過秒数
    • UNIXエポックからの経過ミリ秒数
    • UNIXエポックからの経過マイクロ秒数
    • UNIXエポックからの経過ナノ秒数
  • 時刻
    • 深夜0時からの経過秒数(32bit整数)
    • 深夜0時からの経過ミリ秒数(32bit整数)
    • 深夜0時からの経過マイクロ秒数(64bit整数)
    • 深夜0時からの経過ナノ秒数(64bit整数)
  • リスト(0個以上の同じ型の値を持つ型)
  • 辞書
    • 統計っぽい説明:名義尺度なカテゴリーデータ
    • 実装よりの説明:各値に整数でIDを割り当て、数値で値を表現する型
    • scikit-learnを知っている人向けの説明:sklearn.preprocessing.LabelEncodertransform結果を値として使う型

多次元配列はまだサポートしていません。

Plasmaクライアント機能もまだサポートしていません。

Lua

Lua実装はC実装とlgiを使って自動生成しています。特にパッケージにはしていなくて、以下のようにすれば使えるという状態です。

local lgi = require 'lgi'
local Arrow = lgi.Arrow

参考:arrow/c_glib/example/lua at master · apache/arrow

C実装で使える機能はすべて使えます。

MATLAB

MATLAB実装はC++実装のバインディングです。2018年9月10日現在ではまだ1コミットだけですが、主に開発をしている人は@kevingurneyです。MATLABを開発しているMathWorksの人です。

現在はFeatherフォーマットのデータを読み込む機能だけがあります。Apache Arrow形式のデータはまだ読めません。

Python

Python実装はC++実装のバインディングです。主に開発をしている人たちはC++実装を開発している人たちとだいたい同じです。

C++実装で使える機能はすべて使えます。単に使えるだけではなく、Pythonからより便利に使えるようにAPIが整備されています。

Cythonを使って実装されています。

pandas・NumPyのオブジェクトに簡単に相互変換できる機能も実装されていて、既存のライブラリーとシームレスに使えるようになっています。

R

R実装はC++実装のバインディングです。2018年9月10日現在ではまだ1コミットだけですが、主に開発をしている人は@romainfrancoisです。↓の最初のpull requestにR関係の人が何人かコメントしたりJIRAに新しくissueを作っている人もいるので、今後開発に参加する人は増えていきそうな気配がします。

参考:ARROW-1325: [R] Initial R package that builds against the arrow C++ library by romainfrancois · Pull Request #2489 · apache/arrow

現在はC++実装の簡単に機能を使えるだけでApache Arrowフォーマットのデータを読めたりはしません。ただ、開発環境の整備は進んでいます。すでにTravis CIでテストできるようになっています。

R実装の進め方はGoogle Docsで議論しています。ある程度まとまったらJIRAのチケットにブレイクダウンされる予定です。

参考:Apache Arrow for R - Initial Roadmap - Google Docs

Ruby

Ruby実装はC実装とgobject-introspection gemを使って自動生成しています。主に開発している人たちはC実装を開発している人たちです。

C実装で使える機能はすべて使えます。単にバインディングを自動生成しているだけではなく、Python実装のように、より便利に使うためのAPIも整備してあります。

既存のライブラリーとシームレスに使えるようにするための機能もあります。依存関係を増やしたくないのでRuby実装自体には入っていないのですが、関連ライブラリーとして次のライブラリーがあります。

また、Apache Parquetフォーマットのデータを読み込むためのRed Parquetもあります。

Rust

Rust実装は1から開発しています。主に@andygroveが開発しています。

Rust実装が対応しているデータフレームの型は次の通りです。

  • 真偽値(1bit)
  • 整数
    • 8bit非負整数(リトルエンディアン)
    • 8bit整数(リトルエンディアン)
    • 16bit非負整数(リトルエンディアン)
    • 16bit整数(リトルエンディアン)
    • 32bit非負整数(リトルエンディアン)
    • 32bit整数(リトルエンディアン)
    • 64bit非負整数(リトルエンディアン)
    • 64bit整数(リトルエンディアン)
  • 浮動小数点数
    • 16bit浮動小数点数
    • 32bit浮動小数点数
    • 64bit浮動小数点数
  • 可変長UTF-8文字列
  • リスト(0個以上の同じ型の値を持つ型)
  • 構造体(1個以上のフィールドを持つ型で、各フィールドは別の型にできる)

多次元配列はまだサポートしていません。

Plasmaクライアント機能もまだサポートしていません。

各言語での実装の完成度のまとめ

Apache Arrowの各言語での実装をそれぞれ説明しました。

サポートしている型という観点では、数値型と真偽値型はどの言語の実装でもカバーしています。時間関連の型はGo、Rust実装以外はカバーしています。複合型は共用体以外ならどの言語の実装でもカバーしています。C++、Java、JavaScript実装およびC++実装のバインディングはすべての型に対応しています。

他のフォーマットとの相互変換はC++実装ファミリーが進んでいます。Apache ParquetやApache ORC、Featherフォーマットと相互変換できます。

GPUサポートもC++実装ファミリーが進んでいます。

Plasmaを使いたい場合はC++、Java、Python実装を使うことになります。近いうちにC実装ファミリーでも使えるようになる予定です。

Apache Arrowデータを高速に処理するという観点ではどの実装もまだカバー範囲は狭いです。

Apache Arrowの今後

ここまででApache Arrowの概要、現状を説明しました。最後に今後のことを説明します。

実行エンジンGandivaの取り込み

現状ではApache Arrowデータを高速に処理するところがまだ弱いです。そのあたりをカバーするモジュールがGandivaです。このGandivaが近いうちに取り込まれ、Apache Arrowデータを高速処理できる基盤となるでしょう。

GandivaはC++で実装されているので、C++実装ではすぐにGandivaを使えるようになります。

Gandivaを開発しているDremioは自社で開発しているサービスでApache Arrowを利用しています。このサービスはJavaで実装しているので、C++で実装したGandivaをJavaから使うためのバインディングも開発しています。そのため、Java実装でもすぐにGandivaを使えるようになります。

C++実装ベースのPython実装、C実装ファミリーもすぐに使えるようになるでしょう。

CSVパーサーの追加

世の中では今でもCSVがよく使われています。そのため、CSVをパースしてApache Arrowオブジェクトを作成するパーサーの開発を進めています。

参考:Building a fast Arrow-native delimited file reader (e.g. for CSVs) - Apache Mail Archives

これはC++実装での話です。実装されたらC++実装ファミリーでもすぐに使えるようになるでしょう。

Apache Arrow対応クライアントの追加

Java実装にはJDBCで取得したデータをApache Arrowオブジェクトで返す機能があります。それと同様な機能がC++実装にも入る予定です。

まずは、Apache Hive・Apache Impalaからデータを取得してApache Arrowオブジェクトで返す機能が入りました。

他にもPostgreSQLからデータを取得してApache Arrow形式で返す機能の実装も計画しています。

参考:Developing native Arrow interfaces to database protocols - Apache Mail Archives

同様のことをしているPythonライブラリーがあります。それがturbodbcです。これはODBCで取得したデータをApache Arrowオブジェクトで返すことができます。もしかしたら、今後、この機能がApache Arrowの方に移動して、turobdbcはそれを使うという関係になるかもしれません。

まとめ

Apache Arrowの概要、2018年9月時点での現状、今後のことについて説明しました。Apache Arrowは数年後にはデータ処理界隈で重要なコンポーネントになっているだろうプロジェクトです。日本でもApache Arrowのことを知っている人が増えるといいと思うので日本語でまとめました。Apache Arrowを使う人が増えるといいなぁと思います。さらに言えば開発に参加する人も増えるといいなぁと思います。

この説明では省略しましたが、Apache Arrowの生い立ちについて面白い読み物があります。PMC(プロジェクト管理チームみたいな感じ)のチェアー(1番偉い人)が書いたものです。興味がある人はこちらも読んでみてください。

私が知っていることはまとめたつもりですが、もしかしたらカバーできていない話があるかもしれません。もし、「○○についても知りたい!」という方がいたらRed Data Toolsのチャットで声をかけてください。この記事に追加します。

Apache Arrowについて講演して欲しいという方はお問い合わせフォームからご連絡ください。

私はデータ処理ツールの開発という仕事をしたいと思っています。その中にはもちろんApache Arrowの開発も含まれています。一緒に仕事をしたい!(自社サービスをApache Arrow対応したいとか)という方はお問い合わせフォームからご連絡ください。

つづき: 2018-10-10
2018-09-05

YoctoのWeston上で日本語入力

はじめに

これまでにも何度か紹介していますが、クリアコードではGecko(Firefox)を組み込み機器向けに移植する取り組みを行っています。

その後、課題として残っていたWebRTCも無事に動作するようになり、主だった機能は主ターゲットであるRZ/G1M上で動作するようになっています。

今回は趣向を変えて、同環境上で少しだけ日本語入力を検証してみた経過を紹介します。

uimでの日本語入力の様子

Yoctoでの日本語入力事情

Yoctoの主要なレイヤを概観してみたところ、IMフレームワークとしてはuim、日本語変換エンジンとしてはAnthyが見つかりました。

逆に、これ以外のIMフレームワークや日本語変換エンジンのレシピを見つけることはできませんでしたので、今回はこれを使用してみます(uim-skkでも良いと思いますが、一般向けにはやや紹介しづらいので、今回はAnthyのみを対象とします)。

さて、ウィンドウシステムがX11であれば、おそらく上記レシピをそのままビルドするだけで使用できるでしょう。ですが、今回の対象はYoctoのcore-image-westonであり、ウィンドウシステムはWayland/Westonです。uimにWaylandサポートが入ったのは1.8.7からで、上記レシピは1.8.6ですから、恐らくこれを普通にビルドするだけでは日本語入力できるようにはならないでしょう。実際に試してみましたが、やはりアプリケーションがクラッシュして起動できないという結果となりました。

レシピの修正

上記仮説が正しければ、単にuimを最新版に上げるだけでWeston上でも動作するでしょう。ですが、実際にuimのバージョンを上げてビルドを試してみたところ、同レシピに含まれるパッチがそのままでは当たらないなどの問題が発生しました。そちらを修正するのも難しくはないでしょうが、まずは手っ取り早く動くか動かないかを確認したかったため、uim-1.5.6のまま最低限のパッチを最新のuimからバックポートしてみることにしました。以下がmeta-openembeddedに対するパッチです。

commit f3b0e042986a83a767a967ec352c731037693d98
Author: Takuro Ashie <ashie@clear-code.com>
Date:   Fri Aug 17 13:16:21 2018 +0900

    uim: First aid to work with Wayland

diff --git a/meta-oe/recipes-support/uim/uim/0001-Fix-the-problem-that-the-candidate-window-is-not-sho.patch b/meta-oe/recipes-support/uim/uim/0001-Fix-the-problem-that-the-candidate-window-is-not-sho.patch
new file mode 100644
index 0000000..6ebeb21
--- /dev/null
+++ b/meta-oe/recipes-support/uim/uim/0001-Fix-the-problem-that-the-candidate-window-is-not-sho.patch
@@ -0,0 +1,26 @@
+From f266ff2b59bc3b0cd732c62683a1df9672114c1d Mon Sep 17 00:00:00 2001
+From: Konosuke Watanabe <konosuke@media.mit.edu>
+Date: Sat, 20 Feb 2016 12:30:35 +0900
+Subject: [PATCH] Fix the problem that the candidate window is not shown in
+ GTK3 environment.
+
+---
+ gtk2/immodule/uim-cand-win-gtk.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/gtk2/immodule/uim-cand-win-gtk.c b/gtk2/immodule/uim-cand-win-gtk.c
+index 1bfe759c..41590d06 100644
+--- a/gtk2/immodule/uim-cand-win-gtk.c
++++ b/gtk2/immodule/uim-cand-win-gtk.c
+@@ -225,7 +225,7 @@ uim_cand_win_gtk_init (UIMCandWinGtk *cwin)
+ 
+   gtk_widget_set_size_request(cwin->num_label, DEFAULT_MIN_WINDOW_WIDTH, -1);
+   gtk_window_set_default_size(GTK_WINDOW(cwin), DEFAULT_MIN_WINDOW_WIDTH, -1);
+-  gtk_window_set_resizable(GTK_WINDOW(cwin), FALSE);
++  gtk_window_set_resizable(GTK_WINDOW(cwin), TRUE);
+ }
+ 
+ static void
+-- 
+2.17.1
+
diff --git a/meta-oe/recipes-support/uim/uim/0001-gtk3-support-Wayland-backend.patch b/meta-oe/recipes-support/uim/uim/0001-gtk3-support-Wayland-backend.patch
new file mode 100644
index 0000000..e40caeb
--- /dev/null
+++ b/meta-oe/recipes-support/uim/uim/0001-gtk3-support-Wayland-backend.patch
@@ -0,0 +1,71 @@
+From 06558e571967f3cb989bdb550d1dea05247cc21d Mon Sep 17 00:00:00 2001
+From: Kouhei Sutou <kou@clear-code.com>
+Date: Sat, 30 Dec 2017 21:15:50 +0900
+Subject: [PATCH] gtk3: support Wayland backend
+
+GitHub: fix #71
+
+Debian: 810739
+
+Reported by Thibaut Girka. Thanks!!!
+---
+ gtk2/immodule/gtk-im-uim.c   | 16 ++++++++++++++++
+ gtk2/immodule/key-util-gtk.c |  8 +++++++-
+ 2 files changed, 23 insertions(+), 1 deletion(-)
+
+diff --git a/gtk2/immodule/gtk-im-uim.c b/gtk2/immodule/gtk-im-uim.c
+index ac2918ce..066e5f5b 100644
+--- a/gtk2/immodule/gtk-im-uim.c
++++ b/gtk2/immodule/gtk-im-uim.c
+@@ -535,6 +535,22 @@ layout_candwin(IMUIMContext *uic)
+     gdk_window_get_geometry(uic->win, &x, &y, &width, &height, &depth);
+ #endif
+     gdk_window_get_origin(uic->win, &x, &y);
++    {
++      GtkWindow *window = NULL;
++      GdkWindow *gdk_window = uic->win;
++      while (gdk_window) {
++        gpointer user_data;
++        gdk_window_get_user_data(gdk_window, &user_data);
++        if (user_data && GTK_IS_WINDOW(user_data)) {
++          window = user_data;
++          break;
++        }
++        gdk_window = gdk_window_get_parent(gdk_window);
++      }
++      if (window) {
++        gtk_window_set_transient_for(GTK_WINDOW(uic->cwin), window);
++      }
++    }
+     uim_cand_win_gtk_layout(uic->cwin, x, y, width, height);
+   }
+ }
+diff --git a/gtk2/immodule/key-util-gtk.c b/gtk2/immodule/key-util-gtk.c
+index 27abd834..bd029e73 100644
+--- a/gtk2/immodule/key-util-gtk.c
++++ b/gtk2/immodule/key-util-gtk.c
+@@ -319,6 +319,7 @@ im_uim_init_modifier_keys()
+ #ifdef GDK_WINDOWING_X11
+   int i, k = 0;
+   int min_keycode, max_keycode, keysyms_per_keycode = 0;
++  GdkDisplay *gdk_display;
+   Display *display;
+   GSList *mod1_list, *mod2_list, *mod3_list, *mod4_list, *mod5_list; 
+   XModifierKeymap *map;
+@@ -329,7 +330,12 @@ im_uim_init_modifier_keys()
+ 
+   mod1_list = mod2_list = mod3_list = mod4_list = mod5_list = NULL;
+ 
+-  display = GDK_DISPLAY_XDISPLAY(gdk_display_get_default());
++  gdk_display = gdk_display_get_default();
++  if (!GDK_IS_X11_DISPLAY(gdk_display)) {
++    /* TODO: We may need to something for Wayland. */
++    return;
++  }
++  display = GDK_DISPLAY_XDISPLAY(gdk_display);
+   map = XGetModifierMapping(display);
+   XDisplayKeycodes(display, &min_keycode, &max_keycode);
+   sym = XGetKeyboardMapping(display, min_keycode,
+-- 
+2.17.1
+
diff --git a/meta-oe/recipes-support/uim/uim_1.8.6.bb b/meta-oe/recipes-support/uim/uim_1.8.6.bb
index 271718e..e7241f1 100644
--- a/meta-oe/recipes-support/uim/uim_1.8.6.bb
+++ b/meta-oe/recipes-support/uim/uim_1.8.6.bb
@@ -4,17 +4,19 @@ LICENSE = "BSD-3-Clause & LGPLv2+"
 LIC_FILES_CHKSUM = "file://COPYING;md5=32463fd29aa303fb2360faeeae17256b"
 SECTION = "inputmethods"
 
-SRC_URI = "http://uim.googlecode.com/files/uim-${PV}.tar.bz2"
+SRC_URI = "https://github.com/uim/uim/releases/download/uim-${PV}/uim-${PV}.tar.bz2"
 
 SRC_URI_append_class-target = " file://uim-module-manager.patch \
     file://0001-fix-bug-for-cross-compile.patch \
     file://0001-Add-support-for-aarch64.patch \
+    file://0001-gtk3-support-Wayland-backend.patch \
+    file://0001-Fix-the-problem-that-the-candidate-window-is-not-sho.patch \
 "
 SRC_URI[md5sum] = "ecea4c597bab1fd4ba98ea84edcece59"
 SRC_URI[sha256sum] = "7b1ea803c73f3478917166f04f67cce6e45ad7ea5ab6df99b948c17eb1cb235f"
 
 DEPENDS = "anthy fontconfig libxft libxt glib-2.0 ncurses intltool"
-DEPENDS_append_class-target = " intltool-native gtk+ gtk+3 uim-native takao-fonts"
+DEPENDS_append_class-target = " intltool-native gtk+3 uim-native takao-fonts"
 
 RDEPENDS_uim = "libuim0 libedit"
 RDEPENDS_uim-anthy = "takao-fonts anthy libanthy0 glibc-utils glibc-gconv-euc-jp"
@@ -31,6 +33,7 @@ EXTRA_OECONF += "--disable-emacs \
     --without-canna \
     --without-mana \
     --without-eb \
+    --without-gtk2 \
 "
 
 CONFIGUREOPTS_remove_class-target = "--disable-silent-rules"

なお、当時のmeta-openembeddedのレシピではuimのソースコードをダウンロードすることが出来なったので、上記パッチにはその修正も含まれています。この点については既にOpenEmbeddedプロジェクトに報告済みで、修正が取り込まれています

ビルド方法

ベースのブートイメージのビルド方法はこれまでと同様ですので割愛します。 これにuimを追加するには、以下の設定をconf/local.confに追加してcore-image-westonを再作成します。

IMAGE_INSTALL_append = " uim uim-common uim-gtk3 uim-anthy "

動作設定

Anthyを既定のIMとするには、以下の内容で設定ファイル~/.uimを作成します。

(define default-im-name 'anthy)

firefoxを起動する際に、環境変数GTK_IM_MODULE=uimをセットすることでuimが使えるようになります。

$ WAYLAND_DISPLAY=wayland-0 GTK_IM_MODULE=uim firefox

まとめ

YoctoのWeston上での日本語入力について、現在の検証状況を紹介しました。こんな記事を書いている暇があったらとっととアップストリームのuimをバージョンアップしてしまいたいところではありますが、すぐには作業に取りかかれないため、まずは社内Wikiの情報を切り貼りして公開してみました。

2018-08-31

Debian Maintainerになるには

はじめに

クリアコードの林です。 今回は、Debianパッケージのメンテナンスに関わる上で、Debian Maintainerになるための方法を紹介します。

役割でみるDebianの開発に関わる人々

Debianには多くの開発者が関わっていますが、Debianパッケージをメンテナンスしている開発者の権限の観点で分類するといくつか種類があります。

  • Debian Developer(Debian開発者)
  • Debian Maintainer(Debianメンテナー)
  • Package Maintainer(パッケージメンテナー)

Debian Developerはその名が示すようにDebianの開発そのものに広く関わる権限をもっている人です。 たとえば、Debianパッケージをアップロードしたり、Debianプロジェクトに関わる投票権を保持しています。*1

Debian Maintainerは、Debian Developerにスポンサーをしてもらわずとも、特定のDebianパッケージに関してはアップロードすることを認められている人です。

Package MaintainerはDebianパッケージのメンテナンスに関わってはいますが、アップロードはDebian Developerにお願いしないといけない人のことです。

Debianパッケージをメンテナンスするのに、Debian DeveloperやDebian Maintainerであることは必須ではありません。 ただし、Debian Maintainerであれば、アップストリームが新しいバージョンをリリースしたときに割と早く対応しやすいということはいえます。 これは、Package Maintainerと違ってDebianパッケージを自分でアップロードするところまでできる、というところによります。 都合よくメンテナンスしているパッケージのスポンサーをしてくれる人がいればよいのですが、そうでない場合もあり得ます。 継続的にメンテナンスしているパッケージがあるのであれば、Debian Maintainerになる価値があるかもしれません。

Debian Maintainerになるまでの流れ

2018年8月時点では、次のような流れになっています。

  • Debian Maintainerに専用のWebサイトから応募する
  • なぜDebian Maintainerになりたいのかをアピールする
  • Debian社会契約、フリーソフトウェアガイドライン、マシン使用ポリシーなどに同意する
  • GPG鍵のチェックを受ける
  • Debian Developerの推薦をもらう
  • レビューを受けて承認してもらう

なお、応募資格としてGPGの鍵に(最低1人、もっと多い方が望ましい)Debian Developerに署名してもらっておく必要があります。 もしまだであれば、お近くのDebian勉強会に参加するなどしてDebian Developerに署名してもらうとよいでしょう。 *2 定期的に開催されている勉強会がいくつかあります。

Debian Maintainerに専用のWebサイトから応募する

Debian Maintainerに応募するには専用のサイトである Debian New Members にアクセスします。

Debian New Membersウェブサイト

ページの中ほどにJoin the NM processというリンクがあるのでクリックします。

NMプロセスをはじめる

入力フォームから以下の情報を入力して送信します。

  • GPG鍵のフィンガープリント
  • 名前
  • 連絡先のメールアドレス
  • ユーザー名
  • 自己紹介文

上記のうち、ユーザー名というのはDebian Maintainerに応募する場合には必須ではありません。Debianプロジェクトの機材(サーバーとか)にアクセスするゲストアカウントとして使われるものだからです。将来的に必要そうであれば、予約することができるので申請しておいてもいいでしょう。

入力フォームを送信すると、確認用のメールがGPGで暗号化されて届きます。復号したテキストに含まれるURLにアクセスするとメールアドレスの確認が完了します。 ステータスがDebian Contributorになっているので、「request new status」というリンクをクリックします。

メールアドレス確認後の状態

ステータスが、Debian Contributorになっているのを、Debian Maintainerに変更して送信します。

新規ステータスを設定する

なぜDebian Maintainerになりたいのかをアピールする

Declaration of intentとしてなぜDebian Maintainerになろうとしているのかを書いてGPGで署名したうえで入力フォームから送ります。

なぜDebian Maintainerになりたいのか

Debian社会契約、フリーソフトウェアガイドライン、マシン使用ポリシーなどに同意する

内容を確認した上で、内容に同意する文言にGPGで署名したうえで入力フォームから送ります。

 SC DFSG DMUPに同意する

GPG鍵のチェックを受ける

とくにやることはありません。問題や疑念があれば指摘されるはずです。

Debian Developerの推薦をもらう

Debian DeveloperにDebian Maintainerとして適格者であることを示す推薦文を書いてもらう必要があります。 知り合いのDebian Developerがいればお願いするとよいでしょう。

レビューを受けて承認してもらう

ここまでくるとステータスが「Waiting for review」に変わり、管理者の承認待ちの状態になります。 人によって承認されるまでの期間はばらばらのようです。あせらず一ヶ月くらいをみておくといいかもしれません。

管理者の承認待ちの状態

まとめ

今回はDebian Maintainerになるための方法を紹介しました。 Debianのパッケージのメンテナンスに興味があったり、継続的にPackage Maintainerとして活動している人は、Debian Maintainerを目指してみるのはいかがでしょうか。

*1 Debianのプロジェクトリーダーは選挙で選ばれています。

*2 毎回確実にDebian Developerが参加しているとは限らないことに注意してください。

2018-08-30

fluent-plugin-elasticsearchのSnifferクラスについて

はじめに

fluent-plugin-elasticsearchはよく使われているプラグインの一つです。 このプラグインをメンテナンスするためには、Fluentdの知識だけでなく、Elasticsearchが今後どのようになっていくかも知っておく必要があります。 また、このプラグインはRed Hat社がメンテナンスしているOpenShiftのログコンポーネントの一部としても使われています。

elasticsearch-transportのSnifferクラスとは

elasticsearch-transportには定期的にクラスタの状況を監視するSnifferクラスがあります。このクラスではGET _nodes/httpというクラスタの状況を返答するAPIを叩いており、大抵の場合はこのAPIを叩いておけばElasticsearchクラスタの状況がfluent-plugin-elasticsearchが使っているelasticsearchクライアントに通知されます。 そのため、X-Packを用いない通常の使用方法では問題になりません。

k8sサービス化されたElasticsearchクラスタに接続する

k8sのサービスとはPodから生成したノードを一まとめにしたアクセス手段を提供します。k8sの世界観ではサービスのアクセス先は一定です。しかし、サービスを構成するノードの構成要素はある時は起動していますが、またある時は停止または破棄されています。このノード一つ一つにElasticsearchが立っていても通知速度よりも起動・破棄のサイクルが速ければGET _nodes/httpを使用しても欠点が目立つようになります。 そのため、k8sのサービス化されたElasticsearchクラスタには新たなSnifferクラスの実装が必要になります。

そこで、元々のSnifferクラスのhostsメソッドの実装を見てみると、以下のようになっています。

        # Retrieves the node list from the Elasticsearch's
        # [_Nodes Info API_](http://www.elasticsearch.org/guide/reference/api/admin-cluster-nodes-info/)
        # and returns a normalized Array of information suitable for passing to transport.
        #
        # Shuffles the collection before returning it when the `randomize_hosts` option is set for transport.
        #
        # @return [Array<Hash>]
        # @raise  [SnifferTimeoutError]
        #
        def hosts
          Timeout::timeout(timeout, SnifferTimeoutError) do
            nodes = transport.perform_request('GET', '_nodes/http').body

            hosts = nodes['nodes'].map do |id,info|
              if info[PROTOCOL]
                host, port = info[PROTOCOL]['publish_address'].split(':')

                { :id =>      id,
                  :name =>    info['name'],
                  :version => info['version'],
                  :host =>    host,
                  :port =>    port,
                  :roles =>   info['roles'],
                  :attributes => info['attributes'] }
              end
            end.compact

            hosts.shuffle! if transport.options[:randomize_hosts]
            hosts
          end
        end

nodes = transport.perform_request('GET', '_nodes/http').body の行でElasticsearchクラスタの情報を取りに行き、取りに行った情報から再度クラスタの情報を再構築しています。

もし、接続先のURLやIPアドレスが固定であれば、以下のようなSnifferクラスを作成し、ホスト情報を使い回す振る舞いをさせた方が良いです。

require 'elasticsearch'

class Fluent::Plugin::ElasticsearchIdempotenceSniffer < Elasticsearch::Transport::Transport::Sniffer
  def hosts
    @transport.hosts
  end
end

elasticsearchクライアントは独自Snifferを渡してそのクラスを元にクラスタ情報を再構築するようなカスタマイズをすることができます。

@sniffer = options[:sniffer_class] ? options[:sniffer_class].new(self) : Sniffer.new(self)

これらの変更をfluent-plugin-elasticsearchで扱うには以下のようにすると独自のSnifferクラスを用いてElasticsearchクラスタとやりとりできるようになります。

     config_param :pipeline, :string, :default => nil
     config_param :with_transporter_log, :bool, :default => false
     config_param :emit_error_for_missing_id, :bool, :default => false
+    config_param :sniffer_class_name, :string, :default => nil
     config_param :content_type, :enum, list: [:"application/json", :"application/x-ndjson"], :default => :"application/js
on",
                  :deprecated => <<EOC
 elasticsearch gem v6.0.2 starts to use correct Content-Type. Please upgrade elasticserach gem and stop to use this option
.

#...
     def client
       @_es ||= begin
         adapter_conf = lambda {|f| f.adapter @http_backend, @backend_options }
         transport = Elasticsearch::Transport::Transport::HTTP::Faraday.new(get_connection_options.merge(
                                                                             options: {
                                                                               reload_connections: @reload_connections,
                                                                               reload_on_failure: @reload_on_failure,
                                                                               resurrect_after: @resurrect_after,
                                                                               retry_on_failure: 5,
@@ -287,7 +300,8 @@ EOC
                                                                               http: {
                                                                                 user: @user,
                                                                                 password: @password
-                                                                              }
+                                                                              },
+                                                                              sniffer_class: @sniffer_class,
                                                                             }), &adapter_conf)
         es = Elasticsearch::Client.new transport: transport
 

まとめ

fluent-plugin-elasticsearchのElasticsearchへのリクエストに関わるelasticsearch-transportのSnifferに関するお話を書きました。 記事と同様の働きをするパッチはfluent-plugin-elasticsearchのv2.11.5に取り込んでリリース済みです。 fluent-plugin-elasticsearchでk8sやnginxのプロキシを設置していて接続のリロード時に正常なElasticsearchクラスタの情報が取得できずに困っている場合はsniffer_class_nameの設定項目を初期値から変えてみたり、独自のSnifferクラスを定義したりしてみてください。

タグ: Fluentd
2018-08-22

2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|
タグ: