How We Design Swarm, ჩვენი ინტეგრირებული load-testing framework, რათა შეესაბამება მოთხოვნებს end-to-end ტესტირების, საბაჟო პროტოკები, და დიდი ზომის bot Simulations in gamedev. How We Design Swarm, ჩვენი ინტეგრირებული load-testing framework, რათა შეესაბამება მოთხოვნებს end-to-end ტესტირების, საბაჟო პროტოკები, და დიდი ზომის bot Simulations in gamedev. დღეს, ეს არის ნამდვილად რთული ვფიქრობ, პროგრამული პროდუქტი გარეშე მინიმუმ დონის ტესტირება. ერთეული ტესტირება არის სტანდარტული გზა შეკუმშვის bugs მცირე კოდი, ხოლო end-to-end ტესტირება მოიცავს მთელი განაცხადის სამუშაო ხაზები. GameDev არ არის შეზღუდვა, მაგრამ იგი მოიცავს საკუთარი უნიკალური პრობლემები, რომლებიც არ ყოველთვის შეესაბამება, თუ როგორ ჩვენ ჩვეულებრივ ტესტირება ვებ პროგრამები. ეს სტატიის შესახებ გზა ჩვენ მიიღო, რათა შეესაბამება, რომ უნიკალური საჭიროებებს ჩვენი საკუთარი შიდა ტესტირების ინსტრუმენტი. ზოგიერთი Hello, I'm Andrey Rakhubov, Lead Software Engineer at MY.GAMES! ამ პოსტში, მე გაუზიაროს დეტალები backend გამოიყენება ჩვენი სტუდია, და ჩვენი მიმოხილვა დატვირთვა ტესტირება meta-game War Robots: Frontiers. One Response to ჩვენი არქიტექტურა არასამთავრობო დეტალურად, ჩვენი backend შედგება კლასიკური მომსახურების კომპლექტი და კლასიკური კონტაქტები, სადაც მოთამაშეები ცხოვრობენ. მიუხედავად იმისა, რომ ზოგიერთი სხვა ნაკლებები, რომ შეიძლება არსებობს, ეს მიმოხილვა უკვე შესანიშნავი გადაწყვეტა ერთ-ერთი ყველაზე შეუზღუდავი პრობლემები თამაშის განვითარება: iteration სიჩქარე. თქვენ შეიძლება აღიაროთ, რომ დროს პირველი დღის Rust-lang განვითარება იყო შერჩევა, რომ დოკუმენტაცია შეიცვალა, როგორც თქვენ წაიკითხე რა თქმა უნდა, ეს არის GDD (Game Design Document) დოკუმენტი. ეს მოთამაშეები ეხმარება გზა, რომ ისინი ძალიან ხელმისაწვდომია განახლება და განახლება ადრე ფუნქციონალური დიზაინი და ისინი ძალიან მარტივი refactor როგორც განსხვავებული მომსახურება, თუ საჭიროა შემდეგ. რა თქმა უნდა, ეს შეუზღუდავი დატვირთვის და ინტეგრირების ტესტირება, რადგან თქვენ არ გაქვთ უფრო ნათელი მომსახურების განსხვავება მცირე, კარგად შეზღუდავი API-ები. გარდა ამისა, არსებობს ქსოვილის heterogeneous ღონისძიებები, სადაც არსებობს ძალიან ნაკლებად გამოხატული ან კონტროლირებული კავშირები მოთამაშეების შორის. ტესტირება როგორც თქვენ შეგვიძლია მიუთითოთ, ერთეული ტესტირება მოთამაშეები და მომსახურება (და load/integration ტესტირება მომსახურება) არ განსხვავდება ის, რაც თქვენ ნახავთ ნებისმიერი სხვა ტიპის პროგრამული უზრუნველყოფა. განსხვავებები ჩანს: Load და Integration ტესტირება მოთამაშეები End-to-end და load backend ტესტირება ჩვენ არ ვხედავ ტესტირება სტატისტიკები დეტალურად აქ, რადგან ეს არ არის სპეციფიკური GameDev, მაგრამ დაკავშირებულია სტატისტიკა მოდელი თავს. ჩვეულებრივი მიმოხილვა არის, რომ აქვს სპეციალური ერთი ნომერი კლასტერია, რომელიც განკუთვნილია, რომ შეუწყოს მეხსიერება ერთი ტესტიში და ეს ასევე შეუძლია შეუწყოს ყველა გამოცემული მოთხოვნები და მოვუწოდოს სტატისტიკა API. ჩვენ გამოიყენებთ იგივე მიმოხილვა. ეს თქმა უნდა, ყველაფერი იწყება უფრო საინტერესო, როდესაც ჩვენ ვართ დატვირთვა ან end-to-end ტესტირება - და ეს არის, სადაც ჩვენი ისტორია დაიწყება. ასე რომ, ჩვენი შემთხვევაში, კლიენტული განაცხადის არის თამაში. თამაში გამოიყენებს Unreal Engine, ასე რომ კოდი წერილი C++, და სერვერზე ჩვენ გამოიყენებთ C#. მოთამაშეები ინტეგრირება UI ელემენტები თამაშში, წარმოების მოთხოვნები backend (ან მოთხოვნები გაკეთება უარყოფითი). ამ ეტაპზე, უამრავი უმრავლესობა Frameworks უბრალოდ შეწყვიტოს მუშაობა ჩვენთვის, და ნებისმიერი ტიპის selenium-მაგული კომპლექტი, რომელიც განიხილავს კლიენტული პროგრამები ბრაუზერი არ არის გარკვეული. შემდეგი საკითხი არის, რომ ჩვენ გამოიყენებთ საბაჟო კომუნიკაციის პროტოკოლი კლიენტების და backend შორის. ეს ნაწილი ნამდვილად ღირს განსხვავებული სტატიაში, მაგრამ მე აჩვენებთ ძირითადი კონცეფციები: კომუნიკაცია მოხდება WebSocket კავშირი მეშვეობით ეს არის schema-first; ჩვენ გამოიყენებთ Protobuf შეტყობინება და მომსახურების სტრუქტურა WebSocket- ის შეტყობინებები Protobuf- ის შეტყობინებები, რომლებიც შეფუთულია კონტეინერში metadata- ს ერთად, რომელიც შეკუმშავს ზოგიერთი საჭირო gRPC- ს დაკავშირებული ინფორმაცია, როგორიცაა URL და headers. ასე რომ, ნებისმიერი ინსტრუმენტი, რომელიც არ საშუალებას გაძლევთ აირჩიოთ საბაჟო პროტოკები, არ არის განკუთვნილია სამუშაოთვის. Load ინსტრუმენტი, რათა ტესტირება მათ ყველა ამავე დროს, ჩვენ გსურთ ერთ-ერთი ინსტრუმენტი, რომელიც შეუძლია დააყენოთ REST / gRPC ტესტები და end-to-end ტესტები ჩვენი საბაჟო პროტოკონის გამოყენებით. მას შემდეგ, რაც ყველა მოთხოვნებს, რომლებიც ტესტირება და ზოგიერთი წინასწარ შეტყობინებები შეამოწმოთ, ჩვენ გაქვთ ამ კლიენტებს: k6 Locust NBomber K6 Locust NBOMB თითოეული მათთან აქვს მათი უპირატესობები და უპირატესობები, მაგრამ არსებობს რამდენიმე რამ, რომლებიც მათ შორის არ შეუძლიათ გადაიხადოს დიდი (სამდვილეში შიდა) ცვლილებები: პირველი, არსებობს საჭიროება, რომელიც დაკავშირებულია inter-bot კომუნიკაციის სინქრონიზაციის მიზნით, როგორც ადრე შეხვდა. მეორე, კლიენტები საკმაოდ რეაქტიული და მათი სტატისტიკა შეიძლება განსხვავდეს გარეშე ექსკლუზიური საქმიანობა ტესტი სცენარეთა; ეს იწვევს დამატებითი სინქრონიზაცია და საჭიროება გადაიხადოს ბევრი ცვლილებები თქვენი კოდი. Last but not least, ეს ინსტრუმენტები ძალიან შეუზღუდავი ფოკუსირება შესრულების ტესტირება, გთავაზობთ მრავალფეროვანი ფუნქციები ნაცვლად გაზრდის დატვირთვა ან განკუთვნილია დროები, მაგრამ არ აქვს შესაძლებლობა შექმნას კომპლექსური გაფართოებული სტრატეები მარტივი გზა. დროა მიიღოს ფაქტი, რომ ჩვენ ნამდვილად საჭიროა სპეციალურ ინსტრუმენტი. ეს არის, თუ როგორ ჩვენი ინსტრუმენტი - და დასაწყისი Swarm Swarm მაღალი დონეზე, Swarm- ის სამუშაოა, რათა დაწყოს ბევრი მოთამაშეებს, რომლებიც ქმნის სერვერზე. ეს მოთამაშეებს აცხადებენ ბოტებს, და მათ შეუძლიათ შეიმუშავოთ კლიენტების ქცევა ან სპეციფიკაციური სერვერზე ქცევა. Swarm შედგება ერთი ან რამდენიმე მოთამაშეებს, რომლებიც ამ ბოტებს და კომუნიკაცია ერთად. More formally, აქ არის სია მოთხოვნები ინსტრუმენტი: Reaction to state updates უნდა იყოს მარტივი კონკურენტი არ უნდა იყოს პრობლემა კოდი უნდა იყოს ავტომატურად instrumentable Bot-to-bot კომუნიკაცია უნდა იყოს მარტივი Multiple instances უნდა იყოს joinable, რათა შექმნათ საკმარისი load ინსტრუმენტი უნდა იყოს მსუბუქი და შეუძლია შექმნას საკმაოდ წნევა backend itself როგორც ბონუს, მე ასევე დაამატა ზოგიერთი დამატებითი ნაბიჯები: ორივე ეფექტურობის და End-to-End ტესტი სცენარეთა უნდა იყოს შესაძლებელი ინსტრუმენტი უნდა იყოს სატრანსპორტო agnostic; ჩვენ უნდა იყოს შეუძლიათ დააყენოთ ეს ნებისმიერი სხვა შესაძლებელი სატრანსპორტო, თუ საჭიროა ინსტრუმენტი მოიცავს imperative კოდი სტილი, რადგან მე პირდაპირი ვფიქრობ, რომ declarative სტილი არ არის განკუთვნილია კომპლექსური სტრატეები მოდული გადაწყვეტილებები ბოტებს უნდა იყოს შეუძლიათ არსებობს განსხვავებით ტესტირების ინსტრუმენტი, რაც იმას ნიშნავს, რომ არ უნდა იყოს მძიმე დამოკიდებულებები ჩვენი მიზანი ვფიქრობთ, რომ კოდი, რომ ჩვენ გსურთ წაიკითხოთ; ჩვენ ვფიქრობთ, რომ ბოტი, როგორც ფანჯარა, და ის არ შეუძლია გააკეთოს რამ თავს, მას შეუძლია მხოლოდ შენარჩუნება უბრავი, ხოლო Scenario არის ფანჯარა, რომელიც წაიკითხა 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)); } } Steaming Backend დატოვებს ბევრი განახლებები კლიენტს, რომელიც შეიძლება მოხდეს მოდული; ზედაპირზე მაგალითად, ერთ-ერთი ასეთი მოვლენები არის Robot უნდა იყოს შეუძლიათ ორივე რეაგირება ამ მოვლენებს ინტენსიურად და გაძლევთ შესაძლებლობა, რომ იხილოთ მათ გარე კოდი. GroupInviteAddedEvent public Task SubscribeToGroupState() { Subscription.Listen(OnGroupStateUpdate); Subscription.Start(api.GroupServiceClient.SubscribeToGroupStateUpdates, new SubscribeToGroupStateUpdatesRequest()); return Task.CompletedTask; } მიუხედავად იმისა, რომ კოდი არის საკმაოდ მარტივი (და როგორც თქვენ შეიძლება მივიღოთ ეს არის მხოლოდ ხანგრძლივი გადახდის შემთხვევაში), ბევრი ხდება აქ. OnGroupStateUpdate ეს არის საკუთარი a StreamSubscription IObservable<ObservedEvent<TStreamMessage>> და ეს უზრუნველყოფს სასარგებლო გაფართოება, აქვს დამოკიდებული სიცოცხლის ციკლი, და მოიცავს მეტრიკები. კიდევ ერთი უპირატესობა: ეს არ მოითხოვს სპეციფიკური სინქრონიზაცია, რათა წაიკითხოთ ან შეცვალოს სტატისტიკა, მიუხედავად იმისა, რომ მენეჯერი განახლებულია. case GroupUpdateEventType.GroupInviteAddedEvent: State.IncomingInvites.Add(ev.GroupInviteAddedEvent.GroupId, ev.GroupInviteAddedEvent.Invite); break; კონკურენტი არ უნდა იყოს პრობლემა ზემოთ, კოდი დარეგისტრირებულია როგორც ხაზიური, ერთი ხაზის კოდი. იდეა არის მარტივი: არასოდეს აწარმოეთ კოდი, რომელიც ერთდროულად მოიცავს სცენარეთა ან ამ სცენარში მოპოვებული ბოტი, და გარდა ამისა, ბოტი / მოდულების შიდა კოდი არ უნდა აწარმოოს ერთდროულად ბოტის სხვა ნაწილებთან. ეს არის მსგავსი წაიკითხეთ / წაიკითხეთ კურსი, სადაც კურსი კურსი წაიკითხა (დაზიდული ხელმისაწვდომობა) და ბოტ კურსი წაიკითხა (სხვეწილი ხელმისაწვდომობა). და მიუხედავად იმისა, რომ ჩვენი მიზანია შეიძლება გააკეთა ამ ტიპის კურსი, არსებობს უკეთესი გზა. Task Scheduler და სინქრონიზაციის კონტაქტი ორი მექანიზმები, რომლებიც ამ სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო ეს არის პასუხისმგებლობა ელეგანტურად გადაცემის თქვენი async calls უკან UI thread. DispatcherSynchronizationContext ჩვენი სტრატეგია, ჩვენ არ დაინტერესებთ thread affinity, და ვიდრე უფრო დაინტერესებთ შესრულების ნაბიჯები სტრატეგია. წინასწარ დასაწყისში დაბალი დონეზე კოდი წაიკითხეთ ერთ-ერთი არ ცნობილი კლასის სახელწოდებით. • საწყისი : ConcurrentExclusiveSchedulerPair დოკუმენტი დოკუმენტი გთავაზობთ საქმიანობის გრაფიკებს, რომლებიც აკონტროლებს საქმიანობის გაკეთება, ხოლო უზრუნველყოფს, რომ ერთობლივი საქმიანობა შეიძლება გაკეთდეს ერთდროულად და ექსკლუზიური საქმიანობა არ გაკეთება. გთავაზობთ საქმიანობის გრაფიკებს, რომლებიც აკონტროლებს საქმიანობის გაკეთება, ხოლო უზრუნველყოფს, რომ ერთობლივი საქმიანობა შეიძლება გაკეთდეს ერთდროულად და ექსკლუზიური საქმიანობა არ გაკეთება. ახლა ჩვენ უნდა უზრუნველყოს, რომ ყველა კოდი სინამდვილეში გააქტიურება როდესაც ბოსტის კოდი აწარმოებს . ConcurrentScheduler ExclusiveScheduler ერთი ვარიანტი არის, რომ გამოქვეყნოს იგი როგორც პარამეტრი, რომელიც გაკეთდება, როდესაც სცენარი დაიწყება: public Task LaunchScenarioAsyncThread(SwarmAgent swarm, Launch launch) { return Task.Factory.StartNew( () => { /* <some observability and context preparations> */ return RunScenarioInstance(Scenario, swarm, LaunchOptions, ScenarioActivity, launch); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, Scenario.BotScenarioScheduler.ScenarioScheduler).Unwrap(); } ეს Method, ამავე დროს, აცხადებს, რომ და კონფიდენციალურობის გადაწყვეტილებები, მათ შორის კონფიდენციალურობის გადაწყვეტილებები ( ეს არის კონკურენტული ნაწილი ) და RunScenarioInstance SetUp Run ScenarioScheduler ConcurrentExclusiveSchedulerPair Now, when we are inside the scenario code and we do this... 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(); } ...async სტატისტიკა მანქანა გააკეთებს მისი მუშაობა ჩვენთვის, შენარჩუნების გრაფიკული ჩვენი სამუშაოები. ახლა იგი ასევე აწარმოებს concurrent გრაფიკორზე, ასე რომ ჩვენ იგივე ტრიკს აწარმოებთ, ასევე: 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; }); } Runtime abstraction შეფუთვა გრაფიკაცია იგივე გზა, როგორც დასაწყისში, calling სწორი გრაფიკით Task.Factory.StartNew კოდი Generation OK, ახლა ჩვენ უნდა manually შეფუთვა ყველაფერი შიდა და მიუხედავად იმისა, რომ ეს გადაიხადოს პრობლემა, ეს არის შეცდომა, ადვილად დაგავიწყება, და ზოგადად, კარგად, ეს ხდის ცუდი. Do ვხედავ ამ ნაწილი კოდი კიდევ ერთხელ: await leader.Group.SendGroupInvite(follower.PlayerId); აქ, ჩვენი bot აქვს a ქონება არის მოდული, რომელიც არსებობს მხოლოდ კოდის გაზიანება სხვადასხვა კლასებში და თავიდან ავიცილოთ გაფართოება კლასის 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; } | და ახლა ეს უბრალოდ მუშაობს! არ არის მეტი შეფუთვა და ცუდი კოდი, უბრალოდ მარტივი მოთხოვნები (არ არის საკმაოდ პოპულარული განვითარებლები) შექმნა ინტერფეისი თითოეული მოდულზე. რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, ეს არის. ტელეფონი : Runtime.Do public const string MethodProxy = @" public async $return_type$ $method_name$($method_params$) { $return$await runtime.Do(() => implementation.$method_name$($method_args$)); } "; ინსტრუმენტები შემდეგი ნაბიჯი არის კოდის ინსტრუმენტაცია, რათა შეინახოთ მეტრიკები და ტანსაცმელი. პირველი, თითოეული API call უნდა შეამოწმოთ. ეს ნაწილი არის ძალიან მარტივი, რადგან ჩვენი გადაზიდვა ეფექტურად აჩვენებს, რომ არის gRPC კანელი, ასე რომ ჩვენ უბრალოდ გამოიყენებთ სწორი წერილი interceptor... callInvoker = channel .Intercept(new PerformanceInterceptor()); → სადაც არის gRPC abstraction of client-side RPC invocation. 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); } } Looks familiar, right? While you can still use this feature to create sub-scopes, every module proxy method defines a scope automatically: 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 დასაწყისში მე გსურთ გააქტიუროთ ზოგიერთი შავი პლატფორმა, რომელიც შეიცავს კომუნიკაციისთვის ერთობლივი Key-value შენახვა (გალითად, Redis), მაგრამ კონცეფცია სინამაზის ტესტირების გამოცდილების შემდეგ, გამოჩნდა, რომ მუშაობის რაოდენობა შეიძლება ძალიან შეზღუდული იყოს ორი მარტივი კონცეფცია: ხაზები და შერჩევა. ბილეთები - Tickets რეალური მსოფლიოში, მოთამაშეები კომუნიკაციის ერთად ერთ-ერთი გზა – ისინი წაიკითხე პირდაპირი შეტყობინებები ან გამოიყენოთ საუბუქი ჩატი. ბოტზე, ჩვენ არ უნდა შედუღებოდეს, რომ granularity, და ჩვენ უბრალოდ უნდა კომუნიკაციის მიზნები. ასე რომ, ვიდრე “Hey Killer2000, გთხოვთ, გთხოვთ, რომ მე თქვენი ჯგუფი” ჩვენ უნდა მარტივი “Hey, გთხოვთ, მოვუწოდეთ მე ჯგუფიში”. აქ არის, სადაც ბილეთები ითამაშებენ: ვინმეს 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); } აქ ეს არის უბრალოდ ფანტასტიკური შეფუთვა საერთო კონტაქტი და modulo ოპერაცია, რათა აირჩიოთ სწორი ელემენტი სიაში. RoundRobinRole Cluster of Swarms-ის მოვლენები ახლა, მას შემდეგ, რაც ყველა კომუნიკაცია შეშფოთებულია დახვეწილი კონტაქტორები, ის იღებს მინიმალურია, რომ დააყენოთ ან ანკასტრატორი ნომერი, ან გამოიყენოთ ზოგიერთი ხელმისაწვდომი MQ და KV შენახვა. ფსიქიკური ფაქტი: ჩვენ არასდროს არ დასრულდა ამ ფუნქციონირება. როდესაც QA მიიღო მათი ხელები ერთი ნომერი განახლება SwarmAgent , ისინი დაუყოვნებლივ დაიწყო უბრალოდ გააყენოთ მრავალჯერადი ინტენსიები დამოუკიდებელი მენეჯერი, რათა გაზრდის დატვირთვა. ჩანს, ეს იყო მეტი, ვიდრე საკმარისი. Single Instance შესრულება რა არის ეფექტურობის შესახებ? რა არის დაკარგული ყველა ამ შეფუთვა და შედუღებადი სინკრონიზაცია? მე არ გადატვირთვა თქვენ ყველა სხვადასხვა ტესტიების მიერ, მხოლოდ ყველაზე მნიშვნელოვანი ორი, რომელიც აჩვენა, რომ სისტემა შეუძლია შექმნას საკმარისი კარგი დატვირთვა. Benchmark სტრუქტურა: BenchmarkDotNet v0.14.0, Windows 10 (10.0.19045.4651/22H2/2022Update) Intel Core i7-10875H CPU 2.30GHz, 1 CPU, 16 ლოგიკური და 8 ფიზიკური კედლები .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); } } } პირველ რიგში, ვხედავ თითქმის სუფთა overhead გარიგების შედარებით მარტივი Task spawning. ამ შემთხვევაში, ორივე სატვირთო მეთოდები არის შემდეგი: private ValueTask CounterLoadMethod(int i) { Interlocked.Increment(ref StaticControl.Counter); return ValueTask.CompletedTask; } შედეგები 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 მარტივი 10 5653 კმ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3433 - - 2.8 კგ მარტივი 100 30281 წლამდე 0.2720 წლამდე 0.2544 ის 3.1128 0.061 - 5.7 კგ მარტივი 1000 250693ს 2.162 სათაური 2.4037 იაფი 30.2734 5.8594 - 250.67 კბ გრაფიკი 10 40 629 წლამდე 0784 კუნძული 0 0 0 0 0 0 8.1787 0.1221 - 615 კგ გრაფიკი 100 35386 წლამდე 2.3414ს 1901 წელს 81.0547 14.6484 - 62.09 კბ გრაფიკი 1000 4.685.812 სუნი 24.7917 იაფი 21972 იაფი 812.5 375 - 6617.59 კბ არ არის საკმარისი. სანამ ნამდვილად გააკეთა ტესტი, მე მოითხოვს ბევრი უკეთესი შესრულება, მაგრამ შედეგები ნამდვილად შეუზღუდავი. უბრალოდ ფიქრობთ, თუ რამდენად ხდება ქვეშ კაბა და უპირატესობები ჩვენ მიიღო მხოლოდ ~4us თითო პარამეტრი instance. ნებისმიერ შემთხვევაში, ეს არის მხოლოდ იმიტომ, რომ overhead; ჩვენ სასარგებლო ვართ ბევრი უფრო პრაქტიკული ნიმუში. რა შეიძლება იყოს რეალური ცუდი შემთხვევაში ტესტი სცენარი? რა არის ფუნქცია, რომელიც არაფერი, ვიდრე API call? ერთ-ერთი მხრივ, თითქმის ყველა მეთოდი bot გააკეთებს, მაგრამ მეორე მხრივ, თუ არაფერი, ვიდრე მხოლოდ API calls, მაშინ ყველა სინქრონიზაციის ეფექტები ცუდი, არაფერი? Load Method უბრალოდ მოვუწოდებს ადვილად, RPC კლიენტებს სინათლისგან შეინახება. PingAsync private async ValueTask PingLoadMethod(int i) { await clients[i].PingAsync(new PingRequest()); } აქ არის შედეგები, კიდევ 10 iterations (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.14 მმ მარტივი 1000 69.99 მმ 15.592 მმ 45 730 მმ 9000 7000 77.7 კმ გრაფიკი 100 95.48 მმ 1547 მმ 1.292 მმ 833.3333 333.3333 5.85 კმ გრაფიკი 1000 625.52 მმ 14,697 მმ 24405 მმ 8000 7000 68.57 კმ როგორც მოითხოვს, ეფექტურობის ეფექტი რეალურ სამუშაო სინამდვილეში არ არის შეუზღუდავი. ანალიზი Naturally, there are backend logs, metrics, and traces that provide a good view of what’s happening during loading. მაგრამ Swarm აპირებს ერთი ნაბიჯზე მეტი, წაიკითხოს საკუთარი მონაცემები – ზოგჯერ დაკავშირებული backend – შეამციროს იგი. აქ არის მაგალითია რამდენიმე მეტრიკების ცუდი ტესტიში, რომელიც იზრდება მოვლენების რაოდენობით. ტესტი არ გააკეთა მრავალჯერადი SLA-ის შეზღუდვის გამო (იხილეთ API-ის მოვლენების დროები), და ჩვენ გვაქვს ყველა ინსტრუმენტაცია, რომელიც საჭიროა, რათა შეამოწმოთ, რა ხდება კლიენტების პრაქტიკაში. ჩვეულებრივ, ჩვენ შეჩერებთ ტესტი მთლიანად გარკვეული რაოდენობის შეცდომების შემდეგ, ამიტომ API call lines დასრულდება ნაცვლად. რა თქმა უნდა, რკინიგზები საჭიროა კარგი მოვლენებისთვის. Swarm- ის ტესტირებისთვის კლიენტების რკინიგზები დაკავშირებულია backend- ის რკინიგზებით ერთ-ერთი ხეზე. სპეციალური რკინიგზები შეიძლება იპოვდეს რომელიც დაგეხმარებათ debugging. connectionIdD/botId შენიშვნა: DevServer არის მოსახერხებელი monolith build all-in-one backend მომსახურება, რომელიც დაიწყება Developer PC, და ეს არის სპეციალურად შექმნილია მაგალითია, რათა შეამციროს რაოდენობის დეტალები ეკრანზე. Swarm bots წაიკითხე სხვა სახის წაიკითხე: ისინი იმიტომ, რომ წაიკითხე გამოიყენება ეფექტურობის ანალიზი. დახმარებით ხელმისაწვდომი ინსტრუმენტები, ეს წაიკითხე შეიძლება ნახოთ და ანალიზი გამოყენებით ინტეგრირებული შესაძლებლობები, როგორიცაა PerfettoSQL. რა დასაწყისში მივიღე ჩვენ ახლა გვაქვს SDK, რომელიც ორივე GameClient და Dedicated Server ბოტები მზადდება. ეს ბოტები ახლა მხარს უჭერს ყველაზე თამაშის ლოგიკას – მათ შორის მეგობრები დისზისტენტები, მხარდაჭერა ჯგუფიები და matchmaking. Dedicated ბოტები შეუძლია იმიტომ, რომ მოთამაშეებს იღებს საფასური, პროგრესის მიზნები და ბევრი სხვა. მას შემდეგ, რაც შესაძლებელია მარტივი კოდი შექმნა, შესაძლებელია ახალი მოდულების შექმნა და მარტივი ტესტი სცენარეთა მიერ QA გუნდი. არსებობს იდეა FlowGraph სცენარეთა (ვუზიური პროგრამირება), მაგრამ ეს ჯერ კიდევ მხოლოდ იდეა. ეფექტურობის ტესტიები თითქმის მუდმივად არიან - მე ვფიქრობ, თითქმის, რადგან ჯერ კიდევ უნდა დაიწყოს მათ manually. Swarm არ მხოლოდ დაგეხმარება ტესტირების, მაგრამ ხშირად გამოიყენება reproducing და შეცვალოს bugs თამაშის ლოგიკაში, განსაკუთრებით როდესაც ეს არის რთული გააკეთოთ manually, როგორიცაა, როდესაც თქვენ უნდა რამდენიმე თამაშის კლიენტები შესრულების სპეციალური სერვისი ოპერაციები. შეტყობინება, ჩვენ ძალიან კმაყოფილებული ვართ ჩვენი შედეგად და დარწმუნებული ვართ, რომ არ გვაქვს შეტყობინებები ჩვენი საკუთარი ტესტირების სისტემის განვითარება. მე ვფიქრობ, რომ თქვენ გაქვთ ამ სტატიაში, ეს იყო განიცდიან, რომ განიცდიან ყველა შანსი Swarm განვითარების გარეშე, რომ ეს ტექსტი ძალიან გაფართოებული. მე დარწმუნებული ვარ, რომ მე შეიძლება შეამციროს ზოგიერთი მნიშვნელოვანი დეტალები პროცესში შეესაბამება ტექსტის ზომა და ინფორმაცია, მაგრამ მე ბედნიერი გთავაზობთ მეტი კონტექსტი, თუ თქვენ გაქვთ რაიმე კითხვები!