1
0
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:
Arcadiy Ivanov
2026-06-15 10:38:21 -04:00
committed by Monty
parent ce0bc07763
commit 6d9f06c01b
10 changed files with 1577 additions and 197 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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););

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}