PluginProbe
JCH Optimize / trunk
JCH Optimize vtrunk
6.0.2 6.0.1 trunk 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2.0 1.2.1 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.1.0 2.10.0 2.2.0 2.2.1 All 58 releases
jch-optimize / lib / src / Debug / JchProfiler.php

JchProfiler.php in JCH Optimize trunk, at lib/src/Debug/JchProfiler.php

174 lines 4.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * JCH Optimize - Performs several front-end optimizations for fast downloads
5 *
6 * @package jchoptimize/core
7 * @author Samuel Marshall <samuel@jch-optimize.net>
8 * @copyright Copyright (c) 2025 Samuel Marshall / JCH Optimize
9 * @license GNU/GPLv3, or later. See LICENSE file
10 *
11 * If LICENSE file missing, see <http://www.gnu.org/licenses/>.
12 */
13
14 namespace JchOptimize\Core\Debug;
15
16 use function str_contains;
17 use function str_starts_with;
18
19 final class JchProfiler implements JchProfilerInterface
20 {
21 /** @var array<string, array{time: float, count: int, max: float}> */
22 private array $stats = [];
23
24 /** @var array<int, array{name: string, start: float}> */
25 private array $rootStack = [];
26
27 /** @var array<int, array{name: string, start: float}> */
28 private array $childStack = [];
29
30 /* -------------------------
31 * Top-level entry
32 * ------------------------- */
33 public function profile(string $name, callable $fn): mixed
34 {
35 if (!$this->shouldProfile($name)) {
36 return $fn();
37 }
38
39 $this->rootStack[] = [
40 'name' => $name,
41 'start' => microtime(true),
42 ];
43
44 try {
45 return $fn();
46 } finally {
47 $this->stopRoot();
48 }
49 }
50
51 private function stopRoot(): void
52 {
53 $root = array_pop($this->rootStack);
54
55 if ($root === null) {
56 return;
57 }
58
59 $duration = microtime(true) - $root['start'];
60
61 $this->addSample($root['name'], $duration);
62 }
63
64 /* -------------------------
65 * Child profiling
66 * ------------------------- */
67 public function start(string $name): void
68 {
69 // ❗ Ignore if no active root
70 if (empty($this->rootStack)) {
71 return;
72 }
73
74 $this->childStack[] = [
75 'name' => $name,
76 'start' => microtime(true),
77 ];
78 }
79
80 public function stop(): void
81 {
82 if (empty($this->rootStack)) {
83 return;
84 }
85
86 $child = array_pop($this->childStack);
87
88 if ($child === null) {
89 return;
90 }
91
92 $duration = microtime(true) - $child['start'];
93
94 $rootName = $this->rootStack[array_key_last($this->rootStack)]['name'];
95
96 $key = $rootName . ' > ' . $child['name'];
97
98 $this->addSample($key, $duration);
99 }
100
101 public function measure(string $name, callable $fn): mixed
102 {
103 if (empty($this->rootStack)) {
104 return $fn();
105 }
106
107 $this->start($name);
108
109 try {
110 return $fn();
111 } finally {
112 $this->stop();
113 }
114 }
115
116 /* -------------------------
117 * Stats
118 * ------------------------- */
119 private function addSample(string $key, float $duration): void
120 {
121 if (!isset($this->stats[$key])) {
122 $this->stats[$key] = ['time' => 0.0, 'count' => 0, 'max' => 0.0];
123 }
124
125 $this->stats[$key]['time'] += $duration;
126 $this->stats[$key]['count']++;
127
128 if ($duration > $this->stats[$key]['max']) {
129 $this->stats[$key]['max'] = $duration;
130 }
131 }
132
133 public function snapshot(): array
134 {
135 return $this->stats;
136 }
137
138 public function filterByParent(string $parent): array
139 {
140 $results = [];
141
142 foreach ($this->stats as $key => $stat) {
143 if (
144 $key === $parent ||
145 str_starts_with($key, $parent . ' > ')
146 ) {
147 $results[$key] = $stat;
148 }
149 }
150
151 return $results;
152 }
153
154 public function filterMany(array $parents): array
155 {
156 $results = [];
157
158 foreach ($parents as $parent) {
159 $results[$parent] = $this->filterByParent($parent);
160 }
161
162 return $results;
163 }
164
165 private function shouldProfile(string $name): bool
166 {
167 return match (true) {
168 str_contains($name, 'bootstrap5') => true,
169 str_starts_with($name, 'getCssContents') => true,
170 default => false,
171 };
172 }
173 }
174