Add performance counter system.

This commit is contained in:
Starbeamrainbowlabs 2019-06-21 00:02:26 +01:00
parent b92621aab6
commit 0006c2a921
Signed by: sbrl
GPG key ID: 1BE5172E637709C2
15 changed files with 229 additions and 92 deletions

12
api.php
View file

@ -12,16 +12,23 @@ $autoloader->addPrefix("AirQuality", "logic");
$autoloader->addPrefix("SBRL", "lib/SBRL"); $autoloader->addPrefix("SBRL", "lib/SBRL");
$autoloader->register(); $autoloader->register();
// 1.5: Performance counter
$perfcounter = new \SBRL\PerformanceCounter();
$perfcounter->start("total");
// 2: Settings // 2: Settings
$perfcounter->start("settings");
$settings = new \SBRL\TomlConfig( $settings = new \SBRL\TomlConfig(
"data/settings.toml", "data/settings.toml",
"settings.default.toml" "settings.default.toml"
); );
$perfcounter->end("settings");
// 3: Dependency injection // 3: Dependency injection
$perfcounter->start("di");
$di_builder = new DI\ContainerBuilder(); $di_builder = new DI\ContainerBuilder();
$di_builder->addDefinitions("di_config.php"); $di_builder->addDefinitions("di_config.php");
if($settings->get("env.mode") == "production") { if($settings->get("env.mode") == "production") {
@ -37,6 +44,7 @@ if($settings->get("env.mode") == "production") {
$di_container = $di_builder->build(); $di_container = $di_builder->build();
$perfcounter->end("di");
// 4: Database // 4: Database
@ -68,6 +76,8 @@ if(!class_exists($handler_name)) {
exit("Error: No action with the name '$action' could be found."); exit("Error: No action with the name '$action' could be found.");
} }
// FUTURE: A PSR-7 request/response system would be rather nice here.
$handler = $di_container->get($handler_name); $handler = $di_container->get($handler_name);
$perfcounter->start("handle");
// FUTURE: A PSR-7 request/response system would be rather nice here.
$handler->handle(); $handler->handle();

View file

@ -3,7 +3,7 @@
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use SBRL\TomlConfig; use SBRL\TomlConfig;
use AirQuality\Database; // use AirQuality\Database;
use AirQuality\Repositories\IDeviceRepository; use AirQuality\Repositories\IDeviceRepository;
use AirQuality\Repositories\MariaDBDeviceRepository; use AirQuality\Repositories\MariaDBDeviceRepository;
@ -12,15 +12,23 @@ use AirQuality\Repositories\MariaDBMeasurementDataRepository;
use AirQuality\Repositories\IMeasurementTypeRepository; use AirQuality\Repositories\IMeasurementTypeRepository;
use AirQuality\Repositories\MariaDBMeasurementTypeRepository; use AirQuality\Repositories\MariaDBMeasurementTypeRepository;
use SBRL\PerformanceCounter;
return [ return [
"settings.file_default" => "data/settings.toml", "settings.file_default" => "data/settings.toml",
"settings.file_custom" => "settings.default.toml", "settings.file_custom" => "settings.default.toml",
// These are created during initalisation, but we want them available via dependency injection too
TomlConfig::class => function(ContainerInterface $c) { TomlConfig::class => function(ContainerInterface $c) {
global $settings; global $settings;
return $settings; return $settings;
}, },
PerformanceCounter::class => function(ContainerInterface $c) {
global $perfcounter;
return $perfcounter;
},
// Interfaces that need mapping to their implementations
IDeviceRepository::class => DI\autowire(MariaDBDeviceRepository::class), IDeviceRepository::class => DI\autowire(MariaDBDeviceRepository::class),
IMeasurementDataRepository::class => DI\autowire(MariaDBMeasurementDataRepository::class), IMeasurementDataRepository::class => DI\autowire(MariaDBMeasurementDataRepository::class),
IMeasurementTypeRepository::class => DI\autowire(MariaDBMeasurementTypeRepository::class) IMeasurementTypeRepository::class => DI\autowire(MariaDBMeasurementTypeRepository::class)

View file

@ -0,0 +1,83 @@
<?php
namespace SBRL;
use Exception;
/**
* Records performance information.
*/
class PerformanceCounter
{
/**
* An associative array of performance information.
* It should take the form key => [float] | float.
* HACK: If the float is the only item in an array, then it hasn't been ended yet.
* @var array
*/
private $data = [];
/**
* An array of performance counters to hide when rendering.
* Useful to keep some counters around for later, but keep them hidden so as to not clutter everything up.
* @var string[]
*/
private $hidden = [];
/**
* Creates a new performance counter instea
*/
function __construct() {
// code...
}
/**
* Starts recording the time taken for a given key.
* Calling end($key) is optional
* @param string $key The key to start recording the time taken for.
*/
public function start(string $key) {
$this->data[$key] = [microtime(true)];
}
/**
* Ends recording the time taken for a given key.
* @param string $key The key to end counting for.
* @return float The time taken, in milliseconds
*/
public function end(string $key) {
if(empty($this->data[$key]))
throw new Exception("Error: We can't end counting for $key because we haven't started yet");
$this->data[$key] = (microtime(true) - $this->data[$key][0]) * 1000;
return $this->data[$key];
}
/**
* Renders the performance information as a short string.
* Suitable for sending in a HTTP header, for example.
* @return string The rendered performance information.
*/
public function render() {
$result = "";
$parts = [];
foreach($this->data as $key => $value) {
if(in_array($key, $this->hidden))
continue;
if(is_array($value))
$value = $this->end($key);
$render = round($value, 2) . "ms $key";
if($key == "total") {
$result .= "$render | ";
continue;
}
$parts[] = $render;
}
$result .= implode(", ", $parts);
return $result;
}
}

View file

@ -4,36 +4,40 @@ namespace AirQuality\Actions;
use \SBRL\TomlConfig; use \SBRL\TomlConfig;
use \AirQuality\PerfFormatter;
class Changelog implements IAction { class Changelog implements IAction {
/** @var TomlConfig */ /** @var TomlConfig */
private $settings; private $settings;
/** @var \SBRL\PerformanceCounter */
private $perfcounter;
/** @var \ParsedownExtra */ /** @var \ParsedownExtra */
private $parsedown_ext; private $parsedown_ext;
public function __construct( public function __construct(
TomlConfig $in_settings, TomlConfig $in_settings,
\ParsedownExtra $in_parsedown_ext) { \ParsedownExtra $in_parsedown_ext,
\SBRL\PerformanceCounter $in_perfcounter) {
$this->settings = $in_settings; $this->settings = $in_settings;
$this->parsedown_ext = $in_parsedown_ext; $this->parsedown_ext = $in_parsedown_ext;
$this->perfcounter = $in_perfcounter;
} }
public function handle() : bool { public function handle() : bool {
global $start_time; global $start_time;
$start_handle = microtime(true);
// 1: Parse markdown // 1: Parse markdown
$this->perfcounter->start("parse");
$result = $this->parsedown_ext->text(file_get_contents(ROOT_DIR."Changelog.md")); $result = $this->parsedown_ext->text(file_get_contents(ROOT_DIR."Changelog.md"));
$this->perfcounter->end("parse");
// 2: Send response // 2: Send response
header("content-length: " . strlen($result)); header("content-length: " . strlen($result));
header("content-type: text/html"); header("content-type: text/html");
header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, null)); header("x-time-taken: " . $this->perfcounter->render());
echo($result); echo($result);
return true; return true;
} }

View file

@ -9,11 +9,13 @@ use \AirQuality\Repositories\IMeasurementTypeRepository;
use \AirQuality\ApiResponseSender; use \AirQuality\ApiResponseSender;
use \AirQuality\Validator; use \AirQuality\Validator;
use \AirQuality\PerfFormatter;
class DeviceData implements IAction { class DeviceData implements IAction {
/** @var TomlConfig */ /** @var TomlConfig */
private $settings; private $settings;
/** @var \SBRL\PerformanceCounter */
private $perfcounter;
/** @var IMeasurementDataRepository */ /** @var IMeasurementDataRepository */
private $measurement_repo; private $measurement_repo;
@ -30,11 +32,13 @@ class DeviceData implements IAction {
TomlConfig $in_settings, TomlConfig $in_settings,
IMeasurementDataRepository $in_measurement_repo, IMeasurementDataRepository $in_measurement_repo,
IMeasurementTypeRepository $in_type_repo, IMeasurementTypeRepository $in_type_repo,
ApiResponseSender $in_sender) { ApiResponseSender $in_sender,
\SBRL\PerformanceCounter $in_perfcounter) {
$this->settings = $in_settings; $this->settings = $in_settings;
$this->measurement_repo = $in_measurement_repo; $this->measurement_repo = $in_measurement_repo;
$this->type_repo = $in_type_repo; $this->type_repo = $in_type_repo;
$this->sender = $in_sender; $this->sender = $in_sender;
$this->perfcounter = $in_perfcounter;
$this->validator = new Validator($_GET); $this->validator = new Validator($_GET);
} }
@ -42,7 +46,6 @@ class DeviceData implements IAction {
public function handle() : bool { public function handle() : bool {
global $start_time; global $start_time;
$start_handle = microtime(true);
// 1: Validate params // 1: Validate params
$this->validator->is_numberish("device-id"); $this->validator->is_numberish("device-id");
@ -61,7 +64,7 @@ class DeviceData implements IAction {
if(new \DateTime($_GET["start"]) > new \DateTime($_GET["end"])) { if(new \DateTime($_GET["start"]) > new \DateTime($_GET["end"])) {
$this->sender->send_error_plain( $this->sender->send_error_plain(
400, "Error: The start date must be earlier than the end date.", [ 400, "Error: The start date must be earlier than the end date.", [
[ "x-time-taken", PerfFormatter::format_perf_data($start_time, $start_handle, null) ] [ "x-time-taken", $this->perfcounter->render() ]
] ]
); );
return false; return false;
@ -70,18 +73,19 @@ class DeviceData implements IAction {
if(!empty($_GET["average-seconds"]) && intval($_GET["average-seconds"]) == 0) { if(!empty($_GET["average-seconds"]) && intval($_GET["average-seconds"]) == 0) {
$this->sender->send_error_plain( $this->sender->send_error_plain(
400, "Error: That average-seconds value is invalid (an integer greater than 0 required).", [ 400, "Error: That average-seconds value is invalid (an integer greater than 0 required).", [
[ "x-time-taken", PerfFormatter::format_perf_data($start_time, $start_handle, null) ] [ "x-time-taken", $this->perfcounter->render() ]
] ]
); );
return false; return false;
} }
$this->perfcounter->start("sql");
$reading_type_id = $this->type_repo->get_id($_GET["reading-type"]); $reading_type_id = $this->type_repo->get_id($_GET["reading-type"]);
if($reading_type_id == null) { if($reading_type_id == null) {
$this->sender->send_error_plain( $this->sender->send_error_plain(
400, "Error: That reading type is invalid.", [ 400, "Error: That reading type is invalid.", [
[ "x-time-taken", PerfFormatter::format_perf_data($start_time, $start_handle, null) ] [ "x-time-taken", $this->perfcounter->render() ]
] ]
); );
return false; return false;
@ -95,19 +99,20 @@ class DeviceData implements IAction {
new \DateTime($_GET["end"]), new \DateTime($_GET["end"]),
!empty($_GET["average-seconds"]) ? intval($_GET["average-seconds"]) : 1 !empty($_GET["average-seconds"]) ? intval($_GET["average-seconds"]) : 1
); );
$this->perfcounter->end("sql");
// 2.5: Validate data from database // 2.5: Validate data from database
if(empty($data)) { if(empty($data)) {
http_response_code(404); http_response_code(404);
header("content-type: text/plain"); header("content-type: text/plain");
header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, null)); header("x-time-taken: " . $this->perfcounter->render());
echo("Error: No data has been recorded from the device id for that measurement type in the selected time scale or it doesn't exist."); echo("Error: No data has been recorded from the device id for that measurement type in the selected time scale or it doesn't exist.");
return false; return false;
} }
// 3: Serialise data // 3: Serialise data
$start_encode = microtime(true); $this->perfcounter->start("encode");
$response_type = "application/octet-stream"; $response_type = "application/octet-stream";
$response_suggested_filename = "data-" . date(\DateTime::ATOM) . ""; $response_suggested_filename = "data-" . date(\DateTime::ATOM) . "";
$response = null; $response = null;
@ -123,13 +128,14 @@ class DeviceData implements IAction {
$response = ResponseEncoder::encode_csv($data); $response = ResponseEncoder::encode_csv($data);
break; break;
} }
$this->perfcounter->end("encode");
// 4: Send response // 4: Send response
header("content-length: " . strlen($response)); header("content-length: " . strlen($response));
header("content-type: $response_type"); header("content-type: $response_type");
header("content-disposition: inline; filename=$response_suggested_filename"); header("content-disposition: inline; filename=$response_suggested_filename");
header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, $start_encode)); header("x-time-taken: " . $this->perfcounter->render());
echo($response); echo($response);
return true; return true;
} }

View file

@ -7,11 +7,14 @@ use \AirQuality\Repositories\IMeasurementDataRepository;
use \AirQuality\ApiResponseSender; use \AirQuality\ApiResponseSender;
use \AirQuality\Validator; use \AirQuality\Validator;
use \AirQuality\PerfFormatter;
class DeviceDataBounds implements IAction { class DeviceDataBounds implements IAction {
/** @var TomlConfig */ /** @var TomlConfig */
private $settings; private $settings;
/** @var \SBRL\PerformanceCounter */
private $perfcounter;
/** @var IMeasurementDataRepository */ /** @var IMeasurementDataRepository */
private $measurement_repo; private $measurement_repo;
@ -24,10 +27,12 @@ class DeviceDataBounds implements IAction {
public function __construct( public function __construct(
TomlConfig $in_settings, TomlConfig $in_settings,
IMeasurementDataRepository $in_measurement_repo, IMeasurementDataRepository $in_measurement_repo,
ApiResponseSender $in_sender) { ApiResponseSender $in_sender,
\SBRL\PerformanceCounter $in_perfcounter) {
$this->settings = $in_settings; $this->settings = $in_settings;
$this->measurement_repo = $in_measurement_repo; $this->measurement_repo = $in_measurement_repo;
$this->sender = $in_sender; $this->sender = $in_sender;
$this->perfcounter = $in_perfcounter;
$this->validator = new Validator($_GET); $this->validator = new Validator($_GET);
} }
@ -35,7 +40,6 @@ class DeviceDataBounds implements IAction {
public function handle() : bool { public function handle() : bool {
global $start_time; global $start_time;
$start_handle = microtime(true);
// 1: Validate params // 1: Validate params
$this->validator->is_numberish("device-id"); $this->validator->is_numberish("device-id");
@ -43,29 +47,32 @@ class DeviceDataBounds implements IAction {
// 2: Pull data from database // 2: Pull data from database
$this->perfcounter->start("sql");
$data = $this->measurement_repo->get_device_reading_bounds( $data = $this->measurement_repo->get_device_reading_bounds(
intval($_GET["device-id"]) intval($_GET["device-id"])
); );
$this->perfcounter->end("sql");
// 2.5: Validate data from database // 2.5: Validate data from database
if(empty($data)) { if(empty($data)) {
http_response_code(404); http_response_code(404);
header("content-type: text/plain"); header("content-type: text/plain");
header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, null)); header("x-time-taken: " . $this->perfcounter->render());
echo("Error: No data has been recorded from the device id or it doesn't exist."); echo("Error: No data has been recorded from the device id or it doesn't exist.");
return false; return false;
} }
// 3: Serialise data // 3: Serialise data
$start_encode = microtime(true); $this->perfcounter->start("encode");
$response = json_encode($data); $response = json_encode($data);
$this->perfcounter->end("encode");
// 4: Send response // 4: Send response
header("content-length: " . strlen($response)); header("content-length: " . strlen($response));
header("content-type: application/json"); header("content-type: application/json");
header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, $start_encode)); header("x-time-taken: " . $this->perfcounter->render());
echo($response); echo($response);
return true; return true;
} }

View file

@ -9,7 +9,7 @@ use \AirQuality\Repositories\IMeasurementTypeRepository;
use \AirQuality\ApiResponseSender; use \AirQuality\ApiResponseSender;
use \AirQuality\Validator; use \AirQuality\Validator;
use \AirQuality\PerfFormatter;
/** /**
* Action that retrieves recent data for a device. * Action that retrieves recent data for a device.
@ -17,6 +17,8 @@ use \AirQuality\PerfFormatter;
class DeviceDataRecent implements IAction { class DeviceDataRecent implements IAction {
/** @var TomlConfig */ /** @var TomlConfig */
private $settings; private $settings;
/** @var \SBRL\PerformanceCounter */
private $perfcounter;
/** @var IMeasurementDataRepository */ /** @var IMeasurementDataRepository */
private $measurement_repo; private $measurement_repo;
@ -33,11 +35,13 @@ class DeviceDataRecent implements IAction {
TomlConfig $in_settings, TomlConfig $in_settings,
IMeasurementDataRepository $in_measurement_repo, IMeasurementDataRepository $in_measurement_repo,
IMeasurementTypeRepository $in_type_repo, IMeasurementTypeRepository $in_type_repo,
ApiResponseSender $in_sender) { ApiResponseSender $in_sender,
\SBRL\PerformanceCounter $in_perfcounter) {
$this->settings = $in_settings; $this->settings = $in_settings;
$this->measurement_repo = $in_measurement_repo; $this->measurement_repo = $in_measurement_repo;
$this->type_repo = $in_type_repo; $this->type_repo = $in_type_repo;
$this->sender = $in_sender; $this->sender = $in_sender;
$this->perfcounter = $in_perfcounter;
$this->validator = new Validator($_GET); $this->validator = new Validator($_GET);
} }
@ -45,7 +49,6 @@ class DeviceDataRecent implements IAction {
public function handle() : bool { public function handle() : bool {
global $start_time; global $start_time;
$start_handle = microtime(true);
// 1: Validate params // 1: Validate params
$this->validator->is_numberish("device-id"); $this->validator->is_numberish("device-id");
@ -69,24 +72,26 @@ class DeviceDataRecent implements IAction {
if($reading_type_id == null) { if($reading_type_id == null) {
$this->sender->send_error_plain( $this->sender->send_error_plain(
400, "Error: That reading type is invalid.", [ 400, "Error: That reading type is invalid.", [
[ "x-time-taken", PerfFormatter::format_perf_data($start_time, $start_handle, null) ] [ "x-time-taken", $this->perfcounter->render() ]
] ]
); );
return false; return false;
} }
// 2: Pull data from database // 2: Pull data from database
$this->perfcounter->start("sql");
$data = $this->measurement_repo->get_recent_readings( $data = $this->measurement_repo->get_recent_readings(
intval($_GET["device-id"]), intval($_GET["device-id"]),
$reading_type_id, $reading_type_id,
$count $count
); );
$this->perfcounter->end("sql");
// 2.5: Validate data from database // 2.5: Validate data from database
if(empty($data)) { if(empty($data)) {
http_response_code(404); http_response_code(404);
header("content-type: text/plain"); header("content-type: text/plain");
header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, null)); header("x-time-taken: " . $this->perfcounter->render());
echo("Error: No data has been recorded from the device id for that measurement type in the selected time scale or it doesn't exist."); echo("Error: No data has been recorded from the device id for that measurement type in the selected time scale or it doesn't exist.");
return false; return false;
} }
@ -96,7 +101,7 @@ class DeviceDataRecent implements IAction {
// FUTURE: Refactor this into an AbstractAction or something, but the careful not to shift too much work there // FUTURE: Refactor this into an AbstractAction or something, but the careful not to shift too much work there
// We might want to consider a HttpResponse container or something if we can find a decent PSR implementation // We might want to consider a HttpResponse container or something if we can find a decent PSR implementation
$start_encode = microtime(true); $this->perfcounter->start("encode");
$response_type = "application/octet-stream"; $response_type = "application/octet-stream";
$response_suggested_filename = "data-" . date(\DateTime::ATOM) . ""; $response_suggested_filename = "data-" . date(\DateTime::ATOM) . "";
$response = null; $response = null;
@ -112,13 +117,14 @@ class DeviceDataRecent implements IAction {
$response = ResponseEncoder::encode_csv($data); $response = ResponseEncoder::encode_csv($data);
break; break;
} }
$this->perfcounter->end("encode");
// 4: Send response // 4: Send response
header("content-length: " . strlen($response)); header("content-length: " . strlen($response));
header("content-type: $response_type"); header("content-type: $response_type");
header("content-disposition: inline; filename=$response_suggested_filename"); header("content-disposition: inline; filename=$response_suggested_filename");
header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, $start_encode)); header("x-time-taken: " . $this->perfcounter->render());
echo($response); echo($response);
return true; return true;
} }

View file

@ -4,14 +4,18 @@ namespace AirQuality\Actions;
use \SBRL\TomlConfig; use \SBRL\TomlConfig;
use \AirQuality\Repositories\IDeviceRepository; use \AirQuality\Repositories\IDeviceRepository;
use \AirQuality\Repositories\IMeasurementTypeRepository;
use \AirQuality\ApiResponseSender; use \AirQuality\ApiResponseSender;
use \AirQuality\Validator; use \AirQuality\Validator;
use \AirQuality\PerfFormatter;
class DeviceInfo implements IAction { class DeviceInfo implements IAction {
/** @var TomlConfig */ /** @var TomlConfig */
private $settings; private $settings;
/** @var \SBRL\PerformanceCounter */
private $perfcounter;
/** @var IDeviceRepository */ /** @var IDeviceRepository */
private $device_repo; private $device_repo;
@ -27,10 +31,12 @@ class DeviceInfo implements IAction {
public function __construct( public function __construct(
TomlConfig $in_settings, TomlConfig $in_settings,
IDeviceRepository $in_device_repo, IDeviceRepository $in_device_repo,
ApiResponseSender $in_sender) { ApiResponseSender $in_sender,
\SBRL\PerformanceCounter $in_perfcounter) {
$this->settings = $in_settings; $this->settings = $in_settings;
$this->device_repo = $in_device_repo; $this->device_repo = $in_device_repo;
$this->sender = $in_sender; $this->sender = $in_sender;
$this->perfcounter = $in_perfcounter;
$this->validator = new Validator($_GET); $this->validator = new Validator($_GET);
} }
@ -38,28 +44,30 @@ class DeviceInfo implements IAction {
public function handle() : bool { public function handle() : bool {
global $start_time; global $start_time;
$start_handle = microtime(true);
// 1: Validate params // 1: Validate params
$this->validator->is_numberish("device-id"); $this->validator->is_numberish("device-id");
$this->validator->run(); $this->validator->run();
// 2: Pull data from database // 2: Pull data from database
$this->perfcounter->start("sql");
$data = $this->device_repo->get_device_info_ext( $data = $this->device_repo->get_device_info_ext(
$_GET["device-id"] $_GET["device-id"]
); );
$this->perfcounter->end("sql");
// 2.5: Validate data from database // 2.5: Validate data from database
if(empty($data)) { if(empty($data)) {
$this->sender->send_error_plain(404, "Error: No data could be found for that device id.", [ $this->sender->send_error_plain(404, "Error: No data could be found for that device id.", [
[ "x-time-taken: ", PerfFormatter::format_perf_data($start_time, $start_handle, null) ] [ "x-time-taken: ", $this->perfcounter->render() ]
]); ]);
return false; return false;
} }
// 3: Serialise data // 3: Serialise data
$start_encode = microtime(true); $this->perfcounter->start("encode");
$response = json_encode($data); $response = json_encode($data);
$this->perfcounter->end("encode");
// 4: Send response // 4: Send response
@ -70,7 +78,7 @@ class DeviceInfo implements IAction {
header("content-length: " . strlen($response)); header("content-length: " . strlen($response));
header("content-type: application/json"); header("content-type: application/json");
header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, $start_encode)); header("x-time-taken: " . $this->perfcounter->render());
echo($response); echo($response);
return true; return true;
} }

View file

@ -9,11 +9,13 @@ use \AirQuality\Repositories\IMeasurementTypeRepository;
use \AirQuality\ApiResponseSender; use \AirQuality\ApiResponseSender;
use \AirQuality\Validator; use \AirQuality\Validator;
use \AirQuality\PerfFormatter;
class FetchData implements IAction { class FetchData implements IAction {
/** @var TomlConfig */ /** @var TomlConfig */
private $settings; private $settings;
/** @var \SBRL\PerformanceCounter */
private $perfcounter;
/** @var IMeasurementDataRepository */ /** @var IMeasurementDataRepository */
private $measurement_repo; private $measurement_repo;
@ -30,11 +32,13 @@ class FetchData implements IAction {
TomlConfig $in_settings, TomlConfig $in_settings,
IMeasurementDataRepository $in_measurement_repo, IMeasurementDataRepository $in_measurement_repo,
IMeasurementTypeRepository $in_type_repo, IMeasurementTypeRepository $in_type_repo,
ApiResponseSender $in_sender) { ApiResponseSender $in_sender,
\SBRL\PerformanceCounter $in_perfcounter) {
$this->settings = $in_settings; $this->settings = $in_settings;
$this->measurement_repo = $in_measurement_repo; $this->measurement_repo = $in_measurement_repo;
$this->type_repo = $in_type_repo; $this->type_repo = $in_type_repo;
$this->sender = $in_sender; $this->sender = $in_sender;
$this->perfcounter = $in_perfcounter;
$this->validator = new Validator($_GET); $this->validator = new Validator($_GET);
} }
@ -42,7 +46,6 @@ class FetchData implements IAction {
public function handle() : bool { public function handle() : bool {
global $start_time; global $start_time;
$start_handle = microtime(true);
// 1: Validate params // 1: Validate params
$this->validator->is_datetime("datetime"); $this->validator->is_datetime("datetime");
@ -58,7 +61,7 @@ class FetchData implements IAction {
if($measurement_type_id == null) { if($measurement_type_id == null) {
$this->sender->send_error_plain( $this->sender->send_error_plain(
400, "Error: That reading type is invalid.", [ 400, "Error: That reading type is invalid.", [
[ "x-time-taken", PerfFormatter::format_perf_data($start_time, $start_handle, null) ] [ "x-time-taken", $this->perfcounter->render() ]
] ]
); );
return false; return false;
@ -66,24 +69,26 @@ class FetchData implements IAction {
// 2: Pull data from database // 2: Pull data from database
$this->perfcounter->start("sql");
$data = $this->measurement_repo->get_readings_by_date( $data = $this->measurement_repo->get_readings_by_date(
new \DateTime($_GET["datetime"]), new \DateTime($_GET["datetime"]),
$measurement_type_id $measurement_type_id
); );
$this->perfcounter->end("sql");
// 2.5: Validate data from database // 2.5: Validate data from database
if(empty($data)) { if(empty($data)) {
$this->sender->send_error_plain(404, $this->sender->send_error_plain(404,
"Error: No data could be found for that timestamp.", "Error: No data could be found for that timestamp.",
[ PerfFormatter::format_perf_data($start_time, $start_handle, null) ] [ [ "x-time-taken", $this->perfcounter->render() ] ]
); );
return false; return false;
} }
// 3: Serialise data // 3: Serialise data
$start_encode = microtime(true); $this->perfcounter->start("encode");
$response_type = "application/octet-stream"; $response_type = "application/octet-stream";
$response_suggested_filename = "data-" . date(\DateTime::ATOM) . ""; $response_suggested_filename = "data-" . date(\DateTime::ATOM) . "";
$response = null; $response = null;
@ -99,6 +104,7 @@ class FetchData implements IAction {
$response = ResponseEncoder::encode_csv($data); $response = ResponseEncoder::encode_csv($data);
break; break;
} }
$this->perfcounter->end("encode");
// 4: Send response // 4: Send response
@ -111,7 +117,7 @@ class FetchData implements IAction {
header("content-type: $response_type"); header("content-type: $response_type");
header("content-length: " . strlen($response)); header("content-length: " . strlen($response));
header("content-disposition: inline; filename=$response_suggested_filename"); header("content-disposition: inline; filename=$response_suggested_filename");
header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, $start_encode)); header("x-time-taken: " . $this->perfcounter->render());
echo($response); echo($response);
return true; return true;
} }

View file

@ -4,25 +4,27 @@ namespace AirQuality\Actions;
use \SBRL\TomlConfig; use \SBRL\TomlConfig;
use \AirQuality\PerfFormatter;
class Index implements IAction { class Index implements IAction {
/** @var TomlConfig */ /** @var TomlConfig */
private $settings; private $settings;
/** @var \SBRL\PerformanceCounter */
private $perfcounter;
/** @var \ParsedownExtra */ /** @var \ParsedownExtra */
private $parsedown_ext; private $parsedown_ext;
public function __construct( public function __construct(
TomlConfig $in_settings) { TomlConfig $in_settings,
\SBRL\PerformanceCounter $in_perfcounter) {
$this->settings = $in_settings; $this->settings = $in_settings;
$this->perfcounter = $in_perfcounter;
} }
public function handle() : bool { public function handle() : bool {
global $start_time; global $start_time;
$start_handle = microtime(true); $this->perfcounter->start("handle");
// 1: Parse markdown // 1: Parse markdown
$result = "Welcome to the Air Quality Web HTTP API! $result = "Welcome to the Air Quality Web HTTP API!
@ -38,7 +40,7 @@ Note that if you have deployed your own version of the air quality web interface
// 2: Send response // 2: Send response
header("content-length: " . strlen($result)); header("content-length: " . strlen($result));
header("content-type: text/plain"); header("content-type: text/plain");
header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, null)); header("x-time-taken: " . $this->perfcounter->render());
echo($result); echo($result);
return true; return true;
} }

View file

@ -7,13 +7,14 @@ use \SBRL\ResponseEncoder;
use \AirQuality\Repositories\IDeviceRepository; use \AirQuality\Repositories\IDeviceRepository;
use \AirQuality\ApiResponseSender; use \AirQuality\ApiResponseSender;
use \AirQuality\PerfFormatter;
class ListDevices implements IAction { class ListDevices implements IAction {
/** @var TomlConfig */ /** @var TomlConfig */
private $settings; private $settings;
/** @var IDeviceRepository */ /** @var IDeviceRepository */
private $device_repo; private $device_repo;
/** @var \SBRL\PerformanceCounter */
private $perfcounter;
/** @var ApiResponseSender */ /** @var ApiResponseSender */
private $sender; private $sender;
@ -21,16 +22,17 @@ class ListDevices implements IAction {
public function __construct( public function __construct(
TomlConfig $in_settings, TomlConfig $in_settings,
IDeviceRepository $in_device_repo, IDeviceRepository $in_device_repo,
ApiResponseSender $in_sender) { ApiResponseSender $in_sender,
\SBRL\PerformanceCounter $in_perfcounter) {
$this->settings = $in_settings; $this->settings = $in_settings;
$this->device_repo = $in_device_repo; $this->device_repo = $in_device_repo;
$this->sender = $in_sender; $this->sender = $in_sender;
$this->perfcounter = $in_perfcounter;
} }
public function handle() : bool { public function handle() : bool {
global $start_time; global $start_time;
$start_handle = microtime(true);
// 1: Parse & validate parameters // 1: Parse & validate parameters
$only_with_location = !empty($_GET["only-with-location"]); $only_with_location = !empty($_GET["only-with-location"]);
@ -45,21 +47,23 @@ class ListDevices implements IAction {
// 2: Pull data from database // 2: Pull data from database
$this->perfcounter->start("sql");
$data = $this->device_repo->get_all_devices($only_with_location); $data = $this->device_repo->get_all_devices($only_with_location);
$this->perfcounter->end("sql");
// 2.5: Validate data from database // 2.5: Validate data from database
if(empty($data)) { if(empty($data)) {
$this->sender->send_error_plain(404, $this->sender->send_error_plain(404,
"Error: No devices are currently present in the system.", "Error: No devices are currently present in the system.",
[ "x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, null) ] [ "x-time-taken: ", $this->perfcounter->render() ]
); );
return false; return false;
} }
// 3: Serialise data // 3: Serialise data
$start_encode = microtime(true); $this->perfcounter->start("encode");
$response = null; $response = null;
$response_type = "application/octet-stream"; $response_type = "application/octet-stream";
$response_suggested_filename = "data-" . date(\DateTime::ATOM) . ""; $response_suggested_filename = "data-" . date(\DateTime::ATOM) . "";
@ -75,6 +79,7 @@ class ListDevices implements IAction {
$response = ResponseEncoder::encode_csv($data); $response = ResponseEncoder::encode_csv($data);
break; break;
} }
$this->perfcounter->end("encode");
// 4: Send response // 4: Send response
@ -84,7 +89,7 @@ class ListDevices implements IAction {
header("content-length: " . strlen($response)); header("content-length: " . strlen($response));
header("content-type: $response_type"); header("content-type: $response_type");
header("content-disposition: inline; filename=$response_suggested_filename"); header("content-disposition: inline; filename=$response_suggested_filename");
header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, $start_encode)); header("x-time-taken: " . $this->perfcounter->render());
echo($response); echo($response);
return true; return true;
} }

View file

@ -7,11 +7,13 @@ use \SBRL\ResponseEncoder;
use \AirQuality\Repositories\IMeasurementTypeRepository; use \AirQuality\Repositories\IMeasurementTypeRepository;
use \AirQuality\ApiResponseSender; use \AirQuality\ApiResponseSender;
use \AirQuality\PerfFormatter;
class ListReadingTypes implements IAction { class ListReadingTypes implements IAction {
/** @var TomlConfig */ /** @var TomlConfig */
private $settings; private $settings;
/** @var \SBRL\PerformanceCounter */
private $perfcounter;
/** @var IMeasurementTypeRepository */ /** @var IMeasurementTypeRepository */
private $types_repo; private $types_repo;
@ -21,16 +23,17 @@ class ListReadingTypes implements IAction {
public function __construct( public function __construct(
TomlConfig $in_settings, TomlConfig $in_settings,
IMeasurementTypeRepository $in_types_repo, IMeasurementTypeRepository $in_types_repo,
ApiResponseSender $in_sender) { ApiResponseSender $in_sender,
\SBRL\PerformanceCounter $in_perfcounter) {
$this->settings = $in_settings; $this->settings = $in_settings;
$this->types_repo = $in_types_repo; $this->types_repo = $in_types_repo;
$this->sender = $in_sender; $this->sender = $in_sender;
$this->perfcounter = $in_perfcounter;
} }
public function handle() : bool { public function handle() : bool {
global $start_time; global $start_time;
$start_handle = microtime(true);
// 1: Parse & validate parameters // 1: Parse & validate parameters
$device_id = !empty($_GET["device-id"]) ? intval($_GET["device-id"]) : null; $device_id = !empty($_GET["device-id"]) ? intval($_GET["device-id"]) : null;
@ -45,10 +48,13 @@ class ListReadingTypes implements IAction {
// 1: Pull data from database // 1: Pull data from database
$data = null; $data = null;
$this->perfcounter->start("sql");
if(!is_int($device_id)) if(!is_int($device_id))
$data = $this->types_repo->get_all_types(); $data = $this->types_repo->get_all_types();
else else
$data = $this->types_repo->get_types_by_device($device_id); $data = $this->types_repo->get_types_by_device($device_id);
$this->perfcounter->end("sql");
// 1.5: Validate data from database // 1.5: Validate data from database
if(empty($data)) { if(empty($data)) {
@ -56,7 +62,7 @@ class ListReadingTypes implements IAction {
} }
// 3: Serialise data // 3: Serialise data
$start_encode = microtime(true); $this->perfcounter->start("encode");
$response = null; $response = null;
$response_type = "application/octet-stream"; $response_type = "application/octet-stream";
$response_suggested_filename = "data-" . date(\DateTime::ATOM) . ""; $response_suggested_filename = "data-" . date(\DateTime::ATOM) . "";
@ -72,6 +78,7 @@ class ListReadingTypes implements IAction {
$response = ResponseEncoder::encode_csv($data); $response = ResponseEncoder::encode_csv($data);
break; break;
} }
$this->perfcounter->end("encode");
// 4: Send response // 4: Send response
@ -81,7 +88,7 @@ class ListReadingTypes implements IAction {
header("content-length: " . strlen($response)); header("content-length: " . strlen($response));
header("content-type: $response_type"); header("content-type: $response_type");
header("content-disposition: inline; filename=$response_suggested_filename"); header("content-disposition: inline; filename=$response_suggested_filename");
header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, $start_encode)); header("x-time-taken: " . $this->perfcounter->render());
echo($response); echo($response);
return true; return true;
} }

View file

@ -4,23 +4,24 @@ namespace AirQuality\Actions;
use \SBRL\TomlConfig; use \SBRL\TomlConfig;
use \AirQuality\PerfFormatter;
class Version implements IAction { class Version implements IAction {
/** @var TomlConfig */ /** @var TomlConfig */
private $settings; private $settings;
/** @var \SBRL\PerformanceCounter */
private $perfcounter;
public function __construct( public function __construct(
TomlConfig $in_settings) { TomlConfig $in_settings,
\SBRL\PerformanceCounter $in_perfcounter) {
$this->settings = $in_settings; $this->settings = $in_settings;
$this->perfcounter = $in_perfcounter;
} }
public function handle() : bool { public function handle() : bool {
global $start_time; global $start_time;
$start_handle = microtime(true);
// 1: Parse markdown // 1: Parse markdown
$result = file_get_contents(ROOT_DIR."version"); $result = file_get_contents(ROOT_DIR."version");
@ -29,7 +30,7 @@ class Version implements IAction {
// 2: Send response // 2: Send response
header("content-length: " . strlen($result)); header("content-length: " . strlen($result));
header("content-type: text/plain"); header("content-type: text/plain");
header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, null)); header("x-time-taken: " . $this->perfcounter->render());
echo($result); echo($result);
return true; return true;
} }

View file

@ -10,8 +10,16 @@ use Psr\Container\ContainerInterface;
*/ */
class Database class Database
{ {
// Whether the database connection should be read only or not. /** @var TomlConfig */
// Defaults to true, as we don't need to write _anything_ to the database. private $settings;
/** @var \SBRL\PerformanceCounter */
private $perfcounter;
/**
* Whether the database connection should be read only or not.
* Defaults to true, as we don't need to write _anything_ to the database.
* @var bool
*/
private const read_only = true; private const read_only = true;
private $connection; private $connection;
@ -23,13 +31,15 @@ class Database
\PDO::ATTR_AUTOCOMMIT => false \PDO::ATTR_AUTOCOMMIT => false
]; ];
function __construct(TomlConfig $in_settings) { function __construct(TomlConfig $in_settings, \SBRL\PerformanceCounter $in_perfcounter) {
$this->settings = $in_settings; $this->settings = $in_settings;
$this->perfcounter = $in_perfcounter;
$this->connect(); // Connect automagically $this->connect(); // Connect automagically
} }
public function connect() { public function connect() {
$this->perfcounter->start("dbconnect");
$this->connection = new \PDO( $this->connection = new \PDO(
$this->get_connection_string(), $this->get_connection_string(),
$this->settings->get("database.username"), $this->settings->get("database.username"),
@ -39,6 +49,7 @@ class Database
// Make this connection read-only // Make this connection read-only
if(self::read_only) if(self::read_only)
$this->connection->query("SET SESSION TRANSACTION READ ONLY;"); $this->connection->query("SET SESSION TRANSACTION READ ONLY;");
$this->perfcounter->end("dbconnect");
} }
public function query($sql, $variables = []) { public function query($sql, $variables = []) {

View file

@ -1,27 +0,0 @@
<?php
namespace AirQuality;
/**
* Formats performance data to be sent in a HTTP header.
*/
class PerfFormatter {
function __construct() {
}
public static function format_perf_data($start_init, $start_handle, $start_encode) {
$now = microtime(true);
$time_total = round(($now - $start_init) * 1000, 2);
$time_setup = round(($start_handle - $start_init) * 1000, 2);
$time_handle = round((($start_encode ?? $now) - $start_handle) * 1000, 2);
$time_encode = round(($now - $start_encode) * 1000, 2);
$result = "{$time_total}ms total | {$time_setup}ms setup, {$time_handle}ms handle";
if(!empty($start_encode))
$result .= ", {$time_encode}ms encode";
return $result;
}
}