256 читања

Ниту една алатка не би можела да се справи со нашата рамка за тестирање на товарот за игри – така што го изградивме Swarm

од страна на randrewy...19m2025/05/20
Read on Terminal Reader

Премногу долго; Да чита

Потребна ни беше специјализирана алатка за тестирање на оптоварување која би можела да се справи со прилагодени протоколи WebSocket+Protobuf, синхронизација меѓу ботови и симулации на големи размери. Постоечките алатки беа кратки, па создадоа Swarm: внатрешна рамка на C# каде што „ботовите“ (актерите) работат сценарија под координирани распоредители, поддржуваат комуникација од бот до бот преку билети и селектори, и автоматски ги инструментираат сите кодови за метрики и траги. Дизајнот на Swarm ја става приоритетот на лесното реагирање на надградбите на состојбата, контролата на конкуренцијата преку ConcurrentExclusiveSchedulerPair, генерирање на код преку изворни генератори и целосна опсерва
featured image - Ниту една алатка не би можела да се справи со нашата рамка за тестирање на товарот за игри – така што го изградивме Swarm
Andrew Rakhubov HackerNoon profile picture
0-item
1-item

Како го дизајниравме Swarm, нашата внатрешна рамка за тестирање на товарот, за да се справите со предизвиците на тестирање од крај до крај, прилагодени протоколи и симулации на ботови во големи размери во gamedev.

Како го дизајниравме Swarm, нашата внатрешна рамка за тестирање на товарот, за да се справите со предизвиците на тестирање од крај до крај, прилагодени протоколи и симулации на ботови во големи размери во gamedev.


Денес, навистина е тешко да се замисли софтверски производ без баремНекоиниво на тестирање. Тестовите на единиците се стандарден начин да се фатат грешките во мали парчиња код, додека тестовите од крај до крај ги покриваат целите работни процеси на апликациите. GameDev не е исклучок, но доаѓа со свои уникатни предизвици кои не секогаш се усогласуваат со начинот на кој обично ги тестираме веб апликациите.


Здраво, јас сум Андреј Рахубов, водечки софтверски инженер на MY.GAMES! Во оваа статија, ќе ги споделам деталите за позадината што се користи во нашето студио, и нашиот пристап за тестирање на мета-игра во War Robots: Frontiers.

Забелешка за нашата архитектура

Без да одиме во какви било непотребни детали, нашиот backend се состои од сет на класични услуги и клаустер на меѓусебно поврзани јазли каде што живеат актерите.

backend architecture overview

И покрај некои други недостатоци кои може да постојат, овој пристап се покажа како прилично добар во решавањето на еден од најтешките проблеми во развојот на играта: брзината на итерација.ТоаПа, истото го имаме и за GDD (документ за дизајн на игри).


Актерите помагаат на начин што тие се многу евтини за имплементација и модификација во раните фази на дизајнот на карактеристиките и тие се прилично едноставни за рефактор како посебна услуга ако потребата се појави подоцна.


Очигледно, ова го комплицира тестирањето на оптоварувањето и интеграцијата, бидејќи веќе немате јасна поделба на услугите со мали, добро дефинирани АПИ.

Тестирање

Како што може да претпоставите, тестирањето на единиците за актерите и услугите (и тестирањето на оптоварувањето / интеграцијата за услугите) не се разликува од она што ќе го најдете во кој било друг тип на софтвер.

  • Тестирање на оптоварување и интеграција актери
  • End-to-end и load backend тестирање


Ние нема да ги разгледаме тестирањето на актерите во детали тука, бидејќи тоа не е специфично за GameDev, туку се однесува на самиот модел на актер. Вообичаениот пристап е да се има посебен кластер со еден јазол кој е погоден за поврзување во меморијата во рамките на еден тест и кој исто така е способен да ги потисне сите излезни барања и да ги повика актерите API.


Тоа беше кажано, работите почнуваат да станат поинтересни кога ќе дојдеме до полнење или тестирање од крај до крај – и тука започнува нашата приказна.


Значи, во нашиот случај, клиентската апликација е самата игра.Играта користи Unreal Engine, така што кодот е напишан во C++, а на серверот користиме C#. Играчите комуницираат со елементите на корисничкиот интерфејс во играта, произведувајќи барања до задниот крај (или барањата се прават индиректно).


Во овој момент, огромното мнозинство на рамки само престануваат да работат за нас, и било каков вид на селен-како комплети кои сметаат клиентски апликации прелистувач се надвор од опсег.


Следниот проблем е дека користиме прилагоден комуникациски протокол помеѓу клиентот и задната страна. Овој дел навистина заслужува посебен напис, но јас ќе ги истакнам клучните концепти:

  • Комуникацијата се случува преку WebSocket конекција
  • Тоа е шема-прво; ние користиме Protobuf за да ја дефинираме структурата на пораките и услугите
  • WebSocket пораки се Protobuf пораки упатени во контејнер со метаподатоци кои имитираат некои потребни gRPC-релевантни информации, како што се URL и наслови

Значи, секоја алатка која не дозволува дефинирање на прилагодени протоколи не е погодна за задачата.

Алатката за полнење да ги тестира сите

Во исто време, сакавме да имаме една алатка која може да напише и REST / gRPC тестови за оптоварување и тестови од крај до крај со нашиот прилагоден протокол.По разгледувањето на сите барања кои беа дискутирани во Тестирање и некои прелиминарни дискусии, ние бевме оставени со овие кандидати:


К6ЛокалноНБМБ

Секој од нив имал свои предности и недостатоци, но имало неколку работи кои ниту еден од нив не можел да ги реши без огромни (понекогаш внатрешни) промени:


  • Прво, постоела потреба поврзана со комуникацијата меѓу ботови за синхронизација, како што беше дискутирано порано.
  • Второ, клиентите се прилично реактивни и нивната состојба може да се промени без експлицитна акција од тест сценариото; ова доведува до дополнителна синхронизација и потребата да скокате низ многу скокови во вашиот код.
  • Последно, но не и најмалку важно, тие алатки беа премногу тесно фокусирани на тестирање на перформансите, нудејќи бројни функции за постепено зголемување на товарот или одредување на временските интервали, но немаа способност да креираат сложени разгранети сценарија на едноставен начин.


Време е да се прифати фактот дека навистина ни треба специјализирана алатка.Сваре роден

Свар

На високо ниво, задачата на Swarm е да започне многу актери кои ќе произведуваат оптоварување на серверот. Овие актери се нарекуваат ботови, и тие можат да симулираат однесување на клиентот или одделно однесување на серверот. Swarm се состои од еден или повеќе агенти кои ги хостираат овие ботови и комуницираат едни со други.


Повеќе формално, тука е листа на барања за алатката:


  1. Реакцијата на државните ажурирања треба да биде лесна
  2. Конкуренцијата не треба да биде проблем
  3. Кодот треба да биде автоматски инструментализиран
  4. Комуникацијата бот-то-бот треба да биде лесна
  5. Повеќе инстанции треба да бидат спојувачки за да се создаде доволно оптоварување
  6. Алатката треба да биде лесна и способна да создаде пристоен стрес на самата позадина


Како бонус, додадов и некои дополнителни точки:

  1. Можни се и сценарија за перформанси и сценарија за тестирање од крај до крај.
  2. Алатот треба да биде транспорт-агностичен; ние треба да можеме да го поврземе ова со кој било друг можен транспорт ако е потребно.
  3. Алатката има императивен стил на код, бидејќи јас лично имам цврсто мислење дека декларативниот стил не е погоден за сложени сценарија со условни одлуки.
  4. Ботовите треба да можат да постојат одделно од алатката за тестирање, што значи дека не треба да има тврди зависности


Нашата цел

Замислете го кодот што би сакале да го напишеме; ќе го разгледаме ботот како кукла, и тој не може да ги направи работите сам, тој може само да ги задржи инваријантите, додека Сценарио е куклата која ги повлекува жиците.


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Од на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());


Каде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 повици), и имаме сите инструменти потребни за да го набљудуваме она што се случува од перспектива на клиентот.


Grafana dashboard panels showing failed test


Обично го прекинуваме тестот целосно по одреден број грешки, поради што API повиците завршуваат ненадејно.


Се разбира, трагите се неопходни за добра набљудуваност. За тестирањето на Swarm, трагите на клиентите се поврзани со трагите на задната страна на едно дрво.connectionIdD/botIdТоа помага во дебито.


Distributed tracing connecting Swarm scenario with backend


Забелешка: DevServer е погодна монолитна зграда на сите-во-еден-бак-енд услуги кои ќе бидат лансирани на компјутерите на програмерите, и ова е специјално дизајниран пример за да се намали количината на детали на екранот.


Swarm ботови пишуваат друг вид на трага: тие ги имитираат трагите што се користат во анализата на перформансите.Со помош на постоечките алатки, тие траги може да се гледаат и анализираат со користење на вградени можности како PerfettoSQL.


Adapting perf logs to show bot execution flow

Со што завршивме на крајот

Сега имаме SDK на кој се изградени и ботовите на GameClient и Dedicated Server. Овие ботови сега ја поддржуваат поголемиот дел од логиката на играта - тие вклучуваат подсистеми на пријатели, групи за поддршка и мешање.


Способноста да се напише едноставен код овозможи да се создадат нови модули и едноставни сценарија за тестирање од страна на тимот на QA. Постои идеја за FlowGraph сценарија (визуелно програмирање), но ова останува само идеја во моментов.


Тестовите за перформанси се речиси континуирани - го кажувам речиси затоа што сè уште треба да ги започнете рачно.


Swarm не само што помага со тестови, туку обично се користи за репродукција и поправка на грешки во логиката на играта, особено кога ова е тешко да се направи рачно, како што е кога ви треба неколку игри клиенти извршување на посебен редослед на акции.


За да ги сумираме работите, ние сме исклучително задоволни од нашиот резултат и дефинитивно не жалиме за напорите потрошени за развивање на нашиот сопствен систем за тестирање.



Се надевам дека ви се допадна оваа статија, беше предизвик да ви кажам за сите нијанси на развојот на Swarm без да го направам овој текст прекумерно надуен.

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks