From 1901fec6c5a7d51270ec19b4a087cd8468434641 Mon Sep 17 00:00:00 2001
From: Jared Hancock <jared@osticket.com>
Date: Thu, 14 Aug 2014 15:51:29 -0500
Subject: [PATCH] i18n: Use APC to stash the translation file

Turns out including a PHP file in a PHAR file is not cacheable by APC. This
patch adds APC stash support to the translation loading phase, if supported.
This seems to save about 2.5ms (3.6ms down to 1.1ms) processing time per
request. Most of the savings is in the PHAR file processing time (signature
verification), which is avoided if the translation can be loaded from APC.
---
 include/class.format.php      |  6 +++-
 include/class.translation.php | 65 +++++++++++++++++++++++++++--------
 2 files changed, 56 insertions(+), 15 deletions(-)

diff --git a/include/class.format.php b/include/class.format.php
index d0e4ba203..fbbddb631 100644
--- a/include/class.format.php
+++ b/include/class.format.php
@@ -269,12 +269,16 @@ class Format {
     }
 
     function htmlencode($var) {
+        static $phpversion = null;
 
         if (is_array($var))
             return array_map(array('Format', 'htmlencode'), $var);
 
+        if (!isset($phpversion))
+            $phpversion = phpversion();
+
         $flags = ENT_COMPAT;
-        if (phpversion() >= '5.4.0')
+        if ($phpversion >= '5.4.0')
             $flags |= ENT_HTML401;
 
         try {
diff --git a/include/class.translation.php b/include/class.translation.php
index 06d25b05f..cc21848a3 100644
--- a/include/class.translation.php
+++ b/include/class.translation.php
@@ -304,7 +304,7 @@ class gettext_reader {
     $expr .= ';';
     $res = '';
     $p = 0;
-    for ($i = 0; $i < strlen($expr); $i++) {
+    for ($i = 0, $k = strlen($expr); $i < $k; $i++) {
       $ch = $expr[$i];
       switch ($ch) {
       case '?':
@@ -372,17 +372,23 @@ class gettext_reader {
    * @return int array index of the right plural form
    */
   function select_string($n) {
-    $string = $this->get_plural_forms();
-    $string = str_replace('nplurals',"\$total",$string);
-    $string = str_replace("n",$n,$string);
-    $string = str_replace('plural',"\$plural",$string);
-
-    $total = 0;
-    $plural = 0;
-
-    eval("$string");
-    if ($plural >= $total) $plural = $total - 1;
-    return $plural;
+      // Expression reads
+      // nplurals=X; plural= n != 1
+      if (!isset($this->plural_expression)) {
+          $matches = array();
+          if (!preg_match('`nplurals\s*=\s*(\d+)\s*;\s*plural\s*=\s*(.+$)`',
+                  $this->get_plural_forms(), $matches))
+              return 1;
+
+          $this->plural_expression = create_function('$n',
+              sprintf('return %s;', str_replace('n', '($n)', $matches[2])));
+          $this->plural_total = (int) $matches[1];
+      }
+      $func = $this->plural_expression;
+      $plural = $func($n);
+      return ($plural > $this->plural_total)
+          ? $this->plural_total - 1
+          : $plural;
   }
 
   /**
@@ -531,7 +537,7 @@ class FileReader {
  * source gettext library implementation which should be roughly the same
  * performance as the libc gettext library.
  */
-class Translation extends gettext_reader {
+class Translation extends gettext_reader implements Serializable {
 
     var $charset;
 
@@ -644,6 +650,30 @@ class Translation extends gettext_reader {
         else
             fwrite($stream, $contents);
     }
+
+    static function resurrect($key) {
+        if (!function_exists('apc_fetch'))
+            return false;
+
+        $success = true;
+        if (($translation = apc_fetch($key, $success)) && $success)
+            return $translation;
+    }
+    function cache($key) {
+        if (function_exists('apc_add'))
+            apc_add($key, $this);
+    }
+
+
+    function serialize() {
+        return serialize(array($this->charset, $this->encode, $this->cache_translations));
+    }
+    function unserialize($what) {
+        list($this->charset, $this->encode, $this->cache_translations)
+            = unserialize($what);
+        $this->short_circuit = ! $this->enable_cache
+            = 0 < count($this->cache_translations);
+    }
 }
 
 if (!defined('LC_MESSAGES')) {
@@ -695,6 +725,12 @@ class TextDomain {
             $subpath = self::$LC_CATEGORIES[$category] .
                 '/'.$this->domain.'.mo.php';
 
+            // APC short-circuit (if supported)
+            $key = sha1($locale .':lang:'. $subpath);
+            if ($T = Translation::resurrect($key)) {
+                return $this->l10n[$locale] = $T;
+            }
+
             $locale_names = self::get_list_of_locales($locale);
             $input = null;
             foreach ($locale_names as $T) {
@@ -712,7 +748,8 @@ class TextDomain {
                 }
             }
             // TODO: Handle charset hint from the environment
-            $this->l10n[$locale] = new Translation($input);
+            $this->l10n[$locale] = $T = new Translation($input);
+            $T->cache($key);
         }
         return $this->l10n[$locale];
     }
-- 
GitLab