storage: map_file - read a file straight from memory-mapped flash - #11363
lynt-smitka wants to merge 5 commits into
Conversation
7f1c648 to
070fbc8
Compare
|
Trying to turn off a module on the two boards that are tight on flash |
070fbc8 to
00c8c71
Compare
tannewt
left a comment
There was a problem hiding this comment.
One big question about what happens when the file is modified.
| //| Treat the file as read-only while a view is in use: rewriting it shows the new bytes, and | ||
| //| mid-write it shows a torn file. |
There was a problem hiding this comment.
Can we invalidate the view when it is modified and raise an exception instead of returning junk? I think littlefs might not place a file over itself when writing. Even flash will be all 1s temporarily during a write.
There was a problem hiding this comment.
I went with refusing the write instead: RawSample and MidiTrack keep the raw pointer, so an invalidated view wouldn't help them.
The new commit makes open() for writing and os.remove() on a mapped file raise EACCES until the next reload (rename still works). Non-FAT files now raise EOPNOTSUPP rather than TypeError.
Is EACCES the errno you'd want, or EROFS?
storage.map_file(f) returns a tuple of read-only memoryviews over the flash bytes of an open file on the internal CIRCUITPY drive, one per contiguous cluster run, in file order and 0 copy. Anything that takes a buffer can then use the file without reading it into RAM: a synthio.MidiTrack, a RawSample, a wavetable, a ulab array, a bitmap. Assets stay ordinary files on the drive. Opening a file for reading already builds its FatFs cluster-link map, so the function only reads that map. The supervisor maps a FatFs sector to a flash address through a port hook that also reports how far the mapping stays contiguous, so a run is split where it is not. raspberrypi returns the execute-in-place address (the drive is XIP on every RP2 board); espressif esp_partition_mmap's each drive partition on first use and reports the seam of an extended drive. The function is always present; on a port whose drive is not mapped (CIRCUITPY_STORAGE_MAP_FILE off) it raises NotImplementedError. 600 B of text on pajenicko_picopad, 496 B on adafruit_feather_esp32s3_tft.
A view's consumers (audiocore.RawSample, synthio.MidiTrack) take the pointer through the buffer protocol once, so invalidating the view on a write would not protect them. Instead the file is protected on the write side: map_file records the file's start cluster, and open() for writing or os.remove() of that file raises OSError EACCES until the next reload. Rename is allowed, the data does not move. The record is a VM root pointer, cleared with the other per-run state in cleanup_after_vm. A non-FAT file (littlefs keeps its pointers inside the data blocks) raises OSError EOPNOTSUPP instead of TypeError, so one except clause covers the fallback to read(); the docstring shows it. +280 B on pajenicko_picopad, +196 B on adafruit_feather_esp32s3_tft; 0 B where CIRCUITPY_STORAGE_MAP_FILE is off.
00c8c71 to
a34445b
Compare
The type check already rejects a non-stream, and a cluster chain open() could not walk shows up as a run total shorter than the file (EIO), so the separate fp.err check was never reached. -48 B on pajenicko_picopad.
Boards without USB MSC (esp32, esp32c3, esp32c6) do not include extmod/vfs_fat.h before this header, so the struct in the prototype was scoped to the parameter list and the definition failed with conflicting types under -Werror.
storage.map_file(f)lets use an asset straight from the CIRCUITPY drive's flash, withoutcopying it into RAM. FatFs fragmented file is not a problem: the function returns one read-only
memoryview per contiguous cluster run, in file order, and the caller composes them. A bitmap, a
sample bank or a MIDI track that does not fit in RAM stays an ordinary file on the drive. This
started as
picogame.xip_mapfor the picogame's backgrounds loading. The same works foraudiocore.RawSample,synthio.MidiTrackandsynthio.Notewavetables, which keep their bufferfor as long as they exist, so I moved the function in
storage.In a test with a FAT image written by Linux, 16 % of new files were contiguous after every other
file had been deleted and 58 % after random deletes, and a fragmented file had 2, at most 3 runs. So
the function returns every run. A record inside one run is a slice of that run's memoryview. A
record that straddles a run boundary is copied.
Opening a file for reading already builds its FatFs cluster-link map, so the function only reads
that map. On RP2 the last written 4 KB flash sector can still sit in a RAM copy, so the function
flushes it to flash first and the memoryviews see what was last written. A port hook maps a drive
block to a flash address and says how far the mapping stays contiguous, so a run is split where it
is not. raspberrypi returns the execute-in-place address. espressif maps each drive partition with
esp_partition_mmapon first use and reports the seam of an extended drive, the default on 8 MB andlarger flash. Other ports raise
NotImplementedError. Errors:EINVALfor a closed file or one notopen for reading only,
EOPNOTSUPPon another mount,EIOfor a corrupt chain,MemoryErrorifopen()could not allocate the map.Measured with the 18 background strips (92 KB) of a picogame game, heap used:
map_fileopen().read()Flash: +0.5 kB
The memoryviews are not copies of the file. When the file is rewritten they show the new bytes, and
a mix of old and new while the write runs. On RP2
AudioOutandI2SOutcopy a wholeaudiocore.RawSampleinto a staging buffer, so a mapped sample saves no RAM there. Through anaudiomixer.Mixerthe voice reads the memoryview in place.displayio.Bitmapalways allocates itsown pixel buffer, so a mapped image still has to be copied into one. Letting a Bitmap use the mapped
bytes directly, through the existing
construct_from_bufferwithread_only, is a follow-up.