FilePagesImpl.mesa - per-file operations, locking file header data structure, file page ops
Copyright © 1985 by Xerox Corporation. All rights reserved.
Andrew Birrell December 8, 1983 9:51 am
Levin, August 8, 1983 5:57 pm
Schroeder, June 10, 1983 5:20 pm
Bob Hagmann, January 9, 1986 4:01:58 pm PST
Russ Atkinson (RRA) May 15, 1985 8:29:35 pm PDT
DIRECTORY
Disk USING[ Channel, invalid, Label, ok, PageCount, PageNumber, Request, Status ],
DiskFace USING[ AbsID, DontCare, RelID, Tries, wordsPerPage ],
File USING[ Error, FP, GetVolumeID, PageCount, PageNumber, RC, Volume, VolumeID ],
FileBackdoor USING [],
FileInternal,
VolumeFormat USING[ AbsID, Attributes, lastLogicalRun, LogicalPage, LogicalPageCount, LogicalRun, LogicalRunObject, RelID, RunPageCount ],
VM USING[AddressForPageNumber, Allocate, Free, Interval, PageCount, PageNumber, PageNumberForAddress, PagesForWords, SwapIn],
VMBacking USING[RunTableIndex, RunTableObject, RunTablePageNumber];
Four different Impls all export some of these three data types. All exports are the same.
--DiskFace.--Attributes: PUBLIC TYPE = VolumeFormat.Attributes;
--DiskFace.--AbsID: PUBLIC TYPE = VolumeFormat.AbsID;
--DiskFace.--RelID: PUBLIC TYPE = VolumeFormat.RelID;
Handle: TYPE = REF Object;
--File.--Object: PUBLIC TYPE = FileInternal.Object;
RunTable: TYPE = FileInternal.RunTable;
RunTableObject: TYPE = VMBacking.RunTableObject;
RunTableIndex: TYPE = VMBacking.RunTableIndex;
PhysicalRun: TYPE = FileInternal.PhysicalRun;
lastRun: VMBacking.RunTablePageNumber = LAST[INT]; -- end marker in runTable --
MaxTransferRun:
PUBLIC Disk.PageCount ← 200;
Largest number of disk pages to transfer in single request. This much VM will be pinned during the transfer. Variable, not constant, to allow patching.
scratchWriter: PUBLIC LONG POINTER; -- scratch buffer for writing (all 0)
scratchReader: PUBLIC LONG POINTER; -- scratch buffer for reading
badData: Disk.Status = [unchanged[dataCRCError]]; -- indicates data is bad but label is ok
labelTries: DiskFace.Tries = 5;
notReallyFree:
PUBLIC
INT ← 0;
FileInternal
HeaderLabel:
PUBLIC
PROC[fp: File.
FP]
RETURNS[Disk.Label] = {
FileInternal
RETURN[ [
fileID: [rel[RelID[fp]]],
filePage: 0,
attributes: Attributes[header],
dontCare: LOOPHOLE[LONG[0]]
] ]
};
DataLabel:
PUBLIC
PROC[fp: File.
FP]
RETURNS[Disk.Label] = {
FileInternal
RETURN[ [
fileID: [rel[RelID[fp]]],
filePage: 0,
attributes: Attributes[data],
dontCare: LOOPHOLE[LONG[0]]
] ]
};
FreeLabel:
PUBLIC
PROC[volume: File.Volume]
RETURNS[Disk.Label] = {
FileInternal
RETURN[ [
fileID: [abs[AbsID[File.GetVolumeID[volume]]]],
filePage: 0,
attributes: Attributes[freePage],
dontCare: LOOPHOLE[LONG[0]]
] ]
};
WriteLabels:
PUBLIC
PROC[channel: Disk.Channel, diskPage: Disk.PageNumber, count: Disk.PageCount, data:
LONG
POINTER, label:
POINTER
TO Disk.Label]
RETURNS[ status: Disk.Status, countDone: Disk.PageCount] =
TRUSTED {
FileInternal
req: Disk.Request ← [
diskPage: diskPage,
data: IF data = NIL THEN scratchWriter ELSE data,
incrementDataPtr: data # NIL,
command: [header: verify, label: write, data: write],
count: count ];
[status, countDone] ← FileInternal.DoPinnedIO[channel, label, @req];
};
VerifyLabels:
PUBLIC
PROC[channel: Disk.Channel, diskPage: Disk.PageNumber, count: Disk.PageCount, label:
POINTER
TO Disk.Label]
RETURNS[ status: Disk.Status, countDone: Disk.PageCount] =
TRUSTED {
FileInternal
req: Disk.Request ← [
diskPage: diskPage,
data: FileInternal.scratchReader,
incrementDataPtr: FALSE,
command: [header: verify, label: verify, data: read],
count: count,
tries: labelTries ];
[status, countDone] ← FileInternal.DoPinnedIO[channel, label, @req];
IF status = badData
THEN { status ← Disk.ok; countDone ← countDone+1; label.filePage ← label.filePage+1 };
};
GetScratchPage:
PUBLIC
PROC
RETURNS [data:
LONG
POINTER] =
TRUSTED {
FileInternal
temp: LONG POINTER TO ARRAY [0..DiskFace.wordsPerPage) OF WORD;
interval: VM.Interval ← VM.Allocate[VM.PagesForWords[DiskFace.wordsPerPage]];
VM.SwapIn[interval];
data ← VM.AddressForPageNumber[interval.page];
temp ← LOOPHOLE[data];
temp^ ← ALL[0];
};
FreeScratchPage:
PUBLIC
PROC[data:
LONG
POINTER] =
TRUSTED {
FileInternal
VM.Free[
[ page: VM.PageNumberForAddress[data],
count: VM.PagesForWords[DiskFace.wordsPerPage] ]
];
};
AddRun:
PUBLIC
PROC[file: Handle, run:
POINTER
TO PhysicalRun, logicalPage: VolumeFormat.LogicalPage, okPages: VolumeFormat.RunPageCount] =
TRUSTED {
FileInternal
Either extend last run, or add new run
logical: LONG POINTER TO VolumeFormat.LogicalRunObject ← file.logicalRunTable;
physical: RunTable = file.runTable;
oldNRuns: CARDINAL ← physical.nRuns;
physical.nDataPages ← file.size + okPages;
IF oldNRuns > 0
AND logical[oldNRuns-1].first + logical[oldNRuns-1].size = logicalPage
AND logical[oldNRuns-1].size <= LAST[VolumeFormat.RunPageCount] - okPages
AND run.channel = physical[oldNRuns-1].channel
THEN logical[oldNRuns-1].size ← logical[oldNRuns-1].size + okPages
ELSE {
Add another run
IF physical.nRuns+1 = logical.maxRuns
THEN {
Extend logical run table to ensure there is space to record an extension.
ExtendPhysicalRunTable[file];
logical ← file.logicalRunTable; -- recompute since it is changed by ExtendFileHeader in ExtendPhysicalRunTable
oldNRuns ← physical.nRuns;
};
physical[oldNRuns] ← run^;
logical[oldNRuns].first ← logicalPage;
logical[oldNRuns].size ← okPages;
physical.nRuns ← oldNRuns + 1;
IF physical.nRuns = physical.length
THEN {
extend physical run table object
file.runTable ← NEW[RunTableObject[physical.length*2]];
file.runTable^ ← physical^ . . . . but the compiler can't copy sequences
file.runTable.nDataPages ← physical.nDataPages;
file.runTable.nRuns ← physical.nRuns;
FOR i: CARDINAL IN [0..physical.length) DO file.runTable[i] ← physical[i] ENDLOOP;
};
file.runTable[oldNRuns + 1].filePage ← lastRun;
logical[oldNRuns + 1].first ← VolumeFormat.lastLogicalRun;
};
};
ExtendPhysicalRunTable:
PROC [file: Handle] =
TRUSTED {
Extend logical run table to ensure there is space to record an extension.
Do this by allocating a single run that has enough pages for all header pages except page 0. Then write the labels and header data for the new header pages 1 on up. Write header page 0. If the write succeeds, we have committed to the new header pages. If we crash, we loose the new header pages (should be free but are not), but we have a consistent view of the header: the old one. Free old header pages.
newRunPages: VolumeFormat.LogicalPageCount =
FileInternal.RunsToHeaderPages[file.logicalRunTable.maxRuns] + 1;
diskPage1: Disk.PageNumber;
restOfHeaderSize: Disk.PageCount;
oldChannel: Disk.Channel;
[diskPage: diskPage1, size: restOfHeaderSize, channel: oldChannel] ← FileInternal.FindRun[
start: [-file.logicalRunTable.headerPages+1],
nPages: file.diskRunPages + file.diskPropertyPages - 1,
runTable: file.runTable];
IF restOfHeaderSize # file.diskRunPages + file.diskPropertyPages - 1
THEN
insist that headers 1 on up all are in the same run
ERROR File.Error[fragmented];
FileInternal.ExtendFileHeader[file: file, newRunPages: newRunPages, newPropertyPages: file.diskPropertyPages]; -- recomputes logical.maxRuns
};
RemoveFromRunTable:
PUBLIC
PROC[file: Handle, remove:
INT] =
TRUSTED {
FileInternal
logical: LONG POINTER TO VolumeFormat.LogicalRunObject = file.logicalRunTable;
physical: RunTable = file.runTable;
physical.nDataPages ← file.size;
WHILE remove > 0
DO
IF physical.nRuns = 0 THEN ERROR File.Error[inconsistent];
{
runSize: VolumeFormat.RunPageCount = logical[physical.nRuns-1].size;
amount: VolumeFormat.RunPageCount = MIN[remove, runSize];
logical[physical.nRuns-1].size ← runSize - amount;
IF runSize - amount = 0
THEN {
Remove the new run table entry entirely! --
physical.nRuns ← physical.nRuns - 1;
physical[physical.nRuns].filePage ← lastRun;
logical[physical.nRuns].first ← VolumeFormat.lastLogicalRun;
};
remove ← remove - amount;
};
ENDLOOP;
};
SpliceOutDataPage:
PUBLIC
PROC[file: Handle, filePage: File.PageCount]
RETURNS [oldPage: Disk.PageNumber ← [0], oldChannel: Disk.Channel ←
NIL] =
TRUSTED {
FileBackdoor
runNumber: CARDINAL;
nearTo: VolumeFormat.LogicalPage;
newRun: FileInternal.PhysicalRun;
newLogicalRun: VolumeFormat.LogicalRun ;
labelsThisTime: Disk.PageCount ← 0;
IF filePage >= file.runTable.nDataPages THEN ERROR File.Error[unknownPage];
[diskPage: oldPage, channel: oldChannel] ← FindRun[start: [filePage], nPages: 1, runTable: file.runTable];
IF file.runTable.nRuns+3 >= file.logicalRunTable.maxRuns
THEN {
-- may split too soon
ExtendPhysicalRunTable[file]; -- make sure there is enough room
FileInternal.WriteRunTable[file]; -- write it out
};
[runNumber] ← SplitPhysicalRunTable[file: file, page: [filePage+file.runPages+file.propertyPages]];
IF filePage+1 < file.runTable.nDataPages THEN [] ← SplitPhysicalRunTable[file: file, page: [filePage+file.runPages+file.propertyPages+1]];
IF file.logicalRunTable.runs[runNumber].size # 1 THEN ERROR;
nearTo ← file.logicalRunTable.runs[runNumber].first;
WHILE labelsThisTime # 1
DO
need to loop because we may find pages free in VAM that are not really free
status: Disk.Status;
labelsOK: Disk.PageCount;
freeLabel: Disk.Label ← FileInternal.FreeLabel[file.volume];
newLogicalRun ← FileInternal.Alloc[volume: file.volume, first: nearTo, size: 1, min: 1 ];
freeLabel.filePage ← newLogicalRun.first;
[newRun.channel, newRun.diskPage] ← FileInternal.TranslateLogicalRun[newLogicalRun, file.volume];
nearTo ← [newLogicalRun.first + 1]; -- hint for next call of Alloc, if needed
TRUSTED{ [status, labelsOK] ←
FileInternal.VerifyLabels[newRun.channel, newRun.diskPage, newLogicalRun.size, @freeLabel] };
IF status # Disk.ok THEN notReallyFree ← notReallyFree+1; -- statistics
IF labelsOK = 1
THEN {
Suspected free page was free; write labels and data for header area
dataLabel: Disk.Label ← FileInternal.DataLabel[file.fp];
dataLabel.filePage ← filePage;
TRUSTED{[status, labelsThisTime] ← FileInternal.WriteLabels[newRun.channel,
newRun.diskPage, 1, file.headerVM+DiskFace.wordsPerPage, @dataLabel]};
};
ENDLOOP; -- end of WHILE labelsThisTime # 1 DO
file.logicalRunTable.runs[runNumber].first ← newLogicalRun.first;
FileInternal.WriteRunTable[file];
[] ← FileInternal.TranslateLogicalRunTable[file: file];
retranslate since we have extended and changed everything
};
SplitPhysicalRunTable:
PUBLIC
PROC[file: Handle, page: VolumeFormat.LogicalPage]
RETURNS [runNumber:
CARDINAL] =
TRUSTED {
FileInternal == page is logical page in file with header page 0 as page 0.
logicalRunTable: LONG POINTER TO VolumeFormat.LogicalRunObject = file.logicalRunTable;
filePage: VolumeFormat.LogicalPage ← [0];
FOR runNumber
IN [0..logicalRunTable.maxRuns)
DO
IF logicalRunTable.runs[runNumber].first = VolumeFormat.lastLogicalRun THEN ERROR File.Error[inconsistent];
IF page = filePage THEN EXIT; -- split already in place
IF page >= filePage
AND page < filePage+logicalRunTable.runs[runNumber].size
THEN {
scratchLogicalRun: VolumeFormat.LogicalRun ← logicalRunTable.runs[runNumber];
copy forward all entries at and after the point of insertion
FOR j:
CARDINAL
IN [runNumber+1..logicalRunTable.maxRuns)
DO
-- asumes enough room in the run table
sLogicalRun: VolumeFormat.LogicalRun = logicalRunTable.runs[j];
logicalRunTable.runs[j] ← scratchLogicalRun;
IF scratchLogicalRun.first = VolumeFormat.lastLogicalRun THEN EXIT;
scratchLogicalRun ← sLogicalRun;
ENDLOOP;
logicalRunTable.runs[runNumber].size ← page - filePage;
runNumber ← runNumber+1;
logicalRunTable.runs[runNumber].first ← [logicalRunTable.runs[runNumber].first + page - filePage];
logicalRunTable.runs[runNumber].size ← logicalRunTable.runs[runNumber].size - page + filePage;
RETURN;
};
filePage ← [filePage+logicalRunTable.runs[runNumber].size];
ENDLOOP;
};
FreeRun:
PUBLIC
PROC[logicalRun: VolumeFormat.LogicalRun, volume: File.Volume, verifyLabel:
POINTER
TO Disk.Label ←
NIL] = {
FileInternal
Write page labels as "free" and mark as unused in VAM. Iff verifyLabel # NIL, do so only to pages whose labels verify correctly.
label: Disk.Label ← FreeLabel[volume];
WHILE logicalRun.size > 0
DO
channel: Disk.Channel;
diskPage: Disk.PageNumber;
status: Disk.Status;
verifyStatus: Disk.Status ← Disk.ok;
thisTime: Disk.PageCount ← logicalRun.size;
countDone: Disk.PageCount;
Consume:
PROC = {
logicalRun.first ← [logicalRun.first + countDone];
logicalRun.size ← logicalRun.size - countDone;
IF verifyLabel #
NIL
THEN
TRUSTED {
verifyLabel.filePage ← verifyLabel.filePage + countDone;
};
countDone ← 0;
};
[channel, diskPage] ← FileInternal.TranslateLogicalRun[logicalRun, volume];
IF verifyLabel #
NIL
THEN
TRUSTED {
verify which pages are part of our file
temp: Disk.Label ← verifyLabel^;
[verifyStatus, thisTime] ← VerifyLabels[channel, diskPage, thisTime, @temp];
};
label.filePage ← logicalRun.first;
FileInternal.Free[volume, [first: logicalRun.first, size: thisTime]];
TRUSTED{[status, countDone] ← WriteLabels[channel, diskPage, thisTime, NIL, @label]};
SELECT status
FROM
Disk.ok => NULL;
Disk.invalid => ERROR File.Error[wentOffline, diskPage+countDone];
ENDCASE => countDone ← countDone + 1; -- Page is in our file, but not writeable
Consume[];
IF verifyLabel #
NIL
AND status = Disk.ok
AND verifyStatus # Disk.ok
THEN
TRUSTED {
We've freed everything up to but excluding a label mis-match. Now, look for free pages which should be notified to the VAM in case the VAM thinks they're in use.
free: Disk.Label ← FreeLabel[volume];
free.filePage ← logicalRun.first;
[channel, diskPage] ← FileInternal.TranslateLogicalRun[logicalRun, volume];
[verifyStatus, countDone] ← VerifyLabels[channel, diskPage, logicalRun.size, @free];
IF verifyStatus # Disk.ok
AND countDone = 0
THEN countDone ← 1 -- label isn't in our file, but isn't free, so ignore it
ELSE FileInternal.Free[volume, [first: logicalRun.first, size: countDone]];
Consume[];
}
ENDLOOP;
};
FindRun:
PUBLIC
PROC[start: File.PageNumber, nPages: File.PageCount, runTable: RunTable]
RETURNS [diskPage: Disk.PageNumber, size: Disk.PageCount, channel: Disk.Channel] = {
FileInternal
probe: RunTableIndex ← runTable.nRuns / 2; -- NB: round down --
increment: CARDINAL ← probe;
IF runTable.nRuns = 0 THEN ERROR File.Error[inconsistent];
IF start + nPages > runTable.nDataPages THEN ERROR File.Error[unknownPage, start];
IF start < runTable[0].filePage THEN ERROR File.Error[unknownPage, start];
DO increment ← (increment+1)/2;
SELECT
TRUE
FROM
runTable[probe].filePage > start =>
probe ← IF probe < increment THEN 0 ELSE probe-increment;
runTable[probe+1].filePage <= start =>
probe ← MIN[probe + increment, runTable.nRuns-1];
ENDCASE =>
RETURN[
diskPage: [runTable[probe].diskPage + (start-runTable[probe].filePage)],
size:
IF start + nPages <= runTable[probe+1].filePage
THEN nPages
ELSE runTable[probe+1].filePage - start,
channel: runTable[probe].channel ]
ENDLOOP;
};
maxTransferRun: Disk.PageCount ← 200;
Largest number of disk pages to transfer in single request. This much VM will be pinned during the transfer. Variable, not constant, to allow patching.
Transfer:
PUBLIC
PROC[file: Handle, data:
LONG
POINTER, filePage: File.PageNumber, nPages: File.PageCount, action: FileInternal.ActionType, where: FileInternal.WhereLocation ] = {
FileInternal
label: Disk.Label ←
IF where = header THEN FileInternal.HeaderLabel[file.fp] ELSE FileInternal.DataLabel[file.fp];
label.filePage ← filePage;
WHILE nPages > 0
DO
status: Disk.Status;
countDone: Disk.PageCount;
channel: Disk.Channel;
req: Disk.Request;
firstDiskPage: Disk.PageNumber;
req.data ← data;
req.incrementDataPtr ← TRUE;
req.command ←
IF action = read
THEN [header: verify, label: verify, data: read]
ELSE [header: verify, label: verify, data: write];
TRUSTED{ [diskPage: req.diskPage, size: req.count, channel: channel] ← FileInternal.FindRun[
-- NB: first call of FindRun checks entire transfer is within file
start: IF where = header THEN [-file.logicalRunTable.headerPages+filePage] ELSE filePage,
nPages: nPages,
runTable: file.runTable] };
firstDiskPage ← req.diskPage;
IF req.count > maxTransferRun THEN req.count ← maxTransferRun;
TRUSTED{[status, countDone] ← FileInternal.DoPinnedIO[channel, @label, @req]};
FileInternal.CheckStatus[status, firstDiskPage+countDone]; -- use firstDiskPage instead of req.diskPage since DiskImpl modifies req.diskPage
TRUSTED{data ← data + countDone * DiskFace.wordsPerPage};
nPages ← nPages - countDone;
filePage ← [filePage + countDone];
ENDLOOP;
};
WriteRunTable:
PUBLIC
PROC[file: Handle] =
TRUSTED {
Exported to FileInternal
Caller guarantees header will not need to be expanded.
IF file.diskRunPages = file.runPages
AND file.diskPropertyPages = file.propertyPages
THEN {
Normal case: one page run table and single property page, or header size on disk is correct. Page level atomicity guarantees file invariants.
FileInternal.Transfer[file: file, data: file.headerVM, filePage: [0], nPages: file.runPages, action: write, where: header];
}
ELSE {
Abnormal case: header is being extended. Be careful to build a new header pages 1 on up before writing page 0. It is possible to guarantee file invariants, but still loose some pages (headers written, not in disk run table) if we crash at the wrong time.
labelsThisTime: Disk.PageCount ← 0;
size: VolumeFormat.LogicalPageCount = file.runPages + file.propertyPages - 1 ;
nearTo: VolumeFormat.LogicalPage ← file.logicalRunTable.runs[0].first;
Get run table in acceptable format: run 0 is one page long, run 1 has the rest of the header(s), the rest of the runs are for data pages.
IF file.logicalRunTable.runs[0].size # 1
THEN {
[] ← FileInternal.SplitPhysicalRunTable[file: file, page: [1]];
};
IF file.logicalRunTable.runs[1].size # file.diskRunPages+file.diskPropertyPages-1
THEN {
[] ← FileInternal.SplitPhysicalRunTable[file: file, page: [file.diskRunPages+file.diskPropertyPages]];
};
WHILE labelsThisTime # size
DO
need to loop because we demand run number two contain all the rest of the header
logicalRun: VolumeFormat.LogicalRun;
status: Disk.Status;
labelsOK: Disk.PageCount;
run: FileInternal.PhysicalRun;
freeLabel: Disk.Label ← FileInternal.FreeLabel[file.volume];
logicalRun ← FileInternal.Alloc[volume: file.volume, first: nearTo, size: size, min: size];
freeLabel.filePage ← logicalRun.first;
[run.channel, run.diskPage] ← FileInternal.TranslateLogicalRun[logicalRun, file.volume];
nearTo ← [logicalRun.first + size]; -- hint for next call of Alloc, if needed
TRUSTED {
[status, labelsOK] ← FileInternal.VerifyLabels[run.channel, run.diskPage, logicalRun.size, @freeLabel]
};
IF status # Disk.ok THEN notReallyFree ← notReallyFree+1; -- statistics
IF labelsOK = size
THEN {
Suspected free pages were free; write labels and data for header area
headerLabel: Disk.Label ← FileInternal.HeaderLabel[file.fp];
headerLabel.filePage ← 1;
TRUSTED {
[status, labelsThisTime] ← FileInternal.WriteLabels[run.channel, run.diskPage, size, file.headerVM+DiskFace.wordsPerPage, @headerLabel];
};
IF labelsThisTime = size
THEN {
destroyLogicalRun: VolumeFormat.LogicalRun ← file.logicalRunTable.runs[1];
file.logicalRunTable.runs[1] ← logicalRun;
file.diskRunPages ← file.runPages ;
file.diskPropertyPages ← file.propertyPages ;
file.logicalRunTable.headerPages ← file.diskRunPages + file.diskPropertyPages ;
[] ← FileInternal.TranslateLogicalRunTable[file];
re-translate since we have changed everything
Go for commit on the new header pages.
FileInternal.Transfer[file: file, data: file.headerVM, filePage: [0], nPages: 1, action: write, where: header];
Free old header pages
headerLabel.filePage ← 1;
FileInternal.FreeRun[logicalRun: destroyLogicalRun, volume: file.volume, verifyLabel: @headerLabel];
}
ELSE {
Some pages would not write. Rewrite labels of pages written OK, and fix up VAM.
IF labelsThisTime > 0
THEN {
headerLabel.filePage ← 1;
FileInternal.FreeRun[
[first: [logicalRun.first], size: labelsOK], file.volume, @headerLabel];
};
IF size - labelsThisTime - 1 > 0
THEN {
deallocate pages where labels not changed
FileInternal.Free[volume: file.volume, logicalRun: [first: [logicalRun.first + labelsOK + 1], size: size - labelsThisTime - 1]];
};
};
}
ELSE {
Some labels were not free (verify failed) even though the VAM said they were free. Free pages with the labels changed, and fix up VAM for pages not changed.
IF labelsOK > 0
THEN
FileInternal.Free[volume: file.volume, logicalRun: [first: [logicalRun.first], size: labelsOK]];
IF size - labelsOK - 1 > 0
THEN
FileInternal.Free[volume: file.volume, logicalRun: [first: [logicalRun.first + labelsOK + 1], size: size - labelsOK - 1]];
};
ENDLOOP;
};
};
scratchWriter ← GetScratchPage[];
scratchReader ← GetScratchPage[];
}.