Hack and Secure
Главная
Вход
Регистрация
Пятница, 19.06.2026, 15:52Приветствую Вас Бомж | RSS
Меню

Категории
Лог [19]
В данной ленте мы собираем наиболее интересные новости сторонних источников.

Статистика

Онлайн всего: 1
Гостей: 1
Пользователей: 0

Вход

Главная » 2010 » Август » 20 » Facing refucktoring
14:59
Facing refucktoring



История одного рефакторинга, или о сказ о том как не надо разрабатывать на PHP…

Началось все как всегда, откуда-то появляется заказчик весь в слезах и трансе, орет не своим голосом: «Спасите, помогите, мой проект тормозит, от меня пользователи бегут. У вас неделя...»

Развернули проект на нашем сервере — н-да уж… Дальше был анализ сего творения фирмы «Q», смех сквозь слезы, и истерика в течении недели. Ну для начала был взят XDEBUG и посмотрели профайлером что там и как (кликабельно):


Такую клевую штуку рисует Webgrind, удобно и наглядно


Т.е. у нас страница, которая выводит приветствие + выбор языка + выбор категории для просмотра (захардкоденных кстати) генерировалась за 4,6 секунды на сервере с Core2 Quad CPU Q6600@2.40GHz/16Gb (истерика и лучи ненависти конторе «Q»).

Куда более жутка картина ожидала нас на главной странице:


До применения Zend Framework'a нас отделяло еще несколько недель оптимизации г… кода…

FastTemplate такой «Fast»



Если присмотреться, то на предыдущем скрине видно, что очень дорого нам обходится некая функция parse_body:



Внутри нас ждет:

  • вызов strtoupper — 56560 раз
  • вызов str_replace — 56560 раз


Смотрим на код:
// $content - шаблон
// $rec - переменные
$content = preg_replace("/\\$([A-Z][A-Z0-9_]+)/", "@\\1@", $content);
if (is_array($rec)) {
    foreach ($rec as $key => $value) {
        $name = strtoupper($key);
        $content = str_replace("@$name@", "$value", "$content");
    }
}
return $content;


Т.е. в самом начале мы ищем в шаблонах что-то вроде $HTTP_PATH, заменяем это на @HTTP_PATH@, потом перебором пытаемся заменить все известные переменные. Есть одна проблема — шаблонов у нас много, в них используется совсем чуть-чуть из пары сотен переменных. Простая замена str_replace на preg_replace_callback дала прирост в пару секунд (т.е. около 10%).

Примеры кода


Слабонервным лучше пропустить данный абзац.

Классика плохого PHP кода, встречается у индусов и наших студентов:
function n() {
   global $db, $CONSTANTS, $user, ...; // много одним словом
 
   echo $CONSTANTS[HOMEPAGE_BANNER1_ID];  // думаете это константа?
   echo $CONSTANTS[HOMEPAGE_BANNER2_ID];  // а знаете, что внутри?
   echo $CONSTANTS[HOMEPAGE_BANNER3_ID];  // 1, 2, 3, которые меняются совсем не там где объявляются О_о
}


Использование файловой системы вместо системы контроля версий:
tpl
|-- index.tpl
|-- index2.tpl
|-- index3.tpl
|-- index__.tpl
`-- index.44.tpl


С базой данных тот же номер — таблицы categories_old, items_1 и т.д.

Если мальчик любит труд
тычет в книжку пальчик,
про такого пишут тут:
он хороший мальчик.


Это, как вы понимаете, не про наших «мальчиков», у наших 9 000 notices на главной. Да и manual'ами пользуются только слабаки:
// мы не читаем мануалов
while ($row = mysql_fetch_array($res)) {
      if ($row) {
            foreach ($row AS $key => $field) {
                if (ereg("^[0-9]+", $key)) {
                    unset($row[$key]);
                }
            }
      }
      $rows[] = $row;
}
// если чуть-чуть допилить
// мелочь, конечно, но прирост ~0,1 сек т.к. имеет место 23162 вызовов
while ($row = mysql_fetch_array($res, MYSQL_ASSOC)) {
      $rows[] = $row;
}


Далее просто гениальное решение, тайный смысл этого творения я не осилил:
$sqls[] = $sql;
if (is_array($sqls)) {
    foreach ($sqls AS $ssql) {
        if ($ssql) {
            $res = mysql_db_query($db['name'], $ssql, $db['id']);
            if (!$res) {
                return 0;
            }
        }
    }
} else {
    return 0;
}


Подсчет результатов поиска, что может быть проще:
// перед выполнением запроса можно подсчитать кол-во результатов
// воспользовавшись функцией db_count (вызывается в 96 разных местах)
function db_count($sql) {
    global $db;
    $res = mysql_db_query($db['name'], $sql, $db['id']);
    $result = mysql_num_rows($res);
    return $result;
}


Постраничная навигация, и это тоже можем:
// пошел запрос к БД
$res = mysql_db_query($db['name'], $sql, $db['id']);
// подсчитали итого (хотя до этого уже был вызван db_count)
$row_count = mysql_num_rows($res);
// подсчитали сколько у нас страниц получается
$page_count = floor($row_count / $pager['per_page'] + 1);
 
// это offset
$bi = ($pos - 1) * $pager['per_page'];
 
// теперь выбираем только нужные записи
for ($i = $bi; $i < $bi + $pager["per_page"]; $i++) {
    if ($i >= $row_count) {
        break;
    }
 
    if (!mysql_data_seek($res, $i)) {
        break;
    }
 
    if (!($row = mysql_fetch_assoc($res))) {
        break;
    }
    // складируем результат
    $new_rows[] = $row;
}


Повторение строк — мы не ищем легких путей (str_repeat):
// в файле categories.sql.php
// функция которая строит select для HTML
$offset_string  = '';
for ($i = 1; $i < $rec['level']; $i++) {
    $offset_string .= '&nbsp;&nbsp;&nbsp;&nbsp;';
}


Если нам надо обрезать строку на 100 символов, и при этом не кромсать слова то вот оно решение:
$data['row']['description'] = substr($description, 0, 100);
 
$i = 100;
while (!($description[$i] == " " || $description[$i] == "_") && $i < strlen($description)):
    $data['row']['description'] .= $description[$i];
    $i++;
endwhile;
 
if ($i < strlen($description)) {
    $data['row']['description'] .= "...";
}


У нас так много глобальных переменных, там есть конечно $db, и она же передается во все функции которые работают с БД:
function db_query($db, $sql) {}
function db_sql_query($db, $sql) {}
function db_count($db, $sql) {}
// и т.д.
// но почему не так, ведь у нас одна БД
function db_query($sql) {
    global $db;
}


Пусть на море качка, но мы всегда прибережем обходные пути:
$sql = "SELECT id, name, pasw from users where name = '$_POST[username]'";


Про агрегирование в SQL мы не знаем:
// $rows - записи из БД
foreach ($rows as $value) {
      $total += $value["price"];
}


Необходимо SEO URL? Не проблема:
switch ($params[1]):
       case "usageagreement": 
            $page_id = 13;
       break; 
       case "privacypolicy": 
            $page_id = 14;
       break; 
       case "termsandconditions": 
            $page_id = 15;
       break; 
       case "affiliates": 
            $page_id = 22;
       break; 
       case "aboutus": 
            $page_id = 19; 
       break; 
endswitch;


Ладно с PHP, но HTML то можно было подучить:
<!-- id такой id -->
<div id="banner">...</div>
<div id="banner">...</div>
<div id="banner">...</div>
<div id="banner">...</div>
 
<!-- class это почти style -->
<li class="padding-left:15px;">...</li>
 
<!-- табличная верстка -->
<!-- хотя не стоит 10 вложенных таблиц расписывать -->
 
<!-- margin, что такое margin? -->
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;


Применение Zend_Cache


Кеш спасет мир, подумали мы и прикрутили его для всех SQL запросов (благо кто-то догадался написать единую функцию db_sql_query) и всех вызовов parse_body. Для начала попробовали кешировать в файлы, на тестовом сервере это помогло, на живом — нет. Причина — у нас так много мелких шаблонов (~200 для главной), что операции с файловой системой свели на нет прирост кеширования.

Вторая попытка оказалась более удачной, решили применить memcache — прирост скорости ~180%. Какой клевый показатель, но верен лишь в 100% попадании в кеш, таким образом перед нами вырисовывалась перспектива полного рефакторинга системы.

Немного клиентской оптимизации


Ну что тут можно рассказать, простое добавление следующих правил в .htaccess сильно облегчило навигацию пользователям, которые хоть раз заходили на сайт:

FileETag MTime Size

AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/x-javascript

<ifModule mod_expires.c>
ExpiresActive On
ExpiresDefault "access plus 1 seconds"
ExpiresByType text/html "access plus 1 seconds"
ExpiresByType image/x-icon "access plus 2592000 seconds"
ExpiresByType image/gif "access plus 2592000 seconds"
ExpiresByType image/jpeg "access plus 2592000 seconds"
ExpiresByType image/png "access plus 2592000 seconds"
ExpiresByType text/css "access plus 604800 seconds"
ExpiresByType text/javascript "access plus 216000 seconds"
ExpiresByType application/x-javascript "access plus 216000 seconds"


А далее все по порядку:
  • Оптимизация изображений для web (есть такой пункт в Photoshop) — 40% на всех JPEG фалах
  • Спрайты — кропотливая работа, десятки обращений к серверу можно свести к единицам
  • Жмем Javascript и CSS — ~50%
  • И последним пунктом — nginx для всего этого добра


Zend Framework


А теперь расскажу о том, как проект медленно переезжает на Zend Framework. Начинается всё с простой проверки в index.php (о да в нашей системе одна точка входа):

// список модулей, которые уже отрефакторили
$modules = array (
    '/search/',
    '/about/'
);
 
$path = $_SERVER['REQUEST_URI'];
 
// нас устроила такая простая проверка, 
// но запрос вида /search/?... уже не будет обрабатываться
if(in_array($path, $modules)) {
    // подключаем ZF (внутри стандартный код из сгенерированного public/index.php)
    require 'loader.php';
    exit();
}
 
// а эта будет
foreach ($modules as $module) {
    if (strpos($path, $module) === 0) {
        require 'loader.php';
        exit();
    }
}


Если у нас не одна точка входа, то в каждом файле, которые были затронуты делаем простую вставку:
$_SERVER['REQUEST_URI'] = str_replace($_SERVER['PHP_SELF'], '/admin/', $_SERVER['REQUEST_URI']);
require 'loader.php';
exit();


Что-бы забыть «глобальный» ужас, жизненно-необходимые переменный были закинуты в Zend_Registry (а в дальнейшем закинуты в конфигурационный файл application.ini, где им самое место).

Так же Zend_Translate была скормлена таблица с переводами (см. адаптер array)

Результат


Сложно судить о результате, проект находится в разработке, но вот несколько сравнительных замеров:


Время генерации Время генерации (re) Время генерации (ZF) Объем страницы Объем страницы (ZF)
Главная страница 4 663ms 2 759ms 699.5Kb 288.0Kb
Статические страницы 3 115ms 2 008ms  295ms 263.3Kb 166.2Kb
Cтраница айтема 3 082ms 1 745ms  180ms 589.1Kb 260.8Kb


Еще наглядный скриншот среднего/максимального времени генерации страниц по датам: http://screencast.com/t/NDY1NGE5

UPDATE: Извините за тон статьи, но как-то накипело-то. Если кому-то кажется, что цель статьи показать кто тут белый и пушистый, а кто иной — то нет, основная задумка — поделиться опытом, показать примеры «плохого» кода, и не имеет значения кто его написал, главное чтобы каждый извлек для себя урок, и подобного кода становилось меньше…
Категория: Лог | Просмотров: 730 | Добавил: sepos | Теги: refactoring, php, zend framework | Рейтинг: 5.0/1
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Поиск

Календарь
«  Август 2010  »
Пн Вт Ср Чт Пт Сб Вс
      1
2345678
9101112131415
16171819202122
23242526272829
3031

Архив

Друзья
Юридические услуги. Представительство в судах. Возврат долгов. Все о FreeBSD и прочих UNIX-like операционных системах Чоткие загрузки для успешных пацанов WE SELL LOADS SMS сервис для Вашего Бизнеса Единая база кидал Elite VPN Black SEO Hash Resolver Каталог сайтов OpenLinks.RU 495ru.ru, Доски объявлений, бесплатные объявления, дать объявление в интернет, подать объявление бесплатно
  • Место для вашей рекламы

  • //-->


    Copyright Hack and Secure Team © 2026