PROFILING
por Thiago Rodrigues (xthiago)
Uma técnica eficiente para encontrar gargalos e otimizar sua aplicação
SOBRE
THIAGO RODRIGUES (xthiago)
PHPSP
UPX TECHNOLOGIES
PHP
https://xthiago.com
+ de uma década
evangelista
UEMG
Sistemas de Informação
Diretor de Desenvolvimento
SOCIAL HUB
Por que
otimizar?
-
Aumentar taxas de interação e conversão
-
diminuir abandono
-
Utilizar menos recursos para fazer mais (eficiência)
-
VIABILIZAR A SOLUÇÃO
os 3 vilões da
OTIMIZAÇÃO
Não otimizar
1.
- não se importar com o usuário e o negócio
- "Está na nuvem. É só apertar um botão para escalar."
otimizações inúteis
2.
- if ao invés de switch
- aspas ao invés de apóstrofos
- for ao invés de foreach
otimizações precoces
3.
- complexidade desnecessária
- atrasos nas entregas
- desperdício de tempo
quando
otimizar?
quando
otimizar?
quando houver uma razão
(VOCÊ E SEU TIME SABERÃO)
COMO
otimizar?
COMO
otimizar?
aplicando o princípio de pareto para eliminar gargalos de forma iterativa
o mindset
1. Ache o maior gargalo
2. tente eliminá-lo
3. repita até ficar satisFEITO
Se não conseguir, OK
e como
achar gargalos com eficiÊncia?
Profiling
profiling?
É uma técnica de análise de software que visa mensurar vários parâmetros durante a execução do software para permitir otimizações
- tempo de execução
- quantidade de chamadas
- consumo de memória
Os softwares que capturam tais informações são conhecidos como PROFILER
OS DADOS COLETADOS em uma sessão de profiling ESTÃO PARA O PROGRAMADOR ASSIM COMO OS EXAMES ESTÃO PARA O MÉDICO
HORA DO
SHOW!
PROBLEMA: REDUZIR TEMPO DE EXECUÇÃO DA EXPORTAÇÃO DE comentários em csv
4.500 COMENTÁRIOS
Symfony/Demo com:
- PHP 7.2
- Doctrine
- SQLite
Docker
Profiler: BLACKFIRE
FROM upxlabs/php-fpm:latest
RUN version=$(php -r "echo PHP_MAJOR_VERSION.PHP_MINOR_VERSION;") \
&& curl -A "Docker" -o /tmp/blackfire-probe.tar.gz \-D - -L -s \
https://blackfire.io/api/v1/releases/probe/php/alpine/amd64/$version \
&& tar zxpf /tmp/blackfire-probe.tar.gz -C /tmp \
&& mv /tmp/blackfire-*.so \
$(php -r "echo ini_get('extension_dir');")/blackfire.so && printf \
"extension=blackfire.so\nblackfire.agent_socket=tcp://blackfire:8707\n" \
> $PHP_INI_DIR/conf.d/blackfire.ini
instalando o probe
Dockerfile
version: "2"
services:
phpfpm:
build:
context: ./docker/phpfpm
dockerfile: Dockerfile
volumes:
- ./docker/phpfpm/php.ini:/usr/local/etc/php/php.ini
- ./symfony-demo/:/var/www/app/
links:
- blackfire
blackfire:
image: blackfire/blackfire
environment:
BLACKFIRE_CLIENT_ID: 'XXXX'
BLACKFIRE_CLIENT_TOKEN: 'XXXX'
BLACKFIRE_SERVER_ID: 'XXXX'
BLACKFIRE_SERVER_TOKEN: 'XXXX'
nginx:
image: nginx:1.13.8-alpine
ports:
- "8000:80"
volumes:
- ./docker/nginx/vhost.conf:/etc/nginx/conf.d/default.conf
links:
- phpfpm
configurando o agent
docker-compose.yml
fazendo profiling no chrome
Interface do blackfire
GET http://app.localhost:8000/index.php/pt_BR/blog/comments/export
comment_id, comment_content, comment_published_at, comment_title
4000, Adorei! [..], 2018-03-04 23:55:15, Palestra
4050, Muito bom! [..], 2018-03-04 23:55:15, PHP Experience
4100, Quero ouvir: PHP! [..], 2018-03-04 23:55:15, PHP
// +4.500 linhas
ENDPOINT ALVO DA OTIMIZAÇÃO
class Comment {
/**
* @var int
*/
private $id;
/**
* @var Post
*/
private $post;
/**
* @var string
*/
private $content;
/**
* @var \DateTime
*/
private $publishedAt;
}
entidades v1
class Post {
/**
* @var int
*/
private $id;
/**
* @var Comment[]
*/
private $comments;
// outros campos
}
class BlogController extends AbstractController
{
public function commentsExport(CommentRepository $commentRepo): Response {
$comments = $commentRepo->findComments();
$data = "";
foreach ($comments as $comment) {
$data .= sprintf(
"%s,%s,%s,%s\n",
$comment->getId(),
$comment->getContent(),
$comment->getPublishedAt()->format('Y-m-d H:i:s'),
$comment->getPost()->getTitle()
);
}
$response = new Response($data);
$response->headers->set('Content-Type', 'application/csv');
$response->headers->set('Content-Disposition', 'attachment; filename="file.csv"');
return $response;
}
controlador v1
class CommentRepository extends ServiceEntityRepository
{
public function findComments(): array
{
$query = $this->getEntityManager()
->createQuery('
SELECT c
FROM App:Comment c
ORDER BY c.publishedAt DESC
')
;
return $query->getResult();
}
repositório v1
analisando v1
análise v1
2s em classe de Debug? 😠
Autoloader procurando classes do sistemas de arquivos
INSIGHTS
# Variáveis de ambiente definidas para produção
APP_ENV=prod
APP_DEBUG=0
# Cria autoloader otimizado para prod:
composer dump-autoload --classmap-authoritative
# Limpa e aquece o cache:
php bin/console cache:clear --env=dev --no-debug --no-warmup
php bin/console cache:warmup --env=dev
1ª rodada de ajustes - v2
comparando v1 e v2
análise v2
45% do tempo na query? 🤔
INSIGHTS
class CommentRepository extends ServiceEntityRepository
{
public function findComments(): array
{
$query = $this->getEntityManager()
->createQuery('
SELECT c
FROM App:Comment c
INNER JOIN c.post p
ORDER BY c.publishedAt DESC
')
;
return $query->getResult();
}
2ª rodada de ajustes - v3
análise v3
41% do tempo populando objetos? 🤔
INSIGHTS
Comparado com V2
COMPARANDO V2 E V3
class CommentRepository extends ServiceEntityRepository {
public function findComments(): array {
$query = $this->getEntityManager()->createQuery('
SELECT c.id, c.content, c.publishedAt, p.title
FROM App:Comment [continuação...]
');
// resto do método ...
}}
class BlogController extends AbstractController {
public function commentsExport(CommentRepository $commentRepo): Response {
// ...
foreach ($comments as $comment) {
$data .= sprintf(
"%s,%s,%s,%s\n",
$comment['id'],
$comment['content'],
$comment['publishedAt']->format('Y-m-d H:i:s'),
$comment['title']
);
}
// ...
}}
3ª rodada de ajustes - v4
análise v4
Em termos de velocidade, OK. Mas esse consumo de RAM ai hein? 🤔
INSIGHTS
Comparado com V1
Comparado com V3
resultado v4
class CommentRepository extends ServiceEntityRepository {
public function findComments(): iterable {
// montagem da query ...
return $query->iterate();
}}
4ª rodada de ajustes - v5
análise v5
Foi reduzido o consumo de memória.
E se retornarmos um stream?
INSIGHTS
Comparado com V4
class BlogController extends AbstractController {
public function commentsExport(CommentRepository $commentRepo): Response {
$comments = $commentRepo->findComments();
$response = new StreamedResponse();
$response->setCallback(function() use (&$comments) {
foreach ($comments as $comment) {
$handler = fopen('php://output', 'r+');
$comment = array_shift($comment);
fputs(
$handler,
sprintf(
"%s,%s,%s,%s\n",
$comment['id'],
$comment['content'],
$comment['publishedAt']->format('Y-m-d H:i:s'),
$comment['title']
)
);
}
});
return $response;
}}
5ª rodada de ajustes - v6
análise v6
E esse hydrate do Doctrine ai? Quase 25%.
INSIGHTS
Comparado com V5
Comparado com V1
class CommentRepository extends ServiceEntityRepository {
public function findComments(): iterable
{
$connection = $this->getEntityManager()->getConnection();
$sql = <<<EOF
SELECT
c.id, c.content, c.published_at, p.title
FROM symfony_demo_comment c
INNER JOIN symfony_demo_post p ON c.post_id = p.id
ORDER BY c.published_at DESC
EOF;
$stmt = $connection->prepare($sql);
$stmt->execute();
return $stmt->getIterator();
}
}
6ª rodada de ajustes - v7
análise v7
Comparado com V6:
Comparado com V1:
considerações para melhores resultados
-
Executar o profiling em um servidor separado
-
executar várias vezes para cada versão e obter a média
outras ferramentas de profiling
-
xdebug + [K|q]cachegrind ou webgrind
-
Tideways xhprof (fork)
- Z-RAY
-
ferramentas para análise de syscalls e opcodes
Muito
obrigado!
Meu contato:
Avalie a palestra:
(slides já disponíveis na URL acima)