DEVGRU

プログラミングと競馬予想について書きます

JavaScript の正規表現リテラルの評価タイミングとパフォーマンス

正規表現リテラルと正規表現オブジェクトの評価について、誤解していたのでメモ。

広告

以下の2つのJavaScriptコードを実行した際のパフォーマンスを考える。

regexp-literal.js

for (let i = 0; i < 1000000; i++) {
        /^(3|5|9)/.test(i);
}

regexp-object.js

for (let i = 0; i < 1000000; i++) {
        new RegExp("^(3|5|9)").test(i);
}

手元のV8で実行するとこうなる。正規表現リテラルのほうが正規表現オブジェクトより2倍速い。

time d8 regexp-literal.js
d8 regexp-literal.js  0.10s user 0.03s system 98% cpu 0.135 total
time d8 regexp-object.js
d8 regexp-object.js  0.20s user 0.03s system 97% cpu 0.236 total

しかし、この差がどこから来るのかという点を完全に誤解していた。

最初は正規表現リテラルはコンパイル時に一度作られたっきりで以降は再利用されると思っていたが、どうやら違うようだ。

つまり、このようなコードに近いと思っていた。

regexp-literal-optimized.js

const r = /^(3|5|9)/.test(i);
for (let i = 0; i < 1000000; i++) {
        r.test(i);
}

しかし、それが違うということはバイトコードを出力するとわかる。

$ d8 --print-bytecode regexp-literal.js
[generated bytecode for function:  (0x3b600821248d <SharedFunctionInfo>)]
Parameter count 1
Register count 5
Frame size 40
         0x3b6008212512 @    0 : 0b                LdaZero
         0x3b6008212513 @    1 : 26 f9             Star r1
         0x3b6008212515 @    3 : 0d                LdaUndefined
         0x3b6008212516 @    4 : 26 fa             Star r0
         0x3b6008212518 @    6 : 01 0c 40 42 0f 00 LdaSmi.ExtraWide [1000000]
         0x3b600821251e @   12 : 6a f9 00          TestLessThan r1, [0]
         0x3b6008212521 @   15 : 9b 29             JumpIfFalse [41] (0x3b600821254a @ 56)
         0x3b6008212523 @   17 : 7a 00 01 00       CreateRegExpLiteral [0], [1], #0
         0x3b6008212527 @   21 : 26 f7             Star r3
         0x3b6008212529 @   23 : 28 f7 01 02       LdaNamedProperty r3, [1], [2]
         0x3b600821252d @   27 : 26 f8             Star r2
         0x3b600821252f @   29 : 12 02             LdaConstant [2]
         0x3b6008212531 @   31 : 26 f6             Star r4
         0x3b6008212533 @   33 : 25 f9             Ldar r1
         0x3b6008212535 @   35 : 35 f6 04          Add r4, [4]
         0x3b6008212538 @   38 : 26 f6             Star r4
         0x3b600821253a @   40 : 5a f8 f7 f6 05    CallProperty1 r2, r3, r4, [5]
         0x3b600821253f @   45 : 26 fa             Star r0
         0x3b6008212541 @   47 : 25 f9             Ldar r1
         0x3b6008212543 @   49 : 4d 07             Inc [7]
         0x3b6008212545 @   51 : 26 f9             Star r1
         0x3b6008212547 @   53 : 8b 2f 00          JumpLoop [47], [0] (0x3b6008212518 @ 6)
         0x3b600821254a @   56 : 25 fa             Ldar r0
         0x3b600821254c @   58 : ab                Return
Constant pool (size = 3)
Handler Table (size = 0)
Source Position Table (size = 0)

CreateRegExpLiteralTestLessThanJumpIfFalse の内側にあることから、ループ内で正規表現リテラルの評価が行われている事がわかる。

評価タイミングが予想していたのと全く異なるのだ 1

ちなみに正規表現オブジェクトの場合はこのようになる。

$ d8 --print-bytecode regexp-object.js
[generated bytecode for function:  (0x16e00821248d <SharedFunctionInfo>)]
Parameter count 1
Register count 5
Frame size 40
         0x16e008212516 @    0 : 0b                LdaZero
         0x16e008212517 @    1 : 26 f9             Star r1
         0x16e008212519 @    3 : 0d                LdaUndefined
         0x16e00821251a @    4 : 26 fa             Star r0
         0x16e00821251c @    6 : 01 0c 40 42 0f 00 LdaSmi.ExtraWide [1000000]
         0x16e008212522 @   12 : 6a f9 00          TestLessThan r1, [0]
         0x16e008212525 @   15 : 9b 35             JumpIfFalse [53] (0x16e00821255a @ 68)
         0x16e008212527 @   17 : 13 00 01          LdaGlobal [0], [1]
         0x16e00821252a @   20 : 26 f7             Star r3
         0x16e00821252c @   22 : 12 01             LdaConstant [1]
         0x16e00821252e @   24 : 26 f6             Star r4
         0x16e008212530 @   26 : 25 f7             Ldar r3
         0x16e008212532 @   28 : 66 f7 f6 01 03    Construct r3, r4-r4, [3]
         0x16e008212537 @   33 : 26 f7             Star r3
         0x16e008212539 @   35 : 28 f7 02 05       LdaNamedProperty r3, [2], [5]
         0x16e00821253d @   39 : 26 f8             Star r2
         0x16e00821253f @   41 : 12 03             LdaConstant [3]
         0x16e008212541 @   43 : 26 f6             Star r4
         0x16e008212543 @   45 : 25 f9             Ldar r1
         0x16e008212545 @   47 : 35 f6 07          Add r4, [7]
         0x16e008212548 @   50 : 26 f6             Star r4
         0x16e00821254a @   52 : 5a f8 f7 f6 08    CallProperty1 r2, r3, r4, [8]
         0x16e00821254f @   57 : 26 fa             Star r0
         0x16e008212551 @   59 : 25 f9             Ldar r1
         0x16e008212553 @   61 : 4d 0a             Inc [10]
         0x16e008212555 @   63 : 26 f9             Star r1
         0x16e008212557 @   65 : 8b 3b 00          JumpLoop [59], [0] (0x16e00821251c @ 6)
         0x16e00821255a @   68 : 25 fa             Ldar r0
         0x16e00821255c @   70 : ab                Return
Constant pool (size = 4)
Handler Table (size = 0)
Source Position Table (size = 0)

こちらはRegExpがないが、Construct のところで new RegExp() の評価をしていることがわかる。


さて、じゃあどこから実行時間の差が来るのか、というところをバイトコードを踏まえて考察すると、正規表現オブジェクトのコンストラクタ呼び出しにかかるコストの差ではないかと予想される。バイトコードで直に呼び出すのとの少しだけ差が実行時間に現れた、という理解をしている。

さて、ループ外で正規表現リテラルを作ってそれを参照する場合にはどの程度速くなるのだろうか。

time d8 regexp-literal-optimized.js                                                                                                                                                          
d8 regexp-literal-optimized.js  0.10s user 0.03s system 93% cpu 0.135 total

0.10s → 0.10s なので、ほとんど変わらない。

この謎はV8のソースコードの compilation-cache.h30) で解けた。正規表現をキャッシュしているのである。

つまるところ、ループ内外や正規表現リテラルかオブジェクトの違いに関係なく、一番重い処理である正規表現のコンパイルは1度しか行われず、そこに至る道の微妙な違いがパフォーマンスの差として出てくるだけだった。

よくよく考えると、コンパイル時評価はパフォーマンスとメモリフットプリントの観点で問題がある。使わない正規表現がたくさんある場合にコンパイル時評価をするとその分パフォーマンス低下が起きるし、メモリも無駄に消費する。それを考えると、V8 のように on-demand で評価するのが正解なのだ。


  1. この誤解の原因は明白で、コンパイル時に正規表現リテラルを評価する実装を作ったことがあるからだ

Re: 愛すべきAngularとのお別れ。2,3年後を見据えReactにリプレイスする話

note.com

上記の記事について、現職では主に Angular を使っている立場(※ 社内ではReactのプロダクトも複数あります)でこの記事についての感想を述べます。

広告

理由はAngularを書ける(or書きたい)エンジニアを採用することが難しいからです。それにつきます。

はい、特に異論はありません。

実際、現職でもAngular のプロダクトのフロントエンドエンジニアの採用には苦戦しており、採用が難しい点について概ね事実かと思います。

その差が出たのは、元記事で指摘されている通り、 "React & Vue.js" vs "Angular" の勢力の別れ方が大きいかと思います。

Vue.js と Angular を触った経験としては、パラダイムというか宗派が異なる部分は確かにあるという感触です。

一方でこの記事の読者に気をつけていただきたいのは、 Angular と React の技術的な優劣を評価してのことではないことです 1

Angular は良いフレームワークです。

RxJS という強力な非同期実行フレームワークを基礎としており、更に NgRx という Redux 的なフレームワークもあります。 Breaking Change も気を使っていて、削除する機能は必ず2バージョン前から告知されます。 アップグレードする際には非互換なAPIを自動的にマイグレーションしてくれます。 Google が主体となって開発していて、いきなりメンテナンスが無くなる心配もありません。 ビルド周りはwebpackをいい感じにラップしているので、面倒なことは少ないです。 ユーザ会も活発に活動していて、月に1回はYoutubeでの配信がありますし、相談も非常に親身になって乗ってくれます。 なので、エンタープライズでは非常にありがたいです。

しかし求人が少ないのです。

この違いがどこから来るのかは定かではないですが、以下個人の感想です。

まず、Reactのほうが入り口は低いように感じます。 また、Angular はできることが多い分、設計の難易度がやや高い気もします。RxJS や NgRx を用いた設計に迷うことは少しあります。

そして、日本語の情報は圧倒的に React のほうが多いです。そのため、初学者にとってReactのほうがとっつきやすいです。

その結果としての求人の数の差なのかなと思います 2

ですので、採用についての圧倒的なアドバンテージを考えると、元記事は至極まっとうな判断だと思います。

以上、感想でした。


  1. もちろん優劣はあるでしょうが。

  2. もしかしたらもっと別な技術的な差があるかもしれませんが、Reactの学習についてまだ未着手のため評価しきれず。

ウェブサービスエラーハンドリング指針

これはなにか

現職の社内でこのテーマで書いたが、そちらは社内プロダクトの情報も混じっているので、同じテーマでブログ向けにいちから書き直してみる。 ちょっと長いが、ウェブサービスを運用するならきちんと抑えておきたい。

課題

SPA + HTTP API(概ねREST APIと言っていい)の構成において、エラーハンドリングを正しく行うのは意外と面倒だ。 しかし、特に運用中のプロダクトにおいては、障害・バグの早期検出、ユーザエクスペリエンス向上の観点から、これをきちんとするのが望ましい。

広告

前提

HTTP通信は切断されるし、インフラは落ちる

ローカルで開発をしていると忘れがちだが、HTTP通信は様々な要因で切断される。

クライアント側が公衆回線やWiFiを使っていれば、必ずそれは起きる。車両による移動、電子レンジの干渉、カフェのWiFiが混んできた、 etc.

また、長時間運用しているとどうしてもインフラの障害に襲われる。そして大抵の場合、プロバイダーの障害通知よりもサービスダウンのほうが早い。

データベースも冗長構成にしていないとクラッシュしてフェイルオーバーが終わるまではアクセスできない。

プログラムはクラッシュする

プログラムがクラッシュする可能性を完全には排除できない。

ユーザコードのバグ、ライブラリやフレームワークのバグ、言語処理系のバグ、想定外の入力、過負荷、何でもありえる。

HTTPレスポンスステータスは雄弁

HTTPレスポンスステータスは少ない情報量ながら非常に多くのことを語ってくれる。

ヘッダやボディに情報を詰め込まなくとも、それだけで多くの判断が可能になる。

開発者の時間は有限だ。横着して200系とそれ以外というような大雑把なくくりにせず、ステータスコードから更に処理を分岐して、重要度・緊急度に応じて自動的に対応するようにしよう。

広告

対応指針

主要なHTTPレスポンスステータスの意味

バックエンド開発者もフロントエンド開発者も、HTTPレスポンスステータスを理解する必要がある。

MDNの資料がわかりやすい。

以下は抑えておく必要がある。

レスポンスステータスの種類

  • 1xx - 情報レスポンス。この文脈では気にしなくて良い
  • 2xx - 成功レスポンス。この文脈では気にしなくて良い
  • 3xx - リダイレクションメッセージ。この文脈では気にしなくて良い
  • 4xx - クライアントエラーレスポンス。クライアント側起因のエラー。
  • 5xx - サーバエラーレスポンス。サーバ側起因のエラー。

主要なエラーレスポンスの意味

ステータスコード ステータスメッセージ 意味 よくある原因
400 Bad Request リクエストを理解できない 実装不備、攻撃
401 Unauthorized 未認証 クレデンシャル期限切れ、攻撃
403 Forbidden アクセス権がない ユーザのURL手打ち、攻撃
404 Not Found リソースが存在しない ユーザのURL手打ち、攻撃
500 Internal Server Error サーバがリクエストを処理できない バックエンド実装不備
502 Bad Gateway ゲートウェイなどサーバとクライアントの中間に位置するサービスがサーバから無効なレスポンスを受け取った バックエンドのウェブアプリケーションフレームワークの不具合
503 Service Unavailable サーバがリクエストを処理する準備ができていない 過負荷
504 Gateway Timeout ゲートウェイなどサーバとクライアントの中間に位置するサービスが時間内にレスポンスを得られない 過負荷

バックエンド

バックエンドの実装は、起きうるエラーを明示的にハンドルして、500を返したり、コネクション切断をしてしまわないように実装する必要がある。

RailsやDjangoなどウェブアプリケーションフレームワークの使用を前提とすると、実装起因で500が帰ってしまうのは送出された例外がハンドルされない場合である。この場合、フレームワークが例外を補足してクライアントに500を返す。

そのため、実装がどのような例外を送出しうるかを開発者は熟知して、適切なハンドリングをしなければいけない。一般に4xx系か503を返す。

以下のように例外をすべて補足して一律の対応をしてはいけない。これならフレームワークにハンドリングさせたほうがまだ良い。

try:
    # 例外を送出するかもしれないコード
except:
    # 一律で同じエラー処理をする

一般的なエラーとHTTPレスポンスステータスの対応

  • 400 Bad Request
    • リクエストが合意した仕様に沿っていない
    • バリデーションエラー
  • 401 Unauthorized
    • リクエストに認証トークンが含まれていない
  • 403 Forbidden
    • 認証はしているが、アクセスしてはいけないリソースへのリクエスト
  • 404 Not Found
    • 存在しないリソースへのリクエスト
    • アクセスしてはいけないリソースへのリクエストだが、とりわけクライアントへ存在を隠したい場合
  • 503 Service Unavailable
    • 復旧の見込みのあるデータベースエラー

ロギングと通知

バックエンドの出力はHTTPレスポンスの他に、ログと通知がある。

開発者がエラーを理解・対応するために必要な情報を入れること。また、バックエンドとは別のリソースに保存すること。

以下は抑えておきたい。

  • 日時
  • HTTPリクエストの情報
  • スタックトレース
  • エラーの情報

LTSVよりも構造化ログを使うと情報を多く含めることができ、機械的な処理がしやすい。

HTTPリクエストやデータベースのエラーの情報には個人情報やパスワードなど機微なものが含まれることについては気をつけなければいけない。

この場合は、よりセキュアなリソースに機微な情報を隔離してそこへのポインタのみをログに含めると良い。

具体的には、S3にHTTPリクエストやデータベースへのクエリを保存して、保存先のキーをログに出力する、などである。

また、エラーのうち対応が必要なものについては開発者、運営者やユーザへの通知を行う必要がある。

サービスへの影響を考慮して適切なレベルとチャネルで通知を自動的に行うようにしておこう。

検知できないエラー

特に 502, 504 など、一部のエラーはバックエンドで検出できない。

そのため、フロントエンド側でも対応が必要になる。

フロントエンド

フロントエンドのエラー

フロントエンドは大別してHTTP APIのエラーと、それ以外の実行時エラーについて対応する必要がある。

HTTP APIのエラーは前の節で示している。

実装が完全でなく、補足されない例外が発生した場合は、ブラウザは以下のイベントのいずれかを発火させる。

この2つのイベントはエラーが発生したコンテキストから離れておりまた一律に発火されるため、各々のエラーに対する適切なハンドリングが難しいため、このイベントが起きるような実装には原則してはならない。

また、HTTP APIへのリクエストが、HTTPの層より下(TCP/IP など)で失敗した場合や、CORS エラーの場合、HTTPレスポンスステータスが0としてブラウザに返ってくる。

フィードバック

フロントエンドがエラーを捕捉した場合には、ユーザと開発者(または運営者)へそれぞれフィードバックする必要がある。

一例

  • ユーザへのフィードバック
    • ダイアログやトースト、エラーページへの遷移によるユーザの誘導
    • 自動的な障害ページの更新(非常に高頻度・深刻な場合)
  • 開発者へのフィードバック
    • ログ
    • 通知

リトライ

一部のHTTPレスポンスステータスに関しては、リトライで成功することがある。

ユーザエクスペリエンスのためにも、そのほうが良い。

ただし、利用者の多いサービスにおいてフロントエンドから一斉にリトライが来た場合にバックエンドが過負荷に陥ることがあるため、リトライの実装は注意深くしなければいけない。

必ず Exponential Backoff と Jitter を組み合わせること。

エラーとフィードバックの対応表

ステータス リトライの有無 ユーザへのフィードバック 開発者へのフィードバックレベル 備考
400 Bad Request なし ダイアログ表示、エラーページへ遷移など フロントエンド・バックエンドの食い違いがあり得る
401 Unauthorized なし ログインページへ遷移など クレデンシャルの期限切れなど
403 Forbidden なし ダイアログ表示、エラーページへ遷移など ユーザがURLを手打ちしたなど。高頻度なら通知
404 Not Found なし ダイアログ表示、エラーページへ遷移など ユーザがURLを手打ちしたなど。高頻度なら通知
その他4xx なし ダイアログ表示、エラーページへ遷移など API実装にもよるが、普通ならあまり使わない。実装を確認すること。
500 Internal Server Error なし ダイアログ表示、エラーページへ遷移など 緊急 バックエンド異常のため、早めに状況を確認したほうが良い。
502 Bad Gateway あり ダイアログ表示、エラーページへ遷移など サーバ過負荷など。注視する。
503 Service Unavailable あり ダイアログ表示、エラーページへ遷移など サーバ過負荷など。注視する。
504 Gateway Timeout あり ダイアログ表示、エラーページへ遷移など サーバ過負荷など。注視する。
その他5xx なし ダイアログ表示、エラーページへ遷移など 普通は使わない。実装やフレームワークの仕様を確認すること。
onerror 不可 ダイアログ表示、エラーページへ遷移など 緊急 ユーザに何らかの不利益があり得る。早めに原因を特定し、対処する。
unhandledrejection 不可 ダイアログ表示、エラーページへ遷移など 緊急 ユーザに何らかの不利益があり得る。早めに原因を特定し、対処する。
0 ダイアログ表示、エラーページへの遷移など なし まともに検証していればCORSエラーの可能性は低いので、通信エラーの可能性が高い。

広告

重要なサービス

ユーザへのフィードバック

Atlassian Statuspage

サービスステータスをユーザに報告するSaaS。

いわゆるステータスページの他、問題があったときにサービス上にアイコンを表示する、インシデントのメール・SNSによる報告など、基本的な機能が揃っている。

開発者へのフィードバック

Sentry

バックエンド・フロントエンドでのエラーを収集する。

主要な言語処理系、フレームワークをカバーしており、簡単な設定で補足されない例外を自動的に集めてくれる。また、カスタムな通知も可能。

オンプレミス版は無料。

Datadog

モニタリングプラットフォーム。

エラー収集、ログなど多数の機能が揃っている。

こちらも主要な言語処理系、フレームワークをカバーしており、簡単な設定で補足されない例外を自動的に集めてくれる。また、カスタムな通知も可能。

さらに、時間あたりのエラー回数など設定した条件に一致したら別のサービスに通知することができる。

New Relic

パフォーマンス分析プラットフォーム。

Datadogと一部競合するところがある。

PageDuty

インシデント対応プラットフォーム。

こちらも様々な機能があるが、Datadog から通知を受けて当番の開発者のスマートフォンに電話を掛け、反応がなかったら別の開発者にエスカレーションする、といったことができる。