Како го дизајниравме 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 тестови за оптоварување и тестови од крај до крај со нашиот прилагоден протокол.По разгледувањето на сите барања кои беа дискутирани во Тестирање и некои прелиминарни дискусии, ние бевме оставени со овие кандидати: k6 Locust NBomber К6 Локално НБМБ Секој од нив имал свои предности и недостатоци, но имало неколку работи кои ниту еден од нив не можел да ги реши без огромни (понекогаш внатрешни) промени: Прво, постоела потреба поврзана со комуникацијата меѓу ботови за синхронизација, како што беше дискутирано порано. Второ, клиентите се прилично реактивни и нивната состојба може да се промени без експлицитна акција од тест сценариото; ова доведува до дополнителна синхронизација и потребата да скокате низ многу скокови во вашиот код. Последно, но не и најмалку важно, тие алатки беа премногу тесно фокусирани на тестирање на перформансите, нудејќи бројни функции за постепено зголемување на товарот или одредување на временските интервали, но немаа способност да креираат сложени разгранети сценарија на едноставен начин. Време е да се прифати фактот дека навистина ни треба специјализирана алатка. е роден Свар Свар На високо ниво, задачата на 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 апликација, веројатно знаете дека тоа е асинхрон код. кој е одговорен за елегантно враќање на вашите асинхрони повици назад во UI нишката. DispatcherSynchronizationContext Во нашиот сценарио, не се грижиме за афинитетот на нишките, а наместо тоа се грижиме повеќе за редоследот на извршување на задачите во сценариото. Пред да се брзаме да напишеме код на ниско ниво, ајде да погледнеме на една не широко позната класа наречена Од на : ConcurrentExclusiveSchedulerPair docs Доц Обезбедува распоредувачи на задачи кои координираат за извршување на задачи, додека обезбедува дека истовремени задачи може да се извршуваат истовремено и исклучителни задачи никогаш не го прават тоа. Обезбедува распоредувачи на задачи кои координираат за извршување на задачи, додека обезбедува дека истовремени задачи може да се извршуваат истовремено и исклучителни задачи никогаш не го прават тоа. Ова изгледа токму она што го сакаме! Сега треба да се осигураме дека целиот код во сценариото се извршува на додека кодот на ботот се извршува на . 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()); Каде е gRPC апстракција на клиент-страна RPC повикување. CallInvoker Следно, би било одлично да се опфатат некои делови од кодот за да се измери перформансите. Со следниот интерфејс: 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 повици, тогаш сите напори за синхронизација се губат, нели? Методот на полнење само ќе повика За поедноставност, RPC клиентите се чуваат надвор од ботовите. PingAsync 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 без да го направам овој текст прекумерно надуен.