...

воскресенье, 27 октября 2019 г.

Julia. С чего начать проект?…

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


Проект

Как уже было упомянуто, одноразовые скрипты или блокноты Jupyter Notebook имеют право на существование на рабочем столе одного человека, особенно, когда язык программирования используется как продвинутый калькулятор. Но этот подход совершенно не годится для разработки проектов, которые должны развиваться и эксплуатироваться годами. И, естественно, Julia, как технологическая платформа, имеет средства, предоставляющие разработчикам такую возможность.

Для начала, несколько общих моментов. В Julia для управления пакетами имеется модуль Pkg. Любая библиотека Julia — это модуль (module). Если модуль не входит в базовый комплект Julia, он оформляется как отдельный пакет. Для каждого пакета имеется файл проекта Project.toml, который содержит описание проекта и его зависимости от других пакетов. Существует второй файл — Manifest.toml, который, в отличии от Project.toml, генерируется автоматически и содержит перечень всех необходимых зависимостей с номерами версий пакетов. Формат файлов Toml — это Tom's Obvious, Minimal Language.


Правила именования пакета

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


  • Следует избегать жаргона и неоднозначных сокращений, по-разному используемых в разных областях.
  • Не следует использовать слово Julia в имени пакета.
  • Пакеты, которые предоставляют какие-то функции вместе с новыми типами, в них объявленными, следует именовать во множественном числе.
    ◦ DataFrames предоставляет тип DataFrame.
    ◦ BloomFilters предоставляет тип BloomFilter.
    ◦ В то же время, JuliaParser не предоставляет новый тип, а новая функциональность — это функция JuliaParser.parse().
  • Следует предпочитать прозрачность и понятность, используя полное имя, чем сокращение. RandomMatrices менее двусмысленное, чем RndMat или RMT.
  • Имя может соответствовать специфике предметной области. Примеры:
    ◦ Julia не имеет своих пакетов рисования графиков. Вместо этого, имеются пакеты Gadfly, PyPlot, Winston и пр., каждый из которых реализует свой собственный подход и методику использования.
    ◦ В то же время, SortingAlgorithms предоставляет полноценный интерфейс для использования алгоритмов сортировки.
  • В случаях, когда пакеты являются обёрткой над какими-то сторонними библиотеками, они сохраняют имя этой библиотеки. Примеры:
    CPLEX.jl является обёрткой над библиотекой CPLEX.
    MATLAB.jl предоставляет интерфейс для активации MATLAB из Julia.

В то же время, имя git-репозитория, обычно, имеет суффикс «.jl».


Генерация пакета

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

julia> ]

Заключительным шагом, запустить генератор пакета, указав имя, которое мы хотим присвоить пакету.

(v1.2) pkg> generate HelloWorld

В итоге, в текущей директории появится новая директория, соответствующая имени пакета, состав которой можем увидеть при помощи команды tree (если она установлена):

shell> cd HelloWorld

shell> tree .
.
├── Project.toml
└── src
    └── HelloWorld.jl

1 directory, 2 files

В данном случае, мы видим минимальный, но недостаточный для хорошо оформленного проекта набор файлов.Подробнее см. https://julialang.github.io/Pkg.jl/v1/creating-packages/.

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

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

julia> using PkgTemplates

Формируем шаблон, включающий список авторов, лицензию, требования к Julia, список плагинов для систем непрерывной интеграции (пример из документации к PkgTemplates):

julia> t = Template(;
           user="myusername", # имя в github
           license="ISC",     # тип лицензии
           authors=["Chris de Graaf", "Invenia Technical Computing Corporation"],
           dir="~/code",      # директория, куда поместить пакет
           julia_version=v"1.0", # минимальная версия Julia
           plugins=[          # плагины средств непрерывной интеграции
               TravisCI(),
               Codecov(),
               Coveralls(),
               AppVeyor(),
               GitHubPages(),
               CirrusCI(),
           ],
       )

Получаем шаблон:

Template:
  → User: myusername
  → Host: github.com
  → License: ISC (Chris de Graaf, Invenia Technical Computing Corporation 2018)
  → Package directory: ~/code
  → Minimum Julia version: v0.7
  → SSH remote: No
  → Commit Manifest.toml: No
  → Plugins:
    • AppVeyor:
      → Config file: Default
      → 0 gitignore entries
    • Codecov:
      → Config file: None
      → 3 gitignore entries: "*.jl.cov", "*.jl.*.cov", "*.jl.mem"
    • Coveralls:
      → Config file: None
      → 3 gitignore entries: "*.jl.cov", "*.jl.*.cov", "*.jl.mem"
    • GitHubPages:
      → 0 asset files
      → 2 gitignore entries: "/docs/build/", "/docs/site/"
    • TravisCI:
      → Config file: Default
      → 0 gitignore entries

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

julia> generate(t, "MyPkg1")

B минимальном варианте шаблон может выглядеть, например так:

julia> t = Template(;
       user="rssdev10",
       authors=["rssdev10"])
Template:
  → User:  rssdev10
  → Host: github.com
  → License: MIT (rssdev10 2019)
  → Package directory: ~/.julia/dev
  → Minimum Julia version: v1.0
  → SSH remote: No
  → Add packages to main environment: Yes
  → Commit Manifest.toml: No
  → Plugins: None

Если создадим по этому шаблону пакет с именем MyPkg2:

julia> generate(t, "MyPkg2")

То можем проверить результат прямо из Julia:

julia> run(`git -C $(joinpath(t.dir, "MyPkg2")) ls-files`);
.appveyor.yml
.gitignore
.travis.yml
LICENSE
Project.toml
README.md
REQUIRE
docs/Manifest.toml
docs/Project.toml
docs/make.jl
docs/src/index.md
src/MyPkg2.jl
test/runtests.jl

Следует обратить внимание на следующие поля:


  • user="myusername", — имя для git-регистрационной записи.
  • dir — директория для размещения кода пакета. Если не задано, он будет создан в директории разработки ~/.julia/dev. Причём, в соответствии с правилами unix файловых систем, директория ~/.julia — скрытая.

После создания проекта, будет сгенерирован достаточный набор файлов и создан git-репозиторий. Более того, все сгенерированные файлы будут добавлены в этот репозиторий автоматически.


Типовое расположение файлов в проекте

Картинку с типовым расположением файлов и их содержимым позаимствуем из https://en.wikibooks.org/wiki/Introducing_Julia/Modules_and_packages, но немного расширим:

Calculus.jl/                               # Главная директория проекта Calculus
  deps/                                    # Здесь находятся файлы для сборки пакета
  docs/                                    # Здесь находятся файлы для сборки документации
  src/                                     # Здесь находятся исходники
    Calculus.jl                            # Главный файл модуля — с заглавной буквы.
      module Calculus                      # Внутри этого файла модуль с тем же именем!
        import Base.ctranspose             # А также импорт внешних зависимостей,
        export derivative, check_gradient, # декларация экспорта функций этого пакета 
        ...
        include("derivative.jl")           # подключение файлов реализации модуля
        include("check_derivative.jl")             
        include("integrate.jl")
        import .Derivative
      end                                  # конец файла Calculus.jl
    derivative.jl                          # В этом файле реализованы какие-то функции, 
      module Derivative                    #      подключаемые в файле Calculus.jl
        export derivative
        function derivative()
        ...
        end
        …
      end
    check_derivative.jl                # файл содержит логику вычисления производных, 
       function check_derivative(f::...)#  и он включается инструкцией
         ...                            # "include("check_derivative.jl")" в Calculus.jl
      end
       …
    integrate.jl                           # файл содержит логику вычисления интегралов, 
      function adaptive_simpsons_inner(f::Funct# и он также включается в Calculus.jl
        ...
      end
      ...
    symbolic.jl                            # включает символьные операции для Calculus.jl
      export processExpr, BasicVariable, ...# эти функции доступны пользователям нашего модуля
      import Base.show, ...                 # некоторые функции из модуля Base импортируем, 
      type BasicVariable <: AbstractVariable# ... чтобы добавить к ним новые методы
        ...
      end
      function process(x::Expr)
        ...
      end
      ...     
  test/                                    # Директория тестов для модуля Calculus
    runtests.jl                            # Точка запуска всех тестов
      using Calculus                       # Подключаем нам модуль Calculus... 
      using Test                           # и модуль Base.Test... 
      tests = ["finite_difference", ...    # Имя файлов-тестов храним как строки... 
      for t in tests
        include("$(t).jl")                 # ... запускаем каждый тест индивидуально 
      end
      ...
    finite_difference.jl                   # Этот тест проверяет какие-то конечные вычисления 
      @test ...                            # ... и по имени подключается в  runtests.jl
      ...

Добавим, что директория deps может содержать файлы, необходимые для корректной сборки пакета. Например, deps/build.jl — это скрипт, который автоматически запускается в момент установки пакета. Скрипт может содержать любой код для подготовки данных (скачать набор данных или выполнить предобработку) или других программ, необходимых для работы.

Следует обратить внимание на то, что в проекте может быть только один главный модуль. То есть, в примере выше — Calculus. Однако, в том же примере есть вложенный модуль Derivative, который подключается через include. Обратите внимание на это. include подключает файл как текст, а не как модуль, что происходит в случае using или import. Последние две функции не просто включают модуль, а заставляют Julia компилировать его как отдельную сущность. Кроме того, Julia будет пытаться найти этот модуль в пакетах зависимостей и выдавать предупреждение, что он остутствует в Project.toml. Поэтому, если наша задача сделать иерархический доступ к функциям, разграничив их по пространствам имён, то включаем файлы через include, а активируем модуль через точку, указывая на его локальную принадлежность. То есть:

module Calculus
  include("derivative.jl")
  import .Derivative
  ...
end

Функция derivative, которая экспортирована из модуля Derivative будет доступна нам через Calculus.Derivative.derivative()


Файл проекта Project.toml

Файл проекта является текстовым файлом. Основные его секции раскрываются в описании https://julialang.github.io/Pkg.jl/v1/toml-files/

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

Основными полями являются:

name = "Example"
uuid = "7876af07-990d-54b4-ab0e-23690620f79a"
version = "1.2.5"

name — имя пакета, выбранное согласно правилам именования. uuid — унифицированный идентификатор, который может быть сгенерирован генератором пакета или любым другим uuid генератором. version — номер версии пакета в формате трёх десятичных чисел, разделенных точками. Это соответствует формату Semantic Versioning 2.0.0. До задекларированной версии 1.0.0 возможны любые изменения в программном интерфейсе. После выпуска этой версии владелец пакета обязан соблюдать правила совместимости. Любые совместимые изменения должны быть отражены в младшем числе (справа). Несовместимые изменения должны сопровождаться изменением старшего числа. Естественно, никакого автоматического контроля за правилом версионности нет, но несоблюдение правила просто приведёт к тому, что пользователи пакета начнут массово прекращать использовать и мигрировать на тот пакет, авторы которого соблюдают это правило.

Все зависимости пакета представлены в секции [deps].

[deps]
Example = "7876af07-990d-54b4-ab0e-23690620f79a"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

Эта секция содержит список прямых зависимостей нашего пакета. Каскадные же зависимости отражаются в файле Manifest.toml, который генерируется автоматически в директории проекта. Все зависимости представлены парами имя=идентификатор. И, обычно, эта часть не заполняется руками. Для этого предусмотрены функции пакета Pkg. И, чаще всего, это делают из REPL, переключив его в режим управления пакетами — ]. Далее — операции add, rm, st и пр., но обязательно в контексте текущего пакета. Если нет, надо выполнить activate ..

Manifest.toml может быть сохранен в системе контроля версий git. Такой подход с двумя файлами позволяет жестко зафиксировать пакеты в дереве зависимостей во время тестирования программного продукта, после чего, гарантировать, что если наш пакет будет развёрнут в новом месте, то именно те же версии сторонних пакетов и будут там же повторены. Или же, наоборот, при отсутствии Manifest.toml будет предоставлена возможность использования любых доступных версий, удовлетворяющих базовым условиям.

Секция [compat] позволяет указать конкретные версии пакетов, которые нам требуются.

[deps]
Example = "7876af07-990d-54b4-ab0e-23690620f79a"

[compat]
Example = "1.2"
julia = "1.1"

Пакеты указываются по имени, ранее использованного в секции [compat]. julia указывает версию именно Julia.

При указании версий, действуют правила, перечисленные в https://julialang.github.io/Pkg.jl/dev/compatibility/. Впрочем, эти же правила указаны в Semantic Versioning.

Есть несколько правил указания версий. Например:

[compat]
Example = "1.2, 2"

означает, что подходит любая версия в диапазоне [1.2.0, 3.0.0), не включая 3.0.0. И это полностью соответствует более простому правилу:

[compat]
Example = "1.2"

Более того, простое указание номера версии является сокращенной формой "^1.2". Пример применения которой выглядит:

[compat]
PkgA = "^1.2.3" # [1.2.3, 2.0.0)
PkgB = "^1.2"   # [1.2.0, 2.0.0)
PkgC = "^1"     # [1.0.0, 2.0.0)
PkgD = "^0.2.3" # [0.2.3, 0.3.0)
PkgE = "^0.0.3" # [0.0.3, 0.0.4)
PkgF = "^0.0"   # [0.0.0, 0.1.0)
PkgG = "^0"     # [0.0.0, 1.0.0)

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

[compat]
PkgA = "~1.2.3" # [1.2.3, 1.3.0)
PkgB = "~1.2"   # [1.2.0, 1.3.0)
PkgC = "~1"     # [1.0.0, 2.0.0)
PkgD = "~0.2.3" # [0.2.3, 0.3.0)
PkgE = "~0.0.3" # [0.0.3, 0.0.4)
PkgF = "~0.0"   # [0.0.0, 0.1.0)
PkgG = "~0"     # [0.0.0, 1.0.0)

Ну и, естественно, доступно указание по знакам равенства/неравенства:

[compat]
PkgA = ">= 1.2.3" # [1.2.3,  ∞)
PkgB = "≥ 1.2.3"  # [1.2.3,  ∞)
PkgC = "= 1.2.3"  # [1.2.3, 1.2.3]
PkgD = "< 1.2.3"  # [0.0.0, 1.2.2]

Имеется возможность указать несколько вариантов зависимостей в секции [targets]. Традиционно, в Julia до версии 1.2, её использовали для того, чтобы указать зависимости для использования пакета и для запуска тестов. Для этого, дополнительные пакеты указывались в секции [extras], а в [targets] перечислялись целевые конфигурации с именами пакетов.

[extras]
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Markdown", "Test"]

Начиная с Julia 1.2 рекомендуется просто добавить отдельный файл проекта для тестов test/Project.toml.


Дополнительные зависимости

Дополнительные зависимости могут быть подключены через файл deps/build.jl, однако в структуре проекта Julia предусмотрен файл Artifacts.toml. Библиотека управления проектами Pkg.Artifacts предоставляет функции для автоматизации загрузки дополнительных зависимостей. Пример такого файла:

# Example Artifacts.toml file
[socrates]
git-tree-sha1 = "43563e7631a7eafae1f9f8d9d332e3de44ad7239"
lazy = true

    [[socrates.download]]
    url = "https://github.com/staticfloat/small_bin/raw/master/socrates.tar.gz"
    sha256 = "e65d2f13f2085f2c279830e863292312a72930fee5ba3c792b14c33ce5c5cc58"

    [[socrates.download]]
    url = "https://github.com/staticfloat/small_bin/raw/master/socrates.tar.bz2"
    sha256 = "13fc17b97be41763b02cbb80e9d048302cec3bd3d446c2ed6e8210bddcd3ac76"

[[c_simple]]
arch = "x86_64"
git-tree-sha1 = "4bdf4556050cb55b67b211d4e78009aaec378cbc"
libc = "musl"
os = "linux"

    [[c_simple.download]]
    sha256 = "411d6befd49942826ea1e59041bddf7dbb72fb871bb03165bf4e164b13ab5130"
    url = "https://github.com/JuliaBinaryWrappers/c_simple_jll.jl/releases/download/c_simple+v1.2.3+0/c_simple.v1.2.3.x86_64-linux-musl.tar.gz"

[[c_simple]]
arch = "x86_64"
git-tree-sha1 = "51264dbc770cd38aeb15f93536c29dc38c727e4c"
os = "macos"

    [[c_simple.download]]
    sha256 = "6c17d9e1dc95ba86ec7462637824afe7a25b8509cc51453f0eb86eda03ed4dc3"
    url = "https://github.com/JuliaBinaryWrappers/c_simple_jll.jl/releases/download/c_simple+v1.2.3+0/c_simple.v1.2.3.x86_64-apple-darwin14.tar.gz"

[processed_output]
git-tree-sha1 = "1c223e66f1a8e0fae1f9fcb9d3f2e3ce48a82200"

Подробнее останавливаться не будем, поскольку дальнейшее описание зависит от конкретного сценария использования. Доступны библиотечные функции artifact_hash , download, create_artifact, bind_artifact. Подробнее см. документацию https://julialang.github.io/Pkg.jl/dev/artifacts/ .


Реализация основного кода и отладка

Директорию разработки мы, естественно, явно или неявно указываем при создании пакета. Однако, при необходимости, можем её изменить. Если пакет был сгенерирован PkgTemplates с параметрами по умолчанию, ищете его в директории ~/.julia/dev. Несмотря на то, что директория скрытая, переход в неё возможен по прямой ссылке в файловом навигаторе. Для MacOS в Finder, например, это делается нажатием Command+Shift+G. Если же, пакет создан в любой другой директории, просто откройте её в текстовом редакторе. Оптимальный редактор для работы с кодом на Julia — это Atom и всё, что поддерживает плагин uber-juno. В этом случае, вы получаете текстовый редактор с автоформатированием кода, консоль REPL для интерактивного запуска кода, возможность выполнения только выделенных фрагментов кода и просмотра полученных результатов, включая отображение графики. А также, пошаговый отладчик. Хотя, надо признать, что на данный момент он достаточно медленный, поэтому актуальный режим отладки — сначала думаем что хотим проверить и ставим отладочный вывод, потом запускаем на тест выполнение.

Рекомендуется посмотреть общие шаблоны проектирования для динамических языков програмирования. А также, вышла книга "Hands-On Design Patterns with Julia 1.0. Tom Kwong" и примеры кода к ней. А при реализации программ, следует учитывать рекомендации по стилю программирования Julia Style Guide.

Из тонкостей отладки можно отметить пакет Revise.jl. Его активацию можно установить в файле .julia/config/startup.jl только для интерактивного режима, в котором и запускать REPL из редактора Atom. Revise позволяет без перезапуска сеанса REPL редактировать код функций внутри нашего пакета, а каждый запуск using/import в наших тестах, будет подключать эти обновления.

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


Тесты

Типовое расположение тестов — директория test. Файл test/runtests.jl является точкой запуска всех тестов.

Применительно к выше упомянутому примеру, типовой вид файла имеет вид:

using Calculus                      # Подключаем наш модуль Calculus для тестов...
using Test                          # и модуль Test...
tests = ["finite_difference", "..."]# В этом массиве храним имена файлов-тестов...
for t in tests
  include("$(t).jl")                # А теперь запускаем в цикле все тесты
end

Файлы конкретных тестов рекомендуется разрабатывать по принципу группировки тестируемых функций. Например, в упомянутом модуле Calculus могут присутствовать различные алгоритмы для вычисления производных, интегралов и пр. Логичным будет тестировать их различными тестами, расположенными в разных файлах.

Для проведения модульного тестирования Julia предоставляет модуль Test из состава базового комплекта библиотек. В этом модуле определён макрос @test, назначение которого — проверить верность указанного утверждения. Примеры:

julia> @test true
Test Passed

julia> @test [1, 2] + [2, 1] == [3, 3]
Test Passed
julia> @test π ≈ 3.14 atol=0.01
Test Passed

Обратите внимание на полную форму оператора приближенного сравнения .

Утверждение, проверяющее выбор исключения — @test_throws. Пример — создаём массив и обращаемся к индексу за его пределами:

julia> @test_throws BoundsError [1, 2, 3][4]
Test Passed
      Thrown: BoundsError

Полезной конструкцией является @testset. Она позволяет группировать отдельные утверждения в логически связный тест. Например:

julia> @testset "trigonometric identities" begin
           θ = 2/3*π
           @test sin(-θ) ≈ -sin(θ)
           @test cos(-θ) ≈ cos(θ)
           @test sin(2θ) ≈ 2*sin(θ)*cos(θ)
           @test cos(2θ) ≈ cos(θ)^2 - sin(θ)^2
       end;
Test Summary:            | Pass  Total
trigonometric identities |    4      4

По каждому набору, задекларированному через @testset, формируется своя таблица пройденных тестов. Наборы тестов могут быть вложенными. В случае успешного их прохождения выдаётся сводная таблица, в случае же провала — по каждой группе тестов будет выдана своя статистика.

julia> @testset "Foo Tests" begin
           @testset "Animals" begin
               @testset "Felines" begin
                   @test foo("cat") == 9
               end
               @testset "Canines" begin
                   @test foo("dog") == 9
               end
           end
           @testset "Arrays" begin
               @test foo(zeros(2)) == 4
               @test foo(fill(1.0, 4)) == 15
           end
       end

Arrays: Test Failed
  Expression: foo(fill(1.0, 4)) == 15
   Evaluated: 16 == 15
[...]
Test Summary: | Pass  Fail  Total
Foo Tests     |    3     1      4
  Animals     |    2            2
  Arrays      |    1     1      2
ERROR: Some tests did not pass: 3 passed, 1 failed, 0 errored, 0 broken.

@test_broken, @test_skip и некоторые другие макросы позволяют временно отключать конкретные тесты.


Профилирование

Профилирование кода возможно по разным критериям. Команда julia имеет следующие полезные ключи запуска:

 --code-coverage={none|user|all}, --code-coverage
            Count executions of source lines (omitting setting is equivalent to "user")
 --code-coverage=tracefile.info
            Append coverage information to the LCOV tracefile (filename supports format tokens).
 --track-allocation={none|user|all}, --track-allocation
            Count bytes allocated by each source line (omitting setting is equivalent to "user")

code-coverage — покрытие кода при выполнении програмы. Для каждой строки кода (если строка значима), выполняется подсчёт количество выполнений этой строки. Генерируется файл покрытия, где для каждой строки приводится сколько раз выполнена эта строка. Эти файлы имеют суффикс .cov и должны быть удалены после анализа. Именно этим способом можно легко выявить критические пути выполнения кода программы и провести оптимизацию.
Фрагмент этого файла выглядит следующим образом:

        - function vectorize(str::String)
       96   tokens = str |> tokenizer |> wordpiece
       48   text = ["[CLS]"; tokens; "[SEP]"]
       48   token_indices = vocab(text)
       48   segment_indices = [fill(1, length(tokens) + 2);]
       48   sample = (tok = token_indices, segment = segment_indices)
       48   bert_embedding = sample |> bert_model.embed
       48   collect(sum(bert_embedding, dims=2)[:])
        - end

track-allocation — измерение потребления памяти при выполнении кода. Также как и в случае анализа покрытия, генерируются файлы, имеющие то же имя, что и имя файлов модуля, но с суффиксом .mem.

Фрагмент этого файла выглядит:

        - function vectorize(str::String)
        0   tokens = str |> tokenizer |> wordpiece
  6766790   text = ["[CLS]"; tokens; "[SEP]"]
        0   token_indices = vocab(text)
    11392   segment_indices = [fill(1, length(tokens) + 2);]
     1536   sample = (tok = token_indices, segment = segment_indices)
        0   bert_embedding = sample |> bert_model.embed
   170496   collect(sum(bert_embedding, dims=2)[:])
        - end

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

Универсальный рецепт запуска профилировщика — запуск всех тестов из корневой директории проекта:

julia --project=@.  --code-coverage --track-allocation test/runtests.jl 

Еще один способ профилирования — при помощи пакета Profile.jl и макроса @profile. Возьмём пример из статьи https://julialang.org/blog/2019/09/profilers и запустим профилирование функции вычисления чисел Фибоначчи. В примере будем использовать макрос @noinline, который запрещает компилятору раскрывать код функций и прикасаться к рекурсиям. При этом, намеренно сделаем рекурсию через две функции fib и fib_r.

julia> @noinline function fib(n)
         return n > 1 ? fib_r(n - 1) + fib_r(n - 2) : 1
       end

julia> @noinline fib_r(n) = fib(n)

julia> @time fib(40)
  0.738735 seconds (3.16 k allocations: 176.626 KiB)
165580141

julia> using Profile

julia> @profile fib(40)
165580141

julia> Profile.print(format=:flat, sortedby=:count)
 Count File      Line Function
    12 int.jl      52 -
    14 int.jl      53 +
   212 boot.jl    330 eval
  5717 REPL[2]      1 fib_r
  6028 REPL[1]      2 fib

julia> count(==(0), Profile.fetch())
585

@profile fib(40) запускает измерение количества вызовов всех функций при выполнении этого кода. В распечатке выше Profile.print(format=:flat, sortedby=:count) мы можем видеть количество вызовов каждого метода. И, в данном случае, мы легко можем видеть, что fib_r и fib имеют большое количество вызовов, но рекурсия здесь не видна. Если мы изменим формат вывода, то легко увидим длинную рекурсию:

julia> Profile.print(format=:tree)
260 REPL[1]:2; fib(::Int64)
112 REPL[1]:1; fib_r(::Int64)
212 task.jl:333; REPL.var"##26#27"
 212 REPL.jl:118; macro expansion
  212 REPL.jl:86; eval_user_input
   212 boot.jl:330; eval
  ╎ 210 REPL[1]:2; fib
  ╎  210 REPL[1]:1; fib_r
  ╎   210 REPL[1]:2; fib
  ╎    210 REPL[1]:1; fib_r
  ╎     210 REPL[1]:2; fib
  ╎    ╎ 210 REPL[1]:1; fib_r
  ╎    ╎  210 REPL[1]:2; fib
  ╎    ╎   210 REPL[1]:1; fib_r
  ╎    ╎    210 REPL[1]:2; fib
  ╎    ╎     210 REPL[1]:1; fib_r
  ╎    ╎    ╎ 210 REPL[1]:2; fib
  ╎    ╎    ╎  210 REPL[1]:1; fib_r
  ╎    ╎    ╎   210 REPL[1]:2; fib
  ╎    ╎    ╎    210 REPL[1]:1; fib_r
  ╎    ╎    ╎     210 REPL[1]:2; fib
...

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

Более подробно см. https://github.com/vchuravy/PProf.jl .


Документирование

Заготовки для генератора документации находятся в директории проекта doc. О них уже упоминалось в статье https://habr.com/ru/post/439442/
Также, подробнее, можно посмотреть руководство из документации Julia.

Для генератора документации может быть использован отдельный Project.toml файл с тем, чтобы не смешивать основные и вспомогательные пакеты. Генератор документации собирает комментарии, примеры и прочие описания в формат, пригодный для дальнейшего размещения на веб-сайте или, просто, удобный для использования.


Установка пакета

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


  • Установка по имени пакета из официального реестра. Для этого пакет должен быть необходимо зарегистрировать. Полный реестр см. https://github.com/JuliaRegistries/General Процедуру регистрации см. https://github.com/JuliaRegistries/Registrator.jl
  • Установка по имени пакета из частного реестра. Необходимо создать этот реестр и зарегистрировать в нём пакет. См. https://julialang.github.io/Pkg.jl/v1/registries/
  • Установка пакета по имени git-репозитория. Единственная тонкость — надо обеспечить режим доступа по ключу и без пароля, в противном случае любое обновление пакетов будет автоматически приводить к сканированию этого репозитория, для чего Julia будет требовать пароль и стопорить процесс, если это было активировано скриптом.
  • Установка по имени локальной директории, в которой находятся файлы проекта.

В тех случаях, когда результатом является приложение или сервис, возможны альтернативные варианты. Например, приложение может быть непосредственно скопировано исходным кодом, выгружено при помощи git clone, или быть скопированным как скомпилированное в бинарные файлы приложение. Последние достигается с помощью PackageCompiler.jl. Однако то, как собирать серверные приложения, рассмотрим как-нибудь позже.


Cкрипты для обслуживания пакета

Если у пакета есть какие-то зависимости, необходимы действия по загрузке данных, предварительном их обсчёту (например посчитать векторы для каких-то массивов строк, посчитать нормы для будущего вычисления скалярных произведений), всё это должно быть размещено в директории deps, а запускаться из файла deps/build.jl. Этот файл будет активирован автоматически в процессе установки пакета. Если код пакета был просто скопирован, то, естественно, этот файл может быть активирован вручную. Но, в этом случае, лучше, если он будет активирован через специальные команды, позволяющие ещё и загрузить все зависимости нашего пакета. Для этого, обычно, создаётся файл build.jl, который размещается в корне проекта:

#!/usr/bin/env julia --project=@.

using Pkg
Pkg.activate(".")
Pkg.build() # Pkg.build(; verbose = true) for Julia 1.1 and up
Pkg.test() # (coverage=true)

Комментарии к файлу. Строка julia --project=@. указывает Julia подключить Project.toml из текущей директории. Впрочем, указанный способ — это запуск скрипта build.jl из командной оболочки, когда файл имеет атрибут executable. Но также, мы можем его запустить прямой командой julia --project=@. build.jl.

Команда Pkg.activate(".") переключает пакетный менеджер в контекст текущего пакета (по Project.toml).

Команда Pkg.build() приведёт к загрузке всех зависимостей и запуска их сборки, включая компиляцию сторонних C-библиотек, если это требуется. Именно в этот момент будет активирован и наш deps/build.jl, если он есть.

Строка Pkg.test() запускает тесты текущего пакета. И это нужно сделать, чтобы во-первых, убедиться что пакет установлен корректно, выполняются все необходимые действия. А во-вторых, откомпилировать наш пакет и все его зависимости с тем, чтобы при дальнейшем его запуске не требовалось ждать долгой компиляции. Закомментированный здесь аргумент coverage=true означает запуск тестов покрытия. Это не применяется при развёртывании пакета, но может быть использован при разработке. Но такой файл build.jl не должен дойти до этапа внедрения.

Также, в проекте могут присутствовать скрипты для систем непрерывной интеграции. Они уже были упомянуты выше, в разделе генерации проекта с помощью PkgTempletes. Поскольку подобных систем много — это и Gitlab CI, и Travis CI, стредства от GitHub и многие другие, то рассматривать их здесь не будем.


Заключение

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


Ссылки


Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий