• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

saygoweb / anorm / 18482579375

14 Oct 2025 01:22AM UTC coverage: 84.134% (+3.7%) from 80.483%
18482579375

Pull #42

github

web-flow
Merge f81a33b4f into 025025bcf
Pull Request #42: Implement comprehensive join optimization system to solve N+1 query problem

833 of 948 new or added lines in 19 files covered. (87.87%)

75 existing lines in 4 files now uncovered.

1628 of 1935 relevant lines covered (84.13%)

15.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.06
/src/Relationship/Performance/PerformanceMonitor.php
1
<?php
2

3
namespace Anorm\Relationship\Performance;
4

5
/**
6
 * Performance monitoring for relationship loading operations
7
 *
8
 * Tracks query counts, data transfer volumes, response times, and strategy decisions
9
 * to provide insights into optimization effectiveness.
10
 */
11
class PerformanceMonitor
12
{
13
    /** @var array Performance metrics storage */
14
    private $metrics = [];
15

16
    /** @var array Active operation tracking */
17
    private $activeOperations = [];
18

19
    /** @var bool Whether monitoring is enabled */
20
    private $enabled = true;
21

22
    public function __construct(bool $enabled = true)
23
    {
24
        $this->enabled = $enabled;
12✔
25
        $this->resetMetrics();
12✔
26
    }
27

28
    /**
29
     * Start tracking a relationship loading operation
30
     *
31
     * @param string $operationId Unique identifier for this operation
32
     * @param array $context Operation context (relationship type, model count, etc.)
33
     * @return void
34
     */
35
    public function startOperation(string $operationId, array $context = []): void
36
    {
37
        if (!$this->enabled) {
9✔
38
            return;
2✔
39
        }
40

41
        $this->activeOperations[$operationId] = [
8✔
42
            'start_time' => microtime(true),
8✔
43
            'start_memory' => memory_get_usage(true),
8✔
44
            'context' => $context,
8✔
45
            'queries_before' => $this->getCurrentQueryCount(),
8✔
46
        ];
8✔
47
    }
48

49
    /**
50
     * End tracking a relationship loading operation
51
     *
52
     * @param string $operationId Operation identifier
53
     * @param array $results Operation results (loaded models, etc.)
54
     * @return void
55
     */
56
    public function endOperation(string $operationId, array $results = []): void
57
    {
58
        if (!$this->enabled || !isset($this->activeOperations[$operationId])) {
9✔
59
            return;
3✔
60
        }
61

62
        $operation = $this->activeOperations[$operationId];
7✔
63
        $endTime = microtime(true);
7✔
64
        $endMemory = memory_get_usage(true);
7✔
65

66
        $metrics = [
7✔
67
            'operation_id' => $operationId,
7✔
68
            'duration' => $endTime - $operation['start_time'],
7✔
69
            'memory_used' => $endMemory - $operation['start_memory'],
7✔
70
            'queries_executed' => $this->getCurrentQueryCount() - $operation['queries_before'],
7✔
71
            'context' => $operation['context'],
7✔
72
            'results' => $results,
7✔
73
            'timestamp' => $endTime
7✔
74
        ];
7✔
75

76
        $this->recordMetrics($metrics);
7✔
77
        unset($this->activeOperations[$operationId]);
7✔
78
    }
79

80
    /**
81
     * Record strategy selection decision
82
     *
83
     * @param string $relationshipType Type of relationship
84
     * @param string $selectedStrategy Strategy that was selected
85
     * @param array $decisionFactors Factors that influenced the decision
86
     * @return void
87
     */
88
    public function recordStrategySelection(string $relationshipType, string $selectedStrategy, array $decisionFactors): void
89
    {
90
        if (!$this->enabled) {
5✔
91
            return;
1✔
92
        }
93

94
        if (!isset($this->metrics['strategy_selections'])) {
4✔
NEW
95
            $this->metrics['strategy_selections'] = [];
×
96
        }
97

98
        $this->metrics['strategy_selections'][] = [
4✔
99
            'relationship_type' => $relationshipType,
4✔
100
            'selected_strategy' => $selectedStrategy,
4✔
101
            'decision_factors' => $decisionFactors,
4✔
102
            'timestamp' => microtime(true)
4✔
103
        ];
4✔
104
    }
105

106
    /**
107
     * Record query count reduction
108
     *
109
     * @param int $beforeCount Query count before optimization
110
     * @param int $afterCount Query count after optimization
111
     * @param string $strategy Strategy used
112
     * @return void
113
     */
114
    public function recordQueryReduction(int $beforeCount, int $afterCount, string $strategy): void
115
    {
116
        if (!$this->enabled) {
3✔
NEW
117
            return;
×
118
        }
119

120
        $reduction = $beforeCount - $afterCount;
3✔
121
        $reductionPercentage = $beforeCount > 0 ? ($reduction / $beforeCount) * 100 : 0;
3✔
122

123
        $this->metrics['query_reductions'][] = [
3✔
124
            'before_count' => $beforeCount,
3✔
125
            'after_count' => $afterCount,
3✔
126
            'reduction' => $reduction,
3✔
127
            'reduction_percentage' => $reductionPercentage,
3✔
128
            'strategy' => $strategy,
3✔
129
            'timestamp' => microtime(true)
3✔
130
        ];
3✔
131
    }
132

133
    /**
134
     * Record data transfer volume
135
     *
136
     * @param int $bytesTransferred Number of bytes transferred
137
     * @param string $strategy Strategy used
138
     * @param array $context Additional context
139
     * @return void
140
     */
141
    public function recordDataTransfer(int $bytesTransferred, string $strategy, array $context = []): void
142
    {
143
        if (!$this->enabled) {
2✔
NEW
144
            return;
×
145
        }
146

147
        $this->metrics['data_transfers'][] = [
2✔
148
            'bytes_transferred' => $bytesTransferred,
2✔
149
            'strategy' => $strategy,
2✔
150
            'context' => $context,
2✔
151
            'timestamp' => microtime(true)
2✔
152
        ];
2✔
153
    }
154

155
    /**
156
     * Get comprehensive performance report
157
     *
158
     * @return array Performance analysis report
159
     */
160
    public function getPerformanceReport(): array
161
    {
162
        if (!$this->enabled) {
12✔
163
            return ['monitoring_disabled' => true];
2✔
164
        }
165

166
        return [
11✔
167
            'summary' => $this->generateSummary(),
11✔
168
            'query_optimization' => $this->analyzeQueryOptimization(),
11✔
169
            'strategy_effectiveness' => $this->analyzeStrategyEffectiveness(),
11✔
170
            'data_transfer_analysis' => $this->analyzeDataTransfer(),
11✔
171
            'response_time_analysis' => $this->analyzeResponseTimes(),
11✔
172
            'recommendations' => $this->generateRecommendations()
11✔
173
        ];
11✔
174
    }
175

176
    /**
177
     * Generate performance summary
178
     *
179
     * @return array Summary metrics
180
     */
181
    private function generateSummary(): array
182
    {
183
        $totalOperations = count($this->metrics['operations']);
11✔
184
        $totalDuration = array_sum(array_column($this->metrics['operations'], 'duration'));
11✔
185
        $totalQueries = array_sum(array_column($this->metrics['operations'], 'queries_executed'));
11✔
186
        $totalMemory = array_sum(array_column($this->metrics['operations'], 'memory_used'));
11✔
187

188
        return [
11✔
189
            'total_operations' => $totalOperations,
11✔
190
            'total_duration' => $totalDuration,
11✔
191
            'average_duration' => $totalOperations > 0 ? $totalDuration / $totalOperations : 0,
11✔
192
            'total_queries' => $totalQueries,
11✔
193
            'average_queries_per_operation' => $totalOperations > 0 ? $totalQueries / $totalOperations : 0,
11✔
194
            'total_memory_used' => $totalMemory,
11✔
195
            'average_memory_per_operation' => $totalOperations > 0 ? $totalMemory / $totalOperations : 0
11✔
196
        ];
11✔
197
    }
198

199
    /**
200
     * Analyze query optimization effectiveness
201
     *
202
     * @return array Query optimization analysis
203
     */
204
    private function analyzeQueryOptimization(): array
205
    {
206
        if (empty($this->metrics['query_reductions'])) {
11✔
207
            return ['no_data' => true];
8✔
208
        }
209

210
        $reductions = $this->metrics['query_reductions'];
3✔
211
        $totalReduction = array_sum(array_column($reductions, 'reduction'));
3✔
212
        $averageReduction = array_sum(array_column($reductions, 'reduction_percentage')) / count($reductions);
3✔
213

214
        return [
3✔
215
            'total_queries_saved' => $totalReduction,
3✔
216
            'average_reduction_percentage' => $averageReduction,
3✔
217
            'optimization_instances' => count($reductions),
3✔
218
            'best_reduction' => max(array_column($reductions, 'reduction_percentage')),
3✔
219
            'worst_reduction' => min(array_column($reductions, 'reduction_percentage'))
3✔
220
        ];
3✔
221
    }
222

223
    /**
224
     * Analyze strategy effectiveness
225
     *
226
     * @return array Strategy analysis
227
     */
228
    private function analyzeStrategyEffectiveness(): array
229
    {
230
        if (empty($this->metrics['strategy_selections'])) {
11✔
231
            return ['no_data' => true];
8✔
232
        }
233

234
        $strategies = array_column($this->metrics['strategy_selections'], 'selected_strategy');
4✔
235
        $strategyCounts = array_count_values($strategies);
4✔
236

237
        return [
4✔
238
            'strategy_usage' => $strategyCounts,
4✔
239
            'most_used_strategy' => array_keys($strategyCounts, max($strategyCounts))[0],
4✔
240
            'total_selections' => count($strategies)
4✔
241
        ];
4✔
242
    }
243

244
    /**
245
     * Analyze data transfer patterns
246
     *
247
     * @return array Data transfer analysis
248
     */
249
    private function analyzeDataTransfer(): array
250
    {
251
        if (empty($this->metrics['data_transfers'])) {
11✔
252
            return ['no_data' => true];
9✔
253
        }
254

255
        $transfers = $this->metrics['data_transfers'];
2✔
256
        $totalBytes = array_sum(array_column($transfers, 'bytes_transferred'));
2✔
257
        $averageBytes = $totalBytes / count($transfers);
2✔
258

259
        return [
2✔
260
            'total_bytes_transferred' => $totalBytes,
2✔
261
            'average_bytes_per_operation' => $averageBytes,
2✔
262
            'transfer_operations' => count($transfers),
2✔
263
            'largest_transfer' => max(array_column($transfers, 'bytes_transferred')),
2✔
264
            'smallest_transfer' => min(array_column($transfers, 'bytes_transferred'))
2✔
265
        ];
2✔
266
    }
267

268
    /**
269
     * Analyze response times
270
     *
271
     * @return array Response time analysis
272
     */
273
    private function analyzeResponseTimes(): array
274
    {
275
        if (empty($this->metrics['operations'])) {
11✔
276
            return ['no_data' => true];
5✔
277
        }
278

279
        $durations = array_column($this->metrics['operations'], 'duration');
7✔
280
        sort($durations);
7✔
281

282
        $count = count($durations);
7✔
283
        $median = $count % 2 === 0
7✔
284
            ? ($durations[$count / 2 - 1] + $durations[$count / 2]) / 2
3✔
285
            : $durations[floor($count / 2)];
5✔
286

287
        return [
7✔
288
            'average_response_time' => array_sum($durations) / $count,
7✔
289
            'median_response_time' => $median,
7✔
290
            'fastest_response' => min($durations),
7✔
291
            'slowest_response' => max($durations),
7✔
292
            'p95_response_time' => $durations[floor($count * 0.95)]
7✔
293
        ];
7✔
294
    }
295

296
    /**
297
     * Generate optimization recommendations
298
     *
299
     * @return array Recommendations for improvement
300
     */
301
    private function generateRecommendations(): array
302
    {
303
        $recommendations = [];
11✔
304

305
        // Analyze query patterns
306
        if (!empty($this->metrics['query_reductions'])) {
11✔
307
            $avgReduction = array_sum(array_column($this->metrics['query_reductions'], 'reduction_percentage'))
3✔
308
                          / count($this->metrics['query_reductions']);
3✔
309

310
            if ($avgReduction < 50) {
3✔
311
                $recommendations[] = "Query reduction is below 50%. Consider reviewing strategy selection criteria.";
1✔
312
            }
313
        }
314

315
        // Analyze response times
316
        if (!empty($this->metrics['operations'])) {
11✔
317
            $avgDuration = array_sum(array_column($this->metrics['operations'], 'duration'))
7✔
318
                         / count($this->metrics['operations']);
7✔
319

320
            if ($avgDuration > 0.1) { // 100ms
7✔
321
                $recommendations[] = "Average response time is high. Consider optimizing database indexes or query strategies.";
1✔
322
            }
323
        }
324

325
        // Analyze strategy usage
326
        if (!empty($this->metrics['strategy_selections'])) {
11✔
327
            $strategies = array_column($this->metrics['strategy_selections'], 'selected_strategy');
4✔
328
            $strategyCounts = array_count_values($strategies);
4✔
329

330
            if (
331
                isset($strategyCounts['individual_loading']) &&
4✔
332
                $strategyCounts['individual_loading'] / count($strategies) > 0.5
4✔
333
            ) {
334
                $recommendations[] = "High usage of individual loading strategy. Review batch loading thresholds.";
1✔
335
            }
336
        }
337

338
        return $recommendations;
11✔
339
    }
340

341
    /**
342
     * Record operation metrics
343
     *
344
     * @param array $metrics Metrics to record
345
     * @return void
346
     */
347
    private function recordMetrics(array $metrics): void
348
    {
349
        $this->metrics['operations'][] = $metrics;
7✔
350
    }
351

352
    /**
353
     * Get current query count (simplified implementation)
354
     *
355
     * @return int Current query count
356
     */
357
    private function getCurrentQueryCount(): int
358
    {
359
        // This is a simplified implementation
360
        // In practice, this would integrate with PDO or a query logger
361
        static $queryCount = 0;
8✔
362
        return $queryCount++;
8✔
363
    }
364

365
    /**
366
     * Reset all metrics
367
     *
368
     * @return void
369
     */
370
    public function resetMetrics(): void
371
    {
372
        $this->metrics = [
12✔
373
            'operations' => [],
12✔
374
            'strategy_selections' => [],
12✔
375
            'query_reductions' => [],
12✔
376
            'data_transfers' => []
12✔
377
        ];
12✔
378
    }
379

380
    /**
381
     * Enable or disable monitoring
382
     *
383
     * @param bool $enabled Whether to enable monitoring
384
     * @return void
385
     */
386
    public function setEnabled(bool $enabled): void
387
    {
388
        $this->enabled = $enabled;
1✔
389
    }
390
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc