Skip to content

Commit 11dd8b9

Browse files
authored
Merge pull request #1210 from JuliaLang/precompile
Improve precompilation
2 parents 6c5c88a + de4f664 commit 11dd8b9

File tree

7 files changed

+141
-41
lines changed

7 files changed

+141
-41
lines changed

docs/src/_changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ Changelog](https://keepachangelog.com).
2828
needing to rebuild IJulia to update the kernel after every patch release of
2929
Julia, but it does mean that IJulia will only create kernels for each Julia
3030
minor release instead of each patch release.
31+
- Extended the precompilation workload to cover more calls, and added a
32+
workaround to minimize TTFX when Revise is used ([#1210]).
3133

3234
## [v1.31.1] - 2025-10-20
3335

src/IJulia.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,14 @@ REPL.REPLDisplay(repl::MiniREPL) = repl.display
140140
stop_event::Base.Event = Base.Event()
141141
waitloop_task::RefValue{Task} = Ref{Task}()
142142

143+
# This is a bit strange, but IJulia invalidates a lot of Revise because of
144+
# its definition of Base.lock(::IJuliaStdio). This contributes significantly
145+
# to TTFX because Revise.revise() is automatically set as a preexecute hook,
146+
# so if run_kernel() enables the preexecute hook it also spawns a task to
147+
# precompile Revise.revise() asynchronously. We store the task here to
148+
# prevent any lingering tasks upon shutdown that could cause hangs.
149+
revise_precompile_task::RefValue{Task} = Ref{Task}()
150+
143151
requests_task::RefValue{Task} = Ref{Task}()
144152
watch_stdout_task::RefValue{Task} = Ref{Task}()
145153
watch_stderr_task::RefValue{Task} = Ref{Task}()
@@ -199,6 +207,10 @@ function start_shutdown(kernel::Kernel)
199207
end
200208

201209
function Base.close(kernel::Kernel)
210+
if isassigned(kernel.revise_precompile_task)
211+
wait(kernel.revise_precompile_task[])
212+
end
213+
202214
# Reset the IO streams first so that any later errors get printed
203215
if kernel.capture_stdout
204216
redirect_stdout(orig_stdout[])

src/comm_manager.jl

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ function Comm(target,
2929
return comm
3030
end
3131

32-
comm_target(comm :: Comm{target}) where {target} = target
32+
comm_target(comm :: Comm{target}) where {target} = target::Symbol
3333

3434
function comm_info_request(sock, kernel, msg)
3535
reply = if haskey(msg.content, "target_name")
36-
t = Symbol(msg.content["target_name"])
36+
t = Symbol(msg.content["target_name"]::String)
3737
filter(kv -> comm_target(kv.second) == t, kernel.comms)
3838
else
3939
# reply with all comms.
@@ -87,9 +87,9 @@ end
8787

8888
function comm_open(sock, kernel, msg)
8989
if haskey(msg.content, "comm_id")
90-
comm_id = msg.content["comm_id"]
90+
comm_id = msg.content["comm_id"]::String
9191
if haskey(msg.content, "target_name")
92-
target = msg.content["target_name"]
92+
target = msg.content["target_name"]::String
9393
if !haskey(msg.content, "data")
9494
msg.content["data"] = Dict()
9595
end
@@ -104,11 +104,13 @@ function comm_open(sock, kernel, msg)
104104
msg, "comm_close"))
105105
end
106106
end
107+
108+
nothing
107109
end
108110

109111
function comm_msg(sock, kernel, msg)
110112
if haskey(msg.content, "comm_id")
111-
comm_id = msg.content["comm_id"]
113+
comm_id = msg.content["comm_id"]::String
112114
if haskey(kernel.comms, comm_id)
113115
comm = kernel.comms[comm_id]
114116
else
@@ -121,11 +123,13 @@ function comm_msg(sock, kernel, msg)
121123
end
122124
comm.on_msg(msg)
123125
end
126+
127+
nothing
124128
end
125129

126130
function comm_close(sock, kernel, msg)
127131
if haskey(msg.content, "comm_id")
128-
comm_id = msg.content["comm_id"]
132+
comm_id = msg.content["comm_id"]::String
129133
comm = kernel.comms[comm_id]
130134

131135
if !haskey(msg.content, "data")
@@ -135,6 +139,8 @@ function comm_close(sock, kernel, msg)
135139

136140
delete!(kernel.comms, comm.id)
137141
end
142+
143+
nothing
138144
end
139145

140146
end # module

src/execute_request.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ function execute_request(socket, kernel, msg)
7575
hcode = replace(code, r"^\s*\?" => "")
7676

7777
try
78+
if isassigned(kernel.revise_precompile_task)
79+
wait(kernel.revise_precompile_task[])
80+
end
7881
foreach(invokelatest, IJulia._preexecute_hooks)
7982

8083
kernel.ans = result = if hcode != code # help request

src/kernel.jl

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ function run_kernel()
88
ENV["COLUMNS"] = get(ENV, "COLUMNS", "80")
99

1010
println(Core.stdout, "Starting kernel event loops.")
11-
IJulia.init(ARGS, IJulia.Kernel())
11+
kernel = Kernel()
12+
IJulia.init(ARGS, kernel)
1213

1314
let startupfile = !isempty(DEPOT_PATH) ? abspath(DEPOT_PATH[1], "config", "startup_ijulia.jl") : ""
1415
isfile(startupfile) && Base.JLOptions().startupfile != 2 && Base.include(Main, startupfile)
@@ -19,11 +20,12 @@ function run_kernel()
1920

2021
# check whether Revise is running and as needed configure it to run before every prompt
2122
if isdefined(Main, :Revise)
22-
let mode = get(ENV, "JULIA_REVISE", "auto")
23-
mode == "auto" && IJulia.push_preexecute_hook(Main.Revise.revise)
23+
if get(ENV, "JULIA_REVISE", "auto") == "auto"
24+
IJulia.push_preexecute_hook(Main.Revise.revise)
25+
kernel.revise_precompile_task[] = Threads.@spawn precompile(Main.Revise.revise, ())
2426
end
2527
end
2628

27-
wait(IJulia._default_kernel::Kernel)
28-
close(IJulia._default_kernel::Kernel)
29+
wait(kernel)
30+
close(kernel)
2931
end

src/precompile.jl

Lines changed: 59 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,13 @@ const _TEST_KEY = "a0436f6c-1916-498b-8eb9-e81ab9368e84"
66

77
# How to update the precompilation workload:
88
# 1. Uncomment the `@show` expressions in `recv_ipython()` in msg.jl.
9-
# 2. Copy this workload into tests/kernel.jl and update as desired:
10-
#
11-
# Kernel(profile; capture_stdout=false, capture_stderr=false) do kernel
12-
# jupyter_client(profile) do client
13-
# kernel_info(client)
14-
# execute(client, "42")
15-
# execute(client, "?import")
16-
# execute(client, """error("foo")""")
17-
# end
18-
# end
19-
#
20-
# 3. When the above runs it will print out the contents of the received messages
21-
# as strings. You can copy these verbatim into the precompilation workload
22-
# below. Note that if you modify any step of the workload you will need to
23-
# update *all* the messages to ensure they have the right parent
24-
# headers/signatures.
9+
# 2. Uncomment the call to `run_precompile()` in tests/kernel.jl (and comment
10+
# out the rest of the tests if you like).
11+
# 3. When the `run_precompile()` runs it will print out the contents of the
12+
# received messages as strings. You can copy these verbatim into the
13+
# precompilation workload below. Note that if you modify any step of the
14+
# workload you will need to update *all* the messages to ensure they have the
15+
# right parent headers/signatures.
2516
@compile_workload begin
2617
local profile = create_profile(45_000; key=_TEST_KEY)
2718

@@ -38,21 +29,32 @@ const _TEST_KEY = "a0436f6c-1916-498b-8eb9-e81ab9368e84"
3829
port = profile["shell_port"]
3930
ZMQ.connect(requests_socket, "tcp://$(ip):$(port)")
4031

41-
# kernel_info
42-
idents = ["a1cd3a77-85e3881309b0cc3701e51156"]
43-
signature = "af03588308cec89e76d0568134c0eaf24e9fe869aac729d5b9aed6baac6d369b"
44-
header = "{\"msg_id\": \"a1cd3a77-85e3881309b0cc3701e51156_1694526_0\", \"msg_type\": \"kernel_info_request\", \"username\": \"james\", \"session\": \"a1cd3a77-85e3881309b0cc3701e51156\", \"date\": \"2025-08-29T09:54:47.389494Z\", \"version\": \"5.4\"}"
32+
# Kernel info
33+
idents = ["626c4427-479d61edd6b98ccca470f2d6"]
34+
signature = "306b616a72292e9a736fe42b3c7d6fd51e10653ea2c5bc8f33810a06d33df8b5"
35+
header = "{\"msg_id\": \"626c4427-479d61edd6b98ccca470f2d6_3346283_0\", \"msg_type\": \"kernel_info_request\", \"username\": \"james\", \"session\": \"626c4427-479d61edd6b98ccca470f2d6\", \"date\": \"2025-11-02T18:59:07.097698Z\", \"version\": \"5.4\"}"
4536
parent_header = "{}"
4637
metadata = "{}"
4738
content = "{}"
4839

4940
ZMQ.send_multipart(requests_socket, [only(idents), "<IDS|MSG>", signature, header, parent_header, metadata, content])
5041
ZMQ.recv_multipart(requests_socket, String)
5142

43+
# Completion request
44+
idents = ["626c4427-479d61edd6b98ccca470f2d6"]
45+
signature = "721d3988d167417f9f0f03a823d3870470a5d924184a9a07dff422d8993eefa8"
46+
header = "{\"msg_id\": \"626c4427-479d61edd6b98ccca470f2d6_3346283_1\", \"msg_type\": \"complete_request\", \"username\": \"james\", \"session\": \"626c4427-479d61edd6b98ccca470f2d6\", \"date\": \"2025-11-02T18:59:07.587641Z\", \"version\": \"5.4\"}"
47+
parent_header = "{}"
48+
metadata = "{}"
49+
content = "{\"code\": \"mk\", \"cursor_pos\": 2}"
50+
51+
ZMQ.send_multipart(requests_socket, [only(idents), "<IDS|MSG>", signature, header, parent_header, metadata, content])
52+
ZMQ.recv_multipart(requests_socket, String)
53+
5254
# Execute `42`
53-
idents = ["a1cd3a77-85e3881309b0cc3701e51156"]
54-
signature = "23df4f581ab69b5b249caced71fc0a77bcb8f5c1f4eeb88d44ca49456db16e0d"
55-
header = "{\"msg_id\": \"a1cd3a77-85e3881309b0cc3701e51156_1694526_1\", \"msg_type\": \"execute_request\", \"username\": \"james\", \"session\": \"a1cd3a77-85e3881309b0cc3701e51156\", \"date\": \"2025-08-29T09:54:49.546467Z\", \"version\": \"5.4\"}"
55+
idents = ["626c4427-479d61edd6b98ccca470f2d6"]
56+
signature = "01e135a939f00a11708ca906c6a16ce1226ad07669b7dad49c12c4b792ef6242"
57+
header = "{\"msg_id\": \"626c4427-479d61edd6b98ccca470f2d6_3346283_2\", \"msg_type\": \"execute_request\", \"username\": \"james\", \"session\": \"626c4427-479d61edd6b98ccca470f2d6\", \"date\": \"2025-11-02T18:59:14.947227Z\", \"version\": \"5.4\"}"
5658
parent_header = "{}"
5759
metadata = "{}"
5860
content = "{\"code\": \"42\", \"silent\": false, \"store_history\": true, \"user_expressions\": {}, \"allow_stdin\": true, \"stop_on_error\": true}"
@@ -61,9 +63,9 @@ const _TEST_KEY = "a0436f6c-1916-498b-8eb9-e81ab9368e84"
6163
ZMQ.recv_multipart(requests_socket, String)
6264

6365
# Execute `?import`
64-
idents = ["a1cd3a77-85e3881309b0cc3701e51156"]
65-
signature = "8f2b31cc4751d17bbd4d1216180e129b0759fdc9f05e0de1e91ca03db85fc5e1"
66-
header = "{\"msg_id\": \"a1cd3a77-85e3881309b0cc3701e51156_1694526_2\", \"msg_type\": \"execute_request\", \"username\": \"james\", \"session\": \"a1cd3a77-85e3881309b0cc3701e51156\", \"date\": \"2025-08-29T09:54:49.951328Z\", \"version\": \"5.4\"}"
66+
idents = ["626c4427-479d61edd6b98ccca470f2d6"]
67+
signature = "a0fc54c9ed7250e8a151a766c7293843a1b8f91b1d766579a4761c18f34862df"
68+
header = "{\"msg_id\": \"626c4427-479d61edd6b98ccca470f2d6_3346283_3\", \"msg_type\": \"execute_request\", \"username\": \"james\", \"session\": \"626c4427-479d61edd6b98ccca470f2d6\", \"date\": \"2025-11-02T18:59:15.621963Z\", \"version\": \"5.4\"}"
6769
parent_header = "{}"
6870
metadata = "{}"
6971
content = "{\"code\": \"?import\", \"silent\": false, \"store_history\": true, \"user_expressions\": {}, \"allow_stdin\": true, \"stop_on_error\": true}"
@@ -72,25 +74,52 @@ const _TEST_KEY = "a0436f6c-1916-498b-8eb9-e81ab9368e84"
7274
ZMQ.recv_multipart(requests_socket, String)
7375

7476
# Execute `error("foo")`
75-
idents = ["a1cd3a77-85e3881309b0cc3701e51156"]
76-
signature = "c8415a60d32d231b582128a8f85ecce0996ee76734ab6aecf93af850d7e19e4a"
77-
header = "{\"msg_id\": \"a1cd3a77-85e3881309b0cc3701e51156_1694526_3\", \"msg_type\": \"execute_request\", \"username\": \"james\", \"session\": \"a1cd3a77-85e3881309b0cc3701e51156\", \"date\": \"2025-08-29T09:54:50.755285Z\", \"version\": \"5.4\"}"
77+
idents = ["626c4427-479d61edd6b98ccca470f2d6"]
78+
signature = "7c7810bce6ff448c3ad8b0c928822d03ad8baf838e417ea5c29ec620a2cc36a7"
79+
header = "{\"msg_id\": \"626c4427-479d61edd6b98ccca470f2d6_3346283_4\", \"msg_type\": \"execute_request\", \"username\": \"james\", \"session\": \"626c4427-479d61edd6b98ccca470f2d6\", \"date\": \"2025-11-02T18:59:18.648766Z\", \"version\": \"5.4\"}"
7880
parent_header = "{}"
7981
metadata = "{}"
8082
content = "{\"code\": \"error(\\\"foo\\\")\", \"silent\": false, \"store_history\": true, \"user_expressions\": {}, \"allow_stdin\": true, \"stop_on_error\": true}"
8183

8284
ZMQ.send_multipart(requests_socket, [only(idents), "<IDS|MSG>", signature, header, parent_header, metadata, content])
8385
ZMQ.recv_multipart(requests_socket, String)
8486

87+
# Get history
88+
idents = ["626c4427-479d61edd6b98ccca470f2d6"]
89+
signature = "1d4bc84a2efb28efa0e8efe8eba32bc6b84decfd55d95e765b062a1adae2ae62"
90+
header = "{\"msg_id\": \"626c4427-479d61edd6b98ccca470f2d6_3346283_5\", \"msg_type\": \"history_request\", \"username\": \"james\", \"session\": \"626c4427-479d61edd6b98ccca470f2d6\", \"date\": \"2025-11-02T18:59:18.813677Z\", \"version\": \"5.4\"}"
91+
parent_header = "{}"
92+
metadata = "{}"
93+
content = "{\"raw\": true, \"output\": false, \"hist_access_type\": \"range\", \"session\": 0, \"start\": 0}"
94+
95+
ZMQ.send_multipart(requests_socket, [only(idents), "<IDS|MSG>", signature, header, parent_header, metadata, content])
96+
ZMQ.recv_multipart(requests_socket, String)
97+
98+
# Get comm info
99+
idents = ["626c4427-479d61edd6b98ccca470f2d6"]
100+
signature = "ff8e9a57a81f5cbdebd12a34a09aad953d63e24e0c866fc3af936a4ef764a181"
101+
header = "{\"msg_id\": \"626c4427-479d61edd6b98ccca470f2d6_3346283_6\", \"msg_type\": \"comm_info_request\", \"username\": \"james\", \"session\": \"626c4427-479d61edd6b98ccca470f2d6\", \"date\": \"2025-11-02T18:59:18.953835Z\", \"version\": \"5.4\"}"
102+
parent_header = "{}"
103+
metadata = "{}"
104+
content = "{}"
105+
106+
ZMQ.send_multipart(requests_socket, [only(idents), "<IDS|MSG>", signature, header, parent_header, metadata, content])
107+
ZMQ.recv_multipart(requests_socket, String)
108+
85109
close(requests_socket)
86110
end
87111
end
112+
113+
# Clear global variables
114+
empty!(_preexecute_hooks)
115+
empty!(_postexecute_hooks)
116+
empty!(_posterror_hooks)
88117
end
89118

90119
# This function is executed by Jupyter so make sure that it's precompiled
91120
precompile(run_kernel, ())
92121

93122
# Precompile all the handlers
94-
for f in handlers
123+
for f in values(handlers)
95124
precompile(f, (ZMQ.Socket, Kernel, Msg))
96125
end

test/kernel.jl

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ history(client) = make_request(client.history, client.get_shell_msg)
9191
shutdown(client; wait=true) = make_request(client.shutdown, client.get_control_msg; wait)
9292
execute(client, code) = make_request(client.execute, client.get_shell_msg; code)
9393
inspect(client, code) = make_request(client.inspect, client.get_shell_msg; code)
94+
complete(client, code) = make_request(client.complete, client.get_shell_msg; code)
9495
get_stdin_msg(client) = make_request(Returns(nothing), client.get_stdin_msg)
9596
get_iopub_msg(client) = make_request(Returns(nothing), client.get_iopub_msg)
9697

@@ -130,6 +131,51 @@ function jupyter_client(f, profile)
130131
end
131132
end
132133

134+
function run_precompile()
135+
profile = IJulia.create_profile(; key=IJulia._TEST_KEY)
136+
137+
zmq_recv = """
138+
139+
ZMQ.send_multipart(requests_socket, [only(idents), "<IDS|MSG>", signature, header, parent_header, metadata, content])
140+
ZMQ.recv_multipart(requests_socket, String)
141+
"""
142+
143+
Kernel(profile; capture_stdout=false, capture_stderr=false) do kernel
144+
jupyter_client(profile) do client
145+
println("# Kernel info")
146+
kernel_info(client)
147+
println(zmq_recv)
148+
149+
println("# Completion request")
150+
complete(client, "mk")
151+
println(zmq_recv)
152+
153+
println("# Execute `42`")
154+
execute(client, "42")
155+
println(zmq_recv)
156+
157+
println("# Execute `?import`")
158+
execute(client, "?import")
159+
println(zmq_recv)
160+
161+
println("""# Execute `error("foo")`""")
162+
execute(client, """error("foo")""")
163+
println(zmq_recv)
164+
165+
println("# Get history")
166+
history(client)
167+
println(zmq_recv)
168+
169+
println("# Get comm info")
170+
comm_info(client)
171+
println(zmq_recv)
172+
end
173+
end
174+
end
175+
176+
# Uncomment this to run the precompilation workload
177+
# run_precompile()
178+
133179
@testset "Kernel" begin
134180
profile = IJulia.create_profile(; key=IJulia._TEST_KEY)
135181
profile_kwargs = Dict([Symbol(key) => value for (key, value) in profile])

0 commit comments

Comments
 (0)