greed.js
Version:
Lightweight, private alternative to Colab. Run PyTorch & NumPy in browser with GPU acceleration (8.8x speedup). Fast, secure, runs locally.
654 lines (571 loc) • 16.7 kB
JavaScript
/**
* Pyodide Web Worker - Runs Python code in a separate thread
* Prevents UI blocking during long-running computations
*
* This worker:
* 1. Loads and initializes Pyodide
* 2. Executes Python code asynchronously
* 3. Communicates results back to main thread
* 4. Manages package loading and global state
* 5. Maintains execution context across runs
* 6. Performs automatic memory cleanup
*/
// Context Manager (inline to avoid import issues)
class ContextManager {
constructor() {
this.stats = {
executionCount: 0,
lastCleanup: Date.now(),
memoryPressure: 0,
avgExecutionTime: 0
};
this.config = {
cleanupInterval: 100,
memoryThreshold: 0.8,
maxIdleTime: 5 * 60 * 1000,
preserveGlobals: new Set([
'torch', 'nn', 'optim', 'numpy', 'np', 'sys', 'os', 'math',
'gc', 'builtins', '__builtins__', '__name__', '__doc__',
'__package__', '__loader__', '__spec__', '__annotations__',
'model', 'optimizer', 'loss_fn', 'dataset', 'dataloader',
'train', 'test', 'val', 'X', 'y', 'data',
'losses', 'accuracies', 'epochs', 'history', 'results'
])
};
this.userContext = new Map();
this.lastAccessTime = Date.now();
}
preserve(name) {
this.config.preserveGlobals.add(name);
}
shouldPreserve(name) {
if (this.config.preserveGlobals.has(name)) return true;
const lastAccess = this.userContext.get(name);
if (lastAccess && (Date.now() - lastAccess) < this.config.maxIdleTime) return true;
return false;
}
accessed(name) {
this.userContext.set(name, Date.now());
this.lastAccessTime = Date.now();
}
recordExecution(duration) {
this.stats.executionCount++;
this.stats.avgExecutionTime =
(this.stats.avgExecutionTime * (this.stats.executionCount - 1) + duration) /
this.stats.executionCount;
}
needsCleanup() {
if (this.stats.executionCount % this.config.cleanupInterval === 0) return true;
if (this.stats.memoryPressure > this.config.memoryThreshold) return true;
const idleTime = Date.now() - this.lastAccessTime;
if (idleTime > this.config.maxIdleTime) return true;
return false;
}
getCleanupCode() {
const preservedVars = Array.from(this.config.preserveGlobals).join('", "');
return `
import gc
import sys
preserved_globals = {"${preservedVars}"}
current_globals = list(globals().keys())
deleted_count = 0
for var_name in current_globals:
if var_name in preserved_globals:
continue
if var_name.startswith('__') and var_name.endswith('__'):
continue
if var_name in sys.modules:
continue
if var_name.startswith('_temp_') or var_name.startswith('_out_'):
try:
del globals()[var_name]
deleted_count += 1
except:
pass
gc.collect()
_cleanup_stats = {'deleted': deleted_count, 'memory_freed': True}
`;
}
getMemoryOptimizationCode() {
return `
import gc
import sys
def _cleanup_tensors():
try:
import torch
if hasattr(torch, 'cuda') and torch.cuda.is_available():
torch.cuda.empty_cache()
for obj in gc.get_objects():
if isinstance(obj, torch.Tensor) and obj.grad is not None:
if not obj.requires_grad:
obj.grad = None
except:
pass
_cleanup_tensors()
gc.collect(generation=2)
_memory_optimized = True
`;
}
getStats() {
return {
...this.stats,
idleTime: Date.now() - this.lastAccessTime,
preservedVariables: Array.from(this.config.preserveGlobals)
};
}
reset() {
this.stats = {
executionCount: 0,
lastCleanup: Date.now(),
memoryPressure: 0,
avgExecutionTime: 0
};
this.userContext.clear();
this.lastAccessTime = Date.now();
}
}
// Worker global state
let pyodide = null;
let isInitialized = false;
let installedPackages = new Set();
let initializationPromise = null;
let contextManager = null;
/**
* Initialize Pyodide in the worker context
*/
async function initializePyodide(config) {
if (initializationPromise) {
return initializationPromise;
}
initializationPromise = (async () => {
try {
// Import Pyodide from CDN
importScripts(config.pyodideURL || 'https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js');
// Send progress update
postMessage({
type: 'init:progress',
stage: 'loading',
message: 'Loading Pyodide...'
});
// Load Pyodide
pyodide = await loadPyodide({
indexURL: config.indexURL || 'https://cdn.jsdelivr.net/pyodide/v0.24.1/full/'
});
postMessage({
type: 'init:progress',
stage: 'loaded',
message: 'Pyodide loaded successfully'
});
// Load preload packages
if (config.preloadPackages && config.preloadPackages.length > 0) {
postMessage({
type: 'init:progress',
stage: 'packages',
message: `Loading packages: ${config.preloadPackages.join(', ')}`
});
await pyodide.loadPackage(config.preloadPackages);
config.preloadPackages.forEach(pkg => installedPackages.add(pkg));
postMessage({
type: 'init:progress',
stage: 'packages-loaded',
message: `Packages loaded: ${config.preloadPackages.join(', ')}`
});
}
// Initialize context manager
contextManager = new ContextManager();
isInitialized = true;
postMessage({
type: 'init:complete',
installedPackages: Array.from(installedPackages)
});
return true;
} catch (error) {
postMessage({
type: 'init:error',
error: {
message: error.message,
stack: error.stack
}
});
throw error;
}
})();
return initializationPromise;
}
/**
* Load Python packages
*/
async function loadPackages(packages) {
if (!isInitialized) {
throw new Error('Pyodide not initialized');
}
const packagesToLoad = packages.filter(pkg => !installedPackages.has(pkg));
if (packagesToLoad.length === 0) {
return Array.from(installedPackages);
}
try {
postMessage({
type: 'packages:loading',
packages: packagesToLoad
});
await pyodide.loadPackage(packagesToLoad);
packagesToLoad.forEach(pkg => installedPackages.add(pkg));
postMessage({
type: 'packages:loaded',
packages: packagesToLoad,
allPackages: Array.from(installedPackages)
});
return Array.from(installedPackages);
} catch (error) {
postMessage({
type: 'packages:error',
error: {
message: error.message,
packages: packagesToLoad
}
});
throw error;
}
}
/**
* Execute Python code with optional output capture
*/
async function executePython(taskId, code, options = {}) {
if (!isInitialized) {
throw new Error('Pyodide not initialized');
}
const {
captureOutput = false,
globals = {},
validateInput = true
} = options;
const startTime = performance.now();
try {
// Set globals if provided
for (const [key, value] of Object.entries(globals)) {
try {
pyodide.globals.set(key, value);
contextManager.accessed(key); // Track access
} catch (error) {
postMessage({
type: 'execution:warning',
taskId,
warning: `Failed to set global '${key}': ${error.message}`
});
}
}
let result;
if (captureOutput) {
// Extract streamOutput option (default to true)
const shouldStream = options.streamOutput !== false;
// Inject streaming callback into Python globals if streaming enabled
if (shouldStream && taskId) {
pyodide.globals.set('__greed_worker_emit_stdout__', (tid, output) => {
postMessage({
type: 'execution:stdout',
taskId: tid,
output: output,
timestamp: Date.now()
});
});
}
// Capture stdout with streaming support
const outputCode = `
import sys
from io import StringIO
class StreamingBuffer:
def __init__(self, task_id, emit_callback, should_stream):
self.buffer = StringIO()
self.task_id = task_id
self.emit_callback = emit_callback
self.should_stream = should_stream
def write(self, text):
self.buffer.write(text)
# Emit immediately for real-time streaming
if self.should_stream and self.emit_callback and text:
# Emit immediately - don't wait for time intervals
# This ensures output appears in real-time, even during sleep() calls
self.emit_callback(self.task_id, text)
def flush(self):
self.buffer.flush()
def getvalue(self):
return self.buffer.getvalue()
_temp_output_buffer = StreamingBuffer('${taskId || ''}', ${shouldStream ? '__greed_worker_emit_stdout__' : 'None'}, ${shouldStream ? 'True' : 'False'})
_temp_original_stdout = sys.stdout
sys.stdout = _temp_output_buffer
try:
${code.split('\n').map(line => ' ' + line).join('\n')}
finally:
sys.stdout.flush()
sys.stdout = _temp_original_stdout
_temp_captured_output = _temp_output_buffer.getvalue()
# Debug: also store in a backup variable
_temp_backup_output = _temp_captured_output
`;
await pyodide.runPythonAsync(outputCode);
let capturedOutput = '';
try {
// Try both variables
capturedOutput = pyodide.globals.get('_temp_captured_output') || '';
const backupOutput = pyodide.globals.get('_temp_backup_output') || '';
const bufferValue = pyodide.runPython('_temp_output_buffer.getvalue()');
// Send to main thread console via postMessage
postMessage({
type: 'execution:warning',
taskId,
warning: `[Worker] _temp_captured_output: "${capturedOutput}" (len: ${capturedOutput.length}), backup: "${backupOutput}" (len: ${backupOutput.length}), buffer: "${bufferValue}" (len: ${bufferValue.length})`
});
} catch (error) {
capturedOutput = 'Output capture failed';
postMessage({
type: 'execution:warning',
taskId,
warning: `[Worker] Capture failed: ${error.message}`
});
}
result = { output: capturedOutput };
postMessage({
type: 'execution:warning',
taskId,
warning: `[Worker] Result object: ${JSON.stringify(result)}`
});
// Clean up temporary globals (prefixed with _temp_)
try {
pyodide.globals.delete('_temp_captured_output');
pyodide.globals.delete('_temp_output_buffer');
pyodide.globals.delete('_temp_original_stdout');
} catch (error) {
// Ignore cleanup errors
}
} else {
// Direct execution
result = await pyodide.runPythonAsync(code);
}
// Record execution for stats
const duration = performance.now() - startTime;
contextManager.recordExecution(duration);
// Perform automatic cleanup if needed
if (contextManager.needsCleanup()) {
try {
await pyodide.runPythonAsync(contextManager.getCleanupCode());
// Serialize stats to ensure it's cloneable
const stats = contextManager.getStats();
postMessage({
type: 'execution:cleanup',
taskId,
stats: JSON.parse(JSON.stringify(stats))
});
} catch (cleanupError) {
// Non-fatal - just log
postMessage({
type: 'execution:warning',
taskId,
warning: `Cleanup warning: ${cleanupError.message}`
});
}
}
// Memory optimization every 50 executions
if (contextManager.stats.executionCount % 50 === 0) {
try {
await pyodide.runPythonAsync(contextManager.getMemoryOptimizationCode());
} catch (error) {
// Non-fatal
}
}
// Convert PyProxy objects to JavaScript before sending
let serializedResult = result;
if (result && typeof result === 'object' && result.toJs) {
// It's a PyProxy - convert to JS
try {
serializedResult = result.toJs({ dict_converter: Object.fromEntries });
} catch (error) {
// If conversion fails, try toString
serializedResult = result.toString();
}
}
postMessage({
type: 'execution:complete',
taskId,
result: serializedResult,
stats: {
duration,
executionCount: contextManager.stats.executionCount
}
});
return result;
} catch (error) {
postMessage({
type: 'execution:error',
taskId,
error: {
message: error.message,
stack: error.stack,
type: error.constructor.name
}
});
throw error;
}
}
/**
* Get a global variable from Python
*/
function getGlobal(name) {
if (!isInitialized) {
throw new Error('Pyodide not initialized');
}
try {
return pyodide.globals.get(name);
} catch (error) {
return undefined;
}
}
/**
* Set a global variable in Python
*/
function setGlobal(name, value) {
if (!isInitialized) {
throw new Error('Pyodide not initialized');
}
try {
pyodide.globals.set(name, value);
return true;
} catch (error) {
return false;
}
}
/**
* Delete a global variable from Python
*/
function deleteGlobal(name) {
if (!isInitialized) {
throw new Error('Pyodide not initialized');
}
try {
pyodide.globals.delete(name);
return true;
} catch (error) {
return false;
}
}
/**
* Interrupt current execution
*/
function interrupt() {
if (!isInitialized || !pyodide) {
return;
}
try {
pyodide.interruptBuffer[0] = 2;
postMessage({
type: 'execution:interrupted',
message: 'Execution interrupted by user'
});
} catch (error) {
postMessage({
type: 'interrupt:error',
error: {
message: error.message
}
});
}
}
/**
* Reset the Python environment
*/
async function reset() {
if (!isInitialized) {
return;
}
try {
// Clear all user-defined globals
await pyodide.runPythonAsync(`
import sys
# Get all user-defined names
user_names = [name for name in dir() if not name.startswith('_')]
# Delete them
for name in user_names:
try:
del globals()[name]
except:
pass
`);
postMessage({
type: 'reset:complete',
message: 'Python environment reset'
});
} catch (error) {
postMessage({
type: 'reset:error',
error: {
message: error.message
}
});
}
}
/**
* Message handler - receives commands from main thread
*/
self.onmessage = async function(event) {
const { type, id, ...data } = event.data;
try {
switch (type) {
case 'init':
await initializePyodide(data.config);
postMessage({ type: 'init:ack', id });
break;
case 'loadPackages':
await loadPackages(data.packages);
postMessage({ type: 'loadPackages:ack', id, packages: Array.from(installedPackages) });
break;
case 'execute':
await executePython(data.taskId, data.code, data.options);
postMessage({ type: 'execute:ack', id, taskId: data.taskId });
break;
case 'getGlobal':
const value = getGlobal(data.name);
postMessage({ type: 'getGlobal:result', id, name: data.name, value });
break;
case 'setGlobal':
const setResult = setGlobal(data.name, data.value);
postMessage({ type: 'setGlobal:result', id, name: data.name, success: setResult });
break;
case 'deleteGlobal':
const deleteResult = deleteGlobal(data.name);
postMessage({ type: 'deleteGlobal:result', id, name: data.name, success: deleteResult });
break;
case 'interrupt':
interrupt();
postMessage({ type: 'interrupt:ack', id });
break;
case 'reset':
await reset();
postMessage({ type: 'reset:ack', id });
break;
case 'ping':
postMessage({ type: 'pong', id });
break;
default:
postMessage({
type: 'error',
id,
error: { message: `Unknown message type: ${type}` }
});
}
} catch (error) {
postMessage({
type: 'error',
id,
error: {
message: error.message,
stack: error.stack
}
});
}
};
// Signal that worker is ready
try {
postMessage({ type: 'worker:ready' });
} catch (error) {
// If we can't even send the ready message, something is very wrong
console.error('Worker failed to send ready message:', error);
}