[О блоге] [наверх] [пред] [2020-11-22 15:50:00+03:00] [2fd3f8ce25da63ca16af06964493c87637c9d817]
Темы: [redo]

Снова доработки goredo, redo-stamp и хэширование

Вчера обнаружил что при REDO_NO_HASH на самом деле не работает
redo-stamp поведение. Ничего не падает, но цель остаётся изменённой,
даже если stamp остаётся прежним.

Сам по себе redo-stamp у меня был некрасивым хаком: если во время
определения OOD (out-of-date) цели мы понимаем что она со штампом, то в
конце, если она требует пересборки, запускаем выполнение этой цели,
прямо внутри OOD функции, рекурсивно вызываемой. Это мешало
распараллеливанию -- одна проблема. По сути я делал изменение порядка
выполнения целей и зависящие штампа (а это так или иначе связанные с
redo-always целями), выполняя их раньше, а дальше, благодаря хэшам (на
штампам!), остальные считали что цель не изменилась.

Отключение хэшей приводит к тому, что цели всё равно, не смотря на штамп
пересобираются, ведь ctime то поменялся. А алгоритм понимания OOD цели
взят из redo-c и предельно прост: прочитать .dep, пройтись по каждой
записи в нём, если она имеет .dep, то рекурсивно вызвать OOD на неё.
Просто "сверху вниз" идём и смотрим не устарел ли кто. С отключением
хэшей приходится из-за штампов *заранее* понимать является ли цель
-always или -stamp, а также добавлять состояние о том что в *текущей*
сборке штамп не изменился. Это в итоге у меня заработало, есть коммит,
но это сущий ад уродства и хака, когда мы не просто спускаемся, а
заранее читаем и узнаём про зависимость. Плюс я ещё не добавлял
распараллеливание выполнения out-of-order always/stamped целей (так как,
скорее всего, такие цели на практике будут лёгкими (определение
переменных окружения, версии какой-нибудь), то это не создаёт проблем.
Но в общем случае оно страшно.

Если что-то всё слишком усложняется и добавляется хак то тут, то там
(например просасывание текущего stamp чтобы понять, в момент вызова
redo-stamp, остался ли он прежним), то что-то делается не так. Моя не
работающая версия с redo-stamp по сути делала только одно, давая нужное
и ожидаемое на самом деле поведение (поэтому я был уверен что оно
работало) -- меняла порядок выполнения целей, а дальше хэши делали
нужное поведение.

А не хотелось бы пользователю прописать redo-stamp в каждой цели? В
идеале хотелось бы, поэтому redo-stamp выглядит полнейшим уродством.
Теперь я яростно против него, видя как он на самом деле всё усложняет.
А что говорит apenwarr?
https://redo.readthedocs.io/en/latest/FAQImpl/#why-not-always-use-checksum-based-dependencies-instead-of-timestamps

Могут быть проблемы с пустыми файлами. Так как их хэши всегда остаются
прежними и цель будет считаться всегда свежей. Это единственный аргумент
который я принимаю... в общем случае, если бы не тот факт, что и redo-c
и apenwarr/do и apenwarr/redo и goredo при отсутствии stdout выхлопа не
создают файл, а отсутствие файла является признаком всегда протухшей
цели. На практике из-за этого нет никаких проблем и с желанием делать
пустые файлы, которые не устаревают (например создание какого-нибудь .rc
в котором реально нет никаких команд, выставляющих флаги), и с желанием
делать псевдо-цели "агрегаты". Если есть желание не делать выхлоп, но
всё равно иметь stamp -- ну так сделать запись в файл какого-нибудь хэша
от нужных данных, а файл не использовать. "Родное" хэширование
(go)redo(-c) из-за малого объёма данных будет супер лёгким. Поэтому на
практике это не проблема, повторюсь. Но она требует полноценной
поддержки и работы с stdout захватом и $3 файлом, чего некоторые
реализации не делают, считая захват stdout вредом. Ну вот и получается:
если забить на оригинальное предложение DJB с stdout/$3 -- имеем кучу
проблем и негибкостей, например при постоянной проверке хэшей. Сами
дураки что отказались -- сами же себе создали проблем. Те кто создают
пустые файлы при отсутствии stdout (я уверен что автор redo-c плохо
подумал, влив чей-то глупый pull request в последних коммитах) -- сами
же себе создали невозможность создания агрегатов и постоянно протухающих
целей. Короче, DJB всё предложил как никогда правильно и корректно!

Что ещё apenwarr говорит на тему "почему бы не всегда делать хэши?"

* It makes it hard to force things to rebuild -- просто touch-ем
  действительно не выйдет. Но есть redo (не -ifchange, который по
  умолчанию делает force)
* Targets that are just used for aggregation -- это снова пустые файлы.
  С ними нет проблем, так как они будут отсутствовать
* Calculating checksums for every output file adds time to the build --
  учитывая скорость современных быстрых хэшей (BLAKE2/3, Skein), даже
  просто на современных файловых системах всегда все данные хэшируются
  (ZFS). В теории оно конечно добавляется время, так как оно в любом
  случае что-то делает/занимает на CPU, но так ли это увеличивает время,
  чтобы жертвовать простотой софта и удобством пользователя не заставляя
  его писать каждый раз и думать где нужен redo-stamp? Сколько времени
  будет сэкономлено потому что он не поставил redo-stamp, а выполнение
  какой-нибудь сверх лёгкой цели типа "echo ${GO:-go}" займёт ГОРАЗДО
  больше времени, так как нужно запустить дочерний sh процесс. Я убеждён
  что на практике это совсем не аргумент. Ну и речь же не про абсолютно
  всегда проверку хэша, а если, например, только ctime поменялся --
  поэтому если на диске будет лежать гигабайтный tarball, который просто
  лежит, то хэшироваться он не будет при проверке OOD
* Building stuff unnecessarily and then stamping it is much slower than
  just not building it in the first place, so for almost every use of
  redo-stamp -- не понял к чему, ведь как-раз stamp-ы наоборот позволяют
  не собирать unnecessarily. Или это к тому, что проще собрать "echo
  ${GO:-go}" чем проверять хэш? Возможно только если процесс хэширования
  очень медленный, что не правда на практике
* To steal a line from the Zen of Python: explicit is better than
  implicit. Making people think about when they're using the stamp
  feature. -- Нет уж, снова не согласен и автор противоречит себе.
  Почему же ifcreate/ifchange implicitly ставятся? А как же Zen?
  Потому что это точно удобно будет всем, оно уменьшает нагрузку на
  пользователя, помогает ему (а инструмент должен помогать!). Checksum-ы
  и желание делать redo-stamp абсолютно всегда, если создаётся $3 --
  абсолютно нормально и адекватно
* djb's (as yet unreleased) version of redo doesn't implement checksums
  а вот это не правда, ибо я сам помню что речь про checksum-ы я видел в
  его статьях. Там речь вообще только про них и была, никаких
  ctime/mtime/whatever

Как с самого начала мне не нравились ВСЕ аргументы apenwarr, так сейчас
и подавно. Сейчас на практике я вижу НАСКОЛЬКО это всё усложняет и
уродует. Проблем с aggregation targets и пустыми файлами нет, если не
забивать на stdout и не создавать пустые файлы когда пользователь это
явно не попросил (touch $3). Мне кажется, apenwarr/redo просто борется с
тем, что в Python это всё медленно, хоть сами хэши и на C, но
просасывать данные нужно через Python.

Поэтому я решил выпилить возможность отключения хэшей и выпилил всё
уродство с redo-stamp. Сама команда и даже запись в .dep остались, но
ни на что не влияют. Однако порядок выполнения целей теперь хаотичен,
как и был и как и должен был быть. И проблема в том, что определение OOD
целей, множества целей, может происходить задолго до того как
always цель выполнится. Если поставить маленький уровень параллелизма,
неспешно рассматривая OOD каждой задачи и выполняя их, то всё неплохо. В
противном случае, всё работает, но много целей будут выполнятся потому
что OOD решил что они протухли, ибо always цель ещё не выполнилась. В
общем, проблема (чтобы всё красиво работало: например видя изменение
пути к AR команде, только .a пересобрался, а другие даже не пытались бы)
только с порядком выполнения зависимостей. И проблема и вся эта головная
боль только и только при наличии redo-always зависимостей, что даже
чисто теоретически говорит что раз always, значит протухший всегда,
значит все кто от тебя зависят -- тоже автоматом протухшие, всё честно.
И если следовать принципу apenwarr -- уже лучше пересобрать, чем что-то
недособрать, то можно и вообще ничего не делать.

Но я решил всё же хоть как-то но побороть и оптимизировать это всё.
Сначала я нахожу все always зависимости, попутно полностью собирая
информацию о том как кто на кого влияет. В любом случае. .dep файлы мы и
так читаем, поэтому тут overhead на ОС/диск не увеличиваются, ну кроме
как на чтение из кэша. Затем явно параллельно выполняю их. То есть
форсирую выполнение always раньше всего остального. А дальше я собираю
параллельно все цели которые зависят от каждого always. Затем выполняю
тех кто зависят от них. И продолжаю пока не будет ни одной задачи
протухшей. Звучит как ресурсоёмко, как плохо распараллеливаемо, как то,
что мы идём в обратном направлении по дереву зависимостей (а так и
есть), но на практике все case-ы которые я могу придумать, при
использовании always -- очень быстро должны сходится на нет (у меня во
всех проектах так и есть), если always не изменился. А дальше
запускаются штатные сборки, если, конечно же, они всё же OOD.

Пришлось добавить много кода. Но он касается чисто определения порядка
выполнения целей, без уродств. Если always целей нет, то всё работает
чисто по старинке, максимально просто и тупо как в redo-c. Даже не
смотря на "много" кода -- кол-во добавленных строчек чуть-чуть стало
больше чем я выпилил stamp-specific кода! При этом весь новый код
сосредоточен только в ifchange алгоритме/работе, не затрагивая
OOD/runScript функции.

    [оставить комментарий]