mirror of
https://github.com/MariaDB/server.git
synced 2026-10-23 18:49:29 +03:00
MDEV-40033 Free-list contiguous block tracking for HEAP engine
Replace the HEAP engine's O(records) per-record free-list traversal with O(blocks) by grouping contiguous deleted records into logical blocks. Each block uses two sentinel records (block-start at the lowest address, block-end at the highest) to represent an arbitrarily large contiguous group. Interior "dark" records are implicitly covered by the block's address range and have their metadata bytes cleared by `hp_clear_dark_records()`. **Block metadata layout** (stored inline in deleted records): - Byte 8 (`HP_DEL_FLAG_OFFSET`): `del_flag` -- `HP_DEL_BLOCK_START` (2) on the first record, `HP_DEL_BLOCK_END` (1) on the last, 0 on dark records and singles - Bytes 9-10 (`HP_DEL_COUNT_OFFSET`): `uint16` block record count, stored only on the block-start record **`visible` minimum** bumped from `sizeof(char*)` (8) to `HP_DEL_METADATA_SIZE` (11) for all tables. This has zero memory impact: `recbuffer = ALIGN(visible + 1, 8)` absorbs the shift entirely (16 bytes for all small records). **Delete neighbor coalescing**: `hp_push_free_block_coalesce()` (in `hp_delete.c`) normalizes the free-list head by treating a single record as a block of count 1, then checks adjacency in two directions (above/below). Handles all merge cases uniformly: single-to-single, single-to-block, block-to-single, and block-to-block. Combined count capped at `UINT_MAX16`; falls back to `hp_push_free_record` / `hp_push_free_block` when no adjacency. `hp_push_free_record_coalesce()` is a thin inline wrapper. **Dark record clearing** is centralized in `hp_clear_dark_records`, a strided loop that zeroes only the essential metadata bytes per record (`del_link`, `del_flag`, `visible`) rather than the entire `recbuffer`. Used by both `hp_push_free_block` and `hp_push_free_block_coalesce`. **New API in `heapdef.h`:** - Push: `hp_push_free_record()`, `hp_push_free_block()`, `hp_push_free_record_coalesce()` - Consume: `hp_pop_free_record()`, `hp_take_free_block()` - Traverse: `hp_is_free_block_end()`, `hp_free_block_first()`, `hp_is_free_block_start()`, `hp_free_block_start_count()` Non-inline functions: `hp_push_free_block()` and `hp_push_free_block_coalesce()` in `hp_delete.c`; `hp_take_free_block()` in `hp_write.c`. **Caller updates:** - `hp_delete.c`: `heap_delete()` calls `hp_push_free_record_coalesce()` for single-record deletes - `hp_write.c`: error-path push and `next_free_record_pos()` pop use `hp_push_free_record()` / `hp_pop_free_record()` - `hp_blob.c`: `hp_free_run_chain()` uses coalescing versions; `hp_take_free_list_runs()` extracted from Steps 1 and 3 of `hp_write_one_blob()`, parameterized by `min_avail`; blob allocation uses `hp_take_free_block()` / `hp_pop_free_record()` - `hp_scan.c`: batch-skip entire deleted blocks - `_check.c`: block-aware free-list count and scan - `hp_shrink_tail()`: reclaims entire blocks at the tail **Unit tests** (`hp_test_freelist-t.c`): 13 new tests (252 assertions): block formation, pop/shrink, collapse to single, partial block take, scan batch-skip, shrink-tail with blocks, interleaved singles and blocks, ascending/descending delete coalescing, non-adjacent gap, coalesced block reuse by blob insert, block-to-block via adjacent blob chains.
This commit is contained in:
@@ -39,8 +39,8 @@ a b
|
||||
select * from t1 except all select * from t1 union all select * from t1 union all select * from t1 except all select * from t2;
|
||||
a b
|
||||
4 4
|
||||
2 2
|
||||
4 4
|
||||
2 2
|
||||
4 4
|
||||
4 4
|
||||
4 4
|
||||
|
||||
@@ -47,6 +47,7 @@ int heap_check_heap(const HP_INFO *info, my_bool print_status)
|
||||
ulong records=0, deleted=0, cont_count=0, pos, next_block;
|
||||
ulong del_link_count;
|
||||
uchar *del_ptr;
|
||||
my_bool block_count_error_printed= FALSE;
|
||||
HP_SHARE *share=info->s;
|
||||
uchar *current_ptr= info->current_ptr;
|
||||
DBUG_ENTER("heap_check_heap");
|
||||
@@ -71,13 +72,25 @@ int heap_check_heap(const HP_INFO *info, my_bool print_status)
|
||||
error= 1;
|
||||
}
|
||||
|
||||
/* Verify free list length matches share->deleted */
|
||||
/* Verify free list record count matches share->deleted */
|
||||
del_link_count= 0;
|
||||
for (del_ptr= share->del_link; del_ptr; del_ptr= *((uchar**) del_ptr))
|
||||
del_link_count++;
|
||||
for (del_ptr= share->del_link; del_ptr; )
|
||||
{
|
||||
if (hp_is_free_block_end(del_ptr))
|
||||
{
|
||||
uchar *first= hp_free_block_first(del_ptr);
|
||||
del_link_count+= hp_free_block_start_count(first);
|
||||
del_ptr= *((uchar**) first);
|
||||
}
|
||||
else
|
||||
{
|
||||
del_link_count++;
|
||||
del_ptr= *((uchar**) del_ptr);
|
||||
}
|
||||
}
|
||||
if (del_link_count != share->deleted)
|
||||
{
|
||||
DBUG_PRINT("error",("free list length %lu != share->deleted %lu",
|
||||
DBUG_PRINT("error",("free list record count %lu != share->deleted %lu",
|
||||
del_link_count, (ulong) share->deleted));
|
||||
error= 1;
|
||||
}
|
||||
@@ -105,7 +118,72 @@ int heap_check_heap(const HP_INFO *info, my_bool print_status)
|
||||
current_ptr= hp_find_block(&share->block, pos);
|
||||
|
||||
if (!current_ptr[share->visible])
|
||||
deleted++;
|
||||
{
|
||||
if (hp_is_free_block_start(current_ptr))
|
||||
{
|
||||
uint16 block_count= hp_free_block_start_count(current_ptr);
|
||||
uchar *block_last;
|
||||
|
||||
if (block_count < 2 ||
|
||||
pos + block_count > share->total_records + share->deleted)
|
||||
{
|
||||
if (!block_count_error_printed)
|
||||
{
|
||||
DBUG_PRINT("error",
|
||||
("Block at pos: %lu invalid count: %u "
|
||||
"(total_records+deleted=%lu)",
|
||||
pos, (uint) block_count,
|
||||
(ulong)(share->total_records + share->deleted)));
|
||||
block_count_error_printed= TRUE;
|
||||
}
|
||||
error= 1;
|
||||
deleted++;
|
||||
goto next_record;
|
||||
}
|
||||
|
||||
block_last= current_ptr +
|
||||
(uint32)(block_count - 1) * share->block.recbuffer;
|
||||
|
||||
if (!(block_last[HP_DEL_FLAG_OFFSET] & HP_DEL_BLOCK_END))
|
||||
{
|
||||
DBUG_PRINT("error",
|
||||
("Block at pos %lu: last record missing "
|
||||
"HP_DEL_BLOCK_END flag", pos));
|
||||
error= 1;
|
||||
}
|
||||
#ifndef DBUG_OFF
|
||||
{
|
||||
uchar *dark;
|
||||
for (dark= current_ptr + share->block.recbuffer;
|
||||
dark < block_last; dark+= share->block.recbuffer)
|
||||
{
|
||||
if (dark[HP_DEL_FLAG_OFFSET] != 0)
|
||||
{
|
||||
DBUG_PRINT("error",
|
||||
("Dark record at pos %lu+%u has non-zero del_flag",
|
||||
pos,
|
||||
(uint)((dark - current_ptr) /
|
||||
share->block.recbuffer)));
|
||||
error= 1;
|
||||
}
|
||||
if (dark[share->visible] != 0)
|
||||
{
|
||||
DBUG_PRINT("error",
|
||||
("Dark record at pos %lu+%u has non-zero visible",
|
||||
pos,
|
||||
(uint)((dark - current_ptr) /
|
||||
share->block.recbuffer)));
|
||||
error= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
deleted+= block_count;
|
||||
pos+= block_count - 1;
|
||||
}
|
||||
else
|
||||
deleted++;
|
||||
}
|
||||
else if (hp_is_cont(current_ptr, share->visible))
|
||||
{
|
||||
/*
|
||||
@@ -157,6 +235,7 @@ int heap_check_heap(const HP_INFO *info, my_bool print_status)
|
||||
}
|
||||
}
|
||||
}
|
||||
next_record: ;
|
||||
}
|
||||
|
||||
if (records != share->records || deleted != share->deleted)
|
||||
|
||||
@@ -46,7 +46,7 @@ C_MODE_START
|
||||
Continuation run header: next_cont pointer + run_rec_count.
|
||||
Stored at the beginning of the first blob segment in each run.
|
||||
*/
|
||||
#define HP_CONT_REC_COUNT_SIZE sizeof(uint16)
|
||||
#define HP_CONT_REC_COUNT_SIZE 2
|
||||
#define HP_CONT_HEADER_SIZE (sizeof(uchar*) + HP_CONT_REC_COUNT_SIZE)
|
||||
|
||||
/*
|
||||
@@ -93,12 +93,13 @@ static inline my_bool hp_is_multi_run(const uchar *rec, uint visible)
|
||||
/*
|
||||
Continuation run header accessors.
|
||||
Read next_cont pointer and run_rec_count from the first record of a run.
|
||||
In heap, all delete/chain link pointers in records are 8 byte aligned
|
||||
and thus safe to access directly.
|
||||
*/
|
||||
|
||||
static inline const uchar *hp_cont_next(const uchar *chain)
|
||||
{
|
||||
const uchar *next;
|
||||
memcpy(&next, chain, sizeof(uchar*));
|
||||
return next;
|
||||
return *((const uchar**) chain);
|
||||
}
|
||||
|
||||
static inline uint16 hp_cont_rec_count(const uchar *chain)
|
||||
@@ -151,6 +152,148 @@ static inline uint32 hp_blob_min_run_bytes(uint32 blob_length)
|
||||
return length;
|
||||
}
|
||||
|
||||
/*
|
||||
Delete-list block metadata.
|
||||
|
||||
Deleted records store a del_link pointer in bytes 0..sizeof(uchar*)-1
|
||||
and a flags byte at offset 'visible'. Block coalescing reuses bytes
|
||||
after del_link for block metadata:
|
||||
byte HP_DEL_FLAG_OFFSET (8): del_flag (block-start / block-end bits)
|
||||
bytes HP_DEL_COUNT_OFFSET (9-10): block record count (uint16, LE)
|
||||
|
||||
HP_DEL_METADATA_SIZE (11) is the minimum 'visible' offset needed to
|
||||
guarantee these bytes never overlap with the flags byte.
|
||||
*/
|
||||
#define HP_DEL_FLAG_OFFSET sizeof(uchar*)
|
||||
#define HP_DEL_BLOCK_END 1 /* bit 0: last record of block */
|
||||
#define HP_DEL_BLOCK_START 2 /* bit 1: first record of block */
|
||||
#define HP_DEL_COUNT_OFFSET (sizeof(uchar*) + 1)
|
||||
#define HP_DEL_METADATA_SIZE (sizeof(uchar*) + 1 + HP_CONT_REC_COUNT_SIZE)
|
||||
|
||||
/*
|
||||
Free-list block predicates and accessors.
|
||||
A contiguous group of N >= 2 deleted records is represented as a block:
|
||||
rec[0] (first): del_flag = HP_DEL_BLOCK_START, del_link = next entry,
|
||||
count at bytes 9-10
|
||||
rec[1..N-2] (dark): bytes 0-10 zeroed, pos[visible] = 0
|
||||
rec[N-1] (last): del_flag = HP_DEL_BLOCK_END, del_link = &rec[0]
|
||||
share->del_link = &rec[N-1]
|
||||
Single deleted records have del_flag = 0 (unchanged behavior).
|
||||
*/
|
||||
|
||||
static inline my_bool hp_is_free_block_end(const uchar *pos)
|
||||
{
|
||||
return (pos[HP_DEL_FLAG_OFFSET] & HP_DEL_BLOCK_END) != 0;
|
||||
}
|
||||
|
||||
static inline uchar *hp_free_block_first(uchar *pos)
|
||||
{
|
||||
return *((uchar**) pos);
|
||||
}
|
||||
|
||||
static inline my_bool hp_is_free_block_start(const uchar *pos)
|
||||
{
|
||||
return (pos[HP_DEL_FLAG_OFFSET] & HP_DEL_BLOCK_START) != 0;
|
||||
}
|
||||
|
||||
static inline uint16 hp_free_block_start_count(const uchar *pos)
|
||||
{
|
||||
return uint2korr(pos + HP_DEL_COUNT_OFFSET);
|
||||
}
|
||||
|
||||
/*
|
||||
Clear the metadata bytes of dark records (records between block-start
|
||||
and block-end that are not individually on the free list).
|
||||
Uses a strided loop so only the essential bytes per record are touched.
|
||||
For short record lengths a contiguous bzero over the full range would
|
||||
be faster, but the crossover point has not been measured.
|
||||
*/
|
||||
|
||||
static inline void hp_clear_dark_records(uchar *from, uchar *to,
|
||||
uint recbuffer, uint visible)
|
||||
{
|
||||
uchar *pos;
|
||||
for (pos= from; pos < to; pos+= recbuffer)
|
||||
{
|
||||
*((uchar**) pos)= NULL;
|
||||
pos[HP_DEL_FLAG_OFFSET]= 0;
|
||||
pos[visible]= 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Push a single deleted record onto the free list.
|
||||
Replaces the inlined 3-line pattern in heap_delete(), heap_write()
|
||||
error path, and hp_free_run_chain() for single-record runs.
|
||||
*/
|
||||
|
||||
static inline void hp_push_free_record(HP_SHARE *share, uchar *pos)
|
||||
{
|
||||
*((uchar**) pos)= share->del_link;
|
||||
pos[HP_DEL_FLAG_OFFSET]= 0;
|
||||
share->del_link= pos;
|
||||
pos[share->visible]= 0;
|
||||
share->deleted++;
|
||||
share->total_records--;
|
||||
}
|
||||
|
||||
/*
|
||||
Push a contiguous block of deleted records onto the free list.
|
||||
Defined in hp_delete.c.
|
||||
*/
|
||||
extern void hp_push_free_block(HP_SHARE *share, uchar *first, uint16 count);
|
||||
|
||||
/*
|
||||
Push a contiguous block (count >= 2) or single record (count == 1)
|
||||
onto the free list, coalescing with the head entry if adjacent.
|
||||
Defined in hp_delete.c.
|
||||
*/
|
||||
extern void hp_push_free_block_coalesce(HP_SHARE *share, uchar *first,
|
||||
uint16 count);
|
||||
|
||||
/*
|
||||
Push a deleted record onto the free list, coalescing with the head
|
||||
entry if adjacent. Delegates to hp_push_free_block_coalesce with
|
||||
count=1; both merge branches produce correct results for a single
|
||||
record (dark-record clearing ranges become empty no-ops where no
|
||||
dark records exist).
|
||||
*/
|
||||
|
||||
static inline void hp_push_free_record_coalesce(HP_SHARE *share, uchar *pos)
|
||||
{
|
||||
hp_push_free_block_coalesce(share, pos, 1);
|
||||
}
|
||||
|
||||
/*
|
||||
Take 'count' contiguous records from the head block.
|
||||
Defined in hp_write.c.
|
||||
*/
|
||||
extern uchar *hp_take_free_block(HP_SHARE *share, uint16 count);
|
||||
|
||||
/*
|
||||
Pop one record from the head of the free list.
|
||||
If the head is a block-end, takes the last record and shrinks
|
||||
or collapses the block.
|
||||
*/
|
||||
|
||||
static inline uchar *hp_pop_free_record(HP_SHARE *share)
|
||||
{
|
||||
uchar *pos= share->del_link;
|
||||
|
||||
DBUG_ASSERT(pos);
|
||||
|
||||
if (!hp_is_free_block_end(pos))
|
||||
{
|
||||
/* Single record */
|
||||
share->del_link= *((uchar**) pos);
|
||||
share->deleted--;
|
||||
share->total_records++;
|
||||
return pos;
|
||||
}
|
||||
|
||||
return hp_take_free_block(share, 1);
|
||||
}
|
||||
|
||||
/* Some extern variables */
|
||||
|
||||
extern LIST *heap_open_list,*heap_share_list;
|
||||
|
||||
@@ -82,11 +82,30 @@ void hp_shrink_tail(HP_SHARE *share)
|
||||
|
||||
while (share->del_link == tail_pos)
|
||||
{
|
||||
share->del_link= *((uchar**) share->del_link);
|
||||
share->deleted--;
|
||||
block->last_allocated--;
|
||||
tail_pos-= recbuffer;
|
||||
block_pos--;
|
||||
ulong reclaim_count;
|
||||
|
||||
if (hp_is_free_block_end(share->del_link))
|
||||
{
|
||||
/*
|
||||
Block entry at tail: reclaim the entire block at once.
|
||||
Blocks from hp_free_run_chain() are always within a single
|
||||
leaf (hp_alloc_from_tail caps at the leaf boundary), so the
|
||||
block cannot span leaf boundaries.
|
||||
*/
|
||||
uchar *first= hp_free_block_first(share->del_link);
|
||||
reclaim_count= hp_free_block_start_count(first);
|
||||
share->del_link= *((uchar**) first);
|
||||
}
|
||||
else
|
||||
{
|
||||
reclaim_count= 1;
|
||||
share->del_link= *((uchar**) share->del_link);
|
||||
}
|
||||
|
||||
block->last_allocated-= reclaim_count;
|
||||
tail_pos-= reclaim_count * recbuffer;
|
||||
block_pos-= reclaim_count;
|
||||
share->deleted-= reclaim_count;
|
||||
|
||||
/*
|
||||
When the current leaf block becomes empty (block_pos has reached 0),
|
||||
@@ -153,14 +172,12 @@ void hp_flush_pending_blob_free_impl(HP_INFO *info)
|
||||
|
||||
void hp_free_run_chain(HP_SHARE *share, uchar *chain)
|
||||
{
|
||||
uint recbuffer= share->block.recbuffer;
|
||||
uint visible= share->visible;
|
||||
|
||||
while (chain)
|
||||
{
|
||||
uchar *next_run;
|
||||
uint16 run_rec_count;
|
||||
uint16 j;
|
||||
|
||||
if (hp_is_single_rec(chain, visible))
|
||||
{
|
||||
@@ -171,19 +188,14 @@ void hp_free_run_chain(HP_SHARE *share, uchar *chain)
|
||||
else
|
||||
{
|
||||
/* Case B/C: header present with next_cont and run_rec_count */
|
||||
memcpy(&next_run, chain, sizeof(uchar*));
|
||||
run_rec_count= uint2korr(chain + sizeof(uchar*));
|
||||
next_run= (uchar*) hp_cont_next(chain);
|
||||
run_rec_count= hp_cont_rec_count(chain);
|
||||
}
|
||||
|
||||
for (j= 0; j < run_rec_count; j++)
|
||||
{
|
||||
uchar *pos= chain + j * recbuffer;
|
||||
*((uchar**) pos)= share->del_link;
|
||||
share->del_link= pos;
|
||||
pos[visible]= 0;
|
||||
share->deleted++;
|
||||
share->total_records--;
|
||||
}
|
||||
if (run_rec_count == 1)
|
||||
hp_push_free_record_coalesce(share, chain);
|
||||
else
|
||||
hp_push_free_block_coalesce(share, chain, run_rec_count);
|
||||
|
||||
chain= next_run;
|
||||
}
|
||||
@@ -286,42 +298,68 @@ static void hp_write_run_data(HP_SHARE *share, const uchar *data,
|
||||
|
||||
|
||||
/*
|
||||
Unlink a contiguous group from the delete list and write blob data into it.
|
||||
Does not support zerocopy (always uses HP_ROW_MULTIPLE_REC format).
|
||||
Take runs from the free list and write blob data into them.
|
||||
|
||||
Iterates the free list, accepting blocks with at least min_avail
|
||||
records and singles (avail=1). Stops when all data is written,
|
||||
the free list is exhausted, or a too-small entry is found.
|
||||
|
||||
@param share Table share
|
||||
@param data_ptr Blob data
|
||||
@param data_len Total blob data length
|
||||
@param run_start Lowest address of the contiguous group
|
||||
@param run_count Number of contiguous records in the group
|
||||
@param min_avail Minimum block size to accept (1 = accept anything)
|
||||
@param data_offset [in/out] Current offset into blob data
|
||||
@param first_run [out] Pointer to first run; undefined until return
|
||||
@param first_run [in/out] Pointer to first run
|
||||
@param prev_run_start [in/out] Pointer to previous run's start
|
||||
*/
|
||||
|
||||
static void hp_unlink_and_write_run(HP_SHARE *share, const uchar *data_ptr,
|
||||
uint32 data_len, uchar *run_start,
|
||||
uint16 run_count, uint32 *data_offset,
|
||||
uchar **first_run, uchar **prev_run_start)
|
||||
static void hp_take_free_list_runs(HP_SHARE *share, const uchar *data_ptr,
|
||||
uint32 data_len, uint16 min_avail,
|
||||
uint32 *data_offset,
|
||||
uchar **first_run, uchar **prev_run_start)
|
||||
{
|
||||
uint recbuffer= share->block.recbuffer;
|
||||
DBUG_ASSERT(share->del_link == run_start + (run_count-1) * recbuffer);
|
||||
DBUG_ASSERT(share->del_link >= run_start &&
|
||||
share->del_link < run_start + run_count * recbuffer);
|
||||
uint32 recbuffer= share->block.recbuffer;
|
||||
uint32 first_payload= share->visible - HP_CONT_HEADER_SIZE;
|
||||
|
||||
share->del_link= *(uchar**) (share->del_link -
|
||||
(run_count-1) * recbuffer);
|
||||
share->deleted-= run_count;
|
||||
share->total_records+= run_count;
|
||||
while (share->del_link && *data_offset < data_len)
|
||||
{
|
||||
uchar *pos= share->del_link;
|
||||
uchar *run_start;
|
||||
uint16 avail;
|
||||
uint16 take;
|
||||
uint32 remaining= data_len - *data_offset;
|
||||
uint32 remaining_records=
|
||||
(remaining <= first_payload ? 1 :
|
||||
1 + (remaining - first_payload + recbuffer - 1) / recbuffer);
|
||||
|
||||
hp_write_run_data(share, data_ptr, data_len, run_start,
|
||||
run_count, HP_ROW_MULTIPLE_REC, data_offset);
|
||||
if (hp_is_free_block_end(pos))
|
||||
avail= hp_free_block_start_count(hp_free_block_first(pos));
|
||||
else
|
||||
avail= 1;
|
||||
|
||||
if (*prev_run_start)
|
||||
memcpy(*prev_run_start, &run_start, sizeof(run_start));
|
||||
else
|
||||
*first_run= run_start;
|
||||
*prev_run_start= run_start;
|
||||
if (avail < min_avail)
|
||||
break;
|
||||
|
||||
take= (uint16) MY_MIN((uint32) avail,
|
||||
MY_MIN(remaining_records, (uint32) UINT_MAX16));
|
||||
|
||||
if (avail > 1)
|
||||
run_start= hp_take_free_block(share, take);
|
||||
else
|
||||
{
|
||||
DBUG_ASSERT(take == 1);
|
||||
run_start= hp_pop_free_record(share);
|
||||
}
|
||||
|
||||
hp_write_run_data(share, data_ptr, data_len, run_start,
|
||||
take, HP_ROW_MULTIPLE_REC, data_offset);
|
||||
|
||||
if (*prev_run_start)
|
||||
memcpy(*prev_run_start, &run_start, sizeof(run_start));
|
||||
else
|
||||
*first_run= run_start;
|
||||
*prev_run_start= run_start;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -387,75 +425,16 @@ int hp_write_one_blob(HP_SHARE *share, const uchar *data_ptr,
|
||||
/*
|
||||
Step 1: Try to allocate contiguous runs from the top of the delete list.
|
||||
|
||||
Peek at delete list records by walking next pointers without unlinking.
|
||||
Track contiguous groups (descending addresses - LIFO order from
|
||||
hp_free_run_chain). On discontinuity: if the group qualifies
|
||||
(>= min_run_records), unlink and use it; if it doesn't, the tail
|
||||
of the delete_link is too small. Instead of continue searching
|
||||
for a larger block, we stop searching.
|
||||
With block-coalesced free lists, each block entry encodes contiguity
|
||||
directly (count + boundaries). No per-record walk needed -- just
|
||||
inspect each free-list entry and take from it if large enough.
|
||||
|
||||
remaining_records is recomputed each iteration because each take
|
||||
creates a separate run with its own HP_CONT_HEADER_SIZE overhead,
|
||||
so the per-run payload is less than in a single-run estimate.
|
||||
*/
|
||||
{
|
||||
uchar *run_start;
|
||||
uint16 run_count= 1;
|
||||
uchar *pos;
|
||||
uint32 max_run= MY_MIN(total_records_needed, UINT_MAX16);
|
||||
|
||||
if ((run_start= share->del_link))
|
||||
{
|
||||
pos= *((uchar**) run_start);
|
||||
run_count= 1;
|
||||
for (; pos ; pos= *((uchar**) pos))
|
||||
{
|
||||
/*
|
||||
Only check descending direction: hp_free_run_chain() frees
|
||||
records in ascending address order (j=0..N), so LIFO pushes
|
||||
them onto the delete list in reverse - consecutive delete
|
||||
list entries have descending addresses. Ascending adjacency
|
||||
from unrelated deletes is ignored intentionally; we only
|
||||
recover runs that were freed together.
|
||||
*/
|
||||
if (run_count == total_records_needed)
|
||||
break; /* Use this run */
|
||||
|
||||
if (pos == run_start - recbuffer)
|
||||
{
|
||||
run_start= pos;
|
||||
run_count++;
|
||||
if (run_count < max_run)
|
||||
continue;
|
||||
if (run_count == total_records_needed)
|
||||
break; /* Use this run */
|
||||
/* run_count is now UINT_MAX16 */
|
||||
}
|
||||
|
||||
/*
|
||||
Discontinuity. If the accumulated group qualifies, use it.
|
||||
If not, the top of the delete list is fragmented - give up entirely.
|
||||
*/
|
||||
if (run_count < min_run_records)
|
||||
break;
|
||||
hp_unlink_and_write_run(share, data_ptr, data_len, run_start,
|
||||
run_count, &data_offset,
|
||||
&first_run, &prev_run_start);
|
||||
|
||||
pos= share->del_link;
|
||||
total_records_needed-= run_count;
|
||||
|
||||
/* This cannot be last run */
|
||||
DBUG_ASSERT(data_offset < data_len && pos);
|
||||
DBUG_ASSERT(total_records_needed != 0);
|
||||
|
||||
run_start= pos;
|
||||
run_count= 1;
|
||||
}
|
||||
|
||||
/* Handle the last group after the loop ends */
|
||||
if (run_count >= min_run_records && data_offset < data_len)
|
||||
hp_unlink_and_write_run(share, data_ptr, data_len, run_start,
|
||||
run_count, &data_offset,
|
||||
&first_run, &prev_run_start);
|
||||
}
|
||||
}
|
||||
hp_take_free_list_runs(share, data_ptr, data_len, min_run_records,
|
||||
&data_offset, &first_run, &prev_run_start);
|
||||
|
||||
/*
|
||||
Step 2: Allocate remaining data from the block tail.
|
||||
@@ -541,41 +520,10 @@ int hp_write_one_blob(HP_SHARE *share, const uchar *data_ptr,
|
||||
Step 3: Free-list scavenge fallback.
|
||||
|
||||
When the tail is full but there are deleted records on the free list,
|
||||
walk the entire free list accepting any contiguous group (even a
|
||||
single slot). This produces maximally fragmented chains (many short
|
||||
runs, Case C), which are slower to read but correct. Failing with
|
||||
table-full when free slots exist is worse than a fragmented chain.
|
||||
take whatever blocks or singles remain, accepting any size.
|
||||
*/
|
||||
while (data_offset < data_len && share->del_link)
|
||||
{
|
||||
uchar *run_start= share->del_link;
|
||||
uchar *pos= *((uchar**) run_start);
|
||||
uint16 run_count= 1;
|
||||
uint32 remaining= data_len - data_offset;
|
||||
uint32 remaining_records=
|
||||
(remaining <= first_payload ? 1 :
|
||||
1 + (remaining - first_payload + recbuffer - 1) / recbuffer);
|
||||
uint32 max_run= MY_MIN(remaining_records, UINT_MAX16);
|
||||
|
||||
/* Traverse the delete link list */
|
||||
for (; pos; pos= *((uchar**) pos))
|
||||
{
|
||||
if (run_count >= max_run)
|
||||
break;
|
||||
if (pos == run_start - recbuffer)
|
||||
{
|
||||
/* Found block directly before current block. Combine them */
|
||||
run_start= pos;
|
||||
run_count++;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
hp_unlink_and_write_run(share, data_ptr, data_len, run_start,
|
||||
run_count, &data_offset,
|
||||
&first_run, &prev_run_start);
|
||||
}
|
||||
hp_take_free_list_runs(share, data_ptr, data_len, 1,
|
||||
&data_offset, &first_run, &prev_run_start);
|
||||
|
||||
if (data_offset < data_len)
|
||||
{
|
||||
@@ -686,7 +634,6 @@ static void hp_reassemble_chain(const uchar *chain, uint32 data_len,
|
||||
uint32 remaining= data_len;
|
||||
while (chain && remaining > 0)
|
||||
{
|
||||
uint16 rec;
|
||||
uint16 run_rec_count;
|
||||
uint32 chunk;
|
||||
const uchar *next_cont;
|
||||
@@ -703,16 +650,19 @@ static void hp_reassemble_chain(const uchar *chain, uint32 data_len,
|
||||
remaining-= chunk;
|
||||
|
||||
/* Inner records: recbuffer stride, no flags byte */
|
||||
for (rec= 1; rec < run_rec_count; rec++)
|
||||
{
|
||||
const uchar *rec_ptr= chain + rec * recbuffer;
|
||||
DBUG_ASSERT(remaining != 0);
|
||||
chunk= recbuffer;
|
||||
if (chunk > remaining)
|
||||
chunk= remaining;
|
||||
memcpy(out, rec_ptr, chunk);
|
||||
out+= chunk;
|
||||
remaining-= chunk;
|
||||
const uchar *rec_end= chain + (uint32) run_rec_count * recbuffer;
|
||||
const uchar *rec_ptr;
|
||||
for (rec_ptr= chain + recbuffer; rec_ptr < rec_end; rec_ptr+= recbuffer)
|
||||
{
|
||||
DBUG_ASSERT(remaining != 0);
|
||||
chunk= recbuffer;
|
||||
if (chunk > remaining)
|
||||
chunk= remaining;
|
||||
memcpy(out, rec_ptr, chunk);
|
||||
out+= chunk;
|
||||
remaining-= chunk;
|
||||
}
|
||||
}
|
||||
|
||||
chain= next_cont;
|
||||
|
||||
@@ -71,20 +71,13 @@ int heap_create(const char *name, HP_CREATE_INFO *create_info,
|
||||
DBUG_PRINT("info",("Initializing new table"));
|
||||
|
||||
/*
|
||||
We have to store sometimes uchar* del_link in records,
|
||||
so the visible_offset must be least at sizeof(uchar*)
|
||||
Deleted records store del_link (sizeof(uchar*) bytes), a block
|
||||
flags byte, and a uint16 block count. visible_offset must be
|
||||
at least HP_DEL_METADATA_SIZE so that these fields never overlap
|
||||
the flags byte at offset 'visible'. This also satisfies the
|
||||
blob continuation header requirement (HP_CONT_HEADER_SIZE + 1).
|
||||
*/
|
||||
visible_offset= MY_MAX(reclength, sizeof (char*));
|
||||
/*
|
||||
Blob tables store continuation run headers (next_cont pointer +
|
||||
run_slots count = HP_CONT_HEADER_SIZE bytes) in each run's first
|
||||
slot. Ensure at least 1 byte of payload beyond the header,
|
||||
otherwise hp_write_run_data() underflows computing
|
||||
chunk = visible - HP_CONT_HEADER_SIZE. Only matters for
|
||||
pathological single-TINYBLOB tables (reclength as low as 9).
|
||||
*/
|
||||
if (create_info->blob_count)
|
||||
visible_offset= MY_MAX(visible_offset, HP_CONT_HEADER_SIZE + 1);
|
||||
visible_offset= MY_MAX(reclength, HP_DEL_METADATA_SIZE);
|
||||
|
||||
for (i= key_segs= max_length= 0, keyinfo= keydef; i < keys; i++, keyinfo++)
|
||||
{
|
||||
@@ -337,10 +330,16 @@ static int keys_compare(void *heap_rb_, const void *key1_,
|
||||
|
||||
size_t hp_memory_needed_per_row(size_t reclength)
|
||||
{
|
||||
/* Data needed for storing record + pointer to records */
|
||||
reclength= MY_MAX(reclength, sizeof(char*));
|
||||
/* The + 1 below is for the delete marker at the end of record*/
|
||||
reclength= MY_ALIGN(reclength+1, sizeof(char*));
|
||||
/*
|
||||
Must accommodate del_link + del_flag + block count.
|
||||
The + 1 below is for the required visibility byte at the end of each record.
|
||||
*/
|
||||
reclength= MY_MAX(reclength + 1, HP_DEL_METADATA_SIZE);
|
||||
/*
|
||||
Record has to be aligned for faster memcpy and also for allowing
|
||||
direct access to delete link/chain at start of record.
|
||||
*/
|
||||
reclength= MY_ALIGN(reclength, sizeof(char*));
|
||||
return reclength;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,127 @@
|
||||
|
||||
#include "heapdef.h"
|
||||
|
||||
/*
|
||||
Push a contiguous block of deleted records onto the free list.
|
||||
'first' is the lowest address; the block spans count records
|
||||
at stride share->block.recbuffer.
|
||||
*/
|
||||
|
||||
void hp_push_free_block(HP_SHARE *share, uchar *first, uint16 count)
|
||||
{
|
||||
uint recbuffer= share->block.recbuffer;
|
||||
uint visible= share->visible;
|
||||
uchar *last= first + (uint32)(count - 1) * recbuffer;
|
||||
|
||||
DBUG_ASSERT(count >= 2);
|
||||
|
||||
/* First record: block-start with chain to next free-list entry */
|
||||
*((uchar**) first)= share->del_link;
|
||||
first[HP_DEL_FLAG_OFFSET]= HP_DEL_BLOCK_START;
|
||||
int2store(first + HP_DEL_COUNT_OFFSET, count);
|
||||
first[visible]= 0;
|
||||
|
||||
/* Dark records: clear metadata bytes at stride recbuffer */
|
||||
hp_clear_dark_records(first + recbuffer, last, recbuffer, visible);
|
||||
|
||||
/* Last record: block-end with back-pointer to first */
|
||||
*((uchar**) last)= first;
|
||||
last[HP_DEL_FLAG_OFFSET]= HP_DEL_BLOCK_END;
|
||||
last[visible]= 0;
|
||||
|
||||
share->del_link= last;
|
||||
share->deleted+= count;
|
||||
share->total_records-= count;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Push a contiguous block (count >= 2) or single record (count == 1)
|
||||
onto the free list, coalescing with the head entry if adjacent.
|
||||
Normalizes the head by treating a single record as a block of count 1,
|
||||
then checks adjacency in two directions (above/below).
|
||||
Falls back to hp_push_free_block/hp_push_free_record when no adjacency.
|
||||
*/
|
||||
|
||||
void hp_push_free_block_coalesce(HP_SHARE *share, uchar *first,
|
||||
uint16 count)
|
||||
{
|
||||
uchar *head= share->del_link;
|
||||
uint recbuffer= share->block.recbuffer;
|
||||
uchar *last= first + (uint32)(count - 1) * recbuffer;
|
||||
|
||||
DBUG_ASSERT(count >= 1);
|
||||
|
||||
if (head)
|
||||
{
|
||||
/* Normalize head: treat single record as a block of count 1 */
|
||||
uchar *head_first;
|
||||
uint16 head_count;
|
||||
uint32 combined;
|
||||
|
||||
if (hp_is_free_block_end(head))
|
||||
{
|
||||
head_first= hp_free_block_first(head);
|
||||
head_count= hp_free_block_start_count(head_first);
|
||||
}
|
||||
else
|
||||
{
|
||||
head_first= head;
|
||||
head_count= 1;
|
||||
}
|
||||
combined= (uint32) head_count + count;
|
||||
|
||||
if (combined <= UINT_MAX16)
|
||||
{
|
||||
if (head == first - recbuffer)
|
||||
{
|
||||
/* Head immediately below new block: extend upward */
|
||||
hp_clear_dark_records((head_count > 1 ? head : first), last,
|
||||
recbuffer, share->visible);
|
||||
|
||||
*((uchar**) last)= head_first;
|
||||
last[HP_DEL_FLAG_OFFSET]= HP_DEL_BLOCK_END;
|
||||
last[share->visible]= 0;
|
||||
|
||||
head_first[HP_DEL_FLAG_OFFSET]= HP_DEL_BLOCK_START;
|
||||
int2store(head_first + HP_DEL_COUNT_OFFSET, (uint16) combined);
|
||||
|
||||
share->del_link= last;
|
||||
share->deleted+= count;
|
||||
share->total_records-= count;
|
||||
return;
|
||||
}
|
||||
|
||||
if (head_first == last + recbuffer)
|
||||
{
|
||||
/* Head immediately above new block: extend downward */
|
||||
uchar *next_entry= *((uchar**) head_first);
|
||||
|
||||
hp_clear_dark_records(first + recbuffer, head_first + recbuffer,
|
||||
recbuffer, share->visible);
|
||||
|
||||
*((uchar**) first)= next_entry;
|
||||
first[HP_DEL_FLAG_OFFSET]= HP_DEL_BLOCK_START;
|
||||
int2store(first + HP_DEL_COUNT_OFFSET, (uint16) combined);
|
||||
first[share->visible]= 0;
|
||||
|
||||
*((uchar**) head)= first;
|
||||
head[HP_DEL_FLAG_OFFSET]= HP_DEL_BLOCK_END;
|
||||
|
||||
share->deleted+= count;
|
||||
share->total_records-= count;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 1)
|
||||
hp_push_free_record(share, first);
|
||||
else
|
||||
hp_push_free_block(share, first, count);
|
||||
}
|
||||
|
||||
|
||||
int heap_delete(HP_INFO *info, const uchar *record)
|
||||
{
|
||||
uchar *pos;
|
||||
@@ -83,11 +204,7 @@ int heap_delete(HP_INFO *info, const uchar *record)
|
||||
}
|
||||
}
|
||||
info->update=HA_STATE_DELETED;
|
||||
*((uchar**) pos)=share->del_link;
|
||||
share->del_link=pos;
|
||||
pos[share->visible]=0; /* Record deleted */
|
||||
share->deleted++;
|
||||
share->total_records--;
|
||||
hp_push_free_record_coalesce(share, pos);
|
||||
share->key_version++;
|
||||
#if !defined(DBUG_OFF) && defined(EXTRA_HEAP_DEBUG)
|
||||
DBUG_EXECUTE("check_heap",heap_check_heap(info, 0););
|
||||
|
||||
@@ -93,6 +93,16 @@ retry:
|
||||
}
|
||||
if (!info->current_ptr[share->visible])
|
||||
{
|
||||
if (hp_is_free_block_start(info->current_ptr))
|
||||
{
|
||||
uint16 block_count= hp_free_block_start_count(info->current_ptr);
|
||||
uint skip;
|
||||
DBUG_ASSERT(block_count > 1);
|
||||
skip= block_count - 1;
|
||||
info->current_record+= skip;
|
||||
info->current_ptr+= skip * share->block.recbuffer;
|
||||
goto retry;
|
||||
}
|
||||
DBUG_PRINT("warning",("Found deleted record"));
|
||||
info->update= HA_STATE_PREV_FOUND | HA_STATE_NEXT_FOUND;
|
||||
DBUG_RETURN(my_errno=HA_ERR_RECORD_DELETED);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
||||
bytes 5-6: blob packlength=2 (length, little-endian)
|
||||
bytes 7-14: blob data pointer (portable_sizeof_char_ptr = 8 bytes always)
|
||||
reclength = 15
|
||||
visible_offset = MAX(15, 8) = 15
|
||||
visible_offset = MAX(15, HP_DEL_METADATA_SIZE=11) = 15
|
||||
recbuffer = ALIGN(15 + 1, 8) = 16
|
||||
|
||||
The pointer slot is always portable_sizeof_char_ptr (8) bytes in the record
|
||||
|
||||
@@ -114,11 +114,7 @@ err_delete_written_keys:
|
||||
and hp_write_blobs(). The slot at pos still contains stale data from the
|
||||
delete list, so hp_free_blobs would chase garbage chain pointers.
|
||||
*/
|
||||
share->deleted++;
|
||||
share->total_records--;
|
||||
*((uchar**) pos)=share->del_link;
|
||||
share->del_link=pos;
|
||||
pos[share->visible]= 0; /* Record deleted */
|
||||
hp_push_free_record(share, pos);
|
||||
|
||||
DBUG_RETURN(my_errno);
|
||||
} /* heap_write */
|
||||
@@ -232,6 +228,57 @@ uchar *hp_alloc_from_tail(HP_SHARE *info, uint *blocks)
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Take 'count' contiguous records from the head block.
|
||||
The head must be a block-end with at least 'count' records.
|
||||
Returns the lowest address of the taken range.
|
||||
*/
|
||||
|
||||
uchar *hp_take_free_block(HP_SHARE *share, uint16 count)
|
||||
{
|
||||
uchar *pos= share->del_link;
|
||||
uchar *first;
|
||||
uint recbuffer= share->block.recbuffer;
|
||||
uint16 block_count, remaining;
|
||||
uchar *taken_start;
|
||||
|
||||
DBUG_ASSERT(pos && hp_is_free_block_end(pos));
|
||||
|
||||
first= hp_free_block_first(pos);
|
||||
block_count= hp_free_block_start_count(first);
|
||||
|
||||
DBUG_ASSERT(count >= 1 && count <= block_count);
|
||||
|
||||
remaining= block_count - count;
|
||||
taken_start= first + (uint32) remaining * recbuffer;
|
||||
|
||||
if (remaining == 0)
|
||||
{
|
||||
/* Block fully consumed */
|
||||
share->del_link= *((uchar**) first);
|
||||
}
|
||||
else if (remaining == 1)
|
||||
{
|
||||
/* Collapse to single record; first's del_link already has next entry */
|
||||
first[HP_DEL_FLAG_OFFSET]= 0;
|
||||
share->del_link= first;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Shrink block */
|
||||
uchar *new_last= taken_start - recbuffer;
|
||||
*((uchar**) new_last)= first;
|
||||
new_last[HP_DEL_FLAG_OFFSET]= HP_DEL_BLOCK_END;
|
||||
int2store(first + HP_DEL_COUNT_OFFSET, remaining);
|
||||
share->del_link= new_last;
|
||||
}
|
||||
|
||||
share->deleted-= count;
|
||||
share->total_records+= count;
|
||||
return taken_start;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Find where to place a new record.
|
||||
|
||||
@@ -250,10 +297,7 @@ uchar *next_free_record_pos(HP_SHARE *info)
|
||||
|
||||
if (info->del_link)
|
||||
{
|
||||
pos=info->del_link;
|
||||
info->del_link= *((uchar**) pos);
|
||||
info->deleted--;
|
||||
info->total_records++;
|
||||
pos= hp_pop_free_record(info);
|
||||
DBUG_PRINT("exit",("Used old position: %p", pos));
|
||||
DBUG_RETURN(pos);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user