Titanium JIRA Archive
Titanium SDK/CLI (TIMOB)

[TIMOB-17502] iOS: URLSession Module returns blob instead of file location - causing low memory crash

GitHub Issuen/a
TypeBug
PriorityMedium
StatusClosed
ResolutionFixed
Resolution Date2015-01-07T16:32:50.000+0000
Affected Version/sRelease 3.3.0
Fix Version/sRelease 4.0.0
ComponentsiOS
LabelsTCSupport
ReporterShawn Lan
AssigneeChee Kiat Ng
Created2014-07-31T18:57:59.000+0000
Updated2015-07-16T18:47:41.000+0000

Description

Problem Description

downloadcompleted returns e.data as a blob in physical memory!! If the file size is large, for example, over 1GB, it could cause low memory crash. Plus, using file.write(e.data) in this case takes too much time to run in the background. iOS only allows 30 seconds. Chances are the app will be either crashed or suspended before the file is saved. I suggest: upon downloadcompleted move the file to a temp directory and return the path instead of a blob.

TESTING ENVIRONMENT

CLI version 3.3.0, Titanium SDK version 3.3.0.GA iOS7

CODE SAMPLE

app.js

// Require in the urlSession module
var urlSession = require("com.appcelerator.urlSession");
var session;

// App UI
var win = Ti.UI.createWindow({backgroundColor:"white"});
var progress = Ti.UI.createProgressBar({
    width: 200,
    height: 50,
    min: 0,
    max: 1,
    value: 0,
    style: Ti.UI.iPhone.ProgressBarStyle.PLAIN,
    top: 10,
    message: 'Downloading image URL',
    font: { fontSize: 12, fontWeight: 'bold'},
    color: '#888'
});
win.add(progress);
var imageView = Ti.UI.createImageView({
        top:150,
          height:300,
          width:200
    });
win.add(imageView);

var button = Ti.UI.createButton({
    title:'Download Image (url)',
    height:40,
    width:200,
    top:70
});

button.addEventListener('click', function()
{
    // Create a session configuration
    // The string parameter is an arbitrary string used to identify the session in events
    var sessionConfig = urlSession.createURLSessionBackgroundConfiguration("com.appcelerator.test");
    // Create a session
    session = urlSession.createURLSession(sessionConfig);
    // Create a background download task to get the asset with the URL
    urlSession.backgroundDownloadTaskWithURL(session,"https://raw.github.com/appcelerator-developer-relations/KitchenSink/master/Resources/images/dog@2x~iphone.jpg");

    progress.show();
});

win.add(button);
win.open();
 
// Monitor this event to receive updates on the progress of the download
Ti.App.iOS.addEventListener("downloadprogress", function(e) {
    // Update the progress indicator
    progress.value = (e.totalBytesWritten/e.totalBytesExpectedToWrite);
});


// Monitor this event to know when the download completes
Ti.App.iOS.addEventListener("downloadcompleted", function(e) {
    Ti.API.info("download completed " + JSON.stringify(e));
 
    // Update the image
    imageView.image = e.data;
 
    // Invalidate the session and cancel current session tasks
    urlSession.invalidateAndCancel(session);

    // Notify the user the download is complete if the application is in the background
    Ti.App.iOS.scheduleLocalNotification({
        alertBody:"Download complete!",
        date:new Date().getTime() 
    });
    progress.hide();
});


// Monitor this event to know when all session tasks have completed
Ti.App.iOS.addEventListener('sessioncompleted', function(e) {
    Ti.API.info("sessioncompleted " + JSON.stringify(e));
    if (e.success) {
        alert("Downloads completed successfully.");
    }
});

STEP TO REPRODUCE

- Create a new Project - Double-click the Project's "tiapp.xml" file to open it in the Overview tab. - In the Modules section, click the Add button (green plus sign) to open the Mobile Modules dialog. - Locate and select "com.appcelerator.urlSession". This module is part of the Titanium SDK since Release 3.2.0 - Click OK. - Copy the code segment above to the project "app.js" file - Run the Project

OBSERVED RESULT

If the URL is changed to download a large file, progress bar would not response and the app will crash after some time.

Comments

  1. Mauro Parra-Miranda 2014-08-14

    Thanks for your report. The Platform team will set the priority on this issue.
  2. Shawn Lan 2014-12-03

    When will you set a priority on this issue? This is important to me. If you can release your URLSession module's source code, I can implement this myself. Thanks.
  3. Ingo Muschenetz 2014-12-03

    [~shawnlan] the module is here. We would welcome a PR to fix this issue: https://github.com/appcelerator-modules/ti.urlsession
  4. Shawn Lan 2014-12-03

    I don't see any source file other than this one: https://github.com/appcelerator-modules/ti.urlsession/blob/master/ios/js/app.hjs That's the only source file for this module?
  5. Ingo Muschenetz 2014-12-03

    Yes, since this was built using Hyperloop. It may be relatively difficult to figure out how to update it ATM, but there are instructions on the repo.
  6. Shawn Lan 2014-12-04

    Actually, to fix this issue, I think we have to mainly deal with the iOS application-level events: Titanium.App.iOS.backgroundtransfer Titanium.App.iOS.downloadprogress Titanium.App.iOS.downloadcompleted - especially this one Titanium.App.iOS.sessioncompleted Titanium.App.iOS.sessioneventscompleted they are in the Titanium SDK, right? I'll wait for you guys to fix this. Thanks.
  7. Shawn Lan 2014-12-04

    Here it is: https://github.com/appcelerator/titanium_mobile/blob/afc36d4f421424dc1ef6d618201f43616bb7857b/iphone/Classes/TiApp.m (line 612) Instead of loading the file into a blob, simply move the file into a temporary location and return the path. Should be simple.
  8. Chee Kiat Ng 2014-12-26

    URL Session Module fixed to be able to download file sizes larger than 1Gb with no low memory crash. Not required to return file location. If necessary, it is possible to obtain file via blob.getFile(); updated test code:
       // Require in the urlSession module
       var urlSession = require("com.appcelerator.urlSession");
       var session;
        
       // App UI
       var win = Ti.UI.createWindow({backgroundColor:"white"});
       var progress = Ti.UI.createProgressBar({
           width: 200,
           height: 50,
           min: 0,
           max: 1,
           value: 0,
           style: Ti.UI.iPhone.ProgressBarStyle.PLAIN,
           top: 10,
           message: 'Downloading image URL',
           font: { fontSize: 12, fontWeight: 'bold'},
           color: '#888'
       });
       win.add(progress);
       var imageView = Ti.UI.createImageView({
               top:150,
                 height:300,
                 width:200
           });
       win.add(imageView);
        
       var button = Ti.UI.createButton({
           title:'Download Image (url)',
           height:40,
           width:200,
           top:70
       });
        
       button.addEventListener('click', function()
       {
           // Check if the device is running iOS 8 or later, before registering for local notifications
           if (Ti.Platform.name == "iPhone OS" && parseInt(Ti.Platform.version.split(".")[0]) >= 8) {
               Ti.App.iOS.registerUserNotificationSettings({
                  types: [
                       Ti.App.iOS.USER_NOTIFICATION_TYPE_ALERT,
                       Ti.App.iOS.USER_NOTIFICATION_TYPE_SOUND,
                       Ti.App.iOS.USER_NOTIFICATION_TYPE_BADGE
                   ]
               });
           }
           // Create a session configuration
           // The string parameter is an arbitrary string used to identify the session in events
           var sessionConfig = urlSession.createURLSessionBackgroundConfiguration("com.appcelerator.test");
           // Create a session
           session = urlSession.createURLSession(sessionConfig);
           // Create a background download task to get the asset with the URL
           //urlSession.backgroundDownloadTaskWithURL(session,"https://raw.github.com/appcelerator-developer-relations/KitchenSink/master/Resources/images/dog@2x~iphone.jpg"); //small file
           //urlSession.backgroundDownloadTaskWithURL(session,"http://imgsrc.hubblesite.org/hu/db/images/hs-2004-07-a-full_jpg.jpg"); //medium sized file
           urlSession.backgroundDownloadTaskWithURL(session,"http://www.wswd.net/testdownloadfiles/1GB.zip"); //huge file
           progress.show();
       });
        
       win.add(button);
       win.open();
        
       // Monitor this event to receive updates on the progress of the download
       Ti.App.iOS.addEventListener("downloadprogress", function(e) {
           // Update the progress indicator
           progress.value = (e.totalBytesWritten/e.totalBytesExpectedToWrite);
       });
        
        
       // Monitor this event to know when the download completes
       Ti.App.iOS.addEventListener("downloadcompleted", function(e) {
           Ti.API.info("download completed " + JSON.stringify(e));
           var cacheFile = e.data.getFile();
           var downloadedFile = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory, cacheFile.getName());
           downloadedFile.write(e.data); // copy and save from cache
           Ti.API.info("downloadedFile: " + downloadedFile.getNativePath());
           // set new Image
           //imageView.image = e.data;
           // Invalidate the session and cancel current session tasks
           urlSession.invalidateAndCancel(session);
        
           // Notify the user the download is complete if the application is in the background
           var notification = Ti.App.iOS.scheduleLocalNotification({
               alertBody: "Download completed!",
               badge: 1,
               sound: "/alert.wav",
               date: new Date(new Date().getTime())
           });
       
           progress.hide();
       });
        
        
       // Monitor this event to know when all session tasks have completed
       Ti.App.iOS.addEventListener('sessioncompleted', function(e) {
           Ti.API.info("sessioncompleted " + JSON.stringify(e));
           if (e.success) {
               alert("Downloads completed successfully.");
           }
       });
       
    *Note: the link downloaded is 1GB in size in the test code above. Replace with commented out lines of code linked to smaller files if necessary.*
  9. Chee Kiat Ng 2014-12-26

    PR Here: https://github.com/appcelerator/titanium_mobile/pull/6527
  10. Shawn Lan 2014-12-31

    Your solution doesn't require returning file location, but it also doesn't load the blob into memory. Right? If it loads blob into memory like before, then there would be memory-low issue.
  11. Chee Kiat Ng 2014-12-31

    [~shawnlan], yes, my solution doesn't load blob into memory. The fix is instead of allocating memory, Save a copy of the file in the temporary directory, and the returned blob actually just reference that file.
  12. Tim Statler 2015-01-07

    Hi [~cng], does this change require a doc update?
  13. Chee Kiat Ng 2015-01-08

    Hi [~tstatler], No doc update required since the parameters remain the same. However it'll be good to put a note somewhere about downloading big files it's advisable to make a copy as described in the above sample code, since the returned blob references a file location that's only temporary for that app instance.
  14. Ewan Harris 2015-01-17

    Verified fix on: Mac OSX 10.10.1 Appcelerator Studio, build: 3.4.1.201410281743 Titanium SDK build: 3.6.0.v20150116070927 Titanium CLI, build: 3.4.1 Alloy: 1.5.1 Xcode 6.1.1 iPhone 6 Plus (8.1.1), iPad Air 2 (8.2b4) Using the code in Kiats comment, ran on both devices and downloaded each of the files, the app no longer crashes with low memory. Observed memory in Xcode Instruments during the download as well. Closing ticket
  15. Shawn Lan 2015-04-16

    Will this fix be available with SDK 3.6 or SDK 4.0?
  16. Ingo Muschenetz 2015-04-16

    [~shawnlan] 4.0, as the fix version says.
  17. Shawn Lan 2015-07-16

    Today I found a problem again with large file download. To re-produce, download a large file > 1GB, press "Home" to make the app go to the background. Once the download finishes, only "backgroundtransfer" is fired. The other events are not: sessioneventscompleted, downloadcompleted, etc. Without downloadcompleted, the file can't be saved. Without sessioneventscompleted, the app won't call endBackgroundHandler, and it will run over 30 seconds, eventually being terminated by the system. With a medium size download (2-300MB) in the background, calling endBackgroundHandler when sessioneventscompleted fired throws the following error, and the callback is failed to complete.
        [WARN] :   ing: Application delegate received call to -application:handleEventsForBackgroundURLSession:completionHandler: but the completion handler was never called.
        [WARN] :   assertion failed: 12H143: libxpc.dylib + 51923 [7FF71721-5624-3DDE-9615-146A4D8B7197]: 0x7d
        
  18. Shawn Lan 2015-07-16

JSON Source