5

I've got a MonoTouch app that does an HTTP POST with a 3.5MB file, and it is very unstable on the primary platforms that I test on (iPhone 3G with OS 3.1.2 and iPhone 4 with OS 4.2.1). I'll describe what I'm doing here and maybe someone can tell me if I'm doing something wrong.

In order to rule out the rest of my app, I've whittled this down to a tiny sample app. The app is an iPhone OpenGL Project and it does only this:

  1. At startup, allocate 6MB of memory in 30k chunks. This simulates my app's memory usage.
  2. Read a 3.5MB file into memory.
  3. Create a thread to post the data. (Make a WebRequest object, use GetRequestStream(), and write the 3.5MB data in).
  4. When the main thread detects that the posting thread is done, goto step 2 and repeat.

Also, each frame, I allocate 0-100k to simulate the app doing something. I don't keep any references to this data so it should be getting garbage collected.

iPhone 3G Result: The app gets through 6 to 8 uploads and then the OS kills it. There is no crash log, but there is a LowMemory log showing that the app was jettisoned.

iPhone 4 Result: It gets an Mprotect error around the 11th upload.

A few data points:

  • Instruments does NOT show the memory increasing as the app continues to upload.
  • Instruments doesn't show any significant leaks (maybe 1 kilobyte total).
  • It doesn't matter whether I write the post data in 64k chunks or all at once with one Stream.Write() call.
  • It doesn't matter whether I wait for a response (HttpWebRequest.HaveResponse) or not before starting the next upload.
  • It doesn't matter if the POST data is even valid. I've tried using valid POST data and I've tried sending 3MB of zeros.
  • If the app is not allocating any data each frame, then it takes longer to run out of memory (but as mentioned before, the memory that I'm allocating each frame is not referenced after the frame it was allocated on, so it should be scooped up by the GC).

If nobody has any ideas, I'll file a bug with Novell, but I wanted to see if I'm doing something wrong here first.

If anyone wants the full sample app, I can provide it, but I've pasted the contents of my EAGLView.cs below.

using System;
using System.Net;
using System.Threading;
using System.Collections.Generic;
using System.IO;
using OpenTK.Platform.iPhoneOS;
using MonoTouch.CoreAnimation;
using OpenTK;
using OpenTK.Graphics.ES11;
using MonoTouch.Foundation;
using MonoTouch.ObjCRuntime;
using MonoTouch.OpenGLES;

namespace CrashTest
{
    public partial class EAGLView : iPhoneOSGameView
    {
        [Export("layerClass")]
        static Class LayerClass ()
        {
            return iPhoneOSGameView.GetLayerClass ();
        }

        [Export("initWithCoder:")]
        public EAGLView (NSCoder coder) : base(coder)
        {
            LayerRetainsBacking = false;
            LayerColorFormat = EAGLColorFormat.RGBA8;
            ContextRenderingApi = EAGLRenderingAPI.OpenGLES1;
        }

        protected override void ConfigureLayer (CAEAGLLayer eaglLayer)
        {
            eaglLayer.Opaque = true;
        }


        protected override void OnRenderFrame (FrameEventArgs e)
        {
            SimulateAppAllocations();
            UpdatePost();           

            base.OnRenderFrame (e);
            float[] squareVertices = { -0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f };
            byte[] squareColors = { 255, 255, 0, 255, 0, 255, 255, 255, 0, 0,
            0, 0, 255, 0, 255, 255 };

            MakeCurrent ();
            GL.Viewport (0, 0, Size.Width, Size.Height);

            GL.MatrixMode (All.Projection);
            GL.LoadIdentity ();
            GL.Ortho (-1.0f, 1.0f, -1.5f, 1.5f, -1.0f, 1.0f);
            GL.MatrixMode (All.Modelview);
            GL.Rotate (3.0f, 0.0f, 0.0f, 1.0f);

            GL.ClearColor (0.5f, 0.5f, 0.5f, 1.0f);
            GL.Clear ((uint)All.ColorBufferBit);

            GL.VertexPointer (2, All.Float, 0, squareVertices);
            GL.EnableClientState (All.VertexArray);
            GL.ColorPointer (4, All.UnsignedByte, 0, squareColors);
            GL.EnableClientState (All.ColorArray);

            GL.DrawArrays (All.TriangleStrip, 0, 4);

            SwapBuffers ();
        }


        AsyncHttpPost m_Post;
        int m_nPosts = 1;

        byte[] LoadPostData()
        {
            // Just return 3MB of zeros. It doesn't matter whether this is valid POST data or not.
            return new byte[1024 * 1024 * 3];
        }

        void UpdatePost()
        {
            if ( m_Post == null || m_Post.PostStatus != AsyncHttpPostStatus.InProgress )
            {
                System.Console.WriteLine( string.Format( "Starting post {0}", m_nPosts++ ) );

                byte [] postData = LoadPostData();

                m_Post = new AsyncHttpPost( 
                    "https://api-video.facebook.com/restserver.php", 
                    "multipart/form-data; boundary=" + "8cdbcdf18ab6640",
                    postData );
            }
        }

        Random m_Random = new Random(0);
        List< byte [] > m_Allocations;

        List< byte[] > m_InitialAllocations;

        void SimulateAppAllocations()
        {
            // First time through, allocate a bunch of data that the app would allocate.
            if ( m_InitialAllocations == null )
            {
                m_InitialAllocations = new List<byte[]>();
                int nInitialBytes = 6 * 1024 * 1024;
                int nBlockSize = 30000;
                for ( int nCurBytes = 0; nCurBytes < nInitialBytes; nCurBytes += nBlockSize )
                {
                    m_InitialAllocations.Add( new byte[nBlockSize] );
                }
            }

            m_Allocations = new List<byte[]>();
            for ( int i=0; i < 10; i++ )
            {
                int nAllocationSize = m_Random.Next( 10000 ) + 10;
                m_Allocations.Add( new byte[nAllocationSize] );
            }
        }       
    }




    public enum AsyncHttpPostStatus
    {
        InProgress,
        Success,
        Fail
    }

    public class AsyncHttpPost
    {
        public AsyncHttpPost( string sURL, string sContentType, byte [] postData )
        {
            m_PostData = postData;
            m_PostStatus = AsyncHttpPostStatus.InProgress;
            m_sContentType = sContentType;
            m_sURL = sURL;

            //UploadThread();
            m_UploadThread = new Thread( new ThreadStart( UploadThread ) );
            m_UploadThread.Start();            
        }

        void UploadThread()
        {
            using ( MonoTouch.Foundation.NSAutoreleasePool pool = new MonoTouch.Foundation.NSAutoreleasePool() )
            {
                try
                {
                    HttpWebRequest request = WebRequest.Create( m_sURL ) as HttpWebRequest;
                    request.Method = "POST";
                    request.ContentType = m_sContentType;
                    request.ContentLength = m_PostData.Length;

                    // Write the post data.
                    using ( Stream stream = request.GetRequestStream() )
                    {
                        stream.Write( m_PostData, 0, m_PostData.Length );
                        stream.Close();
                    }

                    System.Console.WriteLine( "Finished!" );

                    // We're done with the data now. Let it be garbage collected.
                    m_PostData = null;

                    // Finished!
                    m_PostStatus = AsyncHttpPostStatus.Success;
                }
                catch ( System.Exception e )
                {
                    System.Console.WriteLine( "Error in AsyncHttpPost.UploadThread:\n" + e.Message );
                    m_PostStatus = AsyncHttpPostStatus.Fail;
                }
            }
        }

        public AsyncHttpPostStatus PostStatus
        {
            get
            {
                return m_PostStatus;
            }
        }


        Thread m_UploadThread;

        // Queued to be handled in the main thread.
        byte [] m_PostData;

        AsyncHttpPostStatus m_PostStatus;
        string m_sContentType;
        string m_sURL;
    }
}
Mike
  • 1,169
  • 9
  • 26
  • This sounds quite similar to a problem I'm having - unfortunately I've not made much progress on it but I am keen to see if anyone else can shed any light on the situation – Luke Mar 31 '11 at 07:49
  • I've posted a question at http://stackoverflow.com/questions/5819700 which may relate to this. We too are doing http work and failing in a similar manner. – mj2008 Apr 28 '11 at 13:52
  • Have you posted a bug to http://bugzilla.xamarin.com/ ? The problem sounds similiar to issues I'm facing and I reported a bug about downloading. The problem still exists in MT 4.0.4 – Krumelur Aug 01 '11 at 09:26

1 Answers1

1

I think you should read in your file 1 KB (or some arbitrary size) at a time and write it to the web request.

Code similar to this:

byte[] buffer = new buffer[1024];
int bytesRead = 0;
using (FileStream fileStream = File.OpenRead("YourFile.txt"))
{
    while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
    {
        httpPostStream.Write(buffer, 0, bytesRead);
    }
}

This is off the top of my head, but I think it's right.

This way you don't have an extra 3MB floating around in memory when you don't really need to. I think tricks like this are even more important on iDevices (or other devices) than on the desktop.

Test the buffer size too, a larger buffer will get you better speeds up to a point (I remember 8KB being pretty good).

jonathanpeppers
  • 26,115
  • 21
  • 99
  • 182
  • Thanks for the suggestion. Unfortunately, it doesn't matter what chunk size I use for the uploading. I just modified my sample to upload 3MB in 8k chunks, and the app is still jettisoned by the iPhone due to low memory after some number of uploads. – Mike Mar 31 '11 at 20:58
  • Couple other things I notice: I don't think you need to use NSAutoReleasePool on the new thread. Also, I would use the Async methods on HttpWebRequest instead of making your own thread. When you make your own thread, you are in reality making 2 threads, since the synchronous methods in HttpWebRequest make their own thread internally--that is how the timeouts work. In my code above, I made a new buffer a local variable as an example. Make it a static or instance variable in your class and reuse it. – jonathanpeppers Mar 31 '11 at 21:38
  • The first time I wrote this upload code, I used all the async methods. Same problem occurred. I posted the bug with the synchronous methods because the repro code is shorter and simpler. – Mike Mar 31 '11 at 22:05
  • I went ahead and [filed a bug with Novell](https://bugzilla.novell.com/show_bug.cgi?id=684316). Hopefully this gets fixed soon. It's severely limiting a major feature of our app. – Mike Mar 31 '11 at 22:25
  • Some more interesting data. I managed to get at least the tiny sample app working properly by doing two things: (1) Instead of using HttpWebRequest, use TcpClient + SslStream and form the HTTP POST header myself. (2) Do small 8k write request like Johathan suggested. Neither one by itself made this program stable, but doing both together made it work. I've now run the app 4 separate times with 30 successful POSTs apiece on my iPhone 3G. I'm going to move this code into my REAL app and see if this fixes the problems. Will report back when that's done. – Mike Apr 01 '11 at 00:29
  • 2
    Ok, some good news and some bad news. THE GOOD: TcpClient+SslStream method successfully avoids the out-of-memory problems in my app. THE BAD: There's a separate problem where a thread gets into GC_remap(), which then calls abort() and the process dies. I've seen this problem on all devices during HTTP uploads, not just the iPhone3G. I'll work on getting a nice repro case outside of my app. – Mike Apr 01 '11 at 22:53
  • @Mike Did you solve this? We are doing HTTPS GETS and also having GC_remap() problems. Cannot pin it down at the moment. – mj2008 Apr 27 '11 at 08:49