Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance of toMap() && parseAttributes() #5854

Closed
Reinmar opened this issue Nov 28, 2019 · 6 comments · Fixed by ckeditor/ckeditor5-utils#323 or ckeditor/ckeditor5-engine#1822
Assignees
Labels
type:improvement This issue reports a possible enhancement of an existing feature. type:performance This issue reports a performance issue or a possible performance improvement.

Comments

@Reinmar
Copy link
Member

Reinmar commented Nov 28, 2019

EDIT: I extended this ticket to cover not only the toMap() implementation in model/Node, but also parseAttributes() in view/Element which has the same issue (IIRC).


This function is called extremely frequently and becomes the most time-consuming thing in #4504 and setting editor data with complex content.

99% of time inside it is spent on isPlainObject(). My guess is that it's so slow because it uses something like Object.prototype.toString.apply( obj ) == '...' to check what we've got inside and perhaps cover some other cases that we don't really need in toMap().

According to the docs, toMap() should work with objects or iterables. I'd actually check with what kind of objects it's really executed because I also expect to see maps and arrays (so, iterables too), but we should make sure we're not breaking it for them.

One thing I noticed is that toMap()'s tests don't actually cover the iterables case 🤦‍♂ . I was able to write an implementation that passes all tests but breaks in the engine.

So, this is perhaps more or less what we need:

export default function toMap( data ) {
	if ( !data ) {
		return new Map();
	}

        // toMap() tests pass without this but break in the engine.
	if ( typeof data[ Symbol.iterator ] === 'function' ) {
		return new Map( data );
	}

        // The two following conditions are not necessary since both arrays and maps are iterable.
        // I just want to show that toMap()'s tests are insufficient if these 2 conditions make them green.
	if ( data instanceof Array ) {
		return new Map( data );
	}

	if ( data instanceof Map ) {
		return new Map( data );
	}

	return objectToMap( data );
}

This allowed me to get from 8s to 6s when loading complex data and from 1843ms to 693ms when removing attributes from content taken from https://github.com/ckeditor/ckeditor5-remove-format/issues/7.

@Reinmar Reinmar added the type:improvement This issue reports a possible enhancement of an existing feature. label Nov 28, 2019
@Reinmar Reinmar added this to the iteration 29 milestone Nov 28, 2019
@Reinmar
Copy link
Member Author

Reinmar commented Nov 28, 2019

cc @scofalik @jodator

@mlewand
Copy link
Contributor

mlewand commented Nov 28, 2019

@Reinmar I faced the very same thing when testing further tweaks for #4504. Also you can reuse our isIterable util function.

Another relatively expensive but easy to improve (performance-wise) bit was prefix matching using a regexp (which happens in many different view and model types). You can check prefix by simply using String.startsWith(), which is simpler and faster.

@jodator
Copy link
Contributor

jodator commented Nov 28, 2019

I'm trying to understand why/where toMap() is used and in some places it looks like Object.entries():

https://github.com/ckeditor/ckeditor5-engine/blob/0a7597faf59b84afe249e6167dface72f6af43aa/src/model/writer.js#L387:

/**
 * Sets values of attributes on a {@link module:engine/model/item~Item model item}
 * or on a {@link module:engine/model/range~Range range}.
 *
 *		writer.setAttributes( {
 *			bold: true,
 *			italic: true
 *		}, range );
 *
 * @param {Object} attributes Attributes keys and values.
 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
 * Model item or range on which the attributes will be set.
 */
setAttributes( attributes, itemOrRange ) {
	for ( const [ key, val ] of toMap( attributes ) ) {
		this.setAttribute( key, val, itemOrRange );
	}
}

After changing this to Object.entries() the only tests in the engine that failed were tests that check if that method works with new Map() - but this is not what the jsdoc says.

This makes me think if we don't overuse toMap() in places where native methods would do it's job.

The toMap() for Map() is useless, the toMap() for Object is Object.entries().

But now for writer.setAttributes( { foo: 'bar' } ) we will create new Map() then we will use it to iterate over its entries. I don't know how (if at all) this would impact the performance and/or memory but I get a feeling that we should at least inspect the usage of this and similar API.

@ma2ciek
Copy link
Contributor

ma2ciek commented Nov 28, 2019

I'm trying to understand why/where toMap() is used and in some places it looks like Object.entries():

I guess that if we want to simply iterate over object props then the for-in loop will always be the fastest way 😅
https://hackernoon.com/5-techniques-to-iterate-over-javascript-object-entries-and-their-performance-6602dcb708a8

@Reinmar
Copy link
Member Author

Reinmar commented Nov 28, 2019

I pushed to ckeditor5 a branch on which I was testing the performance: #3767 (comment).

@Reinmar
Copy link
Member Author

Reinmar commented Nov 28, 2019

One additional note – when doing any performance-oriented work, do always start from a real issue.

There are thousands of ways how we could improve our code as most of it wasn't written with perf in mind and certainly wasn't tested for that. But from my experience, real issues point you to completely different places than intuition.

Also, it's very often that there's no way to improve the performance of a certain code by optimizing that code. Instead, we should be looking at how to not execute it at all or do that far less frequently.

This is all quite well visible in the flame chart combined with some additional simple tests. So, really, start there.

@Reinmar Reinmar changed the title Improve performance of toMap() Improve performance of toMap() && parseAttributes() Jan 17, 2020
@Reinmar Reinmar added the type:performance This issue reports a performance issue or a possible performance improvement. label Jan 17, 2020
@Reinmar Reinmar assigned mlewand and unassigned Reinmar Jan 17, 2020
jodator added a commit to ckeditor/ckeditor5-utils that referenced this issue Feb 6, 2020
Other: Improved `toMap` method performance. This results in improved editor data processing speed. Closes ckeditor/ckeditor5#5854.
jodator added a commit to ckeditor/ckeditor5-engine that referenced this issue Feb 6, 2020
Other: Improved `parseAttributes` function performance. This results in improved editor data processing speed. Closes ckeditor/ckeditor5#5854.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type:improvement This issue reports a possible enhancement of an existing feature. type:performance This issue reports a performance issue or a possible performance improvement.
Projects
None yet
4 participants