Shiny App を高速化したいときに比較的簡単に試せるのがmemoise
を使ったデータのキャッシュ化です。それについて紹介します
memoise とは?
私がこのパッケージを初めて知ったのは、Shiny高速化を調べてる際にpromises
のドキュメントを読んでたときでした
参考 : Case study: converting a Shiny app to async
memoise
がどう役立つかというと、ある関数を同じインプットで何度も呼び出すなら、はじめて呼び出したときにデータをキャッシュ化して、2度目以降の呼び出しはそのキャッシュ化されたデータを返してあげよう というところです
キャッシュ先としては、なんらかのファイルシステム(例えば、 tmpfs) やクラウドストレージ(例えば、S3)を使うことができます
2019-10-21追記 また、キャッシュ先としてRedisを使うことも、次のPR通りにreduxパッケージを使ってやれば実装することができそうです : https://github.com/r-lib/memoise/pull/67
とくにmemoise
と相性が良いのが、Shiny Serverの上で、あるShiny Appを複数人が使用している場合です。よくあるパターンで、別のデータベースで毎日夜にデータが更新され、Shiny Appの閲覧時に独自関数を使ってそのデータをロードする処理が発生することを考えます
最初にそのShiny Appにアクセスしたときは、そのデータベースからデータをロードしますが、その処理をmemoise
でかぶせておけば、データがキャッシュ化されます。その結果、2度目以降にアクセスした人は、そのキャッシュ先からデータをロードすることができるので、高速に呼び出すことができます
では試してみましょう
公式ドキュメント
- Github : https://github.com/r-lib/memoise
やるべきたった1つのこと
キャッシュ化したい関数にmemoise
をかぶせる だけです。まず適当にデータを読み込む関数を用意してみます
library(memoise)
set.seed(42)
load_data <- function(start_date, end_date){
# Preprocess - ここにホントはデータベースへの接続やデータのクレンジング入っている
Sys.sleep(5)
# return data
data <- rnorm(end_date - start_date, mean = 0, sd = 1)
data
}
これのパフォーマンスをチェックしておくと、Sys.slepp
をしたとおりデータのロードに5秒かかりました
> t <- proc.time()
> data <- load_data(as.Date("2019-08-01"), as.Date("2019-08-31"))
> (proc.time() - t)["elapsed"]
elapsed
5.008
これをmemoiseでかぶせてみます。キャッシュさせる先のファイルシステムは今回はホームディレクトリで適当に切っておきます
m_load_data <- memoise(load_data, cache = cache_filesystem("~/tmp"))
まず一回目の実行をしてみます。とくに実行時間は変わらないですね
(キャッシュ先にデータを書き出すということは、ストレージIOがボトルネックになることがあるので、適切な場所を選ばないと、遅くなる可能性はあります)
> t <- proc.time()
> data <- m_load_data(as.Date("2019-08-01"), as.Date("2019-08-31"))
> (proc.time() - t)["elapsed"]
elapsed
5.019
しかし、2回目を実行してみると、ちゃんと爆速になりました(完)
> t <- proc.time()
> data <- m_load_data(as.Date("2019-08-01"), as.Date("2019-08-31"))
> (proc.time() - t)["elapsed"]
elapsed
0.148
補足
のコードを読めばどうやって実現してるかの雰囲気はわかります。memoise
用のnew.env
を作ってその中でハッシュデータをもたせて覚えてる?みたいですね。ファイルシステムへの書き出しはRDS
フォーマットのようです
実際自分が運用して複数人に提供しているShiny Appだと、別のホストのデータベースから複数の集計済みデータを日付指定でロードする必要があり、memoise
を使うことでかなり改善されました。一度試してみるのをおすすめします
(さらにいうと、1回目のアクセスを自動的に別のプロセスで定期的にアクセスさせる処理を書いておけば、より良いと思います)