-
Notifications
You must be signed in to change notification settings - Fork 472
Closes #5284: Adds progress bar to download notification #5285
Conversation
2071ba5
to
f5782c5
Compare
Codecov Report
@@ Coverage Diff @@
## master #5285 +/- ##
============================================
- Coverage 80.3% 79.95% -0.35%
+ Complexity 4219 4205 -14
============================================
Files 545 543 -2
Lines 19174 19193 +19
Branches 2772 2780 +8
============================================
- Hits 15397 15346 -51
- Misses 2618 2689 +71
+ Partials 1159 1158 -1
Continue to review full report at Codecov.
|
ed159d7
to
25aad45
Compare
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.
Oh, thank god. I recently downloaded some large files with Fenix and was completely clueless about what's going on without the progress. :)
} | ||
} | ||
|
||
notificationTimer.scheduleAtFixedRate(timerTask, 0, PROGRESS_UPDATE_INTERVAL) |
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.
I am a bit confused about the lifetime of the tasks. We schedule one task for every download here. But in startDownoadJob()
we cancel all tasks on the timer. So I am wondering:
- Doesn't the
cancel()
call instartDownoadJob()
cause all other notifications to get stale except the new one? - We never cancel a specific task after the download is done right? Only if the service stops and the
Timer
object gets garbage collected then they get away (but the docs are not very specific on when that happens).
I wonder if we could use a single Kotlin Coroutine to update all notifications, something like:
val notificationUpdateScope = MainScope() // Or a different scope if that the notification update does not need to happen on the main thread.
override fun onCreate() {
// [..]
notificationUpdateScope.launch {
delay(...)
updateProgress()
}
}
override fun onDestroy() {
// [..]
notificationUpdateScope.cancel()
}
fun updateProgress() {
// Update progress of all notifications.
}
Does this make sense? I think with a single coroutine it is much easier to understand its lifetime and avoiding multiple zombie threads being left behind. What do you think?
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.
All great points. I have updated this PR with the ideas you suggested.
53babdd
to
75ae0e4
Compare
if (download.status != DownloadJobStatus.ACTIVE) { continue } | ||
// We must be synchronized here to avoid the status getting set to PAUSED on this line | ||
// and then overwriting that with an ongoing notification anyway | ||
displayOngoingDownloadNotification(download.state, download.currentBytesCopied) |
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.
I'm having trouble with this race condition. Do you have any suggestions about how to solve it? I thought putting a synchronized on this and all other state setting would be sufficient but it doesn't seem so 😕
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.
Oh, sorry, this has fallen through the cracks. I can try to help with that this week. What is happening in that race condition? Notifications do not update or with wrong state?
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’s happening is a user presses PAUSE, the notification then gets a timed update to display the current progress (ACTIVE state), so the “pause” action gets eaten but the download is still paused and stuck in a state where the user sees it as “frozen”
75ae0e4
to
3ef13f5
Compare
53263d4
to
15c7a71
Compare
15c7a71
to
e39b6af
Compare
2c7384c
to
4b5f9fb
Compare
*/ | ||
private fun sendDownloadCompleteBroadcast(downloadID: Long, status: DownloadJobStatus) { | ||
private fun sendDownloadNotInProgress(downloadID: Long, status: DownloadJobStatus) { |
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.
Perhaps sendDownloadFinished
is better? I know this name is ugly but I want to be clear about what's going on here.
currentDownloadJobState.status = DownloadJobStatus.PAUSED | ||
synchronized(context) { | ||
currentDownloadJobState.status = DownloadJobStatus.PAUSED | ||
} |
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.
Now that we synchronize you can remove the @Volatile
from DownloadJobState
. Instead you could try adding @GuardedBy("context")
to the status
property to denote that this needs to be sychronized on context
when accessed/modified. That itself doesn't do anything but hopefully the IDE (or lint) will show a warning if you don't do that.
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.
Ooo didn't know about that keyword. Thanks!
...downloads/src/main/java/mozilla/components/feature/downloads/AbstractFetchDownloadService.kt
Outdated
Show resolved
Hide resolved
...downloads/src/main/java/mozilla/components/feature/downloads/AbstractFetchDownloadService.kt
Outdated
Show resolved
Hide resolved
4d01145
to
3988196
Compare
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! 👍
bors r+
254c1d7
to
2f9515a
Compare
bors retry |
5285: Closes #5284: Adds progress bar to download notification r=pocmo a=sblatz Co-authored-by: Sawyer Blatz <[email protected]>
bors r- |
Canceled |
bors r=@pocmo |
5285: Closes #5284: Adds progress bar to download notification r=pocmo a=sblatz Co-authored-by: Sawyer Blatz <[email protected]>
Timed out |
bors retry |
5285: Closes #5284: Adds progress bar to download notification r=pocmo a=sblatz Co-authored-by: Sawyer Blatz <[email protected]>
I'm really at a loss here for what the out of memory could be caused by. It looks like it fails on every run of Any ideas @pocmo / @jonalmeida |
Timed out |
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.
I left some of my thought processes down below.
We can start by looking at the trace from the OOM in the CI logs:
Taskcluster CI logs.
TEST: when the cancel button is clicked onCancelDownload must be called
[Robolectric] mozilla.components.feature.downloads.SimpleDownloadDialogFragmentTest.when the cancel button is clicked onCancelDownload must be called: sdk=28; resources=binary
I/MonitoringInstr: Instrumentation started!
I/MonitoringInstr: Setting context classloader to 'org.robolectric.internal.bytecode.SandboxClassLoader@1005e1f9', Original: 'org.robolectric.internal.bytecode.SandboxClassLoader@1005e1f9'
Exception in thread "DefaultDispatcher-worker-2 @coroutine#69" java.lang.OutOfMemoryError: GC overhead limit exceeded
at net.bytebuddy.description.type.TypeDescription$ForLoadedType.of(TypeDescription.java:8235)
at net.bytebuddy.description.type.TypeDescription$Generic$LazyProjection$ForLoadedReturnType.asErasure(TypeDescription.java:6519)
at net.bytebuddy.description.method.MethodDescription$AbstractBase.asTypeToken(MethodDescription.java:837)
at net.bytebuddy.dynamic.scaffold.MethodGraph$Compiler$Default$Key$Harmonized.extend(MethodGraph.java:973)
at net.bytebuddy.dynamic.scaffold.MethodGraph$Compiler$Default$Key$Store$Entry$Resolved.extendBy(MethodGraph.java:1408)
at net.bytebuddy.dynamic.scaffold.MethodGraph$Compiler$Default$Key$Store.registerTopLevel(MethodGraph.java:1110)
at net.bytebuddy.dynamic.scaffold.MethodGraph$Compiler$Default.doAnalyze(MethodGraph.java:634)
at net.bytebuddy.dynamic.scaffold.MethodGraph$Compiler$Default.compile(MethodGraph.java:567)
at net.bytebuddy.dynamic.scaffold.MethodGraph$Compiler$AbstractBase.compile(MethodGraph.java:465)
at org.mockito.internal.creation.bytebuddy.MockMethodAdvice.isOverridden(MockMethodAdvice.java:134)
at java.io.InputStream.read(InputStream.java:101)
at mozilla.components.feature.downloads.AbstractFetchDownloadService.copyInChunks(AbstractFetchDownloadService.kt:357)
at mozilla.components.feature.downloads.AbstractFetchDownloadService.access$copyInChunks(AbstractFetchDownloadService.kt:68)
at mozilla.components.feature.downloads.AbstractFetchDownloadService$performDownload$1$1.invoke(AbstractFetchDownloadService.kt:332)
at mozilla.components.feature.downloads.AbstractFetchDownloadService$performDownload$1$1.invoke(AbstractFetchDownloadService.kt:68)
at mozilla.components.feature.downloads.AbstractFetchDownloadService.useFileStreamLegacy(AbstractFetchDownloadService.kt:449)
at mozilla.components.feature.downloads.AbstractFetchDownloadService.useFileStream$feature_downloads_debug(AbstractFetchDownloadService.kt:400)
at mozilla.components.feature.downloads.AbstractFetchDownloadService$performDownload$1.invoke(AbstractFetchDownloadService.kt:331)
at mozilla.components.feature.downloads.AbstractFetchDownloadService$performDownload$1.invoke(AbstractFetchDownloadService.kt:68)
at mozilla.components.concept.fetch.Response$Body.useStream(Response.kt:79)
at mozilla.components.feature.downloads.AbstractFetchDownloadService.performDownload$feature_downloads_debug(AbstractFetchDownloadService.kt:327)
at mozilla.components.feature.downloads.AbstractFetchDownloadService.startDownloadJob$feature_downloads_debug(AbstractFetchDownloadService.kt:282)
at mozilla.components.feature.downloads.AbstractFetchDownloadService$broadcastReceiver$2$1$onReceive$3.invokeSuspend(AbstractFetchDownloadService.kt:123)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:561)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:727)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:667)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:655)
Exception in thread "DefaultDispatcher-worker-3 @coroutine#81" java.lang.OutOfMemoryError: GC overhead limit exceeded
at mozilla.components.feature.downloads.AbstractFetchDownloadService.copyInChunks(AbstractFetchDownloadService.kt:356)
at mozilla.components.feature.downloads.AbstractFetchDownloadService.access$copyInChunks(AbstractFetchDownloadService.kt:68)
at mozilla.components.feature.downloads.AbstractFetchDownloadService$performDownload$1$1.invoke(AbstractFetchDownloadService.kt:332)
at mozilla.components.feature.downloads.AbstractFetchDownloadService$performDownload$1$1.invoke(AbstractFetchDownloadService.kt:68)
at mozilla.components.feature.downloads.AbstractFetchDownloadService.useFileStreamLegacy(AbstractFetchDownloadService.kt:449)
at mozilla.components.feature.downloads.AbstractFetchDownloadService.useFileStream$feature_downloads_debug(AbstractFetchDownloadService.kt:400)
at mozilla.components.feature.downloads.AbstractFetchDownloadService$performDownload$1.invoke(AbstractFetchDownloadService.kt:331)
at mozilla.components.feature.downloads.AbstractFetchDownloadService$performDownload$1.invoke(AbstractFetchDownloadService.kt:68)
at mozilla.components.concept.fetch.Response$Body.useStream(Response.kt:79)
at mozilla.components.feature.downloads.AbstractFetchDownloadService.performDownload$feature_downloads_debug(AbstractFetchDownloadService.kt:327)
at mozilla.components.feature.downloads.AbstractFetchDownloadService.startDownloadJob$feature_downloads_debug(AbstractFetchDownloadService.kt:282)
at mozilla.components.feature.downloads.AbstractFetchDownloadService$broadcastReceiver$2$1$onReceive$7.invokeSuspend(AbstractFetchDownloadService.kt:156)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:561)
It's a bit long, but if you follow along the call stack for each coroutine, we can see two exceptions taking place:
Exception in thread "DefaultDispatcher-worker-2 @coroutine#69" java.lang.OutOfMemoryError: GC overhead limit exceeded
at net.bytebuddy.description.type.TypeDescription$ForLoadedType.of(TypeDescription.java:8235)
and
Exception in thread "DefaultDispatcher-worker-3 @coroutine#81" java.lang.OutOfMemoryError: GC overhead limit exceeded
at mozilla.components.feature.downloads.AbstractFetchDownloadService.copyInChunks(AbstractFetchDownloadService.kt:356)
The latter one is our code, so I'm willing to bet it's something in our code vs the net.bytebuddy
that's causing the crash.
If we take a look at that line in AbstractFetchDownloadService.copyInChunks(AbstractFetchDownloadService.kt:356)
:
while (downloadJobState.status == DownloadJobStatus.ACTIVE) {
val data = ByteArray(CHUNK_SIZE) // <- this is the line in question.
val bytesRead = inStream.read(data)
if (bytesRead == -1) { break }
downloadJobState.currentBytesCopied += bytesRead
outStream.write(data, 0, bytesRead)
}
So we're allocating a 4Kb ByteArray, which should explain the memory allocations leading to the OOM, however, what's more interesting to me is that it's in a while block that loops until the status changes out of DownloadJobStatus.ACTIVE
.
Looking at the test, we're trying to cancel a download but I don't see one being started before.
So without much knowledge of how the download feature works, my guess is either:
- The cancel button doesn't work in cancelling the download service (and we need to write better tests to cover this situation or decouple the service from the fragment, so avoid this from happening again).
- There is a download service attached to the test that was in an active state and the test doesn't cover that (which might be related to this change that you removed
251b7e..b16810
)
The whole build passes for me locally
CI machines generally have less memory allocated to the VM so they fail faster. The same OOM error would probably also take place if you reduced the memory allocated for the VM on your local machine.
See my other comment as well that seems to register every download an a resumed one, I'm not sure what the implications for that are, but it doesn't sound like the right state to have.
ongoingDownloadNotification | ||
) | ||
} | ||
|
||
@Suppress("ComplexCondition") | ||
internal fun performDownload(download: DownloadState) { | ||
val isResumingDownload = downloadJobs[download.id]?.currentBytesCopied ?: 0L > 0L |
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.
I noticed this while reading the code. Wouldn't 0L > 0L
always be true?
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.
0L > 0L would always be false, right?
The logic is: isResumingDownload
is TRUE if the bytes copied is more than 0. if bytesCopied is null, we assume it's 0, and thus this would evaluate to false.
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.
Why evaluate instead of simplifying this to be false
?
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.
I'm confused. I want to compare currentBytesCopied
to 0L so I can't just do
downloadJobs[download.id]?.currentBytesCopied ?: false > 0L
Is there some syntax I'm missing 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.
Try this so avoid confusion:
(downloadJobs[download.id]?.currentBytesCopied ?: 0L) > 0L
Otherwise, it looks like:
downloadJobs[download.id]?.currentBytesCopied ?: (0L > 0L)
EDIT
Or better:
val currentBytes = downloadJobs[download.id]?.currentBytesCopied ?: 0L
val isResumingDownload = currentBytes > 0L
bc4143f
to
f8c378b
Compare
bors retry |
5285: Closes #5284: Adds progress bar to download notification r=pocmo a=sblatz Co-authored-by: Sawyer Blatz <[email protected]>
f8c378b
to
fb3847f
Compare
Canceled |
bors r=@pocmo |
5285: Closes #5284: Adds progress bar to download notification r=pocmo a=sblatz Co-authored-by: Sawyer Blatz <[email protected]>
Build succeeded
|
TAKE THAT BORS. |
DEMO:
Pull Request checklist
After merge