AST変換を使用したCodeigniterフレームワークでのプレゼンテーションテンプレートの最適化

最近、私はポータルで働き、Codeigniterで書かれた月に約10万人が出席しました。 すべては問題ありませんが、このポータルのページはサーバーによって少なくとも3秒間与えられました。 同時に、鉄はもはや拡張する場所ではなかったため、アプリケーションのアーキテクチャについては説明しません。 最小限のコード変更でアプリケーションの応答時間を短縮するのに役立つソリューションを見つける必要がありました。







背景



Codeigniterは、間違いなくWebアプリケーションの優れたフレームワークです。 軽量で柔軟性があり、非常に簡単に習得できます。







しかし、いくつかの問題があります。 その1つは、ビューのハンドラーがないことです。 純粋なphp(小さなCodeigniter挿入)がテンプレートエンジンとして使用されます。







多くはこれは問題ではないと言うでしょうが、利点-ページに出力する前の前処理の欠如は、アプリケーションからの応答時間を大幅に短縮することができます。







実際、テンプレートエンジンの大きな利点は、テンプレートをコンパイルし、その後の処理プロセスの通過のためにディスクにキャッシュできることです。 つまり、テンプレートが頻繁に変更されない場合、テンプレートエンジンを使用すると、少なくとも1つのプラスの利便性が得られます。 テンプレートが多数ある場合は、ここにキャッシュが追加されます。 他の開発者がどのように働いているかはわかりませんが、可能な場合はテンプレートエンジンを使用することを好みます。







問題



小さなプロジェクトにCodeigniterを使用する場合、ほとんどの場合、テンプレートに問題はありません。 しかし、プロジェクトが数百のテンプレートに成長すると、テンプレートのレイアウトが遅いことに悩まされます。







私の場合はそうでした-ページの読み込み時に接続されたテンプレートファイルの数は50に達しました(組み込みのget_included_files



関数からの情報)。







エクスペリエンスのために選択したページは、次の外観を持ち、サイトで最もロードされています。







画像







このページには30の要素のリストが表示されます。レストランとそれらに関するさまざまな種類の情報で、それぞれが+-35のテンプレートで構成されています。 phpはテンプレートエンジンとしてのみ使用されるため、キャッシュはありません。 その結果、約900のテンプレートを作成する必要があります。







テンプレートを使用する前に、最小限のコード最適化の助けを借りて、ページ出力時間を1秒(30%)短縮して+ -2秒にすることができました。







 Loading Time: Base Classes 0.0274 Controller Execution Time 1.9403 Total Execution Time 1.9687
      
      





まだ多すぎた







解決策



約900のテンプレートのレイアウトは、特にphpでは高価であることは明らかです。 したがって、ページが要求されるたびにこれを行わないように、これらのテンプレートをすべて1つに「接着」する必要がありました。







twig



smarty



ような既製のテンプレートエンジンの使用は、すべてのコントローラーを書き換える必要があり、多くのテンプレートがあるため、すぐに消えました。







当時、私はすでにASTツリーに少し慣れていました。 テンプレートは、次の形式で何かを表しました。







 ... <div class="brand-block"> <?php $this->load->view('payment_block', array('brand' => $brand); ?> <?php $this->load->view('minimal_block', array('brand' => $brand)); ?> <?php $this->load->view('deliverytime_block', array('brand' => $brand)); ?> <?php if (!$edit): ?> <?php $this->load->view('deliveryprice_block', array('criteria' =>$criteria); ?> <?php endif; ?> </div> ...
      
      





建設業







 $this->load->view(string $templatePath,array $params)
      
      





追加のパラメーターを渡すことで「include」を行います$params





タスクの本質は、そのような呼び出しをすべてテンプレート自体のコンテンツに置き換え、 inline



パラメーターをそれらに渡すことでした。 再帰的に。







おもしろいことに、私はすでに1 つあるツール、 Nikic PHP-Parserを考えて取り上げました。 これは非常に強力なツールであり、コードの抽象構文ツリーであらゆる種類の操作を行い、変更されたツリーをPHPコードに保存し直すことができます。 そして、これはすべてPHP自体で行うことができます-パーサーはc-extensionsに依存せず、PHP 5.2+で動作します。







実装



PHP-Parserは、ASTを操作するための便利なツールを提供します。NodeVisitorおよびNodeTraverserインターフェイスは、オプティマイザーを構築するために使用します。







主なことは、 load



クラスのプロパティでview



メソッドへのすべての呼び出しを見つけて、 load



する必要があるテンプレートの種類を理解することです。 これはNodeVisitorを使用して実行できます。 NodeTraverser



がASTツリーノードを「残す」ときにNodeTraverser



れるメソッドleaveNode(Node $node)



NodeTraverser



があります。







 class MyNodeVisitor extends NodeVisitorAbstract { public function leaveNode(Node $node) { //    -      if ($node instanceof Node\Expr\MethodCall) { // ,       if ($node->name == 'view') { //          //     Codeigniter'a,      //    :) //   ,       . //   -  ,    //       ,        if ($node->args[0]->value instanceof \PhpParser\Node\Scalar\String_) { //    ,       $code = md5(mt_rand(0, 7219832) . microtime(true)); $node->name = 'to_be_changed_' . $code; $params = null; //  ,      `inline` if (count($node->args) > 1) { if ($node->args[1]->value instanceof Node\Expr\Array_) { $params = new Node\Expr\Array_($node->args[1]->value->items, [ 'kind' => Node\Expr\Array_::KIND_SHORT, ]); } else { if ($node->args[1]->value->name != 'this') { $params = $node->args[1]->value; } } } //  ,       //        $this->nodesToSubstitute[] = new TemplateReference($this->nodeIndex, $node->args[0]->value->value, $params, $code); } } ...
      
      





したがって、置き換える必要のあるすべての要素を選択できます。 明示的なrequire、includeなど、他の要素も置き換えることができます。







内陸で再帰的に交換する必要があることを忘れないでください。 これを行うには、テンプレート内部が置換される正確な場所でPHP-Parserのラッパーを作成する必要があります。







ハンドラーコード
 class CodeigniterTemplateOptimizer { private $optimizedFiles = []; private $parser; private $traverser; private $prettyPrinter; private $factory; private $myVisitor; private $templatesFolder = ''; public function __construct(string $templatesFolder) { $this->parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP5); $this->traverser = new MyNodeTraverser(); $this->prettyPrinter = new PrettyPrinter\Standard(); $this->factory = new BuilderFactory(); $this->templatesFolder = $templatesFolder; $this->myVisitor = new MyNodeVisitor(); $this->traverser->addVisitor($this->myVisitor); } public function optimizeTemplate(string $relativePath, $depth = 0, $keepOptimizing = true) { if (substr($relativePath, -4, 4) !== '.php') { $relativePath .= '.php'; } if (!isset($this->optimizedFiles[$relativePath])) { $templatePath = $this->templatesFolder . $relativePath; if (file_exists($templatePath)) { $templateOffset = 0; $notOptimized = file_get_contents($templatePath); //    AST $stmts = $this->parser->parse($notOptimized); if ($keepOptimizing) { $this->myVisitor->clean(); $this->traverser->setCurrentWorkingFile($relativePath); //     AST $stmts = $this->traverser->traverse($stmts); //       MyNodeVisitor $inlineTemplateReference = $this->myVisitor->getNodesToSubstitute(); ++$depth; $stmsBefore = count($stmts); foreach ($inlineTemplateReference as $ref) { //   -     $nestedTemplateStatements = $this->optimizeTemplate($ref->relativePath, $depth); $subtempalteLength = count($nestedTemplateStatements); $insertOffset = $ref->nodeIndex + $templateOffset; $pp = new PrettyPrinter\Standard(); //     `inline`:    `extract` if ($ref->paramsNodes) { array_unshift($nestedTemplateStatements, new Node\Expr\FuncCall(new Node\Name('extract'), [$ref->paramsNodes])); } //    ,      if (get_class($stmts[$insertOffset]) === 'PhpParser\Node\Expr\MethodCall' && ($stmts[$insertOffset]->name === "to_be_changed_" . $ref->code)) { //   ""    AST //    if(1),       $stmts[$insertOffset] = new Node\Stmt\If_(new Node\Scalar\LNumber(1), [ 'stmts' => $nestedTemplateStatements ]); } else { //     ,    ast } } } //    "" . //         $this->optimizedFiles[$relativePath] = $stmts; } else { throw new Exception("File not exists `" . $templatePath . "` when optimizing templates"); } } //    return $this->optimizedFiles[$relativePath]; } public function writeToFile(string $filePath, $nodes) { $code = $this->prettyPrinter->prettyPrintFile($nodes); // create directories in a path if they not exists if (!is_dir(dirname($filePath))) { mkdir(dirname($filePath), 0755, true); } // write to file file_put_contents($filePath, $code); } }
      
      





それだけです、オプティマイザーを実行します:







  //      -    $optimizer = new CodeigniterTemplateOptimizer('./views/'); //      $optimizer->writeToFile($to, $optimizer->optimizeTemplate($from));
      
      





DirectoryIterator



を使用すると、テンプレートフォルダー全体を最適化するスクリプトを2分で構築できます。







結論と結果



テンプレートを最適化されたテンプレートに置き換えた後、実行時間を1秒以上短縮できました。Codeigniterプロファイラーは次のようになります。







 Loading Time: Base Classes 0.0229 Controller Execution Time 0.7975 Total Execution Time 0.8215
      
      





テンプレートの最適化の助けを借りて、PHPコードの最適化よりも多くの時間を短縮できました。 テンプレートを最適化するコストは、多くのコード行を変更することと比較できません。 また、テンプレートの最適化によってアプリケーションの動作が変更されることはありません(これは単に「接着」です)。これは非常に肯定的な事実です。







この記事に記載されているコードスニペットは、理解するのに役立つ一般的なポイントをガイドするためのものであり、動作するふりをするものではありません。








All Articles