A listing says what is left of the disk, and what will fit
dir said what was there and nothing about what was left. SplitDisk has printed the free figure since it was written, so the machine's own listing was the poorer of the two implementations at describing the same disk. 43 files, 3 directories 46 of 64 entries, 1893 blocks free Entries first, because they are the ceiling nobody notices until they hit it: a disk of small files runs out of directory slots long before it runs out of blocks. COUNTED RATHER THAN ASKED. The superblock keeps a free count and this file calls it "a note rather than the truth" in three places. sbfsSpace reads the whole directory table instead, which costs a read per directory block and is the answer rather than a guess. SplitDisk goes on reading the note and saying when it is stale, which is the right place for that check - the host tool is what you audit a disk with. ---- And it is a fact about the disk, not about where you are ---- The first version added the blocks up as the LISTING walked past them, which cost no extra read and was wrong: that walk stops only on entries in the working directory, so the same disk came out as 1,996 blocks free from the root and 2,025 from /Apps. Comparing against SplitDisk is what said so, which is what having two implementations is for. ---- The longest run, which is what decides whether a file fits ---- 4 of 16 entries, 37 blocks free the longest run is 25 Files are laid down contiguously, so the free total does not say whether a file will fit. Both implementations learn it, from one specification. Said only when it differs from the free total. Deleting is what fragments a contiguous store, and a disk that has only been appended to has one gap at the end - so on a healthy disk this is silent, and a line that appears only when something is wrong is a line somebody reads. There is no sort on this machine and the entries are in no order, so a candidate walks the disk: each pass finds the used extent nearest at or after it, and anything the candidate stands inside pushes it to the far end and starts the pass again. The same trick allocating uses. So it costs a pass per gap rather than per file - nearly nothing on a disk with one gap, more the more fragmented the disk is, which is the right way round. holes.img is six files with the second and fourth deleted, because no other disk here can show any of this: none of them has ever had anything deleted from it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
co-authored by
Claude Opus 5
parent
2ad8edf9bc
commit
323d7a0330
@@ -1916,6 +1916,278 @@ sbfsEntryBounds:
|
||||
sbfsBoundsDone:
|
||||
RET
|
||||
|
||||
; ---- What the whole disk is holding ----
|
||||
;
|
||||
; SbfsUsedBlocks becomes what every entry on the disk holds between them, and SbfsUsedEntries
|
||||
; how many slots are filled. Q is zero if the disk could be read.
|
||||
;
|
||||
; EVERY ENTRY, NOT EVERY ENTRY IN A DIRECTORY. Free space is a fact about the disk and not
|
||||
; about where you are standing, so this walks the directory table itself the way allocating
|
||||
; does, rather than using sbfsFirst and sbfsNext - which stop only on entries whose parent is
|
||||
; the working directory, and would have called the same disk emptier from one directory than
|
||||
; from another.
|
||||
;
|
||||
; A DIRECTORY HOLDS NO BLOCKS and adds nought, which is true by its fields rather than by a
|
||||
; test here. An unfinished save holds all of its, and is counted, because the blocks are
|
||||
; spoken for whatever the entry is called - and a listing that left them out would promise
|
||||
; room that is not there.
|
||||
;
|
||||
; The count in the superblock is not consulted. sbfs.asm calls it "a note rather than the
|
||||
; truth" in three places, and this is the truth: it costs a read of every directory block,
|
||||
; which is what the honest answer costs.
|
||||
sbfsSpace:
|
||||
RSTA
|
||||
SETD.0 SbfsUsedBlocks
|
||||
STA.0
|
||||
INCD.0
|
||||
STA.0
|
||||
SETD.0 SbfsUsedEntries
|
||||
STA.0
|
||||
INCD.0
|
||||
STA.0
|
||||
|
||||
SETD.0 SbfsDirStart
|
||||
SETD.1 SbfsBlock
|
||||
CALL sbfsCopyWord
|
||||
SETD.0 SbfsDirBlocks
|
||||
INCD.0
|
||||
LDA.0
|
||||
SETD.1 SbfsLeft
|
||||
STA.1
|
||||
|
||||
sbfsSpaceBlock:
|
||||
CALL sbfsReadBlock
|
||||
BNQ sbfsSpaceFailed
|
||||
SETD.1 SbfsBuffer
|
||||
CALL sbfsBufferOut
|
||||
SETD.2 SbfsBuffer
|
||||
INIA 0d8
|
||||
SETD.1 SbfsCount
|
||||
STA.1
|
||||
|
||||
sbfsSpaceEntry:
|
||||
LDA.2
|
||||
INIB 0x01
|
||||
AND
|
||||
BRQ sbfsSpaceNext ; A free slot holds nothing and fills nothing.
|
||||
|
||||
SETD.0 SbfsUsedEntries
|
||||
CALL sbfsStepWord
|
||||
|
||||
; The blocks this entry holds, which are at the same offset in an entry as they are
|
||||
; everywhere else. sbfsEntryBounds wants DP2 on the entry and gives back both ends, and the
|
||||
; length is the difference - worked out that way rather than read directly, so that one
|
||||
; routine owns where in an entry those fields are.
|
||||
PSHD.2
|
||||
CALL sbfsEntryBounds
|
||||
SETD.0 SbfsEntryEnd
|
||||
SETD.2 SbfsEntryStart
|
||||
CALL sbfsSubWord
|
||||
SETD.0 SbfsUsedBlocks
|
||||
SETD.2 SbfsEntryEnd
|
||||
CALL sbfsAddWord
|
||||
POPD.2
|
||||
|
||||
sbfsSpaceNext:
|
||||
DPUP.2 0d32
|
||||
SETD.1 SbfsCount
|
||||
LDA.1
|
||||
DECA
|
||||
STA.1
|
||||
BNA sbfsSpaceEntry
|
||||
|
||||
SETD.0 SbfsBlock
|
||||
CALL sbfsStepWord
|
||||
SETD.1 SbfsLeft
|
||||
LDA.1
|
||||
DECA
|
||||
STA.1
|
||||
BNA sbfsSpaceBlock
|
||||
|
||||
RSTA
|
||||
RSTB
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
sbfsSpaceFailed:
|
||||
INIA 0x01
|
||||
RSTB
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
; ---- The longest run of free blocks there is ----
|
||||
;
|
||||
; SbfsBiggest becomes the largest number of blocks that are free AND NEXT TO EACH OTHER.
|
||||
; Q is zero if the disk could be read.
|
||||
;
|
||||
; This is the number that says whether a file will fit. Files are laid down contiguously -
|
||||
; first fit, with the directory itself as the map and no allocation table anywhere - so a disk
|
||||
; with a thousand blocks free in ten scattered pieces will refuse a file of two hundred, and
|
||||
; the free total gives no hint of it.
|
||||
;
|
||||
; ---- The same trick allocating uses, for the same reason ----
|
||||
;
|
||||
; There is no sort on this machine and the entries are in no order, so the gaps cannot simply
|
||||
; be listed. Instead a candidate walks the disk: each pass looks for the used extent nearest
|
||||
; at or after it, and anything the candidate is standing inside pushes the candidate to the
|
||||
; far end of it and starts the pass again.
|
||||
;
|
||||
; So the cost is a pass of the directory table per gap, rather than per file - and it is a
|
||||
; measurement that costs nothing on a disk with one gap at the end, which is what a disk that
|
||||
; has only been appended to looks like, and costs more the more fragmented the disk is. Which
|
||||
; is the right way round: it is slow exactly when it has something to say.
|
||||
sbfsLargestRun:
|
||||
RSTA
|
||||
SETD.0 SbfsBiggest
|
||||
STA.0
|
||||
INCD.0
|
||||
STA.0
|
||||
CALL sbfsFirstData
|
||||
|
||||
sbfsRunPass:
|
||||
; Nothing is in the way yet this time round, and the nearest thing ahead is the end of the
|
||||
; disk - which is the answer when there is nothing ahead at all.
|
||||
RSTA
|
||||
SETD.0 SbfsRunMoved
|
||||
STA.0
|
||||
SETD.0 SbfsRunNext
|
||||
SETD.2 SbfsDiskBlocks
|
||||
CALL sbfsSetWord
|
||||
|
||||
SETD.0 SbfsDirStart
|
||||
SETD.1 SbfsBlock
|
||||
CALL sbfsCopyWord
|
||||
SETD.0 SbfsDirBlocks
|
||||
INCD.0
|
||||
LDA.0
|
||||
SETD.1 SbfsLeft
|
||||
STA.1
|
||||
|
||||
sbfsRunBlock:
|
||||
CALL sbfsReadBlock
|
||||
BNQ sbfsRunFailed
|
||||
SETD.1 SbfsBuffer
|
||||
CALL sbfsBufferOut
|
||||
SETD.2 SbfsBuffer
|
||||
INIA 0d8
|
||||
SETD.1 SbfsCount
|
||||
STA.1
|
||||
|
||||
sbfsRunEntry:
|
||||
LDA.2
|
||||
INIB 0x01
|
||||
AND
|
||||
BRQ sbfsRunNextEntry ; A free slot is in nobody's way.
|
||||
LDA.2
|
||||
INIB 0x02
|
||||
AND
|
||||
BNQ sbfsRunNextEntry ; Nor is a directory, which holds no blocks - see allocating.
|
||||
|
||||
CALL sbfsEntryBounds
|
||||
PSHD.2 ; The comparisons want DP2, and the walk wants it back.
|
||||
|
||||
; Behind the candidate entirely, so it says nothing about what is ahead.
|
||||
SETD.0 SbfsEntryEnd
|
||||
SETD.2 SbfsCandidate
|
||||
CALL sbfsCompareWord
|
||||
BRC sbfsRunClear
|
||||
BRQ sbfsRunClear
|
||||
|
||||
; Beginning at or before the candidate, so the candidate is standing inside it. It moves to
|
||||
; the far end and the pass begins again, exactly as allocating does.
|
||||
SETD.0 SbfsEntryStart
|
||||
SETD.2 SbfsCandidate
|
||||
CALL sbfsCompareWord
|
||||
BRC sbfsRunStraddles
|
||||
BRQ sbfsRunStraddles
|
||||
|
||||
; Ahead of the candidate. Is it the nearest thing ahead so far?
|
||||
SETD.0 SbfsEntryStart
|
||||
SETD.2 SbfsRunNext
|
||||
CALL sbfsCompareWord
|
||||
BNC sbfsRunClear
|
||||
SETD.0 SbfsRunNext
|
||||
SETD.2 SbfsEntryStart
|
||||
CALL sbfsSetWord
|
||||
BRI sbfsRunClear
|
||||
|
||||
sbfsRunStraddles:
|
||||
SETD.0 SbfsCandidate
|
||||
SETD.2 SbfsEntryEnd
|
||||
CALL sbfsSetWord
|
||||
INIA 0x01
|
||||
SETD.0 SbfsRunMoved
|
||||
STA.0
|
||||
|
||||
sbfsRunClear:
|
||||
POPD.2
|
||||
|
||||
sbfsRunNextEntry:
|
||||
DPUP.2 0d32
|
||||
SETD.1 SbfsCount
|
||||
LDA.1
|
||||
DECA
|
||||
STA.1
|
||||
BNA sbfsRunEntry
|
||||
|
||||
SETD.0 SbfsBlock
|
||||
CALL sbfsStepWord
|
||||
SETD.1 SbfsLeft
|
||||
LDA.1
|
||||
DECA
|
||||
STA.1
|
||||
BNA sbfsRunBlock
|
||||
|
||||
; The pass is over. If the candidate moved, everything measured against where it used to be
|
||||
; was measured against the wrong place, so the pass is worth nothing and is done again.
|
||||
SETD.0 SbfsRunMoved
|
||||
LDA.0
|
||||
BNA sbfsRunPass
|
||||
|
||||
; Nothing moved, so the gap from the candidate to the nearest thing ahead is a real one.
|
||||
SETD.0 SbfsRunGap
|
||||
SETD.2 SbfsRunNext
|
||||
CALL sbfsSetWord
|
||||
SETD.0 SbfsRunGap
|
||||
SETD.2 SbfsCandidate
|
||||
CALL sbfsSubWord
|
||||
|
||||
SETD.0 SbfsBiggest
|
||||
SETD.2 SbfsRunGap
|
||||
CALL sbfsCompareWord
|
||||
BNC sbfsRunKept ; What is already remembered is the same or bigger.
|
||||
SETD.0 SbfsBiggest
|
||||
SETD.2 SbfsRunGap
|
||||
CALL sbfsSetWord
|
||||
|
||||
sbfsRunKept:
|
||||
; Past the end of the disk is the end of the walk. Otherwise the candidate goes to the
|
||||
; thing that stopped it, which the next pass finds itself standing inside and steps over.
|
||||
SETD.0 SbfsRunNext
|
||||
SETD.2 SbfsDiskBlocks
|
||||
CALL sbfsCompareWord
|
||||
BNC sbfsRunDone
|
||||
SETD.0 SbfsCandidate
|
||||
SETD.2 SbfsRunNext
|
||||
CALL sbfsSetWord
|
||||
BRI sbfsRunPass
|
||||
|
||||
sbfsRunDone:
|
||||
RSTA
|
||||
RSTB
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
sbfsRunFailed:
|
||||
INIA 0x01
|
||||
RSTB
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
; Finds a run of SbfsWantBlocks free blocks and puts where it begins in SbfsFileStart.
|
||||
; Q is zero if there was room.
|
||||
;
|
||||
@@ -3691,6 +3963,24 @@ SbfsScratch1:
|
||||
SbfsPathWanted:
|
||||
0x00
|
||||
|
||||
; What every entry on the disk holds between them, and how many slots are filled. Worked out
|
||||
; by sbfsSpace, which reads the whole directory table to do it.
|
||||
; The longest run of free blocks, and the sweep that finds it: where it has got to ahead of
|
||||
; the candidate, whether anything pushed the candidate this pass, and the gap being measured.
|
||||
SbfsBiggest:
|
||||
0x00 0x00
|
||||
SbfsRunNext:
|
||||
0x00 0x00
|
||||
SbfsRunGap:
|
||||
0x00 0x00
|
||||
SbfsRunMoved:
|
||||
0x00
|
||||
|
||||
SbfsUsedBlocks:
|
||||
0x00 0x00
|
||||
SbfsUsedEntries:
|
||||
0x00 0x00
|
||||
|
||||
SbfsFileStart:
|
||||
0x00 0x00
|
||||
SbfsFileBlocks:
|
||||
|
||||
Reference in New Issue
Block a user