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.
2022-04-07