From 3a1c845f962c7ee703dddff856fae95a823fb8c5 Mon Sep 17 00:00:00 2001 From: MinRK Date: Thu, 10 Jul 2014 17:03:35 -0500 Subject: [PATCH 01/11] support buffers in comm messages - add buffers arg to comm Python api - support binary websocket messages when buffers are present - reimplement utf8 in javascript, because javascript is the best --- IPython/html/base/zmqhandlers.py | 66 ++++++++++++++++++- IPython/html/static/base/js/utils.js | 40 ++++++++++- .../html/static/services/kernels/js/kernel.js | 63 ++++++++++++++++-- 3 files changed, 161 insertions(+), 8 deletions(-) diff --git a/IPython/html/base/zmqhandlers.py b/IPython/html/base/zmqhandlers.py index b3d781036..18f68b470 100644 --- a/IPython/html/base/zmqhandlers.py +++ b/IPython/html/base/zmqhandlers.py @@ -4,6 +4,7 @@ # Distributed under the terms of the Modified BSD License. import json +import struct try: from urllib.parse import urlparse # Py 3 @@ -28,6 +29,61 @@ from IPython.utils.py3compat import PY3, cast_unicode from .handlers import IPythonHandler +def serialize_binary_message(msg): + """serialize a message as a binary blob + + Header: + + 4 bytes: number of msg parts (nbufs) as 32b int + 4 * nbufs bytes: offset for each buffer as integer as 32b int + + Offsets are from the start of the buffer, including the header. + + Returns + ------- + + The message serialized to bytes. + + """ + buffers = msg.pop('buffers') + bmsg = json.dumps(msg, default=date_default).encode('utf8') + buffers.insert(0, bmsg) + nbufs = len(buffers) + sizes = (len(buf) for buf in buffers) + offsets = [4 * (nbufs + 1)] + for buf in buffers[:-1]: + offsets.append(offsets[-1] + len(buf)) + offsets_buf = struct.pack('!' + 'i' * (nbufs + 1), nbufs, *offsets) + buffers.insert(0, offsets_buf) + return b''.join(buffers) + + +def unserialize_binary_message(bmsg): + """unserialize a message from a binary blog + + Header: + + 4 bytes: number of msg parts (nbufs) as 32b int + 4 * nbufs bytes: offset for each buffer as integer as 32b int + + Offsets are from the start of the buffer, including the header. + + Returns + ------- + + message dictionary + """ + nbufs = struct.unpack('i', bmsg[:4])[0] + offsets = list(struct.unpack('!' + 'i' * nbufs, bmsg[4:4*(nbufs+1)])) + offsets.append(None) + bufs = [] + for start, stop in zip(offsets[:-1], offsets[1:]): + bufs.append(bmsg[start:stop]) + msg = json.loads(bufs[0]) + msg['buffers'] = bufs[1:] + return msg + + class ZMQStreamHandler(websocket.WebSocketHandler): def check_origin(self, origin): @@ -92,8 +148,12 @@ class ZMQStreamHandler(websocket.WebSocketHandler): msg['parent_header'].pop('date') except KeyError: pass - msg.pop('buffers') - return json.dumps(msg, default=date_default) + if msg['buffers']: + buf = serialize_binary_message(msg) + return buf + else: + smsg = json.dumps(msg, default=date_default) + return cast_unicode(smsg) def _on_zmq_reply(self, msg_list): # Sometimes this gets triggered when the on_close method is scheduled in the @@ -104,7 +164,7 @@ class ZMQStreamHandler(websocket.WebSocketHandler): except Exception: self.log.critical("Malformed message: %r" % msg_list, exc_info=True) else: - self.write_message(msg) + self.write_message(msg, binary=isinstance(msg, bytes)) def allow_draft76(self): """Allow draft 76, until browsers such as Safari update to RFC 6455. diff --git a/IPython/html/static/base/js/utils.js b/IPython/html/static/base/js/utils.js index 61eb84ab7..d70304ebf 100644 --- a/IPython/html/static/base/js/utils.js +++ b/IPython/html/static/base/js/utils.js @@ -553,7 +553,44 @@ define([ ], callback, errback ); }; - + + var decode_utf8 = function (array) { + // Decode UTF8 Uint8Array to String + // I can't believe Javascript makes us do this + // From http://stackoverflow.com/questions/17191945 + + var out, i, len, c; + var char2, char3; + + out = ""; + len = array.length; + i = 0; + while(i < len) { + c = array[i++]; + switch(c >> 4) { + case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: + // 0xxxxxxx + out += String.fromCharCode(c); + break; + case 12: case 13: + // 110x xxxx 10xx xxxx + char2 = array[i++]; + out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F)); + break; + case 14: + // 1110 xxxx 10xx xxxx 10xx xxxx + char2 = array[i++]; + char3 = array[i++]; + out += String.fromCharCode(((c & 0x0F) << 12) | + ((char2 & 0x3F) << 6) | + ((char3 & 0x3F) << 0)); + break; + } + } + + return out; + }; + var utils = { regex_split : regex_split, uuid : uuid, @@ -579,6 +616,7 @@ define([ ajax_error_msg : ajax_error_msg, log_ajax_error : log_ajax_error, requireCodeMirrorMode : requireCodeMirrorMode, + decode_utf8: decode_utf8, }; // Backwards compatability. diff --git a/IPython/html/static/services/kernels/js/kernel.js b/IPython/html/static/services/kernels/js/kernel.js index 3a25e9066..1ee905330 100644 --- a/IPython/html/static/services/kernels/js/kernel.js +++ b/IPython/html/static/services/kernels/js/kernel.js @@ -846,12 +846,60 @@ define([ } }; + Kernel.prototype._unserialize_binary_message = function(blob, callback) { + // unserialize the binary message format + // callback will be called with a message whose buffers attribute + // will be an array of DataViews. + var reader = new FileReader(); + reader.onload = function(e) { + var data = new DataView(this.result); + // read the header: 1 + nbufs 32b integers + var nbufs = data.getInt32(0); + var offsets = []; + var i; + for (i = 1; i <= nbufs; i++) { + offsets.push(data.getInt32(i * 4)); + } + // the first chunk is the message as utf-8 JSON + var msg = $.parseJSON( + utis.decode_utf8( + new Uint8Array(this.result.slice(offsets[0], offsets[1])) + ) + ); + // the remaining chunks are stored as DataViews in msg.buffers + msg.buffers = []; + var start, stop; + for (i = 1; i < nbufs; i++) { + start = offsets[i]; + stop = offsets[i+1]; + msg.buffers.push(new DataView(this.result.slice(start, stop))); + } + callback(msg); + }; + reader.readAsArrayBuffer(blob); + }; + + + Kernel.prototype._unserialize_msg = function (e, callback) { + // unserialze a message and pass the unpacked message object to callback + if (typeof e.data === "string") { + // text JSON message + callback($.parseJSON(e.data)); + } else { + // binary message + this._unserialize_binary_message(e.data, callback); + } + }; + /** * @function _handle_shell_reply */ Kernel.prototype._handle_shell_reply = function (e) { - var reply = $.parseJSON(e.data); - this.events.trigger('shell_reply.Kernel', {kernel: this, reply: reply}); + this._unserialize_msg(e, $.proxy(this._finish_shell_reply, this)); + }; + + Kernel.prototype._finish_shell_reply = function (reply) { + this.events.trigger('shell_reply.Kernel', {kernel: this, reply:reply}); var content = reply.content; var metadata = reply.metadata; var parent_id = reply.parent_header.msg_id; @@ -978,8 +1026,11 @@ define([ * @function _handle_iopub_message */ Kernel.prototype._handle_iopub_message = function (e) { - var msg = $.parseJSON(e.data); + this._unserialize_msg(e, $.proxy(this._finish_iopub_message, this)); + }; + + Kernel.prototype._finish_iopub_message = function (msg) { var handler = this.get_iopub_handler(msg.header.msg_type); if (handler !== undefined) { handler(msg); @@ -990,7 +1041,11 @@ define([ * @function _handle_input_request */ Kernel.prototype._handle_input_request = function (e) { - var request = $.parseJSON(e.data); + this._unserialize_msg(e, $.proxy(this._finish_input_request, this)); + }; + + + Kernel.prototype._finish_input_request = function (request) { var header = request.header; var content = request.content; var metadata = request.metadata; From c9c131ebf9f261ecdec5ec341f1e11af22fd09cc Mon Sep 17 00:00:00 2001 From: MinRK Date: Thu, 28 Aug 2014 17:04:24 -0700 Subject: [PATCH 02/11] s/unserialize/deserialize --- IPython/html/base/zmqhandlers.py | 8 ++++---- IPython/html/services/kernels/handlers.py | 2 +- .../html/static/services/kernels/js/kernel.js | 17 +++++++++-------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/IPython/html/base/zmqhandlers.py b/IPython/html/base/zmqhandlers.py index 18f68b470..c3f335f01 100644 --- a/IPython/html/base/zmqhandlers.py +++ b/IPython/html/base/zmqhandlers.py @@ -58,8 +58,8 @@ def serialize_binary_message(msg): return b''.join(buffers) -def unserialize_binary_message(bmsg): - """unserialize a message from a binary blog +def deserialize_binary_message(bmsg): + """deserialize a message from a binary blog Header: @@ -133,13 +133,13 @@ class ZMQStreamHandler(websocket.WebSocketHandler): def _reserialize_reply(self, msg_list): """Reserialize a reply message using JSON. - This takes the msg list from the ZMQ socket, unserializes it using + This takes the msg list from the ZMQ socket, deserializes it using self.session and then serializes the result using JSON. This method should be used by self._on_zmq_reply to build messages that can be sent back to the browser. """ idents, msg_list = self.session.feed_identities(msg_list) - msg = self.session.unserialize(msg_list) + msg = self.session.deserialize(msg_list) try: msg['header'].pop('date') except KeyError: diff --git a/IPython/html/services/kernels/handlers.py b/IPython/html/services/kernels/handlers.py index 7d89ad6c7..dbfd5fe3f 100644 --- a/IPython/html/services/kernels/handlers.py +++ b/IPython/html/services/kernels/handlers.py @@ -110,7 +110,7 @@ class ZMQChannelHandler(AuthenticatedZMQStreamHandler): """ idents,msg = self.session.feed_identities(msg) try: - msg = self.session.unserialize(msg) + msg = self.session.deserialize(msg) except: self.log.error("Bad kernel_info reply", exc_info=True) self._request_kernel_info() diff --git a/IPython/html/static/services/kernels/js/kernel.js b/IPython/html/static/services/kernels/js/kernel.js index 1ee905330..e8db4a619 100644 --- a/IPython/html/static/services/kernels/js/kernel.js +++ b/IPython/html/static/services/kernels/js/kernel.js @@ -846,8 +846,9 @@ define([ } }; - Kernel.prototype._unserialize_binary_message = function(blob, callback) { - // unserialize the binary message format + + Kernel.prototype._deserialize_binary_message = function(blob, callback) { + // deserialize the binary message format // callback will be called with a message whose buffers attribute // will be an array of DataViews. var reader = new FileReader(); @@ -880,14 +881,14 @@ define([ }; - Kernel.prototype._unserialize_msg = function (e, callback) { - // unserialze a message and pass the unpacked message object to callback + Kernel.prototype._deserialize_msg = function (e, callback) { + // deserialze a message and pass the unpacked message object to callback if (typeof e.data === "string") { // text JSON message callback($.parseJSON(e.data)); } else { // binary message - this._unserialize_binary_message(e.data, callback); + this._deserialize_binary_message(e.data, callback); } }; @@ -895,7 +896,7 @@ define([ * @function _handle_shell_reply */ Kernel.prototype._handle_shell_reply = function (e) { - this._unserialize_msg(e, $.proxy(this._finish_shell_reply, this)); + this._deserialize_msg(e, $.proxy(this._finish_shell_reply, this)); }; Kernel.prototype._finish_shell_reply = function (reply) { @@ -1026,7 +1027,7 @@ define([ * @function _handle_iopub_message */ Kernel.prototype._handle_iopub_message = function (e) { - this._unserialize_msg(e, $.proxy(this._finish_iopub_message, this)); + this._deserialize_msg(e, $.proxy(this._finish_iopub_message, this)); }; @@ -1041,7 +1042,7 @@ define([ * @function _handle_input_request */ Kernel.prototype._handle_input_request = function (e) { - this._unserialize_msg(e, $.proxy(this._finish_input_request, this)); + this._deserialize_msg(e, $.proxy(this._finish_input_request, this)); }; From 6cef7fcb86c4c9c8290bb0a4ae462d8b4b7747ca Mon Sep 17 00:00:00 2001 From: MinRK Date: Fri, 29 Aug 2014 10:59:00 -0700 Subject: [PATCH 03/11] use utf8.js move message serialization to kernel.serialize module --- IPython/html/static/base/js/utils.js | 38 ------------ .../html/static/services/kernels/js/kernel.js | 55 ++--------------- .../static/services/kernels/js/serialize.js | 61 +++++++++++++++++++ 3 files changed, 66 insertions(+), 88 deletions(-) create mode 100644 IPython/html/static/services/kernels/js/serialize.js diff --git a/IPython/html/static/base/js/utils.js b/IPython/html/static/base/js/utils.js index d70304ebf..8e2530354 100644 --- a/IPython/html/static/base/js/utils.js +++ b/IPython/html/static/base/js/utils.js @@ -554,43 +554,6 @@ define([ ); }; - var decode_utf8 = function (array) { - // Decode UTF8 Uint8Array to String - // I can't believe Javascript makes us do this - // From http://stackoverflow.com/questions/17191945 - - var out, i, len, c; - var char2, char3; - - out = ""; - len = array.length; - i = 0; - while(i < len) { - c = array[i++]; - switch(c >> 4) { - case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: - // 0xxxxxxx - out += String.fromCharCode(c); - break; - case 12: case 13: - // 110x xxxx 10xx xxxx - char2 = array[i++]; - out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F)); - break; - case 14: - // 1110 xxxx 10xx xxxx 10xx xxxx - char2 = array[i++]; - char3 = array[i++]; - out += String.fromCharCode(((c & 0x0F) << 12) | - ((char2 & 0x3F) << 6) | - ((char3 & 0x3F) << 0)); - break; - } - } - - return out; - }; - var utils = { regex_split : regex_split, uuid : uuid, @@ -616,7 +579,6 @@ define([ ajax_error_msg : ajax_error_msg, log_ajax_error : log_ajax_error, requireCodeMirrorMode : requireCodeMirrorMode, - decode_utf8: decode_utf8, }; // Backwards compatability. diff --git a/IPython/html/static/services/kernels/js/kernel.js b/IPython/html/static/services/kernels/js/kernel.js index e8db4a619..7a24f84ef 100644 --- a/IPython/html/static/services/kernels/js/kernel.js +++ b/IPython/html/static/services/kernels/js/kernel.js @@ -7,7 +7,8 @@ define([ 'base/js/utils', 'services/kernels/js/comm', 'widgets/js/init', -], function(IPython, $, utils, comm, widgetmanager) { + './serialize' +], function(IPython, $, utils, comm, widgetmanager, serialize) { "use strict"; /** @@ -846,57 +847,11 @@ define([ } }; - - Kernel.prototype._deserialize_binary_message = function(blob, callback) { - // deserialize the binary message format - // callback will be called with a message whose buffers attribute - // will be an array of DataViews. - var reader = new FileReader(); - reader.onload = function(e) { - var data = new DataView(this.result); - // read the header: 1 + nbufs 32b integers - var nbufs = data.getInt32(0); - var offsets = []; - var i; - for (i = 1; i <= nbufs; i++) { - offsets.push(data.getInt32(i * 4)); - } - // the first chunk is the message as utf-8 JSON - var msg = $.parseJSON( - utis.decode_utf8( - new Uint8Array(this.result.slice(offsets[0], offsets[1])) - ) - ); - // the remaining chunks are stored as DataViews in msg.buffers - msg.buffers = []; - var start, stop; - for (i = 1; i < nbufs; i++) { - start = offsets[i]; - stop = offsets[i+1]; - msg.buffers.push(new DataView(this.result.slice(start, stop))); - } - callback(msg); - }; - reader.readAsArrayBuffer(blob); - }; - - - Kernel.prototype._deserialize_msg = function (e, callback) { - // deserialze a message and pass the unpacked message object to callback - if (typeof e.data === "string") { - // text JSON message - callback($.parseJSON(e.data)); - } else { - // binary message - this._deserialize_binary_message(e.data, callback); - } - }; - /** * @function _handle_shell_reply */ Kernel.prototype._handle_shell_reply = function (e) { - this._deserialize_msg(e, $.proxy(this._finish_shell_reply, this)); + serialize.deserialize(e.data, $.proxy(this._finish_shell_reply, this)); }; Kernel.prototype._finish_shell_reply = function (reply) { @@ -1027,7 +982,7 @@ define([ * @function _handle_iopub_message */ Kernel.prototype._handle_iopub_message = function (e) { - this._deserialize_msg(e, $.proxy(this._finish_iopub_message, this)); + serialize.deserialize(e.data, $.proxy(this._finish_iopub_message, this)); }; @@ -1042,7 +997,7 @@ define([ * @function _handle_input_request */ Kernel.prototype._handle_input_request = function (e) { - this._deserialize_msg(e, $.proxy(this._finish_input_request, this)); + serialize.deserialize(e.data, $.proxy(this._finish_input_request, this)); }; diff --git a/IPython/html/static/services/kernels/js/serialize.js b/IPython/html/static/services/kernels/js/serialize.js new file mode 100644 index 000000000..6ce8bdb3a --- /dev/null +++ b/IPython/html/static/services/kernels/js/serialize.js @@ -0,0 +1,61 @@ +// Copyright (c) IPython Development Team. +// Distributed under the terms of the Modified BSD License. + +define([ + 'jquery', + 'components/utf8/utf8' + ], function ($, utf8) { + "use strict"; + + var _deserialize_binary = function(blob, callback) { + // deserialize the binary message format + // callback will be called with a message whose buffers attribute + // will be an array of DataViews. + var reader = new FileReader(); + reader.onload = function () { + var buf = this.result; // an ArrayBuffer + var data = new DataView(buf); + // read the header: 1 + nbufs 32b integers + var nbufs = data.getInt32(0); + var offsets = []; + var i; + for (i = 1; i <= nbufs; i++) { + offsets.push(data.getInt32(i * 4)); + } + // have to convert array to string for utf8.js + var bytestring = String.fromCharCode.apply(null, + new Uint8Array(buf.slice(offsets[0], offsets[1])) + ); + var msg = $.parseJSON( + utf8.decode( + bytestring + ) + ); + // the remaining chunks are stored as DataViews in msg.buffers + msg.buffers = []; + var start, stop; + for (i = 1; i < nbufs; i++) { + start = offsets[i]; + stop = offsets[i+1] || buf.byteLength; + msg.buffers.push(new DataView(buf.slice(start, stop))); + } + callback(msg); + }; + reader.readAsArrayBuffer(blob); + }; + + var deserialize = function (data, callback) { + // deserialize a message and pass the unpacked message object to callback + if (typeof data === "string") { + // text JSON message + callback($.parseJSON(data)); + } else { + // binary message + _deserialize_binary(data, callback); + } + }; + + return { + deserialize : deserialize + }; +}); \ No newline at end of file From 3693ac83aece47f402ddbcc279814e74df09fda9 Mon Sep 17 00:00:00 2001 From: MinRK Date: Wed, 1 Oct 2014 17:12:30 -0700 Subject: [PATCH 04/11] support binary message from javascript --- IPython/html/base/zmqhandlers.py | 1 - IPython/html/services/kernels/handlers.py | 7 +- .../html/static/services/kernels/js/comm.js | 4 +- .../html/static/services/kernels/js/kernel.js | 11 +- .../static/services/kernels/js/serialize.js | 135 +++++++++++++----- 5 files changed, 114 insertions(+), 44 deletions(-) diff --git a/IPython/html/base/zmqhandlers.py b/IPython/html/base/zmqhandlers.py index c3f335f01..99b707717 100644 --- a/IPython/html/base/zmqhandlers.py +++ b/IPython/html/base/zmqhandlers.py @@ -49,7 +49,6 @@ def serialize_binary_message(msg): bmsg = json.dumps(msg, default=date_default).encode('utf8') buffers.insert(0, bmsg) nbufs = len(buffers) - sizes = (len(buf) for buf in buffers) offsets = [4 * (nbufs + 1)] for buf in buffers[:-1]: offsets.append(offsets[-1] + len(buf)) diff --git a/IPython/html/services/kernels/handlers.py b/IPython/html/services/kernels/handlers.py index dbfd5fe3f..6c5f2dfbf 100644 --- a/IPython/html/services/kernels/handlers.py +++ b/IPython/html/services/kernels/handlers.py @@ -12,7 +12,7 @@ from IPython.utils.py3compat import string_types from IPython.html.utils import url_path_join, url_escape from ...base.handlers import IPythonHandler, json_errors -from ...base.zmqhandlers import AuthenticatedZMQStreamHandler +from ...base.zmqhandlers import AuthenticatedZMQStreamHandler, deserialize_binary_message from IPython.core.release import kernel_protocol_version @@ -150,7 +150,10 @@ class ZMQChannelHandler(AuthenticatedZMQStreamHandler): self.log.info("%s closed, closing websocket.", self) self.close() return - msg = json.loads(msg) + if isinstance(msg, bytes): + msg = deserialize_binary_message(msg) + else: + msg = json.loads(msg) self.session.send(self.zmq_stream, msg) def on_close(self): diff --git a/IPython/html/static/services/kernels/js/comm.js b/IPython/html/static/services/kernels/js/comm.js index 04307cf40..91f3dc826 100644 --- a/IPython/html/static/services/kernels/js/comm.js +++ b/IPython/html/static/services/kernels/js/comm.js @@ -129,12 +129,12 @@ define([ return this.kernel.send_shell_message("comm_open", content, callbacks, metadata); }; - Comm.prototype.send = function (data, callbacks, metadata) { + Comm.prototype.send = function (data, callbacks, metadata, buffers) { var content = { comm_id : this.comm_id, data : data || {}, }; - return this.kernel.send_shell_message("comm_msg", content, callbacks, metadata); + return this.kernel.send_shell_message("comm_msg", content, callbacks, metadata, buffers); }; Comm.prototype.close = function (data, callbacks, metadata) { diff --git a/IPython/html/static/services/kernels/js/kernel.js b/IPython/html/static/services/kernels/js/kernel.js index 7a24f84ef..e01c0bd8f 100644 --- a/IPython/html/static/services/kernels/js/kernel.js +++ b/IPython/html/static/services/kernels/js/kernel.js @@ -70,7 +70,7 @@ define([ /** * @function _get_msg */ - Kernel.prototype._get_msg = function (msg_type, content, metadata) { + Kernel.prototype._get_msg = function (msg_type, content, metadata, buffers) { var msg = { header : { msg_id : utils.uuid(), @@ -81,6 +81,7 @@ define([ }, metadata : metadata || {}, content : content, + buffers : buffers || [], parent_header : {} }; return msg; @@ -597,12 +598,12 @@ define([ * * @function send_shell_message */ - Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata) { + Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata, buffers) { if (!this.is_connected()) { throw new Error("kernel is not connected"); } - var msg = this._get_msg(msg_type, content, metadata); - this.channels.shell.send(JSON.stringify(msg)); + var msg = this._get_msg(msg_type, content, metadata, buffers); + this.channels.shell.send(serialize.serialize(msg)); this.set_callbacks_for_msg(msg.header.msg_id, callbacks); return msg.header.msg_id; }; @@ -753,7 +754,7 @@ define([ }; this.events.trigger('input_reply.Kernel', {kernel: this, content: content}); var msg = this._get_msg("input_reply", content); - this.channels.stdin.send(JSON.stringify(msg)); + this.channels.stdin.send(serialize.serialize(msg)); return msg.header.msg_id; }; diff --git a/IPython/html/static/services/kernels/js/serialize.js b/IPython/html/static/services/kernels/js/serialize.js index 6ce8bdb3a..a43bab002 100644 --- a/IPython/html/static/services/kernels/js/serialize.js +++ b/IPython/html/static/services/kernels/js/serialize.js @@ -6,42 +6,53 @@ define([ 'components/utf8/utf8' ], function ($, utf8) { "use strict"; - - var _deserialize_binary = function(blob, callback) { + + var _deserialize_array_buffer = function (buf) { + var data = new DataView(buf); + // read the header: 1 + nbufs 32b integers + var nbufs = data.getInt32(0); + var offsets = []; + var i; + for (i = 1; i <= nbufs; i++) { + offsets.push(data.getInt32(i * 4)); + } + // have to convert array to string for utf8.js + var bytestring = String.fromCharCode.apply(null, + new Uint8Array(buf.slice(offsets[0], offsets[1])) + ); + var msg = $.parseJSON( + utf8.decode( + bytestring + ) + ); + // the remaining chunks are stored as DataViews in msg.buffers + msg.buffers = []; + var start, stop; + for (i = 1; i < nbufs; i++) { + start = offsets[i]; + stop = offsets[i+1] || buf.byteLength; + msg.buffers.push(new DataView(buf.slice(start, stop))); + } + return msg; + }; + + var _deserialize_binary = function(data, callback) { // deserialize the binary message format // callback will be called with a message whose buffers attribute // will be an array of DataViews. - var reader = new FileReader(); - reader.onload = function () { - var buf = this.result; // an ArrayBuffer - var data = new DataView(buf); - // read the header: 1 + nbufs 32b integers - var nbufs = data.getInt32(0); - var offsets = []; - var i; - for (i = 1; i <= nbufs; i++) { - offsets.push(data.getInt32(i * 4)); - } - // have to convert array to string for utf8.js - var bytestring = String.fromCharCode.apply(null, - new Uint8Array(buf.slice(offsets[0], offsets[1])) - ); - var msg = $.parseJSON( - utf8.decode( - bytestring - ) - ); - // the remaining chunks are stored as DataViews in msg.buffers - msg.buffers = []; - var start, stop; - for (i = 1; i < nbufs; i++) { - start = offsets[i]; - stop = offsets[i+1] || buf.byteLength; - msg.buffers.push(new DataView(buf.slice(start, stop))); - } + if (data instanceof Blob) { + // data is Blob, have to deserialize from ArrayBuffer in reader callback + var reader = new FileReader(); + reader.onload = function () { + var msg = _deserialize_array_buffer(this.result); + callback(msg); + }; + reader.readAsArrayBuffer(data); + } else { + // data is ArrayBuffer, can deserialize directly + var msg = _deserialize_array_buffer(data); callback(msg); - }; - reader.readAsArrayBuffer(blob); + } }; var deserialize = function (data, callback) { @@ -55,7 +66,63 @@ define([ } }; - return { - deserialize : deserialize + var _bytes2buf = function (bytestring) { + // convert bytestring to UInt8Array + var nbytes = bytestring.length; + var buf = new Uint8Array(nbytes); + for (var i = 0; i < nbytes; i++) { + buf[i] = bytestring.charCodeAt(i); + } + return buf; }; + + var _serialize_binary = function (msg) { + // implement the binary serialization protocol + // serializes JSON message to ArrayBuffer + msg = $.extend({}, msg); + var offsets = []; + var buffers = msg.buffers; + delete msg.buffers; + var json_utf8 = _bytes2buf(utf8.encode(JSON.stringify(msg))); + buffers.unshift(json_utf8); + var nbufs = buffers.length; + offsets.push(4 * (nbufs + 1)); + var i; + for (i = 0; i + 1 < buffers.length; i++) { + offsets.push(offsets[offsets.length-1] + buffers[i].byteLength); + } + var msg_buf = new Uint8Array( + offsets[offsets.length-1] + buffers[buffers.length-1].byteLength + ); + // use DataView.setInt32 for network byte-order + var view = new DataView(msg_buf.buffer); + // write nbufs to first 4 bytes + view.setInt32(0, nbufs); + // write offsets to next 4 * nbufs bytes + for (i = 0; i < offsets.length; i++) { + view.setInt32(4 * (i+1), offsets[i]); + } + // write all the buffers at their respective offsets + for (i = 0; i < buffers.length; i++) { + msg_buf.set(new Uint8Array(buffers[i].buffer), offsets[i]); + } + + // return raw ArrayBuffer + return msg_buf.buffer; + }; + + var serialize = function (msg) { + console.log(msg.buffers, msg.buffers.length); + if (msg.buffers && msg.buffers.length) { + return _serialize_binary(msg); + } else { + return JSON.stringify(msg); + } + }; + + var exports = { + deserialize : deserialize, + serialize: serialize + }; + return exports; }); \ No newline at end of file From b548e5155028f64ce5bfb8d90ab9336040b2a3d4 Mon Sep 17 00:00:00 2001 From: MinRK Date: Mon, 6 Oct 2014 12:03:34 -0700 Subject: [PATCH 05/11] use TextEncoding for string<->ArrayBuffer requires text-encoding js polyfill, for now --- .../html/static/services/kernels/js/kernel.js | 8 +++---- .../static/services/kernels/js/serialize.js | 23 +++---------------- IPython/html/templates/notebook.html | 1 + 3 files changed, 8 insertions(+), 24 deletions(-) diff --git a/IPython/html/static/services/kernels/js/kernel.js b/IPython/html/static/services/kernels/js/kernel.js index e01c0bd8f..9ad4fcb00 100644 --- a/IPython/html/static/services/kernels/js/kernel.js +++ b/IPython/html/static/services/kernels/js/kernel.js @@ -5,10 +5,10 @@ define([ 'base/js/namespace', 'jquery', 'base/js/utils', - 'services/kernels/js/comm', - 'widgets/js/init', - './serialize' -], function(IPython, $, utils, comm, widgetmanager, serialize) { + './comm', + './serialize', + 'widgets/js/init' +], function(IPython, $, utils, comm, serialize, widgetmanager) { "use strict"; /** diff --git a/IPython/html/static/services/kernels/js/serialize.js b/IPython/html/static/services/kernels/js/serialize.js index a43bab002..89bde066c 100644 --- a/IPython/html/static/services/kernels/js/serialize.js +++ b/IPython/html/static/services/kernels/js/serialize.js @@ -3,7 +3,6 @@ define([ 'jquery', - 'components/utf8/utf8' ], function ($, utf8) { "use strict"; @@ -16,14 +15,9 @@ define([ for (i = 1; i <= nbufs; i++) { offsets.push(data.getInt32(i * 4)); } - // have to convert array to string for utf8.js - var bytestring = String.fromCharCode.apply(null, - new Uint8Array(buf.slice(offsets[0], offsets[1])) - ); + var json_bytes = new Uint8Array(buf.slice(offsets[0], offsets[1])); var msg = $.parseJSON( - utf8.decode( - bytestring - ) + (new TextDecoder('utf8')).decode(json_bytes) ); // the remaining chunks are stored as DataViews in msg.buffers msg.buffers = []; @@ -66,16 +60,6 @@ define([ } }; - var _bytes2buf = function (bytestring) { - // convert bytestring to UInt8Array - var nbytes = bytestring.length; - var buf = new Uint8Array(nbytes); - for (var i = 0; i < nbytes; i++) { - buf[i] = bytestring.charCodeAt(i); - } - return buf; - }; - var _serialize_binary = function (msg) { // implement the binary serialization protocol // serializes JSON message to ArrayBuffer @@ -83,7 +67,7 @@ define([ var offsets = []; var buffers = msg.buffers; delete msg.buffers; - var json_utf8 = _bytes2buf(utf8.encode(JSON.stringify(msg))); + var json_utf8 = (new TextEncoder('utf8')).encode(JSON.stringify(msg)); buffers.unshift(json_utf8); var nbufs = buffers.length; offsets.push(4 * (nbufs + 1)); @@ -112,7 +96,6 @@ define([ }; var serialize = function (msg) { - console.log(msg.buffers, msg.buffers.length); if (msg.buffers && msg.buffers.length) { return _serialize_binary(msg); } else { diff --git a/IPython/html/templates/notebook.html b/IPython/html/templates/notebook.html index 86949ef73..52e592819 100644 --- a/IPython/html/templates/notebook.html +++ b/IPython/html/templates/notebook.html @@ -311,6 +311,7 @@ class="notebook_app" {% block script %} {{super()}} + From ec769b6ef891c8dd2397803fe356ed735a8630cb Mon Sep 17 00:00:00 2001 From: MinRK Date: Mon, 6 Oct 2014 12:05:17 -0700 Subject: [PATCH 06/11] install text-encoding polyfill --- setupbase.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setupbase.py b/setupbase.py index 2516e4fc2..a4d7ba5c7 100644 --- a/setupbase.py +++ b/setupbase.py @@ -161,7 +161,8 @@ def find_package_data(): pjoin(components, "requirejs", "require.js"), pjoin(components, "underscore", "underscore-min.js"), pjoin(components, "moment", "moment.js"), - pjoin(components, "moment", "min","moment.min.js"), + pjoin(components, "moment", "min", "moment.min.js"), + pjoin(components, "text-encoding", "lib", "encoding.js"), ]) # Ship all of Codemirror's CSS and JS From 3de7e9bd74de9bc35a83a139380704c009e9b20c Mon Sep 17 00:00:00 2001 From: MinRK Date: Mon, 6 Oct 2014 14:57:35 -0700 Subject: [PATCH 07/11] test websocket-friendly binary message roundtrip Python-side --- IPython/html/base/zmqhandlers.py | 20 ++++++++------------ IPython/html/tests/test_serialize.py | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 IPython/html/tests/test_serialize.py diff --git a/IPython/html/base/zmqhandlers.py b/IPython/html/base/zmqhandlers.py index 99b707717..0287502fc 100644 --- a/IPython/html/base/zmqhandlers.py +++ b/IPython/html/base/zmqhandlers.py @@ -23,7 +23,7 @@ from tornado import web from tornado import websocket from IPython.kernel.zmq.session import Session -from IPython.utils.jsonutil import date_default +from IPython.utils.jsonutil import date_default, extract_dates from IPython.utils.py3compat import PY3, cast_unicode from .handlers import IPythonHandler @@ -45,7 +45,9 @@ def serialize_binary_message(msg): The message serialized to bytes. """ - buffers = msg.pop('buffers') + # don't modify msg or buffer list in-place + msg = msg.copy() + buffers = list(msg.pop('buffers')) bmsg = json.dumps(msg, default=date_default).encode('utf8') buffers.insert(0, bmsg) nbufs = len(buffers) @@ -72,13 +74,15 @@ def deserialize_binary_message(bmsg): message dictionary """ - nbufs = struct.unpack('i', bmsg[:4])[0] + nbufs = struct.unpack('!i', bmsg[:4])[0] offsets = list(struct.unpack('!' + 'i' * nbufs, bmsg[4:4*(nbufs+1)])) offsets.append(None) bufs = [] for start, stop in zip(offsets[:-1], offsets[1:]): bufs.append(bmsg[start:stop]) - msg = json.loads(bufs[0]) + msg = json.loads(bufs[0].decode('utf8')) + msg['header'] = extract_dates(msg['header']) + msg['parent_header'] = extract_dates(msg['parent_header']) msg['buffers'] = bufs[1:] return msg @@ -139,14 +143,6 @@ class ZMQStreamHandler(websocket.WebSocketHandler): """ idents, msg_list = self.session.feed_identities(msg_list) msg = self.session.deserialize(msg_list) - try: - msg['header'].pop('date') - except KeyError: - pass - try: - msg['parent_header'].pop('date') - except KeyError: - pass if msg['buffers']: buf = serialize_binary_message(msg) return buf diff --git a/IPython/html/tests/test_serialize.py b/IPython/html/tests/test_serialize.py new file mode 100644 index 000000000..7a88b29f4 --- /dev/null +++ b/IPython/html/tests/test_serialize.py @@ -0,0 +1,26 @@ +"""Test serialize/deserialize messages with buffers""" + +import os + +import nose.tools as nt + +from IPython.kernel.zmq.session import Session +from ..base.zmqhandlers import ( + serialize_binary_message, + deserialize_binary_message, +) + +def test_serialize_binary(): + s = Session() + msg = s.msg('data_pub', content={'a': 'b'}) + msg['buffers'] = [ os.urandom(3) for i in range(3) ] + bmsg = serialize_binary_message(msg) + nt.assert_is_instance(bmsg, bytes) + +def test_deserialize_binary(): + s = Session() + msg = s.msg('data_pub', content={'a': 'b'}) + msg['buffers'] = [ os.urandom(2) for i in range(3) ] + bmsg = serialize_binary_message(msg) + msg2 = deserialize_binary_message(bmsg) + nt.assert_equal(msg2, msg) From 11f57751038f8e2741139bd115791628a4c99e76 Mon Sep 17 00:00:00 2001 From: MinRK Date: Mon, 6 Oct 2014 17:35:15 -0700 Subject: [PATCH 08/11] test binary websocket messages only runs on slimerjs for now --- .../static/services/kernels/js/serialize.js | 5 +- IPython/html/tests/notebook/binary_msg.js | 113 ++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 IPython/html/tests/notebook/binary_msg.js diff --git a/IPython/html/static/services/kernels/js/serialize.js b/IPython/html/static/services/kernels/js/serialize.js index 89bde066c..4f0d48c56 100644 --- a/IPython/html/static/services/kernels/js/serialize.js +++ b/IPython/html/static/services/kernels/js/serialize.js @@ -65,7 +65,10 @@ define([ // serializes JSON message to ArrayBuffer msg = $.extend({}, msg); var offsets = []; - var buffers = msg.buffers; + var buffers = []; + $.map(msg.buffers, function (buf) { + buffers.push(buf); + }); delete msg.buffers; var json_utf8 = (new TextEncoder('utf8')).encode(JSON.stringify(msg)); buffers.unshift(json_utf8); diff --git a/IPython/html/tests/notebook/binary_msg.js b/IPython/html/tests/notebook/binary_msg.js new file mode 100644 index 000000000..fe61bf11f --- /dev/null +++ b/IPython/html/tests/notebook/binary_msg.js @@ -0,0 +1,113 @@ +// +// Test binary messages on websockets. +// Only works on slimer for now, due to old websocket impl in phantomjs. +// + +casper.notebook_test(function () { + if (!this.slimerjs) { + console.log("Can't test binary websockets on phantomjs."); + return; + } + // create EchoBuffers target on js-side. + // it just captures and echos comm messages. + this.then(function () { + var success = this.evaluate(function () { + IPython._msgs = []; + + var EchoBuffers = function(comm) { + this.comm = comm; + this.comm.on_msg($.proxy(this.on_msg, this)); + }; + + EchoBuffers.prototype.on_msg = function (msg) { + IPython._msgs.push(msg); + this.comm.send(msg.content.data, {}, {}, msg.buffers); + }; + + IPython.notebook.kernel.comm_manager.register_target("echo", function (comm) { + return new EchoBuffers(comm); + }); + + return true; + }); + this.test.assertEquals(success, true, "Created echo comm target"); + }); + + // Create a similar comm that captures messages Python-side + this.then(function () { + var index = this.append_cell([ + "import os", + "from IPython.kernel.comm import Comm", + "comm = Comm(target_name='echo')", + "msgs = []", + "def on_msg(msg):", + " msgs.append(msg)", + "comm.on_msg(on_msg)" + ].join('\n'), 'code'); + this.execute_cell(index); + }); + + // send a message with binary data + this.then(function () { + var index = this.append_cell([ + "buffers = [b'\\xFF\\x00', b'\\x00\\x01\\x02']", + "comm.send(data='hi', buffers=buffers)" + ].join('\n'), 'code'); + this.execute_cell(index); + }); + + // wait for capture + this.waitFor(function () { + return this.evaluate(function () { + return IPython._msgs.length > 0; + }); + }); + + // validate captured buffers js-side + this.then(function () { + var msgs = this.evaluate(function () { + return IPython._msgs; + }); + this.test.assertEquals(msgs.length, 1, "Captured comm message"); + var buffers = msgs[0].buffers; + this.test.assertEquals(buffers.length, 2, "comm message has buffers"); + + // extract attributes to test in evaluate, + // because the raw DataViews can't be passed across + var buf_info = function (index) { + var buf = IPython._msgs[0].buffers[index]; + var data = {}; + data.byteLength = buf.byteLength; + data.bytes = []; + for (var i = 0; i < data.byteLength; i++) { + data.bytes.push(buf.getUint8(i)); + } + return data; + }; + + buf0 = this.evaluate(buf_info, 0); + buf1 = this.evaluate(buf_info, 1); + this.test.assertEquals(buf0.byteLength, 2, 'buf[0] has correct size'); + this.test.assertEquals(buf0.bytes, [255, 0], 'buf[0] has correct bytes'); + this.test.assertEquals(buf1.byteLength, 3, 'buf[1] has correct size'); + this.test.assertEquals(buf1.bytes, [0, 1, 2], 'buf[1] has correct bytes'); + }); + + // validate captured buffers Python-side + this.then(function () { + var index = this.append_cell([ + "assert len(msgs) == 1, len(msgs)", + "bufs = msgs[0]['buffers']", + "assert len(bufs) == len(buffers), bufs", + "assert bufs[0].bytes == buffers[0], bufs[0].bytes", + "assert bufs[1].bytes == buffers[1], bufs[1].bytes", + "1", + ].join('\n'), 'code'); + this.execute_cell(index); + this.wait_for_output(index); + this.then(function () { + var out = this.get_output_cell(index); + this.test.assertEquals(out['text/plain'], '1', "Python received buffers"); + }); + }); +}); From 8c743d304fa0318612e10aa2ee49e7127167d1d8 Mon Sep 17 00:00:00 2001 From: MinRK Date: Fri, 10 Oct 2014 11:44:22 -0700 Subject: [PATCH 09/11] unsigned ints for offsets because signed ints for sizes is icky --- IPython/html/base/zmqhandlers.py | 4 ++-- IPython/html/static/services/kernels/js/serialize.js | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/IPython/html/base/zmqhandlers.py b/IPython/html/base/zmqhandlers.py index 0287502fc..d0b77a1f4 100644 --- a/IPython/html/base/zmqhandlers.py +++ b/IPython/html/base/zmqhandlers.py @@ -54,7 +54,7 @@ def serialize_binary_message(msg): offsets = [4 * (nbufs + 1)] for buf in buffers[:-1]: offsets.append(offsets[-1] + len(buf)) - offsets_buf = struct.pack('!' + 'i' * (nbufs + 1), nbufs, *offsets) + offsets_buf = struct.pack('!' + 'I' * (nbufs + 1), nbufs, *offsets) buffers.insert(0, offsets_buf) return b''.join(buffers) @@ -75,7 +75,7 @@ def deserialize_binary_message(bmsg): message dictionary """ nbufs = struct.unpack('!i', bmsg[:4])[0] - offsets = list(struct.unpack('!' + 'i' * nbufs, bmsg[4:4*(nbufs+1)])) + offsets = list(struct.unpack('!' + 'I' * nbufs, bmsg[4:4*(nbufs+1)])) offsets.append(None) bufs = [] for start, stop in zip(offsets[:-1], offsets[1:]): diff --git a/IPython/html/static/services/kernels/js/serialize.js b/IPython/html/static/services/kernels/js/serialize.js index 4f0d48c56..7ba0fe072 100644 --- a/IPython/html/static/services/kernels/js/serialize.js +++ b/IPython/html/static/services/kernels/js/serialize.js @@ -9,11 +9,11 @@ define([ var _deserialize_array_buffer = function (buf) { var data = new DataView(buf); // read the header: 1 + nbufs 32b integers - var nbufs = data.getInt32(0); + var nbufs = data.getUint32(0); var offsets = []; var i; for (i = 1; i <= nbufs; i++) { - offsets.push(data.getInt32(i * 4)); + offsets.push(data.getUint32(i * 4)); } var json_bytes = new Uint8Array(buf.slice(offsets[0], offsets[1])); var msg = $.parseJSON( @@ -81,13 +81,13 @@ define([ var msg_buf = new Uint8Array( offsets[offsets.length-1] + buffers[buffers.length-1].byteLength ); - // use DataView.setInt32 for network byte-order + // use DataView.setUint32 for network byte-order var view = new DataView(msg_buf.buffer); // write nbufs to first 4 bytes - view.setInt32(0, nbufs); + view.setUint32(0, nbufs); // write offsets to next 4 * nbufs bytes for (i = 0; i < offsets.length; i++) { - view.setInt32(4 * (i+1), offsets[i]); + view.setUint32(4 * (i+1), offsets[i]); } // write all the buffers at their respective offsets for (i = 0; i < buffers.length; i++) { From 9e425bf94b94710620054dc2c4245caa19b370cc Mon Sep 17 00:00:00 2001 From: MinRK Date: Fri, 10 Oct 2014 11:45:22 -0700 Subject: [PATCH 10/11] move binary msg test to services/serialize --- .../html/tests/{notebook/binary_msg.js => services/serialize.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename IPython/html/tests/{notebook/binary_msg.js => services/serialize.js} (100%) diff --git a/IPython/html/tests/notebook/binary_msg.js b/IPython/html/tests/services/serialize.js similarity index 100% rename from IPython/html/tests/notebook/binary_msg.js rename to IPython/html/tests/services/serialize.js From 7fd161599b35da0e6b012887a7bcd6fc82a51f4a Mon Sep 17 00:00:00 2001 From: MinRK Date: Sat, 18 Oct 2014 19:04:57 -0700 Subject: [PATCH 11/11] cleanup serialize per review --- IPython/html/static/services/kernels/js/serialize.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/IPython/html/static/services/kernels/js/serialize.js b/IPython/html/static/services/kernels/js/serialize.js index 7ba0fe072..c86e3662f 100644 --- a/IPython/html/static/services/kernels/js/serialize.js +++ b/IPython/html/static/services/kernels/js/serialize.js @@ -2,8 +2,8 @@ // Distributed under the terms of the Modified BSD License. define([ - 'jquery', - ], function ($, utf8) { + 'underscore', + ], function (_) { "use strict"; var _deserialize_array_buffer = function (buf) { @@ -16,7 +16,7 @@ define([ offsets.push(data.getUint32(i * 4)); } var json_bytes = new Uint8Array(buf.slice(offsets[0], offsets[1])); - var msg = $.parseJSON( + var msg = JSON.parse( (new TextDecoder('utf8')).decode(json_bytes) ); // the remaining chunks are stored as DataViews in msg.buffers @@ -53,7 +53,7 @@ define([ // deserialize a message and pass the unpacked message object to callback if (typeof data === "string") { // text JSON message - callback($.parseJSON(data)); + callback(JSON.parse(data)); } else { // binary message _deserialize_binary(data, callback); @@ -63,10 +63,10 @@ define([ var _serialize_binary = function (msg) { // implement the binary serialization protocol // serializes JSON message to ArrayBuffer - msg = $.extend({}, msg); + msg = _.clone(msg); var offsets = []; var buffers = []; - $.map(msg.buffers, function (buf) { + msg.buffers.map(function (buf) { buffers.push(buf); }); delete msg.buffers;