Jest Code Instrumentation for Performance Monitoring
This challenge focuses on implementing custom code instrumentation within your Jest test suite to gain insights into the performance of your application's functions. You'll learn how to intercept function calls, record execution times, and report on this data, which is crucial for identifying performance bottlenecks and optimizing your code.
Problem Description
Your task is to create a Jest transformer or plugin that instruments specific functions within your TypeScript code. This instrumentation should record the start and end times of each function execution, calculate the duration, and aggregate this data for reporting. The goal is to be able to analyze which functions are taking the most time to execute within your test environment.
Key Requirements:
- Instrumentation Target: The instrumentation should be configurable to target specific functions or modules. For this challenge, assume you will instrument all exported functions from a given module.
- Timing Mechanism: Use
performance.now()(or equivalent in Node.js) to accurately measure the execution time of instrumented functions. - Data Aggregation: Store the collected timing data in a structured way, perhaps an array of objects, where each object contains the function name and its execution duration.
- Reporting: Provide a mechanism to access and report the aggregated timing data after tests have run. This could be a custom Jest reporter or a simple console log at the end of the test run.
- No Performance Impact (Ideally): The instrumentation itself should have minimal impact on the execution speed of the instrumented code.
- TypeScript Compatibility: The solution must work seamlessly with TypeScript code and Jest.
Expected Behavior:
When Jest runs tests that involve instrumented functions, the instrumentation should automatically trigger for each call. After all tests complete, a report should be generated showing the total execution time for each instrumented function.
Edge Cases:
- Functions that throw errors: The instrumentation should handle errors gracefully and still record the (partial) execution time before the error.
- Asynchronous functions: The instrumentation needs to correctly measure the duration of
asyncfunctions. - Nested function calls: The instrumentation should be able to handle scenarios where instrumented functions call other instrumented functions.
Examples
Let's consider a simple example where we instrument a module with two functions.
Example 1: Basic Function Timing
Suppose you have a module mathUtils.ts:
// mathUtils.ts
export function add(a: number, b: number): number {
// Simulate some work
for (let i = 0; i < 1000000; i++) {}
return a + b;
}
export function subtract(a: number, b: number): number {
// Simulate some work
for (let i = 0; i < 500000; i++) {}
return a - b;
}
And a test file mathUtils.test.ts:
// mathUtils.test.ts
import { add, subtract } from './mathUtils';
describe('mathUtils', () => {
it('should add numbers correctly', () => {
expect(add(5, 3)).toBe(8);
});
it('should subtract numbers correctly', () => {
expect(subtract(10, 4)).toBe(6);
});
});
Input (Conceptual): Your Jest configuration with the instrumentation transformer/plugin enabled and configured to instrument mathUtils.ts.
Output (Conceptual Report):
--- Performance Report ---
Function: add, Duration: XX.XXms
Function: subtract, Duration: YY.YYms
------------------------
(Note: XX.XXms and YY.YYms will be the measured durations, which will vary. The add function is expected to take longer due to the larger loop.)
Explanation: The instrumentation transformer would modify mathUtils.ts to wrap add and subtract with timing logic. When the tests execute these functions, the timings are recorded. A reporter then prints these aggregated timings.
Example 2: Asynchronous Function Timing
Consider an async function:
// dataFetcher.ts
export async function fetchData(url: string): Promise<string> {
// Simulate network latency
await new Promise(resolve => setTimeout(resolve, 100));
return `Data from ${url}`;
}
And its test:
// dataFetcher.test.ts
import { fetchData } from './dataFetcher';
describe('dataFetcher', () => {
it('should fetch data asynchronously', async () => {
const data = await fetchData('example.com');
expect(data).toBe('Data from example.com');
});
});
Input (Conceptual): Jest configuration instrumenting dataFetcher.ts.
Output (Conceptual Report):
--- Performance Report ---
Function: fetchData, Duration: 100.XXms
------------------------
Explanation: The instrumentation must correctly handle async/await by ensuring the end time is captured after the Promise resolves.
Constraints
- Jest version: 27 or higher.
- TypeScript version: 4.0 or higher.
- The instrumentation should not introduce more than a negligible overhead (e.g., < 1ms per function call) to the actual application logic.
- The instrumentation should be opt-in via Jest configuration.
Notes
- Consider how you will integrate this with Jest's own API, such as custom transformers or plugins.
- Think about how to manage the aggregated data across multiple test files or suites if your instrumentation spans more than one module.
- For reporting, you might want to log to
console.erroror use Jest's own reporting hooks to make the output more visible. - The key challenge is to transform the source code before it's executed by Jest, injecting the instrumentation logic. You might explore using libraries like
@babel/coreorts-morphfor code transformation. - Consider how you would make the instrumentation configurable (e.g., by file path, module name, or specific function decorators). For this challenge, instrumenting all exported functions of a specified module is sufficient.