src/Debug.php source

1 <?php
2
3 /**
4 * @codeCoverageIgnore
5 */
6
7 namespace phasync;
8
9 use Closure;
10 use Fiber;
11 use ReflectionFunction;
12
13 final class Debug
14 {
15 /**
16 * Format a binary string to be printable, for debugging binary protocols
17 * and other data. The characters that can be printed will be kept, while
18 * binary data will be enclosed in ⟪01FA6B⟫, and when the length
19 *
20 * @param string $binary the binary data to print
21 * @param bool $withLength Should byte sequences longer than 8 bytes ble chunked?
22 */
23 public static function binaryString(string $binary, bool $withLength=false): string
24 {
25 $re = '/([\x00-\x1F\x80-\xFF]+([^\x00-\x1F\x80-\xFF]{1,2}[\x00-\x1F\x80-\xFF]+)*)/';
26
27 return \preg_replace_callback($re, subject: $binary, callback: function ($matches) use ($withLength) {
28 $string = $matches[0];
29 $chunked = \implode('∙', \str_split(\bin2hex($string), 8));
30 $length = \strlen($string);
31 if ($withLength && $length > 8) {
32 return '⟪' . $length . '|' . $chunked . '⟫';
33 }
34
35 return '⟪' . $chunked . '⟫';
36 });
37 }
38
39 /**
40 * Return a debug string for various objects.
41 */
42 public static function getDebugInfo($subject): string
43 {
44 $cwd = \getcwd() . \DIRECTORY_SEPARATOR;
45 if ($subject instanceof \Fiber) {
46 // Gather information about the Fiber
47 $rf = new \ReflectionFiber($subject);
48
49 $status = match (true) {
50 $subject->isTerminated() => 'terminated',
51 $subject->isSuspended() => 'suspended at ' . \str_replace($cwd, '', $rf->getExecutingFile()) . '(' . $rf->getExecutingLine() . ')',
52 $subject->isRunning() => 'running',
53 default => 'unknown',
54 };
55
56 return \sprintf('Fiber%d(%s, %s)', \spl_object_id($subject), $status, $subject->isTerminated() ? 'NULL' : self::getDebugInfo($rf->getCallable()));
57 } elseif ($subject instanceof \Closure) {
58 // Use ReflectionFunction to get information about the Closure
59 $ref = new \ReflectionFunction($subject);
60 $startLine = $ref->getStartLine();
61 $endLine = $ref->getEndLine();
62 $filename = \str_replace($cwd, '', $ref->getFileName());
63
64 return \sprintf(
65 'Closure%d(%s(%d))',
66 \spl_object_id($subject),
67 $filename ?: 'unknown file',
68 $startLine
69 );
70 } elseif ($subject instanceof \WeakMap) {
71 $result = 'WeakMap' . \spl_object_id($subject);
72 $kvs = ['count=' . \count($subject)];
73 foreach ($subject as $k => $v) {
74 $kvs[] = Debug::getDebugInfo($k) . '=>' . Debug::getDebugInfo($v);
75 if (\count($kvs) > 2) {
76 break;
77 }
78 }
79
80 return $result . '(' . \implode(' ', $kvs) . ')';
81 } elseif (\is_object($subject)) {
82 $result = \get_class($subject) . \spl_object_id($subject);
83 $kvs = [];
84 if ($subject instanceof \Countable) {
85 $kvs[] = 'count=' . \count($subject);
86 }
87 if (\count($kvs) > 0) {
88 $result .= '(' . \implode(' ', $kvs) . ')';
89 }
90
91 return $result;
92 } elseif (\is_array($subject)) {
93 return 'array(length=' . \count($subject) . ')';
94 } elseif (\is_scalar($subject)) {
95 return \get_debug_type($subject) . '(' . $subject . ')';
96 } elseif (null === $subject) {
97 return 'NULL';
98 }
99
100 return 'Unsupported subject type (' . \get_debug_type($subject) . ').';
101 }
102
103 /**
104 * Scans the entire application for possible leaks; returning static
105 * variables and globals that take a lot of space.
106 */
107 public static function findLeaks(string|array $namespace=''): array
108 {
109 $candidates = [];
110 $classes = \get_declared_classes();
111 $queue = [];
112 foreach ($classes as $className) {
113 if (\is_string($namespace) && !\str_starts_with($className, $namespace)) {
114 continue;
115 } elseif (\is_array($namespace)) {
116 $found = false;
117 foreach ($namespace as $ns) {
118 if (\str_starts_with($className, $ns)) {
119 $found = true;
120 break;
121 }
122 }
123 if (!$found) {
124 continue;
125 }
126 }
127 $rc = new \ReflectionClass($className);
128 $properties = $rc->getStaticProperties();
129 foreach ($properties as $name => $value) {
130 $candidates[$className . '::$' . $name] = $value;
131 if (\is_object($value)) {
132 $queue[] = [$className . '::$' . $name, $value];
133 }
134 }
135
136 foreach ($queue as [$prefix, $instance]) {
137 $rc = new \ReflectionClass($instance);
138 $properties = $rc->getProperties(~\ReflectionProperty::IS_STATIC);
139 foreach ($properties as $rp) {
140 $rp->setAccessible(true);
141 $name = $prefix . '->' . $rp->getName();
142 $candidates[$name] = $rp->getValue($instance);
143 }
144 }
145 }
146
147 return $candidates;
148 }
149 }
150