streams: Add LazySet for subscribers.

This defers O(N*S) operations, where

    N = number of streams
    S = number of subscribers per stream

In many cases we never do an O(N) operation on
a stream.  Exceptions include:

    - checking stream links from the compose box
    - editing a stream
    - adding members to a newly added stream

An operation that used to be O(N)--computing
the number of subscribers--is now O(1), and we
don't even pay O(N) on a one-time basis to
compute it (not counting the cost to build the
array from JSON, but we have to do that).
This commit is contained in:
Steve Howell
2019-12-27 12:16:22 +00:00
committed by Tim Abbott
parent e804f39f0e
commit a3512553a8
6 changed files with 85 additions and 17 deletions

View File

@@ -9,6 +9,8 @@ const noop = function () {};
set_global('$', global.make_zjquery()); set_global('$', global.make_zjquery());
set_global('i18n', global.stub_i18n); set_global('i18n', global.stub_i18n);
const LazySet = zrequire('lazy_set.js').LazySet;
const _navigator = { const _navigator = {
platform: '', platform: '',
}; };
@@ -1328,13 +1330,13 @@ run_test('on_events', () => {
(function test_stream_name_completed_triggered() { (function test_stream_name_completed_triggered() {
const handler = $(document).get_on_handler('streamname_completed.zulip'); const handler = $(document).get_on_handler('streamname_completed.zulip');
stream_data.add_sub(compose_state.stream_name(), { stream_data.add_sub(compose_state.stream_name(), {
subscribers: Dict.from_array([1, 2]), subscribers: LazySet([1, 2]),
}); });
let data = { let data = {
stream: { stream: {
name: 'Denmark', name: 'Denmark',
subscribers: Dict.from_array([1, 2, 3]), subscribers: LazySet([1, 2, 3]),
}, },
}; };
@@ -1379,7 +1381,7 @@ run_test('on_events', () => {
stream: { stream: {
invite_only: true, invite_only: true,
name: 'Denmark', name: 'Denmark',
subscribers: Dict.from_array([1]), subscribers: LazySet([1]),
}, },
}; };

View File

@@ -17,6 +17,7 @@ zrequire('marked', 'third/marked/lib/marked');
const actual_pygments_data = zrequire('actual_pygments_data', 'generated/pygments_data'); const actual_pygments_data = zrequire('actual_pygments_data', 'generated/pygments_data');
zrequire('settings_org'); zrequire('settings_org');
const th = zrequire('typeahead_helper'); const th = zrequire('typeahead_helper');
const LazySet = zrequire('lazy_set.js').LazySet;
stream_data.create_streams([ stream_data.create_streams([
{name: 'Dev', subscribed: true, color: 'blue', stream_id: 1}, {name: 'Dev', subscribed: true, color: 'blue', stream_id: 1},
@@ -24,13 +25,9 @@ stream_data.create_streams([
]); ]);
run_test('sort_streams', () => { run_test('sort_streams', () => {
const popular = {num_items: function () { const popular = LazySet([1, 2, 3, 4, 5, 6]);
return 10;
}};
const unpopular = {num_items: function () { const unpopular = LazySet([1]);
return 2;
}};
let test_streams = [ let test_streams = [
{name: 'Dev', pin_to_top: false, subscribers: unpopular, subscribed: true}, {name: 'Dev', pin_to_top: false, subscribers: unpopular, subscribed: true},

View File

@@ -31,6 +31,7 @@ import "../search_util.js";
import "../keydown_util.js"; import "../keydown_util.js";
import "../lightbox_canvas.js"; import "../lightbox_canvas.js";
import "../rtl.js"; import "../rtl.js";
import "../lazy_set.js";
import "../dict.ts"; import "../dict.ts";
import "../scroll_util.js"; import "../scroll_util.js";
import "../components.js"; import "../components.js";

70
static/js/lazy_set.js Normal file
View File

@@ -0,0 +1,70 @@
exports.LazySet = function (vals) {
/*
This class is optimized for a very
particular use case.
We often have lots of subscribers on
a stream. We get an array from the
backend, because it's JSON.
Often the only operation we need
on subscribers is to get the length,
which is plenty cheap as an array.
Making an array from a set is cheap
for one stream, but it's expensive
for all N streams at page load.
Once somebody does an operation
where sets are useful, such
as has/add/del, we convert it over
to a set for a one-time cost.
*/
const self = {};
self.arr = vals;
self.set = undefined;
self.keys = function () {
if (self.set !== undefined) {
return Array.from(self.set);
}
return self.arr;
};
function make_set() {
if (self.set !== undefined) {
return;
}
self.set = new Set(self.arr);
self.arr = undefined;
}
self.num_items = function () {
if (self.set !== undefined) {
return self.set.size;
}
return self.arr.length;
};
self.map = function (f) {
return _.map(self.keys(), f);
};
self.has = function (v) {
make_set();
return self.set.has(v);
};
self.add = function (v) {
make_set();
self.set.add(v);
};
self.del = function (v) {
make_set();
self.set.delete(v);
};
return self;
};

View File

@@ -1,4 +1,5 @@
const Dict = require('./dict').Dict; const Dict = require('./dict').Dict;
const LazySet = require('./lazy_set').LazySet;
// The stream_info variable maps stream names to stream properties objects // The stream_info variable maps stream names to stream properties objects
@@ -74,7 +75,7 @@ exports.unsubscribe_myself = function (sub) {
exports.add_sub = function (stream_name, sub) { exports.add_sub = function (stream_name, sub) {
if (!_.has(sub, 'subscribers')) { if (!_.has(sub, 'subscribers')) {
sub.subscribers = Dict.from_array([]); sub.subscribers = LazySet([]);
} }
stream_info.set(stream_name, sub); stream_info.set(stream_name, sub);
@@ -507,7 +508,7 @@ exports.maybe_get_stream_name = function (stream_id) {
}; };
exports.set_subscribers = function (sub, user_ids) { exports.set_subscribers = function (sub, user_ids) {
sub.subscribers = Dict.from_array(user_ids || []); sub.subscribers = LazySet(user_ids || []);
}; };
exports.add_subscriber = function (stream_name, user_id) { exports.add_subscriber = function (stream_name, user_id) {
@@ -521,7 +522,7 @@ exports.add_subscriber = function (stream_name, user_id) {
blueslip.error("We tried to add invalid subscriber: " + user_id); blueslip.error("We tried to add invalid subscriber: " + user_id);
return false; return false;
} }
sub.subscribers.set(user_id, true); sub.subscribers.add(user_id);
return true; return true;
}; };

View File

@@ -36,12 +36,9 @@ exports.is_sub_settings_active = function (sub) {
}; };
exports.get_email_of_subscribers = function (subscribers) { exports.get_email_of_subscribers = function (subscribers) {
const emails = []; return subscribers.map(function (user_id) {
subscribers.each(function (o, i) { return people.get_person_from_user_id(user_id).email;
const email = people.get_person_from_user_id(i).email;
emails.push(email);
}); });
return emails;
}; };
function clear_edit_panel() { function clear_edit_panel() {