ISUCON14 振り返り
2024-12-08 に開催された ISUCON14 に参加した記録です。
次回以降に向けてやったことなど振り返っていきます。
去年の記録↓
tl;dr
特に何もできず惨敗でした
ベスト: 8533 最終: 2,705

メンバー
去年と同じ。ただしメンバー2は当日事情があり欠席となった。
役割としては、自分がインフラ周りをメインで見つつ飛び道具的な改善に取り組み、もう一人のメンバーにアプリケーション周りの改善を進めてもらう。
タイムライン
初動(~10:20)
- メンバー1
- ブラウザから動作確認できるように設定
- ER 図の作成
- @NotFounds
全員でマニュアル読み(~11:00)
- マニュアルを読み用語・概念確認
- 今年は SSE かあ。そこまで手が付けられるといいねーとかいう話をする
- Idempotent-Key とかは後でやれそうだねーみたいな感じ
- 点数計算
- 評価とか直接関係ないのか?
- → その後の利用に影響がある
- オーナーの売り上げとか直接関係ないのか
- → 椅子の追加とかに影響がある
- 評価とか直接関係ないのか?
ぱっとできる改善フェーズ(11:00~13:00)
- メンバー1
- Index 貼り
- alp/slp が改善するも MySQL の負荷は高い
- Index 貼り
- @NotFounds
- 少しでも DB 負荷を下げたい & 呼び出し回数が多かったので AccessToken から User/Chair を引く処理をインメモリキャッシュ → 効かない
- ActionLog
- MySQL の負荷が高いので3台目に移す → 3703 → なんか不安定になって revert
一進一退フェーズ(13:00~16:00)
- メンバー1
- @NotFounds
気づきフェーズ(16:00~)
ベンチ結果を見てユーザーの評価が一向に上がらない(すべて不満100%)なことに気づく → マッチングじゃね?となり改善を行う
- メンバー1
- /api/app/rides の N+1 改善
- マッチングでの椅子の空状態の判定処理改善 → 椅子
- @NotFounds
最終調整(17:00~)
- メンバー1
- マッチング修正
- ログを切る作業の準備
- @NotFounds
最終的に問題が解決せず、原因特定のためにこれまでのほとんどの修正を revert したうえで一台構成で終了
KPTA
Keep
- Ansible で同時にサーバーを操作したのはよかった
- 正直 Ansible にこだわりはなかったが NewRelic など重いのを入れるのは楽そうだとおもった
- しっかり書いておけば timeout とかも簡単にできるのはよいと思った
- ファイル操作と標準出力を取ってくるのはやりづらい...
- 試行錯誤のために2週間ほどほぼ毎日 AWS で環境作って壊すをやったので、これでも大分障害耐性がついた
- スコアには影響しなかったが、alp/slp計測→改善→alp/slpが改善されていることの確認はできていてよかった
- 事前に用意していた ActionLogger (ユーザーの行動分析補助)や InMemoryCache は一応生かせて良かった
Problem
- デプロイ・インフラ回り
- Ansible の実行が思ったより遅い(Gather Fact がつらい)
- Ansible を使ったことによりデプロイ処理が俗人化してしまった気がする(メンバーが使いやすいようにするところまでできなかった)
- デプロイスクリプトはかゆいところに手が届かず、結果として複数台構成の失敗につながった可能性がある 三台それぞれに対して Nginx/MySQL/Application の ON/OFF まで管理できなかった
- デプロイ→(ベンチ)→ログ解析はコマンド化していたので楽ではあったが、自分がブロックしてしまったのでよくない また、地味にログの投稿が面倒で時間が取られた
- alp/slp の diff を使ったが、main との差分ではなく、ひとつ前のベンチの結果との比較になったので正確な比較ができなかった(featureブランチ同士の比較になったりした)
- MySQL の slow query の threshold がうまく決められず SlowQuery の解析ができないときがあった(ログが吐かれすぎる)
- どのくらい Query が投げられているのか計測できておらず、MySQL の負荷の原因が突き止められなかった
- 複数台構成は練習不足を感じた
- 実装まわり
- Ruby で雑に非同期処理する方法を作れていない(Sidekiqの導入は負担が大きいので検討しなかった)
- Go だと goroutine 投げて先にレスポンス返したり、Node でも await せずにレスポンス返すみたいなことができる
- 練習で Thread 投げっぱなしにしてみたりしたけどうまく動かなかった
- なんか書き方がよくなかっただけな気もする
- SQL でできる処理なら TRIGGER 使うのがよい?
- 細かいミス(SQL の SyntaxError や Ruby の nil 参照)などは競技耐性不足を感じる
- 型のセットアップは行ったがエラーが多すぎて通すところまでできなかった
- TypeProf 使いたくなった
- (去年は Go だったけど)今年は Ruby の方が慣れているからという理由で選んだが、SQL と戯れる時間が長くてどっちでもいいのでは?となった
- Ruby で雑に非同期処理する方法を作れていない(Sidekiqの導入は負担が大きいので検討しなかった)
- その他
Try
- ログの投稿は自動化したい。もしくはダッシュボードにしたい
- ログはコミットグラフに紐づけられるようにしたい
- 1コマンドでデプロイ→ベンチ→計測までできるのが理想...
- システムのメトリクスを見ながらの改善も大事だがベンチの結果とマニュアルをよく読む
- 仕様に出てくるようなパラメータは極端な値を試してみて挙動を確認する
- データ不整合が出てきた場合ちゃんとロックされているか/トランザクションが張られているかを確認する
Action
- クリティカルエラーの原因は明らかにしておきたいので後日振り返りとしてバグ修正をやる
irbでRSpecのmatcher を使えるようにする
tl;dr
irb で以下の2行を実行するだけ。
require 'rspec/expectations' include RSpec::Matchers
例
$ rails c Loading development environment (Rails 7.1.5.1) irb(main):001> require 'rspec/expectations' irb(main):002> include RSpec::Matchers irb(main):003> irb(main):004> expect(1).to eq(1) => true
背景
コードレビュー中など、ふとした時に RSpec の matcher を試したくなることがある。
しかしわざわざ新しくファイルを作ってテストを実行するのは面倒。
RubyKaigi 2024 に参加しました
5月15日~17日に沖縄で開催された RubyKaigi 2024 に参加してきました。
これまで RubyKaigi はオンラインでちょくちょく参加していましたが、リアルで参加したのは`初めてでした。チケット代から交通費・宿泊費まで全て負担してくれた会社に感謝です。
0日目
会社の代休が余っていたので前日(5/14)に沖縄に移動し前泊しました。Pre-checkin が19時までだったのですが、17時半頃に空港についたため急いで会場に向かいました。
ゆいレールに乗ったときくらいからエンジニアっぽい人々をチラホラ見かけるようになり、県庁前駅で降りてからは5~10人くらいが小雨が降るなか会場を目指して歩いていたと思います。
会場はすでに多くの人で賑わっており、入口のパネルでの記念撮影をする人やスポンサーブースを設営する人、久しぶりの再開に話を弾ませている人、そしてネームタグをケースにいれるの苦戦する人(自分)など様々でした。
RubyKaigi が同窓会みたいになっているのは現地参加の醍醐味ですね。自分の知り合いも何人か来るという情報を聞いていましたが、このときは誰にも会わなかったのでそそくさとホテルへ向かいました。

ホテルでチェックインを済ませたときには19時を過ぎていました。この時間になると沖縄といえど日は落ち、雨がパラパラ降っていました。
移動で疲れていましたがせっかくなら沖縄料理を食べたいなと思い、国際通りに向かいました。バスで Suica が使えなかったときは焦りましたが、お守りとして持っていた千円札のお陰で無賃乗車せずにすみました。
向かったお店はここ 飯ト寿 小やじ maps.app.goo.gl
オリオンビールで乾杯し、刺し身やサラダ、メンチカツを食べました。刺し身は臭みがまったくなく美味しかったです。
会社の同僚に連絡したところ、一緒に飲もうということになったので牧志公設市場の居酒屋で飲みました。
実は職場から RubyKaigi に参加しているメンバーはチームが異なり、これまでほとんど面識がありませんでした。
こうやってイベントがないと、なかなか会うことも難しいのでいい機会となりました。
ここからはざっくりと参加したセッションをピックアップして振り返っていこうと思います。
1日目
Writing Weird Code
資料: https://drive.google.com/file/d/1Dkx15u_5UAGoFqJHCeAuj2FXS-z_U7EE/view
Animated Quine というわけのわからないもの(褒めてる)を初っ端から見せつけられ、RubyKaigi 始まったなぁという感想でした。
一見すると使い道がなさそうなトリック(失礼)が散りばめられているのですが、字句解析や構文解析の穴をつくためのテストコードとしてこういった技術が使われるというのは興味深かったです。
アニメーションつけるのは難しそうですが、普通の Quine は試したいなと思います。
The grand strategy of Ruby Parser
資料: https://speakerdeck.com/yui_knk/the-grand-strategy-of-ruby-parser
お昼ご飯が長引き、途中参加でしました。
パーサージェネレータである Lrama に関する話で、そもそもなぜ必要なのか?から今後の展望までが熱く語られました。
Prism は触ったことがあったのですが、Lrama に関しては初めて知りました。しかし、その必要性や Universal Parser の構想などについてざっくり理解できたと思います。
An adventure of Happy Eyeballs
資料: https://speakerdeck.com/coe401_/an-adventure-of-happy-eyeballs
Socket ライブラリに RFC8305: Happy Eyeballs v2 で提案されているアルゴリズムを実装したという話でした。
実装する上での複雑な状態との戦い方やブロッキングなどの対処として IO.select や IO.pipe を使った実装は参考になりました。
ネットワーク周りの実装するときのテストって大変そうだなって思ったのですが、デモのように簡易サーバー立ち上げるのが一般的なんですかね...?
Official Party
特に説明はないです。海を眺めながらのバーベキューは最高でした。(0日目の天気じゃなくてよかった〜)

2日目
Does Ruby Parser dream of highly expressive grammar?
資料: https://speakerdeck.com/ydah/does-ruby-parser-dream-of-highly-expressive-grammar
Lrama の文法を定義する構文に関する話でした。
Lrama では新しい構文を定義して DSL を使って実装したことによって複雑な文法を抽象化できるようになったということだと理解しました。
パラメタライズや頻出文法のライブラリ化、条件分岐などがあるとのことですが Ruby 以外のパーサーも作ろうと思えば作れるという認識であっているんですかね...?
Embedding it into Ruby code
資料: https://speakerdeck.com/soutaro/embedding-it-into-ruby-code
YARD のように書いたコメントから RBS を生成する rbs-inline gem を作っているという話でした。 YARD との共存?として説明などを書きたいときにどうなっていくのかが気になりますね。 まだ RBS 導入できていないのでこれから導入するときは使ってみたいです。
あと、Live demo のときに snippet を使っていて真似したいなと思いました。
Getting along with YAML comments with Psych
資料: https://speakerdeck.com/qnighy/getting-along-with-yaml-comments-with-psych
Psych という YAML パーサーを拡張してコメントを扱えるようにしたという話でした。(スピーカーは前職の同僚)
以前 YAML の歴史について教えてもらい、実はライブラリ存在はその時に聞いていました。
しかし、発表後半で触れていたライブラリの拡張ライブラリを作る際にどこまでを責務とするか?のような設計の線引の話はあまり聞いていなかったので面白かったです。(以前も聞いてたらごめんなさい..)
RuboCop: LSP and Prism
資料: https://speakerdeck.com/koic/rubocop-lsp-and-prism
RuboCop に LSP を統合した話でした。この話を聞くまで RuboCop が LSP の機能を持っていること知らなかったです。
静的解析→リアルタイムな解析でエラートレラントの必要性を理解できました。
Shopify の Ruby LSP も LSP と linter/formatter を提供しているので、今後どちらが使われていくのかは気になりますね。
Good first issues of TypeProf
資料: https://speakerdeck.com/mame/good-first-issues-of-typeprof
TypeProf の紹介とコントリビューションしてねという話でした。型注釈無しで無理矢理型を推論するのはパワフルですごい... 特にテストに関する考え方が印象的でした。編集過程をテストするため、そのようなテストが書きやすい DSLを作りシナリオテストを実装しているのが面白かったです。
実は TypeProf 自体は以前の発表で知っていたのですが何もできていなかったので、これを期に手を動かしたいなと思います。
夜ご飯
この日はアフターイベントに参加できなかったので一人で飲みに行きました。行ったお店は県庁近くの くもざき
気さくな店長が色々とオススメの泡盛を教えてくれました。大きくてジューシーな焼売と沖縄野菜が美味しかったです。
3日目
YJIT Makes Rails 1.7x Faster
資料: https://speakerdeck.com/k0kubun/rubykaigi-2024
YJIT のパフォーマンス改善の話でした。タイトルつけた時点では 1.7 倍の高速化が、発表のタイミングでは 1.8 倍になったとのことで笑いが起きました(すごい)。
Ruby のコードはまず Ruby VM 命令に変換され、それが JIT コードに変換されて動作するようです。(JIT でない部分はインタプリタ)
ただしすべてのコードが YJIT に変換されるわけではないようで、今回の対応でサポート範囲が増えたことによりパフォーマンスが改善したようです。
他にも VM のスタックの値をメモリーに書き込んでいたものがレジスタに置き換わるなど様々な高速化が行われているようです。
個人的に最近低レイヤに興味が出てきたのでこのあたりの発表が聴けてよかったです。また Ruby のバージョンを上げるだけでどんどん速くなってくれるので嬉しいですね。
Porting mruby/c for the SNES (Super Famicom)
資料: https://speakerdeck.com/gedorinku/c-for-the-snes-super-famicom-rubykaigi-2024
スーパーファミコンに MRuby を移植し、ゲームを動かす話でした。(スピーカーは前職の同期)
以前から話を聞いていましたが、画面更新のタイミングが決められていることやデバッグの難しさ、パフォーマンス問題など改めて発表で聞いて面白かったです。
この取り組みを聞いて自分も低レイヤに興味を持ち、CPU エミュレータを書きはじめたりしているので感謝です。
感想
今回はじめて RubyKaigi に現地参加してみて、現地参加の良さを実感しました。
登壇者や他の参加者との交流ができることはもちろん、会場の熱気や一体感みたいなものは実際に同じ空間にいないと感じることはできないなと強く思いました。
また、オンラインだと本当に気になるセッションしか見ないことが多いです。しかし、現地にいるとせっかくだし除いてみるかという感じでふらっと聴きに行くことも多く、幅を広げられる(知らないことを知れる)という点で結構よかったなと思います。
発表全体に関する所感としては、Ruby のパーサー周りや開発支援系の話が多かった印象です(自分が意識して聴きに行ったのもある)。
他にも YJIT、並列・並行処理などパフォーマンス関連のトピックも多かったです。この辺は実際にアプリケーションを運用する上で直に恩恵を受けられるので今後が楽しみなところですね。
これは僕だけかもしれないですが、JSConf や Go Conference に比べて言語本体の実装やランタイムの話が多いなという印象をもちました。RubyKaigi は 「Ruby にこんな機能を実装しました」「xxx のこの実装はこうなっています」みたいな内部の話が聴けます。これは自分にとってはなかなか新鮮な感じがして楽しかったです。
一方で反省点としては事前の予習が足りなかったなと感じています。前の話にも重なるのですが、Ruby のコア周りの話が多いため予備知識がないと置いてきぼりになってしまうなと... 直近の Ruby 周りの動向を知っているともっと RubyKaigi を楽しむことができ、吸収できることも多かっただろうなと思いました。
一通り話を聞いてみて、RubyKaigi の内容を今すぐ実務に取り入れるイメージはあまりついていないです。しかし、今後はより積極的にコミュニティ貢献するぞというモチベーションになったので参加してよかったなと思っています。
さっそく自分の周りでも RubyKaigi で紹介された gem に対してパッチを送ったり、それに便乗して(?)僕もパッチを投げたりしていて KaigiEffect を実感しています。
自分が今後注視していきたいところは以下の4つです。来年の RubyKaigi では話についていけるようにしっかりとキャッチアップしていきたいと思います。(あわよくば登壇したい...!
- Prism と Lrama の関係
- Ruby 自体
- 文法?機能のフリーズ周りの動向
- まだ使ったことない機能周り(Fiberとか)
- 開発ツール周り全般
- 型の支援ツールなども出てきているが複数あるので今後どうなっていくのか
- 気になった gem とか
そういえば沖縄に行ったのは今回が3度目なのですが、いちばん沖縄料理を堪能できました。沖縄野菜やお刺身が美味しく、オリオンビールと泡盛を飲みすぎてしまったので次の健康診断が心配です。
一つだけ心残りがあるとすれば〆のステーキを食べる元気がなかったので、次は〆ステーキ食べるために沖縄に行こうと思います!
RubyKaigi お疲れ様でした!
編集履歴:
2024-05-29: The grand strategy of Ruby Parser の資料のリンクが間違っていたため修正いたしました。ご指摘いただきありがとうございました。
2024-05-30: Getting along with YAML comments with Psych のセクションがまるっと抜けてたので追記しました。
jqとfzfで雑にJSONを探索する
tl;dr
タイトルにあるとおりだが jq と fzf で JSON を探索する Shell Script を書いた。
具体的にはある JSON ファイルを渡したら JSON Path が一覧され、fzf を使って JSON Path をインタラクティブに絞り込みながら値を preview するというもの。
百聞は一見にしかずというので gif を貼っておく。

背景
同僚が「フィールド名はわかっているけどわざわざ JSON Path を指定するのが面倒」という事を言っていたのでなんか簡単に JSON Path の抽出/絞り込みできないかなと思った。
本体
使い方
自分は .zshrc に貼ってあるので適当に shell を開いて jpath <JSON file> のようにして使う。
おわり
ちなみに同僚が本当に求めていたものは jq '.. | .<field name>? | values' で十分だった。

RubyでStringのサブクラスをprotobufに渡せるようにした話
毎年機会を見つけては OSS に Contribute したいなと思っているのですが、今年もなかなか思うようには貢献できませんでした。
今回は protocolbuffers/protobuf に投げた Pull Request について紹介します。
TL;DL
- Protocol Buffers の Ruby 実装として Google が公式に実装している protocolbuffers/protobuf がある
- Protocol Buffers の string フィールドに対して Ruby の String を渡すことができるが String のサブクラスは受つけず、ランタイムエラーが出てしまっていた
- String のサブクラスも受け取るように提案した PR が受け入れられた 🎉
protocolbuffers/protobuf とは
前提として Protocl Buffers とは Google が提案・利用しているデータシリアライゼーションフォーマットで、ILD を提供しています。
主に gRPC による通信で利用されています。protocolbuffers/protobuf は Google によって実装された公式ライブラリであり、2023年12月現在で C++ / C# / Dart / Go / Java / Kotlin / Objective-C / PHP / Python / Ruby 向けのライブラリを提供しています。
このライブラリを使うことで各言語で任意のオブジェクトを Protocol Buffers の形式にシリアライズすることができます。
今回は社内のマイクロサービス間通信をおこなっている箇所で Rails の gRPC Server を実装したところ、後述する問題に遭遇しました。
何が起きていたか
Protocol Buffers に定義されているスカラデータ型 は15種類あります。 具体的には符号付/符号なし整数型と浮動小数点型、そして文字列型、ブーリアン型、バイト型です。
今回 Ruby の protobuf ライブラリの文字列型(string*1 ) のフィールドに対して Rails の ActiveSupport::SafeBuffer を渡したところ以下のようなエラーが出てしまいました。
Google::Protobuf::TypeError: Invalid argument for string field 'foo_field' (given ActiveSupport::SafeBuffer).
どうあるべきか
ActiveSupport::SafeBuffer は XSS 対策として提供されているクラスで、HTML として出力する際に安全であるとマークされた文字列を表すためのクラスです。
実装を見てみるとわかるのですが String を継承したサブクラスとなっています。
module ActiveSupport # :nodoc: class SafeBuffer < String # ... end end
String のサブクラスが string フィールドに渡せないのはリスコフの置換原則に反しているため修正するのが適切だと考えました。
修正
Ruby の protobuf ライブラリは FFI を用いて実装されており、本体はC言語で書かれています。今回の修正対象もC言語でしたが、運よく適切な関数 `rb_obj_is_kind_of を見つけることができ無事実装できました。
困りポイントとしては fork したリポジトリ上でうまく CI を動かすことができず、本家に Pull Request を投げて実行してもらうまでは CI 通るか確認ができなかったのが大変でした。
github.com その後 Reviewer と何度かやり取りがあり、Pull Request 上は Closed になっていますが 34908e2 で無事取り込まれました 🎉
感想
今回の内容とあまり関係ないのですが、今までは Pull Request の Description やコメントでのやり取りなど英語でのコミュニケーションに課題を感じていてあまり積極的になれませんで。
しかし ChatGPT をはじめとする AI を使い、AI に一度レビューしてもらうことである程度自信をもってやり取りすることができるようになったと感じました。
これを機に2024年はもっと積極的に OSS 活動していきたいです。
ISUCON13 振り返り
2023-11-25 に開催された ISUCON13 に参加した記録です。
次回以降に向けてやったことなど振り返っていきます。
メンバー
4月頃大学の編入同期に声かけたら快諾してくれた。
- メンバー1 ISUCON 初参加。普段は Ruby on Rails を使ってバックエンド開発をしている。
- メンバー2 ISUCON 初参加。普段はセキュリティエンジニア。Web 開発はほとんど未経験。
- 僕 ISUCON は2年ぶり2回目。普段は Web エンジニア。
今回はメンバー1・2にアプリケーションをメインで見てもらい、自分はインフラいじりつつ重そうなところを見ていく方針。
普通だったらメンバーが使える言語の最大公約数を取るけどノリと勢いで Go になった。
練習
10月中盤と本番直前の祝日にそれぞれ1日ずつ使って行った。
環境はそれぞれ AWS に用意した。
1回目
ISUCON12 予選を利用。
目的としては本番の流れをざっくりと理解するためで、あとは index 貼る練習とかをちょこちょこやった。
あとはデプロイ周りの script とかを整理していた(が、本番のときとの環境祭に耐えられなかったので実際は使わなかった。カナシイ)
2回目
private-isu を利用。
6時間くらい何も見ずに触った後に ISUCON 本 を読んで振り返り。
画像の切り出しなどはここで触れてよかった。あと、1ヶ月ぶりの練習で割と色々忘れてたので直前にやって良かったねってなった。
当日
メンバー1の家に集合。寒かったのでコタツでぬくぬく。
開始までの間に空の repository 作ったり、コマンドメモを見返したりしていた。
10:00
最初の役割分担
- メンバー1 マニュアル確認。アプリケーションの動作確認。スロークエリの設定
- メンバー2 マニュアル確認。アプリケーションの動作確認。アイコン切り出し
- 僕 実装の確認。git の設定。alp 導入。デプロイスクリプト設定
設定後に走らせた初回ベンチは 3,653 で10分くらいで初期設定できた。
11:00
ログ眺めながら方針を立て、以下のタスクに取り組んだ。
着実にボトルネックは移り、DB の負荷も減ってきたが対してスコアは伸びない 4,398。
12:00
アイコンの切り出しには成功していたのでアイコンハッシュ周りに取り組み始める。
index がうまく貼れていなかった場所を修正したりして 6,211。 さらに index が貼れないか試行錯誤する。
その後少し伸びて 7,639。
このとき、ちゃんと計測せずに write の負荷が高くなるのを懸念して慎重にやりすぎて時間を使いすぎた気がするので反省。
13:00 ~ 17:00
なぜか家の Network が落ちたのでお昼休憩を取ることにする。一旦休み。
買い出しから帰ってきたら復旧していたので再開するもベンチが止まる。一旦休み。
2台目のサーバーで開発できるように設定。
明らかに悪そうな INNER JOIN を取ったりするけどそんな伸びない。
statistics 周りの N+1 の解消に取り組み始める。他にも NG ワード判定をアプリ側で行うように修正し、やっと 10,261。
17:00 ~ 18:00
ここに来て MySQL の restart に失敗するようになる。
怖くなったので手を付けていなかった3台目のサーバーのセットアップを行い、AWS から VM の再起動を行う。
ログを止めたりしつつ、ベストスコア 11,892 で終了。
個人の振り返り
K
- チームの Discord 鯖 に GPT-4 の bot を用意しておいたが良かった
雑に設定方法を聞いたり、SQL からの ER 図生成などに役立った

ChatGPT が生成した reference を sqldiagram に投げた図 - 物理で集まったのでコミュニケーションコストが低かった
直接画面見ながらペアプロできるのは良い - 練習でやったことが活かせていた
画像の切り出し周りは練習でやってなかったらあの短時間でできなかったと思う - 細かいけど
git commit --allow-empty -m "Score: xxxx"ってしておくのは良かった
壊したときに戻す場所が見えやすいし、振り返りに便利

空コミットでスコアを記録すると便利 git syncが便利 notfounds.hatenablog.com
P
- bot を雑に作りすぎてコードを全部投げられなかったので改善の余地がありそう
そもそもコードの修正などは外部のツールやエディタ上で完結させたほうが良いかも - index を慎重に貼りすぎた
SQL を bot になげて index 貼ってもらうくらいやっても良かったかも - デプロイ周りがあまり自動化できていなかった&属人化していた
- インフラ周りに弱すぎる
練習で複数台構成の練習をサボっていたので本番でやる勇気がなかった
今回スコアを稼ぐ上で重要な DNS 周り何も分からなくて手を付けられなかった
MySQL が再起動しなくなったときなどに復旧できなかった。複数台構成にするなら致命的な気がする - 瞬発力がない
言語への理解&慣れ。ISUCON 頻出問題への対応速度 - キャッシュを効率的に使えなかった
どこをキャッシュして良いのか?そこをキャッシュすると効くのかが見極められていなかった
T
- 最初の環境構築はなるべく自動化したいので Ansible などを勉強する
ログの投稿も手動だったので webhook とか使うようにしたい - 各メンバーがデプロイできるようにする
- 練習で複数台構成を試す
- 数をこなしてたくさんの施策に取り組めるようにする
- レギュレーションやアプリケーションの書き込み箇所をちゃんと把握する
TypeScript で TextDecoder と TypedArray の取り扱いではまった
TypeScript と書いているが、JavaScript にも当てはまる内容になっているはず。
tl;dr
- utf-8 のバイト列(TypedArray) → string への変換で TextDecoder を使った
- Uint8Array をそのまま
TextDecoder.decode()に投げたら生成された文字列がいまいちおかしい Uint8Array.prototype.subarary()を使うとコピーせずに部分配列を作れる
おさらい
いきなり TypedArray とか言われてもピンとこないと思うので関連するオブジェクト周りについておさらいしておく。
ArrayBuffer
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
固定長の生のバイナリデータを扱うためのオブジェクト。他の言語でいうバイト配列。
しかし直接操作することはできないため、後述する DataView や TypedArray を使う必要がある。
DataView
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/DataView
ArrayBuffer を操作するためのインターフェースオブジェクト(ビュー)。ArrayBuffer は生のバイナリデータであるため操作する際は環境毎のエンディアンを考慮する必要があるが DataView を使うとエンディアンに依存せず読み書きできる。
TypedArray
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
ArrayBuffer を配列形式で扱えるようにするためのインターフェースオブジェクト(ビュー)の総称。
あくまで総称なので TypedArray という名前のオブジェクト自体は存在しない。
TypedArray には Uint8Array や Int32Array などが存在する。繰り返しになるが、これはあくまで View であり、実体は ArrayBuffer である。
TextDecoder
https://developer.mozilla.org/ja/docs/Web/API/TextDecoder
Web API の一つで、UTF-8 や EUC-JP などのデコーダーを提供する。
TypedArray を受け取り、デコードした結果を文字列として返す。
対となる API として TextEncoder がある。
はまったこと
以下のようなことをしていた。
// 適当に buffer を確保 const buf = new Uint8Array(32); // 実際はここで Stream からデータを読み込んでいたが、ここでは TextEncoder の書き込みとしておく // e.g. await s.read(buf); new TextEncoder().encodeInto("Hello, World!", buf); // byte列 → string 変換 const decoded = new TextDecoder().decode(buf); console.log(decoded); // "Hello, World!" console.log(decoded.replace("Hello, World!", "")); // "" console.log(decoded.replace("Hello, World!", "").length); // 19 <-- !?
byte 列の操作に慣れている人からしたら当たり前かもしれないが TextDecoder は特定の byte 列からゼロ値までをいい感じに decode してくれるのではない。
与えられた byte 列をすべて decode しようとする。
decoded を表示した際に "Hello, World!" と表示されたが、実際には見えていないだけで Null 文字が28個続いているのである。
どのように対応したか
const buf = new Uint8Array(32); // byte 列操作系のメソッドは大体読み込み・書き込みした byte 数を返してくれる // e.g. const n = await s.read(buf); const { written: n } = new TextEncoder().encodeInto("Hello, World!", buf); const decoded = new TextDecoder().decode(buf.subarray(0, n)); console.log(decoded); // "Hello, World!" console.log(decoded.replace("Hello, World!", "")); // "" console.log(decoded.replace("Hello, World!", "").length); // 0 <-- 👍
TypedArray には TypedArray.prototype.subarray(begin[, end]) があり、メモリのコピーなしに同じ型の新しい TypedArray を返してくれる。
メモリのコピーが発生しないということは、物理メモリ的は元の TypedArray が参照している ArrayBuffer の実体と同じであるということなので、ここで生成された TypedArray への変更は元の TypedArray にも影響をあたえる。逆もまた然りである。
const base = new Uint8Array([4, 2]); const copied = base.subarray(0); copied[0] = 0; console.log(base.toString()); // 0,2 console.log(copied.toString()); // 0.2
配列のコピー・複製を行う場合は TypedArray.from(source[, mapFn[, thisArg]]) を使うと良い。
const base = new Uint8Array([4, 2]); const copied = Uint8Array.from(base); copied[0] = 0; console.log(base.toString()); // 4,2 console.log(copied.toString()); // 0.2
まとめ
TextDecoder に TypedArray を渡すときは subarray を用いて適切な範囲指定の TypedArray 作ってそれを渡してあげたほうが事故がなくて良いと思う。
余談
ちなみになぜこの問題にあたったのかと言うと TypeScript で TCP Server を立てて独自プロトコルの parse をしていたためである。
はじめは通信周りのバグか何かだろうと思い tcpdump を使って binary packet を眺めていたのだがてんで見当違いであった。