Skip to content

Commit c5619fb

Browse files
fix: 6976 - isolates for all multi product search (#6981)
* fix: 6976 - isolates for all multi product search * code refactoring * Better perfs for language refresh * Update packages/smooth_app/lib/database/dao_product.dart
1 parent 7f6bce2 commit c5619fb

File tree

3 files changed

+106
-12
lines changed

3 files changed

+106
-12
lines changed

packages/smooth_app/lib/database/dao_product.dart

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable {
2121
'encoded_gzipped_json';
2222
static const String _TABLE_PRODUCT_COLUMN_LAST_UPDATE = 'last_update';
2323
static const String _TABLE_PRODUCT_COLUMN_LANGUAGE = 'lc';
24+
static const String _TABLE_PRODUCT_COLUMN_PRODUCT_TYPE = 'product_type';
2425

2526
static const List<String> _columns = <String>[
2627
_TABLE_PRODUCT_COLUMN_BARCODE,
2728
_TABLE_PRODUCT_COLUMN_GZIPPED_JSON,
2829
_TABLE_PRODUCT_COLUMN_LAST_UPDATE,
2930
_TABLE_PRODUCT_COLUMN_LANGUAGE,
31+
_TABLE_PRODUCT_COLUMN_PRODUCT_TYPE,
3032
];
3133

3234
static FutureOr<void> onUpgrade(
@@ -50,6 +52,12 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable {
5052
'$_TABLE_PRODUCT_COLUMN_LANGUAGE TEXT',
5153
);
5254
}
55+
if (oldVersion < 8) {
56+
await db.execute(
57+
'alter table $_TABLE_PRODUCT add column '
58+
'$_TABLE_PRODUCT_COLUMN_PRODUCT_TYPE TEXT',
59+
);
60+
}
5361
}
5462

5563
/// Returns the [Product] that matches the [barcode], or null.
@@ -177,7 +185,7 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable {
177185
}
178186
await localDatabase.database.transaction(
179187
(final Transaction transaction) async =>
180-
_bulkReplaceLoop(transaction, products, language),
188+
_bulkReplaceLoop(transaction, products, language, productType),
181189
);
182190
}
183191

@@ -208,6 +216,7 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable {
208216
final DatabaseExecutor databaseExecutor,
209217
final Iterable<Product> products,
210218
final OpenFoodFactsLanguage language,
219+
final ProductType productType,
211220
) async {
212221
final int lastUpdate = LocalDatabase.nowInMillis();
213222
final BulkManager bulkManager = BulkManager();
@@ -221,6 +230,7 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable {
221230
);
222231
insertParameters.add(lastUpdate);
223232
insertParameters.add(language.offTag);
233+
insertParameters.add(product.productType?.offTag ?? productType.offTag);
224234
}
225235
await bulkManager.insert(
226236
bulkInsertable: this,
@@ -344,24 +354,38 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable {
344354

345355
const String tableJoin =
346356
'p.$_TABLE_PRODUCT_COLUMN_BARCODE = a.${DaoProductLastAccess.COLUMN_BARCODE}';
357+
const String columns =
358+
'p.$_TABLE_PRODUCT_COLUMN_GZIPPED_JSON'
359+
',p.$_TABLE_PRODUCT_COLUMN_BARCODE'
360+
',p.$_TABLE_PRODUCT_COLUMN_PRODUCT_TYPE';
361+
// we want rows with a different language - or a null language
347362
final String languageCondition =
348363
' ('
349364
'p.$_TABLE_PRODUCT_COLUMN_LANGUAGE is null '
350365
"or p.$_TABLE_PRODUCT_COLUMN_LANGUAGE != '${language.offTag}'"
351366
') ';
367+
// we want rows with that type - or a null type
368+
final String productTypeCondition =
369+
' ('
370+
'p.$_TABLE_PRODUCT_COLUMN_PRODUCT_TYPE is null '
371+
"or p.$_TABLE_PRODUCT_COLUMN_PRODUCT_TYPE = '${productType.offTag}'"
372+
') ';
352373

374+
// Listing the rows with a last access, ordered by last access (desc).
353375
final String queryWithLastAccess =
354-
'select p.$_TABLE_PRODUCT_COLUMN_GZIPPED_JSON '
376+
'select $columns '
355377
'from'
356378
' $_TABLE_PRODUCT p '
357379
' inner join ${DaoProductLastAccess.TABLE} a'
358380
' on $tableJoin '
359381
'where'
360382
' $languageCondition '
383+
' and $productTypeCondition '
361384
'order by a.${DaoProductLastAccess.COLUMN_LAST_ACCESS} desc';
362385

386+
// Listing the rows without a last access.
363387
final String queryWithoutLastAccess =
364-
'select p.$_TABLE_PRODUCT_COLUMN_GZIPPED_JSON '
388+
'select $columns '
365389
'from'
366390
' $_TABLE_PRODUCT p '
367391
'where'
@@ -370,8 +394,27 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable {
370394
' from ${DaoProductLastAccess.TABLE} a '
371395
' where $tableJoin '
372396
' ) '
397+
' and $productTypeCondition '
373398
' and $languageCondition';
374399

400+
final Map<String, String> updates = <String, String>{};
401+
402+
/// Updates products that didn't have a product_type *in the table column*.
403+
///
404+
/// This way, we lazily populate the database.
405+
/// After one "language refresh", all products will have a populated
406+
/// product_type column, so this method will do nothing afterwards.
407+
Future<void> updateUnknownProductTypes() async {
408+
for (final MapEntry<String, String> entry in updates.entries) {
409+
await localDatabase.database.update(
410+
_TABLE_PRODUCT,
411+
<String, String>{_TABLE_PRODUCT_COLUMN_PRODUCT_TYPE: entry.value},
412+
where: '$_TABLE_PRODUCT_COLUMN_BARCODE = ?',
413+
whereArgs: <String>[entry.key],
414+
);
415+
}
416+
}
417+
375418
// optimization: using 2 more simple queries than a "left join" that proved
376419
// more expensive (less than .1s for each simple query, .5s for "left join")
377420
final List<String> queries = <String>[
@@ -384,21 +427,36 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable {
384427
final QueryCursor queryCursor = await localDatabase.database
385428
.rawQueryCursor(query, null);
386429
while (await queryCursor.moveNext()) {
387-
final Product product = _getProductFromQueryResult(queryCursor.current);
388-
final String barcode = product.barcode!;
430+
final String barcode =
431+
queryCursor.current[_TABLE_PRODUCT_COLUMN_BARCODE]! as String;
389432
if (excludeBarcodes.contains(barcode)) {
390433
continue;
391434
}
392-
if ((product.productType ?? ProductType.food) != productType) {
435+
String? foundProductType =
436+
queryCursor.current[_TABLE_PRODUCT_COLUMN_PRODUCT_TYPE] as String?;
437+
if (foundProductType == null) {
438+
final Product product = _getProductFromQueryResult(
439+
queryCursor.current,
440+
);
441+
foundProductType = product.productType?.offTag;
442+
if (foundProductType != null) {
443+
updates[barcode] = foundProductType;
444+
}
445+
}
446+
if ((foundProductType ?? ProductType.food.offTag) !=
447+
productType.offTag) {
393448
continue;
394449
}
395450
result.add(barcode);
396451
if (result.length == limit) {
452+
await queryCursor.close();
453+
await updateUnknownProductTypes();
397454
return result;
398455
}
399456
}
400457
}
401458

459+
await updateUnknownProductTypes();
402460
return result;
403461
}
404462

packages/smooth_app/lib/database/local_database.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class LocalDatabase extends ChangeNotifier {
3636
Database get database => _database;
3737

3838
UpToDateProductProvider get upToDate => _upToDateProductProvider;
39+
3940
UpToDateProductListProvider get upToDateProductList =>
4041
_upToDateProductListProvider;
4142

@@ -75,7 +76,7 @@ class LocalDatabase extends ChangeNotifier {
7576
final String databasePath = join(databasesRootPath, 'smoothie.db');
7677
final Database database = await openDatabase(
7778
databasePath,
78-
version: 7,
79+
version: 8,
7980
singleInstance: true,
8081
onUpgrade: _onUpgrade,
8182
);

packages/smooth_app/lib/query/search_products_manager.dart

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'package:flutter/foundation.dart';
2+
import 'package:http/http.dart';
13
import 'package:openfoodfacts/openfoodfacts.dart';
24

35
/// Type of "search products" action.
@@ -51,10 +53,43 @@ class SearchProductsManager {
5153
required final SearchProductsType type,
5254
}) async {
5355
await type.waitIfNeeded();
54-
return OpenFoodAPIClient.searchProducts(
55-
user,
56-
configuration,
57-
uriHelper: uriHelper,
58-
);
56+
57+
// It's better to do the HTTP actions outside of "compute", because
58+
// there are init phases for HTTP (like user agent and SSL certificates)
59+
// that would need to be somehow replicated for a new "compute thread".
60+
// Besides, putting HTTP in "compute" wouldn't improve the performances.
61+
final Response response = await configuration.getResponse(user, uriHelper);
62+
TooManyRequestsException.check(response);
63+
64+
final SearchResult result = await compute(_decodeProducts, response.body);
65+
_removeImages(result, configuration);
66+
return result;
67+
}
68+
69+
static Future<SearchResult> _decodeProducts(
70+
final String responseBody,
71+
) async => SearchResult.fromJson(
72+
HttpHelper().jsonDecode(_replaceQuotes(responseBody)),
73+
);
74+
75+
// TODO(monsieurtanuki): somehow move to/make public in off-dart
76+
static String _replaceQuotes(String str) {
77+
const String needle = '&quot;';
78+
if (!str.contains(needle)) {
79+
return str;
80+
}
81+
return str.replaceAll(needle, r'\"');
82+
}
83+
84+
// TODO(monsieurtanuki): somehow move to/make public in off-dart
85+
static void _removeImages(
86+
final SearchResult searchResult,
87+
final AbstractQueryConfiguration configuration,
88+
) {
89+
if (searchResult.products != null) {
90+
searchResult.products!.asMap().forEach((int index, Product product) {
91+
ProductHelper.removeImages(product, configuration.language);
92+
});
93+
}
5994
}
6095
}

0 commit comments

Comments
 (0)