Skip to content

Commit 87a76b6

Browse files
committed
__precompile__(isprecompilable) function for automated opt-in to module precompilation on import (closes #12462)
1 parent 2e8031c commit 87a76b6

File tree

7 files changed

+129
-30
lines changed

7 files changed

+129
-30
lines changed

NEWS.md

+12-4
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,19 @@ New language features
2828
* The syntax `function foo end` can be used to introduce a generic function without
2929
yet adding any methods ([#8283]).
3030

31-
* Incremental compilation of modules: `Base.compile(module::Symbol)` imports the named module,
32-
but instead of loading it into the current session saves the result of compiling it in
31+
* Incremental precompilation of modules: call ``__precompile__()`` at the top of a
32+
module file to automatically precompile it when it is imported ([#12491]), or manually
33+
run `Base.compile(modulename)`. The resulting precompiled `.ji` file is saved in
3334
`~/.julia/lib/v0.4` ([#8745]).
3435

35-
* See manual section on `Module initialization and precompilation` (under `Modules`) for details and errata.
36+
* See manual section on `Module initialization and precompilation` (under `Modules`) for details and errata. In particular, to be safely precompilable a module may need an `__init__` function to separate code that must be executed at runtime rather than precompile-time. Modules that are *not* precompilable should call `__precompile__(false)`.
3637

37-
* New option `--output-incremental={yes|no}` added to invoke the equivalent of ``Base.compile`` from the command line.
38+
* The precompiled `.ji` file includes a list of dependencies (modules and files that
39+
were imported/included at precompile-time), and the module is automatically recompiled
40+
upon `import` when any of its dependencies have changed. Explicit dependencies
41+
on other files can be declared with `include_dependency(path)` ([#12458]).
42+
43+
* New option `--output-incremental={yes|no}` added to invoke the equivalent of ``Base.compilecache`` from the command line.
3844

3945
* The syntax `new{parameters...}(...)` can be used in constructors to specify parameters for
4046
the type to be constructed ([#8135]).
@@ -1557,3 +1563,5 @@ Too numerous to mention.
15571563
[#12137]: https://github.com/JuliaLang/julia/issues/12137
15581564
[#12162]: https://github.com/JuliaLang/julia/issues/12162
15591565
[#12393]: https://github.com/JuliaLang/julia/issues/12393
1566+
[#12458]: https://github.com/JuliaLang/julia/issues/12458
1567+
[#12491]: https://github.com/JuliaLang/julia/issues/12491

base/docs/helpdb.jl

+22-2
Original file line numberDiff line numberDiff line change
@@ -8472,12 +8472,12 @@ whos
84728472
doc"""
84738473
```rst
84748474
::
8475-
compile(module::Symbol)
8475+
Base.compilecache(module::Symbol)
84768476
84778477
Creates a precompiled cache file for module (see help for ``require``) and all of its dependencies. This can be used to reduce package load times. Cache files are stored in LOAD_CACHE_PATH[1], which defaults to `~/.julia/lib/VERSION`. See the manual section `Module initialization and precompilation` (under `Modules`) for important notes.
84788478
```
84798479
"""
8480-
compile
8480+
compilecache
84818481

84828482
doc"""
84838483
```rst
@@ -14579,6 +14579,26 @@ used via `include`. It has no effect outside of compilation.
1457914579
"""
1458014580
include_dependency
1458114581

14582+
doc"""
14583+
```rst
14584+
::
14585+
__precompile__(isprecompilable::Bool=true)
14586+
14587+
Specify whether the file calling this function is precompilable. If
14588+
``isprecompilable`` is ``true``, then ``__precompile__`` throws an exception
14589+
*unless* the file is being precompiled, and in a module file it causes
14590+
the module to be automatically precompiled when it is imported.
14591+
Typically, ``__precompile__()`` should occur before the ``module``
14592+
declaration in the file, or better yet ``VERSION >= v"0.4" &&
14593+
__precompile__()`` in order to be backward-compatible with Julia 0.3.
14594+
14595+
If a module or file is *not* safely precompilable, it should call
14596+
``__precompile__(false)`` in order to throw an error if Julia attempts
14597+
to precompile it.
14598+
```
14599+
"""
14600+
__precompile__
14601+
1458214602
doc"""
1458314603
```rst
1458414604
::

base/exports.jl

+1
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,7 @@ export
11011101
workspace,
11021102

11031103
# loading source files
1104+
__precompile__,
11041105
evalfile,
11051106
include,
11061107
include_string,

base/loading.jl

+52-13
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,30 @@ function include_dependency(path::AbstractString)
131131
return nothing
132132
end
133133

134+
# We throw PrecompilableError(true) when a module wants to be precompiled but isn't,
135+
# and PrecompilableError(false) when a module doesn't want to be precompiled but is
136+
immutable PrecompilableError <: Exception
137+
isprecompilable::Bool
138+
end
139+
function show(io::IO, ex::PrecompilableError)
140+
if ex.isprecompilable
141+
print(io, "__precompile__(true) is only allowed in module files being imported")
142+
else
143+
print(io, "__precompile__(false) is not allowed in files that are being precompiled")
144+
end
145+
end
146+
precompilableerror(ex::PrecompilableError, c) = ex.isprecompilable == c
147+
precompilableerror(ex::LoadError, c) = precompilableerror(ex.error, c)
148+
precompilableerror(ex, c) = false
149+
150+
# put at the top of a file to force it to be precompiled (true), or
151+
# to be prevent it from being precompiled (false).
152+
function __precompile__(isprecompilable::Bool=true)
153+
if myid() == 1 && isprecompilable != (0 != ccall(:jl_generating_output, Cint, ()))
154+
throw(PrecompilableError(isprecompilable))
155+
end
156+
end
157+
134158
# require always works in Main scope and loads files from node 1
135159
toplevel_load = true
136160
function require(mod::Symbol)
@@ -155,8 +179,8 @@ function require(mod::Symbol)
155179
return
156180
end
157181
if JLOptions().incremental != 0
158-
# spawn off a new incremental compile task from node 1 for recursive `require` calls
159-
cachefile = compile(mod)
182+
# spawn off a new incremental precompile task from node 1 for recursive `require` calls
183+
cachefile = compilecache(mod)
160184
if nothing === _require_from_serialized(1, mod, cachefile, last)
161185
warn("require failed to create a precompiled cache file")
162186
end
@@ -166,13 +190,26 @@ function require(mod::Symbol)
166190
name = string(mod)
167191
path = find_in_node_path(name, source_dir(), 1)
168192
path === nothing && throw(ArgumentError("$name not found in path"))
169-
if last && myid() == 1 && nprocs() > 1
170-
# broadcast top-level import/using from node 1 (only)
171-
content = open(readall, path)
172-
refs = Any[ @spawnat p eval(Main, :(Base.include_from_node1($path))) for p in procs() ]
173-
for r in refs; wait(r); end
174-
else
175-
eval(Main, :(Base.include_from_node1($path)))
193+
try
194+
if last && myid() == 1 && nprocs() > 1
195+
# include on node 1 first to check for PrecompilableErrors
196+
eval(Main, :(Base.include_from_node1($path)))
197+
198+
# broadcast top-level import/using from node 1 (only)
199+
refs = Any[ @spawnat p eval(Main, :(Base.include_from_node1($path))) for p in filter(x -> x != 1, procs()) ]
200+
for r in refs; wait(r); end
201+
else
202+
eval(Main, :(Base.include_from_node1($path)))
203+
end
204+
catch ex
205+
if !precompilableerror(ex, true)
206+
rethrow() # rethrow non-precompilable=true errors
207+
end
208+
isinteractive() && info("Precompiling module $mod...")
209+
cachefile = compilecache(mod)
210+
if nothing === _require_from_serialized(1, mod, cachefile, last)
211+
error("__precompile__(true) but require failed to create a precompiled cache file")
212+
end
176213
end
177214
finally
178215
toplevel_load = last
@@ -284,17 +321,19 @@ function create_expr_cache(input::AbstractString, output::AbstractString)
284321
return pobj
285322
end
286323

287-
compile(mod::Symbol) = compile(string(mod))
288-
function compile(name::ByteString)
289-
myid() == 1 || error("can only compile from node 1")
324+
compilecache(mod::Symbol) = compilecache(string(mod))
325+
function compilecache(name::ByteString)
326+
myid() == 1 || error("can only precompile from node 1")
290327
path = find_in_path(name)
291328
path === nothing && throw(ArgumentError("$name not found in path"))
292329
cachepath = LOAD_CACHE_PATH[1]
293330
if !isdir(cachepath)
294331
mkpath(cachepath)
295332
end
296333
cachefile = abspath(cachepath, name*".ji")
297-
create_expr_cache(path, cachefile)
334+
if !success(create_expr_cache(path, cachefile))
335+
error("Failed to precompile $name to $cachefile")
336+
end
298337
return cachefile
299338
end
300339

doc/manual/modules.rst

+19-6
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,22 @@ incremental compile and custom system image.
263263
To create a custom system image that can be used to start julia with the -J option,
264264
recompile Julia after modifying the file ``base/userimg.jl`` to require the desired modules.
265265

266-
To create an incremental precompiled module file,
267-
call ``Base.compile(modulename::Symbol)``.
268-
The resulting cache files will be stored in ``Base.LOAD_CACHE_PATH[1]``.
266+
To create an incremental precompiled module file, add
267+
``__precompile__()`` at the top of your module file (before the
268+
``module`` starts). This will cause it to be automatically compiled
269+
the first time it is imported. Alternatively, you can manually call
270+
``Base.compilecache(modulename)``. The resulting cache files will be
271+
stored in ``Base.LOAD_CACHE_PATH[1]``. Subsequently, the module is
272+
automatically recompiled upon ``import`` whenever any of its
273+
dependencies change; dependencies are modules it imports, the Julia
274+
build, files it includes, or explicit dependencies declared by
275+
``include_dependency(path)`` in the module file(s). Precompiling a
276+
module also recursively precompiles any modules that are imported
277+
therein. If you know that it is *not* safe to precompile your module
278+
(for the reasons described below), you should put
279+
``__precompile__(false)`` in the module file to cause ``Base.compilecache`` to
280+
throw an error (and thereby prevent the module from being imported by
281+
any other precompiled module).
269282

270283
In order to make your module work with precompilation,
271284
however, you may need to change your module to explicitly separate any
@@ -379,15 +392,15 @@ Other known potential failure scenarios include:
379392
# or move the assignment into the runtime:
380393
__init__() = global mystdout = Base.STDOUT #= also works =#
381394

382-
Several additional restrictions are placed on the operations that can be done while compiling code
395+
Several additional restrictions are placed on the operations that can be done while precompiling code
383396
to help the user avoid other wrong-behavior situations:
384397

385398
1. Calling ``eval`` to cause a side-effect in another module.
386-
This will also cause a warning to be emitted when the incremental compile flag is set.
399+
This will also cause a warning to be emitted when the incremental precompile flag is set.
387400

388401
2. ``global const`` statements from local scope after ``__init__()`` has been started (see issue #12010 for plans to add an error for this)
389402

390-
3. Replacing a module (or calling ``workspace()``) is a runtime error while doing an incremental compile.
403+
3. Replacing a module (or calling ``workspace()``) is a runtime error while doing an incremental precompile.
391404

392405
A few other points to be aware of:
393406

src/dump.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -1657,7 +1657,7 @@ static void jl_reinit_item(ios_t *f, jl_value_t *v, int how) {
16571657
jl_errorf("invalid redefinition of constant %s", mod->name->name); // this also throws
16581658
}
16591659
if (jl_generating_output() && jl_options.incremental) {
1660-
jl_errorf("cannot replace module %s during incremental compile", mod->name->name);
1660+
jl_errorf("cannot replace module %s during incremental precompile", mod->name->name);
16611661
}
16621662
jl_printf(JL_STDERR, "WARNING: replacing module %s\n", mod->name->name);
16631663
}

test/compile.jl

+22-4
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ insert!(LOAD_PATH, 1, dir)
77
insert!(Base.LOAD_CACHE_PATH, 1, dir)
88
Foo_module = :Foo4b3a94a1a081a8cb
99
try
10-
file = joinpath(dir, "$Foo_module.jl")
10+
Foo_file = joinpath(dir, "$Foo_module.jl")
1111

12-
open(file, "w") do f
12+
open(Foo_file, "w") do f
1313
print(f, """
14+
__precompile__(true)
1415
module $Foo_module
1516
@doc "foo function" foo(x) = x + 1
1617
include_dependency("foo.jl")
@@ -22,10 +23,17 @@ try
2223
""")
2324
end
2425

25-
cachefile = Base.compile(Foo_module)
26+
if myid() == 1
27+
@test_throws Base.PrecompilableError __precompile__(true)
28+
@test_throws LoadError include(Foo_file) # from __precompile__(true)
29+
end
30+
31+
Base.require(Foo_module)
32+
cachefile = joinpath(dir, "$Foo_module.ji")
2633

2734
# use _require_from_serialized to ensure that the test fails if
2835
# the module doesn't load from the image:
36+
println(STDERR, "\nNOTE: The following 'replacing module' warning indicates normal operation:")
2937
@test nothing !== Base._require_from_serialized(myid(), Foo_module, true)
3038

3139
let Foo = eval(Main, Foo_module)
@@ -39,9 +47,19 @@ try
3947
deps = Base.cache_dependencies(cachefile)
4048
@test sort(deps[1]) == map(s -> (s, Base.module_uuid(eval(s))),
4149
[:Base,:Core,:Main])
42-
@test sort(deps[2]) == [file,joinpath(dir,"bar.jl"),joinpath(dir,"foo.jl")]
50+
@test sort(deps[2]) == [Foo_file,joinpath(dir,"bar.jl"),joinpath(dir,"foo.jl")]
4351
end
4452

53+
Baz_file = joinpath(dir, "Baz.jl")
54+
open(Baz_file, "w") do f
55+
print(f, """
56+
__precompile__(false)
57+
module Baz
58+
end
59+
""")
60+
end
61+
println(STDERR, "\nNOTE: The following 'LoadError: __precompile__(false)' indicates normal operation")
62+
@test_throws ErrorException Base.compilecache("Baz") # from __precompile__(false)
4563
finally
4664
splice!(Base.LOAD_CACHE_PATH, 1)
4765
splice!(LOAD_PATH, 1)

0 commit comments

Comments
 (0)