Kaip mes sukūrėme „Swarm“, mūsų vidinę apkrovos testavimo sistemą, kad galėtume spręsti iššūkius, susijusius su „end-to-end“ testavimu, individualizuotais protokolais ir didelio masto botų modeliavimu „gamedev“.
Kaip mes sukūrėme „Swarm“, mūsų vidinę apkrovos testavimo sistemą, kad galėtume spręsti iššūkius, susijusius su „end-to-end“ testavimu, individualizuotais protokolais ir didelio masto botų modeliavimu „gamedev“.
Šiais laikais tikrai sunku įsivaizduoti programinės įrangos produktą be bentKai kurieBandymų lygis. Vieneto bandymai yra standartinis būdas sugauti klaidas mažose kodo dalyse, o galiniai bandymai apima visą programų darbo eigą. „GameDev“ nėra išimtis, tačiau ji turi savo unikalius iššūkius, kurie ne visada atitinka tai, kaip mes paprastai bandome žiniatinklio programas.
Sveiki, aš esu Andrey Rakhubov, pagrindinis programinės įrangos inžinierius MY.GAMES! Šiame pranešime aš pasidalinsiu detalėmis apie mūsų studijoje naudojamą "backend" ir mūsų požiūrį į karo robotų: sienų meta-žaidimo testavimą.
Pastaba apie mūsų architektūrą
Nesikreipiant į jokias nereikalingas detales, mūsų "backend" susideda iš klasikinių paslaugų ir tarpusavyje susijusių mazgų, kuriuose gyvena veikėjai, grupės.
Nepaisant kai kurių kitų trūkumų, kurie gali egzistuoti, šis metodas pasirodė esąs gana geras sprendžiant vieną iš labiausiai erzinančių žaidimų kūrimo problemų: iteracijos greitį.TaiNa, mes turime tą patį pasakymą apie GDD (žaidimo dizaino dokumentą).
Aktoriai padeda tokiu būdu, kad jie yra labai pigūs įgyvendinti ir modifikuoti ankstyvose funkcijų dizaino stadijose, ir jie yra gana paprasti refaktoriui kaip atskirai paslaugai, jei vėliau atsiranda poreikis.
Akivaizdu, kad tai apsunkina apkrovos ir integracijos bandymus, nes nebėra aiškaus paslaugų atskyrimo su mažais, gerai apibrėžtais API.
Išbandymas
Kaip galite atspėti, vieneto testavimas veikėjams ir paslaugoms (ir apkrovos / integracijos testavimas paslaugoms) nesiskiria nuo to, ką rasite bet kokio kito tipo programinėje įrangoje.
- Apkrovos ir integracijos bandymų veikėjai
- End-to-end ir load backend testavimas
Mes nežiūrėsime išsamiai išbandyti aktorių čia, nes tai nėra specifinė GameDev, bet susijusi su pačiu aktorių modeliu. Įprastas požiūris yra turėti specialų vieno mazgo klasterį, kuris yra tinkamas, kad būtų prijungtas prie atminties viename teste ir kuris taip pat gali išstumti visus išeinančius prašymus ir kreiptis į aktorių API.
Tai sakė, kad dalykai pradeda tapti įdomesni, kai mes ateiname į apkrovą ar testavimą nuo galo iki galo - ir čia prasideda mūsų istorija.
Taigi, mūsų atveju kliento programa yra pats žaidimas.Žaidimas naudoja Unreal Engine, todėl kodas parašytas C++, o serveryje mes naudojame C#.Žaidėjai sąveikauja su žaidimo UI elementais, pateikdami užklausas į galinį galinį (arba užklausos pateikiamos netiesiogiai).
Šiuo metu didžioji dauguma sistemų tiesiog nustoja dirbti mums, ir bet kokie seleno tipo rinkiniai, kurie laiko kliento programas naršykle, yra ne taikymo srityje.
Kitas klausimas yra tai, kad mes naudojame individualizuotą komunikacijos protokolą tarp kliento ir atgalinės dalies.Ši dalis tikrai nusipelno atskiro straipsnio, bet aš išryškinsiu pagrindines sąvokas:
- Bendravimas vyksta per WebSocket ryšį
- Tai schema-pirmas; mes naudojame Protobuf apibrėžti pranešimų ir paslaugų struktūrą
- "WebSocket" pranešimai yra "Protobuf" pranešimai, supakuoti į konteinerį su metaduomenimis, kurie imituoja tam tikrą būtiną gRPC susijusią informaciją, pvz., URL ir antraštes.
Taigi, bet koks įrankis, kuris neleidžia apibrėžti pritaikytų protokolų, netinka užduočiai.
Įkrovimo įrankis išbandyti juos visus
Tuo pačiu metu mes norėjome turėti vieną įrankį, kuris galėtų parašyti tiek REST / gRPC apkrovos testus, tiek galinius testus su mūsų individualizuotu protokolu.
Kiekvienas iš jų turėjo savo privalumus ir trūkumus, tačiau buvo keletas dalykų, kurių nė vienas iš jų negalėjo išspręsti be didžiulių (kartais vidinių) pokyčių:
- Pirma, buvo poreikis, susijęs su bendravimu tarp robotų sinchronizavimo tikslais, kaip aptarta anksčiau.
- Antra, klientai yra gana reaktyvūs ir jų būklė gali pasikeisti be aiškių veiksmų iš bandymo scenarijaus; tai lemia papildomą sinchronizavimą ir poreikį peršokti per daugelį savo kodo.
- Paskutinis, bet ne mažiau svarbus, tie įrankiai buvo pernelyg siaurai orientuoti į našumo testavimą, siūlydami daugybę funkcijų palaipsniui didinti apkrovą ar nurodyti laiko intervalus, tačiau trūko galimybės sukurti sudėtingus šakotus scenarijus paprastu būdu.
Atėjo laikas priimti faktą, kad mums tikrai reikia specializuoto įrankio.SwarmBuvo gimęs
Swarm
Aukšto lygio, "Swarm" užduotis yra pradėti daug veikėjų, kurie sukurs apkrovą serveryje. Šie veikėjai vadinami robotais, ir jie gali imituoti kliento elgesį arba skirto serverio elgesį. "Swarm" susideda iš vieno ar kelių agentų, kurie priima šiuos robotus ir bendrauja tarpusavyje.
Formaliau, čia yra įrankio reikalavimų sąrašas:
- Reaguoti į valstybės atnaujinimus turėtų būti lengva
- Konkurencija neturėtų būti problema
- Kodas turėtų būti automatiškai instrumentuojamas
- Bot-to-bot komunikacija turėtų būti lengva
- Keletas egzempliorių turėtų būti sujungti, kad būtų sukurta pakankamai apkrovos
- Įrankis turėtų būti lengvas ir sugebėti sukurti tinkamą stresą pačiai galinei daliai
Kaip premiją, aš taip pat pridėjau keletą papildomų taškų:
- Tiek našumo, tiek galutinio bandymo scenarijai turėtų būti įmanomi
- Įrankis turėtų būti transportavimo agnostika; prireikus turėtume galėti prijungti šį įrankį prie bet kurio kito galimo transporto.
- Įrankis pasižymi imperatyviu kodo stiliumi, nes aš asmeniškai turiu tvirtą nuomonę, kad deklaratyvus stilius netinka sudėtingiems scenarijams su sąlyginiais sprendimais
- Robotai turėtų egzistuoti atskirai nuo bandymo įrankio, o tai reiškia, kad neturėtų būti sunkių priklausomybių.
Mūsų tikslas
Įsivaizduokime kodą, kurį norėtume parašyti; mes laikysime botą kaip lėlę, ir jis negali daryti dalykų savarankiškai, jis gali išlaikyti tik invariantus, o scenarijus yra lėlės, traukiančios virves.
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));
}
}
Steaminė
"Backend" siunčia klientui daug atnaujinimų, kurie gali įvykti retkarčiais; aukščiau pateiktame pavyzdyje vienas toks įvykis yraGroupInviteAddedEvent
Botas turėtų sugebėti reaguoti į šiuos įvykius viduje ir suteikti galimybę stebėti juos iš išorinio kodo.
public Task SubscribeToGroupState()
{
Subscription.Listen(OnGroupStateUpdate);
Subscription.Start(api.GroupServiceClient.SubscribeToGroupStateUpdates, new SubscribeToGroupStateUpdatesRequest());
return Task.CompletedTask;
}
Nors kodas yra gana paprastas (ir kaip jūs galite atspėtiOnGroupStateUpdate
Viskas, kas vyksta, yra tik ilgas perjungimo atvejis), čia vyksta daug dalykų.
StreamSubscription
Jis pats aIObservable<ObservedEvent<TStreamMessage>>
ir jis suteikia naudingų pratęsimų, turi priklausomą gyvavimo ciklą ir yra padengtas metrika.
Kitas privalumas: nereikia aiškaus sinchronizavimo, kad būtų galima perskaityti ar modifikuoti būseną, nepriklausomai nuo to, kur įgyvendinamas tvarkytuvas.
case GroupUpdateEventType.GroupInviteAddedEvent:
State.IncomingInvites.Add(ev.GroupInviteAddedEvent.GroupId, ev.GroupInviteAddedEvent.Invite);
break;
Konkurencija neturėtų būti problema
Idėja yra paprasta: niekada nevykdyti kodo, kuris vienu metu priklauso scenarijui ar botui, gimusiam tame scenarijuje, ir toliau, vidinis kodas botui / moduliui neturėtų būti vykdomas vienu metu su kitomis boto dalimis.
Tai panašu į skaitymo / rašymo užraktą, kuriame skaitomas scenarijaus kodas (pasidalinta prieiga) ir bot kodas yra rašomas (išskirtinė prieiga).
Užduočių tvarkaraščiai ir sinchronizavimo kontekstas
Šioje antraštėje minėti du mechanizmai dabar yra labai galingos asynchrono kodo dalys C #. Jei kada nors sukūrėte WPF programą, tikriausiai žinote, kad tai yra asynchrono kodas.DispatcherSynchronizationContext
tai yra atsakinga už elegantišką jūsų async skambučių grąžinimą į UI siūlą.
Mūsų scenarijuje mums nerūpi siūlo afinitetas, o vietoj to labiau rūpinamės užduočių vykdymo tvarka scenarijuje.
Prieš skubėdami rašyti žemo lygio kodą, pažvelkime į vieną ne plačiai žinomą klasę, vadinamąConcurrentExclusiveSchedulerPair
Iš The
Teikia užduočių tvarkaraščius, kurie koordinuoja užduočių vykdymą, tuo pačiu užtikrinant, kad vienu metu vykdomos užduotys gali būti vykdomos vienu metu, o išskirtinės užduotys niekada neveikia.
Teikia užduočių tvarkaraščius, kurie koordinuoja užduočių vykdymą, tuo pačiu užtikrinant, kad vienu metu vykdomos užduotys gali būti vykdomos vienu metu, o išskirtinės užduotys niekada neveikia.
Tai atrodo kaip būtent tai, ko mes norime!Dabar turime užtikrinti, kad visas scenarijaus kodas būtų vykdomasConcurrentScheduler
kai roboto kodas vykdomasExclusiveScheduler
.
Kaip nustatyti užduoties tvarkaraštį? Viena galimybė yra aiškiai perduoti jį kaip parametrą, kuris atliekamas, kai paleidžiamas scenarijus:
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();
}
TųRunScenarioInstance
Metodas, savo ruožtu, reikalaujaSetUp
irRun
metodus scenarijuje, todėl jie vykdomi vienu metu tvarkaraštyje (ScenarioScheduler
Tai konkuruojanti dalisConcurrentExclusiveSchedulerPair
) ir
Dabar, kai mes esame scenarijaus kodo viduje ir mes tai darome...
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();
}
Asynchrono būsenos mašina atlieka savo darbą už mus, išlaikydama mūsų užduočių tvarkaraštį.
DabarSendGroupInvite
taip pat veikia kartu su tvarkaraščiu, todėl mes taip pat darome tą patį triuką:
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" abstrakcija supa tvarkaraštį taip pat, kaip ir anksčiau, skambindamaTask.Factory.StartNew
Su tinkamu tvarkaraščiu.
Kodų kartos
Gerai, dabar mes turime rankiniu būdu apvynioti viską vidujeDo
ir nors tai išsprendžia problemą, ji yra linkusi į klaidą, lengva pamiršti, ir apskritai kalbant, gerai, atrodo keista.
Pažvelkime į šią kodo dalį dar kartą:
await leader.Group.SendGroupInvite(follower.PlayerId);
Štai mūsų botas turiGroup
Nekilnojamojo turto .Group
yra modulis, kuris egzistuoja tik norint suskirstyti kodą į atskiras klases ir išvengtiBot
Iš klasės.
public class BotClient : BotClientBase
{
public GroupBotModule Group { get; }
/* ... */
}
Tarkime, kad modulis turėtų turėti sąsają:
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;
}
|
Nėra daugiau apvyniojimo ir bjauraus kodo, tik paprastas reikalavimas (kuris yra gana dažnas kūrėjams) sukurti sąsają kiekvienam moduliui.
Bet kur yra tvarkaraštis? Magija vyksta šaltinio generatoriuje, kuris sukuria kiekvienos IBotModule sąsajos proxy klasę ir supa kiekvieną funkciją įRuntime.Do
Skambinkite :
public const string MethodProxy =
@"
public async $return_type$ $method_name$($method_params$)
{
$return$await runtime.Do(() => implementation.$method_name$($method_args$));
}
";
Instrumentacijos
Kitas žingsnis yra įrankių kodas, siekiant surinkti rodiklius ir pėdsakus. Pirma, kiekvienas API skambutis turėtų būti stebimas. Ši dalis yra labai paprasta, nes mūsų transportas veiksmingai apsimeta gRPC kanalu, todėl mes naudojame tik tinkamai parašytą interceptorių...
callInvoker = channel
.Intercept(new PerformanceInterceptor());
... kurCallInvoker
yra kliento pusės RPC invokacijos gRPC abstrakcija.
Tada būtų puiku apimti kai kurias kodo dalis, kad būtų galima išmatuoti našumą.IInstrumentationFactory
Naudojant šią sąsają:
public interface IInstrumentationFactory
{
public IInstrumentationScope CreateScope(
string? name = null,
IInstrumentationContext? actor = null,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
}
Dabar galite supakuoti dalis, kurios jus domina:
public Task AcceptGroupInvite(GroupInvideId inviteId)
{
using (instrumentationFactory.CreateScope())
{
var result = await api.GroupServiceClient.AcceptGroupInviteAsync(request);
}
}
Nors vis dar galite naudoti šią funkciją, kad sukurtumėte pogrupius, kiekvienas modulio proxy metodas automatiškai apibrėžia taikymo sritį:
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$));
}
}
";
Prietaisų apimtis registruoja vykdymo laiką, išimtis, sukuria paskirstytas pėdsakus, gali rašyti debug logus ir daugelį kitų konfigūruojamų dalykų.
Bot-to-bot ryšys
Pavyzdys skyriuje „Mūsų tikslas“ iš tikrųjų nerodo jokio bendravimo.PlayerId
Nors šis scenarijų rašymo stilius dažnai yra paprastas ir paprastas, yra sudėtingesnių atvejų, kai šis metodas tiesiog neveikia.
Iš pradžių planavau įgyvendinti tam tikrą juodosios plokštės modelį su bendra raktinių reikšmių saugykla komunikacijai (pavyzdžiui, „Redis“), tačiau atlikus kai kuriuos koncepcijos scenarijų bandymus, paaiškėjo, kad darbo apimtis gali būti labai sumažinta iki dviejų paprastesnių koncepcijų: eilės ir atrankos.
Bilietai – bilietai
Realiame pasaulyje žaidėjai kažkaip bendrauja tarpusavyje – jie rašo tiesiogines žinutes arba naudoja balso pokalbį. Su robotais mums nereikia imituoti to granuliarumo, ir mums tiesiog reikia pranešti ketinimus.Taigi, vietoj „Hey Killer2000, pakviesk mane į savo grupę prašau“ mums reikia paprasto „Hey,KažkasPrašau, pakviesk mane į grupę.“ Štai kur bilietai ateina į žaidimą:
public interface ISwarmTickets
{
Task PlaceTicket(SwarmTicketBase ticket);
Task<SwarmTicketBase?> TryGetTicket(Type ticketType, TimeSpan timeout);
}
Botas gali įdėti bilietą, o tada kitas botas gali gauti tą bilietą.ISwarmTickets
yra ne kas kita, kaip pranešimų brokeris su eilės perticketType
(Iš tikrųjų tai yraŠiek tiekdaugiau nei tai, nes yra papildomų galimybių užkirsti kelią robotams gauti savo bilietus, taip pat kitus nedidelius pakeitimus).
Su šia sąsaja galiausiai galime suskirstyti pavyzdinį scenarijų į du nepriklausomus scenarijus. (čia visas papildomas kodas pašalinamas iliustruojant pagrindinę idėją):
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);
}
Selektorius
Žinoma, jie gali būti suskirstyti į du skirtingus scenarijus ir paleisti lygiagrečiai. Kartais tai yra geriausias būdas tai padaryti, kitais atvejais jums gali prireikti dinamiškos grupės dydžio konfigūracijos (keletas pasekėjų vienam lyderiui) arba bet kokios kitos situacijos, kad paskirstytumėte / pasirinktumėte skirtingus duomenis / vaidmenis.
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);
}
Štai čia,RoundRobinRole
yra tik išgalvotas apvyniojimas aplink bendrą skaitiklį ir modulo operaciją, kad pasirinktumėte tinkamą elementą iš sąrašo.
Swarms grupė
Dabar, kai visi ryšiai yra paslėpti už eilių ir bendrų skaitiklių, tampa trivialu įvesti orchestratoriaus mazgą arba naudoti kai kurias esamas MQ ir KV saugyklas.
Įdomus faktas: mes niekada nesibaigė įgyvendinti šią funkciją. Kai QA gavo savo rankas ant vieno mazgo įgyvendinimo SwarmAgent
, jie iš karto pradėjo tiesiog dislokuoti kelis nepriklausomų agentų atvejus, kad padidintų apkrovą.
Vienkartinės veiklos rezultatai
Ką apie našumą? Kiek prarandama visose tose pakuotėse ir netiesioginėse sinchronizacijose? aš neperkrausiu jus su visais atliktais įvairiais testais, tik du svarbiausi, kurie įrodė, kad sistema gali sukurti pakankamai gerą apkrovą.
Benchmark nustatymas:
„BenchmarkDotNet v0.14.0“, „Windows 10“ (10.0.19045.4651/22H2/2022Update)
„Intel Core i7-10875H“ 2.30 GHz procesorius, 1 procesorius, 16 loginių ir 8 fizinių branduolių
.NET SDK 9.0.203 išmanusis ryšys
[Host] : .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);
}
}
}
Pirmiausia pažvelkime į beveik gryną tvarkaraščio viršūnę prieš paprastą užduočių gimdymą.
In this case, both load methods are as follows:
private ValueTask CounterLoadMethod(int i)
{
Interlocked.Increment(ref StaticControl.Counter);
return ValueTask.CompletedTask;
}
Iterijų rezultatai = 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 |
Paprastas
10
3 565Mėnulis
0.0450us
0 0 0 421
0.3433
-
-
2.83 kB
Paprastas
100
30281Mėnulis
0,2720 Lt
0,2544 Lt
3.1128
0.061
-
25,67 kB
Paprastas
1000
250 693
1626 m.
2.4037Mėnulis
30.2734
5.8594
-
250.67 kB
tvarkaraštis
10
40629Mėnulis
07842Mėnulis
0 805
8.1787
0.1221
-
615 kB
tvarkaraštis
100
325.386Tėvynė
2.3414Mėnulis
1901 m.
81.0547
14.6484
-
6299 kB
tvarkaraštis
1000
4 685 812 Lt
247917Užsakymas
219772Užsakymas
812.5
375
-
6617.59 kB
Atrodo blogai? Ne tiksliai. Prieš iš tikrųjų atlikdamas testą, aš tikėjausi daug blogesnių rezultatų, tačiau rezultatai mane tikrai nustebino. Tik pagalvokite apie tai, kiek įvyko po dangteliu ir naudą, kurią gavome tik už ~4us vienam lygiagrečiam atvejui.
Koks galėtų būti realus blogiausio atvejo bandymo scenarijus? Na, o apie funkciją, kuri daro nieko daugiau nei API skambutį? Viena vertus, beveik kiekvienas botas tai daro, bet kita vertus, jei nėra nieko daugiau nei tik API skambučiai, tada visos sinchronizavimo pastangos yra švaistomos, tiesa?
Įkrovimo metodas būtų tik skambintiPingAsync
Dėl paprastumo RPC klientai saugomi už robotų ribų.
private async ValueTask PingLoadMethod(int i)
{
await clients[i].PingAsync(new PingRequest());
}
Štai rezultatai, dar 10 iteracijų (grpc serveris yra vietiniame tinkle):
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 |
Paprastas
100
44,45 ms
1 804 psl
2 148 psl.
600
200
6.4 MB
Paprastas
1000
6956 m2
15 592 psl.
45 730 ms
9000
7000
67,77 MB
tvarkaraštis
100
95.48 ms peržiūra
1547 Mėnulis
1 292 psl.
833.3333
333.3333
685 MB
tvarkaraštis
1000
625,52 psl
14 697 psl.
42 405 ms
8000
7000
68,57 MB
Kaip ir tikėtasi, veiklos poveikis realiems darbo scenarijams yra nereikšmingas.
Analizė
Žinoma, yra "backend" žurnalų, metrų ir pėdsakų, kurie suteikia gerą vaizdą apie tai, kas vyksta įkėlimo metu. bet "Swarm" eina žingsnį toliau, rašydamas savo duomenis - kartais susijusius su "backend" - papildydamas jį.
Testas nepavyko dėl daugelio SLA pažeidimų (žr. API skambučių laiko tarpsnius), ir mes turime visą reikalingą įrangą, kad stebėtume, kas vyksta iš kliento perspektyvos.
Paprastai mes visiškai sustabdome testą po tam tikro skaičiaus klaidų, todėl API skambučių linijos staiga baigiasi.
Žinoma, pėdsakai yra būtini geram stebėjimui. „Swarm“ bandymų metu kliento pėdsakai yra susieti su atsarginėmis pėdsakomis viename medyje.connectionIdD/botId
Tai padeda atlikti debugging.
Pastaba: "DevServer" yra patogus monolitinis "viskas viename" atsarginių paslaugų kūrimas, kuris bus pradėtas kūrėjų kompiuteriuose, ir tai yra specialiai sukurtas pavyzdys, siekiant sumažinti detalių kiekį ekrane.
Swarm robotai rašo kitokį pėdsaką: jie imituoja pėdsakus, naudojamus našumo analizėje.Su esamų įrankių pagalba, tuos pėdsakus galima peržiūrėti ir analizuoti naudojant įmontuotas galimybes, tokias kaip PerfettoSQL.
Ką galų gale padarėme
Dabar turime SDK, kuriame yra pastatyti tiek GameClient, tiek Dedicated Server robotai.Šie robotai dabar palaiko daugumą žaidimo logikos – jie apima draugų posistemius, palaikymo grupes ir matchmaking.Dedicated robotai gali imituoti rungtynes, kuriose žaidėjai uždirba apdovanojimus, pažangos užduotis ir dar daugiau.
Sugebėjimas rašyti paprastą kodą leido QA komandai sukurti naujus modulius ir paprastus bandymų scenarijus.
Veiksmingumo testai yra beveik nuolatiniai - sakau beveik todėl, kad vis dar reikia juos paleisti rankiniu būdu.
Swarm padeda ne tik su testais, bet paprastai naudojamas atkurti ir išspręsti klaidas žaidimo logikoje, ypač kai tai sunku padaryti rankiniu būdu, pavyzdžiui, kai jums reikia kelių žaidimo klientų, atliekančių specialią veiksmų seką.
Apibendrinant, mes esame labai patenkinti mūsų rezultatu ir tikrai nesigailiu dėl pastangų, padarytų kuriant savo bandymų sistemą.
Tikiuosi, kad jums patiko šis straipsnis, buvo sudėtinga jums papasakoti apie visus Swarm vystymosi niuansus, o ne padaryti šį tekstą pernelyg išsipūsti.