Merge lp:~abentley/launchpad/pre-cache-batches into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Merged at revision: 14352
Proposed branch: lp:~abentley/launchpad/pre-cache-batches
Merge into: lp:launchpad
Prerequisite: lp:~abentley/launchpad/view-flags
Diff against target: 429 lines (+224/-29)
4 files modified
lib/lp/bugs/browser/bugtask.py (+4/-1)
lib/lp/bugs/javascript/buglisting.js (+93/-25)
lib/lp/bugs/javascript/tests/test_buglisting.js (+121/-3)
lib/lp/services/features/flags.py (+6/-0)
To merge this branch: bzr merge lp:~abentley/launchpad/pre-cache-batches
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+82716@code.launchpad.net

Commit message

Pre-fetch next batch of bug listings.

Description of the change

= Summary =
Implement limited pre-fetching of bug listings.

== Proposed fix ==
The "next" link is pre-fetched, but only if pre-fetching is activated.

== Pre-implementation notes ==
Discussed with deryck

== Implementation details ==
bugs.dynamic_bug_listings.pre_fetch must be enabled to see this in action.

bugs.dynamic_bug_listings.pre_fetch is declared to be related to the view, so that it is provided in the JSON cache.

The first_batch_config next_batch_config and prev_batch_config are extracted from first_batch, next_batch, and last_batch, because initially, all of those would be pre-fetched.

update ensures we do not pre-fetch batches we already have.

Several methods are updated to understand fetch_only, (i.e. don't render).

== Tests ==
bin/test -t test_buglisting.html

== Demo and Q/A ==
Ensure bugs.dynamic_bug_listings.pre_fetch is active for you. Go to a bugs listing page. Click next. It should update almost instantly, without a roundrip. (This will also cause it to pre-fetch the new next batch.)

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/app/javascript/beta-notification.js
  lib/lp/app/javascript/tests/test_beta_notification.js
  lib/canonical/launchpad/webapp/publisher.py
  lib/lp/bugs/javascript/buglisting.js
  lib/canonical/launchpad/webapp/tests/test_publisher.py
  lib/lp/app/javascript/tests/test_beta_notification.html
  lib/canonical/launchpad/webapp/tests/test_view_model.py
  lib/lp/services/features/flags.py
  lib/lp/bugs/browser/bugtask.py
  lib/lp/bugs/javascript/tests/test_buglisting.js

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Aaron,

Thanks for clearing up my confusion on IRC wrt the Y.bind issue.

typo: ajacent -> adjacent

Your tests provide good coverage and are readable. Thanks.

--Brad

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/bugtask.py'
2--- lib/lp/bugs/browser/bugtask.py 2011-11-18 20:16:26 +0000
3+++ lib/lp/bugs/browser/bugtask.py 2011-11-18 20:16:27 +0000
4@@ -2455,7 +2455,10 @@
5
6 implements(IBugTaskSearchListingMenu)
7
8- related_features = ['bugs.dynamic_bug_listings.enabled']
9+ related_features = (
10+ 'bugs.dynamic_bug_listings.enabled',
11+ 'bugs.dynamic_bug_listings.pre_fetch',
12+ )
13
14 # Only include <link> tags for bug feeds when using this view.
15 feed_types = (
16
17=== modified file 'lib/lp/bugs/javascript/buglisting.js'
18--- lib/lp/bugs/javascript/buglisting.js 2011-11-18 20:16:26 +0000
19+++ lib/lp/bugs/javascript/buglisting.js 2011-11-18 20:16:27 +0000
20@@ -40,6 +40,7 @@
21 backwards_navigation: {valueFn: empty_nodelist},
22 forwards_navigation: {valueFn: empty_nodelist},
23 io_provider: {value: null},
24+ pre_fetch: {value: false},
25 navigation_indices: {valueFn: empty_nodelist}
26 };
27
28@@ -64,6 +65,7 @@
29 this.set('field_visibility', cache.field_visibility);
30 batch_key = this.handle_new_batch(cache);
31 this.set('current_batch', cache);
32+ this.pre_fetch_batches();
33 // Work around mustache.js bug 48 "Blank lines are not preserved."
34 // https://github.com/janl/mustache.js/issues/48
35 if (Y.Lang.isValue(template)) {
36@@ -90,6 +92,7 @@
37 var batch_key = e.newVal.batch_key;
38 var batch = this.get('batches')[batch_key];
39 this.set('current_batch', batch);
40+ this.pre_fetch_batches();
41 this.render();
42 }
43 },
44@@ -167,8 +170,12 @@
45 'inactive', !this.has_next());
46 },
47
48- update_from_new_model: function(query, model){
49- this.update_from_cache(query, this.handle_new_batch(model));
50+ update_from_new_model: function(query, fetch_only, model){
51+ var batch_key = this.handle_new_batch(model);
52+ if (fetch_only) {
53+ return;
54+ }
55+ this.update_from_cache(query, batch_key);
56 },
57
58 /**
59@@ -202,6 +209,22 @@
60 return query;
61 },
62
63+
64+ /**
65+ * Pre-fetch adjacent batches.
66+ */
67+ pre_fetch_batches: function(){
68+ var that=this;
69+ if (!this.get('pre_fetch')){
70+ return;
71+ }
72+ Y.each(this.get_pre_fetch_configs(), function(config){
73+ config.fetch_only = true;
74+ that.update(config);
75+ });
76+ },
77+
78+
79 /**
80 * Update the display to the specified batch.
81 *
82@@ -213,10 +236,13 @@
83 var cached_batch = this.get('batches')[key];
84 var query = this.get_batch_query(config);
85 if (Y.Lang.isValue(cached_batch)) {
86+ if (config.fetch_only){
87+ return;
88+ }
89 this.update_from_cache(query, key);
90 }
91 else {
92- this.load_model(query);
93+ this.load_model(query, config.fetch_only);
94 }
95 },
96
97@@ -232,53 +258,70 @@
98 });
99 },
100
101- /**
102- * Update the navigator to display the first batch.
103- *
104- * The order_by defaults to the current ordering, but may be overridden.
105- */
106- first_batch: function(order_by) {
107+ first_batch_config: function(order_by){
108 if (order_by === undefined) {
109 order_by = this.get('current_batch').order_by;
110 }
111- this.update({
112+ return {
113 forwards: true,
114 memo: null,
115 start: 0,
116 order_by: order_by
117- });
118+ };
119 },
120
121 /**
122- * Update the navigator to display the next batch.
123+ * Update the navigator to display the first batch.
124+ *
125+ * The order_by defaults to the current ordering, but may be overridden.
126 */
127- next_batch: function() {
128+ first_batch: function(order_by) {
129+ this.update(this.first_batch_config(order_by));
130+ },
131+
132+ next_batch_config: function(){
133 var current_batch = this.get('current_batch');
134 if (!this.has_next()){
135- return;
136+ return null;
137 }
138- this.update({
139+ return {
140 forwards: true,
141 memo: current_batch.next.memo,
142 start: current_batch.next.start,
143 order_by: current_batch.order_by
144- });
145+ };
146 },
147-
148 /**
149- * Update the navigator to display the previous batch.
150+ * Update the navigator to display the next batch.
151 */
152- prev_batch: function() {
153+ next_batch: function() {
154+ var config = this.next_batch_config();
155+ if (config === null){
156+ return;
157+ }
158+ this.update(config);
159+ },
160+ prev_batch_config: function(){
161 var current_batch = this.get('current_batch');
162 if (!this.has_prev()){
163- return;
164+ return null;
165 }
166- this.update({
167+ return {
168 forwards: false,
169 memo: current_batch.prev.memo,
170 start: current_batch.prev.start,
171 order_by: current_batch.order_by
172- });
173+ };
174+ },
175+ /**
176+ * Update the navigator to display the previous batch.
177+ */
178+ prev_batch: function() {
179+ var config = this.prev_batch_config();
180+ if (config === null){
181+ return;
182+ }
183+ this.update(config);
184 },
185 /**
186 * Change which fields are displayed in the batch. Input is a config with
187@@ -291,15 +334,28 @@
188 },
189
190 /**
191+ * Generate a list of configs to pre-fetch.
192+ */
193+ get_pre_fetch_configs: function(){
194+ var configs = [];
195+ var next_batch_config = this.next_batch_config();
196+ if (next_batch_config !== null){
197+ configs.push(next_batch_config);
198+ }
199+ return configs;
200+ },
201+
202+ /**
203 * Load the specified batch via ajax. Display & cache on load.
204 *
205 * query is the query string for the URL, as a mapping. (See
206 * get_batch_query).
207 */
208- load_model: function(query) {
209+ load_model: function(query, fetch_only) {
210 var load_model_config = {
211 on: {
212- success: Y.bind(this.update_from_new_model, this, query),
213+ success: Y.bind(
214+ this.update_from_new_model, this, query, fetch_only),
215 failure: this.get('failure_handler')
216 }
217 };
218@@ -328,18 +384,30 @@
219 });
220 };
221
222+
223+/**
224+ * Return the value for a given feature flag in the current scope.
225+ * Only flags declared as "related_features" on the view are available.
226+ */
227+var get_feature_flag = function(flag_name){
228+ return LP.cache.related_features[flag_name].value;
229+};
230+
231+
232 /**
233 * Factory to return a ListingNavigator for the given page.
234 */
235 namespace.ListingNavigator.from_page = function() {
236 var target = Y.one('#client-listing');
237 var navigation_indices = Y.all('.batch-navigation-index');
238+ var pre_fetch = get_feature_flag('bugs.dynamic_bug_listings.pre_fetch');
239 var navigator = new namespace.ListingNavigator({
240 current_url: window.location,
241 cache: LP.cache,
242 template: LP.mustache_listings,
243 target: target,
244- navigation_indices: navigation_indices
245+ navigation_indices: navigation_indices,
246+ pre_fetch: Boolean(pre_fetch)
247 });
248 namespace.linkify_navigation();
249 navigator.set('backwards_navigation', Y.all('.first,.previous'));
250
251=== modified file 'lib/lp/bugs/javascript/tests/test_buglisting.js'
252--- lib/lp/bugs/javascript/tests/test_buglisting.js 2011-11-18 20:16:26 +0000
253+++ lib/lp/bugs/javascript/tests/test_buglisting.js 2011-11-18 20:16:27 +0000
254@@ -59,7 +59,7 @@
255 prev: null
256 };
257 var query = navigator.get_batch_query(batch);
258- navigator.update_from_new_model(query, batch);
259+ navigator.update_from_new_model(query, false, batch);
260 bugtask = navigator.get('current_batch').mustache_model.bugtasks[0];
261 Y.Assert.isFalse(bugtask.hasOwnProperty('show_item'));
262 }
263@@ -330,7 +330,7 @@
264 bugtasks: ['a', 'b', 'c']
265 }};
266 var query = navigator.get_batch_query(batch);
267- navigator.update_from_new_model(query, batch);
268+ navigator.update_from_new_model(query, true, batch);
269 Y.lp.testing.assert.assert_equal_structure(
270 batch, navigator.get('batches')[key]);
271 },
272@@ -470,6 +470,7 @@
273 current_url: url,
274 cache: lp_cache,
275 io_provider: mock_io,
276+ pre_fetch: config.pre_fetch,
277 target: target,
278 template: ''
279 });
280@@ -605,7 +606,10 @@
281 cache: {
282 current_batch: {},
283 next: null,
284- prev: null
285+ prev: null,
286+ related_features: {
287+ 'bugs.dynamic_bug_listings.pre_fetch': {value: 'on'}
288+ }
289 }
290 };
291 },
292@@ -626,6 +630,120 @@
293 }
294 }));
295
296+suite.add(new Y.Test.Case({
297+ name: "pre-fetching batches",
298+
299+ /**
300+ * get_pre_fetch_configs should return a config for the next batch.
301+ */
302+ test_get_pre_fetch_configs: function(){
303+ var navigator = get_navigator();
304+ var configs = navigator.get_pre_fetch_configs();
305+ var batch_keys = [];
306+ Y.each(configs, function(value){
307+ batch_keys.push(navigator.constructor.get_batch_key(value));
308+ });
309+ Y.Assert.areSame('["foo",467,true,500]', batch_keys[0]);
310+ Y.Assert.areSame(1, batch_keys.length);
311+ },
312+
313+ /**
314+ * get_pre_fetch_configs should return an empty list if no next batch.
315+ */
316+ test_get_pre_fetch_configs_no_next: function(){
317+ var navigator = get_navigator('', {no_next: true});
318+ var configs = navigator.get_pre_fetch_configs();
319+ var batch_keys = [];
320+ Y.each(configs, function(value){
321+ batch_keys.push(navigator.constructor.get_batch_key(value));
322+ });
323+ Y.Assert.areSame(0, batch_keys.length);
324+ },
325+
326+ get_pre_fetch_navigator: function(config){
327+ var navigator = get_navigator('', config);
328+ var lp_client = new Y.lp.client.Launchpad();
329+ var batch = lp_client.wrap_resource(null, {
330+ context: {
331+ resource_type_link: 'http://foo_type',
332+ web_link: 'http://foo/bar'
333+ },
334+ next: {memo: 57, start: 56}});
335+ navigator.set('current_batch', batch);
336+ return navigator;
337+ },
338+
339+ /**
340+ * Calling pre_fetch_batches should produce a request for the next batch.
341+ */
342+ test_pre_fetch_batches: function(){
343+ var navigator = this.get_pre_fetch_navigator();
344+ var io_provider = navigator.get('io_provider');
345+ navigator.set('pre_fetch', true);
346+ Y.Assert.isNull(io_provider.last_request);
347+ navigator.pre_fetch_batches();
348+ Y.Assert.areSame(
349+ io_provider.last_request.url,
350+ '/bar/+bugs/++model++?foo=bar&orderby=&memo=57&start=56');
351+ },
352+
353+ /**
354+ * Calling pre_fetch_batches should not produce a request for the next
355+ * batch if Navigator.get('pre_fetch') is false.
356+ */
357+ test_pre_fetch_disabled: function(){
358+ var last_url;
359+ var navigator = this.get_pre_fetch_navigator();
360+ navigator.pre_fetch_batches();
361+ Y.Assert.areSame(null, navigator.get('io_provider').last_request);
362+ },
363+
364+ /**
365+ * Initialization does a pre-fetch.
366+ */
367+ test_pre_fetch_on_init: function(){
368+ var navigator = get_navigator('', {pre_fetch: true});
369+ var last_url = navigator.get('io_provider').last_request.url;
370+ Y.Assert.areSame(
371+ last_url,
372+ '/bar/+bugs/++model++?foo=bar&orderby=foo&memo=467&start=500');
373+ },
374+ /**
375+ * update_from_new_model does a pre-fetch.
376+ */
377+ test_pre_fetch_on_update_from_new_model: function(){
378+ var navigator = get_navigator('');
379+ var io_provider = navigator.get('io_provider');
380+ var lp_client = new Y.lp.client.Launchpad();
381+ var batch = lp_client.wrap_resource(null, {
382+ context: {
383+ resource_type_link: 'http://foo_type',
384+ web_link: 'http://foo/bar'
385+ },
386+ order_by: 'baz',
387+ memo: 'memo1',
388+ next: {
389+ memo: "pi",
390+ start: 314
391+ },
392+ forwards: true,
393+ start: 5,
394+ mustache_model: {
395+ item: [
396+ {name: 'first'},
397+ {name: 'second'}
398+ ],
399+ bugtasks: ['a', 'b', 'c']
400+ }});
401+ Y.Assert.isNull(io_provider.last_request);
402+ navigator.set('pre_fetch', true);
403+ navigator.update_from_new_model({}, false, batch);
404+ Y.Assert.areSame(
405+ io_provider.last_request.url,
406+ '/bar/+bugs/++model++?foo=bar&orderby=baz&memo=pi&start=314');
407+ }
408+}));
409+
410 var handle_complete = function(data) {
411 window.status = '::::' + JSON.stringify(data);
412 };
413
414=== modified file 'lib/lp/services/features/flags.py'
415--- lib/lp/services/features/flags.py 2011-11-16 23:46:01 +0000
416+++ lib/lp/services/features/flags.py 2011-11-18 20:16:27 +0000
417@@ -65,6 +65,12 @@
418 '',
419 'Dynamic bug listings',
420 'https://dev.launchpad.net/LEP/CustomBugListings'),
421+ ('bugs.dynamic_bug_listings.pre_fetch',
422+ 'boolean',
423+ ('Enables pre-fetching bug listing results.'),
424+ '',
425+ 'Listing pre-fetching',
426+ 'https://dev.launchpad.net/LEP/CustomBugListings'),
427 ('code.ajax_revision_diffs.enabled',
428 'boolean',
429 ("Offer expandable inline diffs for branch revisions."),