Skip to content

Commit c87db11

Browse files
Improve jupyter startup time (#53)
* Remove jupyterlab to improve startup time * Disable unused jupyter extensions * Increase sandbox timeout to 20 minutes * Add changeset * Use tighter exponential backoff * Delay pre-warming requests
1 parent 68d9bc5 commit c87db11

File tree

6 files changed

+108
-40
lines changed

6 files changed

+108
-40
lines changed

.changeset/thirty-months-report.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudflare/sandbox": patch
3+
---
4+
5+
Improve jupyterlab config to speed up startup

packages/sandbox/Dockerfile

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ RUN apt-get update && apt-get install -y \
3737
ca-certificates \
3838
gnupg \
3939
lsb-release \
40+
strace \
4041
&& rm -rf /var/lib/apt/lists/*
4142

4243
# Set Python 3.11 as default python3
@@ -56,17 +57,20 @@ RUN apt-get update && apt-get install -y ca-certificates curl gnupg \
5657
COPY --from=bun-source /usr/local/bin/bun /usr/local/bin/bun
5758
COPY --from=bun-source /usr/local/bin/bunx /usr/local/bin/bunx
5859

59-
# Install Jupyter and kernels
60+
# Install minimal Jupyter components
6061
RUN pip3 install --no-cache-dir \
61-
jupyter \
62-
jupyterlab \
62+
jupyter-server \
63+
jupyter-client \
6364
ipykernel \
64-
notebook \
65+
orjson \
66+
&& python3 -m ipykernel install --user --name python3
67+
68+
# Install scientific packages
69+
RUN pip3 install --no-cache-dir \
6570
matplotlib \
6671
numpy \
6772
pandas \
68-
seaborn \
69-
&& python3 -m ipykernel install --user --name python3
73+
seaborn
7074

7175
# Install JavaScript kernel (ijavascript) - using E2B's fork
7276
RUN npm install -g --unsafe-perm git+https://github.com/e2b-dev/ijavascript.git \

packages/sandbox/container_src/jupyter-service.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,16 +133,26 @@ export class JupyterService {
133133
* Pre-warm context pools for better performance
134134
*/
135135
private async warmContextPools() {
136+
// Delay pre-warming to avoid startup rush that triggers Bun WebSocket bug
137+
await new Promise((resolve) => setTimeout(resolve, 2000));
138+
136139
try {
137140
console.log(
138141
"[JupyterService] Pre-warming context pools for better performance"
139142
);
140143

141-
// Enable pool warming with min sizes
144+
// Warm one at a time with delay between them
142145
await this.jupyterServer.enablePoolWarming("python", 1);
146+
147+
// Small delay between different language kernels
148+
await new Promise((resolve) => setTimeout(resolve, 1000));
149+
143150
await this.jupyterServer.enablePoolWarming("javascript", 1);
144151
} catch (error) {
145152
console.error("[JupyterService] Error pre-warming context pools:", error);
153+
console.error(
154+
"[JupyterService] Pre-warming failed but service continues"
155+
);
146156
}
147157
}
148158

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""
2+
Minimal Jupyter configuration focused on kernel-only usage
3+
"""
4+
5+
c = get_config() # noqa
6+
7+
# Disable all authentication - we handle security at container level
8+
c.ServerApp.token = ''
9+
c.ServerApp.password = ''
10+
c.IdentityProvider.token = ''
11+
c.ServerApp.allow_origin = '*'
12+
c.ServerApp.allow_remote_access = True
13+
c.ServerApp.disable_check_xsrf = True
14+
c.ServerApp.allow_root = True
15+
c.ServerApp.allow_credentials = True
16+
17+
# Also set NotebookApp settings for compatibility
18+
c.NotebookApp.token = ''
19+
c.NotebookApp.password = ''
20+
c.NotebookApp.allow_origin = '*'
21+
c.NotebookApp.allow_remote_access = True
22+
c.NotebookApp.disable_check_xsrf = True
23+
c.NotebookApp.allow_credentials = True
24+
25+
# Performance settings
26+
c.ServerApp.iopub_data_rate_limit = 1000000000 # E2B uses 1GB/s
27+
28+
# Minimal logging
29+
c.Application.log_level = 'ERROR'
30+
31+
# Disable browser
32+
c.ServerApp.open_browser = False
33+
34+
# Optimize for container environment
35+
c.ServerApp.ip = '0.0.0.0'
36+
c.ServerApp.port = 8888
37+
38+
# Kernel optimizations
39+
c.KernelManager.shutdown_wait_time = 0.0
40+
c.MappingKernelManager.cull_idle_timeout = 0
41+
c.MappingKernelManager.cull_interval = 0
42+
43+
# Disable terminals
44+
c.ServerApp.terminals_enabled = False
45+
46+
# Disable all extensions to speed up startup
47+
c.ServerApp.jpserver_extensions = {}
48+
c.ServerApp.nbserver_extensions = {}

packages/sandbox/container_src/startup.sh

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
# Function to check if Jupyter is ready
44
check_jupyter_ready() {
5-
curl -s http://localhost:8888/api > /dev/null 2>&1
5+
# Check if API is responsive and kernelspecs are available
6+
curl -s http://localhost:8888/api/kernelspecs > /dev/null 2>&1
67
}
78

89
# Function to notify Bun server that Jupyter is ready
@@ -12,19 +13,10 @@ notify_jupyter_ready() {
1213
echo "[Startup] Jupyter is ready, notified Bun server"
1314
}
1415

15-
# Start Jupyter notebook server in background
16+
# Start Jupyter server in background
1617
echo "[Startup] Starting Jupyter server..."
17-
jupyter notebook \
18-
--ip=0.0.0.0 \
19-
--port=8888 \
20-
--no-browser \
21-
--allow-root \
22-
--NotebookApp.token='' \
23-
--NotebookApp.password='' \
24-
--NotebookApp.allow_origin='*' \
25-
--NotebookApp.disable_check_xsrf=True \
26-
--NotebookApp.allow_remote_access=True \
27-
--NotebookApp.allow_credentials=True \
18+
jupyter server \
19+
--config=/container-server/jupyter_config.py \
2820
> /tmp/jupyter.log 2>&1 &
2921

3022
JUPYTER_PID=$!
@@ -37,40 +29,49 @@ BUN_PID=$!
3729
# Monitor Jupyter readiness in background
3830
(
3931
echo "[Startup] Monitoring Jupyter readiness in background..."
40-
MAX_ATTEMPTS=30
32+
MAX_ATTEMPTS=60
4133
ATTEMPT=0
42-
DELAY=0.5
43-
MAX_DELAY=5
44-
34+
35+
# Track start time for reporting
36+
START_TIME=$(date +%s.%N)
37+
4538
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
4639
if check_jupyter_ready; then
4740
notify_jupyter_ready
48-
echo "[Startup] Jupyter server is ready after $ATTEMPT attempts"
41+
END_TIME=$(date +%s.%N)
42+
ELAPSED=$(awk "BEGIN {printf \"%.2f\", $END_TIME - $START_TIME}")
43+
echo "[Startup] Jupyter server is ready after $ELAPSED seconds ($ATTEMPT attempts)"
4944
break
5045
fi
51-
46+
5247
# Check if Jupyter process is still running
5348
if ! kill -0 $JUPYTER_PID 2>/dev/null; then
5449
echo "[Startup] WARNING: Jupyter process died. Check /tmp/jupyter.log for details"
5550
cat /tmp/jupyter.log
5651
# Don't exit - let Bun server continue running in degraded mode
5752
break
5853
fi
59-
54+
6055
ATTEMPT=$((ATTEMPT + 1))
61-
echo "[Startup] Jupyter not ready yet (attempt $ATTEMPT/$MAX_ATTEMPTS, delay ${DELAY}s)"
62-
63-
# Sleep with exponential backoff
64-
sleep $DELAY
65-
66-
# Increase delay exponentially with jitter, cap at MAX_DELAY
67-
DELAY=$(awk "BEGIN {printf \"%.2f\", $DELAY * 1.5 + (rand() * 0.5)}")
68-
# Use awk for comparison since bc might not be available
69-
if [ $(awk "BEGIN {print ($DELAY > $MAX_DELAY)}") -eq 1 ]; then
70-
DELAY=$MAX_DELAY
56+
57+
# Start with faster checks
58+
if [ $ATTEMPT -eq 1 ]; then
59+
DELAY=0.5 # Start at 0.5s
60+
else
61+
# Exponential backoff with 1.3x multiplier (less aggressive than 1.5x)
62+
DELAY=$(awk "BEGIN {printf \"%.2f\", $DELAY * 1.3}")
63+
# Cap at 2s max (instead of 5s)
64+
if [ $(awk "BEGIN {print ($DELAY > 2)}") -eq 1 ]; then
65+
DELAY=2
66+
fi
7167
fi
68+
69+
# Log with current delay for transparency
70+
echo "[Startup] Jupyter not ready yet (attempt $ATTEMPT/$MAX_ATTEMPTS, next check in ${DELAY}s)"
71+
72+
sleep $DELAY
7273
done
73-
74+
7475
if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
7576
echo "[Startup] WARNING: Jupyter failed to become ready within attempts"
7677
echo "[Startup] Jupyter logs:"
@@ -80,4 +81,4 @@ BUN_PID=$!
8081
) &
8182

8283
# Wait for Bun server (main process)
83-
wait $BUN_PID
84+
wait $BUN_PID

packages/sandbox/src/sandbox.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {
3737

3838
export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
3939
defaultPort = 3000; // Default port for the container's Bun server
40-
sleepAfter = "3m"; // Sleep the sandbox if no requests are made in this timeframe
40+
sleepAfter = "20m"; // Keep container warm for 20 minutes to avoid cold starts
4141
client: JupyterClient;
4242
private sandboxName: string | null = null;
4343
private codeInterpreter: CodeInterpreter;

0 commit comments

Comments
 (0)