package.xml0000664000175000017500000002055015012753166014005 0ustar tstarlingtstarling excimer pecl.php.net Interrupting timer and low-overhead sampling profiler Calls a user-defined callback when a one-shot or periodic timer expires. Also a sampling profiler backend, which aggregates backtraces collected by a periodic timer. Tim Starling tstarling tstarling@wikimedia.org yes Kunal Mehta legoktm legoktm@debian.org yes Timo Tijhof krinkle krinkle@fastmail.com yes 2025-05-20 1.2.5 1.2.5 stable stable Apache 2.0 - Fix build error with libtool 1.5 7.1.0 1.8.0 unix excimer 2025-05-19 1.2.4 1.2.4 - Rewrite the backend to work around a glibc timer aliasing bug (T391426). Timer creation and deletion are now more expensive, so applications should try to avoid unnecessary creation and deletion. It's cheaper to stop and start an existing timer. Handling events is cheaper, so profiling performance is improved. - Compile with -fvisibility=hidden 2024-11-15 1.2.3 1.2.3 - Fix start time stagger, broken by previous release 2024-07-31 1.2.2 1.2.2 - Fix PHP 8.4 compatibility (patch by Remi Collet) 2024-02-29 1.2.1 1.2.1 - Fix compiler warning in excimer_log - Fix invalid OS requirement in package.xml, allow all "unix" 2024-02-28 1.2.0 1.2.0 - Add support for BSD and macOS (only real/wall-clock, no CPU timer). - Add excimer.default_max_depth and default to 1000 (previously unlimited). 2023-03-13 1.1.1 1.1.1 - Restore support for PHP 7.1-7.3 2023-03-01 1.1.0 1.1.0 - Fix leading semi-colon in ExcimerLog::formatCollapsed output - Change ExcimerLog::formatCollapsed to mark truncated frames - Add support for PHP 8.2 - Add ExcimerLog::getSpeedscopeData for Speedscope support 2022-05-07 1.0.4 1.0.4 - Fix arginfo error for PHP 7.1 2022-05-04 1.0.3 1.0.3 - Set return type on ExcimerLog::aggregateByFunction - Set return type on ExcimerProfiler::getLog 2021-10-16 1.0.2 1.0.2 - Fix Iterator prototypes for PHP 8.1 - Add extension version in phpinfo() 2021-09-29 1.0.1 1.0.1 - Filter null bytes out of the collapsed output - Fix segfault in ZTS mode - Fix [-Wincompatible-pointer-types] with PHP 8 2021-02-26 1.0.0 1.0.0 - Initial PECL release excimer-1.2.5/stubs/ExcimerLog.php0000664000175000017500000001031515012753166017672 0ustar tstarlingtstarlinggetLog() as $entry ) { * var_dump( $entry->getTrace() ); * } */ class ExcimerLog implements ArrayAccess, Iterator { /** * ExcimerLog is not constructible by user code. Objects of this type * are available via: * - ExcimerProfiler::getLog() * - ExcimerProfiler::flush() * - The callback to ExcimerProfiler::setFlushCallback() */ private final function __construct() { } /** * Aggregate the stack traces and convert them to a line-based format * understood by Brendan Gregg's FlameGraph utility. Each stack trace is * represented as a series of function names, separated by semicolons. * After this identifier, there is a single space character, then a number * giving the number of times the stack appeared. Then there is a line * break. This is repeated for each unique stack trace. * * @return string */ function formatCollapsed() { } /** * Produce an array with an element for every function which appears in * the log. The key is a human-readable unique identifier for the function, * method or closure. The value is an associative array with the following * elements: * * - self: The number of events in which the function itself was running, * no other userspace function was being called. This includes time * spent in internal functions that this function called. * - inclusive: The number of events in which this function appeared * somewhere in the stack. * * And optionally the following elements, if they are relevant: * * - file: The filename in which the function appears * - line: The exact line number at which the first relevant event * occurred. * - class: The class name in which the method is defined * - function: The name of the function or method * - closure_line: The line number at which the closure was defined * * The event counts in the "self" and "inclusive" fields are adjusted for * overruns. They represent an estimate of the number of profiling periods * in which those functions were present. * * @return array */ function aggregateByFunction() { } /** * Get an array which can be JSON encoded for import into speedscope * * @return array */ function getSpeedscopeData() { } /** * Get the total number of profiling periods represented by this log. * * @return int */ function getEventCount() { } /** * Get the current ExcimerLogEntry object. Part of the Iterator interface. * * @return ExcimerLogEntry|null */ function current() { } /** * Get the current integer key or null. Part of the Iterator interface. * * @return int|null */ function key() { } /** * Advance to the next log entry. Part of the Iterator interface. */ function next() { } /** * Rewind back to the first log entry. Part of the Iterator interface. */ function rewind() { } /** * Check if the current position is valid. Part of the Iterator interface. * * @return bool */ function valid() { } /** * Get the number of log entries contained in this log. This is always less * than or equal to the number returned by getEventCount(), which includes * overruns. * * @return int */ function count() { } /** * Determine whether a log entry exists at the specified array offset. * Part of the ArrayAccess interface. * * @param int $offset * @return bool */ function offsetExists( $offset ) { } /** * Get the ExcimerLogEntry object at the specified array offset. * * @param int $offset * @return ExcimerLogEntry|null */ function offsetGet( $offset ) { } /** * This function is included for compliance with the ArrayAccess interface. * It raises a warning and does nothing. * * @param int $offset * @param mixed $value */ function offsetSet( $offset, $value ) { } /** * This function is included for compliance with the ArrayAccess interface. * It raises a warning and does nothing. * * @param int $offset */ function offsetUnset( $offset ) { } } excimer-1.2.5/stubs/ExcimerLogEntry.php0000664000175000017500000000237215012753166020720 0ustar tstarlingtstarlingsetCallback( $callback ); * $timer->setInterval( $interval ); * $timer->start(); * return $timer; * * Note that you must keep a copy of the return value. If it goes out of scope, * the object will be destroyed and the timer will stop. * * If the callback is not callable, a warning is raised and null is returned. * * @param callable $callback * @param float $interval * @return ExcimerTimer|null */ function excimer_set_timeout( $callback, $interval ) { } excimer-1.2.5/tests/aliasing.phpt0000664000175000017500000000107615012753166017615 0ustar tstarlingtstarling--TEST-- glibc timer aliasing (T389734) --SKIPIF-- --FILE-- setPeriod( 0.001 ); $timer1->setCallback( $noop ); $timer1->start(); usleep( 1000 ); $timer1 = null; $timer2 = new \ExcimerTimer; $timer2->setInterval( 60 ); $timer2->setCallback( $throw ); $timer2->start(); usleep( 1000 ); $timer2 = null; } echo "OK\n"; --EXPECT-- OK excimer-1.2.5/tests/concurrentTimers.phpt0000664000175000017500000000156415012753166021376 0ustar tstarlingtstarling--TEST-- Concurrent timers --SKIPIF-- --FILE-- setInterval($i * $interval); $timer->setCallback(function () use (&$fired, $i) { $fired[$i] = microtime(true); } ); $timers[] = $timer; } $start = microtime(true); foreach ($timers as $timer) { $timer->start(); } while (count($fired) < 10 && (microtime(true) - $start) < 10) { usleep(100000); } for ($i = 1; $i <= 10; $i++) { $min = ($i - 1) * $interval; $max = ($i + 2) * $interval; $t = $fired[$i] - $start; if ($t >= $min && $t <= $max) { print "$i: OK\n"; } else { print "$i: FAILED: $min <= $t <= $max\n"; } } --EXPECT-- 1: OK 2: OK 3: OK 4: OK 5: OK 6: OK 7: OK 8: OK 9: OK 10: OK excimer-1.2.5/tests/cpu.phpt0000664000175000017500000001437015012753166016616 0ustar tstarlingtstarling--TEST-- ExcimerProfiler CPU profile --SKIPIF-- --INI-- zend.assertions=1 --FILE-- getStartLine(); $c2line = (new ReflectionFunction($closure1))->getStartLine(); class C { function member() { return md5(str_repeat('x', 100000)); } } $obj = new C; function foo() { bar(); } function bar() { global $closure1, $closure2, $obj; $closure1(); $closure2(); $obj->member(); baz(); } function baz() { return md5(str_repeat('x', 100000)); } $profiler = new ExcimerProfiler; $profiler->setEventType(EXCIMER_CPU); $profiler->setPeriod(0.1); $profiler->start(); $j = 0; while (count($profiler->getLog()) < 30) { for ($i = 0; $i < 100; $i++) { foo(); } if (++$j > 100000) { break; } } $profiler->stop(); $log = $profiler->flush(); function sort_lines($text) { $lines = explode("\n", trim($text)); sort($lines); return implode("\n", $lines); } // It takes 3 seconds to populate the profiler log, which is quite expensive, // so we do as many tests as possible on the result while we have it. // Test formatCollapsed $collapsed = $log->formatCollapsed(); // Normalize variable file path, and sample count $collapsed = str_replace(__FILE__, '/data/cpu.php', $collapsed); $collapsed = preg_replace('/ \d+$/m', ' 00', $collapsed); $collapsed = sort_lines($collapsed); echo "formatCollapsed:\n$collapsed\n\n"; // Test getSpeedscopeData function check_speedscope($speedscope, $name, $expected) { $ok = false; foreach ($speedscope['profiles'][0]['samples'] as $sample) { if (count($sample) !== count($expected)) { break; } $matches = 0; foreach ($expected as $i => $expectedFrame) { if (!isset($sample[$i])) { break; } $frame = $speedscope['shared']['frames'][$sample[$i]]; if ($frame === $expectedFrame) { $matches++; } else { break; } } if ($matches === count($expected)) { $ok = true; break; } } echo "getSpeedscopeData $name: " . ($ok ? "OK\n" : "FAILED\n"); if (!$ok) { echo "Expected: "; var_dump($expected); echo "Actual: "; var_dump($speedscope); } } $speedscope = $log->getSpeedscopeData(); check_speedscope( $speedscope, 'baz', [ ['name' => __FILE__, 'file' => __FILE__], ['name' => 'foo', 'file' => __FILE__], ['name' => 'bar', 'file' => __FILE__], ['name' => 'baz', 'file' => __FILE__] ] ); // Test foreach (get_iterator handler) $found = []; $count = 0; $eventCount = 0; $traces = []; $firstTimestamp = false; $lastTimestamp = false; foreach ($log as $entry) { $trace = $entry->getTrace(); $traces[] = $trace; if (count($trace) == 4 && ($trace[0]['function'] ?? '') === 'baz' && ($trace[1]['function'] ?? '') === 'bar' && ($trace[2]['function'] ?? '') === 'foo' && ($trace[3]['function'] ?? '') === '') { $found['baz'] = true; } if (count($trace) == 4 && ($trace[0]['closure_line'] ?? '') === $c1line && ($trace[1]['function'] ?? '') === 'bar' && ($trace[2]['function'] ?? '') === 'foo' && ($trace[3]['function'] ?? '') === '') { $found['closure1'] = true; } if (count($trace) == 4 && ($trace[0]['closure_line'] ?? '') === $c2line && ($trace[1]['function'] ?? '') === 'bar' && ($trace[2]['function'] ?? '') === 'foo' && ($trace[3]['function'] ?? '') === '') { $found['closure2'] = true; } if (count($trace) == 4 && ($trace[0]['class'] ?? '') === 'C' && ($trace[0]['function'] ?? '') === 'member' && ($trace[1]['function'] ?? '') === 'bar' && ($trace[2]['function'] ?? '') === 'foo' && ($trace[3]['function'] ?? '') === '') { $found['member'] = true; } if ($firstTimestamp === false) { $firstTimestamp = $entry->getTimestamp(); } if ($lastTimestamp !== false) { assert(is_float($entry->getTimestamp())); assert($entry->getTimestamp() >= 0); assert($entry->getTimestamp() > $lastTimestamp); } $lastTimestamp = $entry->getTimestamp(); assert( $entry->getEventCount() > 0 ); $eventCount += $entry->getEventCount(); $count++; } assert( $log->getEventCount() === $eventCount ); function check_found($found, $func) { if (isset($found[$func])) { echo "foreach found $func: OK\n"; } else { echo "foreach found $func: FAILED\n"; } } check_found($found, 'baz'); check_found($found, 'closure1'); check_found($found, 'closure2'); check_found($found, 'member'); $timestampDiff = $lastTimestamp - $firstTimestamp; if ($timestampDiff > 1 && $timestampDiff < 1000) { echo "getTimestamp: OK\n"; } else { echo "getTimestamp: FAILED $timestampDiff\n"; } echo 'ExcimerLog::count ' . ($count == $log->count() ? "OK\n" : "FAILED\n"); echo 'count(ExcimerLog) ' . ($count == count($log) ? "OK\n" : "FAILED\n"); // Test Iterator interface $count = 0; for ($log->rewind(); $log->valid(); $log->next(), $count++) { $entry = $log->current(); assert(serialize($entry->getTrace()) == serialize($traces[$count])); } assert($count == $log->count()); // Test aggregateByFunction // Typically the parent functions foo() and bar() will have self=0 and // inclusive ~= 30. The other 4 functions will have a count of about 30/4 = 7.5. // The probability of C::member() or baz() having a count of zero is about 1 in 5600. $stats = $log->aggregateByFunction(); assert($stats['foo']['inclusive'] > 10); assert($stats['foo']['self'] < 10); assert($stats['bar']['inclusive'] > 10); assert($stats['bar']['self'] < 10); assert($stats['C::member']['inclusive'] > 0); assert($stats['C::member']['self'] > 0); assert($stats['baz']['inclusive'] > 0); assert($stats['baz']['self'] > 0); // Ensure $stats is sorted $counts = array_column( $stats, 'inclusive' ); $sortedCounts = $counts; rsort( $sortedCounts ); assert( $sortedCounts === $counts ); --EXPECT-- formatCollapsed: /data/cpu.php;foo;bar;C::member 00 /data/cpu.php;foo;bar;baz 00 /data/cpu.php;foo;bar;{closure:/data/cpu.php(5)} 00 /data/cpu.php;foo;bar;{closure:/data/cpu.php(8)} 00 getSpeedscopeData baz: OK foreach found baz: OK foreach found closure1: OK foreach found closure2: OK foreach found member: OK getTimestamp: OK ExcimerLog::count OK count(ExcimerLog) OK excimer-1.2.5/tests/delayedPeriodic.phpt0000664000175000017500000000237015012753166021112 0ustar tstarlingtstarling--TEST-- ExcimerTimer periodic mode with initial delay --SKIPIF-- --FILE-- setInterval(0.25); $timer->setPeriod(0.5); $expCount = 0; $events = []; $timer->setCallback(function($n) use (&$expCount, &$events) { $expCount += $n; $events[] = microtime(true); }); $start = microtime(true); $timer->start(); $elapsed = 0; $interval = 50000; while ($elapsed < 1400000) { usleep($interval); $elapsed += $interval; } $firstDelay = $events[0] - $start; $secondDelay = $events[1] - $events[0]; $count = count($events); if ($count === 3) { echo "periodic event count: OK\n"; } else { echo "periodic event count: FAIL - got $count\n"; } if ($expCount === 3) { echo "periodic expiration count: OK\n"; } else { echo "periodic expiration count: FAIL - got $expCount\n"; } if ($firstDelay >= 0.15 && $firstDelay <= 0.35) { echo "first delay: OK\n"; } else { echo "first delay: FAIL - got $firstDelay\n"; } if ($secondDelay >= 0.4 && $secondDelay <= 0.6) { echo "second delay: OK\n"; } else { echo "second delay: FAIL - got $secondDelay\n"; } --EXPECT-- periodic event count: OK periodic expiration count: OK first delay: OK second delay: OK excimer-1.2.5/tests/getTime.phpt0000664000175000017500000000156215012753166017424 0ustar tstarlingtstarling--TEST-- ExcimerProfiler::getTime --SKIPIF-- --FILE-- setEventType(EXCIMER_REAL); $timer->setPeriod(5); $time = $timer->getTime(); if ($time == 0.0) { echo "remaining time is zero when not started: OK\n"; } else { echo "unexpected remaining time before starting: $time\n"; } $timer->start(); sleep(1); $time = $timer->getTime(); if ($time <= 4.0 && $time >= 3.0) { echo "remaining time: OK\n"; } else { echo "unexpected remaining time: $time\n"; } $timer->stop(); $time = $timer->getTime(); if ($time == 0.0) { echo "remaining time is zero after stopping: OK\n"; } else { echo "unexpected remaining time after stopping: $time\n"; } --EXPECTF-- remaining time is zero when not started: OK remaining time: OK remaining time is zero after stopping: OK excimer-1.2.5/tests/maxDepth.phpt0000664000175000017500000000111415012753166017571 0ustar tstarlingtstarling--TEST-- ExcimerProfiler max depth --SKIPIF-- --FILE-- setEventType(EXCIMER_REAL); $profiler->setPeriod(0.1); $profiler->setMaxDepth(5); function foo( $depth ) { global $profiler; if ( $depth > 0 ) { foo( $depth - 1 ); } else { $profiler->start(); while (!count($profiler->getLog())) { usleep(10000); } $profiler->stop(); } } foo( 20 ); $log = $profiler->flush(); echo $log->formatCollapsed() . "\n"; --EXPECTF-- excimer_truncated;foo;foo;foo;foo;foo;foo %d excimer-1.2.5/tests/oneshot.phpt0000664000175000017500000000135215012753166017502 0ustar tstarlingtstarling--TEST-- ExcimerTimer one-shot mode --SKIPIF-- --FILE-- setInterval(0.5); $count = 0; $timer->setCallback(function() use (&$count) { $count++; }); $timer->start(); $start = microtime(true); $elapsed = 0; $fired_at = 0; $interval = 20000; while ($elapsed < 600000) { $t = microtime(true); $elapsed = ($t - $start) * 1e6; if ($count === 1 && $fired_at === 0) { $fired_at = $elapsed; } usleep($interval); } if ($count === 1 && $fired_at >= 400000 && $fired_at <= 600000) { echo "oneshot: OK\n"; } else { echo "oneshot: FAIL - count: $count, fired_at: $fired_at, elapsed: $elapsed\n"; } --EXPECT-- oneshot: OK excimer-1.2.5/tests/periodic.phpt0000664000175000017500000000125215012753166017620 0ustar tstarlingtstarling--TEST-- ExcimerTimer periodic mode --SKIPIF-- --FILE-- setPeriod(0.1); $callCount = 0; $eventCount = 0; $timer->setCallback(function($n) use (&$callCount, &$eventCount) { $callCount ++; $eventCount += $n; }); $t = microtime(true); $timer->start(); while (microtime(true) - $t < 1) { usleep(10000); } if ($callCount > 5 && $callCount < 15) { echo "call count: OK\n"; } else { echo "call count: FAILED\n"; } if ($eventCount > 5 && $eventCount < 15) { echo "event count: OK\n"; } else { echo "event count: FAILED - got $eventCount\n"; } --EXPECT-- call count: OK event count: OK excimer-1.2.5/tests/real.phpt0000664000175000017500000000125015012753166016743 0ustar tstarlingtstarling--TEST-- ExcimerProfiler real time profile --SKIPIF-- --FILE-- setEventType(EXCIMER_REAL); $profiler->setPeriod(0.1); for ($j = 0; $j < 3; $j++) { $profiler->start(); for ($i = 0; $i < 10; $i++) { foo(); } $profiler->stop(); } $log = $profiler->flush(); $found = 0; foreach ($log as $entry) { $trace = $entry->getTrace(); if (isset($trace[0]['function']) && $trace[0]['function'] === 'foo' && !isset($trace[1]['function'])) { $found++; } } if ($found > 11) { echo "OK\n"; } else { echo "FAILED\n"; } --EXPECT-- OK excimer-1.2.5/tests/stagger.phpt0000664000175000017500000000102015012753166017447 0ustar tstarlingtstarling--TEST-- ExcimerProfiler start time stagger --SKIPIF-- --FILE-- setEventType(EXCIMER_REAL); $profiler->setPeriod(0.999); $profiler->start(); f1(); f2(); $log = $profiler->flush(); if (isset($log[0]) && $log[0]->getTrace()[0]['function'] === 'f2') { echo "OK\n"; return; } } echo "FAILED\n"; --EXPECT-- OK excimer-1.2.5/tests/subprocess.phpt0000664000175000017500000000067215012753166020217 0ustar tstarlingtstarling--TEST-- Long-running subprocess does not interfere with ExcimerTimer --SKIPIF-- --FILE-- setEventType(EXCIMER_REAL); $timer->setInterval(5); $timer->start(); shell_exec('sleep 10 >/dev/null 2>&1 &'); $before = microtime(true); $timer->stop(); $after = microtime(true); if ( ( $after - $before ) < 10 ) { echo 'OK'; } --EXPECTF-- OK excimer-1.2.5/tests/timeout.phpt0000664000175000017500000000045115012753166017510 0ustar tstarlingtstarling--TEST-- excimer_set_timeout --SKIPIF-- --FILE-- --FILE-- setInterval(1); $timer->setCallback(function() { throw new Exception('timeout'); }); $timer->start(); try { while (true) { usleep(100000); } } catch (Exception $e) { echo "OK\n"; } --EXPECT-- OK excimer-1.2.5/timerlib/README.md0000664000175000017500000000121515012753166017050 0ustar tstarlingtstarlingThis is a prototype for a platform-independent timer library to be shared between Excimer and LuaSandbox. The library doesn't have its own build system or packaging. `timerlib_config.h` belongs to the application. It allows the application to configure the library. Linux is the fully tested production platform. The kqueue implementation should support BSDs and Mac OS. We try to be generic enough to allow for future Windows support. In most cases, errors are handled by calling an application-defined function and returning `TIMERLIB_FAILURE`. The return value should be ignorable if the app's error function handled the error sufficiently well. excimer-1.2.5/timerlib/timerlib.h0000664000175000017500000001425315012753166017557 0ustar tstarlingtstarling/* Copyright 2025 Wikimedia Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #ifndef TIMERLIB_TIMERLIB_H #define TIMERLIB_TIMERLIB_H #include "timerlib_config.h" /** * The function pointer type used for notifying the caller of timer events */ typedef void (timerlib_notify_function_t)(void *data, int overrun_count); #if defined(TIMERLIB_USE_POSIX) #include "timerlib_posix.h" #elif defined(TIMERLIB_USE_KQUEUE) #include "timerlib_kqueue.h" #else #error "No timer implementation available" #endif #define timerlib_abort(libfunc, error_number) timerlib_abort_func(__func__, (libfunc), (error_number)) //-------------------------------------------------------------------------------- // Timer functions //-------------------------------------------------------------------------------- /** * Initialize a new timer. * * Regardless of the return value, use timerlib_timer_destroy() to destroy * the structure. * * @param[out] timer Pointer to the timer object to be populated * @param clock May be TIMERLIB_REAL for wall-clock time, or TIMERLIB_CPU for CPU time * @param notify_function Function to be called when the timer expires * @param notify_data The first parameter sent to notify_function * @return TIMERLIB_SUCCESS if the timer was successfully initialized, TIMERLIB_FAILURE otherwise */ int timerlib_timer_init(timerlib_timer_t *timer, int clock, timerlib_notify_function_t *notify_function, void *notify_data); /** * Start a one-shot timer * * @param[in,out] timer * @param[in] duration How long before the timer expires * @return TIMERLIB_SUCCESS if the timer was successfully started, TIMERLIB_FAILURE otherwise */ int timerlib_timer_start_oneshot(timerlib_timer_t *timer, timerlib_timespec_t *duration); /** * Start a periodic timer * * @param[in,out] timer * @param[in] period The interval at which the timer should fire * @return TIMERLIB_SUCCESS if the timer was successfully started, TIMERLIB_FAILURE otherwise */ int timerlib_timer_start_periodic(timerlib_timer_t *timer, timerlib_timespec_t *period); /** * Start a generic timer * * @param[in,out] timer * @param[in] period The period at which the timer should fire, or zero for a one-shot timer * @param[in] initial The initial delay of the timer * @return TIMERLIB_SUCCESS if the timer was successfully started, TIMERLIB_FAILURE otherwise */ int timerlib_timer_start(timerlib_timer_t *timer, timerlib_timespec_t *period, timerlib_timespec_t *initial); /** * Stop a timer. * * @param[in,out] timer * @return TIMERLIB_SUCCESS if the timer was successfully stopped, TIMERLIB_FAILURE otherwise */ int timerlib_timer_stop(timerlib_timer_t *timer); /** * Clean up resources associated with a timer. * * If a timer callback is executing, wait until it returns. * * It is guaranteed that the callback will not be called again after this * function returns. * * @param[in,out] timer */ void timerlib_timer_destroy(timerlib_timer_t *timer); /** * Get the remaining time until the next scheduled expiratioh of a timer. * This is an estimate based on the last reported firing time of the timer and the configured period. * * @param[in] timer * @param[out] remaining Pointer to the timespec struct to be populated with the remaining time * @return TIMERLIB_SUCCESS or TIMERLIB_FAILURE */ int timerlib_timer_get_time(timerlib_timer_t *timer, timerlib_timespec_t *remaining); //-------------------------------------------------------------------------------- // Clock functions //-------------------------------------------------------------------------------- /** * Get the current time relative to some implementation-dependent epoch * @param clock Either TIMERLIB_REAL or TIMERLIB_CPU * @param[out] time Pointer to the timespec to be populated with the current time * @return TIMERLIB_SUCCESS or TIMERLIB_FAILURE */ int timerlib_clock_get_time(int clock, timerlib_timespec_t * time); //-------------------------------------------------------------------------------- // Timespec functions //-------------------------------------------------------------------------------- /** * A long billion */ #define TIMERLIB_BILLION_L 1000000000L /** * A long long billion */ #define TIMERLIB_BILLION_LL 1000000000LL /** * Determine if a timespec is zero * @param[in] ts * @return 1 if the seconds and nanoseconds parts are both zero, 0 otherwise */ static inline int timerlib_timespec_is_zero(timerlib_timespec_t *ts) { return ts->tv_sec == 0 && ts->tv_nsec == 0; } /** * Convert a timespec to a number of nanoseconds * * Overflow will silently wrap around after 585 years. * * @param[in] ts * @return The number of nanoseconds */ static inline uint64_t timerlib_timespec_to_ns(timerlib_timespec_t *ts) { return (uint64_t)ts->tv_nsec + (uint64_t)ts->tv_sec * TIMERLIB_BILLION_LL; } /** * Convert a timespec to a floating-point number of seconds * * Some precision will be lost if the timespec is larger than about 104 days. * * @param[in] ts * @return The number of seconds */ static inline double timerlib_timespec_to_double(timerlib_timespec_t *ts) { return timerlib_timespec_to_ns(ts) * 1e-9; } /** * Add two timespecs like a += b * * @param[in,out] a The destination and left operand * @param[in] b The right operand */ void timerlib_timespec_add(timerlib_timespec_t * a, const timerlib_timespec_t * b); /** * Subtract timespecs like a -= b * * @param[in,out] a The destination and left operand * @param[in] b The right operand */ void timerlib_timespec_subtract(timerlib_timespec_t * a, const timerlib_timespec_t * b); /** * Populate a timespec from a floating-point number of seconds * * @param[out] dest * @param[in] source */ void timerlib_timespec_from_double(timerlib_timespec_t * dest, double source); #endif excimer-1.2.5/timerlib/timerlib_common.c0000664000175000017500000000236015012753166021116 0ustar tstarlingtstarling#include "timerlib.h" #include void timerlib_timespec_add(timerlib_timespec_t * a, const timerlib_timespec_t * b) { a->tv_sec += b->tv_sec; a->tv_nsec += b->tv_nsec; if (a->tv_nsec > TIMERLIB_BILLION_L) { a->tv_nsec -= TIMERLIB_BILLION_L; a->tv_sec++; } } void timerlib_timespec_subtract(timerlib_timespec_t * a, const timerlib_timespec_t * b) { a->tv_sec -= b->tv_sec; if (a->tv_nsec < b->tv_nsec) { a->tv_sec--; a->tv_nsec += TIMERLIB_BILLION_L - b->tv_nsec; } else { a->tv_nsec -= b->tv_nsec; } } void timerlib_timespec_from_double(timerlib_timespec_t * dest, double source) { double fractional, integral; if (source < 0) { dest->tv_sec = dest->tv_nsec = 0; return; } fractional = modf(source, &integral); dest->tv_sec = (time_t)integral; dest->tv_nsec = (long)(fractional * 1e9); if (dest->tv_nsec >= TIMERLIB_BILLION_L) { dest->tv_nsec -= TIMERLIB_BILLION_L; dest->tv_sec ++; } } int timerlib_timer_start_oneshot(timerlib_timer_t *timer, timerlib_timespec_t *duration) { timerlib_timespec_t period = {0}; return timerlib_timer_start(timer, &period, duration); } int timerlib_timer_start_periodic(timerlib_timer_t *timer, timerlib_timespec_t *period) { return timerlib_timer_start(timer, period, period); } excimer-1.2.5/timerlib/timerlib_kqueue.c0000664000175000017500000001477215012753166021137 0ustar tstarlingtstarling/* Copyright 2025 Wikimedia Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "timerlib.h" #include #include #include #include #include #include #include "timerlib_pthread_mutex.h" /** * Handle a single timer event from a kqueue-based timer. * @param timer The timer instance holding the kqueue-based timer. * @param event Pointer to a kevent struct to store the event in. * @return TIMERLIB_SUCCESS if event processing may continue, TIMERLIB_FAILURE otherwise. */ static int timerlib_handle_timer_event(timerlib_timer_t *timer, struct kevent* event) { int ret = kevent(timer->kq, NULL, 0, event, 1, NULL); if (ret == -1) { // EINTR merely implies that a signal was delivered before the timeout expired, so ignore it if it shows up. if (errno != EINTR) { // EBADF implies that the kqueue was closed, so we should exit the thread. if (errno != EBADF) { timerlib_abort("kevent", errno); } return TIMERLIB_FAILURE; } } else if (ret > 0) { // Help emulate POSIX timer_gettime by keeping track of each moment the timer fires. timerlib_mutex_lock(&timer->last_fired_at_mutex); timerlib_clock_get_time(0, &timer->last_fired_at); timerlib_mutex_unlock(&timer->last_fired_at_mutex); // Match the behavior of POSIX's timer_getoverrun, which only counts additional timer expirations. int overrun_count = (int)event->data - 1; timer->notify_function(timer->notify_data, overrun_count); } return TIMERLIB_SUCCESS; } /** * Configure a kqueue-based timer using the given flags and period. * @param timer Pointer to the timer this timer belongs to. * @param flags Flags to use when configuring the kqueue timer. * @param period Period to use for the timer. * @return TIMERLIB_SUCCESS if the timer was successfully setup, TIMERLIB_FAILURE otherwise. */ static int timerlib_setup_kqueue_timer(timerlib_timer_t *timer, int flags, timerlib_timespec_t* period) { struct kevent kev; EV_SET(&kev, 1, EVFILT_TIMER, flags, NOTE_NSECONDS, timerlib_timespec_to_ns(period), (void*)timer); int ret = kevent(timer->kq, &kev, 1, NULL, 0, NULL); if (ret == -1) { timerlib_report_errno("kevent", errno); return TIMERLIB_FAILURE; } return TIMERLIB_SUCCESS; } /** * Main loop for a kqueue-based timer handler thread. * @param arg Pointer to the timer this handler belongs to. */ static void* timerlib_kqueue_handle(void *arg) { struct kevent event; timerlib_timer_t *timer = (timerlib_timer_t*)arg; // kqueue supports either periodic or one-shot timers, but not periodic timers with a delayed initial expiration. // So, if the initial delay is non-zero, wait for the one-shot initial timer to expire, // then - if needed - reconfigure the underlying kqueue as a periodic timer with the proper period going forward. if (!timerlib_timespec_is_zero(&timer->initial)) { if (timerlib_handle_timer_event(timer, &event) == TIMERLIB_FAILURE) { return NULL; } if (timerlib_timespec_is_zero(&timer->period)) { return NULL; } int ret = timerlib_setup_kqueue_timer(timer, EV_ADD | EV_ENABLE, &timer->period); if (ret == TIMERLIB_FAILURE) { return NULL; } } while (timerlib_handle_timer_event(timer, &event) == TIMERLIB_SUCCESS) {} return NULL; } int timerlib_timer_init(timerlib_timer_t *timer, int clock, timerlib_notify_function_t *notify_function, void *notify_data) { *timer = (timerlib_timer_t){ .kq = -1, .notify_function = notify_function, .notify_data = notify_data, .last_fired_at_mutex = PTHREAD_MUTEX_INITIALIZER }; return TIMERLIB_SUCCESS; } int timerlib_timer_start(timerlib_timer_t* timer, timerlib_timespec_t *period, timerlib_timespec_t *initial) { timer->period = *period; timer->initial = *initial; int kq = kqueue(); if (kq == -1) { timerlib_report_errno("kqueue", errno); return TIMERLIB_FAILURE; } timer->kq = kq; timerlib_clock_get_time(0, &timer->last_fired_at); int flags = EV_ADD | EV_ENABLE; int ret; // Use a non-periodic timer if an initial expiration was provided if (!timerlib_timespec_is_zero(initial)) { flags |= EV_ONESHOT; ret = timerlib_setup_kqueue_timer(timer, flags, initial); } else { ret = timerlib_setup_kqueue_timer(timer, flags, period); } if (ret == TIMERLIB_FAILURE) { return TIMERLIB_FAILURE; } ret = pthread_create(&timer->handler_thread_id, NULL, timerlib_kqueue_handle, timer); if (ret != 0) { timerlib_report_errno("pthread_create", ret); return TIMERLIB_FAILURE; } return TIMERLIB_SUCCESS; } int timerlib_timer_stop(timerlib_timer_t* timer) { if (timer->kq != -1) { timer->period.tv_sec = 0; timer->period.tv_nsec = 0; if (close(timer->kq) == -1) { timerlib_report_errno("close", errno); return TIMERLIB_FAILURE; } // Wait for the signal handler thread to finish. int ret = pthread_join(timer->handler_thread_id, NULL); if (ret != 0) { timerlib_report_errno("pthread_join", ret); return TIMERLIB_FAILURE; } } return TIMERLIB_SUCCESS; } void timerlib_timer_destroy(timerlib_timer_t *timer) { int ret = pthread_mutex_destroy(&timer->last_fired_at_mutex); if (ret != 0) { timerlib_report_errno("pthread_mutex_destroy", ret); } } int timerlib_timer_get_time(timerlib_timer_t *timer, timerlib_timespec_t *remaining) { // Get the time at which the timer last fired timerlib_mutex_lock(&timer->last_fired_at_mutex); timerlib_timespec_t last_fired_at = timer->last_fired_at; timerlib_mutex_unlock(&timer->last_fired_at_mutex); // Add the period to get the next expiry time timerlib_timespec_t will_fire_at = timer->period; timerlib_timespec_add(&will_fire_at, &last_fired_at); // Subtract the current time to get the remaining time timerlib_timespec_t now; timerlib_clock_get_time(0, &now); *remaining = will_fire_at; timerlib_timespec_subtract(remaining, &now); return TIMERLIB_SUCCESS; } int timerlib_clock_get_time(int clock, timerlib_timespec_t* time) { if (clock_gettime(CLOCK_MONOTONIC, time) == -1) { timerlib_report_errno("clock_gettime", errno); return TIMERLIB_FAILURE; } return TIMERLIB_SUCCESS; } excimer-1.2.5/timerlib/timerlib_kqueue.h0000664000175000017500000000170115012753166021130 0ustar tstarlingtstarling#ifndef TIMERLIB_KQUEUE_H #define TIMERLIB_KQUEUE_H #include #include typedef struct timespec timerlib_timespec_t; /** Represents a timer backed by kqueue. */ typedef struct { /** File descriptor of the kqueue backing this timer */ int kq; /** The overrun count for this timer */ volatile int overrun_count; /** The period of this timer */ struct timespec period; /** The initial expiration time of this timer */ struct timespec initial; /** Pointer to a callback to be invoked when this timer fires. */ timerlib_notify_function_t *notify_function; /** Data to be passed to the callback */ void *notify_data; /** Thread ID of the kqueue signal handler thread for this timer. */ pthread_t handler_thread_id; /** The time at which this timer last fired. */ struct timespec last_fired_at; /** Mutex to protect last_fired_at from concurrent access */ pthread_mutex_t last_fired_at_mutex; } timerlib_timer_t; #endif excimer-1.2.5/timerlib/timerlib_posix.c0000664000175000017500000002016615012753166020774 0ustar tstarlingtstarling/* Copyright 2025 Wikimedia Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // For gettid, pthread_attr_setsigmask_np #ifndef _GNU_SOURCE #define _GNU_SOURCE 1 #endif #include "timerlib.h" #include #include // https://sourceware.org/bugzilla/show_bug.cgi?id=27417 # ifndef sigev_notify_thread_id # define sigev_notify_thread_id _sigev_un._tid # endif #ifdef HAVE_PTHREAD_ATTR_SETSIGMASK_NP // glibc 2.32+: set the new thread's sigmask using an attribute static inline void timerlib_block_signals(pthread_attr_t *attr, sigset_t *old_sigmask) { sigset_t sigmask; sigfillset(&sigmask); pthread_attr_setsigmask_np(attr, &sigmask); } static inline void timerlib_unblock_signals(sigset_t *old_sigmask) { } #else // glibc before 2.32: save and restore the main thread's sigmask so that the new // thread will inherit a sigmask with all signals blocked static inline void timerlib_block_signals(pthread_attr_t *attr, sigset_t *old_sigmask) { sigset_t sigmask; sigfillset(&sigmask); pthread_sigmask(SIG_BLOCK, &sigmask, old_sigmask); } static inline void timerlib_unblock_signals(sigset_t *old_sigmask) { pthread_sigmask(SIG_SETMASK, old_sigmask, NULL); } #endif #ifndef HAVE_GETTID #include #define gettid() ((pid_t)syscall(SYS_gettid)) #endif #include "timerlib_pthread_mutex.h" /** * This is called by the handler thread to notify the main thread that * timer->tid is valid. */ static void timerlib_notify_ready(timerlib_timer_t *timer) { timerlib_mutex_lock(&timer->ready_mutex); timer->ready = 1; int error = pthread_cond_broadcast(&timer->ready_cond); if (error) { timerlib_abort("pthread_cond_broadcast", error); } timerlib_mutex_unlock(&timer->ready_mutex); } /** * Stop the handler thread (called from the main thread) */ static int timerlib_graceful_exit(timerlib_timer_t *timer) { // We share TIMERLIB_SIGNAL between timer expiration and shutdown, mostly // to be less intrusive on the application. But if an expiration signal // is already pending, sending another will be ignored. We set a variable // so that the thread will terminate anyway in that case. timer->killed = 1; int error = pthread_kill(timer->thread, TIMERLIB_SIGNAL); if (error) { timerlib_report_errno("pthread_kill", error); return TIMERLIB_FAILURE; } return TIMERLIB_SUCCESS; } /** * Join the handler thread, wait for it to exit. */ static int timerlib_join(timerlib_timer_t *timer) { int error = pthread_join(timer->thread, NULL); if (error) { timerlib_report_errno("pthread_join", error); return TIMERLIB_FAILURE; } return TIMERLIB_SUCCESS; } /** * Convert a timerlib clock constant to a POSIX clock * @param clock May be either TIMERLIB_REAL or TIMERLIB_CPU */ static clockid_t timerlib_map_clock(int clock) { if (clock == TIMERLIB_REAL) { return CLOCK_MONOTONIC; } else { clockid_t c = CLOCK_MONOTONIC; int error = pthread_getcpuclockid(pthread_self(), &c); if (error) { timerlib_report_errno("pthread_getcpuclockid", error); } return c; } } /** * The start routine of the handler thread */ static void* timerlib_timer_thread_main(void *data) { timerlib_timer_t *timer = data; timer->tid = gettid(); // Tell the main thread we are ready to start timerlib_notify_ready(timer); // Receive only our signal sigset_t sigset; sigemptyset(&sigset); sigaddset(&sigset, TIMERLIB_SIGNAL); while (1) { siginfo_t si; // There is an identical empty loop in the glibc SIGEV_THREAD // implementation. The documentation indicates that EINTR is the only // possible error. while (sigwaitinfo(&sigset, &si) < 0); // If timer_stop() has been called, exit the thread if (timer->killed) { return NULL; } // If signal occurred due to a timer expiration, call the callback. if (si.si_code == SI_TIMER) { timer->notify_function(timer->notify_data, si.si_overrun); } } } int timerlib_timer_init(timerlib_timer_t *timer, int clock, timerlib_notify_function_t *notify_function, void *notify_data) { // Initialise the data. Fields that are not named are automatically zeroed. *timer = (timerlib_timer_t){ .clock = clock, .notify_function = notify_function, .notify_data = notify_data, .ready_cond = PTHREAD_COND_INITIALIZER, .ready_mutex = PTHREAD_MUTEX_INITIALIZER, }; // Block all signals. This prevents the thread from receiving process-directed // signals which are normally handled by the main thread. pthread_attr_t attr; sigset_t old_sigset; pthread_attr_init(&attr); timerlib_block_signals(&attr, &old_sigset); // Create the thread int error = pthread_create(&timer->thread, &attr, timerlib_timer_thread_main, timer); timerlib_unblock_signals(&old_sigset); pthread_attr_destroy(&attr); if (error) { timerlib_report_errno("pthread_create", error); return TIMERLIB_FAILURE; } timer->thread_valid = 1; // Wait for timer->tid to become valid timerlib_mutex_lock(&timer->ready_mutex); while (!timer->ready) { error = pthread_cond_wait(&timer->ready_cond, &timer->ready_mutex); if (error) { timerlib_report_errno("pthread_cond_wait", error); return TIMERLIB_FAILURE; } } timerlib_mutex_unlock(&timer->ready_mutex); // Create the timer // This needs to be done in the main thread, otherwise it silently fails // to deliver any events in CPU mode. struct sigevent sev = { .sigev_signo = TIMERLIB_SIGNAL, .sigev_notify = SIGEV_THREAD_ID, .sigev_notify_thread_id = timer->tid }; if (timer_create(timerlib_map_clock(timer->clock), &sev, &timer->timer)) { timerlib_report_errno("timer_create", errno); return TIMERLIB_FAILURE; } // Remember that timer->timer is valid and needs to be deleted timer->timer_valid = 1; return TIMERLIB_SUCCESS; } int timerlib_timer_start(timerlib_timer_t *timer, timerlib_timespec_t *period, timerlib_timespec_t *initial) { struct itimerspec its = { .it_interval = *period, .it_value = *initial }; if (!timer->timer_valid) { // No point reporting another error, since we presumably already reported // an error in timerlib_timer_init return TIMERLIB_FAILURE; } if (timerlib_timespec_is_zero(initial)) { // Make sure the timer is armed its.it_value.tv_nsec = 1; } if (timer_settime(timer->timer, 0, &its, NULL) != 0) { timerlib_report_errno("timer_settime", errno); return TIMERLIB_FAILURE; } return TIMERLIB_SUCCESS; } int timerlib_timer_stop(timerlib_timer_t * timer) { struct itimerspec its = {0}; if (!timer->timer_valid) { return TIMERLIB_FAILURE; } if (timer_settime(timer->timer, 0, &its, NULL) != 0) { timerlib_report_errno("timer_settime", errno); return TIMERLIB_FAILURE; } return TIMERLIB_SUCCESS; } void timerlib_timer_destroy(timerlib_timer_t * timer) { if (timer->thread_valid) { timer->thread_valid = 0; if (timerlib_graceful_exit(timer) == TIMERLIB_SUCCESS) { timerlib_join(timer); } } if (timer->timer_valid) { timer->timer_valid = 0; if (timer_delete(timer->timer)) { timerlib_report_errno("timer_delete", errno); } } } int timerlib_timer_get_time(timerlib_timer_t *timer, timerlib_timespec_t *remaining) { int ret = TIMERLIB_FAILURE; struct itimerspec its = {0}; // Write to *remaining even on error, so that an unchecked error value will // not lead to the caller using uninitialised memory. if (timer->timer_valid) { if (timer_gettime(timer->timer, &its)) { timerlib_report_errno("timer_gettime", errno); } else { ret = TIMERLIB_SUCCESS; } } *remaining = its.it_value; return ret; } int timerlib_clock_get_time(int clock, timerlib_timespec_t * time) { if (clock_gettime(timerlib_map_clock(clock), time)) { *time = (timerlib_timespec_t){0}; timerlib_report_errno("timer_gettime", errno); return TIMERLIB_FAILURE; } else { return TIMERLIB_SUCCESS; } } excimer-1.2.5/timerlib/timerlib_posix.h0000664000175000017500000000306415012753166020777 0ustar tstarlingtstarling/* Copyright 2025 Wikimedia Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #define TIMERLIB_HAVE_CPU_CLOCK typedef struct timespec timerlib_timespec_t; typedef struct { // ID of the POSIX timer backing this timer timer_t timer; // True if timer is valid will need to be deleted int timer_valid; // The handler thread pthread_t thread; // True if the thread is valid int thread_valid; // The handler thread ID pid_t tid; // The clock type, TIMERLIB_REAL or TIMERLIB_CPU int clock; // Pointer to a callback to be invoked when this timer fires. timerlib_notify_function_t *notify_function; // Data to be passed to notify_function as the first argument void *notify_data; // The handler thread sets this when it is ready to receive events int ready; // A condition variable associated with "ready" pthread_cond_t ready_cond; // A mutex associated with "ready" pthread_mutex_t ready_mutex; // The main thread sets this to notify the handler thread that it should exit int killed; } timerlib_timer_t; excimer-1.2.5/timerlib/timerlib_pthread_mutex.h0000664000175000017500000000233215012753166022503 0ustar tstarlingtstarling/* Copyright 2025 Wikimedia Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Helper functions for dealing with pthread mutexes */ #define timerlib_mutex_lock(mutex) timerlib_mutex_lock_func(mutex, __func__) #define timerlib_mutex_unlock(mutex) timerlib_mutex_unlock_func(mutex, __func__) static void timerlib_mutex_lock_func(pthread_mutex_t *mutex, const char *func) { int result = pthread_mutex_lock(mutex); if (result != 0) { timerlib_abort_func(func, "pthread_mutex_lock", result); } } static void timerlib_mutex_unlock_func(pthread_mutex_t *mutex, const char *func) { int result = pthread_mutex_unlock(mutex); if (result != 0) { timerlib_abort_func(func, "pthread_mutex_unlock", result); } } excimer-1.2.5/LICENSE0000664000175000017500000002613615012753166015000 0ustar tstarlingtstarling Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. excimer-1.2.5/README.md0000664000175000017500000000112515012753166015241 0ustar tstarlingtstarling# Excimer Excimer is an extension for PHP 7.1+ that provides a low-overhead interrupting timer and sampling profiler. ## Installation Excimer is available from [packages.debian.org](https://packages.debian.org/stable/php-excimer), and can be installed via `apt-get install php-excimer`. For other installation methods, see . ## Documentation For the API, see [PHPDoc stubs](./stubs). For usage examples, see . ## See also * [wikimedia/arc-lamp](https://gerrit.wikimedia.org/g/performance/arc-lamp/) excimer-1.2.5/config.m40000664000175000017500000000347615012753166015504 0ustar tstarlingtstarlingdnl config.m4 for extension excimer PHP_ARG_ENABLE(excimer, whether to enable excimer support, [ --enable-excimer Enable excimer support]) if test "$PHP_EXCIMER" != "no"; then dnl Timers require real-time and pthread library on Linux and not dnl supported on other platforms AC_CHECK_DECL(SIGEV_THREAD_ID, [ AC_CHECK_LIB(rt, timer_create) AC_CHECK_DECL(pthread_attr_setsigmask_np,[ AC_DEFINE(HAVE_PTHREAD_ATTR_SETSIGMASK_NP, 1, [Whether pthread_attr_setsigmask_np is available]) ],,[[ #define _GNU_SOURCE 1 #include ]]) AC_CHECK_DECL(gettid,[ AC_DEFINE(HAVE_GETTID, 1, [Whether gettid is available]) ],,[[ #define _GNU_SOURCE 1 #include ]]) AC_DEFINE(HAVE_SIGEV_THREAD_ID, 1, [Whether SIGEV_THREAD_ID is available on the current platform]) PHP_EVAL_LIBLINE($LIBS, EXCIMER_SHARED_LIBADD) excimer_os_sources=timerlib/timerlib_posix.c ], [ AC_SEARCH_LIBS([kevent], [kqueue], [ AC_DEFINE(HAVE_KQUEUE, 1, [Whether kqueue is available on the current platform]) PHP_EVAL_LIBLINE($LIBS, EXCIMER_SHARED_LIBADD) excimer_os_sources=timerlib/timerlib_kqueue.c ], [ AC_MSG_ERROR([excimer requires timer_create or kevent]) ]) ], [[ #include ]]) AC_SEARCH_LIBS([pthread_mutex_lock], [pthread], [ PHP_EVAL_LIBLINE($LIBS, EXCIMER_SHARED_LIBADD) ]) dnl Avoid exporting symbols unnecessarily AX_CHECK_COMPILE_FLAG([-fvisibility=hidden], [CFLAGS="$CFLAGS -fvisibility=hidden"]) PHP_SUBST(EXCIMER_SHARED_LIBADD) PHP_NEW_EXTENSION(excimer, excimer.c \ excimer_mutex.c \ excimer_timer.c \ excimer_log.c \ timerlib/timerlib_common.c \ $excimer_os_sources, $ext_shared) dnl Create build directory (T394738) PHP_ADD_BUILD_DIR([$ext_builddir/timerlib]) fi excimer-1.2.5/excimer.c0000664000175000017500000012770515012753166015577 0ustar tstarlingtstarling/* Copyright 2018 Wikimedia Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include "php.h" #include "zend_exceptions.h" #include "zend_interfaces.h" #include "ext/spl/spl_exceptions.h" #if PHP_VERSION_ID < 80400 #include "ext/standard/php_mt_rand.h" #else #include "ext/random/php_random.h" #endif #include "ext/standard/info.h" #if PHP_VERSION_ID < 70200 /* For spl_ce_Countable */ #include "ext/spl/spl_iterators.h" #endif #include "php_excimer.h" #include "excimer_timer.h" #include "excimer_log.h" #define EXCIMER_OBJ(type, object) \ ((type ## _obj*)excimer_check_object(object, XtOffsetOf(type ## _obj, std), &type ## _handlers)) #define EXCIMER_OBJ_Z(type, zval) (Z_TYPE(zval) == IS_OBJECT ? EXCIMER_OBJ(type, Z_OBJ(zval)) : NULL) #define EXCIMER_OBJ_ZP(type, zval_ptr) EXCIMER_OBJ(type, Z_OBJ_P(zval_ptr)) #define EXCIMER_NEW_OBJECT(type, ce) \ excimer_object_alloc_init(sizeof(type ## _obj), &type ## _handlers, ce) #define EXCIMER_DEFAULT_PERIOD 0.1 #define EXCIMER_BILLION 1000000000LL /* {{{ types */ /** * ExcimerProfiler_obj: underlying storage for ExcimerProfiler */ typedef struct { /** The period which will be used when the timer is next started */ struct timespec period; /** The initial interval */ struct timespec initial; /** The event type, either EXCIMER_CPU or EXCIMER_REAL */ zend_long event_type; /** The currently-attached log */ zval z_log; /** The flush callback. If this is set, max_samples will also be set. */ zval z_callback; /** The maximum number of samples in z_log before z_callback is called. */ zend_long max_samples; /** Whether a parameter has changed that requires reinitialisation of the timer. */ int need_reinit; /** The timer backend object */ excimer_timer timer; zend_object std; } ExcimerProfiler_obj; /** * ExcimerLog_iterator: Iterator object returned by get_iterator handler, used * by foreach. */ typedef struct { /** * FIXME Iterators use PHP 5 style inheritance. This is actually a * zend_object header. Maybe it is harmless but it seems dodgy to me, * should be fixed upstream. */ zend_user_iterator intern; /** Cached (lazy-initialised) value to use for current() */ zval z_current; /** Current log index */ zend_long index; } ExcimerLog_iterator; /** * ExcimerLog_obj: underlying storage for ExcimerLog */ typedef struct { /** The log backend object */ excimer_log log; /** The cached value to use for current() */ zval z_current; /** The current index, for key() etc. */ zend_long iter_entry_index; zend_object std; } ExcimerLog_obj; /** * ExcimerLogEntry_obj: underlying storage for ExcimerLogEntry */ typedef struct { /** * The ExcimerLog. Note that this can be a circular reference if * ExcimerLog_obj.z_current points here. */ zval z_log; /** The index of this entry in the ExcimerLog */ zend_long index; zend_object std; } ExcimerLogEntry_obj; /** * ExcimerTimer_obj: underlying storage for ExcimerTimer */ typedef struct { /** The timer backend object */ excimer_timer timer; /** The timer period */ struct timespec period; /** The initial expiry, or zero to use the period */ struct timespec initial; /** The event type, EXCIMER_REAL or EXCIMER_CPU */ zend_long event_type; /** Whether a parameter has changed that requires reinitialisation of the timer. */ int need_reinit; /** The event function, or null for no callback */ zval z_callback; zend_object std; } ExcimerTimer_obj; /* }}} */ /* {{{ static function declarations */ static void ExcimerProfiler_start(ExcimerProfiler_obj *profiler); static void ExcimerProfiler_stop(ExcimerProfiler_obj *profiler); static void ExcimerProfiler_event(zend_long event_count, void *user_data); static void ExcimerProfiler_flush(ExcimerProfiler_obj *profiler, zval *zp_old_log); static zend_object *ExcimerProfiler_new(zend_class_entry *ce); static void ExcimerProfiler_free_object(zend_object *object); static void ExcimerProfiler_dtor(zend_object *object); static PHP_METHOD(ExcimerProfiler, setPeriod); static PHP_METHOD(ExcimerProfiler, setEventType); static PHP_METHOD(ExcimerProfiler, setMaxDepth); static PHP_METHOD(ExcimerProfiler, setFlushCallback); static PHP_METHOD(ExcimerProfiler, clearFlushCallback); static PHP_METHOD(ExcimerProfiler, start); static PHP_METHOD(ExcimerProfiler, stop); static PHP_METHOD(ExcimerProfiler, getLog); static PHP_METHOD(ExcimerProfiler, flush); static zend_object *ExcimerLog_new(zend_class_entry *ce); static void ExcimerLog_free_object(zend_object *object); static zend_object_iterator *ExcimerLog_get_iterator(zend_class_entry *ce, zval *object, int by_ref); #if PHP_VERSION_ID < 80000 static int ExcimerLog_count_elements(zval *zp_log, zend_long *lp_count); #else static int ExcimerLog_count_elements(zend_object *object, zend_long *lp_count); #endif static void ExcimerLog_init_entry(zval *zp_dest, zval *zp_log, zend_long index); static void ExcimerLog_iterator_dtor(zend_object_iterator *iter); static int ExcimerLog_iterator_valid(zend_object_iterator *iter); static zval *ExcimerLog_iterator_get_current_data(zend_object_iterator *iter); static void ExcimerLog_iterator_get_current_key(zend_object_iterator *iter, zval *key); static void ExcimerLog_iterator_move_forward(zend_object_iterator *iter); static void ExcimerLog_iterator_rewind(zend_object_iterator *iter); static void ExcimerLog_iterator_invalidate_current(zend_object_iterator *iter); static PHP_METHOD(ExcimerLog, __construct); static PHP_METHOD(ExcimerLog, formatCollapsed); static PHP_METHOD(ExcimerLog, getSpeedscopeData); static PHP_METHOD(ExcimerLog, aggregateByFunction); static PHP_METHOD(ExcimerLog, getEventCount); static PHP_METHOD(ExcimerLog, current); static PHP_METHOD(ExcimerLog, key); static PHP_METHOD(ExcimerLog, next); static PHP_METHOD(ExcimerLog, rewind); static PHP_METHOD(ExcimerLog, valid); static PHP_METHOD(ExcimerLog, count); static PHP_METHOD(ExcimerLog, offsetExists); static PHP_METHOD(ExcimerLog, offsetGet); static PHP_METHOD(ExcimerLog, offsetSet); static PHP_METHOD(ExcimerLog, offsetUnset); static zend_object *ExcimerLogEntry_new(zend_class_entry *ce); static void ExcimerLogEntry_free_object(zend_object *object); static PHP_METHOD(ExcimerLogEntry, __construct); static PHP_METHOD(ExcimerLogEntry, getTimestamp); static PHP_METHOD(ExcimerLogEntry, getEventCount); static PHP_METHOD(ExcimerLogEntry, getTrace); static zend_object *ExcimerTimer_new(zend_class_entry *ce); static void ExcimerTimer_free_object(zend_object *object); static PHP_METHOD(ExcimerTimer, setEventType); static PHP_METHOD(ExcimerTimer, setInterval); static PHP_METHOD(ExcimerTimer, setPeriod); static PHP_METHOD(ExcimerTimer, setCallback); static PHP_METHOD(ExcimerTimer, start); static PHP_METHOD(ExcimerTimer, stop); static PHP_METHOD(ExcimerTimer, getTime); static void ExcimerTimer_start(ExcimerTimer_obj *timer_obj); static void ExcimerTimer_stop(ExcimerTimer_obj *timer_obj); static void ExcimerTimer_event(zend_long event_count, void *user_data); static int ExcimerTimer_set_callback(ExcimerTimer_obj *timer_obj, zval *zp_callback); static PHP_FUNCTION(excimer_set_timeout); /* }}} */ static zend_class_entry *ExcimerProfiler_ce; static zend_class_entry *ExcimerLog_ce; static zend_class_entry *ExcimerLogEntry_ce; static zend_class_entry *ExcimerTimer_ce; static zend_object_handlers ExcimerProfiler_handlers; static zend_object_handlers ExcimerLog_handlers; static zend_object_handlers ExcimerLogEntry_handlers; static zend_object_handlers ExcimerTimer_handlers; /** {{{ arginfo */ #ifndef ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX #define ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(name, return_reference, required_num_args, type, allow_null) \ ZEND_BEGIN_ARG_INFO_EX(name, 0, return_reference, required_num_args) #endif ZEND_BEGIN_ARG_INFO(arginfo_ExcimerProfiler_setPeriod, 0) ZEND_ARG_INFO(0, period) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerProfiler_setEventType, 0) ZEND_ARG_INFO(0, event_type) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerProfiler_setMaxDepth, 0) ZEND_ARG_INFO(0, max_depth) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerProfiler_setFlushCallback, 0) ZEND_ARG_INFO(0, callback) ZEND_ARG_INFO(0, max_samples) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerProfiler_clearFlushCallback, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerProfiler_start, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerProfiler_stop, 0) ZEND_END_ARG_INFO() #if PHP_VERSION_ID >= 70200 ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_ExcimerProfiler_getLog, 0, 0, ExcimerLog, 0) #else ZEND_BEGIN_ARG_INFO(arginfo_ExcimerProfiler_getLog, 0) #endif ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerProfiler_flush, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerLog___construct, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerLog_formatCollapsed, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerLog_getSpeedscopeData, 0) ZEND_END_ARG_INFO() #if PHP_VERSION_ID < 70200 ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO(arginfo_ExcimerLog_aggregateByFunction, IS_ARRAY, NULL, 0) #else ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO(arginfo_ExcimerLog_aggregateByFunction, IS_ARRAY, 0) #endif ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerLog_getEventCount, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_ExcimerLog_current, 0, 0, IS_MIXED, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_ExcimerLog_key, 0, 0, IS_MIXED, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_ExcimerLog_next, 0, 0, IS_VOID, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_ExcimerLog_rewind, 0, 0, IS_VOID, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_ExcimerLog_valid, 0, 0, _IS_BOOL, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_ExcimerLog_count, 0, 0, IS_LONG, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_ExcimerLog_offsetExists, 0, 1, _IS_BOOL, 0) ZEND_ARG_INFO(0, offset) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_ExcimerLog_offsetGet, 0, 1, IS_MIXED, 0) ZEND_ARG_INFO(0, offset) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_ExcimerLog_offsetSet, 0, 2, IS_VOID, 0) ZEND_ARG_INFO(0, offset) ZEND_ARG_INFO(0, value) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_ExcimerLog_offsetUnset, 0, 1, IS_VOID, 0) ZEND_ARG_INFO(0, offset) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerLogEntry___construct, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerLogEntry_getTimestamp, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerLogEntry_getEventCount, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerLogEntry_getTrace, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerTimer_setEventType, 0) ZEND_ARG_INFO(0, event_type) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerTimer_setInterval, 0) ZEND_ARG_INFO(0, interval) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerTimer_setPeriod, 0) ZEND_ARG_INFO(0, period) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerTimer_setCallback, 0) ZEND_ARG_INFO(0, callback) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerTimer_start, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerTimer_stop, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_ExcimerTimer_getTime, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_excimer_set_timeout, 0) ZEND_ARG_INFO(0, callback) ZEND_ARG_INFO(0, interval) ZEND_END_ARG_INFO() /* }}} */ /** {{{ function entries */ static const zend_function_entry ExcimerProfiler_methods[] = { PHP_ME(ExcimerProfiler, setPeriod, arginfo_ExcimerProfiler_setPeriod, 0) PHP_ME(ExcimerProfiler, setEventType, arginfo_ExcimerProfiler_setEventType, 0) PHP_ME(ExcimerProfiler, setMaxDepth, arginfo_ExcimerProfiler_setMaxDepth, 0) PHP_ME(ExcimerProfiler, setFlushCallback, arginfo_ExcimerProfiler_setFlushCallback, 0) PHP_ME(ExcimerProfiler, clearFlushCallback, arginfo_ExcimerProfiler_clearFlushCallback, 0) PHP_ME(ExcimerProfiler, start, arginfo_ExcimerProfiler_start, 0) PHP_ME(ExcimerProfiler, stop, arginfo_ExcimerProfiler_stop, 0) PHP_ME(ExcimerProfiler, getLog, arginfo_ExcimerProfiler_getLog, 0) PHP_ME(ExcimerProfiler, flush, arginfo_ExcimerProfiler_flush, 0) PHP_FE_END }; static const zend_function_entry ExcimerLog_methods[] = { PHP_ME(ExcimerLog, __construct, arginfo_ExcimerLog___construct, ZEND_ACC_PRIVATE | ZEND_ACC_FINAL) PHP_ME(ExcimerLog, formatCollapsed, arginfo_ExcimerLog_formatCollapsed, 0) PHP_ME(ExcimerLog, getSpeedscopeData, arginfo_ExcimerLog_getSpeedscopeData, 0) PHP_ME(ExcimerLog, aggregateByFunction, arginfo_ExcimerLog_aggregateByFunction, 0) PHP_ME(ExcimerLog, getEventCount, arginfo_ExcimerLog_getEventCount, 0) PHP_ME(ExcimerLog, current, arginfo_ExcimerLog_current, 0) PHP_ME(ExcimerLog, key, arginfo_ExcimerLog_key, 0) PHP_ME(ExcimerLog, next, arginfo_ExcimerLog_next, 0) PHP_ME(ExcimerLog, rewind, arginfo_ExcimerLog_rewind, 0) PHP_ME(ExcimerLog, valid, arginfo_ExcimerLog_valid, 0) PHP_ME(ExcimerLog, count, arginfo_ExcimerLog_count, 0) PHP_ME(ExcimerLog, offsetExists, arginfo_ExcimerLog_offsetExists, 0) PHP_ME(ExcimerLog, offsetGet, arginfo_ExcimerLog_offsetGet, 0) PHP_ME(ExcimerLog, offsetSet, arginfo_ExcimerLog_offsetSet, 0) PHP_ME(ExcimerLog, offsetUnset, arginfo_ExcimerLog_offsetUnset, 0) PHP_FE_END }; static zend_object_iterator_funcs ExcimerLog_iterator_funcs = { ExcimerLog_iterator_dtor, ExcimerLog_iterator_valid, ExcimerLog_iterator_get_current_data, ExcimerLog_iterator_get_current_key, ExcimerLog_iterator_move_forward, ExcimerLog_iterator_rewind, ExcimerLog_iterator_invalidate_current }; static const zend_function_entry ExcimerLogEntry_methods[] = { PHP_ME(ExcimerLogEntry, __construct, arginfo_ExcimerLogEntry___construct, ZEND_ACC_PRIVATE | ZEND_ACC_FINAL) PHP_ME(ExcimerLogEntry, getTimestamp, arginfo_ExcimerLogEntry_getTimestamp, 0) PHP_ME(ExcimerLogEntry, getEventCount, arginfo_ExcimerLogEntry_getEventCount, 0) PHP_ME(ExcimerLogEntry, getTrace, arginfo_ExcimerLogEntry_getTrace, 0) PHP_FE_END }; static const zend_function_entry ExcimerTimer_methods[] = { PHP_ME(ExcimerTimer, setEventType, arginfo_ExcimerTimer_setEventType, 0) PHP_ME(ExcimerTimer, setInterval, arginfo_ExcimerTimer_setInterval, 0) PHP_ME(ExcimerTimer, setPeriod, arginfo_ExcimerTimer_setPeriod, 0) PHP_ME(ExcimerTimer, setCallback, arginfo_ExcimerTimer_setCallback, 0) PHP_ME(ExcimerTimer, start, arginfo_ExcimerTimer_start, 0) PHP_ME(ExcimerTimer, stop, arginfo_ExcimerTimer_stop, 0) PHP_ME(ExcimerTimer, getTime, arginfo_ExcimerTimer_getTime, 0) PHP_FE_END }; static const zend_function_entry excimer_functions[] = { PHP_FE(excimer_set_timeout, arginfo_excimer_set_timeout) PHP_FE_END }; /* }}} */ /* {{{ INI Settings */ PHP_INI_BEGIN() PHP_INI_ENTRY("excimer.default_max_depth", "1000", PHP_INI_ALL, NULL) PHP_INI_END() /* }}} */ static void *excimer_object_alloc_init(size_t object_size, zend_object_handlers *handlers, zend_class_entry *ce) /* {{{ */ { #if PHP_VERSION_ID < 70300 char *intern = ecalloc(1, object_size + zend_object_properties_size(ce)); #else char *intern = zend_object_alloc(object_size, ce); #endif const size_t header_size = object_size - sizeof(zend_object); zend_object *object = (zend_object*)(intern + header_size); zend_object_std_init(object, ce); object_properties_init(object, ce); object->handlers = handlers; return intern; } /* }}} */ static inline void* excimer_check_object(zend_object *object, size_t offset, const zend_object_handlers *handlers) { if (object->handlers != handlers) { return NULL; } else { return (void*)((char*)object - offset); } } /* {{{ PHP_MINIT_FUNCTION */ static PHP_MINIT_FUNCTION(excimer) { zend_class_entry ce; REGISTER_INI_ENTRIES(); REGISTER_LONG_CONSTANT("EXCIMER_REAL", EXCIMER_REAL, CONST_CS | CONST_PERSISTENT); // Only define EXCIMER_CPU if the current platform supports it. // This allows application code to detect and gracefully handle a lack of CPU profiling support. #ifdef TIMERLIB_HAVE_CPU_CLOCK REGISTER_LONG_CONSTANT("EXCIMER_CPU", EXCIMER_CPU, CONST_CS | CONST_PERSISTENT); #endif #define REGISTER_EXCIMER_CLASS(class_name) \ INIT_CLASS_ENTRY(ce, #class_name, class_name ## _methods); \ class_name ## _ce = zend_register_internal_class(&ce); \ class_name ## _ce->create_object = class_name ## _new; \ memcpy(&class_name ## _handlers, zend_get_std_object_handlers(), \ sizeof(zend_object_handlers)); \ class_name ## _handlers.offset = XtOffsetOf(class_name ## _obj, std); \ class_name ## _handlers.free_obj = class_name ## _free_object; REGISTER_EXCIMER_CLASS(ExcimerProfiler); ExcimerProfiler_handlers.dtor_obj = ExcimerProfiler_dtor; REGISTER_EXCIMER_CLASS(ExcimerLog); ExcimerLog_ce->get_iterator = ExcimerLog_get_iterator; ExcimerLog_handlers.count_elements = ExcimerLog_count_elements; zend_class_implements(ExcimerLog_ce, 1, zend_ce_iterator); #if PHP_VERSION_ID >= 70200 zend_class_implements(ExcimerLog_ce, 1, zend_ce_countable); zend_class_implements(ExcimerLog_ce, 1, zend_ce_arrayaccess); #elif defined(HAVE_SPL) zend_class_implements(ExcimerLog_ce, 1, spl_ce_Countable); zend_class_implements(ExcimerLog_ce, 1, spl_ce_ArrayAccess); #endif REGISTER_EXCIMER_CLASS(ExcimerLogEntry); REGISTER_EXCIMER_CLASS(ExcimerTimer); #undef REGISTER_EXCIMER_CLASS excimer_timer_module_init(); return SUCCESS; } /* }}} */ /* {{{ PHP_MSHUTDOWN_FUNCTION */ static PHP_MSHUTDOWN_FUNCTION(excimer) { UNREGISTER_INI_ENTRIES(); excimer_timer_module_shutdown(); return SUCCESS; } /* }}} */ /* {{{ PHP_RINIT_FUNCTION */ static PHP_RINIT_FUNCTION(excimer) { excimer_timer_thread_init(); return SUCCESS; } /* }}} */ /* {{{ ZEND_MODULE_POST_ZEND_DEACTIVATE_D */ static ZEND_MODULE_POST_ZEND_DEACTIVATE_D(excimer) { excimer_timer_thread_shutdown(); return SUCCESS; } /* }}} */ /* {{{ PHP_MINFO_FUNCTION */ static PHP_MINFO_FUNCTION(excimer) { php_info_print_table_start(); php_info_print_table_header(2, "excimer support", "enabled"); php_info_print_table_row(2, "excimer version", PHP_EXCIMER_VERSION); php_info_print_table_end(); DISPLAY_INI_ENTRIES(); } /* }}} */ static zend_object *ExcimerProfiler_new(zend_class_entry *ce) /* {{{ */ { ExcimerProfiler_obj *profiler = EXCIMER_NEW_OBJECT(ExcimerProfiler, ce); ExcimerLog_obj *log_obj; struct timespec now_ts; double initial; timerlib_clock_get_time(TIMERLIB_REAL, &now_ts); object_init_ex(&profiler->z_log, ExcimerLog_ce); log_obj = EXCIMER_OBJ_Z(ExcimerLog, profiler->z_log); log_obj->log.max_depth = INI_INT("excimer.default_max_depth"); log_obj->log.epoch = timerlib_timespec_to_ns(&now_ts); ZVAL_NULL(&profiler->z_callback); profiler->event_type = EXCIMER_REAL; profiler->need_reinit = 1; // Stagger start time initial = php_mt_rand() * EXCIMER_DEFAULT_PERIOD / UINT32_MAX; timerlib_timespec_from_double(&profiler->initial, initial); timerlib_timespec_from_double(&profiler->period, EXCIMER_DEFAULT_PERIOD); log_obj->log.period = EXCIMER_DEFAULT_PERIOD * EXCIMER_BILLION; return &profiler->std; } /* }}} */ static void ExcimerProfiler_free_object(zend_object *object) /* {{{ */ { ExcimerProfiler_obj *profiler = EXCIMER_OBJ(ExcimerProfiler, object); if (profiler->timer.is_valid) { excimer_timer_destroy(&profiler->timer); } zval_ptr_dtor(&profiler->z_log); ZVAL_UNDEF(&profiler->z_log); zval_ptr_dtor(&profiler->z_callback); ZVAL_UNDEF(&profiler->z_callback); zend_object_std_dtor(object); } /* }}} */ static void ExcimerProfiler_dtor(zend_object *object) /* {{{ */ { ExcimerProfiler_obj *profiler = EXCIMER_OBJ(ExcimerProfiler, object); ExcimerLog_obj *log_obj = EXCIMER_OBJ_Z(ExcimerLog, profiler->z_log); zval z_old_log; if (log_obj->log.entries_size) { ExcimerProfiler_flush(profiler, &z_old_log); zval_ptr_dtor(&z_old_log); } } /* }}} */ /* {{{ proto void ExcimerProfiler::setPeriod(float period) */ static PHP_METHOD(ExcimerProfiler, setPeriod) { double period, initial; ExcimerProfiler_obj *profiler = EXCIMER_OBJ_ZP(ExcimerProfiler, getThis()); ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_DOUBLE(period) ZEND_PARSE_PARAMETERS_END(); // Stagger start time initial = php_mt_rand() * period / UINT32_MAX; timerlib_timespec_from_double(&profiler->period, period); timerlib_timespec_from_double(&profiler->initial, initial); ExcimerLog_obj *log = EXCIMER_OBJ_ZP(ExcimerLog, &profiler->z_log); log->log.period = period * EXCIMER_BILLION; } /* }}} */ /* {{{ proto void ExcimerProfiler::setEventType(int event_type) */ static PHP_METHOD(ExcimerProfiler, setEventType) { zend_long event_type; ExcimerProfiler_obj *profiler = EXCIMER_OBJ_ZP(ExcimerProfiler, getThis()); ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_LONG(event_type) ZEND_PARSE_PARAMETERS_END(); if (event_type != EXCIMER_CPU && event_type != EXCIMER_REAL) { php_error_docref(NULL, E_WARNING, "Invalid event type"); return; } profiler->event_type = event_type; profiler->need_reinit = 1; } /* }}} */ /* {{{ proto void ExcimerProfiler::setMaxDepth(int max_depth) */ static PHP_METHOD(ExcimerProfiler, setMaxDepth) { zend_long max_depth; ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_LONG(max_depth) ZEND_PARSE_PARAMETERS_END(); ExcimerProfiler_obj *profiler = EXCIMER_OBJ_ZP(ExcimerProfiler, getThis()); ExcimerLog_obj *log_obj = EXCIMER_OBJ_ZP(ExcimerLog, &profiler->z_log); excimer_log_set_max_depth(&log_obj->log, max_depth); } /* }}} */ /* {{{ proto void ExcimerProfiler::setFlushCallback(callable callback, mixed max_samples) */ static PHP_METHOD(ExcimerProfiler, setFlushCallback) { zval *z_callback; zend_long max_samples; ExcimerProfiler_obj *profiler = EXCIMER_OBJ_ZP(ExcimerProfiler, getThis()); char *is_callable_error; ZEND_PARSE_PARAMETERS_START(2, 2) Z_PARAM_ZVAL(z_callback) Z_PARAM_LONG(max_samples) ZEND_PARSE_PARAMETERS_END(); if (!zend_is_callable_ex(z_callback, NULL, 0, NULL, NULL, &is_callable_error)) { php_error_docref(NULL, E_WARNING, "flush callback is not callable: %s", is_callable_error); return; } ZVAL_COPY(&profiler->z_callback, z_callback); profiler->max_samples = max_samples; } /* }}} */ /* {{{ proto void ExcimerProfiler::clearFlushCallback() */ static PHP_METHOD(ExcimerProfiler, clearFlushCallback) { ExcimerProfiler_obj *profiler = EXCIMER_OBJ_ZP(ExcimerProfiler, getThis()); zval_ptr_dtor(&profiler->z_callback); ZVAL_NULL(&profiler->z_callback); profiler->max_samples = 0; } /* }}} */ /* {{{ proto void ExcimerProfiler::start() */ static PHP_METHOD(ExcimerProfiler, start) { ExcimerProfiler_obj *profiler = EXCIMER_OBJ_ZP(ExcimerProfiler, getThis()); ZEND_PARSE_PARAMETERS_START(0, 0); ZEND_PARSE_PARAMETERS_END(); if (profiler->timer.is_running) { ExcimerProfiler_stop(profiler); } ExcimerProfiler_start(profiler); } /* }}} */ /* {{{ proto void ExcimerProfiler::stop() */ static PHP_METHOD(ExcimerProfiler, stop) { ExcimerProfiler_obj *profiler = EXCIMER_OBJ_ZP(ExcimerProfiler, getThis()); ZEND_PARSE_PARAMETERS_START(0, 0); ZEND_PARSE_PARAMETERS_END(); ExcimerProfiler_stop(profiler); } /* }}} */ /* {{{ proto ExcimerLog ExcimerProfiler::getLog() */ static PHP_METHOD(ExcimerProfiler, getLog) { ExcimerProfiler_obj * profiler = EXCIMER_OBJ_ZP(ExcimerProfiler, getThis()); ZEND_PARSE_PARAMETERS_START(0, 0); ZEND_PARSE_PARAMETERS_END(); RETURN_ZVAL(&profiler->z_log, 1, 0); } /* }}} */ /* {{{ proto ExcimerLog ExcimerProfiler::flush() */ static PHP_METHOD(ExcimerProfiler, flush) { ExcimerProfiler_obj *profiler = EXCIMER_OBJ_ZP(ExcimerProfiler, getThis()); ZEND_PARSE_PARAMETERS_START(0, 0); ZEND_PARSE_PARAMETERS_END(); ExcimerProfiler_flush(profiler, return_value); } /* }}} */ static void ExcimerProfiler_start(ExcimerProfiler_obj *profiler) /* {{{ */ { if (profiler->need_reinit || !profiler->timer.is_valid) { if (profiler->timer.is_valid) { excimer_timer_destroy(&profiler->timer); } if (excimer_timer_init(&profiler->timer, profiler->event_type, ExcimerProfiler_event, (void*)profiler) == FAILURE) { /* Error message already sent */ return; } profiler->need_reinit = 0; } excimer_timer_start(&profiler->timer, &profiler->period, &profiler->initial); } /* }}} */ static void ExcimerProfiler_stop(ExcimerProfiler_obj *profiler) /* {{{ */ { if (profiler->timer.is_valid) { excimer_timer_stop(&profiler->timer); } } /* }}} */ static void ExcimerProfiler_event(zend_long event_count, void *user_data) /* {{{ */ { uint64_t now_ns; struct timespec now_ts; ExcimerProfiler_obj *profiler = (ExcimerProfiler_obj*)user_data; ExcimerLog_obj *log_obj = EXCIMER_OBJ_ZP(ExcimerLog, &profiler->z_log); excimer_log *log; log = &log_obj->log; timerlib_clock_get_time(TIMERLIB_REAL, &now_ts); now_ns = timerlib_timespec_to_ns(&now_ts); excimer_log_add(log, EG(current_execute_data), event_count, now_ns); if (profiler->max_samples && log->entries_size >= profiler->max_samples) { zval z_old_log; ExcimerProfiler_flush(profiler, &z_old_log); zval_ptr_dtor(&z_old_log); } } /* }}} */ static void ExcimerProfiler_flush(ExcimerProfiler_obj *profiler, zval *zp_old_log) /* {{{ */ { ExcimerLog_obj *log_obj = EXCIMER_OBJ_ZP(ExcimerLog, &profiler->z_log); excimer_log *log = &log_obj->log; zend_fcall_info fci; zend_fcall_info_cache fcc; char *is_callable_error = NULL; zval retval; int status; /* Rotate the log */ ZVAL_COPY(zp_old_log, &profiler->z_log); Z_DELREF(profiler->z_log); object_init_ex(&profiler->z_log, ExcimerLog_ce); excimer_log_copy_options(&EXCIMER_OBJ_ZP(ExcimerLog, &profiler->z_log)->log, log); if (Z_ISNULL(profiler->z_callback)) { return; } /* Prepare to call the flush callback */ if (zend_fcall_info_init(&profiler->z_callback, 0, &fci, &fcc, NULL, &is_callable_error) != SUCCESS) { php_error(E_WARNING, "ExcimerProfiler callback is not callable (during event): %s", is_callable_error); ExcimerProfiler_stop(profiler); return; } fci.retval = &retval; /* Call it */ zend_fcall_info_argn(&fci, 1, zp_old_log); status = zend_call_function(&fci, &fcc); if (status == SUCCESS) { zval_ptr_dtor(&retval); } zend_fcall_info_args_clear(&fci, 1); } /* }}} */ static zend_object *ExcimerLog_new(zend_class_entry *ce) /* {{{ */ { ExcimerLog_obj *log_obj = EXCIMER_NEW_OBJECT(ExcimerLog, ce); excimer_log_init(&log_obj->log); /* Lazy-initialise z_current to minimise circular references */ ZVAL_NULL(&log_obj->z_current); log_obj->iter_entry_index = 0; return &log_obj->std; } /* }}} */ static void ExcimerLog_free_object(zend_object *object) /* {{{ */ { ExcimerLog_obj *log_obj = EXCIMER_OBJ(ExcimerLog, object); excimer_log_destroy(&log_obj->log); zval_ptr_dtor(&log_obj->z_current); zend_object_std_dtor(object); } /* }}} */ /* {{{ ExcimerLog_get_iterator */ static zend_object_iterator *ExcimerLog_get_iterator( zend_class_entry *ce, zval *zp_log, int by_ref) { ExcimerLog_iterator *iterator; if (by_ref) { zend_throw_exception(spl_ce_RuntimeException, "An iterator cannot be used with foreach by reference", 0); return NULL; } iterator = emalloc(sizeof(ExcimerLog_iterator)); zend_iterator_init((zend_object_iterator*)iterator); ZVAL_COPY(&iterator->intern.it.data, zp_log); iterator->intern.it.funcs = &ExcimerLog_iterator_funcs; iterator->intern.ce = ce; iterator->index = 0; ZVAL_NULL(&iterator->z_current); return &iterator->intern.it; } /* }}} */ static void ExcimerLog_iterator_dtor(zend_object_iterator *iter) /* {{{ */ { ExcimerLog_iterator *iterator = (ExcimerLog_iterator*)iter; zval_ptr_dtor(&iterator->z_current); ZVAL_UNDEF(&iterator->z_current); zval_ptr_dtor(&iterator->intern.it.data); ZVAL_UNDEF(&iterator->intern.it.data); } /* }}} */ static int ExcimerLog_iterator_valid(zend_object_iterator *iter) /* {{{ */ { ExcimerLog_iterator *iterator = (ExcimerLog_iterator*)iter; ExcimerLog_obj *log_obj = EXCIMER_OBJ_Z(ExcimerLog, iterator->intern.it.data); if (iterator->index < log_obj->log.entries_size) { return SUCCESS; } else { return FAILURE; } } /* }}} */ static zval *ExcimerLog_iterator_get_current_data(zend_object_iterator *iter) /* {{{ */ { ExcimerLog_iterator *iterator = (ExcimerLog_iterator*)iter; ExcimerLog_obj *log_obj = EXCIMER_OBJ_Z(ExcimerLog, iterator->intern.it.data); if (Z_ISNULL(iterator->z_current)) { if (iterator->index < log_obj->log.entries_size) { ExcimerLog_init_entry(&iterator->z_current, &iterator->intern.it.data, iterator->index); } else { return NULL; } } return &iterator->z_current; } /* }}} */ static void ExcimerLog_iterator_get_current_key(zend_object_iterator *iter, zval *key) /* {{{ */ { ExcimerLog_iterator *iterator = (ExcimerLog_iterator*)iter; ExcimerLog_obj *log_obj = EXCIMER_OBJ_Z(ExcimerLog, iterator->intern.it.data); if (iterator->index < log_obj->log.entries_size) { ZVAL_LONG(key, iterator->index); } else { ZVAL_NULL(key); } } /* }}} */ static void ExcimerLog_iterator_move_forward(zend_object_iterator *iter) /* {{{ */ { ExcimerLog_iterator *iterator = (ExcimerLog_iterator*)iter; ExcimerLog_obj *log_obj = EXCIMER_OBJ_Z(ExcimerLog, iterator->intern.it.data); zval_ptr_dtor(&iterator->z_current); ZVAL_NULL(&iterator->z_current); if (iterator->index < log_obj->log.entries_size) { iterator->index++; } } /* }}} */ static void ExcimerLog_iterator_rewind(zend_object_iterator *iter) /* {{{ */ { ExcimerLog_iterator *iterator = (ExcimerLog_iterator*)iter; zval_ptr_dtor(&iterator->z_current); ZVAL_NULL(&iterator->z_current); iterator->index = 0; } /* }}} */ static void ExcimerLog_iterator_invalidate_current(zend_object_iterator *iter) /* {{{ */ { ExcimerLog_iterator *iterator = (ExcimerLog_iterator*)iter; zval_ptr_dtor(&iterator->z_current); ZVAL_NULL(&iterator->z_current); } /* }}} */ static void ExcimerLog_init_entry(zval *zp_dest, zval *zp_log, zend_long index) /* {{{ */ { ExcimerLog_obj *log_obj = EXCIMER_OBJ_ZP(ExcimerLog, zp_log); excimer_log_entry *entry = excimer_log_get_entry(&log_obj->log, index); ExcimerLogEntry_obj *entry_obj; if (entry) { object_init_ex(zp_dest, ExcimerLogEntry_ce); entry_obj = EXCIMER_OBJ_ZP(ExcimerLogEntry, zp_dest); ZVAL_COPY(&entry_obj->z_log, zp_log); entry_obj->index = index; } else { ZVAL_NULL(zp_dest); } } /* }}} */ #if PHP_VERSION_ID < 80000 static int ExcimerLog_count_elements(zval *zp_log, zend_long *lp_count) /* {{{ */ { ExcimerLog_obj *log_obj = EXCIMER_OBJ_ZP(ExcimerLog, zp_log); *lp_count = log_obj->log.entries_size; return SUCCESS; } /* }}} */ #else static int ExcimerLog_count_elements(zend_object *object, zend_long *lp_count) /* {{{ */ { ExcimerLog_obj *log_obj = EXCIMER_OBJ(ExcimerLog, object); *lp_count = log_obj->log.entries_size; return SUCCESS; } /* }}} */ #endif /* {{{ proto void ExcimerLog::__construct() */ static PHP_METHOD(ExcimerLog, __construct) { php_error_docref(NULL, E_ERROR, "ExcimerLog cannot be constructed directly"); } /* }}} */ /* {{{ proto string ExcimerLog::formatCollapsed() */ static PHP_METHOD(ExcimerLog, formatCollapsed) { ExcimerLog_obj *log_obj = EXCIMER_OBJ_ZP(ExcimerLog, getThis()); RETURN_STR(excimer_log_format_collapsed(&log_obj->log)); } /* }}} */ /* {{{ proto string ExcimerLog::getSpeedscopeData() */ static PHP_METHOD(ExcimerLog, getSpeedscopeData) { ExcimerLog_obj *log_obj = EXCIMER_OBJ_ZP(ExcimerLog, getThis()); excimer_log_get_speedscope_data(&log_obj->log, return_value); } /* }}} */ /* {{{ proto string ExcimerLog::aggregateByFunction() */ static PHP_METHOD(ExcimerLog, aggregateByFunction) { ExcimerLog_obj *log_obj = EXCIMER_OBJ_ZP(ExcimerLog, getThis()); RETURN_ARR(excimer_log_aggr_by_func(&log_obj->log)); } /* }}} */ /* {{{ proto string ExcimerLog::getEventCount() */ static PHP_METHOD(ExcimerLog, getEventCount) { ExcimerLog_obj *log_obj = EXCIMER_OBJ_ZP(ExcimerLog, getThis()); RETURN_LONG(log_obj->log.event_count); } /* }}} */ /* {{{ proto array ExcimerLog::current() */ static PHP_METHOD(ExcimerLog, current) { ExcimerLog_obj * log_obj = EXCIMER_OBJ_ZP(ExcimerLog, getThis()); ZEND_PARSE_PARAMETERS_START(0, 0); ZEND_PARSE_PARAMETERS_END(); if (Z_ISNULL(log_obj->z_current) && log_obj->iter_entry_index < log_obj->log.entries_size) { ExcimerLog_init_entry(&log_obj->z_current, getThis(), log_obj->iter_entry_index); } RETURN_ZVAL(&log_obj->z_current, 1, 0); } /* }}} */ /* {{{ proto int ExcimerLog::key() */ static PHP_METHOD(ExcimerLog, key) { ExcimerLog_obj * log_obj = EXCIMER_OBJ_ZP(ExcimerLog, getThis()); ZEND_PARSE_PARAMETERS_START(0, 0); ZEND_PARSE_PARAMETERS_END(); if (log_obj->iter_entry_index < log_obj->log.entries_size) { RETURN_LONG(log_obj->iter_entry_index); } else { RETURN_NULL(); } } /* }}} */ /* {{{ proto void ExcimerLog::next() */ static PHP_METHOD(ExcimerLog, next) { ExcimerLog_obj * log_obj = EXCIMER_OBJ_ZP(ExcimerLog, getThis()); ZEND_PARSE_PARAMETERS_START(0, 0); ZEND_PARSE_PARAMETERS_END(); zval_ptr_dtor(&log_obj->z_current); ZVAL_NULL(&log_obj->z_current); if (log_obj->iter_entry_index < log_obj->log.entries_size) { log_obj->iter_entry_index++; } } /* }}} */ /* {{{ proto void ExcimerLog::rewind() */ static PHP_METHOD(ExcimerLog, rewind) { ExcimerLog_obj * log_obj = EXCIMER_OBJ_ZP(ExcimerLog, getThis()); ZEND_PARSE_PARAMETERS_START(0, 0); ZEND_PARSE_PARAMETERS_END(); log_obj->iter_entry_index = 0; zval_ptr_dtor(&log_obj->z_current); ZVAL_NULL(&log_obj->z_current); } /* }}} */ /* {{{ proto bool ExcimerLog::valid() */ static PHP_METHOD(ExcimerLog, valid) { ExcimerLog_obj * log_obj = EXCIMER_OBJ_ZP(ExcimerLog, getThis()); ZEND_PARSE_PARAMETERS_START(0, 0); ZEND_PARSE_PARAMETERS_END(); if (log_obj->iter_entry_index < log_obj->log.entries_size) { RETURN_TRUE; } else { RETURN_FALSE; } } /* }}} */ /* {{{ proto int ExcimerLog::count() */ static PHP_METHOD(ExcimerLog, count) { ExcimerLog_obj *log_obj = EXCIMER_OBJ_ZP(ExcimerLog, getThis()); ZEND_PARSE_PARAMETERS_START(0, 0); ZEND_PARSE_PARAMETERS_END(); RETURN_LONG(log_obj->log.entries_size); } /* }}} */ /* {{{ proto bool ExcimerLog::offsetExists(mixed offset) */ static PHP_METHOD(ExcimerLog, offsetExists) { ExcimerLog_obj *log_obj = EXCIMER_OBJ_ZP(ExcimerLog, getThis()); zend_long offset; ZEND_PARSE_PARAMETERS_START(1, 1); Z_PARAM_LONG(offset) ZEND_PARSE_PARAMETERS_END(); if (offset >= 0 && offset < log_obj->log.entries_size) { RETURN_TRUE; } else { RETURN_FALSE; } } /* }}} */ /* {{{ proto mixed ExcimerLog::offsetGet(mixed offset) */ static PHP_METHOD(ExcimerLog, offsetGet) { ExcimerLog_obj *log_obj = EXCIMER_OBJ_ZP(ExcimerLog, getThis()); zend_long offset; ZEND_PARSE_PARAMETERS_START(1, 1); Z_PARAM_LONG(offset) ZEND_PARSE_PARAMETERS_END(); if (offset < 0 || offset >= log_obj->log.entries_size) { RETURN_NULL(); } ExcimerLog_init_entry(return_value, getThis(), offset); } /* }}} */ /* {{{ proto void ExcimerLog::offsetSet(mixed offset, mixed value) */ static PHP_METHOD(ExcimerLog, offsetSet) { php_error_docref(NULL, E_WARNING, "ExcimerLog cannot be modified"); } /* }}} */ /* {{{ proto void ExcimerLog::offsetUnset(mixed offset) */ static PHP_METHOD(ExcimerLog, offsetUnset) { php_error_docref(NULL, E_WARNING, "ExcimerLog cannot be modified"); } /* }}} */ static zend_object *ExcimerLogEntry_new(zend_class_entry *ce) /* {{{ */ { ExcimerLogEntry_obj *entry_obj = EXCIMER_NEW_OBJECT(ExcimerLogEntry, ce); ZVAL_NULL(&entry_obj->z_log); entry_obj->index = 0; return &entry_obj->std; } /* }}} */ static void ExcimerLogEntry_free_object(zend_object *object) /* {{{ */ { ExcimerLogEntry_obj *entry_obj = EXCIMER_OBJ(ExcimerLogEntry, object); zval_ptr_dtor(&entry_obj->z_log); ZVAL_UNDEF(&entry_obj->z_log); zend_object_std_dtor(object); } /* }}} */ /* {{{ proto void ExcimerLogEntry::__construct() */ static PHP_METHOD(ExcimerLogEntry, __construct) { php_error_docref(NULL, E_ERROR, "ExcimerLogEntry cannot be constructed directly"); } /* }}} */ /* {{{ proto float ExcimerLogEntry::getTimestamp() */ static PHP_METHOD(ExcimerLogEntry, getTimestamp) { ExcimerLogEntry_obj *entry_obj = EXCIMER_OBJ_ZP(ExcimerLogEntry, getThis()); ExcimerLog_obj *log_obj = EXCIMER_OBJ_ZP(ExcimerLog, &entry_obj->z_log); excimer_log_entry *entry = excimer_log_get_entry(&log_obj->log, entry_obj->index); ZEND_PARSE_PARAMETERS_START(0, 0); ZEND_PARSE_PARAMETERS_END(); RETURN_DOUBLE((entry->timestamp - log_obj->log.epoch) / 1e9); } /* }}} */ /* {{{ proto float ExcimerLogEntry::getEventCount() */ static PHP_METHOD(ExcimerLogEntry, getEventCount) { ExcimerLogEntry_obj *entry_obj = EXCIMER_OBJ_ZP(ExcimerLogEntry, getThis()); ExcimerLog_obj *log_obj = EXCIMER_OBJ_ZP(ExcimerLog, &entry_obj->z_log); excimer_log_entry *entry = excimer_log_get_entry(&log_obj->log, entry_obj->index); ZEND_PARSE_PARAMETERS_START(0, 0); ZEND_PARSE_PARAMETERS_END(); RETURN_LONG(entry->event_count); } /* }}} */ /* {{{ proto array ExcimerLogEntry::getTrace() */ static PHP_METHOD(ExcimerLogEntry, getTrace) { ExcimerLogEntry_obj *entry_obj = EXCIMER_OBJ_ZP(ExcimerLogEntry, getThis()); ExcimerLog_obj *log_obj = EXCIMER_OBJ_ZP(ExcimerLog, &entry_obj->z_log); excimer_log_entry *entry = excimer_log_get_entry(&log_obj->log, entry_obj->index); ZEND_PARSE_PARAMETERS_START(0, 0); ZEND_PARSE_PARAMETERS_END(); RETURN_ARR(excimer_log_trace_to_array(&log_obj->log, entry->frame_index)); } /* }}} */ static zend_object *ExcimerTimer_new(zend_class_entry *ce) /* {{{ */ { ExcimerTimer_obj *timer_obj = EXCIMER_NEW_OBJECT(ExcimerTimer, ce); ZVAL_UNDEF(&timer_obj->z_callback); timer_obj->event_type = EXCIMER_REAL; timer_obj->need_reinit = 1; return &timer_obj->std; } /* }}} */ static void ExcimerTimer_free_object(zend_object *object) /* {{{ */ { ExcimerTimer_obj *timer_obj = EXCIMER_OBJ(ExcimerTimer, object); if (timer_obj->timer.is_valid) { excimer_timer_destroy(&timer_obj->timer); } zval_ptr_dtor(&timer_obj->z_callback); ZVAL_UNDEF(&timer_obj->z_callback); } /* }}} */ /* {{{ proto void ExcimerTimer::setEventType(int event_type) */ static PHP_METHOD(ExcimerTimer, setEventType) { ExcimerTimer_obj *timer_obj = EXCIMER_OBJ_ZP(ExcimerTimer, getThis()); zend_long event_type; ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_LONG(event_type) ZEND_PARSE_PARAMETERS_END(); if (event_type != EXCIMER_CPU && event_type != EXCIMER_REAL) { php_error_docref(NULL, E_WARNING, "Invalid event type"); return; } timer_obj->event_type = event_type; timer_obj->need_reinit = 1; } /* }}} */ /* {{{ proto void ExcimerTimer::setInterval(float interval) */ static PHP_METHOD(ExcimerTimer, setInterval) { ExcimerTimer_obj *timer_obj = EXCIMER_OBJ_ZP(ExcimerTimer, getThis()); double initial; ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_DOUBLE(initial) ZEND_PARSE_PARAMETERS_END(); timerlib_timespec_from_double(&timer_obj->initial, initial); } /* }}} */ /* {{{ proto void ExcimerTimer::setPeriod(float period) */ static PHP_METHOD(ExcimerTimer, setPeriod) { ExcimerTimer_obj *timer_obj = EXCIMER_OBJ_ZP(ExcimerTimer, getThis()); double period; ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_DOUBLE(period) ZEND_PARSE_PARAMETERS_END(); timerlib_timespec_from_double(&timer_obj->period, period); } /* }}} */ /* {{{ proto void ExcimerTimer::setCallback(callback callback) */ static PHP_METHOD(ExcimerTimer, setCallback) { ExcimerTimer_obj *timer_obj = EXCIMER_OBJ_ZP(ExcimerTimer, getThis()); zval *zp_callback; ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_ZVAL(zp_callback) ZEND_PARSE_PARAMETERS_END(); if (Z_TYPE_P(zp_callback) == IS_NULL) { zval_ptr_dtor(&timer_obj->z_callback); ZVAL_NULL(&timer_obj->z_callback); } else { ExcimerTimer_set_callback(timer_obj, zp_callback); } } /* }}} */ static int ExcimerTimer_set_callback(ExcimerTimer_obj *timer_obj, zval *zp_callback) /* {{{ */ { char *is_callable_error; if (!zend_is_callable_ex(zp_callback, NULL, 0, NULL, NULL, &is_callable_error)) { php_error_docref(NULL, E_WARNING, "timer callback is not callable: %s", is_callable_error); return FAILURE; } zval_ptr_dtor(&timer_obj->z_callback); ZVAL_COPY(&timer_obj->z_callback, zp_callback); return SUCCESS; } /* }}} */ /* {{{ proto void ExcimerTimer::start() */ static PHP_METHOD(ExcimerTimer, start) { ExcimerTimer_obj *timer_obj = EXCIMER_OBJ_ZP(ExcimerTimer, getThis()); ZEND_PARSE_PARAMETERS_START(0, 0); ZEND_PARSE_PARAMETERS_END(); if (timer_obj->timer.is_running) { ExcimerTimer_stop(timer_obj); } ExcimerTimer_start(timer_obj); } /* }}} */ /* {{{ proto void ExcimerTimer::stop() */ static PHP_METHOD(ExcimerTimer, stop) { ExcimerTimer_obj *timer_obj = EXCIMER_OBJ_ZP(ExcimerTimer, getThis()); ZEND_PARSE_PARAMETERS_START(0, 0); ZEND_PARSE_PARAMETERS_END(); ExcimerTimer_stop(timer_obj); } /* }}} */ /* {{{ proto float ExcimerTimer::getTime() */ static PHP_METHOD(ExcimerTimer, getTime) { ExcimerTimer_obj *timer_obj = EXCIMER_OBJ_ZP(ExcimerTimer, getThis()); struct timespec ts; ZEND_PARSE_PARAMETERS_START(0, 0); ZEND_PARSE_PARAMETERS_END(); excimer_timer_get_time(&timer_obj->timer, &ts); RETURN_DOUBLE(timerlib_timespec_to_double(&ts)); } /* }}} */ static void ExcimerTimer_start(ExcimerTimer_obj *timer_obj) /* {{{ */ { if (timer_obj->need_reinit || !timer_obj->timer.is_valid) { if (timer_obj->timer.is_valid) { excimer_timer_destroy(&timer_obj->timer); } if (excimer_timer_init(&timer_obj->timer, timer_obj->event_type, ExcimerTimer_event, (void*)timer_obj) == FAILURE) { /* Error message already sent */ return; } timer_obj->need_reinit = 0; } excimer_timer_start(&timer_obj->timer, &timer_obj->period, &timer_obj->initial); } /* }}} */ static void ExcimerTimer_stop(ExcimerTimer_obj *timer_obj) /* {{{ */ { if (timer_obj->timer.is_valid) { excimer_timer_stop(&timer_obj->timer); } } /* }}} */ static void ExcimerTimer_event(zend_long event_count, void *user_data) /* {{{ */ { ExcimerTimer_obj *timer_obj = (ExcimerTimer_obj*)user_data; zend_fcall_info fci; zend_fcall_info_cache fcc; zval retval; zval z_event_count; char *is_callable_error; if (Z_ISNULL(timer_obj->z_callback) || Z_ISUNDEF(timer_obj->z_callback)) { return; } if (zend_fcall_info_init(&timer_obj->z_callback, 0, &fci, &fcc, NULL, &is_callable_error) != SUCCESS) { php_error(E_WARNING, "ExcimerTimer callback is not callable (during event): %s", is_callable_error); ExcimerTimer_stop(timer_obj); return; } fci.retval = &retval; ZVAL_LONG(&z_event_count, event_count); zend_fcall_info_argn(&fci, 1, &z_event_count); if (zend_call_function(&fci, &fcc) == SUCCESS) { zval_ptr_dtor(&retval); } zend_fcall_info_args_clear(&fci, 1); } /* }}} */ /* {{{ proto ExcimerTimer excimer_set_timeout(callable callback, float interval) */ PHP_FUNCTION(excimer_set_timeout) { ExcimerTimer_obj *timer_obj; zval * zp_callback; double initial; ZEND_PARSE_PARAMETERS_START(2, 2) Z_PARAM_ZVAL(zp_callback) Z_PARAM_DOUBLE(initial) ZEND_PARSE_PARAMETERS_END(); object_init_ex(return_value, ExcimerTimer_ce); timer_obj = EXCIMER_OBJ_ZP(ExcimerTimer, return_value); if (ExcimerTimer_set_callback(timer_obj, zp_callback) == FAILURE) { zval_ptr_dtor(return_value); ZVAL_NULL(return_value); } timerlib_timespec_from_double(&timer_obj->initial, initial); ExcimerTimer_start(timer_obj); } /* }}} */ static const zend_module_dep excimer_deps[] = { #if PHP_VERSION_ID < 70200 ZEND_MOD_REQUIRED("spl") #endif ZEND_MOD_END }; /* {{{ excimer_module_entry */ zend_module_entry excimer_module_entry = { STANDARD_MODULE_HEADER_EX, NULL, excimer_deps, "excimer", excimer_functions, PHP_MINIT(excimer), PHP_MSHUTDOWN(excimer), PHP_RINIT(excimer), NULL, /* RSHUTDOWN */ PHP_MINFO(excimer), PHP_EXCIMER_VERSION, NO_MODULE_GLOBALS, ZEND_MODULE_POST_ZEND_DEACTIVATE_N(excimer), STANDARD_MODULE_PROPERTIES_EX }; /* }}} */ #ifdef COMPILE_DL_EXCIMER #ifdef ZTS ZEND_TSRMLS_CACHE_DEFINE() #endif ZEND_GET_MODULE(excimer) #endif /* * Local variables: * tab-width: 4 * c-basic-offset: 4 * End: * vim600: noet sw=4 ts=4 fdm=marker * vim<600: noet sw=4 ts=4 */ excimer-1.2.5/excimer_events.h0000664000175000017500000000140015012753166017147 0ustar tstarlingtstarling/* Copyright 2024 Wikimedia Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #ifndef EXCIMER_EVENTS_H #define EXCIMER_EVENTS_H enum { /** Event type: real, wall-clock time */ EXCIMER_REAL, /** Event type: CPU time */ EXCIMER_CPU }; #endifexcimer-1.2.5/excimer_log.c0000664000175000017500000005262715012753166016440 0ustar tstarlingtstarling/* Copyright 2018 Wikimedia Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "php.h" #include "Zend/zend_smart_str.h" #include "php_excimer.h" #include "excimer_log.h" static const char excimer_log_truncated_name[] = "excimer_truncated"; static const char excimer_log_fake_filename[] = "excimer fake file"; static uint32_t excimer_log_find_or_add_frame(excimer_log *log, zend_execute_data *execute_data, zend_long depth); /* {{{ Compatibility functions and macros */ #if PHP_VERSION_ID >= 70300 #define excimer_log_new_array zend_new_array #else static inline HashTable *excimer_log_new_array(uint32_t nSize) { HashTable *ht = emalloc(sizeof(HashTable)); zend_hash_init(ht, nSize, NULL, ZVAL_PTR_DTOR, 0); return ht; } #endif #if PHP_VERSION_ID >= 70200 #define excimer_log_smart_str_get_len smart_str_get_len #define excimer_log_smart_str_extract smart_str_extract #define excimer_log_smart_str_append_printf smart_str_append_printf #define excimer_log_known_string ZSTR_KNOWN #else static inline size_t excimer_log_smart_str_get_len(smart_str *str) { return str->s ? ZSTR_LEN(str->s) : 0; } static inline zend_string *excimer_log_smart_str_extract(smart_str *str) { if (str->s) { zend_string *res; smart_str_0(str); res = str->s; str->s = NULL; return res; } else { return ZSTR_EMPTY_ALLOC(); } } static void excimer_log_smart_str_append_printf(smart_str *dest, const char *format, ...) { va_list arg; size_t len; char *buf; va_start(arg, format); len = vspprintf(&buf, 0, format, arg); va_end(arg); smart_str_appendl(dest, buf, len); efree(buf); } #define excimer_log_known_string(index) CG(known_strings)[index] #endif #if PHP_VERSION_ID >= 80100 #define excimer_log_add_assoc_array add_assoc_array #else static inline void excimer_log_add_assoc_array(zval *dest, const char *key, HashTable *arr) { zval z_tmp; ZVAL_ARR(&z_tmp, arr); add_assoc_zval(dest, key, &z_tmp); } #endif /* }}} */ void excimer_log_init(excimer_log *log) { log->entries_size = 0; log->entries = NULL; log->frames = ecalloc(1, sizeof(excimer_log_frame)); log->frames_size = 1; log->reverse_frames = excimer_log_new_array(0); log->epoch = 0; log->event_count = 0; } void excimer_log_destroy(excimer_log *log) { if (log->entries) { efree(log->entries); } if (log->frames) { int i; for (i = 0; i < log->frames_size; i++) { if (log->frames[i].filename) { zend_string_delref(log->frames[i].filename); } if (log->frames[i].class_name) { zend_string_delref(log->frames[i].class_name); } if (log->frames[i].function_name) { zend_string_delref(log->frames[i].function_name); } } efree(log->frames); } zend_hash_destroy(log->reverse_frames); efree(log->reverse_frames); } void excimer_log_set_max_depth(excimer_log *log, zend_long depth) { log->max_depth = depth; } void excimer_log_copy_options(excimer_log *dest, excimer_log *src) { dest->max_depth = src->max_depth; dest->epoch = src->epoch; dest->period = src->period; } void excimer_log_add(excimer_log *log, zend_execute_data *execute_data, zend_long event_count, uint64_t timestamp) { uint32_t frame_index = excimer_log_find_or_add_frame(log, execute_data, 0); excimer_log_entry *entry; log->entries = safe_erealloc(log->entries, log->entries_size + 1, sizeof(excimer_log_entry), 0); entry = &log->entries[log->entries_size++]; entry->frame_index = frame_index; entry->event_count = event_count; log->event_count += event_count; entry->timestamp = timestamp; } static uint32_t excimer_log_get_truncation_marker(excimer_log *log) { zval* zp_index; zval z_new_index; excimer_log_frame *p_frame; zp_index = zend_hash_str_find(log->reverse_frames, excimer_log_truncated_name, sizeof(excimer_log_truncated_name) - 1); if (zp_index) { return excimer_safe_uint32(Z_LVAL_P(zp_index)); } ZVAL_LONG(&z_new_index, log->frames_size); zend_hash_str_add(log->reverse_frames, excimer_log_truncated_name, sizeof(excimer_log_truncated_name) - 1, &z_new_index); log->frames = safe_erealloc(log->frames, log->frames_size + 1, sizeof(excimer_log_frame), 0); p_frame = &log->frames[log->frames_size++]; p_frame->filename = zend_string_init(excimer_log_fake_filename, sizeof(excimer_log_fake_filename) - 1, 0); p_frame->lineno = 1; p_frame->closure_line = 0; p_frame->class_name = NULL; p_frame->function_name = zend_string_init(excimer_log_truncated_name, sizeof(excimer_log_truncated_name) - 1, 0); p_frame->prev_index = 0; return excimer_safe_uint32(Z_LVAL(z_new_index)); } static uint32_t excimer_log_find_or_add_frame(excimer_log *log, zend_execute_data *execute_data, zend_long depth) { uint32_t prev_index; if (!execute_data) { return 0; } else if (!execute_data->prev_execute_data) { prev_index = 0; } else if (log->max_depth && depth >= log->max_depth) { prev_index = excimer_log_get_truncation_marker(log); } else { prev_index = excimer_log_find_or_add_frame(log, execute_data->prev_execute_data, depth + 1); } if (!execute_data->func || !ZEND_USER_CODE(execute_data->func->common.type)) { return prev_index; } else { zend_function *func = execute_data->func; excimer_log_frame frame = {NULL}; smart_str ss_key = {NULL}; zend_string *str_key; zval* zp_index; frame.filename = func->op_array.filename; zend_string_addref(frame.filename); if (func->common.scope && func->common.scope->name) { frame.class_name = func->common.scope->name; zend_string_addref(frame.class_name); } if (func->common.function_name) { frame.function_name = func->common.function_name; zend_string_addref(frame.function_name); } if (func->op_array.fn_flags & ZEND_ACC_CLOSURE) { frame.closure_line = func->op_array.line_start; } frame.lineno = execute_data->opline->lineno; frame.prev_index = prev_index; /* Make a key for reverse lookup */ smart_str_append(&ss_key, frame.filename); smart_str_appendc(&ss_key, '\0'); excimer_log_smart_str_append_printf(&ss_key, "%d", frame.lineno); smart_str_appendc(&ss_key, '\0'); excimer_log_smart_str_append_printf(&ss_key, "%d", frame.prev_index); str_key = excimer_log_smart_str_extract(&ss_key); /* Look for a matching frame in the reverse hashtable */ zp_index = zend_hash_find(log->reverse_frames, str_key); if (zp_index) { zend_string_free(str_key); zend_string_delref(frame.filename); if (frame.class_name) { zend_string_delref(frame.class_name); } if (frame.function_name) { zend_string_delref(frame.function_name); } return excimer_safe_uint32(Z_LVAL_P(zp_index)); } else { zval z_new_index; /* Create a new entry in the array and reverse hashtable */ ZVAL_LONG(&z_new_index, log->frames_size); zend_hash_add(log->reverse_frames, str_key, &z_new_index); log->frames = safe_erealloc(log->frames, log->frames_size + 1, sizeof(excimer_log_frame), 0); memcpy(&log->frames[log->frames_size++], &frame, sizeof(excimer_log_frame)); zend_string_delref(str_key); return excimer_safe_uint32(Z_LVAL(z_new_index)); } } } zend_long excimer_log_get_size(excimer_log *log) { return log->entries_size; } excimer_log_entry *excimer_log_get_entry(excimer_log *log, zend_long i) { if (i >= 0 && i < log->entries_size) { return &log->entries[i]; } else { return NULL; } } excimer_log_frame *excimer_log_get_frame(excimer_log *log, zend_long i) { if (i > 0 && i < log->frames_size) { return &log->frames[i]; } else { return NULL; } } static void excimer_log_append_no_spaces(smart_str *dest, zend_string *src) { size_t new_len = smart_str_alloc(dest, ZSTR_LEN(src), 0); size_t prev_len = ZSTR_LEN(dest->s); size_t i; for (i = 0; i < ZSTR_LEN(src); i++) { char c = ZSTR_VAL(src)[i]; if (c == ' ' || c == '\0') { c = '_'; } ZSTR_VAL(dest->s)[prev_len + i] = c; } ZSTR_LEN(dest->s) = new_len; } static void excimer_log_append_frame_name(smart_str *ss, excimer_log_frame *frame) { if (frame->closure_line != 0) { /* Annotate anonymous functions with their source location. * Example: {closure:/path/to/file.php(123)} */ smart_str_appends(ss, "{closure:"); excimer_log_append_no_spaces(ss, frame->filename); excimer_log_smart_str_append_printf(ss, "(%d)}", frame->closure_line); } else if (frame->function_name == NULL) { /* For file-scope code, use the file name */ excimer_log_append_no_spaces(ss, frame->filename); } else { if (frame->class_name) { excimer_log_append_no_spaces(ss, frame->class_name); smart_str_appends(ss, "::"); } excimer_log_append_no_spaces(ss, frame->function_name); } } zend_string *excimer_log_format_collapsed(excimer_log *log) { zend_long entry_index; zend_long frame_index; zval *zp_count; zval z_count; smart_str ss_out = {NULL}; HashTable frame_counts_storage, lines_storage; HashTable *ht_frame_counts, *ht_lines; ht_frame_counts = &frame_counts_storage; memset(ht_frame_counts, 0, sizeof(HashTable)); zend_hash_init(ht_frame_counts, 0, NULL, NULL, 0); ht_lines = &lines_storage; memset(ht_lines, 0, sizeof(HashTable)); zend_hash_init(ht_lines, 0, NULL, NULL, 0); excimer_log_frame ** frame_ptrs = NULL; size_t frames_capacity = 0; zend_string *str_line; /* Collate frame counts */ for (entry_index = 0; entry_index < log->entries_size; entry_index++) { excimer_log_entry *entry = excimer_log_get_entry(log, entry_index); zp_count = zend_hash_index_find(ht_frame_counts, entry->frame_index); if (!zp_count) { ZVAL_LONG(&z_count, 0); zp_count = zend_hash_index_add(ht_frame_counts, entry->frame_index, &z_count); } Z_LVAL_P(zp_count) += entry->event_count; } /* Format traces, and deduplicate frames that differ only in hidden line numbers */ ZEND_HASH_FOREACH_NUM_KEY_VAL(ht_frame_counts, frame_index, zp_count) { zend_long current_frame_index = frame_index; zend_long num_frames = 0; excimer_log_frame *frame; zend_long i; int line_start = 1; /* TODO use bool when PHP 7.4 support is dropped */ smart_str ss_line = {NULL}; /* Build the array of frame pointers */ while (current_frame_index) { frame = excimer_log_get_frame(log, current_frame_index); if (num_frames >= frames_capacity) { if (frames_capacity >= ZEND_LONG_MAX - 1) { /* Probably unreachable */ zend_error_noreturn(E_ERROR, "Too many Excimer frames"); } frames_capacity++; frame_ptrs = safe_erealloc(frame_ptrs, frames_capacity, sizeof(*frame_ptrs), 0); } frame_ptrs[num_frames++] = frame; current_frame_index = frame->prev_index; } /* Run through the array in reverse */ for (i = num_frames - 1; i >= 0; i--) { frame = frame_ptrs[i]; if (line_start) { line_start = 0; } else { smart_str_appends(&ss_line, ";"); } excimer_log_append_frame_name(&ss_line, frame); } /* ht_lines[ss_line] += zp_count */ str_line = excimer_log_smart_str_extract(&ss_line); zval *zp_line_count = zend_hash_find(ht_lines, str_line); if (!zp_line_count) { ZVAL_LONG(&z_count, 0); zp_line_count = zend_hash_add(ht_lines, str_line, &z_count); } Z_LVAL_P(zp_line_count) += Z_LVAL_P(zp_count); } ZEND_HASH_FOREACH_END(); /* Concatenate lines */ ZEND_HASH_FOREACH_STR_KEY_VAL(ht_lines, str_line, zp_count) { smart_str_append(&ss_out, str_line); excimer_log_smart_str_append_printf(&ss_out, " " ZEND_LONG_FMT "\n", Z_LVAL_P(zp_count)); } ZEND_HASH_FOREACH_END(); zend_hash_destroy(ht_frame_counts); zend_hash_destroy(ht_lines); efree(frame_ptrs); return excimer_log_smart_str_extract(&ss_out); } static HashTable *excimer_log_frame_to_speedscope_array(excimer_log_frame *frame) { HashTable *ht_func = excimer_log_new_array(0); zval tmp; smart_str ss_name = {NULL}; excimer_log_append_frame_name(&ss_name, frame); ZVAL_STR(&tmp, excimer_log_smart_str_extract(&ss_name)); zend_hash_str_add(ht_func, "name", sizeof("name")-1, &tmp); if (frame->filename) { ZVAL_STR_COPY(&tmp, frame->filename); zend_hash_add_new(ht_func, excimer_log_known_string(ZEND_STR_FILE), &tmp); /* Don't include the line number since it causes speedscope to split functions */ } return ht_func; } static zend_string *excimer_log_get_speedscope_frame_key(excimer_log_frame *frame) { smart_str ss = {NULL}; excimer_log_append_frame_name(&ss, frame); smart_str_appendc(&ss, '\0'); smart_str_append(&ss, frame->filename); return excimer_log_smart_str_extract(&ss); } static uint32_t excimer_log_count_frames(excimer_log *log, uint32_t frame_index) { uint32_t n = 0; while (frame_index) { n++; frame_index = log->frames[frame_index].prev_index; } return n; } void excimer_log_get_speedscope_data(excimer_log *log, zval *zp_data) { array_init(zp_data); add_assoc_string(zp_data, "$schema", "https://www.speedscope.app/file-format-schema.json"); add_assoc_string(zp_data, "exporter", "Excimer"); HashTable *ht_frames = excimer_log_new_array(0); HashTable *ht_indexes_by_key = excimer_log_new_array(0); zend_long *lp_frame_indexes = ecalloc(log->frames_size, sizeof(zend_long)); zend_long i; zval *zp_frame_index; zend_string *str_key; zval z_tmp, *zp_tmp; /* Build the frames array */ for (i = 1; i < log->frames_size; i++) { zend_long index; excimer_log_frame *frame = &log->frames[i]; str_key = excimer_log_get_speedscope_frame_key(frame); zp_frame_index = zend_hash_find(ht_indexes_by_key, str_key); if (!zp_frame_index) { /* Add the frame to ht_frames */ index = zend_hash_num_elements(ht_frames); ZVAL_ARR(&z_tmp, excimer_log_frame_to_speedscope_array(frame)); zend_hash_next_index_insert_new(ht_frames, &z_tmp); /* Add the frame index to ht_indexes_by_key */ ZVAL_LONG(&z_tmp, index); zp_frame_index = zend_hash_add_new(ht_indexes_by_key, str_key, &z_tmp); } lp_frame_indexes[i] = Z_LVAL_P(zp_frame_index); } /* zp_data["shared"] = ["frames" => ht_frames] */ zval z_shared; array_init(&z_shared); excimer_log_add_assoc_array(&z_shared, "frames", ht_frames); add_assoc_zval(zp_data, "shared", &z_shared); /* Build the samples and weights arrays */ HashTable *ht_samples = excimer_log_new_array(log->entries_size); HashTable *ht_weights = excimer_log_new_array(log->entries_size); uint64_t first_timestamp = 0; uint64_t last_timestamp = 0; for (i = 0; i < log->entries_size; i++) { excimer_log_entry *entry = &log->entries[i]; uint32_t frame_index = entry->frame_index; if (i == 0) { first_timestamp = entry->timestamp; } last_timestamp = entry->timestamp; uint32_t num_frames = excimer_log_count_frames(log, frame_index); uint32_t j; /* Create the array with ZEND_HASH_FILL_PACKED. This is just a fast way * to get it into the right state, with num_frames elements. */ HashTable *ht_stack = excimer_log_new_array(num_frames); zend_hash_extend(ht_stack, num_frames, 1); ZEND_HASH_FILL_PACKED(ht_stack) { #if PHP_VERSION_ID < 70400 zval new_val; ZVAL_LONG(&new_val, 0); for (j = 0; j < num_frames; j++) { ZEND_HASH_FILL_ADD(&new_val); } #else for (j = 0; j < num_frames; j++) { ZEND_HASH_FILL_SET_LONG(0); ZEND_HASH_FILL_NEXT(); } #endif } ZEND_HASH_FILL_END(); /* Write the values in reverse order */ ZEND_HASH_REVERSE_FOREACH_VAL(ht_stack, zp_tmp) { ZVAL_LONG(zp_tmp, lp_frame_indexes[frame_index]); frame_index = log->frames[frame_index].prev_index; } ZEND_HASH_FOREACH_END(); ZVAL_ARR(&z_tmp, ht_stack); zend_hash_next_index_insert_new(ht_samples, &z_tmp); ZVAL_LONG(&z_tmp, entry->event_count * log->period); zend_hash_next_index_insert_new(ht_weights, &z_tmp); } /* Build the profile array */ zval z_profile; array_init(&z_profile); add_assoc_string(&z_profile, "type", "sampled"); add_assoc_string(&z_profile, "name", ""); add_assoc_string(&z_profile, "unit", "nanoseconds"); add_assoc_long(&z_profile, "startValue", 0); add_assoc_long(&z_profile, "endValue", last_timestamp - first_timestamp); excimer_log_add_assoc_array(&z_profile, "samples", ht_samples); excimer_log_add_assoc_array(&z_profile, "weights", ht_weights); /* zp_data["profiles"] = [profile] */ zval z_profiles; array_init(&z_profiles); add_next_index_zval(&z_profiles, &z_profile); add_assoc_zval(zp_data, "profiles", &z_profiles); efree(lp_frame_indexes); } HashTable *excimer_log_frame_to_array(excimer_log_frame *frame) { HashTable *ht_func = excimer_log_new_array(0); zval tmp; if (frame->filename) { ZVAL_STR_COPY(&tmp, frame->filename); zend_hash_add_new(ht_func, excimer_log_known_string(ZEND_STR_FILE), &tmp); ZVAL_LONG(&tmp, frame->lineno); zend_hash_add_new(ht_func, excimer_log_known_string(ZEND_STR_LINE), &tmp); } if (frame->class_name) { ZVAL_STR_COPY(&tmp, frame->class_name); zend_hash_add_new(ht_func, excimer_log_known_string(ZEND_STR_CLASS), &tmp); } if (frame->function_name) { ZVAL_STR_COPY(&tmp, frame->function_name); zend_hash_add_new(ht_func, excimer_log_known_string(ZEND_STR_FUNCTION), &tmp); } if (frame->closure_line) { zend_string *s = zend_string_init("closure_line", sizeof("closure_line") - 1, 0); ZVAL_LONG(&tmp, frame->closure_line); zend_hash_add_new(ht_func, s, &tmp); zend_string_delref(s); } return ht_func; } HashTable *excimer_log_trace_to_array(excimer_log *log, zend_long l_frame_index) { HashTable *ht_trace = excimer_log_new_array(0); uint32_t frame_index = excimer_safe_uint32(l_frame_index); while (frame_index) { excimer_log_frame *frame = excimer_log_get_frame(log, frame_index); HashTable *ht_func = excimer_log_frame_to_array(frame); zval tmp; ZVAL_ARR(&tmp, ht_func); zend_hash_next_index_insert(ht_trace, &tmp); frame_index = frame->prev_index; } return ht_trace; } /** * ht[key] += term; */ static void excimer_log_array_incr(HashTable *ht, zend_string *sp_key, zend_long term) { zval *zp_value = zend_hash_find(ht, sp_key); if (!zp_value) { zval z_tmp; ZVAL_LONG(&z_tmp, term); zend_hash_add_new(ht, sp_key, &z_tmp); } else { Z_LVAL_P(zp_value) += term; } } #if PHP_VERSION_ID < 80000 static int excimer_log_aggr_compare(const void *a, const void *b) { zval *zp_a = &((Bucket*)a)->val; zval *zp_b = &((Bucket*)b)->val; #else static int excimer_log_aggr_compare(Bucket *a, Bucket *b) { zval *zp_a = &a->val; zval *zp_b = &b->val; #endif zval *zp_a_incl = zend_hash_str_find(Z_ARRVAL_P(zp_a), "inclusive", sizeof("inclusive")-1); zval *zp_b_incl = zend_hash_str_find(Z_ARRVAL_P(zp_b), "inclusive", sizeof("inclusive")-1); return ZEND_NORMALIZE_BOOL(Z_LVAL_P(zp_b_incl) - Z_LVAL_P(zp_a_incl)); } HashTable *excimer_log_aggr_by_func(excimer_log *log) { HashTable *ht_result = excimer_log_new_array(0); zend_string *sp_inclusive = zend_string_init("inclusive", sizeof("inclusive")-1, 0); zend_string *sp_self = zend_string_init("self", sizeof("self")-1, 0); HashTable *ht_unique_names = excimer_log_new_array(0); size_t entry_index; zval z_zero; ZVAL_LONG(&z_zero, 0); for (entry_index = 0; entry_index < log->entries_size; entry_index++) { excimer_log_entry *entry = excimer_log_get_entry(log, entry_index); uint32_t frame_index = entry->frame_index; int is_top = 1; while (frame_index) { excimer_log_frame *frame = excimer_log_get_frame(log, frame_index); smart_str ss_name = {NULL}; zend_string *sp_name; zval *zp_info; zval z_tmp; /* Make a human-readable name */ if (frame->closure_line != 0) { /* Annotate anonymous functions with their source location. * Example: {closure:/path/to/file.php(123)} */ smart_str_appends(&ss_name, "{closure:"); smart_str_append(&ss_name, frame->filename); excimer_log_smart_str_append_printf(&ss_name, "(%d)}", frame->closure_line); } else if (frame->function_name == NULL) { /* For file-scope code, use the file name */ smart_str_append(&ss_name, frame->filename); } else { if (frame->class_name) { smart_str_append(&ss_name, frame->class_name); smart_str_appends(&ss_name, "::"); } smart_str_append(&ss_name, frame->function_name); } sp_name = excimer_log_smart_str_extract(&ss_name); /* If it is not in ht_result, add it, along with frame info */ zp_info = zend_hash_find(ht_result, sp_name); if (!zp_info) { ZVAL_ARR(&z_tmp, excimer_log_frame_to_array(frame)); zend_hash_add_new(Z_ARRVAL(z_tmp), sp_self, &z_zero); zend_hash_add_new(Z_ARRVAL(z_tmp), sp_inclusive, &z_zero); zp_info = zend_hash_add(ht_result, sp_name, &z_tmp); } /* If this is the top frame of a log entry, increment the "self" key */ if (is_top) { excimer_log_array_incr(Z_ARRVAL_P(zp_info), sp_self, entry->event_count); } /* If this is the first instance of a function in an entry, i.e. * counting recursive functions only once, increment the "inclusive" key */ if (zend_hash_find(ht_unique_names, sp_name) == NULL) { excimer_log_array_incr(Z_ARRVAL_P(zp_info), sp_inclusive, entry->event_count); /* Add the function to the unique_names array */ zend_hash_add_new(ht_unique_names, sp_name, &z_zero); } is_top = 0; frame_index = frame->prev_index; zend_string_delref(sp_name); } zend_hash_clean(ht_unique_names); } zend_hash_destroy(ht_unique_names); zend_string_delref(sp_self); zend_string_delref(sp_inclusive); /* Sort the result in descending order by inclusive */ zend_hash_sort(ht_result, excimer_log_aggr_compare, 0); return ht_result; } excimer-1.2.5/excimer_log.h0000664000175000017500000001244215012753166016434 0ustar tstarlingtstarling/* Copyright 2018 Wikimedia Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #ifndef EXCIMER_LOG_H #define EXCIMER_LOG_H /** * Structure representing a unique location in the code and its backtrace */ typedef struct _excimer_log_frame { /** The filename, or may be fake e.g. "php shell code" */ zend_string *filename; /** The executing line number within the filename */ uint32_t lineno; /** * If the function was a closure, the "start line" of its definition. * Zero if the function was not a closure. */ uint32_t closure_line; /** The class name, or NULL if there was no class name. */ zend_string *class_name; /** * The function name, or NULL if there was no function name, or a * fake thing like "eval()'d code". */ zend_string *function_name; /** * The index within excimer_log.frames of the calling frame. */ uint32_t prev_index; } excimer_log_frame; /** * Structure representing a log entry */ typedef struct _excimer_log_entry { /** * The index within excimer_log.frames of the frame associated with this event. */ uint32_t frame_index; /** * The number of times the timer elapsed before the log entry was finally registered. */ zend_long event_count; /** * The wall clock time at which the event occurred. The interpretation is * caller-defined, but in Excimer it is the number of nanoseconds since boot. */ uint64_t timestamp; } excimer_log_entry; /** * Structure representing the entire log */ typedef struct _excimer_log { /** Array of log entries */ excimer_log_entry *entries; /** Size of the "entries" array */ size_t entries_size; /** Array of frames */ excimer_log_frame *frames; /* Size of the "frames" array */ size_t frames_size; /** * A hashtable where the key is a unique frame identifier combining some * elements of the frame object, and the value is the frame index. Used * for deduplication of frames. */ HashTable *reverse_frames; /** * The maximum stack depth of collected frames. If this is exceeded, the * backtrace is truncated. */ zend_long max_depth; /** * This is used by ExcimerProfiler to store the creation time of the * ExcimerProfiler object. */ uint64_t epoch; /** * The nominal period in nanoseconds */ uint64_t period; /** * The sum of the event counts of all contained log entries */ zend_long event_count; } excimer_log; /** * Initialise the log object. * * @param log Valid memory location at which to place the object */ void excimer_log_init(excimer_log *log); /** * Destroy the log object. This frees internal objects but does not free the * excimer_log itself. * * @param log The log object to destroy */ void excimer_log_destroy(excimer_log *log); /** * Set the max depth * * @param log The log object * @param depth The new depth */ void excimer_log_set_max_depth(excimer_log *log, zend_long depth); /** * Copy persistent options to another log. This is used during log rotation. * * @param dest The destination log object * @param src The source log object */ void excimer_log_copy_options(excimer_log *dest, excimer_log *src); /** * Add a log entry * * @param log The log object * @param execute_data The VM state * @param event_count The number of times the timer expired * @param timestamp The timestamp to store in the log entry */ void excimer_log_add(excimer_log *log, zend_execute_data *execute_data, zend_long event_count, uint64_t timestamp); /** * Get the number of entries in the log * * @param log The log object * @return The number of entries in the log */ zend_long excimer_log_get_size(excimer_log *log); /** * Get a log entry * * @param log The log object * @param i The index of the entry * @return The log entry, or NULL if the index is out of range */ excimer_log_entry *excimer_log_get_entry(excimer_log *log, zend_long i); /** * Get a frame by index * * @param log The log object * @param i The index * @return The frame, or NULL if the index is out of range */ excimer_log_frame *excimer_log_get_frame(excimer_log *log, zend_long i); /** * Format the log in flamegraph.pl collapsed format * * @param log The log object * @return A new zend_string owned by the caller */ zend_string *excimer_log_format_collapsed(excimer_log *log); /** * Get an array in speedscope format * * @param log The log object * @param zp_data The destination */ void excimer_log_get_speedscope_data(excimer_log *log, zval *zp_data); /** * Aggregate the log producing self/inclusive statistics as an array */ HashTable *excimer_log_aggr_by_func(excimer_log *log); /** * Convert a frame to a backtrace array for returning to the user * * @param log The log object * @param l_frame_index The frame index * @return A new hashtable, owned by the caller */ HashTable *excimer_log_trace_to_array(excimer_log *log, zend_long l_frame_index); #endif excimer-1.2.5/excimer_mutex.c0000664000175000017500000000264315012753166017012 0ustar tstarlingtstarling/* Copyright 2024 Wikimedia Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "excimer_mutex.h" #include "php.h" void excimer_mutex_init(pthread_mutex_t *mutex) { int result = pthread_mutex_init(mutex, NULL); if (result != 0) { zend_error_noreturn(E_ERROR, "pthread_mutex_init(): %s", strerror(result)); } } void excimer_mutex_lock(pthread_mutex_t *mutex) { int result = pthread_mutex_lock(mutex); if (result != 0) { fprintf(stderr, "pthread_mutex_lock(): %s", strerror(result)); abort(); } } void excimer_mutex_unlock(pthread_mutex_t *mutex) { int result = pthread_mutex_unlock(mutex); if (result != 0) { fprintf(stderr, "pthread_mutex_unlock(): %s", strerror(result)); abort(); } } void excimer_mutex_destroy(pthread_mutex_t *mutex) { int result = pthread_mutex_destroy(mutex); if (result != 0) { zend_error_noreturn(E_ERROR, "pthread_mutex_destroy(): %s", strerror(result)); } } excimer-1.2.5/excimer_mutex.h0000664000175000017500000000247415012753166017021 0ustar tstarlingtstarling/* Copyright 2024 Wikimedia Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #ifndef EXCIMER_MUTEX_H #define EXCIMER_MUTEX_H #include /** * Initialize the given mutex, raising a PHP Error in case of failure. * @param mutex The mutex to initialize. */ void excimer_mutex_init(pthread_mutex_t *mutex); /** * Lock the given mutex, aborting the process in case of failure. * @param mutex The mutex to lock. */ void excimer_mutex_lock(pthread_mutex_t *mutex); /** * Unlock the given mutex, aborting the process in case of failure. * @param mutex The mutex to unlock. */ void excimer_mutex_unlock(pthread_mutex_t *mutex); /** * Destroy the given mutex, raising a PHP Error in case of failure. * @param mutex The mutex to destroy. */ void excimer_mutex_destroy(pthread_mutex_t *mutex); #endif excimer-1.2.5/excimer_timer.c0000664000175000017500000002026215012753166016765 0ustar tstarlingtstarling/* Copyright 2018 Wikimedia Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include #include "php.h" #include "excimer_mutex.h" #include "excimer_timer.h" #include "zend_types.h" #if PHP_VERSION_ID >= 80200 #define excimer_timer_atomic_bool_store(dest, value) zend_atomic_bool_store(dest, value) #else #define excimer_timer_atomic_bool_store(dest, value) *dest = value #endif excimer_timer_globals_t excimer_timer_globals; ZEND_TLS excimer_timer_tls_t excimer_timer_tls; static void excimer_timer_handle(void * data, int overrun_count); static void excimer_timer_interrupt(zend_execute_data *execute_data); /** * Add a timer to the pending list. Unsynchronised, i.e. the caller is * responsible for locking the mutex if required. */ static void excimer_timer_list_enqueue(excimer_timer *timer) { excimer_timer **head_pp = &excimer_timer_tls.pending_head; if (!timer->pending_next) { if (*head_pp) { timer->pending_next = *head_pp; timer->pending_prev = (*head_pp)->pending_prev; (*head_pp)->pending_prev->pending_next = timer; (*head_pp)->pending_prev = timer; } else { *head_pp = timer; timer->pending_next = timer; timer->pending_prev = timer; } } } /** * Remove the first (FIFO) timer from the pending list and provide a pointer * to it. (unsynchronised) * * @param[out] timer_pp * @return True if a timer was returned, false if the list was empty */ static int excimer_timer_list_dequeue(excimer_timer **timer_pp) { excimer_timer **head_pp = &excimer_timer_tls.pending_head; if (*head_pp) { // Get the pending timer excimer_timer *timer = *timer_pp = *head_pp; if (timer->pending_next == timer) { // List is now empty *head_pp = NULL; } else { // Relink the head and neighbours timer->pending_next->pending_prev = timer->pending_prev; *head_pp = timer->pending_prev->pending_next = timer->pending_next; } // Unlink the timer being returned timer->pending_next = NULL; timer->pending_prev = NULL; return 1; } else { return 0; } } /** * Remove the specified timer from the pending list, if it is in there. If it * is not in the list, do nothing. (unsynchronised) */ static void excimer_timer_list_remove(excimer_timer *timer) { excimer_timer **head_pp = &excimer_timer_tls.pending_head; if (timer->pending_next) { if (timer->pending_next == timer) { *head_pp = NULL; } else { timer->pending_next->pending_prev = timer->pending_prev; timer->pending_prev->pending_next = timer->pending_next; if (*head_pp == timer) { *head_pp = timer->pending_next; } } timer->pending_next = NULL; timer->pending_prev = NULL; } } /** * Atomically dequeue a timer and get its event count at the time of removal * from the queue. The timer may be immediately re-added to the queue by the * event handler. * * @param[out] timer_pp Where to put the pointer to the timer * @param[out] event_count_p Where to put the event count * @return True if a timer was removed, false if the list was empty. */ static int excimer_timer_pending_dequeue(excimer_timer **timer_pp, zend_long *event_count_p) { excimer_mutex_lock(&excimer_timer_tls.mutex); int ret = excimer_timer_list_dequeue(timer_pp); if (ret) { *event_count_p = (*timer_pp)->event_count; (*timer_pp)->event_count = 0; } excimer_mutex_unlock(&excimer_timer_tls.mutex); return ret; } // Note: functions with external linkage are documented in the header void excimer_timer_module_init() { excimer_timer_globals.old_zend_interrupt_function = zend_interrupt_function; zend_interrupt_function = excimer_timer_interrupt; } void excimer_timer_module_shutdown() { } void excimer_timer_thread_init() { excimer_timer_tls = (excimer_timer_tls_t){ .mutex = PTHREAD_MUTEX_INITIALIZER }; } void excimer_timer_thread_shutdown() { if (excimer_timer_tls.timers_active) { // If this ever happens, it means we've got the logic wrong and we need // to rethink. It's very bad for timers to keep existing after thread // termination, because the mutex will be a dangling pointer. It's not // much help to avoid excimer_mutex_destroy() here because the whole TLS // segment will be destroyed and reused. php_error_docref(NULL, E_WARNING, "Timer still active at thread termination"); } else { excimer_mutex_destroy(&excimer_timer_tls.mutex); } } int excimer_timer_init(excimer_timer *timer, int event_type, excimer_timer_callback callback, void *user_data) { zval z_timer; memset(timer, 0, sizeof(excimer_timer)); ZVAL_PTR(&z_timer, timer); timer->vm_interrupt_ptr = &EG(vm_interrupt); timer->callback = callback; timer->user_data = user_data; timer->tls = &excimer_timer_tls; if (timerlib_timer_init(&timer->tl_timer, event_type, &excimer_timer_handle, timer) == FAILURE) { timerlib_timer_destroy(&timer->tl_timer); return FAILURE; } excimer_timer_tls.timers_active++; timer->is_valid = 1; timer->is_running = 0; return SUCCESS; } void excimer_timer_start(excimer_timer *timer, struct timespec *period, struct timespec *initial) { if (!timer->is_valid) { php_error_docref(NULL, E_WARNING, "Unable to start uninitialised timer" ); return; } /* If a periodic timer has an initial value of 0, use the period instead, * since it_value=0 means disarmed */ if (timerlib_timespec_is_zero(initial)) { initial = period; } /* If the value is still zero, flag an error */ if (timerlib_timespec_is_zero(initial)) { php_error_docref(NULL, E_WARNING, "Unable to start timer with a value of zero " "duration and period"); return; } if (timerlib_timer_start(&timer->tl_timer, period, initial) == SUCCESS) { timer->is_running = 1; } } void excimer_timer_stop(excimer_timer *timer) { if (!timer->is_valid) { php_error_docref(NULL, E_WARNING, "Unable to start uninitialised timer" ); return; } if (timer->is_running) { if (timerlib_timer_stop(&timer->tl_timer) == SUCCESS) { timer->is_running = 0; } } } void excimer_timer_destroy(excimer_timer *timer) { if (!timer->is_valid) { /* This could happen if the timer is manually destroyed after * excimer_timer_thread_shutdown() is called */ return; } if (timer->tls != &excimer_timer_tls) { php_error_docref(NULL, E_WARNING, "Cannot delete a timer belonging to a different thread"); return; } /* Stop the timer */ if (timer->is_running) { timer->is_running = 0; timerlib_timer_stop(&timer->tl_timer); } /* Destroy the timer. This will wait until any events are done. */ timerlib_timer_destroy(&timer->tl_timer); excimer_timer_tls.timers_active--; /* Remove the timer from the pending list */ excimer_mutex_lock(&excimer_timer_tls.mutex); excimer_timer_list_remove(timer); excimer_mutex_unlock(&excimer_timer_tls.mutex); timer->is_valid = 0; timer->tls = NULL; } static void excimer_timer_handle(void * data, int overrun_count) { excimer_timer *timer = (excimer_timer*)data; excimer_mutex_lock(&excimer_timer_tls.mutex); timer->event_count += overrun_count + 1; excimer_timer_list_enqueue(timer); excimer_mutex_unlock(&excimer_timer_tls.mutex); excimer_timer_atomic_bool_store(timer->vm_interrupt_ptr, 1); } static void excimer_timer_interrupt(zend_execute_data *execute_data) { excimer_timer *timer = NULL; zend_long count = 0; while (excimer_timer_pending_dequeue(&timer, &count)) { timer->callback(count, timer->user_data); } if (excimer_timer_globals.old_zend_interrupt_function) { excimer_timer_globals.old_zend_interrupt_function(execute_data); } } void excimer_timer_get_time(excimer_timer *timer, struct timespec *remaining) { if (!timer->is_valid || !timer->is_running) { remaining->tv_sec = 0; remaining->tv_nsec = 0; return; } timerlib_timer_get_time(&timer->tl_timer, remaining); } excimer-1.2.5/excimer_timer.h0000664000175000017500000001071315012753166016772 0ustar tstarlingtstarling/* Copyright 2018 Wikimedia Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #ifndef EXCIMER_TIMER_H #define EXCIMER_TIMER_H #include "excimer_events.h" #include "timerlib/timerlib.h" typedef void (*excimer_timer_callback)(zend_long, void *); /* Forward declaration */ typedef struct _excimer_timer_tls_t excimer_timer_tls_t; typedef struct _excimer_timer { /** True if the object has been initialised and not destroyed */ int is_valid; /** True if the timer has started */ int is_running; /** &EG(vm_interrupt) in the relevant thread */ #if PHP_VERSION_ID >= 80200 zend_atomic_bool *vm_interrupt_ptr; #else zend_bool *vm_interrupt_ptr; #endif /** The underlying timerlib timer */ timerlib_timer_t tl_timer; /** The event callback. */ excimer_timer_callback callback; /** The event callback user data */ void *user_data; /** * The next pending timer, in a circular doubly-linked list of pending * timers, or NULL if the timer is not in the list. */ struct _excimer_timer *pending_next; /** The previous pending timer */ struct _excimer_timer *pending_prev; zend_long event_count; /** The thread-local data associated with the thread that created the timer */ excimer_timer_tls_t *tls; } excimer_timer; typedef struct _excimer_timer_globals_t { /** * The old value of the zend_interrupt_function hook. If set, this must be * called to allow pcntl_signal() etc. to work. */ void (*old_zend_interrupt_function)(zend_execute_data *execute_data); } excimer_timer_globals_t; typedef struct _excimer_timer_tls_t { /** The mutex protecting the pending list */ pthread_mutex_t mutex; /** * The head of the list of pending timers. This is a doubly-linked list * because we need to randomly delete members when timers are destroyed. * It's circular, with the last element pointing back to the first element, * because that makes it a bit easier to check whether an element is in the * list. A circular list means that objects have a non-NULL prev/next if and * only if they are in the list. */ excimer_timer *pending_head; /** The number of active timers in this thread */ unsigned long timers_active; } excimer_timer_tls_t; /** * Global initialisation of the timer module */ void excimer_timer_module_init(); /** * Global shutdown of the timer module */ void excimer_timer_module_shutdown(); /** * Thread-local initialisation of the timer module. This must be called before * any timer objects are created. */ void excimer_timer_thread_init(); /** * Thread-local shutdown of the timer module. After calling this, * excimer_timer_thread_init() may be called again to reinitialise the module. */ void excimer_timer_thread_shutdown(); /** * Initialise a timer object allocated by the caller * * @param timer The timer object pointer * @param event_type May be EXCIMER_REAL or EXCIMER_CPU * @param callback The callback to call during VM interrupt * @param user_data An arbitrary pointer passed to the callback * @return SUCCESS or FAILURE */ int excimer_timer_init(excimer_timer *timer, int event_type, excimer_timer_callback callback, void *user_data); /** * Start a timer. If there is no error, timer->is_running will be set to 1. * * @param timer The timer object * @param period The period (it_interval) of the timer * @param initial The initial timer value (it_value) */ void excimer_timer_start(excimer_timer *timer, struct timespec *period, struct timespec *initial); /** * Stop a timer. If there is no error, timer->is_running will be set to 0. * * @param timer The timer object */ void excimer_timer_stop(excimer_timer *timer); /** * Destroy the contents of a timer object * * @param timer The timer object pointer, memory owned by the caller */ void excimer_timer_destroy(excimer_timer *timer); /** * Get remaining time * * @param timer The timer object * @param remaining This struct will be filled with the time remaining */ void excimer_timer_get_time(excimer_timer *timer, struct timespec *remaining); #endif excimer-1.2.5/php_excimer.h0000664000175000017500000000265015012753166016442 0ustar tstarlingtstarling/* Copyright 2018 Wikimedia Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #ifndef PHP_EXCIMER_H #define PHP_EXCIMER_H extern zend_module_entry excimer_module_entry; #define phpext_excimer_ptr &excimer_module_entry #define PHP_EXCIMER_VERSION "1.2.5" #ifdef PHP_WIN32 # define PHP_EXCIMER_API __declspec(dllexport) #elif defined(__GNUC__) && __GNUC__ >= 4 # define PHP_EXCIMER_API __attribute__ ((visibility("default"))) #else # define PHP_EXCIMER_API #endif #ifdef ZTS #include "TSRM.h" #endif #if defined(ZTS) && defined(COMPILE_DL_EXCIMER) ZEND_TSRMLS_CACHE_EXTERN() #endif static inline uint32_t excimer_safe_uint32(zend_long i) { if (i < 0 || i > UINT32_MAX) { zend_error_noreturn(E_ERROR, "Integer out of range"); } return (uint32_t)i; } #endif /* PHP_EXCIMER_H */ /* * Local variables: * tab-width: 4 * c-basic-offset: 4 * End: * vim600: noet sw=4 ts=4 fdm=marker * vim<600: noet sw=4 ts=4 */ excimer-1.2.5/timerlib_config.h0000664000175000017500000000244115012753166017271 0ustar tstarlingtstarling#ifdef HAVE_CONFIG_H #include #endif #include #include #include "excimer_events.h" #if defined(HAVE_SIGEV_THREAD_ID) #define TIMERLIB_USE_POSIX #elif defined(HAVE_KQUEUE) #define TIMERLIB_USE_KQUEUE #else #error "No timer implementation available" #endif #define TIMERLIB_REAL EXCIMER_REAL #define TIMERLIB_CPU EXCIMER_CPU #define TIMERLIB_FAILURE FAILURE #define TIMERLIB_SUCCESS SUCCESS // PHP uses SIGRTMIN for request timeouts #define TIMERLIB_SIGNAL (SIGRTMIN + 1) /** * Report an error from a C library function in the main thread * @param func The function which encountered the error * @param error_number The error number, e.g. EINVAL */ inline static void timerlib_report_errno(const char *func, int error_number) { php_error_docref(NULL, E_WARNING, "Error in %s(): %s", func, strerror(error_number)); } /** * Report an error from a C library function and abort the program * @param tlfunc The timerlib function which caused the error * @param libfunc The C library function which caused the error * @param error_number The error number, e.g. EINVAL */ inline static void timerlib_abort_func(const char *tlfunc, const char *libfunc, int error_number) { fprintf(stderr, "Fatal error in %s/%s(): %s\n", tlfunc, libfunc, strerror(error_number)); abort(); }