Како го дизајниравме Swarm, нашата внатрешна рамка за тестирање на товарот, за да се справите со предизвиците на тестирање од крај до крај, прилагодени протоколи и симулации на ботови во големи размери во gamedev.
Како го дизајниравме Swarm, нашата внатрешна рамка за тестирање на товарот, за да се справите со предизвиците на тестирање од крај до крај, прилагодени протоколи и симулации на ботови во големи размери во gamedev.
Денес, навистина е тешко да се замисли софтверски производ без баремНекоиниво на тестирање. Тестовите на единиците се стандарден начин да се фатат грешките во мали парчиња код, додека тестовите од крај до крај ги покриваат целите работни процеси на апликациите. GameDev не е исклучок, но доаѓа со свои уникатни предизвици кои не секогаш се усогласуваат со начинот на кој обично ги тестираме веб апликациите.
Здраво, јас сум Андреј Рахубов, водечки софтверски инженер на MY.GAMES! Во оваа статија, ќе ги споделам деталите за позадината што се користи во нашето студио, и нашиот пристап за тестирање на мета-игра во War Robots: Frontiers.
Забелешка за нашата архитектура
Без да одиме во какви било непотребни детали, нашиот backend се состои од сет на класични услуги и клаустер на меѓусебно поврзани јазли каде што живеат актерите.
И покрај некои други недостатоци кои може да постојат, овој пристап се покажа како прилично добар во решавањето на еден од најтешките проблеми во развојот на играта: брзината на итерација.ТоаПа, истото го имаме и за GDD (документ за дизајн на игри).
Актерите помагаат на начин што тие се многу евтини за имплементација и модификација во раните фази на дизајнот на карактеристиките и тие се прилично едноставни за рефактор како посебна услуга ако потребата се појави подоцна.
Очигледно, ова го комплицира тестирањето на оптоварувањето и интеграцијата, бидејќи веќе немате јасна поделба на услугите со мали, добро дефинирани АПИ.
Тестирање
Како што може да претпоставите, тестирањето на единиците за актерите и услугите (и тестирањето на оптоварувањето / интеграцијата за услугите) не се разликува од она што ќе го најдете во кој било друг тип на софтвер.
- Тестирање на оптоварување и интеграција актери
- End-to-end и load backend тестирање
Ние нема да ги разгледаме тестирањето на актерите во детали тука, бидејќи тоа не е специфично за GameDev, туку се однесува на самиот модел на актер. Вообичаениот пристап е да се има посебен кластер со еден јазол кој е погоден за поврзување во меморијата во рамките на еден тест и кој исто така е способен да ги потисне сите излезни барања и да ги повика актерите API.
Тоа беше кажано, работите почнуваат да станат поинтересни кога ќе дојдеме до полнење или тестирање од крај до крај – и тука започнува нашата приказна.
Значи, во нашиот случај, клиентската апликација е самата игра.Играта користи Unreal Engine, така што кодот е напишан во C++, а на серверот користиме C#. Играчите комуницираат со елементите на корисничкиот интерфејс во играта, произведувајќи барања до задниот крај (или барањата се прават индиректно).
Во овој момент, огромното мнозинство на рамки само престануваат да работат за нас, и било каков вид на селен-како комплети кои сметаат клиентски апликации прелистувач се надвор од опсег.
Следниот проблем е дека користиме прилагоден комуникациски протокол помеѓу клиентот и задната страна. Овој дел навистина заслужува посебен напис, но јас ќе ги истакнам клучните концепти:
- Комуникацијата се случува преку WebSocket конекција
- Тоа е шема-прво; ние користиме Protobuf за да ја дефинираме структурата на пораките и услугите
- WebSocket пораки се Protobuf пораки упатени во контејнер со метаподатоци кои имитираат некои потребни gRPC-релевантни информации, како што се URL и наслови
Значи, секоја алатка која не дозволува дефинирање на прилагодени протоколи не е погодна за задачата.
Алатката за полнење да ги тестира сите
Во исто време, сакавме да имаме една алатка која може да напише и REST / gRPC тестови за оптоварување и тестови од крај до крај со нашиот прилагоден протокол.По разгледувањето на сите барања кои беа дискутирани во Тестирање и некои прелиминарни дискусии, ние бевме оставени со овие кандидати:
Секој од нив имал свои предности и недостатоци, но имало неколку работи кои ниту еден од нив не можел да ги реши без огромни (понекогаш внатрешни) промени:
- Прво, постоела потреба поврзана со комуникацијата меѓу ботови за синхронизација, како што беше дискутирано порано.
- Второ, клиентите се прилично реактивни и нивната состојба може да се промени без експлицитна акција од тест сценариото; ова доведува до дополнителна синхронизација и потребата да скокате низ многу скокови во вашиот код.
- Последно, но не и најмалку важно, тие алатки беа премногу тесно фокусирани на тестирање на перформансите, нудејќи бројни функции за постепено зголемување на товарот или одредување на временските интервали, но немаа способност да креираат сложени разгранети сценарија на едноставен начин.
Време е да се прифати фактот дека навистина ни треба специјализирана алатка.Сваре роден
Свар
На високо ниво, задачата на Swarm е да започне многу актери кои ќе произведуваат оптоварување на серверот. Овие актери се нарекуваат ботови, и тие можат да симулираат однесување на клиентот или одделно однесување на серверот. Swarm се состои од еден или повеќе агенти кои ги хостираат овие ботови и комуницираат едни со други.
Повеќе формално, тука е листа на барања за алатката:
- Реакцијата на државните ажурирања треба да биде лесна
- Конкуренцијата не треба да биде проблем
- Кодот треба да биде автоматски инструментализиран
- Комуникацијата бот-то-бот треба да биде лесна
- Повеќе инстанции треба да бидат спојувачки за да се создаде доволно оптоварување
- Алатката треба да биде лесна и способна да создаде пристоен стрес на самата позадина
Како бонус, додадов и некои дополнителни точки:
- Можни се и сценарија за перформанси и сценарија за тестирање од крај до крај.
- Алатот треба да биде транспорт-агностичен; ние треба да можеме да го поврземе ова со кој било друг можен транспорт ако е потребно.
- Алатката има императивен стил на код, бидејќи јас лично имам цврсто мислење дека декларативниот стил не е погоден за сложени сценарија со условни одлуки.
- Ботовите треба да можат да постојат одделно од алатката за тестирање, што значи дека не треба да има тврди зависности
Нашата цел
Замислете го кодот што би сакале да го напишеме; ќе го разгледаме ботот како кукла, и тој не може да ги направи работите сам, тој може само да ги задржи инваријантите, додека Сценарио е куклата која ги повлекува жиците.
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));
}
}
Стејминг
Backend притиска многу ажурирања на клиентот што може да се случи спорадично; во примерот погоре, еден таков настан еGroupInviteAddedEvent
Ботот треба да може да реагира на овие настани внатрешно и да даде можност да ги набљудува од надворешен код.
public Task SubscribeToGroupState()
{
Subscription.Listen(OnGroupStateUpdate);
Subscription.Start(api.GroupServiceClient.SubscribeToGroupStateUpdates, new SubscribeToGroupStateUpdatesRequest());
return Task.CompletedTask;
}
Додека кодот е прилично едноставен (и како што може да погодитеOnGroupStateUpdate
манипулатор е само долг случај за менување), многу работи се случуваат тука.
StreamSubscription
Самиот АIObservable<ObservedEvent<TStreamMessage>>
и обезбедува корисни екстензии, има зависен животен циклус и е покриен со метрики.
Друга предност: не бара експлицитна синхронизација за читање или модифицирање на состојбата, без оглед на тоа каде се имплементира манипулаторот.
case GroupUpdateEventType.GroupInviteAddedEvent:
State.IncomingInvites.Add(ev.GroupInviteAddedEvent.GroupId, ev.GroupInviteAddedEvent.Invite);
break;
Конкуренцијата не треба да биде проблем
Идејата е едноставна: никогаш не извршувајте код кој истовремено припаѓа на сценарио или на бот кој е роден во тој сценарио, и понатаму, внатрешниот код за бот/модул не треба да се извршува истовремено со другите делови на бот.
Ова е слично на заклучување за читање / пишување, каде што се чита кодот на сценариото (споделен пристап) и кодот на ботот се пишува (ексклузивен пристап).
Распоредувачи на задачи и контекст на синхронизација
Двете механизми споменати во овој наслов сега се многу моќни делови на асинхрон код во C#. Ако некогаш сте развиле WPF апликација, веројатно знаете дека тоа е асинхрон код.DispatcherSynchronizationContext
кој е одговорен за елегантно враќање на вашите асинхрони повици назад во UI нишката.
Во нашиот сценарио, не се грижиме за афинитетот на нишките, а наместо тоа се грижиме повеќе за редоследот на извршување на задачите во сценариото.
Пред да се брзаме да напишеме код на ниско ниво, ајде да погледнеме на една не широко позната класа нареченаConcurrentExclusiveSchedulerPair
Од на
Обезбедува распоредувачи на задачи кои координираат за извршување на задачи, додека обезбедува дека истовремени задачи може да се извршуваат истовремено и исклучителни задачи никогаш не го прават тоа.
Обезбедува распоредувачи на задачи кои координираат за извршување на задачи, додека обезбедува дека истовремени задачи може да се извршуваат истовремено и исклучителни задачи никогаш не го прават тоа.
Ова изгледа токму она што го сакаме! Сега треба да се осигураме дека целиот код во сценариото се извршува на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();
}
наRunScenarioInstance
Методот, од своја страна, се однесува на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;
});
}
Апстракцијата за време на извршување го опфаќа распоредот на ист начин како и порано, со повикувањеTask.Factory.StartNew
Со соодветен распоред.
Генерација на кодови
Во ред, сега ние треба рачно да се фати сè во внатрешноста наDo
повик; и додека ова го решава проблемот, тоа е склоно кон грешки, лесно да се заборави, и генерално да се зборува, добро, изгледа чудно.
Да го погледнеме овој дел од кодот уште еднаш:
await leader.Group.SendGroupInvite(follower.PlayerId);
Еве, нашиот бот имаGroup
имотот на.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), но по извесното тестирање на концепт-сценарио, се покажа дека количината на работа би можела значително да се намали на два поедноставни концепти: ред и селектор.
Билети - билети
Во реалниот свет, играчите комуницираат едни со други на некој начин – тие пишуваат директни пораки или користат гласовен разговор. Со ботови, ние не треба да симулираме таа грануларност, и ние само треба да комуницираме намери.НекојВе молиме да ме поканите во групата.“ Еве каде влегуваат во игра билетите:
public interface ISwarmTickets
{
Task PlaceTicket(SwarmTicketBase ticket);
Task<SwarmTicketBase?> TryGetTicket(Type ticketType, TimeSpan timeout);
}
Ботот може да постави билет, а потоа друг бот може да го преземе тој билет.ISwarmTickets
не е ништо повеќе од брокер за пораки со ред заticketType
(Всушност тоа емалкуповеќе од тоа, бидејќи постојат дополнителни опции за да се спречат ботовите да ги преземат сопствените билети, како и други мали прилагодувања).
Со овој интерфејс, конечно можеме да го поделиме примерот на сценариото на два независни сценарија. (овде, целиот дополнителен код е отстранет за да ја илустрира основната идеја):
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
е само фантастична обвивка околу споделен бројач и операција модуло за да го изберете вистинскиот елемент од листата.
Кластерот на Swarms
Сега, со сета комуникација скриена зад редови и споделени бројачи, станува тривијално да се воведе или оркестраторски јазол, или да се користат некои постоечки MQ и KV складишта.
Забавен факт: ние никогаш не завршивме со имплементацијата на оваа функција.Кога QA ги доби рацете на еден јазол имплементација на SwarmAgent
, тие веднаш почнаа едноставно да распоредуваат повеќе примероци на независни агенти со цел да го зголемат товарот.
Еднократна перформанса
Што е со перформансите? Колку е изгубено во сите тие обвивки и имплицитни синхронизации? нема да ве преоптоварам со сите различни тестови извршени, само најзначајните две кои докажаа дека системот е способен да создаде доволно добро оптоварување.
Поставување на бенчмарк:
BenchmarkDotNet v0.14.0, Windows 10 (10.0.19045.4651/22H2/2022Update)
Intel Core i7-10875H процесор 2.30GHz, 1 процесор, 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);
}
}
}
Прво, ајде да погледнеме на речиси чиста надмоќ на планирање против едноставна задача за раѓање.
Во овој случај, двата метода на оптоварување се како што следува:
private ValueTask CounterLoadMethod(int i)
{
Interlocked.Increment(ref StaticControl.Counter);
return ValueTask.CompletedTask;
}
Резултати за итерации = 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.0421 во Скопје
0.3433
-
-
3.8 КБ
едноставен
100
30281 возење
0.2720 УС
0.2544Уреди
3.1128
0.061
-
5.67 КБ
едноставен
1000
250 693
2.1626 во Скопје
2.4037Уреди
30.2734
5.8594
-
250.67 КБ
Распоредот
10
40629 во Скопје
7742 возење
08054Јавно
8.1787
0.1221
-
66.15 КБ
Распоредот
100
325.386Уреди
2.3414Јазици
1901 година
81.0547
14.6484
-
62.09 КБ
Распоредот
1000
4 685 812
24.7917Уреди
219772Јавна
812.5
375
-
6617.59 КБ
Изгледа лошо? Не баш. Пред всушност да се спроведе тестот, очекував многу полоши перформанси, но резултатите навистина ме изненадија. Само размислете за тоа колку се случи под капакот и придобивките што ги добивме само за ~4us по паралелен случај. Во секој случај, ова е само илустрација на надворешноста; ние сме заинтересирани за многу повеќе практични референтни вредности.
Од една страна, речиси секој метод на бот го прави тоа, но од друга страна, ако нема ништо повеќе од само 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 МС
1.804 МЗ
2.148 МЗ
600
200
6.4 МБ
едноставен
1000
596.69 МЗ
15 592 МС
45 730 МС
9000
7000
777 МБ
Распоредот
100
95.48 МЗ
1.547 МЗ
1.292 МЗ
833.3333
333.3333
6.85 МБ
Распоредот
1000
625,52 МС
14 697 МЗ
42405 МС
8000
7000
68,57 МБ
Како што се очекува, влијанието на перформансите врз реалните сценарија за работа е незначително.
Анализа
Се разбира, постојат дневници за задната страна, метрики и траги кои обезбедуваат добар преглед на она што се случува за време на товарот. но Swarm оди чекор понатаму, пишувајќи свои податоци - понекогаш поврзани со задната страна - дополнувајќи го.
Еве пример за неколку метрики во тест за неуспех, со зголемен број на актери. Тестот не успеа поради повеќе прекршувања на SLA (види временски интервали во API повици), и имаме сите инструменти потребни за да го набљудуваме она што се случува од перспектива на клиентот.
Обично го прекинуваме тестот целосно по одреден број грешки, поради што API повиците завршуваат ненадејно.
Се разбира, трагите се неопходни за добра набљудуваност. За тестирањето на Swarm, трагите на клиентите се поврзани со трагите на задната страна на едно дрво.connectionIdD/botId
Тоа помага во дебито.
Забелешка: DevServer е погодна монолитна зграда на сите-во-еден-бак-енд услуги кои ќе бидат лансирани на компјутерите на програмерите, и ова е специјално дизајниран пример за да се намали количината на детали на екранот.
Swarm ботови пишуваат друг вид на трага: тие ги имитираат трагите што се користат во анализата на перформансите.Со помош на постоечките алатки, тие траги може да се гледаат и анализираат со користење на вградени можности како PerfettoSQL.
Со што завршивме на крајот
Сега имаме SDK на кој се изградени и ботовите на GameClient и Dedicated Server. Овие ботови сега ја поддржуваат поголемиот дел од логиката на играта - тие вклучуваат подсистеми на пријатели, групи за поддршка и мешање.
Способноста да се напише едноставен код овозможи да се создадат нови модули и едноставни сценарија за тестирање од страна на тимот на QA. Постои идеја за FlowGraph сценарија (визуелно програмирање), но ова останува само идеја во моментов.
Тестовите за перформанси се речиси континуирани - го кажувам речиси затоа што сè уште треба да ги започнете рачно.
Swarm не само што помага со тестови, туку обично се користи за репродукција и поправка на грешки во логиката на играта, особено кога ова е тешко да се направи рачно, како што е кога ви треба неколку игри клиенти извршување на посебен редослед на акции.
За да ги сумираме работите, ние сме исклучително задоволни од нашиот резултат и дефинитивно не жалиме за напорите потрошени за развивање на нашиот сопствен систем за тестирање.
Се надевам дека ви се допадна оваа статија, беше предизвик да ви кажам за сите нијанси на развојот на Swarm без да го направам овој текст прекумерно надуен.