-
Couldn't load subscription status.
- Fork 476
Closes #1126: Rust Places backed implementation of concept-storage #1164
Closes #1126: Rust Places backed implementation of concept-storage #1164
Conversation
|
mozilla/application-services#327 adds an API we'd need here for the system engine, and fixes a bug around titles not being recorded. |
|
Changes required from app-services are done, and scaffolding for this is coming together in #1155. |
| // historyTrackingDelegate = PlacesHistoryTrackingDelegate(applicationContext) | ||
| // } | ||
| // GeckoEngine(applicationContext, defaultSettings) | ||
| // } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI: In the bottom left corner in Android Studio there should be a "build variant" side bar. There you should be able to switch the build variant of samples-browser. In this case you can just use systemUniversalDebug.
ddd4dd8 to
77c4782
Compare
9948212 to
b0f397a
Compare
|
@pocmo @csadilek this is ready for a review. This works for me locally with a HistoryTrackingFeature enabled in the sample browser with a SystemEngine, and will track history with GeckoView once #1238 lands. To get tests to work for Alternatively, we could supply library artifacts for the test machine architectures, and wire that up...? Not entirely sure how that would look. |
|
Relevant app services issue: mozilla/application-services#342 |
|
Pushed out an app-services PR to help out with tests and docs. mozilla/application-services#343 |
|
Still wrapping my head around coroutines, dispatchers and whatnot. Regarding the We don't always care about what happens with the work we schedule. For recording a visit, we really just want to kick off that work and move on. For retrieving visits, we obviously need to wait for it to complete. It's possible we might also want to be able to cancel that work (if an engine view is destroyed?). For autocomplete results, we care about the results and definitely want to have cancellation machinery in place (how deep it should go is another question... down into the places rust lib..? see mozilla/application-services#265). But, this can also be accomplished at the consumer level, and not by using For now, I think it makes most sense to approach this on a method-by-method basis. |
@grigoryk @pocmo and I talked about this as well today. It seems to me that it should be perfectly reasonable for a library API to return |
| */ | ||
| internal object Conn { | ||
| @Volatile | ||
| private var placesConnection: PlacesConnection? = null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems we need to call close on PlacesConnection. Need to think about how we do this otherwise we'd leak every time the app process is killed/restarted. Maybe expose a close method on Conn?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@csadilek yup, good call. I've updated HistoryStorage to let us do that.
b0f397a to
8a268e7
Compare
6612687 to
4d591b0
Compare
Codecov Report
@@ Coverage Diff @@
## master #1164 +/- ##
===========================================
- Coverage 82.57% 81% -1.58%
+ Complexity 1628 905 -723
===========================================
Files 202 115 -87
Lines 6033 3511 -2522
Branches 970 565 -405
===========================================
- Hits 4982 2844 -2138
+ Misses 596 400 -196
+ Partials 455 267 -188
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some things here and there. But as this is a big new thing we want to get in I'm fine with only addressing what you think you can/want address now and then iterate on it in the tree.
| super.onDestroy() | ||
|
|
||
| components.historyStorage.cleanup() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does it mean to close this (or not close this) for the Rust implementation?
- Reading/Writing data is not limited to this activity (e.g. other activities, background jobs, ..). Is this something that only needs to be done if everything in this process is done with the storage?
Activity.onDestroy()is not guaranteed to get called. See "Killable" column: https://developer.android.com/reference/android/app/Activity.html#ActivityLifecycle - What happens if this does not get called?
| * corresponding page URI from [uris]. | ||
| */ | ||
| fun getVisited(uris: List<String>, callback: (List<Boolean>) -> Unit) | ||
| fun getVisited(uris: List<String>): Deferred<List<Boolean>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deferred is definitely better than the callback.
But it means that a storage implementation will launch coroutines internally. And those are not in the control of the caller.
For the sake of and example imagine this would be called from the awesomebar:
AwesomeBar -> SuggestionProvider -> HistoryStorage -> getVisited()
The AwesomeBar launches scoped coroutines for all providers. And the Storage launches one inside getVisited(). The user updates the text and the AwesomeBar cancels the coroutines querying the providers. getVisited() continues to run and doesn't notice that because it runs its coroutine on a different scope.
So I wonder if Deferred is the best option here. For a caller it's simple to wrap the call in its own async {} block in a scope of its choice. I'm not sure if we win much if we do it internally here?
I'm really not sure what the best pattern for such cases is. I'll try to investigate a bit.
| * Implementation of the [HistoryStorage] which is backed by a Rust Places lib via [PlacesConnection]. | ||
| */ | ||
| @VisibleForTesting | ||
| open class TestablePlacesHistoryStorage(private val places: PlacesAPI) : HistoryStorage { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can the visibility be internal?
| settings.historyTrackingDelegate?.let { delegate -> | ||
| runBlocking { | ||
| delegate.onTitleChanged(url, title, privateMode) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it need to be blocking?
|
|
||
| override fun cleanup() { | ||
| // GC will take care of our internal data structures, so there's nothing to do here. | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now we call cleanup() from an Activity. With the activity going away this doesn't mean the process/engine and therefore the delegate going away though.
- If we want to call this while parts of the app are still alive: Should we clear the map?
- If we want to call this when the app process is getting killed: Okay, we do not need to do anything here. But there's no event we could call this from.
| const val DB_NAME = "places.sqlite" | ||
|
|
||
| /** | ||
| * A singleton which holds a shared reference to [PlacesConnection]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does PlacesConnection need to be a singleton or could we have multiple of those concurrently and this is just a design decision here? Another option would be to let the app pass in a "Connection" object that it creates and that only has internal methods for our component.
| placesConnection!!.close() | ||
| placesConnection = null | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slightly unclean: We have a close() here and the user of PlacesConnection could call close too; creating a situation where our connection holder will return a closed instance.
| import org.mozilla.places.VisitObservation | ||
|
|
||
| class PlacesHistoryStorage(context: Context) | ||
| : TestablePlacesHistoryStorage(Conn.getInstance(context.getDatabasePath(DB_NAME).canonicalPath)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What exactly is the reason for TestablePlacesHistoryStorage? I assume that you do not want to make PlacesHistoryStorage open. If this is just the case for mocking then creating this file in the component will allow you to just mock the non-open class: https://github.com/mozilla-mobile/android-components/blob/master/components/browser/engine-gecko/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
| } | ||
|
|
||
| override fun cleanup() { | ||
| Conn.close() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: if we close the connection here: Do we want to cancel all jobs of this coroutine scope too? You may close a connection that is in use here.
settings.gradle
Outdated
| setupProject(':browser-search', 'components/browser/search', 'Search plugins and companion code to load, parse and use them.') | ||
| setupProject(':browser-session', 'components/browser/session', 'An abstract layer hiding the actual browser engine implementation.') | ||
| setupProject(':browser-storage-memory', 'components/browser/storage-memory', 'A memory-backed implementation of core data storage.') | ||
| setupProject(':browser-storage-sync-places', 'components/browser/storage-sync-places', 'A syncable, Rust Places-backed implementation of core data storage.') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be added to artifacts.yml.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And a changelog entry.. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great progress! Let's land/release this and file follow-ups as discussed?
Just some conflicts that need fixing.
71f9a8f to
4b845b8
Compare
This is necessary because places lib was only included in 0.7.2, and various necessary changes to the places lib landed in 0.7.3, 0.8.0 and 0.8.1 to make API more ergonomic and testable.
Underlying HistoryStorage implementations might be blocking on IO (Rust Places, room), or migth be non-blocking (in-memory). A Deferred return type for get* methods makes it convenient to wrap both regular and `async` operations. Use of 'suspend' functions serves the same role for write* methods. This change makes it obvious at the engine call-site that the underlying implementation might suspend, at which point we can wrap calls in `runBlocking` or use another coroutine builder.
A storage implementation might need to cleanup its allocated resources, and this provides an entry point for that to happen. An example might be closing a native database connection that's maintained by the Rust Places library.
4b845b8 to
9a28351
Compare
A quick pass to see how things might fit together. It works!