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