Skip to content

Commit a5d0435

Browse files
committed
Add a precompilation workload
Other notable changes: - Moved `create_profile()` into init.jl so we can use it in `init()` and the precompilation workload. - Added an option to not capture stdin, because during precompilation it's not possible to redirect stdin.
1 parent 20d9452 commit a5d0435

File tree

7 files changed

+130
-43
lines changed

7 files changed

+130
-43
lines changed

Project.toml

+4
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
1212
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
1313
MbedTLS = "739be429-bea8-5141-9913-cc70e7f3736d"
1414
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
15+
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
1516
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
1617
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
1718
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
19+
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
1820
SoftGlobalScope = "b85f4697-e234-5449-a836-ec8e2f98b302"
1921
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
2022
ZMQ = "c2297ded-f4af-51ae-bb23-16f91089e4e1"
@@ -29,9 +31,11 @@ Logging = "1"
2931
Markdown = "1"
3032
MbedTLS = "0.5,0.6,0.7,1"
3133
Pkg = "1"
34+
PrecompileTools = "1.2.1"
3235
Printf = "1"
3336
REPL = "1"
3437
Random = "1"
38+
Sockets = "1"
3539
SoftGlobalScope = "1"
3640
UUIDs = "1"
3741
ZMQ = "1.3"

src/IJulia.jl

+9-2
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ end
9999

100100
capture_stdout::Bool = true
101101
capture_stderr::Bool = !IJULIA_DEBUG
102+
capture_stdin::Bool = true
102103

103104
# This dict holds a map from CommID to Comm so that we can
104105
# pick out the right Comm object when messages arrive
@@ -187,10 +188,15 @@ function Base.close(kernel::Kernel)
187188
close(kernel.read_stderr[])
188189
wait(kernel.watch_stderr_task[])
189190
end
190-
redirect_stdin(orig_stdin[])
191+
if kernel.capture_stdin
192+
redirect_stdin(orig_stdin[])
193+
end
191194

192195
# Reset the logger so that @log statements work and pop the InlineDisplay
193-
Logging.global_logger(orig_logger[])
196+
if isassigned(orig_logger)
197+
# orig_logger seems to not be set during precompilation
198+
Logging.global_logger(orig_logger[])
199+
end
194200
popdisplay()
195201

196202
# Close all sockets
@@ -473,5 +479,6 @@ include("execute_request.jl")
473479
include("handlers.jl")
474480
include("heartbeat.jl")
475481
include("inline.jl")
482+
include("precompile.jl")
476483

477484
end # IJulia

src/init.jl

+35-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Random: seed!
2+
import Sockets
23
import Logging
34
import Logging: AbstractLogger, ConsoleLogger
45

@@ -30,6 +31,35 @@ end
3031
const minirepl = Ref{MiniREPL}()
3132
end
3233

34+
function getports(port_hint, n)
35+
ports = Int[]
36+
37+
for i in 1:n
38+
port, server = Sockets.listenany(Sockets.localhost, port_hint)
39+
close(server)
40+
push!(ports, port)
41+
port_hint = port + 1
42+
end
43+
44+
return ports
45+
end
46+
47+
function create_profile(port_hint=8080; key=uuid4())
48+
ports = getports(port_hint, 5)
49+
50+
Dict(
51+
"transport" => "tcp",
52+
"ip" => "127.0.0.1",
53+
"control_port" => ports[1],
54+
"shell_port" => ports[2],
55+
"stdin_port" => ports[3],
56+
"hb_port" => ports[4],
57+
"iopub_port" => ports[5],
58+
"signature_scheme" => "hmac-sha256",
59+
"key" => key
60+
)
61+
end
62+
3363
"""
3464
init(args, kernel)
3565
@@ -48,16 +78,7 @@ function init(args, kernel, profile=nothing)
4878
else
4979
# generate profile and save
5080
let port0 = 5678
51-
merge!(kernel.profile, Dict{String,Any}(
52-
"ip" => "127.0.0.1",
53-
"transport" => "tcp",
54-
"stdin_port" => port0,
55-
"control_port" => port0+1,
56-
"hb_port" => port0+2,
57-
"shell_port" => port0+3,
58-
"iopub_port" => port0+4,
59-
"key" => uuid4()
60-
))
81+
merge!(kernel.profile, create_profile(port0))
6182
fname = "profile-$(getpid()).json"
6283
kernel.connection_file = "$(pwd())/$fname"
6384
println("connect ipython with --existing $(kernel.connection_file)")
@@ -108,7 +129,10 @@ function init(args, kernel, profile=nothing)
108129
kernel.read_stderr[], = redirect_stderr()
109130
redirect_stderr(IJuliaStdio(stderr, kernel, "stderr"))
110131
end
111-
redirect_stdin(IJuliaStdio(stdin, kernel, "stdin"))
132+
if kernel.capture_stdin
133+
redirect_stdin(IJuliaStdio(stdin, kernel, "stdin"))
134+
end
135+
112136
@static if VERSION < v"1.11"
113137
minirepl[] = MiniREPL(TextDisplay(stdout))
114138
end

src/msg.jl

+10
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ function recv_ipython(socket, kernel)
8686
if signature != hmac(header, parent_header, metadata, content, kernel)
8787
error("Invalid HMAC signature") # What should we do here?
8888
end
89+
90+
# Note: don't remove these lines, they're useful for creating a
91+
# precompilation workload.
92+
# @show idents
93+
# @show signature
94+
# @show header
95+
# @show parent_header
96+
# @show metadata
97+
# @show content
98+
8999
m = Msg(idents, JSON.parse(header), JSON.parse(content), JSON.parse(parent_header), JSON.parse(metadata))
90100
@vprintln("RECEIVED $m")
91101
return m

src/precompile.jl

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import PrecompileTools: @compile_workload
2+
3+
# This key is used by the tests and precompilation workload to keep some
4+
# consistency in the message signatures.
5+
const _TEST_KEY = "a0436f6c-1916-498b-8eb9-e81ab9368e84"
6+
7+
# How to update the precompilation workload:
8+
# 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, "error(42)")
16+
# end
17+
# end
18+
#
19+
# 3. When the above runs it will print out the contents of the received messages
20+
# as strings. You can copy these verbatim into the precompilation workload
21+
# below. Note that if you modify any step of the workload you will need to
22+
# update *all* the messages to ensure they have the right parent
23+
# headers/signatures.
24+
@compile_workload begin
25+
profile = create_profile(45_000; key=_TEST_KEY)
26+
27+
Kernel(profile; capture_stdout=false, capture_stderr=false, capture_stdin=false) do kernel
28+
# Connect as a client to the kernel
29+
requests_socket = ZMQ.Socket(ZMQ.DEALER)
30+
ip = profile["ip"]
31+
port = profile["shell_port"]
32+
ZMQ.connect(requests_socket, "tcp://$(ip):$(port)")
33+
34+
# kernel_info
35+
idents = ["d2bd8e47-b2c9cd130d2967a19f52c1a3"]
36+
signature = "3c4f523a0e8b80e5b3e35756d75f62d12b851e1fd67c609a9119872e911f83d2"
37+
header = "{\"msg_id\": \"d2bd8e47-b2c9cd130d2967a19f52c1a3_3534705_0\", \"msg_type\": \"kernel_info_request\", \"username\": \"james\", \"session\": \"d2bd8e47-b2c9cd130d2967a19f52c1a3\", \"date\": \"2025-02-20T22:29:47.616834Z\", \"version\": \"5.4\"}"
38+
parent_header = "{}"
39+
metadata = "{}"
40+
content = "{}"
41+
42+
ZMQ.send_multipart(requests_socket, [only(idents), "<IDS|MSG>", signature, header, parent_header, metadata, content])
43+
ZMQ.recv_multipart(requests_socket, String)
44+
45+
# Execute `42`
46+
idents = ["d2bd8e47-b2c9cd130d2967a19f52c1a3"]
47+
signature = "758c034ba5efb4fd7fd5a5600f913bc634739bf6a2c1e1d87e88b008706337bc"
48+
header = "{\"msg_id\": \"d2bd8e47-b2c9cd130d2967a19f52c1a3_3534705_1\", \"msg_type\": \"execute_request\", \"username\": \"james\", \"session\": \"d2bd8e47-b2c9cd130d2967a19f52c1a3\", \"date\": \"2025-02-20T22:29:49.835131Z\", \"version\": \"5.4\"}"
49+
parent_header = "{}"
50+
metadata = "{}"
51+
content = "{\"code\": \"42\", \"silent\": false, \"store_history\": true, \"user_expressions\": {}, \"allow_stdin\": true, \"stop_on_error\": true}"
52+
53+
ZMQ.send_multipart(requests_socket, [only(idents), "<IDS|MSG>", signature, header, parent_header, metadata, content])
54+
ZMQ.recv_multipart(requests_socket, String)
55+
56+
# Execute `error(42)`
57+
idents = ["d2bd8e47-b2c9cd130d2967a19f52c1a3"]
58+
signature = "953702763b65d9b0505f34ae0eb195574b9c2c65eebedbfa8476150133649801"
59+
header = "{\"msg_id\": \"d2bd8e47-b2c9cd130d2967a19f52c1a3_3534705_2\", \"msg_type\": \"execute_request\", \"username\": \"james\", \"session\": \"d2bd8e47-b2c9cd130d2967a19f52c1a3\", \"date\": \"2025-02-20T22:29:50.320836Z\", \"version\": \"5.4\"}"
60+
parent_header = "{}"
61+
metadata = "{}"
62+
content = "{\"code\": \"error(42)\", \"silent\": false, \"store_history\": true, \"user_expressions\": {}, \"allow_stdin\": true, \"stop_on_error\": true}"
63+
64+
ZMQ.send_multipart(requests_socket, [only(idents), "<IDS|MSG>", signature, header, parent_header, metadata, content])
65+
ZMQ.recv_multipart(requests_socket, String)
66+
67+
close(requests_socket)
68+
end
69+
end

test/Project.toml

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
1010
PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d"
1111
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
1212
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
13+
ZMQ = "c2297ded-f4af-51ae-bb23-16f91089e4e1"

test/kernel.jl

+2-30
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ using Test
1414
import Sockets
1515
import Sockets: listenany
1616

17+
import ZMQ
1718
import PythonCall
1819
import PythonCall: Py, pyimport, pyconvert, pytype, pystr
1920

@@ -31,35 +32,6 @@ import IJulia: Kernel
3132
import IJulia: ans, In, Out
3233

3334

34-
function getports(port_hint, n)
35-
ports = Int[]
36-
37-
for i in 1:n
38-
port, server = listenany(Sockets.localhost, port_hint)
39-
close(server)
40-
push!(ports, port)
41-
port_hint = port + 1
42-
end
43-
44-
return ports
45-
end
46-
47-
function create_profile(port_hint=8080)
48-
ports = getports(port_hint, 5)
49-
50-
Dict(
51-
"transport" => "tcp",
52-
"ip" => "127.0.0.1",
53-
"control_port" => ports[1],
54-
"shell_port" => ports[2],
55-
"stdin_port" => ports[3],
56-
"hb_port" => ports[4],
57-
"iopub_port" => ports[5],
58-
"signature_scheme" => "hmac-sha256",
59-
"key" => "a0436f6c-1916-498b-8eb9-e81ab9368e84"
60-
)
61-
end
62-
6335
function test_py_get!(get_func, result)
6436
try
6537
result[] = get_func(timeout=0)
@@ -160,7 +132,7 @@ function jupyter_client(f, profile)
160132
end
161133

162134
@testset "Kernel" begin
163-
profile = create_profile()
135+
profile = IJulia.create_profile(; key=IJulia._TEST_KEY)
164136
profile_kwargs = Dict([Symbol(key) => value for (key, value) in profile])
165137
profile_kwargs[:key] = pystr(profile_kwargs[:key]).encode()
166138

0 commit comments

Comments
 (0)