diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 065d8018..aa0999f4 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -4,9 +4,11 @@ jobs: ubuntu: strategy: matrix: - version: ["7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"] + version: ["7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4", "8.5"] runs-on: ubuntu-latest steps: + - name: Set the hugepages parameter + run: sudo sh -c "echo 1 > /proc/sys/vm/nr_hugepages" - name: Checkout apcu uses: actions/checkout@v4 - name: Setup PHP @@ -17,6 +19,8 @@ jobs: run: phpize - name: configure run: ./configure --enable-apcu-debug + - name: Fix missing PHP_EXECUTABLE in Makefile for PHP 7.x + run: sed -i -e 's/^PHP_EXECUTABLE = NONE$/PHP_EXECUTABLE = \/usr\/bin\/php/' Makefile - name: make run: make - name: test @@ -31,28 +35,16 @@ jobs: shell: cmd strategy: matrix: - version: ["7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"] + version: ["8.0", "8.1", "8.2", "8.3", "8.4", "8.5"] arch: [x64] ts: [nts, ts] - os: [windows-2019, windows-2022] - exclude: - - { os: windows-2019, version: "8.4" } - - { os: windows-2019, version: "8.3" } - - { os: windows-2019, version: "8.2" } - - { os: windows-2019, version: "8.1" } - - { os: windows-2019, version: "8.0" } - - { os: windows-2022, version: "7.4" } - - { os: windows-2022, version: "7.3" } - - { os: windows-2022, version: "7.2" } - - { os: windows-2022, version: "7.1" } - - { os: windows-2022, version: "7.0" } - runs-on: ${{matrix.os}} + runs-on: windows-2022 steps: - name: Checkout apcu uses: actions/checkout@v4 - name: Setup PHP id: setup-php - uses: php/setup-php-sdk@v0.10 + uses: php/setup-php-sdk@v0.11 with: version: ${{matrix.version}} arch: ${{matrix.arch}} diff --git a/TECHNOTES.txt b/TECHNOTES.txt index e7cf642b..9a4a4191 100644 --- a/TECHNOTES.txt +++ b/TECHNOTES.txt @@ -3,21 +3,20 @@ APCu Quick-Start Braindump This is a rapidly written braindump of how APCu currently works in the form of a quick-start guide to start hacking on APCu. -1. Install and use APC a bit so you know what it does from the end-user's - perspective. - user-space functions are all explained here: https://www.php.net/apcu +1. Install and use APCu a bit so you know what it does from the end-user's + perspective. PHP functions are all explained here: https://www.php.net/apcu -2. Grab the current APC code from https://github.com/krakjoe/apcu - - apcu/php_apc.c has most of the code for the user-visible stuff. It is - also a regular PHP extension in the sense that there are MINIT, MINFO, - MSHUTDOWN, RSHUTDOWN, etc. functions. +2. Grab the current APCu code from https://github.com/krakjoe/apcu + + Most of the user-visible stuff is implemented in apcu/php_apc.c. + It is also a regular PHP extension in the sense that there are MINIT, MINFO, + MSHUTDOWN, RSHUTDOWN, etc. functions. 3. Build it. cd apcu phpize - ./configure --enable-apcu + ./configure --enable-apcu --enable-apcu-debug make make test cp modules/apcu.so /usr/local/lib/php @@ -25,303 +24,205 @@ form of a quick-start guide to start hacking on APCu. 4. Debugging Hints - apachectl stop - gdb /usr/bin/httpd - break ?? - run -X + apachectl stop + gdb /usr/bin/httpd + break ?? + run -X Grab the .gdbinit from the PHP source tree and have a look at the macros. 5. The basics of APCu - APCu has three main component parts: - 1) shared memory allocator - 2) pooling - 3) user land cache + The caching functionality of APCu is provided by a modified version of the APC source code. + Many tweaks have been applied. There's probably some of my blood in it, if you look real close... (krakjoe) -5.1) APCu SMA - - It is a pretty standard memory allocator, now supporting third party extensions. + APCu has the following main component parts: + 1) shared memory allocator / SMA (apc_sma.c) + 2) user land cache (apc_cache.c) + 3) persistence representation (apc_persist.c) - apc_sma_malloc and apc_sma_free behave to the caller just like malloc and free, - they are generated from macros in apc_sma.h +5.1 SMA - Note: apc_sma.h is formatted and designed such that the SMA APCu - uses can be used by third parties in their own extensions without - interfering with, or consuming the resources of APCu itself + The SMA (shared memory allocator) is a pretty standard memory allocator. It provides + apc_sma_malloc() and apc_sma_free() which behave just like malloc() and free(). - apc_sma is a structure of type apc_sma_t, it is statically allocated at runtime, - appropriate handlers are generated and set, and the structure made ready for initialization. + The SMA is designed such that the SMA can be used by third parties in their own extensions + without interfering with or consuming the resources of APCu itself. - MINIT then initializes apc_sma with apc_sma_api_init(). - APCu SMA then takes care of mmaping the shared memory. - ( which you can obtain in any compilation unit with apc_sma_api_extern(apc_sma) ) + MINIT calls apc_sma_init(), which takes care of mapping and initializing the shared memory segment. + It initializes the smaheader (sma_header_t) at the beginning of the shared memory. The smaheader + serves as a place to store, among other things, statistical information and the lock for the SMA. - At this point, we have a completely useless 32MB chunk of memory at our disposal, before - it can be used, a sma_header_t is initialized at the beginning of the region of - mmapp'ed memory. - - The smaheader serves as a place to store, among other things, statistical information and a lock. - - Immediately after the smaheader come three blocks. The first and last block are 0-sized - and simplify the handling of the linked list. The block between the 0-sized blocks - contains the remaining size of the shared memory which is available for allocation. + Immediately after the smaheader it initializes three blocks. The first and last block are 0-sized + and simplify the handling of the linked list of free blocks and sequential traversal of blocks. + The block between the 0-sized blocks contains the remaining amount of shared memory available + for allocation. - At this point, the shared memory looks like this: + At this point, the shared memory looks like this: - +--------+--------+-----------------------------------+ - | header | 0-size | free-block | 0-size | - +--------+--------+-----------------------------------+ + +-----------+--------+-----------------------------------+ + | smaheader | 0-size | free-block | 0-size | + +-----------+--------+-----------------------------------+ - The blocks are just a simple offset-based doubly linked list (so no pointers): + These three blocks (type block_t) form the initial doubly linked list of free blocks. + Since the whole SMA is implemented relocatable (independent of the starting address + of the shared memory segment), this list is offset-based (so no pointers). - typedef struct block_t block_t; - struct block_t { - size_t size; /* size of this block */ - size_t prev_size; /* size of sequentially previous block, 0 if prev is allocated */ - size_t fnext; /* offset in segment of next free block */ - size_t fprev; /* offset in segment of prev free block */ -#ifdef APC_SMA_CANARIES - size_t canary; /* canary to check for memory overwrites */ -#endif - }; + The macros BLOCKAT and OFFSET are used to simplify the handling of the offset-based blocks: - The BLOCKAT macro turns an offset into an actual address for you: + - The BLOCKAT macro turns an offset into an actual process-local address/pointer: #define BLOCKAT(offset) ((block_t*)((char *)smaheader + offset)) - where smaheader = sma->shaddrs[0] - - And the OFFSET macro goes the other way: + - The OFFSET macro goes the other way: #define OFFSET(block) ((int)(((char*)block) - (char*)smaheader)) - Allocating a block (`sma_allocate`) walks through the doubly linked list of blocks until it - finds one that is >= to the requested size. The first call to allocate will hit the second block. - We then chop up that block so it looks like this: + Both macros assume the presence of the variable "smaheader" that points to the beginning + of the shared memory segment. + + To allocate a block via apc_sma_malloc(), we walk through the doubly linked list of blocks until we + find one that is >= to the requested size (see find_block()). The first call to find_block() will hit + the second block. To get a block of the requested size, we then chop up that block so it looks like this: - +--------+--------+-------+------------------+--------+ - | header | 0-size | block | free-block | 0-size | - +--------+--------+-------+------------------+--------+ + +-----------+--------+-------+------------------+--------+ + | smaheader | 0-size | block | free-block | 0-size | + +-----------+--------+-------+------------------+--------+ Then we unlink that block from the doubly linked list so it won't show up - as an available block on the next allocate. So we actually have: + as an available block on the next allocation. So we actually have: - +--------+--------+ +------------------+--------+ - | header | 0-size |<----->| free-block | 0-size | - +--------+--------+ +------------------+--------+ + +-----------+--------+ +------------------+--------+ + | smaheader | 0-size |<----->| free-block | 0-size | + +-----------+--------+ +------------------+--------+ And smaheader->avail along with block->size of the remaining large free block are updated accordingly. The arrow there represents the link which now points to a block with an offset further along in the segment. - When the block is freed the steps are basically just reversed. + When the block is freed, the steps are basically just reversed. The block is put back and then the deallocate code looks at the block before and after to see - if the block immediately before and after are free and if so the blocks are combined. So you never - have 2 free blocks next to each other, apart from the 0-sized dummy blocks. + if the blocks immediately before or after are free, and if so, the blocks are combined. + So we never have 2 free blocks next to each other, apart from the 0-sized dummy blocks. This mostly prevents fragmentation. Block start pointers are aligned to the system's word boundary (usually 8 bytes) with the `ALIGNWORD` macro. -5.2) APCu Cache - - The caching functionality of APCu is provided by a modified version of the APC source code - - Some simple tweaks have been applied: - Locking is written to use the best kind of locking available, and emulate it where it is not to simplify logic. - Extension of the SMA to support multiple instances, such that additional caches using APCu do not - increase contention of the main APCu cache. - The possibility to control more finely what happens when resources become low for APCu. - An exposed, coherent, and documented API and example included in the distribution. - - There's probably some of my blood in it, if you look real close ... - - The remainder of the document goes on to explain in some detail the cache itself, functionally unchanged by APCu - -6. Next up is apc_cache.c which implements the cache logic. - - Having initialized a suitable allocator, MINIT must call apc_cache_create, using the allocator provided - APCu will create a cache. The parameters to apc_cache_create for APCu are defined by various INI settings. - API users can provide the same options from anywhere ( their globals for example ). - - The cache is stored in/described by this struct allocated locally: - - /* {{{ struct definition: apc_cache_t */ - typedef struct _apc_cache_t { - apc_cache_header_t* header; /* cache header (stored in SHM) */ - uintptr_t* slots; /* array of cache slots (stored in SHM) */ - apc_sma_t* sma; /* shared memory allocator */ - apc_serializer_t* serializer; /* serializer */ - size_t nslots; /* number of slots in cache */ - zend_long gc_ttl; /* maximum time on GC list for a entry */ - zend_long ttl; /* if slot is needed and entry's access time is older than this ttl, remove it */ - zend_long smart; /* smart parameter for gc */ - zend_bool defend; /* defense parameter for runtime */ - } apc_cache_t; /* }}} */ - - Whenever you see functions that take a 'cache' argument, this is what they - take. - - At the beginning of the cache we have a header. The header looks like this: - - /* {{{ struct definition: apc_cache_header_t - Any values that must be shared among processes should go in here. */ - typedef struct _apc_cache_header_t { - apc_lock_t lock; /* header lock */ - zend_long nhits; /* hit count */ - zend_long nmisses; /* miss count */ - zend_long ninserts; /* insert count */ - zend_long nexpunges; /* expunge count */ - zend_long nentries; /* entry count */ - zend_long mem_size; /* used */ - time_t stime; /* start time */ - apc_cache_slam_key_t lastkey; /* last key inserted (not necessarily without error) */ - uintptr_t gc; /* offset in shm to the first entry of gc list */ - } apc_cache_header_t; /* }}} */ - - Since this is at the start of the shared memory segment, these values are accessible - across all processes / threads and hence access to them has to be locked. - - After the header we have an array of slots. The number of slots is user-defined - through the apc.entries_hint ini hint. Each slot is described by: - - /* {{{ struct definition: apc_cache_slot_t */ - typedef struct apc_cache_slot_t apc_cache_slot_t; - struct apc_cache_slot_t { - apc_cache_key_t key; /* slot key */ - apc_cache_entry_t* value; /* slot value */ - apc_cache_slot_t* next; /* next slot in linked list */ - zend_ulong nhits; /* number of hits to this slot */ - time_t ctime; /* time slot was initialized */ - time_t dtime; /* time slot was removed from cache */ - time_t atime; /* time slot was last accessed */ - }; - /* }}} */ - - The apc_cache_slot_t *next there is a linked list to other slots that happened to hash to the - same array position. - - apc_cache_store_internal() shows what happens on a new cache insert. +5.2 Cache - /* calculate hash and entry */ - apc_cache_hash_slot(cache, key, &h, &s); + Next up is apc_cache.c which implements the cache logic. + + Having initialized the shared memory allocator (SMA), MINIT calls apc_cache_create() to initialize + the cache. The parameters to apc_cache_create() for APCu are mostly defined by various INI settings. + + The function apc_cache_create() allocates and returns a struct of the type apc_cache_t, which describes + the created cache. Whenever you see functions that take a 'cache' argument, this is what they take. + + In addition, apc_cache_create() allocates and initializes shared memory for the cache header + (apc_cache_header_t) and the hash slots of the hash table. Since the header and the hash slots are + in the shared memory, these values are accessible across all processes / threads and hence access + to it has to be locked. For this purpose, the header contains a read / write lock that allows access + to either multiple parallel readers or only one writer. + + The number of hash slots is computed / user-defined through the apc.entries_hint ini value. + Therefore, the size of the hash slot array in shared memory can vary. + + Each hash slot consists of a doubly linked list of entries. Because the cache layer is implemented + relocatable (independent of the shared memory's starting address), these doubly linked lists + use offsets relative to the starting address of the cache header instead of pointers. For this reason, + the array of hash slots is just an array of uintptr_t values, each containing an offset to the + first entry of the doubly linked list which belongs to this slot (a value of 0 means the slot is empty). + + The macros ENTRYAT and ENTRYOF are used to convert between offsets and pointers to cache entries. + Both expect the presence of cache->header that points to the cache header in the shared memory segment: - entry = &cache->slots[s]; + #define ENTRYAT(offset) ((apc_cache_entry_t *)((uintptr_t)cache->header + (uintptr_t)offset)) + #define ENTRYOF(entry) (((uintptr_t)entry) - (uintptr_t)cache->header) - cache->slots is our array of slots in the segment. + In Addition, the functions apc_cache_wlocked_link_entry() and apc_cache_wlocked_unlink_entry() + help inserting or removing entries into or from doubly linked lists. Using them, you don't have to + worry about offsets when adding or removing entries from lists. - So, on an insert we find the array position in the slots array by hashing the key provided. - If there are currently no other slots there, we just stick the created `apc_cache_entry_t` into the array. + apc_cache_wlocked_insert() shows what happens when a cache entry is inserted: - while (*entry) { - /* process expired entries and check for entry with matching key */ + /* calculate hash and entry */ + apc_cache_hash_slot(cache, key, &h, &s); + + uintptr_t *entry_offset = &cache->slots[s]; + + while (*entry_offset) { + apc_cache_entry_t *entry = ENTRYAT(*entry_offset); + /* process expired entries and check for entry with matching key */ } + /* link in new entry */ - new_entry->next = *entry; - *entry = new_entry; - - If there are other slots already at this position we walk the link list to get to - the end. - - While walking the linked list we also check to see if the cache has a TTL defined. - If while walking the linked list we see a slot that has expired, we remove it - since we are right there looking at it. This is the only place we remove stale - entries unless the shared memory segment fills up and we force a full expunge via - apc_cache_expunge(). apc_cache_expunge() walks all slots attempting deletion, how - deletion occurs depends on runtime parameters, see INSTALL for runtime parameter - configuration details. - - apc_cache_rlocked_find() simply hashes and returns the entry if it is there. If it is there - but older than the mtime in the entry we are looking for, we return indicating we didn't find it. - - API users are advised to use apc_cache_fetch over find for simplicity, this ensures - correct operation, fetch sets up the call to find and takes care of copying and releasing - the entry from the cache to a zval* provided. - - Next we need to understand what an actual cache entry looks like. Have a look at - apc_cache.h for the structs. Here is the definition of apc_cache_key_t: - - /* {{{ struct definition: apc_cache_key_t */ - typedef struct _apc_cache_key_t { - const char *str; /* pointer to constant string key */ - zend_uint len; /* length of data at str */ - zend_ulong h; /* pre-computed hash of key */ - time_t mtime; /* the mtime of this cached entry */ - apc_cache_owner_t owner; /* the context that created this key */ - } apc_cache_key_t; /* }}} */ - - To create a apc_cache_key_t structure, call apc_cache_make_key(), see apc_cache.h - - Ok, on to the actual cache entry, here is the definition of apc_cache_entry_t: - - /* {{{ struct definition: apc_cache_entry_t */ - typedef struct apc_cache_entry_t apc_cache_entry_t; - struct apc_cache_entry_t { - zval val; /* the zval copied at store time */ - uintptr_t next; /* offset in shm of next entry in linked list */ - zend_long ttl; /* the ttl on this specific entry */ - zend_long ref_count; /* the reference count of this entry */ - zend_long nhits; /* number of hits to this entry */ - time_t ctime; /* time entry was initialized */ - time_t mtime; /* the mtime of this cached entry */ - time_t dtime; /* time entry was removed from cache */ - time_t atime; /* time entry was last accessed */ - zend_long mem_size; /* memory used */ - zend_string key; /* entry key (MUST BE THE LAST FIELD OF THE STRUCT!) */ - }; - /* }}} */ - - To create an apc_cache_entry_t, call apc_cache_make_entry(), see apc_cache.h - - Any of the structures taken by apc_cache_* functions have their equivalent apc_cache_make_* - - If an insertion of an entry should fail, it falls to the caller of insert to free - the pooled resources used to create the entry. - -7. Serializers - - The way data is serialized and unserialized can be found in apc_persist.c. - Saving to shared memory (persist) is done using `apc_persist_context_t`. - Both the entry key and the entry's zval get persisted into shared memory in a continuous block - for the single entry. - - typedef struct _apc_persist_context_t { - /* Serializer to use */ - apc_serializer_t *serializer; - /* Computed size of the needed SMA allocation */ - size_t size; - /* Whether or not we may have to memoize refcounted addresses */ - zend_bool memoization_needed; - /* Whether to serialize the top-level value */ - zend_bool use_serialization; - /* Serialized object/array string, in case there can only be one */ - unsigned char *serialized_str; - size_t serialized_str_len; - /* Whole SMA allocation */ - char *alloc; - /* Current position in allocation */ - char *alloc_cur; - /* HashTable storing refcounteds for which the size has already been counted. */ - HashTable already_counted; - /* HashTable storing already allocated refcounteds. Pointers to refcounteds are stored. */ - HashTable already_allocated; - } apc_persist_context_t; + apc_cache_wlocked_link_entry(cache, entry_offset, new_entry); + + During insertion, we get the array position in the hash slot array (cache->slots) by hashing + the key. If there are currently no other entries in the slot, we just link the new entry + (apc_cache_entry_t) into the slot, which results in a doubly linked list with one entry. + + If there are other entries in the slot, we walk to the end of the linked list. As we traverse + the linked list, we also remove expired entries since we are right there looking at them and + we already have a write-lock. This is the only place where we remove stale entries unless the + shared memory segment is full and we try to cleanup the cache via apc_cache_default_expunge(). + + apc_cache_rlocked_find() simply tries to return an entry by searching for the key (hash lookup + + traversing the linked list). If an entry with the same key exists but its TTL has expired, + we return that it was not found. + + Developers are encouraged to use apc_cache_fetch() instead of find for simplicity. This ensures + correct operation, because fetch sets up the read lock, the call to find, and takes care of + copying and releasing the entry from the cache to a provided zval*. + + The function apc_cache_default_expunge() removes all expired entries and defragments the shared + memory to get as much free contiguous memory as possible. If the removal of expired entries and + defragmentation doesn't free enough contiguous memory, a full cache wipe is performed + via apc_cache_wlocked_real_expunge(). + +5.3 Persistence + + Before an entry can be inserted into a linked list, it must be created (persisted) in the + shared memory segment. This is done with apc_persist() in apc_persist.c. The following steps + are required to persist an entry: + - The amount of memory needed to store the entry is calculated by apc_persist_calc(). + - The shared memory to persist the entry is allocated by apc_sma_malloc(). + - The entry is persisted in the allocated shared memory by apc_persist_create_entry(). + + The persistence representation of an entry is implemented independent of its position + in the shared memory segment, which enables us to move around entries during defragmentation + and to make the entire cache relocatable. This is achieved by converting all entry's pointers + to offsets relative to the entry's starting address during persistence. + + The function apc_unpersist() creates a process-local copy of the persisted entry's value (zval), + which can be passed to the php runtime after an entry has been found. During apc_unpersist(), + all offsets relative to the entry's starting address must be converted back to process-local + addresses / pointers before data access. It is important to note that this conversion must + occur on the stack or in process-local memory and that the shared memory representation of the + entry must not be modified, as this would break the persistence representation. + + In some cases, the value (zval) of an entry is persisted by using a serializer. Whether a + serializer is used depends primarily on the data type of the value (e.g., arrays or objects). + For simple types as null/bool/int/float/string, serializers are unnecessary and not used. The ini setting `apc.serializer` can be used to customize the `apc_serializer_t *serializer`. - This affects which serializer is used for PHP objects or arrays. - (for a top level null/bool/int/float/string, serializers are unnecessary and not used) - - - `apc.serializer=php` (default) will use serialize() and unserialize() for serializing arrays/ - This has lower memory usage than `apc.serializer=default` for most use cases - - `apc.serializer=default` is used for arrays that don't contain objects, and will store the array structure in shared memory - in a form that allows deduplicating values as well as faster unserialization of small arrays, at the cost of generally having higher memory usage. - - For arrays that contain objects, it falls back to php's native serialize()/unserialize() - - APCu can be configured to use third party serializers if they are compiled with support for apcu. - For example, `apc.serializer=igbinary` (https://github.com/igbinary/igbinary) can be used for generally faster unserialization and lower memory usage than apc.serializer=php - (requires that igbinary be configured and compiled after APCu is installed) + This affects which serializer is used (Default: apc.serializer=php). -If you made it to the end of this, you should have a pretty good idea of where things are in -the code. There is much more reading to do in headers ... good luck ... + APCu can be configured to use third-party serializers if they are compiled with support for apcu. + For example, `apc.serializer=igbinary` (https://github.com/igbinary/igbinary) can be used for + generally faster unserialization and lower memory usage than apc.serializer=php + (requires that igbinary is configured and compiled after APCu is installed) + +6. Relocatable design + + All layers of APCu are implemented in a relocatable manner. The goal of the relocatable design + is to allow independent processes to attach to the same shared memory segment in the future, even + if they use different starting addresses for the shared memory segment. This is achieved by using + offset-based addressing instead of pointers in all layers of APCu. Therefore, you will not find + any pointers in the entire shared memory segment. So, do not store pointers (absolute addresses) + in the shared memory segment, as doing so will likely break the relocatable design! + +If you made it to the end of this, you should have a pretty good idea of where things are in +the code. There is much more reading to do in headers ... good luck ... diff --git a/apc.c b/apc.c index 893bdeab..e2d039ca 100644 --- a/apc.c +++ b/apc.c @@ -34,7 +34,7 @@ #include "apc_globals.h" #include "php.h" -/* {{{ console display functions */ +/* console display functions */ #define APC_PRINT_FUNCTION(name, verbosity) \ void apc_##name(const char *format, ...) \ { \ @@ -54,9 +54,7 @@ APC_PRINT_FUNCTION(debug, E_NOTICE) #else void apc_debug(const char *format, ...) {} #endif -/* }}} */ -/* {{{ apc_flip_hash */ HashTable* apc_flip_hash(HashTable *hash) { zval data, *entry; HashTable *new_hash; @@ -79,7 +77,6 @@ HashTable* apc_flip_hash(HashTable *hash) { return new_hash; } -/* }}} */ /* * Serializer API @@ -88,9 +85,7 @@ HashTable* apc_flip_hash(HashTable *hash) { /* pointer to the list of serializers */ static apc_serializer_t apc_serializers[APC_MAX_SERIALIZERS] = {{0,}}; -/* }}} */ -/* {{{ apc_register_serializer */ PHP_APCU_API int _apc_register_serializer( const char* name, apc_serialize_t serialize, apc_unserialize_t unserialize, void *config) { int i; @@ -112,14 +107,12 @@ PHP_APCU_API int _apc_register_serializer( } return 0; -} /* }}} */ +} -/* {{{ apc_get_serializers */ PHP_APCU_API apc_serializer_t* apc_get_serializers() { return &(apc_serializers[0]); -} /* }}} */ +} -/* {{{ apc_find_serializer */ PHP_APCU_API apc_serializer_t* apc_find_serializer(const char* name) { int i; apc_serializer_t *serializer; @@ -131,7 +124,7 @@ PHP_APCU_API apc_serializer_t* apc_find_serializer(const char* name) { } } return NULL; -} /* }}} */ +} /* * Local variables: diff --git a/apc.h b/apc.h index 9335499c..d913c96b 100644 --- a/apc.h +++ b/apc.h @@ -107,32 +107,26 @@ PHP_APCU_API HashTable* apc_flip_hash(HashTable *hash); typedef int (*apc_serialize_t)(APC_SERIALIZER_ARGS); typedef int (*apc_unserialize_t)(APC_UNSERIALIZER_ARGS); -/* {{{ struct definition: apc_serializer_t */ typedef struct apc_serializer_t { const char* name; apc_serialize_t serialize; apc_unserialize_t unserialize; void* config; } apc_serializer_t; -/* }}} */ -/* {{{ _apc_register_serializer - registers the serializer using the given name and parameters */ +/* registers the serializer using the given name and parameters */ PHP_APCU_API int _apc_register_serializer( const char* name, apc_serialize_t serialize, apc_unserialize_t unserialize, void *config); -/* }}} */ -/* {{{ apc_get_serializers - fetches the list of serializers */ -PHP_APCU_API apc_serializer_t* apc_get_serializers(void); /* }}} */ +/* fetches the list of serializers */ +PHP_APCU_API apc_serializer_t* apc_get_serializers(void); -/* {{{ apc_find_serializer - finds a previously registered serializer by name */ -PHP_APCU_API apc_serializer_t* apc_find_serializer(const char* name); /* }}} */ +/* finds a previously registered serializer by name */ +PHP_APCU_API apc_serializer_t* apc_find_serializer(const char* name); -/* {{{ default serializers */ +/* default serializers */ PHP_APCU_API int APC_SERIALIZER_NAME(php) (APC_SERIALIZER_ARGS); -PHP_APCU_API int APC_UNSERIALIZER_NAME(php) (APC_UNSERIALIZER_ARGS); /* }}} */ +PHP_APCU_API int APC_UNSERIALIZER_NAME(php) (APC_UNSERIALIZER_ARGS); #define php_apc_try \ { \ diff --git a/apc.php b/apc.php index dddb4544..046f2286 100644 --- a/apc.php +++ b/apc.php @@ -766,6 +766,8 @@ function block_sort($array1, $array2) $insert_rate_user = sprintf("%.2f", $cache['num_inserts'] ? (($cache['num_inserts'])/$elapsed) : 0); $apcversion = phpversion('apcu'); $phpversion = phpversion(); + $cleanups = $cache['cleanups'] ?? '-'; + $defragmentations = $cache['defragmentations'] ?? '-'; $number_vars = $cache['num_entries']; $size_vars = bsize($cache['mem_size']); $num_hits_and_misses = $cache['num_hits'] + $cache['num_misses']; @@ -804,6 +806,8 @@ function block_sort($array1, $array2) Hit Rate$hit_rate_user cache requests/second Miss Rate$miss_rate_user cache requests/second Insert Rate$insert_rate_user cache requests/second + Cache cleanup count$cleanups + Cache defragmentation count$defragmentations Cache full count{$cache['expunges']} diff --git a/apc_cache.c b/apc_cache.c index 48bb863d..1ff4305e 100644 --- a/apc_cache.c +++ b/apc_cache.c @@ -60,7 +60,7 @@ apc_cache_entry_t *apc_persist( apc_sma_t *sma, apc_serializer_t *serializer, zend_string *key, const zval *val); zend_bool apc_unpersist(zval *dst, const apc_cache_entry_t *entry, apc_serializer_t *serializer); -/* {{{ make_prime */ +/* make_prime */ static int const primes[] = { 257, /* 256 */ 521, /* 512 */ @@ -120,19 +120,17 @@ static int make_prime(int n) } return *(k-1); } -/* }}} */ static inline void free_entry(apc_cache_t *cache, apc_cache_entry_t *entry) { apc_sma_free(cache->sma, entry); } -/* {{{ apc_cache_hash_slot - Note: These calculations can and should be done outside of a lock */ +/* These calculations can and should be done outside of a lock */ static inline void apc_cache_hash_slot( apc_cache_t* cache, zend_string *key, zend_ulong* hash, size_t* slot) { *hash = ZSTR_HASH(key); *slot = *hash % cache->nslots; -} /* }}} */ +} static inline zend_bool apc_entry_key_equals(const apc_cache_entry_t *entry, zend_string *key, zend_ulong hash) { return ZSTR_H(&entry->key) == hash @@ -140,18 +138,18 @@ static inline zend_bool apc_entry_key_equals(const apc_cache_entry_t *entry, zen && memcmp(ZSTR_VAL(&entry->key), ZSTR_VAL(key), ZSTR_LEN(key)) == 0; } -/* An entry is hard expired if the creation time if older than the per-entry TTL. - * Hard expired entries must be treated indentially to non-existent entries. */ +/* An entry is hard expired if the creation time is older than the per-entry TTL. + * Hard expired entries must be treated identically to non-existent entries. */ static zend_bool apc_cache_entry_hard_expired(apc_cache_entry_t *entry, time_t t) { return entry->ttl && (time_t) (entry->ctime + entry->ttl) < t; } -/* An entry is soft expired if no per-entry TTL is set, a global cache TTL is set, +/* An entry is soft expired if a global cache TTL is set, * and the access time of the entry is older than the global TTL. Soft expired entries * are accessible by lookup operation, but may be removed from the cache at any time. */ static zend_bool apc_cache_entry_soft_expired( apc_cache_t *cache, apc_cache_entry_t *entry, time_t t) { - return !entry->ttl && cache->ttl && (time_t) (entry->atime + cache->ttl) < t; + return cache->ttl && (time_t) (entry->atime + cache->ttl) < t; } static zend_bool apc_cache_entry_expired( @@ -160,34 +158,67 @@ static zend_bool apc_cache_entry_expired( || apc_cache_entry_soft_expired(cache, entry, t); } -/* {{{ apc_cache_wlocked_remove_entry */ -static void apc_cache_wlocked_remove_entry(apc_cache_t *cache, uintptr_t *entry_offset) -{ - apc_cache_entry_t *dead = ENTRYAT(*entry_offset); +/* apc_cache_wlocked_move_entry() is called during defragmentation, before an entry is moved to a new position. */ +static zend_bool apc_cache_wlocked_move_entry(apc_cache_t *cache, apc_cache_entry_t *old, apc_cache_entry_t *new) { + /* Check if the entry can be moved. */ + if (old->ref_count > 0) { + return 0; + } - /* unlink entry from list */ - *entry_offset = dead->next; + /* Change all references to this entry to the new position. + * Since “next” is the 1st field of apc_cache_entry_t, the head pointer of the list + * can be changed like a previous entry via ENTRYAT(old->prev)->next. */ + ENTRYAT(old->prev)->next = ENTRYOF(new); + if (old->next) { + ENTRYAT(old->next)->prev = ENTRYOF(new); + } + + return 1; +} + +/* Inserts an entry into a linked list. The argument entry_offset must point either + * to entry->next of an existing entry or to the head pointer of a linked list. */ +static void apc_cache_wlocked_link_entry(apc_cache_t *cache, uintptr_t *entry_offset, apc_cache_entry_t *entry) { + entry->next = *entry_offset; + entry->prev = ENTRYOF(entry_offset); + *entry_offset = ENTRYOF(entry); + if (entry->next) { + ENTRYAT(entry->next)->prev = *entry_offset; + } +} + +/* Removes an entry from a linked list. */ +static void apc_cache_wlocked_unlink_entry(apc_cache_t *cache, apc_cache_entry_t *entry) { + /* Since “next” is the 1st field of apc_cache_entry_t, the head pointer of the list + * can be changed like a previous entry via ENTRYAT(entry->prev)->next. */ + ENTRYAT(entry->prev)->next = entry->next; + if (entry->next) { + ENTRYAT(entry->next)->prev = entry->prev; + } +} + +static void apc_cache_wlocked_remove_entry(apc_cache_t *cache, apc_cache_entry_t *entry) +{ + /* unlink entry from list */ + apc_cache_wlocked_unlink_entry(cache, entry); /* adjust header info */ if (cache->header->mem_size) - cache->header->mem_size -= dead->mem_size; + cache->header->mem_size -= entry->mem_size; if (cache->header->nentries) cache->header->nentries--; /* free entry if there are no references */ - if (dead->ref_count <= 0) { - free_entry(cache, dead); + if (entry->ref_count <= 0) { + free_entry(cache, entry); } else { /* add to gc if there are still refs */ - dead->dtime = time(0); - dead->next = cache->header->gc; - cache->header->gc = ENTRYOF(dead); + entry->dtime = time(0); + apc_cache_wlocked_link_entry(cache, &cache->header->gc, entry); } } -/* }}} */ -/* {{{ apc_cache_wlocked_gc */ static void apc_cache_wlocked_gc(apc_cache_t* cache) { /* This function scans the list of removed cache entries and deletes any @@ -220,13 +251,12 @@ static void apc_cache_wlocked_gc(apc_cache_t* cache) } /* set next and free current entry */ - *entry_offset = entry->next; + apc_cache_wlocked_unlink_entry(cache, entry); free_entry(cache, entry); } } -/* }}} */ -/* {{{ php serializer */ +/* php serializer */ PHP_APCU_API int APC_SERIALIZER_NAME(php) (APC_SERIALIZER_ARGS) { smart_str strbuf = {0}; @@ -254,9 +284,9 @@ PHP_APCU_API int APC_SERIALIZER_NAME(php) (APC_SERIALIZER_ARGS) return 1; } return 0; -} /* }}} */ +} -/* {{{ php unserializer */ +/* php unserializer */ PHP_APCU_API int APC_UNSERIALIZER_NAME(php) (APC_UNSERIALIZER_ARGS) { const unsigned char *tmp = buf; @@ -276,9 +306,8 @@ PHP_APCU_API int APC_UNSERIALIZER_NAME(php) (APC_UNSERIALIZER_ARGS) return 0; } return 1; -} /* }}} */ +} -/* {{{ apc_cache_create */ PHP_APCU_API apc_cache_t* apc_cache_create(apc_sma_t* sma, apc_serializer_t* serializer, zend_long size_hint, zend_long gc_ttl, zend_long ttl, zend_long smart, zend_bool defend) { apc_cache_t* cache; zend_long cache_size; @@ -294,7 +323,7 @@ PHP_APCU_API apc_cache_t* apc_cache_create(apc_sma_t* sma, apc_serializer_t* ser cache_size = sizeof(apc_cache_header_t) + nslots * sizeof(uintptr_t); /* allocate shm */ - cache->header = apc_sma_malloc(sma, cache_size); + cache->header = apc_sma_malloc(sma, cache_size, NULL); if (!cache->header) { zend_error_noreturn(E_CORE_ERROR, "Unable to allocate " ZEND_LONG_FMT " bytes of shared memory for cache structures. Either apc.shm_size is too small or apc.entries_hint too large", cache_size); @@ -308,6 +337,8 @@ PHP_APCU_API apc_cache_t* apc_cache_create(apc_sma_t* sma, apc_serializer_t* ser cache->header->nhits = 0; cache->header->nmisses = 0; cache->header->nentries = 0; + cache->header->ncleanups = 0; + cache->header->ndefragmentations = 0; cache->header->nexpunges = 0; cache->header->gc = 0; cache->header->stime = time(NULL); @@ -326,7 +357,7 @@ PHP_APCU_API apc_cache_t* apc_cache_create(apc_sma_t* sma, apc_serializer_t* ser CREATE_LOCK(&cache->header->lock); return cache; -} /* }}} */ +} static inline zend_bool apc_cache_wlocked_insert( apc_cache_t *cache, apc_cache_entry_t *new_entry, zend_bool exclusive) { @@ -356,7 +387,7 @@ static inline zend_bool apc_cache_wlocked_insert( return 0; } - apc_cache_wlocked_remove_entry(cache, entry_offset); + apc_cache_wlocked_remove_entry(cache, entry); break; } @@ -365,7 +396,7 @@ static inline zend_bool apc_cache_wlocked_insert( * entries, so we don't always have to skip past a bunch of stale entries. */ if (apc_cache_entry_expired(cache, entry, t)) { - apc_cache_wlocked_remove_entry(cache, entry_offset); + apc_cache_wlocked_remove_entry(cache, entry); continue; } @@ -374,8 +405,7 @@ static inline zend_bool apc_cache_wlocked_insert( } /* link in new entry */ - new_entry->next = *entry_offset; - *entry_offset = ENTRYOF(new_entry); + apc_cache_wlocked_link_entry(cache, entry_offset, new_entry); cache->header->mem_size += new_entry->mem_size; cache->header->nentries++; @@ -388,7 +418,7 @@ static void apc_cache_set_entry_values(apc_cache_entry_t *entry, const int32_t t { entry->ttl = ttl; entry->next = 0; - entry->ref_count = 0; + entry->prev = 0; entry->nhits = 0; entry->ctime = t; entry->mtime = t; @@ -421,6 +451,9 @@ static inline zend_bool apc_cache_store_internal( return 0; } + /* release entry, because the ref_count of a new entry is initialized to 1 during allocation */ + apc_cache_entry_release(cache, entry); + return 1; } @@ -499,7 +532,6 @@ static inline apc_cache_entry_t *apc_cache_rlocked_find_incref( return entry; } -/* {{{ apc_cache_store */ PHP_APCU_API zend_bool apc_cache_store( apc_cache_t* cache, zend_string *key, const zval *val, const int32_t ttl, const zend_bool exclusive) { @@ -534,17 +566,20 @@ PHP_APCU_API zend_bool apc_cache_store( ret = apc_cache_wlocked_insert(cache, entry, exclusive); } php_apc_finally { apc_cache_wunlock(cache); - } php_apc_end_try(); - if (!ret) { - free_entry(cache, entry); - } + if (ret) { + /* release entry, because the ref_count of a new entry is initialized to 1 during allocation */ + apc_cache_entry_release(cache, entry); + } else { + /* the entry mustn't be released before it is freed to prevent defragmentation from moving the entry */ + free_entry(cache, entry); + } + } php_apc_end_try(); return ret; -} /* }}} */ +} #ifndef ZTS -/* {{{ data_unserialize */ static zval data_unserialize(const char *filename) { zval retval; @@ -617,7 +652,7 @@ static int apc_load_data(apc_cache_t* cache, const char *data_file) apc_cache_store( cache, name, &data, 0, 1); zend_string_release(name); - zval_dtor(&data); + zval_ptr_dtor_nogc(&data); } return 1; } @@ -627,7 +662,7 @@ static int apc_load_data(apc_cache_t* cache, const char *data_file) } #endif -/* {{{ apc_cache_preload shall load the prepared data files in path into the specified cache */ +/* apc_cache_preload shall load the prepared data files in path into the specified cache */ PHP_APCU_API zend_bool apc_cache_preload(apc_cache_t* cache, const char *path) { #ifndef ZTS @@ -661,16 +696,13 @@ PHP_APCU_API zend_bool apc_cache_preload(apc_cache_t* cache, const char *path) apc_error("Cannot load data from apc.preload_path=%s in thread-safe mode", path); return 0; #endif -} /* }}} */ +} -/* {{{ apc_cache_entry_release */ PHP_APCU_API void apc_cache_entry_release(apc_cache_t *cache, apc_cache_entry_t *entry) { ATOMIC_DEC(entry->ref_count); } -/* }}} */ -/* {{{ apc_cache_detach */ PHP_APCU_API void apc_cache_detach(apc_cache_t *cache) { /* Important: This function should not clean up anything that's in shared memory, @@ -683,9 +715,7 @@ PHP_APCU_API void apc_cache_detach(apc_cache_t *cache) free(cache); } -/* }}} */ -/* {{{ apc_cache_wlocked_real_expunge */ static void apc_cache_wlocked_real_expunge(apc_cache_t* cache) { size_t i; @@ -696,7 +726,7 @@ static void apc_cache_wlocked_real_expunge(apc_cache_t* cache) { for (i = 0; i < cache->nslots; i++) { uintptr_t *entry_offset = &cache->slots[i]; while (*entry_offset) { - apc_cache_wlocked_remove_entry(cache, entry_offset); + apc_cache_wlocked_remove_entry(cache, ENTRYAT(*entry_offset)); } } @@ -711,9 +741,8 @@ static void apc_cache_wlocked_real_expunge(apc_cache_t* cache) { /* resets lastkey */ memset(&cache->header->lastkey, 0, sizeof(apc_cache_slam_key_t)); -} /* }}} */ +} -/* {{{ apc_cache_clear */ PHP_APCU_API void apc_cache_clear(apc_cache_t* cache) { if (!cache) { @@ -729,33 +758,39 @@ PHP_APCU_API void apc_cache_clear(apc_cache_t* cache) /* set info */ cache->header->stime = apc_time(); + cache->header->ncleanups = 0; + cache->header->ndefragmentations = 0; cache->header->nexpunges = 0; apc_cache_wunlock(cache); } -/* }}} */ -/* {{{ apc_cache_default_expunge */ -PHP_APCU_API void apc_cache_default_expunge(apc_cache_t* cache, size_t size) +PHP_APCU_API zend_bool apc_cache_default_expunge(apc_cache_t* cache, size_t size) { time_t t; size_t i; if (!cache) { - return; + return 1; } + /* get the number of cleanups before acquiring the lock */ + zend_long ncleanups = cache->header->ncleanups; + /* apc_time() depends on globals, don't read it if there's no cache. This may happen if SHM * is too small and the initial cache creation during MINIT triggers an expunge. */ t = apc_time(); /* get the lock for header */ if (!apc_cache_wlock(cache)) { - return; + return 1; } - /* gc */ - apc_cache_wlocked_gc(cache); + /* skip processing if another default expunge operation was performed while waiting for the write lock */ + if (ncleanups < cache->header->ncleanups) { + apc_cache_wunlock(cache); + return 0; + } /* smart > 1 increases the probability of a full cache wipe, * so expunge() is called less often when memory is low. */ @@ -768,7 +803,7 @@ PHP_APCU_API void apc_cache_default_expunge(apc_cache_t* cache, size_t size) apc_cache_entry_t *entry = ENTRYAT(*entry_offset); if (apc_cache_entry_expired(cache, entry, t)) { - apc_cache_wlocked_remove_entry(cache, entry_offset); + apc_cache_wlocked_remove_entry(cache, entry); continue; } @@ -777,20 +812,39 @@ PHP_APCU_API void apc_cache_default_expunge(apc_cache_t* cache, size_t size) } } - /* if the cache now has space, then reset last key */ - if (apc_sma_get_avail_size(cache->sma, size)) { - /* wipe lastkey */ - memset(&cache->header->lastkey, 0, sizeof(apc_cache_slam_key_t)); - } else { - /* with not enough space left in cache, we are forced to expunge */ + /* gc */ + apc_cache_wlocked_gc(cache); + + /* if all free blocks together do not provide enough memory, we immediately perform a real expunge */ + if (!apc_sma_check_avail(cache->sma, size)) { apc_cache_wlocked_real_expunge(cache); + goto end_lbl; } + /* increment defragmentation statistics */ + cache->header->ndefragmentations++; + + /* run defragmentation to coalesce free blocks */ + apc_sma_defrag(cache->sma, cache, (apc_sma_move_f)apc_cache_wlocked_move_entry); + + /* if size bytes can't be allocated as a contiguous block after defragmentation, we do a real expunge */ + if (!apc_sma_check_avail_contiguous(cache->sma, size)) { + apc_cache_wlocked_real_expunge(cache); + goto end_lbl; + } + + /* wipe lastkey */ + memset(&cache->header->lastkey, 0, sizeof(apc_cache_slam_key_t)); + +end_lbl: + /* Increment cache cleanup statistics (removal of expired entries). + * This should be done late to detect stacking of default expunge operations. */ + cache->header->ncleanups++; + apc_cache_wunlock(cache); + return 1; } -/* }}} */ -/* {{{ apc_cache_fetch */ PHP_APCU_API zend_bool apc_cache_fetch(apc_cache_t* cache, zend_string *key, time_t t, zval *dst) { apc_cache_entry_t *entry; @@ -818,9 +872,8 @@ PHP_APCU_API zend_bool apc_cache_fetch(apc_cache_t* cache, zend_string *key, tim } php_apc_end_try(); return retval; -} /* }}} */ +} -/* {{{ apc_cache_exists */ PHP_APCU_API zend_bool apc_cache_exists(apc_cache_t* cache, zend_string *key, time_t t) { apc_cache_entry_t *entry; @@ -833,14 +886,12 @@ PHP_APCU_API zend_bool apc_cache_exists(apc_cache_t* cache, zend_string *key, ti return 0; } - entry = apc_cache_rlocked_find_nostat(cache, key, t); + entry = apc_cache_rlocked_find(cache, key, t); apc_cache_runlock(cache); return entry != NULL; } -/* }}} */ -/* {{{ apc_cache_update */ PHP_APCU_API zend_bool apc_cache_update( apc_cache_t *cache, zend_string *key, apc_cache_updater_t updater, void *data, zend_bool insert_if_not_found, zend_long ttl) @@ -888,9 +939,7 @@ PHP_APCU_API zend_bool apc_cache_update( return 0; } -/* }}} */ -/* {{{ apc_cache_atomic_update_long */ PHP_APCU_API zend_bool apc_cache_atomic_update_long( apc_cache_t *cache, zend_string *key, apc_cache_atomic_updater_t updater, void *data, zend_bool insert_if_not_found, zend_long ttl) @@ -938,9 +987,7 @@ PHP_APCU_API zend_bool apc_cache_atomic_update_long( return 0; } -/* }}} */ -/* {{{ apc_cache_delete */ PHP_APCU_API zend_bool apc_cache_delete(apc_cache_t *cache, zend_string *key) { zend_ulong h; @@ -965,7 +1012,7 @@ PHP_APCU_API zend_bool apc_cache_delete(apc_cache_t *cache, zend_string *key) /* check for a match by hash and identifier */ if (apc_entry_key_equals(entry, key, h)) { /* executing removal */ - apc_cache_wlocked_remove_entry(cache, entry_offset); + apc_cache_wlocked_remove_entry(cache, entry); apc_cache_wunlock(cache); return 1; @@ -977,15 +1024,12 @@ PHP_APCU_API zend_bool apc_cache_delete(apc_cache_t *cache, zend_string *key) apc_cache_wunlock(cache); return 0; } -/* }}} */ -/* {{{ apc_cache_entry_fetch_zval */ PHP_APCU_API zend_bool apc_cache_entry_fetch_zval( apc_cache_t *cache, apc_cache_entry_t *entry, zval *dst) { return apc_unpersist(dst, entry, cache->serializer); } -/* }}} */ static inline void array_add_long(zval *array, zend_string *key, zend_long lval) { zval zv; @@ -999,7 +1043,6 @@ static inline void array_add_double(zval *array, zend_string *key, double dval) zend_hash_add_new(Z_ARRVAL_P(array), key, &zv); } -/* {{{ apc_cache_link_info */ static zval apc_cache_link_info(apc_cache_t *cache, apc_cache_entry_t *p) { zval link, zv; @@ -1019,9 +1062,7 @@ static zval apc_cache_link_info(apc_cache_t *cache, apc_cache_entry_t *p) return link; } -/* }}} */ -/* {{{ apc_cache_info */ PHP_APCU_API zend_bool apc_cache_info(zval *info, apc_cache_t *cache, zend_bool limited) { zval list; @@ -1047,7 +1088,9 @@ PHP_APCU_API zend_bool apc_cache_info(zval *info, apc_cache_t *cache, zend_bool add_assoc_double(info, "num_misses", (double) cache->header->nmisses); add_assoc_double(info, "num_inserts", (double) cache->header->ninserts); add_assoc_long(info, "num_entries", cache->header->nentries); - add_assoc_double(info, "expunges", (double) cache->header->nexpunges); + add_assoc_long(info, "cleanups", cache->header->ncleanups); + add_assoc_long(info, "defragmentations", cache->header->ndefragmentations); + add_assoc_long(info, "expunges", cache->header->nexpunges); add_assoc_long(info, "start_time", cache->header->stime); array_add_double(info, apc_str_mem_size, (double) cache->header->mem_size); @@ -1102,11 +1145,8 @@ PHP_APCU_API zend_bool apc_cache_info(zval *info, apc_cache_t *cache, zend_bool return 1; } -/* }}} */ -/* - fetches information about the key provided -*/ +/* fetches information about the key provided */ PHP_APCU_API void apc_cache_stat(apc_cache_t *cache, zend_string *key, zval *stat) { zend_ulong h; size_t s; @@ -1150,7 +1190,6 @@ PHP_APCU_API void apc_cache_stat(apc_cache_t *cache, zend_string *key, zval *sta } php_apc_end_try(); } -/* {{{ apc_cache_defense */ PHP_APCU_API zend_bool apc_cache_defense(apc_cache_t *cache, zend_string *key, time_t t) { /* only continue if slam defense is enabled */ @@ -1190,16 +1229,14 @@ PHP_APCU_API zend_bool apc_cache_defense(apc_cache_t *cache, zend_string *key, t return 0; } -/* }}} */ -/* {{{ apc_cache_serializer */ PHP_APCU_API void apc_cache_serializer(apc_cache_t* cache, const char* name) { if (cache && !cache->serializer) { cache->serializer = apc_find_serializer(name); } -} /* }}} */ +} -PHP_APCU_API void apc_cache_entry(apc_cache_t *cache, zend_string *key, zend_fcall_info *fci, zend_fcall_info_cache *fcc, zend_long ttl, zend_long now, zval *return_value) {/*{{{*/ +PHP_APCU_API void apc_cache_entry(apc_cache_t *cache, zend_string *key, zend_fcall_info *fci, zend_fcall_info_cache *fcc, zend_long ttl, zend_long now, zval *return_value) { apc_cache_entry_t *entry = NULL; if (!cache) { @@ -1239,7 +1276,6 @@ PHP_APCU_API void apc_cache_entry(apc_cache_t *cache, zend_string *key, zend_fca apc_cache_wunlock(cache); } php_apc_end_try(); } -/*}}}*/ /* * Local variables: diff --git a/apc_cache.h b/apc_cache.h index e800854f..90aebb22 100644 --- a/apc_cache.h +++ b/apc_cache.h @@ -46,11 +46,10 @@ struct apc_cache_slam_key_t { #endif }; -/* {{{ struct definition: apc_cache_entry_t */ typedef struct apc_cache_entry_t apc_cache_entry_t; struct apc_cache_entry_t { - zval val; /* the zval copied at store time */ - uintptr_t next; /* offset in shm of next entry in linked list */ + uintptr_t next; /* offset to next entry (MUST BE THE 1st FIELD OF THE STRUCT!) */ + uintptr_t prev; /* offset to previous entry / head-pointer of the linked list */ zend_long ttl; /* the ttl on this specific entry */ zend_long ref_count; /* the reference count of this entry */ zend_long nhits; /* number of hits to this entry */ @@ -59,26 +58,26 @@ struct apc_cache_entry_t { time_t dtime; /* time entry was removed from cache */ time_t atime; /* time entry was last accessed */ zend_long mem_size; /* memory used */ + zval val; /* the zval copied at store time */ zend_string key; /* entry key (MUST BE THE LAST FIELD OF THE STRUCT!) */ }; -/* }}} */ -/* {{{ struct definition: apc_cache_header_t - Any values that must be shared among processes should go in here. */ +/* Any values that must be shared among processes should go in here. */ typedef struct _apc_cache_header_t { apc_lock_t lock; /* header lock */ zend_long nhits; /* hit count */ zend_long nmisses; /* miss count */ zend_long ninserts; /* insert count */ - zend_long nexpunges; /* expunge count */ + zend_long ncleanups; /* default expunge count */ + zend_long ndefragmentations; /* defragmentation count */ + zend_long nexpunges; /* real expunge count */ zend_long nentries; /* entry count */ zend_long mem_size; /* used */ time_t stime; /* start time */ apc_cache_slam_key_t lastkey; /* last key inserted (not necessarily without error) */ uintptr_t gc; /* offset in shm to the first entry of gc list */ -} apc_cache_header_t; /* }}} */ +} apc_cache_header_t; -/* {{{ struct definition: apc_cache_t */ typedef struct _apc_cache_t { apc_cache_header_t* header; /* cache header (stored in SHM) */ uintptr_t* slots; /* array of cache slots (stored in SHM) */ @@ -89,13 +88,11 @@ typedef struct _apc_cache_t { zend_long ttl; /* if slot is needed and entry's access time is older than this ttl, remove it */ zend_long smart; /* smart parameter for gc */ zend_bool defend; /* defense parameter for runtime */ -} apc_cache_t; /* }}} */ +} apc_cache_t; -/* {{{ typedef: apc_cache_updater_t */ -typedef zend_bool (*apc_cache_updater_t)(apc_cache_t*, apc_cache_entry_t*, void* data); /* }}} */ +typedef zend_bool (*apc_cache_updater_t)(apc_cache_t*, apc_cache_entry_t*, void* data); -/* {{{ typedef: apc_cache_atomic_updater_t */ -typedef zend_bool (*apc_cache_atomic_updater_t)(apc_cache_t*, zend_long*, void* data); /* }}} */ +typedef zend_bool (*apc_cache_atomic_updater_t)(apc_cache_t*, zend_long*, void* data); /* * apc_cache_create creates the shared memory cache. @@ -126,6 +123,7 @@ typedef zend_bool (*apc_cache_atomic_updater_t)(apc_cache_t*, zend_long*, void* PHP_APCU_API apc_cache_t* apc_cache_create( apc_sma_t* sma, apc_serializer_t* serializer, zend_long size_hint, zend_long gc_ttl, zend_long ttl, zend_long smart, zend_bool defend); + /* * apc_cache_preload preloads the data at path into the specified cache */ @@ -149,6 +147,7 @@ PHP_APCU_API void apc_cache_clear(apc_cache_t* cache); PHP_APCU_API zend_bool apc_cache_store( apc_cache_t* cache, zend_string *key, const zval *val, const int32_t ttl, const zend_bool exclusive); + /* * apc_cache_update updates an entry in place. The updater function must not bailout. * The update is performed under write-lock and doesn't have to be atomic. @@ -167,7 +166,6 @@ PHP_APCU_API zend_bool apc_cache_atomic_update_long( /* * apc_cache_fetch fetches an entry from the cache directly into dst - * */ PHP_APCU_API zend_bool apc_cache_fetch(apc_cache_t* cache, zend_string *key, time_t t, zval *dst); @@ -182,7 +180,8 @@ PHP_APCU_API zend_bool apc_cache_exists(apc_cache_t* cache, zend_string *key, ti */ PHP_APCU_API zend_bool apc_cache_delete(apc_cache_t* cache, zend_string *key); -/* apc_cache_fetch_zval copies a cache entry value to be usable at runtime. +/* + * apc_cache_fetch_zval copies a cache entry value to be usable at runtime. */ PHP_APCU_API zend_bool apc_cache_entry_fetch_zval( apc_cache_t *cache, apc_cache_entry_t *entry, zval *dst); @@ -199,20 +198,20 @@ PHP_APCU_API zend_bool apc_cache_entry_fetch_zval( PHP_APCU_API void apc_cache_entry_release(apc_cache_t *cache, apc_cache_entry_t *entry); /* - fetches information about the cache provided for userland status functions -*/ + * fetches information about the cache provided for userland status functions + */ PHP_APCU_API zend_bool apc_cache_info(zval *info, apc_cache_t *cache, zend_bool limited); /* - fetches information about the key provided -*/ + * fetches information about the key provided + */ PHP_APCU_API void apc_cache_stat(apc_cache_t *cache, zend_string *key, zval *stat); /* * apc_cache_defense: guard against slamming a key -* will return true if the following conditions are met: -* the key provided has a matching hash and length to the last key inserted into cache -* the last key has a different owner +* will return true if the following conditions are met: +* - the key provided has a matching hash and length to the last key inserted into cache +* - the last key has a different owner * in ZTS mode, TSRM determines owner * in non-ZTS mode, PID determines owner * Note: this function sets the owner of key during execution @@ -220,8 +219,7 @@ PHP_APCU_API void apc_cache_stat(apc_cache_t *cache, zend_string *key, zval *sta PHP_APCU_API zend_bool apc_cache_defense(apc_cache_t *cache, zend_string *key, time_t t); /* -* apc_cache_serializer -* sets the serializer for a cache, and by proxy contexts created for the cache +* apc_cache_serializer sets the serializer for a cache, and by proxy contexts created for the cache. * Note: this avoids race conditions between third party serializers and APCu */ PHP_APCU_API void apc_cache_serializer(apc_cache_t* cache, const char* name); @@ -240,7 +238,11 @@ PHP_APCU_API void apc_cache_serializer(apc_cache_t* cache, const char* name); * Note: beware of locking (copy it exactly), setting states is also important */ -/* {{{ apc_cache_default_expunge +/* +* apc_cache_default_expunge() is executed by the sma layer when there is not enough +* free shared memory to satisfy an allocation request. It attempts to free memory +* (e.g., by removing entries) so that the allocation request can be satisfied. +* * Where smart is not set: * 1) Perform cleanup of stale entries * 2) If available memory is less than the size requested, run full expunge @@ -251,7 +253,7 @@ PHP_APCU_API void apc_cache_serializer(apc_cache_t* cache, const char* name); * * The TTL of an entry takes precedence over the TTL of a cache */ -PHP_APCU_API void apc_cache_default_expunge(apc_cache_t* cache, size_t size); +PHP_APCU_API zend_bool apc_cache_default_expunge(apc_cache_t* cache, size_t size); /* * apc_cache_entry: generate and create or fetch an entry diff --git a/apc_globals.h b/apc_globals.h index 0eb1da1f..27100bd4 100644 --- a/apc_globals.h +++ b/apc_globals.h @@ -44,7 +44,8 @@ ZEND_BEGIN_MODULE_GLOBALS(apcu) zend_long smart; /* smart value */ #ifdef APC_MMAP - char *mmap_file_mask; /* mktemp-style file-mask to pass to mmap */ + char *mmap_file_mask; /* mktemp-style file-mask to pass to mmap */ + zend_long mmap_hugepage_size; /* hugepage size flag to pass to mmap (0: none)*/ #endif /* module variables */ diff --git a/apc_iterator.c b/apc_iterator.c index 9181085c..535a9df7 100644 --- a/apc_iterator.c +++ b/apc_iterator.c @@ -45,7 +45,6 @@ zend_class_entry* apc_iterator_get_ce(void) { return; \ } -/* {{{ apc_iterator_item */ static apc_iterator_item_t* apc_iterator_item_ctor( apc_iterator_t *iterator, apc_cache_entry_t *entry) { zval zv; @@ -108,17 +107,13 @@ static apc_iterator_item_t* apc_iterator_item_ctor( return item; } -/* }}} */ -/* {{{ apc_iterator_item_dtor */ static void apc_iterator_item_dtor(apc_iterator_item_t *item) { zend_string_release(item->key); zval_ptr_dtor(&item->value); efree(item); } -/* }}} */ -/* {{{ acp_iterator_free */ static void apc_iterator_free(zend_object *object) { apc_iterator_t *iterator = apc_iterator_fetch_from(object); @@ -148,9 +143,7 @@ static void apc_iterator_free(zend_object *object) { zend_object_std_dtor(object); } -/* }}} */ -/* {{{ apc_iterator_create */ zend_object* apc_iterator_create(zend_class_entry *ce) { apc_iterator_t *iterator = (apc_iterator_t*) emalloc(sizeof(apc_iterator_t) + zend_object_properties_size(ce)); @@ -166,11 +159,8 @@ zend_object* apc_iterator_create(zend_class_entry *ce) { return &iterator->obj; } -/* }}} */ -/* {{{ apc_iterator_search_match - * Verify if the key matches our search parameters - */ +/* Verifies if the key matches our search parameters */ static int apc_iterator_search_match(apc_iterator_t *iterator, apc_cache_entry_t *entry) { int rval = 1; @@ -194,9 +184,7 @@ static int apc_iterator_search_match(apc_iterator_t *iterator, apc_cache_entry_t return rval; } -/* }}} */ -/* {{{ apc_iterator_check_expiry */ static int apc_iterator_check_expiry(apc_cache_t* cache, apc_cache_entry_t *entry, time_t t) { if (entry->ttl) { @@ -207,9 +195,7 @@ static int apc_iterator_check_expiry(apc_cache_t* cache, apc_cache_entry_t *entr return 1; } -/* }}} */ -/* {{{ apc_iterator_fetch_active */ static size_t apc_iterator_fetch_active(apc_iterator_t *iterator) { apc_cache_t *cache = apc_user_cache; size_t count = 0; @@ -249,9 +235,7 @@ static size_t apc_iterator_fetch_active(apc_iterator_t *iterator) { return count; } -/* }}} */ -/* {{{ apc_iterator_fetch_deleted */ static size_t apc_iterator_fetch_deleted(apc_iterator_t *iterator) { apc_cache_t *cache = apc_user_cache; size_t count = 0; @@ -287,9 +271,7 @@ static size_t apc_iterator_fetch_deleted(apc_iterator_t *iterator) { return count; } -/* }}} */ -/* {{{ apc_iterator_totals */ static void apc_iterator_totals(apc_iterator_t *iterator) { apc_cache_t *cache = apc_user_cache; time_t t = apc_time(); @@ -320,7 +302,6 @@ static void apc_iterator_totals(apc_iterator_t *iterator) { apc_cache_runlock(cache); } php_apc_end_try(); } -/* }}} */ void apc_iterator_obj_init(apc_iterator_t *iterator, zval *search, zend_long format, size_t chunk_size, zend_long list) { @@ -506,7 +487,6 @@ PHP_METHOD(APCUIterator, getTotalHits) { RETURN_LONG(iterator->hits); } -/* }}} */ PHP_METHOD(APCUIterator, getTotalSize) { apc_iterator_t *iterator = apc_iterator_fetch(getThis()); @@ -540,7 +520,6 @@ PHP_METHOD(APCUIterator, getTotalCount) { RETURN_LONG(iterator->count); } -/* {{{ apc_iterator_init */ int apc_iterator_init(int module_number) { zend_class_entry ce; @@ -573,13 +552,11 @@ int apc_iterator_init(int module_number) { return SUCCESS; } -/* }}} */ int apc_iterator_shutdown(int module_number) { return SUCCESS; } -/* {{{ apc_iterator_delete */ int apc_iterator_delete(zval *zobj) { apc_iterator_t *iterator; zend_class_entry *ce = Z_OBJCE_P(zobj); @@ -606,8 +583,6 @@ int apc_iterator_delete(zval *zobj) { return 1; } -/* }}} */ - /* * Local variables: diff --git a/apc_iterator.h b/apc_iterator.h index 9feda387..d38a5a35 100644 --- a/apc_iterator.h +++ b/apc_iterator.h @@ -46,7 +46,6 @@ #define APC_ITER_NONE 0 #define APC_ITER_ALL (0xffffffffL) -/* {{{ apc_iterator_t */ typedef struct _apc_iterator_t { short int initialized; /* sanity check in case __construct failed */ zend_long format; /* format bitmask of the return values ie: key, value, info */ @@ -69,17 +68,14 @@ typedef struct _apc_iterator_t { zend_long count; /* count total */ zend_object obj; } apc_iterator_t; -/* }}} */ #define apc_iterator_fetch_from(o) ((apc_iterator_t*)((char*)o - XtOffsetOf(apc_iterator_t, obj))) #define apc_iterator_fetch(z) apc_iterator_fetch_from(Z_OBJ_P(z)) -/* {{{ apc_iterator_item */ typedef struct _apc_iterator_item_t { zend_string *key; zval value; } apc_iterator_item_t; -/* }}} */ PHP_APCU_API void apc_iterator_obj_init( apc_iterator_t *iterator, diff --git a/apc_lock.h b/apc_lock.h index 7288cb9f..a808814c 100644 --- a/apc_lock.h +++ b/apc_lock.h @@ -22,10 +22,10 @@ /* APCu works most efficiently where there is access to native read/write locks If the current system has native rwlocks present they will be used, if they are - not present, APCu will emulate their behavior with standard mutex. + not present, APCu will emulate their behavior with standard mutex. While APCu is emulating read/write locks, reads and writes are exclusive, - additionally the write lock prefers readers, as is the default behaviour of - the majority of Posix rwlock implementations + additionally the write lock prefers readers, as is the default behavior of + the majority of Posix rwlock implementations */ #ifdef HAVE_CONFIG_H @@ -65,13 +65,14 @@ typedef apc_windows_cs_rwlock_t apc_lock_t; # define APC_LOCK_SHARED #endif -/* {{{ functions */ /* - The following functions should be called once per process: - apc_lock_init initializes attributes suitable for all locks - apc_lock_cleanup destroys those attributes - This saves us from having to create and destroy attributes for - every lock we use at runtime */ + The following functions should be called once per process: + - apc_lock_init initializes attributes suitable for all locks + - apc_lock_cleanup destroys those attributes + + This saves us from having to create and destroy attributes for + every lock we use at runtime + */ PHP_APCU_API zend_bool apc_lock_init(void); PHP_APCU_API void apc_lock_cleanup(void); /* @@ -82,16 +83,15 @@ PHP_APCU_API zend_bool apc_lock_rlock(apc_lock_t *lock); PHP_APCU_API zend_bool apc_lock_wlock(apc_lock_t *lock); PHP_APCU_API zend_bool apc_lock_runlock(apc_lock_t *lock); PHP_APCU_API zend_bool apc_lock_wunlock(apc_lock_t *lock); -PHP_APCU_API void apc_lock_destroy(apc_lock_t *lock); /* }}} */ +PHP_APCU_API void apc_lock_destroy(apc_lock_t *lock); -/* {{{ generic locking macros */ +/* generic locking macros */ #define CREATE_LOCK(lock) apc_lock_create(lock) #define DESTROY_LOCK(lock) apc_lock_destroy(lock) #define WLOCK(lock) apc_lock_wlock(lock) #define WUNLOCK(lock) { apc_lock_wunlock(lock); HANDLE_UNBLOCK_INTERRUPTIONS(); } #define RLOCK(lock) apc_lock_rlock(lock) #define RUNLOCK(lock) { apc_lock_runlock(lock); HANDLE_UNBLOCK_INTERRUPTIONS(); } -/* }}} */ /* atomic operations */ #ifdef PHP_WIN32 diff --git a/apc_mmap.c b/apc_mmap.c index ad739818..225de401 100644 --- a/apc_mmap.c +++ b/apc_mmap.c @@ -51,7 +51,36 @@ # define MAP_ANON MAP_ANONYMOUS #endif -void *apc_mmap(char *file_mask, size_t size) +static int apc_mmap_hugepage_flags(size_t size, zend_long hugepage_size) +{ + if (!hugepage_size) return 0; // not use hugepages + +#if defined(MAP_HUGETLB) && defined(MAP_HUGE_MASK) && defined(MAP_HUGE_SHIFT) + if (size % hugepage_size) { + zend_error_noreturn(E_CORE_ERROR, "apc.shm_size must be a multiple of apc.mmap_hugepage_size"); + } + + zend_long page_size = hugepage_size; + int log2_page_size = -1; + + // calculate log2 of hugepage size + while (page_size) { + page_size >>= 1; + log2_page_size++; + } + + if (!log2_page_size || (log2_page_size & MAP_HUGE_MASK) != log2_page_size) { + // maybe hugepage size is too large or small + zend_error_noreturn(E_CORE_ERROR, "Invalid hugepage size: %ld", hugepage_size); + } + + return MAP_HUGETLB | ((unsigned int)log2_page_size << MAP_HUGE_SHIFT); +#else + zend_error_noreturn(E_CORE_ERROR, "This system does not support hugepages"); +#endif +} + +void *apc_mmap(char *file_mask, size_t size, zend_long hugepage_size) { void *shmaddr; int fd = -1; @@ -84,15 +113,22 @@ void *apc_mmap(char *file_mask, size_t size) unlink(file_mask); } + flags |= apc_mmap_hugepage_flags(size, hugepage_size); shmaddr = (void *)mmap(NULL, size, PROT_READ | PROT_WRITE, flags, fd, 0); if ((long)shmaddr == -1) { - zend_error_noreturn(E_CORE_ERROR, "apc_mmap: Failed to mmap %zu bytes. Is your apc.shm_size too large?", size); + if (hugepage_size) { + zend_error_noreturn(E_CORE_ERROR, "apc_mmap: Failed to mmap %zu bytes with hugepage size %ld. apc.shm_size may be too large, apc.mmap_hugepage_size may be invalid, or the system lacks sufficient reserved hugepages.", size, hugepage_size); + } else { + zend_error_noreturn(E_CORE_ERROR, "apc_mmap: Failed to mmap %zu bytes. apc.shm_size may be too large.", size); + } } #ifdef MADV_HUGEPAGE /* enable transparent huge pages to reduce TLB misses (Linux only) */ - madvise(shmaddr, size, MADV_HUGEPAGE); + if (!hugepage_size) { + madvise(shmaddr, size, MADV_HUGEPAGE); + } #endif if (fd != -1) close(fd); diff --git a/apc_mmap.h b/apc_mmap.h index 142a6e55..ab5aaec7 100644 --- a/apc_mmap.h +++ b/apc_mmap.h @@ -35,7 +35,7 @@ /* Wrapper functions for shared memory mapped files */ #ifdef APC_MMAP -void *apc_mmap(char *file_mask, size_t size); +void *apc_mmap(char *file_mask, size_t size, zend_long hugepage_size); void apc_unmap(void *shmaddr, size_t size); #endif diff --git a/apc_persist.c b/apc_persist.c index 8f74e347..e322b8d2 100644 --- a/apc_persist.c +++ b/apc_persist.c @@ -333,7 +333,8 @@ static const uint32_t uninitialized_bucket[-HT_MIN_MASK] = {HT_INVALID_IDX, HT_I static zend_array *apc_persist_copy_ht(apc_persist_context_t *ctxt, const HashTable *orig_ht) { #if PHP_VERSION_ID >= 70300 if (orig_ht->nNumOfElements == 0) { - return (HashTable *)&zend_empty_array; + /* To indicate using zend_empty_array during unpersist, we point to the entry's starting address. */ + return (HashTable *)ctxt->alloc; } #endif HashTable *ht = COPY(orig_ht, sizeof(HashTable)); @@ -474,6 +475,13 @@ static apc_cache_entry_t *apc_persist_create_entry( return entry; } +static void apc_persist_sma_init_entry(apc_cache_entry_t *entry) { + /* The ref_count must be initialized during allocation. This ensures that the entry + * is not moved by defragmentation before all persistence operations are completed + * and the entry is stored in the hash table. */ + entry->ref_count = 1; +} + apc_cache_entry_t *apc_persist( apc_sma_t *sma, apc_serializer_t *serializer, zend_string *key, const zval *val) { apc_persist_context_t ctxt; @@ -515,7 +523,7 @@ apc_cache_entry_t *apc_persist( } } - ctxt.alloc = ctxt.alloc_cur = apc_sma_malloc(sma, ctxt.size); + ctxt.alloc = ctxt.alloc_cur = apc_sma_malloc(sma, ctxt.size, (apc_sma_malloc_init_f)apc_persist_sma_init_entry); if (!ctxt.alloc) { apc_persist_destroy_context(&ctxt); return NULL; @@ -708,7 +716,8 @@ static void apc_unpersist_zval_impl(apc_unpersist_context_t *ctxt, zval *zv) { return; case IS_ARRAY: #if PHP_VERSION_ID >= 70300 - if (Z_ARR_P(zv)->nNumOfElements == 0) { + if (Z_ARR_P(zv) == (zend_array *)ctxt->alloc) { + /* If the zval points to the entry's starting address, we use the zend_empty_array optimization. */ ZVAL_EMPTY_ARRAY(zv); /* #323 */ return; } diff --git a/apc_signal.c b/apc_signal.c index bb47f77f..bb12026e 100644 --- a/apc_signal.c +++ b/apc_signal.c @@ -52,9 +52,7 @@ static void apc_clear_cache(int signo, siginfo_t *siginfo, void *context); extern apc_cache_t* apc_user_cache; -/* {{{ apc_core_unmap - * Coredump signal handler, detached from shm and calls previously installed handlers - */ +/* Coredump signal handler, detached from shm and calls previously installed handlers */ static void apc_core_unmap(int signo, siginfo_t *siginfo, void *context) { if (apc_user_cache) { @@ -67,11 +65,11 @@ static void apc_core_unmap(int signo, siginfo_t *siginfo, void *context) #else raise(signo); #endif -} /* }}} */ +} #if defined(SIGUSR1) && defined(APC_CLEAR_SIGNAL) -/* {{{ apc_reload_cache */ +/* Clears the cache on SIGUSR1 */ static void apc_clear_cache(int signo, siginfo_t *siginfo, void *context) { if (apc_user_cache) { apc_cache_clear(apc_user_cache); @@ -84,12 +82,10 @@ static void apc_clear_cache(int signo, siginfo_t *siginfo, void *context) { #else raise(signo); #endif -} /* }}} */ +} #endif -/* {{{ apc_rehandle_signal - * Call the previously registered handler for a signal - */ +/* Call the previously registered handler for a signal */ static void apc_rehandle_signal(int signo, siginfo_t *siginfo, void *context) { int i; @@ -106,12 +102,9 @@ static void apc_rehandle_signal(int signo, siginfo_t *siginfo, void *context) } } -} /* }}} */ +} -/* {{{ apc_register_signal - * Set a handler for a previously installed signal and save so we can - * callback when handled - */ +/* Set a handler for a previously installed signal and save so we can callback when handled */ static int apc_register_signal(int signo, void (*handler)(int, siginfo_t*, void*)) { struct sigaction sa; @@ -150,10 +143,9 @@ static int apc_register_signal(int signo, void (*handler)(int, siginfo_t*, void* return SUCCESS; } return FAILURE; -} /* }}} */ +} -/* {{{ apc_set_signals - * Install our signal handlers */ +/* Install our signal handlers */ void apc_set_signals() { if (apc_signal_info.installed == 0) { @@ -196,10 +188,9 @@ void apc_set_signals() #endif } } -} /* }}} */ +} -/* {{{ apc_set_signals - * cleanup signals for shutdown */ +/* Cleanup signals for shutdown */ void apc_shutdown_signals() { int i=0; @@ -211,7 +202,6 @@ void apc_shutdown_signals() apc_signal_info.installed = 0; /* just in case */ } } -/* }}} */ #endif /* HAVE_SIGACTION */ diff --git a/apc_sma.c b/apc_sma.c index 3559c545..1d31256e 100644 --- a/apc_sma.c +++ b/apc_sma.c @@ -80,7 +80,7 @@ struct block_t { /* macros for getting the next or previous sequential block */ #define NEXT_SBLOCK(block) ((block_t*)((char*)block + block->size)) -#define PREV_SBLOCK(block) (block->prev_size ? ((block_t*)((char*)block - block->prev_size)) : NULL) +#define PREV_SBLOCK(block) ((block_t*)((char*)block - block->prev_size)) /* Canary macros for setting, checking and resetting memory canaries */ #ifdef APC_SMA_CANARIES @@ -98,14 +98,34 @@ struct block_t { /* How many extra blocks to check for a better fit */ #define BEST_FIT_LIMIT 3 +static inline void link_free_block_at_start(sma_header_t *smaheader, block_t *cur) { + block_t *dst = BLOCKAT(ALIGNWORD(sizeof(sma_header_t))); + + /* insert cur as first block in the free list */ + cur->fnext = dst->fnext; + cur->fprev = OFFSET(dst); + dst->fnext = OFFSET(cur); + BLOCKAT(cur->fnext)->fprev = dst->fnext; +} + +static inline void unlink_free_block(sma_header_t *smaheader, block_t *cur) { + BLOCKAT(cur->fprev)->fnext = cur->fnext; + BLOCKAT(cur->fnext)->fprev = cur->fprev; +} + static inline block_t *find_block(sma_header_t *smaheader, size_t realsize) { - block_t *cur, *prv = BLOCKAT(ALIGNWORD(sizeof(sma_header_t))); + block_t *cur = BLOCKAT(ALIGNWORD(sizeof(sma_header_t))); block_t *found = NULL; uint32_t i; - CHECK_CANARY(prv); + CHECK_CANARY(cur); + + /* First, ensure that at least realsize free bytes are available, even if they are not contiguous. */ + if (smaheader->avail < realsize) { + return NULL; + } - while (prv->fnext) { - cur = BLOCKAT(prv->fnext); + while (cur->fnext) { + cur = BLOCKAT(cur->fnext); CHECK_CANARY(cur); /* Found a suitable block */ @@ -113,42 +133,30 @@ static inline block_t *find_block(sma_header_t *smaheader, size_t realsize) { found = cur; break; } - - prv = cur; } if (found) { /* Try to find a smaller block that also fits */ - prv = cur; - for (i = 0; i < BEST_FIT_LIMIT && prv->fnext; i++) { - cur = BLOCKAT(prv->fnext); + for (i = 0; i < BEST_FIT_LIMIT && cur->fnext; i++) { + cur = BLOCKAT(cur->fnext); CHECK_CANARY(cur); if (cur->size >= realsize && cur->size < found->size) { found = cur; } - - prv = cur; } } return found; } -/* {{{ sma_allocate: tries to allocate at least size bytes of shared memory */ +/* sma_allocate: tries to allocate at least size bytes of shared memory */ static APC_HOTSPOT size_t sma_allocate(sma_header_t *smaheader, size_t size) { - block_t* prv; /* block prior to working block */ block_t* cur; /* working block in list */ size_t realsize; /* actual size of block needed, including block header */ - size_t block_header_size = ALIGNWORD(sizeof(block_t)); - realsize = ALIGNWORD(size + block_header_size); - - /* First, ensure that the segment contains at least realsize free bytes, even if they are not contiguous. */ - if (smaheader->avail < realsize) { - return SIZE_MAX; - } + realsize = ALIGNWORD(size + ALIGNWORD(sizeof(block_t))); cur = find_block(smaheader, realsize); if (!cur) { @@ -156,11 +164,11 @@ static APC_HOTSPOT size_t sma_allocate(sma_header_t *smaheader, size_t size) return SIZE_MAX; } + /* remove cur from the list of free blocks */ + unlink_free_block(smaheader, cur); + if (cur->size >= realsize && cur->size < (realsize + smaheader->min_block_size)) { - /* cur is big enough for realsize, but too small to split - unlink it */ - prv = BLOCKAT(cur->fprev); - prv->fnext = cur->fnext; - BLOCKAT(cur->fnext)->fprev = OFFSET(prv); + /* cur is big enough for realsize, but too small to split */ NEXT_SBLOCK(cur)->prev_size = 0; /* block is alloc'd */ } else { /* cur is too big; split it into two smaller blocks */ @@ -175,25 +183,25 @@ static APC_HOTSPOT size_t sma_allocate(sma_header_t *smaheader, size_t size) NEXT_SBLOCK(nxt)->prev_size = nxt->size; /* adjust size */ SET_CANARY(nxt); - /* replace cur with next in free list */ - nxt->fnext = cur->fnext; - nxt->fprev = cur->fprev; - BLOCKAT(nxt->fnext)->fprev = OFFSET(nxt); - BLOCKAT(nxt->fprev)->fnext = OFFSET(nxt); + /* put the remaining block (nxt) back into the free list */ + link_free_block_at_start(smaheader, nxt); } + /* mark cur as allocated */ cur->fnext = 0; + /* store used space to be able to reclaim unused space during defragmentation */ + cur->fprev = realsize; + /* update the segment header */ smaheader->avail -= cur->size; SET_CANARY(cur); - return OFFSET(cur) + block_header_size; + return OFFSET(cur) + ALIGNWORD(sizeof(block_t)); } -/* }}} */ -/* {{{ sma_deallocate: deallocates the block at the given offset */ +/* sma_deallocate: deallocates the block at the given offset */ static APC_HOTSPOT size_t sma_deallocate(sma_header_t *smaheader, size_t offset) { block_t* cur; /* the new block to insert */ @@ -212,12 +220,12 @@ static APC_HOTSPOT size_t sma_deallocate(sma_header_t *smaheader, size_t offset) size = cur->size; if (cur->prev_size != 0) { - /* remove prv from list */ + /* remove prv from the list of free blocks */ prv = PREV_SBLOCK(cur); - BLOCKAT(prv->fnext)->fprev = prv->fprev; - BLOCKAT(prv->fprev)->fnext = prv->fnext; + unlink_free_block(smaheader, prv); + /* cur and prv share an edge, combine them */ - prv->size +=cur->size; + prv->size += cur->size; RESET_CANARY(cur); cur = prv; @@ -226,30 +234,26 @@ static APC_HOTSPOT size_t sma_deallocate(sma_header_t *smaheader, size_t offset) nxt = NEXT_SBLOCK(cur); if (nxt->fnext != 0) { assert(NEXT_SBLOCK(NEXT_SBLOCK(cur))->prev_size == nxt->size); + /* remove nxt from the list of free blocks */ + unlink_free_block(smaheader, nxt); + /* cur and nxt shared an edge, combine them */ - BLOCKAT(nxt->fnext)->fprev = nxt->fprev; - BLOCKAT(nxt->fprev)->fnext = nxt->fnext; cur->size += nxt->size; CHECK_CANARY(nxt); RESET_CANARY(nxt); } + /* mark in the sequentially next block that the previous block is free */ NEXT_SBLOCK(cur)->prev_size = cur->size; - /* insert new block after prv */ - prv = BLOCKAT(ALIGNWORD(sizeof(sma_header_t))); - cur->fnext = prv->fnext; - prv->fnext = OFFSET(cur); - cur->fprev = OFFSET(prv); - BLOCKAT(cur->fnext)->fprev = OFFSET(cur); + /* insert cur into the free list */ + link_free_block_at_start(smaheader, cur); return size; } -/* }}} */ -/* {{{ APC SMA API */ -PHP_APCU_API void apc_sma_init(apc_sma_t* sma, void** data, apc_sma_expunge_f expunge, size_t size, size_t min_alloc_size, char *mask) { +PHP_APCU_API void apc_sma_init(apc_sma_t* sma, void** data, apc_sma_expunge_f expunge, size_t size, size_t min_alloc_size, char *mask, zend_long hugepage_size) { if (sma->initialized) { return; } @@ -260,7 +264,7 @@ PHP_APCU_API void apc_sma_init(apc_sma_t* sma, void** data, apc_sma_expunge_f ex sma->size = ALIGNWORD(size > 0 ? size : SMA_DEFAULT_SEGSIZE); #ifdef APC_MMAP - sma->shmaddr = apc_mmap(mask, sma->size); + sma->shmaddr = apc_mmap(mask, sma->size, hugepage_size); #else sma->shmaddr = apc_shm_attach(sma->size); #endif @@ -269,6 +273,7 @@ PHP_APCU_API void apc_sma_init(apc_sma_t* sma, void** data, apc_sma_expunge_f ex SMA_CREATE_LOCK(&smaheader->sma_lock); smaheader->min_block_size = min_alloc_size > 0 ? ALIGNWORD(min_alloc_size + ALIGNWORD(sizeof(block_t))) : MINBLOCKSIZE; smaheader->avail = sma->size - ALIGNWORD(sizeof(sma_header_t)) - ALIGNWORD(sizeof(block_t)) - ALIGNWORD(sizeof(block_t)); + sma->max_alloc_size = smaheader->avail - ALIGNWORD(sizeof(block_t)); block_t *first = BLOCKAT(ALIGNWORD(sizeof(sma_header_t))); first->size = 0; @@ -307,33 +312,45 @@ PHP_APCU_API void apc_sma_detach(apc_sma_t* sma) { #endif } -PHP_APCU_API void* apc_sma_malloc(apc_sma_t* sma, size_t n) { +PHP_APCU_API void* apc_sma_malloc(apc_sma_t* sma, size_t n, apc_sma_malloc_init_f init_callback) { size_t off; zend_bool nuked = 0; restart: assert(sma->initialized); + /* Prevent cache wipes caused by huge allocations that don't fit into shm */ + if (n > sma->max_alloc_size) { + return NULL; + } + if (!SMA_LOCK(sma)) { return NULL; } off = sma_allocate(SMA_HDR(sma), n); - SMA_UNLOCK(sma); - if (off != SIZE_MAX) { void *p = (void *)(SMA_ADDR(sma) + off); + + if (init_callback) { + /* Perform initializations that must be done before releasing the lock */ + init_callback(p); + } + + SMA_UNLOCK(sma); #ifdef VALGRIND_MALLOCLIKE_BLOCK VALGRIND_MALLOCLIKE_BLOCK(p, n, 0, 0); #endif return p; } + SMA_UNLOCK(sma); + /* Expunge cache in hope of freeing up memory, but only once */ if (!nuked) { - sma->expunge(*sma->data, n); - nuked = 1; + /* nuke is not set if expunge() was skipped internally to get another try */ + nuked = sma->expunge(*sma->data, n); goto restart; } @@ -380,24 +397,29 @@ PHP_APCU_API apc_sma_info_t *apc_sma_info(apc_sma_t* sma, zend_bool limited) { return info; } - SMA_LOCK(sma); + if (!SMA_LOCK(sma)) { + efree(info); + return NULL; + } + sma_header_t *smaheader = SMA_HDR(sma); - block_t *prv = BLOCKAT(ALIGNWORD(sizeof(sma_header_t))); + block_t *cur = BLOCKAT(ALIGNWORD(sizeof(sma_header_t))); apc_sma_link_t **link = &info->list; - /* For each free block */ - while (BLOCKAT(prv->fnext)->fnext != 0) { - block_t *cur = BLOCKAT(prv->fnext); + /* Skip 1st (0-sized) block */ + cur = BLOCKAT(cur->fnext); + /* For each free block */ + while (cur->fnext != 0) { CHECK_CANARY(cur); *link = emalloc(sizeof(apc_sma_link_t)); (*link)->size = cur->size; - (*link)->offset = prv->fnext; + (*link)->offset = OFFSET(cur); (*link)->next = NULL; link = &(*link)->next; - prv = cur; + cur = BLOCKAT(cur->fnext); } SMA_UNLOCK(sma); @@ -420,7 +442,11 @@ PHP_APCU_API size_t apc_sma_get_avail_mem(apc_sma_t* sma) { return SMA_HDR(sma)->avail; } -PHP_APCU_API zend_bool apc_sma_get_avail_size(apc_sma_t* sma, size_t size) { +PHP_APCU_API zend_bool apc_sma_check_avail(apc_sma_t *sma, size_t size) { + return SMA_HDR(sma)->avail >= ALIGNWORD(size + ALIGNWORD(sizeof(block_t))); +} + +PHP_APCU_API zend_bool apc_sma_check_avail_contiguous(apc_sma_t *sma, size_t size) { size_t realsize = ALIGNWORD(size + ALIGNWORD(sizeof(block_t))); sma_header_t *smaheader = SMA_HDR(sma); @@ -429,7 +455,10 @@ PHP_APCU_API zend_bool apc_sma_get_avail_size(apc_sma_t* sma, size_t size) { return 0; } - SMA_LOCK(sma); + if (!SMA_LOCK(sma)) { + return 0; + } + block_t *cur = BLOCKAT(ALIGNWORD(sizeof(sma_header_t))); /* Look for a contiguous block of memory */ @@ -447,12 +476,67 @@ PHP_APCU_API zend_bool apc_sma_get_avail_size(apc_sma_t* sma, size_t size) { return 0; } -PHP_APCU_API void apc_sma_check_integrity(apc_sma_t* sma) -{ - /* dummy */ -} +PHP_APCU_API void apc_sma_defrag(apc_sma_t *sma, void *data, apc_sma_move_f move) { + sma_header_t *smaheader = SMA_HDR(sma); + block_t *cur = BLOCKAT(ALIGNWORD(sizeof(sma_header_t)) + ALIGNWORD(sizeof(block_t))); + block_t *first = BLOCKAT(ALIGNWORD(sizeof(sma_header_t))); + size_t reclaimed_size = 0; + + if (!SMA_LOCK(sma)) { + return; + } -/* }}} */ + /* empty the free list */ + first->fnext = sma->size - ALIGNWORD(sizeof(block_t)); + BLOCKAT(first->fnext)->fprev = OFFSET(first); + + /* loop through all blocks */ + while (cur->size != 0) { + /* continue until cur points to a free block */ + if (!cur->fnext) { + cur = NEXT_SBLOCK(cur); + continue; + } + + /* if cur is free, nxt must be an allocated block, since we never have two consecutive free blocks */ + block_t *nxt = NEXT_SBLOCK(cur); + + /* if nxt is the last block, or if nxt can't be moved, cur can't be combined with other free blocks */ + if (nxt->size == 0 || !move(data, (char *)nxt + ALIGNWORD(sizeof(block_t)), (char *)cur + ALIGNWORD(sizeof(block_t)))) { + /* insert cur into the free list */ + link_free_block_at_start(smaheader, cur); + + cur->prev_size = 0; + nxt->prev_size = cur->size; + + cur = NEXT_SBLOCK(nxt); + continue; + } + + /* reclaim unused space from the allocated block (nxt->fprev contains the used space) */ + size_t free_size = nxt->size - nxt->fprev; + reclaimed_size += free_size; + nxt->size -= free_size; + free_size += cur->size; + + /* swap cur and nxt by moving nxt (incl. header) and initializing a new block header for cur behind it */ + memmove(cur, nxt, nxt->size); + cur->prev_size = 0; + cur = NEXT_SBLOCK(cur); + cur->size = free_size; + cur->fnext = 1; /* mark cur as free */ + + /* if the next block is also free, combine cur and nxt to one larger free block */ + nxt = NEXT_SBLOCK(cur); + if (nxt->fnext) { + cur->size += nxt->size; + } + } + + smaheader->avail += reclaimed_size; + + SMA_UNLOCK(sma); +} /* * Local variables: diff --git a/apc_sma.h b/apc_sma.h index 472a8c72..6bd35a65 100644 --- a/apc_sma.h +++ b/apc_sma.h @@ -28,33 +28,29 @@ #ifndef APC_SMA_H #define APC_SMA_H -/* {{{ SMA API - APC SMA API provides support for shared memory allocators to external libraries ( and to APC ) - Skip to the bottom macros for error free usage of the SMA API +/* + * SMA API + * APC SMA API provides support for shared memory allocators to external libraries ( and to APC ) + * Skip to the bottom macros for error free usage of the SMA API */ #include "apc.h" -/* {{{ struct definition: apc_sma_link_t */ typedef struct apc_sma_link_t apc_sma_link_t; struct apc_sma_link_t { zend_long size; /* size of this free block */ zend_long offset; /* offset in segment of this block */ apc_sma_link_t* next; /* link to next free block */ }; -/* }}} */ -/* {{{ struct definition: apc_sma_info_t */ typedef struct apc_sma_info_t apc_sma_info_t; struct apc_sma_info_t { size_t seg_size; /* segment size */ apc_sma_link_t* list; /* list of free blocks */ }; -/* }}} */ -typedef void (*apc_sma_expunge_f)(void *pointer, size_t size); /* }}} */ +typedef zend_bool (*apc_sma_expunge_f)(void *pointer, size_t size); -/* {{{ struct definition: apc_sma_t */ typedef struct _apc_sma_t { zend_bool initialized; /* flag to indicate this sma has been initialized */ @@ -64,8 +60,9 @@ typedef struct _apc_sma_t { /* info */ size_t size; /* segment size */ + size_t max_alloc_size; /* max size of memory available for allocation */ void *shmaddr; /* address of shm segment */ -} apc_sma_t; /* }}} */ +} apc_sma_t; /* * apc_sma_init will initialize a shared memory allocator with the given size of shared memory @@ -74,7 +71,7 @@ typedef struct _apc_sma_t { */ PHP_APCU_API void apc_sma_init( apc_sma_t* sma, void** data, apc_sma_expunge_f expunge, - size_t size, size_t min_alloc_size, char *mask); + size_t size, size_t min_alloc_size, char *mask, zend_long hugepage_size); /* * apc_sma_detach will detach from shared memory and cleanup local allocations. @@ -82,9 +79,12 @@ PHP_APCU_API void apc_sma_init( PHP_APCU_API void apc_sma_detach(apc_sma_t* sma); /* -* apc_smap_api_malloc will allocate a block from the sma of the given size +* apc_smap_api_malloc will allocate a block from the sma of the given size. +* The init_callack() can be used to perform initializations that must be completed +* before the lock of the sma layer is released. */ -PHP_APCU_API void* apc_sma_malloc(apc_sma_t* sma, size_t size); +typedef void (*apc_sma_malloc_init_f)(void *pointer); +PHP_APCU_API void* apc_sma_malloc(apc_sma_t* sma, size_t size, apc_sma_malloc_init_f init_callback); /* * apc_sma_api_free will free p (which should be a pointer to a block allocated from sma) @@ -107,18 +107,29 @@ PHP_APCU_API void apc_sma_free_info(apc_sma_t* sma, apc_sma_info_t* info); PHP_APCU_API size_t apc_sma_get_avail_mem(apc_sma_t* sma); /* -* apc_sma_api_get_avail_size will return true if at least size contiguous bytes are available to the sma +* apc_sma_check_avail returns true if at least size bytes are available across all free blocks +*/ +PHP_APCU_API zend_bool apc_sma_check_avail(apc_sma_t *sma, size_t size); + +/* +* apc_sma_check_avail_contiguous returns true if at least size contiguous bytes can be allocated from the sma */ -PHP_APCU_API zend_bool apc_sma_get_avail_size(apc_sma_t* sma, size_t size); +PHP_APCU_API zend_bool apc_sma_check_avail_contiguous(apc_sma_t *sma, size_t size); /* -* apc_sma_api_check_integrity will check the integrity of sma +* apc_sma_defrag defragments the shared memory by shifting all allocated blocks to the left, +* allowing all free blocks to be coalesced to one larger free block on the right side. +* +* The move() callback is called for each allocated block before it is moved. Therefore, move() can be used +* to prepare for the move or to prevent the block from being moved by returning 0. The argument "data" is +* passed as the first argument from apc_sma_defrag() to move(), while the old and the new address of the +* allocation is passed as the 2nd and 3rd argument. The callback must not write to the new memory area +* because the area is not yet allocated during the callback. */ -PHP_APCU_API void apc_sma_check_integrity(apc_sma_t* sma); /* }}} */ +typedef zend_bool (*apc_sma_move_f)(void *data, void *pointer_old, void *pointer_new); +PHP_APCU_API void apc_sma_defrag(apc_sma_t *sma, void *data, apc_sma_move_f move); -/* {{{ ALIGNWORD: pad up x, aligned to the system's word boundary */ +/* ALIGNWORD: pad up x, aligned to the system's word boundary */ #define ALIGNWORD(x) ZEND_MM_ALIGNED_SIZE(x) -/* }}} */ #endif - diff --git a/apc_stack.c b/apc_stack.c index 22d81b9f..57823e33 100644 --- a/apc_stack.c +++ b/apc_stack.c @@ -93,7 +93,6 @@ int apc_stack_size(apc_stack_t* stack) return stack->size; } - /* * Local variables: * tab-width: 4 diff --git a/package.xml b/package.xml index f9c90514..692dafeb 100644 --- a/package.xml +++ b/package.xml @@ -28,9 +28,9 @@ nikic@php.net yes - 2024-09-21 + 2025-12-07 - 5.1.25-dev + 5.1.28 5.1.18 @@ -39,7 +39,13 @@ PHP License -- TBD + - Defragmentation now reclaims unused space from moved entries, resulting in better compaction. + - Shared memory for new entries is allocated faster in scenarios with many free memory blocks. + This should improve APCu's insertion performance when entries are frequently deleted or + replaced, or when APCu is used with larger amounts of memory. + - Trying to insert entries larger than the shared memory no longer results in cache wipes. + - Fix build against PHP 8.6. + - Fix apc.php compatibility with older apcu versions. @@ -75,6 +81,7 @@ + @@ -83,17 +90,21 @@ + + - + + + + + + - - - @@ -185,6 +196,89 @@ + + 2025-08-28 + + 5.1.27 + 5.1.18 + + + stable + stable + + PHP License + + - Fixed another hang introduced in apcu 5.1.25, which can occur when defragmentation is triggered + under load. + + Internal changes: + - The report_memleaks INI directive has been removed from all tests as it will be deprecated in + PHP 8.5. + + + + 2025-08-05 + + 5.1.26 + 5.1.18 + + + stable + stable + + PHP License + + - Fixed hang introduced in apcu 5.1.25, which can occur when defragmentation is triggered under + load. + - Fixed a test on PHP 8.5. + + + + 2025-07-28 + + 5.1.25 + 5.1.18 + + + stable + stable + + PHP License + + - If the cache is full, try to clean up expired entries based on their per-entry hard TTL even + if the soft apc.ttl is 0. Previously the entire cache was discarded. + - If a new entry cannot be inserted due to fragmentation, the cache will be defragmented, + combining many small free blocks into one big free block by moving around cache entries. + This avoids the need to discard the entire cache in more cases. + - The access time (which is used by the soft apc.ttl) is now also updated when using + apcu_exists(). + - apc.entries_hint now defaults to 512 entries per 1MB of shared memory. Previously the + default was 4096, independent of shm_size. This could lead to a large number of hash + collisions if shm_size was increased without also increasing entries_hint. + - Added apc.mmap_hugepage_size to use huge pages of a certain size for the apcu shared memory + segment. This requires support for huge pages to be enabled in the kernel. Note that even if + this option is not set, shared memory is still configured to use transparent huge pages. + - The apc.shm_segments ini option has been removed. Multiple SHM segments are no longer + supported. (They were already not supported when using mmap, which is the default mode of + operation) + - The apc.smart configuration setting should now work more reliably. Values > 1 can be used + to increase the chance of discarding the entire cache when the amount of memory freed by + removing expired entries was too small. This could be useful if performance degrades due to + executing the logic to remove expired entries (+ defragmentation) too frequently during + periods of high memory usage. + - The number of cache cleanups performed (removal of expired entries) is now available + in the array returned by apcu_cache_info() (via array key "cleanups"). + - The number of defragmentations performed is now available in the array returned by + apcu_cache_info() (via array key "defragmentations"). + - Fixed several issues that caused inserting new entries to fail unexpectedly. + + Internal changes: + - Fixed -Wclobbered compiler warnings. + - All cache data structures are now relocatable, i.e. independent of the base address of the + cache. This enables defragmentation support. + - Hash slots now use doubly linked lists. This is necessary for defragmentation. + + 2024-09-21 diff --git a/php_apc.c b/php_apc.c index 3aff354a..392b05ac 100644 --- a/php_apc.c +++ b/php_apc.c @@ -62,7 +62,6 @@ #include "apc_signal.h" #endif -/* {{{ ZEND_DECLARE_MODULE_GLOBALS(apcu) */ ZEND_DECLARE_MODULE_GLOBALS(apcu) /* True globals */ @@ -87,11 +86,10 @@ static void php_apc_init_globals(zend_apcu_globals* apcu_globals) apcu_globals->serializer_name = NULL; apcu_globals->entry_level = 0; } -/* }}} */ -/* {{{ PHP_INI */ +/* PHP_INI */ -static PHP_INI_MH(OnUpdateShmSize) /* {{{ */ +static PHP_INI_MH(OnUpdateShmSize) { #if PHP_VERSION_ID >= 80200 zend_long s = zend_ini_parse_quantity_warn(new_value, entry->name); @@ -114,7 +112,32 @@ static PHP_INI_MH(OnUpdateShmSize) /* {{{ */ return SUCCESS; } -/* }}} */ + +#if defined(APC_MMAP) +static PHP_INI_MH(OnUpdateMmapHugepageSize) +{ + zend_long s; + +#if PHP_VERSION_ID >= 80200 + s = zend_ini_parse_quantity_warn(new_value, entry->name); +#else + s = zend_atol(new_value->val, new_value->len); +#endif + + if (s < 0) { + php_error_docref(NULL, E_CORE_ERROR, "apc.mmap_hugepage_size must be a positive integer"); + return FAILURE; + } + + if (s & (s - 1)) { + php_error_docref(NULL, E_CORE_ERROR, "apc.mmap_hugepage_size must be a power of 2"); + return FAILURE; + } + + APCG(mmap_hugepage_size) = s; + return SUCCESS; +} +#endif PHP_INI_BEGIN() STD_PHP_INI_BOOLEAN("apc.enabled", "1", PHP_INI_SYSTEM, OnUpdateBool, enabled, zend_apcu_globals, apcu_globals) @@ -124,7 +147,8 @@ STD_PHP_INI_ENTRY("apc.gc_ttl", "3600", PHP_INI_SYSTEM, OnUpdateLong, STD_PHP_INI_ENTRY("apc.ttl", "0", PHP_INI_SYSTEM, OnUpdateLong, ttl, zend_apcu_globals, apcu_globals) STD_PHP_INI_ENTRY("apc.smart", "0", PHP_INI_SYSTEM, OnUpdateLong, smart, zend_apcu_globals, apcu_globals) #ifdef APC_MMAP -STD_PHP_INI_ENTRY("apc.mmap_file_mask", NULL, PHP_INI_SYSTEM, OnUpdateString, mmap_file_mask, zend_apcu_globals, apcu_globals) +STD_PHP_INI_ENTRY("apc.mmap_file_mask", NULL, PHP_INI_SYSTEM, OnUpdateString, mmap_file_mask, zend_apcu_globals, apcu_globals) +STD_PHP_INI_ENTRY("apc.mmap_hugepage_size", "0", PHP_INI_SYSTEM, OnUpdateMmapHugepageSize, mmap_hugepage_size, zend_apcu_globals, apcu_globals) #endif STD_PHP_INI_BOOLEAN("apc.enable_cli", "0", PHP_INI_SYSTEM, OnUpdateBool, enable_cli, zend_apcu_globals, apcu_globals) STD_PHP_INI_BOOLEAN("apc.slam_defense", "0", PHP_INI_SYSTEM, OnUpdateBool, slam_defense, zend_apcu_globals, apcu_globals) @@ -134,14 +158,11 @@ STD_PHP_INI_BOOLEAN("apc.use_request_time", "0", PHP_INI_ALL, OnUpdateBool, use_ STD_PHP_INI_ENTRY("apc.serializer", "php", PHP_INI_SYSTEM, OnUpdateStringUnempty, serializer_name, zend_apcu_globals, apcu_globals) PHP_INI_END() -/* }}} */ - zend_bool apc_is_enabled(void) { return APCG(enabled); } -/* {{{ PHP_MINFO_FUNCTION(apcu) */ static PHP_MINFO_FUNCTION(apcu) { php_info_print_table_start(); @@ -188,9 +209,7 @@ static PHP_MINFO_FUNCTION(apcu) php_info_print_table_end(); DISPLAY_INI_ENTRIES(); } -/* }}} */ -/* {{{ PHP_MINIT_FUNCTION(apcu) */ static PHP_MINIT_FUNCTION(apcu) { #if defined(ZTS) && defined(COMPILE_DL_APCU) @@ -219,10 +238,12 @@ static PHP_MINIT_FUNCTION(apcu) if (APCG(enabled)) { if (!APCG(initialized)) { -#ifdef APC_MMAP - char *mmap_file_mask = APCG(mmap_file_mask); -#else char *mmap_file_mask = NULL; + zend_long mmap_hugepage_size = 0; + +#ifdef APC_MMAP + mmap_file_mask = APCG(mmap_file_mask); + mmap_hugepage_size = APCG(mmap_hugepage_size); #endif /* ensure this runs only once */ @@ -231,7 +252,7 @@ static PHP_MINIT_FUNCTION(apcu) /* initialize shared memory allocator */ apc_sma_init( &apc_sma, (void **) &apc_user_cache, (apc_sma_expunge_f) apc_cache_default_expunge, - APCG(shm_size), APC_ENTRY_SIZE(0), mmap_file_mask); + APCG(shm_size), APC_ENTRY_SIZE(0), mmap_file_mask, mmap_hugepage_size); REGISTER_LONG_CONSTANT(APC_SERIALIZER_CONSTANT, (zend_long)&_apc_register_serializer, CONST_PERSISTENT | CONST_CS); @@ -261,9 +282,7 @@ static PHP_MINIT_FUNCTION(apcu) return SUCCESS; } -/* }}} */ -/* {{{ PHP_MSHUTDOWN_FUNCTION(apcu) */ static PHP_MSHUTDOWN_FUNCTION(apcu) { #define X(str) zend_string_release(apc_str_ ## str); @@ -293,9 +312,8 @@ static PHP_MSHUTDOWN_FUNCTION(apcu) UNREGISTER_INI_ENTRIES(); return SUCCESS; -} /* }}} */ +} -/* {{{ PHP_RINIT_FUNCTION(apcu) */ static PHP_RINIT_FUNCTION(apcu) { #if defined(ZTS) && defined(COMPILE_DL_APCU) @@ -315,9 +333,8 @@ static PHP_RINIT_FUNCTION(apcu) } return SUCCESS; } -/* }}} */ -/* {{{ proto void apcu_clear_cache() */ +/* proto void apcu_clear_cache() */ PHP_FUNCTION(apcu_clear_cache) { if (zend_parse_parameters_none() == FAILURE) { @@ -327,9 +344,8 @@ PHP_FUNCTION(apcu_clear_cache) apc_cache_clear(apc_user_cache); RETURN_TRUE; } -/* }}} */ -/* {{{ proto array apcu_cache_info([bool limited]) */ +/* proto array apcu_cache_info([bool limited]) */ PHP_FUNCTION(apcu_cache_info) { zend_bool limited = 0; @@ -344,9 +360,8 @@ PHP_FUNCTION(apcu_cache_info) RETURN_FALSE; } } -/* }}} */ -/* {{{ proto array apcu_key_info(string key) */ +/* proto array apcu_key_info(string key) */ PHP_FUNCTION(apcu_key_info) { zend_string *key; @@ -356,9 +371,9 @@ PHP_FUNCTION(apcu_key_info) ZEND_PARSE_PARAMETERS_END(); apc_cache_stat(apc_user_cache, key, return_value); -} /* }}} */ +} -/* {{{ proto array apcu_sma_info([bool limited]) */ +/* proto array apcu_sma_info([bool limited]) */ PHP_FUNCTION(apcu_sma_info) { zend_bool limited = 0; @@ -407,9 +422,7 @@ PHP_FUNCTION(apcu_sma_info) add_assoc_zval(return_value, "block_lists", &block_lists); apc_sma_free_info(&apc_sma, info); } -/* }}} */ -/* {{{ php_apc_update */ zend_bool php_apc_update( zend_string *key, apc_cache_atomic_updater_t updater, void *data, zend_bool insert_if_not_found, time_t ttl) @@ -421,10 +434,7 @@ zend_bool php_apc_update( return apc_cache_atomic_update_long(apc_user_cache, key, updater, data, insert_if_not_found, ttl); } -/* }}} */ -/* {{{ apc_store_helper(INTERNAL_FUNCTION_PARAMETERS, const zend_bool exclusive) - */ static void apc_store_helper(INTERNAL_FUNCTION_PARAMETERS, const zend_bool exclusive) { zval *key; @@ -480,33 +490,24 @@ static void apc_store_helper(INTERNAL_FUNCTION_PARAMETERS, const zend_bool exclu RETURN_FALSE; } } -/* }}} */ -/* {{{ proto bool apcu_enabled(void) - returns true when apcu is usable in the current environment */ +/* proto bool apcu_enabled(void): returns true when apcu is usable in the current environment */ PHP_FUNCTION(apcu_enabled) { if (zend_parse_parameters_none() == FAILURE) { return; } RETURN_BOOL(APCG(enabled)); } -/* }}} */ -/* {{{ proto int apcu_store(mixed key, mixed var [, long ttl ]) - */ +/* proto int apcu_store(mixed key, mixed var [, long ttl ]) */ PHP_FUNCTION(apcu_store) { apc_store_helper(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0); } -/* }}} */ -/* {{{ proto int apcu_add(mixed key, mixed var [, long ttl ]) - */ +/* proto int apcu_add(mixed key, mixed var [, long ttl ]) */ PHP_FUNCTION(apcu_add) { apc_store_helper(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1); } -/* }}} */ - -/* {{{ php_inc_updater */ struct php_inc_updater_args { zend_long step; @@ -519,8 +520,7 @@ static zend_bool php_inc_updater(apc_cache_t *cache, zend_long *entry, void *dat return 1; } -/* {{{ proto long apcu_inc(string key [, long step [, bool& success [, long ttl]]]) - */ +/* proto long apcu_inc(string key [, long step [, bool& success [, long ttl]]]) */ PHP_FUNCTION(apcu_inc) { zend_string *key; struct php_inc_updater_args args; @@ -549,10 +549,8 @@ PHP_FUNCTION(apcu_inc) { RETURN_FALSE; } -/* }}} */ -/* {{{ proto long apcu_dec(string key [, long step [, bool &success [, long ttl]]]) - */ +/* proto long apcu_dec(string key [, long step [, bool &success [, long ttl]]]) */ PHP_FUNCTION(apcu_dec) { zend_string *key; struct php_inc_updater_args args; @@ -582,19 +580,15 @@ PHP_FUNCTION(apcu_dec) { RETURN_FALSE; } -/* }}} */ -/* {{{ php_cas_updater */ static zend_bool php_cas_updater(apc_cache_t *cache, zend_long *entry, void *data) { zend_long *vals = (zend_long *) data; zend_long old = vals[0]; zend_long new = vals[1]; return ATOMIC_CAS(*entry, old, new); } -/* }}} */ -/* {{{ proto int apcu_cas(string key, int old, int new) - */ +/* proto int apcu_cas(string key, int old, int new) */ PHP_FUNCTION(apcu_cas) { zend_string *key; zend_long vals[2]; @@ -612,10 +606,8 @@ PHP_FUNCTION(apcu_cas) { RETURN_BOOL(apc_cache_atomic_update_long(apc_user_cache, key, php_cas_updater, &vals, 0, 0)); } -/* }}} */ -/* {{{ proto mixed apcu_fetch(mixed key[, bool &success]) - */ +/* proto mixed apcu_fetch(mixed key[, bool &success]) */ PHP_FUNCTION(apcu_fetch) { zval *key; zval *success = NULL; @@ -667,10 +659,8 @@ PHP_FUNCTION(apcu_fetch) { RETURN_FALSE; } } -/* }}} */ -/* {{{ proto mixed apcu_exists(mixed key) - */ +/* proto mixed apcu_exists(mixed key) */ PHP_FUNCTION(apcu_exists) { zval *key; time_t t; @@ -710,10 +700,8 @@ PHP_FUNCTION(apcu_exists) { RETURN_FALSE; } } -/* }}} */ -/* {{{ proto mixed apcu_delete(mixed keys) - */ +/* proto mixed apcu_delete(mixed keys) */ PHP_FUNCTION(apcu_delete) { zval *keys; @@ -762,7 +750,6 @@ PHP_FUNCTION(apcu_entry) { apc_cache_entry(apc_user_cache, key, &fci, &fcc, ttl, now, return_value); } -/* }}} */ #ifdef APC_DEBUG /* This function is used to test TTL behavior without having to perform sleeps. */ @@ -787,8 +774,7 @@ PHP_FUNCTION(apcu_inc_request_time) { } #endif -/* {{{ module definition structure */ - +/* module definition structure */ zend_module_entry apcu_module_entry = { STANDARD_MODULE_HEADER, PHP_APCU_EXTNAME, @@ -801,7 +787,6 @@ zend_module_entry apcu_module_entry = { PHP_APCU_VERSION, STANDARD_MODULE_PROPERTIES }; -/* }}} */ #ifdef COMPILE_DL_APCU ZEND_GET_MODULE(apcu) @@ -809,7 +794,6 @@ ZEND_GET_MODULE(apcu) ZEND_TSRMLS_CACHE_DEFINE(); #endif #endif -/* }}} */ /* * Local variables: diff --git a/php_apc.h b/php_apc.h index 3ca816fb..96f5b01e 100644 --- a/php_apc.h +++ b/php_apc.h @@ -33,7 +33,7 @@ #include "apc.h" #include "apc_globals.h" -#define PHP_APCU_VERSION "5.1.25-dev" +#define PHP_APCU_VERSION "5.1.28" #define PHP_APCU_EXTNAME "apcu" PHP_APCU_API zend_bool apc_is_enabled(void); diff --git a/tests/apc_006.phpt b/tests/apc_006.phpt index 6ec03915..6df3b7c8 100644 --- a/tests/apc_006.phpt +++ b/tests/apc_006.phpt @@ -9,7 +9,6 @@ if (PHP_VERSION_ID >= 70300) die('skip Only for PHP < 7.3'); apc.enabled=1 apc.enable_cli=1 apc.serializer=php -report_memleaks=0 --FILE-- = 80100) die('skip Only for PHP < 8.1'); apc.enabled=1 apc.enable_cli=1 apc.serializer=php -report_memleaks=0 --FILE-- = 8.1'); apc.enabled=1 apc.enable_cli=1 apc.serializer=php -report_memleaks=0 --FILE-- = $entry_size) { + apcu_store(sprintf("key%06d", $i), str_repeat('x', 500)); + $i++; +} apcu_inc_request_time(1); apcu_fetch("no_ttl_accessed"); apcu_inc_request_time(1); -// Fill the cache -$i = 0; -while (apcu_exists("dummy")) { - apcu_store("key" . $i, str_repeat('x', 500)); - $i++; -} +// Trigger a default expunge after the entries have soft-expired. +apcu_store("large_entry", str_repeat('x', 1000)); var_dump(apcu_fetch("no_ttl_unaccessed")); var_dump(apcu_fetch("no_ttl_accessed")); @@ -39,4 +44,4 @@ var_dump(apcu_fetch("ttl")); --EXPECT-- bool(false) int(24) -int(42) +bool(false) diff --git a/tests/apc_026.phpt b/tests/apc_026.phpt new file mode 100644 index 00000000..947f016e --- /dev/null +++ b/tests/apc_026.phpt @@ -0,0 +1,21 @@ +--TEST-- +Huge allocations which don't fit into shm shouldn't cause cache wipes +--INI-- +apc.enabled=1 +apc.enable_cli=1 +apc.shm_size=1M +--FILE-- + +--EXPECT-- +bool(false) +bool(true) diff --git a/tests/apc_defrag.phpt b/tests/apc_defrag.phpt new file mode 100644 index 00000000..533772b5 --- /dev/null +++ b/tests/apc_defrag.phpt @@ -0,0 +1,102 @@ +--TEST-- +Test defragmentation +--SKIPIF-- + +--INI-- +apc.enabled=1 +apc.enable_cli=1 +apc.use_request_time=1 +apc.shm_size=1M +--FILE-- += $entry_size) { + $i++; + apcu_store(sprintf("ttl3_%010d", $i), $i, 3); + + if (apcu_sma_info(true)['avail_mem'] >= $entry_size) { + apcu_store(sprintf("ttl1_%010d", $i), $i, 1); + } + } + + return $i; +} + +// store the first entry with ttl=1, which causes all entries +// behind this entry to be moved during defragmentation +apcu_store("ttl1_int", 123, 1); + +// store entries of different datatypes with ttl=3, which must be present after expiration + defragmentation +$reference = "b"; +apcu_store("ttl3_int", 123456789, 3); +apcu_store("ttl3_string", "abc", 3); +apcu_store("ttl3_array", [1, "a", &$reference, []], 3); +apcu_store("ttl3_object", (object) ["prop1" => "val1", "prop2" => 2], 3); + +// safe available memory for later comparison +$avail_before_filled = apcu_sma_info(true)['avail_mem']; + +// fill cache with alternating ttl=1 + ttl=3 entries +fill_cache(); + +// expire all ttl1_* entries +apcu_inc_request_time(2); + +// this insertion should trigger an default_expunge which removes all ttl1_ entries and performs a defragmentation +var_dump(apcu_store("large_entry", str_repeat('x', 1000), 1)); + +// delete large_entry to be able to check the available memory in the next step +var_dump(apcu_delete("large_entry")); + +// the defragmentation should have freed more than 50% of the filled memory, because "ttl1_int" was also freed +var_dump(apcu_sma_info(true)['avail_mem'] > $avail_before_filled / 2); + +// after the default expunge, all ttl1_ entries should not be present anymore +var_dump(apcu_fetch("ttl1_int") === false); + +// all ttl3_ entries must be present (and correct after defragmentation) +var_dump(apcu_fetch("ttl3_int") === 123456789); +var_dump(apcu_fetch("ttl3_string") === "abc"); +var_dump(apcu_fetch("ttl3_array") === [1, "a", &$reference, []]); +var_dump(apcu_fetch("ttl3_object") == (object) ["prop1" => "val1", "prop2" => 2]); + +// check that cache cleanup and defragmentation have been performed, but no real expunge +var_dump(apcu_cache_info(true)["cleanups"] === 1); +var_dump(apcu_cache_info(true)["defragmentations"] === 1); +var_dump(apcu_cache_info(true)["expunges"] === 0); + +// this insertion should trigger an default_expunge which performs a real expunge (but no defragmentation) +var_dump(apcu_store("huge_entry", str_repeat('x', 700000), 1)); + +// check that cache cleanup and real expunge have been performed, but no defragmentation +var_dump(apcu_cache_info(true)["cleanups"] === 2); +var_dump(apcu_cache_info(true)["defragmentations"] === 1); +var_dump(apcu_cache_info(true)["expunges"] === 1); + +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/tests/apc_entry_003.phpt b/tests/apc_entry_003.phpt index e04bf4bd..d074f9ae 100644 --- a/tests/apc_entry_003.phpt +++ b/tests/apc_entry_003.phpt @@ -9,7 +9,8 @@ apc.enable_cli=1 --EXPECTF-- diff --git a/tests/apc_mmap_hugepage_001.phpt b/tests/apc_mmap_hugepage_001.phpt new file mode 100644 index 00000000..54e2b4ad --- /dev/null +++ b/tests/apc_mmap_hugepage_001.phpt @@ -0,0 +1,18 @@ +--TEST-- +Disable hugepage +--SKIPIF-- + +--INI-- +apc.enabled=1 +apc.enable_cli=1 +apc.mmap_hugepage_size=0 +apc.shm_size=2M +--FILE-- +===DONE=== +--EXPECT-- +===DONE=== diff --git a/tests/apc_mmap_hugepage_002.phpt b/tests/apc_mmap_hugepage_002.phpt new file mode 100644 index 00000000..106e6abf --- /dev/null +++ b/tests/apc_mmap_hugepage_002.phpt @@ -0,0 +1,28 @@ +--TEST-- +Enable hugepage +--SKIPIF-- + +--INI-- +apc.enabled=1 +apc.enable_cli=1 +apc.mmap_hugepage_size=2M +apc.shm_size=2M +--FILE-- +===DONE=== +--EXPECT-- +===DONE=== diff --git a/tests/apc_mmap_hugepage_003.phpt b/tests/apc_mmap_hugepage_003.phpt new file mode 100644 index 00000000..0fdfd9bf --- /dev/null +++ b/tests/apc_mmap_hugepage_003.phpt @@ -0,0 +1,28 @@ +--TEST-- +Error if apc.mmap_hugepage_size is negative +--SKIPIF-- + +--INI-- +apc.enabled=1 +apc.enable_cli=1 +apc.mmap_hugepage_size=-1 +apc.shm_size=2M +--FILE-- +Irrelevant +--EXPECTF-- +%A: apc.mmap_hugepage_size must be a positive integer in Unknown on line 0 diff --git a/tests/apc_mmap_hugepage_004.phpt b/tests/apc_mmap_hugepage_004.phpt new file mode 100644 index 00000000..8505c4f7 --- /dev/null +++ b/tests/apc_mmap_hugepage_004.phpt @@ -0,0 +1,28 @@ +--TEST-- +Error if apc.mmap_hugepage_size is not a power of 2 +--SKIPIF-- + +--INI-- +apc.enabled=1 +apc.enable_cli=1 +apc.mmap_hugepage_size=1000 +apc.shm_size=2M +--FILE-- +Irrelevant +--EXPECTF-- +%A: apc.mmap_hugepage_size must be a power of 2 in Unknown on line 0 diff --git a/tests/apc_mmap_hugepage_005.phpt b/tests/apc_mmap_hugepage_005.phpt new file mode 100644 index 00000000..d97294e7 --- /dev/null +++ b/tests/apc_mmap_hugepage_005.phpt @@ -0,0 +1,28 @@ +--TEST-- +Error if apc.shm_size is not a multiple of apc.mmap_hugepage_size +--SKIPIF-- + +--INI-- +apc.enabled=1 +apc.enable_cli=1 +apc.mmap_hugepage_size=2M +apc.shm_size=3M +--FILE-- +Irrelevant +--EXPECTF-- +%A: apc.shm_size must be a multiple of apc.mmap_hugepage_size in Unknown on line 0 diff --git a/tests/apc_mmap_hugepage_006.phpt b/tests/apc_mmap_hugepage_006.phpt new file mode 100644 index 00000000..a5dc1105 --- /dev/null +++ b/tests/apc_mmap_hugepage_006.phpt @@ -0,0 +1,28 @@ +--TEST-- +Error if apc.mmap_hugepage_size is too small +--SKIPIF-- + +--INI-- +apc.enabled=1 +apc.enable_cli=1 +apc.mmap_hugepage_size=1 +apc.shm_size=2M +--FILE-- +Irrelevant +--EXPECTF-- +%A: Invalid hugepage size: %d in Unknown on line 0 diff --git a/tests/apc_repeated_unpersist.phpt b/tests/apc_repeated_unpersist.phpt new file mode 100644 index 00000000..da54ed82 --- /dev/null +++ b/tests/apc_repeated_unpersist.phpt @@ -0,0 +1,73 @@ +--TEST-- +APCU: Test that repeated unpersist does not break the persistence representation in SHM +--SKIPIF-- + +--INI-- +apc.enabled=1 +apc.enable_cli=1 +apc.serializer=default +apc.shm_size=1M +--FILE-- + "val1", "prop2" => 2]); +apcu_store("array_empty", []); +apcu_store("array_simple", $tmp); +apcu_store("array_nested", [$tmp]); +apcu_store("array_referenced", [&$tmp, &$tmp]); + +echo "1st unpersist:\n"; +var_dump(apcu_fetch("int") === 123); +var_dump(apcu_fetch("string") === "abc"); +var_dump(apcu_fetch("object") == (object) ["prop1" => "val1", "prop2" => 2]); +var_dump(apcu_fetch("array_empty") === []); +var_dump(apcu_fetch("array_simple") === $tmp); +var_dump(apcu_fetch("array_nested") === [$tmp]); +var_dump(apcu_fetch("array_referenced") === [&$tmp, &$tmp]); + +echo "2nd unpersist (check, if the 1st unpersist didn't break the representation in SHM):\n"; +var_dump(apcu_fetch("int") === 123); +var_dump(apcu_fetch("string") === "abc"); +var_dump(apcu_fetch("object") == (object) ["prop1" => "val1", "prop2" => 2]); +var_dump(apcu_fetch("array_empty") === []); +var_dump(apcu_fetch("array_simple") === $tmp); +var_dump(apcu_fetch("array_nested") === [$tmp]); +var_dump(apcu_fetch("array_referenced") === [&$tmp, &$tmp]); + +echo "Check if the reference has been preserved:\n"; +$tmp = apcu_fetch("array_referenced"); +$tmp[0][0] = 2; +$tmp[0][1] = "b"; +var_dump($tmp[1][0] === 2); +var_dump($tmp[1][1] === "b"); + +?> +--EXPECT-- +1st unpersist: +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +2nd unpersist (check, if the 1st unpersist didn't break the representation in SHM): +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +Check if the reference has been preserved: +bool(true) +bool(true) diff --git a/tests/apc_soft_expired.phpt b/tests/apc_soft_expired.phpt new file mode 100644 index 00000000..0fcfe9da --- /dev/null +++ b/tests/apc_soft_expired.phpt @@ -0,0 +1,60 @@ +--TEST-- +apcu_inc/dec() should not inc/dec soft expired entries based on global TTL setting +--SKIPIF-- + +--INI-- +apc.enabled=1 +apc.enable_cli=1 +apc.use_request_time=1 +apc.ttl=2 +--FILE-- + +--EXPECT-- +T+0: +int(1) +int(1) +int(-1) +int(-1) +T+1: +int(2) +int(2) +int(-2) +int(-2) +T+4: +int(1) +int(1) +int(-1) +int(-1) diff --git a/tests/bug63224.phpt b/tests/bug63224.phpt deleted file mode 100644 index 92ea3c90..00000000 --- a/tests/bug63224.phpt +++ /dev/null @@ -1,310 +0,0 @@ ---TEST-- -APC: Bug #63224 error in __sleep whit reference to other classes ---SKIPIF-- - ---CONFLICTS-- -server ---FILE-- -b->f(); - return array('b'); - } -} - - -class B{ - const A_CONSTANT = 1; - public \$var; - - public function f(){ - \$this->var = self::A_CONSTANT; - } -} - - -if(isset(\$_SESSION['lalala'])){ - echo "
";
-	\$a = \$_SESSION['lalala'];
-	print_r(\$a);
-} else {
-	echo "no session yet, first run\n";
-}
-
-//	another file
-//	class A and B use autoload
-\$b = new B();
-\$a = new A();
-\$a->b = \$b;
-
-\$_SESSION['lalala'] = \$a;
-session_write_close();
-FL;
-
-$args = array(
-	'apc.enabled=1',
-	'apc.cache_by_default=1',
-	'apc.enable_cli=1',
-    'session.gc_probability=0',
-);
-
-server_start($file, $args);
-
-$sid = md5(uniqid("call me maybe", true));
-for ($i = 0; $i < 10; $i++) {
-	$send = "GET / HTTP/1.1\n" .
-			"Host: " . PHP_CLI_SERVER_HOSTNAME . "\n" .
-			"Cookie: PHPSESSID=$sid;" .
-			"\r\n\r\n";
-	for ($j = 0; $j < $num_servers; $j++) {
-		run_test(PHP_CLI_SERVER_HOSTNAME, PHP_CLI_SERVER_PORT+$j, $send);
-	}
-}
-echo 'done';
---EXPECT--
-no session yet, first run
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-
A Object
-(
-    [b] => B Object
-        (
-            [var] => 1
-        )
-
-)
-done
diff --git a/tests/get_included_files_inc1.inc b/tests/get_included_files_inc1.inc
deleted file mode 100644
index 344e300e..00000000
--- a/tests/get_included_files_inc1.inc
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/tests/get_included_files_inc2.inc b/tests/get_included_files_inc2.inc
deleted file mode 100644
index 318eba00..00000000
--- a/tests/get_included_files_inc2.inc
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/tests/get_included_files_inc3.inc b/tests/get_included_files_inc3.inc
deleted file mode 100644
index f666edf2..00000000
--- a/tests/get_included_files_inc3.inc
+++ /dev/null
@@ -1,4 +0,0 @@
-