Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Memory Management - optymalizacja magazynów kastrum

zarządco magazynów! Konsul Caesar.js zauważył, że nasz fort zaczyna być przeciążony – magazyny pękają w szwach, a legion ma coraz mniej miejsca na manewry. Czas nauczyć się zarządzania pamięcią w Node.js!

Czym jest zarządzanie pamięcią w świecie legionariuszów?

Wyobraź sobie aplikacja jako system magazynów:

  • Heap - główna ładownia, gdzie przechowywane są tributy (obiekty)
  • Stack - formacja, gdzie tymczasowo układane są rzeczy (zmienne lokalne)
  • Garbage Collector - legion sprzątająca nieużywane przedmioty
  • Memory leaks - tributy, które zostały zapomniane i nie można ich usunąć

Dobre zarządzanie pamięcią = sprawny, szybki fort!

Monitoring pamięci

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 przy 512MB
14 private readonly HISTORY_SIZE = 100;
15
16 @Cron('*/10 * * * * *') // Co 10 sekund
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 // Zachowaj tylko ostatnie 100 pomiarów
28 if (this.memoryHistory.length > this.HISTORY_SIZE) {
29 this.memoryHistory.shift();
30 }
31
32 // Sprawdź czy pamięć nie przekracza progu
33 const heapUsedMB = usage.heapUsed / 1024 / 1024;
34 if (heapUsedMB > this.MEMORY_THRESHOLD_MB) {
35 console.warn(` Wysokie użycie pamięci: ${heapUsedMB.toFixed(2)}MB`);
36 this.suggestOptimizations(usage);
37 }
38
39 // Loguj co minutę
40 if (new Date().getSeconds() === 0) {
41 this.logMemoryStatus(usage);
42 }
43 }
44
45 private getExternalMemory(): number {
46 // Przybliżona pamięć zewnętrzna (bufory, 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(' Status magazynów kastrum:');
54 console.log(` Heap używany: ${formatMB(usage.heapUsed)}MB`);
55 console.log(` Heap całkowity: ${formatMB(usage.heapTotal)}MB`);
56 console.log(` RSS (pamięć fizyczna): ${formatMB(usage.rss)}MB`);
57 console.log(` Zewnętrzna: ${formatMB(usage.external)}MB`);
58 
59 // Oblicz wzrost pamięci
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, // Więcej niż 1MB/min
80 growthRate,
81 };
82 }
83
84 private suggestOptimizations(usage: NodeJS.MemoryUsage): void {
85 console.log(' Sugestie optymalizacji:');
86 
87 if (usage.heapUsed / usage.heapTotal > 0.9) {
88 console.log(' - Uruchom garbage collection: global.gc()');
89 }
90 
91 if (usage.external > 50 * 1024 * 1024) {
92 console.log(' - Sprawdź bufory i strumienie - może za dużo danych w pamięci');
93 }
94 
95 console.log(' - Sprawdź potencjalne memory leaks w cache'ach');
96 console.log(' - Rozważ zmniejszenie TTL dla danych tymczasowych');
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), // Ostatnie 20 pomiarów
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(`Wysokie użycie heap: ${heapUsedMB.toFixed(2)}MB`);
122 }
123 
124 if (usage.heapUsed / usage.heapTotal > 0.85) {
125 alerts.push('Heap zapełniony w 85% - rozważ garbage collection');
126 }
127 
128 const growthInfo = this.calculateMemoryGrowth();
129 if (growthInfo.isGrowing && growthInfo.growthRate > 5) {
130 alerts.push(`Szybki wzrost pamięci: ${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: zwolniono ${freedMB.toFixed(2)}MB`);
146 
147 return {
148 before: beforeMB,
149 after: afterMB,
150 freed: freedMB,
151 };
152 } else {
153 console.warn('Garbage collection niedostępny - uruchom z flagą --expose-gc');
154 return { before: beforeMB, after: beforeMB, freed: 0 };
155 }
156 }
157}

Optymalizacja cache'a i kolekcji

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; // Maksymalna liczba elementów
13 private maxMemoryMB = 100; // Maksymalna pamięć cache'a
14 private currentMemoryBytes = 0;
15
16 set(key: string, value: any, ttl: number = 300000): void {
17 const size = this.estimateSize(value);
18 
19 // Sprawdź czy dodanie nowego elementu nie przekroczy limitów
20 if (this.cache.size >= this.maxSize || 
21 (this.currentMemoryBytes + size) > this.maxMemoryMB * 1024 * 1024) {
22 this.evictOldest();
23 }
24
25 // Usuń stary element jeśli istnieje
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 // Ustaw 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 // Aktualizuj statystyki dostępu
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 // Eksmisja według różnych strategii
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': // Największe elementy pierwsze
81 this.evictLargest();
82 break;
83 default:
84 this.evictOldestByTime();
85 }
86 }
87
88 private getEvictionStrategy(): string {
89 // Inteligentny wybór strategii na podstawie stanu pamięci
90 const memoryPressure = this.currentMemoryBytes / (this.maxMemoryMB * 1024 * 1024);
91 
92 if (memoryPressure > 0.9) {
93 return 'SIZE'; // Pod wysokim ciśnieniem pamięci - usuń duże elementy
94 } else if (this.cache.size > this.maxSize * 0.8) {
95 return 'LRU'; // Dużo elementów - usuń najrzadziej używane
96 } else {
97 return 'LFU'; // Normalnie - usuń najmniej popularne
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 // Przybliżone obliczenie rozmiaru obiektu w pamięci
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 // Czyszczenie przeterminowanych elementów
197 @Cron('*/5 * * * * *') // Co 5 minut
198 cleanupExpired(): void {
199 const now = Date.now();
200 const toDelete = [];
201
202 this.cache.forEach((item, key) => {
203 // Usuń elementy starsze niż godzina bez dostępu
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(`🧹 Wyczyszczono ${toDelete.length} przestarzałych elementów cache'a`);
215 }
216 }
217}
Vai a CodeWorlds