Anton Tarasenko
2 years ago
13 changed files with 827 additions and 44 deletions
@ -0,0 +1,44 @@ |
|||||||
|
/** |
||||||
|
* Simple object that represents a job, capable of being scheduled on the |
||||||
|
* `SchedulerAPI`. Use `IsCompleted()` to mark job as completed. |
||||||
|
* Copyright 2022 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class MockJob extends SchedulerJob; |
||||||
|
|
||||||
|
var public string mark; |
||||||
|
var public int unitsLeft; |
||||||
|
|
||||||
|
// We use `default` value only |
||||||
|
var public string callStack; |
||||||
|
|
||||||
|
public function bool IsCompleted() |
||||||
|
{ |
||||||
|
return (unitsLeft <= 0); |
||||||
|
} |
||||||
|
|
||||||
|
public function DoWork(int allottedWorkUnits) |
||||||
|
{ |
||||||
|
unitsLeft -= allottedWorkUnits; |
||||||
|
if (IsCompleted()) { |
||||||
|
default.callStack = default.callStack $ mark; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
} |
@ -0,0 +1,216 @@ |
|||||||
|
/** |
||||||
|
* Set of tests for Scheduler API. |
||||||
|
* Copyright 2022 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class TEST_SchedulerAPI extends TestCase |
||||||
|
abstract; |
||||||
|
|
||||||
|
var int diskUses; |
||||||
|
|
||||||
|
protected static function UseDisk() |
||||||
|
{ |
||||||
|
default.diskUses += 1; |
||||||
|
} |
||||||
|
|
||||||
|
protected static function MockJob MakeJob(string mark, int totalUnits) |
||||||
|
{ |
||||||
|
local MockJob newJob; |
||||||
|
|
||||||
|
newJob = MockJob(__().memory.Allocate(class'MockJob')); |
||||||
|
newJob.mark = mark; |
||||||
|
newJob.unitsLeft = totalUnits; |
||||||
|
return newJob; |
||||||
|
} |
||||||
|
|
||||||
|
protected static function TESTS() |
||||||
|
{ |
||||||
|
Test_MockJob(); |
||||||
|
} |
||||||
|
|
||||||
|
protected static function Test_MockJob() |
||||||
|
{ |
||||||
|
Context("Testing job scheduling."); |
||||||
|
SubText_SimpleScheduling(); |
||||||
|
SubText_ManyScheduling(); |
||||||
|
SubText_DiskScheduling(); |
||||||
|
SubText_DiskSchedulingDeallocate(); |
||||||
|
SubText_JobDiskMix(); |
||||||
|
} |
||||||
|
|
||||||
|
protected static function SubText_SimpleScheduling() |
||||||
|
{ |
||||||
|
Issue("Simple scheduling doesn't process jobs in intended order"); |
||||||
|
class'MockJob'.default.callStack = ""; |
||||||
|
__().scheduler.ManualTick(); // Reset work units |
||||||
|
__().scheduler.AddJob(MakeJob("A", 2400)); |
||||||
|
__().scheduler.AddJob(MakeJob("B", 3000)); |
||||||
|
__().scheduler.AddJob(MakeJob("C", 7600)); |
||||||
|
__().scheduler.AddJob(MakeJob("D", 1000)); |
||||||
|
__().scheduler.ManualTick(); // 10,000 units => -2,500 units per job |
||||||
|
TEST_ExpectTrue(class'MockJob'.default.callStack == "AD"); |
||||||
|
TEST_ExpectTrue(__().scheduler.GetJobsAmount() == 2); |
||||||
|
__().scheduler.ManualTick(); // 10,000 units => -5,000 units per job |
||||||
|
TEST_ExpectTrue(class'MockJob'.default.callStack == "ADB"); |
||||||
|
TEST_ExpectTrue(__().scheduler.GetJobsAmount() == 1); |
||||||
|
__().scheduler.ManualTick(); // 10,000 units => -5,000 units per job |
||||||
|
TEST_ExpectTrue(class'MockJob'.default.callStack == "ADBC"); |
||||||
|
TEST_ExpectTrue(__().scheduler.GetJobsAmount() == 0); |
||||||
|
} |
||||||
|
|
||||||
|
protected static function SubText_ManyScheduling() |
||||||
|
{ |
||||||
|
Issue("After scheduling jobs over per-tick limit, scheduler doesn't process" |
||||||
|
@ "jobs in intended order"); |
||||||
|
class'MockJob'.default.callStack = ""; |
||||||
|
__().scheduler.ManualTick(); // Reset work units |
||||||
|
// 10,000 units => 2,000 units per job for 5 jobs |
||||||
|
__().scheduler.AddJob(MakeJob("A", 3000)); |
||||||
|
__().scheduler.AddJob(MakeJob("B", 3000)); |
||||||
|
__().scheduler.AddJob(MakeJob("C", 3000)); |
||||||
|
__().scheduler.AddJob(MakeJob("D", 1000)); |
||||||
|
__().scheduler.AddJob(MakeJob("E", 3000)); |
||||||
|
__().scheduler.AddJob(MakeJob("F", 3000)); |
||||||
|
__().scheduler.AddJob(MakeJob("G", 1000)); |
||||||
|
__().scheduler.AddJob(MakeJob("H", 5000)); |
||||||
|
__().scheduler.AddJob(MakeJob("I", 1000)); |
||||||
|
__().scheduler.ManualTick(); |
||||||
|
// A:1000, B:1000, C:1000, D:0, E:1000, F:3000, G:1000, H:5000, I:1000 |
||||||
|
TEST_ExpectTrue(class'MockJob'.default.callStack == "D"); |
||||||
|
TEST_ExpectTrue(__().scheduler.GetJobsAmount() == 8); |
||||||
|
__().scheduler.ManualTick(); |
||||||
|
// A:0, B:1000, C:1000, D:0, E:1000, F:1000, G:0, H:3000, I:0 |
||||||
|
TEST_ExpectTrue(class'MockJob'.default.callStack == "DGIA"); |
||||||
|
TEST_ExpectTrue(__().scheduler.GetJobsAmount() == 5); |
||||||
|
__().scheduler.ManualTick(); |
||||||
|
// A:0, B:0, C:0, D:0, E:0, F:0, G:0, H:1000, I:0 |
||||||
|
TEST_ExpectTrue(class'MockJob'.default.callStack == "DGIABCEF"); |
||||||
|
TEST_ExpectTrue(__().scheduler.GetJobsAmount() == 1); |
||||||
|
__().scheduler.ManualTick(); |
||||||
|
// A:0, B:0, C:0, D:0, E:0, F:0, G:0, H:0, I:0 |
||||||
|
TEST_ExpectTrue(class'MockJob'.default.callStack == "DGIABCEFH"); |
||||||
|
TEST_ExpectTrue(__().scheduler.GetJobsAmount() == 0); |
||||||
|
} |
||||||
|
|
||||||
|
protected static function SubText_DiskScheduling() |
||||||
|
{ |
||||||
|
local Text objectInstance; |
||||||
|
|
||||||
|
Issue("Disk scheduling doesn't happen at expected intervals."); |
||||||
|
default.diskUses = 0; |
||||||
|
__().scheduler.ManualTick(1.0); // Pre-fill cooldown, just in case |
||||||
|
objectInstance = __().text.FromString("whatever"); |
||||||
|
__().scheduler.RequestDiskAccess(objectInstance).connect = UseDisk; |
||||||
|
__().scheduler.RequestDiskAccess(objectInstance).connect = UseDisk; |
||||||
|
__().scheduler.RequestDiskAccess(objectInstance).connect = UseDisk; |
||||||
|
__().scheduler.RequestDiskAccess(objectInstance).connect = UseDisk; |
||||||
|
__().scheduler.ManualTick(0.001); |
||||||
|
TEST_ExpectTrue(default.diskUses == 1); |
||||||
|
__().scheduler.ManualTick(0.21); |
||||||
|
TEST_ExpectTrue(default.diskUses == 1); |
||||||
|
TEST_ExpectTrue(__().scheduler.GetDiskQueueSize() == 3); |
||||||
|
__().scheduler.ManualTick(0.2); |
||||||
|
TEST_ExpectTrue(default.diskUses == 2); |
||||||
|
__().scheduler.ManualTick(0.21); |
||||||
|
TEST_ExpectTrue(default.diskUses == 2); |
||||||
|
TEST_ExpectTrue(__().scheduler.GetDiskQueueSize() == 2); |
||||||
|
__().scheduler.ManualTick(0.2); |
||||||
|
TEST_ExpectTrue(default.diskUses == 3); |
||||||
|
TEST_ExpectTrue(__().scheduler.GetDiskQueueSize() == 1); |
||||||
|
__().scheduler.ManualTick(0.1); |
||||||
|
TEST_ExpectTrue(default.diskUses == 3); |
||||||
|
__().scheduler.ManualTick(0.1); |
||||||
|
TEST_ExpectTrue(default.diskUses == 3); |
||||||
|
TEST_ExpectTrue(__().scheduler.GetDiskQueueSize() == 1); |
||||||
|
__().scheduler.ManualTick(0.1); |
||||||
|
TEST_ExpectTrue(default.diskUses == 4); |
||||||
|
TEST_ExpectTrue(__().scheduler.GetDiskQueueSize() == 0); |
||||||
|
} |
||||||
|
|
||||||
|
protected static function SubText_DiskSchedulingDeallocate() |
||||||
|
{ |
||||||
|
local Text objectInstance, deletedInstance; |
||||||
|
|
||||||
|
Issue("Disk scheduling cannot correctly handle deallocated receivers."); |
||||||
|
default.diskUses = 0; |
||||||
|
__().scheduler.ManualTick(1.0); // Pre-fill cooldown, just in case |
||||||
|
objectInstance = __().text.FromString("whatever"); |
||||||
|
deletedInstance = __().text.FromString("heh"); |
||||||
|
__().scheduler.RequestDiskAccess(objectInstance).connect = UseDisk; |
||||||
|
__().scheduler.RequestDiskAccess(deletedInstance).connect = UseDisk; |
||||||
|
__().scheduler.RequestDiskAccess(objectInstance).connect = UseDisk; |
||||||
|
__().scheduler.RequestDiskAccess(deletedInstance).connect = UseDisk; |
||||||
|
// Fuck off the `deletedInstance` object |
||||||
|
deletedInstance.FreeSelf(); |
||||||
|
// Test! |
||||||
|
__().scheduler.ManualTick(0.001); |
||||||
|
TEST_ExpectTrue(default.diskUses == 1); |
||||||
|
__().scheduler.ManualTick(0.21); |
||||||
|
TEST_ExpectTrue(default.diskUses == 1); |
||||||
|
__().scheduler.ManualTick(0.2); |
||||||
|
TEST_ExpectTrue(default.diskUses == 2); |
||||||
|
__().scheduler.ManualTick(0.21); |
||||||
|
TEST_ExpectTrue(default.diskUses == 2); |
||||||
|
__().scheduler.ManualTick(0.2); |
||||||
|
TEST_ExpectTrue(default.diskUses == 2); |
||||||
|
__().scheduler.ManualTick(1.0); |
||||||
|
TEST_ExpectTrue(default.diskUses == 2); |
||||||
|
} |
||||||
|
|
||||||
|
protected static function SubText_JobDiskMix() |
||||||
|
{ |
||||||
|
local Text objectInstance; |
||||||
|
|
||||||
|
Issue("Job and disk scheduling doesn't happen at expected intervals."); |
||||||
|
objectInstance = __().text.FromString("whatever"); |
||||||
|
class'MockJob'.default.callStack = ""; |
||||||
|
default.diskUses = 0; |
||||||
|
__().scheduler.ManualTick(1.0); // Reset work units |
||||||
|
// 0.2 * 10,000 = 2,000 units => 1,000 units per job for 2 jobs |
||||||
|
__().scheduler.AddJob(MakeJob("A", 30000)); |
||||||
|
__().scheduler.AddJob(MakeJob("B", 10000)); |
||||||
|
__().scheduler.RequestDiskAccess(objectInstance).connect = UseDisk; |
||||||
|
__().scheduler.RequestDiskAccess(objectInstance).connect = UseDisk; |
||||||
|
// Reset disk cooldown |
||||||
|
__().scheduler.ManualTick(0.2); |
||||||
|
// A:25000, B:5000 |
||||||
|
TEST_ExpectTrue(default.diskUses == 1); |
||||||
|
__().scheduler.ManualTick(0.2); // Disk on cooldown |
||||||
|
// A:20000, B:0 |
||||||
|
TEST_ExpectTrue(class'MockJob'.default.callStack == "B"); |
||||||
|
TEST_ExpectTrue(__().scheduler.GetJobsAmount() == 1); |
||||||
|
TEST_ExpectTrue(default.diskUses == 1); |
||||||
|
TEST_ExpectTrue(__().scheduler.GetDiskQueueSize() == 1); |
||||||
|
__().scheduler.ManualTick(0.2); // Disk got off cooldown, do writing |
||||||
|
// A:10000, B:0 |
||||||
|
TEST_ExpectTrue(class'MockJob'.default.callStack == "B"); |
||||||
|
TEST_ExpectTrue(__().scheduler.GetJobsAmount() == 1); |
||||||
|
TEST_ExpectTrue(default.diskUses == 2); |
||||||
|
TEST_ExpectTrue(__().scheduler.GetDiskQueueSize() == 0); |
||||||
|
__().scheduler.ManualTick(0.2); // Disk on cooldown |
||||||
|
// A:0, B:0 |
||||||
|
TEST_ExpectTrue(class'MockJob'.default.callStack == "BA"); |
||||||
|
TEST_ExpectTrue(__().scheduler.GetJobsAmount() == 0); |
||||||
|
TEST_ExpectTrue(default.diskUses == 2); |
||||||
|
TEST_ExpectTrue(__().scheduler.GetDiskQueueSize() == 0); |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
caseName = "SchedulerAPI" |
||||||
|
caseGroup = "Scheduler" |
||||||
|
} |
@ -0,0 +1,421 @@ |
|||||||
|
/** |
||||||
|
* API that provides functions for scheduling jobs and expensive tasks such |
||||||
|
* as writing onto the disk. Also provides methods for users to inform API that |
||||||
|
* they've recently did an expensive operation, so that `SchedulerAPI` is to |
||||||
|
* try and use less resources when managing jobs. |
||||||
|
* Copyright 2022 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class SchedulerAPI extends AcediaObject |
||||||
|
config(AcediaSystem); |
||||||
|
|
||||||
|
/** |
||||||
|
* # `SchedulerAPI` |
||||||
|
* |
||||||
|
* UnrealScript is inherently single-threaded and whatever method you call, |
||||||
|
* it will be completely executed within a single game's tick. |
||||||
|
* This API is meant for scheduling various actions over time to help emulating |
||||||
|
* multi-threading by spreading some code executions over several different |
||||||
|
* game/server ticks. |
||||||
|
* |
||||||
|
* ## Usage |
||||||
|
* |
||||||
|
* ### Job scheduling |
||||||
|
* |
||||||
|
* One of the reasons which is faulty infinite loop detection system that |
||||||
|
* will crash the game/server if it thinks UnrealScript code has executed too |
||||||
|
* many operations (it is not about execution time, logging a lot of messages |
||||||
|
* with `Log()` can take a lot of time and not crash anything, while simple |
||||||
|
* loop, that would've finished much sooner, can trigger a crash). |
||||||
|
* This is a very atypical problem for mods to have, but Acedia's |
||||||
|
* introduction of databases and avarice link can lead to users trying to read |
||||||
|
* (from database or network) an object that is too big, leading to a crash. |
||||||
|
* Jobs are not about performance, they're about crash prevention. |
||||||
|
* |
||||||
|
* In case you have such a job of your own, that can potentially take too |
||||||
|
* many steps to finish without crashing, you can convert it into |
||||||
|
* a `SchedulerJob` (you make a subclass for your type of the job and |
||||||
|
* instantiate it for each execution of the job). This requires you to |
||||||
|
* restructure your algorithm in such a way, that it is able to run for some |
||||||
|
* finite (maybe small) amount of steps and postpone the rest of calculations |
||||||
|
* to the next tick and put it into a method |
||||||
|
* `SchedulerJob.DoWork(int allottedWorkUnits)`, where `allottedWorkUnits` is |
||||||
|
* how much your method is allowed to do during this call, assuming `10000` |
||||||
|
* units of work on their own won't lead to a crash. |
||||||
|
* Another method `SchedulerJob.IsCompleted()` needs to be setup to return |
||||||
|
* `true` iff your job is done. |
||||||
|
* After you prepared an instance of your job subclass, simply pass it to |
||||||
|
* `_.scheduler.AddJob()`. |
||||||
|
* |
||||||
|
* ### Disk usage requests |
||||||
|
* |
||||||
|
* Writing to the disk (saving data into config file, saving local database |
||||||
|
* changes) can be an expensive operation and to avoid lags in gameplay you |
||||||
|
* might want to spread such operations over time. |
||||||
|
* `_.scheduler.RequestDiskAccess()` method allows you to do that. It is not |
||||||
|
* exactly a signal, but it acts similar to one: to request a right to save to |
||||||
|
* the disk, just do the following: |
||||||
|
* `_.scheduler.RequestDiskAccess(<receiver>).connect = <disk_writing_method>` |
||||||
|
* and `disk_writing_method()` will be called once your turn come up. |
||||||
|
* |
||||||
|
* ## Manual ticking |
||||||
|
* |
||||||
|
* If any kind of level core (either server or client one) was created, |
||||||
|
* this API will automatically perform necessary actions every tick. |
||||||
|
* Otherwise, if only base API is available, there's no way to do that, but |
||||||
|
* you can manually decide when to tick this API by calling `ManualTick()` |
||||||
|
* method. |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* How often can files be saved on disk. This is a relatively expensive |
||||||
|
* operation and we don't want to write a lot of different files at once. |
||||||
|
* But since we lack a way to exactly measure how much time that saving will |
||||||
|
* take, AcediaCore falls back to simply performing every saving with same |
||||||
|
* uniform time intervals in-between. |
||||||
|
* This variable decides how much time there should be between two file |
||||||
|
* writing accesses. |
||||||
|
* Negative and zero values mean that all writing disk access will be |
||||||
|
* granted as soon as possible, without any cooldowns. |
||||||
|
*/ |
||||||
|
var private config float diskSaveCooldown; |
||||||
|
/** |
||||||
|
* Maximum total work units for jobs allowed per tick. Jobs are expected to be |
||||||
|
* constructed such that they don't lead to a crash if they have to perform |
||||||
|
* this much work. |
||||||
|
* |
||||||
|
* Changing default value of `10000` is not advised. |
||||||
|
*/ |
||||||
|
var private config int maxWorkUnits; |
||||||
|
/** |
||||||
|
* How many different jobs can be performed per tick. This limit is added so |
||||||
|
* that `maxWorkUnits` won't be spread too thin if a lot of jobs get registered |
||||||
|
* at once. |
||||||
|
*/ |
||||||
|
var private config int maxJobsPerTick; |
||||||
|
|
||||||
|
// We can (and will) automatically tick |
||||||
|
var private bool tickAvailable; |
||||||
|
// `true` == it is safe to use server API for a tick |
||||||
|
// `false` == it is safe to use client API for a tick |
||||||
|
var private bool tickFromServer; |
||||||
|
// Our `Tick()` method is currently connected to the `OnTick()` signal. |
||||||
|
// Keeping track of this allows us to disconnect from `OnTick()` signal |
||||||
|
// when it is not necessary. |
||||||
|
var private bool connectedToTick; |
||||||
|
|
||||||
|
// How much time if left until we can write to the disk again? |
||||||
|
var private float currentDiskCooldown; |
||||||
|
|
||||||
|
// There is a limit (`maxJobsPerTick`) to how many different jobs we can |
||||||
|
// perform per tick and if we register an amount jobs over that limit, we need |
||||||
|
// to uniformly spread execution time between them. |
||||||
|
// To achieve that we simply cyclically (in order) go over `currentJobs` |
||||||
|
// array, each time executing exactly `maxJobsPerTick` jobs. |
||||||
|
// `nextJobToPerform` remembers what job is to be executed next tick. |
||||||
|
var private int nextJobToPerform; |
||||||
|
var private array<SchedulerJob> currentJobs; |
||||||
|
// Storing receiver objects, following example of signals/slots, is done |
||||||
|
// without increasing their reference count, allowing them to get deallocated |
||||||
|
// while we are still keeping their reference. |
||||||
|
// To avoid using such deallocated receivers, we keep track of the life |
||||||
|
// versions they've had when their disk requests were registered. |
||||||
|
var private array<SchedulerDiskRequest> diskQueue; |
||||||
|
var private array<AcediaObject> receivers; |
||||||
|
var private array<int> receiversLifeVersions; |
||||||
|
|
||||||
|
/** |
||||||
|
* Registers new scheduler job `newJob` to be executed in the API. |
||||||
|
* |
||||||
|
* @param newJob New job to be scheduled for execution. |
||||||
|
* Does nothing if given `newJob` is already added. |
||||||
|
*/ |
||||||
|
public function AddJob(SchedulerJob newJob) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
|
||||||
|
if (newJob == none) { |
||||||
|
return; |
||||||
|
} |
||||||
|
for (i = 0; i < currentJobs.length; i += 1) |
||||||
|
{ |
||||||
|
if (currentJobs[i] == newJob) { |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
newJob.NewRef(); |
||||||
|
currentJobs[currentJobs.length] = newJob; |
||||||
|
UpdateTickConnection(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Requests another disk access. |
||||||
|
* |
||||||
|
* Use it like signal: `RequestDiskAccess(<receiver>).connect = <handler>`. |
||||||
|
* Since it is meant to be used as a signal, so DO NOT STORE/RELEASE returned |
||||||
|
* wrapper object `SchedulerDiskRequest`. |
||||||
|
* |
||||||
|
* @param receiver Same as for signal/slots, this is an object, responsible |
||||||
|
* for the disk request. If this object gets deallocated - request will be |
||||||
|
* thrown away. |
||||||
|
* Typically this should be an object in which connected method will be |
||||||
|
* executed. |
||||||
|
* @return Wrapper object that provides `connect` delegate. |
||||||
|
*/ |
||||||
|
public function SchedulerDiskRequest RequestDiskAccess(AcediaObject receiver) |
||||||
|
{ |
||||||
|
local SchedulerDiskRequest newRequest; |
||||||
|
|
||||||
|
if (receiver == none) return none; |
||||||
|
if (!receiver.IsAllocated()) return none; |
||||||
|
|
||||||
|
newRequest = |
||||||
|
SchedulerDiskRequest(_.memory.Allocate(class'SchedulerDiskRequest')); |
||||||
|
diskQueue[diskQueue.length] = newRequest; |
||||||
|
receivers[receivers.length] = receiver; |
||||||
|
receiversLifeVersions[receiversLifeVersions.length] = |
||||||
|
receiver.GetLifeVersion(); |
||||||
|
UpdateTickConnection(); |
||||||
|
return newRequest; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Tells you how many incomplete jobs are currently registered in |
||||||
|
* the scheduler. |
||||||
|
* |
||||||
|
* @return How many incomplete jobs are currently registered in the scheduler. |
||||||
|
*/ |
||||||
|
public function int GetJobsAmount() |
||||||
|
{ |
||||||
|
CleanCompletedJobs(); |
||||||
|
return currentJobs.length; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Tells you how many disk access requests are currently registered in |
||||||
|
* the scheduler. |
||||||
|
* |
||||||
|
* @return How many incomplete disk access requests are currently registered |
||||||
|
* in the scheduler. |
||||||
|
*/ |
||||||
|
public function int GetDiskQueueSize() |
||||||
|
{ |
||||||
|
CleanDiskQueue(); |
||||||
|
return diskQueue.length; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* In case neither server, nor client core is registered, scheduler must be |
||||||
|
* ticked manually. For that call this method each separate tick (or whatever |
||||||
|
* is your closest approximation available for that). |
||||||
|
* |
||||||
|
* Before manually invoking this method, you should check if scheduler |
||||||
|
* actually started to tick *automatically*. Use `_.scheduler.IsAutomated()` |
||||||
|
* for that. |
||||||
|
* |
||||||
|
* NOTE: If neither server-/client- core is created, nor `ManualTick()` is |
||||||
|
* invoked manually, `SchedulerAPI` won't actually do anything. |
||||||
|
* |
||||||
|
* @param delta Time (real one) that is supposedly passes from the moment |
||||||
|
* `ManualTick()` was called last time. Used for tracking disk access |
||||||
|
* cooldowns. How `SchedulerJob`s are executed is independent from this |
||||||
|
* value. |
||||||
|
*/ |
||||||
|
public final function ManualTick(optional float delta) |
||||||
|
{ |
||||||
|
Tick(delta, 1.0); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Is scheduler ticking automated? It can only be automated if either |
||||||
|
* server or client level cores are created. Scheduler can automatically enable |
||||||
|
* automation and it cannot be prevented, but can be helped by using |
||||||
|
* `UpdateTickConnection()` method. |
||||||
|
* |
||||||
|
* @return `true` if scheduler's tick is automatically called and `false` |
||||||
|
* otherwise (and calling `ManualTick()` is required). |
||||||
|
*/ |
||||||
|
public function bool IsAutomated() |
||||||
|
{ |
||||||
|
return tickAvailable; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Causes `SchedulerAPI` to try automating itself by searching for level cores |
||||||
|
* (checking if server/client APIs are enabled). |
||||||
|
*/ |
||||||
|
public function UpdateTickConnection() |
||||||
|
{ |
||||||
|
local bool needsConnection; |
||||||
|
local UnrealAPI api; |
||||||
|
|
||||||
|
if (!tickAvailable) |
||||||
|
{ |
||||||
|
if (_server.IsAvailable()) |
||||||
|
{ |
||||||
|
tickAvailable = true; |
||||||
|
tickFromServer = true; |
||||||
|
} |
||||||
|
else if (_client.IsAvailable()) |
||||||
|
{ |
||||||
|
tickAvailable = true; |
||||||
|
tickFromServer = false; |
||||||
|
} |
||||||
|
if (!tickAvailable) { |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
needsConnection = (currentJobs.length > 0 || diskQueue.length > 0); |
||||||
|
if (connectedToTick == needsConnection) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (tickFromServer) { |
||||||
|
api = _server.unreal; |
||||||
|
} |
||||||
|
else { |
||||||
|
api = _client.unreal; |
||||||
|
} |
||||||
|
if (connectedToTick && !needsConnection) { |
||||||
|
api.OnTick(self).Disconnect(); |
||||||
|
} |
||||||
|
else if (!connectedToTick && needsConnection) { |
||||||
|
api.OnTick(self).connect = Tick; |
||||||
|
} |
||||||
|
connectedToTick = needsConnection; |
||||||
|
} |
||||||
|
|
||||||
|
private function Tick(float delta, float dilationCoefficient) |
||||||
|
{ |
||||||
|
delta = delta / dilationCoefficient; |
||||||
|
// Manage disk cooldown |
||||||
|
if (currentDiskCooldown > 0) { |
||||||
|
currentDiskCooldown -= delta; |
||||||
|
} |
||||||
|
if (currentDiskCooldown <= 0 && diskQueue.length > 0) |
||||||
|
{ |
||||||
|
currentDiskCooldown = diskSaveCooldown; |
||||||
|
ProcessDiskQueue(); |
||||||
|
} |
||||||
|
// Manage jobs |
||||||
|
if (currentJobs.length > 0) { |
||||||
|
ProcessJobs(); |
||||||
|
} |
||||||
|
UpdateTickConnection(); |
||||||
|
} |
||||||
|
|
||||||
|
private function ProcessJobs() |
||||||
|
{ |
||||||
|
local int unitsPerJob; |
||||||
|
local int jobsToPerform; |
||||||
|
|
||||||
|
CleanCompletedJobs(); |
||||||
|
jobsToPerform = Min(currentJobs.length, maxJobsPerTick); |
||||||
|
if (jobsToPerform <= 0) { |
||||||
|
return; |
||||||
|
} |
||||||
|
unitsPerJob = maxWorkUnits / jobsToPerform; |
||||||
|
while (jobsToPerform > 0) |
||||||
|
{ |
||||||
|
if (nextJobToPerform >= currentJobs.length) { |
||||||
|
nextJobToPerform = 0; |
||||||
|
} |
||||||
|
currentJobs[nextJobToPerform].DoWork(unitsPerJob); |
||||||
|
nextJobToPerform += 1; |
||||||
|
jobsToPerform -= 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private function ProcessDiskQueue() |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
|
||||||
|
// Even if we clean disk queue here, we still need to double check |
||||||
|
// lifetimes in the code below, since we have no idea what `.connect()` |
||||||
|
// calls might do |
||||||
|
CleanDiskQueue(); |
||||||
|
if (diskQueue.length <= 0) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (diskSaveCooldown > 0) |
||||||
|
{ |
||||||
|
if (receivers[i].GetLifeVersion() == receiversLifeVersions[i]) { |
||||||
|
diskQueue[i].connect(); |
||||||
|
} |
||||||
|
_.memory.Free(diskQueue[0]); |
||||||
|
diskQueue.Remove(0, 1); |
||||||
|
receivers.Remove(0, 1); |
||||||
|
receiversLifeVersions.Remove(0, 1); |
||||||
|
return; |
||||||
|
} |
||||||
|
for (i = 0; i < diskQueue.length; i += 1) |
||||||
|
{ |
||||||
|
if (receivers[i].GetLifeVersion() == receiversLifeVersions[i]) { |
||||||
|
diskQueue[i].connect(); |
||||||
|
} |
||||||
|
_.memory.Free(diskQueue[i]); |
||||||
|
} |
||||||
|
diskQueue.length = 0; |
||||||
|
receivers.length = 0; |
||||||
|
receiversLifeVersions.length = 0; |
||||||
|
} |
||||||
|
|
||||||
|
// Removes completed jobs |
||||||
|
private function CleanCompletedJobs() |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
|
||||||
|
while (i < currentJobs.length) |
||||||
|
{ |
||||||
|
if (currentJobs[i].IsCompleted()) |
||||||
|
{ |
||||||
|
if (i < nextJobToPerform) { |
||||||
|
nextJobToPerform -= 1; |
||||||
|
} |
||||||
|
currentJobs[i].FreeSelf(); |
||||||
|
currentJobs.Remove(i, 1); |
||||||
|
} |
||||||
|
else { |
||||||
|
i += 1; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Remove disk requests with deallocated receivers |
||||||
|
private function CleanDiskQueue() |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
|
||||||
|
while (i < diskQueue.length) |
||||||
|
{ |
||||||
|
if (receivers[i].GetLifeVersion() == receiversLifeVersions[i]) |
||||||
|
{ |
||||||
|
i += 1; |
||||||
|
continue; |
||||||
|
} |
||||||
|
_.memory.Free(diskQueue[i]); |
||||||
|
diskQueue.Remove(i, 1); |
||||||
|
receivers.Remove(i, 1); |
||||||
|
receiversLifeVersions.Remove(i, 1); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
diskSaveCooldown = 0.25 |
||||||
|
maxWorkUnits = 10000 |
||||||
|
maxJobsPerTick = 5 |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
/** |
||||||
|
* Slot-like object that represents a request for a writing disk access, |
||||||
|
* capable of being scheduled on the `SchedulerAPI`. |
||||||
|
* Copyright 2022 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class SchedulerDiskRequest extends AcediaObject; |
||||||
|
|
||||||
|
delegate connect() |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
} |
@ -0,0 +1,47 @@ |
|||||||
|
/** |
||||||
|
* Template object that represents a job, capable of being scheduled on the |
||||||
|
* `SchedulerAPI`. Use `IsCompleted()` to mark job as completed. |
||||||
|
* Copyright 2022 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class SchedulerJob extends AcediaObject |
||||||
|
abstract; |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks if caller `SchedulerJob` was completed. |
||||||
|
* Once this method returns `true`, it shouldn't start returning `false` again. |
||||||
|
* |
||||||
|
* @return `true` if `SchedulerJob` is already completed and doesn't need to |
||||||
|
* be further executed and `false` otherwise. |
||||||
|
*/ |
||||||
|
public function bool IsCompleted(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Called when scheduler decides that `SchedulerJob` should be executed, taking |
||||||
|
* amount of abstract "work units" that it is allowed to spend for work. |
||||||
|
* |
||||||
|
* @param allottedWorkUnits Work units allotted to the caller |
||||||
|
* `SchedulerJob`. By default there is `10000` work units per second, so |
||||||
|
* you can expect about 10000 / 1000 = 10 work units per millisecond or, |
||||||
|
* on servers with 30 tick rate, about 10000 * (30 / 1000) = 300 work units |
||||||
|
* per tick to be allotted to all the scheduled jobs. |
||||||
|
*/ |
||||||
|
public function DoWork(int allottedWorkUnits); |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
} |
Loading…
Reference in new issue