Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/utils/docs/profiler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Profile

The `Profiler` class provides a clean, type-safe API for performance monitoring that integrates seamlessly with Chrome DevTools. It supports both synchronous and asynchronous operations with smart defaults for custom track visualization, enabling developers to track performance bottlenecks and optimize application speed.

## Features

- **Type-Safe API**: Fully typed UserTiming API for [Chrome DevTools Extensibility API](https://developer.chrome.com/docs/devtools/performance/extension)
- **Measure API**: Easy-to-use methods for measuring synchronous and asynchronous code execution times.
- **Custom Track Configuration**: Fully typed reusable configurations for custom track visualization.
- **Process buffered entries**: Captures and processes buffered profiling entries.
- **3rd Party Profiling**: Automatically processes third-party performance entries.
- **Clean measure names**: Automatically adds prefixes to measure names, as well as start/end postfix to marks, for better organization.

## NodeJS Features

- **Crash-save Write Ahead Log**: Ensures profiling data is saved even if the application crashes.
- **Recoverable Profiles**: Ability to resume profiling sessions after interruptions or crash.
- **Automatic Trace Generation**: Generates trace files compatible with Chrome DevTools for in-depth performance analysis.
- **Multiprocess Support**: Designed to handle profiling over sharded WAL.
- **Controllable over env vars**: Easily enable or disable profiling through environment variables.
20 changes: 10 additions & 10 deletions packages/utils/src/lib/exit-process.int.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import process from 'node:process';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { SIGNAL_EXIT_CODES, installExitHandlers } from './exit-process.js';
import { SIGNAL_EXIT_CODES, subscribeProcessExit } from './exit-process.js';

describe('installExitHandlers', () => {
describe('subscribeProcessExit', () => {
const onError = vi.fn();
const onExit = vi.fn();
const processOnSpy = vi.spyOn(process, 'on');
Expand All @@ -26,7 +26,7 @@ describe('installExitHandlers', () => {
});

it('should install event listeners for all expected events', () => {
expect(() => installExitHandlers({ onError, onExit })).not.toThrow();
expect(() => subscribeProcessExit({ onError, onExit })).not.toThrow();

expect(processOnSpy).toHaveBeenCalledWith(
'uncaughtException',
Expand All @@ -43,7 +43,7 @@ describe('installExitHandlers', () => {
});

it('should call onError with error and kind for uncaughtException', () => {
expect(() => installExitHandlers({ onError })).not.toThrow();
expect(() => subscribeProcessExit({ onError })).not.toThrow();

const testError = new Error('Test uncaught exception');

Expand All @@ -55,7 +55,7 @@ describe('installExitHandlers', () => {
});

it('should call onError with reason and kind for unhandledRejection', () => {
expect(() => installExitHandlers({ onError })).not.toThrow();
expect(() => subscribeProcessExit({ onError })).not.toThrow();

const testReason = 'Test unhandled rejection';

Expand All @@ -67,7 +67,7 @@ describe('installExitHandlers', () => {
});

it('should call onExit and exit with code 0 for SIGINT', () => {
expect(() => installExitHandlers({ onExit })).not.toThrow();
expect(() => subscribeProcessExit({ onExit })).not.toThrow();

(process as any).emit('SIGINT');

Expand All @@ -80,7 +80,7 @@ describe('installExitHandlers', () => {
});

it('should call onExit and exit with code 0 for SIGTERM', () => {
expect(() => installExitHandlers({ onExit })).not.toThrow();
expect(() => subscribeProcessExit({ onExit })).not.toThrow();

(process as any).emit('SIGTERM');

Expand All @@ -93,7 +93,7 @@ describe('installExitHandlers', () => {
});

it('should call onExit and exit with code 0 for SIGQUIT', () => {
expect(() => installExitHandlers({ onExit })).not.toThrow();
expect(() => subscribeProcessExit({ onExit })).not.toThrow();

(process as any).emit('SIGQUIT');

Expand All @@ -106,7 +106,7 @@ describe('installExitHandlers', () => {
});

it('should call onExit for successful process termination with exit code 0', () => {
expect(() => installExitHandlers({ onExit })).not.toThrow();
expect(() => subscribeProcessExit({ onExit })).not.toThrow();

(process as any).emit('exit', 0);

Expand All @@ -117,7 +117,7 @@ describe('installExitHandlers', () => {
});

it('should call onExit for failed process termination with exit code 1', () => {
expect(() => installExitHandlers({ onExit })).not.toThrow();
expect(() => subscribeProcessExit({ onExit })).not.toThrow();

(process as any).emit('exit', 1);

Expand Down
61 changes: 45 additions & 16 deletions packages/utils/src/lib/exit-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,19 @@ export type ExitHandlerOptions = {
fatalExitCode?: number;
};

export function installExitHandlers(options: ExitHandlerOptions = {}): void {
/**
*
* @param options - Options for the exit handler
* @param options.onExit - Callback to be called when the process exits
* @param options.onError - Callback to be called when an error occurs
* @param options.exitOnFatal - Whether to exit the process on fatal errors
* @param options.exitOnSignal - Whether to exit the process on signals
* @param options.fatalExitCode - The exit code to use for fatal errors
* @returns A function to unsubscribe from the exit handlers
*/
export function subscribeProcessExit(
options: ExitHandlerOptions = {},
): () => void {
// eslint-disable-next-line functional/no-let
let closedReason: CloseReason | undefined;
const {
Expand All @@ -57,40 +69,57 @@ export function installExitHandlers(options: ExitHandlerOptions = {}): void {
onExit?.(code, reason);
};

process.on('uncaughtException', err => {
const uncaughtExceptionHandler = (err: unknown) => {
onError?.(err, 'uncaughtException');
if (exitOnFatal) {
close(fatalExitCode, {
kind: 'fatal',
fatal: 'uncaughtException',
});
}
});
};

process.on('unhandledRejection', reason => {
const unhandledRejectionHandler = (reason: unknown) => {
onError?.(reason, 'unhandledRejection');
if (exitOnFatal) {
close(fatalExitCode, {
kind: 'fatal',
fatal: 'unhandledRejection',
});
}
});
};

(['SIGINT', 'SIGTERM', 'SIGQUIT'] as const).forEach(signal => {
process.on(signal, () => {
close(SIGNAL_EXIT_CODES()[signal], { kind: 'signal', signal });
if (exitOnSignal) {
// eslint-disable-next-line n/no-process-exit
process.exit(SIGNAL_EXIT_CODES()[signal]);
}
});
});
const signalHandlers = (['SIGINT', 'SIGTERM', 'SIGQUIT'] as const).map(
signal => {
const handler = () => {
close(SIGNAL_EXIT_CODES()[signal], { kind: 'signal', signal });
if (exitOnSignal) {
// eslint-disable-next-line unicorn/no-process-exit
process.exit(SIGNAL_EXIT_CODES()[signal]);
}
};
process.on(signal, handler);
return { signal, handler };
},
);

process.on('exit', code => {
const exitHandler = (code: number) => {
if (closedReason) {
return;
}
close(code, { kind: 'exit' });
});
};

process.on('uncaughtException', uncaughtExceptionHandler);
process.on('unhandledRejection', unhandledRejectionHandler);
process.on('exit', exitHandler);

return () => {
process.removeListener('uncaughtException', uncaughtExceptionHandler);
process.removeListener('unhandledRejection', unhandledRejectionHandler);
process.removeListener('exit', exitHandler);
signalHandlers.forEach(({ signal, handler }) => {
process.removeListener(signal, handler);
});
};
}
30 changes: 15 additions & 15 deletions packages/utils/src/lib/exit-process.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import os from 'node:os';
import process from 'node:process';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { SIGNAL_EXIT_CODES, installExitHandlers } from './exit-process.js';
import { SIGNAL_EXIT_CODES, subscribeProcessExit } from './exit-process.js';

describe('exit-process tests', () => {
describe('subscribeProcessExit', () => {
const onError = vi.fn();
const onExit = vi.fn();
const processOnSpy = vi.spyOn(process, 'on');
Expand All @@ -27,7 +27,7 @@ describe('exit-process tests', () => {
});

it('should install event listeners for all expected events', () => {
expect(() => installExitHandlers({ onError, onExit })).not.toThrow();
expect(() => subscribeProcessExit({ onError, onExit })).not.toThrow();

expect(processOnSpy).toHaveBeenCalledWith(
'uncaughtException',
Expand All @@ -44,7 +44,7 @@ describe('exit-process tests', () => {
});

it('should call onError with error and kind for uncaughtException', () => {
expect(() => installExitHandlers({ onError })).not.toThrow();
expect(() => subscribeProcessExit({ onError })).not.toThrow();

const testError = new Error('Test uncaught exception');

Expand All @@ -56,7 +56,7 @@ describe('exit-process tests', () => {
});

it('should call onError with reason and kind for unhandledRejection', () => {
expect(() => installExitHandlers({ onError })).not.toThrow();
expect(() => subscribeProcessExit({ onError })).not.toThrow();

const testReason = 'Test unhandled rejection';

Expand All @@ -69,7 +69,7 @@ describe('exit-process tests', () => {

it('should call onExit with correct code and reason for SIGINT', () => {
expect(() =>
installExitHandlers({ onExit, exitOnSignal: true }),
subscribeProcessExit({ onExit, exitOnSignal: true }),
).not.toThrow();

(process as any).emit('SIGINT');
Expand All @@ -85,7 +85,7 @@ describe('exit-process tests', () => {

it('should call onExit with correct code and reason for SIGTERM', () => {
expect(() =>
installExitHandlers({ onExit, exitOnSignal: true }),
subscribeProcessExit({ onExit, exitOnSignal: true }),
).not.toThrow();

(process as any).emit('SIGTERM');
Expand All @@ -101,7 +101,7 @@ describe('exit-process tests', () => {

it('should call onExit with correct code and reason for SIGQUIT', () => {
expect(() =>
installExitHandlers({ onExit, exitOnSignal: true }),
subscribeProcessExit({ onExit, exitOnSignal: true }),
).not.toThrow();

(process as any).emit('SIGQUIT');
Expand All @@ -117,7 +117,7 @@ describe('exit-process tests', () => {

it('should not exit process when exitOnSignal is false', () => {
expect(() =>
installExitHandlers({ onExit, exitOnSignal: false }),
subscribeProcessExit({ onExit, exitOnSignal: false }),
).not.toThrow();

(process as any).emit('SIGINT');
Expand All @@ -132,7 +132,7 @@ describe('exit-process tests', () => {
});

it('should not exit process when exitOnSignal is not set', () => {
expect(() => installExitHandlers({ onExit })).not.toThrow();
expect(() => subscribeProcessExit({ onExit })).not.toThrow();

(process as any).emit('SIGTERM');

Expand All @@ -146,7 +146,7 @@ describe('exit-process tests', () => {
});

it('should call onExit with exit code and reason for normal exit', () => {
expect(() => installExitHandlers({ onExit })).not.toThrow();
expect(() => subscribeProcessExit({ onExit })).not.toThrow();

const exitCode = 42;
(process as any).emit('exit', exitCode);
Expand All @@ -159,7 +159,7 @@ describe('exit-process tests', () => {

it('should call onExit with fatal reason when exitOnFatal is true', () => {
expect(() =>
installExitHandlers({ onError, onExit, exitOnFatal: true }),
subscribeProcessExit({ onError, onExit, exitOnFatal: true }),
).not.toThrow();

const testError = new Error('Test uncaught exception');
Expand All @@ -177,7 +177,7 @@ describe('exit-process tests', () => {

it('should use custom fatalExitCode when exitOnFatal is true', () => {
expect(() =>
installExitHandlers({
subscribeProcessExit({
onError,
onExit,
exitOnFatal: true,
Expand All @@ -200,7 +200,7 @@ describe('exit-process tests', () => {

it('should call onExit with fatal reason for unhandledRejection when exitOnFatal is true', () => {
expect(() =>
installExitHandlers({ onError, onExit, exitOnFatal: true }),
subscribeProcessExit({ onError, onExit, exitOnFatal: true }),
).not.toThrow();

const testReason = 'Test unhandled rejection';
Expand Down Expand Up @@ -244,7 +244,7 @@ describe('exit-process tests', () => {

it('should call onExit only once even when close is called multiple times', () => {
expect(() =>
installExitHandlers({ onExit, exitOnSignal: true }),
subscribeProcessExit({ onExit, exitOnSignal: true }),
).not.toThrow();

(process as any).emit('SIGINT');
Expand Down
Loading
Loading