Systrace.js 9.86 KB
/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule Systrace
 * @flow
 */
'use strict';

const invariant = require('fbjs/lib/invariant');

type RelayProfiler = {
  attachProfileHandler(
    name: string,
    handler: (name: string, state?: any) => () => void
  ): void,

  attachAggregateHandler(
    name: string,
    handler: (name: string, callback: () => void) => void
  ): void,
};

/* eslint no-bitwise: 0 */
const TRACE_TAG_REACT_APPS = 1 << 17;
const TRACE_TAG_JS_VM_CALLS = 1 << 27;

let _enabled = false;
let _asyncCookie = 0;
const _markStack = [];
let _markStackIndex = -1;
let _canInstallReactHook = false;
let _useFiber = false;

// Implements a subset of User Timing API necessary for React measurements.
// https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API
const REACT_MARKER = '\u269B';
const userTimingPolyfill = __DEV__ ? {
  mark(markName: string) {
    if (_enabled) {
      _markStackIndex++;
      _markStack[_markStackIndex] = markName;
      let systraceLabel = markName;
      // Since perf measurements are a shared namespace in User Timing API,
      // we prefix all React results with a React emoji.
      if (markName[0] === REACT_MARKER) {
        // This is coming from React.
        // Removing component IDs keeps trace colors stable.
        const indexOfId = markName.lastIndexOf(' (#');
        const cutoffIndex = indexOfId !== -1 ? indexOfId : markName.length;
        // Also cut off the emoji because it breaks Systrace
        systraceLabel = markName.slice(2, cutoffIndex);
      }
      Systrace.beginEvent(systraceLabel);
    }
  },
  measure(measureName: string, startMark: ?string, endMark: ?string) {
    if (_enabled) {
      invariant(
        typeof measureName === 'string' &&
        typeof startMark === 'string' &&
        typeof endMark === 'undefined',
        'Only performance.measure(string, string) overload is supported.'
      );
      const topMark = _markStack[_markStackIndex];
      invariant(
        startMark === topMark,
        'There was a mismatching performance.measure() call. ' +
        'Expected "%s" but got "%s."',
        topMark,
        startMark,
      );
      _markStackIndex--;
      // We can't use more descriptive measureName because Systrace doesn't
      // let us edit labels post factum.
      Systrace.endEvent();
    }
  },
  clearMarks(markName: string) {
    if (_enabled) {
      if (_markStackIndex === -1) {
        return;
      }
      if (markName === _markStack[_markStackIndex]) {
        // React uses this for "cancelling" started measurements.
        // Systrace doesn't support deleting measurements, so we just stop them.
        if (userTimingPolyfill != null) {
          userTimingPolyfill.measure(markName, markName);
        }
      }
    }
  },
  clearMeasures() {
    // React calls this to avoid memory leaks in browsers, but we don't keep
    // measurements anyway.
  },
} : null;

// A hook to get React Stack markers in Systrace.
const reactDebugToolHook = __DEV__ ? {
  onBeforeMountComponent(debugID) {
    const ReactComponentTreeHook = require('ReactGlobalSharedState').ReactComponentTreeHook;
    const displayName = ReactComponentTreeHook.getDisplayName(debugID);
    Systrace.beginEvent(`ReactReconciler.mountComponent(${displayName})`);
  },
  onMountComponent(debugID) {
    Systrace.endEvent();
  },
  onBeforeUpdateComponent(debugID) {
    const ReactComponentTreeHook = require('ReactGlobalSharedState').ReactComponentTreeHook;
    const displayName = ReactComponentTreeHook.getDisplayName(debugID);
    Systrace.beginEvent(`ReactReconciler.updateComponent(${displayName})`);
  },
  onUpdateComponent(debugID) {
    Systrace.endEvent();
  },
  onBeforeUnmountComponent(debugID) {
    const ReactComponentTreeHook = require('ReactGlobalSharedState').ReactComponentTreeHook;
    const displayName = ReactComponentTreeHook.getDisplayName(debugID);
    Systrace.beginEvent(`ReactReconciler.unmountComponent(${displayName})`);
  },
  onUnmountComponent(debugID) {
    Systrace.endEvent();
  },
  onBeginLifeCycleTimer(debugID, timerType) {
    const ReactComponentTreeHook = require('ReactGlobalSharedState').ReactComponentTreeHook;
    const displayName = ReactComponentTreeHook.getDisplayName(debugID);
    Systrace.beginEvent(`${displayName}.${timerType}()`);
  },
  onEndLifeCycleTimer(debugID, timerType) {
    Systrace.endEvent();
  },
} : null;

const Systrace = {
  installReactHook(useFiber: boolean) {
    if (_enabled) {
      if (__DEV__) {
        if (useFiber) {
          global.performance = userTimingPolyfill;
        } else {
          require('ReactDebugTool').addHook(reactDebugToolHook);
        }
      }
    }
    _useFiber = useFiber;
    _canInstallReactHook = true;
  },

  setEnabled(enabled: boolean) {
    if (_enabled !== enabled) {
      if (__DEV__) {
        if (enabled) {
          global.nativeTraceBeginLegacy && global.nativeTraceBeginLegacy(TRACE_TAG_JS_VM_CALLS);
        } else {
          global.nativeTraceEndLegacy && global.nativeTraceEndLegacy(TRACE_TAG_JS_VM_CALLS);
        }
        if (_canInstallReactHook) {
          if (_useFiber) {
            if (enabled && global.performance === undefined) {
              global.performance = userTimingPolyfill;
            }
          } else {
            const ReactDebugTool = require('ReactDebugTool');
            if (enabled) {
              ReactDebugTool.addHook(reactDebugToolHook);
            } else {
              ReactDebugTool.removeHook(reactDebugToolHook);
            }
          }
        }
      }
      _enabled = enabled;
    }
  },

  isEnabled(): boolean {
    return _enabled;
  },

  /**
   * beginEvent/endEvent for starting and then ending a profile within the same call stack frame
  **/
  beginEvent(profileName?: any, args?: any) {
    if (_enabled) {
      profileName = typeof profileName === 'function' ?
        profileName() : profileName;
      global.nativeTraceBeginSection(TRACE_TAG_REACT_APPS, profileName, args);
    }
  },

  endEvent() {
    if (_enabled) {
      global.nativeTraceEndSection(TRACE_TAG_REACT_APPS);
    }
  },

  /**
   * beginAsyncEvent/endAsyncEvent for starting and then ending a profile where the end can either
   * occur on another thread or out of the current stack frame, eg await
   * the returned cookie variable should be used as input into the endAsyncEvent call to end the profile
  **/
  beginAsyncEvent(profileName?: any): any {
    const cookie = _asyncCookie;
    if (_enabled) {
      _asyncCookie++;
      profileName = typeof profileName === 'function' ?
        profileName() : profileName;
      global.nativeTraceBeginAsyncSection(TRACE_TAG_REACT_APPS, profileName, cookie);
    }
    return cookie;
  },

  endAsyncEvent(profileName?: any, cookie?: any) {
    if (_enabled) {
      profileName = typeof profileName === 'function' ?
        profileName() : profileName;
      global.nativeTraceEndAsyncSection(TRACE_TAG_REACT_APPS, profileName, cookie);
    }
  },

  /**
   * counterEvent registers the value to the profileName on the systrace timeline
  **/
  counterEvent(profileName?: any, value?: any) {
    if (_enabled) {
      profileName = typeof profileName === 'function' ?
        profileName() : profileName;
      global.nativeTraceCounter &&
        global.nativeTraceCounter(TRACE_TAG_REACT_APPS, profileName, value);
    }
  },

  /**
   * Relay profiles use await calls, so likely occur out of current stack frame
   * therefore async variant of profiling is used
  **/
  attachToRelayProfiler(relayProfiler: RelayProfiler) {
    relayProfiler.attachProfileHandler('*', (name, state?) => {
      if (state != null && state.queryName !== undefined) {
        name += '_' + state.queryName
      }
      const cookie = Systrace.beginAsyncEvent(name);
      return () => {
        Systrace.endAsyncEvent(name, cookie);
      };
    });

    relayProfiler.attachAggregateHandler('*', (name, callback) => {
      Systrace.beginEvent(name);
      callback();
      Systrace.endEvent();
    });
  },

  /* This is not called by default due to perf overhead but it's useful
     if you want to find traces which spend too much time in JSON. */
  swizzleJSON() {
    Systrace.measureMethods(JSON, 'JSON', [
      'parse',
      'stringify'
    ]);
  },

 /**
  * Measures multiple methods of a class. For example, you can do:
  * Systrace.measureMethods(JSON, 'JSON', ['parse', 'stringify']);
  *
  * @param object
  * @param objectName
  * @param methodNames Map from method names to method display names.
  */
 measureMethods(object: any, objectName: string, methodNames: Array<string>): void {
   if (!__DEV__) {
     return;
   }

   methodNames.forEach(methodName => {
     object[methodName] = Systrace.measure(
       objectName,
       methodName,
       object[methodName]
     );
   });
 },

 /**
  * Returns an profiled version of the input function. For example, you can:
  * JSON.parse = Systrace.measure('JSON', 'parse', JSON.parse);
  *
  * @param objName
  * @param fnName
  * @param {function} func
  * @return {function} replacement function
  */
 measure(objName: string, fnName: string, func: any): any {
   if (!__DEV__) {
     return func;
   }

   const profileName = `${objName}.${fnName}`;
   return function() {
     if (!_enabled) {
       return func.apply(this, arguments);
     }

     Systrace.beginEvent(profileName);
     const ret = func.apply(this, arguments);
     Systrace.endEvent();
     return ret;
   };
 },
};

if (__DEV__) {
  // This is needed, because require callis in polyfills are not processed as
  // other files. Therefore, calls to `require('moduleId')` are not replaced
  // with numeric IDs
  // TODO(davidaurelio) Scan polyfills for dependencies, too (t9759686)
  (require: any).Systrace = Systrace;
}

module.exports = Systrace;