From 826b24d9e266b0e59948ac4778bd9750e8e51973 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Wed, 9 Jul 2025 16:10:50 +0300 Subject: [PATCH] Metrics server (#390) --- package.json | 4 +- rsbuild.config.ts | 10 ++++ scripts/requestData.ts | 42 +++++++++++++++++ scripts/wsServer.ts | 45 ++++++++++++++++++ src/devtools.ts | 103 +++++++++++++++++++++++++++++++++++++++++ src/env.d.ts | 1 + 6 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 scripts/requestData.ts create mode 100644 scripts/wsServer.ts diff --git a/package.json b/package.json index a09b92a3..fe4adb16 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dev-proxy": "node server.js", "start": "run-p dev-proxy dev-rsbuild watch-mesher", "start2": "run-p dev-rsbuild watch-mesher", + "start-metrics": "ENABLE_METRICS=true rsbuild dev", "build": "pnpm build-other-workers && rsbuild build", "build-analyze": "BUNDLE_ANALYZE=true rsbuild build && pnpm build-other-workers", "build-single-file": "SINGLE_FILE_BUILD=true rsbuild build", @@ -32,7 +33,8 @@ "run-all": "run-p start run-playground", "build-playground": "rsbuild build --config renderer/rsbuild.config.ts", "watch-playground": "rsbuild dev --config renderer/rsbuild.config.ts", - "update-git-deps": "tsx scripts/updateGitDeps.ts" + "update-git-deps": "tsx scripts/updateGitDeps.ts", + "request-data": "tsx scripts/requestData.ts" }, "keywords": [ "prismarine", diff --git a/rsbuild.config.ts b/rsbuild.config.ts index 219fe57e..e264f6b7 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -15,6 +15,7 @@ import { appAndRendererSharedConfig } from './renderer/rsbuildSharedConfig' import { genLargeDataAliases } from './scripts/genLargeDataAliases' import sharp from 'sharp' import supportedVersions from './src/supportedVersions.mjs' +import { startWsServer } from './scripts/wsServer' const SINGLE_FILE_BUILD = process.env.SINGLE_FILE_BUILD === 'true' @@ -59,6 +60,8 @@ const configSource = (SINGLE_FILE_BUILD ? 'BUNDLED' : (process.env.CONFIG_JSON_S const faviconPath = 'favicon.png' +const enableMetrics = process.env.ENABLE_METRICS === 'true' + // base options are in ./renderer/rsbuildSharedConfig.ts const appConfig = defineConfig({ html: { @@ -159,6 +162,7 @@ const appConfig = defineConfig({ 'process.env.INLINED_APP_CONFIG': JSON.stringify(configSource === 'BUNDLED' ? configJson : null), 'process.env.ENABLE_COOKIE_STORAGE': JSON.stringify(process.env.ENABLE_COOKIE_STORAGE || true), 'process.env.COOKIE_STORAGE_PREFIX': JSON.stringify(process.env.COOKIE_STORAGE_PREFIX || ''), + 'process.env.WS_PORT': JSON.stringify(enableMetrics ? 8081 : false), }, }, server: { @@ -216,6 +220,12 @@ const appConfig = defineConfig({ await execAsync('pnpm run build-mesher') } fs.writeFileSync('./dist/version.txt', buildingVersion, 'utf-8') + + // Start WebSocket server in development + if (dev && enableMetrics) { + await startWsServer(8081, false) + } + console.timeEnd('total-prep') } if (!dev) { diff --git a/scripts/requestData.ts b/scripts/requestData.ts new file mode 100644 index 00000000..dc866a1b --- /dev/null +++ b/scripts/requestData.ts @@ -0,0 +1,42 @@ +import WebSocket from 'ws' + +function formatBytes(bytes: number) { + return `${(bytes).toFixed(2)} MB` +} + +function formatTime(ms: number) { + return `${(ms / 1000).toFixed(2)}s` +} + +const ws = new WebSocket('ws://localhost:8081') + +ws.on('open', () => { + console.log('Connected to metrics server, waiting for metrics...') +}) + +ws.on('message', (data) => { + try { + const metrics = JSON.parse(data.toString()) + console.log('\nPerformance Metrics:') + console.log('------------------') + console.log(`Load Time: ${formatTime(metrics.loadTime)}`) + console.log(`Memory Usage: ${formatBytes(metrics.memoryUsage)}`) + console.log(`Timestamp: ${new Date(metrics.timestamp).toLocaleString()}`) + if (!process.argv.includes('-f')) { // follow mode + process.exit(0) + } + } catch (error) { + console.error('Error parsing metrics:', error) + } +}) + +ws.on('error', (error) => { + console.error('WebSocket error:', error) + process.exit(1) +}) + +// Exit if no metrics received after 5 seconds +setTimeout(() => { + console.error('Timeout waiting for metrics') + process.exit(1) +}, 5000) diff --git a/scripts/wsServer.ts b/scripts/wsServer.ts new file mode 100644 index 00000000..43035f52 --- /dev/null +++ b/scripts/wsServer.ts @@ -0,0 +1,45 @@ +import {WebSocketServer} from 'ws' + +export function startWsServer(port: number = 8081, tryOtherPort: boolean = true): Promise { + return new Promise((resolve, reject) => { + const tryPort = (currentPort: number) => { + const wss = new WebSocketServer({ port: currentPort }) + .on('listening', () => { + console.log(`WebSocket server started on port ${currentPort}`) + resolve(currentPort) + }) + .on('error', (err: any) => { + if (err.code === 'EADDRINUSE' && tryOtherPort) { + console.log(`Port ${currentPort} in use, trying ${currentPort + 1}`) + wss.close() + tryPort(currentPort + 1) + } else { + reject(err) + } + }) + + wss.on('connection', (ws) => { + console.log('Client connected') + + ws.on('message', (message) => { + try { + // Simply relay the message to all connected clients except sender + wss.clients.forEach(client => { + if (client !== ws && client.readyState === WebSocket.OPEN) { + client.send(message.toString()) + } + }) + } catch (error) { + console.error('Error processing message:', error) + } + }) + + ws.on('close', () => { + console.log('Client disconnected') + }) + }) + } + + tryPort(port) + }) +} diff --git a/src/devtools.ts b/src/devtools.ts index b9267127..547d0571 100644 --- a/src/devtools.ts +++ b/src/devtools.ts @@ -209,3 +209,106 @@ setInterval(() => { }, 1000) // --- + +// Add type declaration for performance.memory +declare global { + interface Performance { + memory?: { + usedJSHeapSize: number + totalJSHeapSize: number + jsHeapSizeLimit: number + } + } +} + +// Performance metrics WebSocket client +let ws: WebSocket | null = null +let wsReconnectTimeout: NodeJS.Timeout | null = null +let metricsInterval: NodeJS.Timeout | null = null + +// Start collecting metrics immediately +const startTime = performance.now() + +function collectAndSendMetrics () { + if (!ws || ws.readyState !== WebSocket.OPEN) return + + const metrics = { + loadTime: performance.now() - startTime, + memoryUsage: (performance.memory?.usedJSHeapSize ?? 0) / 1024 / 1024, + timestamp: Date.now() + } + + ws.send(JSON.stringify(metrics)) +} + +function getWebSocketUrl () { + const wsPort = process.env.WS_SERVER + if (!wsPort) return null + + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const { hostname } = window.location + return `${protocol}//${hostname}:${wsPort}` +} + +function connectWebSocket () { + if (ws) return + + const wsUrl = getWebSocketUrl() + if (!wsUrl) { + console.log('WebSocket server not configured') + return + } + + ws = new WebSocket(wsUrl) + + ws.onopen = () => { + console.log('Connected to metrics server') + if (wsReconnectTimeout) { + clearTimeout(wsReconnectTimeout) + wsReconnectTimeout = null + } + + // Start sending metrics immediately after connection + collectAndSendMetrics() + + // Clear existing interval if any + if (metricsInterval) { + clearInterval(metricsInterval) + } + + // Set new interval + metricsInterval = setInterval(collectAndSendMetrics, 500) + } + + ws.onclose = () => { + console.log('Disconnected from metrics server') + ws = null + + // Clear metrics interval + if (metricsInterval) { + clearInterval(metricsInterval) + metricsInterval = null + } + + // Try to reconnect after 3 seconds + wsReconnectTimeout = setTimeout(connectWebSocket, 3000) + } + + ws.onerror = (error) => { + console.error('WebSocket error:', error) + } +} + +// Connect immediately +connectWebSocket() + +// Add command to request current metrics +window.requestMetrics = () => { + const metrics = { + loadTime: performance.now() - startTime, + memoryUsage: (performance.memory?.usedJSHeapSize ?? 0) / 1024 / 1024, + timestamp: Date.now() + } + console.log('Current metrics:', metrics) + return metrics +} diff --git a/src/env.d.ts b/src/env.d.ts index 4cb5bafd..c743d6b4 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -3,6 +3,7 @@ declare namespace NodeJS { // Build configuration NODE_ENV: 'development' | 'production' SINGLE_FILE_BUILD?: string + WS_SERVER?: string DISABLE_SERVICE_WORKER?: string CONFIG_JSON_SOURCE?: 'BUNDLED' | 'REMOTE' LOCAL_CONFIG_FILE?: string