From f9314aff6308c7ef82ae9938d53d1e89555cfe4d Mon Sep 17 00:00:00 2001 From: ttomalak Date: Tue, 12 Sep 2023 13:50:31 +0200 Subject: [PATCH 01/40] Allow all entries to be soft_expired We have a lot of keys put into the storage, but some of them with Day of TTL might be accessed only few times. We should allow such keys to be expired, and therefore remove check for entry->ttl. --- apc_cache.c | 6 ++--- tests/apc_019.phpt | 2 +- tests/apc_020.phpt | 2 +- tests/apc_026.phpt | 60 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 tests/apc_026.phpt diff --git a/apc_cache.c b/apc_cache.c index 2ea5b521..e2658237 100644 --- a/apc_cache.c +++ b/apc_cache.c @@ -146,12 +146,12 @@ 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( @@ -273,7 +273,7 @@ PHP_APCU_API int APC_UNSERIALIZER_NAME(php) (APC_UNSERIALIZER_ARGS) result = php_var_unserialize(value, &tmp, buf + buf_len, &var_hash); PHP_VAR_UNSERIALIZE_DESTROY(var_hash); BG(serialize_lock)--; - + if (!result) { php_error_docref(NULL, E_NOTICE, "Error at offset %ld of %ld bytes", (zend_long)(tmp - buf), (zend_long)buf_len); ZVAL_NULL(value); diff --git a/tests/apc_019.phpt b/tests/apc_019.phpt index 8415e5c2..a9baeb6e 100644 --- a/tests/apc_019.phpt +++ b/tests/apc_019.phpt @@ -3,7 +3,7 @@ The per-entry TTL should take precedence over the global TTL --SKIPIF-- --INI-- apc.enabled=1 diff --git a/tests/apc_020.phpt b/tests/apc_020.phpt index ea388d31..a4537eaa 100644 --- a/tests/apc_020.phpt +++ b/tests/apc_020.phpt @@ -3,7 +3,7 @@ Test default expunge logic wrt global and per-entry TTLs --SKIPIF-- --INI-- apc.enabled=1 diff --git a/tests/apc_026.phpt b/tests/apc_026.phpt new file mode 100644 index 00000000..0fcfe9da --- /dev/null +++ b/tests/apc_026.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) From 7bd94a794deae0a988014516566cc5fc7e1e62b9 Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Mon, 26 May 2025 20:59:39 +0200 Subject: [PATCH 02/40] Small SMA code cleanups / improvements --- apc_sma.c | 54 ++++++++++++++++++++++-------------------------------- apc_sma.h | 5 ----- 2 files changed, 22 insertions(+), 37 deletions(-) diff --git a/apc_sma.c b/apc_sma.c index 3559c545..d487a22d 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 @@ -99,13 +99,18 @@ struct block_t { #define BEST_FIT_LIMIT 3 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,22 +118,17 @@ 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; } } @@ -141,14 +141,8 @@ 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) { @@ -189,7 +183,7 @@ static APC_HOTSPOT size_t sma_allocate(sma_header_t *smaheader, size_t size) SET_CANARY(cur); - return OFFSET(cur) + block_header_size; + return OFFSET(cur) + ALIGNWORD(sizeof(block_t)); } /* }}} */ @@ -217,7 +211,7 @@ static APC_HOTSPOT size_t sma_deallocate(sma_header_t *smaheader, size_t offset) BLOCKAT(prv->fnext)->fprev = prv->fprev; BLOCKAT(prv->fprev)->fnext = prv->fnext; /* cur and prv share an edge, combine them */ - prv->size +=cur->size; + prv->size += cur->size; RESET_CANARY(cur); cur = prv; @@ -382,22 +376,23 @@ PHP_APCU_API apc_sma_info_t *apc_sma_info(apc_sma_t* sma, zend_bool limited) { SMA_LOCK(sma); 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); @@ -447,11 +442,6 @@ 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 */ -} - /* }}} */ /* diff --git a/apc_sma.h b/apc_sma.h index 472a8c72..7c15775f 100644 --- a/apc_sma.h +++ b/apc_sma.h @@ -111,11 +111,6 @@ PHP_APCU_API size_t apc_sma_get_avail_mem(apc_sma_t* sma); */ PHP_APCU_API zend_bool apc_sma_get_avail_size(apc_sma_t* sma, size_t size); -/* -* apc_sma_api_check_integrity will check the integrity of sma -*/ -PHP_APCU_API void apc_sma_check_integrity(apc_sma_t* sma); /* }}} */ - /* {{{ ALIGNWORD: pad up x, aligned to the system's word boundary */ #define ALIGNWORD(x) ZEND_MM_ALIGNED_SIZE(x) /* }}} */ From 289addf45b8d6b435ffb2ece90ab51e3f77161cb Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Wed, 14 May 2025 20:52:41 +0200 Subject: [PATCH 03/40] Refactor linked lists of the cache layer to doubly linked lists Cache entries now contain a reference to the previous entry or the head pointer of the linked list. This is to prepare for defragmentation, as it allows entries to be moved in memory if the address containing the reference to the current entry is not available in the context. --- TECHNOTES.txt | 5 +++-- apc_cache.c | 56 ++++++++++++++++++++++++++++++++++----------------- apc_cache.h | 5 +++-- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/TECHNOTES.txt b/TECHNOTES.txt index e7cf642b..bd6a19dd 100644 --- a/TECHNOTES.txt +++ b/TECHNOTES.txt @@ -258,8 +258,8 @@ form of a quick-start guide to start hacking on APCu. /* {{{ 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 */ @@ -268,6 +268,7 @@ form of a quick-start guide to start hacking on APCu. 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!) */ }; /* }}} */ diff --git a/apc_cache.c b/apc_cache.c index 48bb863d..2627130c 100644 --- a/apc_cache.c +++ b/apc_cache.c @@ -160,29 +160,47 @@ static zend_bool apc_cache_entry_expired( || apc_cache_entry_soft_expired(cache, entry, t); } +/* 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; + } +} + /* {{{ apc_cache_wlocked_remove_entry */ -static void apc_cache_wlocked_remove_entry(apc_cache_t *cache, uintptr_t *entry_offset) +static void apc_cache_wlocked_remove_entry(apc_cache_t *cache, apc_cache_entry_t *entry) { - apc_cache_entry_t *dead = ENTRYAT(*entry_offset); - - /* unlink entry from list */ - *entry_offset = dead->next; + /* 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); } } /* }}} */ @@ -220,7 +238,7 @@ 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); } } @@ -356,7 +374,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 +383,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 +392,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,6 +405,7 @@ static void apc_cache_set_entry_values(apc_cache_entry_t *entry, const int32_t t { entry->ttl = ttl; entry->next = 0; + entry->prev = 0; entry->ref_count = 0; entry->nhits = 0; entry->ctime = t; @@ -696,7 +714,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)); } } @@ -768,7 +786,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; } @@ -965,7 +983,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; diff --git a/apc_cache.h b/apc_cache.h index e800854f..9fcf9ef9 100644 --- a/apc_cache.h +++ b/apc_cache.h @@ -49,8 +49,8 @@ struct apc_cache_slam_key_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 */ + 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,6 +59,7 @@ 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!) */ }; /* }}} */ From d0a1fa9b138b6e0a5a11fca4bda48561253dda92 Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Fri, 23 May 2025 22:34:51 +0200 Subject: [PATCH 04/40] Add defragmentation to apcu This adds defragmentation logic to apcu, which is performed during the default_expunge operation. It works by shifting all allocated blocks to the left (low addresses), allowing all free blocks to be coalesced to one larger free block on the right side. --- apc_cache.c | 47 +++++++++++++++++++----- apc_sma.c | 62 ++++++++++++++++++++++++++++++- apc_sma.h | 22 ++++++++++- package.xml | 1 + tests/apc_defrag.phpt | 85 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 205 insertions(+), 12 deletions(-) create mode 100644 tests/apc_defrag.phpt diff --git a/apc_cache.c b/apc_cache.c index 2627130c..55765b0b 100644 --- a/apc_cache.c +++ b/apc_cache.c @@ -160,6 +160,24 @@ static zend_bool apc_cache_entry_expired( || apc_cache_entry_soft_expired(cache, entry, t); } +/* 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; + } + + /* 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) { @@ -772,9 +790,6 @@ PHP_APCU_API void apc_cache_default_expunge(apc_cache_t* cache, size_t size) return; } - /* gc */ - apc_cache_wlocked_gc(cache); - /* smart > 1 increases the probability of a full cache wipe, * so expunge() is called less often when memory is low. */ size = (cache->smart > 0L) ? (size_t) (cache->smart * size) : size; @@ -795,15 +810,29 @@ 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); + apc_cache_wunlock(cache); + return; + } + + /* 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); + apc_cache_wunlock(cache); + return; } + /* wipe lastkey */ + memset(&cache->header->lastkey, 0, sizeof(apc_cache_slam_key_t)); + apc_cache_wunlock(cache); } /* }}} */ diff --git a/apc_sma.c b/apc_sma.c index d487a22d..8b00adc0 100644 --- a/apc_sma.c +++ b/apc_sma.c @@ -415,7 +415,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); @@ -442,6 +446,62 @@ 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_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))); + + 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 */ + cur->fnext = first->fnext; + cur->fprev = OFFSET(first); + first->fnext = OFFSET(cur); + BLOCKAT(cur->fnext)->fprev = first->fnext; + cur->prev_size = 0; + nxt->prev_size = cur->size; + + cur = NEXT_SBLOCK(nxt); + continue; + } + + /* swap cur and nxt by moving nxt (incl. header) and initializing a new block header for cur behind it */ + size_t free_size = cur->size; + 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; + } + } + + SMA_UNLOCK(sma); +} + /* }}} */ /* diff --git a/apc_sma.h b/apc_sma.h index 7c15775f..a444c9d5 100644 --- a/apc_sma.h +++ b/apc_sma.h @@ -107,9 +107,27 @@ 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_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); + +/* +* 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_check_avail_contiguous(apc_sma_t *sma, size_t size); + +/* +* 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. +*/ +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 */ #define ALIGNWORD(x) ZEND_MM_ALIGNED_SIZE(x) diff --git a/package.xml b/package.xml index f9c90514..157585cc 100644 --- a/package.xml +++ b/package.xml @@ -75,6 +75,7 @@ + diff --git a/tests/apc_defrag.phpt b/tests/apc_defrag.phpt new file mode 100644 index 00000000..38a44dea --- /dev/null +++ b/tests/apc_defrag.phpt @@ -0,0 +1,85 @@ +--TEST-- +Test defragmentation +--SKIPIF-- + +--INI-- +apc.enabled=1 +apc.enable_cli=1 +apc.use_request_time=1 +apc.shm_size=1M +--FILE-- += $min_entry_size) { + $i++; + apcu_store(sprintf("ttl3_%010d", $i), $i, 3); + + if (apcu_sma_info(true)['avail_mem'] >= $min_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 +apcu_store("ttl3_int", 123456789, 3); +apcu_store("ttl3_string", "abc", 3); +apcu_store("ttl3_array", [1, 2, "a", "b"], 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(); + +// ensure that cache is full +var_dump(apcu_sma_info(true)['avail_mem']); + +// 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, 2, "a", "b"]); +var_dump(apcu_fetch("ttl3_object") == (object) ["prop1" => "val1", "prop2" => 2]); + +?> +--EXPECT-- +float(0) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) From 19eb5cf5cd4e03d318e94906eb7c53b4ef25e5a5 Mon Sep 17 00:00:00 2001 From: KitanoTom <81726287+KitanoTom@users.noreply.github.com> Date: Wed, 4 Jun 2025 04:46:56 +0900 Subject: [PATCH 05/40] Add support for hugetlb pages on Linux (#559) Enable huge pages on Linux by calling mmap() with the MAP_HUGETLB flag. Using huge pages can improve performance when working with large amounts of shared memory. This PR adds the apc.mmap_hugepage_size setting, which accepts the huge page size. --- .github/workflows/config.yml | 2 ++ apc_globals.h | 3 ++- apc_mmap.c | 42 +++++++++++++++++++++++++++++--- apc_mmap.h | 2 +- apc_sma.c | 4 +-- apc_sma.h | 2 +- package.xml | 6 +++++ php_apc.c | 40 ++++++++++++++++++++++++++---- tests/apc_mmap_hugepage_001.phpt | 18 ++++++++++++++ tests/apc_mmap_hugepage_002.phpt | 28 +++++++++++++++++++++ tests/apc_mmap_hugepage_003.phpt | 28 +++++++++++++++++++++ tests/apc_mmap_hugepage_004.phpt | 28 +++++++++++++++++++++ tests/apc_mmap_hugepage_005.phpt | 28 +++++++++++++++++++++ tests/apc_mmap_hugepage_006.phpt | 28 +++++++++++++++++++++ 14 files changed, 246 insertions(+), 13 deletions(-) create mode 100644 tests/apc_mmap_hugepage_001.phpt create mode 100644 tests/apc_mmap_hugepage_002.phpt create mode 100644 tests/apc_mmap_hugepage_003.phpt create mode 100644 tests/apc_mmap_hugepage_004.phpt create mode 100644 tests/apc_mmap_hugepage_005.phpt create mode 100644 tests/apc_mmap_hugepage_006.phpt diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 065d8018..c667a977 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -7,6 +7,8 @@ jobs: version: ["7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"] 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 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_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_sma.c b/apc_sma.c index 8b00adc0..128ade53 100644 --- a/apc_sma.c +++ b/apc_sma.c @@ -243,7 +243,7 @@ static APC_HOTSPOT size_t sma_deallocate(sma_header_t *smaheader, size_t offset) /* }}} */ /* {{{ 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; } @@ -254,7 +254,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 diff --git a/apc_sma.h b/apc_sma.h index a444c9d5..c3204fbf 100644 --- a/apc_sma.h +++ b/apc_sma.h @@ -74,7 +74,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. diff --git a/package.xml b/package.xml index 157585cc..27a9cfd9 100644 --- a/package.xml +++ b/package.xml @@ -90,6 +90,12 @@ + + + + + + diff --git a/php_apc.c b/php_apc.c index 3aff354a..a00a04e1 100644 --- a/php_apc.c +++ b/php_apc.c @@ -116,6 +116,33 @@ static PHP_INI_MH(OnUpdateShmSize) /* {{{ */ } /* }}} */ +#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) STD_PHP_INI_ENTRY("apc.shm_size", "32M", PHP_INI_SYSTEM, OnUpdateShmSize, shm_size, zend_apcu_globals, apcu_globals) @@ -124,7 +151,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) @@ -219,10 +247,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 +261,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); 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 From 4292854263085de533792d32bebd8cb925536760 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Tue, 3 Jun 2025 21:54:08 +0200 Subject: [PATCH 06/40] Disable Windows CI for PHP 7.x (#565) This requires windows-2019, which is being phased out: actions/runner-images#12045 --- .github/workflows/config.yml | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index c667a977..1c905f8b 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -33,22 +33,10 @@ 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"] 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 From 01e5c4dec29dbb5295f800ea7da7297549a193b2 Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Sat, 7 Jun 2025 00:00:52 +0200 Subject: [PATCH 07/40] Fix Linux CI for PHP 7.x to not silently abort tests (#567) All PHP 7.x Linux pipelines silently aborted the tests because the PHP executable could not be found. This was caused by the definition of "PHP_EXECUTABLE = NONE" in the Makefile, as "php-config --php-binary" returned "NONE" during ./configure. Since it seems time-consuming to fix the root cause, this is a temporary workaround until PHP 7.x is removed from the CI pipeline. --- .github/workflows/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 1c905f8b..23145290 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -19,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 From ebd7a02d236d8c3020b142b0455624b26ce0639a Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Sun, 8 Jun 2025 17:26:51 +0200 Subject: [PATCH 08/40] Update atime and statistics when accessing entries with apcu_exists() (#564) The access time and access statistics are now updated when using apcu_exists(). The documentation does not indicate that this function behaves differently than apcu_fetch() in this regard. This should also help to use values other than 0 for apc.ttl, since entries frequently checked with apcu_exists() no longer soft-expire. --- apc_cache.c | 2 +- tests/apc_020.phpt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apc_cache.c b/apc_cache.c index 55765b0b..2ef4cbe2 100644 --- a/apc_cache.c +++ b/apc_cache.c @@ -880,7 +880,7 @@ 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; diff --git a/tests/apc_020.phpt b/tests/apc_020.phpt index 7c74ee3d..375863fd 100644 --- a/tests/apc_020.phpt +++ b/tests/apc_020.phpt @@ -14,10 +14,9 @@ apc.shm_size=1M --FILE-- Date: Sun, 8 Jun 2025 17:44:31 +0200 Subject: [PATCH 09/40] Update changelog --- package.xml | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/package.xml b/package.xml index 27a9cfd9..5c4b58f7 100644 --- a/package.xml +++ b/package.xml @@ -39,7 +39,27 @@ PHP License -- TBD + - 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, shaed 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) + + 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. From 51b745da08694dcbf196fa2511d0f07b48acab4b Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Mon, 9 Jun 2025 16:40:57 +0200 Subject: [PATCH 10/40] Update changelog (#569) --- package.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/package.xml b/package.xml index 5c4b58f7..ba3392dd 100644 --- a/package.xml +++ b/package.xml @@ -51,15 +51,22 @@ 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, shaed memory is still configured to use transparent huge pages. + 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. + - 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. From 66150a135bed6f1630d15a01e19dc7851504cfc3 Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Sat, 14 Jun 2025 20:56:15 +0200 Subject: [PATCH 11/40] Add cache cleanup and defragmentation counts to apcu_cache_info() (#570) The cache cleanup and defragmentation counts are now available in the array returned by apcu_cache_info(). --- apc.php | 2 ++ apc_cache.c | 14 +++++++++++++- apc_cache.h | 4 +++- package.xml | 4 ++++ tests/apc_defrag.phpt | 32 ++++++++++++++++++++++++-------- 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/apc.php b/apc.php index dddb4544..afe7b8e5 100644 --- a/apc.php +++ b/apc.php @@ -804,6 +804,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{$cache['cleanups']} + Cache defragmentation count{$cache['defragmentations']} Cache full count{$cache['expunges']} diff --git a/apc_cache.c b/apc_cache.c index 2ef4cbe2..521ea020 100644 --- a/apc_cache.c +++ b/apc_cache.c @@ -344,6 +344,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); @@ -765,6 +767,8 @@ 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); @@ -794,6 +798,9 @@ PHP_APCU_API void apc_cache_default_expunge(apc_cache_t* cache, size_t size) * so expunge() is called less often when memory is low. */ size = (cache->smart > 0L) ? (size_t) (cache->smart * size) : size; + /* increment cache cleanup statistics (removal of expired entries) */ + cache->header->ncleanups++; + /* look for junk */ for (i = 0; i < cache->nslots; i++) { uintptr_t *entry_offset = &cache->slots[i]; @@ -820,6 +827,9 @@ PHP_APCU_API void apc_cache_default_expunge(apc_cache_t* cache, size_t size) return; } + /* 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); @@ -1094,7 +1104,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); diff --git a/apc_cache.h b/apc_cache.h index 9fcf9ef9..a9c60803 100644 --- a/apc_cache.h +++ b/apc_cache.h @@ -71,7 +71,9 @@ typedef struct _apc_cache_header_t { 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 */ diff --git a/package.xml b/package.xml index ba3392dd..77e8ff15 100644 --- a/package.xml +++ b/package.xml @@ -60,6 +60,10 @@ 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: diff --git a/tests/apc_defrag.phpt b/tests/apc_defrag.phpt index 38a44dea..162b6821 100644 --- a/tests/apc_defrag.phpt +++ b/tests/apc_defrag.phpt @@ -16,15 +16,15 @@ apc.shm_size=1M // fill_cache() fills the cache with small entries with alternating ttl=1 and ttl=3 function fill_cache(): int { $i = 0; - $min_entry_size = apcu_sma_info(true)['avail_mem']; + $entry_size = apcu_sma_info(true)['avail_mem']; apcu_store(sprintf("ttl1_%010d", $i), $i, 1); - $min_entry_size -= apcu_sma_info(true)['avail_mem']; + $entry_size -= apcu_sma_info(true)['avail_mem']; - while (apcu_sma_info(true)['avail_mem'] >= $min_entry_size) { + while (apcu_sma_info(true)['avail_mem'] >= $entry_size) { $i++; apcu_store(sprintf("ttl3_%010d", $i), $i, 3); - if (apcu_sma_info(true)['avail_mem'] >= $min_entry_size) { + if (apcu_sma_info(true)['avail_mem'] >= $entry_size) { apcu_store(sprintf("ttl1_%010d", $i), $i, 1); } } @@ -48,9 +48,6 @@ $avail_before_filled = apcu_sma_info(true)['avail_mem']; // fill cache with alternating ttl=1 + ttl=3 entries fill_cache(); -// ensure that cache is full -var_dump(apcu_sma_info(true)['avail_mem']); - // expire all ttl1_* entries apcu_inc_request_time(2); @@ -72,9 +69,28 @@ var_dump(apcu_fetch("ttl3_string") === "abc"); var_dump(apcu_fetch("ttl3_array") === [1, 2, "a", "b"]); 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-- -float(0) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) bool(true) bool(true) bool(true) From 322dceab88483aaee12f135d244171520dc45f91 Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Sat, 12 Jul 2025 10:07:33 +0200 Subject: [PATCH 12/40] Update TECHNOTES (#574) A large portion of the TECHNOTES no longer reflected the current state of apcu. The TECHNOTES have therefore been revised to make it easier to get started. Since the copies of the struct definitions are not required to provide a general overview, they have been removed to avoid duplicate maintenance. --- TECHNOTES.txt | 424 +++++++++++++++++++------------------------------- 1 file changed, 162 insertions(+), 262 deletions(-) diff --git a/TECHNOTES.txt b/TECHNOTES.txt index bd6a19dd..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,304 +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 { - 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 */ - 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 */ - zval val; /* the zval copied at store time */ - 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 ... From 3bab43a0604ee045ff9bfb94b188396a7611ca1e Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Sat, 12 Jul 2025 10:13:22 +0200 Subject: [PATCH 13/40] Fix relocatable representation of zend_empty_array (#575) When using the zend_empty_array optimization, the process-local address of zend_empty_array was stored in shared memory during persistence, which is inconsistent with the relocatable representation. To indicate the use of zend_empty_array during unpersist, the entry's starting address is now used during persist instead. --- apc_persist.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apc_persist.c b/apc_persist.c index 8f74e347..9f7f14e2 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)); @@ -708,7 +709,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; } From 8e7ed9a499ded5d9ddaf8cb3aefc598e54a35489 Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Sat, 26 Jul 2025 17:27:51 +0200 Subject: [PATCH 14/40] Add test that a repeated unpersist doesn't break representation (#577) The purpose of this test is to ensure that the unpersist-code does not accidentally change the persistence representation in SHM (e.g., by the offset -> pointer conversion), causing a second unpersist to fail. --- package.xml | 1 + tests/apc_repeated_unpersist.phpt | 73 +++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 tests/apc_repeated_unpersist.phpt diff --git a/package.xml b/package.xml index 77e8ff15..eb0dadb5 100644 --- a/package.xml +++ b/package.xml @@ -115,6 +115,7 @@ + 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) From 16b021c98e8bd1093621cae06313af54ab56afd2 Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Sun, 27 Jul 2025 20:33:50 +0200 Subject: [PATCH 15/40] Add reference and empty array to the defrag test (#576) A reference and an empty array have been added to the tested types. This allows the defragmentation test to verify whether references or the special case zend_empty_array work correctly after defragmentation. --- tests/apc_defrag.phpt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/apc_defrag.phpt b/tests/apc_defrag.phpt index 162b6821..533772b5 100644 --- a/tests/apc_defrag.phpt +++ b/tests/apc_defrag.phpt @@ -37,9 +37,10 @@ function fill_cache(): int { 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, 2, "a", "b"], 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 @@ -66,7 +67,7 @@ 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, 2, "a", "b"]); +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 From 616c72d58cfc2a7ffce7512f9ae91e49d2d06ab9 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Mon, 28 Jul 2025 20:49:01 +0200 Subject: [PATCH 16/40] Release apcu 5.1.25 --- package.xml | 4 ++-- php_apc.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.xml b/package.xml index eb0dadb5..e511c65d 100644 --- a/package.xml +++ b/package.xml @@ -28,9 +28,9 @@ nikic@php.net yes - 2024-09-21 + 2025-07-28 - 5.1.25-dev + 5.1.25 5.1.18 diff --git a/php_apc.h b/php_apc.h index 3ca816fb..30622016 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.25" #define PHP_APCU_EXTNAME "apcu" PHP_APCU_API zend_bool apc_is_enabled(void); From e63ac232f0c7b2b8c57e09cdf242e876808e0618 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Mon, 28 Jul 2025 20:52:41 +0200 Subject: [PATCH 17/40] Back to dev --- package.xml | 80 +++++++++++++++++++++++++++++++---------------------- php_apc.h | 2 +- 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/package.xml b/package.xml index e511c65d..7231224c 100644 --- a/package.xml +++ b/package.xml @@ -30,7 +30,7 @@ 2025-07-28 - 5.1.25 + 5.1.26-dev 5.1.18 @@ -39,38 +39,6 @@ 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. @@ -224,6 +192,52 @@ + + 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.h b/php_apc.h index 30622016..6a26e99a 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" +#define PHP_APCU_VERSION "5.1.26-dev" #define PHP_APCU_EXTNAME "apcu" PHP_APCU_API zend_bool apc_is_enabled(void); From 1ca8ca981c374a85921984271be8d837393aa746 Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Tue, 29 Jul 2025 22:04:03 +0200 Subject: [PATCH 18/40] Use different fatal error in test for PHP 8.5 Since PHP 8.5, a missing trait is no longer a fatal error. Therefore, we must use a different fatal error. --- tests/apc_entry_003.phpt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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-- From 459e208cd11b29d3397b2586164ee02288236524 Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Tue, 29 Jul 2025 22:05:16 +0200 Subject: [PATCH 19/40] Add PHP 8.5 to Linux CI matrix --- .github/workflows/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 23145290..7cbb83d9 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -4,7 +4,7 @@ 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 From 6b23303ae02322af3ee136532814e2e5fecfe7ae Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Mon, 4 Aug 2025 21:12:05 +0200 Subject: [PATCH 20/40] Fix stacking of default expunge operations Under high load, multiple parallel insert operations could trigger multiple default expunge operations at the same time. To prevent this, pending default expunge operations are now aborted if another default expunge operation has run in the meantime. --- apc_cache.c | 30 ++++++++++++++++++++---------- apc_cache.h | 2 +- apc_sma.c | 4 ++-- apc_sma.h | 2 +- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/apc_cache.c b/apc_cache.c index 521ea020..981cb63d 100644 --- a/apc_cache.c +++ b/apc_cache.c @@ -776,31 +776,37 @@ PHP_APCU_API void apc_cache_clear(apc_cache_t* 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; + } + + /* 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. */ size = (cache->smart > 0L) ? (size_t) (cache->smart * size) : size; - /* increment cache cleanup statistics (removal of expired entries) */ - cache->header->ncleanups++; - /* look for junk */ for (i = 0; i < cache->nslots; i++) { uintptr_t *entry_offset = &cache->slots[i]; @@ -823,8 +829,7 @@ PHP_APCU_API void apc_cache_default_expunge(apc_cache_t* cache, size_t size) /* 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); - apc_cache_wunlock(cache); - return; + goto end_lbl; } /* increment defragmentation statistics */ @@ -836,14 +841,19 @@ PHP_APCU_API void apc_cache_default_expunge(apc_cache_t* cache, size_t size) /* 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); - apc_cache_wunlock(cache); - return; + 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; } /* }}} */ diff --git a/apc_cache.h b/apc_cache.h index a9c60803..d90327a7 100644 --- a/apc_cache.h +++ b/apc_cache.h @@ -254,7 +254,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_sma.c b/apc_sma.c index 128ade53..3964a6ac 100644 --- a/apc_sma.c +++ b/apc_sma.c @@ -326,8 +326,8 @@ PHP_APCU_API void* apc_sma_malloc(apc_sma_t* sma, size_t n) { /* 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; } diff --git a/apc_sma.h b/apc_sma.h index c3204fbf..2c6784f2 100644 --- a/apc_sma.h +++ b/apc_sma.h @@ -52,7 +52,7 @@ struct apc_sma_info_t { }; /* }}} */ -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 { From 66b4f12952a956736812305e1cbda03904270086 Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Tue, 5 Aug 2025 21:31:35 +0200 Subject: [PATCH 21/40] Fix race condition during allocation / defragmentation (#584) This fixes a race condition that can occur when a newly allocated entry is moved by defragmentation before it is inserted into the hash table. --- apc_cache.c | 8 ++++++-- apc_persist.c | 9 ++++++++- apc_sma.c | 13 ++++++++++--- apc_sma.h | 7 +++++-- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/apc_cache.c b/apc_cache.c index 981cb63d..720c5932 100644 --- a/apc_cache.c +++ b/apc_cache.c @@ -330,7 +330,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); @@ -426,7 +426,6 @@ static void apc_cache_set_entry_values(apc_cache_entry_t *entry, const int32_t t entry->ttl = ttl; entry->next = 0; entry->prev = 0; - entry->ref_count = 0; entry->nhits = 0; entry->ctime = t; entry->mtime = t; @@ -459,6 +458,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; } @@ -571,6 +573,8 @@ PHP_APCU_API zend_bool apc_cache_store( php_apc_try { ret = apc_cache_wlocked_insert(cache, entry, exclusive); } php_apc_finally { + /* release entry, because the ref_count of a new entry is initialized to 1 during allocation */ + apc_cache_entry_release(cache, entry); apc_cache_wunlock(cache); } php_apc_end_try(); diff --git a/apc_persist.c b/apc_persist.c index 9f7f14e2..e322b8d2 100644 --- a/apc_persist.c +++ b/apc_persist.c @@ -475,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; @@ -516,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; diff --git a/apc_sma.c b/apc_sma.c index 3964a6ac..c7151fca 100644 --- a/apc_sma.c +++ b/apc_sma.c @@ -301,7 +301,7 @@ 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; @@ -314,16 +314,23 @@ PHP_APCU_API void* apc_sma_malloc(apc_sma_t* sma, size_t n) { 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) { /* nuke is not set if expunge() was skipped internally to get another try */ diff --git a/apc_sma.h b/apc_sma.h index 2c6784f2..91e155c8 100644 --- a/apc_sma.h +++ b/apc_sma.h @@ -82,9 +82,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) From 43719b82a7a1344b80d4609397f1618b21283126 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Tue, 5 Aug 2025 21:43:21 +0200 Subject: [PATCH 22/40] Release apcu 5.1.26 --- package.xml | 7 +++++-- php_apc.h | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/package.xml b/package.xml index 7231224c..818a8f03 100644 --- a/package.xml +++ b/package.xml @@ -28,9 +28,9 @@ nikic@php.net yes - 2025-07-28 + 2025-08-05 - 5.1.26-dev + 5.1.26 5.1.18 @@ -39,6 +39,9 @@ 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. diff --git a/php_apc.h b/php_apc.h index 6a26e99a..6230ab7b 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.26-dev" +#define PHP_APCU_VERSION "5.1.26" #define PHP_APCU_EXTNAME "apcu" PHP_APCU_API zend_bool apc_is_enabled(void); From 3625ba49b397e9e95d68c9e510f8ffb98265704d Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Tue, 5 Aug 2025 21:47:33 +0200 Subject: [PATCH 23/40] Back to dev --- package.xml | 22 ++++++++++++++++++---- php_apc.h | 2 +- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/package.xml b/package.xml index 818a8f03..7addbb23 100644 --- a/package.xml +++ b/package.xml @@ -30,7 +30,7 @@ 2025-08-05 - 5.1.26 + 5.1.27-dev 5.1.18 @@ -39,9 +39,6 @@ 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. @@ -195,6 +192,23 @@ + + 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 diff --git a/php_apc.h b/php_apc.h index 6230ab7b..b702b73c 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.26" +#define PHP_APCU_VERSION "5.1.27-dev" #define PHP_APCU_EXTNAME "apcu" PHP_APCU_API zend_bool apc_is_enabled(void); From 3c53e4ca53f8463232934fb31182c965e6843ed8 Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Wed, 13 Aug 2025 19:04:25 +0200 Subject: [PATCH 24/40] Cleanup code comments (#585) The comments for a legacy code folding mechanism and other useless comments have been removed. In addition, several other comments have been improved. --- apc.c | 15 +++------ apc.h | 20 +++++------- apc_cache.c | 75 +++++++++++--------------------------------- apc_cache.h | 45 +++++++++++++-------------- apc_iterator.c | 27 +--------------- apc_iterator.h | 4 --- apc_lock.h | 24 +++++++-------- apc_signal.c | 32 +++++++------------ apc_sma.c | 9 ++---- apc_sma.h | 20 +++++------- apc_stack.c | 1 - php_apc.c | 84 ++++++++++++-------------------------------------- 12 files changed, 103 insertions(+), 253 deletions(-) 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_cache.c b/apc_cache.c index 720c5932..514734fa 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,8 +138,8 @@ 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; } @@ -199,7 +197,6 @@ static void apc_cache_wlocked_unlink_entry(apc_cache_t *cache, apc_cache_entry_t } } -/* {{{ apc_cache_wlocked_remove_entry */ static void apc_cache_wlocked_remove_entry(apc_cache_t *cache, apc_cache_entry_t *entry) { /* unlink entry from list */ @@ -221,9 +218,7 @@ static void apc_cache_wlocked_remove_entry(apc_cache_t *cache, apc_cache_entry_t 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 @@ -260,9 +255,8 @@ static void apc_cache_wlocked_gc(apc_cache_t* cache) free_entry(cache, entry); } } -/* }}} */ -/* {{{ php serializer */ +/* php serializer */ PHP_APCU_API int APC_SERIALIZER_NAME(php) (APC_SERIALIZER_ARGS) { smart_str strbuf = {0}; @@ -290,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; @@ -312,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; @@ -364,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) { @@ -539,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) { @@ -583,10 +575,9 @@ PHP_APCU_API zend_bool apc_cache_store( } return ret; -} /* }}} */ +} #ifndef ZTS -/* {{{ data_unserialize */ static zval data_unserialize(const char *filename) { zval retval; @@ -669,7 +660,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 @@ -703,16 +694,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, @@ -725,9 +713,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; @@ -753,9 +739,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) { @@ -777,9 +762,7 @@ PHP_APCU_API void apc_cache_clear(apc_cache_t* cache) apc_cache_wunlock(cache); } -/* }}} */ -/* {{{ apc_cache_default_expunge */ PHP_APCU_API zend_bool apc_cache_default_expunge(apc_cache_t* cache, size_t size) { time_t t; @@ -859,9 +842,7 @@ PHP_APCU_API zend_bool apc_cache_default_expunge(apc_cache_t* cache, size_t size 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; @@ -889,9 +870,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; @@ -909,9 +889,7 @@ PHP_APCU_API zend_bool apc_cache_exists(apc_cache_t* cache, zend_string *key, ti 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) @@ -959,9 +937,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) @@ -1009,9 +985,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; @@ -1048,15 +1022,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; @@ -1070,7 +1041,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; @@ -1090,9 +1060,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; @@ -1175,11 +1143,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; @@ -1223,7 +1188,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 */ @@ -1263,16 +1227,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) { @@ -1312,7 +1274,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 d90327a7..90aebb22 100644 --- a/apc_cache.h +++ b/apc_cache.h @@ -46,7 +46,6 @@ 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 { uintptr_t next; /* offset to next entry (MUST BE THE 1st FIELD OF THE STRUCT!) */ @@ -62,10 +61,8 @@ struct apc_cache_entry_t { 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 */ @@ -79,9 +76,8 @@ typedef struct _apc_cache_header_t { 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) */ @@ -92,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. @@ -129,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 */ @@ -152,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. @@ -170,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); @@ -185,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); @@ -202,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 @@ -223,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); @@ -243,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 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_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 c7151fca..af47f622 100644 --- a/apc_sma.c +++ b/apc_sma.c @@ -135,7 +135,7 @@ static inline block_t *find_block(sma_header_t *smaheader, size_t realsize) { 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 */ @@ -185,9 +185,8 @@ static APC_HOTSPOT size_t sma_allocate(sma_header_t *smaheader, size_t 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 */ @@ -240,9 +239,7 @@ static APC_HOTSPOT size_t sma_deallocate(sma_header_t *smaheader, size_t offset) 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, zend_long hugepage_size) { if (sma->initialized) { return; @@ -509,8 +506,6 @@ PHP_APCU_API void apc_sma_defrag(apc_sma_t *sma, void *data, apc_sma_move_f move SMA_UNLOCK(sma); } -/* }}} */ - /* * Local variables: * tab-width: 4 diff --git a/apc_sma.h b/apc_sma.h index 91e155c8..522f7d78 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 zend_bool (*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 */ @@ -65,7 +61,7 @@ typedef struct _apc_sma_t { /* info */ size_t size; /* segment size */ 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 @@ -132,9 +128,7 @@ PHP_APCU_API zend_bool apc_sma_check_avail_contiguous(apc_sma_t *sma, size_t siz 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/php_apc.c b/php_apc.c index a00a04e1..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,10 +112,9 @@ static PHP_INI_MH(OnUpdateShmSize) /* {{{ */ return SUCCESS; } -/* }}} */ #if defined(APC_MMAP) -static PHP_INI_MH(OnUpdateMmapHugepageSize) /* {{{ */ +static PHP_INI_MH(OnUpdateMmapHugepageSize) { zend_long s; @@ -140,7 +137,6 @@ static PHP_INI_MH(OnUpdateMmapHugepageSize) /* {{{ */ APCG(mmap_hugepage_size) = s; return SUCCESS; } -/* }}} */ #endif PHP_INI_BEGIN() @@ -162,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(); @@ -216,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) @@ -291,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); @@ -323,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) @@ -345,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) { @@ -357,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; @@ -374,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; @@ -386,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; @@ -437,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) @@ -451,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; @@ -510,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; @@ -549,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; @@ -579,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; @@ -612,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]; @@ -642,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; @@ -697,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; @@ -740,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; @@ -792,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. */ @@ -817,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, @@ -831,7 +787,6 @@ zend_module_entry apcu_module_entry = { PHP_APCU_VERSION, STANDARD_MODULE_PROPERTIES }; -/* }}} */ #ifdef COMPILE_DL_APCU ZEND_GET_MODULE(apcu) @@ -839,7 +794,6 @@ ZEND_GET_MODULE(apcu) ZEND_TSRMLS_CACHE_DEFINE(); #endif #endif -/* }}} */ /* * Local variables: From fe599cd1045a664fb83e633fbe8705184dc08fee Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Thu, 14 Aug 2025 22:02:25 +0200 Subject: [PATCH 25/40] Fix releasing of ref_count in apc_cache_store() (#587) In apc_cache_store(), the ref_count was decremented even when the entry needed to be deleted. This allowed the entry to be moved by defragmentation (by another process) before deletion. As a result, the pointer no longer pointed to the desired entry during deletion, causing segmentation faults. --- apc_cache.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apc_cache.c b/apc_cache.c index 514734fa..7a2b92cd 100644 --- a/apc_cache.c +++ b/apc_cache.c @@ -565,14 +565,16 @@ PHP_APCU_API zend_bool apc_cache_store( php_apc_try { ret = apc_cache_wlocked_insert(cache, entry, exclusive); } php_apc_finally { - /* release entry, because the ref_count of a new entry is initialized to 1 during allocation */ - apc_cache_entry_release(cache, entry); 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; } From 25cd3bc1977bdf640dc17660473e34962724ef94 Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Wed, 20 Aug 2025 11:48:22 +0200 Subject: [PATCH 26/40] Remove report_memleaks ini directive from tests (#590) The "report_memleaks" ini directive is deprecated in PHP 8.5 and has therefore been removed from the tests. --- tests/apc_006.phpt | 1 - tests/apc_006_php73.phpt | 1 - tests/apc_006_php81.phpt | 1 - 3 files changed, 3 deletions(-) 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-- Date: Thu, 28 Aug 2025 11:32:57 +0200 Subject: [PATCH 27/40] Release apcu 5.1.27 --- package.xml | 10 ++++++++-- php_apc.h | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/package.xml b/package.xml index 7addbb23..2183d8c2 100644 --- a/package.xml +++ b/package.xml @@ -28,9 +28,9 @@ nikic@php.net yes - 2025-08-05 + 2025-08-28 - 5.1.27-dev + 5.1.27 5.1.18 @@ -39,6 +39,12 @@ 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. diff --git a/php_apc.h b/php_apc.h index b702b73c..6e20f2f1 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.27-dev" +#define PHP_APCU_VERSION "5.1.27" #define PHP_APCU_EXTNAME "apcu" PHP_APCU_API zend_bool apc_is_enabled(void); From 3245a254e39b2f21e8a176a8e2d3d42d83e43cc2 Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Thu, 28 Aug 2025 11:44:24 +0200 Subject: [PATCH 28/40] Back to dev --- package.xml | 28 +++++++++++++++++++++------- php_apc.h | 2 +- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/package.xml b/package.xml index 2183d8c2..b3291d95 100644 --- a/package.xml +++ b/package.xml @@ -30,7 +30,7 @@ 2025-08-28 - 5.1.27 + 5.1.28-dev 5.1.18 @@ -39,12 +39,6 @@ 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. @@ -198,6 +192,26 @@ + + 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 diff --git a/php_apc.h b/php_apc.h index 6e20f2f1..640e9f67 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.27" +#define PHP_APCU_VERSION "5.1.28-dev" #define PHP_APCU_EXTNAME "apcu" PHP_APCU_API zend_bool apc_is_enabled(void); From 023c16b809e653db57a6f87dc8ecf058a28c94c6 Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Tue, 19 Aug 2025 23:24:14 +0200 Subject: [PATCH 29/40] Reclaim unused space from allocated blocks during defragmentation The fprev field now stores the used memory size for allocated blocks. This is used during defragmentation to shrink blocks if more memory has been allocated than necessary. --- apc_sma.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apc_sma.c b/apc_sma.c index af47f622..98a3e962 100644 --- a/apc_sma.c +++ b/apc_sma.c @@ -178,6 +178,9 @@ static APC_HOTSPOT size_t sma_allocate(sma_header_t *smaheader, size_t size) 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; @@ -454,6 +457,7 @@ 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; @@ -488,8 +492,13 @@ PHP_APCU_API void apc_sma_defrag(apc_sma_t *sma, void *data, apc_sma_move_f move 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 */ - size_t free_size = cur->size; memmove(cur, nxt, nxt->size); cur->prev_size = 0; cur = NEXT_SBLOCK(cur); @@ -503,6 +512,8 @@ PHP_APCU_API void apc_sma_defrag(apc_sma_t *sma, void *data, apc_sma_move_f move } } + smaheader->avail += reclaimed_size; + SMA_UNLOCK(sma); } From a6b94a5dea803607257ee0a74713ead25cf64bf1 Mon Sep 17 00:00:00 2001 From: Arndt Kaiser Date: Sun, 21 Sep 2025 20:34:50 +0200 Subject: [PATCH 30/40] Remove test bug63224.phpt The test has been removed because it serves no purpose anymore, as the opcode caching functionality was removed from the project long ago. Furthermore, the magic method __sleep() is deprecated in PHP 8.5, causing this test to fail the CI pipeline starting with PHP 8.5. --- package.xml | 1 - tests/bug63224.phpt | 310 -------------------------------------------- 2 files changed, 311 deletions(-) delete mode 100644 tests/bug63224.phpt diff --git a/package.xml b/package.xml index b3291d95..29250ed8 100644 --- a/package.xml +++ b/package.xml @@ -96,7 +96,6 @@ - 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

From d17bf0bb14caa7ebd4573168444428cb36813223 Mon Sep 17 00:00:00 2001
From: Arndt Kaiser 
Date: Wed, 24 Sep 2025 16:08:29 +0200
Subject: [PATCH 31/40] Removed obsolete files from the test directory

These files are no longer used by any test and have therefore been
deleted.
---
 package.xml                       | 3 ---
 tests/get_included_files_inc1.inc | 3 ---
 tests/get_included_files_inc2.inc | 4 ----
 tests/get_included_files_inc3.inc | 4 ----
 4 files changed, 14 deletions(-)
 delete mode 100644 tests/get_included_files_inc1.inc
 delete mode 100644 tests/get_included_files_inc2.inc
 delete mode 100644 tests/get_included_files_inc3.inc

diff --git a/package.xml b/package.xml
index 29250ed8..fd225db4 100644
--- a/package.xml
+++ b/package.xml
@@ -97,9 +97,6 @@
     
     
     
-    
-    
-    
     
     
     
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 @@
-

From c48075c2af465c001a09be10b2de4671730eb256 Mon Sep 17 00:00:00 2001
From: Arndt Kaiser 
Date: Fri, 26 Sep 2025 15:43:31 +0200
Subject: [PATCH 32/40] Add PHP 8.5 to Windows CI

---
 .github/workflows/config.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml
index 7cbb83d9..aa0999f4 100644
--- a/.github/workflows/config.yml
+++ b/.github/workflows/config.yml
@@ -35,7 +35,7 @@ jobs:
         shell: cmd
     strategy:
       matrix:
-        version: ["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]
     runs-on: windows-2022
@@ -44,7 +44,7 @@ jobs:
         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}}

From efcaa355fd2aad17e10eff0b6253290c76a92146 Mon Sep 17 00:00:00 2001
From: Arndt Kaiser 
Date: Fri, 10 Oct 2025 18:15:06 +0200
Subject: [PATCH 33/40] Improve SMA allocation performance and lock contention

The function sma_allocate() now inserts the unused part of a split block
at the beginning of the free list, instead of reinserting it at the
position of the original block. This prevents large blocks from falling
behind in the list when many small blocks are inserted at the beginning
of the list by sma_deallocate(). Benchmarks show that this improves
allocation performance and reduces the time the SMA lock must be held.
---
 apc_sma.c   | 21 +++++++++++----------
 package.xml |  3 +++
 2 files changed, 14 insertions(+), 10 deletions(-)

diff --git a/apc_sma.c b/apc_sma.c
index 98a3e962..1929069a 100644
--- a/apc_sma.c
+++ b/apc_sma.c
@@ -138,7 +138,6 @@ static inline block_t *find_block(sma_header_t *smaheader, size_t realsize) {
 /* 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 */
 
@@ -150,11 +149,12 @@ 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 */
+	BLOCKAT(cur->fprev)->fnext = cur->fnext;
+	BLOCKAT(cur->fnext)->fprev = cur->fprev;
+
 	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 */
@@ -169,11 +169,12 @@ 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 */
+		block_t *dst = BLOCKAT(ALIGNWORD(sizeof(sma_header_t)));
+		nxt->fnext = dst->fnext;
+		nxt->fprev = OFFSET(dst);
+		dst->fnext = OFFSET(nxt);
+		BLOCKAT(nxt->fnext)->fprev = dst->fnext;
 	}
 
 	cur->fnext = 0;
diff --git a/package.xml b/package.xml
index fd225db4..9eb3d4c6 100644
--- a/package.xml
+++ b/package.xml
@@ -39,6 +39,9 @@
  
  PHP License
  
+  - 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.
  
  
   

From 6846950ac8c26206218146a50f93e17b6524e031 Mon Sep 17 00:00:00 2001
From: Arndt Kaiser 
Date: Fri, 10 Oct 2025 18:29:42 +0200
Subject: [PATCH 34/40] Small SMA code improvements

The new functions link_free_block() and unlink_free_block_at_start()
are used to insert or remove blocks from the free list. This improves
readability and removes duplicate code. Also, some code comments have
been refined.
---
 apc_sma.c | 51 +++++++++++++++++++++++++++++----------------------
 1 file changed, 29 insertions(+), 22 deletions(-)

diff --git a/apc_sma.c b/apc_sma.c
index 1929069a..a848b5d2 100644
--- a/apc_sma.c
+++ b/apc_sma.c
@@ -98,6 +98,21 @@ 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 = BLOCKAT(ALIGNWORD(sizeof(sma_header_t)));
 	block_t *found = NULL;
@@ -150,8 +165,7 @@ static APC_HOTSPOT size_t sma_allocate(sma_header_t *smaheader, size_t size)
 	}
 
 	/* remove cur from the list of free blocks */
-	BLOCKAT(cur->fprev)->fnext = cur->fnext;
-	BLOCKAT(cur->fnext)->fprev = cur->fprev;
+	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 */
@@ -170,13 +184,10 @@ static APC_HOTSPOT size_t sma_allocate(sma_header_t *smaheader, size_t size)
 		SET_CANARY(nxt);
 
 		/* put the remaining block (nxt) back into the free list */
-		block_t *dst = BLOCKAT(ALIGNWORD(sizeof(sma_header_t)));
-		nxt->fnext = dst->fnext;
-		nxt->fprev = OFFSET(dst);
-		dst->fnext = OFFSET(nxt);
-		BLOCKAT(nxt->fnext)->fprev = dst->fnext;
+		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 */
@@ -209,10 +220,10 @@ 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;
 
@@ -223,23 +234,21 @@ 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;
 }
@@ -482,10 +491,8 @@ PHP_APCU_API void apc_sma_defrag(apc_sma_t *sma, void *data, apc_sma_move_f move
 		/* 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 */
-			cur->fnext = first->fnext;
-			cur->fprev = OFFSET(first);
-			first->fnext = OFFSET(cur);
-			BLOCKAT(cur->fnext)->fprev = first->fnext;
+			link_free_block_at_start(smaheader, cur);
+
 			cur->prev_size = 0;
 			nxt->prev_size = cur->size;
 

From 9eb62d17a2ade37cd3e5839e94ab864be6efdaf3 Mon Sep 17 00:00:00 2001
From: Arndt Kaiser 
Date: Fri, 17 Oct 2025 18:27:01 +0200
Subject: [PATCH 35/40] Check result of SMA lock acquisitions

This should never happen, but it should still be handled properly to
avoid worse.
---
 apc_sma.c | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/apc_sma.c b/apc_sma.c
index a848b5d2..e3057b89 100644
--- a/apc_sma.c
+++ b/apc_sma.c
@@ -391,7 +391,11 @@ 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 *cur = BLOCKAT(ALIGNWORD(sizeof(sma_header_t)));
 	apc_sma_link_t **link = &info->list;
@@ -445,7 +449,10 @@ PHP_APCU_API zend_bool apc_sma_check_avail_contiguous(apc_sma_t *sma, size_t siz
 		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 */

From d720b628ed2c6a99237c398ce0e449c7a5574b7a Mon Sep 17 00:00:00 2001
From: Arndt Kaiser 
Date: Wed, 22 Oct 2025 01:36:48 +0200
Subject: [PATCH 36/40] Prevent PHP warnings when using apc.php with APCu
 versions <= 5.1.24

Since the array keys "cleanups" and "defragmentations" only exist since
APCu 5.1.25, PHP warnings could occur if you are using a current version
of apc.php with APCu versions <= 5.1.24.
---
 apc.php | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/apc.php b/apc.php
index afe7b8e5..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,8 +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{$cache['cleanups']}
-            Cache defragmentation count{$cache['defragmentations']}
+            Cache cleanup count$cleanups
+            Cache defragmentation count$defragmentations
             Cache full count{$cache['expunges']}
         
         

From 7d1c95b0f5fe045f088b78cfefb885f93015460d Mon Sep 17 00:00:00 2001
From: k0d3r1s 
Date: Thu, 4 Dec 2025 16:09:14 +0200
Subject: [PATCH 37/40] The zval_dtor() alias of zval_ptr_dtor_nogc() has been
 removed

Call zval_ptr_dtor_nogc() directly instead
---
 apc_cache.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/apc_cache.c b/apc_cache.c
index 7a2b92cd..a12a9377 100644
--- a/apc_cache.c
+++ b/apc_cache.c
@@ -652,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;
 		}

From 352caaf7d52a088cb28242858ff445b981ee3e95 Mon Sep 17 00:00:00 2001
From: Arndt Kaiser 
Date: Sun, 7 Dec 2025 08:52:22 +0100
Subject: [PATCH 38/40] Prevent cache wipes caused by huge allocation attempts

Trying to insert entries larger than the shared memory no longer results
in the whole cache being discarded.
---
 apc_sma.c          |  6 ++++++
 apc_sma.h          |  1 +
 package.xml        |  2 ++
 tests/apc_026.phpt | 21 +++++++++++++++++++++
 4 files changed, 30 insertions(+)
 create mode 100644 tests/apc_026.phpt

diff --git a/apc_sma.c b/apc_sma.c
index e3057b89..1d31256e 100644
--- a/apc_sma.c
+++ b/apc_sma.c
@@ -273,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;
@@ -318,6 +319,11 @@ PHP_APCU_API void* apc_sma_malloc(apc_sma_t* sma, size_t n, apc_sma_malloc_init_
 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;
 	}
diff --git a/apc_sma.h b/apc_sma.h
index 522f7d78..6bd35a65 100644
--- a/apc_sma.h
+++ b/apc_sma.h
@@ -60,6 +60,7 @@ 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;
 
diff --git a/package.xml b/package.xml
index 9eb3d4c6..d3f47ac5 100644
--- a/package.xml
+++ b/package.xml
@@ -39,9 +39,11 @@
  
  PHP License
  
+  - 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.
  
  
   
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)

From 819fce90c469a5666b1706036cd66120af419f1f Mon Sep 17 00:00:00 2001
From: Nikita Popov 
Date: Sun, 7 Dec 2025 08:58:16 +0100
Subject: [PATCH 39/40] Release apcu 5.1.28

---
 package.xml | 6 ++++--
 php_apc.h   | 2 +-
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/package.xml b/package.xml
index d3f47ac5..8c995b22 100644
--- a/package.xml
+++ b/package.xml
@@ -28,9 +28,9 @@
   nikic@php.net
   yes
  
- 2025-08-28
+ 2025-12-07
  
-  5.1.28-dev
+  5.1.28
   5.1.18
  
  
@@ -44,6 +44,8 @@
     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.
  
  
   
diff --git a/php_apc.h b/php_apc.h
index 640e9f67..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.28-dev"
+#define PHP_APCU_VERSION "5.1.28"
 #define PHP_APCU_EXTNAME "apcu"
 
 PHP_APCU_API zend_bool apc_is_enabled(void);

From ca7e208b34f9c10ff6b1538de503a37b11af560d Mon Sep 17 00:00:00 2001
From: Albert Skonieczny <50720306+albertsko@users.noreply.github.com>
Date: Wed, 26 Aug 2026 08:24:15 +0200
Subject: [PATCH 40/40] chore(PLATFORM-11898): cover soft expiry of entries
 with per-entry TTL

---
 package.xml                 |  1 +
 tests/apc_019.phpt          |  6 ++--
 tests/apc_020.phpt          | 23 ++++++++------
 tests/apc_soft_expired.phpt | 60 +++++++++++++++++++++++++++++++++++++
 4 files changed, 78 insertions(+), 12 deletions(-)
 create mode 100644 tests/apc_soft_expired.phpt

diff --git a/package.xml b/package.xml
index 8c995b22..692dafeb 100644
--- a/package.xml
+++ b/package.xml
@@ -91,6 +91,7 @@
     
     
     
+    
     
     
     
diff --git a/tests/apc_019.phpt b/tests/apc_019.phpt
index a9baeb6e..8d827e59 100644
--- a/tests/apc_019.phpt
+++ b/tests/apc_019.phpt
@@ -1,9 +1,9 @@
 --TEST--
-The per-entry TTL should take precedence over the global TTL
+The global TTL may soft-expire entries with a per-entry TTL
 --SKIPIF--
 
 --INI--
 apc.enabled=1
@@ -34,7 +34,7 @@ var_dump(apcu_fetch("EzFY"));
 --EXPECT--
 T+2
 bool(false)
-int(42)
+bool(false)
 T+4
 bool(false)
 bool(false)
diff --git a/tests/apc_020.phpt b/tests/apc_020.phpt
index fa038ae9..bd497f11 100644
--- a/tests/apc_020.phpt
+++ b/tests/apc_020.phpt
@@ -3,7 +3,7 @@ Test default expunge logic wrt global and per-entry TTLs
 --SKIPIF--
 
 --INI--
 apc.enabled=1
@@ -18,18 +18,23 @@ apcu_store("no_ttl_unaccessed", str_repeat('x', 500));
 apcu_store("no_ttl_accessed", 24);
 apcu_store("ttl", 42, 3);
 
+// Fill the cache without triggering an expunge.
+$entry_size = apcu_sma_info(true)['avail_mem'];
+apcu_store(sprintf("key%06d", 0), str_repeat('x', 500));
+$entry_size -= apcu_sma_info(true)['avail_mem'];
+$i = 1;
+while (apcu_sma_info(true)['avail_mem'] >= $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;
-do {
-    $tmp_avail = apcu_sma_info(true)['avail_mem'];
-    apcu_store("key" . $i, str_repeat('x', 500));
-    $i++;
-} while (apcu_sma_info(true)['avail_mem'] < $tmp_avail);
+// 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_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)