-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HMR Dev Feedback Stats - RsPack #21
Labels
enhancement
New feature or request
Comments
About using /**
* Implementation Plan for Rspack Developer Feedback Plugin
*
* A plugin to measure and collect real-time developer feedback metrics in Rspack,
* specifically tracking time-to-feedback for file changes during development.
*/
import { Compiler, Compilation } from '@rspack/core';
import WebSocket from 'ws';
import path from 'path';
import { createServer } from 'http';
interface TimingEntry {
file: string;
changeDetectedAt: number;
compileStartAt?: number;
compileDoneAt?: number;
hmrAppliedAt?: number;
}
interface MetricsPayload {
file: string;
totalDuration: number;
phases: {
changeToCompile: number;
compileDuration: number;
hmrApplyDuration: number;
};
metadata: {
fileType: string;
fileSize: number;
projectType: string;
};
}
/**
* Main plugin class for collecting development feedback metrics
*/
class RspackDevFeedbackPlugin {
private changeMap = new Map<string, TimingEntry>();
private wsServer: WebSocket.Server;
private connections: Set<WebSocket> = new Set();
private metricsBuffer: MetricsPayload[] = [];
private options: {
metricsEndpoint?: string;
logLocally?: boolean;
anonymizePaths?: boolean;
flushInterval?: number;
};
constructor(options = {}) {
this.options = {
logLocally: true,
anonymizePaths: true,
flushInterval: 5000,
...options
};
// Initialize WebSocket server for real-time metrics
const httpServer = createServer();
this.wsServer = new WebSocket.Server({ server: httpServer });
httpServer.listen(0, () => {
const port = (httpServer.address() as any).port;
console.log(`Metrics WebSocket server listening on port ${port}`);
});
this.setupWebSocket();
this.startMetricsFlush();
}
private setupWebSocket() {
this.wsServer.on('connection', (ws) => {
this.connections.add(ws);
ws.on('close', () => this.connections.delete(ws));
});
}
private startMetricsFlush() {
setInterval(() => {
if (this.metricsBuffer.length > 0) {
this.flushMetrics();
}
}, this.options.flushInterval);
}
private async flushMetrics() {
if (this.options.metricsEndpoint) {
try {
await fetch(this.options.metricsEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.metricsBuffer)
});
} catch (error) {
console.error('Failed to flush metrics:', error);
}
}
this.metricsBuffer = [];
}
apply(compiler: Compiler) {
// 1. File Change Detection
compiler.hooks.watchRun.tap('RspackDevFeedbackPlugin', (compiler) => {
const changedFiles = compiler.modifiedFiles;
if (!changedFiles) return;
for (const file of changedFiles) {
const normalizedPath = this.normalizePath(file);
this.changeMap.set(normalizedPath, {
file: normalizedPath,
changeDetectedAt: Date.now()
});
}
});
// 2. Compilation Start
compiler.hooks.compile.tap('RspackDevFeedbackPlugin', () => {
for (const [file, timing] of this.changeMap.entries()) {
timing.compileStartAt = Date.now();
}
});
// 3. Compilation Complete
compiler.hooks.done.tap('RspackDevFeedbackPlugin', (stats) => {
const now = Date.now();
for (const [file, timing] of this.changeMap.entries()) {
timing.compileDoneAt = now;
}
});
// 4. Client Code Injection via Runtime Module
compiler.hooks.compilation.tap('RspackDevFeedbackPlugin', (compilation) => {
compilation.hooks.runtimeModule.tap('RspackDevFeedbackPlugin', () => {
class DevFeedbackRuntimeModule extends compilation.runtimeModule {
constructor() {
super('rspack-dev-feedback');
}
generate() {
return this.generateClientCode();
}
}
compilation.addRuntimeModule(new DevFeedbackRuntimeModule());
});
});
}
private injectClientCode(compilation: Compilation) {
const clientCode = this.generateClientCode();
// Create a virtual module for the client code
compilation.emitAsset(
'rspack-dev-feedback-client.js',
{
source: () => clientCode,
size: () => clientCode.length
}
);
}
private generateClientCode(): string {
// Get WebSocket port dynamically
const wsPort = (this.wsServer.address() as any).port;
return `
// Rspack Dev Feedback Client
(() => {
const ws = new WebSocket('ws://localhost:${wsPort}');
let moduleUpdateTimer;
// Handle different HMR implementations
if (module.hot) {
// Traditional Webpack-style HMR
module.hot.addStatusHandler((status) => {
handleHMRStatus(status);
});
} else if (window.__RSPACK_HMR__) {
// Rspack-specific HMR implementation
window.__RSPACK_HMR__.on('status', (status) => {
handleHMRStatus(status);
});
}
function handleHMRStatus(status) {
if (status === 'idle' || status === 'ready') {
clearTimeout(moduleUpdateTimer);
moduleUpdateTimer = setTimeout(() => {
ws.send(JSON.stringify({
type: 'hmrApplied',
timestamp: Date.now()
}));
}, 50); // Small delay to ensure DOM updates
}
}
// Track actual DOM updates
const observer = new MutationObserver(() => {
if (moduleUpdateTimer) {
clearTimeout(moduleUpdateTimer);
moduleUpdateTimer = setTimeout(() => {
ws.send(JSON.stringify({
type: 'domUpdated',
timestamp: Date.now()
}));
}, 50);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
characterData: true
});
})();
`;
}
private normalizePath(filePath: string): string {
const normalized = path.normalize(filePath);
if (this.options.anonymizePaths) {
// Replace absolute path with project-relative path
return path.relative(process.cwd(), normalized);
}
return normalized;
}
private collectMetadata(file: string) {
return {
fileType: path.extname(file),
fileSize: 0, // TODO: Implement file size calculation
projectType: this.detectProjectType()
};
}
private detectProjectType(): string {
// TODO: Implement project type detection (React, Vue, etc.)
return 'unknown';
}
}
/**
* Test harness for the plugin
*/
async function createTestHarness() {
const webpack = require('@rspack/core');
const MemoryFS = require('memory-fs');
const plugin = new RspackDevFeedbackPlugin();
const compiler = webpack({
// Test configuration
entry: './test/fixtures/entry.js',
plugins: [plugin]
});
const fs = new MemoryFS();
compiler.outputFileSystem = fs;
return {
compiler,
plugin,
async simulateFileChange(file: string) {
// Simulate file change and measure timing
const startTime = Date.now();
await new Promise(resolve => compiler.hooks.done.tap('test', resolve));
return Date.now() - startTime;
}
};
}
export { RspackDevFeedbackPlugin, createTestHarness }; This optimization brings several benefits:
|
Merged
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The primary goal is to measure and collect data about the developer's real-time feedback loop during local development. Specifically, we want to track how long it takes from when a developer saves a file change until they can see that change reflected in their browser. This "time-to-feedback" metric is crucial for understanding developer experience and identifying potential bottlenecks in the development workflow. By collecting this timing data across different projects and development setups, we can identify patterns, compare build tool performance, and make data-driven decisions about development tooling.
For instance, we can understand if certain types of changes take longer to reflect, if particular project structures impact build times, or how different build tools (like Vite vs Rspack) perform in real-world development scenarios rather than just benchmark tests.
here's he plan for rspack implementation
Let me explain the key improvements and changes made based on the feedback:
invalid
towatchRun
hook for more reliable file change detectionprocessAssets
instead ofafterHash
for client code injectionKey architectural decisions:
The text was updated successfully, but these errors were encountered: