Event-Driven Architecture and Asynchronous I/O
Core Mechanism of the Event Loop
Event Loop Working Principle:
Node.js’s event loop is implemented based on libuv, a continuously running process responsible for checking the event queue and executing corresponding callback functions. The event loop is divided into multiple phases, each handling specific types of events.
// Simplified representation of the event loop
while (true) {
// 1. Execute timer callbacks (setTimeout/setInterval)
processTimers();
// 2. Execute I/O callbacks (system-level callbacks)
processIOCallbacks();
// 3. Execute idle/prepare phase (internal use)
idlePrepare();
// 4. Poll phase (retrieve new I/O events)
poll();
// 5. Check phase (setImmediate callbacks)
check();
// 6. Close callbacks (e.g., socket.on('close'))
closeCallbacks();
}Event Loop Flowchart:
- Timers Phase: Executes setTimeout and setInterval callbacks
- Pending Callbacks Phase: Executes system-level operation callbacks (e.g., TCP errors)
- Idle/Prepare Phase: Internal use
- Poll Phase: Retrieves new I/O events and executes I/O-related callbacks
- Check Phase: Executes setImmediate callbacks
- Close Callbacks Phase: Executes callbacks for close events (e.g., socket.on(‘close’))
libuv Library Working Principle
libuv Architecture:
libuv is the core library of Node.js, providing cross-platform asynchronous I/O capabilities. It includes two main components: a thread pool and an event-driven system.
// Simplified pseudocode for libuv event loop
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
while (r != 0 && loop->stop_flag == 0) {
uv__update_time(loop);
uv__run_timers(loop); // Timers phase
uv__run_pending(loop); // Pending Callbacks phase
uv__run_idle(loop); // Idle/Prepare phase
uv__run_prepare(loop); // Idle/Prepare phase
blocked = uv__io_poll(loop, timeout); // Poll phase
uv__run_check(loop); // Check phase
uv__run_closing_handles(loop); // Close Callbacks phase
if (mode == UV_RUN_ONCE) {
uv__update_time(loop);
uv__run_timers(loop);
}
}
return r;
}Thread Pool Implementation:
libuv uses a default thread pool of 4 threads to handle file I/O, DNS, and other operations.
// Thread pool workflow
void uv__work_submit(uv_loop_t* loop, uv__work* w) {
// 1. Add task to queue
QUEUE_INSERT_TAIL(&loop->wq, &w->wq);
// 2. Notify worker thread
uv_async_send(&loop->wq_async);
}
// Worker thread execution function
static void uv__worker(void* arg) {
while (1) {
// 1. Wait for tasks
QUEUE_WAIT(&loop->wq);
// 2. Execute task
w->work(w);
// 3. Complete callback
w->done(w);
}
}Asynchronous I/O Implementation
Non-Blocking I/O Operations:
Node.js implements non-blocking I/O through libuv, primarily using an event notification mechanism.
// Non-blocking file read example
const fs = require('fs');
fs.readFile('example.txt', (err, data) => {
if (err) throw err;
console.log(data.toString());
});
console.log('Continue executing other operations');Underlying Implementation Process:
- The application calls
fs.readFile - Node.js initiates an asynchronous file read request via libuv
- libuv adds the request to the thread pool or uses system-level asynchronous I/O
- The main thread continues executing subsequent code
- Upon I/O completion, libuv adds the callback to the event queue
- The event loop executes the callback during the Poll phase
Event Loop Phases
Detailed Phase Analysis:
- Timers Phase:
- Executes setTimeout and setInterval callbacks
- Accuracy depends on the operating system’s scheduling
setTimeout(() => {
console.log('Timeout 1');
}, 0);
setTimeout(() => {
console.log('Timeout 2');
}, 10);- Pending Callbacks Phase:
- Executes callbacks for system-level operations (e.g., TCP errors)
- Typically used internally by libuv
- Idle/Prepare Phase:
- Internal use, not relevant to developers
- Poll Phase:
- Retrieves new I/O events
- Executes I/O-related callbacks
- May block if no callbacks are pending
const fs = require('fs');
fs.readFile('file1.txt', () => {
console.log('File 1 read');
});
fs.readFile('file2.txt', () => {
console.log('File 2 read');
});- Check Phase:
- Executes setImmediate callbacks
- Runs immediately after the Poll phase
setImmediate(() => {
console.log('Immediate 1');
});
setImmediate(() => {
console.log('Immediate 2');
});- Close Callbacks Phase:
- Executes callbacks for close events
- e.g., socket.on(‘close’)
Best Practices for Asynchronous Programming
Avoiding Callback Hell:
// Callback hell example
fs.readFile('file1.txt', (err, data1) => {
if (err) return console.error(err);
fs.readFile('file2.txt', (err, data2) => {
if (err) return console.error(err);
fs.writeFile('output.txt', data1 + data2, err => {
if (err) return console.error(err);
console.log('File merge completed');
});
});
});
// Improved with Promises
function readFilePromise(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
readFilePromise('file1.txt')
.then(data1 => readFilePromise('file2.txt')
.then(data2 => writeFilePromise('output.txt', data1 + data2))
.then(() => console.log('File merge completed'))
.catch(console.error)
);
// Improved with async/await
async function mergeFiles() {
try {
const data1 = await readFilePromise('file1.txt');
const data2 = await readFilePromise('file2.txt');
await writeFilePromise('output.txt', data1 + data2);
console.log('File merge completed');
} catch (err) {
console.error(err);
}
}Event-Driven Architecture Design:
// Custom event emitter example
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
// Register event listener
myEmitter.on('event', (a, b) => {
console.log('Event triggered', a, b);
});
// Trigger event
myEmitter.emit('event', 'Parameter 1', 'Parameter 2');
// One-time listener
myEmitter.once('onceEvent', () => {
console.log('Event triggered once');
});
// Error handling
myEmitter.on('error', (err) => {
console.error('Error occurred:', err);
});Processes and Threads
Child Process Module
Spawn Method:
const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']);
ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
ls.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});
ls.on('close', (code) => {
console.log(`Child process exited with code ${code}`);
});Exec Method:
const { exec } = require('child_process');
exec('ls -lh /usr', (error, stdout, stderr) => {
if (error) {
console.error(`Execution error: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
if (stderr) {
console.error(`stderr: ${stderr}`);
}
});Fork Method:
// parent.js
const { fork } = require('child_process');
const child = fork('child.js');
child.on('message', (msg) => {
console.log('Message from child:', msg);
});
child.send({ hello: 'world' });
// child.js
process.on('message', (msg) => {
console.log('Message from parent:', msg);
process.send({ response: 'Message received' });
});Cluster Mode
Basic Cluster Example:
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`Master process ${process.pid} is running`);
// Fork worker processes
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker process ${worker.process.pid} exited`);
});
} else {
// Workers can share any TCP connection
http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello World\n');
}).listen(8000);
console.log(`Worker process ${process.pid} started`);
}Load Balancing Strategy:
cluster.schedulingPolicy = cluster.SCHED_RR; // Default round-robin scheduling
// or
cluster.schedulingPolicy = cluster.SCHED_NONE; // OS schedulingWorker Threads Pool
Basic Thread Example:
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
if (isMainThread) {
// Main thread
const worker = new Worker(__filename, { workerData: { start: 1, end: 100 } });
worker.on('message', (result) => {
console.log('Calculation result:', result);
});
worker.on('error', (err) => {
console.error('Worker thread error:', err);
});
worker.on('exit', (code) => {
if (code !== 0) {
console.error(`Worker thread exited abnormally with code ${code}`);
}
});
} else {
// Worker thread
let sum = 0;
for (let i = workerData.start; i <= workerData.end; i++) {
sum += i;
}
parentPort.postMessage(sum);
}Thread Pool Implementation:
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
const os = require('os');
if (isMainThread) {
class ThreadPool {
constructor(size = os.cpus().length) {
this.workers = [];
this.taskQueue = [];
for (let i = 0; i < size; i++) {
this.addWorker();
}
}
addWorker() {
const worker = new Worker(__filename);
worker.on('message', (result) => {
const { resolve } = this.taskQueue.shift();
resolve(result);
this.processNext();
});
worker.on('error', (err) => {
const { reject } = this.taskQueue.shift();
reject(err);
this.addWorker(); // Replace crashed worker
});
this.workers.push(worker);
}
processNext() {
if (this.taskQueue.length > 0) {
const { task } = this.taskQueue[0];
const worker = this.workers.find(w => !w.isBusy);
if (worker) {
worker.isBusy = true;
worker.postMessage(task);
}
}
}
execute(task) {
return new Promise((resolve, reject) => {
this.taskQueue.push({ task, resolve, reject });
this.processNext();
});
}
}
// Use thread pool
const pool = new ThreadPool();
pool.execute({ start: 1, end: 1000000 }).then(console.log);
} else {
// Worker thread logic
parentPort.on('message', (task) => {
let sum = 0;
for (let i = task.start; i <= task.end; i++) {
sum += i;
}
parentPort.postMessage(sum);
});
}Inter-Process Communication
IPC Communication Example:
// parent.js
const { fork } = require('child_process');
const child = fork('child.js');
// Send message to child process
child.send({ message: 'Hello, child process' });
// Receive message from child process
child.on('message', (msg) => {
console.log('Message from child:', msg);
});
// child.js
process.on('message', (msg) => {
console.log('Message from parent:', msg);
// Reply to parent process
process.send({ response: 'Hello, parent process' });
});Shared Memory Communication:
// Using SharedArrayBuffer for shared memory
// Note: Requires --experimental-worker flag
const { Worker } = require('worker_threads');
const sharedBuffer = new SharedArrayBuffer(1024);
const arr = new Int32Array(sharedBuffer);
const worker = new Worker('./worker.js', { workerData: sharedBuffer });
worker.on('message', (msg) => {
console.log('Main thread received message:', msg);
console.log('Shared array value:', arr[0]);
});
// worker.js
const { parentPort, workerData } = require('worker_threads');
const arr = new Int32Array(workerData);
// Modify shared memory
arr[0] = 42;
// Notify main thread
parentPort.postMessage('Shared memory updated');Process Management and Resource Control
Process Management Example:
const { spawn } = require('child_process');
const child = spawn('node', ['long-running-task.js'], {
stdio: 'pipe',
detached: true // Make child process independent of parent
});
// Set process group ID for batch termination
child.unref(); // Allow parent to exit without waiting for child
// Resource limit example
const { exec } = require('child_process');
const maxMemory = '500MB'; // Maximum memory limit
exec(`node --max-old-space-size=${parseSize(maxMemory)} memory-intensive.js`,
(error, stdout, stderr) => {
if (error) {
console.error('Process execution failed:', error);
}
}
);
function parseSize(size) {
const units = { KB: 1024, MB: 1024 * 1024, GB: 1024 * 1024 * 1024 };
const match = size.match(/^(\d+)(KB|MB|GB)$/);
if (!match) throw new Error('Invalid size format');
return parseInt(match[1]) * units[match[2]];
}Process Monitoring:
const { spawn } = require('child_process');
const child = spawn('node', ['app.js']);
// Monitor resource usage
setInterval(() => {
const usage = process.memoryUsage();
console.log(`Memory usage: ${Math.round(usage.rss / 1024 / 1024)}MB`);
}, 1000);
// Handle child process exit
child.on('exit', (code, signal) => {
console.log(`Child process exited with code: ${code}, signal: ${signal}`);
// Implement auto-restart logic here if needed
});
// Handle errors
child.on('error', (err) => {
console.error('Child process error:', err);
});Memory Management and Performance Optimization
V8 Engine Memory Management
V8 Memory Structure:
- Young Generation: Stores newly created objects, reclaimed using the Scavenge algorithm
- Old Generation: Stores long-lived objects, reclaimed using Mark-Sweep and Mark-Compact algorithms
- Large Object Space: Stores objects larger than 1.5MB
Memory Allocation Example:
// Small objects allocated in young generation
function createSmallObjects() {
const objects = [];
for (let i = 0; i < 10000; i++) {
objects.push({ id: i }); // Small objects
}
return objects;
}
// Large objects allocated directly in old generation
function createLargeObject() {
// Allocate Buffer larger than 1.5MB
return Buffer.alloc(2 * 1024 * 1024); // 2MB
}Garbage Collection Triggers:
- Scavenge collection triggered when young generation space is full
- Mark-Sweep or Mark-Compact collection triggered when old generation space is full
- More aggressive collection triggered on allocation failure
Memory Leak Detection and Analysis
heapdump Usage Example:
const heapdump = require('heapdump');
// Manually trigger heap snapshot
process.on('SIGUSR2', () => {
const filename = `/tmp/heapdump-${Date.now()}.heapsnapshot`;
heapdump.writeSnapshot(filename, (err, filename) => {
if (err) console.error(err);
else console.log('Heap snapshot saved to', filename);
});
});
// Periodically trigger heap snapshot (for debugging)
setInterval(() => {
if (process.memoryUsage().heapUsed > 500 * 1024 * 1024) {
heapdump.writeSnapshot(`/tmp/heapdump-auto-${Date.now()}.heapsnapshot`);
}
}, 60000);clinic.js Usage Example:
# Install clinic.js
npm install -g clinic
# Generate performance report
clinic doctor -- node app.js
# Generate flame graph
clinic flame -- node app.js
# Generate bubble graph
clinic bubbleprof -- node app.jsCommon Memory Leak Patterns:
- Unintended global variables
- Uncleaned timers
- Unreleased closures
- Unremoved event listeners
- Unbounded cache growth
Performance Monitoring Tools
Node.js Inspector:
# Start Inspector
node --inspect app.js
# Or specify port
node --inspect=9229 app.jsChrome DevTools Debugging:
- Open Chrome browser
- Navigate to chrome://inspect
- Click “Open dedicated DevTools for Node”
- Use Performance and Memory panels for analysis
pm2 Monitoring:
# Install pm2
npm install -g pm2
# Start application with monitoring
pm2 start app.js --name "my-app"
# View real-time monitoring
pm2 monit
# Generate performance report
pm2 report
# Memory usage limit
pm2 start app.js --max-memory-restart 300MCode Optimization Strategies
Reducing Callback Nesting:
// Bad practice: Callback hell
fs.readFile('file1.txt', (err, data1) => {
if (err) return console.error(err);
fs.readFile('file2.txt', (err, data2) => {
if (err) return console.error(err);
// More nesting...
});
});
// Good practice: Promise chain
function readFilePromise(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
readFilePromise('file1.txt')
.then(data1 => readFilePromise('file2.txt'))
.then(data2 => {
// Process data
})
.catch(console.error);
// Best practice: async/await
async function readFiles() {
try {
const data1 = await readFilePromise('file1.txt');
const data2 = await readFilePromise('file2.txt');
// Process data
} catch (err) {
console.error(err);
}
}Avoiding Blocking Operations:
// Blocking operation example (avoid this)
function calculateSync() {
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
}
return sum;
}
// Non-blocking operation example
function calculateAsync() {
return new Promise(resolve => {
setImmediate(() => {
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
}
resolve(sum);
});
});
}
// Better approach: Use Worker threads
const { Worker } = require('worker_threads');
function calculateWithWorker() {
return new Promise((resolve, reject) => {
const worker = new Worker(`
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
}
parentPort.postMessage(sum);
`, { eval: true });
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', code => {
if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`));
});
});
}High Concurrency Scenario Tuning
Cluster Mode Optimization:
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
// Master process
console.log(`Master process ${process.pid} is running`);
// Create worker processes based on CPU core count
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// Handle process exit
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker process ${worker.process.pid} exited`);
// Auto-restart crashed worker
cluster.fork();
});
// Adjust load balancing strategy
cluster.schedulingPolicy = cluster.SCHED_RR; // Round-robin scheduling
} else {
// Worker process
// Create HTTP server
const server = http.createServer((req, res) => {
// Simulate request processing
setTimeout(() => {
res.writeHead(200);
res.end('Hello World\n');
}, Math.random() * 100); // Random delay to simulate processing time
});
server.listen(8000);
console.log(`Worker process ${process.pid} started`);
}Connection Pool Management:
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
const { GenericPool } = require('generic-pool');
if (isMainThread) {
// Main thread creates connection pool
const factory = {
create: () => {
// Create database connection
return new Promise(resolve => {
const connection = { id: Math.random() };
console.log('Created new connection:', connection.id);
setTimeout(() => resolve(connection), 100); // Simulate connection creation delay
});
},
destroy: (connection) => {
// Destroy connection
console.log('Destroyed connection:', connection.id);
},
validate: (connection) => {
// Validate connection
return Promise.resolve(true);
}
};
const pool = GenericPool.createPool(factory, {
max: 10, // Maximum connections
min: 2, // Minimum connections
acquireTimeoutMillis: 3000, // Connection acquisition timeout
idleTimeoutMillis: 30000 // Idle connection timeout
});
// Use connection pool
(async () => {
const connection = await pool.acquire();
try {
// Process request with connection
console.log('Using connection:', connection.id);
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate work
} finally {
pool.release(connection);
}
})();
} else {
// Worker thread logic
}Memory Optimization Techniques:
// 1. Use Buffer instead of strings for binary data
function processFile() {
// Bad practice: String concatenation for binary data
// let data = '';
// fs.createReadStream('file.bin').on('data', chunk => {
// data += chunk; // Inefficient string concatenation
// });
// Good practice: Use Buffer
const chunks = [];
fs.createReadStream('file.bin')
.on('data', chunk => {
chunks.push(chunk); // Efficient Buffer concatenation
})
.on('end', () => {
const data = Buffer.concat(chunks);
// Process data
});
}
// 2. Avoid global variables
// Bad practice:
let globalCache = {}; // Global variables are not garbage collected
// Good practice:
function createCache() {
const cache = {}; // Local variables are collected after function ends
return {
get(key) { return cache[key]; },
set(key, value) { cache[key] = value; }
};
}
// 3. Explicitly release large objects no longer needed
function processLargeData() {
let largeData = new Array(1e6).fill('data');
// Process data...
// Explicitly release reference when no longer needed
largeData.length = 0; // Clear array
// or
largeData = null; // Dereference
}Through this in-depth analysis, we gain a comprehensive understanding of Node.js’s core advanced features, including event-driven architecture, process and thread management, memory management, and performance optimization. Mastering these concepts is crucial for building high-performance, scalable Node.js applications.



