voluptasmollitia
Version:
Monorepo for the Firebase JavaScript SDK
341 lines (315 loc) • 10.7 kB
text/typescript
/**
* @license
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
TRACE_START_MARK_PREFIX,
TRACE_STOP_MARK_PREFIX,
TRACE_MEASURE_PREFIX,
OOB_TRACE_PAGE_LOAD_PREFIX,
FIRST_PAINT_COUNTER_NAME,
FIRST_CONTENTFUL_PAINT_COUNTER_NAME,
FIRST_INPUT_DELAY_COUNTER_NAME
} from '../constants';
import { Api } from '../services/api_service';
import { logTrace } from '../services/perf_logger';
import { ERROR_FACTORY, ErrorCode } from '../utils/errors';
import {
isValidCustomAttributeName,
isValidCustomAttributeValue
} from '../utils/attributes_utils';
import {
isValidMetricName,
convertMetricValueToInteger
} from '../utils/metric_utils';
import { PerformanceTrace } from '@firebase/performance-types';
const enum TraceState {
UNINITIALIZED = 1,
RUNNING,
TERMINATED
}
export class Trace implements PerformanceTrace {
private state: TraceState = TraceState.UNINITIALIZED;
startTimeUs!: number;
durationUs!: number;
private customAttributes: { [key: string]: string } = {};
counters: { [counterName: string]: number } = {};
private api = Api.getInstance();
private randomId = Math.floor(Math.random() * 1000000);
private traceStartMark!: string;
private traceStopMark!: string;
private traceMeasure!: string;
/**
* @param name The name of the trace.
* @param isAuto If the trace is auto-instrumented.
* @param traceMeasureName The name of the measure marker in user timing specification. This field
* is only set when the trace is built for logging when the user directly uses the user timing
* api (performance.mark and performance.measure).
*/
constructor(
readonly name: string,
readonly isAuto = false,
traceMeasureName?: string
) {
if (!this.isAuto) {
this.traceStartMark = `${TRACE_START_MARK_PREFIX}-${this.randomId}-${this.name}`;
this.traceStopMark = `${TRACE_STOP_MARK_PREFIX}-${this.randomId}-${this.name}`;
this.traceMeasure =
traceMeasureName ||
`${TRACE_MEASURE_PREFIX}-${this.randomId}-${this.name}`;
if (traceMeasureName) {
// For the case of direct user timing traces, no start stop will happen. The measure object
// is already available.
this.calculateTraceMetrics();
}
}
}
/**
* Starts a trace. The measurement of the duration starts at this point.
*/
start(): void {
if (this.state !== TraceState.UNINITIALIZED) {
throw ERROR_FACTORY.create(ErrorCode.TRACE_STARTED_BEFORE, {
traceName: this.name
});
}
this.api.mark(this.traceStartMark);
this.state = TraceState.RUNNING;
}
/**
* Stops the trace. The measurement of the duration of the trace stops at this point and trace
* is logged.
*/
stop(): void {
if (this.state !== TraceState.RUNNING) {
throw ERROR_FACTORY.create(ErrorCode.TRACE_STOPPED_BEFORE, {
traceName: this.name
});
}
this.state = TraceState.TERMINATED;
this.api.mark(this.traceStopMark);
this.api.measure(
this.traceMeasure,
this.traceStartMark,
this.traceStopMark
);
this.calculateTraceMetrics();
logTrace(this);
}
/**
* Records a trace with predetermined values. If this method is used a trace is created and logged
* directly. No need to use start and stop methods.
* @param startTime Trace start time since epoch in millisec
* @param duration The duraction of the trace in millisec
* @param options An object which can optionally hold maps of custom metrics and custom attributes
*/
record(
startTime: number,
duration: number,
options?: {
metrics?: { [key: string]: number };
attributes?: { [key: string]: string };
}
): void {
if (startTime <= 0) {
throw ERROR_FACTORY.create(ErrorCode.NONPOSITIVE_TRACE_START_TIME, {
traceName: this.name
});
}
if (duration <= 0) {
throw ERROR_FACTORY.create(ErrorCode.NONPOSITIVE_TRACE_DURATION, {
traceName: this.name
});
}
this.durationUs = Math.floor(duration * 1000);
this.startTimeUs = Math.floor(startTime * 1000);
if (options && options.attributes) {
this.customAttributes = { ...options.attributes };
}
if (options && options.metrics) {
for (const metric of Object.keys(options.metrics)) {
if (!isNaN(Number(options.metrics[metric]))) {
this.counters[metric] = Number(Math.floor(options.metrics[metric]));
}
}
}
logTrace(this);
}
/**
* Increments a custom metric by a certain number or 1 if number not specified. Will create a new
* custom metric if one with the given name does not exist. The value will be floored down to an
* integer.
* @param counter Name of the custom metric
* @param numAsInteger Increment by value
*/
incrementMetric(counter: string, numAsInteger = 1): void {
if (this.counters[counter] === undefined) {
this.putMetric(counter, numAsInteger);
} else {
this.putMetric(counter, this.counters[counter] + numAsInteger);
}
}
/**
* Sets a custom metric to a specified value. Will create a new custom metric if one with the
* given name does not exist. The value will be floored down to an integer.
* @param counter Name of the custom metric
* @param numAsInteger Set custom metric to this value
*/
putMetric(counter: string, numAsInteger: number): void {
if (isValidMetricName(counter, this.name)) {
this.counters[counter] = convertMetricValueToInteger(numAsInteger);
} else {
throw ERROR_FACTORY.create(ErrorCode.INVALID_CUSTOM_METRIC_NAME, {
customMetricName: counter
});
}
}
/**
* Returns the value of the custom metric by that name. If a custom metric with that name does
* not exist will return zero.
* @param counter
*/
getMetric(counter: string): number {
return this.counters[counter] || 0;
}
/**
* Sets a custom attribute of a trace to a certain value.
* @param attr
* @param value
*/
putAttribute(attr: string, value: string): void {
const isValidName = isValidCustomAttributeName(attr);
const isValidValue = isValidCustomAttributeValue(value);
if (isValidName && isValidValue) {
this.customAttributes[attr] = value;
return;
}
// Throw appropriate error when the attribute name or value is invalid.
if (!isValidName) {
throw ERROR_FACTORY.create(ErrorCode.INVALID_ATTRIBUTE_NAME, {
attributeName: attr
});
}
if (!isValidValue) {
throw ERROR_FACTORY.create(ErrorCode.INVALID_ATTRIBUTE_VALUE, {
attributeValue: value
});
}
}
/**
* Retrieves the value a custom attribute of a trace is set to.
* @param attr
*/
getAttribute(attr: string): string | undefined {
return this.customAttributes[attr];
}
removeAttribute(attr: string): void {
if (this.customAttributes[attr] === undefined) {
return;
}
delete this.customAttributes[attr];
}
getAttributes(): { [key: string]: string } {
return { ...this.customAttributes };
}
private setStartTime(startTime: number): void {
this.startTimeUs = startTime;
}
private setDuration(duration: number): void {
this.durationUs = duration;
}
/**
* Calculates and assigns the duration and start time of the trace using the measure performance
* entry.
*/
private calculateTraceMetrics(): void {
const perfMeasureEntries = this.api.getEntriesByName(this.traceMeasure);
const perfMeasureEntry = perfMeasureEntries && perfMeasureEntries[0];
if (perfMeasureEntry) {
this.durationUs = Math.floor(perfMeasureEntry.duration * 1000);
this.startTimeUs = Math.floor(
(perfMeasureEntry.startTime + this.api.getTimeOrigin()) * 1000
);
}
}
/**
* @param navigationTimings A single element array which contains the navigationTIming object of
* the page load
* @param paintTimings A array which contains paintTiming object of the page load
* @param firstInputDelay First input delay in millisec
*/
static createOobTrace(
navigationTimings: PerformanceNavigationTiming[],
paintTimings: PerformanceEntry[],
firstInputDelay?: number
): void {
const route = Api.getInstance().getUrl();
if (!route) {
return;
}
const trace = new Trace(OOB_TRACE_PAGE_LOAD_PREFIX + route, true);
const timeOriginUs = Math.floor(Api.getInstance().getTimeOrigin() * 1000);
trace.setStartTime(timeOriginUs);
// navigationTimings includes only one element.
if (navigationTimings && navigationTimings[0]) {
trace.setDuration(Math.floor(navigationTimings[0].duration * 1000));
trace.putMetric(
'domInteractive',
Math.floor(navigationTimings[0].domInteractive * 1000)
);
trace.putMetric(
'domContentLoadedEventEnd',
Math.floor(navigationTimings[0].domContentLoadedEventEnd * 1000)
);
trace.putMetric(
'loadEventEnd',
Math.floor(navigationTimings[0].loadEventEnd * 1000)
);
}
const FIRST_PAINT = 'first-paint';
const FIRST_CONTENTFUL_PAINT = 'first-contentful-paint';
if (paintTimings) {
const firstPaint = paintTimings.find(
paintObject => paintObject.name === FIRST_PAINT
);
if (firstPaint && firstPaint.startTime) {
trace.putMetric(
FIRST_PAINT_COUNTER_NAME,
Math.floor(firstPaint.startTime * 1000)
);
}
const firstContentfulPaint = paintTimings.find(
paintObject => paintObject.name === FIRST_CONTENTFUL_PAINT
);
if (firstContentfulPaint && firstContentfulPaint.startTime) {
trace.putMetric(
FIRST_CONTENTFUL_PAINT_COUNTER_NAME,
Math.floor(firstContentfulPaint.startTime * 1000)
);
}
if (firstInputDelay) {
trace.putMetric(
FIRST_INPUT_DELAY_COUNTER_NAME,
Math.floor(firstInputDelay * 1000)
);
}
}
logTrace(trace);
}
static createUserTimingTrace(measureName: string): void {
const trace = new Trace(measureName, false, measureName);
logTrace(trace);
}
}