andreasgal/j2me.js — pull request #640 by mykmelez (2014-11-24 12:22:30+00:00)
Title: improve fs perf with in-memory cache, synchronous calls

This changeset speeds up *fs* API calls by caching files in memory and returning from *fs.write/flush/close* calls without waiting for database writes to complete. It also unrefactors IDB access (merging async_storage.js into libs/fs.js) in preparation for additional improvements to database perf, and it makes a couple other changes, described below.

The memory cache works in the obvious way: when a file is read, the new *Store* abstraction adds it to the cache, and when the file is read again, the *Store* retrieves it from the cache instead of the database. Writes update both the cache and the database. There's no invalidation, so memory usage could be an issue, although I haven't seen too much unbounded growth in my favorite midlet.

Returning from *fs.write/flush/close* calls without waiting for database writes to complete improves the performance of various natives that can now be implemented synchronously. (It isn't only the cost of the database writes themselves that we save; merely resolving a *Promise* also showed up in my profiles.) We can do it because IDB handles locking the datastore and processing the writes in a way that prevents a subsequent read from retrieving old data.

There's also a fix for *TestMediaImage* in here (cc: @marco-c). Before, its *playerUpdate* method would get called three times, all three of which would be counted by the test harness as successful tests. But with these fixes, the cleanup code in the *test* method, which calls `file.delete(); file.close();`, returns quickly, before the second and third calls to *playerUpdate*, so the harness doesn't count them as successful tests.

It seems like the actual problem is that the test doesn't completely clean up after itself. It should also call *removePlayerListener*, and that's what this fix does. With that change, *playerUpdate* only gets called once, and it doesn't matter how long it takes for the file operations to finish.

There's also a *TestPerf* class that writes/flushes to a file a bunch of times. Run it via http://localhost:8000/?main=TestPerf to see what happens: it shows that various operations are now relatively fast, up until the last one, which is waiting on IDB transactions for the 1000 writes/flushes it initiates:

> Time to open file: 26ms
> Time to create file: 2ms
> Time to write/flush to output stream: 241ms
> Time to close output stream: 4ms
> Time to delete file: 5ms
> Time to close file: 1ms
> Time to reopen file: 2ms
> Time to access another file: 7916ms

That numbers are on desktop; apparently IDB is expensive! And Firefox burns CPU time while processing the transactions. So we might want to look into platform issues (cc: @EricRahm).

But coalescing those transactions would also be a good followup. At the very least, we should update data and metadata (stat) in the same transaction, so both succeed if any do (for correctness). It would also make sense to coalesce multiple consecutive updates to the same file. And possibly multiple updates to multiple files, depending on the cost of a transaction.

Finally, we should look at the files that our favorite midlets write and see if any of them are not worth persisting (because they're log files or temporary files). Storing those only in memory (or even not at all) should save us some time.