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

ククログ

«前月 最新 翌月»
タグ:

Unixシェルで手軽にデータを集計する (後編)

この記事は5月16日の記事「Unixシェルで手軽にデータを集計する (前編)」の続きです。

前回に引き続いて「コマンドの実行履歴を集計する」を素材に、 Unix環境でデータを集計する方法を解説していきたいと思います。

前回のおさらい

前回はhistoryで出力した生データから、awkで実行コマンドを抽出する所まで見ました。 前編の内容を思い出すために、次のコマンドを手元で打ち込んでみましょう:

$ history | awk '{print $2}'
ls
vim
ls
vim
mv
...

ご覧いただけるように、このコマンドでデータの余計な部分を省いて、 実行したコマンドの一覧だけを取り出すことができました。 この抽出されたデータから、コマンドごとの出現回数(lsがN回、vimがM回...)をカウントできれば、 当初の目的の統計を手に入れることができます。

問題は、この集計処理をどう実現するかです。

構成要素(2): sortとuniqによる集計

結論を先取りすれば、Unixの世界ではsortuniqという二つのプログラムを組み合わせて、 カウント処理を実現します。まずは実際に次のコマンドを実行してみましょう:

$ history | awk '{print $2}' | sort | uniq -c
 3 apt
 8 bundle
13 cat
32 cd
...

データが集計されて、コマンドごとの実行回数が出力されるのがご確認いただけると思います。 ここで重要なポイントは、なぜsortuniqという(一見すると)集計処理とは何の関係も無さそうなプログラムの組み合わせで処理を実現できるのか、です。

それぞれのコマンドを簡単に見てみましょう。まず、sortの方は比較的単純で、 名前の通り入力データを(原則はアルファベット順で)並びかえるプログラムです。

$ history | awk '{print $2}' | sort
apt
apt
apt
bundle
bundle
...

並び替えが(文字単位ではなく)行単位で行われることにご注意ください。

もう一つのuniqは、もともとはデータから重複を除去するためのプログラムです。 実際に、引数を何もつけずに実行すると、次のような出力が得られます。

$ history | awk '{print $2}' | sort | uniq

apt
bundle
cat
cd
...

最初の実行例で見た通り、このプログラムは -c / --count オプションを受けとり、 このオプションを指定すると、単に重複を除去するだけではなく、各行の出現回数も一緒に出力してくれます。 集計処理をsort | uniq -cというフィルタで実現できる理由はここにあります。

このuniqコマンドの重要なポイントは、このプログラムにはソート済みのデータを流し込む必要があるということです。 前にsortを付けているのはこのためで、実際、整列してないデータを流し込むと意味不明な結果が返却されます。

$ history | awk '{print $2}' | uniq -c
2 ls
1 vim
1 ls
2 vim
1 mv
...

なぜこうなるかと言うと、根本的な理由はuniqが動作する仕組みにあります。 枝葉を除いて幹の部分だけを取り出すと、uniqは非常に単純なアルゴリズムで実装されています。

  1. 入力データから次の一行を読みこむ。
  2. 行の内容が前の行と異なっていれば出力し、同じであればスキップする。
  3. (入力データの終点に到達するまで1と2を繰り返す)

もし入力データが整列されているならば、このアルゴリズムで重複を除去することができるのは明らかでしょう。 もちろん、少し工夫すれば「整列済み」の仮定を外した、万能の重複除去プログラムを作ることもできるのですが、 uniqプログラムの最初の作者(歴史をたどると"Unixの父"のケン・トンプソンのようです)はそうしませんでした。 なぜか?というと、これは筆者の想像になるのですが:

  1. このアルゴリズムだと直前の行だけ記憶しておけばいいので、メモリ使用量が格段に少ない。巨大なデータにもスケールする。
  2. Unixには他にもcommjoinといった整列済みのデータを扱うツールがあるので、ソートする部分だけ独立のプログラムに切り出すのが自然。

というところではないかと思います。実際、uniqは非常に高速に動作し、 テラバイトレベルのデータも問題なく処理できます。これは根幹のアルゴリズムの簡潔さのなせる技です。

構成要素(3): 上位N件を取り出す

ここまでの作業で得られた集計結果をランキングとして出力しましょう。

ここで再び登場するのがsortプログラムです。 今回は-nオプションをつけて(アルファベット順ではなく)数字の大小で並び替え、 それに加えて-rオプションで並び順を(昇順ではなく)降順にします。

単純に出力すると結果が多すぎて画面から溢れるので、上位10件だけ取り出します。 これにはheadというコマンドを使います。

$ history | awk '{print $2}' | sort | uniq -c | sort -rn | head
    345 vim
    159 python
    101 ls
     52 cd
     48 grep
     39 ll
     32 cat
     31 history
     26 git
     23 rm

これでコマンドの実行回数ランキングを出すことができました。

まとめ

この前後編の記事では、Unixシェルでデータを集計する方法を紹介しました。 このテクニックを応用すれば、例えば、次のようなタスクも手軽にこなせるようになります:

  • ApacheのアクセスログからIPアドレスごとのアクセス数を取り出す
  • SSHのログから失敗したログインの時間別統計をとる
  • 雑多なテキストデータから単語の出現頻度を数える

日常の様々な管理業務をより楽にできると思いますので、ぜひともご活用ください!

なお「もっと一般的なUnixの使い方を学びたい」という方は、弊社の結城が執筆している「シス管系女子」という連載がおすすめです。
現在、第3巻が発売されてますので、あわせてご参照ください。

タグ: Unix
2018-06-01

RubyKaigi 2018 - My way with Ruby #rubykaigi

RubyKaigiの2日目のキーノートスピーカーとして話した須藤です。今年もクリアコードはシルバースポンサーとしてRubyKaigiを応援しました。

関連リンク:

なお、RubyKaigi 2018に合わせてRabbit Slide Showをレスポンシブ対応したので、画面が小さな端末でも見やすくなりました。

内容

例年の内容よりキーノートっぽい内容にできるといいなぁと思って内容を考えました。最初は「インターフェイス」というテーマでまとめていたのですが、うまくまとまりませんでした。そのため、他の人と違う活動という観点でまとめてみました。その結果、「Rubyでできることを増やす」・「ライブラリーをメンテナンスする」という内容になりました。キーノートっぽかったでしょ?

この話を聞いて、私と同じように「Rubyでできることを増やす」・「ライブラリーをメンテナンスする」に取り組む人が増えるといいなぁと思ってこんな内容になりました。その取り組みの中で、Ruby本体をよくする機会もでてくるとさらにいいなぁと思っています。その気になった人はぜひ取り組んでみてください。

やりたいけどどこから始めればいいんだろうという人はRed Data Toolsに参加するのがよいでしょう。まずはチャットで相談したり東京での毎月の開発イベントに参加してください。

やりたくてお金はあるんだけど技術が足りない・時間が足りないという会社の人は、クリアコードにお仕事として発注してください。この話を聞いた人ならクリアコードに頼めば安心だと思ってくれるはず!ご相談は問い合わせフォームからどうぞ。

参考情報:

やりたいんだけど時間が足りないという人はクリアコードに入社して仕事としてやるのを検討するのはどうでしょうか。ただ、そういう仕事がないと仕事の時間ではできないですし、そもそも仕事がないと給料を払うのが難しいです。そういうことも考えた上でまだ選択肢としてよさそうならまずは会社説明会に申し込んでください。(このページの内容は少し古くなっているので更新しないといけない。。。。)

あ、そうだ、「クリアコードをいい感じにする人」として入社して、私が「Rubyでできることを増やす」・「ライブラリーをメンテナンスする」に使える時間を増やすという方法もあるかも。うーん、間接的すぎて微妙かな。。。

RubyData Workshop

RubyKaigi 2017に引き続き、RubyKaigi 2018でもRubyData Workshop(Data Science in RubyRed Data Tools Lightning Talks)を開催しました。Rubyでデータ処理したくなったでしょ?その気になった人はRed Data Toolsに参加して一緒に取り組んでいきましょう。

Data Science in Rubyの資料はRubyData/rubykaigi2018にあります。

Red Data Tools Lighting Talksの資料(の一部)は以下にあります。

なお、ワークショップのおやつのどら焼きはエス・エム・エスさんから提供してもらいました。ありがとうございます。

コード懇親会

Rubyは楽しくプログラムを書けるように設計されています。実際、RubyKaigiに参加するような人たちはRubyで楽しくプログラムを書いています。だったら、Rubyでコードを書く懇親会は楽しいんじゃない?というアイディアを思いつきました。それを実現する企画が「コード懇親会」です。実現にあたりSpeeeさんと楽天 仙台支社さんに協力してもらいました。ありがとうございます。

Speeeさんには運営や飲食物の提供などイベント開催のもろもろ、楽天さんには会場提供で協力してもらいました。参加者多数のため急遽定員を増やしたのですが、それにはSpeeeさんの飲食物の追加、楽天さんの机・椅子の追加がなければ実現できませんでした。

参加したみなさんは楽しんでくれたようです。興味がある人はアンケート結果を見てみてください。

参考情報:

懇親会の様子:

来年もあるかどうかはまだわかりません。「今回よかった!」と思った人はぜひインターネット上に思ったことなどをまとめてみてください。

今回はSpeeeさんに協力してもらいましたが、いろんなスポンサーが開催するようになるといいなぁと思っています。やりたい人・やりたい企業の方はぜひやってみてください。コード懇親会のリポジトリーのREADMEに説明がありますし、声をかけてもらえれば相談にのります。Speeeさんのコード懇親会レポートも参考にしてください。

なお、「コード懇親会」という企画をSpeeeさんが独占するよりもみんなで共有する方がSpeeeさんにとってメリットがあります。「最初に開催したのはSpeee」ということで名声が広まるからです。よさそう!と思った人はどんどん「コード懇親会」を開催してください。そのまま真似してもいいですし、アレンジを加えながら開催してもよいです。今回の実装を自由に使ってください。

リーダブルコードサイン会

3日目のAfternoon Breakのときにジュンク堂さんがサイン会をやっていました。通りかかったら長田さんに声をかけてもらったのでサイン会に混ぜてもらってリーダブルコードの解説にサインしていました。4,5冊売れました。「すでに持っている」という人の方が多かった気がします。いい本だから何冊あってもいいよね!

まとめ

RubyKaigi 2018でキーノートスピーカーとして話をしてきました。クリアコードは今年もシルバースポンサーとしてRubyKaigiを応援しました。

RubyData Workshop・コード懇親会・リーダブルコードサイン会のこともまとめました。

コード懇親会の進行のこともまとめようと思ったのですが、力尽きました。いつか、機会があれば。。。

タグ: Ruby
2018-06-04

Firefox 62で修正されたFirefox 61での後退バグに見る、不要なコードを削除する事の大切さ

一般的に、プログラムの継続的な開発においては「機能の追加」や「不具合の修正」が行われる事は多いですが、「機能の削除」が明示的に行われる事はそう多くないのではないでしょうか*1。この度、使われなくなっていた機能が残っていた事により意図しない所で不具合が発生し、機能を削除することで不具合が解消された事例がありましたので、使われなくなった機能を削除する事の意義を示す一時例としてご紹介いたします。

発生していた現象

現在のFirefoxでは、インストールするアドオンは必ずMozillaによる電子署名が施されていなくてはならず、未署名のファイルからはアドオンをインストールできないようになっています。ただ、それではアドオンの開発そのものができないため、開発者向けの機能であるabout:debuggingから、開発中のアドオンを「一時的なアドオン」として読み込んで動作させられるようになっています。

この機能について、Firefox 61で「条件によっては、アドオンを一時的なアドオンとして読み込めない」「条件によっては、一時的なアドオンの読み込みに非常に時間がかかる」という不具合が発生していましたが、Firefox 62で修正される事になりました*2

現象が起こり始めたきっかけと、原因の調査

開発中のアドオンを一時的なアドオンとして読み込めないという現象について、現象発生のタイミングを二分探索で絞り込むツールであるmozregressionを使って調査した所、1452827 - XPIInstall has a bunch of cruft that needs to be cleaned upでの変更が投入されて以降発生するようになっていた事が分かりました。

また、この現象が発生していた場面では、WSL*3で作成したシンボリックリンクがリポジトリ内に含まれている場合に、開発者用のコンソールにwinLastError: 1920という情報が出力されるという状況でした。これはWindowsが返すファイル関連のエラーコードのひとつであるERROR_CANT_ACCESS_FILEを意味する物で、Bugzilla上において、上記変更の中に含まれているファイルの情報を取得する処理がWSLのシンボリックリンクに対してこのエラーを報告している、という情報を頂く事ができました。

実際の修正

ファイルの情報を調べようとして何らかのエラーが発生するファイルというものは、WSLのシンボリックリンク以外にも存在し得ます。また、一般的に、規模が大きな変更は予想外の影響をもたらすリスクがあり、変更は可能な限り最小限に留める事が望ましいとされます。これらの理由から、当初は「ファイルの情報を取得しようとした時のエラーを適切にハンドルすることで、特殊なファイルがあっても問題が起こらないようにする」という方向でパッチを作成し提出しました。

しかしながら、このパッチはレビューの結果却下され、そもそもこの「ファイルの情報を取得する処理」自体が不要だからそれを削除する方がよいのではないかという指摘を受けました。そこで改めて調査したところ、以下の状況である事を確認できました。

  • この処理は、アドオンの総ファイルサイズを計算する過程で呼ばれている。
  • しかしながら、現行のFirefoxではアドオンのファイルサイズは画面上に表示される事が無く、また、ファイルサイズの情報が有効に使われる部分も存在していない。
    • 過去には使われていた機能のようだが、機能の改廃が進んだ結果、現在は「ファイルサイズの計算が正しいかどうか」を検証する自動テストの中で参照されるだけの機能になっていた。
    • 類似の情報として、自動更新を通じてダウンロードされたアドオンのインストールパッケージのファイルサイズという物もある。
      • こちらは現在も使われているが、今回問題となっている「アドオンの総ファイルサイズ」とは別の物である。
  • よって、アドオンの総ファイルサイズという情報自体が現在では無用となっている。

既にある機能や情報の削除は、その機能を使っている箇所が1つでも残っていれば不可能なため、影響範囲の調査は特に念入りに行う必要があります。今回の事例では、

  • Mozillaのスタッフの人から、使われていない機能を削除する方向での対応が望ましい旨のコメントが出ていた。
  • 実際に調査した限りでは、確かに目に見える部分や他の機能からは参照されていないという事の確認が取れた。

という2つの根拠があったため、思い切って、アドオンの総ファイルサイズの計算に関わるコードと、サイズ情報を参照している箇所*4を削除するという内容でパッチを再作成しました。その結果、パッチは無事に取り込まれ、Firefox 62ではこの問題が修正される事になりました。

また、このパッチが取り込まれた結果、「条件によって一時的なアドオンの読み込みに非常に時間がかかる」という別のBugにも影響が及んでいた事が後になって分かりました。こちらのBugは、開発中のアドオンのフォルダー配下にnode_modulesのようなフォルダーがあると*5、その配下の大量のファイルをスキャンするために時間がかかるようになってしまった、という物です。アドオンの総ファイルサイズの計算自体を行わなくした事により、棚ぼた的にこちらのBugも解消されたという状況でした。

ただ、実はその後、「WSLのシンボリックリンクのせいでエラーが発生してアドオンを読み込めない」という状況自体が今となってはほぼ発生しなくなっているという事も分かりました。具体的には、Bugの報告時にエラーの原因になっていたシンボリックリンクはWindows 10 Creators UpdateやWindows 10 Fall Creators UpdateのWSLで作成された物でしたが、その後Windows 10 April 2018 UpdateのWSLなどではNTFSの妥当なシンボリックリンクが作成されるようWSLが改良されたため、この問題の影響を受けるのは「古いWSLで(npm installを実行するなどして間接的に作成される場合も含めて)シンボリックリンクを作って、それをその後もずっと使い続けている場合」だけという事になっています。一般ユーザーに影響が及ばないアドオン開発者向けの機能であるという事に加え、プラットフォーム側の改善により今では問題自体が発生しなくなっている(新たにシンボリックリンクを作り直すだけでよい)という事も相まって、このパッチはFirefox 61には取り込まれないという判断がなされています。

まとめ

以上、Firefox 62で取り込まれたパッチを題材に、既に不要となっていた機能が問題を引き起こしていた事例についてご紹介しました。

Firefox 56およびそれ以前のバージョンのFirefoxにおいて使用できていた従来型アドオンは、今回削除されたコードのような「Firefox内部では既に使われなくなった機能」を使用している事が度々ありました。そのためFirefox全体として、アドオンが動作しなくなる事を警戒して古いコードを大量に抱え込まざるを得ない状態が長く続いていました。Firefox57での「従来型アドオン(XULアドオン)廃止」という大きな変化には、このような状態を是正するためという意味合いもあったと言えるでしょう。これを反面教師として、不要になった機能や実装はその都度速やかに削除してコードを簡潔に保っていくという事の重要さを実感していただければ幸いです。

*1 ただし、機能一式の刷新にあたって「新しい実装に持ち越されなかった機能」が結果的に「旧バージョンから削除された機能」として見える、という事はあります。

*2 一般ユーザーへの影響は小さいと判断された結果、Firefox 61への修正の反映は見送られています。

*3 Windows Subsystem for Linux

*4 大半は自動テスト内で、UI上に現れる物もFirefoxでは使われていないThunderbird用に残されたコードに1箇所あるだけでした。

*5 静的な文法チェックを行うためにeslintを使っている場合などに、このような状況が発生し得ます。

2018-06-07

Fluentd UIのFluentd v1対応のロードマップ

fluentd-uiというFluentdの設定を管理できるWebアプリケーションがあります。 Fluentd v1 がリリースされる前から機能の追加やFluentdの新しい機能への対応はされていませんでした。

手を入れる前は、以下のような状態でした。

  • Rails 4.2.8
  • Fluentd v0.12
  • Vue.js v0.11.4
  • sb-admin-v2
  • filter非対応
  • label feature非対応
  • systemd非対応
  • reload非対応
  • Fluentd v1に対応していないプラグインがおすすめプラグインに載っている

2018年4月中旬から手を入れ始めて、新しいバージョンをいくつかリリースしました。

  • v0.4.5
    • 約一年ぶりのリリース
    • Rails 4.2.10に更新
    • 使用しているgemを一通り更新
    • poltergeistからheadless chromeに移行
  • v1.0.0-alpha.1
    • Rails 5.2.0に更新
    • Fluentd v1のサポートを開始し、Fluentd v0.12以前のサポートをやめた
    • Vue.js v2.5.16 に更新
    • startbootstrap-sb-admin 4.0.0 に更新
    • JavaScript まわりを sprockets から webpacker に移行 *1
  • v1.0.0-alpha.2
    • v1.0.0-aplha.1で動かない箇所があったのを修正した

今後の予定は以下の通りです。

  1. Fluent::Config::ConfigureProxy#dump_config_definitionで得られる情報を利用して、設定UIを構築する
  2. 複雑な設定を持つプラグインは個別にフォームを作成し、使いやすくする
  3. owned plugin (parserやformatterなど)の設定方法について検討し、実装する
  4. テストの修正
  5. filterサポート
  6. label featureサポート
  7. おすすめプラグインの更新
  8. issuesの対応

3まで完了したらalpha.3かbeta.1をリリースする予定です。

*1 CSSはsprocketsのまま

タグ: Fluentd
2018-06-08

Groongaクラッシュ時のログの解析方法

Groonga周辺を便利にするツールもいろいろと作っている須藤です。

Groongaはクラッシュセーフではないので、クラッシュするタイミングによってはデータベースが壊れてしまうことがあります。データベースが壊れていそうかどうかはログを解析することで判断できます。この記事ではクラッシュ時のログを解析するためのスクリプトを紹介します。

インストール

クラッシュ時のログを解析するスクリプトはgroonga-query-log gemの中に入っています。このスクリプトを使うと以下のことがわかります。

  • いつクラッシュしたか
  • クラッシュしたときに実行していたクエリーはなにか
  • flushの変更コマンド(loadtable_createなど)(これらがあるとデータベースが壊れている可能性が高い)
  • 解析対象のログ中にある重要度が高いメッセージ
  • (クラッシュ時ではなく)正常終了時にメモリーリークしていたか

事前にRubyをインストールします。Rubyをインストールしたら以下のコマンドでインストールできます。

% gem install groonga-query-log

使い方

インストールするとgroonga-query-log-check-crashというコマンドが使えるようになります。これでクラッシュ時のログを解析すると前述のこと(いつクラッシュしたかなど)がわかります。

使う時は次のように通常のログ(--log-path ...を指定すると出力されるログ)とクエリーログ(--query-log-path ...を指定すると出力されるログ)を「すべて」指定します。結果が多くなることがあるので、標準出力をリダイレクトして結果をファイルに保存しておくと便利です。

% groonga-query-log-check-crash groonga.log* groonga-query-log* > analyze.log

指定する順番は気にしなくてよいです。ファイル名に含まれているタイムスタンプ情報を元にコマンド内部で自動で並び替えるからです。なお、タイムスタンプ情報のフォーマットはstrftime(3)で言うと%Y-%m-%d-%H-%M-%S-%6N%6Nstrftime(3)にはないけど、6桁のミリ秒のつもり)です。このフォーマットは--log-rotate-threshold-size--query-log-rotate-threshold-sizeを使ったときに使われているフォーマットなので、これらのオプションを使ってログローテーションしている場合はとくに気にする必要はありません。

(違うタイムスタンプ情報のフォーマットもサポートして欲しい場合は https://github.com/groonga/groonga-query-log/issues で相談してください。)

また、gzip・ZIPで圧縮されていても構いません。自動で伸張して解析します。

改めてまとめると、持っている通常のログ・クエリーログをすべて渡してください、ということです。

結果

コマンドを実行するとログを解析し、クラッシュを検出すると随時解析結果を出力します。ログが大きいほど時間がかかります。ログはストリームで解析しているので大量のログを解析してもメモリー使用量が比例して大きくなることはありません。

出力結果は特に決まったフォーマットになっているわけではなく(たとえばMarkdownでマークアップされているわけではない)、解析結果が1つずつ表示されるだけです。フォーマットは今後も変わっていきます。(改良していきます。)そのため、ここで現時点のフォーマットの説明はしません。

以下の情報がわかるような出力になっています。

  • いつクラッシュしたか
  • クラッシュしたときに実行していたクエリーはなにか
  • flushの変更コマンド(loadtable_createなど)(これらがあるとデータベースが壊れている可能性が高い)
  • 解析対象のログ中にある重要度が高いメッセージ
  • (クラッシュ時ではなく)正常終了時にメモリーリークしていたか

また、より詳しく知りたい場合はどのログファイルを確認すればよいかもわかるようになっています。

まとめ

Groongaがクラッシュしたときのログ解析を支援するツールがあることを紹介しました。Groongaを運用している人はぜひ活用してください。

ログの解析ロジックは私が手動で解析するときのロジックです。そのため、未対応なパターンも十分にありえます。未対応のパターンがあった場合は https://github.com/groonga/groonga-query-log/issues で教えてください。

タグ: Groonga
2018-06-11

WebExtensionsによるFirefox用の拡張機能で設定の読み書きを容易にするライブラリ:Configs.js

(この記事は、Firefoxの従来型アドオン(XULアドオン)の開発経験がある人向けに、WebExtensionsでの拡張機能開発でのノウハウを紹介する物です。)

XULアドオンでは、設定の保存や読み書きにはpreferencesという仕組みを使うのが一般的でした。これはFirefoxの基本的な設定データベースとなっているkey-value storeで、保持されている設定の一覧はabout:configで閲覧することができます。

一方、WebExtensionsベースの拡張機能の場合はこのような「設定を保存するための仕組み」は特に用意されていません。Indexed DB、Cookie、Web Storage APIなどのWebページ用の一般的な仕組みや、あるいはWebExtensionsのstorage APIのように、データを永続的に保存する様々な仕組みの中から好みの物を選んで使えます。とはいえ、選択肢が多すぎると却って判断に迷うもので、何らかの指針や一般的な方法があればそれに従っておきたい所でしょう。

一般的には、WebExtensionsベースの拡張機能では設定の保存先としてstorage APIが使われるケースが多いようです。storage APIはさらにstorage.localstorage.syncstorage.managedの3種類が存在し、Firefox Syncとの連携を考慮する場合はstorage.sync、そうでない場合はstorage.localが使われるという具合の使い分けがなされます。ただ、これらのAPIは非常に低レベルのAPIと言う事ができ、設定画面・バックグラウンドスクリプト・コンテントスクリプトの間での値の同期のような場面まで考慮すると、上手く整合性を保つのはなかなか大変です。

そこで、preferencesの代替として使いやすいAPIを備えた、設定の読み書きに特化した軽量ライブラリとして、Configs.jsという物を開発しました。

基本的な使い方

必要な権限

このライブラリは設定値をstorage APIで保存するため、使用にあたってはmanifest.jsonpermissionsの宣言にstorageを加える必要があります。

{
  ...
  "permissions": [
    "storage", 
    ...
  ],
  ...
}
設定オブジェクトの作成と読み込み

このライブラリは単一のファイルConfigs.jsのみで構成されており、読み込むと、その名前空間でConfigsという名前のクラスを参照できるようになります。実際に設定を読み書きするためには、これを使って設定オブジェクトConfigsクラスのインスタンス)を作る必要があります。具体的には以下の要領です。

var configs = new Configs({
  enabled: true,
  count:   0,
  url:     'http://example.com/',
  items:   ['open', 'close', 'edit']
});

設定のキーと既定値はこの例の通り、Configsクラスの引数に渡すオブジェクトで定義します。オブジェクトのプロパティ名が設定のキー、値が既定値になり、値はJSON形式が許容する物であれば何でも保持できます。

これをcommon.jsのような名前で保存し、以下のようにConfigs.jsと併せて読み込むようにします。

HTMLファイルから読み込む場合:

<script type="application/javascript" src="./Configs.js"></script>
<script type="application/javascript" src="./common.js"></script>

manifest.jsonで指定する場合:

{
  ...
  "background": {
    "scripts": [
      "./Configs.js",
      "./common.js",
      ...
    ]
  },
  ...
  "content_scripts": [
    {
      "matches": [
        "*://*.example.com/*"
      ],
      "js": [
        "./Configs.js",
        "./common.js",
        ...
      ]
    }
  ],
  ...
}

manifest.jsonの記述例から分かる通り、設定を読み書きしたい名前空間のそれぞれで個別にConfigs.jsと設定オブジェクトの作成用スクリプトを読み込む必要があります。

なお、storage APIを使う都合上、このライブラリはコンテントスクリプトのみで使う事はできません(コンテントスクリプトからはstorage APIにアクセスできません)。サイドバーやパネルなどを含まずコンテントスクリプトだけで動作する拡張機能である場合は必ず、設定オブジェクトのインスタンスを作成するだけのバックグラウンドページを読み込んでおいて下さい。こうする事で、コンテントスクリプト内で行われた設定の変更はバックグラウンドページ経由で保存され、逆に、保存されていた値はバックグラウンドページを経由してコンテントスクリプトに読み込まれる事になります。

保存された設定値の読み込み

各スクリプトを読み込んだそれぞれの名前空間では、設定オブジェクトのインスタンスが作成されると同時に、保存された設定値が自動的に読み込まれます。設定オブジェクトは値がPromiseであるプロパティ $loadedを持っており、このPromiseは設定値の読み込みが完了した時点で解決されます。例えば保存された設定値を使ってページの初期化処理を行いたい場合は、以下のようにする事になります。

window.addEventListener('DOMContentLoaded', async () => {
  await configs.$loaded;
  // ...
  // 読み込まれた設定値を使った初期化処理
  // ...
}, { once: true });
設定値の参照と変更

設定オブジェクトは、インスタンス作成時に指定された各設定のキーと同名のプロパティを持っており、プロパティの値が設定値となっています。$loadedのPromiseの解決後に各プロパティを参照すると、読み込まれたユーザー設定値または設定オブジェクト作成時の既定値が返されます。

console.log(configs.enabled); // => true
console.log(configs.count);   // => 0

また、設定値を変更するには、設定オブジェクトの各プロパティに値を代入します。

configs.enabled = false;
configs.count   = 1;

設定値は型情報を持ちません。初期値と異なる型の値を設定した場合、値は初期値と同じ型に変換されるのではなく、設定値の型のまま保存されます。例えば真偽値だった設定のconfigs.enabledに数値として0を代入した場合、次に取得した時に返される値はfalseではなく0となります。

値がObjectArrayである場合、以下の例のように必ず、元のオブジェクトの複製を作り、そちらを書き換えて、新しい値としてconfigsのプロパティに設定する必要があります。

var newEntries = JSON.parse(JSON.stringify(configs.entries)); // deep clone
entries.push('added item');
configs.entries = newEntries; // 設定値の変更

var newCache = JSON.parse(JSON.stringify(configs.cache)); // deep clone
newCache.addedItem = true;
configs.cache = newCache; // 設定値の変更

言い換えると、configs.entries.push('added item')configs.cache.addedItem = trueのように値のオブジェクトそのものを変更する方法では、変更結果は保存されませんvar entries = configs.entriesのように「値を参照した時に取得したオブジェクト」そのものを扱う場面では、値の変更前に必ずJSON.parse(JSON.stringify(entries))などの方法でディープコピーしてから変更するように気をつけて下さい。

また、未知のプロパティに値を設定した場合、その値は保存されません。設定のキーを増やしたい場合は、必ず設定オブジェクト作成時に既定値とセットで定義する必要があります。

設定値の変更の監視

設定値の変更は、ライブラリ自身によって各名前空間の間で暗黙的に通知・共有されます。if (configs.enabled) { ... }のように処理の過程で設定値を参照している場合、参照する時点で最新の設定値が返されますので、特に何かする必要はありません。

一方、「設定値が変わったらボタンのバッジを変更する」といった風に、設定値の変更を検知して何らかの処理を実行したい場合、設定オブジェクトの$addObserver()メソッドでオブザーバーを登録することができます。オブザーバーには関数を指定でき、第1引数として変更が行われた設定のキーが文字列として渡されます。以下は、関数(アロー関数)をオブザーバーとして登録する例です。

configs.$addObserver(aKey => {
  const newValue = configs[aKey];
  switch (aKey) {
    case 'enabled':
      ...
    case 'count':
      ...
  }
});
Firefox Syncで同期する設定、同期しない設定

初期状態では、設定オブジェクト作成時に定義した各設定はFirefox Syncでは同期されません。同期の対象にするには、「同期したい設定のキーの一覧」あるいは「同期させたくない設定のキーの一覧」を設定オブジェクト作成時にオプションで指定する必要があります。

Configsクラスは第2引数として各種オプションの指定のためのオブジェクトを受け取ります。基本的には設定を同期せず、一部の設定のみを同期したいという場合、同期したい設定のキーの配列をsyncKeysオプションとして指定します。

var configs = new Configs({ /* 既定値の指定 */ }, {
  syncKeys: [
    'enabled',
    'url',
    'items',
  ]
});

このように指定すると、syncKeysに列挙された設定のみFirefox Syncで同期されるようになります。

また、基本的には設定を同期し、一部の設定のみを同期対象外にしたいという場合、同期したくない設定のキーの配列をlocakKeysオプションとして指定します。

var configs = new Configs({ /* 既定値の指定 */ }, {
  localKeys: [
    'count',
  ]
});

このように指定すると、localKeysに列挙されなかったすべての設定がFirefox Syncで同期されるようになります。

Firefox Syncで同期された設定は、その実行環境で保持されたユーザー設定値よりも優先的に反映されます。 ただし、システム管理者によって定義された値がある設定は、それが最も優先されます。

システム管理者が設定を指定できるようにするには

企業等の組織でアドオンを使用する場合、システム管理者が設定を指定し固定したい場合があります。普通のアドオンでそのような事をしたい場合には、アドオンの中に書き込まれた既定値を書き換えた改造版を作る必要がある場合が結構ありますが、Configs.jsを使って設定を管理しているアドオンでは、そのような改造をせずとも、システム管理者が任意の設定値を指定できます。

システム管理者が設定値を指定するには、Managed Storageマニフェストという特殊なマニフェストファイルを作成し、Windowsではさらにレジストリに情報を登録する必要があります。Windows以外のプラットフォームでは、特定の位置にファイルを配置するだけですConfigs.jsを使用しているアドオンの一つであるIE View WEを例として、設定の手順を説明します。

まず、以下のような内容でJSON形式のファイルを作成します。

{
  "name": "ieview-we@clear-code.com",
  "description": "Managed Storage for IE View WE",
  "type": "storage",
  "data": {
    "forceielist"      : "http://www.example.com/*",
    "disableForce"     : false,
    "closeReloadPage"  : true,
    "contextMenu"      : true,
    "onlyMainFrame"    : true,
    "ignoreQueryString": false,
    "sitesOpenedBySelf": "",
    "disableException" : false,
    "logging"          : true,
    "debug"            : false
  }
}

nameにはアドオンの識別子(WebExtensionsでの内部IDではなく、Mozilla Add-onsに登録する際に必要となるIDの方)を、descriptionには何か知らの説明文を、typeにはstorageを記入します。設定値として使用する情報はdataに記述し、記述の仕方はnew Configs()の第1引数に指定する既定値と同様の形式です(JavaScriptではなくJSONなので、キーは明示的に文字列として書く必要がある点に注意して下さい)。

内容を準備できたら、ファイル名を(アドオンの識別子).jsonとして保存します。この例であればieview-we@clear-code.com.jsonとなります。

次に、Windowsではレジストリの HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\ManagedStorage\(アドオンの識別子)またはHKEY_CURRENT_USER\SOFTWARE\Mozilla\ManagedStorage\(アドオンの識別子)の位置にキーを作り、「標準」の値として先のJSONファイルのフルパスを文字列型のデータとして保存します。キーの位置は32bit版Firefoxでも64bit版Firefoxでも同一である(WOW6432Node配下ではない)という事に注意して下さい。 LinuxやmacOSでは、MDNに記載があるパスのディレクトリ配下にJSONファイルを置くだけで充分です。

当然ですが、管理者でないユーザーがファイルを書き換えて設定を変更してしまう事がないように、このJSONファイルは一般ユーザーでは読み取り専用・ファイルの書き込みを禁止するようにアクセス権を設定しておく必要があります。

このようにして管理者が設定値を定めた項目は、Configs.jsで参照する時は「ロックされた」状態になり、値を変更できなくなります*1

まとめ

以上、Firefox用のWebExtensionsベースのアドオンにおける設定の読み書きを容易にするライブラリであるConfigs.jsの使い方を解説しました。

XULアドオンでの感覚に近い開発を支援する軽量ライブラリは他にもいくつかあります。以下の解説も併せてご覧下さい。

*1 値を設定しても特に例外等は発生しませんが、変更の内容は無視されます。

タグ: Mozilla
2018-06-12

GNOME環境で作成したパスワード付きZIPが解凍できない時は

p7zipパッケージをシステムから取り除くと、 解凍できるZIPファイルが作れるようになるかもしれません。

こんにちは、クリアコードの藤本です。今回の記事では:

  1. GNOME標準のアーカイバでパスワード付きZIPファイルを作成したら、
  2. Windowsのエクスプローラやunzipコマンドで開けなかった。

という方向けに、この問題の原因と対策をお伝えしたいと思います。

問題の要点

GNOMEには標準のアーカイバとして「File Roller」というプログラムが付属しています。 普段意識して使うことは少ないかもしれないのですが、例えば、 Nautilusの「ファイルを圧縮する」メニューはこのプログラムが提供しており、 意外と多くの場面で利用されています。

今回取り上げる問題は、このプログラムでパスワード付きZIPファイルを作成すると、 他のプログラムで解凍できなくなることがある、というものです。 実際に、私の環境でFile Rollerを使ってパスワード付きZIPファイルを作成し、 unzipコマンドで解凍しようとすると、次のような警告が表示されます:

$ unzip  password.zip
Archive:  password.zip
   skipping: test.txt               need PK compat. v5.1 (can do v4.6)

また、このZIPファイルをWindows 10のエクスプローラで展開しようとすると、 例外が発生して処理が中断してしまいます。

予期しないエラーのため、ファイルをコピーできません。このエラーが再発する場合は、
エラーコードを利用して、この問題についてのヘルプを検索してください。

エラー 0x80004005: エラーを特定できません。
何が原因なのか?

この現象の本質的な原因は、File Rollerが「AES-128」でファイルを暗号化することにあります。

まず話の前提として、ZIPファイルの暗号化方式について簡単に整理します:

  1. もともと、ZIPには(PKZIP v1.0の時代から)単純なストリーム暗号が備え付けられており、 これが2018年現在も最もよく使われる暗号化方式となっています。

  2. しかし、この方式は暗号としては非常に弱く、 比較的簡単に突破できることが既に20年以上前から知られています。

  3. このため、「もっと強固な暗号化方式を導入して、セキュリティの向上を図ろう」という改良が、 いくつかのグループで進められており、その改良案の中でも現在最も有力なのが、 WinZipが2003年に導入したAESベースの暗号化仕様です。

  4. ただし「有力」とは言っても、他の実装の追従は極めて遅く、 公開から15年近く経った今でも、多くのZIPアーカイバはこの暗号化仕様をサポートしていません。

ここで、File Rollerに話を戻すと、 このプログラムは『7-Zipがインストールされている時は、AES-128を利用して暗号化する』 という実装になっています。システムに7-Zipが見つからない場合は、伝統的な暗号化方式が使われます。 File Rollerで生成したZIPファイルが、他のアーカイバで解凍できなくなったりする原因はここにあります。 要するに、実行時のシステムの状態によって、適用される暗号アルゴリズムがバックグラウンドで切り替わるのです。

実際に、先ほど解凍できなかったZIPアーカイブを検査してみると、 格納されているファイルがAES-128で暗号化されていることが確認できます:

$ 7z l -slt password.zip | grep Method
Method = AES-128 Store
暫定的な対応策

結論を先に述べると、File Rollerの暗号化方式を互換性のある旧方式に戻したい場合、 ユーザーには「7-Zipをアンインストールする」という選択肢しか現状はありません。 というのも、前節で解説した処理はソースに(暗号化アルゴリズムの選択含めて) ハードコードされており、設定やオプションで変えられる仕組みになっていないからです。

# Red Hat系なら `dnf remove p7zip`
$ sudo apt remove p7zip-full

参考までに、以下に該当するソースコードの箇所を示します:

もちろん、適切な環境があれば、 該当箇所を変更した独自ビルドを作ることで問題を迂回することもできます。 しかし、手元でコードをいじって回避するよりは、 アップストリームで根本から問題を解決したいものです。

根本的な解決に向けて

そこで私たちは、今回の問題についてGNOMEプロジェクトに修正パッチを投げています。

https://gitlab.gnome.org/GNOME/file-roller/merge_requests/1

ただし、このパッチがそのまま取り込まれることはさほど期待しておらず、 これを起点に議論を進めていきたい、というのが基本的な立場です (というのも、最終的な解決までに議論が必要な論点がいくつかあるためです)。 詳細はパッチに付けた説明をご参照ください。

もし読者の皆様の中で、「これは」という意見をお持ちの方がいらっしゃれば、 ぜひとも上のスレッドにコメントをお寄せください。 皆様のご意見をお待ちしています。

2018-06-13

リーダブルなコードを目指して:コードへのコメント(1)

リーダブルコードの解説を書いた須藤です。

リーダブルコードの解説を読んだ大木さんから次のような連絡をもらいました。

私はプログラマーなのですが、まだまだ未熟で、人が読めるコードを書くことができません。 本の内容を意識しコーディングしてみましたが、自信がありません。 そこで、添削していただきたいと考え、メールを送りさせていただきました。

リーダブルコードの解説に「私にコードを見せてみるといい」と書いていたので連絡してくれたとのことです。解説を書いてから6年経ちますがこのような連絡をもらったのは初めてです!うれしいですね。

大木さんから提供してもらったコードは https://github.com/yu-chan/Mario です。そこそこ量があるので何回かに分けて少しずつコメントします。

なお、私と大木さんだけでやりとりしてコメントをするのではなく、誰でも参照できるところでコメントするのは大木さんと合意しています。できるだけ多くの人と情報を共有したいという私の希望もある(リーダブルコードの解説がCreative Commonsなのも同じ理由)のですが、大木さんにとっても私以外の人からもコメントをもらえる機会がある方がよいと考えたからです。ということで、この記事を読んだ人で興味がある人はコメントしてみてください。この記事のように文章でまとめて https://github.com/yu-chan/Mario/issues で大木さんに連絡するのもよいですし、リポジトリー上のコードに直接コメントするのでもよいでしょう。

今回のコメントに関するやりとりをするissueは https://github.com/yu-chan/Mario/issues/1 です。

注意点は次の通りです。

  • 悪いところ探しではない
    • 目的はどんなコードがリーダブルだろう?どうしてリーダブルなんだろう?を共有することです。既存のリーダブルなところを見つけてそれを明文化することはそのためにすごく大事なことです。
    • 悪いところ探し「だけ」をしたい人はコメントしない方がよいでしょう。
  • 自分の考えを押し付けることは大事ではない
    • リーダブルかどうかは読む人によって変わります。「自分」がリーダブルなら他の人もリーダブルであるべきだ、ということではありません。
    • コメントする中でいろんな視点に気づき、いろんなリーダブルがあることを知ることが大事です。それは自分がリーダブルなコードを書くときにも使えますし、チームのリーダブルを見つけるときにも使えるはずです。
    • 自分の考えを押し付けたい人はコメントしない方がよいでしょう。

README

私はこのコードの前提知識がないので、まずはREADMEから確認します。

トップレベルのREADME.mdは次のようになっています。

"# Mario" 

どこから手を付ければよいかのヒントを期待したので期待と違いました。

トップレベルには次のファイル・ディレクトリーがありました。

  • Mario/
  • Mario.sln
  • README.md

拡張子が.slnのファイルがあるのでVisual Studioでビルドするんだな、ということがわかります。README.md.slnのファイルを除くとあとはMario/ディレクトリーしかないので、このディレクトリーを見てみましょう。

このディレクトリーの下のファイルの拡張子の多くは.cpp.hです。C++で実装していることがわかります。このディレクトリーにもREADMEがあるので見てみましょう。

1. 開発環境

OS       : Windows7/10
(ゲーム作成したときの環境に7、10を使用していた。
もしかしたら8でもいけるかもしれない。)
OSビット : 64ビット
IDE      : Visual Studio Express 2012 for Windows Desktop

まず開発者が使っている環境が書いてあります。これはすごくよいですね!この環境なら動くということがわかるので、読む人にとって非常に助かる情報です。

また、「もしかしたら8でもいけるかもしれない」と「やっていないこと」についても書いているのがよいです。コードのコメントに書くときもそうですが、「やっていないこと」についての情報は有用なことがあります。「やっていない」条件で使わないといけなくなったときにどう対応すればよいかの指標になります。「いけるかもしれない」であれば、「動くなら動いた方がよい」ということがわかります。この場合なら、Windows 8で動くことを確認できたらここの記述を更新すればよいし、動かなくても簡単に動くようにできそうなら頑張るということがわかります。

「やっていないこと」と同じように「やらないこと」もあれば書いてあると読む人の役に立つことがあります。たとえば、「Windows 8はサポートしない」と書いていれば、Windows 8用の作業はしないと判断できます。

READMEのこれ以降の説明を読むとDXライブラリを使っていることがわかりました。DXライブラリのURLも書いているのでDXライブラリのサイトにすぐにアクセスできました。サイトには次のような説明があるのでゲーム開発用の便利ライブラリーだということがわかりました。

DirectXを使ったWindowsデスクトップアプリの開発に必ず付いて回るDirectXやWindows関連のプログラムを使い易くまとめた形で利用できるようにしたC++言語用のゲームライブラリ

コードの構成の説明は特にないのでメイン関数(プログラムの処理を始める関数)から見ていくことにします。手元で動かせるならまずは動かすのですが、私の環境はWindows 7/10ではないので今回は動かさずにコードを見るだけにします。

メイン関数

どこにメイン関数があるか探し方はいろいろあります(たとえばmainで検索する)が、ファイルを眺めていたらmain.cppという名前のファイルがあったのですぐにわかりました。よい名前ですね。

メイン関数をmain.cppという名前をつけるのはよくあるやり方です。よくあるやり方に合わせるのは理解しやすくなる人が増えるのでリーダブルという観点でよいやり方です。

他によくあるやり方は実行ファイルと同じ名前を使うやり方です。今回はMario.exeができると思うのでMario.cppという名前にするという感じです。

main.cppの中は次のようになっていました。上から順に見ていきましょう。

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
	ChangeWindowMode(TRUE);
	SetWindowSize(WINDOW_WIDTH, WINDOW_HEIGHT);

	if(DxLib_Init() == -1) {
		return -1;
	}
	SetDrawScreen(DX_SCREEN_BACK);

	srand((unsigned)time(NULL));

	//フレームレート制御
	if(!Framerate::instance()) {
		Framerate::create();
	}
	if(!SoundManager::instance()) {
		SoundManager::create();
	}
	if(!Sequence::Parent::instance()) {
		Sequence::Parent::create();
	}

	while(!InputInterface::isOn(KEY_INPUT_Q)) { //Qを押したら終了
		if(ProcessMessage() != 0) {
			break;
		}
		InputInterface::updateKey();
		Framerate::instance()->update();
		ClearDrawScreen();

		//ゲーム開始
		Sequence::Parent::instance()->update();

		ScreenFlip();
		Framerate::instance()->wait();
	}

	Sequence::Parent::destroy();
	SoundManager::destroy();
	Framerate::destroy();
	DxLib_End();

	return 0;
}

最初の行で「ん?」と思いました。

	ChangeWindowMode(TRUE);

なぜ「ん?」と思ったかというと、「TRUEだとどんなモードということなのか」ということがわからなかったからです。ChangeWindowMode()について調べるとDXライブラリが提供している関数でした。DXライブラリには「ウインドウモード」と「フルスクリーンモード」というモードがあるらしく、ChangeWindowMode(TRUE)は「ウインドウモードにする」ということでした。

説明を読んだ結果、「この行はDXライブラリのことを知っていればわかるんだろうなぁ」と思いました。おそらく、このコードを読む人はDXライブラリのことを知っているべきだと思う(前提知識としてもよさそう)のでこのままでもよいと思いました。(前提知識によってリーダブルなコードがどんなコードかは変わってくるので前提知識をどこに置くかは大事です。)

ただ、もしDXライブラリのAPIが次のようになっていればDXライブラリのことを知っていなくてもわかりそうだなぁと思いました。

	UseWindowMode();

もし、DXライブラリをすでに使っている人でも同じような気持ちになるなら、DXライブラリにフィードバックしてよりよいAPIを模索するのがよさそうだなぁと思います。

DXライブラリを知らない人でも「ん?」と思いにくくするならアプリケーション内でUseWindowMode()という名前の関数を定義してそれを使うという方法があります。こうすればUseWindowsModeという名前から「あぁ、ウインドウモードというものを使うんだな」ということがわかります。「ウインドウモード」というのがわかりにくそうなので、DisableFullScreenとして「あぁ、フルスクリーンじゃない(普通のウインドウを使う)んだな」というコードにするのもよいでしょう。

static int UseWindowMode() {
	return ChangeWindowMode(TRUE);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
	UseWindowMode();
	// ..
}

それでは次の行を見てみましょう。

	SetWindowSize(WINDOW_WIDTH, WINDOW_HEIGHT);

これもDXライブラリが提供する関数で、ウインドウモードの設定をするものでした。DXライブラリを知らない人でもわかりやすくするならこれも合わせて次のようにするとよいと思います。

static void InitializeWindowMode() {
	ChangeWindowMode(TRUE);
	SetWindowSize(WINDOW_WIDTH, WINDOW_HEIGHT);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
	InitializeWindowMode();
	// ..
}

次のようにコメントで補足する方法もありますが、今回のケースでは関数に切り出す方がよいでしょう。関数に切り出すと読む人は詳細を知らなくてもよくなるからです。「あぁ、まずウインドウモードというやつを初期化するんだなー」くらいの理解でよいなら関数に切り出した方がよいです。「まずウィンドウモードというやつを初期化するんだな。そして、それはこういう内容なんだな。」というところまで知っておいた方がよいなら切り出さずにWinMain()の中に書いておいた方がよいです。

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
	// ウインドウモードの初期化
	ChangeWindowMode(TRUE);
	SetWindowSize(WINDOW_WIDTH, WINDOW_HEIGHT);
	// ..
}

SetWindowSize()の引数を640, 480と書かずに、WINDOW_WIDTH, WINDOW_HEIGHTと書いているのはよいですね!関数名がSetWindowSizeなので引数が幅と高さだろうということはすぐにわかるので640, 480でもそんなに悪くないのですが、「他にもウインドウサイズに関係している処理があったらどうしよう。ここの値だけを変更するのでは変更漏れがあるかも。」という気持ちになるので、値に名前をつけておくとリーダブルになります。

リーダブルかどうかの判断基準の大きな部分は「そのコードがなにをやっているかがすぐにわかるかどうか」ですが、「そのコードを読んでもやっとしないかどうか」も大事な要素です。「もやっとする」と他のところが気になってしまって集中できません。そうすると次のコードに進みにくくなってリーダブル度が下がってしまいます。

まとめ

リーダブルコードの解説を読んで「自分が書いたコードにコメントして欲しい」という連絡があったのでコメントをはじめました。

まだ、READMEとメイン関数の2行目までしかコメントしていませんが、引き続きコメントしていきます。「リーダブルなコードはどんなコードか」を一緒に考えていきたい人はぜひ一緒にコメントして考えていきましょう。

2018-06-14

apitraceを使ったOpenGL ESが絡むFirefoxのデバッグ方法

はじめに

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

当プロジェクトでは移植コストを少しでも低減するために、Firefoxの延長サポート版(ESR)のみを対象としています。これまではESR45やESR52をベースにハードウェアアクセラレーションに対応させるための作業を行ってきました。 現在はESR60に対応し、そのバグを修正する作業を進めています。

この作業に関連するOpenGL ESのデバッグをする必要が生じたのでその方法を解説します。 今回のデバッグにはapitraceというOpenGLに関するAPIの呼び出しを取得できるツールを使いました。このツールを元に今回のデバッグ方法を解説します。

OpenGLのAPIの呼び出しをトレースするには

apitraceを用いてOpenGLのAPIの呼び出しをトレースするには、以下のようにして行います。

$ apitrace trace --api=egl --output=/tmp/dump.trace [トレースするプログラム]

例えば、FirefoxのOpenGL ESのAPIの呼び出しのトレースを取得するには以下のようにすると取得できます。

$ apitrace trace --api=egl --output=/tmp/dump.trace firefox

OpenGLのAPIの呼び出しのダンブを解析するには

apitraceには採取したダンプを解析する機能もあります。

$ apitrace dump /tmp/dump.trace

とすると、OpenGL ESのAPIの呼び出しを記録したダンプの解析を行えます。 例えば、Firefoxを用いてOpenGL ESのAPIの呼び出し結果を取得し、その結果を解析すると以下のような出力が得られます。

// process.name = "/usr/lib/firefox/firefox"
0 eglGetDisplay(display_id = 0xb6921120) = 0x1
1 eglInitialize(dpy = 0x1, major = NULL, minor = NULL) = EGL_TRUE
9 eglChooseConfig(dpy = 0x1, attrib_list = {EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_NONE}, configs = {0x2, 0x1, 0x3}, config_size = 64, num_config = &3) = EGL_TRUE
14 eglCreateWindowSurface(dpy = 0x1, config = 0x2, win = 0x9d1e7af0, attrib_list = {}) = 0x9ce7d000
15 eglBindAPI(api = EGL_OPENGL_ES_API) = EGL_TRUE
16 eglCreateContext(dpy = 0x1, config = 0x2, share_context = NULL, attrib_list = {EGL_CONTEXT_MAJOR_VERSION, 2, EGL_NONE}) = 0x9d1638b0
140 eglGetCurrentContext() = NULL
141 eglMakeCurrent(dpy = 0x1, draw = 0x9ce7d000, read = 0x9ce7d000, ctx = 0x9d1638b0) = EGL_TRUE
# ...
27660 glDeleteProgram(program = 1050015)
27661 eglGetCurrentContext() = 0x9d1638b0
27662 glDeleteProgram(program = 840012)
27663 glBindFramebuffer(target = GL_FRAMEBUFFER, framebuffer = 0)
27664 glDeleteBuffers(n = 1, buffers = &140002)
27665 glDeleteBuffers(n = 1, buffers = &70001)
27666 eglGetCurrentContext() = 0x9d1638b0
27667 eglGetCurrentContext() = 0x9d1638b0
27668 eglDestroyContext(dpy = 0x1, ctx = 0x9d1638b0) = EGL_TRUE
27669 eglMakeCurrent(dpy = 0x1, draw = NULL, read = NULL, ctx = NULL) = EGL_TRUE
27670 eglDestroySurface(dpy = 0x1, surface = 0x9ce7d000) = EGL_TRUE

この結果を見ると、FirefoxがEGLを使用する際には、EGLDisplayを取得し、EGLのコンフィグを元にしてEGL WindowSurfaceを作成してからようやくEGLのコンテキストが動き出していることが記録されていることがわかります。

反対に、終了時には動いているEGLのプログラムを片付けてから現在のEGLコンテキストの破棄、EGL WindowSurfaceの破棄を行なっていることが読み取れます。

apitraceを用いるとOpenGL ESのAPIの呼び出しが成功しているのか、失敗しているのかがある程度判別できます。

14 eglCreateWindowSurface(dpy = 0x1, config = 0x2, win = 0x9d1e7af0, attrib_list = {}) = 0x9ce7d000

の箇所を例にとると、上記の呼び出しでは正常にWindowSurfaceが作成されていることが読み取れますが、

14 eglCreateWindowSurface(dpy = 0x1, config = 0x2, win = NULL, attrib_list = {}) // incomplete

となってしまっている場合は eglCreateWindowSurface の呼び出し直後に正常の動作ではない状態になっています。

実際に、このような状況になってしまった時はfirefoxがSEGVしました。

まとめ

FirefoxのOpenGL ESのAPIの呼び出しのダンプを取ることを例にしてapitraceの基本的な使用方法を解説しました。 OpenGL (ES)はハードウェアが絡むためログを仕込むような伝統的なデバッグ手法によるデバッグが難しく、このようなツールに頼ることで別の視点からの情報が得られることが分かりました。 グラフィックに関するデバッグで困っている場合はこのようなツールの助けを借りることも検討してみてはいかかでしょうか。

つづき: 2018-07-02
タグ: Mozilla
2018-06-15

Groongaのクエリーログを手軽に再生する方法

Groongaを使っていて問題が起きた時に、問題を再現させるために、問題が起きた時に実行していたクエリーを手元の環境でも実行したくなります。 実行するクエリーが少ない場合(1つか2つくらい)であれば、手作業で実行してもそんなに苦ではありませんが、実行するクエリーが大量にある場合は、手作業では限界があります。

Groongaには、groonga-query-log という便利なツールがあります。 この groonga-query-log には、クエリーログを手軽に再生するスクリプトが含まれています。 このスクリプトを使うことで、クエリーログに記載されているクエリーをログに記録されている順番で再生することができます。つまり、クエリーログさえあれば、簡単にログに記録されているクエリーを実行できます。

Groongaのクエリーログは、以下のようにGroongaを起動する際に--query-log-pathを指定することで、指定されたパスにクエリーログを作成できます。 (以下の例ですと、ホームディレクトリの直下にgroonga.query.logという名前でクエリーログができます。)

% groonga --query-log-path ~/groonga.query.log ~/db/db

ちなみに、groonga-query-log には、今回紹介するクエリーログを手軽に再生するスクリプトだけではなく、先日このブログで紹介されていた、クラッシュ時のログを解析するスクリプトや スロークエリーを特定するスクリプト、回帰テストを実行するスクリプト等、様々なスクリプトがありますので、興味が湧いたら他の機能についても使ってみて下さい。

クエリーログを再生するスクリプトの使い方は以下の通りです。

事前にRubyをインストールします。Rubyをインストールしたら以下のコマンドで groonga-query-log をインストールします。

% gem install groonga-query-log

次は、再生するクエリーを実行するためにデータベースを準備します。

ログを取得したマシンのデータベースが使える場合は、そちらを使うと新たにデータベースを準備しなくてすみますが、問題が起きたときには問題が発生したデータベースは使えないこともあるので、問題が発生したデータベースのダンプ等から、新しく手元の環境でデータベースを生成するほうが多いかもしれません。

今回の例では、以下のコマンドで、データベースのダンプから、新しくデータベースを作成します。

% groonga --file dump.grn -n ~/testdb/db 

次は再生するクエリーを実行するGroongaを起動します。Groongaはサーバーモードで起動します。プロトコルは、gqtp, httpのどちらでも問題ありません。 例えば、httpプロトコルを使用する場合は、以下のように起動します。

% groonga -s --protocol http ~/testdb/db

Groongaをサーバーモードで起動したら、準備完了です。 以下のように、 groonga-query-log-replay コマンドを実行して、クエリーログを再生できます。

% groonga-query-log-replay  --n-clients=1 --output-responses=./response.log replay-query.log

--n-clients はクエリーを実行するクライアント数を設定できます。 --output-responces は実行したクエリーの結果を保存するファイルを指定します。最後に、再生するクエリーログ(上の例では、replay-query.log)を指定して実行します。

これで、指定したクエリーログに記録されているクエリーが実行され、実行結果が、--output-responsesで指定したファイルに記録されます。

デフォルトでは、localhostの10041ポートに接続しますが、--host オプションと --port オプションを使うことで、それぞれ変更することができます。

つづき: 2018-06-26
タグ: Groonga
2018-06-19

Groongaで遅いクエリーを手軽に特定する方法

Groongaを使っていると、時々他の検索と比べて応答が遅い検索があることがあります。 そういった時は、ボトルネックを改善するために、どのクエリーの応答がどのくらい遅いのかを測定したくなります。

Groongaには、groonga-query-logという便利なツールがあります。 このgroonga-query-logには、クエリーログを解析して、どのクエリーの応答がどのくらい遅いのかを出力してくれるスクリプトが含まれています。 したがって、このツールを使えば、クエリーログを取得するだけでボトルネックの測定ができます。

ちなみに、groonga-query-logには、今回紹介する遅いクエリーを特定するスクリプトだけではなく、先日このブログで紹介されていた、クラッシュ時のログを解析するスクリプトクエリーログを手軽に再生するスクリプト、回帰テストを実行するスクリプト等、様々なスクリプトがありますので、興味が湧いたら他の機能についても使ってみて下さい。

遅いクエリーを特定するスクリプトの使い方は以下の通りです。

事前にRubyをインストールします。Rubyをインストールしたら以下のコマンドでgroonga-query-logをインストールします。

% gem install groonga-query-log

その後は、ボトルネックを測定するためにクエリーログを取得します。 クエリーログは以下のように--query-log-pathを指定してGroongaを起動することで取得できます。 例えば、サーバーモードでGroongaを起動して、クエリーログを取得する場合は、以下のように実行します。

% groonga -s --protocol http --query-log-path ~/benchmark/query.log ~/testdb/db

クエリーログを取得したら、取得したクエリーログを解析します。 クエリーログの解析は以下のコマンドで実行できます。

% groonga-query-log-analyze ~/benchmark/query.log

上記のコマンドを実行すると、標準出力に以下のような結果が出力されます。標準出力ではなく、ファイルに結果を出力したい場合は、--outputオプションで保存先を指定することができます。 情報が多いですが、1つずつ解説していきます。

Summary:
  Threshold:
    slow response     : 0.2
    slow operation    : 0.1
  # of responses      : 20
  # of slow responses : 1
  responses/sec       : 0.1351168407649807
  start time          : 2018-06-26T13:26:01.958965+09:00
  last time           : 2018-06-26T13:28:29.979003+09:00
  period(sec)         : 148.020038707
  slow response ratio : 5.000%
  total response time : 0.9940333010000002
  Slow Operations:

Slow Queries:
 1) [2018-06-26T13:26:42.318254+09:00-2018-06-26T13:26:42.853803+09:00 (0.53554963)](0): load --table Site
  name: <load>
  parameters:
    <table>: <Site>
  1) 0.53550895:       load(     9)

 2) [2018-06-26T13:27:20.700551+09:00-2018-06-26T13:27:20.838092+09:00 (0.13754151)](0): column_create --table Terms --name blog_title --flags COLUMN_INDEX|WITH_POSITION --type Site --source title
  name: <column_create>
  parameters:
    <table>: <Terms>
    <name>: <blog_title>
    <flags>: <COLUMN_INDEX|WITH_POSITION>
    <type>: <Site>
    <source>: <title>

 3) [2018-06-26T13:26:30.927046+09:00-2018-06-26T13:26:31.062374+09:00 (0.13532895)](0): column_create --table Site --name title --type ShortText
  name: <column_create>
  parameters:
    <table>: <Site>
    <name>: <title>
    <type>: <ShortText>

 4) [2018-06-26T13:26:13.510750+09:00-2018-06-26T13:26:13.599616+09:00 (0.08886603)](0): table_create --name Site --flags TABLE_HASH_KEY --key_type ShortText
  name: <table_create>
  parameters:
    <name>: <Site>
    <flags>: <TABLE_HASH_KEY>
    <key_type>: <ShortText>

 5) [2018-06-26T13:27:12.821842+09:00-2018-06-26T13:27:12.909721+09:00 (0.08787940)](0): table_create --name Terms --flags TABLE_PAT_KEY --key_type ShortText --default_tokenizer TokenBigram --normalizer NormalizerAuto
  name: <table_create>
  parameters:
    <name>: <Terms>
    <flags>: <TABLE_PAT_KEY>
    <key_type>: <ShortText>
    <default_tokenizer>: <TokenBigram>
    <normalizer>: <NormalizerAuto>

 6) [2018-06-26T13:28:10.403229+09:00-2018-06-26T13:28:10.407243+09:00 (0.00401451)](0): select --table Site --offset 7 --limit 3
  name: <select>
  parameters:
    <table>: <Site>
    <offset>: <7>
    <limit>: <3>
  1) 0.00010690:     select(     9)
  2) 0.00387348:     output(     2)

 7) [2018-06-26T13:26:56.509382+09:00-2018-06-26T13:26:56.509947+09:00 (0.00056562)](0): select --table Site --query _id:1
  name: <select>
  parameters:
    <table>: <Site>
    <query>: <_id:1>
  1) 0.00037286:     filter(     1) query: _id:1
  2) 0.00001580:     select(     1)
  3) 0.00010979:     output(     1)

 8) [2018-06-26T13:27:45.307960+09:00-2018-06-26T13:27:45.308409+09:00 (0.00044912)](0): select --table Site --output_columns _key,title,_score --query title:@test
  name: <select>
  parameters:
    <table>: <Site>
    <output_columns>: <_key,title,_score>
    <query>: <title:@test>
  1) 0.00029620:     filter(     9) query: title:@test
  2) 0.00001499:     select(     9)
  3) 0.00008281:     output(     9) _key,title,_score

 9) [2018-06-26T13:28:24.234937+09:00-2018-06-26T13:28:24.235383+09:00 (0.00044695)](0): select --table Site --query title:@test --output_columns _id,_score,title --sort_keys -_score
  name: <select>
  parameters:
    <table>: <Site>
    <query>: <title:@test>
    <output_columns>: <_id,_score,title>
    <sort_keys>: <-_score>
  1) 0.00027925:     filter(     9) query: title:@test
  2) 0.00001296:     select(     9)
  3) 0.00004034:       sort(     9)
  4) 0.00005845:     output(     9) _id,_score,title

10) [2018-06-26T13:28:29.978590+09:00-2018-06-26T13:28:29.979003+09:00 (0.00041371)](0): select --table Site --query title:@test --output_columns _id,_score,title --sort_keys -_score,_id
  name: <select>
  parameters:
    <table>: <Site>
    <query>: <title:@test>
    <output_columns>: <_id,_score,title>
    <sort_keys>: <-_score,_id>
  1) 0.00023013:     filter(     9) query: title:@test
  2) 0.00000878:     select(     9)
  3) 0.00004086:       sort(     9)
  4) 0.00005566:     output(     9) _id,_score,title

Summary

まず、Summaryの内容について解説します。 Summaryには、以下の項目があります。

  • Threshold:実行に何秒かかったら、遅いクエリー、遅いオペレーションとするかのしきい値を表示しています。
    • slow responseに表示されている値より時間のかかったクエリーを遅いクエリーと判定します。
      • 単位は秒で、デフォルト値は、0.2秒ですが、groonga-query-log-analyze実行時に--slow-response-thresholdオプションを使って、しきい値を変更できます。
    • slow operationに表示されている値より時間のかかったオペレーションを、遅いオペレーションと判定します。
      • 単位は秒で、デフォルト値は、0.1秒ですが、groonga-query-log-analyze実行時に--slow-operation-thresholdオプションを使って、しきい値を変更できます。
    • 例えば、slow responseのしきい値を1.0秒、slow operationのしきい値を1.0秒にしたい場合は、以下のようにgroonga-query-log-analyzeを実行します。
% groonga-query-log-analyze --slow-response-threshold=1.0 --slow-operation-threshold=1.0 query.log
  • of responses:解析に使用したクエリーログに含まれる全てのクエリーの数を表します。
    • 例えば、of responsesが10だったとすると、query.logというクエリーログには、全部で10個のクエリーが記録されている事になります。
  • of slow responsesslow responseに設定したしきい値を超えたクエリーがいくつあったかを表します。
    • 例えば、of slow responsesが2だったとすると、slow responseに指定したしきい値を超えたクエリーが2つあった事になります。
  • responses/sec:平均応答時間を表します。解析したクエリーが平均してどのくらいの応答時間なのかを表示します。単位は秒です。
  • start timelast time:クエリーの実行開始時間と終了時間を表します。
  • period(sec):クエリーを流していた時間を表します。start timeend timeの差を表します。
  • slow response ratio:全体の中で遅いクエリーが占める割合を表します。例えば、slow response ratioの値が5.000%だった場合は、実行したクエリーのうち5%が遅いクエリーということになります。
  • total response time:クエリーの総実行時間を表します。単位は秒です。例えば、total response timeが0.9940333010000002だった場合は、全てのクエリーを実行するのに、約0.99秒かかったことになります。

Slow Operations

Slow Operationsは、具体的に遅いオペレーションを表示します。 例えば以下のような表示になります。

  Slow Operations:
    [56.021284]( 3.77%) [168](70.59%)    filter: title == "test"

この例の場合は、filter条件のtitle == "test"が遅いオペレーションと出ています。

  • 一番左の[56.021284]は、このオペレーションを実行した総実行時間を表します。単位は秒です。したがって、この場合は、filter == "test"というオペレーションを全部で約56秒実行したことになります。
  • 左から二番目の( 3.77%)は、他のオペレーションも含めた全実行時間のうちこのオペレーションが占める割合を表しています。したがって、この例の場合は、title == "test"というオペレーションが、全体のうち3.77%を占める事を表しています。
  • 左から3番目の[168]はこのオペレーションを実行した回数を表しています。この例の場合は、[168]となっているので、title == "test"というオペレーションが168回実行されています。
  • 左から4番目の(70.59%)は、他のオペレーションも含めた全実行回数のうちこのオペレーションが占める割合を表しています。この例の場合は、(70.59%)となっているので、全体のオペレーションのうち約70%は、title == testを実行していることになります。

Slow Queries

Slow Queriesは、具体的に遅いクエリーを遅い順に表示します。表示件数は、--n-entriesまたは-nオプションで変更でき、デフォルトでは10件表示されます。 例えば、5件表示させたい場合は、以下のように実行します。

% groonga-query-log-analyze --n-entries=5 ~/benchmark/query.log

or

% groonga-query-log-analyze -n 5 ~/benchmark/query.log

クエリーに複数の条件が含まれている場合は、それぞれの条件にどのくらい時間がかかったかも表示されます。 例えば以下のような表示の場合は、filterselectoutputの実行にそれぞれ0.00037286秒0.00001580秒0.00010979秒かかったことになります。

クエリー全体の実行時間は、最初の行に出力されます。以下の表示の場合は、7) [2018-06-26T13:26:56.509382+09:00-2018-06-26T13:26:56.509947+09:00 (0.00056562)](0): select --table Site --query _id:1(0.00056562)の部分がクエリー全体の実行時間を表しています。

 7) [2018-06-26T13:26:56.509382+09:00-2018-06-26T13:26:56.509947+09:00 (0.00056562)](0): select --table Site --query _id:1
  name: <select>
  parameters:
    <table>: <Site>
    <query>: <_id:1>
  1) 0.00037286:     filter(     1) query: _id:1
  2) 0.00001580:     select(     1)
  3) 0.00010979:     output(     1)

まとめ

groonga-query-log-analyzeを使うことで、このようにして遅いクエリーや、オペレーションを特定することができます。 どのくらい遅いのかの他に、全体に占める割合も出力することができ、また、どの条件に時間がかかっているかも出力できるので、チューニングのポイントを探すのに役に立ちます。

例えば、とても遅いクエリーだが、全体に占める割合が少ないクエリーをチューニングするよりも、そこそこ遅いクエリーで全体に占める割合が多いクエリーをチューニングしたほうが、性能向上に寄与します。 どのクエリー、オペレーションが遅いかだけの情報だと、上記のような判断はできませんが、groonga-query-log-analyzeを使った解析なら、上記のような判断もでき、より効果的なチューニングが実施できます。

タグ: Groonga
2018-06-26

IBus変換エンジンでネストしたメニューを正しく表示できるようにするには

はじめに

GNOME Shellには、ネストしたメニューを正しく展開できないという不具合があります。 今回はその問題への対処方法について紹介します。

問題の詳細

IBus変換エンジンのメニューは、IBusPropertyIBusPropList を組み合わせて実装します。 サンプルコードの抜粋を以下に示します。

IBusText *label = ibus_text_new_from_static_string("Menu");
IBusPropList *root = ibus_prop_list_new();
g_object_ref_sink(root);
IBusPropList *submenulist = ibus_prop_list_new();
IBusProperty *menu = ibus_property_new("InputMode",
                                       PROP_TYPE_MENU,
                                       label,
                                       NULL,
                                       NULL,
                                       TRUE,
                                       TRUE,
                                       PROP_STATE_UNCHECKED,
                                       submenulist);
g_object_ref_sink(menu);
ibus_prop_list_append(root, menu);

label = ibus_text_new_from_static_string("SubMenu");
IBusPropList *subsubmenulist = ibus_prop_list_new();
IBusProperty *submenu = ibus_property_new("SubMenuKey",
                                          PROP_TYPE_MENU,
                                          label,
                                          NULL,
                                          NULL,
                                          TRUE,
                                          TRUE,
                                          PROP_STATE_UNCHECKED,
                                          subsubmenulist);
g_object_ref_sink(submenu);
ibus_prop_list_append(submenulist, submenu);

label = ibus_text_new_from_static_string("SubSubMenu");
IBusProperty subsubmenu = ibus_property_new("SubSubMenuKey",
                                            PROP_TYPE_NORMAL,
                                            label,
                                            NULL,
                                            NULL,
                                            TRUE,
                                            TRUE,
                                            PROP_STATE_UNCHECKED,
                                            NULL);
g_object_ref_sink(subsubmenu);
ibus_prop_list_append(subsubmenulist, subsubmenu);

メニューが子のメニューを持つ場合には、該当するメニューは IBusPropList を保持する、というのが基本です。 上記の例だと menu の下に submenulist という IBusPropList を追加して、その下に IBusProperty である submenu を追加しています。 このようにすることでメニューの親子関係を表現できるというわけです。 孫にあたるメニューの追加も同様です。

では、ネストしたメニューを実装したIBus変換エンジンのメニューは実際にどのように表示されるでしょうか。

GNOME ShellとXfceそれぞれでみてみましょう。

GNOME Shellの場合

GNOME Shellの場合、SubMenu をクリックしてもその子階層に配置したはずの SubSubMenu は表示されません。 SubMenu が閉じられてしまうという挙動になります。

Xfceの場合

Xfceの場合、SubMenu をクリックしたらその子階層に配置した SubSubMenu も期待通りに表示されます。

したがって、3階層のメニューを実装した場合、GNOME Shellでは正常に機能しないということがわかります。

問題への対応について

ネストしたメニューが表示されないというこの挙動ですが、既知の問題でした。 しかし、報告から何年も経過しているものの、まだ修正されていません。 GNOME Shell自体の修正が必要なケースですが、たいていのIBus変換エンジンではそれほどネストが深いメニューを実装していないために大きな問題にはなっていないのかもしれません。

幸いにも、該当バグにパッチが投稿されていました。 最近のバージョンに合わせて多少手直しすることで、Ubuntu 18.04のGNOME Shellで問題が解決できることがわかっています。

そのため、以下のようにパッチを更新して追加のフィードバックをしておきました。

まとめ

今回は、IBusでネストしたメニューを正しく表示できないことに気づいてフィードバックしたときの知見を紹介しました。 まだアップストリームであるGNOME Shellにパッチが取り込まれていませんが、修正されればGNOME Shellでもネストしたメニューをきちんと表示できるようになるはずです。

つづき: 2018-07-06
2018-06-27

リーダブルなコードを目指して:コードへのコメント(2)

1週間に1回ずつコメントできるといいなぁと思っていたけど2週間に1回のペースになっている須藤です。

リーダブルなコードを目指して:コードへのコメント(1)の続きです。前回はWinMain()の最初の2行目まで読んでコメントしました。ここまではウィンドウモードの設定をしていただけです。さて、続きはどうなっているでしょうか。

リポジトリー: https://github.com/yu-chan/Mario

今回のコメントに関するやりとりをするissue: https://github.com/yu-chan/Mario/issues/2

メイン関数

ウィンドウモードの設定の次の処理は↓のようになっていました。

	if(DxLib_Init() == -1) {
		return -1;
	}

DxLib_Init()という関数名からDXライブラリを初期化していることがわかります。また、おそらく初期化に失敗したら-1が返ってくるのだろうということもわかります。

失敗したらすぐにreturnしているのはいいですね。次のように「成功したらメインの処理を実行」というように書くこともできますが、そうするとインデントが深くなって見通しが悪くなってしまいます。また、「このプログラムで重要な処理はなんなのか」が伝わりにくいコードになりがちです。参考:ifとreturnの使い方

	if(DxLib_Init() == 0) {
		// メインの処理
	}

次のようにreturn -1の代わりにEXIT_FAILUREを使うこともできます。

#include <stdlib.h>

// ...
	if(DxLib_Init() == -1) {
		return EXIT_FAILURE;
	}

EXIT_FAILUREを使うと「失敗扱いで終了するんだなー」ということをコードで伝えやすくなります。が、0以外なら失敗というのはよく知られているので-1returnしても特段わかりにくいというわけではありません。

なお、プロセスの終了値でなにかを伝えたいプログラムの場合はEXIT_FAILUREを使わない方がいいです。たとえば、diff(1)は変更があったときは1を返し、エラーがあったときは2を返します。EXIT_FAILUREの値が0以外のなんなのか(1なのか8なのかとか)わからないので、「0以外の値」以上の意味がある場合以外はEXIT_FAILUREを使わない方がよいです。

ところで、Windowsでは負の数を返すのは普通なのでしょうか。Linuxでは正の数を返すことが多い気がしていたので少し意外でした。

あ、そうだ、失敗したときはなにかメッセージを出してから終了する方がいいです。たとえばこんな感じです。

	if(DxLib_Init() == -1) {
		std::cerr << "DXライブラリの初期化に失敗しました。" << std::endl;
		return -1;
	}

なお、「なにか」というのは「問題解決に役立つ情報」です。↑でもなにもないよりはヒントになるのですが、↑よりもたとえば「サウンドデバイスの初期化に失敗しました。」の方がより問題解決の役に立ちます。問題を解決するための次のアクション(サウンドデバイスがおかしくないか調べようとか)につながりやすいからです。参考:問題解決につながるエラーメッセージ

次の行を見てみましょう。

	SetDrawScreen(DX_SCREEN_BACK);

バックのスクリーンに描画するように設定しているのでしょう。「バックのスクリーンに描画ってどういうこと?」と思ったのでSetDrawScreen()のドキュメントを読んでみました。ダブルバッファリングの設定のようです。読んでいたらtypoを見つけたので報告しておきました。

次の行を見てみましょう。

	srand((unsigned)time(NULL));

現在時刻で疑似乱数のシードを設定しているのがわかります。どこかでrand()も使っているのでしょう。

C++なのでCスタイルのキャストよりもC++スタイルのキャストを使った方がよいでしょう。

	srand(static_cast<unsigned>(time(NULL)));

C++スタイルのキャストの方が安全でないキャストに気づきやすいので、できるだけC++スタイルのキャストを使うのがよいでしょう。

インスタンス作成

次は似たようなコードが集まっていました。

	//フレームレート制御
	if(!Framerate::instance()) {
		Framerate::create();
	}
	if(!SoundManager::instance()) {
		SoundManager::create();
	}
	if(!Sequence::Parent::instance()) {
		Sequence::Parent::create();
	}

インスタンスが存在しなかったら作成する、というコードに見えます。create()の結果をどこにも代入していないのでシングルトンパターンなのでしょう。シングルトンパターンではinstanceという名前を使って実現することが多いので、instanceというクラスメソッドにしているのはリーダブルなコードにつながります。

メイン関数の最後の方には次のようなコードがあります。

	Sequence::Parent::destroy();
	SoundManager::destroy();
	Framerate::destroy();

ここでcreate()で作ったインスタンスを解放しているのでしょう。

シングルトンパターンは少し行儀のよいグローバル変数のようなものなので、できるだけ使わない方がよいです。グローバル変数のようにどこからも参照できる(スコープが広い)ので、影響範囲を把握しにくくなります。これはリーダブルさを下げてしまいます。

今回のケースでは、メイン関数内のローカル変数としてFramerateSoundManagerSequence::Parentのインスタンスを作成してそれを適切なオブジェクトに渡すことでも実現できるんじゃないかなぁと思いました。

たとえば次のような感じです。

	{
		Framerate framerate;
		SoundManager sound_manager;
		Sequence::Parent parent;

		while(!InputInterface::isOn(KEY_INPUT_Q)) { //Qを押したら終了
			if(ProcessMessage() != 0) {
				break;
			}
			InputInterface::updateKey();
			framerate.update();
			ClearDrawScreen();

			//ゲーム開始
			parent.update();

			ScreenFlip();
			framerate.wait();
		}
	}

ローカル変数で実現できるならそのスコープを抜けるときに自動でデストラクターが走るので、手動でdestroy()を呼ぶ必要はありません。そうすると解放処理の呼び忘れがなくなり、安全なプログラムになりやすいです。また、読む側もスコープが小さいほど覚えておくことが少なくなって理解しやすくなります。

後処理

メインループ(while() {...})は次回以降見ていくとして、今回は後処理部分を見て終わりにしましょう。

	DxLib_End();

	return 0;

DXライブラリの終了処理をしてプログラムを正常終了していることがすぐにわかりますね。0の代わりにEXIT_SUCCESSを使ってもよいでしょう。

まとめ

リーダブルコードの解説を読んで「自分が書いたコードにコメントして欲しい」という連絡があったのでコメントしています。今回はメイン関数の全体を読んでコメントしました。次回はメインループの中に入ります。

「リーダブルなコードはどんなコードか」を一緒に考えていきたい人はぜひ一緒にコメントして考えていきましょう。なお、コメントするときは「悪いところ探しではない」、「自分お考えを押し付けることは大事ではない」点に注意しましょう。詳細はリーダブルなコードを目指して:コードへのコメント(1)を参照してください。

つづき: 2018-07-20
2018-06-28

«前月 最新 翌月»
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|
タグ: