Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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
  • Loading branch information...
commit b57f1c4d65298a6223384b0536239d934ba20bc1 1 parent 9b58e3a
@glasser glasser authored
View
14 packages/facts/facts.html
@@ -0,0 +1,14 @@
+<template name='serverFacts'>
+ <ul>
+ {{#each factsByPackage}}
+ <li>{{ _id }}
+ <dl>
+ {{#each facts}}
+ <dt>{{ name }}</dt>
+ <dd>{{ value }}</dd>
+ {{/each}}
+ </dl>
+ </li>
+ {{/each}}
+ </ul>
+</template>
View
79 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;
+ };
+}
View
21 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');
+});
+
View
4 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];
}
};
View
11 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 () {
View
3  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');
View
11 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;
View
3  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');
Please sign in to comment.
Something went wrong with that request. Please try again.