4

I am trying to upload many (3 for now) files in parallel using XMLHttpRequest. If have some code that pulls them from a list of many dropped files and makes sure that at each moment I am sending 3 files (if available).

Here is my code, which is standard as far as I know:

            var xhr = item._xhr = new XMLHttpRequest();
            var form = new FormData();
            var that = this;

            angular.forEach(item.formData, function(obj) {
                angular.forEach(obj, function(value, key) {
                    form.append(key, value);
                });
            });

            form.append(item.alias, item._file, item.file.name);

            xhr.upload.onprogress = function(event) {
                // ...
            };

            xhr.onload = function() {
                // ...
            };

            xhr.onerror = function() {
                // ...
            };

            xhr.onabort = function() {
                // ...
            };

            xhr.open(item.method, item.url, true);

            xhr.withCredentials = item.withCredentials;

            angular.forEach(item.headers, function(value, name) {
                xhr.setRequestHeader(name, value);
            });

            xhr.send(form);

Looking at the network monitor in Opera's developer tools, I see that this kinda works and I get 3 files "in progress" at all times:

network traffic

However, if I look the way the requests are progressing, I see that 2 of the 3 uploads (here, the seemingly long-running ones) are being put in status "Pending" and only 1 of the 3 requests is truly active at a time. This gets reflected in the upload times as well, since no time improvement appears to happen due to this parallelism.

I have placed console logs all over my code and it seems like this is not a problem with my code.

Are there any browser limitations to uploading files in parallel that I should know about? As far as I know, the AJAX limitations are quite higher in number of requests than what I use here... Is adding a file to the request changing things?

user2173353
  • 4,316
  • 4
  • 47
  • 79
  • 2
    Seems like an error on the server side. There is no problem on browser end. The browser had already requested handshakes at the same time for 3 requests. Your server is probably able to handle only 1 request at a time. That’s why the handshake is in Pending state for long. You need to increase the number of requests your server is able to handle. – yeshashah Jun 22 '18 at 06:07
  • @yeshashah My server is ISS and there is no limit set on it. My framework is ASP.NET and nowhere in my code do I lock/block things. *But*, after your comment and after some search I stumbled across this, which could well be the case: https://stackoverflow.com/a/9016375/2173353. If this is true, I will have to find a way to bypass this behavior... – user2173353 Jun 22 '18 at 06:18
  • 2
    Yeah. Many servers avoid processing requests with same sessionId simultaneously to avoid Race conditions while write-operations. AWS S3 allows direct Browser uploads in which the file is broken in separate chunks, each chunk sent simultaneously to the server and then the server assembles them in correct order from the fileId and chunkId. You explore something like that. – yeshashah Jun 22 '18 at 06:42
  • @yeshashah But how does using chunks help? Won't this have the same problem eventually? Again it will be multiple requests racing for the same resource (in my case the user Session object). Of course the lock times will be smaller, but the sum of those lock times will still be the same (more or less) as the time used for the complete file upload. I am surprised that this improves upload times. – user2173353 Jun 22 '18 at 07:09
  • No, creating chunks won't help you here. AWS S3 - Direct Browser Uploads is just an example I gave to prove that browsers can send multiple requests simultaneously to same domain and servers can accept those requests, simultaneously. I have not explored the part where it is with _same_ sessionId or different. But it must be through same sessionId as the file being uploaded in chunks belongs to same client. – yeshashah Jun 22 '18 at 07:25
  • 1
    A solution I found for ASP servers is disabling the session state: https://stackoverflow.com/a/4319204/2873331 `Setting EnableSessionState="ReadOnly" will prevent that page from gaining an exclusive lock on the SessionState (but the page itself would have to wait for other non-ReadOnly requests by the user to finish before loading).` Not sure how well that would work. – yeshashah Jun 22 '18 at 07:26
  • @yeshashah Yes, I was going to test that just now. ;) – user2173353 Jun 22 '18 at 07:42
  • I have found the solution and added it as an answer. It works great! ;) – user2173353 Jun 22 '18 at 08:29

2 Answers2

4

This turned out to be ASP.NET causing the issue. Multiple requests coming from the same SessionId get serialized, because they lock the session object.

See here.

My fix was to make the session read-only for this particular action. That way, no locking was required. This is my code (original code taken from here):

public class CustomControllerFactory : DefaultControllerFactory
{
    protected override SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, Type controllerType)
    {
        if (controllerType == null)
        {
            return SessionStateBehavior.Default;
        }

        var actionName = requestContext.RouteData.Values["action"].ToString();
        MethodInfo actionMethodInfo;
        var methods = controllerType.GetMethods(BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
        actionMethodInfo = methods.FirstOrDefault(x => x.Name == actionName && x.GetCustomAttribute<ActionSessionStateAttribute>() != null);
        if (actionMethodInfo != null)
        {
            var actionSessionStateAttr = actionMethodInfo.GetCustomAttributes(typeof(ActionSessionStateAttribute), false)
                .OfType<ActionSessionStateAttribute>()
                .FirstOrDefault();

            if (actionSessionStateAttr != null)
            {
                return actionSessionStateAttr.Behavior;
            }
        }
        return base.GetControllerSessionBehavior(requestContext, controllerType);
    }
}


[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class ActionSessionStateAttribute : Attribute
{
    public SessionStateBehavior Behavior { get; private set; }
    public ActionSessionStateAttribute(SessionStateBehavior behavior)
    {
        this.Behavior = behavior;
    }
}

// In your Global.asax.cs
protected void Application_Start(object sender, EventArgs e)
{
    // .........
    ControllerBuilder.Current.SetControllerFactory(typeof(CustomControllerFactory));
}


// You use it on the controller action like that:
[HttpPost]
[Authorize(Roles = "Administrators")]
[ValidateAntiForgeryToken]
[ActionSessionState(SessionStateBehavior.ReadOnly)]
public async Task<ActionResult> AngularUpload(HttpPostedFileBase file){}

And here is the glorious result: result

user2173353
  • 4,316
  • 4
  • 47
  • 79
  • Did you check the total time taken by both mechanisms? – Prakhar Londhe Aug 20 '21 at 07:46
  • @PrakharLondhe Yes, having requests execute in parallel did reduce the total time. But it depends on your server code. E.g. if it contains some other locking code (apart from the session lock itself), if it handles parallel requests well enough, if there are no other bottlenecks like DB calls, etc. – user2173353 Aug 20 '21 at 09:35
  • What was the speedup you got? The issue is since upload speed is constrained, even having multiple requests would still combine to same total speed as compared to one request.. – Prakhar Londhe Aug 21 '21 at 06:52
  • Also in your time taken field, if you see earlier your each request took 500 ms, while in the next ss each took almost 5 seconds – Prakhar Londhe Aug 21 '21 at 06:54
  • @PrakharLondhe In my case, the upload speed was not the bottleneck most of the times, but the fact that we weren't processing files in parallel, because we couldn't send files in parallel (we were doing some post processing that was blocking the request until the file was processed - so not all time was spend on the actual upload). I cannot recall the speed boost right now, since some years have passed, but in theory you could achieve multiplying the speed by considerable amounts, because our processing was quite heavy. – user2173353 Aug 22 '21 at 22:50
  • @PrakharLondhe So, it boils down to the fact that we were processing the files synchronously and did not use a queue for them and some asynchronous user-feedback for the processing status. Regarding the times in the two pictures, we should probably take into account that the code was running in a developer machine, with limited RAM, limited CPU parallelism, RAM-hungry applications open, and unoptimized software (SQL Server Express, perhaps even IIS Express), all running in the same machine. But you are very observant and this is nice. :) – user2173353 Aug 22 '21 at 22:56
2

The HTTP/1.1 RFC

Section 8.1.4 of the HTTP/1.1 RFC says a “single-user client SHOULD NOT maintain more than 2 connections with any server or proxy.

Read more here: Roundup on Parallel Connections

Kosh
  • 16,966
  • 2
  • 19
  • 34
  • The next sentence is: "The key here is the word “should.” Web clients don’t have to follow this guideline. " which they don't. Parallel connections is a pretty neat technique which is used for optimisation at many places like AWS S2 Browser uploads by sending chunks of a file simultaneously to server and assembling it on the server to make uploads faster. There is nothing wrong with making more than 1 connections with the server or proxy. – yeshashah Jun 22 '18 at 06:39