В Screeps применяется ли ограничение ЦП таким образом, чтобы можно было написать надежный код ограничения ЦП?

В Screeps использование CPU каждым игроком ограничено, но документация по этой функции не дает достаточно четкого способа применения этого кода для написания надежного кода ограничения CPU. Я рассмотрел следующие четыре возможности:


1. Цикл игрока никогда не прерывается.

В одном крайнем случае десериализация памяти игрока, выполнение основного сценария и повторная сериализация памяти никогда не прерываются, и превышение лимита ЦП просто означает, что цикл проигрывателя будет пропущен на последующих тактах, пока долг ЦП не будет погашен. Надежный код ограничения ЦП в этом случае не является строго необходимым, но все равно было бы полезно определить, когда цикл игрока пропущен, и, возможно, начать действовать более эффективно. Этого легко достичь, используя такой код:

module.exports.loop = function()
{
  var skippedTicks = 0;

  if ( 'time' in Memory )
  {
    skippedTicks = Game.time - Memory.time - 1;
  }

  // Main body of loop goes here, and possibly uses skippedTicks to try to do
  // things more efficiently.

  Memory.time = Game.time;
};

Этот способ управления использованием процессора игроками уязвим для злоупотреблений бесконечными циклами, и я почти уверен, что это не поведение Screeps.

2. Цикл игрока атомарный.

Следующая возможность состоит в том, что цикл игрока является атомарным. Если лимит ЦП превышен, цикл игрока прерывается, но не изменяется ни запланированное состояние игры, ни изменения в памяти. Повышение эффективности становится все более важным при обнаружении прерванного цикла, поскольку игнорирование этого означает, что сценарий игрока не сможет изменить состояние игры или память. Однако обнаружить прерванные циклы все еще просто:

module.exports.loop = function()
{
  var failedTicks = 0;

  if ( 'time' in Memory )
  {
    failedTicks = Game.time - Memory.time - 1;

    // N failed ticks means the result of this calculation failed to commit N times.
    Memory.workQuota /= Math.pow( 2, failedTicks );
  }

  // Main body of loop goes here, and uses Memory.workQuota to limit the number
  // of active game objects to process.

  Memory.time = Game.time;
}

2.5. Изменения в памяти являются атомарными, но изменения в игровых объектах - нет.

РЕДАКТИРОВАТЬ: Эта возможность пришла мне в голову после прочтения документации для объекта RawMemory. Если сценарий прерывается, любые запланированные изменения состояния игры фиксируются, но изменения в памяти не фиксируются. Это имеет смысл, учитывая функциональность, предоставляемую RawMemory, потому что если сценарий прерывается до запуска настраиваемой сериализации памяти, запускается сериализация JSON по умолчанию, что усложняет настраиваемую сериализацию памяти: настраиваемая десериализация должна быть способна обрабатывать JSON по умолчанию в дополнение к любому формату, который записал пользовательский serialize.

3. Операторы JavaScript являются атомарными.

Другая возможность состоит в том, что цикл игрока не атомарный, а операторы JavaScript. Когда цикл игрока прерывается из-за превышения лимита ЦП, незавершенные изменения состояния игры и изменения памяти фиксируются, но с осторожным кодированием - оператор, который выполняет вызов API Screeps, должен назначить результат вызова ключу памяти - состоянию игры Изменения и изменения памяти не будут противоречить друг другу. Написание полностью надежного кода с ограничением CPU для этого случая кажется сложным - я еще не решил эту проблему, и я хотел бы быть уверен, что это истинное поведение Screeps, прежде чем пытаться его выполнить.

4. Ничто не атомарно.

С другой стороны, даже отдельные операторы не являются атомарными: оператор, назначающий результат вызова API Screeps ключу в памяти, может быть прерван между завершением вызова и назначением результата, и неполное изменение состояния игры и неполные изменения памяти (которые теперь несовместимы друг с другом) совершаются. В этом случае возможности написания надежного кода, ограничивающего процессор, очень ограничены. Например, хотя наличие значения, записанного в Memory с помощью следующего оператора, несомненно, указывает на то, что вызов API Screeps завершен, его отсутствие не будет означать, что вызов не завершился:

Memory.callResults[ Game.time ][ creep.name ] = creep.move( TOP );


Кто-нибудь знает, что это за поведение Скрипса? Или это что-то еще, что я не учел? Следующая цитата из документации:

Ограничение CPU на 100 означает, что через 100 мс выполнение вашего скрипта будет прекращено, даже если он еще не выполнил какую-либо работу.

намекает, что это может быть случай 3 или случай 4, но не очень убедительно.

С другой стороны, результат эксперимента в режиме симуляции с одним ползучестью, следующий основной цикл и выбор "Завершить" в диалоговом окне для неотвечающего скрипта:

module.exports.loop = function()
{
  var failedTicks = 0;

  if ( 'time' in Memory )
  {
    var failedTicks = Game.time - Memory.time - 1;

    console.log( '' + failedTicks + ' failed ticks.' );
  }

  for ( var creepName in Game.creeps )
  {
    var creep = Game.creeps[ creepName ];

    creep.move( TOP );
  }

  if ( failedTicks < 3 )
  {
    // This intentional infinite loop was initially commented out, and
    // uncommented after Memory.time had been successfully initialized.

    while ( true )
    {
    }
  }

  Memory.time = Game.time;
};

было то, что крип двигался только на тиках, где бесконечный цикл был пропущен, потому что failTicks достиг своего порогового значения. Это указывает на случай 2, но не является окончательным, поскольку ограничение ЦП в режиме симуляции отличается от онлайн - оно кажется бесконечным, если не завершено с помощью кнопки "Завершить" в диалоговом окне.

3 ответа

Решение

Случай 4 по умолчанию, но модифицируемый для случая 2.5

Как подозревали nehegeb и dwurf, и эксперименты с частным сервером подтвердили, поведение по умолчанию - вариант 4. Изменения как игрового состояния, так и памяти, которые произошли до фиксации прерывания.

Однако выполнение сериализации JSON по умолчанию основным циклом сервера контролируется наличием недокументированного ключа _parsed в RawMemory; значение ключа является ссылкой на память. Удаление ключа в начале основного цикла сценария и его восстановление в конце приводит к тому, что весь набор изменений памяти, вносимых основным циклом сценария, становится атомарным, т.е. случай 2.5:

module.exports.loop = function()
{
  // Run the default JSON deserialize. This also creates a key '_parsed' in
  // RawMemory - that '_parsed' key and Memory refer to the same object, and the
  // existence of the '_parsed' key tells the server main loop to run the
  // default JSON serialize.
  Memory;

  // Disable the default JSON serialize by deleting the key that tells the
  // server main loop to run it.
  delete RawMemory._parsed;

  ...

  // An example of code that would be wrong without a way to make it CPU limit
  // robust:

  mySpawn.memory.queue.push('harvester');
  // If the script is interrupted here, myRoom.memory.harvesterCreepsQueued is
  // no longer an accurate count of the number of 'harvester's in
  // mySpawn.memory.queue.
  myRoom.memory.harvesterCreepsQueued++;

  ...

  // Re-enable the default JSON serialize by restoring the key that tells the
  // server main loop to run it.
  RawMemory._parsed = Memory;
};

Один из "советов дня" в игре гласит:

TIP OF THE DAY: If CPU limit raises, your script will execute only partially.

Поэтому я бы сказал, что это, скорее всего, # 4!
Как говорит dwurf, следующий подход к компоновке скрипта в большинстве случаев должен помочь:

Большинство игроков решают эту проблему, просто помещая критический код в начале своего основного цикла, например, сначала стоит код башни, затем код возрождения, затем движение / работа по крипу. [...]

Это не № 1 или № 2. Держу пари, что это №4. Было бы разумнее отслеживать использование ЦП вне основного цикла и уничтожать его при достижении предела. № 3 потребует сложного кода на сервере screeps для выполнения транзакций на уровне операторов. Как вы обнаружили, в симуляторе нет ограничений по процессору.

Большинство игроков решают эту проблему, просто помещая критический код в начале своего основного цикла, например, сначала стоит код башни, затем код возрождения, затем движение / работа по крипу. Это также защищает от необработанных исключений в вашем коде, так как ваши наиболее важные функции (надеюсь) уже будут выполнены. Это плохое решение для ограничения ЦП, хотя, по моим наблюдениям, ваш код пропускается каждые 2 такта, как только вы используете весь ЦП в своем сегменте и постоянно превышаете свой обычный лимит.

У меня сейчас нет проблем с процессором (у меня есть подписка), но я бы решил эту проблему, поместив код, интенсивно использующий процессор, в конец и, по возможности, выполняя его только тогда, когда у вас достаточно процессора в корзине и вы находятся далеко от предела 500 тактов на такт. Это также помогает иметь более крупные крипы, обычно для нахождения пути или даже просто движения (0,2 за ход) требуется значительная часть процессорного времени, а большие крипы означают меньшее количество крипов.

Другие вопросы по тегам