Last updated: 2022-01-16 19:47:49 +0000
Upstream URL: git clone http://chriswarbo.net/git/asv-nix.git
Contents of README follows
Airspeed Velocity (asv) is a benchmarking tool for Python. Its features include:
All of this is very nice, but this last feature is rather tricky. There is a plugin mechanism for defining environments, and plugins are provided for virtualenv, conda and a “dummy” plugin which uses the system’s installed packages.
This is a bit unfortunate for two reasons:
subprocess
to call out to
our “real” project, that just turns our “real” project into a dependency
of the benchmarks (as the previous point states) such dependencies can’t
be managed unless they’re Python packages.If we solve the first problem, we’ve automatically solved the second. That’s what this project tries to do.
Nix is a build/packaging system which is very powerful and modular. Packages can be defined using a Turing-complete, pure functional, lazily evaluated, domain specific programming language. Every package is built in isolation, the results are cached along with the hashes of all inputs, and only rebuilt if some input changes.
Rather than defining packages directly, it’s common to write a bunch
of helper functions and define a package in terms of those. The
‘nixpkgs’ repository contains a large collection of packages and helper
functions, for example buildPythonPackage
which takes care
of Python-specific tasks like setting module directories, or
haskellPackages.mkDerivation
which handles tasks specific
to Haskell projects. This flexibility makes Nix usable for almost any
project in any programming language.
This project provides an asv plugin, which uses Nix to manage dependencies instead of conda or virtualenv. The architecture of asv is understandably a little Python-centric, so we take a few liberties in our configuration to make using Nix more seamless.
To run a benchmark, you’ll need a Python package with both
asv
and this plugin installed as modules. You could do this
using pip
, apt-get
, etc. but we might as well
use Nix: the default.nix
file defines a Nix package for
this plugin. It depends on an asv
package, but at the time
of writing nixpkgs
doesn’t include one. The
shell.nix
file has instructions to fetch one, so you should
be able to just run nix-shell
and be dropped into an
environment with both asv
and this plugin available.
Most of the asv instructions apply as normal, so we won’t cover them here. Instead, we’ll highlight the aspects which are unique to the Nix plugin.
asv
allows benchmarks to be managed separately to your
project. That complicates things a little, so let’s assume that your
benchmarks live alongside your code. In your project repo’s top-level
directory, run asv quickstart
to generate some example
benchmarks and a config file.
In your asv.conf.json
, make sure plugins
is
set to [ "asv_nix" ]
and environment_type
is
"nix"
. Also set builders
to {}
and matrix
to {}
.
The easiest way to specify your benchmarking environment is to define
it using a Nix file. Let’s make a file called
benchmarks.nix
in your project’s root directory. For the
sake of argument, we’ll assume your project has a
default.nix
which defines a package containing some
binaries in bin/
. The following benchmarks.nix
file will provide a python
executable, which has access to
that bin/
directory in its PATH
:
args:
with import <nixpkgs> {};
with { pkg = callPackage args.root {}; };
runCommand "env" { buildInputs = [ makeWrapper ]; } ''
mkdir -p "$out/bin"
makeWrapper "${python}/bin/python" "$out/bin/python" \
--prefix PATH : "${pkg}/bin"
''
Now we’ll tell asv
to use this as our environment. In
our asv.conf.json
we set installer
to the
following:
"args: import (args.root + ''/benchmarks.nix'') args"
This should be everything we need to run the benchmarks, using
asv run
. However, there is an annoying problem: we’re
looking for benchmarks.nix
in root
, which is
the version of the project that’s checked out of version control. Hence
we need to commit benchmarks.nix
to the repo before it will
be available; this isn’t a good idea, since it forces us to commit
changes before we’ve tested them, and different commits may end up being
tested with different benchmarks.nix
files, making
comparisons unjustified.
To improve this, we need to load benchmarks.nix
from our
working tree instead. We can do this by defining a “builder”, since
they’re given the working tree as their dir
argument.
First, alter your builders
attribute to the
following:
{ "myDep": "{ dir, version }: dir" }
Now we need to specify a “version” to use; this can be an arbitrary
Nix expression, and since we’re ignoring it we might as well use
null
. Alter your matrix
to be:
{ "myDep": [ "null" ] }
Now we need to make use of myDep
in
installer
. Change your installer
to:
"args: import (args.myDep + ''/benchmarks.nix'') args"
Now this will read benchmarks.nix
from the working tree
(the dir
value we returned from the myDeps
builder), whilst pkg
will still use the checked out copy
(which is what we want to benchmark).
If your needs are simple, this is enough to work with: just adjust
the contents of benchmarks.nix
(and rename/move the file)
as appropriate to define an environment for your project. Alter the
benchmarks as you like.
The recommended way to access features of the environment from within
a benchmark is to use environment variables (for data), or augment
PATH
(for programs). We use makeWrapper
to do
the latter in the above example; the same can be done for environment
variables using the --set
option.
To benchmark a project, you’ll need an asv.conf.json
file. You can use asv quickstart
to generate one, and
follow the asv
documentation for setting the fields. The
settings which are specific to the Nix plugin are as follows:
plugins
: This tells asv what Python modules to
import for plugins. You should make sure "asv_nix"
is
included. For example:
"plugins": [ "asv_nix" ]
environment_type
: This tells asv how it should build
the environment that the benchmarks will be run in. Use the value
"nix"
. For example:
"environment_type": "nix"
env_dir
: This tells asv where to store environments,
so they don’t have to be rebuilt from scratch. The Nix plugin extends
this by including a “garbage collector root” at this path inside
each environment. This prevents Nix from deleting the environment
when its garbage collector runs. Deleting these roots (symlinks) will
cause the environments to be garbage collected. You can also just delete
the entire contents of env_dir
if you need to.
For example:
"env_dir": ".asv/env"
This will tell asv
to make a directory
.asv/env
in the benchmarking project’s directory. Each
commit of the project being benchmarked will be put in its own directory
in here; and inside each of those will be another
.asv/env
directory, containing a garbage collector root for
each set of dependencies that commit has been benchmarked with.
matrix
: This tells asv which versions of which
dependencies to include in the environment. It is an object where each
property names a dependency, and each value is a list of “versions” for
that dependency to benchmark. For example if the dependencies are
{"a": [ 1, 2, 3 ], "b": [ 7 ]}
then the benchmarks will be
run with in an environment with a
version 1 and
b
version 7, then again in an environment with
a
version 2 and b
version 7, and again in an
environment with a
version 3 and b
version
7.
In normal asv
projects, these dependencies are Python
packages. Since Nix is more general, we add a layer of indirection: each
dependency name is a key for the builders
object described
below. Each “version” is a string containing a Nix expression, which
will be passed as an argument to that “builder”.
For example, here are two ways we might specify a package to use:
python
explicitly imports nixpkgs
and projects
out the desired package attribute; shell
just gives a
string of the attribute’s name (Nix strings can use "
or
''
); myLib
seems to be specifying git
revisions:
"matrix": {
"python": ["(import <nixpkgs> {}).python2",
"(import <nixpkgs> {}).python3",
"(import <nixpkgs> {}).pypy" ],
"shell": ["''bash''", "''dash''", "''zsh''" ],
"myLib": ["''42db882''", "''2ddf9fe''"]
}
builders
: This property isn’t part of a standard
asv
project. It defines, for each dependency, a Nix
expression for building it. The keys correspond to those used in
matrix
. The values should be strings containing a Nix
expression, which should be a function taking a set of two arguments:
version
, which comes from the matrix
entry;
and dir
, which is the path to the benchmarks’ root
directory. The latter is useful when we want to write large Nix
expressions in a separate file and have the configuration import them.
Note that dir
is the root of the benchmarking
project; this isn’t necessarily the project being
benchmarked, since asv allows benchmarks to be managed
separately.
Continuing the example from matrix
, we might have these
implementations: Since python
passes in a package for each
“version”, the implementation can just return them as-is;
shell
only provides names, so its implementation must look
them up. myLib
uses dir
to import a Nix
expression from disk, and passes on the given version
.
"builders": {
"python": "args.version,
"shell": "args: builtins.getAttr args.version (import <nixpkgs> {})",
"myLib": "args: import args.dir { inherit (args) version; }"
}
installer
: This is another Nix-specific property. It
is a string containing a Nix expression, which will build the whole
benchmark environment. It should be a function accepting a set
containing all of the dependencies (so shell
,
python
and myLib
in the above example), as
well as root
which is the path to the root directory of the
project being benchmarked (i.e. this is the opposite of the
dir
argument for builders). Importantly, the contents of
root
will change as different versions (e.g. git commits)
of the project are benchmarked.
The result of this function should include a bin/python
executable, which will be used to run the benchmarks. Presumably, you
will want other programs, environment variables, etc. to be set during
the benchmark’s execution; the standard way to do this is to “wrap” the
python executable, e.g. using makeWrapper
from
nixpkgs
, or some similar mechanism.
For example:
"installer": "args: with import <nixpkgs> {};
with {
pkg = callPackage args.root { shell = args.shell; };
};
runCommand ''bench-env''
{
inherit pkg;
inherit (args) python;
buildInputs = [ makeWrapper ];
}
''
mkdir -p $out/bin
makeWrapper $python/bin/python $out/bin/python \
--prefix PATH : $pkg/bin
--set MYLIB $myLib
''"
More likely, we’ll write expressions like these in a file and either
have the installer
import them from root
, or
have a builder
import them from dir
.
To see a working example, take a look at example.nix
,
which is used as a test during installation.