Skip to content

Commit ef9e320

Browse files
Replace Jupyter with lightweight interpreter system (#76)
* Replace Jupyter with lightweight interpreter system Jupyter's 7-second startup time was unacceptable for LLM use cases where users expect immediate code execution. The heavy dependency chain and complex kernel management created unnecessary overhead. This replaces the entire Jupyter infrastructure with a direct process pool architecture. Each language (Python, JavaScript, TypeScript) now runs in dedicated executor processes that communicate via JSON over stdin/stdout. The new system eliminates Jupyter startup entirely. Cold start times are now ~175ms for JavaScript, ~215ms for TypeScript, and ~1800ms for Python. Process pools pre-warm executors at container startup, reducing process acquisition to 2-6ms for immediate availability. The Python executor uses IPython for rich output capture, JavaScript uses Node.js VM for persistent context, and TypeScript uses esbuild for fast transpilation. Build-time compilation optimizes startup performance. All existing APIs remain unchanged - clients continue to work without modification while benefiting from the architectural improvements under the hood. * Fix linting and TypeScript issues - Use node: protocol for Node.js built-in imports - Replace string concatenation with template literals - Organize imports consistently - Remove unused private class members - Add missing output types to RichOutput interface - Fix react-markdown component types (remove non-existent inline property) - Add @types/react-katex for proper LaTeX component typing * Add backward compatibility exports for renamed error types Exports JupyterNotReadyError and isJupyterNotReadyError as deprecated aliases to maintain API compatibility while encouraging migration to InterpreterNotReadyError and isInterpreterNotReadyError. * Add Jupyter notebook integration guide for SDK users Shows how to extend sandbox containers with Jupyter server for users who need full notebook interface and traditional .ipynb file support alongside the built-in lightweight code interpreters. * Create changeset * Fix linting issues while preserving deprecation warnings Uses biome-ignore blocks to suppress organizeImports rule around deprecated exports, ensuring JSDoc deprecation warnings work properly in IDEs while maintaining code style compliance.
1 parent 9375c1e commit ef9e320

33 files changed

+9885
-5962
lines changed

.changeset/fresh-sheep-exist.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+
Replace Jupyter with lightweight interpreters for >90% faster cold starts for `.runCode` calls, while maintaining full code execution capabilities and rich output support.

docs/jupyter-notebooks.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Running Jupyter Notebooks in Sandboxes
2+
3+
The Sandbox SDK provides lightweight code interpreters by default for optimal performance. However, you can easily add full Jupyter notebook support to your custom containers for advanced use cases like interactive data analysis, research environments, or educational platforms.
4+
5+
## Overview
6+
7+
This guide shows how to extend your sandbox container with Jupyter server to enable:
8+
- Full Jupyter notebook interface at `http://your-preview-url:8888`
9+
- Interactive Python and JavaScript kernels
10+
- Rich visualizations and data analysis tools
11+
- Traditional notebook file (.ipynb) management
12+
13+
## Container Setup
14+
15+
Create a custom Dockerfile that extends the base sandbox image:
16+
17+
```dockerfile
18+
FROM docker.io/cloudflare/sandbox:latest
19+
20+
# Install Jupyter components
21+
RUN pip3 install --no-cache-dir \
22+
jupyter-server \
23+
jupyter-client \
24+
ipykernel \
25+
orjson \
26+
&& python3 -m ipykernel install --user --name python3
27+
28+
# Install scientific packages for data analysis
29+
RUN pip3 install --no-cache-dir \
30+
matplotlib \
31+
numpy \
32+
pandas \
33+
seaborn \
34+
plotly \
35+
scipy \
36+
scikit-learn
37+
38+
# Install JavaScript kernel (optional)
39+
RUN npm install -g ijavascript \
40+
&& ijsinstall --install=global
41+
42+
# Copy Jupyter configuration
43+
COPY jupyter_config.py /root/.jupyter/
44+
45+
# Expose Jupyter port
46+
EXPOSE 8888
47+
48+
# Start both sandbox service and Jupyter
49+
COPY start-jupyter.sh /
50+
RUN chmod +x /start-jupyter.sh
51+
CMD ["/start-jupyter.sh"]
52+
```
53+
54+
## Configuration Files
55+
56+
**jupyter_config.py** - Minimal Jupyter configuration:
57+
58+
```python
59+
"""Jupyter configuration for sandbox environment"""
60+
61+
c = get_config()
62+
63+
# Disable authentication (container handles security)
64+
c.ServerApp.token = ''
65+
c.ServerApp.password = ''
66+
c.ServerApp.allow_origin = '*'
67+
c.ServerApp.allow_remote_access = True
68+
c.ServerApp.disable_check_xsrf = True
69+
c.ServerApp.allow_root = True
70+
71+
# Network settings
72+
c.ServerApp.ip = '0.0.0.0'
73+
c.ServerApp.port = 8888
74+
c.ServerApp.open_browser = False
75+
76+
# Performance optimizations
77+
c.ServerApp.iopub_data_rate_limit = 1000000000
78+
c.Application.log_level = 'WARN'
79+
c.KernelManager.shutdown_wait_time = 1.0
80+
81+
# Disable terminals and unnecessary extensions
82+
c.ServerApp.terminals_enabled = False
83+
c.ServerApp.jpserver_extensions = {}
84+
```
85+
86+
**start-jupyter.sh** - Startup script:
87+
88+
```bash
89+
#!/bin/bash
90+
91+
# Start Jupyter in background
92+
jupyter server --config=/root/.jupyter/jupyter_config.py &
93+
94+
# Start the main sandbox service
95+
exec /container-server/startup.sh
96+
```
97+
98+
## Usage
99+
100+
Once deployed, access Jupyter through your sandbox:
101+
102+
```typescript
103+
import { getSandbox } from "@cloudflare/sandbox";
104+
105+
export default {
106+
async fetch(request, env) {
107+
const sandbox = getSandbox(env.Sandbox, "jupyter-env");
108+
109+
// Expose Jupyter port
110+
const preview = await sandbox.exposePort(8888, { name: "jupyter" });
111+
112+
return new Response(`Jupyter available at: ${preview.url}`);
113+
}
114+
};
115+
```
116+
117+
## Example: Setting Up Jupyter Environment
118+
119+
```typescript
120+
// Create sample data files for analysis
121+
await sandbox.writeFile("/workspace/sample_data.csv", `
122+
date,sales,marketing_spend
123+
2024-01-01,1200,450
124+
2024-01-02,980,520
125+
2024-01-03,1100,480
126+
2024-01-04,1350,600
127+
2024-01-05,1050,400
128+
`);
129+
130+
// Create a starter notebook
131+
await sandbox.writeFile("/workspace/analysis.ipynb", JSON.stringify({
132+
"cells": [
133+
{
134+
"cell_type": "code",
135+
"execution_count": null,
136+
"metadata": {},
137+
"outputs": [],
138+
"source": [
139+
"import pandas as pd\nimport matplotlib.pyplot as plt\n\n# Load the sample data\ndf = pd.read_csv('sample_data.csv')\nprint(\"Data loaded successfully!\")\ndf.head()"
140+
]
141+
}
142+
],
143+
"metadata": {
144+
"kernelspec": {
145+
"display_name": "Python 3",
146+
"language": "python",
147+
"name": "python3"
148+
}
149+
},
150+
"nbformat": 4,
151+
"nbformat_minor": 4
152+
}));
153+
154+
// Expose Jupyter interface
155+
const preview = await sandbox.exposePort(8888);
156+
console.log(`Jupyter notebook interface: ${preview.url}`);
157+
console.log(`Open analysis.ipynb to start analyzing the sample data`);
158+
```
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import React from "react";
2+
import { InlineMath, BlockMath } from "react-katex";
3+
import "katex/dist/katex.min.css";
4+
5+
interface LaTeXRendererProps {
6+
content: string;
7+
}
8+
9+
export function LaTeXRenderer({ content }: LaTeXRendererProps) {
10+
// Parse the entire content at once to handle multi-line LaTeX
11+
const parseContent = (): React.ReactNode[] => {
12+
const elements: React.ReactNode[] = [];
13+
14+
// Regular expressions for finding LaTeX delimiters
15+
const combinedRegex = /(\$\$[\s\S]*?\$\$|\$[^\$\n]+?\$)/g;
16+
17+
let lastIndex = 0;
18+
let match;
19+
20+
while ((match = combinedRegex.exec(content)) !== null) {
21+
// Add any text before the match
22+
if (match.index > lastIndex) {
23+
const textBefore = content.substring(lastIndex, match.index);
24+
if (textBefore) {
25+
// Split by newlines and add line breaks
26+
const lines = textBefore.split('\n');
27+
lines.forEach((line, idx) => {
28+
if (line) {
29+
elements.push(
30+
<span key={`text-${lastIndex}-${idx}`}>{line}</span>
31+
);
32+
}
33+
// Add line break except for last line
34+
if (idx < lines.length - 1) {
35+
elements.push(<br key={`br-${lastIndex}-${idx}`} />);
36+
}
37+
});
38+
}
39+
}
40+
41+
const matchedText = match[0];
42+
43+
// Check if it's display math ($$...$$) or inline math ($...$)
44+
if (matchedText.startsWith('$$') && matchedText.endsWith('$$')) {
45+
// Display math - extract content between $$
46+
const formula = matchedText.slice(2, -2).trim();
47+
elements.push(
48+
<div key={`block-${match.index}`} className="latex-display">
49+
<BlockMath
50+
math={formula}
51+
renderError={(error: Error) => (
52+
<div style={{ color: "red" }}>
53+
Error rendering LaTeX: {error.message}
54+
<pre>{formula}</pre>
55+
</div>
56+
)}
57+
/>
58+
</div>
59+
);
60+
} else if (matchedText.startsWith('$') && matchedText.endsWith('$')) {
61+
// Inline math - extract content between $
62+
const formula = matchedText.slice(1, -1).trim();
63+
elements.push(
64+
<InlineMath
65+
key={`inline-${match.index}`}
66+
math={formula}
67+
renderError={(error: Error) => (
68+
<span style={{ color: "red", fontSize: "0.9em" }}>
69+
[Error: {formula}]
70+
</span>
71+
)}
72+
/>
73+
);
74+
}
75+
76+
lastIndex = match.index + matchedText.length;
77+
78+
// Check if there's a newline immediately after this formula
79+
if (content[lastIndex] === '\n') {
80+
elements.push(<br key={`br-after-${match.index}`} />);
81+
lastIndex++; // Skip the newline
82+
}
83+
}
84+
85+
// Add any remaining text after the last match
86+
if (lastIndex < content.length) {
87+
const remainingText = content.substring(lastIndex);
88+
if (remainingText) {
89+
// Split by newlines and add line breaks
90+
const lines = remainingText.split('\n');
91+
lines.forEach((line, idx) => {
92+
if (line) {
93+
elements.push(
94+
<span key={`text-final-${lastIndex}-${idx}`}>{line}</span>
95+
);
96+
}
97+
// Add line break except for last line
98+
if (idx < lines.length - 1) {
99+
elements.push(<br key={`br-final-${lastIndex}-${idx}`} />);
100+
}
101+
});
102+
}
103+
}
104+
105+
// If no LaTeX was found, return the original content
106+
if (elements.length === 0) {
107+
elements.push(<span key="text-only">{content}</span>);
108+
}
109+
110+
return elements;
111+
};
112+
113+
return (
114+
<div className="latex-container">
115+
{parseContent()}
116+
</div>
117+
);
118+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import ReactMarkdown from "react-markdown";
2+
import remarkGfm from "remark-gfm";
3+
4+
interface MarkdownRendererProps {
5+
content: string;
6+
}
7+
8+
export function MarkdownRenderer({ content }: MarkdownRendererProps) {
9+
return (
10+
<div className="markdown-container">
11+
<ReactMarkdown
12+
remarkPlugins={[remarkGfm]}
13+
components={{
14+
// Style the components inline to match dark theme
15+
h1: ({children}) => <h1 style={{fontSize: "1.5em", marginTop: "1em", marginBottom: "0.5em"}}>{children}</h1>,
16+
h2: ({children}) => <h2 style={{fontSize: "1.3em", marginTop: "0.8em", marginBottom: "0.4em"}}>{children}</h2>,
17+
h3: ({children}) => <h3 style={{fontSize: "1.1em", marginTop: "0.6em", marginBottom: "0.3em"}}>{children}</h3>,
18+
19+
table: ({children}) => (
20+
<table style={{
21+
borderCollapse: "collapse",
22+
width: "100%",
23+
marginTop: "0.5em",
24+
marginBottom: "0.5em"
25+
}}>
26+
{children}
27+
</table>
28+
),
29+
30+
th: ({children}) => (
31+
<th style={{
32+
border: "1px solid #30363d",
33+
padding: "0.5em",
34+
backgroundColor: "#161b22",
35+
textAlign: "left"
36+
}}>
37+
{children}
38+
</th>
39+
),
40+
41+
td: ({children}) => (
42+
<td style={{
43+
border: "1px solid #30363d",
44+
padding: "0.5em"
45+
}}>
46+
{children}
47+
</td>
48+
),
49+
50+
code({className, children, ...props}) {
51+
// Detect inline vs block code by checking if there's a language class
52+
const isBlock = className && className.startsWith('language-');
53+
54+
if (!isBlock) {
55+
return (
56+
<code style={{
57+
background: "#2d333b",
58+
padding: "0.2em 0.4em",
59+
borderRadius: "3px",
60+
fontSize: "0.9em"
61+
}}>
62+
{children}
63+
</code>
64+
);
65+
}
66+
return (
67+
<pre style={{
68+
background: "#161b22",
69+
padding: "1em",
70+
borderRadius: "6px",
71+
border: "1px solid #30363d",
72+
overflowX: "auto",
73+
margin: "0.5em 0"
74+
}}>
75+
<code className={className} {...props}>
76+
{children}
77+
</code>
78+
</pre>
79+
);
80+
},
81+
82+
blockquote: ({children}) => (
83+
<blockquote style={{
84+
borderLeft: "3px solid #30363d",
85+
paddingLeft: "1em",
86+
marginLeft: "0",
87+
fontStyle: "italic",
88+
color: "#8b949e"
89+
}}>
90+
{children}
91+
</blockquote>
92+
),
93+
94+
ul: ({children}) => <ul style={{marginLeft: "1.5em", marginTop: "0.3em", marginBottom: "0.3em"}}>{children}</ul>,
95+
ol: ({children}) => <ol style={{marginLeft: "1.5em", marginTop: "0.3em", marginBottom: "0.3em"}}>{children}</ol>,
96+
li: ({children}) => <li style={{marginTop: "0.2em", marginBottom: "0.2em"}}>{children}</li>,
97+
hr: () => <hr style={{border: "0", borderTop: "1px solid #30363d", margin: "1em 0"}} />,
98+
p: ({children}) => <p style={{marginBottom: "0.5em", lineHeight: "1.6"}}>{children}</p>,
99+
strong: ({children}) => <strong style={{fontWeight: "600"}}>{children}</strong>,
100+
em: ({children}) => <em style={{fontStyle: "italic"}}>{children}</em>,
101+
}}
102+
>
103+
{content}
104+
</ReactMarkdown>
105+
</div>
106+
);
107+
}

0 commit comments

Comments
 (0)