node.jsのTanchiki-最適化

初めてプレイしようとしたみんなに感謝します。 ゲームの恐ろしいブレーキに非常に多くの人々を失望させたことは残念です。 しかし、あなたのためではないにしても、私は彼らの原因を推測できませんでした。 サーバーは順番に最適化されていますが、同時ゲームの数は5つに増えました。 これは重要ではありませんが、ポイントはサーバーのパフォーマンスではありませんが、最悪の夜にはインターネットの速度がそれ以上の速度を許さないという事実です。 熱意のために、ゲームを開始する前にレベルを選択することが可能です。 また、 「不快な」コメントに応じて、2対2でプレイできるようになりました。つまり、 デモ代替サーバー別のサーバーです。 今、私が急いでいないこと、そしてサーバーが失敗しないことを願っています。 カットの下で、最初のバージョンで行ったナンセンスを説明します。



プロファイリング



そのため、何かの最適化を開始するには、ボトルネックを見つける必要があります。 node.jsの場合、次のことを行いました。--profオプションでゲームを開始します



node --prof src/server
      
      





スクリプトの最後に、v8.logファイルが現在のフォルダーに表示されます。 消化しやすいものに変えるために、v8ソースのlinux-tick-processorユーティリティを使用しました。 linux-tick-processorの詳細には触れませんでしたが、自分で取得して動作させるために、いくつかのコマンドを実行する必要がありました。



 svn co http://v8.googlecode.com/svn/branches/bleeding_edge/ v8 cd v8 svn co http://gyp.googlecode.com/svn/trunk build/gyp make native
      
      





ネイティブにすると、linux-tick-processorで使用されるバイナリを含むoutフォルダーが現在のフォルダーに表示されます。 v8.logをv8フォルダーで処理するには、次を実行します。



 tools/linux-tick-processor /full/path/to/v8.log > /full/path/to/v8.out
      
      





結果のv8.outで、結果を確認します。 ここから取られたプロファイリング情報。 頑張りすぎて、誰かがもっと良い方法を知っているなら、喜んで知るでしょう。



負荷をシミュレートする



二度目に、私は自分の運を試して、何百人ものHabraの人々が戦車でプレイするという希望を台無しにしたくありませんでした。 そして、サーバーの負荷を自分でシミュレートすることにしました。 SeleniumまたはSelenium Serverは、これに適しています。 誰かが慣れていない場合、Selenium Serverはほとんどすべてのブラウザーを起動して、リンクをクリックする、ボタンを押す、ページ上の特定のテキストの有無を確認するなど、さまざまなコマンドを実行できるJavaアプリケーションです。 多くのプログラミング言語のクライアントも実装されています。

目的のために、ページを開いてログインし、ゲームを開始して30秒待機する小さなphpスクリプト作成しました。 いくつかのコンソールで実行時にこのスクリプトを実行することにより:



 phpunit --repeat=10 test.php
      
      





負荷をシミュレートする良い方法が得られます。



最適化1-閉鎖



プロファイラーでノードを起動し、ログを処理して、v8.outを確認します。 v8.outでは、関数は実行時間の降順でソートされます。 ログの最初はArray.forEach関数です。



 ticks total nonlib name 1514 2.3% 14.9% LazyCompile: *forEach native array.js:1019
      
      





それから私は何が起こっているのかすぐに理解しませんでした。 私はこれがいくつかの場所で構造を使用するためだと決めました:



 someArray.forEach(function(item){ ... }, this);
      
      





このコードを使用して関数を呼び出すたびに、クロージャー関数が作成されます。 しかし、ほとんどの場所で私は閉鎖の可能性を使用していないので、そのようなコードを次のように簡単に書き直しました。



 Class.prototype.handler = function(item) { ... } Class.prototype.func = function() { someArray.forEach(this.handler, this); }
      
      





このようなコードはより速く実行されるはずですが、後で判明したように、それはポイントではありませんでした。



最適化2-交差点検索



プロファイラーを起動し、セレンテストを実行してさらに調査します。



  ticks total nonlib name 31043 3.8% 16.5% LazyCompile: MapArrayed.intersects src/utils/map_arrayed.js:45 26763 3.3% 14.3% Stub: CEntryStub 22800 2.8% 12.1% LazyCompile: *forEach native array.js:1019 16323 2.0% 8.7% LazyCompile: IN native runtime.js:353 13800 1.7% 7.4% Stub: KeyedLoadElementStub 6911 0.9% 3.7% LazyCompile: *MapArrayed.forEach src/utils/map_arrayed.js:59
      
      





まあ、当然のことです。 交差点の検索には時間がかかりすぎるため、マップを書き直す必要があります。 前回の記事では、座標インデックスを持つ3次元配列、長方形のリスト、ネストされた長方形のツリーという3つの方法を説明しました。 ツリーで遊んだ後、マップ上に1000個のオブジェクトがある場合、大きなパフォーマンスの向上は機能しないという結論に達しました。 原則として、座標インデックスを持つ3次元配列のアイデアは好きではありません。 私は次の方法に決めました。それは長方形で定義されたオブジェクトにも基づいていましたが、毎回すべてのマップオブジェクトを実行しないように、オブジェクトは小さなセルにグループ化されます。 つまり、同じ3次元配列を取得しますが、意味は異なります。 最初の2つの次元はセルインデックスであり、実際は同じ座標であり、大まかな近似値のみを使用しています。 3番目の次元は、このセルと交差する長方形オブジェクトのリストです。 Oblitusの提案との違いはこの方法ではオブジェクトの最小移動ステップに制限が課せられず、そのような配列の粒度は自由に変更できるという点です。 これはまだ最善の選択肢ではない可能性があります。



最適化3-forEach



繰り返しますが、プロファイリングの結果を見てください。 ここで奇妙な画像が表示されます。1ゲームしかプレイしなかった場合、次のようになります。



  ticks total nonlib name 51 0.1% 11.6% LazyCompile: *Loggable.log battlecity/src/server/loggable.js:32 12 0.0% 2.7% LazyCompile: MapTiled.intersects battlecity/src/utils/map_tiled.js:106 8 0.0% 1.8% Stub: CEntryStub 6 0.0% 1.4% LazyCompile: *forEach native array.js:1019 4 0.0% 0.9% Stub: StringAddStub 4 0.0% 0.9% Stub: ArgumentsAccessStub_NewNonStrictFast
      
      





セレンを開始し、サーバーが数十のゲームを失った場合:



  ticks total nonlib name 4108 2.0% 16.1% LazyCompile: *forEach native array.js:1019 3626 1.8% 14.3% Stub: CEntryStub 2176 1.1% 8.6% LazyCompile: *MapTiled.forEach battlecity/src/utils/map_tiled.js:139 2048 1.0% 8.0% LazyCompile: IN native runtime.js:353 1755 0.9% 6.9% Stub: KeyedLoadElementStub {1} 1475 0.7% 5.8% LazyCompile: *Loggable.log battlecity/src/server/loggable.js:32 337 0.2% 1.3% LazyCompile: MapTiled.intersects battlecity/src/utils/map_tiled.js:106 336 0.2% 1.3% Stub: ToBooleanStub_Bool
      
      





奇妙なことは、時間の経過とともに、forEach()はますます多くの時間を消費することです。 マップ上の各オブジェクトには固有のIDがあり、このIDはグローバルです。つまり、時間の経過とともに増加するだけです。 しかし、マップ上のオブジェクトの数は、パリアからパリアに変化しません。 これはあまり良いトリックではありません。各競技場のオブジェクトIDをローカルにして、IDが無期限に増加しないようにできますが、javascriptはC ++のように配列を保存しません。 javascriptでは、ハッシュテーブルのようなものでなければならないのに、なぜforEachはどんどん大きくなっていますか? この場所で、私は長い間、頭を壁にぶつけて、そのような実験を行うことにしました。



 a=[1]; a[1000000]=1; console.time('qwe'); a.forEach(function(i){console.log(i);}) console.timeEnd('qwe'); console.time('asd'); for (var i in a) console.log(a[i]); console.timeEnd('asd');
      
      





結果は残念な結果です。



Ff:

 qwe: 163ms asd: 2ms
      
      





Chrome:

 qwe: 254ms asd: 1ms
      
      





さて、Operaの全体像:

 qwe: 0ms (188µsec) asd: 0ms (87µsec)
      
      





Operaで見られるように、forEach()とfor(var i in ...)の実行時間は根本的に違いはありませんが、ChromeとFirefoxは本当に私を怒らせます。 何もする必要はありません。forEach()をfor(var i in ...)に書き換えます。 そして、ああ、奇跡! メモリリークに起因するブレーキはなくなりました!



いくつかのコンソールで「phpunit --repeat = 100 test.php」を実行してノードを数時間放置し、以下を確認します。



  ticks total nonlib name 746 0.2% 16.1% LazyCompile: *Loggable.log battlecity/src/server/loggable.js:28 128 0.0% 2.8% LazyCompile: *Game._stepItem battlecity/src/core/game.js:77 101 0.0% 2.2% LazyCompile: MapTiled.intersects battlecity/src/utils/map_tiled.js:102 61 0.0% 1.3% Stub: CEntryStub 52 0.0% 1.1% Function: EventEmitter.emit events.js:38 50 0.0% 1.1% Stub: SubStringStub 46 0.0% 1.0% LazyCompile: *MapTiled.add battlecity/src/utils/map_tiled.js:24 45 0.0% 1.0% LazyCompile: FILTER_KEY native runtime.js:398
      
      





最後に、プロファイリングの結果には、私が想定したことがあり、forEach()がどこから来たのかは明確ではありません。



最適化4-トラフィック



次に、プロファイラーから少し離れることに決めました。 実際、以前の最適化の検索では、クライアントとサーバー間のトラフィックを計算しました。 ゲームの最中に判明したのは、クライアントに最大30kb / sで送信できることです。 明らかに、戦車のようなゲームの場合、これはとんでもない数字です。 しかし、これにはいくつかの理由があります。 まず、1つのプロパティのみを変更すると、オブジェクトはクライアント全体に送信されます。 第二に、オブジェクトはJSONで送信されるため、送信されるデータのサイズも大幅に増加します。 最初は、オブジェクトは次のように送信されました。

 Bullet.prototype.serialize = function() { return { type: 'Bullet', id: this.id, x: this.x, y: this.y, z: this.z, speedX: this.speedX, speedY: this.speedY, finalX: this.finalX, finalY: this.finalY }; };
      
      





文字列{"type": "Bullet"、 "id":777、 "x":123、 "y":456、 "z":1、 "speedX":2、 "speedY":0、 「FinalX」:123、「finalY」:456}約100バイトの長さ。 少し考えてから、オブジェクトではなく配列になるように、オブジェクトのシリアル化をやり直しました。

 Bullet.prototype.serialize = function() { return [ battleCityTypesSerialize['Bullet'], // 0 this.id, // 1 this.x, // 2 this.y, // 3 this.speedX, // 4 this.speedY, // 5 this.finalX, // 6 todo remove this.finalY // 7 todo remove ]; };
      
      





その結果、約25バイト[0,777,123,456,2,0,123,456]が得られます。 トラフィックは、ゲームの高さで約7〜8 kb / sに低下しました。 変更されたプロパティのみを渡し、制御コマンドのみを渡すことで、数回減らすことができますが、この変更は将来のために残しました。



最適化5-クライアントとの同期



前の記事の同期アルゴリズムは失敗しました。 この選択の唯一の理由は、関心のあるすべての顧客に即座に変更を送信するのではなく、新しく接続したクライアントが現在のデータとまったく同じ方法で過去のすべての変更を受信できるためです。 また、このメソッドの実装中に、オブジェクトをコレクションにグループ化し、オブジェクト自体ではなくオブジェクトのコレクションに更新を適用するというアイデアを思いつきました。 このようなコレクションは、「すべてのユーザー」、「一般チャットのメッセージ」、「ゲームのリスト」、「現在のゲームのユーザー」、「現在のゲームのメッセージ」、「現在のゲームのマップ上のオブジェクト」です。

同期を行う新しい方法は、ユーザーオブジェクトへの変更を即座に配信し、そこで蓄積することです。 また、Userオブジェクトは、50msごとにデータをブラウザーに送信します。 初期データをいつどのように同期するのかという疑問は残りますか? 2つのメソッドをユーザーオブジェクトに追加することにしました:watchCollection()およびunwathCollection()オブジェクトのグループに接続するとき、ユーザーはすべてのオブジェクトを新しく作成されたようにクライアントに送信します。



 /** * @param collection * @param syncKey ,          */ ServerUser.prototype.watchCollection = function(collection, syncKey) { this.unwatchCollection(syncKey); //      this.updateCollector[syncKey] = []; var user = this; var cb = function(item, type) { user.onCollectionUpdate(syncKey, item, type); }; //    collection.on('update', cb); //  callback,     ,     this.collections[syncKey] = {'callback': cb, 'collection': collection}; //     ,    collection.traversal(function(item){ this.onCollectionUpdate(syncKey, item, 'add'); }, this); }; ServerUser.prototype.unwatchCollection = function(syncKey) { if (this.collections[syncKey]) { //   this.collections[syncKey].collection.removeListener('update', this.collections[syncKey].callback); //  ,        this.clientMessage('clearCollection', syncKey); delete this.collections[syncKey]; delete this.updateCollector[syncKey]; } };
      
      





したがって、サーバー上のユーザーの承認後すぐに、Userオブジェクトはオブジェクト(コレクション)の3つのグループに接続されます。

 user.watchCollection(registry.users, 'users'); user.watchCollection(registry.premades, 'premades'); user.watchCollection(registry.messages, 'messages');
      
      





そして、ゲームに入るときとゲームを出るときに、ユーザーはそれに応じて関心のある他のコレクションに接続したり切断したりします。

 Premade.prototype.join = function(user, clanId) { // ... user.watchCollection(this.users, 'premade.users'); user.watchCollection(this.messages, 'premade.messages'); // ... }; Premade.prototype.unjoin = function(user) { // ... user.unwatchCollection('premade.users'); user.unwatchCollection('premade.messages'); user.unwatchCollection('f'); user.unwatchCollection('game.botStack'); // ... }; Premade.prototype.startGame = function() { // ... this.users.traversal(function(user){ // ... user.watchCollection(this.game.field, 'f'); user.watchCollection(user.clan.enemiesClan.botStack, 'game.botStack'); // ... }, this); // ... }
      
      





単純で正しい考えがすぐに思い浮かばないのは残念です。

その結果、次のことができます。

  ticks total nonlib name 2074 0.5% 9.9% LazyCompile: *Game._stepItem battlecity/src/core/game.js:29 751 0.2% 3.6% LazyCompile: MapTiled.intersects battlecity/src/utils/map_tiled.js:102 489 0.1% 2.3% LazyCompile: MapTiled.traversal battlecity/src/utils/map_tiled.js:132 376 0.1% 1.8% LazyCompile: FILTER_KEY native runtime.js:398
      
      





Game._stepItemが上位に表示され、以前の2.8%ではなく9.9%の時間が実行され始めたという事実から判断すると、変更が成功したと見なされます。 この時点で、サーバーには10の同時ゲームが約50%読み込まれています。 最悪の夜の時間にインターネットの速度が200kByte / s以下に低下するという事実のために、私はデモに20のゲームを同時にあえて入れませんでした。



最適化6-ゲームオブジェクトのバイパス



当初、これについては考えていませんでした。ループ内のフィールドの各オブジェクトに対して、_stepItem()関数を呼び出しました。

 Game.prototype._stepItem = function(item) { // tanks and Base processing within Clan.step if (item.step && !(item instanceof Tank) && !(item instanceof Base)) { // todo item.step(); } }; Game.prototype.step = function() { this.field.traversal(this._stepItem, this); this.premade.clans[0].step(); this.premade.clans[1].step(); };
      
      







この関数は壁のすべての部分に対して呼び出され、各オブジェクトのプロトタイプもチェックします。 この不名誉を取り除くために、私はstepableItemsの配列を開始しました。これは、マップにオブジェクトを追加および削除すると変更されます。 また、頻繁に呼び出される関数で複雑なチェックを行う必要がなくなりました。

 Game = function Game(level, premade) { // ... this.stepableItems = []; this.field.on('add', this.onAddObject.bind(this)); this.field.on('remove', this.onRemoveObject.bind(this)); // ... }; Game.prototype.onAddObject = function(object) { if (object.step && !(object instanceof Tank) && !(object instanceof Base)) { this.stepableItems[object.id] = object; } }; Game.prototype.onRemoveObject = function(object) { delete this.stepableItems[object.id]; }; Game.prototype.step = function() { for (var i in this.stepableItems) { this.stepableItems[i].step(); } // ... };
      
      







その結果、オブジェクトの交差点に再び戻りましたが、レベルはまったく異なります。

 ticks total nonlib name 129 0.0% 2.4% LazyCompile: MapTiled.intersects battlecity/src/utils/map_tiled.js:102 66 0.0% 1.2% Stub: SubStringStub 54 0.0% 1.0% Stub: CEntryStub 47 0.0% 0.9% Function: EventEmitter.emit events.js:38 39 0.0% 0.7% LazyCompile: MapTiled.add battlecity/src/utils/map_tiled.js:24 30 0.0% 0.6% Function: Socket._writeOut net.js:389
      
      







現在、5つのゲームがプロセッサー時間の15-16%を同時に占めています。 つまり、私の古いサーバーは1つのスレッドで約30のゲームをプルするはずです。



相続を持つバガ



私は1つのバグと戦わなければなりませんでした。 継承中に、親を「コンストラクター」と呼ぶのを忘れていました。

 function Parent() { this.property = []; } function Child() { // Parent.apply(this, argiments); -  }; Child.prototype = new Parent(); Child.prototype.constructor = Child;
      
      





その結果、プロパティ配列はプロトタイプにのみ現れ、これには現れませんでした。 つまり、Childのすべてのインスタンス間で共有されるため、実行時にエラーは発生しませんでしたが、検出が困難なバグが発生しました。 それと同じように、javascriptを使用すれば、足を踏み入れるのに費用はかかりません。



今後の計画



最初に行うことは交通量を減らすことであり、空中のアイデアから始めることを考えています-「移動の開始」、「停止」などの制御コマンドのみをクライアントに送信します。 非同期に問題があると思いました。 しかし、各制御チームとの同期のために座標を転送する場合、それらは簡単に解決できるように思えます。

クライアントも最適化する必要がありますが、具体的なアイデアはまだありません。 より正確には、私はまだクライアントのプロファイルを作成していません。そして、正確に何が遅くなるのかわかりません。 一般的に、このプロジェクトはゲーム自体のためではなく、アイデアの実験的なものとして私にとって興味深いものです。 たとえば、OOPに関する本やいくつかの記事を読んでいるときに、得た知識を適用できるコードを手元に置いていました。 だから私はあなたが読むことができるアドバイスを喜んでいるでしょう。



前編



All Articles