angular / angular.js

AngularJS - HTML enhanced for web apps!
https://angularjs.org
MIT License
58.85k stars 27.52k forks source link

angular.copy Very poor performance (large objects) #11099

Open Tomino2112 opened 9 years ago

Tomino2112 commented 9 years ago

Our application loads resources from API and renders them in a table. API request takes ~200ms, rendering table ~140ms but when you actually looking at the page it takes about 6s to display it.

I have drilled through the code and realized this was caused by using angular.copy . I need to keep track of the old data so I copied the whole object to backup object via angular.copy() function. The copying itself took over 2s (~2500ms).

I have searched for better ways, to copy objects and stumbled upon this code:

cloneObject: function(o){
            const gdcc = "__getDeepCircularCopy__";
            if (o !== Object(o)) {
                return o; // primitive value
            }

            var set = gdcc in o,
                cache = o[gdcc],
                result;
            if (set && typeof cache == "function") {
                return cache();
            }
            // else
            o[gdcc] = function() { return result; }; // overwrite
            if (o instanceof Array) {
                result = [];
                for (var i=0; i<o.length; i++) {
                    result[i] = this.cloneObject(o[i]);
                }
            } else {
                result = {};
                for (var prop in o)
                    if (prop != gdcc)
                        result[prop] = this.cloneObject(o[prop]);
                    else if (set)
                        result[prop] = this.cloneObject(cache);
            }
            if (set) {
                o[gdcc] = cache; // reset
            } else {
                delete o[gdcc]; // unset again
            }
            return result;
        },

This is my implementation of this: http://stackoverflow.com/a/10729242/820942

Replacing all angular.copy() in my code with this hugely improved the webapp overall. The same example above takes ~41ms instead of ~2s.

I should mention that the data I am mentioning is pretty large (renders into 10 000 cells table)

After very brief look on the angular.copy code I am aware of that there are some added angular goodies ($$hashkey) but still the difference in performance is just too big.

pkozlowski-opensource commented 9 years ago

@Tomino2112 did you try to put this code and see if it passes all the existing tests? As you've noticed, angular.copy is more of an internal utility and we kind of regret exposing it as a public API... but now the damage is done... So if your version works better for your particular case - by all means use it!

It is kind of hard to say more without an isolated reproduction scenario which we could profile to actually see what is going on. We are missing this as well as other essential info (ex.: browser and its version used for tests).

In short: please provide an isolated reproduce scenario, otherwise it is not very actionable.

vitaly-t commented 9 years ago

@Tomino2112, it may be interesting to see if your approach can be somehow used within angular for speed optimization, but in the meantime, it is never a good approach trying to display 10,000 cells on a page at once through a single request. It is a very poor approach from the architecture point of view.

@pkozlowski-opensource, I find his research credible enough to suggest that Angular's implementation for deep copy is to be reviewed, based on implementation by @Tomino2112. The performance difference seems to be huge.

Tomino2112 commented 9 years ago

Hi guys, @pkozlowski-opensource When I have a minute I will create sample fiddle. To answer the other questions - no I didn't try to run tests, I am not replacing the angular.copy function, just have it on the side in my "utils" library. The browsers tested were FF, IE and Chrome. Obviously chrome had best results and not so big gap in performance, FF was the one with most obvious performance difference.

@VitalyTomilov Yeah its not great to render that many cells, actually it is edge scenario, usually its around hundereds of cells - still not great though, but thats what you get when you try to render sheets of data online. I have some ideas for rendering on my list, just not enough time to play around with them at the moment.

I will prepare some code example so you cna see it in action (i mean the angular.copy)

jbedard commented 9 years ago

I think the main thing slowing angular.copy is the use of a stack (arrays) to track the already copied objects. The snippet proposed above modifies the objects being copied which might be faster but will have a lot of issues if you want a general purpose copy method (it won't work at all on immutable objects, what if it crashes in the middle leaving a bunch of objects with modifications, etc.).

11215 helps a bit mainly by reducing the size of the stack (today sub object/arrays are pushed twice!).

However I have found that replacing the stack with an ES6 Map improves performance significantly. I'm not sure if it's worth shimming Map just for the copy method though. Would Map be useful anywhere else?

gkalpak commented 9 years ago

I wonder if a shimmed Map would have the same performance benefits as the "native" Map. Isn't the private HashMap sort of a shim for Map ? Would it help ?

jbedard commented 9 years ago

HashMap modifies the key object so that won't work for this case. A shimmed Map has to use the 2-array/indexOf method so it wouldn't have any performance benefits.

gkalpak commented 9 years ago

That's what I thought :)

vitaly-t commented 9 years ago

Maybe it is a bit sinister having just one Copy version for both simple data and immutable objects, so if one wants a simpler, high-performance copy without any advanced provisions, it is not available.

If this is the case, the best approach is to provide two separate Copy implementations either through 2 separate functions or through an extra parameter for Copy.

emmagamma commented 9 years ago

Using something like this:

destination = JSON.parse(JSON.stringify(source));

I copied 10,000 key/value pairs of an object in ~5 milliseconds on my machine. I know this leaves out a lot of the functionality found in angular.copy, but perhaps there is some good way of merging this approach in?

On Mon, Mar 2, 2015 at 10:30 AM, Vitaly Tomilov notifications@github.com wrote:

Maybe it is a bit sinister having just one Copy version for both simple data and immutable objects, so if one wants a simpler, high-performance copy without any advanced provisions, it is not available.

If this is the case, the best approach is to provide two separate Copy implementations either through 2 separate functions or through an extra parameter for Copy.

— Reply to this email directly or view it on GitHub https://github.com/angular/angular.js/issues/11099#issuecomment-76771332 .

jbedard commented 9 years ago

JSON.parse(JSON.stringify(source)) is often a good method if you know it can handle your data (no circles, no functions, no objects other then string/number/plain-object/null). A standard copy method could never make those assumptions though, and detecting such a thing would require traversing the entire source object which would kill any performance gain. Generally utility methods like this don't belong in angular anyway...

emmagamma commented 9 years ago

That's a good point, I didn't realize that code doesn't copy over functions or object methods :/

realityking commented 9 years ago

@gkalpak @jbedard How about using Map in newer browsers and falling back to a shim in old browsers (by my count Internet Explorer 9-10, Safari/iOS 6-7 and Android Browser)

jbedard commented 9 years ago

@realityking that's what I was proposing, I'm just not sure if creating that shim is worth one use case...

gkalpak commented 9 years ago

Considering how copy() is used in deep watchers and sometimes in AST.prototype.primary(), it might be worth the trouble.

Two things I would take into account:

  1. How much is the code overhead (in terms of loc).
  2. How much is the performance benefit in "newer" browsers.
jbedard commented 9 years ago

When I tried it out (on top of #11215) I think it was ~50 LOC and made all modern browsers ~5x faster with my random over complicated test data. I could open a PR if people think this is worth it?

dpogue commented 9 years ago

:+1: for using Map where supported

lgalfaso commented 9 years ago

@jbedard what about with not so complex data?

jbedard commented 9 years ago

As the complexity goes down (less objects) using Map has less benefits and is slower with simple data. For example I found copy({a:"b", c: {d: "e"}}) was about 30% slower using Map.

However in those simple data cases it is normally isTypedArray(destination) consuming the most time, not new Map or Map.get/set. So that 30% can easily be made up for with other changes such as https://github.com/jbedard/angular.js/commit/8a2503bdb5786dd31ebf503903406d7cbb686d5a which makes that simple copy 50% faster...

emmagamma commented 9 years ago

I don't have time for it right now, but if no one else does it by tomorrow I can run some tests with varying sizes of each data-type being passed into copy, both using Map and not using Map. Then we can pinpoint the number of bytes in the median(s) of the valley(s) in the performance curve and implement a check where the performance benefits of map outweigh the cons (as @jbedard pointed out) and then copy can decide whether or not to use it.

What if we did something like the code in the post below?

And figure out where that threshold would be exactly? Or would it even be worth it to check? Perhaps the performance tradeoffs are negligible? I don't really know because I haven't run the tests or looked at the data, but maybe jbedard@8a2503b already knows where that would be?

emmagamma commented 9 years ago

sorry, that code highlighter did not work out lol...

    var byteThreshold = /* Some number of bytes where objects
            of this size or greater are benefited
            by using map, and objects less than
            this byte size take a performance hit
            due to isTypedArray(destination).
            */;

    if (sizeOf(source) > byteThreshold) {
        // use map
    } else {
        // don't use map
    }
jbedard commented 9 years ago

It is not worth having 2 implementations...

Ledzz commented 9 years ago

Had an issue: when I try to copy a large object, it copies in ~300ms, but sometimes it can copy for 18-20s. jbedard's method JSON.parse(JSON.stringify(source)) worked good for me.

jbedard commented 8 years ago

33c67ce785cf8be7f0c294b3942ca4a337c5759d has improved this a bit more often making copy 1.5-3x faster.

Tomino2112 commented 8 years ago

I have tried MANY solutions from all over the internet, but in the end I end-up writing specific function for copying my object. After all the tests and benchmark, the result is, if you have large arrays/objects, its better to write your own copy function specifically for that object with for loops because they are insanely fast compare to whatever other solutions. I still use angular's clone function, but only for small objects

frfancha commented 8 years ago

@jbedard Sorry for the naive question, but 33c67ce has a commit date June 9 and doesn't seem included in Angular 1.4.7, could you help me to understand version numbers? Thanks

jbedard commented 8 years ago

It was just put into master a few days ago so it will be in the next 1.5 release.

frfancha commented 8 years ago

@jbedard Ok thanks for the info. We will try 1.5 then, I'm very curious about the performance difference. I hope there won't be too many breaking changes in 1.5

stanleyxu2005 commented 8 years ago

Maybe there is some performance regression with 1.4.8. After upgrading alone with npm update, I noticed one occurrence with angular.copy() slow down to 2 seconds. I simply replace the function with _.clone() and it shorten to 200ms.

From project practice, I think angular's built-in array/object util functions are not full cover the usage. I have to use lodash (or underscore) anyway. So I think angular should move these helper functions eg. isNumber(), forEach(), copy(), isEqual() into angular-util package, so that the core package size can be even smaller, and we might be able to use lodash instead.

gkalpak commented 8 years ago

@stanleyxu2005, (as stated before) Angular never intended to provide a replacement for general purpose utility libraries. Its helper functions (copy(), forEach(), isXyz() etc) where implemented for internal use (so they are focused on supporting/performing well only for the intermally needed usecases - they might work well for other usecases, but it's not their purpose).

At that point, it seemed like a good idea to expose the utility functions to the developers (since they were implemented anyway), so they didn't have to import a whole different library for simple usecases. This actually turned out to be a bad idea - among other things, it's much more difficult to make breaking changes to them to better support the evolving needs of the framework.

There's not much we can do now, because removing them would break too many apps. The functions are also used internally, so removing them from the core is not an option (that's their purpose in the first place). So, they'll continue to exist, but users should keep in mind that they might not be the best option for their usecase.

coli commented 8 years ago

I just replaced angular.copy with JSON.parse(JSON.stringify()) on a 18meg tree structure. The speed difference was huge...

bcherny commented 8 years ago

One more case study....

let as = [...] // array of ~4000 objects

console.time('angular')
angular.copy(as)
console.timeEnd('angular')
// => 71048.443ms

console.time('lodash')
_.cloneDeep(as)
console.timeEnd('lodash')
// => 521.026ms

console.time('json')
JSON.parse(JSON.stringify(as))
console.timeEnd('json')
// => 92.422ms

Please deprecate the public angular.copy API, and add a note on the docs page!

e-cloud commented 8 years ago

The team should be aware that source of angular.copy has too much nested functions, that's part of why it's so slow.

gkalpak commented 8 years ago

@e-cloud, is there any evidence that inlining the functions would improve performance?

e-cloud commented 8 years ago

Nope, what i mean is every time you call angular.copy will create nested functions, which cost more memory and time. Of source, in small scale it doesn't matter.

stanleyxu2005 commented 8 years ago

Regarding @coli and @bcherny 's experiment, if the code change is tiny and the performance is remarkable, I'd vote +1 to apply this change. This is a gratis improvement proposal for the angular team, seriously.

angular.copy = function(obj) {
  return JSON.parse(JSON.stringify(obj));
}

@gkalpak Improve performance of frequently use functions is a common sense. I feel a bit confused of using angular functions. If those functions are intended to be used internally, better rename foo to internal.foo or _foo. If we don't have much knowledge of angular's history, we never know these functions are not for public usage. In order not to fall into any performance trap, my current practice is only use lodash as the only one util-lib in my JS projects.

jbedard commented 8 years ago

@stanleyxu2005 JSON.parse(JSON.stringify(obj)) is very limited and does not work for the internal uses of angular.copy.

gkalpak commented 8 years ago

every time you call angular.copy will create nested functions, which cost more memory and time.

@e-cloud, the question is whether the cost is neglible compared to other operations involved or not. If there is evidence that we can noticeably improve the performance of angular.copy (without sacrifizing functionality), we would be more than happy to do it.

jbedard commented 8 years ago

@e-cloud I tested your idea and it really makes no difference. It's only 3 closures per copy call. If it were 3 per object (and sub object) it would probably make a difference but that's not the case.

e-cloud commented 8 years ago

@jbedard , Do you try it in scale?

Of source, in small scale it doesn't matter.

I've mention before, if only few calls.

stripathix commented 6 years ago

In my use case, I did not saw any a difference in angular.copy vs JSON.parse(JSON.stringfy(dataobject));

Rather .map(dataobject, .clone) was super fast.

angular.copy(dataobject) 2500ms

JSON.parse(JSON.stringfy(dataobject)) 2500ms

.map(dataobject, .clone) 115ms