Paano natuklasan namin ang Swarm, ang aming in-house load-testing framework, upang matugunan ang mga problema ng end-to-end testing, custom protocols, at malaking-scale bot simulations sa gamedev.
Paano natuklasan namin ang Swarm, ang aming in-house load-testing framework, upang matugunan ang mga problema ng end-to-end testing, custom protocols, at malaking-scale bot simulations sa gamedev.
Sa mga araw na ito, ito ay napaka-imagine ng isang software na produkto na walangilanglevel ng pagsubok. Unit na pagsubok ay isang standard na paraan upang i-catch bugs sa maliit na bahagi ng code, habang end-to-end na pagsubok ay naglalaman ng buong application workflows. GameDev ay hindi isang pagkakaiba, ngunit ito ay kasama ang kanyang mga katangian na natutunan na hindi kailanman matatagpuan sa kung paano kami normal na pagsubok ng mga web application. Ang artikulong ito ay tungkol sa path na ginamit namin upang matugunan ang mga natutunan na pangangailangan na may aming mga internal testing tool.
Hi, ako ay Andrey Rakhubov, isang Lead Software Engineer sa MY.GAMES! Sa post na ito, i-share ko ang mga detalye tungkol sa backend na ginagamit sa aming studio, at ang aming paraan ng pag-load testing ng meta-game sa War Robots: Frontiers.
Isang Note sa aming Arkitektura
Walang pumunta sa anumang hindi kinakailangang detalye, ang aming backend ay binubuo ng isang set ng mga klasikong mga serbisyo at isang cluster ng interconnected nodes kung saan ang mga aktor ay matatagpuan.
Sa gitna ng Pagpatay ng sarili niyang Olympic maskot , Ang Dysfunctional clean up Ng maruming tubig nito, ang Pag-shutdown ng doping lab nito , Ang Deklarasyon ng isang emergency na pinansiyal , Ang Pagkakaroon ng virus Zika , At Iba't ibang mga kalamidad , Ang Olympic ambitions ng Rio ay isang kalamidad.angWell, kami ay may parehong sinabi tungkol sa GDD (game design document).
Ang mga manlalaro ay tumutulong sa isang paraan na sila ay napaka-expensive upang i-implementate at i-modify sa mga unang bahagi ng disenyo ng feature at sila ay napaka-simple upang refactor bilang isang partikular na serbisyo kung ang kailangan ay lumabas pagkatapos.
Sa katunayan, ito ay nangangailangan ng load at integration testing, dahil hindi ka na magkaroon ng isang malinaw na pagkilala ng mga serbisyo na may maliit na, malinaw na APIs. Sa halip, mayroong isang cluster ng heterogeneous nodes, kung saan mayroong mas mababang malinaw o mababago na koneksyon sa pagitan ng mga aktor.
pagsubok
Tulad nila Tirso at Lyn ang mga anak nila, very friendly at magalang.
- Mga Actors sa Load at Integration Test
- End-to-end at load backend testing
Hindi namin makikita ang pagsubok ng mga aktor sa detalye dito dahil ito ay hindi spesifiko sa GameDev, ngunit may kaugnayan sa modelo ng mga aktor mismo. Ang karaniwang paraan ay magkaroon ng isang espesyal na single-node cluster na magagamit upang i-wire up sa memory sa loob ng isang single test at na din ay matatagpuan ang lahat ng outgoing requests at i-invocate ang mga actors API.
Dahil dito, ang mga bagay ay nagsisimula na maging mas interesado kapag kami ay dumating sa load o end-to-end testing - at ito ay kung saan nagsisimula ang aming kasaysayan.
Kaya, sa aming kaso, ang application client ay ang laro mismo. Ang laro ay gumagamit ng Unreal Engine, kaya ang code ay itinuturing sa C++, at sa server na ginagamit namin ang C#. Ang mga manlalaro ay makipag-ugnayan sa mga elemento ng UI sa laro, lumikha ng mga pangangailangan sa backend (o ang mga pangangailangan ay ginawa indirectly).
Sa oras na ito, ang karamihan ng mga frameworks lamang ay hindi gumagana para sa amin, at ang anumang uri ng selenium-like kits na tinanggap ng mga application client isang browser ay out of scope.
Ang susunod na problema ay na ginagamit namin ang isang custom na protocol ng komunikasyon sa pagitan ng client at backend. Ang bahagi na ito ay ganap na meron ng isang partikular na artikulo sa lahat, ngunit magpapatuloy ko ang mga pangunahing konsepto:
- Ang komunikasyon ay ginawa sa pamamagitan ng isang WebSocket connection
- Ito ay schema-first; ginagamit namin ang Protobuf upang i-defined ang mensahe at serbisyo structure
- Ang mga mensahe ng WebSocket ay mga mensahe ng Protobuf na inilapat sa isang container na may mga metadata na inilarawan ng ilang mga kinakailangan gRPC-related na impormasyon, tulad ng URL at headers.
Kaya, ang anumang tool na hindi nagpapahintulot sa pag-defining ng mga custom protocol ay hindi matatagpuan para sa gawain.
Ang load tool upang i-test ang lahat
Samakatuwid, gusto namin ang isang single tool na maaaring magsulat ang parehong REST / gRPC load tests at end-to-end tests gamit ang aming custom protocol. Pagkatapos ng pag-aaral ng lahat ng mga kinakailangan na tinutukoy sa Testing at ilang preliminary discussions, kami ay ibinigay sa mga kandidato na ito:
Lahat ng mga ito ay may kanilang mga pros at cons, ngunit mayroong ilang mga bagay na walang mga ito ay maaaring solusyon nang walang malaking (sometimes internal) mga pagbabago:
- Sa unang, may isang pangangailangan na may kaugnayan sa inter-bot komunikasyon para sa mga pangangailangan ng synchronization, tulad ng tinutukoy na nakaraang.
- Ang ikalawang, ang mga kliyente ay napaka-reactive at ang kanilang estado ay maaaring mababago nang walang eksplicit na aksyon mula sa test scenario; ito ay nagdadala sa karagdagang pag-synchronization at ang pangangailangan upang pumunta sa pamamagitan ng maraming mga hoops sa iyong code.
- Last but not least, ang mga tool na ito ay napaka-focused sa pag-test ng pagganap, na nag-aalok ng maraming mga tampok para sa gradually increasing load o pag-specify ang mga interval ng oras, ngunit hindi na may kakayahan na lumikha ng mga kompleksong mga scenarios sa isang simpleng paraan.
Ito ay ang oras upang makakakuha ng ang katotohanan na kailangan namin ng isang specialized tool. Ito ay kung paano ang aming tool -ang swarmNagsimula na siya.
ang swarm
Sa isang mataas na antas, ang trabaho ng Swarm ay upang magsisimula ng maraming mga aktor na makakuha ng load sa server. Ang mga aktor na ito ay tinatawag na bots, at sila ay maaaring simulate ang behavior ng client o dedicated server.
Sa mas formal, dito ay isang listahan ng mga kinakailangan para sa tool:
- Ang pag-react sa state updates ay maaaring maging madaling
- Ang competition ay hindi isang problema
- Ang code ay automatically instrumentable
- Ang bot-to-bot na komunikasyon ay dapat maging madaling
- Maraming mga instance ay dapat na sumali upang lumikha ng sapat na load
- Ang tool ay dapat maging madaling at maaaring lumikha ng mas mahusay na stress sa backend na ito.
Bilang isang bonus, inilagay ko rin ang ilang mga extra points:
- Ang parehong performance at end-to-end test scenarios ay dapat
- Ang tool ay dapat na transport-agnostic; dapat nating i-connect ito sa anumang iba pang posible na transport kung kinakailangan
- Ang tool ay naglalaman ng isang imperative code style, dahil ang aking personal na may karaniwang opinyon na ang isang declarative style ay hindi matatagpuan para sa mga kompleksong mga scenario na may conditional decisions
- Ang mga bots ay dapat magkaroon ng pagkakaiba-iba mula sa test tool, na kung saan ay hindi dapat magkaroon ng mga hard dependencies
ang target
I-imagine ang code na gusto natin upang i-writ; makikita natin ang bot bilang isang puppet, at ito ay hindi maaaring gawin ang mga bagay sa sarili, ito ay maaaring lamang matatagpuan ang invariants, habang ang Scenario ay ang pupeteer na nagtatrabaho ng mga string.
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));
}
}
ang steaming
Ang backend ay nagtatampok ng maraming mga update sa client na maaaring lumikha ng sporadically; sa halimbawa sa itaas, isa sa mga ito ayGroupInviteAddedEvent
Ang bot ay dapat upang pare-react sa mga ito events internally at magbibigay ng isang pagkakataon upang tingnan ang mga ito mula sa external code.
public Task SubscribeToGroupState()
{
Subscription.Listen(OnGroupStateUpdate);
Subscription.Start(api.GroupServiceClient.SubscribeToGroupStateUpdates, new SubscribeToGroupStateUpdatesRequest());
return Task.CompletedTask;
}
Kahit na ang code ay napaka-simple (at tulad ng ikaw ay maaaring ipinakitaOnGroupStateUpdate
ang handle ay lamang isang long switch case), maraming bagay na nangyayari dito.
StreamSubscription
Ito ay isangIObservable<ObservedEvent<TStreamMessage>>
at ito ay nagbibigay ng mga useful extensions, ay may isang dependent life cycle, at ay naka-covered ng mga metric.
Ang isa pang halaga: ito ay hindi kinakailangan ng eksplicit na synchronization upang basahin o i-modify ang estado, kahit na kung saan ang manipulator ay i-implementado.
case GroupUpdateEventType.GroupInviteAddedEvent:
State.IncomingInvites.Add(ev.GroupInviteAddedEvent.GroupId, ev.GroupInviteAddedEvent.Invite);
break;
Ang competition ay hindi isang problema
Sa itaas, ang code ay inilagay bilang isang linear, single-threaded code. Ang ideya ay simple: hindi kailanman i-execute code na simulan sa isang scenario o sa isang bot na ipinanganak sa scenario na ito, at higit pa, ang internal code para sa isang bot / module ay hindi kailangang i-execute simulan sa iba pang mga bahagi ng isang bot.
Ito ay tulad ng isang read/write lock, kung saan ang script code ay read (shared access) at bot code ay write (exclusive access).
Task Schedulers at ang konteksto ng synchronization
Ang dalawang mekanismo na tinatawag sa headline na ito ay ngayon ay napaka-powerful na bahagi ng async code sa C#. Kung ikaw ay nangangailangan ng isang WPF application, alam mo na ito ay isangDispatcherSynchronizationContext
na responsable para sa eleganteng ibalik ang iyong async calls sa thread ng UI.
Sa aming scenario, hindi namin interesado tungkol sa thread affinity, at sa halip ay interesado sa higit pa tungkol sa pagganap ng mga gawain ng mga gawain sa isang scenario.
Pero kapag nalaman niyang nakilala mo siya dahil binasa mo ang diary... get ready for the consequences.ConcurrentExclusiveSchedulerPair
mula sa the
Nagbibigay ng mga schedulers ng mga gawain na koordinate upang i-execute ang mga gawain habang nagtatagumpay na ang mga gawain ng parallel ay maaaring i-execute simultaneously at ang mga gawain ng exclusive ay hindi gawin.
Nagbibigay ng mga schedulers ng mga gawain na koordinate upang i-execute ang mga gawain habang nagtatagumpay na ang mga gawain ng parallel ay maaaring i-execute simultaneously at ang mga gawain ng exclusive ay hindi gawin.
Ngayon kailangan nating sigurado na ang lahat ng code sa ilalim ng scenario ay i-execute saConcurrentScheduler
habang ang code ng bot ay i-execute saExclusiveScheduler
.
Paano i-set ang isang scheduler para sa isang trabaho? Isang pagpipilian ay ang eksplicit na ipasok ito bilang isang parameter, na kung saan ay ginawa kapag inilunsad ang isang scenario:
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();
}
angRunScenarioInstance
Sa pamamagitan ng metriko, inihahanda angSetUp
at angRun
Pumili ng kategorya, gaya ng mga gasolinahan o grocery store, o maghanap ng partikular na uri ng lugar (ScenarioScheduler
Ito ay isang competitive part ngConcurrentExclusiveSchedulerPair
ang mga ito)
Ngayon, kapag kami ay sa ilalim ng script code at kami ay gawin ito ...
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();
}
... ang async state machine ay gawin ang kanyang trabaho para sa amin sa pamamagitan ng paghahatid ng isang schedule para sa aming mga trabaho.
NgayonSendGroupInvite
Ito ay gumagana sa parallel scheduler, kaya gumagawa namin ang parehong trick para sa kanya, too:
public Task<SendGroupInviteResponse> SendGroupInvite(PlayerId inviteePlayerId)
{
return Runtime.Do(async () =>
{
var result = await api.GroupServiceClient.SendGroupInviteAsync(/* ... */);
if (!result.HasError)
{
State.OutgoingInvites.Add(inviteePlayerId);
}
return result;
});
}
Runtime abstraction wraps schedule sa parehong paraan tulad ng bago, sa pamamagitan ng pag-callTask.Factory.StartNew
Dahil sa proper schedule.
Generasyon ng Code
Okay, ngayon kailangan natin upang manually i-wrap ang lahat sa loob ngDo
ang telepono; at habang ito solves ang problema, ito ay nagkakaroon ng error, madaling mag-atubiling, at sa pangkalahatan, okay, ito ay nakikita na masaya.
Tingnan natin ang bahagi ng code na ito:
await leader.Group.SendGroupInvite(follower.PlayerId);
Ito, ang aming bot ay may isangGroup
Mga PropertyGroup
ay isang module na ginagamit lamang upang i-divide ang code sa mga partikular na mga klase at maiwasan angBot
ang klase.
public class BotClient : BotClientBase
{
public GroupBotModule Group { get; }
/* ... */
}
Sabihin na ang isang module ay dapat magkaroon ng isang interface:
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;
}
|
At ngayon ito ay gumagana lamang! Walang higit pa na-wrapping at mabuti na code, lamang ang isang simpleng kinakailangan (na ito ay karamihan sa mga developer) upang lumikha ng isang interface para sa bawat module.
Ang magic ay nangyayari sa loob ng source generator na lumikha ng isang proxy class para sa bawat IBotModule interface at i-wrap ang bawat function saRuntime.Do
ang call:
public const string MethodProxy =
@"
public async $return_type$ $method_name$($method_params$)
{
$return$await runtime.Do(() => implementation.$method_name$($method_args$));
}
";
Mga instrumento
Ang susunod na step ay upang i-instrumento ang code upang makuha ang mga metrikong at mga trace. unang, ang bawat API call ay dapat na tinatanggap. Ang bahagi na ito ay napaka-simple, dahil ang aming transportasyon ay nangangahulugan na maging isang gRPC channel, kaya ginagamit namin lamang ang isang napaka-written interceptor...
callInvoker = channel
.Intercept(new PerformanceInterceptor());
Nasaan angCallInvoker
ay isang gRPC abstraction ng client-side RPC invocation.
Pagkatapos, ito ay mahusay na mag-scope ng ilang mga bahagi ng code upang matukoy ang pagganap. Dahil dito, ang bawat module injectsIInstrumentationFactory
sa pamamagitan ng mga interfaces:
public interface IInstrumentationFactory
{
public IInstrumentationScope CreateScope(
string? name = null,
IInstrumentationContext? actor = null,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
}
Ngayon maaari mong i-wrap ang mga bahagi na interesado sa iyo:
public Task AcceptGroupInvite(GroupInvideId inviteId)
{
using (instrumentationFactory.CreateScope())
{
var result = await api.GroupServiceClient.AcceptGroupInviteAsync(request);
}
}
Kahit na maaari mong gamitin ang feature na ito upang bumuo ng sub-scopes, ang bawat module proxy method ay automatically defines ng isang scope:
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$));
}
}
";
Ang scope ng instrumento ay nag-record execution time, exceptions, lumikha ng distributed traces, ito ay maaaring mag-script debug logs, at ito ay gumagana ng maraming iba pang configurable stuff.
Mga Komunikasyon Bot-to-Bot
Ang halimbawa sa seksyon ng "Our goal" ay hindi talagang nagpapakita ng anumang komunikasyon. Alam namin lamang ang bot ng mga followersPlayerId
Habang ang estilo na ito ng pag-script ng mga scenarios ay karaniwang madaling at simpleng, mayroong mas karaniwang mga kaso kung saan ang paraan na ito ay hindi gumagana.
Ipinanganak ko ang plano upang i-implementate ang ilang uri ng blackboard pattern na may isang shared key-value storage para sa komunikasyon ( tulad ng Redis), ngunit pagkatapos ng pag-execute ng ilang proof ng konsepto scenario tests, natagpuan na ang halaga ng trabaho ay maaaring mas mababang hanggang sa dalawang mas simpleng mga konsepto: isang tagumpay, at isang selector.
Mga Tiket - Tickets
Sa real na mundo, ang mga manlalaro ay makipag-ugnayan sa ibang tao sa anumang paraan - sila ay mag-sign ng mga direktang mensahe o gumagamit ng voice chat. Sa mga bot, hindi namin kailangan upang simulate na granularity, at kailangan natin lamang upang makipag-ugnayan intentions. Kaya, sa halip ng "Hey Killer2000, mag-invite ko sa iyong grupo please" kailangan namin ng isang simpleng "Hey,ang isang“Please, get me into the group please.” Ito ang lugar kung saan ang mga tiket ay dumating sa laro:
public interface ISwarmTickets
{
Task PlaceTicket(SwarmTicketBase ticket);
Task<SwarmTicketBase?> TryGetTicket(Type ticketType, TimeSpan timeout);
}
Ang bot ay maaaring ilagay ng isang tiket, at pagkatapos ay ang isa pang bot ay maaaring i-recover ang tiket.ISwarmTickets
ay walang bagay na higit sa isang mensahe broker na may isang queue perticketType
(Ang mga ito ayMalapit nahigit pa sa ito, dahil mayroong mga karagdagang mga pagpipilian upang maiwasan ang mga bots mula sa pagkuha ng kanilang mga tiket, pati na rin ang iba pang minor tweaks).
Sa pamamagitan ng interface na ito, maaari naming i-split ang sample scenario sa dalawang independiyenteng scenarios. (hindi, ang lahat ng mga extra code ay inilipat upang ilustro ang pangunahing ideya):
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);
}
ang selector
Kami ay may dalawang iba't ibang mga behavior, isa para sa lider, isa para sa mga tagapagsalita. Gayunpaman, ang mga ito ay maaaring i-split sa dalawang iba't ibang mga scenario at inilunsad sa parallel. Sa ilang mga oras, ito ay ang pinakamahusay na paraan upang gawin ito, sa iba't ibang mga oras maaari mong kailangan ng isang dinamic configuration ng grupo size (mga mga tagapagsalita sa bawat single leader) o anumang iba't ibang situasyon upang i-distribute / piliin ang iba't ibang data / mga papel.
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);
}
dito ay,RoundRobinRole
ay lamang isang fancy wrapper sa paligid ng isang shared counter at isang modulo operation upang piliin ang parehong elemento mula sa listahan.
Mga pahinang tumuturo sa Swarms
Ngayon, na may lahat ng komunikasyon na ibinibigay sa ilalim ng mga tagumpay at pag-share counter, ito ay naging trivial upang i-introduce ang isang orchestrator node, o gamitin ang ilang kasalukuyang MQ at KV storage.
Fun fact: wala kaming umuwi ang implementation ng feature na ito. Kapag ang QA ay nakuha ang kanilang mga kamay sa isang single node implementation ng SwarmAgent
Nagsimula sila ngayong mag-deploy ng ilang instansya ng mga independiyenteng mga agente upang lumaki ang load.
Single instance ang performance
Ano ang kinakailangan tungkol sa pagganap? Anong mabuti sa loob ng lahat ng mga wrappers at implisitong synchronizations? Hindi ko magpapatakbo sa iyo sa lahat ng iba't ibang mga pagsubok na ginawa, lamang ang dalawang pinaka-mahal na kung saan natuklasan na ang sistema ay maaaring lumikha ng isang mahusay na sapat na load.
Ang benchmark ay:
BenchmarkDotNet v0.14.0, Windows 10 (10.0.19045.4651/22H2/2022Update)
Intel Core i7-10875H CPU 2.30GHz, 1 CPU, 16 logic at 8 physical core
Paggamit ng .NET SDK 9.0.203
[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);
}
}
}
Iba pang tingnan natin ang halos purong overhead ng pag-planning laban sa simple Task spawning.
Sa kaso na ito, ang parehong mga paraan ng load ay tulad ng sumusunod:
private ValueTask CounterLoadMethod(int i)
{
Interlocked.Increment(ref StaticControl.Counter);
return ValueTask.CompletedTask;
}
Ang mga resulta para sa Iterations = 10:
WorkerType |
Parallelism |
Mean |
Error |
StdDev |
Gen0 |
Gen1 |
Gen2 |
Allocated |
---|---|---|---|---|---|---|---|---|
Simple |
10 |
3.565us |
0.0450us |
0.0421us |
0.3433 |
- |
- |
2.83KB |
Simple |
100 |
30.281us |
0.2720us |
0.2544us |
3.1128 |
0.061 |
- |
25.67KB |
Simple |
1000 |
250.693us |
2.1626us |
2.4037us |
30.2734 |
5.8594 |
- |
250.67KB |
Scheduler |
10 |
40.629us |
0.7842us |
0.8054us |
8.1787 |
0.1221 |
- |
66.15KB |
Scheduler |
100 |
325.386us |
2.3414us |
2.1901us |
81.0547 |
14.6484 |
- |
662.09KB |
Scheduler |
1000 |
4,685.812us |
24.7917us |
21.9772us |
812.5 |
375 |
- |
6617.59KB |
Simple ang
10
565 ang
0.0450 ang
04421 ang
0.3433
-
-
sa pamamagitan ng 83kb
Simple ang
100
30281 ang
ang 0.2720
sa pamamagitan ng 2544
3.1128
0.061
-
sa loob ng 25.67kb
Simple ang
1000
sa pamamagitan ng 250.693us
1626 ang
24737 ang
30.2734
5.8594
-
sa pamamagitan ng 250.67KB
ang schedule
10
40629 ang
842 sa pamamagitan ng
ang 8054
8.1787
0.1221
-
615 sa loob ng
ang schedule
100
325386 ang
24414 ang
1901 ang
81.0547
14.6484
-
6669kb
ang schedule
1000
4685.812 ang
24917 sa pamamagitan ng
219772 ang
812.5
375
-
6617.59KB sa pamamagitan ng
Kahit na nakikita? Hindi sa katunayan. Bago sa katunayan ng pagsusuri, inisip ko na mas mababa ang pagganap, ngunit ang mga resulta ay talagang naninirahan sa akin. Iyon lamang ang pag-iisip tungkol sa kung ano ang nangyari sa ilalim ng kape at ang mga karapatan na natagpuan lamang para sa ~4us per parallel instance. Anyway, ito ay lamang isang ilustrasyon ng overhead; kami ay interesado sa higit pa sa mga practical benchmarks.
Ano ang maaaring maging isang realistic worst case test scenario? Well, ano ang tungkol sa isang function na hindi gumagawa ng higit sa isang API call? Sa isang side, halos lahat ng paraan ng bot ay gumagana ito, ngunit sa iba pa, kung walang higit sa mga API calls, pagkatapos ay ang lahat ng mga pag-sinkronisasyon na pagsasanay, hindi ba?
Ang metriko para sa espasyo-panahong Schwarzschild na may sistemang koordinatong (PingAsync
Para sa simplicity, ang mga client ng RPC ay ibinigay sa labas ng mga bots.
private async ValueTask PingLoadMethod(int i)
{
await clients[i].PingAsync(new PingRequest());
}
Narito ang mga resulta, taas 10 iterations (grpc server ay sa local network):
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 |
Simple ang
100
94.45 sa loob ng
1,804 sa loob ng
4148 sa loob ng
600
200
4.6 MB ang
Simple ang
1000
69.69 sa loob ng
15 592 ang nakalipas
45730 sa pamamagitan ng
9000
7000
77.7 MB ang nakaraan
ang schedule
100
95.48 sa loob ng
sa pamamagitan ng 1.547 ms
1,292 sa loob ng
833.3333
333.3333
Mga pahinang tumuturo sa 5.85 MB
ang schedule
1000
5252 sa pamamagitan ng
14 697 sa pamamagitan ng
44,405 sa loob ng
8000
7000
Mga pahinang tumuturo: 68.57 MB
Tulad ng inaasahan, ang epekto ng pagganap sa mga real-life work scenarios ay negligible.
Mga Analysis
Gayunpaman, mayroong mga backend logs, metric, at traces na nagbibigay ng isang mahusay na view ng kung ano ang nangyayari sa panahon ng pag-load. Ngunit Swarm ay pumunta sa isang step higit pa, mag-script ang kanyang sarili na data - karaniwang nakipag-ugnayan sa backend - complementing ito.
Narito ang isang halimbawa ng ilang mga metric sa isang pag-uulat na pagsubok, na may karagdagang bilang ng mga aktor. Ang pagsubok ay nangangailangan dahil sa maraming mga pag-atake sa SLA (tingnan ang mga timeout sa API calls), at mayroon kaming lahat ng instrumento na kinakailangan upang makita kung ano ang nangyayari mula sa prospekto ng client.
Kami ay karaniwang nagsisimula ang pagsusuri na ganap na pagkatapos ng isang anumang bilang ng mga error, kaya ang mga API call lines ay nagsisimula abruptly.
Gayunpaman, ang mga traces ay kinakailangan para sa isang mahusay na observability. Para sa Swarm test runs, ang mga traces ng client ay nakatuon sa mga backend traces sa isang tree.connectionIdD/botId
Ito ay tumutulong sa mga debugging.
Note: DevServer ay isang magandang monolith build ng lahat-in-one backend serbisyo na magsisimula sa mga developer PC, at ito ay isang espesyal na ginanap na halimbawa upang mabawasan ang halaga ng mga detalye sa screen.
Swarm bots mag-script ng isang iba't ibang uri ng trace: sila mag-imite ang mga trace na ginagamit sa performance analysis. Sa tulong ng mga existing tool, ang mga trace na ito ay maaaring i-view at i-analysis gamit ang mga binubuo na mga kakayahan tulad ng PerfettoSQL.
Ano ang natutunan natin sa final
Ngayon mayroon kaming isang SDK na binubuo sa parehong GameClient at Dedicated Server bots. Ang mga bots na ngayon ay sumusuporta sa karamihan ng logic ng laro - naglalaman sila ng mga subsystem ng mga kaibigan, mga grupo ng suporta, at matchmaking. Dedicated bots ay maaaring simulate matches kung saan ang mga manlalaro makakuha ng mga rewards, progression quests, at higit pa.
Kung maaari mong i-writ simple code ay maaaring lumikha ng mga bagong modules at simple test scenarios sa pamamagitan ng QA team. May isang ideya tungkol FlowGraph scenarios (visual programming), ngunit ito ay lamang isang ideya sa oras na ito.
Ang mga pagsubok ng pagganap ay halos katapusan - sinasabi ko halos dahil kailangan mong magsimula ang mga ito manually.
Ang Swarm ay hindi lamang gumagamit sa mga pagsubok, ngunit ginagamit nang karaniwang upang i-replicate at i-fix ang mga bug sa logic ng laro, lalo na kapag ito ay mahirap upang gawin manually, tulad ng kapag kailangan mo ng ilang mga client ng laro na bumuo ng isang espesyal na sequence ng mga aksyon.
Upang sumunod ang mga bagay, kami ay napaka-satisfied sa aming resulta at siguradong walang pag-iisip tungkol sa mga pagsasanay na itinuturing sa pag-unlad ng aming sarili na sistema ng pagsubok.
I hope you this article, it was challenging to tell you about all the nuances of Swarm development without making this text excessively bloated. I'm sure I may have excluded some important details in the process of balancing text size and information, ngunit ako ay happy upang magbigay ng higit pa ng kontekstong ito kung mayroon kang anumang mga tanong!