検索や推薦をWebWorkerでやる
スライドにした
さすがにページ内テキストの全文検索はサーバーでやる
こういう実装になっている
Reactのレンダリングのタイミングで最終計算する
理由
世の中の進歩
スマホでもCPUが4つ以上ついているのが当たり前の時代になった
serverのCPU増やすと金がかかるけど、clientのCPUは無料で増える
回線やcacheが良くなってきている
ファイルサイズの多少の差よりもTCPの接続・往復時間の方が気になる
Scrapbox固有の事情
ドカッと返したJSONに、完全一致する文字列重複が多い
サーバーのCPUを使いたくない
Reactのrenderで1回あたり100msec固まっちゃう、なんて事は避けたい
テキストエディタなので、キー入力毎にrenderする
IME切ってキー押しっぱなしにしても、なめらかに表示されてほしい
ページ編集毎に検索・推薦用の辞書の再構築が必要な場合がある
他人が同projectの別ページを編集しても、自分の検索・推薦に即反映されてほしい
特にページ間リンクは重要なので、
リンク記法の補完は全員の持つ辞書に即時反映させたい
こういう役割分担にした
server
何を表示するかを返す
[ {title: "タイトル1", links: ["リンク先1", "リンク先2", "リンク先3"] }, {title, links}, {title, links} ]
みたいな配列をドカッと返す
client
WebWorker
どう表示するかを計算する
どのページが何つながりで推薦されているのか、見出しを付ける
スコアを計算してソートする
同じページを何回も重複表示させない
等
UIスレッド
Ajaxでserverから「何を表示するか」を取得
「どう表示するか」をstateに格納→Reactに渡す→Virtual DOM更新→DOM更新
UIスレッド
1 threadを使いまわす
setTimeout、requsetAnimationFrame
XHRなどの非同期関数
CPUを1つしか使えない
マルチCPU化したスマホの性能を捨ててる
CPU
通電しているだけでCPU100%時の25%ぐらい電力使うらしい(だっけ?)
WebWorker
UIスレッドとは別のCPUを複数使える
効能
他の人の編集が即サジェストに反映される機能
projectに1万ページぐらいあって、その各ページに10個ぐらいリンクやhashtagがある場合
サジェスト用の辞書作成を
UIスレッドで、Reactを固めないようにこまめにsleepしながら実行した場合
5秒ぐらいかかる
サーバーで実行した場合
これは最初から試してない
「ページ作った」「リンク変更した」等の情報を差分でやりとりしている
サーバーはその差分情報の中継しかしていない
もしサーバーで計算したら人数分CPU食うのは明らか
WebWorkerで実行した場合
sleepなしでぶん回せるので、500msecもかからない
差分が来たら、毎回サジェスト用の辞書を全再構築している
初期化時と差分時で2種類書くのが面倒だった
計算量的にWebWorker使えば問題ないだろと最初から狙ってた
1project10万ページで速度が足りなくなっても、ここをチューニングすればまだまだ行けそう
という心の余裕として取っておく
projectに1万ページぐらいあって、その各ページに10個ぐらいリンクやhashtagがある場合
そこそこ重い計算になる
共通のリンク先を持つページをグループ化する
リンク数などでスコアを計算して並び替える
重複表示をしない
ページ内のリンク記法やhashtagが変更される度に再計算している
サーバーで実行した場合
最悪15秒ぐらいCPUをフルに専有してた
アルゴリズムが良くなかった。 O(N^2)
だった
でも改善しても2秒ぐらいかかる場合があった
それをUIスレッドで実行した場合
最悪、キー入力して2秒ぐらい固まってしまう
WebWorkerで実行
Reactのレンダリング時間分だけ固まる程度で済む
推薦される関連ページ数が多い場合はまだ重い
window.Worker
WebWorkerが動くブラウザ
だいたい動くのではないか
callbackなので、返ってこないと困る
返ってこい
Workerの作り方
3種類ある
HTML内にscriptタグとして埋め込む
コードをbase64 encodeして new Worker("data:text/javascript:base64encodeされた文字列")
する
普通に const worker = new Worker('/path/to/worker.js')
Workerを使いまわす
事前に作っておく
タスクを投げる直前に作らない
new Worker(url)
するとHTTPリクエストがサーバーに飛んで、(おそらく)そこでブロッキングする
Worker複数作る
並行実行したい処理の数だけ new Worker(url)
する
それぞれ別のCPUで実行される
new Worker(url)
サーバーが301 not modifyを返せばcacheが使われる
app初期化時に必要な数だけ new Worker(url)
すべし
初期レンダリングではWebWorker使わない
読むだけならそんなに新しくないブラウザでも見れる
書くにはWebWorker動くブラウザ使ってね、という方針
module bundle
1つのworkerに複数の機能を詰め込める
UIスレッドのclient jsとコードが共有できる

ファイルサイズに注意
UIスレッドとWebWorkerで同じ関数を共有できるようにしておく
日頃からなるべく純粋な関数で書くようにしている
UIスレッド・WebWorker・サーバーのどこでも動かせるように作っておく
様子を見て後で変えれる
エラートラッキング
worker.onerror
postMessageはcallbackなので絶対に返ってきてほしい
処理内容がスピードを求める物なだけにtry-catchすると遅い
悩む
try-catch捨てた
WebWorkerの中でもUIスレッドと同様に
Sentryが使える
workerのバグ見たら即直す
何度もWebWorkerに仕事させる
Promiseの多重実行を防ぐ