Skip to content

Commit

Permalink
Merge pull request #773 from MetaCell/feature/netpyne-44
Browse files Browse the repository at this point in the history
Netpyne-44 Add mechanism to be able to react on kernel status change from the
  • Loading branch information
ddelpiano authored Jan 24, 2024
2 parents 63a2f71 + 3ef3185 commit 038eb64
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 21 deletions.
118 changes: 114 additions & 4 deletions webapp/components/NetPyNE.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import {
Dialog,
ConfirmationDialog,
LaunchDialog,
TutorialObserver
TutorialObserver,
} from 'netpyne/components';
import { loadModel } from '../redux/actions/general';
import { loadModel, openDialog } from '../redux/actions/general';
// import { execPythonMessage } from './general/GeppettoJupyterUtils';
import { replayAll } from './general/CommandRecorder';

const styles = ({ zIndex }) => ({
root: {
Expand Down Expand Up @@ -46,6 +48,11 @@ class NetPyNE extends React.Component {
super(props);
this.openPythonCallDialog = this.openPythonCallDialog.bind(this);
this.loaded = false;
this.kernelRestartState = {
state: "idle",
kernelID: undefined,
crashLoop: false
}
}

componentDidMount () {
Expand All @@ -66,7 +73,7 @@ class NetPyNE extends React.Component {
return;
}
this.loaded = true;
console.log('Netpyne is ready');
console.log('NetPyNE-UI component is ready');
if (window !== window.parent) {
window.parent.postMessage({
type: 'APP_READY',
Expand All @@ -84,7 +91,107 @@ class NetPyNE extends React.Component {
};
// A message from the parent frame can specify the file to load
window.addEventListener('message', loadFromEvent);
// window.load = loadFromEvent

// Logic for kernel reinit
const handleKernelRestart = ({ detail: { kernel, type } }) => {
switch (this.kernelRestartState.state) {
case "restarting":
if (type === "kernel_ready" || type === "kernel_autorestarting") {
console.log("Replaying all commands since the beginning of the session")
replayAll(this.kernelRestartState.kernelID)
this.kernelRestartState = {
...this.kernelRestartState,
state: "idle",
kernelID: undefined,
}
}
case "idle":
if (type === "kernel_connected") {
console.log("Kernel is connecting/starting, being init")
this.kernelRestartState = {
...this.kernelRestartState,
state: "init",
kernelID: kernel.id
}
}
else if (type === "kernel_autorestarting") {
console.log("Kernel restart event caught, trying to re-init the current model")
this.kernelRestartState = {
...this.kernelRestartState,
state: "restarting",
kernelID: kernel.id
}
if (!this.kernelRestartState.crashLoop) {
this.props.dispatchAction(openDialog({
title: "Kernel restart",
message: "An action occured that made the kernel restart. We are reloading your model and all the actions you applied on it."
}))
}
}
else if (type === "kernel_restarting") {
console.log("Kernel restart, perhaps it's a special restart?")
this.kernelRestartState = {
...this.kernelRestartState,
state: "special_restart",
kernelID: kernel.id
}
}
case "init":
if (type === "kernel_ready") {
console.log("Kernel properly initialized")
this.kernelRestartState = {
...this.kernelRestartState,
state: "idle",
kernelID: undefined,
}
}
case "special_restart":
if (type == "kernel_autorestarting") {
console.log("Kernel autorestart after a start, we might not have the ready event, we force it then")
replayAll(this.kernelRestartState.kernelID)
this.kernelRestartState = {
...this.kernelRestartState,
state: 'restarting',
kernelID: kernel.id,
}
} else {
console.log("Regular restart detected")
this.kernelRestartState = {
...this.kernelRestartState,
state: 'idle',
kernelID: undefined,
}
}
}
}
window.addEventListener('kernelstatus', handleKernelRestart)

// Dedicated code to handle crash loops
const kernelRestartLoopHandler = ( ) => {
if (!this.kernelRestartState.crashLoop) {
this.props.dispatchAction(openDialog({
title: "Kernel restart loop stabilization",
message: "One of your actions triggered a kernel restart loop. We are trying to identify the faulty command and to restore your model until this point. Close this window and wait for the kernel stabilization notification."
}))
} else {
clearTimeout(this.kernelRestartState.crashLoop)
}
const taskID = setTimeout((_this) => {
this.props.dispatchAction(openDialog({
title: "Kernel restart loop stabilized",
message: "The kernel is now stabilized."
}));
this.kernelRestartState = {
...this.kernelRestartState,
crashLoop: false
}
}, 8000, this)
this.kernelRestartState = {
...this.kernelRestartState,
crashLoop: taskID
}
}
window.addEventListener('kernelRestartLoop', kernelRestartLoopHandler)
}

componentWillUnmount () {
Expand Down Expand Up @@ -115,6 +222,9 @@ class NetPyNE extends React.Component {
<div className={classes.container}>
<div className={classes.topbar}>
<Topbar />
{/* <button onClick={() => {
execPythonMessage("utils.convertToJS(netpyne_geppetto.importCellTemplate(utils.convertToPython('{\"cellArgs\":{},\"fileName\":\"/home/vince/git-repository/metacell/NetPyNE-UI/workspace/cells/FScell.hoc\",\"cellName\":\"FScell\",\"label\":\"CellType1\",\"modFolder\":\"/home/vince/git-repository/metacell/NetPyNE-UI/workspace/mod\",\"importSynMechs\":false,\"compileMod\":false}')))")
}}>CRASH ME</button> */}
</div>
<Box p={1} flex={1} display="flex" alignItems="stretch">
<Grid container spacing={1} className={classes.content} alignItems="stretch">
Expand Down
103 changes: 103 additions & 0 deletions webapp/components/general/CommandRecorder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { KERNEL_HANDLING } from '../../constants';
import { store } from '../../redux/actiondomainStore'
import { recordCommand, dropLastCommand, dropFromIndex } from '../../redux/actions/actiondomain';
import { execPythonMessage, execPythonMessageWithoutRecording } from './GeppettoJupyterUtils';


const registerKernelListeners = () => {
try {
if(IPython.notebook.kernel == null) {
console.warn("Kernel not initialized. Waiting to register kernel event listeners");
setTimeout(registerKernelListeners, 500);
return;
}
} catch (error) {
console.warn("IPython not initialized. Waiting to register kernel event listeners");
setTimeout(registerKernelListeners, 500);
return
}

const notebook = IPython.notebook;
const handleKernelStatusChange = (event, data) => {
const kernelStatusEvent = new CustomEvent("kernelstatus", {
detail: {
"type": event.type,
...data
},
});
window.dispatchEvent(kernelStatusEvent);
};

const handleExecutionRequest = (event, data) => {
if (data.content.netpyne_ui_triggered) {
return
}
const { kernel, content } = data;
record(kernel.id, content.code);
}

// Kernel lifecycle requests
notebook.events.on('kernel_created.Kernel', handleKernelStatusChange);
notebook.events.on('kernel_reconnecting.Kernel', handleKernelStatusChange);
notebook.events.on('kernel_connected.Kernel', handleKernelStatusChange);
notebook.events.on('kernel_starting.Kernel', handleKernelStatusChange);
notebook.events.on('kernel_restarting.Kernel', handleKernelStatusChange);
notebook.events.on('kernel_autorestarting.Kernel', handleKernelStatusChange);
notebook.events.on('kernel_interrupting.Kernel', handleKernelStatusChange);
notebook.events.on('kernel_disconnected.Kernel', handleKernelStatusChange);
notebook.events.on('kernel_ready.Kernel', handleKernelStatusChange);
notebook.events.on('kernel_killed.Kernel', handleKernelStatusChange);
notebook.events.on('kernel_dead.Kernel', handleKernelStatusChange);

// Execution requests
notebook.events.on('execution_request.Kernel', handleExecutionRequest);
}
registerKernelListeners();


const record = (kernelID, command) => {
store.dispatch(recordCommand(kernelID, command))
}

const TIMEFRAME = 10 * 1000; // 10s
let lastReplayTime = 0

const getCommands = (kernelID) => {
return [
"from jupyter_geppetto import jupyter_geppetto",
"from jupyter_geppetto import utils",
"from netpyne_ui.netpyne_geppetto import netpyne_geppetto",
"netpyne_geppetto.deleteModel({})",
`netpyne_geppetto.loadFromIndexFile("${KERNEL_HANDLING.tmpModelPath}")`,
...store.getState()[kernelID]
]
}

const replayAll = (kernelID, fromRec = false) => {
const currentTimestamp = Date.now();
const commands = getCommands(kernelID);

if (!fromRec && currentTimestamp - lastReplayTime < TIMEFRAME) {
const restartLoop = new CustomEvent("kernelRestartLoop", {
detail: {
"kernel": kernelID,
"state": "looping"
}
});
window.dispatchEvent(restartLoop);
store.dispatch(dropLastCommand(kernelID))
replayAll(kernelID, true)
return
}

const lastCommand = commands.pop() // we drop the last command which is probably the faulty one
const script = commands.join('\n')
console.log("Playing", script)
console.log("Skipping last command", lastCommand)
lastReplayTime = currentTimestamp
execPythonMessageWithoutRecording(script).then(() => {
store.dispatch(dropLastCommand(kernelID))
})
}

export { record, replayAll }
26 changes: 16 additions & 10 deletions webapp/components/general/Dialog.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import React from 'react';
import Button from '@material-ui/core/Button';
import MuiDialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import Typography from '@material-ui/core/Typography';
import DialogTitle from '@material-ui/core/DialogTitle';
import Paper from '@material-ui/core/Paper';
import Box from '@material-ui/core/Box';
import Link from '@material-ui/core/Link';
import Icon from '@material-ui/core/Icon';
import { Button,DialogActions, DialogContent, DialogTitle, DialogContentText } from '@material-ui/core';
import { Typography, Paper, Box, Link, Icon } from '@material-ui/core';
import { withStyles } from '@material-ui/core/styles';
import { secondaryColor, bgLight } from '../../theme';
import logoNetpyne from '../../static/netpyne-logo_white.png';
Expand Down Expand Up @@ -135,18 +128,31 @@ const ContributeContent = withStyles(styles)(({ classes }) => (
</Paper>
));


const titleContentMapping = {
Contribute: <ContributeContent />,
About: <AboutContent />
}

export default function Dialog ({
open,
title,
message,
handleClose,
}) {
const selectMessageContent = () => {
if (title in titleContentMapping) {
return titleContentMapping[title]
}
return <DialogContentText>{message}</DialogContentText>
}
return (
<div>
<MuiDialog fullWidth maxWidth="sm" open={open} onClose={handleClose}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
{title === 'Contribute' ? <ContributeContent /> : <AboutContent />}
{/* {title === 'Contribute' ? <ContributeContent /> : <AboutContent />} */}
{selectMessageContent()}
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary" autoFocus>
Expand Down
31 changes: 26 additions & 5 deletions webapp/components/general/GeppettoJupyterUtils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { record as recordCommand } from './CommandRecorder';


const handle_output = function (data) {
// data is the object passed to the callback from the kernel execution
switch (data.msg_type) {
Expand Down Expand Up @@ -29,9 +32,23 @@ const handle_output = function (data) {
}
};

const execPythonMessage = function (command, callback = handle_output) {
const execPythonMessage = function (command, callback = handle_output, record = true) {
const { kernel } = IPython.notebook;
const messageID = kernel.execute(command, { iopub: { output: callback } }, { silent: false, stop_on_error: true, store_history: true });
if (record) {
recordCommand(kernel.id, command)
}
const messageID = kernel.execute(
command,
{
iopub: { output: callback }
},
{
silent: false,
stop_on_error: true,
store_history: true,
netpyne_ui_triggered: true
});


return new Promise((resolve, reject) => GEPPETTO.on(GEPPETTO.Events.Receive_Python_Message, (data) => {
if (data.data.id == messageID) {
Expand All @@ -40,11 +57,15 @@ const execPythonMessage = function (command, callback = handle_output) {
}));
};

const execPythonMessageWithoutRecording = function (command, callback = handle_output) {
return execPythonMessage(command, callback, false)
}

const addslashes = function (str) {
return (str + '').replace(/[\\"']/g, '\\$&').replace(/\u0000/g, '\\0');
}

const evalPythonMessage = function (command, parameters, parse = true) {
const evalPythonMessage = function (command, parameters, parse = true, record = true) {
let parametersString = '';
if (parameters) {
if (parameters.length > 0) {
Expand All @@ -58,7 +79,7 @@ const evalPythonMessage = function (command, parameters, parse = true) {
if (parse) {
finalCommand = `utils.convertToJS(${finalCommand})`;
}
return execPythonMessage(finalCommand, handle_output);
return execPythonMessage(finalCommand, handle_output, record);
};

export { execPythonMessage, evalPythonMessage };
export { execPythonMessage, evalPythonMessage, execPythonMessageWithoutRecording };
5 changes: 4 additions & 1 deletion webapp/components/topbar/dialogs/ActionDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,13 @@ class ActionDialog extends React.Component {
}
}
}
this.setState({ hide: true });
if (this.props.onAction) {
this.props.onAction();
}
this.setState({ hide: true });
if (this.props.onRequestClose) {
this.props.onRequestClose();
}
};

clearErrorDialogBox () {
Expand Down
3 changes: 3 additions & 0 deletions webapp/components/topbar/dialogs/ActionValidationDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ const ActionValidationDialog = (props) => {
if (props.onAction) {
props.onAction()
}
if (props.onRequestClose) {
props.onRequestClose()
}
}

const clearErrorDialogBox = () => {
Expand Down
Loading

0 comments on commit 038eb64

Please sign in to comment.