From 79db33098381dd843cd8b9a1932688d3bafd5cc4 Mon Sep 17 00:00:00 2001
From: alecpl <alec@alec.pl>
Date: Sun, 27 Nov 2011 10:11:20 -0500
Subject: [PATCH] - Fix fit_string_to_size() renders browser and ui unresponsive (#1488207): 1) improve its performance by half, 2) don't call it on UI init, it's called after getunread action 3) don't call it for big number of folders (limit is 100 for IE and 500 for others)
---
program/include/rcube_cache.php | 484 +++++++++++++++++++++++++++++++++++++++--------------
1 files changed, 355 insertions(+), 129 deletions(-)
diff --git a/program/include/rcube_cache.php b/program/include/rcube_cache.php
index d38c237..018d5f5 100644
--- a/program/include/rcube_cache.php
+++ b/program/include/rcube_cache.php
@@ -28,7 +28,7 @@
* @package Cache
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
- * @version 1.0
+ * @version 1.1
*/
class rcube_cache
{
@@ -41,72 +41,62 @@
private $type;
private $userid;
private $prefix;
+ private $ttl;
+ private $packed;
+ private $index;
private $cache = array();
private $cache_keys = array();
private $cache_changes = array();
private $cache_sums = array();
-
/**
* Object constructor.
*
- * @param string $type Engine type ('db' or 'memcache')
+ * @param string $type Engine type ('db' or 'memcache' or 'apc')
* @param int $userid User identifier
* @param string $prefix Key name prefix
+ * @param int $ttl Expiration time of memcache/apc items in seconds (max.2592000)
+ * @param bool $packed Enables/disabled data serialization.
+ * It's possible to disable data serialization if you're sure
+ * stored data will be always a safe string
*/
- function __construct($type, $userid, $prefix='')
+ function __construct($type, $userid, $prefix='', $ttl=0, $packed=true)
{
$rcmail = rcmail::get_instance();
-
- if (strtolower($type) == 'memcache') {
+ $type = strtolower($type);
+
+ if ($type == 'memcache') {
$this->type = 'memcache';
$this->db = $rcmail->get_memcache();
+ }
+ else if ($type == 'apc') {
+ $this->type = 'apc';
+ $this->db = function_exists('apc_exists'); // APC 3.1.4 required
}
else {
$this->type = 'db';
$this->db = $rcmail->get_dbh();
}
- $this->userid = (int) $userid;
- $this->prefix = $prefix;
+ $this->userid = (int) $userid;
+ $this->ttl = (int) $ttl;
+ $this->packed = $packed;
+ $this->prefix = $prefix;
}
/**
* Returns cached value.
*
- * @param string $key Cache key
+ * @param string $key Cache key name
*
* @return mixed Cached value
*/
function get($key)
{
- $key = $this->prefix.$key;
-
- if ($this->type == 'memcache') {
- return $this->read_cache_record($key);
- }
-
- // read cache (if it was not read before)
- if (!count($this->cache)) {
- $do_read = true;
- }
- else if (isset($this->cache[$key])) {
- $do_read = false;
- }
- // Find cache prefix, we'll load data for all keys
- // with specified (class) prefix into internal cache (memory)
- else if ($pos = strpos($key, '.')) {
- $prefix = substr($key, 0, $pos);
- $regexp = '/^' . preg_quote($prefix, '/') . '/';
- if (!count(preg_grep($regexp, array_keys($this->cache_keys)))) {
- $do_read = true;
- }
- }
-
- if ($do_read) {
- return $this->read_cache_record($key);
+ if (!array_key_exists($key, $this->cache)) {
+ return $this->read_record($key);
}
return $this->cache[$key];
@@ -116,13 +106,11 @@
/**
* Sets (add/update) value in cache.
*
- * @param string $key Cache key
- * @param mixed $data Data
+ * @param string $key Cache key name
+ * @param mixed $data Cache data
*/
function set($key, $data)
{
- $key = $this->prefix.$key;
-
$this->cache[$key] = $data;
$this->cache_changed = true;
$this->cache_changes[$key] = true;
@@ -130,42 +118,89 @@
/**
+ * Returns cached value without storing it in internal memory.
+ *
+ * @param string $key Cache key name
+ *
+ * @return mixed Cached value
+ */
+ function read($key)
+ {
+ if (array_key_exists($key, $this->cache)) {
+ return $this->cache[$key];
+ }
+
+ return $this->read_record($key, true);
+ }
+
+
+ /**
+ * Sets (add/update) value in cache and immediately saves
+ * it in the backend, no internal memory will be used.
+ *
+ * @param string $key Cache key name
+ * @param mixed $data Cache data
+ *
+ * @param boolean True on success, False on failure
+ */
+ function write($key, $data)
+ {
+ return $this->write_record($key, $this->packed ? serialize($data) : $data);
+ }
+
+
+ /**
* Clears the cache.
*
- * @param string $key Cache key name or pattern
- * @param boolean $pattern_mode Enable it to clear all keys with name
- * matching PREG pattern in $key
+ * @param string $key Cache key name or pattern
+ * @param boolean $prefix_mode Enable it to clear all keys starting
+ * with prefix specified in $key
*/
- function remove($key=null, $pattern_mode=false)
+ function remove($key=null, $prefix_mode=false)
{
+ // Remove all keys
if ($key === null) {
- foreach (array_keys($this->cache) as $key)
- $this->clear_cache_record($key);
-
$this->cache = array();
$this->cache_changed = false;
$this->cache_changes = array();
+ $this->cache_keys = array();
}
- else if ($pattern_mode) {
- $key = $this->prefix.$key;
-
+ // Remove keys by name prefix
+ else if ($prefix_mode) {
foreach (array_keys($this->cache) as $k) {
- if (preg_match($key, $k)) {
- $this->clear_cache_record($k);
+ if (strpos($k, $key) === 0) {
+ $this->cache[$k] = null;
$this->cache_changes[$k] = false;
- unset($this->cache[$key]);
+ unset($this->cache_keys[$k]);
}
}
- if (!count($this->cache)) {
- $this->cache_changed = false;
- }
}
+ // Remove one key by name
else {
- $key = $this->prefix.$key;
-
- $this->clear_cache_record($key);
+ $this->cache[$key] = null;
$this->cache_changes[$key] = false;
- unset($this->cache[$key]);
+ unset($this->cache_keys[$key]);
+ }
+
+ // Remove record(s) from the backend
+ $this->remove_record($key, $prefix_mode);
+ }
+
+
+ /**
+ * Remove cache records older than ttl
+ */
+ function expunge()
+ {
+ if ($this->type == 'db' && $this->db) {
+ $this->db->query(
+ "DELETE FROM ".get_table_name('cache').
+ " WHERE user_id = ?".
+ " AND cache_key LIKE ?".
+ " AND " . $this->db->unixtimestamp('created')." < ?",
+ $this->userid,
+ $this->prefix.'.%',
+ time() - $this->ttl);
}
}
@@ -184,64 +219,83 @@
if ($this->cache_changes[$key]) {
// Make sure we're not going to write unchanged data
// by comparing current md5 sum with the sum calculated on DB read
- $data = serialize($data);
+ $data = $this->packed ? serialize($data) : $data;
+
if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) {
- $this->write_cache_record($key, $data);
+ $this->write_record($key, $data);
}
}
}
+
+ $this->write_index();
}
/**
- * Returns cached entry.
+ * Reads cache entry.
*
- * @param string $key Cache key
+ * @param string $key Cache key name
+ * @param boolean $nostore Enable to skip in-memory store
*
* @return mixed Cached value
- * @access private
*/
- private function read_cache_record($key)
+ private function read_record($key, $nostore=false)
{
if (!$this->db) {
return null;
}
- if ($this->type == 'memcache') {
- $data = $this->db->get($key);
-
- if ($data) {
- $this->cache_sums[$key] = md5($data);
- $data = unserialize($data);
+ if ($this->type != 'db') {
+ if ($this->type == 'memcache') {
+ $data = $this->db->get($this->ckey($key));
}
- return $this->cache[$key] = $data;
- }
+ else if ($this->type == 'apc') {
+ $data = apc_fetch($this->ckey($key));
+ }
- // Find cache prefix, we'll load data for all keys
- // with specified (class) prefix into internal cache (memory)
- if ($pos = strpos($key, '.')) {
- $prefix = substr($key, 0, $pos);
- $where = " AND cache_key LIKE '$prefix%'";
+ if ($data) {
+ $md5sum = md5($data);
+ $data = $this->packed ? unserialize($data) : $data;
+
+ if ($nostore) {
+ return $data;
+ }
+
+ $this->cache_sums[$key] = $md5sum;
+ $this->cache[$key] = $data;
+ }
+ else {
+ $this->cache[$key] = null;
+ }
}
else {
- $where = " AND cache_key = ".$this->db->quote($key);
- }
+ $sql_result = $this->db->limitquery(
+ "SELECT cache_id, data, cache_key".
+ " FROM ".get_table_name('cache').
+ " WHERE user_id = ?".
+ " AND cache_key = ?".
+ // for better performance we allow more records for one key
+ // get the newer one
+ " ORDER BY created DESC",
+ 0, 1, $this->userid, $this->prefix.'.'.$key);
- // get cached data from DB
- $sql_result = $this->db->query(
- "SELECT cache_id, data, cache_key".
- " FROM ".get_table_name('cache').
- " WHERE user_id = ?".$where,
- $this->userid);
-
- while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
- $sql_key = $sql_arr['cache_key'];
- $this->cache_keys[$sql_key] = $sql_arr['cache_id'];
- if (!isset($this->cache[$sql_key])) {
+ if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+ $key = substr($sql_arr['cache_key'], strlen($this->prefix)+1);
$md5sum = $sql_arr['data'] ? md5($sql_arr['data']) : null;
- $data = $sql_arr['data'] ? unserialize($sql_arr['data']) : false;
- $this->cache[$sql_key] = $data;
- $this->cache_sums[$sql_key] = $md5sum;
+ if ($sql_arr['data']) {
+ $data = $this->packed ? unserialize($sql_arr['data']) : $sql_arr['data'];
+ }
+
+ if ($nostore) {
+ return $data;
+ }
+
+ $this->cache[$key] = $data;
+ $this->cache_sums[$key] = $md5sum;
+ $this->cache_keys[$key] = $sql_arr['cache_id'];
+ }
+ else {
+ $this->cache[$key] = null;
}
}
@@ -250,28 +304,40 @@
/**
- * Writes single cache record.
+ * Writes single cache record into DB.
*
- * @param string $key Cache key
- * @param mxied $data Cache value
- * @access private
+ * @param string $key Cache key name
+ * @param mxied $data Serialized cache data
+ *
+ * @param boolean True on success, False on failure
*/
- private function write_cache_record($key, $data)
+ private function write_record($key, $data)
{
if (!$this->db) {
return false;
}
- if ($this->type == 'memcache') {
- $result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED);
- if (!$result)
- $result = $this->db->set($key, $data, MEMCACHE_COMPRESSED);
- return $result;
+ if ($this->type == 'memcache' || $this->type == 'apc') {
+ return $this->add_record($this->ckey($key), $data);
+ }
+
+ $key_exists = $this->cache_keys[$key];
+ $key = $this->prefix . '.' . $key;
+
+ // Remove NULL rows (here we don't need to check if the record exist)
+ if ($data == 'N;') {
+ $this->db->query(
+ "DELETE FROM ".get_table_name('cache').
+ " WHERE user_id = ?".
+ " AND cache_key = ?",
+ $this->userid, $key);
+
+ return true;
}
// update existing cache record
- if ($this->cache_keys[$key]) {
- $this->db->query(
+ if ($key_exists) {
+ $result = $this->db->query(
"UPDATE ".get_table_name('cache').
" SET created = ". $this->db->now().", data = ?".
" WHERE user_id = ?".
@@ -280,49 +346,209 @@
}
// add new cache record
else {
- $this->db->query(
+ // for better performance we allow more records for one key
+ // so, no need to check if record exist (see rcube_cache::read_record())
+ $result = $this->db->query(
"INSERT INTO ".get_table_name('cache').
" (created, user_id, cache_key, data)".
" VALUES (".$this->db->now().", ?, ?, ?)",
$this->userid, $key, $data);
+ }
- // get cache entry ID for this key
- $sql_result = $this->db->query(
- "SELECT cache_id".
- " FROM ".get_table_name('cache').
- " WHERE user_id = ?".
- " AND cache_key = ?",
- $this->userid, $key);
+ return $this->db->affected_rows($result);
+ }
- if ($sql_arr = $this->db->fetch_assoc($sql_result))
- $this->cache_keys[$key] = $sql_arr['cache_id'];
+
+ /**
+ * Deletes the cache record(s).
+ *
+ * @param string $key Cache key name or pattern
+ * @param boolean $prefix_mode Enable it to clear all keys starting
+ * with prefix specified in $key
+ *
+ */
+ private function remove_record($key=null, $prefix_mode=false)
+ {
+ if (!$this->db) {
+ return;
+ }
+
+ if ($this->type != 'db') {
+ $this->load_index();
+
+ // Remove all keys
+ if ($key === null) {
+ foreach ($this->index as $key) {
+ $this->delete_record($key, false);
+ }
+ $this->index = array();
+ }
+ // Remove keys by name prefix
+ else if ($prefix_mode) {
+ foreach ($this->index as $k) {
+ if (strpos($k, $key) === 0) {
+ $this->delete_record($k);
+ }
+ }
+ }
+ // Remove one key by name
+ else {
+ $this->delete_record($key);
+ }
+
+ return;
+ }
+
+ // Remove all keys (in specified cache)
+ if ($key === null) {
+ $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.%');
+ }
+ // Remove keys by name prefix
+ else if ($prefix_mode) {
+ $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.'.$key.'%');
+ }
+ // Remove one key by name
+ else {
+ $where = " AND cache_key = " . $this->db->quote($this->prefix.'.'.$key);
+ }
+
+ $this->db->query(
+ "DELETE FROM ".get_table_name('cache').
+ " WHERE user_id = ?" . $where,
+ $this->userid);
+ }
+
+
+ /**
+ * Adds entry into memcache/apc DB.
+ *
+ * @param string $key Cache key name
+ * @param mxied $data Serialized cache data
+ * @param bollean $index Enables immediate index update
+ *
+ * @param boolean True on success, False on failure
+ */
+ private function add_record($key, $data, $index=false)
+ {
+ if ($this->type == 'memcache') {
+ $result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
+ if (!$result)
+ $result = $this->db->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
+ }
+ else if ($this->type == 'apc') {
+ if (apc_exists($key))
+ apc_delete($key);
+ $result = apc_store($key, $data, $this->ttl);
+ }
+
+ // Update index
+ if ($index && $result) {
+ $this->load_index();
+
+ if (array_search($key, $this->index) === false) {
+ $this->index[] = $key;
+ $data = serialize($this->index);
+ $this->add_record($this->ikey(), $data);
+ }
+ }
+
+ return $result;
+ }
+
+
+ /**
+ * Deletes entry from memcache/apc DB.
+ */
+ private function delete_record($key, $index=true)
+ {
+ if ($this->type == 'memcache')
+ $this->db->delete($this->ckey($key));
+ else
+ apc_delete($this->ckey($key));
+
+ if ($index) {
+ if (($idx = array_search($key, $this->index)) !== false) {
+ unset($this->index[$idx]);
+ }
}
}
/**
- * Clears cache for single record.
- *
- * @param string $key Cache key
- * @access private
+ * Writes the index entry into memcache/apc DB.
*/
- private function clear_cache_record($key)
+ private function write_index()
{
if (!$this->db) {
- return false;
+ return;
}
- if ($this->type == 'memcache') {
- return $this->db->delete($key);
+ if ($this->type == 'db') {
+ return;
}
- $this->db->query(
- "DELETE FROM ".get_table_name('cache').
- " WHERE user_id = ?".
- " AND cache_key = ?",
- $this->userid, $key);
+ $this->load_index();
- unset($this->cache_keys[$key]);
+ // Make sure index contains new keys
+ foreach ($this->cache as $key => $value) {
+ if ($value !== null) {
+ if (array_search($key, $this->index) === false) {
+ $this->index[] = $key;
+ }
+ }
+ }
+
+ $data = serialize($this->index);
+ $this->add_record($this->ikey(), $data);
}
+
+ /**
+ * Gets the index entry from memcache/apc DB.
+ */
+ private function load_index()
+ {
+ if (!$this->db) {
+ return;
+ }
+
+ if ($this->index !== null) {
+ return;
+ }
+
+ $index_key = $this->ikey();
+ if ($this->type == 'memcache') {
+ $data = $this->db->get($index_key);
+ }
+ else if ($this->type == 'apc') {
+ $data = apc_fetch($index_key);
+ }
+
+ $this->index = $data ? unserialize($data) : array();
+ }
+
+
+ /**
+ * Creates per-user cache key name (for memcache and apc)
+ *
+ * @param string $key Cache key name
+ *
+ * @return string Cache key
+ */
+ private function ckey($key)
+ {
+ return sprintf('%d:%s:%s', $this->userid, $this->prefix, $key);
+ }
+
+
+ /**
+ * Creates per-user index cache key name (for memcache and apc)
+ *
+ * @return string Cache key
+ */
+ private function ikey()
+ {
+ // This way each cache will have its own index
+ return sprintf('%d:%s%s', $this->userid, $this->prefix, 'INDEX');
+ }
}
--
Gitblit v1.9.1