We use cookies to enhance your experience on the site
CodeWorlds

Memory Management - optimizing fort storage

Storage manager! Consul Caesar.js noticed that our fort is becoming overloaded - warehouses are bursting at the seams and the legion has less and less room to maneuver. It's time to learn memory management in Node.js!

What is memory management in the world of legionaries?

Imagine the application as a warehouse system:

  • Heap - the main storehouse where tributes (objects) are stored
  • Stack - the formation where things are temporarily placed (local variables)
  • Garbage Collector - the cleaning legion removing unused items
  • Memory leaks - tributes that have been forgotten and cannot be removed

Good memory management = an efficient, fast fort!

Memory monitoring

1// memory-monitor.service.ts
2import { Injectable } from '@nestjs/common';
3import { Cron } from '@nestjs/schedule';
4
5@Injectable()
6export class MemoryMonitorService {
7 private memoryHistory: Array<{
8 timestamp: Date;
9 usage: NodeJS.MemoryUsage;
10 external: number;
11 }> = [];
12
13 private readonly MEMORY_THRESHOLD_MB = 512; // Alert at 512MB
14 private readonly HISTORY_SIZE = 100;
15
16 @Cron('*/10 * * * * *') // Every 10 seconds
17 checkMemoryUsage(): void {
18 const usage = process.memoryUsage();
19 const externalMemory = this.getExternalMemory();
20 
21 this.memoryHistory.push({
22 timestamp: new Date(),
23 usage,
24 external: externalMemory,
25 });
26
27 // Keep only the last 100 measurements
28 if (this.memoryHistory.length > this.HISTORY_SIZE) {
29 this.memoryHistory.shift();
30 }
31
32 // Check if memory exceeds the threshold
33 const heapUsedMB = usage.heapUsed / 1024 / 1024;
34 if (heapUsedMB > this.MEMORY_THRESHOLD_MB) {
35 console.warn(` High memory usage: ${heapUsedMB.toFixed(2)}MB`);
36 this.suggestOptimizations(usage);
37 }
38
39 // Log every minute
40 if (new Date().getSeconds() === 0) {
41 this.logMemoryStatus(usage);
42 }
43 }
44
45 private getExternalMemory(): number {
46 // Approximate external memory (buffers, C++ addons)
47 return process.memoryUsage().external || 0;
48 }
49
50 private logMemoryStatus(usage: NodeJS.MemoryUsage): void {
51 const formatMB = (bytes: number) => (bytes / 1024 / 1024).toFixed(2);
52 
53 console.log(' Fort storage status:');
54 console.log(` Heap used: ${formatMB(usage.heapUsed)}MB`);
55 console.log(` Heap total: ${formatMB(usage.heapTotal)}MB`);
56 console.log(` RSS (physical memory): ${formatMB(usage.rss)}MB`);
57 console.log(` External: ${formatMB(usage.external)}MB`);
58 
59 // Calculate memory growth
60 const growthInfo = this.calculateMemoryGrowth();
61 if (growthInfo.isGrowing) {
62 console.log(`Trend: +${growthInfo.growthRate.toFixed(2)}MB/min`);
63 }
64 }
65
66 private calculateMemoryGrowth(): { isGrowing: boolean; growthRate: number } {
67 if (this.memoryHistory.length < 10) return { isGrowing: false, growthRate: 0 };
68
69 const recent = this.memoryHistory.slice(-10);
70 const oldest = recent[0];
71 const newest = recent[recent.length - 1];
72 
73 const timeDiffMin = (newest.timestamp.getTime() - oldest.timestamp.getTime()) / 60000;
74 const memoryDiffMB = (newest.usage.heapUsed - oldest.usage.heapUsed) / 1024 / 1024;
75 
76 const growthRate = memoryDiffMB / timeDiffMin;
77 
78 return {
79 isGrowing: growthRate > 1, // More than 1MB/min
80 growthRate,
81 };
82 }
83
84 private suggestOptimizations(usage: NodeJS.MemoryUsage): void {
85 console.log(' Optimization suggestions:');
86 
87 if (usage.heapUsed / usage.heapTotal > 0.9) {
88 console.log(' - Run garbage collection: global.gc()');
89 }
90 
91 if (usage.external > 50 * 1024 * 1024) {
92 console.log(' - Check buffers and streams - perhaps too much data in memory');
93 }
94 
95 console.log(' - Check for potential memory leaks in caches');
96 console.log(' - Consider reducing TTL for temporary data');
97 }
98
99 getMemoryReport() {
100 const current = process.memoryUsage();
101 const formatMB = (bytes: number) => (bytes / 1024 / 1024).toFixed(2);
102 
103 return {
104 current: {
105 heapUsed: formatMB(current.heapUsed) + 'MB',
106 heapTotal: formatMB(current.heapTotal) + 'MB',
107 rss: formatMB(current.rss) + 'MB',
108 external: formatMB(current.external) + 'MB',
109 },
110 trend: this.calculateMemoryGrowth(),
111 history: this.memoryHistory.slice(-20), // Last 20 measurements
112 alerts: this.generateAlerts(current),
113 };
114 }
115
116 private generateAlerts(usage: NodeJS.MemoryUsage): string[] {
117 const alerts = [];
118 const heapUsedMB = usage.heapUsed / 1024 / 1024;
119 
120 if (heapUsedMB > this.MEMORY_THRESHOLD_MB) {
121 alerts.push(`High heap usage: ${heapUsedMB.toFixed(2)}MB`);
122 }
123 
124 if (usage.heapUsed / usage.heapTotal > 0.85) {
125 alerts.push('Heap is 85% full - consider garbage collection');
126 }
127 
128 const growthInfo = this.calculateMemoryGrowth();
129 if (growthInfo.isGrowing && growthInfo.growthRate > 5) {
130 alerts.push(`Rapid memory growth: ${growthInfo.growthRate.toFixed(2)}MB/min`);
131 }
132 
133 return alerts;
134 }
135
136 // Force garbage collection (wymaga --expose-gc flag)
137 forceGarbageCollection(): { before: number; after: number; freed: number } {
138 const beforeMB = process.memoryUsage().heapUsed / 1024 / 1024;
139 
140 if (global.gc) {
141 global.gc();
142 const afterMB = process.memoryUsage().heapUsed / 1024 / 1024;
143 const freedMB = beforeMB - afterMB;
144 
145 console.log(`Garbage collection: freed ${freedMB.toFixed(2)}MB`);
146 
147 return {
148 before: beforeMB,
149 after: afterMB,
150 freed: freedMB,
151 };
152 } else {
153 console.warn('Garbage collection unavailable - run with --expose-gc flag');
154 return { before: beforeMB, after: beforeMB, freed: 0 };
155 }
156 }
157}

Cache and collection optimization

1// memory-optimized-cache.service.ts
2@Injectable()
3export class MemoryOptimizedCacheService {
4 private cache = new Map<string, {
5 data: any;
6 timestamp: number;
7 accessCount: number;
8 lastAccess: number;
9 size: number;
10 }>();
11
12 private maxSize = 1000; // Maximum number of elements
13 private maxMemoryMB = 100; // Maximum cache memory
14 private currentMemoryBytes = 0;
15
16 set(key: string, value: any, ttl: number = 300000): void {
17 const size = this.estimateSize(value);
18 
19 // Check if adding a new element will exceed the limits
20 if (this.cache.size >= this.maxSize || 
21 (this.currentMemoryBytes + size) > this.maxMemoryMB * 1024 * 1024) {
22 this.evictOldest();
23 }
24
25 // Remove old element if it exists
26 if (this.cache.has(key)) {
27 const old = this.cache.get(key)!;
28 this.currentMemoryBytes -= old.size;
29 }
30
31 this.cache.set(key, {
32 data: value,
33 timestamp: Date.now(),
34 accessCount: 0,
35 lastAccess: Date.now(),
36 size,
37 });
38
39 this.currentMemoryBytes += size;
40 
41 // Set TTL
42 setTimeout(() => {
43 this.delete(key);
44 }, ttl);
45 }
46
47 get(key: string): any {
48 const item = this.cache.get(key);
49 if (!item) return null;
50
51 // Update access statistics
52 item.accessCount++;
53 item.lastAccess = Date.now();
54
55 return item.data;
56 }
57
58 delete(key: string): boolean {
59 const item = this.cache.get(key);
60 if (item) {
61 this.currentMemoryBytes -= item.size;
62 return this.cache.delete(key);
63 }
64 return false;
65 }
66
67 // Eviction using different strategies
68 private evictOldest(): void {
69 if (this.cache.size === 0) return;
70
71 const strategy = this.getEvictionStrategy();
72 
73 switch (strategy) {
74 case 'LRU': // Least Recently Used
75 this.evictLRU();
76 break;
77 case 'LFU': // Least Frequently Used
78 this.evictLFU();
79 break;
80 case 'SIZE': // Largest elements first
81 this.evictLargest();
82 break;
83 default:
84 this.evictOldestByTime();
85 }
86 }
87
88 private getEvictionStrategy(): string {
89 // Intelligent strategy selection based on memory state
90 const memoryPressure = this.currentMemoryBytes / (this.maxMemoryMB * 1024 * 1024);
91 
92 if (memoryPressure > 0.9) {
93 return 'SIZE'; // Under high memory pressure - remove large elements
94 } else if (this.cache.size > this.maxSize * 0.8) {
95 return 'LRU'; // Many elements - remove least recently used
96 } else {
97 return 'LFU'; // Normally - remove least popular
98 }
99 }
100
101 private evictLRU(): void {
102 let oldestKey = '';
103 let oldestTime = Date.now();
104
105 this.cache.forEach((item, key) => {
106 if (item.lastAccess < oldestTime) {
107 oldestTime = item.lastAccess;
108 oldestKey = key;
109 }
110 });
111
112 if (oldestKey) {
113 console.log(`LRU eviction: ${oldestKey}`);
114 this.delete(oldestKey);
115 }
116 }
117
118 private evictLFU(): void {
119 let leastUsedKey = '';
120 let leastUsedCount = Infinity;
121
122 this.cache.forEach((item, key) => {
123 if (item.accessCount < leastUsedCount) {
124 leastUsedCount = item.accessCount;
125 leastUsedKey = key;
126 }
127 });
128
129 if (leastUsedKey) {
130 console.log(`LFU eviction: ${leastUsedKey}`);
131 this.delete(leastUsedKey);
132 }
133 }
134
135 private evictLargest(): void {
136 let largestKey = '';
137 let largestSize = 0;
138
139 this.cache.forEach((item, key) => {
140 if (item.size > largestSize) {
141 largestSize = item.size;
142 largestKey = key;
143 }
144 });
145
146 if (largestKey) {
147 console.log(`SIZE eviction: ${largestKey} (${largestSize} bytes)`);
148 this.delete(largestKey);
149 }
150 }
151
152 private evictOldestByTime(): void {
153 let oldestKey = '';
154 let oldestTime = Date.now();
155
156 this.cache.forEach((item, key) => {
157 if (item.timestamp < oldestTime) {
158 oldestTime = item.timestamp;
159 oldestKey = key;
160 }
161 });
162
163 if (oldestKey) {
164 console.log(`TIME eviction: ${oldestKey}`);
165 this.delete(oldestKey);
166 }
167 }
168
169 private estimateSize(obj: any): number {
170 // Approximate calculation of object size in memory
171 const jsonString = JSON.stringify(obj);
172 return jsonString.length * 2; // UTF-16 = 2 bytes per character
173 }
174
175 getStats() {
176 const memoryUsageMB = this.currentMemoryBytes / 1024 / 1024;
177 const avgItemSize = this.cache.size > 0 ? this.currentMemoryBytes / this.cache.size : 0;
178 
179 let totalAccesses = 0;
180 this.cache.forEach(item => {
181 totalAccesses += item.accessCount;
182 });
183
184 return {
185 size: this.cache.size,
186 maxSize: this.maxSize,
187 memoryUsageMB: memoryUsageMB.toFixed(2),
188 maxMemoryMB: this.maxMemoryMB,
189 avgItemSize: Math.round(avgItemSize),
190 totalAccesses,
191 fillRatio: (this.cache.size / this.maxSize * 100).toFixed(1) + '%',
192 memoryRatio: (memoryUsageMB / this.maxMemoryMB * 100).toFixed(1) + '%',
193 };
194 }
195
196 // Cleaning up expired elements
197 @Cron('*/5 * * * * *') // Every 5 minutes
198 cleanupExpired(): void {
199 const now = Date.now();
200 const toDelete = [];
201
202 this.cache.forEach((item, key) => {
203 // Remove elements older than an hour without access
204 if (now - item.lastAccess > 3600000) {
205 toDelete.push(key);
206 }
207 });
208
209 toDelete.forEach(key => {
210 this.delete(key);
211 });
212
213 if (toDelete.length > 0) {
214 console.log(`🧹 Cleaned up ${toDelete.length} expired cache elements`);
215 }
216 }
217}
Go to CodeWorlds