(core) remove metrics

Summary: This removes some old metric code. There's also a user preference dialog that has a single option (whether to allow metrics) this is left in place with a dummy option. It could be ripped out as well, probably.

Test Plan: existing tests pass

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2622
This commit is contained in:
Paul Fitzpatrick
2020-09-29 18:16:29 -04:00
parent e5b67fee7e
commit 2edf64c132
12 changed files with 2 additions and 877 deletions

View File

@@ -8,26 +8,11 @@ export interface GristServerAPI extends
DocListAPI,
LoginSessionAPI,
BasketClientAPI,
ServerMetricsAPI,
UserAPI,
SharingAPI,
MiscAPI {}
interface ServerMetricsAPI {
/**
* Registers the list of client metric names. The calls to pushClientMetrics() send metric
* values as an array parallel to this list of names.
*/
registerClientMetrics(clientMetricsList: string[]): Promise<void>;
/**
* Sends bucketed client metric data to the server. The .values arrays contain one value for
* each of the registered metric names, as a parallel array.
*/
pushClientMetrics(clientBuckets: Array<{startTime: number, values: number[]}>): Promise<void>;
}
interface UserAPI {
/**
* Gets the Grist configuration from the server.

View File

@@ -1,101 +0,0 @@
const _ = require('underscore');
const metricConfig = require('./metricConfig');
const metricTools = require('./metricTools');
const gutil = require('app/common/gutil');
/**
* Base class for metrics collection used by both the server metrics collector, ServerMetrics.js,
* and the client metrics collector, ClientMetrics.js. Should not be instantiated.
* Establishes interval attempts to push metrics to the server on creation.
*/
function MetricsCollector() {
this.startTime = metricTools.getBucketStartTime(Date.now());
this.readyToExport = [];
// used (as a protected member) by the derived ServerMetrics class.
this._collect = setTimeout(() => this.scheduleBucketPreparation(), metricTools.getDeltaMs(Date.now()));
}
// Should return a map from metric names (as entered in metricConfig.js) to their metricTools.
MetricsCollector.prototype.getMetrics = function() {
throw new Error("Not implemented");
};
// Should return a promise that is resolved when the metrics have been pushed.
MetricsCollector.prototype.pushMetrics = function() {
throw new Error("Not implemented");
};
// Should return a bucket of metric data, formatted for either the client or server.
MetricsCollector.prototype.createBucket = function(bucketStart) {
throw new Error("Not implemented");
};
// Takes a list of metrics specifications and creates an object mapping metric names to
// a new instance of the metric gathering tool matching that metric's type.
MetricsCollector.prototype.initMetricTools = function(metricsList) {
var metrics = {};
metricsList.forEach(metricInfo => {
metrics[metricInfo.name] = new metricTools[metricInfo.type](metricInfo.name);
});
return metrics;
};
// Called each push interval.
MetricsCollector.prototype.attemptPush = function() {
this.pushMetrics(this.readyToExport);
this.readyToExport = [];
};
// Pushes bucket to the end of the readyToExport queue. Should be called sequentially, since it
// handles deletion of buckets older than the export memory limit.
MetricsCollector.prototype.queueBucket = function(bucket) {
// If readyToExport is at maximum length, delete the oldest element
this.readyToExport.push(bucket);
var length = this.readyToExport.length;
if (length > metricConfig.MAX_PENDING_BUCKETS) {
this.readyToExport.splice(0, length - metricConfig.MAX_PENDING_BUCKETS);
}
};
MetricsCollector.prototype.scheduleBucketPreparation = function() {
this.prepareCompletedBuckets(Date.now());
this._collect = setTimeout(() => this.scheduleBucketPreparation(), metricTools.getDeltaMs(Date.now()));
};
/**
* Checks if each bucket since the last update is completed and for each one adds all data and
* pushes it to the export ready array.
*/
MetricsCollector.prototype.prepareCompletedBuckets = function(now) {
var bucketStart = metricTools.getBucketStartTime(now);
while (bucketStart > this.startTime) {
this.queueBucket(this.createBucket(this.startTime));
this.startTime += metricConfig.BUCKET_SIZE;
}
};
/**
* Collects primitive metrics tools into a list.
*/
MetricsCollector.prototype.collectPrimitiveMetrics = function() {
var metricTools = [];
_.forEach(this.getMetrics(), metricTool => {
gutil.arrayExtend(metricTools, metricTool.getPrimitiveMetrics());
});
return metricTools;
};
/**
* Loops through metric tools for a chosen bucket and performs the provided callback on each.
* Resets each tool after the callback is performed.
* @param {Number} bucketStart - The desired bucket's start time in milliseconds
* @param {Function} callback - The callback to perform on each metric tool.
*/
MetricsCollector.prototype.forEachBucketMetric = function(bucketEnd, callback) {
this.collectPrimitiveMetrics().forEach(tool => {
callback(tool);
tool.reset(bucketEnd);
});
};
module.exports = MetricsCollector;

View File

@@ -2,7 +2,6 @@
* Interface for the user's config found in config.json.
*/
export interface UserConfig {
enableMetrics?: boolean;
docListSortBy?: string;
docListSortDir?: number;
features?: ISupportedFeatures;

View File

@@ -1,252 +0,0 @@
/**
* File for configuring the metric collection bucket duration, data push intervals between client, server,
* and Grist Metrics EC2 instance, as well as individual metrics collected in the client and server.
*/
// Time interval settings (ms)
exports.BUCKET_SIZE = 60 * 1000;
exports.CLIENT_PUSH_INTERVAL = 120 * 1000;
exports.SERVER_PUSH_INTERVAL = 120 * 1000;
exports.MAX_PENDING_BUCKETS = 40;
exports.CONN_RETRY = 20 * 1000;
// Metrics use the general form:
// <category>.<short desc>
// With prefixes, measurement type, and clientId/serverId added automatically on send.
// 'type' is the measurement tool type, with options 'Switch', 'Counter', 'Gauge', 'Timer', and
// 'ExecutionTimer'. (See metricTools.js for details)
// Suffixes are added to the metric names depending on their measurement tool.
// 'Switch' => '.instances'
// 'Gauge' => '.total'
// 'Counter' => '.count'
// 'Timer' => '.time'
// 'ExecutionTimer' => '.execution_time', '.count' (Execution timer automatically records a count)
exports.clientMetrics = [
// General
{
name: 'sidepane.opens',
type: 'Counter',
desc: 'Number of times the side pane is opened'
},
{
name: 'app.client_active_span',
type: 'Timer',
desc: 'Total client time spent using grist'
},
{
name: 'app.connected_to_server_span',
type: 'Timer',
desc: 'Total time spent connected to the server'
},
{
name: 'app.disconnected_from_server_span',
type: 'Timer',
desc: 'Total time spent disconnected from the server'
},
// Docs
{
name: 'docs.num_open_6+_tables',
type: 'SamplingGauge',
desc: 'Number of open docs with more than 5 tables'
},
{
name: 'docs.num_open_0-5_tables',
type: 'SamplingGauge',
desc: 'Number of open docs with 0-5 tables'
},
// Tables
{
name: 'tables.num_tables',
type: 'SamplingGauge',
desc: 'Number of open tables'
},
{
name: 'tables.num_summary_tables',
type: 'SamplingGauge',
desc: 'Number of open sections in the current view'
},
// Views
{
name: 'views.code_view_open_span',
type: 'Timer',
desc: 'Time spent with code viewer open'
},
// Sections
{
name: 'sections.grid_open_span',
type: 'Timer',
desc: 'Time spent with gridview open'
},
{
name: 'sections.detail_open_span',
type: 'Timer',
desc: 'Time spent with gridview open'
},
{
name: 'sections.num_grid_sections',
type: 'SamplingGauge',
desc: 'Number of open sections in the current view'
},
{
name: 'sections.num_detail_sections',
type: 'SamplingGauge',
desc: 'Number of open sections in the current view'
},
{
name: 'sections.num_chart_sections',
type: 'SamplingGauge',
desc: 'Number of open sections in the current view'
},
{
name: 'sections.multiple_open_span',
type: 'Timer',
desc: 'Time spent with multiple sections open'
},
// Performance
{
name: 'performance.server_action',
type: 'ExecutionTimer',
desc: 'Time for a server action to complete'
},
{
name: 'performance.doc_load',
type: 'ExecutionTimer',
desc: 'Time to load a document'
},
// Columns
{
name: 'cols.num_formula_cols',
type: 'SamplingGauge',
desc: 'Number of formula columns in open documents'
},
{
name: 'cols.num_text_cols',
type: 'SamplingGauge',
desc: 'Number of text columns in open documents'
},
{
name: 'cols.num_int_cols',
type: 'SamplingGauge',
desc: 'Number of integer columns in open documents'
},
{
name: 'cols.num_numeric_cols',
type: 'SamplingGauge',
desc: 'Number of numeric columns in open documents'
},
{
name: 'cols.num_date_cols',
type: 'SamplingGauge',
desc: 'Number of date columns in open documents'
},
{
name: 'cols.num_datetime_cols',
type: 'SamplingGauge',
desc: 'Number of datetime columns in open documents'
},
{
name: 'cols.num_ref_cols',
type: 'SamplingGauge',
desc: 'Number of reference columns in open documents'
},
{
name: 'cols.num_attachments_cols',
type: 'SamplingGauge',
desc: 'Number of attachments columns in open documents'
},
{
name: 'performance.front_end_errors',
type: 'Counter',
desc: 'Number of frontend errors'
}
// TODO: Implement the following:
// {
// name: 'grist-rt.performance.view_swap',
// type: 'ExecutionTimer',
// desc: 'Time to swap views'
// }
];
exports.serverMetrics = [
// General
{
name: 'app.server_active',
type: 'Switch',
desc: 'Number of users currently using grist'
},
{
name: 'app.server_active_span',
type: 'Timer',
desc: 'Total server time spent using grist'
},
{
name: 'app.have_doc_open',
type: 'Switch',
desc: 'Number of users with at least one doc open'
},
{
name: 'app.doc_open_span',
type: 'Timer',
desc: 'Total time spent with at least one doc open'
},
// Docs
{
name: 'docs.num_open',
type: 'Gauge',
desc: 'Number of open docs'
},
{
name: 'performance.node_memory_usage',
type: 'SamplingGauge',
desc: 'Memory utilization in bytes of the node process'
}
// TODO: Implement the following:
// {
// name: 'grist-rt.docs.total_size_open',
// type: 'Gauge',
// desc: 'Cumulative size of open docs'
// }
// {
// name: 'grist-rt.performance.open_standalone_app',
// type: 'ExecutionTimer',
// desc: 'Time to start standalone app'
// }
// {
// name: 'grist-rt.performance.sandbox_recalculation',
// type: 'ExecutionTimer',
// desc: 'Time for sandbox recalculation to occur'
// }
// {
// name: 'grist-rt.performance.open_standalone_app',
// type: 'ExecutionTimer',
// desc: 'Time to start standalone app'
// }
// {
// name: 'grist-rt.performance.node_cpu_usage',
// type: 'SamplingGauge',
// desc: 'Amount of time node was using the cpu in the interval'
// }
// {
// name: 'grist-rt.performance.sandbox_cpu_usage',
// type: 'SamplingGauge',
// desc: 'Amount of time the sandbox was using the cpu in the interval'
// }
// {
// name: 'grist-rt.performance.chrome_cpu_usage',
// type: 'SamplingGauge',
// desc: 'Amount of time chrome was using the cpu in the interval'
// }
// {
// name: 'grist-rt.performance.sandbox_memory_usage',
// type: 'SamplingGauge',
// desc: 'Memory utilization in bytes of the sandbox process'
// }
// {
// name: 'grist-rt.performance.chrome_memory_usage',
// type: 'SamplingGauge',
// desc: 'Memory utilization in bytes of the chrome process'
// }
];

View File

@@ -1,261 +0,0 @@
const _ = require('underscore');
const gutil = require('./gutil');
const metricConfig = require('./metricConfig');
// TODO: Create a metric test class and write tests for each metric tool.
/**
* Base class for tools to gather metrics. Should not be instantiated.
*/
function MetricTool(name) {
this.name = name;
}
// Should be implemented by extending classes
MetricTool.prototype._getSuffix = function() {
throw new Error("Not implemented");
};
// Should be overridden by extending classes depending on desired reset behavior
MetricTool.prototype.reset = _.noop;
// Returns the name of the metric with its suffix appended to the end.
// NOTE: Should return names in the same order as getValues.
MetricTool.prototype.getName = function() {
return this.name + '.' + this._getSuffix();
};
// Should be implemented by extending classes. Returns the value of the tool for a bucket.
// @param {Number} bucketEndTime - The desired bucket's end time in milliseconds
MetricTool.prototype.getValue = function(bucketEndTime) {
throw new Error("Not implemented");
};
// Returns a list of all primitive metrics this tool is made up of.
// Only requires overridding by non-primitive metrics.
MetricTool.prototype.getPrimitiveMetrics = function() {
return [this];
};
/**
* Counts the number of times an event has occurred in the current bucket.
*/
function Counter(name) {
MetricTool.call(this, name);
this.val = 0;
}
_.extend(Counter.prototype, MetricTool.prototype);
Counter.prototype.inc = function() {
this.val += 1;
};
Counter.prototype._getSuffix = function() {
return 'count';
};
Counter.prototype.getValue = function(bucketEndTime) {
// If the bucket is more recent than the last one where counting occurred, return 0
return this.val;
};
Counter.prototype.reset = function(bucketEndTime) {
this.val = 0;
};
exports.Counter = Counter;
/**
* Keeps track of a count that persists across buckets.
*/
function Gauge(name) {
MetricTool.call(this, name);
this.val = null;
}
_.extend(Gauge.prototype, MetricTool.prototype);
Gauge.prototype.set = function(num) {
this.val = num;
};
Gauge.prototype.inc = function() {
this.val = (this.val ? this.val + 1 : 1);
};
Gauge.prototype.dec = function() {
this.val -= 1;
};
Gauge.prototype._getSuffix = function() {
return 'total';
};
Gauge.prototype.getValue = function(bucketEndTime) {
return this.val;
};
exports.Gauge = Gauge;
/**
* A gauge that pulls samples using a callback function
*/
function SamplingGauge(name) {
MetricTool.call(this, name);
this.callback = _.constant(null);
}
_.extend(SamplingGauge.prototype, MetricTool.prototype);
SamplingGauge.prototype.assignCallback = function(callback) {
this.callback = callback;
};
SamplingGauge.prototype._getSuffix = function() {
return 'total';
};
SamplingGauge.prototype.getValue = function(bucketEndTime) {
return this.callback();
};
exports.SamplingGauge = SamplingGauge;
/**
* Keeps track of whether or not a certain condition is met. Useful for statistics
* which measure the number of users who meet a certain criteria. Persists across buckets.
*/
function Switch(name) {
MetricTool.call(this, name);
this.val = null;
}
_.extend(Switch.prototype, Gauge.prototype);
Switch.prototype.set = function(bool) {
this.val = bool ? 1 : 0;
};
Switch.prototype._getSuffix = function() {
return 'instances';
};
Switch.prototype.getValue = function(bucketEndTime) {
return this.val;
};
exports.Switch = Switch;
/**
* Keeps track of the amount of time in each bucket that an event is occurring (ms).
*/
function Timer(name) {
MetricTool.call(this, name);
this.val = 0; // The sum of all runtimes in the last updated bucket
this.startTime = 0; // The time (in ms since the bucket started) when the timer was started
this.running = false;
}
_.extend(Timer.prototype, MetricTool.prototype);
Timer.prototype.setRunning = function(bool) {
return bool ? this.start() : this.stop();
};
Timer.prototype.start = function() {
if (this.running) {
return;
}
// Record start time and set to running
this.startTime = Date.now();
this.running = true;
};
Timer.prototype.stop = function() {
if (!this.running) {
return;
}
// Add time since start to value and set running to false
var stopTime = Date.now();
this.val += stopTime - this.startTime;
this.running = false;
};
Timer.prototype._getSuffix = function() {
return 'time';
};
Timer.prototype.getValue = function(bucketEndTime) {
// Add the value and the time to the end of the bucket if the timer is running
return this.val + (this.running ? Math.max(0, bucketEndTime - this.startTime) : 0);
};
Timer.prototype.reset = function(bucketEndTime) {
this.val = 0;
this.startTime = Math.max(bucketEndTime, this.startTime);
};
exports.Timer = Timer;
/**
* Keeps track of the amount of time in an event takes, and the number of times that event occurs (ms).
*/
function ExecutionTimer(name) {
MetricTool.call(this, name);
this.startTime = 0; // The last time (in ms) the timer was started
this.val = 0;
this.running = false;
// Counter keeps track of the total number of executions in the current bucket.
// An execution is in a bucket if it ended in that bucket.
this.counter = new Counter(name);
}
_.extend(ExecutionTimer.prototype, MetricTool.prototype);
ExecutionTimer.prototype.setRunning = function(bool) {
return bool ? this.start() : this.stop();
};
ExecutionTimer.prototype.start = function() {
if (this.running) {
return;
}
this.startTime = Date.now();
this.running = true;
};
ExecutionTimer.prototype.stop = function() {
if (!this.running) {
return;
}
var stopTime = Date.now();
this.val += stopTime - this.startTime;
this.counter.inc();
this.running = false;
};
ExecutionTimer.prototype._getSuffix = function() {
return 'execution_time';
};
ExecutionTimer.prototype.getValue = function(bucketEndTime) {
return this.val;
};
ExecutionTimer.prototype.reset = function(bucketEndTime) {
this.val = 0;
this.counter.reset();
};
ExecutionTimer.prototype.getPrimitiveMetrics = function() {
return [this, this.counter];
};
exports.ExecutionTimer = ExecutionTimer;
// Returns the time rounded down to the start of the current bucket's time window (in ms).
function getBucketStartTime(now) {
return gutil.roundDownToMultiple(now, metricConfig.BUCKET_SIZE);
}
exports.getBucketStartTime = getBucketStartTime;
// Returns the time until the start of the next bucket (in ms).
function getDeltaMs(now) {
return getBucketStartTime(now) + metricConfig.BUCKET_SIZE - now;
}
exports.getDeltaMs = getDeltaMs;

View File

@@ -61,13 +61,6 @@ export function getInitialDocAssignment(): string|null {
return getGristConfig().assignmentId || null;
}
// Return true if we are on a page that can send metrics.
// TODO: all pages should send suitable metrics.
export function pageHasMetrics(): boolean {
// No metric support on hosted grist.
return !getGristConfig().homeUrl;
}
// Return true if we are on a page that can supply a doc list.
// TODO: the doclist object isn't relevant to hosted grist and should be factored out.
export function pageHasDocList(): boolean {