2010年7月19日月曜日

PHPExcelのメモリ馬鹿食いについて

===============
2010-10-20追記
関連POST
PHPExcel1.7.4の不具合?について
===============

PHPExcelのシート追加でメモリをガンガン消費しちゃう件。
リンク先を参考に解析。

SE奮闘記: 【PHP】PHPExcelがループ内でメモリを使いすぎる

ワーク
シート数
メモリ使用量(Byte)
改善前 改善後
1 21,233,664 21,233,664
2 22,544,384 22,544,384
3 25,165,824 23,855,104
4 30,408,704 24,903,680
5 40,632,320 25,952,256
6 60,293,120 27,262,976
7 100,139,008 28,311,552
8 180,092,928 29,360,128
9 340,000,768(推定) 30,408,704
10 659,816,448(推定) 31,457,280

改善前

$aSheet = $this->excel->getActiveSheet();
for ($i = 0; $i < count($this->data); $i++) {
    if ($i != 0) {
        $this->excel->addSheet(clone $aSheet);
    }
}

改善後

$aSheet = $this->excel->getActiveSheet();
$newSheets = array();
for ($i = 0; $i < count($this->data); $i++) {
    if ($i != 0) {
        $newSheets[] = clone $aSheet;
    }
}
foreach ($newSheets as $newSheet) {
    $this->excel->addSheet($newSheet);
}

改善前は大体次の数式のようにメモリ使用量が増えていってるっぽくてO(2^n)のオーダー(かな?)
f(n+1) = 2.5MB * 2^(n+1) + f(n)
改善後は次の数式でO(n)のオーダー。
f(n) = 1.25MB * n + 20MB

リンク先で触れられていた「シートをコピーして追加する」の処理をやめて「テンプレートからシートを流し込むとよい」という内容は確認できませんでした。
リンク先とは状況が違うとは思いますが、こちらの解析ではPHPExcel_Worksheetを__cloneする時にメモリがどんどん消費されていっているらしくて、PHPExcel_Worksheet#copy()も内部的には__cloneを呼んでいる。

だからと言って、
$this->excel->addSheet(clone $aSheet);

$newSheets[] = clone $aSheet;
になったことでそんなに劇的に変わる理由がよく分かりませんが、
$aSheet = $this->excel->getActiveSheet();
した$aSheetが指しているactiveなシートの参照先が
$this->excel->addSheet(clone $aSheet);
したことによってどうにかなってしまったんではなかろうか、と想像。

PHPExcelの実装の問題なのか、cloneする変数の参照先の親オブジェクトにループ内でメンバ変数追加するとそうなってしまうPHPの仕様なのか、何とも言えませんが、今後の為にメモ。

2010年7月9日金曜日

zend + dojo : 画像のsubmitの挙動が変だったハナシ

Zend + Dojoで画像ボタンでsubmitしようとしたんですが、サーバからの返却が何やらおかしかった。
<input type="submit">を<input type="image" src="">にしたい場面はたくさんありますが、ajaxでの接続で挙動がおかしい場面があったのでメモ。

通常のボタンでsubmitする場合

※HTML

<form action="#" id="hoge">
    <button name="btn_submit" id="btn_hoge" type="button" value="hoge">hoge</button>
</form>

// dojo.connect
<script type="text/javascript">
    // イベントハンドラの登録
    dojo.addOnLoad(function() {
        // btn_hoge要素にイベントハンドラを設定する。
        // onclickイベントを引っ掛けて、hogeフォームの内容をsubmit
        // /fuga/piyoアクションをコールして、json形式でレスポンスを受け取る。
        dojo.connect(dojo.byId("btn_hoge"), "onclick", function() {
            dojo.xhrPost({
                url: "/fuga/piyo/format/json",
                content:{"a":"b"},
                form: "hoge",
                handleAs: "json",
                load: function(data){
                    alert('OK');
                },
                error: function(error, args) {
                    alert('NG');
                }
            });
        });
    });
</script>
※Zend

/**
 * Fugaコントローラ
 */
class FugaController extends Zend_Controller_Action
{
    /**
     * piyoアクションのコンテキストとしてjsonを登録
     */
    public function init()
    {
        parent::init();
        $contextSwitch = $this->_helper->getHelper('contextSwitch');
        $contextSwitch->addActionContext('piyo', 'json')
                      ->initContext();
    }

   /**
     * Piyoアクション
     */
    public function PiyoAction()
    {
        // View変数をシリアライズしてjsonとして返却
        // {"viewMessages":"abc"}
        $this->view->message = 'abc';
    }
}

上記は、普通にajax通信できますが、以下のようにボタンを画像にすると、dojoのイベントハンドラでerror関数がコールされ、error変数として「Error: Unable to load /fuga/piyo/format/json status:0」という良く分からないエラーが返却されます。

※HTML

<form action="#" id="hoge">
    <input type="image" src="" name="btn_submit" id="btn_hoge" value="btn_submit" />
</form>

サーバでログ取ってみても、postされてたりされてなかったり、規則性なくランダムにサーバにリクエストが到達しているようでした。
error関数も複数回コールされることがあり、1回のボタン押下で何回か/puga/piyoアクションがコールされることもありました。

何でそんなことになるのか分かりませんでしたが、dojo.connectでinput属性のエレメントにハンドラを設定しているのが問題っぽい。

<form action="#" id="hoge">
</form>
<input type="image" src="" name="btn_submit" id="btn_hoge" value="btn_submit" />

こんな感じでformの外にimage要素を配置すると、サーバには1回だけ行儀よくPOSTし、返却値も期待するJSON形式で受け取れるようになります。

formの中にいる<input type="image">な要素にdojo.connectすると、dojoで接続されたイベントハンドラと、form自体のactionとの両方が実行されてしまうようです。