Simple Typescript performance functions

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