-
Notifications
You must be signed in to change notification settings - Fork 818
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
lfs_file_seek() poor performance when seeking backward by one byte with no apparent reason #810
Comments
Hi @LuchkaOnTheMoon, thanks for creating an issue. Performance is a complicated issue in littlefs, and in the general case, poor, with several pitfalls. It hasn't been a priority, and this shows, though it's something I'm looking into and hoping to improve in the future. First thing to note:
It's not uncommon for filesystems to finish write operations during seek. In lfs_file_write, littlefs doesn't know if there will be more data coming, so it may just store data in the file's cache and wait for more data. But when lfs_file_seek is called, it's unlikely more data we be written at exactly that position, so littlefs goes ahead and writes out the pending data. This helps keep the file state consistent, since the cache's contents depends on the file's internal file position. I suppose it could be made a bit smarter by keeping a separate cache position, but since the file cache is used for both reads and writes, this would lead to lfs_file_read writing to disk which is probably even more unintuitive.
This looks suspiciously like the issue in #783 TLDR: Metadata is compacted on-demand when a metadata block fills up. This runs in There are also some ideas around moving this cost to background work: #791 And one workaround is the using the metadata_max config to artificially limit the size of metadata blocks. More info the in above thread: #783 (comment) |
Oh, it's also worth noting this only happens when a file is open for writing. Read-only files will never write to disk. |
Hi @geky, first of all thanks for your time and your comments.
Ok, now this is clear and could be considered "logic" at least to me, so I tried to replace lfs_file_seek() with lfs_file_close() followed by lfs_file_open() - because in my test what I want to do is basically seeking to the beginning of the file - and all the wait time has now moved to lfs_file_close() operation instead of seek, as expected. But what sounds strange to me is the following: First close() operation - marked in green - is taking just 10 ms; so, expanding this sentence to something more detailed, closing a file about 1Mb big, after writing about 1048576 bytes of valid data from the beginning, swapping cache from RAM to FLASH, writing metadata, etc., takes only 10 ms. |
Not sure how much work it would be to skip the cache invalidation on seek if the new seek position is still within the same block that the cache is pointing to anyway. Especially when just seeking backwards one byte it's quite likely that subsquent operations will affect the same block that the cache currently holds. |
Don't know too, but it looks it's not just a matter of cache invalidation, because it occurrs when seeking back by one byte but doesn't occurr when seeking back the whole file. |
EDIT 1: EDIT 2: EDIT 3: |
Today I experimented rolling back version from 2.5.1 to 1.7.2, because some days ago I was checking out issue #214 and I read @diederikvdv has obtained an improvement by switching to LFSv1 (even if I'm not sure from which version to which version he switched - maybe from 2.0.2 to 1.0.6?) but, even if the logs looks different because version 1.7.2 doesn't include LFS_TRACE and I had to implement my own printf to log, I see no improvements in performance. EDIT: |
Ah, I was thinking too low-level. I think this is a symptom of #27. Not sure how I missed that. littlefs's implementation of random-writes could be described as "excruciatingly naive", and you could argue littlefs really don't support random-writes on large files. The file data structure is append-only, so rewriting the beginning of a file ends up writing out the rest of the file contents. This is something I'm looking into fixing, but it requires a new data-structure, which is going to take some time to introduce. Unfortunately there's really not a way around this in the current design.
This is a good question, it's likely the caching can be made smarter. This would need separate pointers for the file and the cache, but that is likely not a big deal. Though hopefully the above issue highlights why this isn't a high priority for me at the moment.
Ah, you can use
This makes sense. v2 mainly involved a redesign of the metadata layout, the data layout remained the same and has the same limitation. I would highly discourage using v1 still as it had a number of fundamental problems, |
Unfortunately these days I'm busy on other tests not involving LittleFS, I'll be back as soon as possibile for further investigation. Just to anticipate something, I have in mind to move validation byte (1 byte written at file beginning) to file end, to see if this changes performance benchmarks dramatically according to this:
|
Hi @geky Using this test sequence, which is presented above for a total file size of 2049 bytes (2048 bytes of data + 1 application-related validation byte at file end), I don't see anomalous wait times anymore.
appears to be false, at least when the file is opened in R-only mode (I didn't do the test in W-only or RW modes), method succeeds but file pointer is not rewind back to file start position. |
I confirm that I'm not saying this is not fine, I'm just saying that I replaced To replace |
I just didn't read the entire thread. BTW as far as I remember (it's over a year ago) searching the next data block is a time consuming process, as lfs has to read the blocks from file start. And this results in reading some (and more) data blocks just to determine the next block, although this information might be read previously (on a previous seek). Example: For a larger file lfs has to read block 1, 4, 9, 15, 19, 25 to determine the block for the current seek position. A small cache might be helpful. Small blocks that only contains the block number and the block chain data. Those were my thoughts over a year ago. Of course, I don't know if they still apply. |
Hi @robekras thanks for your contribution, I think this issue is not related with #631 and related because initially in my tests (please, read before jumping to conclusions) I was seeking from end of file to begin of file (which requires FS to read from Flash with no doubts) and this operation was taking ~ 10ms, then I was writing a single byte and seeking back to begin again, which was more probably not just refreshing cache but re-writing the whole file to Flash, as mentioned by @geky here:
From my point of view, this issue could be closed, I'll continue to run other performance tests but for this specific issue the solution is to avoid to randomly-write a file, preferring linear writes instead. |
For this specific issue the solution is to avoid write files at random position, preferring linear writes instead (from begin to end). |
I think this is the best solution at the moment, for better or worse.
I haven't been able to look into more creative forms of caching. There's probably some low-hanging fruit to be found. But an interesting thing to note about caching here. We can think of the CTZ skip-list as just a tree, since it must be tree shaped if lookups are If we read a file, i.e., traverse each branch of the tree one after the other, a simple least-recently-used (LRU) cache will throw out the root of the tree unless there is enough cache to save the entire height of the tree. Apparently some database people have had success with a sort of most-recently-used (MRU) cache, but that seems like an easy way to get cached entries "stuck" without some sort of periodic eviction. I looked into this a bit for another data structure, and I think if you go deeper you end up needing a full branch-predictor of sorts. Also it's worth noting littlefs is a bit smarter than "reading the whole block". Internally each read has a cache hint so we only read the prefix of pointers at each block. Though if Lines 2787 to 2789 in ec3ec86
|
After experiencing poor performance in regular everyday use of LittleFS v2.5.1 apparently when working with large files (~100kB), I decided to develop a small deterministic test to stress the filesystem and gain a more accurate control of what was going wrong when long wait times occurred (~40 seconds or worse).
This is how my test looks:
After mounting the filesystem (you can see every filesystem configuration detail at the beginning of the log), I open "test.bin" file with LFS_O_RDWR | LFS_O_CREAT | LFS_O_TRUNC options (white), then I write a chunk of 512 bytes (red), I seek back to the beginning of the file (orange), then I write one "magic" byte (yellow) as first byte, I seek back again to the beginning of the file (green), I read 512 bytes back from file (cyan) and verify they match both the magic byte and what I wrote later, then I finally close the file (purple).
I then repeat this test sequence again and again, but on each loop I multiply read/write size by 2, so first iteration writes and checks 512 bytes, second 1024, third 2048, etc. so "test.bin" file is growing exponentially in size during the test.
In the first column of all my logs, on the left, you can see the timestamp in milliseconds of each operation.
This is what happens, sporadically and with no apparent reason:
As you can see, after writing one byte at the beginning of the file, when seeking file back by just one byte takes ~2 seconds.
Here, later:
in the same situation, seeking back by one byte takes ~4 seconds.
I forgot to mention, this test is performed on i.MX RT1052 evaluation board at 600MHz, so it's running directly on the target. I can also share the call stack, during this intervals in which the filesystem looks "freezed" during seek:
From the call stack, you see lfs_file_seek() is invoking my own Filesystem_FlashErase() implementation, so it's erasing the FLASH. I have no idea while the filesystem is erasing during seek, but that's it.
Thanks in advance for your attention
The text was updated successfully, but these errors were encountered: