Development

Interactive use of Julia

When a Julia program is executed, Julia first loads and parses all code. Then, the just-in-time compiler has to compile all functions at their first use, which incurs an overhead each time a program is run. For proper packages and commands executed in the REPL (= "return-eval-print loop", which is what the Julia community calls the interactive command-line prompt that opens when executing julia without any files as arguments), however, the previously compiled functions are cached. Therefore, Trixi.jl should generally always be used interactively from the REPL without closing Julia during development, as it allows much faster turnaround times.

If you naively run Trixi.jl from the REPL, you will not be able to change your Trixi.jl source files and then run the changed code without restarting the REPL, which destroys any potential benefits from caching. However, restarting Julia can be avoided by using the Revise.jl package, which tracks changed files and re-loads them automatically. Therefore, it is highly recommended to first install Revise with the following command in Julia: To enter the package REPL mode, press ] in the standard Julia REPL mode. Then, execute

(@v1.9) pkg> add Revise

Now you are able to run Trixi.jl from the REPL, change Trixi.jl code between runs, and enjoy the advantages of the compilation cache! Before you start using Revise regularly, please be aware of some of the Pitfalls when using Revise.

Another recommended package for working from the REPL is OhMyREPL.jl. It can be installed by running

(@v1.9) pkg> add OhMyREPL

and adds syntax highlighting, bracket highlighting, and other helpful improvements for using Julia interactively. To automatically use OhMyREPL when starting the REPL, follow the instructions given in the official documentation.

Running Trixi.jl interactively in the global environment

If you've installed Trixi.jl and Revise in your default environment, begin by executing:

julia

This will start the Julia REPL. Then, run

julia> using Revise; using Trixi

You can run a simulation by executing

julia> trixi_include(default_example())

Together, all of these commands can take some time, roughly half a minute on a modern workstation. Most of the time is spent on compilation of Julia code etc. If you execute the last command again in the same REPL, it will finish within a few milliseconds (maybe ~45 on a modern workstation). This demonstrates the second reason for using the REPL: the compilation cache. That is, those parts of the code that do not change between two Trixi.jl runs do not need to be recompiled and thus execute much faster after the first run.

Manually starting Trixi.jl in the local environment

If you followed the installation instructions for developers, execute Julia with the project directory set to the run directory of the program/tool you want to use. For example, to run Trixi.jl this way, you need to start the REPL with

julia --project=path/to/Trixi.jl/run

and execute

julia> using Revise; using Trixi

to load Revise and Trixi.jl. You can then proceed with the usual commands and run Trixi.jl as in the example above. The --project flag is required such that Julia can properly load Trixi.jl and all dependencies if Trixi.jl is not installed in the global environment. The same procedure also applies should you opt to install the postprocessing tool Trixi2Vtk manually such that you can modify their implementations.

Pitfalls when using Revise

While Revise is a great help for developing Julia code, there are a few situations to watch out for when using Revise. The following list of potential issues is based on personal experiences of the Trixi.jl developers and probably incomplete. Further information on limitations and possible issues with Revise can be found in the official documentation.

If in doubt, restart the REPL

Oftentimes, it is possible to recover from issues with Revise by fixing the offending code. Sometimes, however, this is not possible or you might have troubles finding out what exactly caused the problems. Therefore, in these cases, or if in doubt, restart the REPL to get a fresh start.

Syntax errors are easy to miss

Revise does not stop on syntax errors, e.g., when you accidentally write a[i) instead of a[i]. In this case, Revise reports an error but continues to use the old version of your files! This is especially dangerous for syntax errors, as they are detected while Revise reloads changed code, which happens in the beginning of a new execution. Thus, the syntax error message quickly disappears from the terminal once Trixi.jl starts writing output to the screen and you might not even have noticed that an error occurred at all.

Therefore, when you are deep in a coding/debugging session and wonder why your code modifications do not seem to have any effect, scroll up in your terminal to check if you missed earlier syntax errors, or - if in doubt - restart your REPL.

Files are not tracked after changing branches

Sometimes, Revise stops tracking files when changing the Git branch. That is, modifications to Trixi.jl's source files will not be reloaded by Revise and thus have no effect of a currently running REPL session. This issue is particularly annoying for a developer, since it does not come with any warning! Therefore, it is good practice to always restart the REPL after changing branches.

Changes to type definitions are not allowed

Revise cannot handle changes to type definitions, e.g., when modifying the fields in a struct. In this case, Revise reports an error and refuses to run your code unless you undo the modifications. Once you undo the changes, Revise will usually continue to work as expected again. However, if you want to keep your type modifications, you need to restart the REPL.

Using the Julia REPL effectively

The Julia manual is an excellent resource to learn Julia. Here, we list some helpful commands than can increase your productivity in the Julia REPL.

  • Use the REPL help mode entered by typing ?.

    julia> using Trixi
    
    help?> trixi_include
    search: trixi_include
    
      trixi_include([mod::Module=Main,] elixir::AbstractString; kwargs...)
    
      include the file elixir and evaluate its content in the global scope of module mod. You can override specific
      assignments in elixir by supplying keyword arguments. It's basic purpose is to make it easier to modify some
      parameters while running Trixi.jl from the REPL. Additionally, this is used in tests to reduce the computational
      burden for CI while still providing examples with sensible default values for users.
    
      Examples
      ≡≡≡≡≡≡≡≡≡≡
    
      julia> trixi_include(@__MODULE__, default_example(), tspan=(0.0, 0.1))
      [...]
    
      julia> sol.t[end]
      0.1
  • You can copy and paste REPL history including julia> prompts into the REPL.

  • Use tab completion in the REPL, both for names of functions/types/variables and for function arguments.

    julia> flux_ranocha( # and TAB
    flux_ranocha(u_ll, u_rr, orientation::Integer, equations::CompressibleEulerEquations1D) in Trixi at ~/.julia/dev/Trixi/src/equations/compressible_euler_1d.jl:390
    flux_ranocha(u_ll, u_rr, orientation::Integer, equations::CompressibleEulerEquations2D) in Trixi at ~/.julia/dev/Trixi/src/equations/compressible_euler_2d.jl:839
    [...]
  • Use methodswith to discover methods associated to a given type etc.

    julia> methodswith(CompressibleEulerEquations2D)
    [1] initial_condition_convergence_test(x, t, equations::CompressibleEulerEquations2D) in Trixi at ~/.julia/dev/Trixi/src/equations/compressible_euler_2d.jl:51
    [...]
  • Use @which (or @edit) for method calls.

    julia> @which trixi_include(default_example())
    trixi_include(elixir::AbstractString; kwargs...) in Trixi at ~/.julia/dev/Trixi/src/auxiliary/special_elixirs.jl:36
  • Use apropos to search through the documentation and docstrings.

    julia> apropos("MHD")
    Trixi.IdealGlmMhdEquations3D
    Trixi.IdealGlmMhdMulticomponentEquations2D
    Trixi.calc_fast_wavespeed_roe
    Trixi.IdealGlmMhdEquations1D
    Trixi.initial_condition_constant
    Trixi.flux_nonconservative_powell
    Trixi.GlmSpeedCallback
    Trixi.flux_derigs_etal
    Trixi.flux_hindenlang_gassner
    Trixi.initial_condition_convergence_test
    Trixi.min_max_speed_naive
    Trixi.IdealGlmMhdEquations2D
    Trixi.IdealGlmMhdMulticomponentEquations1D
    [...]

Text editors

When writing code, the choice of text editor can have a significant impact on productivity and developer satisfaction. While using the default text editor of the operating system has its own benefits (specifically the lack of an explicit installation procure), usually it makes sense to switch to a more programming-friendly tool. In the following, a few of the many options are listed and discussed:

VS Code

Visual Studio Code is a modern open source editor with good support for Julia. While Juno had some better support in the past, the developers of Juno and the Julia VS Code plugin are joining forces and concentrating on VS Code since support of Atom has been suspended. Basically, all comments on Juno below also apply to VS Code.

Juno

If you are new to programming or do not have a preference for a text editor yet, Juno is a good choice for developing Julia code. It is based on Atom, a sophisticated and widely used editor for software developers, and is enhanced with several Julia-specific features. Furthermore and especially helpful for novice programmers, it has a MATLAB-like appearance with easy and interactive access to the current variables, the help system, and a debugger.

Vim or Emacs

Vim and Emacs are both very popular editors that work great with Julia. One of their advantages is that they are text editors without a GUI and as such are available for almost any operating system. They also are preinstalled on virtually all Unix-like systems. However, Vim and Emacs come with their own, steep learning curve if they have never been used before. Therefore, if in doubt, it is probably easier to get started with a classic GUI-based text editor (like Juno). If you decide to use Vim or Emacs, make sure that you install the corresponding Vim plugin julia-vim or Emacs major mode julia-emacs.

Debugging

Julia offers several options for debugging. A classical debugger is available with the Debugger.jl package or in the Julia extension for VS Code. However, it can be quite slow and, at the time of writing (January 2023), currently does not work properly with Trixi.jl. The Infiltrator.jl package on the other hand does not offer all features of a full debugger, but is a fast and simple tool that allows users to set breakpoints to open a local REPL session and access the call stack and variables.

Infiltrator

The Infiltrator package provides fast, interactive breakpoints using the @infiltrate command, which drops the user into a local REPL session. From there, it is possible to access local variables, see the call stack, and execute statements.

The package can be installed in the Julia REPL by executing

(@v1.9) pkg> add Infiltrator

To load the package in the Julia REPL execute

julia> using Infiltrator

Breakpoints can be set by adding a line with the @infiltrate macro at the respective position in the code. Use Revise if you want to set and delete breakpoints in your package without having to restart Julia.

Use `@autoinfiltrate` when debugging Trixi.jl

When running Julia inside a package environment, e.g., inside the source code of Trixi.jl itself, the @infiltrate macro only works if Infiltrator has been added to the package dependencies. To avoid this, you can use the (non-exported) @autoinfiltrate macro in Trixi.jl, which only requires Infiltrator.jl to be available in the current environment stack and will auto-load it for you.

Triggering the breakpoint starts a REPL session where it is possible to interact with the current local scope. Possible commands are:

  • @locals: Print the local variables.
  • @exfiltrate: Save the local variables to a global storage, which can be accessed with the safehouse variable outside the Infiltrator session.
  • @trace: Print the current stack trace.
  • Execute other arbitrary statements
  • ?: Print a help list with all options

To finish a debugging session, either use @continue to continue and eventually stop at the next breakpoint or @exit to skip further breakpoints. After the code has finished, local variables saved with @exfiltrate can be accessed in the REPL using the safehouse variable.

Limitations of using Infiltrator.jl are that local variables cannot be changed, and that it is not possible to step into further calls or access other function scopes.

Releasing a new version of Trixi.jl, Trixi2Vtk

  • Check whether everything is okay, tests pass etc.

  • Set the new version number in Project.toml according to the Julian version of semver. Commit and push.

  • Comment @JuliaRegistrator register on the commit setting the version number.

  • JuliaRegistrator will create a PR with the new version in the General registry. Wait for it to be merged.

  • Increment the version number in Project.toml again with suffix -pre. For example, if you have released version v0.2.0, use v0.2.1-pre as new version number.

  • When a new version of Trixi.jl was released, check whether the [compat] entries in test/Project.toml in Trixi2Vtk should be updated. When a new version of Trixi2Vtk was released, check whether the [compat] entries in docs/Project.toml in Trixi.jl should be updated.

    These entries will also be checked regularly by CompatHelper (once a day). Hence, if everything was released correctly, you should only need to do these checks manually if new minor versions with changes in the docs of Trixi2Vtk were released but no new version of Trixi.jl was released afterwards.

Preview of the documentation

You can build the documentation of Trixi.jl locally by running

julia --project=docs -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()'
julia --project=docs --color=yes docs/make.jl

from the Trixi.jl main directory. Then, you can look at the html files generated in docs/build. For PRs triggered from branches inside the Trixi.jl main repository previews of the new documentation are generated at https://trixi-framework.github.io/Trixi.jl/previews/PRXXX, where XXX is the number of the PR. This does not work for PRs from forks for security reasons (since anyone could otherwise push arbitrary stuff to the Trixi.jl website, including malicious code).

Developing Trixi2Vtk

Trixi2Vtk has Trixi.jl as dependency and uses Trixi.jl's implementation to, e.g., load mesh files. When developing Trixi2Vtk, one may want to change functions in Trixi.jl to allow them to be reused in Trixi2Vtk. To use a locally modified Trixi.jl clone instead of a Trixi.jl release, one can tell Pkg to use the local source code of Trixi.jl instead of a registered version by running

(@v1.9) pkg> develop path/to/Trixi.jl