История одного рефакторинга, или о сказ о том как не надо разрабатывать на PHP…
Началось все как всегда, откуда-то появляется заказчик весь в слезах и трансе, орет не своим голосом: «Спасите, помогите, мой проект тормозит, от меня пользователи бегут. У вас неделя…»
Развернули проект на нашем сервере — н-да уж… Дальше был анализ сего творения фирмы «Qwerty123» (кто следит за мной, знает о какой украинской фирме речь), смех сквозь слезы, и истерика в течении недели. Ну для начала был взят XDEBUG и посмотрели профайлером что там и как (кликабельно):
Такую клевую штуку рисует Webgrind, удобно и наглядно. Другие тулзы для профайлера можно найти на их домашней странице, если же не хотите заморачиваться — то в PHPStorm есть встроенная тулза для просмотра :)
Т.е. у нас страница, которая выводит приветствие + выбор языка + выбор категории для просмотра (захардкоденных кстати) генерировалась за 4,6 секунды на сервере с Core2 Quad CPU Q6600@2.40GHz/16Gb (истерика и лучи ненависти конторе «Qwerty123»).
Куда более жутка картина ожидала нас на главной странице:
До применения Zend Framework’a нас отделяло еще несколько недель оптимизации г..кода…
FastTemplate такой «Fast»
Если присмотреться, то на предыдущем скрине видно, что очень дорого нам обходится некая функция parse_body()
:
Внутри нас ждет:
- вызов strtoupper — 56560 раз
- вызов str_replace — 56560 раз
Смотрим на код:
01 02 03 04 05 06 07 08 09 10 | // $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 кода, встречается у индусов и наших студентов:
1 2 3 4 5 6 7 | function n() { global $db , $CONSTANTS , $user , ...; // много одним словом echo $CONSTANTS [HOMEPAGE_BANNER1_ID]; // думаете это константа? echo $CONSTANTS [HOMEPAGE_BANNER2_ID]; // а знаете, что внутри? echo $CONSTANTS [HOMEPAGE_BANNER3_ID]; // 1, 2, 3, которые меняются совсем не там где объявляются О_о } |
Использование файловой системы вместо системы контроля версий:
1 2 3 4 5 6 | tpl |-- index.tpl |-- index2.tpl |-- index3.tpl |-- index__.tpl `-- index.44.tpl |
С базой данных тот же номер — таблицы categories_old, items_1 и т.д.
Если мальчик любит труд
тычет в книжку пальчик,
про такого пишут тут:
он хороший мальчик.
Это, как вы понимаете, не про наших «мальчиков», у наших 9 000 notices на главной. Да и manual’ами пользуются только слабаки:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | // мы не читаем мануалов 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 ; } |
Далее просто гениальное решение, тайный смысл этого творения я не осилил:
01 02 03 04 05 06 07 08 09 10 11 12 13 | $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; } |
Подсчет результатов поиска, что может быть проще:
1 2 3 4 5 6 7 8 | // перед выполнением запроса можно подсчитать кол-во результатов // воспользовавшись функцией 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 ; } |
Постраничная навигация, и это тоже можем:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // пошел запрос к БД $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):
1 2 3 4 5 6 | // в файле categories.sql.php // функция которая строит select для HTML $offset_string = '' ; for ( $i = 1; $i < $rec [ 'level' ]; $i ++) { $offset_string .= ' ' ; } |
Если нам надо обрезать строку на 100 символов, и при этом не кромсать слова то вот оно решение:
01 02 03 04 05 06 07 08 09 10 11 | $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
, и она же передается во все функции которые работают с БД:
1 2 3 4 5 6 7 8 | function db_query( $db , $sql ) {} function db_sql_query( $db , $sql ) {} function db_count( $db , $sql ) {} // и т.д. // но почему не так, ведь у нас одна БД function db_query( $sql ) { global $db ; } |
Пусть на море качка, но мы всегда прибережем обходные пути:
1 | $sql = "SELECT id, name, pasw from users where name = '$_POST[username]'" ; |
Про агрегирование в SQL мы не знаем:
1 2 3 4 | // $rows - записи из БД foreach ( $rows as $value ) { $total += $value [ "price" ]; } |
Необходимо SEO URL? Не проблема:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | 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 то можно было подучить:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | <!-- 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? --> |
Применение Zend_Cache
Кеш спасет мир, подумали мы и прикрутили его для всех SQL запросов (благо кто-то догадался написать единую функцию db_sql_query()
) и всех вызовов parse_body()
. Для начала попробовали кешировать в файлы, на тестовом сервере это помогло, на живом — нет. Причина — у нас так много мелких шаблонов (~200 для главной), что операции с файловой системой свели на нет прирост кеширования.
Вторая попытка оказалась более удачной, решили применить memcache — прирост скорости ~180%. Какой клевый показатель, но верен лишь в 100% попадании в кеш, таким образом перед нами вырисовывалась перспектива полного рефакторинга системы.
Немного клиентской оптимизации
Ну что тут можно рассказать, простое добавление следующих правил в .htaccess
сильно облегчило навигацию пользователям, которые хоть раз заходили на сайт:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | 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" < /ifModule > |
А далее все по порядку:
- Оптимизация изображений для web (есть такой пункт в Photoshop) — 40% на всех JPEG фалах
- Спрайты — кропотливая работа, десятки обращений к серверу можно свести к единицам
- Жмем JavaScript и CSS — ~50%
- И последним пунктом — nginx для всего этого добра
Zend Framework
А теперь расскажу о том, как проект медленно переезжает на Zend Framework. Начинается всё с простой проверки в index.php
(о да в нашей системе одна точка входа):
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // список модулей, которые уже отрефакторили $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 (); } } |
Если у нас не одна точка входа, то в каждом файле, которые были затронуты делаем простую вставку:
1 2 3 | $_SERVER [ 'REQUEST_URI' ] = str_replace ( $_SERVER [ 'PHP_SELF' ], '/admin/' , $_SERVER [ 'REQUEST_URI' ]); require 'loader.php' ; exit (); |
Что-бы забыть «глобальный» ужас, жизненно-необходимые переменный были закинуты в Zend_Registry
(а в дальнейшем закинуты в конфигурационный файл application.ini
, где им самое место).
Так же Zend_Translate
была скормлена таблица с переводами (см. адаптер array
)
Результат
Сложно судить о результате, проект находится в разработке, но вот несколько сравнительных замеров:
Time (old) | Time (re) | Time (ZF) | Size (old) | Size (ZF) | |
---|---|---|---|---|---|
Homepage | 4 663ms | 2 759ms | — | 699.5Kb | 288.0Kb |
Static pages | 3 115ms | 2 008ms | 295ms | 263.3Kb | 166.2Kb |
Item’s page | 3 082ms | 1 745ms | 180ms | 589.1Kb | 260.8Kb |
Еще наглядный скриншот среднего/максимального времени генерации страниц по датам: http://screencast.com/t/NDY1NGE5
P.S. Все имена вымышлены, совпадения случайны…
А итоговый результат какой? Было->стало.
не заметил, да, главное в рефакторинге итоговый результат?
можно было бы вообще не лезть в код и надеть на сайт WSS
что значит “надеть на сайт ВСС”?
мы там прикрутили простенький логгер, так что было->стало в динамике вот такой пока что:
http://screencast.com/t/NDY1NGE5
оно правда не очень отображает, т.к. по хорошему надо было бы сравнивать только оптимизированные страницы с неоптимизированными.
+ логгер был прикручен уже после добавления мемкешеда, а до того среднее время генерации страницы было ~30 секунд.
WSS – WEBO Site Speedup
Искренне сочувствую…
А каков итог ускорения?
Со скольких на сколько стало быстрее?
зы: код пахнет индусами, им за количество строк платят
Да, действительно, сейчас выложу сравнительную таблицу.
P.S. Клепалось нашими…
Помоему от кода за версту несет украинусами. И удивительного ничего нет,- пока что, всё ещё, именно так и выглядит 90% кода изготавливаемого вполне себе коммерчески успешными компании на территории ссср.
Хотя почитать про сам процесс рефакторинга было очень даже интересно, спасибо.
вот пример рекурсивного обхода директории в одну строку
new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory), true);
сколько это занимает со многими строками описывать не буду.
так что, я получается меньше получу… бред какой-то…. А скорость, а опыт…
Что-то скриншоты при ресайзе совсем нечитабельные стали.
Я правильно понимаю что “Q” – это вполне себе такая коммерчески успешная контора?
Хотя я бы не переживал. Чем больше таких контор на рынке, тем более оплачиваемой будет работа у хороших спецов :)
А откуда такой вывод про хороших спецов?
одним словом – чернуха
Вот есть статейка – PHP-разгон:_серебряная_пуля_из_автомата_Комменца-Вальтера»_(Commentz-Walter), с таким количеством str_replace можно попробовать, а для вот этих самых переменных самое оно. Если попробуешь – напиши, я в бою еще не использовал.
Вам ребята молоко за вредность надо. Санитары говнокода
не молоко! а кило баксы!
Со слов “Zend Framework” я был убит. Лично для меня понятия ЗФ и производительность – несовместимые)
Довольно странно слышать об «оптимизации изображений для web», когда скриншоты таблиц в этой же статье приводятся в формате JPEG. ;-)
Та да, фигня какае-то…
Я недавно тоже оптимизировал некий шаблонизатор, который тоже в цикле юзал str_replace:
while
() {
$result
.=
str_replace
(
$key
,
$val
,
$result
);
}
после того как я переделал на нечто вида:
while
() {
$keys
[] =
$key
;
$values
[] =
$val
;
}
$result
=
str_replace
(
$keys
,
$values
,
$result
);
Производительность улучшилась на 50% наверное, уже не помню… но больше 10%
Индусский говнокод цветет и пахнет. Не завидую вам ребята, так как сам не так давно проводил рефакторинг кода одной знаменитой украинской компании, после которого производительность выросла на 57%.
Антон, большое спасибо за статью!!!
Я давно так не ржал! Это действительно, настоящий пятничный пост )))
Грустно. Очень грустно.
Но мне интересно – вы знаете что во многих компаниях нет code review?
Вы знаете что в маленьких компаниях часто нет отделов QA?
Вы знаете что в маленьких компаниях часто есть заказчики, которые не хотят много платить а хотят “проект и быстро!” – и поэтому берётся старый код, быстро-быстро натягивается новый дизайн и продаётся.
Оптимизация? Когда мной была упомянута оптимизация, мой знакомый фрилансер с большим опытом выпучил глаза – а зачем ты её делаешь, тебе за неё не заплатят! Моё объяснение что “я её делаю потому что я не хочу чтобы мне было стыдно за мою работу” его не убедило, он всё-таки хотел покрутить пальцем у виска.
Почитайте любых классиков программирования. Вот не помню сейчас где именно, по-моему в “Совершенном коде” у МакКонелла написано: “Программа не должна быть идеальной. Как бы вам не хотелось сделать её идеальной, это неразумно. Программа просто должна удовлетворять клиента/быть достаточной/выполнять те функции которые запрошены.”
Несомненно Макконнелл прав, но в данном случае программа перестала удовлетворять клиента (если вообще когда-то удовлетворяла). Несомненно и Вы правы, что практически нигде нет code review и других активностей, направленных на повышение качества кода, и наличие такого рода проектов не удивительно… Но удивителен настолько некачественный код, настолько “своеобразные” решения, да и цель поста, по-моему мнению – показать, как можно хоть немного исправить это.
Также есть очень хорошее видео от Дяди Боба из которого следует, что сознательно писать плохой код просто не профессионально.
Дякую, дуже цінний досвід.
З Webgrind можливо створити граф переходів по функціях?
Антон, а имя название компании озвучьте) А то очень оригинальные решения применяют.
Под конец рабочего валяюсь под столом)))
Спасибо большое)
маловастенький прирост, это всего в пару раз чтоли? кешируйте дальше, хотя бы до 0.5 главную догоните, а то несерьезно :)
а дерьмокодеров да, хватает :(
Весело конечно:) Когда читаю подобного рода творения, удивляет как они до такого додумывались? Некоторые вещи не подвластны нормальной логике и им точно не учат в статьях и книжках.
margin, что такое margin?
насчет ПХП помолчу, сам 3-й месяц вебдизайном занимаюсь, а вот эта фраза из неразрывных пробелов зацепила за душу – примерно так выглядел мой первый личный сайт :) Судя по посту, меня ждет великое вебдизайнерское будущее, осталось открыть фирму S и захватить мировой рынок :)
узнаю оскомерц во всей свое “красе”:)
HTML-“код” просто таки доставил (((: class-style…да они просто ниндзя!) Идея достойная – объединить и класс и стиль ))
Спасибо Вам интересная статья.