/src/postgres/src/backend/access/transam/generic_xlog.c
Line | Count | Source |
1 | | /*------------------------------------------------------------------------- |
2 | | * |
3 | | * generic_xlog.c |
4 | | * Implementation of generic xlog records. |
5 | | * |
6 | | * |
7 | | * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group |
8 | | * Portions Copyright (c) 1994, Regents of the University of California |
9 | | * |
10 | | * src/backend/access/transam/generic_xlog.c |
11 | | * |
12 | | *------------------------------------------------------------------------- |
13 | | */ |
14 | | #include "postgres.h" |
15 | | |
16 | | #include "access/bufmask.h" |
17 | | #include "access/generic_xlog.h" |
18 | | #include "access/xlogutils.h" |
19 | | #include "miscadmin.h" |
20 | | |
21 | | /*------------------------------------------------------------------------- |
22 | | * Internally, a delta between pages consists of a set of fragments. Each |
23 | | * fragment represents changes made in a given region of a page. A fragment |
24 | | * is made up as follows: |
25 | | * |
26 | | * - offset of page region (OffsetNumber) |
27 | | * - length of page region (OffsetNumber) |
28 | | * - data - the data to place into the region ('length' number of bytes) |
29 | | * |
30 | | * Unchanged regions of a page are not represented in its delta. As a result, |
31 | | * a delta can be more compact than the full page image. But having an |
32 | | * unchanged region between two fragments that is smaller than the fragment |
33 | | * header (offset+length) does not pay off in terms of the overall size of |
34 | | * the delta. For this reason, we merge adjacent fragments if the unchanged |
35 | | * region between them is <= MATCH_THRESHOLD bytes. |
36 | | * |
37 | | * We do not bother to merge fragments across the "lower" and "upper" parts |
38 | | * of a page; it's very seldom the case that pd_lower and pd_upper are within |
39 | | * MATCH_THRESHOLD bytes of each other, and handling that infrequent case |
40 | | * would complicate and slow down the delta-computation code unduly. |
41 | | * Therefore, the worst-case delta size includes two fragment headers plus |
42 | | * a full page's worth of data. |
43 | | *------------------------------------------------------------------------- |
44 | | */ |
45 | 0 | #define FRAGMENT_HEADER_SIZE (2 * sizeof(OffsetNumber)) |
46 | 0 | #define MATCH_THRESHOLD FRAGMENT_HEADER_SIZE |
47 | | #define MAX_DELTA_SIZE (BLCKSZ + 2 * FRAGMENT_HEADER_SIZE) |
48 | | |
49 | | /* Struct of generic xlog data for single page */ |
50 | | typedef struct |
51 | | { |
52 | | Buffer buffer; /* registered buffer */ |
53 | | int flags; /* flags for this buffer */ |
54 | | int deltaLen; /* space consumed in delta field */ |
55 | | char *image; /* copy of page image for modification, do not |
56 | | * do it in-place to have aligned memory chunk */ |
57 | | char delta[MAX_DELTA_SIZE]; /* delta between page images */ |
58 | | } GenericXLogPageData; |
59 | | |
60 | | /* |
61 | | * State of generic xlog record construction. Must be allocated at an I/O |
62 | | * aligned address. |
63 | | */ |
64 | | struct GenericXLogState |
65 | | { |
66 | | /* Page images (properly aligned, must be first) */ |
67 | | PGIOAlignedBlock images[MAX_GENERIC_XLOG_PAGES]; |
68 | | /* Info about each page, see above */ |
69 | | GenericXLogPageData pages[MAX_GENERIC_XLOG_PAGES]; |
70 | | bool isLogged; |
71 | | }; |
72 | | |
73 | | static void writeFragment(GenericXLogPageData *pageData, OffsetNumber offset, |
74 | | OffsetNumber length, const char *data); |
75 | | static void computeRegionDelta(GenericXLogPageData *pageData, |
76 | | const char *curpage, const char *targetpage, |
77 | | int targetStart, int targetEnd, |
78 | | int validStart, int validEnd); |
79 | | static void computeDelta(GenericXLogPageData *pageData, Page curpage, Page targetpage); |
80 | | static void applyPageRedo(Page page, const char *delta, Size deltaSize); |
81 | | |
82 | | |
83 | | /* |
84 | | * Write next fragment into pageData's delta. |
85 | | * |
86 | | * The fragment has the given offset and length, and data points to the |
87 | | * actual data (of length length). |
88 | | */ |
89 | | static void |
90 | | writeFragment(GenericXLogPageData *pageData, OffsetNumber offset, OffsetNumber length, |
91 | | const char *data) |
92 | 0 | { |
93 | 0 | char *ptr = pageData->delta + pageData->deltaLen; |
94 | | |
95 | | /* Verify we have enough space */ |
96 | 0 | Assert(pageData->deltaLen + sizeof(offset) + |
97 | 0 | sizeof(length) + length <= sizeof(pageData->delta)); |
98 | | |
99 | | /* Write fragment data */ |
100 | 0 | memcpy(ptr, &offset, sizeof(offset)); |
101 | 0 | ptr += sizeof(offset); |
102 | 0 | memcpy(ptr, &length, sizeof(length)); |
103 | 0 | ptr += sizeof(length); |
104 | 0 | memcpy(ptr, data, length); |
105 | 0 | ptr += length; |
106 | |
|
107 | 0 | pageData->deltaLen = ptr - pageData->delta; |
108 | 0 | } |
109 | | |
110 | | /* |
111 | | * Compute the XLOG fragments needed to transform a region of curpage into the |
112 | | * corresponding region of targetpage, and append them to pageData's delta |
113 | | * field. The region to transform runs from targetStart to targetEnd-1. |
114 | | * Bytes in curpage outside the range validStart to validEnd-1 should be |
115 | | * considered invalid, and always overwritten with target data. |
116 | | * |
117 | | * This function is a hot spot, so it's worth being as tense as possible |
118 | | * about the data-matching loops. |
119 | | */ |
120 | | static void |
121 | | computeRegionDelta(GenericXLogPageData *pageData, |
122 | | const char *curpage, const char *targetpage, |
123 | | int targetStart, int targetEnd, |
124 | | int validStart, int validEnd) |
125 | 0 | { |
126 | 0 | int i, |
127 | 0 | loopEnd, |
128 | 0 | fragmentBegin = -1, |
129 | 0 | fragmentEnd = -1; |
130 | | |
131 | | /* Deal with any invalid start region by including it in first fragment */ |
132 | 0 | if (validStart > targetStart) |
133 | 0 | { |
134 | 0 | fragmentBegin = targetStart; |
135 | 0 | targetStart = validStart; |
136 | 0 | } |
137 | | |
138 | | /* We'll deal with any invalid end region after the main loop */ |
139 | 0 | loopEnd = Min(targetEnd, validEnd); |
140 | | |
141 | | /* Examine all the potentially matchable bytes */ |
142 | 0 | i = targetStart; |
143 | 0 | while (i < loopEnd) |
144 | 0 | { |
145 | 0 | if (curpage[i] != targetpage[i]) |
146 | 0 | { |
147 | | /* On unmatched byte, start new fragment if not already in one */ |
148 | 0 | if (fragmentBegin < 0) |
149 | 0 | fragmentBegin = i; |
150 | | /* Mark unmatched-data endpoint as uncertain */ |
151 | 0 | fragmentEnd = -1; |
152 | | /* Extend the fragment as far as possible in a tight loop */ |
153 | 0 | i++; |
154 | 0 | while (i < loopEnd && curpage[i] != targetpage[i]) |
155 | 0 | i++; |
156 | 0 | if (i >= loopEnd) |
157 | 0 | break; |
158 | 0 | } |
159 | | |
160 | | /* Found a matched byte, so remember end of unmatched fragment */ |
161 | 0 | fragmentEnd = i; |
162 | | |
163 | | /* |
164 | | * Extend the match as far as possible in a tight loop. (On typical |
165 | | * workloads, this inner loop is the bulk of this function's runtime.) |
166 | | */ |
167 | 0 | i++; |
168 | 0 | while (i < loopEnd && curpage[i] == targetpage[i]) |
169 | 0 | i++; |
170 | | |
171 | | /* |
172 | | * There are several possible cases at this point: |
173 | | * |
174 | | * 1. We have no unwritten fragment (fragmentBegin < 0). There's |
175 | | * nothing to write; and it doesn't matter what fragmentEnd is. |
176 | | * |
177 | | * 2. We found more than MATCH_THRESHOLD consecutive matching bytes. |
178 | | * Dump out the unwritten fragment, stopping at fragmentEnd. |
179 | | * |
180 | | * 3. The match extends to loopEnd. We'll do nothing here, exit the |
181 | | * loop, and then dump the unwritten fragment, after merging it with |
182 | | * the invalid end region if any. If we don't so merge, fragmentEnd |
183 | | * establishes how much the final writeFragment call needs to write. |
184 | | * |
185 | | * 4. We found an unmatched byte before loopEnd. The loop will repeat |
186 | | * and will enter the unmatched-byte stanza above. So in this case |
187 | | * also, it doesn't matter what fragmentEnd is. The matched bytes |
188 | | * will get merged into the continuing unmatched fragment. |
189 | | * |
190 | | * Only in case 3 do we reach the bottom of the loop with a meaningful |
191 | | * fragmentEnd value, which is why it's OK that we unconditionally |
192 | | * assign "fragmentEnd = i" above. |
193 | | */ |
194 | 0 | if (fragmentBegin >= 0 && i - fragmentEnd > MATCH_THRESHOLD) |
195 | 0 | { |
196 | 0 | writeFragment(pageData, fragmentBegin, |
197 | 0 | fragmentEnd - fragmentBegin, |
198 | 0 | targetpage + fragmentBegin); |
199 | 0 | fragmentBegin = -1; |
200 | 0 | fragmentEnd = -1; /* not really necessary */ |
201 | 0 | } |
202 | 0 | } |
203 | | |
204 | | /* Deal with any invalid end region by including it in final fragment */ |
205 | 0 | if (loopEnd < targetEnd) |
206 | 0 | { |
207 | 0 | if (fragmentBegin < 0) |
208 | 0 | fragmentBegin = loopEnd; |
209 | 0 | fragmentEnd = targetEnd; |
210 | 0 | } |
211 | | |
212 | | /* Write final fragment if any */ |
213 | 0 | if (fragmentBegin >= 0) |
214 | 0 | { |
215 | 0 | if (fragmentEnd < 0) |
216 | 0 | fragmentEnd = targetEnd; |
217 | 0 | writeFragment(pageData, fragmentBegin, |
218 | 0 | fragmentEnd - fragmentBegin, |
219 | 0 | targetpage + fragmentBegin); |
220 | 0 | } |
221 | 0 | } |
222 | | |
223 | | /* |
224 | | * Compute the XLOG delta record needed to transform curpage into targetpage, |
225 | | * and store it in pageData's delta field. |
226 | | */ |
227 | | static void |
228 | | computeDelta(GenericXLogPageData *pageData, Page curpage, Page targetpage) |
229 | 0 | { |
230 | 0 | int targetLower = ((PageHeader) targetpage)->pd_lower, |
231 | 0 | targetUpper = ((PageHeader) targetpage)->pd_upper, |
232 | 0 | curLower = ((PageHeader) curpage)->pd_lower, |
233 | 0 | curUpper = ((PageHeader) curpage)->pd_upper; |
234 | |
|
235 | 0 | pageData->deltaLen = 0; |
236 | | |
237 | | /* Compute delta records for lower part of page ... */ |
238 | 0 | computeRegionDelta(pageData, curpage, targetpage, |
239 | 0 | 0, targetLower, |
240 | 0 | 0, curLower); |
241 | | /* ... and for upper part, ignoring what's between */ |
242 | 0 | computeRegionDelta(pageData, curpage, targetpage, |
243 | 0 | targetUpper, BLCKSZ, |
244 | 0 | curUpper, BLCKSZ); |
245 | | |
246 | | /* |
247 | | * If xlog debug is enabled, then check produced delta. Result of delta |
248 | | * application to curpage should be equivalent to targetpage. |
249 | | */ |
250 | | #ifdef WAL_DEBUG |
251 | | if (XLOG_DEBUG) |
252 | | { |
253 | | PGAlignedBlock tmp; |
254 | | |
255 | | memcpy(tmp.data, curpage, BLCKSZ); |
256 | | applyPageRedo(tmp.data, pageData->delta, pageData->deltaLen); |
257 | | if (memcmp(tmp.data, targetpage, targetLower) != 0 || |
258 | | memcmp(tmp.data + targetUpper, targetpage + targetUpper, |
259 | | BLCKSZ - targetUpper) != 0) |
260 | | elog(ERROR, "result of generic xlog apply does not match"); |
261 | | } |
262 | | #endif |
263 | 0 | } |
264 | | |
265 | | /* |
266 | | * Start new generic xlog record for modifications to specified relation. |
267 | | */ |
268 | | GenericXLogState * |
269 | | GenericXLogStart(Relation relation) |
270 | 0 | { |
271 | 0 | GenericXLogState *state; |
272 | 0 | int i; |
273 | |
|
274 | 0 | state = (GenericXLogState *) palloc_aligned(sizeof(GenericXLogState), |
275 | 0 | PG_IO_ALIGN_SIZE, |
276 | 0 | 0); |
277 | 0 | state->isLogged = RelationNeedsWAL(relation); |
278 | |
|
279 | 0 | for (i = 0; i < MAX_GENERIC_XLOG_PAGES; i++) |
280 | 0 | { |
281 | 0 | state->pages[i].image = state->images[i].data; |
282 | 0 | state->pages[i].buffer = InvalidBuffer; |
283 | 0 | } |
284 | |
|
285 | 0 | return state; |
286 | 0 | } |
287 | | |
288 | | /* |
289 | | * Register new buffer for generic xlog record. |
290 | | * |
291 | | * Returns pointer to the page's image in the GenericXLogState, which |
292 | | * is what the caller should modify. |
293 | | * |
294 | | * If the buffer is already registered, just return its existing entry. |
295 | | * (It's not very clear what to do with the flags in such a case, but |
296 | | * for now we stay with the original flags.) |
297 | | */ |
298 | | Page |
299 | | GenericXLogRegisterBuffer(GenericXLogState *state, Buffer buffer, int flags) |
300 | 0 | { |
301 | 0 | int block_id; |
302 | | |
303 | | /* Search array for existing entry or first unused slot */ |
304 | 0 | for (block_id = 0; block_id < MAX_GENERIC_XLOG_PAGES; block_id++) |
305 | 0 | { |
306 | 0 | GenericXLogPageData *page = &state->pages[block_id]; |
307 | |
|
308 | 0 | if (BufferIsInvalid(page->buffer)) |
309 | 0 | { |
310 | | /* Empty slot, so use it (there cannot be a match later) */ |
311 | 0 | page->buffer = buffer; |
312 | 0 | page->flags = flags; |
313 | 0 | memcpy(page->image, BufferGetPage(buffer), BLCKSZ); |
314 | 0 | return (Page) page->image; |
315 | 0 | } |
316 | 0 | else if (page->buffer == buffer) |
317 | 0 | { |
318 | | /* |
319 | | * Buffer is already registered. Just return the image, which is |
320 | | * already prepared. |
321 | | */ |
322 | 0 | return (Page) page->image; |
323 | 0 | } |
324 | 0 | } |
325 | | |
326 | 0 | elog(ERROR, "maximum number %d of generic xlog buffers is exceeded", |
327 | 0 | MAX_GENERIC_XLOG_PAGES); |
328 | | /* keep compiler quiet */ |
329 | 0 | return NULL; |
330 | 0 | } |
331 | | |
332 | | /* |
333 | | * Apply changes represented by GenericXLogState to the actual buffers, |
334 | | * and emit a generic xlog record. |
335 | | */ |
336 | | XLogRecPtr |
337 | | GenericXLogFinish(GenericXLogState *state) |
338 | 0 | { |
339 | 0 | XLogRecPtr lsn; |
340 | 0 | int i; |
341 | |
|
342 | 0 | if (state->isLogged) |
343 | 0 | { |
344 | | /* Logged relation: make xlog record in critical section. */ |
345 | 0 | XLogBeginInsert(); |
346 | |
|
347 | 0 | START_CRIT_SECTION(); |
348 | | |
349 | | /* |
350 | | * Compute deltas if necessary, write changes to buffers, mark buffers |
351 | | * dirty, and register changes. |
352 | | */ |
353 | 0 | for (i = 0; i < MAX_GENERIC_XLOG_PAGES; i++) |
354 | 0 | { |
355 | 0 | GenericXLogPageData *pageData = &state->pages[i]; |
356 | 0 | Page page; |
357 | 0 | PageHeader pageHeader; |
358 | |
|
359 | 0 | if (BufferIsInvalid(pageData->buffer)) |
360 | 0 | continue; |
361 | | |
362 | 0 | page = BufferGetPage(pageData->buffer); |
363 | 0 | pageHeader = (PageHeader) pageData->image; |
364 | | |
365 | | /* |
366 | | * Compute delta while we still have both the unmodified page and |
367 | | * the new image. Not needed if we are logging the full image. |
368 | | */ |
369 | 0 | if (!(pageData->flags & GENERIC_XLOG_FULL_IMAGE)) |
370 | 0 | computeDelta(pageData, page, (Page) pageData->image); |
371 | | |
372 | | /* |
373 | | * Apply the image, being careful to zero the "hole" between |
374 | | * pd_lower and pd_upper in order to avoid divergence between |
375 | | * actual page state and what replay would produce. |
376 | | */ |
377 | 0 | memcpy(page, pageData->image, pageHeader->pd_lower); |
378 | 0 | memset(page + pageHeader->pd_lower, 0, |
379 | 0 | pageHeader->pd_upper - pageHeader->pd_lower); |
380 | 0 | memcpy(page + pageHeader->pd_upper, |
381 | 0 | pageData->image + pageHeader->pd_upper, |
382 | 0 | BLCKSZ - pageHeader->pd_upper); |
383 | |
|
384 | 0 | MarkBufferDirty(pageData->buffer); |
385 | |
|
386 | 0 | if (pageData->flags & GENERIC_XLOG_FULL_IMAGE) |
387 | 0 | { |
388 | 0 | XLogRegisterBuffer(i, pageData->buffer, |
389 | 0 | REGBUF_FORCE_IMAGE | REGBUF_STANDARD); |
390 | 0 | } |
391 | 0 | else |
392 | 0 | { |
393 | 0 | XLogRegisterBuffer(i, pageData->buffer, REGBUF_STANDARD); |
394 | 0 | XLogRegisterBufData(i, pageData->delta, pageData->deltaLen); |
395 | 0 | } |
396 | 0 | } |
397 | | |
398 | | /* Insert xlog record */ |
399 | 0 | lsn = XLogInsert(RM_GENERIC_ID, 0); |
400 | | |
401 | | /* Set LSN */ |
402 | 0 | for (i = 0; i < MAX_GENERIC_XLOG_PAGES; i++) |
403 | 0 | { |
404 | 0 | GenericXLogPageData *pageData = &state->pages[i]; |
405 | |
|
406 | 0 | if (BufferIsInvalid(pageData->buffer)) |
407 | 0 | continue; |
408 | 0 | PageSetLSN(BufferGetPage(pageData->buffer), lsn); |
409 | 0 | } |
410 | 0 | END_CRIT_SECTION(); |
411 | 0 | } |
412 | 0 | else |
413 | 0 | { |
414 | | /* Unlogged relation: skip xlog-related stuff */ |
415 | 0 | START_CRIT_SECTION(); |
416 | 0 | for (i = 0; i < MAX_GENERIC_XLOG_PAGES; i++) |
417 | 0 | { |
418 | 0 | GenericXLogPageData *pageData = &state->pages[i]; |
419 | |
|
420 | 0 | if (BufferIsInvalid(pageData->buffer)) |
421 | 0 | continue; |
422 | 0 | memcpy(BufferGetPage(pageData->buffer), |
423 | 0 | pageData->image, |
424 | 0 | BLCKSZ); |
425 | | /* We don't worry about zeroing the "hole" in this case */ |
426 | 0 | MarkBufferDirty(pageData->buffer); |
427 | 0 | } |
428 | 0 | END_CRIT_SECTION(); |
429 | | /* We don't have a LSN to return, in this case */ |
430 | 0 | lsn = InvalidXLogRecPtr; |
431 | 0 | } |
432 | |
|
433 | 0 | pfree(state); |
434 | |
|
435 | 0 | return lsn; |
436 | 0 | } |
437 | | |
438 | | /* |
439 | | * Abort generic xlog record construction. No changes are applied to buffers. |
440 | | * |
441 | | * Note: caller is responsible for releasing locks/pins on buffers, if needed. |
442 | | */ |
443 | | void |
444 | | GenericXLogAbort(GenericXLogState *state) |
445 | 0 | { |
446 | 0 | pfree(state); |
447 | 0 | } |
448 | | |
449 | | /* |
450 | | * Apply delta to given page image. |
451 | | */ |
452 | | static void |
453 | | applyPageRedo(Page page, const char *delta, Size deltaSize) |
454 | 0 | { |
455 | 0 | const char *ptr = delta; |
456 | 0 | const char *end = delta + deltaSize; |
457 | |
|
458 | 0 | while (ptr < end) |
459 | 0 | { |
460 | 0 | OffsetNumber offset, |
461 | 0 | length; |
462 | |
|
463 | 0 | memcpy(&offset, ptr, sizeof(offset)); |
464 | 0 | ptr += sizeof(offset); |
465 | 0 | memcpy(&length, ptr, sizeof(length)); |
466 | 0 | ptr += sizeof(length); |
467 | |
|
468 | 0 | memcpy(page + offset, ptr, length); |
469 | |
|
470 | 0 | ptr += length; |
471 | 0 | } |
472 | 0 | } |
473 | | |
474 | | /* |
475 | | * Redo function for generic xlog record. |
476 | | */ |
477 | | void |
478 | | generic_redo(XLogReaderState *record) |
479 | 0 | { |
480 | 0 | XLogRecPtr lsn = record->EndRecPtr; |
481 | 0 | Buffer buffers[MAX_GENERIC_XLOG_PAGES]; |
482 | 0 | uint8 block_id; |
483 | | |
484 | | /* Protect limited size of buffers[] array */ |
485 | 0 | Assert(XLogRecMaxBlockId(record) < MAX_GENERIC_XLOG_PAGES); |
486 | | |
487 | | /* Iterate over blocks */ |
488 | 0 | for (block_id = 0; block_id <= XLogRecMaxBlockId(record); block_id++) |
489 | 0 | { |
490 | 0 | XLogRedoAction action; |
491 | |
|
492 | 0 | if (!XLogRecHasBlockRef(record, block_id)) |
493 | 0 | { |
494 | 0 | buffers[block_id] = InvalidBuffer; |
495 | 0 | continue; |
496 | 0 | } |
497 | | |
498 | 0 | action = XLogReadBufferForRedo(record, block_id, &buffers[block_id]); |
499 | | |
500 | | /* Apply redo to given block if needed */ |
501 | 0 | if (action == BLK_NEEDS_REDO) |
502 | 0 | { |
503 | 0 | Page page; |
504 | 0 | PageHeader pageHeader; |
505 | 0 | char *blockDelta; |
506 | 0 | Size blockDeltaSize; |
507 | |
|
508 | 0 | page = BufferGetPage(buffers[block_id]); |
509 | 0 | blockDelta = XLogRecGetBlockData(record, block_id, &blockDeltaSize); |
510 | 0 | applyPageRedo(page, blockDelta, blockDeltaSize); |
511 | | |
512 | | /* |
513 | | * Since the delta contains no information about what's in the |
514 | | * "hole" between pd_lower and pd_upper, set that to zero to |
515 | | * ensure we produce the same page state that application of the |
516 | | * logged action by GenericXLogFinish did. |
517 | | */ |
518 | 0 | pageHeader = (PageHeader) page; |
519 | 0 | memset(page + pageHeader->pd_lower, 0, |
520 | 0 | pageHeader->pd_upper - pageHeader->pd_lower); |
521 | |
|
522 | 0 | PageSetLSN(page, lsn); |
523 | 0 | MarkBufferDirty(buffers[block_id]); |
524 | 0 | } |
525 | 0 | } |
526 | | |
527 | | /* Changes are done: unlock and release all buffers */ |
528 | 0 | for (block_id = 0; block_id <= XLogRecMaxBlockId(record); block_id++) |
529 | 0 | { |
530 | 0 | if (BufferIsValid(buffers[block_id])) |
531 | 0 | UnlockReleaseBuffer(buffers[block_id]); |
532 | 0 | } |
533 | 0 | } |
534 | | |
535 | | /* |
536 | | * Mask a generic page before performing consistency checks on it. |
537 | | */ |
538 | | void |
539 | | generic_mask(char *page, BlockNumber blkno) |
540 | 0 | { |
541 | 0 | mask_page_lsn_and_checksum(page); |
542 | |
|
543 | 0 | mask_unused_space(page); |
544 | 0 | } |