Планировщик маршрутов v2.0

Я послушал ваши отзывы и комментарии по поводу моего небольшого проекта. Реализовал некоторые из высказанных идей и, конечно, добавил к ним часть своих.

  1. Самое главное, самое сложное и самое незаметное: поменялся алгоритм поиска. Вместо точного и медленного метода ветвей и границ теперь почти точный и метаэвристичекий метод муравьиной колонии. В основе проекта лежит Travelling Salesman Problem (Задача коммивояжёра), которая является NP-полной. А это означает, что нет другого пути решения её, кроме как перебрать все варианты. Количество вариантов можно посчитать по формуле \(n! \over 2\), где \(n\) — количество промежуточных городов. Если подставить в эту формулу, например, 15 получим 653 837 184 000 вариантов, а это уже очень много. Поэтому пришлось воспользоваться приближенным алгоритмом, который работает быстро, но и решение выдаёт не всегда оптимальное. Кстати, так как внутри алгоритма задействован генератор случайных чисел, решение может меняться от запуска к запуску, достаточно нажать кнопку обновить.

  2. Я убрал из проекта AngularDart, что позволило уменьшить размер JavaScript-файла в 3 раза.

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

  4. Упростилось добавление городов в маршрут. Из вариантов объектов теперь убираются лишние, если вариант только один, то он добавляется автоматически. И самое главное: можно добавлять города, даже не прикасаясь к мышке, используя для выбора варианта клавиши «Вверх», «Вниз» и «Enter».

  5. А ещё теперь можно развернуть карту на весь экран, чтобы было удобнее её изучать.

  6. Исправлены ошибки.

Маршрут по Европе

Маршрут по Европе

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

Проблемы с запуском планировщика

Поделюсь некоторыми сложностями, с которыми я столкнулся, запуская планировщик маршрута.

Конечно, во время самой разработки все сложности сводились к алгоритмическим. В Dartium всё работало как положено с нужным количеством ошибок и предупреждений. Проблемы начались сразу после компиляции в JavaScript и запуске в обычном браузере (кто бы сомневался). Проект не запустился, выдав странного вида stack trace. Ну что же, пересобираем проект без минификации. Stack trace стал читаемым, ошибки понятными. Как оказалось, вся проблема была в аннотации @MirrorsUsed. Дело в том, что mirrors (так называется reflection в Dart) занимает непозволительно много места в скомпилированном коде. И для того, чтобы этого избежать, авторы Dart предлагают использовать эту аннотацию для указания библиотек и классов, информацию о которых нужно будет получить во время исполнения (кстати, в будущем обещают доработать компилятор, что избавит от необходимости использовать эту аннотацию). Так вот, мой класс контроллера не был указан в @MirrorsUsed и, соответственно, AngularDart не мог с ним работать.

Исправил, скомпилировал, не запустилось. Правда, в этот раз я увидел несколько больше, чем просто пустую страницу. Немного поисков — и обнаружилась вторая проблема: нельзя в шаблонах AngularDart использовать методы встроенных объектов Dart. То есть в Dart можно, а в JavaScript нельзя. Конкретно в моем случае было написано:

{{segment[0].distanceTo(segment[1]).round()}}

Понятное дело, JavaScript и не догадывается о методе round у типа double. Да, собственно, про существование double он тоже не догадывается. Поэтому нужно писать геттер или вспомогательную функцию. Например так:

{{segment[0].distanceToRound(segment[1])}}

Ура, запустилось! Теперь осталось собрать минифицированную версию и выложить. Но после сборки проект снова упал с непонятным stack trace, как и в самом начале.

Не один час был потрачен на гугление всевозможных причин. Дело почти дошло до того, чтобы написать новый багрепорт о неправильной компиляции. Но я вовремя остановился, и хорошо, потому что ошибка была у меня и, кстати, довольна простая: я пытался проинициализировать Яндекс.Карты, а функция-конструктор не существовала. Вот проблемная строка, она соответствует map = new ymaps.Map(“map”, options); в JavaScript:

map = new JsObject(context['ymaps']['Map'], ["map",  options]);

Но ведь несжатая версия работает! Моё предположение, что сжатая версия приложения инициализируется значительно быстрее и поэтому API Яндекс.Карт ещё не готово к тому времени, оказалось верным, и вызов функции ymaps.ready всё исправил.

Итог. Более-менее сложное Dart-приложение хорошо работает в браузерах без поддержки Dart. Но зато когда сталкиваешься с проблемой, которая возникает только в скомпилированном и минифицированном коде, можно потратить довольно много времени на поиск решения просто потому, что подсказок никто не даст, а код выглядит довольно запутанно. Но в своей практике я сталкивался со случаями, когда и обычное JavaScript-приложение после сжатия начинало вести себя неправильно и возникали те же проблемы поиска решения.

Хочется отметить минификатор JS, встроенный в Dart. Он заменяет все ; на переводы строк. Из-за этого в результате получается файл со множеством строк, а не с одной, как в случае с другими минификаторами. А это, в свою очередь, сильно упрощает ручной анализ кода.

Планировщик маршрутов

Здравствуйте, дорогие читатели!

За окном вовсю сияет март, а мы уже начали готовиться к новому большому летнему путешествию (обязательно посмотрите предыдущее, если, конечно, ещё не видели его). К сожалению, идея проехать Соединённые Штаты поперёк накрылась медным тазом отложена до лучших времён. Но это не причина для уныния, ведь есть ещё сотни стран, требующих нашего внимания. И мы решили не заморачиваться и повторить прошлогодний опыт бэкпэкерства с InterRail, благо пол-Европы осталось неизъезженной.

Сказано — сделано. Набросали списочек городов для посещения и оказалось, что маршрут, в отличие от предыдущего года, не очевиден. В каком порядке их нужно объезжать, чтобы расстояние, преодолённое на поезде, получилось минимальным? Поиск нужных сервисов для составления маршрутов ничего не дал, а значит нужно написать свой! Что я и сделал.

Представляю вам планировщик маршрутов. Сервис довольно простой: вводишь начальный город, конечный, а также список городов посередине, — и получаешь оптимальный маршрут. Расстояния между городами считаются по прямой, поэтому результат оптимален только для самолётов. А для остального транспорта стоит учитывать, что дороги кривые и ездят по ним с разной скоростью. Но для того, чтобы прикинуть маршрут, информации достаточно. Конечно, между некоторыми пунктами дороги может вообще не существовать, но не беда: любой отрезок из пути можно исключить и система выдаст другой маршрут с учётом этого ограничения.

Проложить свой маршрут

Проложить свой маршрут

Для приготовления сервиса мне понадобились: Яндекс.Карты, AngularDart, Bootstrap. Туда пришлось добавить немного логики и свободного времени. Всё это было смешано, взболтано, вылито в высокий бокал и украшено методом ветвей и границ, прямо как учили в универе. Приятного аппетита.

Прошу опробовать и написать свои замечания и пожелания в комментариях. Исходники всего это дела можно увидеть всё там же — на GitHub.

Всегда к вашим услугам,
Редакция блога.

И снова Dart

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

  1. Поддержка Dart в продуктах JetBrains просто отличная. Писать на Dart гораздо удобнее, чем на JavaScript, а ведь плагин для JavaScript разрабатывается и улучшается намного дольше. И всё это из-за более строгой структуры языка. Она позволяет IDE лучше просчитывать зависимости и в нужное время дополнять необходимые конструкции. Но не обошлось без ложки дёгтя. Иногда плагин просто перестаёт дополнять что бы то ни было, спасает только перезагрузка среды. И ещё не хватает привычных intentions, вроде склеить два if в один или заменить одинарные кавычки на двойные, но я думаю, что это будет реализовано со временем.

  2. В Dart есть приватные члены класса, но нет зарезервированного слова private. Всё потому, что все идентификаторы, начинающиеся с подчёркивания (_), считаются приватными. И это правильно.

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

    class Singleton {
      static final Singleton _singleton = new Singleton._internal();
      factory Singleton() {
        return _singleton;
      }
      Singleton._internal();
    }
    
    var singleton = new Singleton();

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

  4. В языке довольно много различного синтаксического сахара: тот же cascade operator (..). Крайне удобная штука для работы с DOM и стилями.

    element.style
      ..visibility = 'visible'
      ..display = 'block';
  5. Ещё прекрасно сделана подстановка значений переменных и выражений в строку. Это выглядит частью языка, а не afterthought, как, например, в PHP.

    previousLink.attributes['title'] += ' (${isMac ? '⌥←' : 'Ctrl + ←'})';

    Обращаем внимание на то, что внутри ${...} может быть любое выражение точно в таком же виде, в каком вы бы его увидели и без строки. Кавычки внутри выражения не закрывают строку, и их не нужно экранировать, что вполне логично, но в том же PHP этого нет.

  6. Dart — язык с необязательной типизацией. То есть если хочешь, можно указать тип переменной, а можно просто написать var и не париться. Всё это будет работать до тех пор, пока не попадётся, например, функция с указанным типом параметров. Вот тут-то тип твоей переменной будет проверен. И будет выведена ошибка, если типы не совпадают. Но когда вместо var указываешь реальный тип переменной, то проверки происходят гораздо чаще, буквально в каждом операторе, везде, где может возникнуть несовместимость типов.

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

    String str = '';
    if (!str) {
      // do something
    }

    Но не стоит особо волноваться: о большинстве потенциальных проблем статический анализатор языка (то есть IDE) сообщит заранее.

    Все эти проверки работают в так называемом «checked mode». Чтобы этот режим включить, виртуальная машина должна быть запущена со специальным параметром, например:

    dart --checked main.dart

    Или в случае с сайтом сам браузер нужно запускать так (пример для Linux):

    DART_FLAGS='--checked' ./chrome

    Если этого не сделать, то виртуальная машина будет работать в production-режиме и ни одной дополнительной проверки не будет сделано. Зато выполняться скрипт будет очень быстро! Поэтому можно думать о типах, как об автоматических инструкциях assert в языке в помощь разработчику. Кстати, просто assert тоже присутствует и тоже не работает в production-режиме.

    А ещё в Dart есть Generics. И это позволяет создавать весьма сложные зависимости типов и опять же проверки. К сожалению, Generics можно применить только к классам, может, в будущем добавят поддержку отдельных функций.

    class ParseContext<S extends Iterable> {
      final S source;
    
      // More definitions
    }
  7. Переопределение операторов. Не думаю, что нужно рассказывать про плюсы и минусы этого решения. Все, кто когда-либо сталкивался с ними в любом другом языке, и так о них знают. Динамический Dart может добавить к этому ещё немного проблем. И хотелось бы иметь возможность создания произвольных операторов, как в Haskell. Было бы круто.

  8. Из недостатков можно выделить то, что интерфейсы визуально не отличаются от классов. То есть нет зарезивированного слова interface, вместо него используется тот же class. Но при этом есть слова и extends, и implements. Всё различие заключается в том, что может присутствовать в классе, но не может присутствовать в интерфейсе, и наоборот. К примеру, в интерфейсе может быть factory-конструктор, который будет возвращать реализацию этого интерфейса по-умолчанию. Но в этом случае не получится унаследоваться от такого интерфейса при помощи extends. Компилятор выдаст «The generative constructor expected, but factory found». А это не особо очевидно, особенно когда видишь это сообщение первый раз.

  9. Ещё один минус — размер js-файла на выходе. Пока не у всех есть браузер с нативной поддержкой Dart, и многим придётся загружать сконвертированный код. Ладно, будем честными: браузера пока ни у кого нет, кроме разработчиков, которые на Dart пишут (он называется Dartium, его можно скачать на сайте проекта). Так вот, сгенерированный JavaScript для этого блога весит 207 Кб (сравните с 8 Кб минифицированного Dart-кода). А всё потому, что в js-файл компилятору приходится встраивать всю ту огромную прослойку для поддержки разницы в DOM и событиях между языками.

Я думаю, что Google не оставит своих планов выпустить nextgen язык для Web. Меня лично эти планы вполне устраивают, так как я отлично представляю, чем этот язык будет лучше и удобнее. Как минимум он позволит выбросить весь тот backward-compatibility хлам, который уже давно никто не использует, но который обязан присутствовать в каждом браузере, потому что его могут использовать сайты, созданные 15 лет назад.

И не надо говорить, что всякие надстройки используют те, кто не осилил выучить нативный JavaScript.

P. S. К сожалению, нормальной поддержки подсветки Dart в блоге пока нет, но я надеюсь это в скором времени исправить. Исправил.

lowmemorykiller

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

Вс началось с того, что сайт перестал автоматически обновляться при добавлении поста или комментария. Вероятно, это произошло, когда я переехал на другой хостинг. Конечно же, первой мыслью было то, что перестал нормально отрабатывать WebHook на GitHub. Если кто не в курсе, WebHook — это запрос на определённый URL, который GitHub выполняет при коммите. Так вот, из настроек GitHub нужный URL никуда не исчез, да и изучение логов сервера показало, что запросы приходят нормально.

Запустил обновление ручками и увидел в конце консольного вывода строчку «Killed». Запустил на домашней машине — нет ошибок. Посмотрел потребление памяти и загруженность процессора. Показатели были в пределах нормы, да и полная перегенерация сайта (гораздо более долгая и затратная операция) отрабатывала как надо. Но я обнаружил, что swap-раздел отключён. Я его довольно быстро включил обратно, но проблема не исчезла. Swap не использовался, это и укоренило меня в мысли, что проблема не в ресурсах. А раз не в ресурсах, значит что-то не так с самой программой. Не буду рассказывать сколько вариантов компиляции я перепробовал, сколько исходников просмотрел. Ничего не помогало, более того, строка «Killed» нигде не встречалась. На некоторое время я отложил баг в сторону и запускал полную перегенерацию сайта по мере необходимости.

И уже потом я наткнулся на StackOverflow, что «Killed» может выдаваться ядром Linux в случае, если процессу пришёл сигнал SIGKILL (безапелляционное закрытие процесса). Но кто может отправлять этот сигнал моему процессу? Ну конечно, OOM (Out of Memory) Killer! Но ведь ресурсов достаточно, да и swap не используется, какой же тут Out of Memory? А ещё, судя по форумам, в системном логе (cat /var/log/kern.log или dmesg) при закрытии процесса OOM Killer’ом должна появляться строчка, что-то вроде:

ruby invoked oom-killer: gfp_mask=0x200da, order=0, oom_adj=0, oom_score_adj=0 ruby cpuset=/ mems_allowed=0 Pid: 27311, comm: ruby Not tainted 2.6.32-279.el6.x86_64 #1

У меня же были совсем другие записи.

Feb  4 02:28:44 dikmax kernel: [1430123.088968] select 1 (init), adj 0, size 146, to kill
Feb  4 02:28:44 dikmax kernel: [1430123.089000] select 378 (rsyslogd), adj 0, size 377, to kill
Feb  4 02:28:44 dikmax kernel: [1430123.089011] select 5584 (dropbox), adj 0, size 21725, to kill
Feb  4 02:28:44 dikmax kernel: [1430123.089018] select 17651 (dikmax-name), adj 0, size 86942, to kill
Feb  4 02:28:44 dikmax kernel: [1430123.089020] send sigkill to 17651 (dikmax-name), adj 0, size 86942

И всё, никаких деталей. Дальнейшие поиски навели на информацию о том, что похожие строчки могут встречаться на Android. И там их выдаёт lowmemorykiller — аналог OOM Killer. Ещё несколько запросов в гугл вывели причину их появления у меня: оказывается, lowmemorykiller был портирован в ядро Ubuntu (возможно, только серверную версию) и некоторое время там уже обитает.

Всё это хорошо, но ведь как показывают замеры, у моего приложения не было проблем с памятью и swap не использовался. Почему же тогда lowmemorykiller так нехорошо себя ведет? Ответ придет, если вспомнить, что Android — система с небольшим количеством памяти и совсем без swap-раздела. И lowmemorykiller ведёт себя соответственно платформе. Зачем ждать, пока память закончится, можно начинать убивать процессы заранее. Вот и у меня в соответствии с настройками первые кандидаты на убийство появлялись, если свободной памяти становилось меньше 64 Мб, а это целых 12.5% от памяти на сервере. Понятное дело, что до swap дело даже не доходило, что, конечно, не так плохо для общей производительности, но плохо для некоторых приложений.

Понимание проблемы позволило заняться поиском решения. Настойки lowmemorykiller можно поменять во время работы сервера с помощью записи в системные файлы /sys/module/lowmemorykiller/parameters. Например, вот такая команда фактически выключает lowmemorykiller:

sudo echo '9999' > /sys/module/lowmemorykiller/parameters/adj && echo '1' > /sys/module/lowmemorykiller/parameters/minfree

Осталось только добавить команду в автозапуск. Как всегда crontab придёт на помощь. Только не забываем, что конкретно эта команда должна выполняться от имени root-пользователя и редактировать crontab нужно от его же имени (sudo crontab -e).

@reboot echo '9999' > /sys/module/lowmemorykiller/parameters/adj && echo '1' > /sys/module/lowmemorykiller/parameters/minfree

По итогу 512 Мб оказалось недостаточно для нормального запуска обновления сайта. Но учитывая то, что оно выполняется всего несколько раз в неделю и не продолжается дольше 1-й минуты, использование 50 Мб swap мне не кажется большой проблемой.

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

← СтаршеМоложе →