Merge lp:~openerp-dev/openerp-web/trunk-readgroup-ged into lp:openerp-web

Proposed by Géry Debongnie
Status: Merged
Merged at revision: 3967
Proposed branch: lp:~openerp-dev/openerp-web/trunk-readgroup-ged
Merge into: lp:openerp-web
Diff against target: 597 lines (+226/-239)
2 files modified
addons/web/static/src/js/data.js (+31/-8)
addons/web_graph/static/src/js/pivot_table.js (+195/-231)
To merge this branch: bzr merge lp:~openerp-dev/openerp-web/trunk-readgroup-ged
Reviewer Review Type Date Requested Status
Xavier (Open ERP) Pending
Review via email: mp+214498@code.launchpad.net

Description of the change

Graph view performance optimizations:

this work completely changes the way data is loaded into graph views. It makes use of the new 'eager' functionality of read_group in such a way that it now requires a fixed number of read_group requests (or, more precisely, a number of requests only depending on the dimensions of the pivot table and not depending on the number of groups in the db).

To post a comment you must log in.
3964. By Géry Debongnie

[MERGE] merge from trunk

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'addons/web/static/src/js/data.js'
2--- addons/web/static/src/js/data.js 2014-04-02 08:53:59 +0000
3+++ addons/web/static/src/js/data.js 2014-04-08 09:09:18 +0000
4@@ -27,6 +27,7 @@
5 this._fields = fields;
6 this._filter = [];
7 this._context = {};
8+ this._lazy = true;
9 this._limit = false;
10 this._offset = 0;
11 this._order_by = [];
12@@ -36,6 +37,7 @@
13 var q = new instance.web.Query(this._model, this._fields);
14 q._context = this._context;
15 q._filter = this._filter;
16+ q._lazy = this._lazy;
17 q._limit = this._limit;
18 q._offset = this._offset;
19 q._order_by = this._order_by;
20@@ -51,6 +53,7 @@
21 q._context = new instance.web.CompoundContext(
22 q._context, to_set.context);
23 break;
24+ case 'lazy':
25 case 'limit':
26 case 'offset':
27 case 'order_by':
28@@ -140,6 +143,7 @@
29 domain: this._model.domain(this._filter),
30 context: ctx,
31 offset: this._offset,
32+ lazy: this._lazy,
33 limit: this._limit,
34 orderby: instance.web.serialize_sort(this._order_by) || false
35 }).then(function (results) {
36@@ -148,8 +152,9 @@
37 result.__context = result.__context || {};
38 result.__context.group_by = result.__context.group_by || [];
39 _.defaults(result.__context, ctx);
40+ var grouping_fields = self._lazy ? [grouping[0]] : grouping;
41 return new instance.web.QueryGroup(
42- self._model.name, grouping[0], result);
43+ self._model.name, grouping_fields, result);
44 });
45 });
46 },
47@@ -176,6 +181,18 @@
48 return this.clone({filter: domain});
49 },
50 /**
51+ * Creates a new query with the provided parameter lazy replacing the current
52+ * query's own.
53+ *
54+ * @param {Boolean} lazy indicates if the read_group should return only the
55+ * first level of groupby records, or should return the records grouped by
56+ * all levels at once (so, it makes only 1 db request).
57+ * @returns {openerp.web.Query}
58+ */
59+ lazy: function (lazy) {
60+ return this.clone({lazy: lazy});
61+ },
62+ /**
63 * Creates a new query with the provided limit replacing the current
64 * query's own limit
65 *
66@@ -213,7 +230,7 @@
67 });
68
69 instance.web.QueryGroup = instance.web.Class.extend({
70- init: function (model, grouping_field, read_group_group) {
71+ init: function (model, grouping_fields, read_group_group) {
72 // In cases where group_by_no_leaf and no group_by, the result of
73 // read_group has aggregate fields but no __context or __domain.
74 // Create default (empty) values for those so that things don't break
75@@ -221,12 +238,12 @@
76 {__context: {group_by: []}, __domain: []},
77 read_group_group);
78
79- var raw_field = grouping_field && grouping_field.split(':')[0];
80+ var count_key = (grouping_fields[0] && grouping_fields[0].split(':')[0]) + '_count';
81 var aggregates = {};
82 _(fixed_group).each(function (value, key) {
83 if (key.indexOf('__') === 0
84- || key === raw_field
85- || key === raw_field + '_count') {
86+ || _.contains(grouping_fields, key)
87+ || (key === count_key)) {
88 return;
89 }
90 aggregates[key] = value || 0;
91@@ -235,15 +252,21 @@
92 this.model = new instance.web.Model(
93 model, fixed_group.__context, fixed_group.__domain);
94
95- var group_size = fixed_group[raw_field + '_count'] || fixed_group.__count || 0;
96+ var group_size = fixed_group[count_key] || fixed_group.__count || 0;
97 var leaf_group = fixed_group.__context.group_by.length === 0;
98
99+ var value = (grouping_fields.length === 1)
100+ ? fixed_group[grouping_fields[0]]
101+ : _.map(grouping_fields, function (field) { return fixed_group[field]; });
102+ var grouped_on = (grouping_fields.length === 1)
103+ ? grouping_fields[0]
104+ : grouping_fields;
105 this.attributes = {
106 folded: !!(fixed_group.__fold),
107- grouped_on: grouping_field,
108+ grouped_on: grouped_on,
109 // if terminal group (or no group) and group_by_no_leaf => use group.__count
110 length: group_size,
111- value: fixed_group[raw_field],
112+ value: value,
113 // A group is open-able if it's not a leaf in group_by_no_leaf mode
114 has_children: !(leaf_group && fixed_group.__context['group_by_no_leaf']),
115
116
117=== modified file 'addons/web_graph/static/src/js/pivot_table.js'
118--- addons/web_graph/static/src/js/pivot_table.js 2014-02-03 11:33:32 +0000
119+++ addons/web_graph/static/src/js/pivot_table.js 2014-04-08 09:09:18 +0000
120@@ -82,7 +82,9 @@
121
122 get_values: function (id1, id2, default_values) {
123 var cell = _.findWhere(this.cells, {x: Math.min(id1, id2), y: Math.max(id1, id2)});
124- return (cell !== undefined) ? cell.values : (default_values || new Array(this.measures.length));
125+ return (cell !== undefined) ?
126+ cell.values :
127+ (default_values || new Array(this.measures.length));
128 },
129
130 // ----------------------------------------------------------------------
131@@ -144,12 +146,16 @@
132 get_ancestors: function (header) {
133 var self = this;
134 if (!header.children) return [];
135- return [].concat.apply([], _.map(header.children, function (c) {return self.get_ancestors_and_self(c); }));
136+ return [].concat.apply([], _.map(header.children, function (c) {
137+ return self.get_ancestors_and_self(c);
138+ }));
139 },
140
141 get_ancestors_and_self: function (header) {
142 var self = this;
143- return [].concat.apply([header], _.map(header.children, function (c) { return self.get_ancestors_and_self(c); }));
144+ return [].concat.apply([header], _.map(header.children, function (c) {
145+ return self.get_ancestors_and_self(c);
146+ }));
147 },
148
149 get_total: function (header) {
150@@ -205,54 +211,27 @@
151 expand: function (header_id, groupby) {
152 var self = this,
153 header = this.get_header(header_id),
154- otherRoot = this.get_other_root(header),
155- fields = otherRoot.groupby.concat(this.measures);
156+ other_root = this.get_other_root(header),
157+ this_gb = [groupby.field],
158+ other_gbs = _.pluck(other_root.groupby, 'field');
159
160 if (header.path.length === header.root.groupby.length) {
161 header.root.groupby.push(groupby);
162 }
163- groupby = [groupby].concat(otherRoot.groupby);
164-
165- return this.get_groups(groupby, fields, header.domain).then(function (groups) {
166- _.each(groups.reverse(), function (group) {
167- // make header
168- var child = self.make_header(group, header);
169+ return this.perform_requests(this_gb, other_gbs, header.domain).then(function () {
170+ var data = Array.prototype.slice.call(arguments).slice(other_gbs.length + 1);
171+ _.each(data, function (data_pt) {
172+ self.make_headers_and_cell(
173+ data_pt, header.root.headers, other_root.headers, 1, header.path, true);
174+ });
175+ header.expanded = true;
176+ header.children.forEach(function (child) {
177 child.expanded = false;
178- header.children.splice(0,0, child);
179- header.root.headers.splice(header.root.headers.indexOf(header) + 1, 0, child);
180- // make cells
181- _.each(self.get_ancestors_and_self(group), function (data) {
182- var values = _.map(self.measures, function (m) {
183- return data.attributes.aggregates[m.field];
184- });
185- var other = _.find(otherRoot.headers, function (h) {
186- if (header.root === self.cols) {
187- return _.isEqual(data.path.slice(1), h.path);
188- } else {
189- return _.isEqual(_.rest(data.path), h.path);
190- }
191- });
192- if (other) {
193- self.add_cell(child.id, other.id, values);
194- }
195- });
196+ child.root = header.root;
197 });
198- header.expanded = true;
199 });
200 },
201
202- make_header: function (group, parent) {
203- var title = parent ? group.attributes.value : _t('Total');
204- return {
205- id: _.uniqueId(),
206- path: parent ? parent.path.concat(title) : [],
207- title: title,
208- children: [],
209- domain: parent ? group.model._domain : this.domain,
210- root: parent ? parent.root : undefined,
211- };
212- },
213-
214 swap_axis: function () {
215 var temp = this.rows;
216 this.rows = this.cols;
217@@ -262,206 +241,191 @@
218 // ----------------------------------------------------------------------
219 // Data updating methods
220 // ----------------------------------------------------------------------
221- // Load the data from the db, using the method this.load_data
222 // update_data will try to preserve the expand/not expanded status of each
223 // column/row. If you want to expand all, then set this.cols.headers/this.rows.headers
224 // to null before calling update_data.
225- update_data: function () {
226- var self = this;
227-
228- return this.load_data().then (function (result) {
229- if (result) {
230- self.no_data = false;
231- self[self.cols.headers ? 'update_headers' : 'expand_headers'](self.cols, result.col_headers);
232- self[self.rows.headers ? 'update_headers' : 'expand_headers'](self.rows, result.row_headers);
233- } else {
234- self.no_data = true;
235- }
236- });
237- },
238-
239- expand_headers: function (root, new_headers) {
240- root.headers = new_headers;
241- _.each(root.headers, function (header) {
242- header.root = root;
243- header.expanded = (header.children.length > 0);
244- });
245- },
246-
247- update_headers: function (root, new_headers) {
248- _.each(root.headers, function (header) {
249- var corresponding_header = _.find(new_headers, function (h) {
250- return _.isEqual(h.path, header.path);
251- });
252- if (corresponding_header && header.expanded) {
253- corresponding_header.expanded = true;
254- _.each(corresponding_header.children, function (c) {
255- c.expanded = false;
256- });
257- }
258- if (corresponding_header && (!header.expanded)) {
259- corresponding_header.expanded = false;
260- }
261- });
262- var updated_headers = _.filter(new_headers, function (header) {
263- return (header.expanded !== undefined);
264- });
265- _.each(updated_headers, function (header) {
266- if (!header.expanded) {
267- header.children = [];
268- }
269- header.root = root;
270- });
271- root.headers = updated_headers;
272- },
273-
274- // ----------------------------------------------------------------------
275- // Data loading methods
276- // ----------------------------------------------------------------------
277-
278- // To obtain all the values required to draw the full table, we have to do
279- // at least 2 + min(row.groupby.length, col.groupby.length)
280- // calls to readgroup. To simplify the code, we will always do
281- // 2 + row.groupby.length calls. For example, if row.groupby = [r1, r2, r3]
282- // and col.groupby = [c1, c2], then we will make the call with the following
283- // groupbys: [r1,r2,r3], [c1,r1,r2,r3], [c1,c2,r1,r2,r3], [].
284- load_data: function () {
285+ update_data: function () {
286+ var self = this;
287+ return this.perform_requests().then (function () {
288+ var data = Array.prototype.slice.call(arguments);
289+ self.no_data = !data[0][0].attributes.length;
290+ if (self.no_data) {
291+ return;
292+ }
293+ var row_headers = [],
294+ col_headers = [];
295+ self.cells = [];
296+
297+ var dim_col = self.cols.groupby.length,
298+ i, j, index;
299+
300+ for (i = 0; i < self.rows.groupby.length + 1; i++) {
301+ for (j = 0; j < dim_col + 1; j++) {
302+ index = i*(dim_col + 1) + j;
303+ self.make_headers_and_cell(data[index], row_headers, col_headers, i);
304+ }
305+ }
306+ self.set_headers(row_headers, self.rows);
307+ self.set_headers(col_headers, self.cols);
308+ });
309+ },
310+
311+ make_headers_and_cell: function (data_pts, row_headers, col_headers, index, prefix, expand) {
312+ var self = this;
313+ data_pts.forEach(function (data_pt) {
314+ var row_value = (prefix || []).concat(data_pt.attributes.value.slice(0,index));
315+ var col_value = data_pt.attributes.value.slice(index);
316+
317+ if (expand && !_.find(col_headers, function (hdr) {return _.isEqual(col_value, hdr.path);})) {
318+ return;
319+ }
320+ var row = self.find_or_create_header(row_headers, row_value, data_pt);
321+ var col = self.find_or_create_header(col_headers, col_value, data_pt);
322+
323+ var cell_value = _.map(self.measures, function (m) {
324+ return data_pt.attributes.aggregates[m.field];
325+ });
326+ self.cells.push({
327+ x: Math.min(row.id, col.id),
328+ y: Math.max(row.id, col.id),
329+ values: cell_value
330+ });
331+ });
332+ },
333+
334+ make_header: function (values) {
335+ return _.extend({
336+ children: [],
337+ domain: this.domain,
338+ expanded: undefined,
339+ id: _.uniqueId(),
340+ path: [],
341+ root: undefined,
342+ title: undefined
343+ }, values || {});
344+ },
345+
346+ find_or_create_header: function (headers, path, data_pt) {
347+ var hdr = _.find(headers, function (header) {
348+ return _.isEqual(path, header.path);
349+ });
350+ if (hdr) {
351+ return hdr;
352+ }
353+ if (!path.length) {
354+ hdr = this.make_header({title: _t('Total')});
355+ headers.push(hdr);
356+ return hdr;
357+ }
358+ hdr = this.make_header({
359+ path:path,
360+ domain:data_pt.model._domain,
361+ title: _t(_.last(path))
362+ });
363+ var parent = _.find(headers, function (header) {
364+ return _.isEqual(header.path, _.initial(path, 1));
365+ });
366+
367+ var previous = parent.children.length ? _.last(parent.children) : parent;
368+ headers.splice(headers.indexOf(previous) + 1, 0, hdr);
369+ parent.children.push(hdr);
370+ return hdr;
371+ },
372+
373+ perform_requests: function (group1, group2, domain) {
374 var self = this,
375- cols = this.cols.groupby,
376- rows = this.rows.groupby,
377- visible_fields = rows.concat(cols, self.measures);
378-
379- if (this.measures.length === 0) {
380- return $.Deferred.resolve().promise();
381- }
382-
383- var groupbys = _.map(_.range(cols.length + 1), function (i) {
384- return cols.slice(0, i).concat(rows);
385- });
386- groupbys.push([]);
387-
388- var get_data_requests = _.map(groupbys, function (groupby) {
389- return self.get_groups(groupby, visible_fields, self.domain);
390- });
391-
392- return $.when.apply(null, get_data_requests).then(function () {
393- var data = Array.prototype.slice.call(arguments),
394- row_data = data[0],
395- col_data = (cols.length !== 0) ? data[data.length - 2] : [],
396- has_data = data[data.length - 1][0];
397-
398- return has_data && self.format_data(col_data, row_data, data);
399- });
400- },
401-
402- get_groups: function (groupbys, fields, domain, path) {
403- var self = this,
404- groupby = (groupbys.length) ? groupbys[0] : [];
405- path = path || [];
406-
407- return this._query_db(groupby, fields, domain, path).then(function (groups) {
408- if (groupbys.length > 1) {
409- var get_subgroups = $.when.apply(null, _.map(groups, function (group) {
410- return self.get_groups(_.rest(groupbys), fields, group.model._domain, path.concat(group.attributes.value)).then(function (subgroups) {
411- group.children = subgroups;
412- });
413- }));
414- return get_subgroups.then(function () {
415- return groups;
416- });
417- } else {
418- return groups;
419+ requests = [],
420+ row_gbs = _.pluck(this.rows.groupby, 'field'),
421+ col_gbs = _.pluck(this.cols.groupby, 'field'),
422+ field_list = row_gbs.concat(col_gbs, _.pluck(this.measures, 'field')),
423+ fields = field_list.map(function (f) { return self.raw_field(f); });
424+
425+ group1 = group1 || row_gbs;
426+ group2 = group2 || col_gbs;
427+
428+ var i,j, groupbys;
429+ for (i = 0; i < group1.length + 1; i++) {
430+ for (j = 0; j < group2.length + 1; j++) {
431+ groupbys = group1.slice(0,i).concat(group2.slice(0,j));
432+ requests.push(self.get_groups(groupbys, fields, domain || self.domain));
433 }
434- });
435-
436- },
437-
438- _query_db: function (groupby, fields, domain, path) {
439- var self = this,
440- field_ids = _.without(_.pluck(fields, 'field'), '__count'),
441- fields = _.map(field_ids, function(f) { return self.raw_field(f); });
442-
443- return this.model.query(field_ids)
444- .filter(domain)
445- .group_by(groupby.field)
446- .then(function (results) {
447- var groups = _.filter(results, function (group) {
448- return group.attributes.length > 0;
449- });
450- return _.map(groups, function (g) { return self.format_group(g, path); });
451- });
452- },
453+ }
454+ return $.when.apply(null, requests);
455+ },
456+
457+ // set the 'expanded' status of new_headers more or less like root.headers, with root as root
458+ set_headers: function(new_headers, root) {
459+ if (root.headers) {
460+ _.each(root.headers, function (header) {
461+ var corresponding_header = _.find(new_headers, function (h) {
462+ return _.isEqual(h.path, header.path);
463+ });
464+ if (corresponding_header && header.expanded) {
465+ corresponding_header.expanded = true;
466+ _.each(corresponding_header.children, function (c) {
467+ c.expanded = false;
468+ });
469+ }
470+ if (corresponding_header && (!header.expanded)) {
471+ corresponding_header.expanded = false;
472+ corresponding_header.children = [];
473+ }
474+ });
475+ var updated_headers = _.filter(new_headers, function (header) {
476+ return (header.expanded !== undefined);
477+ });
478+ _.each(updated_headers, function (header) {
479+ header.root = root;
480+ });
481+ root.headers = updated_headers;
482+ } else {
483+ root.headers = new_headers;
484+ _.each(root.headers, function (header) {
485+ header.root = root;
486+ header.expanded = (header.children.length > 0);
487+ });
488+ }
489+ return new_headers;
490+ },
491+
492+ get_groups: function (groupbys, fields, domain) {
493+ var self = this;
494+ return this.model.query(_.without(fields, '__count'))
495+ .filter(domain)
496+ .lazy(false)
497+ .group_by(groupbys)
498+ .then(function (groups) {
499+ return groups.filter(function (group) {
500+ return group.attributes.length > 0;
501+ }).map(function (group) {
502+ var attrs = group.attributes,
503+ grouped_on = attrs.grouped_on instanceof Array ? attrs.grouped_on : [attrs.grouped_on],
504+ raw_grouped_on = grouped_on.map(function (f) {
505+ return self.raw_field(f);
506+ });
507+ if (grouped_on.length === 1) {
508+ attrs.value = [attrs.value];
509+ }
510+ attrs.value = _.range(grouped_on.length).map(function (i) {
511+ if (attrs.value[i] === false) {
512+ return _t('Undefined');
513+ } else if (attrs.value[i] instanceof Array) {
514+ return attrs.value[i][1];
515+ }
516+ return attrs.value[i];
517+ });
518+ attrs.aggregates.__count = group.attributes.length;
519+ attrs.grouped_on = raw_grouped_on;
520+ return group;
521+ });
522+ });
523+ },
524
525 // if field is a fieldname, returns field, if field is field_id:interval, retuns field_id
526 raw_field: function (field) {
527 return field.split(':')[0];
528 },
529
530- // add the path to the group and sanitize the value...
531- format_group: function (group, current_path) {
532- var attrs = group.attributes,
533- value = attrs.value,
534- grouped_on = attrs.grouped_on ? this.raw_field(attrs.grouped_on) : false;
535-
536- if (value === false) {
537- group.attributes.value = _t('Undefined');
538- } else if (grouped_on && this.fields[grouped_on].type === 'selection') {
539- var selection = this.fields[grouped_on].selection,
540- value_lookup = _.where(selection, {0:value});
541- group.attributes.value = value_lookup ? value_lookup[0][1] : _t('Undefined');
542- } else if (value instanceof Array) {
543- group.attributes.value = value[1];
544- }
545-
546- group.path = (value !== undefined) ? (current_path || []).concat(group.attributes.value) : [];
547- group.attributes.aggregates.__count = group.attributes.length;
548-
549- return group;
550- },
551-
552- format_data: function (col_data, row_data, cell_data) {
553- var self = this,
554- dim_row = this.rows.groupby.length,
555- dim_col = this.cols.groupby.length,
556- col_headers = this.get_ancestors_and_self(this.make_headers(col_data, dim_col)),
557- row_headers = this.get_ancestors_and_self(this.make_headers(row_data, dim_row));
558-
559- this.cells = [];
560- _.each(cell_data, function (data, index) {
561- self.make_cells(data, index, [], row_headers, col_headers);
562- }); // not pretty. make it more functional?
563-
564- return {col_headers: col_headers, row_headers: row_headers};
565- },
566-
567- make_headers: function (data, depth, parent) {
568- var self = this,
569- main = this.make_header(data, parent);
570-
571- if (main.path.length < depth) {
572- main.children = _.map(data.children || data, function (data_pt) {
573- return self.make_headers (data_pt, depth, main);
574- });
575- }
576- return main;
577- },
578-
579- make_cells: function (data, index, current_path, rows, cols) {
580- var self = this;
581- _.each(data, function (group) {
582- var attr = group.attributes,
583- path = attr.grouped_on ? current_path.concat(attr.value) : current_path,
584- values = _.map(self.measures, function (measure) { return attr.aggregates[measure.field]; }),
585- row = _.find(rows, function (header) { return _.isEqual(header.path, path.slice(index)); }),
586- col = _.find(cols, function (header) { return _.isEqual(header.path, path.slice(0, index)); });
587-
588- self.add_cell(row.id, col.id, values);
589- if (group.children) {
590- self.make_cells (group.children, index, path, rows, cols);
591- }
592- });
593- },
594-
595 });
596
597 })();