openfoodfacts / smooth-app

🤳🥫 The new Open Food Facts mobile application for Android and iOS, crafted with Flutter and Dart
https://world.openfoodfacts.org/open-food-facts-mobile-app?utm_source=off&utf_medium=web&utm_campaign=github-repo
Apache License 2.0
827 stars 277 forks source link

Refactor the use of database #55

Closed monsieurtanuki closed 3 years ago

monsieurtanuki commented 3 years ago

For the moment, user preferences are stored in SharedPreferences, which is appropriate, and products are stored in a Sembast database in full_products_database.dart.

I'm not 100% convinced by Sembast, my personal taste is rather SQLite (with a bit of JSON in it), but this can be challenged, of course.

My concern is rather what we do with the database. For the moment, not that much: we populate the database when we download products, but we never query the database.

I think it would be interesting to actually use the database:

stephanegigandet commented 3 years ago

There are 2 distinct uses:

stephanegigandet commented 3 years ago

Regarding the database, the technical document that @PrimaelQuemerais has written for the app has a large section on why we need one and what we want to do with it: https://docs.google.com/document/d/1dOryz657AaAC7feaVMWvzdxB8RQ1Ze8O7PEk1QiJUww/edit#heading=h.u53wgh648h3k

monsieurtanuki commented 3 years ago

Disclaimer: I'm a former dev Oracle DBA, and I spent years creating, loading and querying relational databases with optimization in mind. It doesn't mean I know everything and I'm always right, it means I can be trusted regarding relational database (e.g. SQLite). I don't know that much about NoSQL.

A typical use of the full product database is to get the product attached to a barcode (or products attached to barcodes).

It's a normal use of a relational database, whose core is the "primary key" (a unique and non null value), and which is optimized for that (through the transparent use of "unique index"). A similar behavior can be found in Sembast, with a syntax like await store.record(barcode).put(db, product.toJson()); and final Map<String, dynamic> jsonProduct = await store.record(barcode).get(database) as Map<String, dynamic>;.

The good thing about Sembast is that it supports Map<String, dynamic> as a variable type, and we are already comfortable with JSON (as we get JSON data from internet, and we will store the same data in the database). In SQLite, basically we would have to encode/decode the JSON to/from String.

An additional good thing about Sembast is that, as it stores JSON, we may be able to run queries like that:

await store.find(database,
    finder: Finder(
        filter: Filter.and([
      Filter.matches('product_name', 'Wasa'),
      Filter.greaterThan('ecoscore', 'b'),
    ])));

We couldn't do that easily in SQLite, as we would have to scan the whole product database, decode Strings into JSON and so on...

Let's talk about the second use case: history, favorites and custom lists of products.

Both Sembast and SQLite (and even SharedPreferences) can handle that. I would even say that SQLite is not appropriate for a custom list of products that the end user can reorder: if we want to change the order, we need to change all the records' custom order value. In Sembast, we can just store a List of barcodes, let the end user reorder it, then save in one step the List (List<dynamic> is supported by Sembast). Anyway, I don't think it would make sense to onboard a second database system just for that list management.

It was not my initial choice, but from what I read about Sembast I think it would be perfect for both use cases.

Later, we may find issues with the performances (too much memory involved, too slow queries, too slow app startup), but for the moment I think we can stay with one database system, named Sembast.

PrimaelQuemerais commented 3 years ago

Thank you for the detailed review. Performances was indeed one of the concerns we had while evaluating the databases. I did write a benchmark a while ago but sadly can't find the results back. I recall Sembast was a little bit slow for writing big chunks of data in one shot, nevertheless writing can be done asynchronously without impacting the user experience so we decided to stick with it.

The JSON format and the query system were the reasons why we preferred Sembast over something like Hive which performances were superior but without any custom query possible. The main advantage of storing the data in a JSON format is the ability to use the classes and functions from openfoodfacts-dart, querying products from the local database is done in the exact same way as querying products from the API.

stephanegigandet commented 3 years ago

Regarding the order of lists: I don't really see the use case for letting users re-order manually the lists in a custom order (as in dragging and dropping a list item at a specific place). But there is a big use case to let them display their lists in different orders (e.g. by date of adding the product to the list, by number of days before the products will perish, by number of instances of the product they have consumed / they currently own, by their food preferences, or something else etc.).

One use case to think of: for a given product, we need to know in which lists it belongs to (if any). e.g. you scan a product, and the app tells you it's already in the list "Products in my fridge", or that you scanned it one month ago.

Those use cases can be implemented in many different ways of course.

monsieurtanuki commented 3 years ago

@stephanegigandet OK, we won't reorder the lists. I agree with you: those use cases can be implemented with different way of thinking or database system.

@PrimaelQuemerais Performances are already flagged as problematic with Sembast, then.

My main concern with Sembast is this: "The whole document based database resides in a single file and is loaded in memory when opened." (found here). That means the bigger the database, the slower the database opening and the higher the level of memory used. It stinks.

With SQLite, the startup is "almost" immediate : "Whenever a database is opened, the entire schema is scanned and parsed and a parse tree for the schema is held in memory. That means that database connection startup time and initial memory usage is proportional to the size of the schema." (the schema meaning the metadata about the tables) (found here). That means the database size has no impact on the duration of the database opening and the amount of memory used.

From what I read so far, smoothie's only concern is "I give you a barcode, you give me the JSON". Not something more elaborated like Filter.and([Filter.matches('product_name', 'Wasa'), Filter.greaterThan('ecoscore', 'b')]). In that case we can use SQLite; the only difference is that we need an extra step with the standard conversion json.decode / json.encode. How much more does it cost?

I'm ready to run a test between Sembast and SQLite:

Do you agree with that test, guys?

stephanegigandet commented 3 years ago

@monsieurtanuki Just curious, with SQLite, do we have to pre-declare the maximum size of the JSON when the table is created (e.g. something like VARCHAR(4096), or is that handled automagically?

One thing that would be very useful to test as well: what if we have 10k products, 100k products, or even 1M products. e.g. for offline use, we could do something similar to OsmAnd where you can download maps for offline use for the countries you want. We could for instance have downlable archives of the top 10k or 100k most popular products. Or as you suggested, more specific datasets like all vegetarian products or all products sold in a specific store.

monsieurtanuki commented 3 years ago

@stephanegigandet SQLite does not care about variable types, actually. Could be a number, could be a string (but no specified size). And there's no limit - actually there's one limit: 1 billion bytes. We'll have a problem with VERY fat foods ;)

There shouldn't be a problem with the number of products in SQLite (our main concern will be with the primary key index's size). My first test will be Sembast vs. SQLite, until the first of them crashes. Then, another test until the winner crashes.

PS: funny you mention OsmAnd as I contribute to osmdroid.

monsieurtanuki commented 3 years ago

Guys, we're lucky. I've got a 6-year-old canary-in-the-coalmine smartphone.

My first test crashed with Sembast: I couldn't reopen my 8K product database. While the SQLite was pretty cool. As initially thought, Sembast asks for too much memory when opening the database because it loads all the data.

I'll give you more details tomorrow, but to me it's a NO GO for Sembast.

PrimaelQuemerais commented 3 years ago

Once the functions to store and retrieve a Product from the database are written, Sembast offers little to no advantage over SQLite, except that each time we update the Product class the functions need to be updated aswell, which wasn't the case with Sembast.

The benchmark seems a little bit unfair to me as Sembast would allow to query elements by looking at nested fields in the product, which would not be possible if the data is stored in SQLite in a single field. That being said the chose comes down to which features we actually need. Waiting for the benchmark details :)

monsieurtanuki commented 3 years ago

More details about the benchmark:

Results:

Memory usage with Sembast (I don't have similar screenshot for SQLite, the memory usage is so low it doesn't show) image

Logs with Sembast ``` I/flutter (13533): 1609316762464:sembast version I/flutter (13533): 1609316762465:iteration 0 : begin I/flutter (13533): 1609316762484:opened // 19 ms I/flutter (13533): 1609316762484:store I/flutter (13533): 1609316762485:size1=0 I/flutter (13533): 1609316769307:size2=1000 I/flutter (13533): 1609316769311:first item(string length): 10641 I/flutter (13533): 1609316769313:last item(key count): 16 I/flutter (13533): 1609316769314:closed I/flutter (13533): 1609316769316:dbFileSize=10557636 I/flutter (13533): 1609316769317:iteration 0 : end I/flutter (13533): 1609316769318:iteration 1 : begin I/flutter (13533): 1609316772733:opened // 3 seconds I/flutter (13533): 1609316772734:store I/flutter (13533): 1609316772735:size1=1000 I/flutter (13533): 1609316779567:size2=2000 I/flutter (13533): 1609316779571:first item(string length): 10641 I/flutter (13533): 1609316779572:last item(key count): 16 I/flutter (13533): 1609316779574:closed I/flutter (13533): 1609316779575:dbFileSize=21115246 I/flutter (13533): 1609316779577:iteration 1 : end I/flutter (13533): 1609316779578:iteration 2 : begin I/flutter (13533): 1609316786957:opened // 7 seconds I/flutter (13533): 1609316786958:store I/flutter (13533): 1609316786958:size1=2000 I/flutter (13533): 1609316793928:size2=3000 I/flutter (13533): 1609316793933:first item(string length): 10641 I/flutter (13533): 1609316793934:last item(key count): 16 I/flutter (13533): 1609316793936:closed I/flutter (13533): 1609316793938:dbFileSize=31672856 I/flutter (13533): 1609316793940:iteration 2 : end I/flutter (13533): 1609316793942:iteration 3 : begin I/flutter (13533): 1609316804481:opened // 11 seconds I/flutter (13533): 1609316804482:store I/flutter (13533): 1609316804482:size1=3000 I/flutter (13533): 1609316811239:size2=4000 I/flutter (13533): 1609316811243:first item(string length): 10641 I/flutter (13533): 1609316811244:last item(key count): 16 I/flutter (13533): 1609316811246:closed I/flutter (13533): 1609316811248:dbFileSize=42230466 I/flutter (13533): 1609316811249:iteration 3 : end I/flutter (13533): 1609316811250:iteration 4 : begin I/flutter (13533): 1609316825184:opened // 14 seconds I/flutter (13533): 1609316825185:store I/flutter (13533): 1609316825185:size1=4000 I/flutter (13533): 1609316832017:size2=5000 I/flutter (13533): 1609316832021:first item(string length): 10641 I/flutter (13533): 1609316832023:last item(key count): 16 I/flutter (13533): 1609316832024:closed I/flutter (13533): 1609316832025:dbFileSize=52788076 I/flutter (13533): 1609316832027:iteration 4 : end I/flutter (13533): 1609316832028:iteration 5 : begin I/flutter (13533): 1609316849436:opened // 17 seconds I/flutter (13533): 1609316849437:store I/flutter (13533): 1609316849437:size1=5000 I/flutter (13533): 1609316856134:size2=6000 I/flutter (13533): 1609316856137:first item(string length): 10641 I/flutter (13533): 1609316856138:last item(key count): 16 I/flutter (13533): 1609316856138:closed I/flutter (13533): 1609316856140:dbFileSize=63345686 I/flutter (13533): 1609316856141:iteration 5 : end I/flutter (13533): 1609316856141:iteration 6 : begin I/flutter (13533): 1609316876930:opened // 21 seconds I/flutter (13533): 1609316876931:store I/flutter (13533): 1609316876931:size1=6000 I/flutter (13533): 1609316883713:size2=7000 I/flutter (13533): 1609316883717:first item(string length): 10641 I/flutter (13533): 1609316883718:last item(key count): 16 I/flutter (13533): 1609316883718:closed I/flutter (13533): 1609316883719:dbFileSize=73903296 I/flutter (13533): 1609316883720:iteration 6 : end I/flutter (13533): 1609316883720:iteration 7 : begin I/flutter (13533): 1609316908542:opened // 25 seconds I/flutter (13533): 1609316908544:store I/flutter (13533): 1609316908545:size1=7000 Lost connection to device. ```
Logs with SQLite ``` Launching lib/main.dart on SM G360F in profile mode... Running Gradle task 'assembleProfile'... ✓ Built build/app/outputs/flutter-apk/app-profile.apk (10.4MB). Installing build/app/outputs/flutter-apk/app.apk... Waiting for SM G360F to report its views... Debug service listening on ws://127.0.0.1:51287/DZ2dA52Ledg=/ws I/flutter (20063): 1609317780001:sqlite version (bulk : true) I/flutter (20063): 1609317780001:iteration 0 : begin I/flutter (20063): 1609317780073:opened I/flutter (20063): 1609317780074:store (nothing) I/flutter (20063): 1609317780077:size1=0 I/flutter (20063): 1609317787692:size2=1000 I/flutter (20063): 1609317787697:first item(string length): 10641 I/flutter (20063): 1609317787704:last item(key count): 16 I/flutter (20063): 1609317787712:closed I/flutter (20063): 1609317787714:dbFileSize=11837440 I/flutter (20063): 1609317787715:iteration 0 : end I/flutter (20063): 1609317787715:iteration 1 : begin I/flutter (20063): 1609317787723:opened I/flutter (20063): 1609317787724:store (nothing) I/flutter (20063): 1609317787727:size1=1000 I/flutter (20063): 1609317794838:size2=2000 I/flutter (20063): 1609317794845:first item(string length): 10641 I/flutter (20063): 1609317794851:last item(key count): 16 I/flutter (20063): 1609317794857:closed I/flutter (20063): 1609317794858:dbFileSize=23650304 I/flutter (20063): 1609317794861:iteration 1 : end I/flutter (20063): 1609317794861:iteration 2 : begin I/flutter (20063): 1609317794870:opened I/flutter (20063): 1609317794870:store (nothing) I/flutter (20063): 1609317794874:size1=2000 I/flutter (20063): 1609317802511:size2=3000 I/flutter (20063): 1609317802517:first item(string length): 10641 I/flutter (20063): 1609317802524:last item(key count): 16 I/flutter (20063): 1609317802529:closed I/flutter (20063): 1609317802531:dbFileSize=35459072 I/flutter (20063): 1609317802532:iteration 2 : end I/flutter (20063): 1609317802532:iteration 3 : begin I/flutter (20063): 1609317802541:opened I/flutter (20063): 1609317802542:store (nothing) I/flutter (20063): 1609317802545:size1=3000 I/flutter (20063): 1609317810467:size2=4000 I/flutter (20063): 1609317810474:first item(string length): 10641 I/flutter (20063): 1609317810482:last item(key count): 16 I/flutter (20063): 1609317810488:closed I/flutter (20063): 1609317810490:dbFileSize=47280128 I/flutter (20063): 1609317810490:iteration 3 : end I/flutter (20063): 1609317810491:iteration 4 : begin I/flutter (20063): 1609317810505:opened I/flutter (20063): 1609317810506:store (nothing) I/flutter (20063): 1609317810515:size1=4000 I/flutter (20063): 1609317818688:size2=5000 I/flutter (20063): 1609317818694:first item(string length): 10641 I/flutter (20063): 1609317818702:last item(key count): 16 I/flutter (20063): 1609317818708:closed I/flutter (20063): 1609317818709:dbFileSize=59101184 I/flutter (20063): 1609317818710:iteration 4 : end I/flutter (20063): 1609317818711:iteration 5 : begin I/flutter (20063): 1609317818721:opened I/flutter (20063): 1609317818722:store (nothing) I/flutter (20063): 1609317818726:size1=5000 I/flutter (20063): 1609317826943:size2=6000 I/flutter (20063): 1609317826949:first item(string length): 10641 I/flutter (20063): 1609317826955:last item(key count): 16 I/flutter (20063): 1609317826961:closed I/flutter (20063): 1609317826964:dbFileSize=70909952 I/flutter (20063): 1609317826964:iteration 5 : end I/flutter (20063): 1609317826966:iteration 6 : begin I/flutter (20063): 1609317826975:opened I/flutter (20063): 1609317826976:store (nothing) I/flutter (20063): 1609317826980:size1=6000 I/flutter (20063): 1609317835238:size2=7000 I/flutter (20063): 1609317835244:first item(string length): 10641 I/flutter (20063): 1609317835252:last item(key count): 16 I/flutter (20063): 1609317835258:closed I/flutter (20063): 1609317835260:dbFileSize=82714624 I/flutter (20063): 1609317835260:iteration 6 : end I/flutter (20063): 1609317835261:iteration 7 : begin I/flutter (20063): 1609317835270:opened I/flutter (20063): 1609317835271:store (nothing) I/flutter (20063): 1609317835276:size1=7000 I/flutter (20063): 1609317843367:size2=8000 I/flutter (20063): 1609317843373:first item(string length): 10641 I/flutter (20063): 1609317843379:last item(key count): 16 I/flutter (20063): 1609317843383:closed I/flutter (20063): 1609317843384:dbFileSize=94547968 I/flutter (20063): 1609317843385:iteration 7 : end I/flutter (20063): 1609317843385:iteration 8 : begin I/flutter (20063): 1609317843393:opened I/flutter (20063): 1609317843394:store (nothing) I/flutter (20063): 1609317843398:size1=8000 I/flutter (20063): 1609317850983:size2=9000 I/flutter (20063): 1609317850989:first item(string length): 10641 I/flutter (20063): 1609317850996:last item(key count): 16 I/flutter (20063): 1609317851002:closed I/flutter (20063): 1609317851004:dbFileSize=106340352 I/flutter (20063): 1609317851006:iteration 8 : end I/flutter (20063): 1609317851007:iteration 9 : begin I/flutter (20063): 1609317851017:opened I/flutter (20063): 1609317851018:store (nothing) I/flutter (20063): 1609317851022:size1=9000 I/flutter (20063): 1609317859352:size2=10000 I/flutter (20063): 1609317859357:first item(string length): 10641 I/flutter (20063): 1609317859365:last item(key count): 16 I/flutter (20063): 1609317859371:closed I/flutter (20063): 1609317859373:dbFileSize=118173696 I/flutter (20063): 1609317859374:iteration 9 : end I/flutter (20063): 1609317859374:iteration 10 : begin I/flutter (20063): 1609317859384:opened I/flutter (20063): 1609317859385:store (nothing) I/flutter (20063): 1609317859399:size1=10000 I/art (20063): Background partial concurrent mark sweep GC freed 3058(842KB) AllocSpace objects, 61(1263KB) LOS objects, 29% free, 4MB/6MB, paused 6.488ms total 19.990ms I/flutter (20063): 1609317867597:size2=11000 I/flutter (20063): 1609317867602:first item(string length): 10641 I/flutter (20063): 1609317867608:last item(key count): 16 I/flutter (20063): 1609317867614:closed I/flutter (20063): 1609317867615:dbFileSize=129986560 I/flutter (20063): 1609317867617:iteration 10 : end I/flutter (20063): 1609317867617:iteration 11 : begin I/flutter (20063): 1609317867626:opened I/flutter (20063): 1609317867627:store (nothing) I/flutter (20063): 1609317867631:size1=11000 I/flutter (20063): 1609317876263:size2=12000 I/flutter (20063): 1609317876269:first item(string length): 10641 I/flutter (20063): 1609317876276:last item(key count): 16 I/flutter (20063): 1609317876281:closed I/flutter (20063): 1609317876282:dbFileSize=141791232 I/flutter (20063): 1609317876283:iteration 11 : end I/flutter (20063): 1609317876283:iteration 12 : begin I/flutter (20063): 1609317876292:opened I/flutter (20063): 1609317876292:store (nothing) I/flutter (20063): 1609317876296:size1=12000 I/flutter (20063): 1609317884617:size2=13000 I/flutter (20063): 1609317884622:first item(string length): 10641 I/flutter (20063): 1609317884629:last item(key count): 16 I/flutter (20063): 1609317884635:closed I/flutter (20063): 1609317884636:dbFileSize=153628672 I/flutter (20063): 1609317884637:iteration 12 : end I/flutter (20063): 1609317884637:iteration 13 : begin I/flutter (20063): 1609317884647:opened I/flutter (20063): 1609317884647:store (nothing) I/flutter (20063): 1609317884651:size1=13000 I/flutter (20063): 1609317893552:size2=14000 I/flutter (20063): 1609317893557:first item(string length): 10641 I/flutter (20063): 1609317893564:last item(key count): 16 I/flutter (20063): 1609317893569:closed I/flutter (20063): 1609317893570:dbFileSize=165457920 I/flutter (20063): 1609317893570:iteration 13 : end I/flutter (20063): 1609317893571:iteration 14 : begin I/flutter (20063): 1609317893580:opened I/flutter (20063): 1609317893581:store (nothing) I/flutter (20063): 1609317893586:size1=14000 I/flutter (20063): 1609317901638:size2=15000 I/flutter (20063): 1609317901644:first item(string length): 10641 I/flutter (20063): 1609317901650:last item(key count): 16 I/flutter (20063): 1609317901655:closed I/flutter (20063): 1609317901656:dbFileSize=177262592 I/flutter (20063): 1609317901657:iteration 14 : end I/flutter (20063): 1609317901657:iteration 15 : begin I/flutter (20063): 1609317901665:opened I/flutter (20063): 1609317901666:store (nothing) I/flutter (20063): 1609317901671:size1=15000 I/art (20063): Background sticky concurrent mark sweep GC freed 2106(769KB) AllocSpace objects, 52(1055KB) LOS objects, 26% free, 4MB/6MB, paused 5.141ms total 13.645ms I/flutter (20063): 1609317910807:size2=16000 I/flutter (20063): 1609317910814:first item(string length): 10641 I/flutter (20063): 1609317910821:last item(key count): 16 I/flutter (20063): 1609317910828:closed I/flutter (20063): 1609317910829:dbFileSize=189087744 I/flutter (20063): 1609317910830:iteration 15 : end I/flutter (20063): 1609317910831:iteration 16 : begin I/flutter (20063): 1609317910839:opened I/flutter (20063): 1609317910839:store (nothing) I/flutter (20063): 1609317910844:size1=16000 I/flutter (20063): 1609317919153:size2=17000 I/flutter (20063): 1609317919158:first item(string length): 10641 I/flutter (20063): 1609317919165:last item(key count): 16 I/flutter (20063): 1609317919170:closed I/flutter (20063): 1609317919171:dbFileSize=200937472 I/flutter (20063): 1609317919172:iteration 16 : end I/flutter (20063): 1609317919172:iteration 17 : begin I/flutter (20063): 1609317919180:opened I/flutter (20063): 1609317919181:store (nothing) I/flutter (20063): 1609317919185:size1=17000 I/flutter (20063): 1609317927984:size2=18000 I/flutter (20063): 1609317927990:first item(string length): 10641 I/flutter (20063): 1609317927996:last item(key count): 16 I/flutter (20063): 1609317928001:closed I/flutter (20063): 1609317928003:dbFileSize=212799488 I/flutter (20063): 1609317928004:iteration 17 : end I/flutter (20063): 1609317928004:iteration 18 : begin I/flutter (20063): 1609317928012:opened I/flutter (20063): 1609317928013:store (nothing) I/flutter (20063): 1609317928018:size1=18000 I/flutter (20063): 1609317936854:size2=19000 I/flutter (20063): 1609317936860:first item(string length): 10641 I/flutter (20063): 1609317936866:last item(key count): 16 I/flutter (20063): 1609317936871:closed I/flutter (20063): 1609317936872:dbFileSize=224673792 I/flutter (20063): 1609317936873:iteration 18 : end I/flutter (20063): 1609317936873:iteration 19 : begin I/flutter (20063): 1609317936882:opened I/flutter (20063): 1609317936884:store (nothing) I/flutter (20063): 1609317936889:size1=19000 I/flutter (20063): 1609317945250:size2=20000 I/flutter (20063): 1609317945255:first item(string length): 10641 I/flutter (20063): 1609317945262:last item(key count): 16 I/flutter (20063): 1609317945267:closed I/flutter (20063): 1609317945268:dbFileSize=236523520 I/flutter (20063): 1609317945269:iteration 19 : end I/flutter (20063): 1609317945270:iteration 20 : begin I/flutter (20063): 1609317945279:opened I/flutter (20063): 1609317945279:store (nothing) I/flutter (20063): 1609317945284:size1=20000 I/flutter (20063): 1609317954995:size2=21000 I/flutter (20063): 1609317955000:first item(string length): 10641 I/flutter (20063): 1609317955007:last item(key count): 16 I/flutter (20063): 1609317955012:closed I/flutter (20063): 1609317955013:dbFileSize=248360960 I/flutter (20063): 1609317955014:iteration 20 : end I/flutter (20063): 1609317955014:iteration 21 : begin I/flutter (20063): 1609317955022:opened I/flutter (20063): 1609317955023:store (nothing) I/flutter (20063): 1609317955028:size1=21000 I/flutter (20063): 1609317963971:size2=22000 I/flutter (20063): 1609317963977:first item(string length): 10641 I/flutter (20063): 1609317963984:last item(key count): 16 I/flutter (20063): 1609317963989:closed I/flutter (20063): 1609317963990:dbFileSize=260194304 I/flutter (20063): 1609317963991:iteration 21 : end I/flutter (20063): 1609317963991:iteration 22 : begin I/flutter (20063): 1609317963999:opened I/flutter (20063): 1609317964001:store (nothing) I/flutter (20063): 1609317964007:size1=22000 I/flutter (20063): 1609317972429:size2=23000 I/flutter (20063): 1609317972434:first item(string length): 10641 I/flutter (20063): 1609317972440:last item(key count): 16 I/flutter (20063): 1609317972446:closed I/flutter (20063): 1609317972448:dbFileSize=272044032 I/flutter (20063): 1609317972449:iteration 22 : end I/flutter (20063): 1609317972450:iteration 23 : begin I/flutter (20063): 1609317972459:opened I/flutter (20063): 1609317972461:store (nothing) I/flutter (20063): 1609317972466:size1=23000 I/flutter (20063): 1609317981060:size2=24000 I/flutter (20063): 1609317981067:first item(string length): 10641 I/flutter (20063): 1609317981075:last item(key count): 16 I/flutter (20063): 1609317981080:closed I/flutter (20063): 1609317981082:dbFileSize=283901952 I/flutter (20063): 1609317981082:iteration 23 : end I/flutter (20063): 1609317981082:iteration 24 : begin I/flutter (20063): 1609317981092:opened I/flutter (20063): 1609317981092:store (nothing) I/flutter (20063): 1609317981099:size1=24000 I/flutter (20063): 1609317989998:size2=25000 I/flutter (20063): 1609317990004:first item(string length): 10641 I/flutter (20063): 1609317990010:last item(key count): 16 I/flutter (20063): 1609317990016:closed I/flutter (20063): 1609317990016:dbFileSize=295768064 I/flutter (20063): 1609317990017:iteration 24 : end I/flutter (20063): 1609317990017:iteration 25 : begin I/flutter (20063): 1609317990026:opened I/flutter (20063): 1609317990027:store (nothing) I/flutter (20063): 1609317990033:size1=25000 I/flutter (20063): 1609317999014:size2=26000 I/flutter (20063): 1609317999020:first item(string length): 10641 I/flutter (20063): 1609317999026:last item(key count): 16 I/flutter (20063): 1609317999032:closed I/flutter (20063): 1609317999033:dbFileSize=307625984 I/flutter (20063): 1609317999034:iteration 25 : end I/flutter (20063): 1609317999035:iteration 26 : begin I/flutter (20063): 1609317999044:opened I/flutter (20063): 1609317999045:store (nothing) I/flutter (20063): 1609317999050:size1=26000 I/flutter (20063): 1609318008384:size2=27000 I/flutter (20063): 1609318008389:first item(string length): 10641 I/flutter (20063): 1609318008395:last item(key count): 16 I/flutter (20063): 1609318008400:closed I/flutter (20063): 1609318008401:dbFileSize=319483904 I/flutter (20063): 1609318008402:iteration 26 : end I/flutter (20063): 1609318008402:iteration 27 : begin I/flutter (20063): 1609318008411:opened I/flutter (20063): 1609318008412:store (nothing) I/flutter (20063): 1609318008417:size1=27000 I/flutter (20063): 1609318019836:size2=28000 I/flutter (20063): 1609318019843:first item(string length): 10641 I/flutter (20063): 1609318019852:last item(key count): 16 I/flutter (20063): 1609318019858:closed I/flutter (20063): 1609318019860:dbFileSize=331350016 I/flutter (20063): 1609318019861:iteration 27 : end I/flutter (20063): 1609318019861:iteration 28 : begin I/flutter (20063): 1609318019871:opened I/flutter (20063): 1609318019871:store (nothing) I/flutter (20063): 1609318019877:size1=28000 I/flutter (20063): 1609318028950:size2=29000 I/flutter (20063): 1609318028956:first item(string length): 10641 I/flutter (20063): 1609318028963:last item(key count): 16 I/flutter (20063): 1609318028969:closed I/flutter (20063): 1609318028971:dbFileSize=343199744 I/flutter (20063): 1609318028972:iteration 28 : end I/flutter (20063): 1609318028972:iteration 29 : begin I/flutter (20063): 1609318028982:opened I/flutter (20063): 1609318028983:store (nothing) I/flutter (20063): 1609318028989:size1=29000 I/flutter (20063): 1609318039114:size2=30000 I/flutter (20063): 1609318039119:first item(string length): 10641 I/flutter (20063): 1609318039125:last item(key count): 16 I/flutter (20063): 1609318039130:closed I/flutter (20063): 1609318039133:dbFileSize=355065856 I/flutter (20063): 1609318039134:iteration 29 : end I/flutter (20063): 1609318039135:iteration 30 : begin I/flutter (20063): 1609318039147:opened I/flutter (20063): 1609318039147:store (nothing) I/flutter (20063): 1609318039153:size1=30000 I/flutter (20063): 1609318049842:size2=31000 I/flutter (20063): 1609318049848:first item(string length): 10641 I/flutter (20063): 1609318049855:last item(key count): 16 I/flutter (20063): 1609318049861:closed I/flutter (20063): 1609318049862:dbFileSize=366940160 I/flutter (20063): 1609318049862:iteration 30 : end I/flutter (20063): 1609318049863:iteration 31 : begin I/flutter (20063): 1609318049871:opened I/flutter (20063): 1609318049872:store (nothing) I/flutter (20063): 1609318049878:size1=31000 I/flutter (20063): 1609318060015:size2=32000 I/flutter (20063): 1609318060020:first item(string length): 10641 I/flutter (20063): 1609318060026:last item(key count): 16 I/flutter (20063): 1609318060032:closed I/flutter (20063): 1609318060033:dbFileSize=378793984 I/flutter (20063): 1609318060034:iteration 31 : end I/flutter (20063): 1609318060034:iteration 32 : begin I/flutter (20063): 1609318060042:opened I/flutter (20063): 1609318060043:store (nothing) I/flutter (20063): 1609318060049:size1=32000 I/flutter (20063): 1609318069874:size2=33000 I/flutter (20063): 1609318069882:first item(string length): 10641 I/flutter (20063): 1609318069889:last item(key count): 16 I/flutter (20063): 1609318069896:closed I/flutter (20063): 1609318069897:dbFileSize=390660096 I/flutter (20063): 1609318069898:iteration 32 : end I/flutter (20063): 1609318069899:iteration 33 : begin I/flutter (20063): 1609318069908:opened I/flutter (20063): 1609318069910:store (nothing) I/flutter (20063): 1609318069918:size1=33000 I/flutter (20063): 1609318079694:size2=34000 I/flutter (20063): 1609318079699:first item(string length): 10641 I/flutter (20063): 1609318079705:last item(key count): 16 I/flutter (20063): 1609318079710:closed I/flutter (20063): 1609318079712:dbFileSize=402530304 I/flutter (20063): 1609318079713:iteration 33 : end I/flutter (20063): 1609318079714:iteration 34 : begin I/flutter (20063): 1609318079722:opened I/flutter (20063): 1609318079723:store (nothing) I/flutter (20063): 1609318079730:size1=34000 I/flutter (20063): 1609318090633:size2=35000 I/flutter (20063): 1609318090638:first item(string length): 10641 I/flutter (20063): 1609318090645:last item(key count): 16 I/flutter (20063): 1609318090652:closed I/flutter (20063): 1609318090655:dbFileSize=414396416 I/flutter (20063): 1609318090655:iteration 34 : end I/flutter (20063): 1609318090656:iteration 35 : begin I/flutter (20063): 1609318090666:opened I/flutter (20063): 1609318090667:store (nothing) I/flutter (20063): 1609318090674:size1=35000 I/flutter (20063): 1609318100402:size2=36000 I/flutter (20063): 1609318100408:first item(string length): 10641 I/flutter (20063): 1609318100416:last item(key count): 16 I/flutter (20063): 1609318100421:closed I/flutter (20063): 1609318100422:dbFileSize=426303488 I/flutter (20063): 1609318100423:iteration 35 : end I/flutter (20063): 1609318100423:iteration 36 : begin I/flutter (20063): 1609318100432:opened I/flutter (20063): 1609318100433:store (nothing) I/flutter (20063): 1609318100439:size1=36000 I/art (20063): Background partial concurrent mark sweep GC freed 3094(771KB) AllocSpace objects, 62(1249KB) LOS objects, 29% free, 4MB/6MB, paused 5.091ms total 24.268ms I/flutter (20063): 1609318111134:size2=37000 I/flutter (20063): 1609318111139:first item(string length): 10641 I/flutter (20063): 1609318111145:last item(key count): 16 I/flutter (20063): 1609318111152:closed I/flutter (20063): 1609318111154:dbFileSize=438210560 I/flutter (20063): 1609318111154:iteration 36 : end I/flutter (20063): 1609318111154:iteration 37 : begin I/flutter (20063): 1609318111163:opened I/flutter (20063): 1609318111164:store (nothing) I/flutter (20063): 1609318111171:size1=37000 I/flutter (20063): 1609318122732:size2=38000 I/flutter (20063): 1609318122738:first item(string length): 10641 I/flutter (20063): 1609318122745:last item(key count): 16 I/flutter (20063): 1609318122751:closed I/flutter (20063): 1609318122752:dbFileSize=450060288 I/flutter (20063): 1609318122753:iteration 37 : end I/flutter (20063): 1609318122753:iteration 38 : begin I/flutter (20063): 1609318122762:opened I/flutter (20063): 1609318122762:store (nothing) I/flutter (20063): 1609318122770:size1=38000 I/flutter (20063): 1609318133485:size2=39000 I/flutter (20063): 1609318133491:first item(string length): 10641 I/flutter (20063): 1609318133498:last item(key count): 16 I/flutter (20063): 1609318133503:closed I/flutter (20063): 1609318133505:dbFileSize=461914112 I/flutter (20063): 1609318133506:iteration 38 : end I/flutter (20063): 1609318133506:iteration 39 : begin I/flutter (20063): 1609318133514:opened I/flutter (20063): 1609318133515:store (nothing) I/flutter (20063): 1609318133521:size1=39000 I/flutter (20063): 1609318143695:size2=40000 I/flutter (20063): 1609318143703:first item(string length): 10641 I/flutter (20063): 1609318143710:last item(key count): 16 I/flutter (20063): 1609318143717:closed I/flutter (20063): 1609318143718:dbFileSize=473747456 I/flutter (20063): 1609318143719:iteration 39 : end I/flutter (20063): 1609318143720:iteration 40 : begin I/flutter (20063): 1609318143731:opened I/flutter (20063): 1609318143732:store (nothing) I/flutter (20063): 1609318143740:size1=40000 I/flutter (20063): 1609318155096:size2=41000 I/flutter (20063): 1609318155102:first item(string length): 10641 I/flutter (20063): 1609318155109:last item(key count): 16 I/flutter (20063): 1609318155114:closed I/flutter (20063): 1609318155115:dbFileSize=485580800 I/flutter (20063): 1609318155115:iteration 40 : end I/flutter (20063): 1609318155116:iteration 41 : begin I/flutter (20063): 1609318155124:opened I/flutter (20063): 1609318155125:store (nothing) I/flutter (20063): 1609318155132:size1=41000 I/flutter (20063): 1609318165135:size2=42000 I/flutter (20063): 1609318165140:first item(string length): 10641 I/flutter (20063): 1609318165147:last item(key count): 16 I/flutter (20063): 1609318165153:closed I/flutter (20063): 1609318165154:dbFileSize=497414144 I/flutter (20063): 1609318165155:iteration 41 : end I/flutter (20063): 1609318165155:iteration 42 : begin I/flutter (20063): 1609318165164:opened I/flutter (20063): 1609318165165:store (nothing) I/flutter (20063): 1609318165172:size1=42000 I/flutter (20063): 1609318176934:size2=43000 I/flutter (20063): 1609318176939:first item(string length): 10641 I/flutter (20063): 1609318176946:last item(key count): 16 I/flutter (20063): 1609318176951:closed I/flutter (20063): 1609318176953:dbFileSize=509288448 I/flutter (20063): 1609318176953:iteration 42 : end I/flutter (20063): 1609318176953:iteration 43 : begin I/flutter (20063): 1609318176961:opened I/flutter (20063): 1609318176963:store (nothing) I/flutter (20063): 1609318176970:size1=43000 I/flutter (20063): 1609318188849:size2=44000 I/flutter (20063): 1609318188857:first item(string length): 10641 I/flutter (20063): 1609318188864:last item(key count): 16 I/flutter (20063): 1609318188869:closed I/flutter (20063): 1609318188871:dbFileSize=521166848 I/flutter (20063): 1609318188872:iteration 43 : end I/flutter (20063): 1609318188872:iteration 44 : begin I/flutter (20063): 1609318188880:opened I/flutter (20063): 1609318188880:store (nothing) I/flutter (20063): 1609318188888:size1=44000 I/flutter (20063): 1609318200050:size2=45000 I/flutter (20063): 1609318200056:first item(string length): 10641 I/flutter (20063): 1609318200065:last item(key count): 16 I/flutter (20063): 1609318200070:closed I/flutter (20063): 1609318200072:dbFileSize=533024768 I/flutter (20063): 1609318200073:iteration 44 : end I/flutter (20063): 1609318200073:iteration 45 : begin I/flutter (20063): 1609318200082:opened I/flutter (20063): 1609318200083:store (nothing) I/flutter (20063): 1609318200091:size1=45000 I/flutter (20063): 1609318213460:size2=46000 I/flutter (20063): 1609318213466:first item(string length): 10641 I/flutter (20063): 1609318213472:last item(key count): 16 I/flutter (20063): 1609318213478:closed I/flutter (20063): 1609318213479:dbFileSize=544940032 I/flutter (20063): 1609318213479:iteration 45 : end I/flutter (20063): 1609318213479:iteration 46 : begin I/flutter (20063): 1609318213488:opened I/flutter (20063): 1609318213488:store (nothing) I/flutter (20063): 1609318213496:size1=46000 I/flutter (20063): 1609318225616:size2=47000 I/flutter (20063): 1609318225623:first item(string length): 10641 I/flutter (20063): 1609318225631:last item(key count): 16 I/flutter (20063): 1609318225636:closed I/flutter (20063): 1609318225637:dbFileSize=556830720 I/flutter (20063): 1609318225638:iteration 46 : end I/flutter (20063): 1609318225638:iteration 47 : begin I/flutter (20063): 1609318225648:opened I/flutter (20063): 1609318225648:store (nothing) I/flutter (20063): 1609318225660:size1=47000 I/flutter (20063): 1609318236743:size2=48000 I/flutter (20063): 1609318236749:first item(string length): 10641 I/flutter (20063): 1609318236755:last item(key count): 16 I/flutter (20063): 1609318236761:closed I/flutter (20063): 1609318236763:dbFileSize=568688640 I/flutter (20063): 1609318236764:iteration 47 : end I/flutter (20063): 1609318236764:iteration 48 : begin I/flutter (20063): 1609318236773:opened I/flutter (20063): 1609318236774:store (nothing) I/flutter (20063): 1609318236782:size1=48000 I/flutter (20063): 1609318247102:size2=49000 I/flutter (20063): 1609318247107:first item(string length): 10641 I/flutter (20063): 1609318247113:last item(key count): 16 I/flutter (20063): 1609318247118:closed I/flutter (20063): 1609318247120:dbFileSize=580530176 I/flutter (20063): 1609318247120:iteration 48 : end I/flutter (20063): 1609318247120:iteration 49 : begin I/flutter (20063): 1609318247131:opened I/flutter (20063): 1609318247131:store (nothing) I/flutter (20063): 1609318247139:size1=49000 I/flutter (20063): 1609318258442:size2=50000 I/flutter (20063): 1609318258456:first item(string length): 10641 I/flutter (20063): 1609318258463:last item(key count): 16 I/flutter (20063): 1609318258469:closed I/flutter (20063): 1609318258470:dbFileSize=592375808 I/flutter (20063): 1609318258471:iteration 49 : end I/flutter (20063): 1609318258472:iteration 50 : begin I/flutter (20063): 1609318258480:opened I/flutter (20063): 1609318258481:store (nothing) I/flutter (20063): 1609318258489:size1=50000 I/flutter (20063): 1609318269685:size2=51000 I/flutter (20063): 1609318269690:first item(string length): 10641 I/flutter (20063): 1609318269696:last item(key count): 16 I/flutter (20063): 1609318269702:closed I/flutter (20063): 1609318269703:dbFileSize=604196864 I/flutter (20063): 1609318269704:iteration 50 : end I/flutter (20063): 1609318269705:iteration 51 : begin I/flutter (20063): 1609318269714:opened I/flutter (20063): 1609318269715:store (nothing) I/flutter (20063): 1609318269722:size1=51000 I/art (20063): Background partial concurrent mark sweep GC freed 2929(742KB) AllocSpace objects, 59(1199KB) LOS objects, 29% free, 4MB/6MB, paused 5.226ms total 18.277ms I/flutter (20063): 1609318279572:size2=52000 I/flutter (20063): 1609318279577:first item(string length): 10641 I/flutter (20063): 1609318279585:last item(key count): 16 I/flutter (20063): 1609318279592:closed I/flutter (20063): 1609318279594:dbFileSize=616038400 I/flutter (20063): 1609318279594:iteration 51 : end I/flutter (20063): 1609318279594:iteration 52 : begin I/flutter (20063): 1609318279605:opened I/flutter (20063): 1609318279606:store (nothing) I/flutter (20063): 1609318279616:size1=52000 I/flutter (20063): 1609318291348:size2=53000 // 12 seconds I/flutter (20063): 1609318291354:first item(string length): 10641 I/flutter (20063): 1609318291361:last item(key count): 16 I/flutter (20063): 1609318291366:closed I/flutter (20063): 1609318291367:dbFileSize=627884032 I/flutter (20063): 1609318291368:iteration 52 : end I/flutter (20063): 1609318291368:iteration 53 : begin I/flutter (20063): 1609318291377:opened I/flutter (20063): 1609318291377:store (nothing) I/flutter (20063): 1609318291385:size1=53000 I/flutter (20063): 1609318307319:size2=54000 // 16 seconds I/flutter (20063): 1609318307325:first item(string length): 10641 I/flutter (20063): 1609318307331:last item(key count): 16 I/flutter (20063): 1609318307336:closed I/flutter (20063): 1609318307337:dbFileSize=639758336 I/flutter (20063): 1609318307338:iteration 53 : end I/flutter (20063): 1609318307339:iteration 54 : begin I/flutter (20063): 1609318307347:opened I/flutter (20063): 1609318307348:store (nothing) I/flutter (20063): 1609318307356:size1=54000 I/flutter (20063): 1609318317495:size2=55000 // 10 seconds I/flutter (20063): 1609318317501:first item(string length): 10641 I/flutter (20063): 1609318317507:last item(key count): 16 I/flutter (20063): 1609318317513:closed I/flutter (20063): 1609318317514:dbFileSize=651628544 I/flutter (20063): 1609318317515:iteration 54 : end I/flutter (20063): 1609318317515:iteration 55 : begin I/flutter (20063): 1609318317524:opened I/flutter (20063): 1609318317524:store (nothing) I/flutter (20063): 1609318317533:size1=55000 I/flutter (20063): 1609318328724:size2=56000 // 11 seconds I/flutter (20063): 1609318328729:first item(string length): 10641 I/flutter (20063): 1609318328736:last item(key count): 16 I/flutter (20063): 1609318328741:closed I/flutter (20063): 1609318328742:dbFileSize=663543808 I/flutter (20063): 1609318328743:iteration 55 : end I/flutter (20063): 1609318328743:iteration 56 : begin I/flutter (20063): 1609318328752:opened I/flutter (20063): 1609318328752:store (nothing) I/flutter (20063): 1609318328761:size1=56000 I/flutter (20063): 1609318340411:size2=57000 // 12 seconds I/flutter (20063): 1609318340416:first item(string length): 10641 I/flutter (20063): 1609318340422:last item(key count): 16 I/flutter (20063): 1609318340428:closed I/flutter (20063): 1609318340429:dbFileSize=675430400 I/flutter (20063): 1609318340429:iteration 56 : end I/flutter (20063): 1609318340430:iteration 57 : begin I/flutter (20063): 1609318340438:opened I/flutter (20063): 1609318340438:store (nothing) I/flutter (20063): 1609318340447:size1=57000 E/flutter (20063): [ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: DatabaseException(database or disk is full (code 13)) sql 'COMMIT' args []} E/flutter (20063): #0 wrapDatabaseException (package:sqflite/src/exception_impl.dart:11) E/flutter (20063): E/flutter (20063): #1 SqfliteDatabaseMixin.txnSynchronized (package:sqflite_common/src/database_mixin.dart:312) E/flutter (20063): E/flutter (20063): #2 SqfliteDatabaseMixin.endTransaction (package:sqflite_common/src/database_mixin.dart:459) E/flutter (20063): E/flutter (20063): #3 SqfliteDatabaseMixin._runTransaction (package:sqflite_common/src/database_mixin.dart:481) E/flutter (20063): E/flutter (20063): #4 BasicLock.synchronized (package:synchronized/src/basic_lock.dart:34) E/flutter (20063): E/flutter (20063): #5 SqfliteDatabaseMixin.txnSynchronized (package:sqflite_common/src/database_mixin.dart:337) E/flutter (20063): E/flutter (20063): #6 _MyHomePageState._sqliteVersion (package:smooth_profiler/main.dart:68) E/flutter (20063): E/flutter (20063): I/Timeline(20063): Timeline: Activity_idle id: android.os.BinderProxy@9a3b585 time:87359870 ```
flutter code ```dart import 'dart:convert'; import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart'; import 'package:flutter/material.dart'; import 'package:sembast/sembast.dart' as sembast; import 'package:sembast/sembast_io.dart' as sembast; import 'package:sqflite/sqflite.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Counter App', home: MyHomePage(title: 'Counter App Home Page'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State { int _counter = 0; void _sqliteVersion(final dynamic inputProducts, final String dbPath) async { const bool bulkOr1by1 = true; const String TABLE_NAME = 'Test'; const String COLUMN_KEY = 'barcode'; const String COLUMN_VALUE = 'myjson'; String firstKey; String lastKey; // cf. https://pub.dev/packages/sqflite myprint('sqlite version (bulk : $bulkOr1by1)'); for (int ite = 0; ite < 100; ite++) { // 100 instead of 10! myprint('iteration $ite : begin'); final Database database = await openDatabase(dbPath, version: 1, onCreate: (Database db, int version) async { // When creating the db, create the table await db.execute( 'CREATE TABLE $TABLE_NAME ($COLUMN_KEY string PRIMARY KEY, $COLUMN_VALUE string)'); }); myprint('opened'); myprint('store (nothing)'); int count = Sqflite.firstIntValue( await database.rawQuery('SELECT COUNT(*) FROM $TABLE_NAME')); myprint('size1=$count'); for (int i = 0; i < 10; i++) { final int timestamp = nowInMillis(); if (bulkOr1by1) { // transaction version await database.transaction((txn) async { for (final dynamic inputProduct in inputProducts) { final String fakeKey = '${inputProduct['code']}/$timestamp'; if (firstKey == null) { firstKey = fakeKey; } lastKey = fakeKey; await txn.insert( TABLE_NAME, {COLUMN_KEY: fakeKey, COLUMN_VALUE: json.encode(inputProduct)}, ); } }); } else { // one by one version for (final dynamic inputProduct in inputProducts) { final String fakeKey = '${inputProduct['code']}/$timestamp'; if (firstKey == null) { firstKey = fakeKey; } lastKey = fakeKey; await database.insert(TABLE_NAME, {COLUMN_KEY: fakeKey, COLUMN_VALUE: json.encode(inputProduct)}); } } } count = Sqflite.firstIntValue( await database.rawQuery('SELECT COUNT(*) FROM $TABLE_NAME')); myprint('size2=$count'); List> result; result = await database.query( TABLE_NAME, columns: [COLUMN_VALUE], where: '$COLUMN_KEY = ?', whereArgs: [firstKey], ); myprint('first item(string length): ${result[0][COLUMN_VALUE].length}'); result = await database.query( TABLE_NAME, columns: [COLUMN_VALUE], where: '$COLUMN_KEY = ?', whereArgs: [lastKey], ); myprint( 'last item(key count): ${json.decode(result[0][COLUMN_VALUE]).length}'); await database.close(); myprint('closed'); final File dbFile = File(dbPath); final int dbFileSize = await dbFile.length(); myprint('dbFileSize=$dbFileSize'); myprint('iteration $ite : end'); } } void _sembastVersion(final dynamic inputProducts, final String dbPath) async { String firstKey; String lastKey; final sembast.DatabaseFactory dbFactory = sembast.databaseFactoryIo; myprint('sembast version'); for (int ite = 0; ite < 10; ite++) { myprint('iteration $ite : begin'); final sembast.Database db = await dbFactory.openDatabase(dbPath); myprint('opened'); final sembast.StoreRef store = sembast.StoreRef.main(); myprint('store'); int size = await store.count(db); myprint('size1=$size'); for (int i = 0; i < 10; i++) { final int timestamp = nowInMillis(); for (final dynamic inputProduct in inputProducts) { final String fakeKey = '${inputProduct['code']}/$timestamp'; if (firstKey == null) { firstKey = fakeKey; } lastKey = fakeKey; await store.record(fakeKey).put(db, inputProduct); } } size = await store.count(db); myprint('size2=$size'); dynamic result; result = await store.record(firstKey).get(db); myprint('first item(string length): ${json.encode(result).length}'); result = await store.record(lastKey).get(db); myprint('last item(key count): ${result.length}'); await db.close(); myprint('closed'); final File dbFile = File(dbPath); final int dbFileSize = await dbFile.length(); myprint('dbFileSize=$dbFileSize'); myprint('iteration $ite : end'); } } void myprint(final String message) { const int COLSIZE = 1000; final String result = '${nowInMillis()}:$message'; int i = 0; final int max = result.length; while (i < max) { int j = i + COLSIZE; if (j >= max) { j = max; } print(result.substring(i, j)); i += COLSIZE; } } int nowInMillis() => DateTime.now().millisecondsSinceEpoch; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'You have pushed the button this many times:', ), Text( '$_counter', // Provide a Key to this specific Text widget. This allows // identifying the widget from inside the test suite, // and reading the text. key: Key('counter'), style: Theme.of(context).textTheme.headline4, ), ], ), ), floatingActionButton: FloatingActionButton( // Provide a Key to this button. This allows finding this // specific button inside the test suite, and tapping it. key: Key('increment'), onPressed: () async { final String inputString = await DefaultAssetBundle.of(context) .loadString('assets/smoothTest.json'); final dynamic inputJson = json.decode(inputString); final dynamic inputProducts = inputJson['products']; String directory = await getDatabasesPath(); directory = (await getExternalStorageDirectory()).path; final String dbPath = join(directory, 'sample.db'); // from scratch try { final File file = File(dbPath); print('file:$file'); await file.delete(); } catch (e) { // we don't care } //_sembastVersion(inputProducts, dbPath); _sqliteVersion(inputProducts, dbPath); }, tooltip: 'Increment', child: Icon(Icons.add), ), ); } } ```
stephanegigandet commented 3 years ago

Thanks for all the tests!

  • Saving 1000 new products in the database (duration)

    • Sembast: always around 7s
    • SQLite: always around 12s (there's an extra json.decode step to store as a String btw)

I wonder if those 12 seconds are mostly due to the json.encode. What if we want to insert 10k products (from a downloadable file with the most popular products for instance), where each product data is available as a json string. Could you check how long it would take?

monsieurtanuki commented 3 years ago

@stephanegigandet I relaunched the same SQLite test: this time loading 1K product took around 8s. And when I used json.encode only once every 100 products (instead of for every product), loading 1K product took around 5.5 s. For the details: when I load 1K products I run a 10-time loop of running a transaction of 100 insert statements.

I think the database insert performances are not as important as the database opening:

For the record, what a product look like, data-wise (10Kb when you remove the spaces) ```json { "product_name": "Salmón ahumado", "lang": "en", "brands": "Ocean Sea", "serving_size": "50g", "nutrition_grade_fr": "d", "code": "20034658", "selected_images": { "front": { "thumb": { "fr": "https://static.openfoodfacts.org/images/products/20034658/front_fr.98.100.jpg" }, "display": { "fr": "https://static.openfoodfacts.org/images/products/20034658/front_fr.98.400.jpg" }, "small": { "fr": "https://static.openfoodfacts.org/images/products/20034658/front_fr.98.200.jpg" } }, "ingredients": { "small": { "es": "https://static.openfoodfacts.org/images/products/20034658/ingredients_es.78.200.jpg", "fr": "https://static.openfoodfacts.org/images/products/20034658/ingredients_fr.83.200.jpg" }, "thumb": { "es": "https://static.openfoodfacts.org/images/products/20034658/ingredients_es.78.100.jpg", "fr": "https://static.openfoodfacts.org/images/products/20034658/ingredients_fr.83.100.jpg" }, "display": { "es": "https://static.openfoodfacts.org/images/products/20034658/ingredients_es.78.400.jpg", "fr": "https://static.openfoodfacts.org/images/products/20034658/ingredients_fr.83.400.jpg" } }, "nutrition": { "display": { "fr": "https://static.openfoodfacts.org/images/products/20034658/nutrition_fr.93.400.jpg" }, "thumb": { "fr": "https://static.openfoodfacts.org/images/products/20034658/nutrition_fr.93.100.jpg" }, "small": { "fr": "https://static.openfoodfacts.org/images/products/20034658/nutrition_fr.93.200.jpg" } } }, "nutriments": { "fat_100g": 10, "sodium": 1.28, "energy_100g": 728, "saturated-fat_100g": 1.5, "nova-group_100g": 3, "sugars_unit": "g", "sugars": 0, "fiber_serving": 0, "fat_unit": "g", "fat": 10, "proteins": 21, "sodium_unit": "g", "proteins_value": 21, "salt_serving": 1.6, "saturated-fat_value": 1.5, "energy_unit": "kcal", "nutrition-score-fr_100g": 13, "nutrition-score-fr": 13, "energy-kcal": 174, "energy_serving": 364, "energy": 728, "nova-group": 3, "energy-kcal_value": 174, "fiber_unit": "g", "sodium_100g": 1.28, "carbohydrates_value": 0, "saturated-fat_serving": 0.75, "sodium_value": 1.28, "energy-kcal_100g": 174, "salt_100g": 3.2, "sugars_100g": 0, "nova-group_serving": 3, "proteins_serving": 10.5, "salt_unit": "g", "fat_serving": 5, "energy_value": 174, "fiber": 0, "fiber_value": 0, "fruits-vegetables-nuts-estimate-from-ingredients_100g": 0, "salt": 3.2, "carbohydrates_100g": 0, "saturated-fat_unit": "g", "salt_value": 3.2, "carbon-footprint-from-known-ingredients_serving": 321, "saturated-fat": 1.5, "proteins_unit": "g", "carbon-footprint-from-known-ingredients_product": 1280, "energy-kcal_serving": 87, "carbohydrates_serving": 0, "fiber_100g": 0, "carbohydrates_unit": "g", "energy-kcal_unit": "kcal", "proteins_100g": 21, "fat_value": 10, "carbohydrates": 0, "sugars_serving": 0, "sugars_value": 0, "sodium_serving": 0.64 }, "quantity": "200gr", "attribute_groups": [{ "attributes": [{ "id": "nutriscore", "title": "Nutri-Score D", "name": "Nutri-Score", "description_short": "Poor nutritional quality", "match": 32.5, "icon_url": "https://static.openfoodfacts.org/images/misc/nutriscore-d.svg", "description": "", "status": "known" }, { "icon_url": "https://static.openfoodfacts.org/images/icons/nutrient-level-salt-high.svg", "match": 0, "description_short": "3.2 g / 100 g", "status": "known", "id": "low_salt", "name": "Salt", "title": "Salt in high quantity" }, { "name": "Fat", "title": "Fat in moderate quantity", "id": "low_fat", "status": "known", "icon_url": "https://static.openfoodfacts.org/images/icons/nutrient-level-fat-medium.svg", "description_short": "10 g / 100 g", "match": 55.2941176470588 }, { "title": "Sugars in low quantity", "name": "Sugars", "id": "low_sugars", "status": "known", "match": 100, "description_short": "0 g / 100 g", "icon_url": "https://static.openfoodfacts.org/images/icons/nutrient-level-sugars-low.svg" }, { "status": "known", "icon_url": "https://static.openfoodfacts.org/images/icons/nutrient-level-saturated-fat-medium.svg", "match": 80, "description_short": "1.5 g / 100 g", "title": "Saturated fat in moderate quantity", "name": "Saturated fat", "id": "low_saturated_fat" }], "id": "nutritional_quality", "name": "Nutritional quality" }, { "id": "allergens", "attributes": [{ "title": "Does not contain: Milk", "name": "Milk", "id": "allergens_no_milk", "debug": "3 ingredients (0 unknown)", "status": "known", "icon_url": "https://static.openfoodfacts.org/images/icons/no-milk.svg", "match": 100 }, { "status": "known", "icon_url": "https://static.openfoodfacts.org/images/icons/no-gluten.svg", "match": 100, "name": "Gluten", "title": "Does not contain: Gluten", "id": "allergens_no_gluten", "debug": "3 ingredients (0 unknown)" }, { "status": "known", "match": 100, "icon_url": "https://static.openfoodfacts.org/images/icons/no-celery.svg", "title": "Does not contain: Celery", "name": "Celery", "id": "allergens_no_celery", "debug": "3 ingredients (0 unknown)" }, { "title": "Does not contain: Soybeans", "name": "Soybeans", "debug": "3 ingredients (0 unknown)", "id": "allergens_no_soybeans", "status": "known", "icon_url": "https://static.openfoodfacts.org/images/icons/no-soybeans.svg", "match": 100 }, { "status": "known", "icon_url": "https://static.openfoodfacts.org/images/icons/no-mustard.svg", "match": 100, "name": "Mustard", "title": "Does not contain: Mustard", "debug": "3 ingredients (0 unknown)", "id": "allergens_no_mustard" }, { "icon_url": "https://static.openfoodfacts.org/images/icons/no-none.svg", "match": 100, "status": "known", "debug": "3 ingredients (0 unknown)", "id": "allergens_no_none", "title": "Does not contain: None", "name": "None" }, { "name": "Fish", "title": "Does not contain: Fish", "id": "allergens_no_fish", "debug": "3 ingredients (0 unknown)", "status": "known", "icon_url": "https://static.openfoodfacts.org/images/icons/no-fish.svg", "match": 100 }, { "name": "Molluscs", "title": "Does not contain: Molluscs", "debug": "3 ingredients (0 unknown)", "id": "allergens_no_molluscs", "status": "known", "icon_url": "https://static.openfoodfacts.org/images/icons/no-molluscs.svg", "match": 100 }, { "debug": "3 ingredients (0 unknown)", "id": "allergens_no_crustaceans", "title": "Does not contain: Crustaceans", "name": "Crustaceans", "icon_url": "https://static.openfoodfacts.org/images/icons/no-crustaceans.svg", "match": 100, "status": "known" }, { "debug": "3 ingredients (0 unknown)", "id": "allergens_no_sesame_seeds", "title": "Does not contain: Sesame seeds", "name": "Sesame seeds", "match": 100, "icon_url": "https://static.openfoodfacts.org/images/icons/no-sesame-seeds.svg", "status": "known" }, { "match": 100, "icon_url": "https://static.openfoodfacts.org/images/icons/no-peanuts.svg", "status": "known", "debug": "3 ingredients (0 unknown)", "id": "allergens_no_peanuts", "title": "Does not contain: Peanuts", "name": "Peanuts" }, { "status": "known", "icon_url": "https://static.openfoodfacts.org/images/icons/no-lupin.svg", "match": 100, "name": "Lupin", "title": "Does not contain: Lupin", "debug": "3 ingredients (0 unknown)", "id": "allergens_no_lupin" }, { "icon_url": "https://static.openfoodfacts.org/images/icons/no-sulphur-dioxide-and-sulphites.svg", "match": 100, "status": "known", "debug": "3 ingredients (0 unknown)", "id": "allergens_no_sulphur_dioxide_and_sulphites", "name": "Sulphur dioxide and sulphites", "title": "Does not contain: Sulphur dioxide and sulphites" }, { "title": "Does not contain: Nuts", "name": "Nuts", "debug": "3 ingredients (0 unknown)", "id": "allergens_no_nuts", "status": "known", "match": 100, "icon_url": "https://static.openfoodfacts.org/images/icons/no-nuts.svg" }, { "icon_url": "https://static.openfoodfacts.org/images/icons/no-eggs.svg", "match": 100, "status": "known", "debug": "3 ingredients (0 unknown)", "id": "allergens_no_eggs", "title": "Does not contain: Eggs", "name": "Eggs" }], "warning": "There is always a possibility that data about allergens may be missing, incomplete, incorrect or that the product's composition has changed. If you are allergic, always check the information on the actual product packaging.", "name": "Allergens" }, { "name": "Ingredients", "id": "ingredients_analysis", "attributes": [{ "title": "Non-vegan", "status": "known", "name": "Vegan", "match": 0, "id": "vegan", "icon_url": "https://static.openfoodfacts.org/images/icons/non-vegan.svg" }, { "match": 0, "icon_url": "https://static.openfoodfacts.org/images/icons/non-vegetarian.svg", "id": "vegetarian", "title": "Non-vegetarian", "status": "known", "name": "Vegetarian" }, { "status": "known", "title": "Palm oil free", "id": "palm-oil-free", "icon_url": "https://static.openfoodfacts.org/images/icons/palm-oil-free.svg", "match": 100 }] }, { "name": "Food processing", "id": "processing", "attributes": [{ "match": 50, "description_short": "Processed foods", "icon_url": "https://static.openfoodfacts.org/images/misc/nova-group-3.svg", "description": "", "status": "known", "id": "nova", "title": "NOVA 3", "name": "NOVA group" }] }, { "name": "", "id": "ingredients", "attributes": [{ "title": "Without additives", "status": "known", "name": "Additives", "match": 100, "id": "additives", "icon_url": "https://static.openfoodfacts.org/images/icons/0-additives.svg" }] }, { "id": "environment", "attributes": [{ "status": "known", "match": 28.6396590441916, "description_short": "High environmental impact", "icon_url": "https://static.openfoodfacts.org/images/icons/ecoscore-d.svg", "description": "", "title": "Eco-Score D", "name": "Eco-Score", "id": "ecoscore" }, { "icon_url": "https://static.openfoodfacts.org/images/icons/forest-footprint-not-computed.svg", "description": "", "description_short": "Currently only for products with chicken or eggs", "match": 0, "status": "known", "id": "forest_footprint", "title": "Forest footprint not computed", "name": "Forest footprint" }], "name": "Environment" }, { "name": "Labels", "id": "labels", "attributes": [{ "name": "Organic farming", "title": "Not an organic product", "id": "labels_organic", "status": "known", "description": "Organic farming aims to protect the environment and to conserve biodiversity by prohibiting or limiting the use of synthetic fertilizers, pesticides and food additives.", "icon_url": "https://static.openfoodfacts.org/images/icons/not-organic.svg", "description_short": "Organic products promote ecological sustainability and biodiversity.", "match": 0 }, { "match": 0, "description_short": "Fair trade products help producers in developping countries.", "description": "When you buy fair trade products, producers in developing countries are paid an higher and fairer price, which helps them improve and sustain higher social and often environmental standards.", "icon_url": "https://static.openfoodfacts.org/images/icons/not-fair-trade.svg", "status": "known", "id": "labels_fair_trade", "name": "Fair trade", "title": "Not a fair trade product" }] }], "categories_tags": ["en:seafood", "en:fishes", "en:salmons", "en:smoked-fishes", "en:smoked-salmons", "en:smoked-salmons-from-farming"], "labels_tags": [], "additives_tags": [], "environment_impact_level_tags": [], "product_quantity": "200", "nutrient_levels": { "fat": "moderate", "salt": "high", "sugars": "low", "saturated-fat": "moderate" }, "ingredients_analysis_tags": ["en:palm-oil-free", "en:non-vegan", "en:non-vegetarian"], "image_small_url": "https://static.openfoodfacts.org/images/products/20034658/front.4.200.jpg" } ```
stephanegigandet commented 3 years ago

Thanks for all the tests. I agree SQLite seems to be the best option for our needs.

monsieurtanuki commented 3 years ago

@stephanegigandet Great! Let's start with SQLite then. I'll send a PR within a week.

We won't be stuck with SQLite forever anyway: as far as most developers are concerned, they will call a black-box that returns a product stored locally, from its barcode. Regardless of the type of database behind. If for some very good reason we feel appropriate in the future to switch to another type of database, nothing should stop us and it will be transparent for the developers.

More specifically about the new SQLite database, we'll create a "table" in it (that's the relational database terminology), that will contain all the (e.g. downloaded) products. For obvious reasons, the "primary key" (unique, not null) will be a "column" name barcode, of type TEXT. Another obvious column is the JSON version of the product, maybe called encoded_json, of type TEXT (maybe BLOB, I have to double-check that). Another interesting column would be the last database insert/upsert timestamp, maybe called last_upsert, of type INT.

As we already saw, the extraction of the JSON part is more or less painful.

stephanegigandet commented 3 years ago

At this point, I'm more thinking of the SQLite database as a big cache of product data, that we access only as a key (code) / value (json) cache. Whenever we need data for a specific product (that we scan, or that we have in a list like the history of scanned products), we look for the corresponding codes through the database (and through the API if we have some network).

Those lists will probably be reasonably small (less than 1000 products), so if we want to filter them, we can do it outside the database.

If we wanted to support a full offline search / exploration (like the current "category browser" that is in the app), then we could have separate fields for categories etc. but at this point this feature is very experimental and very likely to change. Let's do it through the OFF API first, and if there's really a need to make it available offline (which is less and less of a need as connectivity gets more ubiquitous), we can see that later.

We could consider adding a last access time (so that we can expire entries that are never accessed), but then that would mean updating the entries whenever we access them. Not sure if it's worth it.

A "source" field would be useful, so that we know if the entry was added through a scan, a search, a downloaded / initial database etc. Maybe also a field to indicate whether the product belongs to at least one list.

monsieurtanuki commented 3 years ago

Ok, I'll stay with my 3 columns then for SQLite table product. Something like that:

create table product(
    barcode TEXT PRIMARY KEY,
    encoded_json TEXT NOT NULL,
    last_upsert INT NOT NULL
)

And about additional fields like "source", it's not related to a product, it's related to a product list:

M123-dev commented 3 years ago

I think it is more logical to make a list out of the last one. This leads to a better history for and statistics for example how often a user scanned a certain produkt.

M123-dev commented 3 years ago

I don't know if this has already been said somewhere, but I wanted to mention that scanning no longer works for me in the current master.

E/flutter (15852): [ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: FormatException: Invalid double
E/flutter (15852): null
E/flutter (15852): #0      double.parse (dart:core-patch/double_patch.dart:111:28)
E/flutter (15852): #1      AttributeGroups.fromJson (package:openfoodfacts/model/AttributeGroups.dart:67:27)
E/flutter (15852): #2      _$ProductFromJson (package:openfoodfacts/model/Product.g.dart:71:25)
E/flutter (15852): #3      new Product.fromJson (package:openfoodfacts/model/Product.dart:180:7)
E/flutter (15852): #4      _$ProductResultFromJson (package:openfoodfacts/model/ProductResult.g.dart:16:19)
E/flutter (15852): #5      new ProductResult.fromJson (package:openfoodfacts/model/ProductResult.dart:20:7)
E/flutter (15852): #6      OpenFoodAPIClient.getProduct (package:openfoodfacts/openfoodfacts.dart:143:42)
PrimaelQuemerais commented 3 years ago

I don't know if this has already been said somewhere, but I wanted to mention that scanning no longer works for me in the current master.

E/flutter (15852): [ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: FormatException: Invalid double
E/flutter (15852): null
E/flutter (15852): #0      double.parse (dart:core-patch/double_patch.dart:111:28)
E/flutter (15852): #1      AttributeGroups.fromJson (package:openfoodfacts/model/AttributeGroups.dart:67:27)
E/flutter (15852): #2      _$ProductFromJson (package:openfoodfacts/model/Product.g.dart:71:25)
E/flutter (15852): #3      new Product.fromJson (package:openfoodfacts/model/Product.dart:180:7)
E/flutter (15852): #4      _$ProductResultFromJson (package:openfoodfacts/model/ProductResult.g.dart:16:19)
E/flutter (15852): #5      new ProductResult.fromJson (package:openfoodfacts/model/ProductResult.dart:20:7)
E/flutter (15852): #6      OpenFoodAPIClient.getProduct (package:openfoodfacts/openfoodfacts.dart:143:42)

This error comes from the openfoodfacts-dart plugin, will investigate

monsieurtanuki commented 3 years ago

@PrimaelQuemerais I think it's fixed in https://github.com/openfoodfacts/openfoodfacts-dart/pull/65, where the double match field is more safely loaded with JsonObject.parseDouble(json[_JSON_TAG_MATCH]) rather that double.parse(attribute['match'].toString()). Is that normal that we get null for match: that's another story... Hey, work on your exams instead ;)

PrimaelQuemerais commented 3 years ago

Great! I will take a look at the PR. Haha thanks I was just taking a break :smile: