##// END OF EJS Templates
optimization of WidgetModel.apply_update
sylvain.corlay -
Show More
@@ -1,494 +1,496 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define(["widgets/js/manager",
5 5 "underscore",
6 6 "backbone",
7 7 "jquery",
8 8 "base/js/namespace",
9 9 ], function(widgetmanager, _, Backbone, $, IPython){
10 10
11 11 var WidgetModel = Backbone.Model.extend({
12 12 constructor: function (widget_manager, model_id, comm) {
13 13 // Constructor
14 14 //
15 15 // Creates a WidgetModel instance.
16 16 //
17 17 // Parameters
18 18 // ----------
19 19 // widget_manager : WidgetManager instance
20 20 // model_id : string
21 21 // An ID unique to this model.
22 22 // comm : Comm instance (optional)
23 23 this.widget_manager = widget_manager;
24 24 this._buffered_state_diff = {};
25 25 this.pending_msgs = 0;
26 26 this.msg_buffer = null;
27 this.key_value_lock = null;
27 this.state_lock = null;
28 28 this.id = model_id;
29 29 this.views = [];
30 30
31 31 if (comm !== undefined) {
32 32 // Remember comm associated with the model.
33 33 this.comm = comm;
34 34 comm.model = this;
35 35
36 36 // Hook comm messages up to model.
37 37 comm.on_close($.proxy(this._handle_comm_closed, this));
38 38 comm.on_msg($.proxy(this._handle_comm_msg, this));
39 39 }
40 40 return Backbone.Model.apply(this);
41 41 },
42 42
43 43 send: function (content, callbacks) {
44 44 // Send a custom msg over the comm.
45 45 if (this.comm !== undefined) {
46 46 var data = {method: 'custom', content: content};
47 47 this.comm.send(data, callbacks);
48 48 this.pending_msgs++;
49 49 }
50 50 },
51 51
52 52 _handle_comm_closed: function (msg) {
53 53 // Handle when a widget is closed.
54 54 this.trigger('comm:close');
55 55 delete this.comm.model; // Delete ref so GC will collect widget model.
56 56 delete this.comm;
57 57 delete this.model_id; // Delete id from model so widget manager cleans up.
58 58 _.each(this.views, function(view, i) {
59 59 view.remove();
60 60 });
61 61 },
62 62
63 63 _handle_comm_msg: function (msg) {
64 64 // Handle incoming comm msg.
65 65 var method = msg.content.data.method;
66 66 switch (method) {
67 67 case 'update':
68 68 this.apply_update(msg.content.data.state);
69 69 break;
70 70 case 'custom':
71 71 this.trigger('msg:custom', msg.content.data.content);
72 72 break;
73 73 case 'display':
74 74 this.widget_manager.display_view(msg, this);
75 75 break;
76 76 }
77 77 },
78 78
79 79 apply_update: function (state) {
80 80 // Handle when a widget is updated via the python side.
81 var that = this;
82 _.each(state, function(value, key) {
83 that.key_value_lock = [key, value];
81 this.state_lock = state;
84 82 try {
85 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
83 var that = this;
84 WidgetModel.__super__.set.apply(this, [Object.keys(state).reduce(function(obj, key) {
85 obj[key] = that._unpack_models(state[key]);
86 return obj;
87 }, {})]);
86 88 } finally {
87 that.key_value_lock = null;
89 this.state_lock = null;
88 90 }
89 });
90 91 },
91 92
92 93 _handle_status: function (msg, callbacks) {
93 94 // Handle status msgs.
94 95
95 96 // execution_state : ('busy', 'idle', 'starting')
96 97 if (this.comm !== undefined) {
97 98 if (msg.content.execution_state ==='idle') {
98 99 // Send buffer if this message caused another message to be
99 100 // throttled.
100 101 if (this.msg_buffer !== null &&
101 102 (this.get('msg_throttle') || 3) === this.pending_msgs) {
102 103 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
103 104 this.comm.send(data, callbacks);
104 105 this.msg_buffer = null;
105 106 } else {
106 107 --this.pending_msgs;
107 108 }
108 109 }
109 110 }
110 111 },
111 112
112 113 callbacks: function(view) {
113 114 // Create msg callbacks for a comm msg.
114 115 var callbacks = this.widget_manager.callbacks(view);
115 116
116 117 if (callbacks.iopub === undefined) {
117 118 callbacks.iopub = {};
118 119 }
119 120
120 121 var that = this;
121 122 callbacks.iopub.status = function (msg) {
122 123 that._handle_status(msg, callbacks);
123 124 };
124 125 return callbacks;
125 126 },
126 127
127 128 set: function(key, val, options) {
128 129 // Set a value.
129 130 var return_value = WidgetModel.__super__.set.apply(this, arguments);
130 131
131 132 // Backbone only remembers the diff of the most recent set()
132 133 // operation. Calling set multiple times in a row results in a
133 134 // loss of diff information. Here we keep our own running diff.
134 135 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
135 136 return return_value;
136 137 },
137 138
138 139 sync: function (method, model, options) {
139 140 // Handle sync to the back-end. Called when a model.save() is called.
140 141
141 142 // Make sure a comm exists.
142 143 var error = options.error || function() {
143 144 console.error('Backbone sync error:', arguments);
144 145 };
145 146 if (this.comm === undefined) {
146 147 error();
147 148 return false;
148 149 }
149 150
150 151 // Delete any key value pairs that the back-end already knows about.
151 152 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
152 if (this.key_value_lock !== null) {
153 var key = this.key_value_lock[0];
154 var value = this.key_value_lock[1];
155 if (attrs[key] === value) {
153 if (this.state_lock !== null) {
154 var keys = Object.keys(this.state_lock);
155 for (var i=0; i<keys.length; i++)
156 var key = keys[i];
157 if (attrs[key] === this.state_lock[key]) {
156 158 delete attrs[key];
157 159 }
158 160 }
159 161
160 162 // Only sync if there are attributes to send to the back-end.
161 163 attrs = this._pack_models(attrs);
162 164 if (_.size(attrs) > 0) {
163 165
164 166 // If this message was sent via backbone itself, it will not
165 167 // have any callbacks. It's important that we create callbacks
166 168 // so we can listen for status messages, etc...
167 169 var callbacks = options.callbacks || this.callbacks();
168 170
169 171 // Check throttle.
170 172 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
171 173 // The throttle has been exceeded, buffer the current msg so
172 174 // it can be sent once the kernel has finished processing
173 175 // some of the existing messages.
174 176
175 177 // Combine updates if it is a 'patch' sync, otherwise replace updates
176 178 switch (method) {
177 179 case 'patch':
178 180 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
179 181 break;
180 182 case 'update':
181 183 case 'create':
182 184 this.msg_buffer = attrs;
183 185 break;
184 186 default:
185 187 error();
186 188 return false;
187 189 }
188 190 this.msg_buffer_callbacks = callbacks;
189 191
190 192 } else {
191 193 // We haven't exceeded the throttle, send the message like
192 194 // normal.
193 195 var data = {method: 'backbone', sync_data: attrs};
194 196 this.comm.send(data, callbacks);
195 197 this.pending_msgs++;
196 198 }
197 199 }
198 200 // Since the comm is a one-way communication, assume the message
199 201 // arrived. Don't call success since we don't have a model back from the server
200 202 // this means we miss out on the 'sync' event.
201 203 this._buffered_state_diff = {};
202 204 },
203 205
204 206 save_changes: function(callbacks) {
205 207 // Push this model's state to the back-end
206 208 //
207 209 // This invokes a Backbone.Sync.
208 210 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
209 211 },
210 212
211 213 _pack_models: function(value) {
212 214 // Replace models with model ids recursively.
213 215 var that = this;
214 216 var packed;
215 217 if (value instanceof Backbone.Model) {
216 218 return "IPY_MODEL_" + value.id;
217 219
218 220 } else if ($.isArray(value)) {
219 221 packed = [];
220 222 _.each(value, function(sub_value, key) {
221 223 packed.push(that._pack_models(sub_value));
222 224 });
223 225 return packed;
224 226
225 227 } else if (value instanceof Object) {
226 228 packed = {};
227 229 _.each(value, function(sub_value, key) {
228 230 packed[key] = that._pack_models(sub_value);
229 231 });
230 232 return packed;
231 233
232 234 } else {
233 235 return value;
234 236 }
235 237 },
236 238
237 239 _unpack_models: function(value) {
238 240 // Replace model ids with models recursively.
239 241 var that = this;
240 242 var unpacked;
241 243 if ($.isArray(value)) {
242 244 unpacked = [];
243 245 _.each(value, function(sub_value, key) {
244 246 unpacked.push(that._unpack_models(sub_value));
245 247 });
246 248 return unpacked;
247 249
248 250 } else if (value instanceof Object) {
249 251 unpacked = {};
250 252 _.each(value, function(sub_value, key) {
251 253 unpacked[key] = that._unpack_models(sub_value);
252 254 });
253 255 return unpacked;
254 256
255 257 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
256 258 var model = this.widget_manager.get_model(value.slice(10, value.length));
257 259 if (model) {
258 260 return model;
259 261 } else {
260 262 return value;
261 263 }
262 264 } else {
263 265 return value;
264 266 }
265 267 },
266 268
267 269 on_bulk_change: function(keys, callback, context) {
268 270 this.on('change', function() {
269 271 if (keys.some(this.hasChanged, this)) {
270 272 callback.apply(context);
271 273 }
272 274 }, this);
273 275
274 276 },
275 277 });
276 278 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
277 279
278 280
279 281 var WidgetView = Backbone.View.extend({
280 282 initialize: function(parameters) {
281 283 // Public constructor.
282 284 this.model.on('change',this.update,this);
283 285 this.options = parameters.options;
284 286 this.child_model_views = {};
285 287 this.child_views = {};
286 288 this.model.views.push(this);
287 289 this.id = this.id || IPython.utils.uuid();
288 290 this.on('displayed', function() {
289 291 this.is_displayed = true;
290 292 }, this);
291 293 },
292 294
293 295 update: function(){
294 296 // Triggered on model change.
295 297 //
296 298 // Update view to be consistent with this.model
297 299 },
298 300
299 301 create_child_view: function(child_model, options) {
300 302 // Create and return a child view.
301 303 //
302 304 // -given a model and (optionally) a view name if the view name is
303 305 // not given, it defaults to the model's default view attribute.
304 306
305 307 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
306 308 // it would be great to have the widget manager add the cell metadata
307 309 // to the subview without having to add it here.
308 310 options = $.extend({ parent: this }, options || {});
309 311 var child_view = this.model.widget_manager.create_view(child_model, options, this);
310 312
311 313 // Associate the view id with the model id.
312 314 if (this.child_model_views[child_model.id] === undefined) {
313 315 this.child_model_views[child_model.id] = [];
314 316 }
315 317 this.child_model_views[child_model.id].push(child_view.id);
316 318
317 319 // Remember the view by id.
318 320 this.child_views[child_view.id] = child_view;
319 321 return child_view;
320 322 },
321 323
322 324 pop_child_view: function(child_model) {
323 325 // Delete a child view that was previously created using create_child_view.
324 326 var view_ids = this.child_model_views[child_model.id];
325 327 if (view_ids !== undefined) {
326 328
327 329 // Only delete the first view in the list.
328 330 var view_id = view_ids[0];
329 331 var view = this.child_views[view_id];
330 332 delete this.child_views[view_id];
331 333 view_ids.splice(0,1);
332 334 child_model.views.pop(view);
333 335
334 336 // Remove the view list specific to this model if it is empty.
335 337 if (view_ids.length === 0) {
336 338 delete this.child_model_views[child_model.id];
337 339 }
338 340 return view;
339 341 }
340 342 return null;
341 343 },
342 344
343 345 do_diff: function(old_list, new_list, removed_callback, added_callback) {
344 346 // Difference a changed list and call remove and add callbacks for
345 347 // each removed and added item in the new list.
346 348 //
347 349 // Parameters
348 350 // ----------
349 351 // old_list : array
350 352 // new_list : array
351 353 // removed_callback : Callback(item)
352 354 // Callback that is called for each item removed.
353 355 // added_callback : Callback(item)
354 356 // Callback that is called for each item added.
355 357
356 358 // Walk the lists until an unequal entry is found.
357 359 var i;
358 360 for (i = 0; i < new_list.length; i++) {
359 361 if (i >= old_list.length || new_list[i] !== old_list[i]) {
360 362 break;
361 363 }
362 364 }
363 365
364 366 // Remove the non-matching items from the old list.
365 367 for (var j = i; j < old_list.length; j++) {
366 368 removed_callback(old_list[j]);
367 369 }
368 370
369 371 // Add the rest of the new list items.
370 372 for (; i < new_list.length; i++) {
371 373 added_callback(new_list[i]);
372 374 }
373 375 },
374 376
375 377 callbacks: function(){
376 378 // Create msg callbacks for a comm msg.
377 379 return this.model.callbacks(this);
378 380 },
379 381
380 382 render: function(){
381 383 // Render the view.
382 384 //
383 385 // By default, this is only called the first time the view is created
384 386 },
385 387
386 388 show: function(){
387 389 // Show the widget-area
388 390 if (this.options && this.options.cell &&
389 391 this.options.cell.widget_area !== undefined) {
390 392 this.options.cell.widget_area.show();
391 393 }
392 394 },
393 395
394 396 send: function (content) {
395 397 // Send a custom msg associated with this view.
396 398 this.model.send(content, this.callbacks());
397 399 },
398 400
399 401 touch: function () {
400 402 this.model.save_changes(this.callbacks());
401 403 },
402 404
403 405 after_displayed: function (callback, context) {
404 406 // Calls the callback right away is the view is already displayed
405 407 // otherwise, register the callback to the 'displayed' event.
406 408 if (this.is_displayed) {
407 409 callback.apply(context);
408 410 } else {
409 411 this.on('displayed', callback, context);
410 412 }
411 413 },
412 414 });
413 415
414 416
415 417 var DOMWidgetView = WidgetView.extend({
416 418 initialize: function (parameters) {
417 419 // Public constructor
418 420 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
419 421 this.on('displayed', this.show, this);
420 422 this.after_displayed(function() {
421 423 this.update_visible(this.model, this.model.get("visible"));
422 424 this.update_css(this.model, this.model.get("_css"));
423 425 }, this);
424 426 this.model.on('msg:custom', this.on_msg, this);
425 427 this.model.on('change:visible', this.update_visible, this);
426 428 this.model.on('change:_css', this.update_css, this);
427 429 },
428 430
429 431 on_msg: function(msg) {
430 432 // Handle DOM specific msgs.
431 433 switch(msg.msg_type) {
432 434 case 'add_class':
433 435 this.add_class(msg.selector, msg.class_list);
434 436 break;
435 437 case 'remove_class':
436 438 this.remove_class(msg.selector, msg.class_list);
437 439 break;
438 440 }
439 441 },
440 442
441 443 add_class: function (selector, class_list) {
442 444 // Add a DOM class to an element.
443 445 this._get_selector_element(selector).addClass(class_list);
444 446 },
445 447
446 448 remove_class: function (selector, class_list) {
447 449 // Remove a DOM class from an element.
448 450 this._get_selector_element(selector).removeClass(class_list);
449 451 },
450 452
451 453 update_visible: function(model, value) {
452 454 // Update visibility
453 455 this.$el.toggle(value);
454 456 },
455 457
456 458 update_css: function (model, css) {
457 459 // Update the css styling of this view.
458 460 var e = this.$el;
459 461 if (css === undefined) {return;}
460 462 for (var i = 0; i < css.length; i++) {
461 463 // Apply the css traits to all elements that match the selector.
462 464 var selector = css[i][0];
463 465 var elements = this._get_selector_element(selector);
464 466 if (elements.length > 0) {
465 467 var trait_key = css[i][1];
466 468 var trait_value = css[i][2];
467 469 elements.css(trait_key ,trait_value);
468 470 }
469 471 }
470 472 },
471 473
472 474 _get_selector_element: function (selector) {
473 475 // Get the elements via the css selector.
474 476 var elements;
475 477 if (!selector) {
476 478 elements = this.$el;
477 479 } else {
478 480 elements = this.$el.find(selector).addBack(selector);
479 481 }
480 482 return elements;
481 483 },
482 484 });
483 485
484 486 var widget = {
485 487 'WidgetModel': WidgetModel,
486 488 'WidgetView': WidgetView,
487 489 'DOMWidgetView': DOMWidgetView,
488 490 };
489 491
490 492 // For backwards compatability.
491 493 $.extend(IPython, widget);
492 494
493 495 return widget;
494 496 });
General Comments 0
You need to be logged in to leave comments. Login now