JavaScriptに"Maximum call stack …"で怒られた1例と解決までにやったこと

Web開発/デザインjavascript

どうも最近はCordovaでのアプリ開発熱がすごいです。そうなると使わなくてはならないのがWeb系の言語たちです。特に自分の実装ではHTML+CSS+JS(+jQuery)というのがデフォルトという感じになっています。ReactさんやAngularさんなど新しい技術はたくさん出てきますが、やはり慣れ親しんだ技術の方が使いやすいですね。

さて、今回はJavaScriptのお話です。というのも先日アプリを開発しているとき、初めて見る下記のようなエラーに遭遇しました。

エラーの内容としては大したことは無く、ただ「呼んだ関数なんかを置いておくスタックが一杯になっちゃったよ」というだけです。しかし、コードを確認しても目視ではスタックが一杯になりそうな箇所を見つけることはできませんでした。そんなわけで、今回はこのエラーを退治するためにやったことを備忘録的にまとめようと思います。

時間のある方は本文を読んで原因を推理しながら進んでみてください。時間のない方は”答え合わせ”をご覧ください。



jQuery: 1.4.1
Chrome: 58.0.3029.110 (64-bit)


1. 症状の確認

さて、まずは症状の確認です。具体的には、何をした時に発生するのか、(プログラムの)どこで発生するのかをできる限り確認します。もちろんこれは開発しているプログラムによって全く異なる項目です。そのため具体的なコードは提示できませんが、該当箇所を見つけやすくするための方法を一つ紹介しておきます。

使うのはおなじみWebブラウザのChromeです。ChromeにはWeb開発において便利な機能がたくさんありますが、今回使うのはデベロッパーツールにある”Perfomance(正しいツールの呼び方は知りません)”です。このツールを使うと、メモリやらネットワークやらCPU使用量だったりをモニタリングしてヒストグラムのような図にまとめて表示してくれます。

では早速使ってみましょう。”Perfomance”を起動すると以下の画像のような画面が出てきます。

そうしたらモニタリングしたいページを開き、画像内の左上にある黒丸を押して録画(仮)を始めます。この状態のまま、通常通りWebページを操作してください。今回の場合では、件のエラーが発生するような操作を実際に行ってみてください。一通り操作を終えたら、もう一度、丸ボタンを押して録画を終了してください。すると、画面が以下の画像のようになると思います。ちなみに、この画像は記事のために撮ったもので、単にYoutubeをブラウジングした結果です。

今回、この画像で重要になってくるのが、画像中央部の黄色やら青色、紫色なんかが下の方に伸びている”Main”の項目です。画像では途切れてしまっていますが、右側のスクロールバーを動かすことによって下までながーく伸びているのがわかると思います。このグラフを拡大してみて見ると、下の画像のように各ボックスが関数の呼び出しやfor文なんかに対応しているのがわかると思います。

今回はYoutubeさんの結果なので関数名がかなりわかりづらくなっていますが、ご自身のプログラムなら見覚えのある関数名もちらほらみられると思います。ちなみに今回は最小化されたjQueryを使っているということで、関数名が短くなっていました。もし短くされた関数名がわかりづらいなら、デバック中は最小化されていない物を使ってみるのも手だと思います。

さて録画の結果をもとに具体的にどこが悪そうかを確認します。今回はスタックがオーバーするほどの呼び出しが行われているので、”Main”でのボックスがながーーく伸びているところが怪しいです。なので、その部分に登場する関数名やそれがプログラムのどこから呼ばれているのか(“Main”の画面上ではボックスが伸びている根っこの部分になると思います)を詳しく確認しておいてください。

今回遭遇した例では、jQueryのanimate関数、それを呼び出している自作したfade_out_chart関数がたくさん見られました。ここら辺を手がかりにデバッグしていこうと思います。

2. 閑話 既出の例

さて、このページにたどり着いた方はすでにやっていると思いますが、このエラーが出た人はどんなことをしてしまったのかを調べてみようと思います。すると、以下のような例がたくさん出てきました。

. 再帰呼び出しに終わりがない,深すぎる
. アニメーションが重すぎる

調べた限りですとほとんどの場合、原因は1番目みたいです。一応超簡単に再帰呼び出しを説明すると関数なんかを実行する際に処理内部で自身をもう一度呼び出すなんてことをする関数です。極めて単純な例だと以下のコードにあるhoge関数が再帰呼び出しをする関数になります(以下のコードは無限に続くためスタックオーバーのエラーが出ます。なので実行しないことをオススメします)。

この場合の対処法としては、再帰が極端に多くならないようにコードを書き換えるかsetTimeoutという非同期で関数を呼び出す関数を使ってスタックを溜めないようにするという方法があります。非同期での方法はこちらのサイトがわかりやすい気がするので、おいておきます。

もう一つの原因でのアニメーションが重すぎるという物ですが、jQueryのアニメーションでは内部的に再帰呼び出しを使っているらしい(未確認)ので、根本的な原因は一緒ということになります。こちらの対策としては、アニメーションを軽くするか、CSS3の機能でのアニメーションを使うといいらしいです(未検証)。

というわけで、調べると以上のような例が挙がります。しかし、残念ながら今回これらは原因ではありませんでした。先ほど確認したようにjQueryのアニメーションが”Main”のボックスに頻出していましたが、この部分をコメントアウトしてもスタックオーバーが発生したので、関係はありませんでした。

3. 自分のプログラムをみる

閑話休題。既出の原因は該当しなかったので、改めて自分のコードを確認します。アニメーションは原因でないことはコメントアウトで確認できたので、もう一つの原因候補である自作関数fade_out_chartを深堀していきます。

この関数はその名の通り画面に表示しているチャートをフェードアウトさせる関数です。この関数はHTML内で記述されているcanvasタグがクリックされた時に発火するようにonClickに登録しています。今回の症状では、フェードアウトを何回か行うとスタックオーバーが発生するという感じです。もちろんこの関数では再帰呼び出しは行なっていませんし、アニメーションをしないように書き換えてもスタックは溢れました。

余談ですが、この関数と対になるfade_in_chartという関数も使っています。こちらは動的に生成される項目リストの各要素(liタグ)にonClick登録しています。こちらは、何度実行しても全く問題はありませんでした。

4. printfデバッグ

もしかすると玄人さんなら、上記の説明だけで原因の検討がついてしまったかもしれませんが、もう少し続きます。

皆さんは、デバッグするときはどのような手法を使いますでしょうか。世間には色々な手法やツールがあると思いますが、自分がよく使うのは”printfデバッグ”です。これはコードの確認したい部分に適当な出力命令を書いて、その部分における変数の値なんかを出力して確認する手法です。また、適当に出力命令を書くことで、その部分までプログラムが到達しているか、いつに何回実行されたかというのを確認しやすくなるという側面もあります。今回はJavaScriptということで、printfの代わりにconsole.log(‘…’)を使っていきます。

さて今回の場合だと怪しいのはfade_out_chart関数なので、この関数内にconsole.logを置いてみました。こうすることによりfade_out_chartが実行された瞬間、ブラウザのコンソールにログが出力されるはずです。あとは、そのログを確認することで、この関数がいつ何回呼ばれたかが分かります。

確認の結果、canvasをクリックし、fade_out_chartを実行するにつれ、出力されるログの数が1,2,4,8,16,32,…と倍々に増えていっているのが確認できました。もちろん本来ならfade_out_chartの実行1回につき1つのログが増えるはずです。このことから、fade_out_chart関数が実行されるにつれ、倍々の回数だけ処理が行われてしまっていることが分かります。

5. シンキングタイム

さて、ここで状況をまとめてみます。問題となっているのは”‘Maximum call stack size exceeded’というエラーが発生する”ということでした。この原因を推理してみてください。上記の文章をまとめると以下のようになります。

・ animate関数とfade_out_chart関数がスタックに頻出
・ animate関係をコメントアウトしても解決ならず
・ fade_out_chartは再帰呼び出しをしない
・ fade_out_chartはcanvasをクリックすると実行される
・ fade_out_chartの処理数が倍々で増えていく

これだけで、わかった方は相当な玄人さんだと思います。そもそも文章だけで答えを考えるの酷なんですが、おまけで追加のヒントです。

・ canvasへの関数紐付けはclick関数を利用

・ 問題無かったfade_in_chart関数は動的に生成されるリストへ紐付けしていた
・ fade_in_chartとfade_out_chartの紐付けは同じタイミングで行なっていた

さて、原因はわかりましたでしょうか?

6. 答え合わせ

では答え合わせです。全ての元凶はclick関数による関数の紐付けでした。動的に生成されたHTMLタグに対し、いくら紐付け済みのクラスが付与されていたとしても、改めて紐付けを行わなくてはいけないのは、有名な話です(盲点になりやすいですが……)。つまり新しい要素が生成される毎にclick関数を実行しなくてはいけません。

そのため、今回のケースではチャートがフェードアウトし、リストが表示(生成)されるたびにfade_in_chart関数を紐づけるためのclick関数を実行していました。

しかしこれと同時に静的なタグのcanvasに対してもfade_out_chart関数の紐付けをしてしまっていました。以下のコードを試してみると分かると思いますが、jQueryにおけるclick(+ $.on など)での紐付けは追加式なんですね。

なので、一つの要素に複数回の紐付けを行うと、その回数だけ処理が行われてしまうわけです。

今回はそのせいで、倍々に増えていくfade_out_chartに耐えられずスタックオーバーという結論でした。そこまで大規模なプログラムでもありませんし、原因なんてこんなものです。ただ、たったこれだけの原因で原因調査に丸一日持っていかれたのが悔やまれます。

解決方法としては、複数回clickを実行されないようにコードを書き換えるか、clickイベントを紐付けする前に$.off(‘click’)を実行すればいいみたいです。


コーディングで分からないことがあれば
プログラミングや環境構築で分からないことがあったら『teratail』というエンジニア特化型のQ&Aサービスがオススメです。自分もどうしても分からないことがあったら、時々質問しにいきますが、かなりニッチな質問にも意外と早く回答がつくのでとても頼もしいです。という宣伝でした。


おわりに

一旦、未知のバグが発生すると、その原因を発見するのは至難であることは多々あると思います。ですが昔の人は言いました 「プログラムは思った通りには動かない。書いた通りに動く」と。つまり……何が言いたいのかわからなくなってきたところで今回は終了です。

2017-06-13Web開発/デザインjavascript