ライブラリを改善する

ニコニコ生放送(PC)では生放送の映像を再生するためhls.jsを利用しています。 hls.jsはHLSに非対応ブラウザ(ChromeやFirefoxなど)でもHLSを再生するためのライブラリです。

HLS自体については次のサイトなどを見てください。

指標についてでも書いていますが、視聴ページのメインコンテンツは映像、映像の上に描画するコメントです。

映像とコメントの例

hls.jsはこの映像を再生に利用するため、hls.jsのパフォーマンスはそのまま視聴ページのパフォーマンスや安定性に繋がります。

ここではまずhls.jsのパフォーマンスを分析し、改善できる点がないかを探すことにしました。

関数プロファイル

hls.jsMedia Source Extensions(MSE)というAPIを利用し映像を再生します。 hls.jsではこのMSEに渡せるように、映像を取得、デコードまた映像のバッファをチェックするといった処理が行われています。

hls.jsの処理はDOM APIにほとんど依存してない(一部Web Workerなどがあります)処理なので、そのままJavaScriptコードのプロファイルを取ればボトルネックが見えてきそうです。

ブラウザの開発者ツールでもプロファイルを取れますが、開発者ツールだと他のコードなどもまざってしまいノイズが多くなってしまいました。 そこで、node-sjspを少し改変してhls.jsのコードだけのプロファイルを取れるようにしたものを使いました。

コードも一部改変しましたが、次のようなnode-sjspを使ってコード変換できるwebpackのLoaderを書きました。 このLoaderを特定のパス(hls.js)のパスだけに適応すれば、特定のコードだけの関数プロファイルがコンソールに出力されます。

"use strict";
const inject = require("node-sjsp").inject;
const path = require("path");
const currentDir = __dirname
module.exports = function (jsCode, inMap) {
    var filepath = path.relative(currentDir, this.resourcePath);
    var injectedCode = inject(filepath, jsCode, 10);
    return injectedCode;
};

実際に取得したhls.jsの関数ごとのプロファイルを見てみてみます。

hls.jsの関数プロファイル

合計の処理時間順に並べてるとイベントの仕組みやメインループでのtick、バッファチェックなどが大部分を占めていることがわかります。

  • EventEmitter
  • tick (バッファのチェックなどをして正しく再生できているかを100msごとにチェックする)
  • _checkAppendedParsed(バッファが断片化してないかをチェックする)

それぞれのtick 1回の処理は数ミリ秒などとても小さいですが、 hls.jsの仕組み上どこかで数百ミリ秒の処理が発生すると、それは映像がそこで止まるということを意味します。 そのため、常に安定した時間で処理を回し続けることが大切であるという認識です。

不要な処理を無効化

他にもnode-sjspを使っていろいろな状況でプロファイルを取っていると、Cea608という見慣れない処理が出てくることがありました。

hls-caption.png

CEA-608はキャプション(字幕)のことです。 字幕がない動画に対してもこの処理が行われているため、なにか無駄な処理を行っていそうです。

hls.jsのソースコードやドキュメントを見るとenableCEA708Captionsというオプションで字幕処理を無効化できることがわかります。 (これらはデフォルトが有効です)

このenableCEA708CaptionsオプションをfalseにすることでCea608に関する処理をしなくなることが確認できました。

📝 hls.jsではこのオプションが有効時にフラグメントを取得するたびに、cea608Parserをリセットする処理が行われていた。

hls.jsを修正する

関数のプロファイルを見てみるとhls.jsで処理の中心となっているのは高頻度が呼ばれるtick_checkAppendedParsedなどでした。 これらの処理は高頻度で呼ばれるため、少しでも改善すると映像再生の安定化に寄与するだろうと仮説がありました。 また、_checkAppendedParsedなどは保持しているフラグメントの数(HLSは映像がセグメント単位に細かく切ったものを取得し再生します)と処理時間に相関がありそうでした。 実際に fragments という配列に対して、forループでチェック処理を行うため、フラグメントの数が増えると処理時間が増加していました。

hls-js-fragments-count.png

フラグメント数と_checkAppendedParsedの処理コストの相関

これらのパフォーマンスを改善するにはhls.jsそのものに手を入れる必要があるため、hls.jsにPull Requestして修正するようにしました。

さきほどの_checkAppendedParsedの問題もループ内の処理を最適化するPRを出すなどして、マイクロベンチ上は3.38倍早くなりました。

improve `_checkAppendedParsed`

Improve StreamController#_checkAppendedParsed performance #1528

これに加えてバッファチェックの仕組みの構造を修正するといった改善も行っています。

またhls.jsは複雑なステートをもつコードですが、それに対してテストの量が足りていなかったのでテストを追加したり、 SauceLabを使ったCIが不安定だったのを問題を安定化するなどを行いました。

最終的には数十のコミットやPull Requestをした結果、hls.jsのコラボレータとして活動しています。

ForkではなくPull Request

hls.jsのパフォーマンス改善を行う場合に、Pull Requestを送るのではなくForkしてしまうという選択肢もありました。 しかしながら、hls.jsは十分複雑なライブラリです。そのため安易にForkすると、Upstreamに追従するのも難しくメンテナンスコストが高くなりやすいです。

これはhls.jsに限らずさまざまなOSSを使った開発で発生する問題です。 あるライブラリを利用する際には、そのライブラリに問題が発生したときにどうするかを考えてから選択しても遅くはないかもしれません。

results matching ""

    No results matching ""