1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\Memory; 11 12use Zend\Cache\Storage\ClearByNamespaceInterface as ClearByNamespaceCacheStorage; 13use Zend\Cache\Storage\FlushableInterface as FlushableCacheStorage; 14use Zend\Cache\Storage\StorageInterface as CacheStorage; 15 16/** 17 * Memory manager 18 * 19 * This class encapsulates memory menagement operations, when PHP works 20 * in limited memory mode. 21 */ 22class MemoryManager 23{ 24 /** 25 * Storage cache object 26 * 27 * @var CacheStorage 28 */ 29 private $cache = null; 30 31 /** 32 * Memory grow limit. 33 * Default value is 2/3 of memory_limit php.ini variable 34 * Negative value means no limit 35 * 36 * @var int 37 */ 38 private $memoryLimit = -1; 39 40 /** 41 * Minimum value size to be swapped. 42 * Default value is 16K 43 * Negative value means that memory objects are never swapped 44 * 45 * @var int 46 */ 47 private $minSize = 16384; 48 49 /** 50 * Overall size of memory, used by values 51 * 52 * @var int 53 */ 54 private $memorySize = 0; 55 56 /** 57 * Id for next Zend\Memory object 58 * 59 * @var int 60 */ 61 private $nextId = 0; 62 63 /** 64 * List of candidates to unload 65 * 66 * It also represents objects access history. Last accessed objects are moved to the end of array 67 * 68 * array( 69 * <id> => <memory container object>, 70 * ... 71 * ) 72 * 73 * @var array 74 */ 75 private $unloadCandidates = array(); 76 77 /** 78 * List of object sizes. 79 * 80 * This list is used to calculate modification of object sizes 81 * 82 * array( <id> => <size>, ...) 83 * 84 * @var array 85 */ 86 private $sizes = array(); 87 88 /** 89 * Last modified object 90 * 91 * It's used to reduce number of calls necessary to trace objects' modifications 92 * Modification is not processed by memory manager until we do not switch to another 93 * object. 94 * So we have to trace only _first_ object modification and do nothing for others 95 * 96 * @var \Zend\Memory\Container\Movable 97 */ 98 private $lastModified = null; 99 100 /** 101 * Unique memory manager id 102 * 103 * @var int 104 */ 105 private $managerId; 106 107 /** 108 * This function is intended to generate unique id, used by memory manager 109 */ 110 private function generateMemManagerId() 111 { 112 /** 113 * @todo !!! 114 * uniqid() php function doesn't really guarantee the id to be unique 115 * it should be changed by something else 116 * (Ex. backend interface should be extended to provide this functionality) 117 */ 118 $this->managerId = str_replace('.', '_', uniqid('ZendMemManager', true)) . '_'; 119 } 120 121 /** 122 * Memory manager constructor 123 * 124 * If cache is not specified, then memory objects are never swapped 125 * 126 * @param CacheStorage $cache 127 */ 128 public function __construct(CacheStorage $cache = null) 129 { 130 if ($cache === null) { 131 return; 132 } 133 134 $this->cache = $cache; 135 $this->generateMemManagerId(); 136 137 $memoryLimitStr = trim(ini_get('memory_limit')); 138 if ($memoryLimitStr != '' && $memoryLimitStr != -1) { 139 $this->memoryLimit = (int) $memoryLimitStr; 140 switch (strtolower($memoryLimitStr[strlen($memoryLimitStr) - 1])) { 141 case 'g': 142 $this->memoryLimit *= 1024; 143 // no break 144 case 'm': 145 $this->memoryLimit *= 1024; 146 // no break 147 case 'k': 148 $this->memoryLimit *= 1024; 149 break; 150 default: 151 break; 152 } 153 154 $this->memoryLimit = (int) ($this->memoryLimit*2/3); 155 } // No limit otherwise 156 } 157 158 /** 159 * Object destructor 160 * 161 * Clean up cache storage 162 */ 163 public function __destruct() 164 { 165 if ($this->cache !== null) { 166 if ($this->cache instanceof ClearByNamespaceCacheStorage) { 167 $this->cache->clearByNamespace($this->cache->getOptions()->getNamespace()); 168 } elseif ($this->cache instanceof FlushableCacheStorage) { 169 $this->cache->flush(); 170 } 171 } 172 } 173 174 /** 175 * Set memory grow limit 176 * 177 * @param int $newLimit 178 */ 179 public function setMemoryLimit($newLimit) 180 { 181 $this->memoryLimit = $newLimit; 182 183 $this->swapCheck(); 184 } 185 186 /** 187 * Get memory grow limit 188 * 189 * @return int 190 */ 191 public function getMemoryLimit() 192 { 193 return $this->memoryLimit; 194 } 195 196 /** 197 * Set minimum size of values, which may be swapped 198 * 199 * @param int $newSize 200 */ 201 public function setMinSize($newSize) 202 { 203 $this->minSize = $newSize; 204 } 205 206 /** 207 * Get minimum size of values, which may be swapped 208 * 209 * @return int 210 */ 211 public function getMinSize() 212 { 213 return $this->minSize; 214 } 215 216 /** 217 * Create new Zend\Memory value container 218 * 219 * @param string $value 220 * @return Container\ContainerInterface 221 * @throws Exception\ExceptionInterface 222 */ 223 public function create($value = '') 224 { 225 return $this->_create($value, false); 226 } 227 228 /** 229 * Create new Zend\Memory value container, which has value always 230 * locked in memory 231 * 232 * @param string $value 233 * @return Container\ContainerInterface 234 * @throws Exception\ExceptionInterface 235 */ 236 public function createLocked($value = '') 237 { 238 return $this->_create($value, true); 239 } 240 241 /** 242 * Create new Zend\Memory object 243 * 244 * @param string $value 245 * @param bool $locked 246 * @return \Zend\Memory\Container\ContainerInterface 247 * @throws \Zend\Memory\Exception\ExceptionInterface 248 */ 249 private function _create($value, $locked) 250 { 251 $id = $this->nextId++; 252 253 if ($locked || ($this->cache === null) /* Use only memory locked objects if backend is not specified */) { 254 return new Container\Locked($value); 255 } 256 257 // Commit other objects modifications 258 $this->commit(); 259 260 $valueObject = new Container\Movable($this, $id, $value); 261 262 // Store last object size as 0 263 $this->sizes[$id] = 0; 264 // prepare object for next modifications 265 $this->lastModified = $valueObject; 266 267 return new Container\AccessController($valueObject); 268 } 269 270 /** 271 * Unlink value container from memory manager 272 * 273 * Used by Memory container destroy() method 274 * 275 * @internal 276 * @param Container\Movable $container 277 * @param int $id 278 * @return null 279 */ 280 public function unlink(Container\Movable $container, $id) 281 { 282 if ($this->lastModified === $container) { 283 // Drop all object modifications 284 $this->lastModified = null; 285 unset($this->sizes[$id]); 286 return; 287 } 288 289 if (isset($this->unloadCandidates[$id])) { 290 unset($this->unloadCandidates[$id]); 291 } 292 293 $this->memorySize -= $this->sizes[$id]; 294 unset($this->sizes[$id]); 295 } 296 297 /** 298 * Process value update 299 * 300 * @internal 301 * @param \Zend\Memory\Container\Movable $container 302 * @param int $id 303 */ 304 public function processUpdate(Container\Movable $container, $id) 305 { 306 /** 307 * This method is automatically invoked by memory container only once per 308 * "modification session", but user may call memory container touch() method 309 * several times depending on used algorithm. So we have to use this check 310 * to optimize this case. 311 */ 312 if ($container === $this->lastModified) { 313 return; 314 } 315 316 // Remove just updated object from list of candidates to unload 317 if (isset($this->unloadCandidates[$id])) { 318 unset($this->unloadCandidates[$id]); 319 } 320 321 // Reduce used memory mark 322 $this->memorySize -= $this->sizes[$id]; 323 324 // Commit changes of previously modified object if necessary 325 $this->commit(); 326 327 $this->lastModified = $container; 328 } 329 330 /** 331 * Commit modified object and put it back to the loaded objects list 332 */ 333 private function commit() 334 { 335 if (($container = $this->lastModified) === null) { 336 return; 337 } 338 339 $this->lastModified = null; 340 341 $id = $container->getId(); 342 343 // Calculate new object size and increase used memory size by this value 344 $this->memorySize += ($this->sizes[$id] = strlen($container->getRef())); 345 346 if ($this->sizes[$id] > $this->minSize) { 347 // Move object to "unload candidates list" 348 $this->unloadCandidates[$id] = $container; 349 } 350 351 $container->startTrace(); 352 353 $this->swapCheck(); 354 } 355 356 /** 357 * Check and swap objects if necessary 358 * 359 * @throws Exception\RuntimeException 360 */ 361 private function swapCheck() 362 { 363 if ($this->memoryLimit < 0 || $this->memorySize < $this->memoryLimit) { 364 // Memory limit is not reached 365 // Do nothing 366 return; 367 } 368 369 // walk through loaded objects in access history order 370 foreach ($this->unloadCandidates as $id => $container) { 371 $this->swap($container, $id); 372 unset($this->unloadCandidates[$id]); 373 374 if ($this->memorySize < $this->memoryLimit) { 375 // We've swapped enough objects 376 return; 377 } 378 } 379 380 throw new Exception\RuntimeException('Memory manager can\'t get enough space.'); 381 } 382 383 /** 384 * Swap object data to disk 385 * Actually swaps data or only unloads it from memory, 386 * if object is not changed since last swap 387 * 388 * @param \Zend\Memory\Container\Movable $container 389 * @param int $id 390 */ 391 private function swap(Container\Movable $container, $id) 392 { 393 if ($container->isLocked()) { 394 return; 395 } 396 397 if (!$container->isSwapped()) { 398 $this->cache->setItem($this->managerId . $id, $container->getRef()); 399 } 400 401 $this->memorySize -= $this->sizes[$id]; 402 403 $container->markAsSwapped(); 404 $container->unloadValue(); 405 } 406 407 /** 408 * Load value from swap file. 409 * 410 * @internal 411 * @param \Zend\Memory\Container\Movable $container 412 * @param int $id 413 */ 414 public function load(Container\Movable $container, $id) 415 { 416 $value = $this->cache->getItem($this->managerId . $id); 417 418 // Try to swap other objects if necessary 419 // (do not include specified object into check) 420 $this->memorySize += strlen($value); 421 $this->swapCheck(); 422 423 // Add loaded object to the end of loaded objects list 424 $container->setValue($value); 425 426 if ($this->sizes[$id] > $this->minSize) { 427 // Add object to the end of "unload candidates list" 428 $this->unloadCandidates[$id] = $container; 429 } 430 } 431} 432