Skip to content
This repository has been archived by the owner on Oct 3, 2023. It is now read-only.

Commit

Permalink
Performance timing recording and span conversion (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
draffensperger authored Feb 14, 2019
1 parent 93d6a7a commit 89561ae
Show file tree
Hide file tree
Showing 12 changed files with 1,223 additions and 7 deletions.
14 changes: 13 additions & 1 deletion packages/opencensus-web-core/src/trace/model/attribute-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ export const ATTRIBUTE_HTTP_RESP_ENCODED_BODY_SIZE =
export const ATTRIBUTE_HTTP_RESP_DECODED_BODY_SIZE =
`${HTTP_PREFIX}resp_decoded_body_size`;

/** Attribute prefix for spans that represent navigations in the browser. */
const NAVIGATION_PREFIX = 'nav.';

/**
* The type of browser navigation. See
* https://www.w3.org/TR/navigation-timing-2/#sec-performance-navigation-types
Expand All @@ -81,3 +81,15 @@ export const ATTRIBUTE_NAV_TYPE = `${NAVIGATION_PREFIX}type`;
*/
export const ATTRIBUTE_NAV_REDIRECT_COUNT =
`${NAVIGATION_PREFIX}redirect_count`;

/**
* Attribute prefix for spans that are for "long tasks" (long JS event loops).
* See https://www.w3.org/TR/longtasks/
*/
export const LONG_TASK_PREFIX = 'long_task.';
/**
* A JSON string of the `attribution` field of a long task timing. This gives
* a little additional information about what on the page may have caused the
* long task.
*/
export const ATTRIBUTE_LONG_TASK_ATTRIBUTION = `${LONG_TASK_PREFIX}attribution`;
6 changes: 3 additions & 3 deletions packages/opencensus-web-core/src/trace/model/span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@
*/

import * as coreTypes from '@opencensus/core';

import {LOGGER} from '../../common/console-logger';
import {getDateForPerfTime} from '../../common/time-util';
import {randomSpanId} from '../../internal/util';

import {MessageEvent, SpanKind} from './types';

/** Default span name if none is specified. */
const DEFAULT_SPAN_NAME = 'unnamed';

/** A span represents a single operation within a trace. */
export class Span implements coreTypes.Span {
id = randomSpanId();
constructor(
/** The ID of this span. Defaults to a random span ID. */
public id = randomSpanId()) {}

/** If the parent span is in another process. */
remoteParent = false;
Expand Down
5 changes: 5 additions & 0 deletions packages/opencensus-web-core/test/test-span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ describe('Span', () => {
expect(span.id).toMatch('^[a-z0-9]{16}$');
});

it('allows initializing id in constructor', () => {
const span = new Span('000000000000000b');
expect(span.id).toBe('000000000000000b');
});

it('calculates time fields based on startPerfTime/endPerfTime', () => {
expect(span.ended).toBe(false);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* Copyright 2019, OpenCensus Authors
*
* 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 {Annotation, ATTRIBUTE_HTTP_URL, ATTRIBUTE_HTTP_USER_AGENT, ATTRIBUTE_LONG_TASK_ATTRIBUTION, ATTRIBUTE_NAV_TYPE, parseUrl, RootSpan, Span, SpanKind, Tracer} from '@opencensus/web-core';
import {GroupedPerfEntries} from './perf-recorder';
import {PerformanceLongTaskTiming, PerformanceNavigationTimingExtended} from './perf-types';
import {getResourceSpan} from './resource-span';
import {annotationsForPerfTimeFields} from './util';

/**
* These are properties of PerformanceNavigationTiming that will be turned
* into span annotations on the navigation span.
*/
const NAVIGATION_TIMING_EVENTS = [
'domLoading',
'domInteractive',
'domContentLoaded',
'domComplete',
'loadEventStart',
'loadEventEnd',
'unloadEventStart',
'unloadEventEnd',
];

/**
* Returns a root span for the initial page load along with child spans for
* resource timings and long tasks that were part of the load. The root span
* represents the full time between when the navigation was initiated in the
* browser to when the `load` event fires.
* @param tracer The tracer to associate the root span with
* @param perfEntries Performance timing entries grouped by type. These should
* include the entries up to soon after the `load` browser event fires.
* @param navigationFetchSpanId This is the ID for the span that represents the
* HTTP request to get the initial HTML. This should be sent back from the
* server to enable linking the server and client side spans for the initial
* HTML fetch.
* @param traceId The trace ID that all spans returned should have. This should
* also be specified by the server to enable linking between the server and
* client spans for the initial HTML fetch.
*/
export function getInitialLoadRootSpan(
tracer: Tracer, perfEntries: GroupedPerfEntries,
navigationFetchSpanId: string, traceId: string): RootSpan {
const navTiming = perfEntries.navigationTiming;
const navigationUrl = navTiming ? navTiming.name : location.href;
const parsedNavigationUrl = parseUrl(navigationUrl);
const navigationPath = parsedNavigationUrl.pathname;
const root = new RootSpan(tracer, {
name: `Nav.${navigationPath}`,
spanContext: {
traceId,
// This becomes the parentSpanId field of the root span, and the actual
// span ID for the root span gets assigned to a random number.
spanId: '',
},
kind: SpanKind.UNSPECIFIED,
});
root.startPerfTime = 0;
root.annotations = getNavigationAnnotations(perfEntries);
root.attributes[ATTRIBUTE_HTTP_URL] = navigationUrl;
root.attributes[ATTRIBUTE_HTTP_USER_AGENT] = navigator.userAgent;

if (navTiming) {
root.endPerfTime = navTiming.loadEventEnd;
root.attributes[ATTRIBUTE_NAV_TYPE] = navTiming.type;
const navFetchSpan = getNavigationFetchSpan(
navTiming, navigationUrl, traceId, root.id, navigationFetchSpanId);
root.spans.push(navFetchSpan);
}

const resourceSpans = perfEntries.resourceTimings.map(
(resourceTiming) => getResourceSpan(resourceTiming, traceId, root.id));
const longTaskSpans = perfEntries.longTasks.map(
(longTaskTiming) => getLongTaskSpan(longTaskTiming, traceId, root.id));

root.spans = root.spans.concat(resourceSpans, longTaskSpans);
return root;
}

/** Returns a parent span for the HTTP request to retrieve the initial HTML. */
function getNavigationFetchSpan(
navigationTiming: PerformanceNavigationTimingExtended,
navigationName: string, traceId: string, parentSpanId: string,
spanId: string): Span {
const span = getResourceSpan(navigationTiming, traceId, parentSpanId, spanId);
span.startPerfTime = navigationTiming.fetchStart;
return span;
}

/** Formats a performance long task event as a span. */
function getLongTaskSpan(
longTask: PerformanceLongTaskTiming, traceId: string,
parentSpanId: string): Span {
const span = new Span();
span.traceId = traceId;
span.parentSpanId = parentSpanId;
span.name = 'Long JS task';
span.startPerfTime = longTask.startTime;
span.endPerfTime = longTask.startTime + longTask.duration;
span.attributes[ATTRIBUTE_LONG_TASK_ATTRIBUTION] =
JSON.stringify(longTask.attribution);
return span;
}

/** Gets annotations for a navigation span including paint timings. */
function getNavigationAnnotations(perfEntries: GroupedPerfEntries):
Annotation[] {
const navigation = perfEntries.navigationTiming;
if (!navigation) return [];

const navAnnotations =
annotationsForPerfTimeFields(navigation, NAVIGATION_TIMING_EVENTS);

for (const paintTiming of perfEntries.paintTimings) {
navAnnotations.push({
timestamp: paintTiming.startTime,
description: paintTiming.name,
attributes: {},
});
}
return navAnnotations;
}
98 changes: 98 additions & 0 deletions packages/opencensus-web-instrumentation-perf/src/perf-recorder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* Copyright 2019, OpenCensus Authors
*
* 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 {PerformanceLongTaskTiming, PerformanceNavigationTimingExtended, PerformanceObserverEntryList, PerformancePaintTiming, PerformanceResourceTimingExtended, WindowWithPerformanceObserver} from './perf-types';

/** Cast `window` to have PerformanceObserver. */
const windowWithPerfObserver = window as WindowWithPerformanceObserver;

/** Store long task performance entries recorded with PerformanceObserver. */
const longTasks: PerformanceLongTaskTiming[] = [];

/** How big to set the performance timing buffer so timings aren't lost. */
const RESOURCE_TIMING_BUFFER_SIZE = 2000;

/** Represent all collected performance timings grouped by type. */
export interface GroupedPerfEntries {
navigationTiming?: PerformanceNavigationTimingExtended;
paintTimings: PerformancePaintTiming[];
resourceTimings: PerformanceResourceTimingExtended[];
longTasks: PerformanceLongTaskTiming[];
}

/**
* Begin recording performance timings. This starts tracking `longtask` timings
* and also increases the resource timing buffer size.
*/
export function recordPerfEntries() {
if (!windowWithPerfObserver.performance) return;

if (performance.setResourceTimingBufferSize) {
performance.setResourceTimingBufferSize(RESOURCE_TIMING_BUFFER_SIZE);
}

if (windowWithPerfObserver.PerformanceObserver) {
const longTaskObserver =
new windowWithPerfObserver.PerformanceObserver(onLongTasks);
longTaskObserver.observe({entryTypes: ['longtask']});
}
}

function onLongTasks(entryList: PerformanceObserverEntryList) {
// These must be PerformanceLongTaskTiming objects because we only observe
// 'longtask' above.
longTasks.push(...(entryList.getEntries() as PerformanceLongTaskTiming[]));
}

/** Returns the recorded performance entries but does not clear them. */
export function getPerfEntries(): GroupedPerfEntries {
if (!windowWithPerfObserver.performance) {
return {
resourceTimings: [],
longTasks: [],
paintTimings: [],
};
}

const perf = windowWithPerfObserver.performance;

const entries: GroupedPerfEntries = {
resourceTimings: perf.getEntriesByType('resource') as
PerformanceResourceTimingExtended[],
paintTimings: perf.getEntriesByType('paint') as PerformancePaintTiming[],
longTasks: longTasks.slice(),
};

const navEntries = perf.getEntriesByType('navigation');
if (navEntries.length) {
entries.navigationTiming =
navEntries[0] as PerformanceNavigationTimingExtended;
}

return entries;
}

/** Clears resource timings, marks, measures and stored long task timings. */
export function clearPerfEntries() {
if (!windowWithPerfObserver.performance) return;
longTasks.length = 0;
windowWithPerfObserver.performance.clearResourceTimings();
windowWithPerfObserver.performance.clearMarks();
windowWithPerfObserver.performance.clearMeasures();
}

/** Expose the resource timing buffer size for unit test. */
export const TEST_ONLY = {RESOURCE_TIMING_BUFFER_SIZE};
Loading

0 comments on commit 89561ae

Please sign in to comment.