Skip to content

[WIP] React Profiler Experiments #2660

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 102 additions & 4 deletions packages/react/src/profiler.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getCurrentHub } from '@sentry/browser';
import { Integration, IntegrationClass } from '@sentry/types';
import { logger } from '@sentry/utils';
import { Integration, IntegrationClass, SpanContext, Transaction } from '@sentry/types';
import { parseSemver, logger, SemVer, timestampWithMs } from '@sentry/utils';
import * as hoistNonReactStatic from 'hoist-non-react-statics';
import * as React from 'react';

Expand Down Expand Up @@ -39,14 +39,34 @@ function afterNextFrame(callback: Function): void {
timeout = window.setTimeout(done, 100);
}

/**
* isProfilingModeOn tells us if the React.Profiler will correctly
* Profile it's components. This is only active in development mode
* and in profiling mode.
*
* Learn how to do that here: https://gist.github.com/bvaughn/25e6233aeb1b4f0cdb8d8366e54a3977
*/
function isProfilingModeOn(): boolean {
const fake = React.createElement('div') as any;

// tslint:disable-next-line: triple-equals no-unsafe-any
if (fake._owner != null && fake._owner.actualDuration != null) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sucks, but I can't find a better way to figure out if a user can profile or not.

// if the component has a valid owner, and that owner has a duration
// React is profiling all it's components
return true;
}

return false;
}

const getInitActivity = (name: string): number | null => {
const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);

if (tracingIntegration !== null) {
// tslint:disable-next-line:no-unsafe-any
return (tracingIntegration as any).constructor.pushActivity(name, {
description: `<${name}>`,
op: 'react',
op: 'react.mount',
});
}

Expand All @@ -62,20 +82,75 @@ export type ProfilerProps = {

class Profiler extends React.Component<ProfilerProps> {
public activity: number | null;
public hasProfilingMode: boolean | null = null;
public reactVersion: SemVer = parseSemver(React.version);

public constructor(props: ProfilerProps) {
super(props);

this.activity = getInitActivity(this.props.name);
}

public componentDidMount(): void {
afterNextFrame(this.finishProfile);
if (!this.hasProfilingMode) {
afterNextFrame(this.finishProfile);
}
}

public componentWillUnmount(): void {
afterNextFrame(this.finishProfile);
}

/**
*
* React calls handleProfilerRender() any time a component within the profiled
* tree “commits” an update.
*
*/
public handleProfilerRender = (
// The id prop of the Profiler tree that has just committed
id: string,
// Identifies whether the tree has just been mounted for the first time
// or re-rendered due to a change in props, state, or hooks
phase: 'mount' | 'update',
// Time spent rendering the Profiler and its descendants for the current update
actualDuration: number,
// Duration of the most recent render time for each individual component within the Profiler tree
_baseDuration: number,
// Timestamp when React began rendering the current update
// pageload = startTime of 0
_startTime: number,
// Timestamp when React committed the current update
_commitTime: number,
) => {
if (phase === 'mount') {
afterNextFrame(this.finishProfile);
}

const componentName = this.props.name === UNKNOWN_COMPONENT ? id : this.props.name;

const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
if (tracingIntegration !== null) {
// tslint:disable-next-line: no-unsafe-any
const activeTransaction = (tracingIntegration as any).constructor._activeTransaction as Transaction;
console.table({ id, phase, actualDuration, _baseDuration, _startTime, _commitTime });
console.log(activeTransaction);

if (activeTransaction) {
const now = timestampWithMs();
const spanContext: SpanContext = {
description: `<${componentName}>`,
op: 'react.update',
startTimestamp: now - actualDuration,
};

const span = activeTransaction.startChild(spanContext);

span.finish(now);
}
}
};

public finishProfile = () => {
if (!this.activity) {
return;
Expand All @@ -90,6 +165,29 @@ class Profiler extends React.Component<ProfilerProps> {
};

public render(): React.ReactNode {
const { name } = this.props;

if (
// React <= v16.4
(this.reactVersion.major && this.reactVersion.major <= 15) ||
(this.reactVersion.major === 16 && this.reactVersion.minor && this.reactVersion.minor <= 4)
) {
return this.props.children;
}

if (this.hasProfilingMode === null) {
// TODO: This should be a global check
this.hasProfilingMode = isProfilingModeOn();
}

if (this.hasProfilingMode) {
return (
<React.Profiler id={name} onRender={this.handleProfilerRender}>
{this.props.children}
</React.Profiler>
);
}

return this.props.children;
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/src/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ const SEMVER_REGEXP = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\
/**
* Represents Semantic Versioning object
*/
interface SemVer {
export interface SemVer {
major?: number;
minor?: number;
patch?: number;
Expand Down