Răsfoiți Sursa

Show warning for large graphs

Lutz Roeder 7 ani în urmă
părinte
comite
1f228a15ee
11 a modificat fișierele cu 513 adăugiri și 462 ștergeri
  1. 1 1
      src/caffe-model.js
  2. 1 1
      src/caffe2-model.js
  3. 1 1
      src/coreml-model.js
  4. 1 1
      src/keras-model.js
  5. 1 1
      src/mxnet-model.js
  6. 2 2
      src/onnx-model.js
  7. 1 1
      src/tf-model.js
  8. 1 1
      src/tflite-model.js
  9. 59 52
      src/view-browser.js
  10. 75 55
      src/view-electron.js
  11. 370 346
      src/view.js

+ 1 - 1
src/caffe-model.js

@@ -656,6 +656,6 @@ class CaffeOperatorMetadata
 class CaffeError extends Error {
     constructor(message) {
         super(message);
-        this.name = 'Caffe Error';
+        this.name = 'Error loading Caffe model.';
     }
 }

+ 1 - 1
src/caffe2-model.js

@@ -489,6 +489,6 @@ class Caffe2OperatorMetadata
 class Caffe2Error extends Error {
     constructor(message) {
         super(message);
-        this.name = 'Caffe2 Error';
+        this.name = 'Error loading Caffe2 model.';
     }
 }

+ 1 - 1
src/coreml-model.js

@@ -862,6 +862,6 @@ class CoreMLOperatorMetadata
 class CoreMLError extends Error {
     constructor(message) {
         super(message);
-        this.name = 'CoreML Error';
+        this.name = 'Error loading CoreML model.';
     }
 }

+ 1 - 1
src/keras-model.js

@@ -895,6 +895,6 @@ class KerasOperatorMetadata {
 class KerasError extends Error {
     constructor(message) {
         super(message);
-        this.name = 'Keras Error';
+        this.name = 'Error loading Keras model.';
     }
 }

+ 1 - 1
src/mxnet-model.js

@@ -538,6 +538,6 @@ class MXNetOperatorMetadata {
 class MXNetError extends Error {
     constructor(message) {
         super(message);
-        this.name = 'MXNet Error';
+        this.name = 'Error loading MXNet model.';
     }
 }

+ 2 - 2
src/onnx-model.js

@@ -21,7 +21,7 @@ class OnnxModelFactory {
                 model = onnx.ModelProto.decode(buffer);
             }
             catch (error) {
-                callback(new OnnxError('Protocol Buffer loader failed to decode onnx.ModelProto input stream (' + error.message + ').'), null);
+                callback(new OnnxError('Protocol Buffer loader failed to decode ModelProto input stream (' + error.message + ').'), null);
                 return;
             }
             var result = null;
@@ -944,6 +944,6 @@ class OnnxOperatorMetadata {
 class OnnxError extends Error {
     constructor(message) {
         super(message);
-        this.name = 'ONNX Error';
+        this.name = 'Error loading ONNX model.';
     }
 }

+ 1 - 1
src/tf-model.js

@@ -1099,6 +1099,6 @@ class TensorFlowOperatorMetadata {
 class TensorFlowError extends Error {
     constructor(message) {
         super(message);
-        this.name = 'TensorFlow Error';
+        this.name = 'Error loading TensorFlow model.';
     }
 }

+ 1 - 1
src/tflite-model.js

@@ -706,6 +706,6 @@ class TensorFlowLiteOperatorMetadata {
 class TensorFlowLiteError extends Error {
     constructor(message) {
         super(message);
-        this.name = 'TensorFlow Lite Error';
+        this.name = 'Error loading TensorFlow Lite model.';
     }
 }

+ 59 - 52
src/view-browser.js

@@ -13,40 +13,7 @@ class BrowserHost {
         this._view = view;
 
         window.addEventListener('keydown', (e) => {
-            if (this._view) {
-                if (!e.altKey && !e.shiftKey && (e.ctrlKey || e.metaKey)) {
-                    switch (e.keyCode) {
-                        case 70: // F
-                            this._view.find();
-                            e.preventDefault();
-                            break;
-                        case 68: // D
-                            this._view.toggleDetails();
-                            e.preventDefault();
-                            break;
-                        case 85: // U
-                            this._view.toggleNames();
-                            e.preventDefault();
-                            break;
-                        case 13: // Return
-                            document.getElementById('model-properties-button').click();
-                            e.preventDefault();
-                            break;
-                        case 8: // Backspace
-                            this._view.resetZoom();
-                            e.preventDefault();
-                            break;
-                        case 38: // Up
-                            document.getElementById('zoom-in-button').click();
-                            e.preventDefault();
-                            break;
-                        case 40: // Down
-                            document.getElementById('zoom-out-button').click();
-                            e.preventDefault();
-                            break;
-                    }
-                }
-            }
+            this.keyHandler(e);
         });
 
         var fileElement = Array.from(document.getElementsByTagName('meta')).filter(e => e.name == 'file').shift();
@@ -96,10 +63,14 @@ class BrowserHost {
         });
     }
     
-    showError(message) {
-        alert(message);
+    error(message, detail) {
+        alert(message + ' ' + detail);
     }
     
+    confirm(message, detail) {
+        return confirm(message + ' ' + detail);
+    }
+
     import(file, callback) {
         var url = this.url(file);
         for (var i = 0; i < document.scripts.length; i++) {
@@ -182,22 +153,22 @@ class BrowserHost {
         request.onload = () => {
             if (request.status == 200) {
                 var buffer = new Uint8Array(request.response);
-                this._view.openBuffer(buffer, file, (err) => {
+                this._view.openBuffer(buffer, file, (err, model) => {
+                    this._view.show(null);
                     if (err) {
-                        this.showError(err.toString());
-                        this._view.show(null);
+                        this.error(err.name, err.message);
                     }
-                    else {
+                    if (model) {
                         document.title = file;
                     }
                 });
             }
             else {
-                this._view.showError(request.status);
+                this.error('Model load request failed.', request.status);
             }
         };
         request.onerror = () => {
-            this._view.showError(request.status);
+            this.error('Error while requesting model.', request.status);
         };
         request.open('GET', url, true);
         request.send();
@@ -209,13 +180,14 @@ class BrowserHost {
 
     openFile(file) {
         this._view.show('spinner');
-        this.openBuffer(file, (err) => {
+        this.openBuffer(file, (err, model) => {
+            this._view.show(null);
             if (err) {
-                this.showError(err.toString());
-                this._view.show(null);
-                return;
+                this.error(err.name, err.message);
+            }
+            if (model) {
+                document.title = file.name;
             }
-            document.title = file.name;
         });
     }
 
@@ -224,16 +196,51 @@ class BrowserHost {
         var reader = new FileReader();
         reader.onloadend = () => {
             if (reader.error) {
-                callback(reader.error);
+                callback(reader.error, null);
                 return;
             }
             var buffer = new Uint8Array(reader.result);
-            this._view.openBuffer(buffer, file.name, (err) => {
-                callback(err);
+            this._view.openBuffer(buffer, file.name, (err, model) => {
+                callback(err, model);
             });
         };
         reader.readAsArrayBuffer(file);
     }
+
+    keyHandler(e) {
+        if (!e.altKey && !e.shiftKey && (e.ctrlKey || e.metaKey)) {
+            switch (e.keyCode) {
+                case 70: // F
+                    this._view.find();
+                    e.preventDefault();
+                    break;
+                case 68: // D
+                    this._view.toggleDetails();
+                    e.preventDefault();
+                    break;
+                case 85: // U
+                    this._view.toggleNames();
+                    e.preventDefault();
+                    break;
+                case 13: // Return
+                    document.getElementById('model-properties-button').click();
+                    e.preventDefault();
+                    break;
+                case 8: // Backspace
+                    this._view.resetZoom();
+                    e.preventDefault();
+                    break;
+                case 38: // Up
+                    document.getElementById('zoom-in-button').click();
+                    e.preventDefault();
+                    break;
+                case 40: // Down
+                    document.getElementById('zoom-out-button').click();
+                    e.preventDefault();
+                    break;
+            }
+        }
+    }
 }
 
 if (!window.TextDecoder) {
@@ -244,7 +251,7 @@ if (!window.TextDecoder) {
         decode(buffer) {
             var result = '';
             var length = buffer.length;
-            var i = 0
+            var i = 0;
             switch (this._encoding) {
                 case 'utf-8':
                     while (i < length) {
@@ -274,7 +281,7 @@ if (!window.TextDecoder) {
             }
             return result;
         }
-    }
+    };
 }
 
 window.host = new BrowserHost();

+ 75 - 55
src/view-electron.js

@@ -18,25 +18,8 @@ class ElectronHost {
         this._view.show('welcome');
 
         electron.ipcRenderer.on('open', (event, data) => {
-            var file = data.file;
-            if (file) {
-                this._view.show('spinner');
-                this.openFile(file, (err) => {
-                    if (err) {
-                        this.showError(err.toString());
-                        this._view.show(null);
-                        this.update('path', null);
-                        this.update('show-details', this._view.showDetails);
-                        this.update('show-names', this._view.showNames);
-                        return;
-                    }
-                    this.update('path', file);
-                    this.update('show-details', this._view.showDetails);
-                    this.update('show-names', this._view.showNames);
-                });
-            }
+            this.openFile(data.file);
         });
-
         electron.ipcRenderer.on('export', (event, data) => {
             this._view.export(data.file);
         });
@@ -109,10 +92,28 @@ class ElectronHost {
         electron.ipcRenderer.send('drop-files', { files: files });
     }
 
-    showError(message) {
-        if (message) {
-            electron.remote.dialog.showErrorBox(electron.remote.app.getName(), message);        
-        }
+    error(message, detail) {
+        var owner = electron.remote.BrowserWindow.getFocusedWindow();
+        var options = {
+            type: 'error',
+            message: message,
+            detail: detail,
+        };
+        electron.remote.dialog.showMessageBox(owner, options);
+    }
+
+    confirm(message, detail) {
+        var owner = electron.remote.BrowserWindow.getFocusedWindow();
+        var options = {
+            type: 'question',
+            message: message,
+            detail: detail,
+            buttons: ['Yes', 'No'],
+            defaultId: 0,
+            cancelId: 1
+        };
+        var result = electron.remote.dialog.showMessageBox(owner, options);
+        return result == 0;
     }
 
     import(file, callback) {
@@ -146,13 +147,13 @@ class ElectronHost {
             }
             catch (e)
             {
-                this.showError(e);
+                this.error('Export failure.', e);
                 return;
             }    
         }
         fs.writeFile(file, data, encoding, (err) => {
             if (err) {
-                this.showError(err);
+                this.error('Export write failure.', err);
             }
         });
     }
@@ -180,46 +181,65 @@ class ElectronHost {
         electron.shell.openExternal(url);
     }
 
-    openFile(file, callback) {
+    openFile(file) {
+        if (file) {
+            this._view.show('spinner');
+            this.readFile(file, (err, buffer) => {
+                if (err) {
+                    this._view.show(null);
+                    this.error('Error while reading file.', err.message);
+                    this.update('path', null);
+                    return;
+                }
+                this._view.openBuffer(buffer, path.basename(file), (err, model) => {
+                    this._view.show(null);
+                    if (err) {
+                        this.error(err.name, err.message);
+                        this.update('path', null);
+                    }
+                    if (model) {
+                        this.update('path', file);
+                    }
+                    this.update('show-details', this._view.showDetails);
+                    this.update('show-names', this._view.showNames);
+                });
+            });
+        }
+    }
+
+    readFile(file, callback) {
         fs.exists(file, (exists) => {
             if (!exists) {
-                this._view.showError('File not found.');
+                callback(new Error('The file \'' + file + '\' does not exist.'), null);
+                return;
             }
-            else {
-                fs.stat(file, (err, stats) => {
+            fs.stat(file, (err, stats) => {
+                if (err) {
+                    callback(err, null);
+                    return;
+                }
+                fs.open(file, 'r', (err, fd) => {
                     if (err) {
-                        this._view.showError(err);
+                        callback(err, null);
+                        return;
                     }
-                    else {
-                        var size = stats.size;
-                        var buffer = new Uint8Array(size);
-                        fs.open(file, 'r', (err, fd) => {
+                    var size = stats.size;
+                    var buffer = new Uint8Array(size);
+                    fs.read(fd, buffer, 0, size, 0, (err, bytesRead, buffer) => {
+                        if (err) {
+                            callback(err, null);
+                            return;
+                        }
+                        fs.close(fd, (err) => {
                             if (err) {
-                                this._view.showError(err);
-                            }
-                            else {
-                                fs.read(fd, buffer, 0, size, 0, (err, bytesRead, buffer) => {
-                                    if (err) {
-                                        this._view.showError(err);
-                                    }
-                                    else {
-                                        fs.close(fd, (err) => {
-                                            if (err) {
-                                                this._view.showError(err);
-                                            }
-                                            else {
-                                                this._view.openBuffer(buffer, path.basename(file), (err) => {
-                                                    callback(err);
-                                                });
-                                            }
-                                        });
-                                    }
-                                });
+                                callback(err, null);
+                                return;
                             }
+                            callback(null, buffer);
                         });
-                    }
+                    });
                 });
-            }
+            });
         });
     }
 }

+ 370 - 346
src/view.js

@@ -36,7 +36,7 @@ class View {
     show(page) {
 
         if (!page) {
-            page = (!this._model && !this._graph) ? 'welcome' : 'graph';
+            page = (!this._model && !this._activeGraph) ? 'welcome' : 'graph';
         }
 
         this._sidebar.close();
@@ -56,10 +56,8 @@ class View {
             graphElement.style.display = 'none';
             graphElement.style.opacity = 0;
             toolbarElement.style.display = 'none';
-            this._model = null;
-            this._graph = false;
         }
-    
+
         if (page == 'spinner') {
             document.body.style.cursor = 'wait';
             welcomeElement.style.display = 'block';
@@ -117,7 +115,11 @@ class View {
     toggleDetails() {
         this._showDetails = !this._showDetails;
         this.show('spinner');
-        this.updateGraph(this._model, this._activeGraph);
+        this.updateGraph(this._model, this._activeGraph, (err) => {
+            if (err) {
+                this.error('Graph update failed.', err);
+            }
+        });
     }
 
     get showDetails() {
@@ -127,7 +129,11 @@ class View {
     toggleNames() {
         this._showNames = !this._showNames;
         this.show('spinner');
-        this.updateGraph(this._model, this._activeGraph);
+        this.updateGraph(this._model, this._activeGraph, (err) => {
+            if (err) {
+                this.error('Graph update failed.', err);
+            }
+        });
     }
 
     get showNames() {
@@ -231,6 +237,12 @@ class View {
         next();
     }
 
+    error(message, err) {
+        this._sidebar.close();
+        this._host.error(message, err.toString());
+        this.show('welcome');
+    }
+
     openBuffer(buffer, identifier, callback) {
         this._sidebar.close();
         setTimeout(() => {
@@ -240,410 +252,422 @@ class View {
                 }
                 else {
                     setTimeout(() => {
-                        this._graph = false;
-                        try {
-                            var graph = model.graphs.length > 0 ? model.graphs[0] : null;
-                            this.updateGraph(model, graph);
-                            this._model = model;
-                            this._activeGraph = graph;
-                            callback(null);
-                        }
-                        catch (err) {
-                            try {
-                                this.updateGraph(this._model, this._activeGraph);
-                            }
-                            catch (obj) {
-                                this._model = null;
-                                this._activeGraph = null;
-                            }
-                            callback(err);
-                        }
-                    }, 2);   
+                        var graph = model.graphs.length > 0 ? model.graphs[0] : null;
+                        this.updateGraph(model, graph, (err, model) => {
+                            callback(err, model);
+                        });
+                    }, 20);   
                 }
             });    
         }, 2);
     }
 
-    showError(err) {
-        this._sidebar.close();
-        this._host.showError(err.toString());
-        this.show('welcome');
-    }
-
     updateActiveGraph(name) {
         this._sidebar.close();
         if (this._model) {
             var model = this._model;
-            var graph = model.graphs.filter(graph => graph.name).shift();
+            var graph = model.graphs.filter(graph => name == graph.name).shift();
             if (graph) {
                 this.show('spinner');
                 setTimeout(() => {
-                    try {
-                        this.updateGraph(model, graph);
-                        this._model = model;
-                        this._activeGraph = graph;
-                    }
-                    catch (obj) {
-                        this._model = null;
-                        this._activeGraph = null;
-                    }
-                }, 2);
-    
+                    this.updateGraph(model, graph, (err, model) => {
+                        if (err) {
+                            this.error('Graph update failed.', err);
+                        }
+                    });
+                }, 200);
             }
         }
     }
-    
-    updateGraph(model, graph) {
-
-        if (!graph) {
-            this.show('graph');
-            return;
-        }
-    
-        var svgElement = document.getElementById('graph');
-        while (svgElement.lastChild) {
-            svgElement.removeChild(svgElement.lastChild);
-        }
 
-        this._zoom = null;
-
-        var groups = graph.groups;
-
-        var graphOptions = {};
-        if (!this._showDetails) {
-            graphOptions.nodesep = 25;
-            graphOptions.ranksep = 25;
-        }
+    updateGraph(model, graph, callback) {
+        setTimeout(() => {
+            if (graph && graph != this._activeGraph && graph.nodes.length > 1500 && !this._host.confirm('Large model detected.', 'This graph contains a large number of nodes and might take a long time to render. Do you want to continue?')) {
+                this.show(null);
+                callback(null, null);
+            }
+            else {
+                this.renderGraph(graph, (err) => {
+                    if (err) {
+                        this.renderGraph(this._activeGraph, (nestedError) => {
+                            if (nestedError) {
+                                this._model = null;
+                                this._activeGraph = null;
+                                this.show('welcome');
+                            }
+                            else {
+                                this.show('graph');
+                            }
+                            callback(err, this._model);
+                        });
+                    }
+                    else {
+                        this._model = model;
+                        this._activeGraph = graph;                            
+                        this.show('graph');
+                        callback(null, this._model);
+                    }
+                });
+            }
+        }, 100);
+    }
 
-        var g = new dagre.graphlib.Graph({ compound: groups });
-        g.setGraph(graphOptions);
-        // g.setGraph({ align: 'DR' });
-        // g.setGraph({ ranker: 'network-simplex' });
-        // g.setGraph({ ranker: 'tight-tree' });
-        // g.setGraph({ ranker: 'longest-path' });
-        // g.setGraph({ acyclicer: 'greedy' });
-        g.setDefaultEdgeLabel(() => { return {}; });
+    renderGraph(graph, callback) {
+        try {
+            if (!graph) {
+                callback(null);
+            }
+            else {
+                var svgElement = document.getElementById('graph');
+                while (svgElement.lastChild) {
+                    svgElement.removeChild(svgElement.lastChild);
+                }
     
-        var nodeId = 0;
-        var edgeMap = {};
+                this._zoom = null;
     
-        var clusterMap = {};
-        var clusterParentMap = {};
+                var groups = graph.groups;
     
-        if (groups) {
-            graph.nodes.forEach((node) => {
-                if (node.group) {
-                    var path = node.group.split('/');
-                    while (path.length > 0) {
-                        var name = path.join('/');
-                        path.pop();
-                        clusterParentMap[name] = path.join('/');
-                    }
+                var graphOptions = {};
+                if (!this._showDetails) {
+                    graphOptions.nodesep = 25;
+                    graphOptions.ranksep = 25;
                 }
-            });
-        }
-
-        graph.nodes.forEach((node) => {
-
-            var formatter = new NodeFormatter();
     
-            function addOperator(view, formatter, node) {
-                if (node) {
-                    var styles = [ 'node-item-operator' ];
-                    var category = node.category;
-                    if (category) {
-                        styles.push('node-item-operator-' + category.toLowerCase());
-                    }
-                    var text = view.showNames && node.name ? node.name : (node.primitive ? node.primitive : node.operator);
-                    var title = view.showNames && node.name ? node.operator : node.name;
-                    formatter.addItem(text, styles, title, () => { 
-                        view.showNodeProperties(node, null);
+                var g = new dagre.graphlib.Graph({ compound: groups });
+                g.setGraph(graphOptions);
+                g.setDefaultEdgeLabel(() => { return {}; });
+            
+                var nodeId = 0;
+                var edgeMap = {};
+            
+                var clusterMap = {};
+                var clusterParentMap = {};
+    
+                var nodes = graph.nodes;
+        
+                if (groups) {
+                    nodes.forEach((node) => {
+                        if (node.group) {
+                            var path = node.group.split('/');
+                            while (path.length > 0) {
+                                var name = path.join('/');
+                                path.pop();
+                                clusterParentMap[name] = path.join('/');
+                            }
+                        }
                     });
                 }
-            }
-
-            addOperator(this, formatter, node);
-            addOperator(this, formatter, node.inner);
-    
-            var primitive = node.primitive;
     
-            var hiddenInputs = false;
-            var hiddenInitializers = false;
+                nodes.forEach((node) => {
     
-            node.inputs.forEach((input) => {
-                // TODO what about mixed input & initializer
-                if (input.connections.length > 0) {
-                    var initializers = input.connections.filter(connection => connection.initializer);
-                    var inputClass = 'node-item-input';
-                    if (initializers.length == 0) {
-                        inputClass = 'node-item-input';
-                        if (input.hidden) {
-                            hiddenInputs = true;
+                    var formatter = new NodeFormatter();
+            
+                    function addOperator(view, formatter, node) {
+                        if (node) {
+                            var styles = [ 'node-item-operator' ];
+                            var category = node.category;
+                            if (category) {
+                                styles.push('node-item-operator-' + category.toLowerCase());
+                            }
+                            var text = view.showNames && node.name ? node.name : (node.primitive ? node.primitive : node.operator);
+                            var title = view.showNames && node.name ? node.operator : node.name;
+                            formatter.addItem(text, styles, title, () => { 
+                                view.showNodeProperties(node, null);
+                            });
                         }
                     }
-                    else {
-                        if (initializers.length == input.connections.length) {
-                            inputClass = 'node-item-constant';
-                            if (input.hidden) {
-                                hiddenInitializers = true;
+    
+                    addOperator(this, formatter, node);
+                    addOperator(this, formatter, node.inner);
+            
+                    var primitive = node.primitive;
+            
+                    var hiddenInputs = false;
+                    var hiddenInitializers = false;
+            
+                    node.inputs.forEach((input) => {
+                        // TODO what about mixed input & initializer
+                        if (input.connections.length > 0) {
+                            var initializers = input.connections.filter(connection => connection.initializer);
+                            var inputClass = 'node-item-input';
+                            if (initializers.length == 0) {
+                                inputClass = 'node-item-input';
+                                if (input.hidden) {
+                                    hiddenInputs = true;
+                                }
                             }
-                        }
-                        else {
-                            inputClass = 'node-item-constant';
-                            if (input.hidden) {
-                                hiddenInputs = true;
+                            else {
+                                if (initializers.length == input.connections.length) {
+                                    inputClass = 'node-item-constant';
+                                    if (input.hidden) {
+                                        hiddenInitializers = true;
+                                    }
+                                }
+                                else {
+                                    inputClass = 'node-item-constant';
+                                    if (input.hidden) {
+                                        hiddenInputs = true;
+                                    }
+                                }
+                            }
+            
+                            if (this._showDetails) {
+                                if (!input.hidden) {
+                                    var types = input.connections.map(connection => connection.type ? connection.type : '').join('\n');
+                                    formatter.addItem(input.name, [ inputClass ], types, () => {
+                                        this.showNodeProperties(node, input);
+                                    });    
+                                }
                             }
+            
+                            input.connections.forEach((connection) => {
+                                if (!connection.initializer) {
+                                    var tuple = edgeMap[connection.id];
+                                    if (!tuple) {
+                                        tuple = { from: null, to: [] };
+                                        edgeMap[connection.id] = tuple;
+                                    }
+                                    tuple.to.push({ 
+                                        node: nodeId, 
+                                        name: input.name
+                                    });
+                                }
+                            });    
                         }
-                    }
-    
+                    });
+            
                     if (this._showDetails) {
-                        if (!input.hidden) {
-                            var types = input.connections.map(connection => connection.type ? connection.type : '').join('\n');
-                            formatter.addItem(input.name, [ inputClass ], types, () => {
-                                this.showNodeProperties(node, input);
+                        if (hiddenInputs) {
+                            formatter.addItem('...', [ 'node-item-input' ], '', () => {
+                                this.showNodeProperties(node, null);
+                            });    
+                        }
+                        if (hiddenInitializers) {
+                            formatter.addItem('...', [ 'node-item-constant' ], '', () => {
+                                this.showNodeProperties(node, null);
                             });    
                         }
                     }
-    
-                    input.connections.forEach((connection) => {
-                        if (!connection.initializer) {
+            
+                    node.outputs.forEach((output) => {
+                        output.connections.forEach((connection) => {
                             var tuple = edgeMap[connection.id];
                             if (!tuple) {
                                 tuple = { from: null, to: [] };
                                 edgeMap[connection.id] = tuple;
                             }
-                            tuple.to.push({ 
-                                node: nodeId, 
-                                name: input.name
+                            tuple.from = { 
+                                node: nodeId,
+                                name: output.name
+                            };    
+                        });
+                    });
+            
+                    var dependencies = node.dependencies;
+                    if (dependencies && dependencies.length > 0) {
+                        formatter.setControlDependencies();
+                    }
+            
+                    if (this._showDetails) {
+                        var attributes = node.attributes; 
+                        if (attributes && !primitive) {
+                            formatter.setAttributeHandler(() => { 
+                                this.showNodeProperties(node, null);
+                            });
+                            attributes.forEach((attribute) => {
+                                if (attribute.visible) {
+                                    var attributeValue = '';
+                                    if (attribute.tensor) {
+                                        attributeValue = '[...]';
+                                    }
+                                    else {
+                                        attributeValue = attribute.value;
+                                        if (attributeValue.length > 25) {
+                                            attributeValue = attributeValue.substring(0, 25) + '...';
+                                        }
+                                    }
+                                    formatter.addAttribute(attribute.name, attributeValue, attribute.type);
+                                }
                             });
                         }
-                    });    
-                }
-            });
+                    }
     
-            if (this._showDetails) {
-                if (hiddenInputs) {
-                    formatter.addItem('...', [ 'node-item-input' ], '', () => {
-                        this.showNodeProperties(node, null);
-                    });    
-                }
-                if (hiddenInitializers) {
-                    formatter.addItem('...', [ 'node-item-constant' ], '', () => {
-                        this.showNodeProperties(node, null);
-                    });    
-                }
-            }
+                    var name = node.name;
+                    if (name) {
+                        g.setNode(nodeId, { id: 'node-' + name, label: formatter.format(svgElement) });
+                    }
+                    else {
+                        g.setNode(nodeId, { label: formatter.format(svgElement) });
+                    }
+            
+                    function createCluster(name) {
+                        if (!clusterMap[name]) {
+                            g.setNode(name, { rx: 5, ry: 5});
+                            clusterMap[name] = true;
+                            var parent = clusterParentMap[name];
+                            if (parent) {
+                                createCluster(parent);
+                                g.setParent(name, parent);
+                            }
+                        }
+                    }
     
-            node.outputs.forEach((output) => {
-                output.connections.forEach((connection) => {
-                    var tuple = edgeMap[connection.id];
+                    if (groups) {
+                        var groupName = node.group;
+                        if (groupName && groupName.length > 0) {
+                            if (!clusterParentMap.hasOwnProperty(groupName)) {
+                                var lastIndex = groupName.lastIndexOf('/');
+                                if (lastIndex != -1) {
+                                    groupName = groupName.substring(0, lastIndex);
+                                    if (!clusterParentMap.hasOwnProperty(groupName)) {
+                                        groupName = null;
+                                    }
+                                }
+                                else {
+                                    groupName = null;
+                                }
+                            }
+                            if (groupName) {
+                                createCluster(groupName);
+                                g.setParent(nodeId, groupName);
+                            }
+                        }
+                    }
+                
+                    nodeId++;
+                });
+            
+                graph.inputs.forEach((input) => {
+                    var tuple = edgeMap[input.id];
                     if (!tuple) {
                         tuple = { from: null, to: [] };
-                        edgeMap[connection.id] = tuple;
+                        edgeMap[input.id] = tuple;
                     }
                     tuple.from = { 
                         node: nodeId,
-                        name: output.name
-                    };    
-                });
-            });
-    
-            var dependencies = node.dependencies;
-            if (dependencies && dependencies.length > 0) {
-                formatter.setControlDependencies();
-            }
+                    };
     
-            if (this._showDetails) {
-                var attributes = node.attributes; 
-                if (attributes && !primitive) {
-                    formatter.setAttributeHandler(() => { 
-                        this.showNodeProperties(node, null);
+                    var formatter = new NodeFormatter();
+                    formatter.addItem(input.name, [ 'graph-item-input' ], input.type, () => {
+                        this.showModelProperties();
+                    });
+                    g.setNode(nodeId++, { label: formatter.format(svgElement), class: 'graph-input' } ); 
+                });
+            
+                graph.outputs.forEach((output) => {
+                    var outputId = output.id;
+                    var outputName = output.name;
+                    var tuple = edgeMap[outputId];
+                    if (!tuple) {
+                        tuple = { from: null, to: [] };
+                        edgeMap[outputId] = tuple;
+                    }
+                    tuple.to.push({
+                        node: nodeId,
+                        // name: valueInfo.name
+                    });
+            
+                    var formatter = new NodeFormatter();
+                    formatter.addItem(output.name, [ 'graph-item-output' ], output.type, () => {
+                        this.showModelProperties();
                     });
-                    attributes.forEach((attribute) => {
-                        if (attribute.visible) {
-                            var attributeValue = '';
-                            if (attribute.tensor) {
-                                attributeValue = '[...]';
+                    g.setNode(nodeId++, { label: formatter.format(svgElement) } ); 
+                });
+            
+                Object.keys(edgeMap).forEach((edge) => {
+                    var tuple = edgeMap[edge];
+                    if (tuple.from != null) {
+                        tuple.to.forEach((to) => {
+                            var text = '';
+                            if (tuple.from.name && to.name) {
+                                text = tuple.from.name + ' \u21E8 ' + to.name;
+                            }
+                            else if (tuple.from.name) {
+                                text = tuple.from.name;
                             }
                             else {
-                                attributeValue = attribute.value;
-                                if (attributeValue.length > 25) {
-                                    attributeValue = attributeValue.substring(0, 25) + '...';
-                                }
+                                text = to.name;
+                            }
+            
+                            if (this._showNames) {
+                                text = edge;
+                            }
+                            if (!this._showDetails) {
+                                text = '';
                             }
-                            formatter.addAttribute(attribute.name, attributeValue, attribute.type);
-                        }
-                    });
-                }
-            }
-
-            var name = node.name;
-            if (name) {
-                g.setNode(nodeId, { id: 'node-' + name, label: formatter.format(svgElement) });
-            }
-            else {
-                g.setNode(nodeId, { label: formatter.format(svgElement) });
-            }
     
-            function createCluster(name) {
-                if (!clusterMap[name]) {
-                    g.setNode(name, { rx: 5, ry: 5});
-                    clusterMap[name] = true;
-                    var parent = clusterParentMap[name];
-                    if (parent) {
-                        createCluster(parent);
-                        g.setParent(name, parent);
+                            if (to.dependency) { 
+                                g.setEdge(tuple.from.node, to.node, { label: text, id: 'edge-' + edge, arrowhead: 'vee', curve: d3.curveBasis, class: 'edge-path-control' } );
+                            }
+                            else {
+                                g.setEdge(tuple.from.node, to.node, { label: text, id: 'edge-' + edge, arrowhead: 'vee', curve: d3.curveBasis } );
+                            }
+                        });
                     }
-                }
-            }
-
-            if (groups) {
-                var name = node.group;
-                if (name && name.length > 0) {
-                    if (!clusterParentMap.hasOwnProperty(name)) {
-                        var lastIndex = name.lastIndexOf('/');
-                        if (lastIndex != -1) {
-                            name = name.substring(0, lastIndex);
-                            if (!clusterParentMap.hasOwnProperty(name)) {
-                                name = null;
+                });
+            
+                // Workaround for Safari background drag/zoom issue:
+                // https://stackoverflow.com/questions/40887193/d3-js-zoom-is-not-working-with-mousewheel-in-safari
+                var backgroundElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                backgroundElement.setAttribute('id', 'background');
+                backgroundElement.setAttribute('width', '100%');
+                backgroundElement.setAttribute('height', '100%');
+                backgroundElement.setAttribute('fill', 'none');
+                backgroundElement.setAttribute('pointer-events', 'all');
+                svgElement.appendChild(backgroundElement);
+            
+                var originElement = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+                originElement.setAttribute('id', 'origin');
+                svgElement.appendChild(originElement);
+            
+                // Set up zoom support
+                this._zoom = d3.zoom();
+                this._zoom.scaleExtent([0.1, 2]);
+                this._zoom.on('zoom', (e) => {
+                    d3.select(originElement).attr('transform', d3.event.transform);
+                });
+                var svg = d3.select(svgElement);
+                this._zoom(svg);
+                this._zoom.transform(svg, d3.zoomIdentity);
+                this._svg = svg;
+            
+                setTimeout(() => {
+                    try {
+                        var graphRenderer = new GraphRenderer(originElement);
+                        graphRenderer.render(g);
+            
+                        var svgSize = svgElement.getBoundingClientRect();
+            
+                        var inputElements = svgElement.getElementsByClassName('graph-input');
+                        if (inputElements && inputElements.length > 0) {
+                            // Center view based on input elements
+                            var x = 0;
+                            var y = 0;
+                            for (var i = 0; i < inputElements.length; i++) {
+                                var inputTransform = inputElements[i].transform.baseVal.consolidate().matrix;
+                                x += inputTransform.e;
+                                y += inputTransform.f;
                             }
+                            x = x / inputElements.length;
+                            y = y / inputElements.length;
+            
+                            this._zoom.transform(svg, d3.zoomIdentity.translate((svgSize.width / 2) - x, (svgSize.height / 4) - y));
                         }
                         else {
-                            name = null;
+                            this._zoom.transform(svg, d3.zoomIdentity.translate((svgSize.width - g.graph().width) / 2, (svgSize.height - g.graph().height) / 2));
                         }
-                    }
-                    if (name) {
-                        createCluster(name);
-                        g.setParent(nodeId, name);
-                    }
-                }
-            }
-        
-            nodeId++;
-        });
-    
-        graph.inputs.forEach((input) => {
-            var tuple = edgeMap[input.id];
-            if (!tuple) {
-                tuple = { from: null, to: [] };
-                edgeMap[input.id] = tuple;
-            }
-            tuple.from = { 
-                node: nodeId,
-            };
-
-            var formatter = new NodeFormatter();
-            formatter.addItem(input.name, [ 'graph-item-input' ], input.type, () => {
-                this.showModelProperties();
-            });
-            g.setNode(nodeId++, { label: formatter.format(svgElement), class: 'graph-input' } ); 
-        });
-    
-        graph.outputs.forEach((output) => {
-            var outputId = output.id;
-            var outputName = output.name;
-            var tuple = edgeMap[outputId];
-            if (!tuple) {
-                tuple = { from: null, to: [] };
-                edgeMap[outputId] = tuple;
-            }
-            tuple.to.push({
-                node: nodeId,
-                // name: valueInfo.name
-            });
-    
-            var formatter = new NodeFormatter();
-            formatter.addItem(output.name, [ 'graph-item-output' ], output.type, () => {
-                this.showModelProperties();
-            });
-            g.setNode(nodeId++, { label: formatter.format(svgElement) } ); 
-        });
-    
-        Object.keys(edgeMap).forEach((edge) => {
-            var tuple = edgeMap[edge];
-            if (tuple.from != null) {
-                tuple.to.forEach((to) => {
-                    var text = '';
-                    if (tuple.from.name && to.name) {
-                        text = tuple.from.name + ' \u21E8 ' + to.name;
-                    }
-                    else if (tuple.from.name) {
-                        text = tuple.from.name;
-                    }
-                    else {
-                        text = to.name;
-                    }
-    
-                    if (this._showNames) {
-                        text = edge;
-                    }
-                    if (!this._showDetails) {
-                        text = '';
-                    }
 
-                    if (to.dependency) { 
-                        g.setEdge(tuple.from.node, to.node, { label: text, id: 'edge-' + edge, arrowhead: 'vee', curve: d3.curveBasis, class: 'edge-path-control' } );
+                        callback(null);
                     }
-                    else {
-                        g.setEdge(tuple.from.node, to.node, { label: text, id: 'edge-' + edge, arrowhead: 'vee', curve: d3.curveBasis } );
+                    catch (err) {
+                        callback(err);
                     }
-                });
-            }
-        });
-    
-        // Workaround for Safari background drag/zoom issue:
-        // https://stackoverflow.com/questions/40887193/d3-js-zoom-is-not-working-with-mousewheel-in-safari
-        var backgroundElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
-        backgroundElement.setAttribute('id', 'background');
-        backgroundElement.setAttribute('width', '100%');
-        backgroundElement.setAttribute('height', '100%');
-        backgroundElement.setAttribute('fill', 'none');
-        backgroundElement.setAttribute('pointer-events', 'all');
-        svgElement.appendChild(backgroundElement);
-    
-        var originElement = document.createElementNS('http://www.w3.org/2000/svg', 'g');
-        originElement.setAttribute('id', 'origin');
-        svgElement.appendChild(originElement);
-    
-        // Set up zoom support
-        this._zoom = d3.zoom();
-        this._zoom.scaleExtent([0.1, 2]);
-        this._zoom.on('zoom', (e) => {
-            d3.select(originElement).attr('transform', d3.event.transform);
-        });
-        var svg = d3.select(svgElement);
-        this._zoom(svg);
-        this._zoom.transform(svg, d3.zoomIdentity);
-        this._svg = svg;
-    
-        setTimeout(() => {
-    
-            var graphRenderer = new GraphRenderer(originElement);
-            graphRenderer.render(g);
-
-            var svgSize = svgElement.getBoundingClientRect();
-    
-            var inputElements = svgElement.getElementsByClassName('graph-input');
-            if (inputElements && inputElements.length > 0) {
-                // Center view based on input elements
-                var x = 0;
-                var y = 0;
-                for (var i = 0; i < inputElements.length; i++) {
-                    var inputTransform = inputElements[i].transform.baseVal.consolidate().matrix;
-                    x += inputTransform.e;
-                    y += inputTransform.f;
-                }
-                x = x / inputElements.length;
-                y = y / inputElements.length;
-    
-                this._zoom.transform(svg, d3.zoomIdentity.translate((svgSize.width / 2) - x, (svgSize.height / 4) - y));
+                }, 20);
             }
-            else {
-                this._zoom.transform(svg, d3.zoomIdentity.translate((svgSize.width - g.graph().width) / 2, (svgSize.height - g.graph().height) / 2));
-            }    
-        
-            this.show('graph');
-        }, 2);
+        }
+        catch (err) {
+            callback(err);
+        }
     }
 
     copyStylesInline(destinationNode, sourceNode) {