Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Netpyne-44 Add mechanism to be able to react on kernel status change from the #773

Merged
merged 16 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading