← Back to home

Simple Typescript performance functions

Simple functions and annotations to add to other functions to monitor performance.

Here are some simple performance monitoring functions I’ve been using for all sorts of TS stuff. Really helps measure React functions, in addition to class-level methods that might be long-running. Includes conditional compilation to drop dead branches when building for prod.

// Define this in the build process in a Makefile maybe.
// It must be defined. It is the only way to do conditional
// compilation that I know of that is clean. The one issue
// is that you can't wrap it with other helper functions,
// or else we don't get dead-code elimination, because at
// compile time all evaluation involving the constant
// is replaced with booleans. Eg:
// `process.env.NODE_ENV !== "production"` becomes `true`,
// which drops the dead branch off of if-statements. Or at
// least it works this way with `esbuild`.
//
// The reason we want to drop it is because each timer fn
// invocation is not free, and in production if you're calling
// them alot, they add up. Useful for debugging, developing,
// but less useful in production.
declare const process: { env: { NODE_ENV: string } };

type DescriptorWrapper = (
    target: unknown,
    propKey: string,
    descriptor?: PropertyDescriptor
) => PropertyDescriptor;

type NamedDescriptorWrapper = (name?: string) => DescriptorWrapper;

export type Timed<T> = (name: string, f: (...args: unknown[]) => T)
    => (...args: unknown[]) => T;

// Decorator/annotation for method to time how long it takes to run.
let Time: NamedDescriptorWrapper;

let interiorTimer: Timed<unknown>;

if (process.env.NODE_ENV === "production") {
  Time =
      (_?: string) => (
          target: unknown,
          propKey: string,
          descriptor?: PropertyDescriptor,
      ) =>
          descriptor || Object.getOwnPropertyDescriptor(target, propKey);
  interiorTimer =
      <T>(name: string, f: (...args: unknown[]) => T) =>
          (...args) =>
              f(...args);
} else {
  Time = (name?: string) => {
    return (
        target: unknown,
        propKey: string,
        descriptor?: PropertyDescriptor,
    ) => {
      descriptor = descriptor || Object.getOwnPropertyDescriptor(
          target,
          propKey
      );
      const originalMethod = descriptor.value;
      descriptor.value = function (...args: unknown[]) {
        const prefix = (name ? name + "." : "") + propKey;
        const key = performance.now();
        const start = performance.mark(prefix + "-start-" + key);
        const result = originalMethod.apply(this, args);
        const end = performance.mark(prefix + "-end-" + key);
        performance.measure(prefix + "-" + key, start.name, end.name);
        return result;
      };
      return descriptor;
    };
  };
  interiorTimer = <T>(name: string, f: (...args: unknown[]) => T) =>
      (...args) => {
        const key = performance.now().toString();
        const start = performance.mark(name + "-start-" + key);
        const result = f(...args);
        const end = performance.mark(name + `-end-` + key);
        performance.measure(name + "-" + key, start.name, end.name);
        return result;
      };
}

// Wrap a function `f` as timed. Does NOT execute function though.
function timed<T>(name: string, f: (...args: unknown[]) => T):
    (...args: unknown[]) => T {
  return interiorTimer(name, f) as (...args: unknown[]) => T;
}

// Time one function that returns T.
function time<T>(name: string, f: (...args: unknown[]) => T): T {
  return (interiorTimer(name, f) as (...args: unknown[]) => T)();
}

export {TimePerformance, timed, time};

Use it like this:

class Thing {
  @Time("Thing")
  longRunningProcess() {
    // ...do big computation
  }

  another: () => void = timed("Thing.another", () => {
    // ...do another computation
  });

  plainFn() {
    time("interior", () => {
      // ...do big computation
    });
  }
}

I use them on React rendering functions sometimes, because it’s easier to understand performance outside the React dev tools extension, in the actual performance flame graph and timeline.

typescript | javascript | snippets | workspace
2022-04-07