PerformanceMonitor.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. <?php
  2. namespace App\Http\Middleware;
  3. use Closure;
  4. use Illuminate\Http\Request;
  5. use Illuminate\Support\Facades\DB;
  6. use Illuminate\Support\Facades\Log;
  7. use Symfony\Component\HttpFoundation\Response;
  8. class PerformanceMonitor
  9. {
  10. /**
  11. * Initial performance markers
  12. */
  13. private float $startTime;
  14. private int $startMemory;
  15. private int $startPeakMemory;
  16. /**
  17. * Output buffer
  18. */
  19. private string $buffer = '';
  20. /**
  21. * ANSI color codes for console output
  22. */
  23. private const COLORS = [
  24. 'green' => "\033[32m",
  25. 'yellow' => "\033[33m",
  26. 'magenta' => "\033[35m",
  27. 'cyan' => "\033[36m",
  28. 'gray' => "\033[90m",
  29. 'reset' => "\033[0m"
  30. ];
  31. /**
  32. * Performance thresholds (in milliseconds)
  33. */
  34. private const SLOW_REQUEST_THRESHOLD = 1000;
  35. private const SLOW_QUERY_THRESHOLD = 100;
  36. public function __construct()
  37. {
  38. $this->startTime = defined('LARAVEL_START') ? LARAVEL_START : microtime(true);
  39. $this->startMemory = memory_get_usage();
  40. $this->startPeakMemory = memory_get_peak_usage();
  41. }
  42. public function handle(Request $request, Closure $next): Response
  43. {
  44. if (app()->environment('local', 'development')) {
  45. DB::enableQueryLog();
  46. }
  47. $response = $next($request);
  48. if (app()->environment('local', 'development')) {
  49. $this->logPerformanceMetrics($request);
  50. $this->flushBuffer();
  51. }
  52. return $response;
  53. }
  54. private function gatherMetrics(): array
  55. {
  56. $memoryLimit = $this->getMemoryLimit();
  57. return [
  58. 'time' => $this->getExecutionTime(),
  59. 'memory' => $this->getMemoryUsage(),
  60. 'peakMemory' => $this->getPeakMemoryUsage(),
  61. 'memoryLimit' => $memoryLimit,
  62. 'memoryPercentage' => $this->calculateMemoryPercentage($memoryLimit),
  63. 'queries' => $this->getQueryMetrics(),
  64. 'cpu' => $this->getCpuMetrics(),
  65. 'cache' => $this->getCacheMetrics(),
  66. ];
  67. }
  68. private function getExecutionTime(): float
  69. {
  70. return (microtime(true) - $this->startTime) * 1000;
  71. }
  72. private function getMemoryUsage(): float
  73. {
  74. return (memory_get_usage() - $this->startMemory) / 1024 / 1024;
  75. }
  76. private function getPeakMemoryUsage(): float
  77. {
  78. return (memory_get_peak_usage() - $this->startPeakMemory) / 1024 / 1024;
  79. }
  80. private function getMemoryLimit(): int
  81. {
  82. $limit = ini_get('memory_limit');
  83. if (!$limit) {
  84. return 0;
  85. }
  86. preg_match('/^(\d+)(K|M|G)?$/i', $limit, $matches);
  87. $value = (int)($matches[1] ?? 0);
  88. $unit = strtoupper($matches[2] ?? '');
  89. return match ($unit) {
  90. 'G' => $value * 1024 * 1024 * 1024,
  91. 'M' => $value * 1024 * 1024,
  92. 'K' => $value * 1024,
  93. default => $value,
  94. };
  95. }
  96. private function calculateMemoryPercentage(int $limit): float
  97. {
  98. if ($limit === 0) {
  99. return 0.0;
  100. }
  101. return (memory_get_usage(true) / $limit) * 100;
  102. }
  103. private function getQueryMetrics(): ?array
  104. {
  105. $queries = DB::getQueryLog();
  106. if (empty($queries)) {
  107. return null;
  108. }
  109. $totalTime = 0;
  110. $formattedQueries = [];
  111. foreach ($queries as $query) {
  112. $totalTime += $query['time'];
  113. if ($query['time'] > self::SLOW_QUERY_THRESHOLD) {
  114. $formattedQueries[] = [
  115. 'sql' => $this->formatSql($query['query'], $query['bindings']),
  116. 'time' => $query['time'],
  117. ];
  118. }
  119. }
  120. return [
  121. 'count' => count($queries),
  122. 'time' => $totalTime,
  123. 'slow_queries' => $formattedQueries,
  124. ];
  125. }
  126. private function formatSql(string $sql, array $bindings): string
  127. {
  128. return vsprintf(str_replace(['%', '?'], ['%%', "'%s'"], $sql), $bindings);
  129. }
  130. private function getCpuMetrics(): ?array
  131. {
  132. if (!function_exists('getrusage')) {
  133. return null;
  134. }
  135. $usage = getrusage();
  136. return [
  137. 'user' => ($usage['ru_utime.tv_sec'] * 1000 + intval($usage['ru_utime.tv_usec'] / 1000)),
  138. 'system' => ($usage['ru_stime.tv_sec'] * 1000 + intval($usage['ru_stime.tv_usec'] / 1000)),
  139. ];
  140. }
  141. private function getCacheMetrics(): ?array
  142. {
  143. if (!function_exists('opcache_get_status')) {
  144. return null;
  145. }
  146. $status = opcache_get_status(false);
  147. if (!$status) {
  148. return null;
  149. }
  150. $stats = $status['opcache_statistics'];
  151. $hits = $stats['hits'];
  152. $misses = $stats['misses'];
  153. $total = $hits + $misses;
  154. return [
  155. 'hit_rate' => $total > 0 ? ($hits / $total * 100) : 0,
  156. 'memory_usage' => $status['memory_usage'],
  157. ];
  158. }
  159. private function appendToBuffer(string $text): void
  160. {
  161. $this->buffer .= $text;
  162. }
  163. private function flushBuffer(): void
  164. {
  165. if (empty($this->buffer)) {
  166. return;
  167. }
  168. try {
  169. $written = file_put_contents('php://stderr', $this->buffer);
  170. if ($written === false) {
  171. error_log('Failed to write performance metrics to stderr');
  172. }
  173. } catch (\Throwable $e) {
  174. error_log("Error writing performance metrics: {$e->getMessage()}");
  175. } finally {
  176. $this->buffer = '';
  177. }
  178. }
  179. private function colorize(string $text, string $color): string
  180. {
  181. return self::COLORS[$color] . $text . self::COLORS['reset'];
  182. }
  183. private function logPerformanceMetrics(Request $request): void
  184. {
  185. $metrics = $this->gatherMetrics();
  186. // Request information
  187. $this->appendToBuffer(sprintf(
  188. "\n%s %s %s[%s]%s\n",
  189. $this->colorize($request->method(), 'green'),
  190. $this->colorize($request->getRequestUri(), 'green'),
  191. self::COLORS['gray'],
  192. date('Y-m-d H:i:s'),
  193. self::COLORS['reset']
  194. ));
  195. // Core metrics
  196. $this->appendToBuffer(sprintf(
  197. "➜ Time: %s%.2fms%s | Memory: %s%.2fMB%s (%.1f%%) | Peak: %s%.2fMB%s",
  198. self::COLORS['yellow'],
  199. $metrics['time'],
  200. self::COLORS['reset'],
  201. self::COLORS['magenta'],
  202. $metrics['memory'],
  203. self::COLORS['reset'],
  204. $metrics['memoryPercentage'],
  205. self::COLORS['magenta'],
  206. $metrics['peakMemory'],
  207. self::COLORS['reset']
  208. ));
  209. // Query metrics
  210. if ($metrics['queries']) {
  211. $this->appendToBuffer(sprintf(
  212. " | Queries: %s%d%s (%s%.2fms%s)",
  213. self::COLORS['cyan'],
  214. $metrics['queries']['count'],
  215. self::COLORS['reset'],
  216. self::COLORS['cyan'],
  217. $metrics['queries']['time'],
  218. self::COLORS['reset']
  219. ));
  220. }
  221. // CPU metrics
  222. if ($metrics['cpu']) {
  223. $totalCpu = $metrics['cpu']['user'] + $metrics['cpu']['system'];
  224. $this->appendToBuffer(sprintf(
  225. " | CPU: %s%.2fms%s",
  226. self::COLORS['cyan'],
  227. $totalCpu,
  228. self::COLORS['reset']
  229. ));
  230. }
  231. // Cache metrics
  232. if ($metrics['cache']) {
  233. $this->appendToBuffer(sprintf(
  234. " | OPcache: %s%.1f%%%s",
  235. self::COLORS['cyan'],
  236. $metrics['cache']['hit_rate'],
  237. self::COLORS['reset']
  238. ));
  239. }
  240. $this->appendToBuffer("\n");
  241. // Log slow queries
  242. if ($metrics['time'] > self::SLOW_REQUEST_THRESHOLD && !empty($metrics['queries']['slow_queries'])) {
  243. $this->appendToBuffer("\n" . $this->colorize("Slow Queries Detected:", 'yellow') . "\n");
  244. foreach ($metrics['queries']['slow_queries'] as $query) {
  245. $this->appendToBuffer(sprintf(
  246. "➜ %s%.2fms%s: %s\n",
  247. self::COLORS['yellow'],
  248. $query['time'],
  249. self::COLORS['reset'],
  250. $query['sql']
  251. ));
  252. }
  253. }
  254. }
  255. }