Skip to content

Performance of 'http' + anonymous functions eventually slows down #3659

Closed

Description

@SynerG

When using anonymous functions (in IIFEs or Promises) to prepare the response of an HTTP request, the performance of the server suddenly slows down. Performance remains low until the server is restarted.

I initially found the problem using Promises. A script to replicate the problem:

var http = require('http');

var srv = http.createServer(function (req, res) {
    promiseValue().then(function(value){
        res.writeHead(200, {'Content-Type': 'text/plain'});
        res.end(value);
    });
});

function promiseValue() {
    return Promise.resolve().then(function() {
        var j = 0;
        for (var i = 0, l = 300000; i < l; i++) { j = j + 2; }
        return "Hi " + j;
    });
}

But Promises are not required:

var http = require('http');
var srv = http.createServer(function (req, res) {
    callbackValue(function(value){
        res.writeHead(200, {'Content-Type': 'text/plain'});
        res.end(value);
    });
});

function callbackValue(cb) {
    (function() {
        var j = 0;
        for (var i = 0, l = 300000; i < l; i++) {
            j = j + 2;
        }
        cb("Hi " + j);
    })();
}

srv.listen(3000, '127.0.0.1', function() {
    "use strict";
    console.log("Server listening on 127.0.0.1:3000");
});

Using a HTTP benchmarking tool to test the server, e.g. wrk:

#! /bin/bash
while :
do
  wrk -t2 -c32 -d5s "http://127.0.0.1:3000" | grep "Requests"
done

The server starts providing good performance, but an unpredictable point in time the performance drops:

Requests/sec:   2389.40
Requests/sec:   2394.00
Requests/sec:   2396.53
Requests/sec:   1071.95
Requests/sec:    890.80
Requests/sec:    889.23
Requests/sec:    893.15

Once it drops, it doesn't matter if you stop the load testing tool and let the server recover for a while, performance remains low until the server is restarted.

¿Is it a bug or am I doing something wrong?
If it is a bug, I don't know if it belongs to Nodejs or V8, although I have been unable to reproduce the problem only with Promises/IIFEs without the 'http' module.

Some additional details I have collected while hunting down the issue that may or may not be of interest to solve the problem:

  • It can be reproduced in all versions of Nodejs, at least in OSX. In particular, using Promises, I have been able to reproduce it in: v5.0.0, v4.2.1, iojsv1.0.0, even v0.11.13 (the first version that introduced Promise). Previous versions (v10.x, <v0.11.13) didn't have native Promise support, I have tested them using a 3rd party library (Bluebird) and performance matches the low one since the start up.
  • Performance drops after an unpredictable amount of time or requests. Following the same steps, I have found the drop in performance in a wide range of time (e.g. 15 seconds - 30 minutes).
  • The anonymous function used in the .then(function(){...}) of promiseValue() is the main cause of performance.
  • According to its performance, during the 1st phase the function seems to be optimized and during the 2nd phase it seems to become deoptimized (see comment).
    • If the anonymous function is refactorized as a named function, performance matches the high level and does NOT drop over time.
    • If an instruction that prevents optimization (e.g. try {} catch (e) {}) is added to the anonymous function, performance matches the low level and doesn't vary over time.
  • According to v8, the function is always not optimized (I could copy the snippet of how I tested this if required. Basically, it follows @petkaantonov's tip). (EDIT: The function starts being optimized and, at some point, becomes deoptimized also according to v8, see comment).

(I can also provide snippets for these points)

EDIT:

Activity

  1. changed the title [-]Performance of 'http' + anonymous functions eventually slowdown[/-] [+]Performance of 'http' + anonymous functions eventually slows down[/+] on Nov 4, 2015
  2. alexlamsl commented on Nov 4, 2015

    @alexlamsl

    Might not be related, but can you run netstat on the command line and see if you get a lot of stale TCP connections over the course of the test?

  3. YurySolovyov commented on Nov 4, 2015

    @YurySolovyov

    @alexlamsl but that would not explain why it performs ok without IIFE or promises.

  4. ChALkeR commented on Nov 4, 2015

    @ChALkeR
    Member

    @SynerG Could you also log server memory usage?

    @SynerG Also, --trace-gc.

    https://code.google.com/p/v8/issues/detail?id=4121 could be related.
    Edit: If 0.11.13 is affected, then it shouldn't.

    Ok, I ran it with --trace-gc, and it's not the culprit. Memory usage stays at ~50 MiB, gc times are low.

  5. SynerG commented on Nov 4, 2015

    @SynerG
    Author

    @YuriSolovyov, I think you could also reproduce the issue in your machine, but you will probably need to run the script for a longer period of time.
    I have just tried it in a Linux VM and in a droplet in Digital Ocean and both have eventually degraded, but it has required several minutes of the server+script running.
    From the DO VM:

    > node -v
    v4.2.1
    > uname -a
    Linux testingDroplet 3.16.0-4-amd64 #1 SMP Debian 3.16.7-ckt11-1 (2015-05-24) x86_64 GNU/Linux
    

    @alexlamsl, I have run netstat -an in the Linux VMs after performance has degraded (stopping the load testing tool, but leaving the server open) and it doesn't seem to leave open connections. I check the state after stopping wrk to see if there are connections left, because when I run wrk again for the second time, performance remains already degraded since the beginning.

    @ChALkeR, regarding memory usage:
    GC show lines like these:

    Initially:
    [2321]    12224 ms: Scavenge 6.3 (44.0) -> 5.7 (44.0) MB, 3.0 ms [allocation failure].
    [2321]    12356 ms: Scavenge 7.1 (44.0) -> 6.2 (45.0) MB, 2.0 ms [allocation failure].
    [2321]    12787 ms: Scavenge 10.5 (45.0) -> 6.8 (45.0) MB, 0.8 ms [allocation failure].
    ...
    After a while (still good performance):
    [2321]   444784 ms: Scavenge 47.7 (76.0) -> 32.3 (76.0) MB, 2.8 ms [allocation failure].
    ....
    Performance degraded:
    2321]   719454 ms: Scavenge 47.1 (90.0) -> 31.8 (90.0) MB, 2.0 ms [allocation failure].
    [2321]   723029 ms: Scavenge 47.3 (90.0) -> 32.0 (90.0) MB, 2.0 ms [allocation failure].
    [2321]   726270 ms: Scavenge 47.5 (90.0) -> 32.2 (90.0) MB, 1.5 ms [allocation failure].
    

    Also, the state of node process according to top:

    • After a while:
    VIRT=669824 RES=87292   SHR=12908   %MEM=17,2%
    
    • Performance degraded:
    VIRT=684160 RES=101252  SHR=12908   %MEM=20% 102000 free
    

    So it looks like the implementation creates a memory leakage, although the VM still has free memory even after performance has degraded.

  6. YurySolovyov commented on Nov 4, 2015

    @YurySolovyov

    I wonder if you can build node with this and try again, just in case.

  7. SynerG commented on Nov 4, 2015

    @SynerG
    Author

    @YuriSolovyov, I am not familiar with the process to rebuild Nodejs with a different version of v8.
    I guess I have to:

    • Clone Nodejs repo
    • Clone v8 repo
    • Replace node/deps/v8 directory with the /v8 folder from v8 repo
    • Build Nodejs

    Is this the correct process?

  8. targos commented on Nov 4, 2015

    @targos
    Member

    @SynerG It is not the easiest process. First you can try to directly apply the patch.

    Download it: https://github.com/proxy/gist.github.com/targos/b057bbaf1fd1589d4dba
    And from the nodejs repo, run git apply 4dd45e115be0122ee8b6ba1d9b3be5e86ea3b97c.diff --directory=deps/v8

    If you are lucky, it will apply cleanly

  9. kzc commented on Nov 4, 2015

    @kzc

    Since the problem is 100% reproducible given the excellent test case @SynerG provided any contributer could test out any patch they wish.

  10. evanlucas commented on Nov 4, 2015

    @evanlucas
    Contributor

    That patch does actually seem to prevent the large drop, but the average seems to be a bit lower right off the bat.

    on the master branch:

    Before

    Requests/sec:   2917.59
    Requests/sec:   2951.20
    Requests/sec:   2962.39
    Requests/sec:   2993.20
    Requests/sec:   3016.23
    Requests/sec:   3031.21
    Requests/sec:   2993.41
    Requests/sec:   2993.89
    Requests/sec:   2998.26
    Requests/sec:   3003.57
    Requests/sec:   3026.83
    Requests/sec:   2972.55
    Requests/sec:   2806.44
    Requests/sec:   2882.28
    Requests/sec:   2958.43
    Requests/sec:   3001.17
    Requests/sec:   3005.86
    Requests/sec:   2973.36
    Requests/sec:   2988.07
    Requests/sec:   1257.81
    Requests/sec:   1061.08
    Requests/sec:   1059.37
    Requests/sec:   1088.27
    

    After

    Requests/sec:   2690.31
    Requests/sec:   2982.13
    Requests/sec:   2640.89
    Requests/sec:   2544.31
    Requests/sec:   2692.97
    Requests/sec:   2657.87
    Requests/sec:   2724.81
    Requests/sec:   2544.41
    Requests/sec:   2601.36
    Requests/sec:   2487.85
    Requests/sec:   2893.20
    Requests/sec:   2488.91
    Requests/sec:   2363.16
    Requests/sec:   2324.68
    Requests/sec:   2358.04
    Requests/sec:   2584.03
    Requests/sec:   2469.70
    Requests/sec:   2436.66
    Requests/sec:   2389.08
    Requests/sec:   2378.98
    Requests/sec:   2388.63
    Requests/sec:   2494.49
    Requests/sec:   2459.86
    Requests/sec:   2651.36
    Requests/sec:   2321.81
    Requests/sec:   2400.41
    Requests/sec:   2207.65
    Requests/sec:   2502.09
    Requests/sec:   2691.86
    Requests/sec:   2454.27
    Requests/sec:   2350.09
    Requests/sec:   2759.47
    Requests/sec:   2730.08
    Requests/sec:   2380.81
    Requests/sec:   2472.89
    Requests/sec:   2900.46
    Requests/sec:   2660.17
    Requests/sec:   2462.02
    Requests/sec:   2672.50
    Requests/sec:   2595.16
    Requests/sec:   2520.38
    Requests/sec:   2562.00
    Requests/sec:   2355.87
    
  11. YurySolovyov commented on Nov 4, 2015

    @YurySolovyov

    @evanlucas I was blinded by this too, try to run it at least 5-10 mins.

  12. SynerG commented on Nov 4, 2015

    @SynerG
    Author

    @targos thanks for the tip. I was already rebuilding node the hard way when I read your comment.

    I have rebuild node with the latest version of v8 in the repo. It should already contain the desired commit as there is a path between the two according to git:

    git log 4dd45e115be0122ee8b6ba1d9b3be5e86ea3b97c..master --ancestry-path
    

    In my OSX development machine, the patch doesn't solve the problem:

    Requests/sec:   2422.61
    Requests/sec:   2428.32
    Requests/sec:   1996.10
    Requests/sec:    892.94
    Requests/sec:    893.19
    Requests/sec:    893.40
    

    @evanlucas could you keep running the test longer? It could eventually reach the point when it drops yet

  13. kzc commented on Nov 4, 2015

    @kzc

    Another data point - I tried out the (no Promise) test case on Mac with node v5.0.0 and got the same throughput drop off numbers as @SynerG at top of ticket: 2400 requests/sec => 900 requests/sec after 5 minutes.

    It can be reproduced in all versions of Nodejs, at least in OSX

    Then for the heck of it, I tried running the test case on jxcore, a node port using the mozilla JS engine - not v8:

    $ jx -v
    v0.10.40
    $ jx -jxv
    v Beta-0.3.0.4
    $ jx -jsv
    Mozilla SpiderMonkey v34
    

    After an hour I did not see any drop off:

    09:55:21 Requests/sec:   4508.56
    09:55:26 Requests/sec:   4461.28
    09:55:31 Requests/sec:   4478.79
    ...
    10:22:28 Requests/sec:   4882.16
    10:22:33 Requests/sec:   4864.88
    10:22:38 Requests/sec:   4873.53
    ...
    10:49:54 Requests/sec:   4452.07
    10:49:59 Requests/sec:   4470.95
    10:50:04 Requests/sec:   4868.22
    ...
    10:59:09 Requests/sec:   4787.37
    10:59:14 Requests/sec:   4786.83
    10:59:19 Requests/sec:   4816.61
    

    which suggests it may be a v8 issue.

  14. added
    httpIssues and PRs related to the http subsystem.
    on Nov 4, 2015
  15. SynerG commented on Nov 4, 2015

    @SynerG
    Author

    @kzc good point using jxcore. Do they only replace the JS engine or could they have also reimplemented node features such as the http module?

    It may be a v8 issue, but I have been unable to reproduce the problem using only v8 features.
    I have tried to remove the http module and replace it with continuous calls to callbackValue(), but performance doesn't degrade over time.

    For example, using the following script:

    // An attempt to replace http.createServer and trigger continuous calls
    (function triggerCall(){
        callbackValue(function(value){
            statistics();
            process.nextTick(triggerCall);
        });
    })();
    
    // Same function than before
    function callbackValue(cb) {
        (function() {
            var j = 0;
            for (var i = 0, l = 300000; i < l; i++) {
                j = j + 2;
            }
            cb("Hi " + j);
        })();
    }
    
    // Print basic statistics periodically
    var startTime = (new Date()).getTime();
    var lastPrintTime = startTime;
    var MIN_ELAPSED_TIME = 5000;
    var currentTime, elapsedTime;
    var callsCounter = 0;
    
    function statistics(){
        callsCounter++;
        currentTime = (new Date()).getTime();
        elapsedTime = currentTime-lastPrintTime;
        if ( elapsedTime >= MIN_ELAPSED_TIME){
            console.log( callsCounter/(MIN_ELAPSED_TIME/1000)+" calls/sec\t\t "+ (currentTime-startTime)/1000 +" seconds up");
            callsCounter=0;
            lastPrintTime=currentTime;
        }
    }

    The snippet doesn't use http, but performance remains high even after almost 2 hours running:

    2870.8 calls/sec         6750.002 seconds up
    2889.6 calls/sec         6755.002 seconds up
    2880.4 calls/sec         6760.002 seconds up
    

    This alternative to replace http also fails to reproduce the issue (i.e. performance remains high after an hour):

    function mockServerF(fn){
        fn();
    }
    
    var mockServer = mockServerF.bind(null, function(){
        callbackValue(function(value){
            statistics();
            process.nextTick(mockServer);
        });
    });
    
    mockServer();
    2868.6 calls/sec         4535 seconds up
    2891.6 calls/sec         4540 seconds up
    2871.8 calls/sec         4545 seconds up
    

    If we were able to come up with a snippet that triggered the problem without http, using only v8 features, we could run the script in Chrome/Chromium, without Nodejs, and reproduce the issue. We could determine that the problem belongs to v8.

    Is somebody able to come up with a good script for this?

  16. 31 remaining items

  17. trevnorris commented on Nov 9, 2015

    @trevnorris
    Contributor

    Maybe just hard-core enrolling the object to have timers fields manually upon instantiation?

    This is pretty much what we should be doing. On every object instantiation we should be setting all possible future fields to at least null. This is enough to prevent object map mutation.

  18. added
    performanceIssues and PRs related to the performance of Node.js.
    on Feb 16, 2016
  19. jasnell commented on Apr 9, 2016

    @jasnell
    Member

    Ping. any updates on this one?

  20. targos commented on Jan 2, 2017

    @targos
    Member

    I ran the test several times with v5, v6 and v7 and can only reproduce the performance drop in v5.
    I believe the issue was fixed by a V8 update, so I'm going to close this.

  21. MylesBorins commented on Jan 2, 2017

    @MylesBorins
    Contributor

    @targos did you measure v4?

  22. targos commented on Jan 2, 2017

    @targos
    Member

    I didn't but it's probably still affected

  23. MylesBorins commented on Jan 2, 2017

    @MylesBorins
    Contributor

    would we be able to backport the V8 changes?

  24. targos commented on Jan 2, 2017

    @targos
    Member

    We'd have to identify them. I reopen with the v4.x label in case someone wants to do it.

  25. reopened this on Jan 2, 2017
  26. added
    help wantedIssues that need assistance from volunteers or PRs that need help to proceed.
    on Jul 15, 2017
  27. YurySolovyov commented on Nov 15, 2017

    @YurySolovyov

    Still the case on node 8.3+ ?

  28. apapirovski commented on Dec 6, 2017

    @apapirovski
    Contributor

    Given that Node.js 4.x is now in maintenance mode (and has less than 5 months to go in its life) and there has been absolutely zero interest in anyone taking this on, I'm going to go ahead and close it but feel free to reopen if you believe I've made an error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    help wantedIssues that need assistance from volunteers or PRs that need help to proceed.httpIssues and PRs related to the http subsystem.performanceIssues and PRs related to the performance of Node.js.v8 engineIssues and PRs related to the V8 dependency.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions