ByteArrayキャッシュ

今回は、Flash内で扱うデータのキャッシュについて。

CPUバウンドな処理を行うFlashを公開するにあたり、処理を軽くする方法をいろいろ考えましたが、
シンプルに考えて、計算結果をどこかにキャッシュしておくことにしました。

少し前に書いたFlashでステレオビジョン入門を例に取ります。

例えば、ブロックマッチング法などでは動的計画法(DP)を用いて途中の計算結果を使い回しますが、
“アプリケーション”であれば、最終的な計算結果のみをどこかにキャッシュしておき、
次のリクエストからはそのキャッシュデータを引っぱってきた方が効率的です。

ここで、キャッシュを組み込んだステレオマッチングのデモを上げておきます。
“use cache” チェックボックスをonにすると、キャッシュを利用して視差マップを生成し、
チェックボックスをoffにすると、毎回ステレオマッチングの計算を行います。
パラメータを大きくしていくと違いが分かってくると思います。
Experiments in Stereo Vision

ステレオマッチングの計算では Vector ではなく ByteArray をコンテナとして使っています。
Vector の場合は、ストリームに流す前に一度シリアライズする必要がありますが、
ByteArray をコンテナとして使えば、計算後そのままシームレスに流すことができます。
シリアライズのコストが削減できるし、コードも小さくまとめられるので個人的にオススメ。

また、今回のデモの場合は最終的にデータが反映されるのは BitmapData なので、
PNGEncoder などを使ってエンコード処理を入れる必要もありません。
画像ヘッダなどはこの場合ノイズになるし、エンコードのコストも発生するのでムダかと。
また、ByteArray のみのやりとりなので、URLLoader + Loader で通信処理を書く必要もなく、
URLStream にそのままデータを流し込んでやればOK。
Loader を起動するよりも高速かつオブジェクトサイズも小さいです。

キーにはURIを利用し、リソース指向でキャッシュの仕組みを作ります。
GETでリクエストされたらFlash側にデータを返し、POSTならストレージにデータを保存するだけ。
URIは、例えばステレオマッチング処理のパラメータを使って組み立てます。
http://foo.com/stereovison/{画像ファイル名}/{探索ウィンドウサイズ}/{探索距離}/
みたいな感じでURIを一意に決めておき、パスをキャッシュのキーに使います。

・データキャッシュ用クライアントの例
[as]
package {
import flash.events.*;
import flash.net.URLRequest;
import flash.net.URLRequestHeader;
import flash.net.URLRequestMethod;
import flash.net.URLStream;
import flash.utils.ByteArray;

public class CacheClient extends EventDispatcher {
public static const FETCH_SUCCESS:String = ‘fetch_success’;
public static const STORE_SUCCESS:String = ‘store_success’;
public static const IO_ERROR:String = ‘io_error’;
private const BASE_URI:String = ‘URI base here ..’;
private var _data:ByteArray;
private var request:URLRequest;
private var header:URLRequestHeader;
private var stream:URLStream;

public function CacheClient(appid:String) {
_data = new ByteArray();
request = new URLRequest();
stream = new URLStream();
header = new URLRequestHeader(“X-AppID”, appid);
request.requestHeaders.push(header);
}
public function get data():ByteArray {
return _data;
}
// fetch data from kvs
public function fetch(key:String):void {
request.url = BASE_URI + key; // Resource Oriented
request.method = URLRequestMethod.GET;
stream.addEventListener(Event.COMPLETE, onFetchComplete);
stream.addEventListener(IOErrorEvent.IO_ERROR, onError);
stream.load(request);
}
// store data to kvs
public function store(key:String, value:ByteArray):void {
value.compress();
request.url = BASE_URI + key;
request.method = URLRequestMethod.POST;
request.contentType = ‘application/octet-stream’;
request.data = value;
stream.addEventListener(Event.COMPLETE, onStoreComplete);
stream.addEventListener(IOErrorEvent.IO_ERROR, onError);
stream.load(request);
}
private function onFetchComplete(e:Event):void {
try {
_data.clear();
stream.readBytes(_data);
if(_data.bytesAvailable) _data.uncompress();
dispatchEvent(new Event(FETCH_SUCCESS));
}catch(e:Error) {
dispatchEvent(new Event(IO_ERROR));
}
}
private function onStoreComplete(e:Event):void {
dispatchEvent(new Event(STORE_SUCCESS));
}
private function onError(e:IOErrorEvent):void {
dispatchEvent(new Event(IO_ERROR));
}
}
}
[/as]

サーバ側では .htaccess でリクエストをキャッシュ処理を行うスクリプトに集め、
URIのパスを key、リクエストデータを value にしてストレージに突っこむだけです。

・.htaccess 例 (mod_rewrite)

RewriteEngine on
RewriteBase /contents/flex/stereovision
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*) bmp_cache.rb

上記のような .htaccess ファイルをベースURIのディレクトリに置いてください。
つまり、
https://rest-term.com/contents/flex/stereovision/tsukuba_stereopair/5/10
などでアクセスされた場合、
https://rest-term.com/contents/flex/stereovision/bmp_cache.rb
にリクエストを集めます。そしてキャッシュのキーには、

/contents/flex/stereovison/tsukuba_stereopair/5/10

を利用することになります。

サーバ側の処理は各々好きな言語で。
簡単なWeb APIを作ればいいので特につまづく所もないかと思います。
AMF-RPCサービスを作ってやりとりする方法でもいいですが、
なんにせよ退屈な処理なので、工数が少なくて済む方法を選ぶといいです。

・サーバサイド 擬似コード :Sinatra ライク
[ruby]
# GET リクエストの場合はデータを返却
get ‘/stereovision/*’ do
content_type ‘application/octet-stream’
storage[request.path] # storage : 任意のストレージ
end

# POST リクエストの場合はデータを保存
post ‘/stereovison/*’ do
storage[request.path] = request.body.read
end
[/ruby]
サーバ側でクライアントのチェックはしておいてください。
あと、Rubyとかムリ!でもPHPならできるよ!という人は、
Limonade (is a PHP micro framework ~) というのもありますよ。
(「FlasherのためのLimonade入門」みたいな記事があると多くの人が幸せになれる、かも)

キャッシュバックエンド

計算結果をキャッシュしておくストレージには memcached と SQLite を試しました。

1. memcached (memcached 1.4.5, libmemcached 0.40)
画像データ部のみ格納された ByteArray のやりとりは特に問題なし。
ByteArray.compress() で圧縮したデータでも大丈夫でした。
でも共用のレンタルサーバだと memcached が使えない。。

※ “問題なし”というのは BitmapData.setPixels() でエラーが発生しないことを指します

2. SQLite (SQLite 3.7.3, ファイルDB)
SQLite を Key-Value Store 的に利用。
これも ByteArray のやりとりに支障はありませんでした。
memcached より速度面ではさすがに劣りますが、どの共用レンタルサーバでも大体使えます。

ステレオマッチングのデモは、キャッシュ用ストレージに SQLite を選択しました。
Flash Developer の方々はサーバサイドの言語にPHPを選ぶ人が多い(?みたいなので、
PEARの Cache_Lite とかで試してみてもいいかもしれません。

今回のようなステレオマッチング処理では、探索範囲などのパラメータにも大きく依存しますが、
256×192 pixel の画像でもマッチングの計算に数秒はかかってしまうので、
ファイル, ネットワークI/Oにかかる時間はほぼ無視できると考えていいと思います。
ByteArray.compress() にかかるコストも予想していたより小さくて安心しました。
キャッシュは行わずに、Alchemy を使ってC言語で計算させるパターンも試しましたが、
CPUに負荷をかけることには変わりない上に、パラメータ依存なので速度にムラが出ます。

また、データを一時的にキャッシュするのではなく、サーバ側で保存/管理するのが目的であれば、
SQLite などのファイル型ストレージを選択すればもちろん永続化できます。
例えば、ペイント系Flashで描画した BitmapData をサーバに保存する用途などに応用できるかと。

まぁ、
そもそもActionScriptが速ければキャッシュなんてする必要ないんですけど。。

・関連記事
Flashでステレオビジョン入門

あわせて読む:

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です