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)