From dd47801e2aee140b80fa77f18b8e6f3588d507fc Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 15 Jan 2013 18:30:16 -0500 Subject: [PATCH] facts: proof-of-concept server-side statistics. Publishes statistics from the server to a Minimongo collection, Facts.server. (On the server side, Facts is *not* backed by MongoDB.) By default, this is available to all users if autopublish is on, or no users if it is not on, but you can configure this with Facts.setUserIdFilter. You can add an ugly view on the stats to your app with {{> serverFacts }}. Current stats, by package: - mongo-livedata: - observe-handles - live-results-sets (when this is less than observe-handles, cursor de-dup is helping you) - livedata - subscriptions - crossbar-listeners - sessions --- History.md | 2 + packages/facts/.gitignore | 1 + packages/facts/facts.html | 14 +++++ packages/facts/facts.js | 79 +++++++++++++++++++++++++ packages/facts/package.js | 21 +++++++ packages/livedata/crossbar.js | 4 ++ packages/livedata/livedata_server.js | 11 +++- packages/livedata/package.js | 3 + packages/mongo-livedata/mongo_driver.js | 11 ++++ packages/mongo-livedata/package.js | 3 + 10 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 packages/facts/.gitignore create mode 100644 packages/facts/facts.html create mode 100644 packages/facts/facts.js create mode 100644 packages/facts/package.js diff --git a/History.md b/History.md index 52aae4d4cc..a08a49620f 100644 --- a/History.md +++ b/History.md @@ -13,6 +13,8 @@ * Increase the maximum size spiderable will return for a page from 200kB to 5MB. +* New 'facts' package publishes internal statistics about Meteor. + * Upgraded dependencies: * SockJS server from 0.3.7 to 0.3.8 diff --git a/packages/facts/.gitignore b/packages/facts/.gitignore new file mode 100644 index 0000000000..677a6fc263 --- /dev/null +++ b/packages/facts/.gitignore @@ -0,0 +1 @@ +.build* diff --git a/packages/facts/facts.html b/packages/facts/facts.html new file mode 100644 index 0000000000..b41b7ae6fe --- /dev/null +++ b/packages/facts/facts.html @@ -0,0 +1,14 @@ + diff --git a/packages/facts/facts.js b/packages/facts/facts.js new file mode 100644 index 0000000000..07acf1a5c4 --- /dev/null +++ b/packages/facts/facts.js @@ -0,0 +1,79 @@ +Facts = {}; + +var serverFactsCollection = 'Facts.server'; + +if (Meteor.isServer) { + // By default, we publish facts to no user if autopublish is off, and to all + // users if autopublish is on. + var userIdFilter = function (userId) { + return !!Package.autopublish; + }; + + // XXX make this take effect at runtime too? + Facts.setUserIdFilter = function (filter) { + userIdFilter = filter; + }; + + // XXX Use a minimongo collection instead and hook up an observeChanges + // directly to a publish. + var factsByPackage = {}; + var activeSubscriptions = []; + + Facts.incrementServerFact = function (pkg, fact, increment) { + if (!_.has(factsByPackage, pkg)) { + factsByPackage[pkg] = {}; + factsByPackage[pkg][fact] = increment; + _.each(activeSubscriptions, function (sub) { + sub.added(serverFactsCollection, pkg, factsByPackage[pkg]); + }); + return; + } + + var packageFacts = factsByPackage[pkg]; + if (!_.has(packageFacts, fact)) + factsByPackage[pkg][fact] = 0; + factsByPackage[pkg][fact] += increment; + var changedField = {}; + changedField[fact] = factsByPackage[pkg][fact]; + _.each(activeSubscriptions, function (sub) { + sub.changed(serverFactsCollection, pkg, changedField); + }); + }; + + // Deferred, because we have an unordered dependency on livedata. + // XXX is this safe? could somebody try to connect before Meteor.publish is + // called? + Meteor.defer(function () { + // XXX Also publish facts-by-package. + Meteor.publish("facts", function () { + var sub = this; + if (!userIdFilter(this.userId)) { + sub.ready(); + return; + } + activeSubscriptions.push(sub); + _.each(factsByPackage, function (facts, pkg) { + sub.added(serverFactsCollection, pkg, facts); + }); + sub.onStop(function () { + activeSubscriptions = _.without(activeSubscriptions, sub); + }); + sub.ready(); + }); + }); +} else { + Facts.server = new Meteor.Collection(serverFactsCollection); + Meteor.subscribe("facts"); + + Template.serverFacts.factsByPackage = function () { + return Facts.server.find(); + }; + Template.serverFacts.facts = function () { + var factArray = []; + _.each(this, function (value, name) { + if (name !== '_id') + factArray.push({name: name, value: value}); + }); + return factArray; + }; +} diff --git a/packages/facts/package.js b/packages/facts/package.js new file mode 100644 index 0000000000..f3aafae09d --- /dev/null +++ b/packages/facts/package.js @@ -0,0 +1,21 @@ +Package.describe({ + summary: "Publish internal and custom app statistics" +}); + +Package.on_use(function (api) { + api.use(['underscore'], ['client', 'server']); + api.use(['templating', 'mongo-livedata', 'livedata'], ['client']); + + // Detect whether autopublish is used. + api.use('autopublish', 'server', {weak: true}); + + // Unordered dependency on livedata, since livedata has a (weak) dependency on + // us. + api.use('livedata', 'server', {unordered: true}); + + api.add_files('facts.html', ['client']); + api.add_files('facts.js', ['client', 'server']); + + api.export('Facts'); +}); + diff --git a/packages/livedata/crossbar.js b/packages/livedata/crossbar.js index e4c8be26db..613ae13840 100644 --- a/packages/livedata/crossbar.js +++ b/packages/livedata/crossbar.js @@ -27,8 +27,12 @@ _.extend(DDPServer._InvalidationCrossbar.prototype, { var self = this; var id = self.next_id++; self.listeners[id] = {trigger: EJSON.clone(trigger), callback: callback}; + Package.facts && Package.facts.Facts.incrementServerFact( + "livedata", "crossbar-listeners", 1); return { stop: function () { + Package.facts && Package.facts.Facts.incrementServerFact( + "livedata", "crossbar-listeners", -1); delete self.listeners[id]; } }; diff --git a/packages/livedata/livedata_server.js b/packages/livedata/livedata_server.js index 67e707c823..f8dfd4e6fc 100644 --- a/packages/livedata/livedata_server.js +++ b/packages/livedata/livedata_server.js @@ -257,6 +257,9 @@ var Session = function (server, version, socket) { Fiber(function () { self.startUniversalSubs(); }).run(); + + Package.facts && Package.facts.Facts.incrementServerFact( + "livedata", "sessions", 1); }; _.extend(Session.prototype, { @@ -341,7 +344,6 @@ _.extend(Session.prototype, { view.changed(subscriptionHandle, id, fields); }, - startUniversalSubs: function () { var self = this; // Make a shallow copy of the set of universal handlers and start them. If @@ -371,6 +373,8 @@ _.extend(Session.prototype, { // Drop the merge box data immediately. self.collectionViews = {}; self.inQueue = null; + Package.facts && Package.facts.Facts.incrementServerFact( + "livedata", "sessions", -1); }, // Send a message (doing nothing if no socket is connected right now.) @@ -768,6 +772,9 @@ var Subscription = function ( idStringify: LocalCollection._idStringify, idParse: LocalCollection._idParse }; + + Package.facts && Package.facts.Facts.incrementServerFact( + "livedata", "subscriptions", 1); }; _.extend(Subscription.prototype, { @@ -855,6 +862,8 @@ _.extend(Subscription.prototype, { return; self._deactivated = true; self._callStopCallbacks(); + Package.facts && Package.facts.Facts.incrementServerFact( + "livedata", "subscriptions", -1); }, _callStopCallbacks: function () { diff --git a/packages/livedata/package.js b/packages/livedata/package.js index adace40793..250c116f22 100644 --- a/packages/livedata/package.js +++ b/packages/livedata/package.js @@ -22,6 +22,9 @@ Package.on_use(function (api) { // runs Meteor.publish while it's loaded. api.use('autopublish', 'server', {weak: true}); + // If the facts package is loaded, publish some statistics. + api.use('facts', 'server', {weak: true}); + api.export('DDP'); api.export('DDPServer', 'server'); diff --git a/packages/mongo-livedata/mongo_driver.js b/packages/mongo-livedata/mongo_driver.js index 1f7e999542..aa68b16ba6 100644 --- a/packages/mongo-livedata/mongo_driver.js +++ b/packages/mongo-livedata/mongo_driver.js @@ -989,6 +989,9 @@ var LiveResultsSet = function (cursorDescription, mongoHandle, ordered, Meteor.clearInterval(intervalHandle); }); } + + Package.facts && Package.facts.Facts.incrementServerFact( + "mongo-livedata", "live-results-sets", 1); }; _.extend(LiveResultsSet.prototype, { @@ -1000,6 +1003,8 @@ _.extend(LiveResultsSet.prototype, { throw new Error("Call _addFirstObserveHandle before polling!"); self._observeHandles[handle._observeHandleId] = handle; + Package.facts && Package.facts.Facts.incrementServerFact( + "mongo-livedata", "observe-handles", 1); // Run the first _poll() cycle synchronously (delivering results to the // first ObserveHandle). @@ -1115,6 +1120,8 @@ _.extend(LiveResultsSet.prototype, { throw new Error("Duplicate observe handle ID"); self._observeHandles[handle._observeHandleId] = handle; --self._addHandleTasksScheduledButNotPerformed; + Package.facts && Package.facts.Facts.incrementServerFact( + "mongo-livedata", "observe-handles", 1); // Send initial adds. if (handle._added || handle._addedBefore) { @@ -1143,6 +1150,8 @@ _.extend(LiveResultsSet.prototype, { if (!_.has(self._observeHandles, handle._observeHandleId)) throw new Error("Unknown observe handle ID " + handle._observeHandleId); delete self._observeHandles[handle._observeHandleId]; + Package.facts && Package.facts.Facts.incrementServerFact( + "mongo-livedata", "observe-handles", -1); if (_.isEmpty(self._observeHandles) && self._addHandleTasksScheduledButNotPerformed === 0) { @@ -1151,6 +1160,8 @@ _.extend(LiveResultsSet.prototype, { // - stops the poll timer // - removes us from the invalidation crossbar _.each(self._stopCallbacks, function (c) { c(); }); + Package.facts && Package.facts.Facts.incrementServerFact( + "mongo-livedata", "live-results-sets", -1); // This will cause future _addObserveHandleAndSendInitialAdds calls to // throw. self._observeHandles = null; diff --git a/packages/mongo-livedata/package.js b/packages/mongo-livedata/package.js index ce94ac1c37..400bf7de31 100644 --- a/packages/mongo-livedata/package.js +++ b/packages/mongo-livedata/package.js @@ -30,6 +30,9 @@ Package.on_use(function (api) { // (for questionable reasons) initialized by the webapp package. api.use('webapp', 'server', {weak: true}); + // If the facts package is loaded, publish some statistics. + api.use('facts', 'server', {weak: true}); + // Stuff that should be exposed via a real API, but we haven't yet. api.export('MongoInternals', 'server');