BCoz – a Causal profiler for JS + React

Sep 28, 2019

A profiler that shows you where the low hanging fruit is. It finds functions (even async ones) that, if optimized, will directly affect total app performance.

Compare this with a traditional profiler which tell you which functions are spending the most cpu cycles.

Based off Coz

How it works

BCoz works by creating a small experiment, e.g. if I speedup function fnA by 10%, will I see a difference in the performance of the app? It then tries another function, fnB, and speeds that up by 10% and sees if that affects the performance of the app. There's a trick though.

We can't actually speedup any arbitrary function (if we could we would never need profilers in the first place). We instead create a virtual speedup by slowing everything down, and then skipping the artificial slowdown if we are in the function we want to speed up. For the fnA example, It's kind of like we made the computer slower, except when we are in fnA.

Setup

To setup BCoz, you need to record two things: Throughput and Latency. To track throughput we use progress markers (the markProgress(label?: string) function). To track latency, we mark the start and end of the transaction we care about with markStart(txLabel: string) and markEnd(txLabel: string) respectively. The labels for markStart and markEnd should be the same for the same transaction.

Toy Example

const fnA = async () => {
  // Wait 10s
  await new Promise(resolve => setTimeout(() => resolve(), 10e3));
};

const fnB = async () => {
  // Wait 17s
  await new Promise(resolve => setTimeout(() => resolve(), 17e3));
};

const MyApp = () => {
  useEffect(() => markStart("loadApp"), []);
  const [fnAFinished, setFnAFinished] = useState(false);
  const [fnBFinished, setFnBFinished] = useState(false);

  // Initial load
  useEffect(() => {
    Promise.all([
      fnA().then(() => setFnAFinished(true)),
      fnB().then(() => setFnBFinished(true))
    ]).then(() => {
      markProgress();
      markEnd("loadApp");
    });
  }, []);

  return (
    <div>
      Fns finished: A:{JSON.stringify(fnAFinished)}, B:
      {JSON.stringify(fnBFinished)}
    </div>
  );
};

In this example, no matter how much you optimize fnA, you'll have no effect on the load time of the app, but if you optimize fnB by 10%, you'll affect the app performance by 10%.


How it's implemented:

When functions are called, they are wrapped. If the function returns a promise (It is async), then we return a promise that delays the result of the original promise. If the function is a sync function, then we do spend some cpu cycles waiting (i.e. increment a counter).