Skip to content

Commit

Permalink
Bug 972731 - [Messages] Reduce Thread rendering reflows
Browse files Browse the repository at this point in the history
Delay thread rendering by grouping threads into larger
sets before calling appendThread.

The goal is to reduce reflows on rendering the thread list.

Delay setting contact info for threads after initial render size
until they become visible via scrolling.
  • Loading branch information
yor-mozilla-com authored and julienw committed Apr 10, 2014
1 parent baff504 commit f816bab
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 29 deletions.
1 change: 1 addition & 0 deletions apps/sms/index.html
Expand Up @@ -41,6 +41,7 @@
<script defer src="shared/js/settings_listener.js"></script>
<script defer src="shared/js/sim_picker.js"></script>
<script defer src="shared/js/multi_sim_action_button.js"></script>
<script defer src="shared/js/tag_visibility_monitor.js"></script>
<script defer src="js/dialog.js"></script>
<script defer src="js/blacklist.js"></script>
<script defer src="js/contacts.js"></script>
Expand Down
87 changes: 74 additions & 13 deletions apps/sms/js/thread_list_ui.js
Expand Up @@ -4,7 +4,7 @@
/*global Template, Utils, Threads, Contacts, Threads,
WaitingScreen, MozSmsFilter, MessageManager, TimeHeaders,
Drafts, Thread, ThreadUI, OptionMenu, ActivityPicker,
PerformanceTestingHelper, StickyHeader */
PerformanceTestingHelper, StickyHeader, monitorTagVisibility */

/*exported ThreadListUI */
(function(exports) {
Expand All @@ -14,7 +14,11 @@ var ThreadListUI = {
draftLinks: null,
draftRegistry: null,
DRAFT_SAVED_DURATION: 5000,

INITIAL_RENDER_LIMIT: 20,
INITIAL_RENDER_SIZE: 2,
BATCH_RENDER_SIZE: 15,
monitor: null,

// Used to track timeouts
timeouts: {
onDraftSaved: null
Expand All @@ -27,6 +31,27 @@ var ThreadListUI = {
// Set to |true| when in edit mode
inEditMode: false,

onRowOnscreen: function thlui_onRowOnscreen(row) {
this.setContactIfNotSet(row);
},

onRowOffscreen: function thlui_onRowOffscreen(row) {
},

startMonitor: function thlui_startMonitor() {
if (this.monitor) {
return;
}
var scrollMargin = ~~(this.container.getBoundingClientRect().height * 1.5);
// NOTE: Making scrollDelta too large will cause janky scrolling
// due to bursts of onscreen() calls from the monitor.
var scrollDelta = Math.floor(scrollMargin / 15);
this.monitor = monitorTagVisibility(this.container, 'li',
scrollMargin, scrollDelta,
this.onRowOnscreen.bind(this),
this.onRowOffscreen.bind(this));
},

init: function thlui_init() {
this.tmpl = {
thread: Template('messages-thread-tmpl')
Expand Down Expand Up @@ -106,6 +131,12 @@ var ThreadListUI = {
}
},

setContactIfNotSet: function thlui_setContactIfNotSet(node) {
if (!node.dataset.contactSet) {
this.setContact(node);
}
},

setContact: function thlui_setContact(node) {
var thread = Threads.get(node.dataset.threadId);
var draft = Drafts.get(node.dataset.threadId);
Expand Down Expand Up @@ -159,6 +190,8 @@ var ThreadListUI = {
});

photo.style.backgroundImage = 'url(' + src + ')';

node.dataset.contactSet = true;
});
},

Expand Down Expand Up @@ -400,9 +433,8 @@ var ThreadListUI = {
// If there is currently no list item rendered for this
// draft, then proceed.
if (!this.draftRegistry[draft.id]) {
this.appendThread(
Thread.create(draft)
);
var node = this.appendThread(Thread.create(draft));
this.setContact(node);
}
}
}, this);
Expand All @@ -417,6 +449,7 @@ var ThreadListUI = {
},

startRendering: function thlui_startRenderingThreads() {
this.count = 0;
this.setEmpty(false);
},

Expand All @@ -436,25 +469,53 @@ var ThreadListUI = {
PerformanceTestingHelper.dispatch('will-render-threads');

var hasThreads = false;
var firstPanelCount = 9; // counted on a Peak
var FIRST_PANEL_COUNT = 9; // counted on a Peak
var threadsBatch = [];

this.prepareRendering();

var appendThreads = function() {
var delaySetContact = (this.count > this.INITIAL_RENDER_LIMIT);
for (var i = 0, l = threadsBatch.length; i < l; i++) {
var node = this.appendThread(threadsBatch[i]);
if (!delaySetContact) {
this.setContact(node);
}
}
threadsBatch.length = 0;
}.bind(this);

function onRenderThread(thread) {
/* jshint validthis: true */
if (!hasThreads) {
hasThreads = true;
this.startRendering();
}

this.appendThread(thread);
if (--firstPanelCount === 0) {
PerformanceTestingHelper.dispatch('above-the-fold-ready');
threadsBatch.push(thread);
this.count++;

if (this.count === this.INITIAL_RENDER_LIMIT) {
this.startMonitor();
}

if ((this.count <= this.INITIAL_RENDER_LIMIT &&
threadsBatch.length >= this.INITIAL_RENDER_SIZE) ||
threadsBatch.length >= this.BATCH_RENDER_SIZE) {
appendThreads();
if (this.count >= FIRST_PANEL_COUNT) {
PerformanceTestingHelper.dispatch('above-the-fold-ready');
}
}
}

function onThreadsRendered() {
/* jshint validthis: true */
if (threadsBatch.length > 0) {
appendThreads();
}

this.startMonitor();

/* We set the view as empty only if there's no threads and no drafts,
* this is done to prevent races between renering threads and drafts. */
Expand Down Expand Up @@ -612,7 +673,8 @@ var ThreadListUI = {
// remove the current thread node in order to place the new one properly
this.removeThread(thread.id);
}
this.appendThread(thread);
var node = this.appendThread(thread);
this.setContact(node);
this.setEmpty(false);
this.sticky.refresh();
},
Expand All @@ -636,9 +698,6 @@ var ThreadListUI = {
// We create the DOM element of the thread
var node = this.createThread(thread);

// Update info given a number
this.setContact(node);

// Is there any container already?
var threadsContainerID = 'threadsContainer_' +
Utils.getDayDate(timestamp);
Expand Down Expand Up @@ -671,6 +730,8 @@ var ThreadListUI = {
if (this.inEditMode) {
this.checkInputs();
}

return node;
},

// Adds a new grouping header if necessary (today, tomorrow, ...)
Expand Down
32 changes: 32 additions & 0 deletions apps/sms/test/unit/mock_tag_visibility_monitor.js
@@ -0,0 +1,32 @@
/*exported monitorTagVisibility */

'use strict';

function monitorTagVisibility(
container,
tag,
scrollMargin,
scrollDelta,
onscreenCallback,
offscreenCallback
) {

//====================================
// API
//====================================

function pauseMonitoringMutations() {
}

function resumeMonitoringMutations(forceVisibilityUpdate) {
}

function stopMonitoring() {
}

return {
pauseMonitoringMutations: pauseMonitoringMutations,
resumeMonitoringMutations: resumeMonitoringMutations,
stop: stopMonitoring
};
}
22 changes: 22 additions & 0 deletions apps/sms/test/unit/thread_list_mockup.js
@@ -1,5 +1,6 @@
/*global getMockupedDate */
/*exported MockThreadList */
/*exported MockThreadListBySize */

'use strict';

Expand Down Expand Up @@ -51,3 +52,24 @@ function MockThreadList() {

return threadsMockup;
}

function MockThreadListBySize(size) {
var threadTemplate = {
id: 1,
participants: ['1977'],
lastMessageType: 'sms',
body: 'Bogus Message',
timestamp: +getMockupedDate(0),
unreadCount: 0
};

var threadList = [];
for (var i = 1; i <= size; i++) {
var thread = JSON.parse(JSON.stringify(threadTemplate));
thread.id = i;
thread.participants = ['1977'+i];
threadList.push(thread);
}

return threadList;
}
83 changes: 67 additions & 16 deletions apps/sms/test/unit/thread_list_ui_test.js
@@ -1,7 +1,7 @@
/*global mocha, MocksHelper, loadBodyHTML, MockL10n, ThreadListUI,
MessageManager, WaitingScreen, Threads, Template, MockMessages,
MockThreadList, MockTimeHeaders, Draft, Drafts, Thread, ThreadUI,
MockOptionMenu, Utils
MockOptionMenu, Utils, MockThreadListBySize
*/

'use strict';
Expand All @@ -24,6 +24,7 @@ requireApp('sms/test/unit/mock_message_manager.js');
requireApp('sms/test/unit/mock_messages.js');
requireApp('sms/test/unit/mock_utils.js');
requireApp('sms/test/unit/mock_waiting_screen.js');
requireApp('sms/test/unit/mock_tag_visibility_monitor.js');
require('/shared/test/unit/mocks/mock_contact_photo_helper.js');
require('/test/unit/thread_list_mockup.js');
require('/test/unit/utils_mockup.js');
Expand Down Expand Up @@ -844,6 +845,41 @@ suite('thread_list_ui', function() {
});
});

function testThreadListRendering(numThreads, self, done) {
var container = ThreadListUI.container;

self.sinon.stub(MessageManager, 'getThreads',
function(options) {
var threadsMockup = new MockThreadListBySize(8);

var each = options.each;
var end = options.end;
var done = options.done;

for (var i = 0; i < threadsMockup.length; i++) {
each && each(threadsMockup[i]);
}

end && end();
done && done();

// Check that the right number of threads are inserted
var threads = container.querySelectorAll(
'[data-last-message-type="sms"],' +
'[data-last-message-type="mms"]'
);
assert.equal(threads.length, threadsMockup.length);
});

ThreadListUI.renderThreads(function() {
done(function checks() {
sinon.assert.calledWith(ThreadListUI.finalizeRendering, false);
assert.isTrue(ThreadListUI.noMessages.classList.contains('hide'));
assert.isFalse(ThreadListUI.container.classList.contains('hide'));
});
});
}

suite('renderThreads', function() {
setup(function() {
this.sinon.spy(ThreadListUI, 'setEmpty');
Expand Down Expand Up @@ -882,24 +918,19 @@ suite('thread_list_ui', function() {
function(options) {
var threadsMockup = new MockThreadList();

var each = options.each;
var end = options.end;
var done = options.done;

for (var i = 0; i < threadsMockup.length; i++) {
each && each(threadsMockup[i]);

var threads = container.querySelectorAll(
'[data-last-message-type="sms"],' +
'[data-last-message-type="mms"]'
);

// Check that a thread is inserted per iteration
assert.equal(threads.length, i + 1);
options.each && options.each(threadsMockup[i]);
}

end && end();
done && done();
options.end && options.end();
options.done && options.done();

// Check that the right number of threads are inserted
var threads = container.querySelectorAll(
'[data-last-message-type="sms"],' +
'[data-last-message-type="mms"]'
);
assert.equal(threads.length, threadsMockup.length);
});

ThreadListUI.renderThreads(function() {
Expand All @@ -921,6 +952,26 @@ suite('thread_list_ui', function() {
});
});
});

test('Rendering fewer than INITIAL_RENDER_SIZE', function (done) {
testThreadListRendering(8, this, done);
});

test('Rendering more than INITIAL_RENDER_SIZE', function (done) {
testThreadListRendering(13, this, done);
});

test('Rendering fewer than BATCH_RENDER_SIZE', function (done) {
testThreadListRendering(50, this, done);
});

test('Rendering more than BATCH_RENDER_SIZE', function (done) {
testThreadListRendering(80, this, done);
});

test('Rendering several BATCH_RENDER_SIZE', function (done) {
testThreadListRendering(300, this, done);
});
});

suite('renderDrafts', function() {
Expand Down

0 comments on commit f816bab

Please sign in to comment.