Safari/Mobile Safari(iOS) にクロージャを使った計算で桁溢れするバグ

前回のエントリ JavaScriptのfor/forEach/jQuery.each/Ext.each のパフォーマンスを計ってみた。の検証中に発見したバグの話し。

Safari 6.0.4 (8536.29.13) 及び、Mobile Safari(iOS 6.1.4)では、クロージャを使った計算では、桁溢れする可能性があります。前回検証に使ったスクリプト http://jsfiddle.net/dsuket/DR33Q/ の、nativeForEach 関数などがその対象です。

    var nativeForEach = function() {
        var sum = 0;
        list.forEach(function(item){
            sum += item;
        });
        return sum;
    }

上記スクリプトをループ回数 1,000,000回以上にした場合、桁あふれが起きて計算結果が正しくでません。

WebKitのバグレポートにもありました。
Bug 104967 - Summary: javascript integer overflow

どうやらJITコンパイラの最適化のバグのようです。JavaScriptの仕様的には Number は IEEE754標準の64ビット浮動小数点数形式で、表現可能な最大値は±1.7976931348623157×10308 です。しかし、パフォーマンスアップのため小さい数の場合はJITが int32として計算するようです。大きくなるとint64となるはずなのですが、クロージャを使った場合うまくいかないという問題です。そのため32bitの範囲(+2147483647〜-2147483647)を超えると桁溢れを起こします。

なお、ChromeFireFoxではもちろん期待通り動作します。SafariでもWebインスペクタを開いていると、この現象は起きない(ちゃんと計算できている)ためデバッグ中は気がつきにくい。。。

上記のバグレポートでは半年も前に RESOLVED FIXED になっているのですが、まだ Safari/Mobile Safariには反映されていません。普段 Safari をメインに使うことはあまりありませんが、iOSの場合は注意が必要ですね。

実はこのような話は昔からあるようです。

JavaScriptのfor/forEach/jQuery.each/Ext.each のパフォーマンスを計ってみた。

先日、Sencha Touch 2 ソースコード読書会 第2回@東京でナビゲータを務めさせて頂きました。その時の様子は以下などに。

この中で、Array.forEachと Ext.each のどちらがどのくらい速いのか、というのが話題に出ました。そういえば昔、JS ArrayのforEach, filter, map の速度を調べてみた。というエントリで調べました。あれから1年くらい立ってるんだけど、最近はどうなのかまた調べてみました。

jsfiddle を使って、生のfor文、Array.forEach、Ext.each、jQuery.each の比較ができるようにしました。
http://jsfiddle.net/dsuket/DR33Q/

配列に0から指定数まで数字を入れて、ループでそれらを足し合わせる計算時間を測定しています。私の環境(iMac Early 2009 )だと 1000万回くらいから差が出てきました。以下は OS X 10.8.3/Chromeで、各五回実行した最小の測定結果。単位は ミリ秒。

Chrome 27.0.1453.93

回数 for forEach jQuery.each Ext.each
100万 0 7 11 10
1000万 2 106 156 149
1億 20 762 1284 1189

for文が圧倒的に速いですね。jQuery や Ext の each は ネイティブの forEach に比べると 60%程遅いようです。あと、なぜか jQuery より Ext のほうが何回やってもちょっとだけ速い!

FireFoxSafariでもやってみました。

FireFox 21.0

回数 for forEach jQuery.each Ext.each
100万 0 2 4 2
1000万 3 15 30 15
1億 51 138 322 141

Safari 6.0.4 (8536.29.13)

回数 for forEach jQuery.each Ext.each
100万 6 27 22 25
1000万 68 265 256 257
1億 680 2915 2460 2507

FireFox 速い!forEach がかなり速いですね。Ext.eachも負けないくらい速いです。それに比べ、Safari はループが遅い上に、初期化もすごく時間かかりました。さらに、Safariにはとんでもないバグがあることがわかりました。。

その話はまた次のエントリで。
→ 書きました! JavaScriptのfor/forEach/jQuery.each/Ext.each のパフォーマンスを計ってみた。

gruntで更新されたファイルを対象にタスクを動かす方法

JavaScript のビルドツール grunt が熱いということで使い始めました。

Sencha Touchの場合は senchaコマンドでビルドできるのですが、それ以外のプロジェクトでリリースビルドを作るのに、sassのコンパイル、cssのminify、jsの結合とminify、そしてリリースディレクトリへのコピーなどを一括で行うために使っています。

便利なんだけど、毎回全ビルドが走るのがどうにも非効率だなぁ、と感じていました。更新されたファイルだけ対象にしてくれればいいのに。というかantとかrakeとか他のビルドツールには普通あるでしょ!

同じような事を思っている人は当然いるわけで、gruntに提案されています。しかしまだ取り込まれていないようです。→ Provide conditional support for file inclusion

ここでも言われてますが、gruntのタスクに定義するファイルには filter オプションというのがあって、タスク実行時にsrcファイルを任意の関数でフィルタリングすることができます。これを使えば、更新されたファイルだけ対象することができそうです。

と、いうわけで作ってみた。dsuket/Gruntfile.js

3つのフィルタ関数を作りました。

  • newer
    • srcがdestよりも新しければ true を返す。
  • expandNewer
    • destにディレクトリを指定して、srcの各ファイルと比較する。
  • newerAny
    • srcの何れかがdescよりも新しければ true を返す。

grunt のカスタムフィルタ関数は、タスク実行前に srcファイルを引数に呼ばれます。true を返せばそのファイルを対象とし、falseならば対象としません。Custom Filter Function

フィルタ関数の引数には srcしか渡されないため、destファイルがわかりません。そのため、フィルタ関数を返す関数を作成し、その引数に dest ファイルを指定するようにしました。やや冗長ですが仕方ない。

悩ましいのが、newerAny。フィルタ関数は1つのsrcファイル毎に呼び出されるため、予め全てのsrcがわかりません。そこで第2引数にsrcの定義を取るようにしました。配列でも定義できますが、文字列で与えた場合、その変数の値を取ります。(あんまりいけてない)

大体使えるようになりましたが、まだ問題はいくつかあります。

テンプレートが使えない。

テンプレートの展開は grunt.initConfig() の中で行われるので、newerのターゲットでテンプレートを使っても展開されません。

watch で newerAny が更新されない

最初のsrcファイルのフィルタ実行時にしか判定しないため、watchで起動したときは更新されません。毎回チェックするようにすればいいんですが、今のところwatchはあまり使っていないのでペンディング。

newerAny で expand が使えない。

srcファイルの展開は実行時行われるため、定義時にsrcの一覧が取得できません。


と、まぁ幾つか細かい問題がありますが、このフィルタの最大の問題点は、uglify で失敗するということです。

grunt-contrib-uglify タスクが、srcファイルが無い場合エラーで止まる問題があります。filterの結果、更新ファイルがなければ src は空になるのですが、その場合以降のタスクが止まってしまいます。他のタスクはsrcが空でも動くようになっているので、grunt-contrib-uglifyのバグだと思います。一応修正版を github にアップしました。(pull requestも出しておいた)
https://github.com/dsuket/grunt-contrib-uglify


参考:

ネイティブよりも速いと評判のfastbookの裏側を調べてみる

本記事は、Sencha Advent Calendar 2012 の23日目の記事です。1日遅れでごめんなさい!あ、今日であってた(;´▽`A 当初完走は無理なんじゃないかと思ってたけど、みんなのがんばりで完走目前!おめでとうございます!

Sencha Touchで作ったネイティブよりも速いfacebookアプリ「fastbook」が話題になっていますね。


f:id:dsuke:20121223112253p:plain

ネイティブを凌ぐすごいパフォーマンスに驚きますよね!これらは実際にはどんなコンポーネントを使って作られているのかすごく気になります。そこで、ちょっと調べてみたいと思いました。

ネイティブアプリ向けパッケージが、https://github.com/extjs/fastbook にあるようなのですが、メンテナンス中と出てアクセスできません。。ソースみたかった。。。2012.12.26追記: 今見るとアクセスできますね。けど、本当にWebViewでラップしただけで中身はWebから表示してるだけでした。つまりJSのソースは無い) 仕方ないので公開されているものを解析しつつ推測してみます。

基本的に3つのスクリプトファイルからできているようです

  • index.js: 他のスクリプトやCSSを読み込むローダー
  • app.js: アプリケーション本体
  • sdk.js: Sencha Touch SDK(3.0alpha!)

app.js にアプリのロジックが圧縮されて入っています。圧縮と言ってもそれほど複雑な物ではないので、Webインスペクターで Pretty print すれば、まあなんとか読めます。気になるのは、sdkですね。Ext.versionを見てみると、versionが "3.0alpha" とあります!まだ一般公開されていない、次期 Sencha Touch で作られてるんですね!

とりあえず app.js を記事で触れられている改善点を中心に見てみました。

パフォーマンス改善のポイントはいくつかありますが、特に無限Listコンポーネントが重要のようです。これは最近、Sencha Touch 2.1で改良された無限ListコンポーネントWhat’s Coming in Sencha Touch 2.1)のことかと思ったら、実はそうでもないようです。

Sencha Touch 2.1.0 の SDK に含まれる examples に、touchtweets というTwitterアプリのサンプルがあります。これは新しい無限リストに対応しているそうです。(オンライン上のデモはまだ2.0.1のようだ)

fastbookではiframeを使ってDOMツリーを分割したとあります(実際はobjectタグだった。iframeではなくobjectなのはなぜだろう?)。しかし、先のサンプルのリストはiframeではありませんでした。fastbookで使われているのは「Sandboxコンテナー」というまったく新しい機能のようです。残念ながらまだSDKには入っていません。 Sencha Market なんかで出してくれないかなー、と期待してます。

次の、TaskQueueは、Ext.TaskQueueのことですね。これはSencha Touch 2.1から新しく追加された機能です。複数のDOM要素の操作をバッチ処理のようにまとめることで、DOMのリフローとリペイントを最小化するようです。リフローについては ブラウザ動作の理解-リフローとリペイント及びその最適化 に詳しくあります。ただ、まだこのクラスはAPIドキュメント上はprivateとなっていて、フレームワーク内部での利用しか推奨されていません。

次に AnimationQueue を追加したとあります。これもfastbookのための新規クラスです。これは内部に taskQueue と idleQueue の二つのキューを持ち、taskQueueがある間はそれを実行し、taskQueueが無くなったら idleQueue を実行する、という仕組みでアニメーションやロードの処理を分割しているようです。特にアニメーションに特化した機能では無く、汎用的な仕組みなため、これも早く標準APIへ組み込んで欲しいですね。

Web Workerは、Ext.Xhrという新しいクラスに組み込まれています。また、これ用に xhr-worker.js が読み込まれています。このスクリプトでWorkerとのmessageやりとりをしているようです。Web WorkerではAjaxでのリソース取得、デコードを行っています。元々Ajaxで非同期通信ではあるのですが、WebWorkerを使うことでUIスレッドから切り離し、よりスクロールの滑らかさなどを維持するようです。特に現在のマルチコアプロセッサを持つスマートフォンなどでは有効だそうです。

総じて、やはりfastbookはかなりチューニングされています。通常Sencha Touchで使われるMVCアーキテクチャでのアプリケーションのロード方法とも少し違います。カリカリにチューニングされているからこそ、この性能が出るんですね。

単純にSencha Touchを使ったからといって、皆が皆ここまでのアプリケーションを作れるわけではありません。それはネイティブにとっても言えることでしょう。ネイティブだからパフォーマンスが良い、HTML5(Sencha Touch)だから優れている、というのはナンセンスです。大筋の傾向はありますが、個別事例ではそれが全てではありません。結局は作り手の技術と情熱次第。

そういう意味で、fastbookはSenchaが優れているということではなく、まさしく作り手次第でHTML5でもここまで出来るんだ、ということを証明してくれたのだと思います。

Sencha Touch 2.1の変更点まとめ

本記事は、Sencha Advent Calendar 2012 の3日目の記事です。Sencha Advent Calendar ですが、今のところ参加者がとても少ないです。。どんなネタでも大歓迎ですので、ご参加お待ちしています!

先月、11月6日に Sencha Touch 2.1 がリリースされました。ざっくりと以下のような点が追加・改善されたようです。

  • パフォーマンスの改善
    • ネストしたレイアウトでのパフォーマンス改善
    • paintedイベントへの変更
    • TaskQueueメカニズムの追加
  • Chart の機能改善
    • Drawパッケージの改善
      • 拡張可能なスプライト
      • 修飾子と属性の定義
      • フライウェイト・インスタンス化・スプライト
      • Retinaディスプレイサポート
    • Chartパッケージに新しいチャートを追加
      • 集合チャート
      • 金融チャート
  • コンポーネントの強化
    • 無限スクロール付きリスト
  • 新しいSencha Cmd
    • 強化されたネイティブサポート

詳しくは以下をみてください。

ところが、2.1で新しくなったのはこれだけではありません。というか、全然もっとある!!ということで Release Notesから、個人的に興味あるものをピックアップして紹介したいと思います。

チャート系

  • Implemented SVG support for Draw
  • SVG is now forced on Android 4.1 devices due to Canvas issues
  • Added webkitBackingStorePixelRatio support

チャート系のアップデートに伴い、draw系パッケージも各種強化されました。詳細は割愛。詳しくはRelease Notesみて下さい。Android 4.1 ではCanvasの問題から強制的にSVGになるようです。

dataパッケージ系

  • [TOUCH-2792] Implemented filters, sorters and paging into WebStorage proxy
  • [TOUCH-2844] Optimized clear method on WebStorage Proxy

WebStorageでもフィルタ、ストア、クリアができるようになった!

  • [TOUCH-2800] Implemented new WebSQL proxy
  • [TOUCH-3551] Added filtering and sorting capabilities to the SQL proxy

ようやくWebSQLプロキシが実装されました!!フィルタとソートも追加。

Sencha Command 関連

  • in-app purchases API for iOS
  • Contacts APIs for iOS and Android
  • Push notifications for iOS
  • openURL support

アプリ内課金、連絡先API、プッシュ通知などにも対応!

パフォーマンス

  • SizeMonitor refactored for improved performance
  • Improved Android 4 Scroll Indicator performance
  • Update Button SASS so it looks better on device
  • Change ListItems to not use vertical docking for headers. This improves performance since there is less DOM and less box layouts

スクロールや回転、リストのパフォーマンスアップ。ボタンデザインの修正。

  • [TOUCH-3497] Added support for iPhone 5 resolution startup screen

解像度が変わりましたからねー。

その他

  • [TOUCH-2888] Changed the default zIndex in message box to a larger number to stop accidental zIndex issues when creating other floating panels/components
    • 1.xの頃からあるバグで昔私も報告したなー。ようやく直ったようで。
  • [TOUCH-3042] Allow hideOnMaskTap to be changed dynamically
    • これは地味に欲しい機能。
  • [TOUCH-3031] Allow JS files to be specified as remote
    • JSの部分遅延ロードができるのかな?
  • Ext.browser, Ext.feature, Ext.os が追加
    • 機能判別やブラウザ、OS判定が便利になった!
  • POST calls will now honor the disableCache option
    • 例の iOS6 のPOSTキャッシュのバグ回避?
  • Added proper support for IE10 animateStartTime
    • IE10サポートも進んでいるようです。

総括

Release notesからざっくりと拾い出してみました。実はまだドキュメントに載っていない新機能が色々とありました。ソースを見ると入っているのですが、ドキュメント化が間に合ってないみたい。2.1 になって、細かい使い勝手などが結構改善されています。2.0 → 2.1 への乗り換えはそんなにコスト掛からないと思うので、是非検討されるとよいんじゃないでしょうか?

あと、たまにRelase notesを見ると思わぬ情報があったりしておもしろいですねー。ちゃんとドキュメント読みましょう、ということですね。

さて、Sencha Advent Calendar 明日は、また @kawanoshinobu さんです!その次が未定なので、ぜひ誰かJOINしてください!(切実

Chrome/Safari でスマホサイズの表示テストをする方法

スマホ用のWebページ(アプリ)なんかを作っていると、ChromeSafari でその画面サイズで表示を確認したいことが多いですよね。インスペクタ便利だし。実機とかエミュレータとかめんどくさい。

一般的なスマホの画面サイズ 320 x 480 にブラウザのウインドウサイズを変更すれば、メディアクエリとかで自動的に表示を切り替えてくれるはず。が、私のMacだとなぜか Chrome/Safariの横幅が 400px までしか縮められない!微妙に足りない!!(Windowsならもうちょっと縮むとか??)

サイズを変更する Chrome Extension とか試してみましたが、だめでした。やはり 400px 以下になりません。しかし、JavaScriptで別窓を開けば自由にリサイズできることが分かりました。

そこで、今見てる画面を、スマホサイズで別窓で開くブックマークレットを使うことにしました。 コードは以下の通り。 javascript:window.open(location.href, 'phone', 'width=320,height=480'); このアドレスを適当な名前(PhoneViewとか)でブックマークに登録しておくと、一発でスマホサイズで確認できて便利!(ブラウザのポップアップブロックにひっかかる場合があるので注意)

ほんとは UA とかも変えたかったけど、JSでは変更できないため断念。Extension作ればUAの変更できるらしいが、とりあえずそれはまた別の機会に。

HTML5 x Touch UI の UXを考える(補足)

昨日、ありえるえりあミニ勉強会#3 ~Sencha Touch で、「HTML5 x Touch UI の UXを考える」 を発表しました。 発表資料は こちら HTML5 × Touch UI の UX を考える

川野さんの「Sencha Touch - カスタムコンポーネントを作るためにおさえておきたい 10 のステップ」、森本さんの「About Sencha Architect」も非常に面白く、勉強になりました!Sencha 使ってない方でも勉強になるかと思いますので、ぜひ次回でも覗いてみて下さい!

そして、懇親会も楽しかった!まさかみんな Sublime Text 2 ユーザーになっているとはw 他にも Inkpod(Desktop版) を使ったことあるという方もいてびっくり!色々と刺激を受けることが出来ました。

帰って Ust で自分の発表見直してみたら、やたらと咳払いが多いですね。。。あと、ちょこちょこ移動したせいで音声が聞こえないところもチラホラ。せっかくマイク用意してもらったのに。。等々 お聞き苦しいところも多々有り申し訳ないです(−−; 今後改善したいと思います。

さて、このEntryでは、資料では書ききれなかった点、こぼれ話なんかを補足したいと思います。

Touch Position (p.5)

Nikkei BP さんの記事 iPhone/iPadに込められた「見えないデザイン」を参考にさせて頂きました。この話は、あくまで特許から実装されている機能を予測する、というもので、実際にはどうなのか検証された方もいました。日経BP『iPhone/iPadには”見えないデザイン”が込められてる』記事がホントかどうか検証してみた(エミュレータ編)

少なくともエミュレータでは6角形ではないようです。ただ、当たり判定についてはやはり動的に変わっているように見えます。iPhoneでソフトウェアキーボードを英語で表示して、「S」キー(少し左寄り)を連打すると、「Sa」と入力されます。Xperiaなどではキーピッチが見た目自体可変でしたけど、iPhoneは内部的に可変の可能性が十分にあると思います。

また、iOSでは押した場所(実際にタッチされた箇所)よりも確実に、少し上が認識されます。指で普通に使う分にはむしろ自然に狙った位置がポイントされます。タッチペンをつかってもあまり違和感を感じません。しかし、タッチペンを垂直に立てるなどすると、上向きにずれているのがよく分かります。逆さにしても同様。ボタンがタッチにしにくくなるのがわかります。

これは斜めに画面にタッチすることを想定し、指(またはペン)の軸の延長線上をタッチポイントとしたほうが自然と感じるのだと思われます。HW的なタッチエリアの楕円形状や扁平率から、傾きを検知し、ずらす量を調整、までしていればすごいんだけど、そこまではやっていないようです。

なお、Android(2.3,3.0,4.0で検証)ではこの上方向への補正は感じられません。恐らくAppleの特許が影響しているのではないかと思います。

 

Touch Feedback (p.6)

視覚フィードバックの他に、バイブレーションを使う方法もあります。Androidなんかでは結構使われています。iOS で使われていないのは特許のせいかポリシーかは分かりません。

Webで使うには、Vibration API というのが現在仕様策定中です。以下のように使えるようです。

  navigator.vibrate(1000);

ただ、2012/07/27現在、これが使えるのは Firefox Aurora Preview(Android 4.0以上)のみだとのことです。

 

Touch Event Specification (p.8)

jQuery の中の人のBlog  jQuery Blog: GETTING TOUCHY ABOUT PATENTS で、タッチイベントの歴史、現在の状況、将来、について非常に良くまとめられています。MSPointerが技術的には将来有望だとみている点など、興味深い話です。

 

Touch Event Interface (p.10)

W3Cの仕様で最後の Editor's Draft で大変興味深い仕様がありました。

Touch Events version 2 W3C Editor's Draft 14 November 2011

TouchEvent に radiusX, radiusY, rotationAngle というプロパティがあります。これは、タッチした範囲の楕円半径と、その傾き角度。つまり、タッチエリアのrawデータがとれるということです。

これが使えれば、上述の指の傾きを検知してずらす量を調整などもアプリ側でできるかもしれない。是非とも仕様に入れて欲しい!

 

TouchEvent のはまり所

touchEvent ではまりそうな所を2点。

touchEvent は現在タッチされているポイントを、touches、changedTouches, targetTouches というプロパティとして持っています。touches はすべてのタッチ、changedTouches は変更があった点、targetTouches はその要素上のタッチの配列として取得できます。

注意するのは、toucheend イベントの時、touches は空になっている、という事です。既に指が離れたため、現在タッチしている点は無いよ、ということなのだと思います。そのため、離れたタッチを取得するには changedTouches を使用します。

二つ目は、 2本以上で同時にタッチした時、touchstart イベントが1回の場合と2回発生する場合がある。ということ。

完全に同時にタッチしたときはtouchstart イベントは1回だけ発生し、changedTouchesに二つのタッチが入ります。わずかでもタイミングがずれると、touchstart イベントは2回発生し、それぞれ changedTouches に 1つずつ入ります。

なので、マルチタッチ操作を実装する場合、どちらでも動くように対応する必要があります。

 

GestureEvent (p.11)

ジェスチャーイベントの発生順序は少し癖があるため、注意が必要です。

GestureEvent Class Reference に詳しくありますが、タッチイベントとジェスチャーイベントの発生順序は以下のようになっています。

  1. touchstart: 1本目の指がタッチ。
  2. gesturestart: 2本目の指が触れると gesturestart が発生。
  3. touchstart: 2本目の指のタッチイベント
  4. gesturechange: 指を離さず、動かしたときに発生
  5. touchmove: 動いた指のタッチイベント
  6. gestureend: 2本目の指が離れたら(指が残り1本になったら)発生
  7. touchend: 2本目の指が離れたタッチイベント
  8. touchend: 最後の指が離れたタッチイベント

GestureEvent は touches プロパティをもっていません。そのため、タッチポイントを取りたいときは、TouchEvent で取得したものを保持しておく必要があります。

しかし、上記の順序 2. で、まだ2本目の指の touchstart イベントが来る前に、先にジェスチャーイベントが発生します。このとき、まだ touches は 1 つしかないのため、2つのTouchを取得できません。

 

User action event restrictions (p.21)

私が勝手に命名しました(^^; 

キーボードが出ない理由にはついては不明ですが、動画の再生については Apple から説明があります。

携帯が従量課金とかの可能性があるから、autoplayとautobufferは無効にしてるよ。ユーザが意図して再生を開始するまで、データはロードされないよ。

ということだそうです。

 

ちなみに、Sencha Touch (1.1の場合) でこれらを操作するために、ちょっとした裏技があります。Sencha Touch では通常ブラウザが出すクリックイベントは抑止して出ないようになっていますが、以下のコードをアプリ起動前に設定することで、デフォルトのクリックイベントを抑止しません。

  Ext.gesture.Manager.defaultPreventedMouseEvents = [];

これを設定すると、ブラウザが出すクリックイベントと、Sencah Touch がエミュレートしたものと2個発生します。Sencha Touch  がエミュレートしたクリックイベントは isSimulated フラグが true になるので、それでどちらか判断できます。

ブラウザが出す clickイベントの方で、これらの制限機能を使うことができるようになります。

 

Scroller (p.22)

iScroll は素晴らしいのですが、全画面でのスクロールのみになってしまいます。App Storeのサムネイルみたいに、部分的にスクロール対応したい時もあります。そういう時は以下のjQueryプラグインが便利です。

flickGal・・・iPhoneでフリックギャラリーを簡単に実装できるjQueryプラグインです 

ただ、惜しいかな縦/横スクロールのロックがありません。そこで、githubで公開されているものをフォークして、スクロール方向ロックオプションを付けたものをアップしました。

https://github.com/dsuket/flickGal

lockDirection を true にすると、 scrollMargin 分動くまでスクロールを待機し、動き出した後はその方向でロックします。