Closed Bug 815941 Opened 12 years ago Closed 12 years ago

Remove the usages of nsIDownload.id and nsIDownloadManager.getDownload from the downloads panel code

Categories

(Firefox :: Downloads Panel, defect)

x86_64
Linux
defect
Not set
normal

Tracking

()

RESOLVED FIXED
Firefox 20

People

(Reporter: ehsan.akhgari, Assigned: mconley)

References

Details

Attachments

(1 file, 6 obsolete files)

Follow-up from bug 801232 comment 50.
Attached patch WIP (obsolete) — Splinter Review
OK this is a WIP, please give me feedback!

One thing that I have not gotten right I think is in the generator in _activeDownloads.  I seem to be getting a null at runtime which breaks the download panel as you might expect.  It could be a result of not knowing this fancy JS stuff. ;-)
Assignee: nobody → ehsan
Status: NEW → ASSIGNED
Attachment #686421 - Flags: feedback?(paolo.mozmail)
Attachment #686421 - Flags: feedback?(mconley)
Attachment #686421 - Flags: feedback?(mak77)
Blocks: 801232
Summary: Remove the rest of the usages of nsIDownload.id from the download panel code → Remove the usages of nsIDownload.id and nsIDownloadManager.getDownload from the downloads panel code
Note that I'd also appreciate if you guys can apply the patch and test it while giving feedback to make sure that there isn't anything innocent looking in the patch which breaks the downloads panel.  :-)
>for each (let download in downloads) {

I believe you want |let download of downloads|.
Attached patch WIP 2 (obsolete) — Splinter Review
This patch fixes a number of bugs...  There are no more JS errors.  But there are at least three things broken with it:

1. The download button doesn't show the time progress bar.
2. You can't pause and resume downloads from the panel (the pause button does not show up)
3. Retrying a canceled download causes nsIDownload::Retry to throw.
Attachment #686421 - Attachment is obsolete: true
Attachment #686421 - Flags: feedback?(paolo.mozmail)
Attachment #686421 - Flags: feedback?(mconley)
Attachment #686421 - Flags: feedback?(mak77)
Attachment #686578 - Flags: feedback?(paolo.mozmail)
Attachment #686578 - Flags: feedback?(mconley)
Attachment #686578 - Flags: feedback?(mak77)
Comment on attachment 686578 [details] [diff] [review]
WIP 2

Review of attachment 686578 [details] [diff] [review]:
-----------------------------------------------------------------

::: browser/components/downloads/src/DownloadsCommon.jsm
@@ +246,4 @@
>  
>      // If no download has been loaded, don't use the methods of the Download
>      // Manager service, so that it is not initialized unnecessarily.
> +    for (let download in aDownloads) {

This isn't going to work; download will be an index into the array.
Comment on attachment 686578 [details] [diff] [review]
WIP 2

Review of attachment 686578 [details] [diff] [review]:
-----------------------------------------------------------------

Most of this looks OK to me so far. Gonna give this one a spin and see if I can figure out why those few things are still busted.

::: browser/components/downloads/content/downloads.js
@@ +1244,5 @@
>      {
>        if (this.dataItem.inProgress) {
> +        this.dataItem.getDownload(function(aDownload) {
> +          aDownload.cancel();
> +          aDownload.remove();

Hold up - are we sure we want to be removing the download at this stage? The user only gave the command to cancel.

::: browser/components/downloads/src/DownloadsCommon.jsm
@@ +1476,5 @@
>      // If no download has been loaded, don't use the methods of the Download
>      // Manager service, so that it is not initialized unnecessarily.
> +    let downloads = DownloadsCommon.data.dataItems;
> +    if (downloads.length > 0) {
> +      for each (let download in downloads) {

I think we're getting a bit confused between downloads and dataItems - I think I'd prefer "downloads" in this context to be renamed dataItems.

Also, I think you can simplify this with:

if (dataItems.length > 0) {
  for (let dataItem of dataItems) {
    yield dataItem;
  }
}
Comment on attachment 686578 [details] [diff] [review]
WIP 2

Review of attachment 686578 [details] [diff] [review]:
-----------------------------------------------------------------

::: browser/components/downloads/src/DownloadsCommon.jsm
@@ +1475,5 @@
>    {
>      // If no download has been loaded, don't use the methods of the Download
>      // Manager service, so that it is not initialized unnecessarily.
> +    let downloads = DownloadsCommon.data.dataItems;
> +    if (downloads.length > 0) {

So this doesn't work because dataItems is an Object, so it doesn't have a .length.

An alternative would be to use one of those fancy new Map things (https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Map) - that'll give us our hashmap, as well as a .size property.

If that doesn't work, you can always check the cardinality of an Object with Object.keys(dataItems).length. Not pretty, but would work here, I think.

Fixing this will fix the indicator button / progress bar thing.
Comment on attachment 686578 [details] [diff] [review]
WIP 2

Review of attachment 686578 [details] [diff] [review]:
-----------------------------------------------------------------

I've also spent some time on getting this patch locally. After applying these changes, the patch seems to work, but please do some thorough testing on the updated version (I've only tried a few significant cases).

::: browser/components/downloads/content/downloads.js
@@ +1236,5 @@
>  
> +      this.dataItem.getDownload(function(aDownload) {
> +        aDownload.cancel();
> +        aDownload.remove();
> +      });

We're calling "this.commands.downloadsCmd_cancel.apply(this);" but this is not going to work anymore in asynchronous mode. We should call getDownload once, and inside it:
- If in progress, call cancel, then remove the file if present _after_ cancel returns.
- After that, in any case, remove the download.

About item removal, globally change "download-manager-remove-download" into "download-manager-remove-download-guid", then use: aSubject.QueryInterface(Ci.nsISupportsCString).data

@@ +1244,5 @@
>      {
>        if (this.dataItem.inProgress) {
> +        this.dataItem.getDownload(function(aDownload) {
> +          aDownload.cancel();
> +          aDownload.remove();

Mike's right.

@@ +1367,5 @@
> +          aDownload.resume();
> +        } else {
> +          aDownload.pause();
> +        }
> +      }.bind(this.dataItem));

Please bind "this" for better readability.

::: browser/components/downloads/src/DownloadsCommon.jsm
@@ +246,4 @@
>  
>      // If no download has been loaded, don't use the methods of the Download
>      // Manager service, so that it is not initialized unnecessarily.
> +    for (let download in aDownloads) {

Yes, let...of is the correct form.

We have a collection of iterable items: "let element of list" is equivalent to "let [index, element] in Iterator(list)".

@@ +705,5 @@
> +            dataItem.getDownload(function(aDownload) {
> +              if (!aDownload) {
> +                this._removeDataItem(dataItem.downloadGuid);
> +              }
> +            }.bind(this));

You don't want to go through the lazy getter here, since this will return a valid reference if the download was accessed previously, but what we care about the current existence state of the item.

Either access nsIDownloadManager directly, or create a different asynchronous method on the dataItem.

@@ +960,5 @@
> +        }
> +      }, Ci.nsIThread.DISPATCH_NORMAL);
> +    } else {
> +      Services.downloads.getDownloadByGUID(this.downloadGuid, function(status, result) {
> +        aCallback(result);

It seems you don't assign _download anywhere.

For better diagnostics:

+        if (!Components.isSuccessCode(status)) {
+          Cu.reportError(new Components.Exception(
+                         "Cannot retrieve download for GUID: " + this.downloadGuid,
+                         status));
+        } else {
+          this._download = result;
+          aCallback(result);
+        }

I'd rather you defined the property explicitly, and checked for null instead of existence. See the "_attention" property below in the same file for style reference.

nit: status -> aStatus

@@ +1482,1 @@
>        }

+      for each (let dataItem in DownloadsCommon.data.dataItems) {
+        if (dataItem && dataItem.inProgress) {
+          yield dataItem;
+        }
+      }

DownloadsCommon.data.dataItems is an object used like a map, not an array, and it can contain properties which map to null values (for removed downloads, to handle a possible race condition in database loading).

There's no need to check for _itemCount anymore: this was an optimization, and if there are no in-progress items, this generator would simply yield no items at all.

Globally, please check that comments still reflect what the code currently does.
Attachment #686578 - Flags: feedback?(paolo.mozmail)
Comment on attachment 686578 [details] [diff] [review]
WIP 2

Review of attachment 686578 [details] [diff] [review]:
-----------------------------------------------------------------

::: browser/components/downloads/content/downloads.js
@@ +1234,5 @@
>      {
>        this.commands.downloadsCmd_cancel.apply(this);
>  
> +      this.dataItem.getDownload(function(aDownload) {
> +        aDownload.cancel();

In this function, it's possible that aDownload was already cancelled, which will cause an assertion to fail if we try to cancel it again.

You should check the inProgress state of the dataItem like in downloadsCmd_cancel before doing a .cancel().
(In reply to Ehsan Akhgari [:ehsan] from comment #4)
> 2. You can't pause and resume downloads from the panel (the pause button
> does not show up)

Just FYI - there *is* no pause button. That command only exists in the context menu. The only buttons that will appear on the download item are Cancel, Retry, and Open Containing Folder (if completed).
Attached patch Patch v1 (obsolete) — Splinter Review
I'm gonna scoop this one from Ehsan and take some pressure off.

So, this patch appears to do the job, and the tests pass (I had to do a bit of fiddling to make the tests work again).

Gonna give 'er a once over and then r?.
Assignee: ehsan → mconley
Attachment #686578 - Attachment is obsolete: true
Attachment #686578 - Flags: feedback?(mconley)
Attachment #686578 - Flags: feedback?(mak77)
Attached patch Patch v2 (obsolete) — Splinter Review
Updated some documentation to reflect these changes. I think we're good to go here for review.
Attachment #686744 - Attachment is obsolete: true
Attachment #686770 - Flags: review?(mak77)
Attachment #686770 - Flags: feedback?(paolo.mozmail)
Comment on attachment 686770 [details] [diff] [review]
Patch v2

Review of attachment 686770 [details] [diff] [review]:
-----------------------------------------------------------------

Looks good overall!

::: browser/components/downloads/content/downloads.js
@@ +1313,2 @@
>          }
> +      });

Missing .bind(this)

::: browser/components/downloads/src/DownloadsCommon.jsm
@@ +708,5 @@
> +
> +          if (dataItemBinding) {
> +            Services.downloads.getDownloadByGUID(dataItemBinding.downloadGuid,
> +                                                 function(aStatus, aResult) {
> +              if (!aResult) {

Per documentation, this should check either !isSuccessCode(aStatus) or more specifically aStatus == Cr.NS_ERROR_NOT_AVAILABLE.

@@ +763,5 @@
>      // When a download is retried, we create a different download object from
>      // the database with the same ID as before. This means that the nsIDownload
>      // that the dataItem holds might now need updating.
> +    if (dataItem._download) {
> +      dataItem._download = aDownload;

nit: clarifying why we assign only if the value is not null can be helpful.

@@ +964,5 @@
> +      Services.tm.mainThread.dispatch({
> +        run: function() {
> +          aCallback(download);
> +        }
> +      }, Ci.nsIThread.DISPATCH_NORMAL);

Given the [function] IDL attribute on nsIRunnable, you can just dispatch "function() aCallback(download)".

@@ +1488,5 @@
>     */
>    _activeDownloads: function DID_activeDownloads()
>    {
>      // If no download has been loaded, don't use the methods of the Download
>      // Manager service, so that it is not initialized unnecessarily.

Outdated comment.
Attachment #686770 - Flags: feedback?(paolo.mozmail) → feedback+
Attached patch Patch v3 (feedback+ from paolo) (obsolete) — Splinter Review
Thanks Paolo! Fixed those issues.
Attachment #686770 - Attachment is obsolete: true
Attachment #686770 - Flags: review?(mak77)
Attachment #686855 - Flags: review?(mak77)
Thanks a lot Mike!  :-)
Blocks: 675902
Comment on attachment 686855 [details] [diff] [review]
Patch v3 (feedback+ from paolo)

Review of attachment 686855 [details] [diff] [review]:
-----------------------------------------------------------------

Nothing particularly bad, some things to cleanup

thank you

::: browser/components/downloads/content/downloads.js
@@ +1231,5 @@
>     */
>    commands: {
>      cmd_delete: function DVIC_cmd_delete()
>      {
> +      this.dataItem.getDownload(function(aDownload) {

really-really-really-global-nit: as code style for this component (and more generally browser) we prefer a whitespace for anon functions like "function ()"

::: browser/components/downloads/src/DownloadsCommon.jsm
@@ +210,3 @@
>     * statistics about that collection.
>     *
> +   * @param aDownloads An iterable collection of DownloadsDataItems.

DownloadsDataItems is a typo, should be DownloadDataItems.. there are 2 instances here, but I actually 
found another one in existing code that I'd appreciate if you could fix while here
http://mxr.mozilla.org/mozilla-central/source/browser/components/downloads/src/DownloadsCommon.jsm#1566


The param should be renamed, from aDownloads to aDataItems aDownloadDataItems, whatever you prefer

@@ +271,4 @@
>            download.state != nsIDM.DOWNLOAD_CANCELED &&
>            download.state != nsIDM.DOWNLOAD_FAILED) {
> +        summary.totalSize += download.maxBytes;
> +        summary.totalTransferred += download.currBytes;

Everywhere where we have "download" here we should have dataItem... the input was previously nsIDownloads, it's now DownloadDataItems.

@@ +705,5 @@
> +          // Bug 449811 - We have to bind to the dataItem because Javascript
> +          // doesn't do fresh let-bindings per loop iteration.
> +          let dataItemBinding = dataItem;
> +
> +          if (dataItemBinding) {

I'd prefer if you invert these (assignment and check), the if (dataItem) condition can be checked externally and the "binding" is actually just needed in the callback, so I like it being defined just above it. I honestly thought this was not even considered a bug, we use the same "bindind" method in many code pieces.

@@ +945,3 @@
>      } else {
>        this.resumable = true;
>      }

I'm unsure about this change, to me looks like we should default this.resumable = true, and in the downloading case update it with the callback result to eventually false.
If I'm not wrong this is used for the pause/resume action, is it better to tell the user "we can't resume" when we can, or "we can resume" when we can't? I suppose the latter is the better compromise (well this is likely to be resolved even before the user may look at the button, so minor bug regardless).

@@ +966,5 @@
> +      // Return the download object asynchronously to the caller
> +      let download = this._download;
> +      Services.tm.mainThread.dispatch({
> +        run: function() aCallback(download)
> +      }, Ci.nsIThread.DISPATCH_NORMAL);

As paolo (I think) said, you don't need a runnable object here, just dispatch a function

@@ +1491,5 @@
>    _activeDownloads: function DID_activeDownloads()
>    {
> +    for each (let dataItem in DownloadsCommon.data.dataItems) {
> +      if (dataItem && dataItem.inProgress) {
> +        yield dataItem;

So, this was returning nsIDownloads, it is now returning dataItems... it should be renamed to _activeDataItems, both here (name and label) and where we pass it to summarizeDownloads

@@ +1689,5 @@
>     * to generate statistics about the downloads we care about - in this case,
>     * it's the downloads in this._dataItems after the first few to exclude,
>     * which was set when constructing this DownloadsSummaryData instance.
>     */
>    _downloadsForSummary: function DSD_downloadsForSummary()

as well as this one is no more _downloadsForSummary, it's now _dataItemsForSummary

::: browser/components/downloads/test/browser/head.js
@@ +178,5 @@
>    let statement = Services.downloads.DBConnection.createAsyncStatement(
>                    "INSERT INTO moz_downloads (" + columnNames +
>                                      ") VALUES(" + parameterNames + ")");
>    try {
> +    for (let i = 0; i < aDataRows.length; ++i) {

I'm honestly not sure why we were doing this loop in reverse and now we do it forward... sorting by startTime? I think we may want to startTime++ at each added object, like we already do with endTime.

@@ +193,5 @@
>        }
>  
> +      // Because we're adding downloads directly to the database, we don't
> +      // get GUID's created for us automatically, so we have to do it ourselves.
> +      statement.params.guid = UUIDGenerator.generateUUID().toString();

We don't use UUIDs as guids, we use a special 12 chars GUID format...

but you should be happy since the solution is even simpler:
1. don't add guid to gDownloadRowTemplate
2. change the query to
INSERT INTO moz_downloads (" + columnNames +", guid) VALUES(" + parameterNames +", GENERATE_GUID())

And then you can also remove the uuidGenerator lazyServiceGetter
Attachment #686855 - Flags: review?(mak77) → review+
Attached patch Patch v4 (r+'d by mak) (obsolete) — Splinter Review
(In reply to Marco Bonardo [:mak] from comment #17)
> Comment on attachment 686855 [details] [diff] [review]
> Patch v3 (feedback+ from paolo)
> 
> Review of attachment 686855 [details] [diff] [review]:
> -----------------------------------------------------------------
> 
> Nothing particularly bad, some things to cleanup
> 
> thank you
> 
> ::: browser/components/downloads/content/downloads.js
> @@ +1231,5 @@
> >     */
> >    commands: {
> >      cmd_delete: function DVIC_cmd_delete()
> >      {
> > +      this.dataItem.getDownload(function(aDownload) {
> 
> really-really-really-global-nit: as code style for this component (and more
> generally browser) we prefer a whitespace for anon functions like "function
> ()"
> 

Ah, I never knew that. Fixed.

> ::: browser/components/downloads/src/DownloadsCommon.jsm
> @@ +210,3 @@
> >     * statistics about that collection.
> >     *
> > +   * @param aDownloads An iterable collection of DownloadsDataItems.
> 
> DownloadsDataItems is a typo, should be DownloadDataItems.. there are 2
> instances here, but I actually 
> found another one in existing code that I'd appreciate if you could fix
> while here
> http://mxr.mozilla.org/mozilla-central/source/browser/components/downloads/
> src/DownloadsCommon.jsm#1566
> 

All fixed - good catch.

> 
> The param should be renamed, from aDownloads to aDataItems
> aDownloadDataItems, whatever you prefer
> 
> @@ +271,4 @@
> >            download.state != nsIDM.DOWNLOAD_CANCELED &&
> >            download.state != nsIDM.DOWNLOAD_FAILED) {
> > +        summary.totalSize += download.maxBytes;
> > +        summary.totalTransferred += download.currBytes;
> 
> Everywhere where we have "download" here we should have dataItem... the
> input was previously nsIDownloads, it's now DownloadDataItems.
> 

Done.

> @@ +705,5 @@
> > +          // Bug 449811 - We have to bind to the dataItem because Javascript
> > +          // doesn't do fresh let-bindings per loop iteration.
> > +          let dataItemBinding = dataItem;
> > +
> > +          if (dataItemBinding) {
> 
> I'd prefer if you invert these (assignment and check), the if (dataItem)
> condition can be checked externally and the "binding" is actually just
> needed in the callback, so I like it being defined just above it. I honestly
> thought this was not even considered a bug, we use the same "bindind" method
> in many code pieces.
> 

Done.

> @@ +945,3 @@
> >      } else {
> >        this.resumable = true;
> >      }
> 
> I'm unsure about this change, to me looks like we should default
> this.resumable = true, and in the downloading case update it with the
> callback result to eventually false.
> If I'm not wrong this is used for the pause/resume action, is it better to
> tell the user "we can't resume" when we can, or "we can resume" when we
> can't? I suppose the latter is the better compromise (well this is likely to
> be resolved even before the user may look at the button, so minor bug
> regardless).
> 

I went with your suggestion, and default to resumable = true. I also added a comment about that.

> @@ +966,5 @@
> > +      // Return the download object asynchronously to the caller
> > +      let download = this._download;
> > +      Services.tm.mainThread.dispatch({
> > +        run: function() aCallback(download)
> > +      }, Ci.nsIThread.DISPATCH_NORMAL);
> 
> As paolo (I think) said, you don't need a runnable object here, just
> dispatch a function
> 

Ok, now just dispatching a function.

> @@ +1491,5 @@
> >    _activeDownloads: function DID_activeDownloads()
> >    {
> > +    for each (let dataItem in DownloadsCommon.data.dataItems) {
> > +      if (dataItem && dataItem.inProgress) {
> > +        yield dataItem;
> 
> So, this was returning nsIDownloads, it is now returning dataItems... it
> should be renamed to _activeDataItems, both here (name and label) and where
> we pass it to summarizeDownloads
> 

Done!

> @@ +1689,5 @@
> >     * to generate statistics about the downloads we care about - in this case,
> >     * it's the downloads in this._dataItems after the first few to exclude,
> >     * which was set when constructing this DownloadsSummaryData instance.
> >     */
> >    _downloadsForSummary: function DSD_downloadsForSummary()
> 
> as well as this one is no more _downloadsForSummary, it's now
> _dataItemsForSummary
> 

Fixed.

> ::: browser/components/downloads/test/browser/head.js
> @@ +178,5 @@
> >    let statement = Services.downloads.DBConnection.createAsyncStatement(
> >                    "INSERT INTO moz_downloads (" + columnNames +
> >                                      ") VALUES(" + parameterNames + ")");
> >    try {
> > +    for (let i = 0; i < aDataRows.length; ++i) {
> 
> I'm honestly not sure why we were doing this loop in reverse and now we do
> it forward... sorting by startTime? I think we may want to startTime++ at
> each added object, like we already do with endTime.
> 

Ah, thanks. Ok, now that we're incrementing startTime, I've put the reverse-loop back, as well as the original comment.

> @@ +193,5 @@
> >        }
> >  
> > +      // Because we're adding downloads directly to the database, we don't
> > +      // get GUID's created for us automatically, so we have to do it ourselves.
> > +      statement.params.guid = UUIDGenerator.generateUUID().toString();
> 
> We don't use UUIDs as guids, we use a special 12 chars GUID format...
> 
> but you should be happy since the solution is even simpler:
> 1. don't add guid to gDownloadRowTemplate
> 2. change the query to
> INSERT INTO moz_downloads (" + columnNames +", guid) VALUES(" +
> parameterNames +", GENERATE_GUID())
> 
> And then you can also remove the uuidGenerator lazyServiceGetter

I guess it was kind of a long shot. :) Thanks, yes, that's much better.
Attachment #686855 - Attachment is obsolete: true
Ok, did a quick self-review, fixed a trailing whitespace, trailing comma, and an unneeded newline.

I think we're good here. I'm gonna land this.

Thanks again, Marco!
Attachment #687345 - Attachment is obsolete: true
https://hg.mozilla.org/mozilla-central/rev/29c8aceb7068
Status: ASSIGNED → RESOLVED
Closed: 12 years ago
Resolution: --- → FIXED
Target Milestone: --- → Firefox 20
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: