This PR introduces a Profiling feature for our story book tests. It also implements a new CI job : front-sb-test-performance, that only runs stories suffixed with `.perf.stories.tsx` ## How it works It allows to wrap any component into an array of React Profiler components that will run tests many times to have the most replicable average render time possible. It is simply used by calling the new `getProfilingStory` util. Internally it creates a defined number of tests, separated by an arbitrary waiting time to allow the CPU to give more stable results. It will do 3 warm-up and 3 finishing runs of tests because the first and last renders are always a bit erratic, so we want to measure only the runs in-between. On the UI side it gives a table of results : <img width="515" alt="image" src="https://github.com/twentyhq/twenty/assets/26528466/273d2d91-26da-437a-890e-778cb6c1f993"> On the programmatic side, it stores the result in a div that can then be parsed by the play fonction of storybook, to expect a defined threshold. ```tsx play: async ({ canvasElement }) => { await findByTestId( canvasElement, 'profiling-session-finished', {}, { timeout: 60000 }, ); const profilingReport = getProfilingReportFromDocument(canvasElement); if (!isDefined(profilingReport)) { return; } const p95result = profilingReport?.total.p95; expect( p95result, `Component render time is more than p95 threshold (${p95ThresholdInMs}ms)`, ).toBeLessThan(p95ThresholdInMs); }, ```
65 lines
2.3 KiB
TypeScript
65 lines
2.3 KiB
TypeScript
import { ProfilingDataPoint } from '~/testing/profiling/types/ProfilingDataPoint';
|
|
import { ProfilingReportByComponent } from '~/testing/profiling/types/ProfilingReportByComponent';
|
|
|
|
export const computeProfilingReportByComponent = (
|
|
profilingReport: Record<string, ProfilingDataPoint[]>,
|
|
) => {
|
|
const reportByComponent = {} as ProfilingReportByComponent;
|
|
|
|
const dataPoints = Object.entries(profilingReport)
|
|
.map(([, dataPoints]) => dataPoints)
|
|
.flat(1);
|
|
|
|
for (const dataPoint of dataPoints) {
|
|
reportByComponent[dataPoint.componentName] = {
|
|
...reportByComponent?.[dataPoint.componentName],
|
|
sumById: {
|
|
...reportByComponent?.[dataPoint.componentName]?.sumById,
|
|
[dataPoint.id]:
|
|
(reportByComponent[dataPoint.componentName]?.sumById?.[
|
|
dataPoint.id
|
|
] ?? 0) + dataPoint.durationInMs,
|
|
},
|
|
sum:
|
|
(reportByComponent[dataPoint.componentName]?.sum ?? 0) +
|
|
dataPoint.durationInMs,
|
|
};
|
|
}
|
|
|
|
for (const componentName of Object.keys(reportByComponent)) {
|
|
const ids = Object.keys(reportByComponent[componentName].sumById);
|
|
const valuesUnsorted = Object.values(
|
|
reportByComponent[componentName].sumById,
|
|
);
|
|
|
|
const valuesSortedAsc = [...valuesUnsorted].sort((a, b) => a - b);
|
|
|
|
const numberOfIds = ids.length;
|
|
|
|
reportByComponent[componentName].average =
|
|
reportByComponent[componentName].sum / numberOfIds;
|
|
|
|
reportByComponent[componentName].min = Math.min(
|
|
...Object.values(reportByComponent[componentName].sumById),
|
|
);
|
|
|
|
reportByComponent[componentName].max = Math.max(
|
|
...Object.values(reportByComponent[componentName].sumById),
|
|
);
|
|
|
|
const p50Index = Math.floor(numberOfIds * 0.5);
|
|
const p80Index = Math.floor(numberOfIds * 0.8);
|
|
const p90Index = Math.floor(numberOfIds * 0.9);
|
|
const p95Index = Math.floor(numberOfIds * 0.95);
|
|
const p99Index = Math.floor(numberOfIds * 0.99);
|
|
|
|
reportByComponent[componentName].p50 = valuesSortedAsc[p50Index];
|
|
reportByComponent[componentName].p80 = valuesSortedAsc[p80Index];
|
|
reportByComponent[componentName].p90 = valuesSortedAsc[p90Index];
|
|
reportByComponent[componentName].p95 = valuesSortedAsc[p95Index];
|
|
reportByComponent[componentName].p99 = valuesSortedAsc[p99Index];
|
|
}
|
|
|
|
return reportByComponent;
|
|
};
|