tsutsuiの作業記録置き場

NetBSDとかPC-6001とかの作業記録のうち、Twitterの140字では収まらない内容や記事としてまとめるべき内容をとりあえず置いてみる予定

mikutter GTK3対応(3) 〜miracle painter〜

次回以降は、このあたりの歴史や実際の修正作業について背景の解説(多分に推測を含む)を含めて書いてみたいと思います。

mikutter GTK3対応(2) 〜サーベイ中〜 - tsutsuiの作業記録置き場

前回のエントリの続きで、 mikutter GTK3対応の歴史や実際の修正作業について、Redmineその他の資料サーベイ結果とともに背景の解説(多分に推測を含む)を書いてみたいと思います。

ruby-gnome 3.5.0

……と、いきなりですが本題の GTK3の歴史の前に、 ruby-gnome 本家に動きがあったので少しだけ。

日本時間の 12月18日朝に、変更履歴である NEWS ファイルに 3.5.0 の記載が追加されたようです。

github.com

すでに 2021/12/18 の日付が入っているようですが、 GitHub のコミット状況を見る限りではもう少し時間がかかりそうな雰囲気です。

一方、 pkgsrc に更新版の 3.5.0 対応をコミットするには全gem分の修正と動作確認という事前作業量が多いので、 pkgsrc-2021Q4 freeze 前に対応するには日程的にちょっと厳しい感じです。

そもそも本体の mikutter 5.0 GTK3対応版は 12月25日リリース予定なので、どのみち対応は pkgsrc-2021Q4 リリース後の freeze 期間明けになるかと思います。

mikutter 5.0.0-alpha1

このブログエントリ記事を書き上げるのが遅れているうちに、12月19日に mikutter 本体の方もリリーステスト版とも言うべき 5.0.0-alpha1 がリリースされています。

mikutter.hatenablog.com

ただ、特にバグ修正が一段落した版というわけではなく、12/25の本番(?)リリース作業のテストという位置付けです。よって、パッケージシステム等の管理者としてのテストでリリース版 tar ball が必要となる作業を除けば、ブログ記事本文にあるとおり gitで develop ブランチを取得してテストするほうがよいでしょう。

mikutter GTK3対応:その長い道のり

というわけで、ようやくですが本題の mikutter GTK3対応についてです。

GTK3対応の始まり

mikutterの GTK3対応は、今から5年前、2016年 9月10日付けで「gtk3」の Redmineチケットが登録されたのが始まりと言えます。

dev.mikutter.hachune.net

チケット履歴にある通り、当時京都で開かれたRubykaigi2016で GTK3対応関連の会話があったようで、かつてmikutterユーザ会@関東を開いたこともある Katsuyoshi氏が担当としてアサインされました。

ここでGTK3対応の前準備としての機械的なメソッドその他の置き換えが行われましたが、このときの Katsuyoshi氏の置き換え作業は 2016/9/13まで。その後、具体的にmikutterが GTK3で動き出すところまでには至らなかったようです。

yuntan_t氏によるGTK3対応着手

GTK3対応活動から 2年ほど経過した 2018年 8月25日になって、それまで mikutter Redmineでも積極的に活動されていた yuntan_t氏が GTK3対応に名乗りを上げてくれました。

こんにちは.gtk3対応に興味があったのでやってみています. https://github.com/yuntan/mikutter/tree/gtk3 に置いてます.topic/887-gtk3をmaster(557b03b)にrebaseしてその続きで作業しています.起動できるところまで作業を進めましたが,MiracrlePainterで苦戦しています.

機能 #887: gtk3 - mikutter - やること

この作業が toshi_aさんの目に止まり、 yuntan_t氏はめでたく mikutterのコミッタとなり、上記 miracle painter の課題解決に取り組んでくれました。

動作がかなり不安定(表示されたりされなかったりする)ですが,MiraclePainterを表示させることが出来ました.

https://i.gyazo.com/bc5c3bfdeb91ecc3440d358c828ed1b6.png

機能 #887: gtk3 - mikutter - やること

これらの yuntan_t氏のコメントから察せられるかと思いますが、 mikutter の GTK3対応の本質は、「単なるGTK2→GTK3の移行作業」というだけではなく、この「miracle painter」の対応の困難さにありました。

"miracle painter"

「miracle painterとはなんなのか?」ということを説明するには、 miracle painter が最初にmikutter に実装された mikutter 0.0.3.5 リリース前後の mikutter blogの記載を引用するのが早道です。関連するエントリをピックアップしてみます。

ブログエントリ その1

mikutter blogのこちらの記事が初出と思われます。

mikutter.hatenablog.com

ひっそりやってたんだけど思ったより拡散したので、解りづらい形式で説明します。
あまり説明に時間を食いたくないので簡単に。

cairoとは

すごくかわいくなります

mikutter cairo版とは

萌ゆいものとかわいいものが一緒になってやばいです

具体的に何をしたの?

タイムラインをリストビューにして、各セルをcairoでレンダリングしています。見た目はできるだけ今までと変わらないようにしています。
だから別にcairoを使ったことだけが新しいわけじゃないんだけど、それに気づいたときにはみんな知ってたのでておくれました。

何が良いのか

高速になって、メモリ消費が少なくなって、ライブラリのバグを踏む確率が下がることを期待しています

欠点は

とくになし。ただし、リプライのUIが若干変わってしまうかも。これは最終的に従来の方法を実現できる可能性がある。
それさえなければ欠点は特になさそう
cairo版と言われてるものについて - mikutter blog

わかる人にはわかる「これぞ toshi_aさん節」というエントリですが、ポイントは本文中でも言及されている「cairoでレンダリング」というところかと思います。

ブログエントリ その2

miracle painterについてもう少し詳細が記載されたのが次のエントリです。

mikutter.hatenablog.com

こんばんわ。先程新UIをリリースしました!何がどうなったのか簡単に説明するよ。
 
mikutterの従来のレンダリング方法
 
今までのmikutterは、ウィジェットを綺麗に並べてタイムラインを実現していました。この方法だと、あらゆるウィジェットを使用することができるので、タイムラインのレンダリングがとても自由にできます。
 
したがって、mikutterのタイムラインは他のTwitterクライアントのそれに比べてかなりリッチな部類だと思います。なかでも、アイコンオーバーボタン(アイコンにマウスカーソルを重ねるとボタンが出てくる機能)、ふぁぼられ、リツイート累積表示、リプライ宛先表示などのmikutterの代表的な機能は、この方法でタイムラインをレンダリングしたからこそ容易に達成できた、という背景がありました。
 
何が悪いのか
 
本来、ウィジェットは頻繁に追加削除されるものではないので(大体は作ったらそれでおしまいですよね)、ある程度タイムラインの流れが速いと、異常な速度でウィジェットの作成/削除/パッキングが行われます。
配置の自由度が高いぶん、こういった変化があったときの再描画のコストは馬鹿になりません。また、Ubuntuの最新版に入っているRubyGTKはとても古く(nattyには0.19.3が入っているが、最新は0.90.8)、大量のバグが含まれているのが現状です。そのようなAPIに頻繁にアクセスするので、ライブラリがSegmentation Faultしてmikutterが巻き添えになることがしばしばありました。
 
他のTwitterクライアントのレンダリング
 
mikutterは他のTwitterクライアントと比較して、レンダリングが飛び抜けて遅いですね。説明は上記の通りだけど、ではどうしてそんなことをしたかというと、現在のmikutterは、とりあえず表示できるものを仮に作った段階だったのです。つまり、mikutterのUIは新たにつくり直す必要がありました。
 
では、他のTwitterクライアントはどうしているかというと、リストビューを使用していると思われます。
リストビューは、パーツが縦に並ぶことを前提にしているのでどうやら速いらしい。ソートオーダーなどの細かい話もGtkがやってくれるのでRubyでやるよりは早くなりそう。
 
しかし、じゃあリストビューにすればいいじゃない、と簡単にはいかない。というのも、この中に置けるのは、せいぜい画像と文字くらいだからで、新しいRendererを作成するにしても、今までのように好きなウィジェットを思いのままに設置するとはいきません。
 
Cairo+リストビュー
じゃあ、全部画像にしたらよくね!?という奇想天外なことを思いつきました。画像だったらすきにレンダリングできるけれど、キャッシュすれば速さもでるんじゃねーのと思ったわけです。

リストビューは、任意のRendererを設定できるので、今回は実際にTLを描画するやつをMiraclePainterという名前にしました。TLの先まですべてミク色に染めていただきましょう!!!

で、リッチな表示ができないとか言ってたけど蓋を開けたら右のようになってます。ついでだから微妙にマイナーチェンジしましたが。これがどの程度速くなったかは、実際に起動してもらったほうがよさそうなのでここでは書かないけれど、

  • ボトルネックになってた部分は100倍くらいの効率のアップ
  • 起動時間は2秒
  • 新しいタブを作ったりした時にブラックアウトすることもなくなった
  • 入力中に固まってイライラすることもなくなった
  • RubyGtkのAPIを叩く回数が減ったので、Segmentation Faultの確率が減るかも!?

と、圧倒的な進化を遂げました。CPU使用率も大幅に減ったので、いままで遅くて使えんと言っていた人もつかえるようになるんじゃないかな。
新UIについて - mikutter blog

こちらも全開の toshi_aさん節ですが、ポイントは以下かと。

  • 全部画像にしたらよくね!?
    • 言い換えると、このエントリの本題であるGTK3対応の困難さを暗示している?
  • ソートオーダーなどの細かい話もGtkがやってくれる
    • 「全部画像」の一方、リストビュー表示についてはGTKに依存していることになる?
  • Ubuntuの最新版に入っているRubyGTKはとても古く
    • Ruby-gnome2 0.19.3」というキーワードが toshi_aさんのトラウマになっていた時期もありました
    • 「RubyGtkのAPIを叩く回数が減ったので、Segmentation Faultの確率が減るかも」というのも隔世の感
  • 今回は実際にTLを描画するやつをMiraclePainterという名前にしました。TLの先まですべてミク色に染めていただきましょう!!!
    • わかる人にはわかるとおり、「miracle painter」の語源は言わずと知れた以下の動画の曲ですね

実際の効果も相当なものでした。

当時の mikutter を思い返してみると、「ふぁぼられを表示する」「ふぁぼられたツイートをageる」の機能とも相まって、タイムライン上でふぁぼ爆撃合戦が始まると各ツイートが「シュパパパパパパ」と凄まじい勢いでウインドウ内を上下に飛び交う、という描画を目の当たりにすることになりました。

Redmine "miracle painter" チケット

実際の miracle painter の実装作業の様子が垣間見えるのが以下の mikutter Redmineのチケットです。

dev.mikutter.hachune.net

チケットの登録は 2011年5月8日のGW明け。おそらく、構想自体はGW中に練られたのではないかと思います。

そして、上述のとおり miracle painterが実装された mikutter 0.0.3.5 がリリースされたのは 2011年5月23日。

関連する子チケットから重そうなものだけをピックアップすると以下の通り。

つまり、わずか 2週間あまりでこれだけの機能が一気に実装されたことになります。

テキストの選択」って何? と思われるかもしれませんが、チケットの説明にある通り「ドラッグドロップでテキストを選択できるようにする」ということです。

どういうことかいうと、単純なテキストの描画も「全部画像」なので、 miracle painter つまり mikutter独自の実装として
マウスがクリックされた位置に描画されているテキストを識別し、ドラッグされた範囲の文字列を反転させる表示についても cairoで描画している
ということになるかと思います。

これだけを見ても「ヤバい」という雰囲気は感じられるのではないかと思います。

miracle painterの呪縛

一気呵成の勢いで実装された miracle painterですが、実装から数年の時が経過し、徐々に「勢いとのトレードオフ」とも言える互換性問題に直面していくことになります。

"miracle painter 2"

miracle painter の課題が記述されている資料の一つは、以下の "miracle painter 2" の mikutter Redmineチケットです。

dev.mikutter.hachune.net

起票は miracle painter 実装から7年後(!)の 2018年 5月14日。課題としてあげられているのは以下の項目です。

  • MiraclePainterのスーパクラスがGtk::Objectになっているのをやめる
    • 「Gtk3にはそんなクラス無いので」という記述に今日の問題が垣間見えるかと
  • MiraclePainterをTreeViewから独立させる
  • MiraclePainterを単一のプラグインにする
  • 再描画の最適化

しかし、このチケットに対する具体的なコミット等は行われることはなく、ほぼ1年後の 2019年 6月9日に「却下」でクローズされることになります。

"MiraclePainterをGtk::Widgetベースの実装に書き直す"

次に miracle painterの課題について言及されているのは、前述の yuntan_t氏の miracle painter対応の提案に対する以下の toshi_aさんコメントです。

まず、プロジェクトとしてMiraclePainterを置き換えるモチベーションはありません(gtk3は、色んな所に書いている通り、やりたいと思っています)。
UI周りのAPIが複雑なところであり、ここを置き換えると大きく互換性が損なわれます。多分、このチケットを提案した理由は速度やコードの単純化などだと思いますが、それらを達成するにはいまのMiraclePainterの互換性を壊さないことには十分な成果は得られないはずです。


とはいえ、互換性を無視して今書き直せば、数倍程度の高速化、半分程度のコード量は簡単に達成できると思います。互換性を気にして長い時間を掛け、大した効果を産めないよりは、forkなどの手段を取ってもらって、そういったしがらみを無視してMiraclePainterの書き換えはやってもらったほうが良いのではないか、という結論に至る可能性が高いと思っています。

提案 #1453: gtk3: MiraclePainterをGtk::Widgetベースの実装に書き直す - mikutter - やること

以前にも言及していますが、mikutterの大きな特徴は「プラグインによる拡張」が可能であるというところです。mikutter本体に付属するプラグインはいうに及ばず、各ユーザーが自由に実装したプラグインも、mikutter内部の miracle painterの構造に暗黙的に依存したものが多数存在します。

miracle painterの改善のために構造に手を入れようとしても、既存のプラグインに対する互換性維持という足枷により思い切った変更ができないという、世間の他のアプリでもよく見かける問題かと思います。たとえば、かの有名な動画編集ソフトである AviUtlでも同様のコメントが挙げられていました。

miracle painter関連コミット

前述のような困難がある中、 yuntan_t氏は持ち前の若さ(?)でこつこつと miracle painter の GTK3対応を進めていってくれました。

前述の通り「MiraclePainterを表示させることが出来ました」という報告が 2019年10月。

その後、最終的な方針転換が行われたのが 2020年 6月の以下のチケットコメント?

実装方針を変更しました.

  • MiraclePainterGtk::ListBoxRowにする.
  • MessageMixinincludeしたDiva::Modelを描画したいときは,Gtk::ListBoxを使う.
  • タイムラインに表示されるWidgetと,モーダルウィンドウの中に表示されるWidgetは同じウィジェット
機能 #1399: MiraclePainterをmoduleとして再実装する - mikutter - やること

細かい内容については、「わかる人にはわかる」ということで代表的なコミットのログと差分を見ていただくのがよいかと思います。

上記の miracle paiter対応以外にも、GTK2→GTK3移行において誰もが直面するであろう非互換対応として、「設定」「抽出タブ」「アクティビティ」「mastodon」「アカウント切り替え」「subparts (ふぁぼられ、引用等の表示)」等々のmikutter本体付属の各種プラグインに対する修正も多数コミットされています。

ruby-gnome "virtual function" 対応

miracle painter の GTK3対応で一番の大きな課題、そして、今年9月以降の本格的な mikutter GTK3対応活動にもつながったトピックとして、 ruby-gnome の "virtual function対応" というものがあります。

「virtual function とは何か」ということを書こうとするといくら紙面があっても足りない(というか私自身もちゃんと理解していない)のですが、yuntan_t氏が 2020年 5月5日に起票した以下の ruby-gnome に対する pull request を引用するのが早いかと思います。

Rubyで定義したサブクラスから,GObjectのvirtual functionをoverrideする機能です.

機能追加の目的は,GTK3のカスタムウィジット(Gtk::Widgetのサブクラス)のheight-for-width対応で, GtkWidget#get_request_mode等のvirtual methodをoverrideするためです.詳細はCustom WidgetsCustom Containersにあります.

gi: add support for implementing virtual functions in Ruby by yuntan · Pull Request #1386 · ruby-gnome/ruby-gnome · GitHub

上記で引用されている Custom Widgets と Custom Containers の説明をそれぞれ引用すると以下:

Custom Widgets

By deriving directly from Gtk::Widget you can do all the drawing for your widget directly, instead of just arranging child widgets. For instance, a Gtk::Label draws the text of the label, but does not do this by using other widgets.
When deriving from Gtk::Widget, you should override the following virtual methods.

Custom Containers

When deriving a custom container widget directly from Gtk::Widget, you should override the following virtual methods:

要は、「アプリが直接ウィジェットをいじるときに必要となる、メソッドのオーバーライドのための手法」という感じ。

この「アプリが直接ウィジェットをいじる」というのは、前述の通りまさに「miracle painterがやっていること」にほかなりません。実際に GTK3対応版 mikutter で使用されている virtual function は plugin/gtk3/widget/miraclepainter.rb 内の以下の3つのメソッドのようです。

# override virtual function Gtk::Widget.get_request_mode
def virtual_do_get_request_mode

# override virtual function Gtk::Widget.get_preferred_width
def virtual_do_get_preferred_width

# override virtual function Gtk::Widget.get_preferred_height_for_width
def virtual_do_get_preferred_height_for_width(width)

この pull request に対し、 ruby-gnome のメンテナである kou氏からの返信は以下:

ありがとうございます!
これをベースにフックのかけ方とかAPIとか整理して仕上げますね。

ただ、私が最近立て込んでいて、数ヶ月単位で先になってしまうかもしれません。ごめんなさい。
このブランチにpushしていくのでこのまま残しておいてもらえますか?

gi: add support for implementing virtual functions in Ruby by yuntan · Pull Request #1386 · ruby-gnome/ruby-gnome · GitHub

ただ、上述の通りメンテナの kou氏が多忙であったこと、さらに、 pull request 自体にもコンセプトレベルの実装が残っている状態であったことから、 ruby-gnome 本体への virtual function 取り込みにはかなりの時間を要することになりました。

この状況の中、 yuntan_t氏は mikutter の gtk3 プラグインGemfile に以下のような黒魔術を記述して開発を進めていくことになります。

dev.mikutter.hachune.net

実際の Gemfile の記述は以下。最近になってこの記述に気づいたときは「こんなことできるのか!(していたのか!)」と、ちょっと驚きました。

source 'https://rubygems.org'

git 'https://github.com/yuntan/ruby-gnome', branch: 'hook-up-vfunc' do
  gem 'gtk3'
end

virtual function pull request マージと gtk2 gem 削除

前述の virtual function pull request と ruby-gnome リリースの経過を時系列で書くと以下:

  • 2020/5/2
    ruby-gnome 3.4.2 リリース
  • 2020/5/5
    virtual function pull request起票
  • 同日
    kouさんより:
    「ただ、私が最近立て込んでいて、数ヶ月単位で先になってしまうかもしれません。ごめんなさい。」 
    「このブランチにpushしていくのでこのまま残しておいてもらえますか?」
  • 2020/5/11
    ruby-gnome 3.4.3 リリースWindows版の小修正のみ。
  • 2020/7/18
    kouさんより
    「(本当に数ヶ月経ってしまっているんですが、今月来月中にはマージできるようにしたいなぁという気持ちがあります。)」
  • 2020/8/30
    kouさんの virtual function ブランチへのコミット
  • 2021/5/12
    ようやくマージ。kouさんより
    「1年経ってしまいましたがようやく作業しました。。。」
    「既存のコールバック呼び出し処理を使うようにしたので大体の引数で動きます。」
  • 2021/7/7
    ruby-gnome 3.4.5 リリース
    "This is a release that supports implementing virtual functions in Ruby."

pull request起票から苦節1年、 mikutter GTK3対応のための virtual function サポートがようやく ruby-gnome 本体に取り込まれました!

……が、上記 ruby-gnome 3.4.5 リリースの前、 2021/4/22 にリリースされた ruby-gnome 3.4.4 リリースノート の中で 、mikutter自身の未来を左右する大きな事件(?)が起きていました。

Ruby/GTK2

  • Removed.

mikutter GTK3対応のためには、 ruby-gnome側での virtual function対応は必須。

一方、2021年5月当時の mikutter GTK3対応はおそらく道半ばの状態。GTK2版と比べると実用とするにはまだまだブラッシュアップが必要な段階だったと思われます。

ruby-gnomeからしてみれば、世間動向として GTK2が obsoleteとなるのは間違いない中で、 「virtual function対応という大きな変更を GTK2に入れる工数を投入する価値はない」という判断がなされたのではないかと推測しています。

「mikutter GTK3対応のために GTK2版 mikutterの将来が閉ざされる」という難しい状況の中、私が担当している pkgsrc の mikutter (のための ruby gtk2 パッケージ)のほか、積極的(?)な mikutterサポートで有名な debian apt の mikutter (のための ruby-gtk2 パッケージ)の各管理者も大きな決断を迫られることになります。

次回のエントリでは、この debian ruby-gtk2 パッケージのツイートを発端とした「mikutter GTK3対応の最終ステージ」について書いてみたいと思います。なんとか 12/25 の mikutter 5.0 リリースに間に合わせたい