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');