I’m pleased to announce that Revise 3.0 is available for beta-testing on Julia 1.0, 1.5, and nightly. Switch to it by updating your packages. (EDIT: this was originally written when Revise 3 was in beta but it is now released, and the changes are so minor it is not worth a new announcement.)
Revise 3 is an extensive rework with many exciting new features. It also has one area where an important new configuration might need to be tweaked via user feedback. Since that setting won’t change once the official Revise 3.0 is out, by beta testing you have a chance to affect the next stage of of the Revise experience. Read on for the full details.
Faster startup
The biggest negative of Revise 2.x is that it initially slows you down: on my machine, the aggregate overhead of using Revise (startup + first subsequent package load) is about 3.5s. With Revise 3, on my machine that time is down to 0.9s; on nightly (what will become Julia 1.6), it may get as low as 0.7s depending on the fate of a still-to-be-decided pull request to Julia.
Most of these gains have been achieved by deferring compilation, not making compilation faster or reducing the need for compilation. Basically, Revise now does much less work when it starts up, and goes to pains to ensure that it doesn’t trigger very much compilation until its functionality is actually needed. Consequently, this is mostly a bookkeeping improvement: time that used to be spent waiting for Revise to start will now be spent as extra latency for your first actual revision. To me, that still seems like a big win, because now Revise costs you very little until the moment when it’s actually delivering value. As before, it should be fast after the first revision; it’s only Revise’s own internal compilation time that I’m discussing here.
As part of the latency-reduction, Revise no longer tracks its own code or that of its dependencies. Call Revise.add_revise_deps()
after startup if you want it to track itself.
Better error handling and more Julian error messages
The error-handling system has been extensively reworked. While there are still some printing differences between errors with and without Revise (mostly from using @error
rather than error(...)
), the messages and stack traces should now be very similar. It should also be less intrusive: now, only the first error triggered in a file is printed, and Revise warns you of outstanding revisions only by coloring your prompt yellow until you’ve resolved the outstanding errors.
More robustness when making revisions
Revise 2, while quite capable, still found ways to make annoying mistakes. For example, it used a somewhat arbitrary order to enact its revisions, and sometimes it got the order wrong. In Revise 3, if PkgA depends on PkgB, then PkgB’s revisions will be processed before PkgA’s, ensuring that new definitions in PkgB will be available for changes in PkgA. Likewise, if MyPkg loads "file1.jl"
before it loads "file2.jl"
, it will process "file1.jl"
's revisions before processing those of "file2.jl"
. There are still corner cases that can break this (e.g., when "file1.jl"
does an include("file2.jl")
partway through and then uses some newly-defined types in "file2.jl"
), but the number of opportunities for breakage should be vastly reduced.
A similar improvement concerns the process moving methods from one file to another, something that is fairly common when you’re working on larger packages or ecosystems. With Revise 2, unless you remembered to complete the move (copying to the destination and deleting from the source) before triggering a revision, Revise would “helpfully” delete the entire method from your session when you got around to deleting it from its original location. Revise 3 fixes this by (transiently) supporting multiple locations for method definitions, and deleting methods only when their last instantiation is gone.
Selective evaluation
From a technical standpoint, the most impressive new feature of Revise 3 is support for selective evaluation. This is an area where the defaults could still change depending on user feedback, and one of the major motivations for this beta period is to find out what choices work best in practice before locking them in throughout the Revise 3.x release cycle.
The short version of this feature can be summed up as follows: here on discourse, it’s been pretty common to advise users of includet
to split their files, with “code” going in a package and “data processing” going in a script that does not get tracked by Revise. In Revise 3, this essentially happens automatically: Revise will (subject to alteration by some user-configurable settings) refuse to alter data but will happily modify methods, even when the two are densely interwoven.
Here’s the longer version: when defining methods, Julia keeps track of “dependencies,” so that if foo
calls bar
and you redefine bar
, then foo
will automatically be recompiled the next time you call it. We do not have the same notion of dependencies for data: if I used x
to compute y
, should I re-compute y
when x
changes? In Revise 2, the answer was “re-evaluate every changed expression and don’t worry about any dependencies.” That works sometimes, but sometimes causes big problems: either you got changes you didn’t want (forcing you to recompute the setup for your problem from scratch again), and/or long computation times since Revise performs changes using the interpreter.
Currently in the beta, the defaults are to re-evaluate all changes in packages and only method definitions in includet
scripts. In other words, in includet
scripts Revise will re-evaluate methods but it will not touch your data. That’s true even if you explicitly change a line defining a variable, because Revise doesn’t know what else you might need to recompute in order to stay consistent; if you want to modify data, you need to take care of it yourself. Fortunately, with both major IDEs now supporting inline evaluation, this is easier than ever. (An alternative is to alter Revise’s settings, described below.)
Perhaps the best way to illustrate this is with a demo; create a file
a = rand() # "data": will not be revised
b = 2 # "data": will not be revised even if you change it
f() = 1 # "method": will be revised
for (T, varname, e) in ((Float32, :c, rand()), (Float64, :d, rand()))
@eval begin
$varname = rand() # hiding data inside an @eval doesn't fool Revise!
g(::$T) = $e # and it can still revise methods
end
end
and then do this in your session:
julia> @show a b c d
a = 0.14642990011901702
b = 2
c = 0.6799833688203913
d = 0.12299256361483968
0.12299256361483968
julia> f()
1
julia> g(1.0)
0.6651919620816014
julia> g(1.0) # the value is static
0.6651919620816014
julia> g(1.0f0)
0.14537943287044364
julia> g(1.0f0)
0.14537943287044364
Now, change the value assigned to b
and all the methods by modifying the script to
a = rand() # "data": will not be revised
b = 3 # "data": will not be revised even if you change it
f() = 2 # "method": will be revised
for (T, varname, e) in ((Float32, :c, rand()), (Float64, :d, rand()))
@eval begin
$varname = rand() # hiding data inside an @eval doesn't fool Revise!
g(::$T) = $e+1 # and it can still revise methods
end
end
and then display the same values:
julia> @show a b c d
a = 0.14642990011901702
b = 2
c = 0.6799833688203913
d = 0.12299256361483968
0.12299256361483968
which you can see are unchanged (even though some are interwoven with method definitions), but all the methods have been redefined:
julia> f()
2
julia> g(1.0)
1.4382250538965402
julia> g(1.0f0)
1.4809866331295454
Some of the cool magic here is more obvious if you notice that Revise did need to assign values to some variables, notably T
and e
as it iterated through the loop, but c
and d
were not involved in method definition and hence were not modified.
If you don’t like the defaults, you can set __revise_mode__ = :evalassign
and Revise will also evaluate changed assignments; __revise_mode__ = :eval
will evaluate everything. But keep in mind that some of these settings will trigger new rand()
numbers being assigned in some of those assignments above.
Again, by default this new mode only applies to includet
: currently packages default to __revise_mode__ = :eval
. You can set the mode of packages on a per-module basis by setting __revise_mode__
within the module.
If everybody hates the new defaults we can change them before Revise 3.0 release, but I will be reluctant to make changes after the release. So this is a good time to take Revise 3 for a spin and tell me what you like and what you dislike.
Those are the major changes; let the testing begin!