Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ shutdown handler:
$profiler->start(false);
```

`start()` is the default integration path for short-lived PHP runtimes such as
FPM or mod_php.

## Using config file

You can create `config/config.php` and load config from there:
Expand Down Expand Up @@ -103,6 +106,54 @@ $profiler_data = $profiler->disable();
$profiler->save($profiler_data);
```

For long-lived runtimes, prefer `enable()` + `stop()` around each request so
request context is captured at request start instead of at process shutdown.

## Request context providers

By default, the profiler captures request context from `$_SERVER`, `$_GET`,
`$_ENV`, and the CLI `argv` fallback. Long-lived runtimes can provide their own
request-scoped snapshot through `profiler.request_context_provider`. That
snapshot is captured when profiling starts via `enable()` / `start()`, not when
profiling stops.

Custom providers must implement
`Xhgui\Profiler\RequestContext\Provider\RequestContextProviderInterface`
and return a request-context object for the current profiling run. The request
time and server snapshot should describe the same captured request.
Custom providers are responsible for passing the request URL or CLI command
explicitly when constructing those snapshots, and for making sure the server
snapshot includes `REQUEST_TIME_FLOAT` for the captured request.
Include `REQUEST_TIME` too if you want it preserved in the saved `meta.SERVER`
payload.

```php
use Xhgui\Profiler\RequestContext\Provider\RequestContextProviderInterface;
use Xhgui\Profiler\RequestContext\RequestContext;

class AppRequestContextProvider implements RequestContextProviderInterface
{
public function capture()
{
return RequestContext::fromHttp(
'/example',
array(),
array(),
array(
'REQUEST_URI' => '/example',
'REQUEST_METHOD' => 'GET',
'HTTP_HOST' => 'example.test',
'PHP_SELF' => '/index.php',
'DOCUMENT_ROOT' => '/srv/app',
'REQUEST_TIME_FLOAT' => 1234.56789,
)
);
}
}

$config['profiler.request_context_provider'] = new AppRequestContextProvider();
```

## Autoloader

To be able to profile autoloader, this project provides `autoload.php` that
Expand Down
5 changes: 5 additions & 0 deletions autoload.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@

require_once __DIR__ . '/src/Exception/ProfilerException.php';
require_once __DIR__ . '/src/Config.php';
require_once __DIR__ . '/src/RequestContext/RequestContextInterface.php';
require_once __DIR__ . '/src/RequestContext/RequestContext.php';
require_once __DIR__ . '/src/RequestContext/Provider/RequestContextProviderInterface.php';
require_once __DIR__ . '/src/RequestContext/Provider/DefaultProvider.php';
require_once __DIR__ . '/src/RequestContextFactory.php';
require_once __DIR__ . '/src/Profiler.php';
require_once __DIR__ . '/src/ProfilerFactory.php';
require_once __DIR__ . '/src/Profilers/AbstractProfiler.php';
Expand Down
5 changes: 5 additions & 0 deletions config/config.default.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
'profiler.options' => array(),
'profiler.exclude-env' => array(),
'profiler.exclude-all-env' => false,
// Set this to an implementation of
// Xhgui\Profiler\RequestContext\Provider\RequestContextProviderInterface
// when integrating with long-lived runtimes that must capture
// request-scoped data without relying on mutable globals.
'profiler.request_context_provider' => null,
'profiler.simple_url' => function ($url) {
return preg_replace('/=\d+/', '', $url);
},
Expand Down
5 changes: 5 additions & 0 deletions examples/autoload.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@
// Environment variables to exclude from profiling data
'profiler.exclude-env' => array(),
'profiler.options' => array(),
// Set this to an implementation of
// Xhgui\Profiler\RequestContext\Provider\RequestContextProviderInterface
// when integrating with long-lived runtimes that must capture
// request-scoped data without relying on mutable globals.
'profiler.request_context_provider' => null,

/**
* Determine whether the profiler should run.
Expand Down
67 changes: 59 additions & 8 deletions src/Profiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace Xhgui\Profiler;

use Xhgui\Profiler\Exception\ProfilerException;
use Xhgui\Profiler\RequestContext\Provider\RequestContextProviderInterface;
use Xhgui\Profiler\RequestContext\RequestContextInterface;
use Xhgui\Profiler\Profilers\ProfilerInterface;
use Xhgui\Profiler\Saver\SaverInterface;

Expand Down Expand Up @@ -36,6 +38,16 @@ final class Profiler
*/
private $profiler;

/**
* @var RequestContextProviderInterface|null
*/
private $requestContextProvider;

/**
* @var RequestContextInterface|null
*/
private $requestContext;

/**
* Simple state variable to hold the value of 'Is the profiler running or not?'
*
Expand Down Expand Up @@ -99,12 +111,6 @@ public function enable($flags = null, $options = null)
{
$this->running = false;

// 'REQUEST_TIME_FLOAT' isn't available before 5.4.0
// https://www.php.net/manual/en/reserved.variables.server.php
if (!isset($_SERVER['REQUEST_TIME_FLOAT'])) {
$_SERVER['REQUEST_TIME_FLOAT'] = microtime(true);
}

$profiler = $this->getProfiler();
if (!$profiler) {
throw new ProfilerException('Unable to create profiler: No suitable profiler found');
Expand All @@ -122,7 +128,9 @@ public function enable($flags = null, $options = null)
$options = $this->config['profiler.options'];
}

$context = $this->captureRequestContext();
$profiler->enable($flags, $options);
$this->requestContext = $context;
$this->running = true;
}

Expand All @@ -144,10 +152,18 @@ public function disable()
throw new ProfilerException('Unable to create profiler: No suitable profiler found');
}

$profile = new ProfilingData($this->config);
$context = $this->requestContext;
$this->requestContext = null;
$this->running = false;
$data = $profiler->disable();

if (!$context instanceof RequestContextInterface) {
Comment thread
glensc marked this conversation as resolved.
throw new ProfilerException('Unable to disable profiler: Request context is missing');
}
Comment thread
glensc marked this conversation as resolved.

$profile = new ProfilingData($this->config);

return $profile->getProfilingData($profiler->disable());
return $profile->getProfilingData($data, $context);
}
Comment thread
glensc marked this conversation as resolved.

/**
Expand Down Expand Up @@ -277,4 +293,39 @@ private function getSaver()

return $this->saveHandler ?: null;
}

/**
* @return RequestContextProviderInterface
*/
private function getRequestContextProvider()
{
if ($this->requestContextProvider === null) {
$this->requestContextProvider = RequestContextFactory::create($this->config);
}

return $this->requestContextProvider;
}

/**
* @return RequestContextInterface
*/
private function captureRequestContext()
{
$context = $this->getRequestContextProvider()->capture();

if (!$context instanceof RequestContextInterface) {
throw new ProfilerException('Request context provider must return a RequestContextInterface');
}

$server = $context->getServer();
if (!is_array($server) || !array_key_exists('REQUEST_TIME_FLOAT', $server)) {
throw new ProfilerException('Request context provider must capture REQUEST_TIME_FLOAT in server data');
}
Comment thread
glensc marked this conversation as resolved.

Comment thread
glensc marked this conversation as resolved.
if (!is_numeric($server['REQUEST_TIME_FLOAT'])) {
throw new ProfilerException('Request context provider must capture a numeric REQUEST_TIME_FLOAT in server data');
}

return $context;
}
}
27 changes: 14 additions & 13 deletions src/ProfilingData.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Xhgui\Profiler;

use Xhgui\Profiler\RequestContext\RequestContextInterface;

/**
* @internal
*/
Expand Down Expand Up @@ -45,19 +47,21 @@ public function __construct(Config $config)
}

/**
* @param array $profile
* @param RequestContextInterface $context
* @return array
*/
public function getProfilingData(array $profile)
public function getProfilingData(array $profile, RequestContextInterface $context)
Comment thread
glensc marked this conversation as resolved.
{
$url = $this->getUrl();

list($sec, $usec) = $this->getRequestTime($_SERVER['REQUEST_TIME_FLOAT']);
$url = $this->getUrl($context);
$server = $context->getServer();
list($sec, $usec) = $this->getRequestTime($server['REQUEST_TIME_FLOAT']);

$meta = array(
'url' => $url,
'get' => $_GET,
'env' => $this->getEnvironment($_ENV),
'SERVER' => $this->getServer($_SERVER),
'get' => $context->getQuery(),
'env' => $this->getEnvironment($context->getEnv()),
'SERVER' => $this->getServer($server),
'simple_url' => $this->getSimpleUrl($url),
'request_ts_micro' => array('sec' => $sec, 'usec' => $usec),
// these are superfluous and should be dropped in the future
Expand Down Expand Up @@ -109,15 +113,12 @@ private function getSimpleUrl($url)
}

/**
* @param RequestContextInterface $context
* @return string
*/
private function getUrl()
private function getUrl(RequestContextInterface $context)
{
$url = array_key_exists('REQUEST_URI', $_SERVER) ? $_SERVER['REQUEST_URI'] : null;
if (!$url && isset($_SERVER['argv'])) {
$cmd = basename($_SERVER['argv'][0]);
$url = $cmd . ' ' . implode(' ', array_slice($_SERVER['argv'], 1));
}
$url = $context->getUrl();

if (is_callable($this->replaceUrl)) {
$url = call_user_func($this->replaceUrl, $url);
Expand Down
60 changes: 60 additions & 0 deletions src/RequestContext/Provider/DefaultProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace Xhgui\Profiler\RequestContext\Provider;

use Xhgui\Profiler\RequestContext\RequestContext;

/**
* @internal
*/
class DefaultProvider implements RequestContextProviderInterface
{
public function capture()
{
$server = $_SERVER;

// 'REQUEST_TIME_FLOAT' isn't available before 5.4.0
// https://www.php.net/manual/en/reserved.variables.server.php
if (!isset($server['REQUEST_TIME_FLOAT'])) {
$server['REQUEST_TIME_FLOAT'] = microtime(true);
}
if (!isset($server['REQUEST_TIME'])) {
$server['REQUEST_TIME'] = (int) $server['REQUEST_TIME_FLOAT'];
}

if (array_key_exists('REQUEST_URI', $server)) {
return RequestContext::fromHttp(
$server['REQUEST_URI'],
$_GET,
$_ENV,
$server
);
}

return RequestContext::fromCli(
$this->getCommand(isset($server['argv']) ? $server['argv'] : array()),
$_ENV,
$server
);
}

/**
* @param array $argv
* @return string
*/
private function getCommand(array $argv)
{
if (!isset($argv[0])) {
return '';
}

$cmd = basename($argv[0]);
$args = array_slice($argv, 1);

if (!$args) {
return $cmd;
}

return $cmd . ' ' . implode(' ', $args);
}
}
18 changes: 18 additions & 0 deletions src/RequestContext/Provider/RequestContextProviderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Xhgui\Profiler\RequestContext\Provider;

use Xhgui\Profiler\RequestContext\RequestContextInterface;

interface RequestContextProviderInterface
{
/**
* Capture request-scoped profiler metadata.
*
* Implementations should return a request-context object whose request time
* and server snapshot describe the same request.
*
* @return RequestContextInterface
*/
public function capture();
}
Loading