Swarm は、GameDev でエンドトップテスト、カスタマイズされたプロトコル、大規模なボットシミュレーションの課題に対処するための内部負荷テストフレームワークです。
Swarm は、GameDev でエンドトップテスト、カスタマイズされたプロトコル、大規模なボットシミュレーションの課題に対処するための内部負荷テストフレームワークです。
今日では、少なくともソフトウェア製品がなければ想像することは本当に困難です。いくつかテストのレベル ユニットテストは、コードの小さな部分でバグを捕獲する標準的な方法であり、エンドトップテストはアプリケーションの全ワークフローをカバーします。GameDevは例外ではありませんが、独自のユニークな課題を持っていますが、私たちが通常Webアプリケーションをテストする方法と常に一致していません。
Hi, I'm Andrey Rakhubov, a Lead Software Engineer at MY.GAMES! この投稿では、私たちのスタジオで使用されているバックエンドの詳細と、War Robots: Frontiersのメタゲームのロードテストのアプローチを共有します。
A Note on Our Architecture (私たちの建築について)
不要な詳細に進むことなく、私たちのバックエンドは、クラシックなサービスのセットと、俳優が住んでいる相互接続されたノードのクラスターで構成されています。
いくつかの他の欠点にもかかわらず、このアプローチは、ゲーム開発における最も困難な問題の1つを解決するのに十分に優れていることが証明されています:イテレーションスピード。ITGDD(Game Design Document)についても同じことが言えます。
俳優は、機能設計の初期段階で実装し、変更するのが非常に安い方法で役立ち、その必要性が後で発生した場合、別々のサービスとして再現するのがかなり簡単です。
明らかに、これは負荷および統合テストを複雑にし、あなたはもはや小さい、正確に定義されたAPIでサービスの明確な分離を持っていないためです。
テスト
あなたが推測しているように、アクターとサービスの単位テスト(およびサービスの負荷/統合テスト)は、他の種類のソフトウェアで見つけるものとは異なりません。
- Load and Integration Testing Actors(ロード・インテグレーション・テスト・エージェント)
- End-to-end and load backend テスト
ここでは、GameDevに特異なものではなく、俳優モデル自体に関連しているため、詳細に俳優をテストするつもりはありません。通常のアプローチは、単一テスト内でメモリにケーブル化するのに適した特別な単一ノードクラスターを持つことです。
言い換えれば、物事がより興味深くなり始めるのは、ロードやエンド・トゥー・エンド・テストのときであり、そこから私たちの物語が始まります。
したがって、私たちのケースでは、クライアントアプリケーションはゲーム自体です。ゲームはUnreal Engineを使用しますので、コードはC++で書かれ、サーバーではC#を使用します。
この時点で、大半のフレームワークは私たちのために機能しなくなり、クライアントアプリケーションをブラウザとみなすセレニウムのようなキットは適用範囲外です。
次の問題は、クライアントとバックエンド間のカスタマイズされたコミュニケーションプロトコルを使用することです. This part really deserves a separate article altogether, but I will highlight the key concepts:
- 通信は WebSocket 接続を通じて行われます。
- それはスキーマファーストで、メッセージとサービスの構造を定義するためにProtobufを使用します。
- WebSocket メッセージは、URLやヘッダーなどの必要な gRPC 関連の情報を模するメタデータを含むコンテナに包装された Protobuf メッセージです。
したがって、カスタムプロトコルを定義できないツールは、このタスクに適していません。
すべてをテストするためのロードツール
同時に、REST/gRPC負荷テストとカスタムプロトコルでのエンドトップテストの両方を書くことができる単一のツールを用意したいと考えました。テストで議論されたすべての要件といくつかの予備的な議論を検討した後、私たちは以下の候補者に残されました:
それぞれに利点と欠点がありましたが、大きな(時には内部の)変化なしに解決できなかったいくつかのものがありました。
- まず、前述したように、同期目的のボット間のコミュニケーションに関連する必要性がありました。
- 第二に、クライアントはかなり反応的で、その状態はテストシナリオからの明示的な行動なしに変化することができ、これは追加の同期とあなたのコードで多くのハップを跳ね抜く必要性につながります。
- 最後に、これらのツールはパフォーマンステストにあまりにも狭く焦点を当てており、負荷を徐々に増やしたり、時間の間隔を指定したりするための多くの機能を提供し、複雑な分岐シナリオを単純に作成する能力が欠けていた。
本当に専門のツールが必要であるという事実を受け入れる時が来た。スワーム生まれた。
スワーム
高いレベルで、Swarmのタスクは、サーバーに負荷を生成する多くの俳優を開始することです これらの俳優はボットと呼ばれ、彼らはクライアントの行動や専用サーバーの行動をシミュレートすることができます。
より公式に、以下はツールの要件のリストです。
- ステータスアップデートへの反応は簡単でなければならない
- 競争は問題であってはならない
- コードは自動的に道具化されなければならない。
- ボット対ボットのコミュニケーションは簡単でなければならない。
- 十分なロードを作成するために複数のインスタンスを統合する必要があります。
- ツールは軽量で、バックエンド自体に適切なストレスを作り出すことができる必要があります。
ボーナスとして、私もいくつかの追加ポイントを追加しました:
- パフォーマンスとエンド・トゥー・エンド・テストの両方のシナリオが可能であるべきである。
- ツールは輸送アグネスティックでなければならず、必要に応じてこれを他の可能な輸送に接続することができるはずです。
- このツールには imperative コード スタイルが含まれていますが、個人的には declarative スタイルは条件付きの決定を伴う複雑なシナリオには適していません。
- ボットはテストツールとは別に存在することができるはずで、つまり硬い依存性は存在しないはずです。
私たちの目標
私たちが書きたいコードを想像してみましょう; ボットは人形のように考えますが、それ自体で物事をすることができず、変数だけを維持することができ、シナリオは弦を引く人形です。
public class ExampleScenario : ScenarioBase
{
/* ... */
public override async Task Run(ISwarmAgent swarm)
{
// spawn bots and connect to backend
var leader = SpawnClient();
var follower = SpawnClient();
await leader.Login();
await follower.Login();
// expect incoming InviteAddedEvent
var followerWaitingForInvite = follower.Group.Subscription
.ListenOnceUntil(GroupInviteAdded)
.ThrowIfTimeout();
// leader sends invite and followers waits for it
await leader.Group.SendGroupInvite(follower.PlayerId);
await followerWaitingForInvite;
Assert.That(follower.Group.State.IncomingInvites.Count, Is.EqualTo(1));
var invite = follower.Group.State.IncomingInvites[0];
// now vice versa, the leader waits for an event...
var leaderWaitingForAccept = leader.Group.Subscription
.ListenOnceUntil(InviteAcceptedBy(follower.PlayerId))
.ThrowIfTimeout();
// ... and follower accept invite, thus producing the event
await follower.Group.AcceptGroupInvite(invite.Id);
await leaderWaitingForAccept;
Assert.That(follower.Group.State.GroupId, Is.EqualTo(leader.Group.State.GroupId));
PlayerId[] expectedPlayers = [leader.PlayerId, follower.PlayerId];
Assert.That(leader.Group.State.Players, Is.EquivalentTo(expectedPlayers));
Assert.That(follower.Group.State.Players, Is.EquivalentTo(expectedPlayers));
}
}
STEAMING
バックエンドはクライアントに多くのアップデートを押し付けるが、これは偶発的に起こり得る; 上記の例では、そのようなイベントの1つがGroupInviteAddedEvent
ボットは、これらのイベントに内部的に反応し、外部コードからそれらを観察する機会を与えることができます。
public Task SubscribeToGroupState()
{
Subscription.Listen(OnGroupStateUpdate);
Subscription.Start(api.GroupServiceClient.SubscribeToGroupStateUpdates, new SubscribeToGroupStateUpdatesRequest());
return Task.CompletedTask;
}
コードはかなりシンプルですが(そしてあなたが推測したように)OnGroupStateUpdate
トレーダーは長いスイッチケースに過ぎません)、ここで多くのことが起こります。
StreamSubscription
自分はAIObservable<ObservedEvent<TStreamMessage>>
それは有用な拡張機能を提供し、依存的なライフサイクルを持っており、メトリクスで覆われています。
もう一つの利点は、トレーダーがどこで実装されているかに関係なく、明示的な同期を必要としない状態の読み取りや修正です。
case GroupUpdateEventType.GroupInviteAddedEvent:
State.IncomingInvites.Add(ev.GroupInviteAddedEvent.GroupId, ev.GroupInviteAddedEvent.Invite);
break;
競争は問題であってはならない
アイデアは単純である:そのシナリオに属するコードやそのシナリオで生まれたボットに同時に属するコードを実行しないこと、さらに、ボット/モジュールの内部コードは、ボットの他の部分と同時に実行してはならない。
これは、シナリオコードが読まれる(共有アクセス)とボットコードが書かれる(専用アクセス)の読書/書き込みロックに似ています。
タスクスケジュールと同期の文脈
このタイトルで言及された2つのメカニズムは、C#のアシンクコードの非常に強力な部分です. あなたがWPFアプリケーションを開発したことがあるなら、あなたはおそらくそれを知っているでしょう。DispatcherSynchronizationContext
優雅にアシンク呼び出しをUIトレードに戻すことに責任があります。
私たちのシナリオでは、トレードアフィニティーは気にしないが、代わりにシナリオ内のタスクの実行順序が気になる。
低レベルのコードを書く前に、広く知られていないクラスを調べてみましょう。ConcurrentExclusiveSchedulerPair
. from the
タスクスケジュールを提供し、同時に同時に実行されるタスクが同時に実行され、独占的なタスクが実行されないようにします。
タスクスケジュールを提供し、同時に同時に実行されるタスクが同時に実行され、独占的なタスクが実行されないようにします。
これは正確に私たちが望んでいるように見えます! 今、シナリオ内のすべてのコードが実行されていることを確認する必要があります。ConcurrentScheduler
ボットのコードが実行されると、ExclusiveScheduler
.
タスクのスケジュールをどのように設定しますか? 一つのオプションは、シナリオが起動したときに実行されるパラメータとして明示的に送信することです。
public Task LaunchScenarioAsyncThread(SwarmAgent swarm, Launch launch)
{
return Task.Factory.StartNew(
() =>
{
/* <some observability and context preparations> */
return RunScenarioInstance(Scenario, swarm, LaunchOptions, ScenarioActivity, launch);
},
CancellationToken.None,
TaskCreationOptions.DenyChildAttach,
Scenario.BotScenarioScheduler.ScenarioScheduler).Unwrap();
}
THERunScenarioInstance
方法は、反対に、呼びかける。SetUp
そしてRun
シナリオの方法で、それらは共通のスケジュールで実行されます(ScenarioScheduler
競合する部分は、ConcurrentExclusiveSchedulerPair
( )
今、私たちがシナリオコードの中にいて、これを行うと、
public Task LaunchScenarioAsyncThread(SwarmAgent swarm, Launch launch)
{
return Task.Factory.StartNew(
() =>
{
/* <some observability and context preparations> */
return RunScenarioInstance(Scenario, swarm, LaunchOptions, ScenarioActivity, launch);
},
CancellationToken.None,
TaskCreationOptions.DenyChildAttach,
Scenario.BotScenarioScheduler.ScenarioScheduler).Unwrap();
}
...アシンクステートマシンは、私たちのタスクのスケジュールを保持することによって、私たちのために仕事をします。
今はSendGroupInvite
同時にスケジュールも実行するので、同じトリックも実行します。
public Task<SendGroupInviteResponse> SendGroupInvite(PlayerId inviteePlayerId)
{
return Runtime.Do(async () =>
{
var result = await api.GroupServiceClient.SendGroupInviteAsync(/* ... */);
if (!result.HasError)
{
State.OutgoingInvites.Add(inviteePlayerId);
}
return result;
});
}
Runtime abstraction wraps scheduling in the same way as before, by callingTask.Factory.StartNew
適切なスケジュールで
コード世代
OK, now we need to manually wrap everything inside theDo
これが問題を解決する一方で、それは間違いの傾向があり、忘れやすいし、一般的に言えば、それは奇妙に見える。
コードのこの部分をもう一度見てみましょう:
await leader.Group.SendGroupInvite(follower.PlayerId);
ここで、ボットはAGroup
財産Group
コードを別々のクラスに分割し、コードを膨らませないために存在するモジュールです。Bot
クラス
public class BotClient : BotClientBase
{
public GroupBotModule Group { get; }
/* ... */
}
例えば、モジュールにはインタフェースがあるとします。
public class BotClient : BotClientBase
{
public IGroupBotModule Group { get; }
/* ... */
}
public interface IGroupBotModule : IBotModule, IAsyncDisposable
{
GroupBotState State { get; }
Task<SendGroupInviteResponse> SendGroupInvite(PlayerId toPlayerId);
/* ... */
}
public class GroupBotModule :
BotClientModuleBase<GroupBotModule>,
IGroupBotModule
{
/* ... */
public async Task<SendGroupInviteResponse> SendGroupInvite(PlayerId inviteePlayerId)
{
// no wrapping with `Runtime.Do` here
var result = await api.GroupServiceClient.SendGroupInviteAsync(new() /* ... */);
if (!result.HasError)
{
State.OutgoingInvites.Add(new GroupBotState.Invite(inviteePlayerId, /* ... */));
}
return result;
}
|
もはや包装や醜いコードはなく、すべてのモジュールのインターフェイスを作成するための単純な要件(開発者にとってはかなり一般的です)です。
しかし、スケジュールはどこにありますか? 魔法は、各IBotModuleインターフェイスのためのプロキシクラスを生成し、各機能を包み込むソースジェネレーターの内部で起こります。Runtime.Do
電話:
public const string MethodProxy =
@"
public async $return_type$ $method_name$($method_params$)
{
$return$await runtime.Do(() => implementation.$method_name$($method_args$));
}
";
楽器化
次のステップは、メトリックと痕跡を収集するためにコードを楽器化することです。まず、すべてのAPI呼び出しを観察する必要があります。この部分は非常にシンプルで、私たちの輸送は効果的にgRPCチャンネルであるふりをするので、正しく書かれたインタビュー器を使用するだけです。
callInvoker = channel
.Intercept(new PerformanceInterceptor());
...どこCallInvoker
gRPCはクライアント側のRPC呼び出しの抽象化です。
次に、パフォーマンスを測定するために、コードのいくつかの部分を範囲化することは素晴らしいでしょう。IInstrumentationFactory
以下のインターフェイスで、
public interface IInstrumentationFactory
{
public IInstrumentationScope CreateScope(
string? name = null,
IInstrumentationContext? actor = null,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
}
今、あなたが興味を持っている部分を包み込むことができます:
public Task AcceptGroupInvite(GroupInvideId inviteId)
{
using (instrumentationFactory.CreateScope())
{
var result = await api.GroupServiceClient.AcceptGroupInviteAsync(request);
}
}
この機能を使用してサブスケープを作成することもできますが、各モジュールのプロキシメソッドはスケープを自動的に定義します。
public const string MethodProxy =
@"
public async $return_type$ $method_name$($method_params$)
{
using(instrumentationFactory.CreateScope(/* related args> */))
{
$return$await runtime.Do(() => implementation.$method_name$($method_args$));
}
}
";
機器の範囲は、実行時間、例外を記録し、分散したトラックを作成し、デバッグログを書くことができます。
Bot-to-bot コミュニケーション
「私たちの目標」セクションの例では、実際にはコミュニケーションが表示されていません。PlayerId
シナリオを書くこのスタイルはしばしば簡単でシンプルですが、このアプローチがうまくいかない複雑なケースがあります。
最初は、コミュニケーションのための共有キー値ストレージ(Redisのように)でブラックボードのパターンを実装する予定でしたが、コンセプトシナリオテストのいくつかの実証実験を実行した後、作業量は2つの単純なコンセプトに大幅に削減できることが判明しました:列と選択器。
チケット - Tickets
実際の世界では、プレイヤーは何らかの方法で互いにコミュニケーション - 彼らは直接のメッセージを書くか、音声チャットを使用します. ボットでは、我々はその細かいものをシミュレートする必要はありません、我々は意図を伝える必要があるだけです。誰かグループに招待してください」これがチケットの出場地です。
public interface ISwarmTickets
{
Task PlaceTicket(SwarmTicketBase ticket);
Task<SwarmTicketBase?> TryGetTicket(Type ticketType, TimeSpan timeout);
}
ボットはチケットを置き、別のボットはそのチケットを取得することができます。ISwarmTickets
is nothing more than a message broker with a queue per メッセージブローカーticketType
(実際は軽くそれ以上に、ボットが自身のチケットを取り戻すのを防ぐための追加のオプションがあり、他の小さな変更もあります)。
このインターフェイスでは、ようやく例のシナリオを2つの独立したシナリオに分割することができます(ここでは、すべての追加コードは、基本的なアイデアを示すために削除されます):
private async Task RunLeaderRole(ISwarmAgent swarm)
{
var ticket = await swarm.Blackboard.Tickets
.TryGetTicket<BotWantsGroupTicket>(TimeSpan.FromSeconds(5))
.ThrowIfTicketIsNull();
await bot.Group.SendGroupInvite(ticket.Owner);
await bot.Group.Subscription.ListenOnceUntil(
GotInviteAcceptedEvent,
TimeSpan.FromSeconds(5))
.ThrowIfTimeout();
}
private async Task RunFollowerRole(ISwarmAgent swarm)
{
var waitingForInvite = bot.Group.Subscription.ListenOnceUntil(
GotInviteAddedEvent,
TimeSpan.FromSeconds(5))
.ThrowIfTimeout();
await swarm.Blackboard.Tickets.PlaceTicket(new BotWantsGroupTicket(bot.PlayerId));
await waitingForInvite;
await bot.Group.AcceptGroupInvite(bot.Group.State.IncomingInvites[0].Id);
}
選択者
当然、二つの異なるシナリオに分割され、並行して開始することができる。時には、これが最善の方法であり、他の時には、グループサイズのダイナミックな構成(単一のリーダーあたりいくつかのフォロワー)または異なるデータ/役割を配布/選択する他の状況が必要かもしれません。
public override async Task Run(ISwarmAgent swarm)
{
var roleAction = await swarm.Blackboard
.RoundRobinRole(
"leader or follower",
Enumerable.Repeat(RunFollowerRole, config.GroupSize - 1).Union([RunLeaderRole]));
await roleAction(swarm);
}
こちら、RoundRobinRole
is just a fancy wrapper around a shared counter and a modulo operation to select the right element from the list. 共有カウンターと、リストから適切な要素を選択するためのモジュール操作のまわりに飾るだけです。
Cluster of Swarmsについて
今、すべてのコミュニケーションが列と共有カウンターの後ろに隠されているので、オーケストラノードを導入するか、既存のMQおよびKVストレージを使用するかは微妙になります。
Fun fact: we never finished the implementation of this feature. QA got their hands on a single node implementation of this feature. 面白い事実:我々はこの機能の実装を決して完了しませんでした。 SwarmAgent
彼らは直ちに、負荷を増やすために独立したエージェントの複数のインスタンスを単に展開し始めました。
単一実施例
パフォーマンスについてはどうですか? すべての包装と暗示的な同期の内部でどれだけ失われているのですか? 私は、システムが十分に良い負荷を作成することができると証明した最も重要な2つのテストで、さまざまなテストであなたを圧倒するつもりはありません。
ベンチマーク設定:
BenchmarkDotNet v0.14.0, Windows 10 (10.0.19045.4651/22H2/2022Update)
Intel Core i7-10875H CPU 2.30GHz, 1 CPU, 16 論理コア, 8 物理コア
.NET SDK 9.0.203 について
[ホスト] : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2
.NET 9.0 : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2
private async Task SimpleWorker()
{
var tasks = new List<Task>();
for (var i = 0; i < Parallelism; ++i)
{
var index = i;
tasks.Add(Task.Run(async () =>
{
for (var j = 0; j < Iterations; ++j)
{
await LoadMethod(index);
}
}));
}
await Task.WhenAll(tasks);
}
private async Task SchedulerWorker()
{
// scenarios are prepared in GlobalSetup
await Task.WhenAll(scenarios.Select(LaunchScenarioAsyncThread));
}
public class TestScenario : ScenarioBase
{
public override async Task Run()
{
for (var iteration = 0; iteration < iterations; ++iteration)
{
await botClient.Do(BotLoadMethod);
}
}
}
First let’s look at almost pure overhead of scheduling against simple Task spawning.
この場合、両方のロード方法は次のとおりです。
private ValueTask CounterLoadMethod(int i)
{
Interlocked.Increment(ref StaticControl.Counter);
return ValueTask.CompletedTask;
}
Iterations = 10 の結果:
WorkerType |
Parallelism |
Mean |
Error |
StdDev |
Gen0 |
Gen1 |
Gen2 |
Allocated |
---|---|---|---|---|---|---|---|---|
Simple |
10 |
3.565us |
0.0450us |
0.0421us |
0.3433 |
- |
- |
2.83KB |
Simple |
100 |
30.281us |
0.2720us |
0.2544us |
3.1128 |
0.061 |
- |
25.67KB |
Simple |
1000 |
250.693us |
2.1626us |
2.4037us |
30.2734 |
5.8594 |
- |
250.67KB |
Scheduler |
10 |
40.629us |
0.7842us |
0.8054us |
8.1787 |
0.1221 |
- |
66.15KB |
Scheduler |
100 |
325.386us |
2.3414us |
2.1901us |
81.0547 |
14.6484 |
- |
662.09KB |
Scheduler |
1000 |
4,685.812us |
24.7917us |
21.9772us |
812.5 |
375 |
- |
6617.59KB |
シンプル
10
5656人
0.0450円
0 0 0 421
0.3433
-
-
2.83KB
シンプル
100
281位
0.2720円
0.2544 ウィルス
3.1128
0.061
-
25.67KB
シンプル
1000
250 693
16262 イギリス
2.4037 イギリス
30.2734
5.8594
-
250.67KB
日程
10
40629人
7842 イギリス
0 0 0 0 0 0 0 0
8.1787
0.1221
-
66.15KB
日程
100
325.386 ドイツ
2.3414 ドイツ
2 1901年
81.0547
14.6484
-
6629KB
日程
1000
4685 812
24 717 ドイツ
21972 イギリス
812.5
375
-
6617.59KB
見た目は悪くない? 正確にはいきません。 実際にテストを行う前に、私はより悪いパフォーマンスを期待していましたが、結果は本当に私を驚かせました。 キャップの下でどれだけのことが起こったかについて考えて、パラレルインスタンスあたりわずか4USの利益を得ました。
現実的な最悪のケーステストシナリオはどうでしょうか? API 呼び出し以外の何もしない機能はどうでしょうか? 一方で、ボットのほぼすべての方法はそうしますが、反対に、ただの API 呼び出し以外の何もしない場合は、すべての同期努力が無駄になりますよね?
ロードメソッドはただ呼ぶだけPingAsync
シンプルさのために、RPCクライアントはボットの外側に保存されています。
private async ValueTask PingLoadMethod(int i)
{
await clients[i].PingAsync(new PingRequest());
}
以下は、再び10回のイーテレーション(grpcサーバはローカルネットワークにあります)の結果です。
WorkerType |
Parallelism |
Mean |
Error |
StdDev |
Gen0 |
Gen1 |
Allocated |
---|---|---|---|---|---|---|---|
Simple |
100 |
94.45 ms |
1.804 ms |
2.148 ms |
600 |
200 |
6.14 MB |
Simple |
1000 |
596.69 ms |
15.592 ms |
45.730 ms |
9000 |
7000 |
76.77 MB |
Scheduler |
100 |
95.48 ms |
1.547 ms |
1.292 ms |
833.3333 |
333.3333 |
6.85 MB |
Scheduler |
1000 |
625.52 ms |
14.697 ms |
42.405 ms |
8000 |
7000 |
68.57 MB |
シンプル
100
94.45 ms
804 ms
284 ms
600
200
6.14 MB
シンプル
1000
596.69 マス
15 592 ms
45730 ms
9000
7000
77.77 MB
日程
100
95.48 ms
1 547 ms
1 292 ms
833.3333
333.3333
6.85 MB
日程
1000
625.52 ms
14 697 ms
42405 ms
8000
7000
68,57 MB
予想どおり、実際の仕事のシナリオへのパフォーマンスの影響は軽微です。
分析
もちろん、バックエンドのログ、メトリクス、およびトラックは、ロード中に起こっていることの良い見方を提供しますが、Swarmは、時にはバックエンドに接続されている独自のデータを書くことで、さらに一歩進みます。
テストは、複数の SLA 違反により失敗しました(API 呼び出しのタイムアウトを参照)、そして、クライアントの視点から何が起こっているかを観察するために必要なすべての道具を持っています。
通常、一定の数のエラーが発生した後、テストを完全に中止するので、APIの呼び出しラインは突然終了します。
もちろん、足跡は良い観察性のために必要です。Swarmのテストを実行する場合、クライアントの足跡は1本の木にバックエンドの足跡を結びつけることができます。connectionIdD/botId
デバッグするのに役立ちます。
注: DevServer は、開発者の PC でリリースされるすべてのバックエンド サービスの便利な単一ビルドであり、これは画面上の詳細を減らすために特別に設計された例です。
Swarm ボットは別の種類のトラックを書く: パフォーマンス分析に使用されるトラックを模する. 既存のツールの助けを借りて、これらのトラックは PerfettoSQL のような内蔵機能を使用して表示および分析することができます.
What we ended up with at the end
今では、GameClientとDedicated Serverの両方のボットが構築されているSDKがあります。これらのボットは、ゲームのロジックの大部分をサポートしています - 彼らには、友人のサブシステム、サポートグループ、マッチングが含まれています。
シンプルなコードを書くことができ、QAチームが新しいモジュールやシンプルなテストシナリオを作成することが可能になりました。
パフォーマンステストはほぼ継続的で、ほとんど手動で開始する必要があるからだ。
Swarm はテストだけでなく、ゲームの論理におけるバグを再現し、修正するのに一般的に使用されます、特に手動で行うのが難しい場合、例えば、いくつかのゲームクライアントが特別なアクションの順序を実行する必要がある場合などです。
要するに、我々は我々の結果に非常に満足しており、我々の独自のテストシステムの開発に費やされた努力を絶対に後悔していない。
私はあなたがこの記事を楽しんだことを願っています、私はこのテキストを過度に膨らませることなく、Swarmの開発のすべてのニュアンスについてあなたに話すことは困難でした。私はあなたがテキストのサイズと情報のバランスを取るプロセスでいくつかの重要な詳細を除外した可能性がありますが、私は喜んであなたの質問がある場合は、より多くの文脈を提供します!