Redmine 6.1.0向けにガントチャートのサーバ側処理時間を40%短縮した


My Redmine

前田です。今年のゴールデンウィークに取り組んだことの一つはRedmineのガントチャートの表示速度改善です。ガントチャートは数千件のチケットを表示しようとするとサーバ側の処理で数秒かかることがあります。デフォルト設定では500件以上のチケットはガントチャートに表示されないので著しく遅くなることはありませんが、Redmineの「管理」→「設定」→「チケットトラッキング」画面の設定「ガントチャート最大表示件数」を大きな値に変更するとこの問題が顕在化します。


Redmineのガントチャート

今回の改善により、3000個のチケットと60個のバージョンがあるプロジェクトにおけるガントチャート表示のサーバ側の処理時間を約40%削減することができました。

以下はRedmine公式サイトに作成した、その改善に関するチケットです。

Feature #42663: Optimize Gantt chart rendering by reducing version-related queries

今回のブログでは、どのように改善を進めたのかその過程を紹介します。

改善結果:

3000個のチケットと60個のバージョンがあるプロジェクトにおいて /projects/ecookbook/issues/gantt の表示を5回実行してみたところ、サーバ側でガントチャートの画面(HTML)を生成するのにかかった時間は平均1900ミリ秒でした。

Completed 200 OK in 1903ms (Views: 1742.4ms | ActiveRecord: 149.7ms (2426 queries, 2132 cached) | GC: 128.0ms)
Completed 200 OK in 1910ms (Views: 1759.8ms | ActiveRecord: 144.4ms (2426 queries, 2132 cached) | GC: 130.7ms)
Completed 200 OK in 1899ms (Views: 1747.2ms | ActiveRecord: 146.0ms (2426 queries, 2132 cached) | GC: 129.1ms)
Completed 200 OK in 1914ms (Views: 1763.4ms | ActiveRecord: 145.0ms (2426 queries, 2132 cached) | GC: 130.2ms)
Completed 200 OK in 1877ms (Views: 1726.7ms | ActiveRecord: 144.8ms (2426 queries, 2132 cached) | GC: 129.3ms)

改善後は平均1174ミリ秒となり、処理時間がおよそ40%短縮されました。

Completed 200 OK in 1192ms (Views: 1051.1ms | ActiveRecord: 135.6ms (433 queries, 201 cached) | GC: 121.0ms)
Completed 200 OK in 1171ms (Views: 1019.2ms | ActiveRecord: 142.5ms (433 queries, 201 cached) | GC: 84.4ms)
Completed 200 OK in 1183ms (Views: 1027.8ms | ActiveRecord: 145.2ms (433 queries, 201 cached) | GC: 110.6ms)
Completed 200 OK in 1143ms (Views: 992.8ms | ActiveRecord: 139.9ms (433 queries, 201 cached) | GC: 81.0ms)
Completed 200 OK in 1182ms (Views: 1033.5ms | ActiveRecord: 141.9ms (433 queries, 201 cached) | GC: 111.1ms)

どのようなアプローチで改善に取り組むか決める

ガントチャートを表示するときには、Redmineのアプリケーションがガントチャートの完全なHTMLを生成してブラウザに送っています。HTMLの生成処理は lib/redmine/helpers/gantt.rb で行われているので、このファイルに記述されているコードが改善の第1候補になります。

ヒントを得るためにChatGPTに入力してみました。

結果、以下の候補が提案されました。

さて、これらのうちどれに取り組むかですが、改善項目1番目の is_descendant_of? メソッドと2番目の leaf? メソッドはすでに十分効率的であることを私は知っていますので検討対象から除外し、効果が「中」とされている「project_versions の呼び出し最適化」についてRedmineのコードを読んでみることにしました。

project_versions メソッドの最適化

lib/redmine/helpers/gantt.rb 内で定義されている projet_versions メソッドは、あるプロジェクト内のチケットにセットされているすべての対象バージョンを返すメソッドです。

プロジェクト内のすべてのチケットについてそのチケットの対象バージョンのオブジェクトを生成し、最後に uniq メソッドで重複するオブジェクトを取り除くという処理を行っていました。プロジェクト内のチケット数が多いと時間がかかりそうなコードです。

# Returns the distinct versions of the issues that belong to +project+
def project_versions(project)
  project_issues(project).filter_map(&:fixed_version).uniq
end

しかも、 project_versions メソッドはガントチャート描画中に複数回呼ばれるので、全体では相当なコストとなります。したがって、このメソッドの最適化は改善効果が大きいと判断しました。

そこで、以下の改善を行いました。

  1. 全チケットに対して fixed_version メソッドを呼んでVersionオブジェクトを生成してから重複排除するのではなく、単なる整数値である fixed_version_id を取得してから重複排除を行ってから一気に複数のVersionオブジェクトを生成する。これにより、データベースに対する複数のクエリを1回で済ませることができる
  2. メソッドの結果をキャッシュし、同じプロジェクトに対して project_versions メソッドが呼ばれたら即座に結果を返せるようにする

改善後のコードは以下のようになりました。

# Returns the distinct versions of the issues that belong to +project+
def project_versions(project)
  @project_versions ||= {}
  @project_versions[project&.id] ||= begin
    ids = project_issues(project).filter_map(&:fixed_version_id).uniq
    Version.where(id: ids).to_a
  end
end

Versionオブジェクトの生成回数の抑制

project_versions の見直し後、続いて Version オブジェクトが関係するほかのコードについて改善の余地がないか確認しました。すると、アソシエーションの参照によって以下のクエリが大量に発生していて、チケット数が非常に多い場合にパフォーマンスに大きな影響を与えていることがわかりました。

  Version Load (0.3ms)  SELECT `versions`.* FROM `versions` WHERE `versions`.`id` = 2 LIMIT 1
  ↳ lib/redmine/helpers/gantt.rb:235:in 'block (2 levels) in edmine::Helpers::Gantt#render_project'

該当のコードは Issue#fixed_version が存在するかどうかのみを調べていたので、わざわざ Version オブジェクトを生成しなくても fixed_version_idnil かどうかをチェックしさえすればデータベースのクエリを行わずに済みます。そこで、以下の変更を行いました。

変更前:

issues = project_issues(project).select {|i| i.fixed_version.nil?}

変更後:

issues = project_issues(project).select {|i| i.fixed_version_id.nil?}

そのほかの変更

これ以外にも gantt.rb 内で Version オブジェクトを扱う箇所を中心に細々とした変更を見つけました。これらの変更対象は development.log に出力されるクエリの記録を観察することで見つけました。

すべての変更内容はRedmineのリポジトリで確認できます。

ただ体感はあまり速くならない

ガントチャートの描画に関して、私の試験環境ではサーバ側の処理時間が約1.9秒から約1.2秒と大幅に短縮できましたが、それでも実は利用者が体感するレスポンスタイムは残念ながら大きくは変わりません。

というのも、3000件ものチケットがある状態だとRedmineからブラウザに送られるHTMLのサイズが6MBにもなり、ブラウザがそれを完全に描画するのに10秒近くかかるためです。これではサーバ側の処理が0.7秒程度短縮されたところでユーザーが感じるレスポンスタイムは大きくは変わりません。

今後ガントチャートのレスポンスタイムをさらに改善するためにはガントチャートの描画方式の抜本的な変更が必要です。

My Redmine

こちらの記事もオススメです!
資料「Redmineの意外と知らない便利機能」をRedmine 6.0に更新しました
「Redmineの以外と知らない便利機能」をおよそ3年ぶりに更新、Redmine 6.0対応になりました。
ファーエンドテクノロジーに入社して感じたこと
ファーエンドテクノロジーに入社しました。初めての環境であらゆることへの対応の早さに驚いています。
AWS認定ソリューションアーキテクト - アソシエイトを受験した話
AWS認定ソリューションアーキテクト–アソシエイト(SAA‑C03)に合格。学習で活用した教材を紹介します。
新しいスキャナで業務効率が上がりました
新しいスキャナを購入。スキャンする速度が速くなって業務効率が上がりました。
ポモドーロで見つけた私に効果的な時間管理方法
ポモドーロ・テクニックを活用して集中力と勉強効率を高めた実践例と工夫を紹介します。
ファーエンドテクノロジーからのお知らせ(2025/06/11更新)
RedMica 3.2 バージョンアップのお知らせ
My Redmineで提供しているソフトウェアをRedMica(ファーエンドテクノロジー版Redmine) 3.1 から 3.2 へバージョンアップいたします。
プロジェクト管理ツール「RedMica」バージョン 3.2.0をリリース Redmine互換のオープンソースソフトウェア
今日使える明日のRedmine「RedMica」の最新バージョン3.2.0をリリースしました。
2025年6月12日 オライリー本の全冊公開日のお知らせ(もくもく勉強会も同時開催)
ファーエンドテクノロジーが所蔵するオライリー本(全冊)公開日のご案内です。公開日には「もくもく勉強会」も同時開催します。
エンタープライズプラン向け「優先サポート」を開始
My Redmineでは、エンタープライズプランをご契約のお客様向けにサポート対応を優先的に行う「優先サポート(プライオリティサポート)」を開始いたしました。
Redmineの最新情報をメールでお知らせする「Redmine News」配信中
新バージョンやセキュリティ修正のリリース情報、そのほか最新情報を迅速にお届け