Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Performance Optimization - zwiększanie prędkości kohorty

inżynierze legionowy! Konsul Caesar.js zauważył, że nasz fort zaczyna pracować wolniej pod dużym obciążeniem. Czas na kompleksową optymalizację wydajności - każdy element musi działać jak najlepiej naoliwiony mechanizm!

Czym jest optymalizacja wydajności?

Wyobraź sobie aplikacja jako skomplikowaną maszynę:

  • Silnik (CPU) - musi być wydajny
  • Tarcze (pamięć) - muszą być dobrze wyważone
  • Ładownia (baza danych) - musi być zorganizowana
  • legion (procesy) - musi współpracować sprawnie
  • Rumpel (I/O operations) - musi być płynny

Optymalizacja to sztuka harmonijnego dostrojenia wszystkich elementów!

Profiling - diagnoza problemów

1// performance-profiler.service.ts
2import { Injectable } from '@nestjs/common';
3import { performance, PerformanceObserver } from 'perf_hooks';
4
5@Injectable()
6export class PerformanceProfilerService {
7 private measurements = new Map<string, number[]>();
8 private observer: PerformanceObserver;
9
10 constructor() {
11 this.setupPerformanceObserver();
12 }
13
14 // Decorator do mierzenia czasu wykonania
15 static Measure(operationName?: string) {
16 return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
17 const originalMethod = descriptor.value;
18 const measureName = operationName || `${target.constructor.name}.${propertyName}`;
19
20 descriptor.value = async function (...args: any[]) {
21 const startMark = `${measureName}-start-${Date.now()}`;
22 const endMark = `${measureName}-end-${Date.now()}`;
23 
24 performance.mark(startMark);
25 
26 try {
27 const result = await originalMethod.apply(this, args);
28 performance.mark(endMark);
29 performance.measure(measureName, startMark, endMark);
30 return result;
31 } catch (error) {
32 performance.mark(endMark);
33 performance.measure(`${measureName}-error`, startMark, endMark);
34 throw error;
35 }
36 };
37 };
38 }
39
40 startMeasurement(name: string): void {
41 performance.mark(`${name}-start`);
42 }
43
44 endMeasurement(name: string): number {
45 const endMark = `${name}-end`;
46 performance.mark(endMark);
47 performance.measure(name, `${name}-start`, endMark);
48 
49 const measurement = performance.getEntriesByName(name, 'measure')[0];
50 return measurement?.duration || 0;
51 }
52
53 private setupPerformanceObserver(): void {
54 this.observer = new PerformanceObserver((list) => {
55 list.getEntries().forEach((entry) => {
56 if (entry.entryType === 'measure') {
57 this.recordMeasurement(entry.name, entry.duration);
58 }
59 });
60 });
61 
62 this.observer.observe({ entryTypes: ['measure'] });
63 }
64
65 private recordMeasurement(name: string, duration: number): void {
66 if (!this.measurements.has(name)) {
67 this.measurements.set(name, []);
68 }
69 
70 const measurements = this.measurements.get(name)!;
71 measurements.push(duration);
72 
73 // Zachowaj tylko ostatnie 100 pomiarów
74 if (measurements.length > 100) {
75 measurements.shift();
76 }
77 
78 // Loguj wolne operacje
79 if (duration > 1000) {
80 console.warn(`Wolna operacja: ${name} - ${duration.toFixed(2)}ms`);
81 }
82 }
83
84 getStatistics(operationName: string) {
85 const measurements = this.measurements.get(operationName) || [];
86 if (measurements.length === 0) return null;
87
88 const sorted = [...measurements].sort((a, b) => a - b);
89 const avg = measurements.reduce((a, b) => a + b, 0) / measurements.length;
90 
91 return {
92 count: measurements.length,
93 average: avg.toFixed(2),
94 median: sorted[Math.floor(sorted.length / 2)].toFixed(2),
95 min: sorted[0].toFixed(2),
96 max: sorted[sorted.length - 1].toFixed(2),
97 p95: sorted[Math.floor(sorted.length * 0.95)].toFixed(2),
98 };
99 }
100
101 getAllStatistics() {
102 const stats = {};
103 this.measurements.forEach((_, name) => {
104 stats[name] = this.getStatistics(name);
105 });
106 return stats;
107 }
108}
109
110// Użycie w serwisie
111@Injectable()
112export class TributeService {
113 constructor(private profiler: PerformanceProfilerService) {}
114
115 @PerformanceProfilerService.Measure('tribute-search')
116 async findTributes(criteria: any) {
117 // Złożone wyszukiwanie tributów
118 await this.complexDatabaseQuery(criteria);
119 return [];
120 }
121
122 async manualMeasurement() {
123 this.profiler.startMeasurement('manual-operation');
124 
125 // Jakaś operacja
126 await this.doSomething();
127 
128 const duration = this.profiler.endMeasurement('manual-operation');
129 console.log(`Operacja zajęła: ${duration}ms`);
130 }
131}

Database Query Optimization

1// query-optimizer.service.ts
2@Injectable()
3export class QueryOptimizerService {
4 constructor(
5 @InjectRepository(Tribute) private tributeRepo: Repository<Tribute>,
6 ) {}
7
8 // Zamiast N+1 queries
9 async getBadTributesWithOwners(): Promise<any[]> {
10 const tributes = await this.tributeRepo.find();
11 
12 // ❌ To generuje N zapytań dodatkowych!
13 const tributesWithOwners = [];
14 for (const tribute of tributes) {
15 const owner = await this.getOwner(tribute.ownerId);
16 tributesWithOwners.push({ ...tribute, owner });
17 }
18 
19 return tributesWithOwners;
20 }
21
22 // Optymalna wersja z JOIN
23 async getOptimizedTributesWithOwners(): Promise<any[]> {
24 // ✅ Jedno zapytanie z JOIN
25 return await this.tributeRepo.find({
26 relations: ['owner'],
27 select: {
28 id: true,
29 name: true,
30 value: true,
31 owner: {
32 id: true,
33 name: true,
34 rank: true,
35 },
36 },
37 });
38 }
39
40 // Batch processing dla dużych zbiorów
41 async processTributesInBatches(batchSize: number = 100): Promise<void> {
42 let offset = 0;
43 let batch;
44 
45 do {
46 batch = await this.tributeRepo.find({
47 skip: offset,
48 take: batchSize,
49 order: { id: 'ASC' },
50 });
51 
52 if (batch.length > 0) {
53 await this.processTributeBatch(batch);
54 offset += batchSize;
55 
56 // Daj czas na odśwież garbage collectora
57 await this.delay(10);
58 }
59 } while (batch.length === batchSize);
60 }
61
62 // Paginacja z cursor dla bardzo dużych zbiorów
63 async getTributesWithCursor(cursor?: number, limit: number = 50) {
64 const qb = this.tributeRepo.createQueryBuilder('tribute');
65 
66 if (cursor) {
67 qb.where('tribute.id > :cursor', { cursor });
68 }
69 
70 const tributes = await qb
71 .orderBy('tribute.id', 'ASC')
72 .limit(limit + 1) // +1 żeby sprawdzić czy są kolejne
73 .getMany();
74 
75 const hasNext = tributes.length > limit;
76 const items = hasNext ? tributes.slice(0, -1) : tributes;
77 const nextCursor = hasNext ? tributes[tributes.length - 2].id : null;
78 
79 return {
80 items,
81 nextCursor,
82 hasNext,
83 };
84 }
85
86 // Optymalizacja z indeksami
87 async searchTributesOptimized(filters: any) {
88 const qb = this.tributeRepo.createQueryBuilder('tribute');
89 
90 // Używaj indeksowanych kolumn w WHERE
91 if (filters.type) {
92 qb.andWhere('tribute.type = :type', { type: filters.type });
93 }
94 
95 if (filters.minValue) {
96 qb.andWhere('tribute.value >= :minValue', { minValue: filters.minValue });
97 }
98 
99 // Użyj indeksu na created_at
100 if (filters.dateRange) {
101 qb.andWhere('tribute.createdAt BETWEEN :start AND :end', {
102 start: filters.dateRange.start,
103 end: filters.dateRange.end,
104 });
105 }
106 
107 return await qb
108 .orderBy('tribute.value', 'DESC') // Indeks na value
109 .limit(filters.limit || 100)
110 .getMany();
111 }
112
113 private async delay(ms: number): Promise<void> {
114 return new Promise(resolve => setTimeout(resolve, ms));
115 }
116
117 private async processTributeBatch(tributes: Tribute[]): Promise<void> {
118 // Przetwarzanie batch'a
119 console.log(`Przetwarzam ${tributes.length} tributów...`);
120 }
121}
Przejdź do CodeWorlds