How many bytes did I read?

フルリロード、差分リロードした際に読み込んだバイト数を記録しているのだが、これは実は概算であって正確な値ではない。というのは意外なことに javascript から実際に読み込んだバイト数を得る方法はけっこう限られている。特に、レスポンスボディが圧縮されているとそれが顕著になる。

まずレスポンスヘッダ内の Content-Length があれば、それが読み込んだバイト数となる。終わり。圧縮されている場合は圧縮されている状態の転送バイト数となる。ふたばのサーバでは、html ファイルを読み込むと gzip 圧縮されたものが Content-Length 付きで返ってくる。しかし、差分リロードした際のリクエスト(これも gzip 圧縮されている)には Content-Length はない。したがって差分リロードの際に何バイト読み込んだかは全くわからない。

次に XMLHttpRequest の progress イベントをリスンすると、loaded プロパティと total プロパティで転送バイト数が得られる。ただし、内容が圧縮されている場合はこのイベントは少なくとも Chrome ではあまり役に立たない。全体を読み込み終えたあとで 1 回だけ発生し、その際 total は 0、loaded は展開後のバイト数が格納されている。つまり、圧縮状態の転送バイト数は読み取れない。

また、ここまではレスポンスボディの話だったがレスポンスヘッダのサイズも javascript からは正確なその値を取得できない。XMLHttpRequest#getAllResponseHeaders() が返す値は微妙に加工されている(HTTP/1.1 云々のラインなど)。加えて書くと、プロトコルが HTTP/2 の場合はヘッダも圧縮されているのでなおのこと実際に転送されたバイト数はわからない。

ということで差分リロードの際の転送バイト数および https 上の各読み込み時のヘッダに関してはヒューリスティックな謎の係数を使って圧縮後のサイズを概算しているのであった。これがだいたいの場合だいたい合ってるようなのだが合わないときは盛大に合わない。

さてどうしたものか。

Resolving Character References

文字参照というものがある。" とか、  とか、  とかいったアレだ。赤福プラスが html ソースを DOM ツリーに変換する前に、それらを解決してしまいたい。解決というのは文字参照の表記から文字そのものに変換するということだ。

それが必要な理由は、たまに絵文字の表現としてそのコードポイントをサロゲートペアの文字参照で表記されるケースがあるからだ。これはそのコメントを送出した UA つまりブラウザか、拡張か、あるいは専ブラか……の明らかなミスで、サロゲートペアを文字参照で表記してはいけないのである。いけない表記をすると当然ペナルティがあり、それらは DOM パーサによって U+FFFD に置換されてしまう。これを、置換される前に気を利かせて解決したい。従ってそのために必要な処理は DOM パーサに渡す前段で行わなければいけないということになる。

ただし、この不正な文字参照の修復が可能なのは自前で DOM パーサにかける処理が入るフルリロード、及び内部データを JSON で取り扱う差分リロードをした場合のみである。最初にスレッドを開いたケースは構築済みの DOM ツリーから innerHTML を得るしかないため、既に書き込まれてる不正なサロゲートペアの文字参照は修復できない。この辺ふたばの鯖側で巧いことやってくれるとうれしいのだけど、しかし前述の通り原因はへんてこな UA の動作なのでふたば側がそんなことをする筋合いもないのが難しい。

Kenya Television Network #4

Firefox で window.getSelection().modify() が動かない件。textarea 上で動かないのはそうなのだが、textarea の外では動く。textarea の外というのは例えば contentEditable=true にした要素のことだ。ということは textarea の代わりにそれをコメント欄に置けばいいことになる。

などと簡単に言ってしまったが、これはかなり面倒くさい。Chrome では textarea 上でも modify() は普通に動くし、個人的に Firefox は常用しておらずちょっとしたショートカットが動かなかろうが特に困らないので、「Firefox などというゴミみたいなダサいブラウザでは動きません」という対応でもいいのだが、それもなんなので、まあやってみよう。

ということでやってみた。とても疲れた。コメント欄が div 要素になったことで:

  • placeholder も自前で表示せざるを得なくなったのだが、その代わり多少表現の自由度が高まったのでとりあえず斜めにしてみた
  • 将来的には絵文字を画像で挿入したりできるかもしれない。その辺や絵文字パレットはやる気が満ち溢れてきたらやる

ところでこの赤福プラスが内包しているちょっとしたショートカットのうち Ctrl+A は 1 回押すと全選択し、全選択の状態でさらに押すと最初に押す前のキャレット位置から前方に向かって折り返し位置境界へ飛ぶ。これはそういうふうに動くように組んであり、仕様である。

Reloading Optimization #2

別のアプローチからも高速化を施してみよう。お知らせによると、2018/11/09 付でリロードの高速化が施されたとある。つまり、ふたばが標準で返してくる html における [リロード] ボタンの動作が変更されたということだ。

リロードボタンを押した時、従来はまず HEAD リクエストを飛ばして、更新されていたらページ全体をリロードという形になっていた(たぶん)が、11/9 の変更ではこのページ全体のリロードに代えて、XMLHttpRequest で [cci]futaba.php?mode=json[/cci] 云々をリクエストして、新しく増えたレスの情報だけを json 形式で受け取るようになっている。これによりまず転送量が劇的に減る。転送量が減ることでレスポンスタイムも短くなる。これは立派な差分取得 API と捉えられるので使わない手はない。

ということで組み込んでみた。ここで重要なのは、リロードの手段が 2 つになるということだ。差分形式で得られるのは新着レスとそうだねの全データのみであり、ID 付与やレス削除を同期するにはフルリロードしなければならない。なので、対応する UI としてはまず従来の [続きを読む] リンクの隣に、[差分で続きを読む] リンクを置くことが考えられる。あるいは逆にデフォルトを差分読み込みにして、[フルで続きを読む] でもいい。しかしこれはユーザに対していちいち気にしながらリロード手段を振り分けることを強いているわけで、あまりいい UI ではないかもしれない。自分を含めて、「」もとしあきもそんなに賢くない。

従ってあくまでリロードボタンは 1 個だけで、赤福プラス側がよきに計らって適宜フルと差分を切り替えるのが良いように思える。例えば最後にフルリロードした時刻を覚えておいて、そこから n 分経過するまでは差分リロードとかでどうか。n は定数でもいいし、スレッドの勢いか何かから動的に算出する形式でもいいだろう。

とそういうわけでそうしてみた。n は設定可能な定数にした。デフォルトは 2 分間隔でフルリロードする。この状態でいつもどおりスレッドを見ると、転送量はだいたい 90% 削減できるようだ。もちろん読み込みも速い。すごい。

このあたりの話の流れは、むしろ懐かしい。というのは12年くらい前に Opera 版の赤福プラスに続きを読む処理を加えた際、もともとリクエストヘッダに Range を追記しておんなじような転送量削減の措置を施していたからだ。ところがほどなくして Apache の Range の取り扱いに関して脆弱性が発覚してしまった。そんなわけで双葉の鯖もパッチが当てられ、Range は無事無視されるようになったのであった。がっくり。

Reloading Optimization

続きを読む処理の高速化。まず続きを読む際に何をするかを列挙してみよう。

  1. ふたばから html を読み込む
  2. html のうちレス群以外を xml に変換する
  3. xml を元に、スレ本文の諸々及びその他のバインディングを更新する
  4. html のうちレス群を xml に変換する
  5. xml を元に、読み込み済みのレス群の以下の項目を更新する:
    • 書き込んだ人による削除、スレッドを立てた人による削除、強制的なIP開示、なー
    • ID の開示
    • そうだねの増減
  6. 新しく増えたレス群を html に変換し、そのフラグメントを DOM に追加する
  7. 全レスに対して、ID の出現頻度をカウントし直す

この処理の内で現在ボトルネックになっているのは 5 と 7 だ。それぞれの処理をプロファイルしながら地道に高速化していく必要がある。

というわけで高速化した。1500レスくらいのスレッドで続きを読む全体の処理に 1 秒ちょいくらいかかってたのが、300msくらいになったのでだいたい 3 倍くらいか。あとは 4 も 100ms ほど時間がかかるのだがまあこれはしかたないかな。

あと関係ないけどついでに may とかで、添付された画像のファイル名を引用する習慣があるようなのでそういう形式の引用にもポップアップするようにした。

Let the box be light #2

lightbox を一新した後、コメントにクリックするとCPUパワーを専有してしまうというバグが報告されたのだが、なかなか再現できなかった。そもそも lightbox の中に CPU を専有しそうな重い処理がない。

が、クリック時に 200ms ほどやけに重くなる現象は再現できた。画像のドラッグ時にテキストが選択されることがないように pointerdown/pointerup で [cci]document.body.style.userSelect[/cci] を操作しているのだが、これがリフローを呼び起こすらしく同時に使っている [cci]getBoundingClientRect()[/cci] と相性が悪いようだ。

userSelect を none にするのはドラッグ時ではなく lightbox 実行時全体にすることでこれを回避できた。もちろん副作用としてテキスト(倍率とか)を選択できなくなったがまあ許してくれるねグッドトリップ。

CPU 専有現象自体は Vivaldi 特有のものなのか環境によるものなのかは不明。以前、あべ☆アニを作ってた際にも同じようなことがあったので、Vivaldi 自身がなんかやってるのかなあ…?

Switching to

switch ステートメントのラベルに定数じゃなく式…例えば正規表現を書きたいときってあるじゃないですか。それに対するよくあるソリューションというのが


switch (true) {
case /404/.test(status):
break;
case /304/.test(status):
break;
case /5../.test(status):
break;
}

というのだけどもう1行目からして気持ち悪い。どうにかならないの。

例えば

switch (status) {
case '404':
break;
case '304':
break;
case /5../.test(status) && status
break;
}

という書き方ならどうか。

Difference between notices #2

まず diff を取る対象はマークアップレベルのテキストにした。ただし a 要素の href 属性以外は全て削ぎ落とされる。

また diff ライブラリとしては jsdifflib を利用するようにした。行単位の比較だし、diff 本体とプレゼンテーションのためのコードが分離しているのも都合がいいし、10KB ちょいとコンパクト。

というわけで注意書きの変更を検知すると、window.alert() でその旨を通知したのち自動的に注意書きパネルが開き、以下のように変更点が表示される:

多分初見で意味が分かると思うけど、赤背景で打ち消し線が引いてあるのが変更後に削除された行、緑背景が追加された行だ。

Difference between notices

ふたばの画像掲示板では送信フォームの下部に様々な注意書きを表示しているが、赤福プラスはロードした際にその注意書きの内容を覚えて保持しており、n キーを押すとパネルに注意書きが表示される。

さて、リロードした際にリロード前後の注意書きの内容を比較して、差異があったらそれを通知したい。できれば加えて、何が増えたのか減ったのかをグラフィカルに表示したい。つまり、diff の結果を表示したい。ところが、この diff のアルゴリズムというのは簡単なように見えて恐ろしく難しくて、それだけで1本論文が書けちゃうほどのものだったりする。それを真面目に実装するのも大変だし、既存のライブラリを組み込むにも、下手すると赤福プラス本体よりデカいとかありうる。

さらに、diff 自体の難しさと並行して、diff の対象をマークアップの段階にするか、人が読んで意味を取れる文章の段階にするかという問題もある。前者にすると、html マークアップ内の属性の変化も検知できるが、それがユーザが欲してる情報かどうかはやや疑わしい。例えば Chrome に ScriptSafe というエクステンションを入れると副作用としてありとあらゆる要素に ss-なんちゃらという属性を付けて回るが、この変化も検知してしまう。しかしこれはユーザにとってはどうでもいい情報だ。それを通知されても困る。

一方で後者、つまり各要素の内容だけをフィルタした結果にすると、例えば a 要素の href 属性と言った、割と重要な情報も見逃してしまう恐れがあるのである。どちらに振っても良い結果にならない。

どうするか。

From crying 16yo to grieving 18yo #2

以前 xubuntu を 16.04 から 18.04 にアップグレードして以来、LightDM のログインスクリーンでパスワード打ち込んでもログインできず、オンスクリーンキーボードを経由せざるを得ない現象が発生してとても不便。

/var/log/lightdm/lightdm.log を見ていたら、起動時に読み込まれる設定ファイルの中に /usr/share/lightdm/lightdm.conf.d/50-xubuntu-numlock.conf というものがあった。はーなるほど numlock がかかってるのね。

ちなみにこの設定ファイルは xubuntu-numlockx を呼び出しているだけなのだが、このファイルは /etc/X11/Xsession.d/55numlockx への薄いラッパでしかない。 55numlockx はシェルスクリプトで、/etc/default/numlockx に記述されている設定に従って X が起動する際の numlock キーの状態を変更するようだ。設定が auto の場合は実行マシンがラップトップかどうかとか、接続されているデバイスを見てよしなにしてくれる。

ということで、/etc/default/numlockx における設定を NUMLOCK=auto から NUMLOCK=off に変更。

不具合なってた理由はつまり、55numlockx 内で USB キーボードが接続されているかを検知し、接続されていたら有無を言わさず numlock をオンにするようになっているからだ。うちのキーボードは USB 接続だが、テンキーがないタイプなのであった。そのケースを想定していない。

 * * *

ところでこの PC は設定で10分放置するとディスプレイを消灯するようにしているのだが、実際にはそれが無視される状態が続いていた。関係あるのかないのかよく分からないのだが、上記の変更を施したらなぜかその設定も反映されるようになってしまった。なんで…?

一応記録のために書いておくと上記の変更の他に /etc/lightdm/lightdm.conf.d/10-xubuntu.conf も以下のような感じで変更してある:

--- 10-xubuntu.conf.old
+++ 10-xubuntu.conf
@@ -1,2 +1,2 @@
-[SeatDefaults]
+[Seat:*]
user-session=xubuntu

でもいずれにしても、電源周りには関係なさそうで、謎。まあ、いっか。