diff --git a/entrypoint.sh b/entrypoint.sh index c27bdac1..d3e6b569 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -70,6 +70,16 @@ else fi fi +# +# Enable CPU profiling +# +if [ "$WAHA_DEBUG_MODE" = "true" ] || [ "$WAHA_DEBUG_MODE" = "True" ] || [ "$WAHA_DEBUG_MODE" = "1" ]; then + case " $NODE_OPTIONS " in + *" --cpu-prof "*) ;; + *) export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--cpu-prof" ;; + esac +fi + # # Start your application using node with exec to ensure proper signal handling # diff --git a/src/api/server.debug.controller.ts b/src/api/server.debug.controller.ts index fde6429e..343a6674 100644 --- a/src/api/server.debug.controller.ts +++ b/src/api/server.debug.controller.ts @@ -1,4 +1,6 @@ +import * as inspector from 'node:inspector'; import * as v8 from 'node:v8'; +import { Readable } from 'node:stream'; import { Controller, @@ -18,7 +20,10 @@ import { WorkingSessionParam, } from '@waha/nestjs/params/SessionApiParam'; import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe'; -import { BrowserTraceQuery } from '@waha/structures/server.debug.dto'; +import { + BrowserTraceQuery, + CpuProfileQuery, +} from '@waha/structures/server.debug.dto'; import { createReadStream } from 'fs'; @ApiSecurity('api_key') @@ -36,6 +41,56 @@ export class ServerDebugController { this.enabled = this.config.debugModeEnabled; } + @Get('cpu') + @ApiOperation({ + summary: 'Collect and return a CPU profile for the current nodejs process', + description: 'Uses the Node.js inspector profiler to capture a .cpuprofile', + }) + @UsePipes(new WAHAValidationPipe()) + async cpuProfile(@Query() query: CpuProfileQuery) { + if (!this.enabled) { + throw new NotFoundException('WAHA_DEBUG_MODE is disabled'); + } + const { seconds } = query; + this.logger.log(`Collecting CPU profile for ${seconds}s...`); + + const session = new inspector.Session(); + session.connect(); + + const profile = await new Promise( + (resolve, reject) => { + session.post('Profiler.enable', (enableError) => { + if (enableError) { + session.disconnect(); + return reject(enableError); + } + session.post('Profiler.start', (startError) => { + if (startError) { + session.disconnect(); + return reject(startError); + } + setTimeout(() => { + session.post('Profiler.stop', (stopError, params) => { + session.disconnect(); + if (stopError) { + return reject(stopError); + } + resolve(params.profile); + }); + }, seconds * 1000); + }); + }); + }, + ); + + const filename = `CPU.${Date.now()}.${process.pid}.cpuprofile`; + const stream = Readable.from([JSON.stringify(profile)]); + return new StreamableFile(stream, { + type: 'application/json', + disposition: `attachment; filename=${filename}`, + }); + } + @Get('heapsnapshot') @ApiOperation({ summary: 'Return a heapsnapshot for the current nodejs process', diff --git a/src/structures/server.debug.dto.ts b/src/structures/server.debug.dto.ts index 628cb340..6f8804af 100644 --- a/src/structures/server.debug.dto.ts +++ b/src/structures/server.debug.dto.ts @@ -1,6 +1,19 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; -import { IsArray, IsNumber, IsOptional, IsString } from 'class-validator'; +import { IsArray, IsNumber, IsOptional, IsString, Min } from 'class-validator'; + +export class CpuProfileQuery { + @Type(() => Number) + @IsNumber() + @Min(1) + @ApiProperty({ + description: 'How many seconds to sample CPU', + example: 30, + required: false, + default: 30, + }) + seconds: number = 30; +} export class BrowserTraceQuery { @Type(() => Number)