image

Идея несколько необычна, но вполне интересна — диспетчер задач для вашего кода в браузере. Даже с учётом современной ситуации js-пей полных аналогов не найдено. Отдалённо похожий диспетчер был обнаружен только в ExtJS.

Это статья о небольшой библиотеке, которая позволяет управлять отложенным выполнением кода несколько более удобно нежели setTimeout и setInterval. Задания могут останавливаться, запускаться, выполняться немедленно. Есть графический интерфейс и логи.

Однозначно не несёт никакой пользы опытному разработчику но будет интересна новичку, например для отладки или управления обновлением данных через http-запросы.

И да, это не про Node.js, библиотека написана для старого доброго браузерного Javascript.

Базовая функциональность


Библиотеку и пример использования можно скачать здесь. Для использования её необходимо подключить к проекту привычным вам способом, например через тэг

    <script type="text/javascript" src="chronoshift.js"></script>

Базовая сущность, с которой мы работаем — задача, task. В ней хранится исполняемый код, параметры выполнения и информация для чтения человеком. Подробный пример:

task:{
  // код, который будет выполнять эта задача
  handler: ()=>{conslole.log("Hello world!")}
  // задержка перед выполнением
  delay: 5000,
  // повторять или нет, по сути выбор между setTimeout и setInterval
  repeat: false,
  // имя задачи, должно соответствовать правилам именования переменных
  name: "task1",
  // описание задачи, используется только в логах и интерфейсе
  description: "This task created at the name of Invisible Pink Unicorn!"
  // время выполнения, для цикличных задач время первого запуска
  at: "2017-09-20",
  // идентификатор процесса
 pid: 42
}

Для хранения и управления ими создадим экземпляр библиотеки:

var verboseLogs = true,
  verboseTime = true,
  writeLogs = true;
var cs = new Chronoshift(verboseLogs, verboseTime, writeLogs) ;

Имена переменных достаточно понятны, однако разъясним аргументы конструктора подробнее:
verboseLogs — требуется ли выводить логи работы в консоль
verboseTime — требуется ли выводить время события, может использоваться в вызове без вывода лога
writeLogs — требуется ли хранить логи.

Хранение логов может стать достаточно дорогой операцией, примерно 0.1 КБ на событие. В современном мире эта цифра может показаться смешной, но… Тема хорошо раскрыта в «Ученике Чародея» Диснеем ещё в прошлом веке.

Для создания задач используется метод runTask. Очевидно, что вызывать его по полному описанию было бы довольно непродуктивно. Необходимый минимум — функция и задержка:

cs.runTask( () => {console.log("task 1 executed")}, 3000);

Этот код создал задачу, которая выведет в консоль сообщение с задержкой в три секунды. Однако в консоли мы увидим чуть больше ожидаемого:

cs.runTask( () => {console.log("task 1 executed")}, 3000)
[20.09.2017, 12:33:58.326]
Creating task: {"delay":3000}
[20.09.2017, 12:33:58.327]
Added a task task_7a0dh9bllhg with timeout 3000
2
task 1 executed
[20.09.2017, 12:34:01.327]
Task executed: task_7a0dh9bllhg

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

В пояснении нуждается цифра 2 — это идентификатор процесса. Его выкидывает в консоль нативная функция setTimeout/setInterval. Подробнее можно почитать здесь.
Далее задача была выполнена и лог этого также был выведен.

Далеко не всегда нужны столь подробные логи. Способ логирования можно поменять в любой момент, не только в конструкторе, нужно вызвать метод setLogging, который принимает на вход те же аргументы:

cs.setLogging(false, false, true);

Теперь лишняя информация не будет выводиться в консоль, но доступ к ней останется через cs.logs. Если вам потребуется посмотреть логи в читаемом виде, вызовите метод showLogs:



Немного сахара: библиотека умеет создавать задачи по дате/времени. Стоит признать, что этот метод пока сильно не додуман, однако весьма интересен. В приведённых ниже примерах происходит автодополнение даты.

В первом вызове дата по умолчанию — сегодня, но если времени уже больше 23:55, то вызов будет назначен на завтра. Во втором примере по умолчанию задача назначится на этот год, но если седьмое июля уже прошло то вызов будет назначен на следующий год.

//выполним код за пять минут до полуночи
cs.runTaskAt(()=>{console.log("Уж полночь близится, а Германа всё нет!");}, "22:55", "German");
//или утром седьмого июля
cs.runTaskAt(()=>{console.log("Пора готовить топоры!");}, "07-07 07:07:07", "p777", "Время трёх топоров");

Ближе к концу статьи приведено описание подводных камней этого метода.

Управление задачами


Несмотря на то, что для создания задачи обязательны только два параметра работать с ней намного удобнее при использовании несколько более полного описания при создании. Давайте попробуем сделать так:

cs.runTask( () => {console.log("task 1 executed")}, 3000, false, "task1");

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

cs.executeTask("task1");

Сработает этот метод вне зависимости от того была ли наша задача выполнена или ещё только ожидает выполнения. Собственно, и в момент выполнения, если вы сумеете удачно синхронизировать вызовы, она также будет выполнена.

Ещё два метода сильно облегчают отладку, это stopTask и restartTask. Названия более чем говорящие, приведём код их вызова для лучшего запоминания:

cs.stopTask("task1");
cs.restartTask("task1");

Первый метод помечает задачу как остановленную и отменяет её выполнение. Второй метод возвращает задачу в очередь исполнения если она была остановлена или уже выполнилась либо перезапускает её с прежними параметрами но с текущего момента времени.

Заметьте, что перезапуск выполняется именно как перезапуск, не как продолжение прерванного интервала. Например:

// создадим задачу, которая выполнится через три секунды
cs.runTask( () => {console.log("task 1 executed")}, 3000, false, "task1");
...
//какой-то код выполнялся две секунды, задача должна выполниться через секунду но мы её остановим
cs.stopTask("task1");
...
//какой-то код выполнялся ещё две секунды, то есть всего прошло 4 секунды и мы перезапускаем задачу
cs.restartTask("task1");
//задача выполнится ещё через три секунды, итого семь секунд спустя

И последний метод, который хотелось бы упомянуть это removeTask, который, как видно из названия, полностью удаляет задачу. От остановки он отличается именно удалением из памяти, не только из списка. Удалённый метод будет полностью недоступен, однако также будет освобождена вся память, которая могла на нём замусориться через замыкания. Выглядит вызов так:

//в качестве аргумента для любого метода изменяющего задачи могут использоваться как имя так и  id процесса
 cs.removeTask(2);

Графический интерфейс


Говорить тут особо не о чем. Вот скриншот из примера, который лежит в репозитории:



Для запуска необходимо трижды нажать клавишу Ctrl, закрывается интерфейс так же либо нажатием Esc.

Расти, прямо скажем, есть куда. Выполняются все перечисленные функции, в планах добавить управление режимом логирования и вызов showLogs и showTasks, просто для завершённости.

Подводные камни


Для проекта в 500 строк кода их ожидаемо немного, но достойны упоминания.

Камень первый, вызов setTimeout в методе runTaskAt. Казалось бы, какой может быть подвох? Собственно тут и подвоха-то особого нет, просто сложно предположить, что такая задержка может кому-то потребоваться. Итак, по порядку.

Метод runTaskAt получает на вход дату в текстовом формате и, при необходимости, дополняет её нужными значениями. Например, в приведённом выше коде такое значение «07-07 07:07:07». Поскольку статья написана в сентябре, дата дополнилась до седьмого июля следующего года. Затем дата была преобразована в метку времени, была получена разница в милисекундах между введённой датой и текущим моментом и вызвана функция setTimeout с этой задержкой.

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

Происходит это из за того, что максимально допустимая задержка для setTimeout составляет 2147483647, поскольку хранится в 32-битной целочисленной переменной, до которой и обрезается любое переданное значение. Если значение больше этого максимума то функция выполнится немедленно.

Если перевести в человекочитаемые единицы, то максимальная задержка составит чуть меньше чем 24 дня 20 часов 31 минуту и 23,65 секунды. В библиотеке поставлена проверка на переполнение.

Второй подводный камень — замыкания. Оставим его для самых пытливых читателей, поскольку это довольно распространённая задача. Борьба с замыканиями двумя разными, но схожими способами происходит в коде самой библиотеки и в коде примера.

Небольшое послесловие


В процессе написания статьи библиотека сильно изменилась, хоть и происходило это всего семь часов. Можно сказать что статья выступила в роли некоего более масштабного аналога резиновой уточки.

Каких-то особых планов по развитию проекта нет. Делался он в основном для собственного удовольствия и вряд ли тянет на жизненно необходимый инструмент. Тем не менее, область применения ему явно найдётся и некоторые улучшения обязательно будут добавлены в ближайшее время. Замечания и пожелания, если таковые появятся, будут реализованы максимально быстро, по крайней мере насколько это позволит наличие свободного времени.

Комментарии (10)


  1. kahi4
    21.09.2017 09:30
    -1

    Идея несколько необычна, но вполне интересна — диспетчер задач для вашего кода в браузере. Даже с учётом современной ситуации js-пей полных аналогов не найдено.

    Не хочу докапываться, но раз, два, три, сотни их, хотя некоторые все же просто кроны. Как и ваш, только с другим синтаксисом, по той причине, что нельзя останавливать уже работающий процесс (что в js нельзя обойти никак, не считая расстановки служебных вызовов везде по-середине).


    1. Electrohedgehog Автор
      21.09.2017 13:39
      +2

      Вы, простите, статью читали? Специально кидаете ссылки на модули Node.js, утверждая что это одно и то же? Статью обязательно следует прочесть перед комментированием.
      Из ваших ссылок сколько-нибудь адекватной может считаться только первая, но это 1) библиотека поверх библиотеки 2) не имеющая системы управления заданиями 3)без логирования 4)без графического интерфейса.
      Я писал инструмент для прозрачного выполнения кода с возможностью управления им. Вы даёте ссылки на планировщики заданий. Разница весьма существенна.


      1. mayorovp
        21.09.2017 13:45

        Почему вы считаете третью ссылку модулем node.js, который нельзя запустить в браузере?


        1. Electrohedgehog Автор
          21.09.2017 13:55
          +1

          Возможно я ошибся, но беглый просмотр не дал никаких оснований считать, что он запустится в браузере. Есть причины думать иначе?


          1. mayorovp
            21.09.2017 13:57

            Конечно же такие основания есть. Он не использует ни одного nodejs-специфичного модуля.


            1. Electrohedgehog Автор
              21.09.2017 14:05
              +1

              Да, почитал код, был неправ. Тем не менее, это опять же, другой инструмент.


      1. kahi4
        21.09.2017 18:09
        +3

        В каком месте у вас в библиотеке управление задачами? У вас такой же ровно крон, позволяющий исключать из расписания задачи, выставлять обратно, запускать мгновенно. По факту, что я кинул, позволяют плюс-минус тоже самое, только с разным синтаксисом.
        Беглый осмотр говорит, что это позволяет делать тоже самое, что и ваше


        Нет логирования — косяк библиотеки, хотя скорее всего он есть.
        Графический интерфейс вообще странное преимущество. Зачем он мне в проде? В лучшем случае, это должно быть подключаемым модулем.


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


        И я еще могу понять зачем нужен планировщик задач на js, но если вы хотите сделать удобную обертку над setTimeout и setInterval, которая позволяет вместо кода


        const timer = setTimeout(() => {}, 1000);
        
        clearTimeout();

        писать


        const task = new Task(() => {}, 1000);
        task.stop(1000)

        • я не понимаю, зачем это делать синглтоном, кроме как порождать лишние утечки памяти, забыв где-то что-то закрыть. Один черт вам дальше в коде нужно таскать PID или название таски, чтобы знать что останавливать. И опять же раз, два, много

        Хорошо, допустим, вы хотите написать некий дашборд, в котором каждый виджет живет своей жизнью и является приложением самим в себе, и вы хотите централизованно иметь возможность останавливать в них "жизнь" и восстанавливать и все такое, тогда согласен — ваша реализация может выступать в роли "уникальной", хотя конечно же не очень


        Ах да, в ней нет GUI, беру свои слова обратно.


        Немного конструктивного по коду
          //Watch keyup and waiting for Ctr Ctrl Ctrl to open control panel
          window.addEventListener("keyup", function(e){
            if (e.key == "Control")
              self.controlCount++;
            else if (e.key == "Escape")
              self.closeControlPanel();
            if (self.controlCount>=3){
              self.controlCount = 0;
              if(!self.cpOpened)
                self.openControlPanel(true);
              else
                self.closeControlPanel(true);
            }
          });
        
          this.verboseLogs = verboseLogs;
          this.verboseTime = verboseTime;
          this.writeLogs = writeLogs;
          this.runTask( () => {
            console.log("Example function executed");
          },
          42000000,
          false,
          "example",
          "This is a sample task. It will do nothing.");

        Че это оно безусловно шлет что-то в консоль, да еще и само навешивается на keypress? Дайте хотя бы метод registerGUI или типа того.


            while (this.tasks[name])
              name = "task_"+Math.random().toString(32).substr(2);

        Зачем? Введите статическое свойство у chronoshift как name, увеличивайте на 1 каждый раз при генерировании имени и не будет конфликта.
        И опять же, при попытке создать таску с уже имеющимся именем (если пользователь задал его явно) лучше кидать исключение, скорее всего это ошибка, чем автоматом присваивать случайное.


        setInterval зло. Следует всегда избегать его по-возможности хотя бы в пользу setTimeout. Да и неплохо было бы добавить возможность "приостанавливать" таску в автоматическом режиме при переходе в другую вкладку (делается через requestAnimationFrame).


        Ну и по мелочи: airbnb codestyle (хотя ваше право, но он вроде как близок к победе). Вы используете let и const, но не используете class. В итоге ни туда, ни сюда.


          this.redrawReqest = function(){
            let e = new Event("csredrawrequired"); // зачем это? А почему просто вызвать redrawReqest нельзя?
            // через свой же диспетчер задач, создав первую задачу как system.
            window.dispatchEvent(e);
          }
        
          window.addEventListener("csredrawrequired",function(){
            if (!self.cpOpened)
              return;
            let vl = this.verboseLogs,
              vt = this.verboseTime,
              wl = this.writeLogs;
            self.setLogging(0, 0, 0);
            self.closeControlPanel();
            self.openControlPanel();
            self.setLogging(vl, vt, wl);
          }, false);

        Ну это жесть конечно. И то, что перерисовывается много ненужного, и то, что стили инлайново внутри функции, а не вынесены в переменную до кода, и куча куча куча всего.


        И опять же, umd, он не просто так существует.
        Да и научите функцию принимать конкретный элемент вместо body, чтобы я сам решал где оно будет отрисовываться.


        1. Electrohedgehog Автор
          22.09.2017 06:15

          Большое спасибо вам за конструктивную критику.
          По первому пункту — просто опечатка, не доглядел. Должен вызываться this.log.
          По второму — мне не понравилась куча однотипных имён, глазу проще зацепиться за разницу в несколько символов чем в один.
          Остальное буду улучшать.
          Я правильно понимаю, что основная ваша претензия заключается в том, что аналогичные библиотеки уже существуют а я просто написал велосипед с квадратными колёсами?


          1. kahi4
            22.09.2017 10:15

            Я правильно понимаю, что основная ваша претензия заключается в том, что аналогичные библиотеки уже существуют а я просто написал велосипед с квадратными колёсами?

            Изначально я лишь заметил, что то, что аналогов нет несколько не точно. Ну а позже лишь заметил, что, фактически, это обычный запуск функций по таймеру, а не полноценный диспетчер задач с переключениями контекстов и реализацией многопоточности внутри однопоточного js, что, вероятно, было бы даже интересно, но на данный момент даже с костылями невозможно. А отсюда следует, что разница с приведенными мной библиотеками только в API.


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


            P.S. Вот вам еще один feature-request: Вообще было бы неплохо добавить еще два метода: suspend и resume, которые бы запоминали через сколько должна выполнится функция и перевыставляли таймер с этим значением.


            И, если хочется развивать эту библиотеку в реально полезную для широкого круга задач, разбейте ее на модули. Дайте возможность создавать задачи без контейнера, управлять ими через дескриптор самой задачи, а не через контейнер, контейнер сделайте модулем, заодно это упростит реализацию более сложных задач. Вынесите рендер GUI в отдельный модуль, в идеале в отдельный репозиторий (и npm пакет соответственно). Ах да, это то тоже неплохо было бы поместить в npm, уже давно никто напрямую файлики не качает.


            Ну либо двигайтесь в обратную сторону от обертки над timeout в сторону более реального планировщика ОС, заставляйте передавать не просто функцию, а объект с методами run, exit, suspend, не знаю. Дайте возможность устанавливать приоритеты "потокам", в общем — тут тоже есть где разыграться. Если сильно упороться, можно вот что сделать: каждый "процесс" должен дергать функцию типа dispatcher.processTasks, который позволит в это время переключить на другую функцию, если эта выполнялась уже слишком долго, и все такое. Это будет даже здорово и интересно, но опять же придется решать проблему с теми "потоками", которые выполняются слишком долго, при этом не дергая эту функцию. (Подсказка: webWorkers можно убивать принудительно, но сами webWorkers сделают всю вашу разработку бессмысленной, покуда их будет контроллировать "взрослый" диспетчер задач. Пока же можно просто кидать предупреждение, что "вот такой поток жрет слишком много ресурсов".)


            Хм, кажется, я только что придумал тестовое задание :)


  1. Alex_1985
    21.09.2017 13:22

    Ну не знаю, но лично я ссылочку сохранил, вдруг пригодится…