Skip to content

perf: Improve JSON.stringify performance #3751

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

Merged
merged 6 commits into from
Sep 29, 2017

Conversation

obastemur
Copy link
Collaborator

Kraken/json-stringify-* perf ~25% better. Acme Air - LTO gain ~1.5%

PR Details:

  • Improve replacer != function && !HasObjectArray case. (most common use of JSON.stringify)

  • Add JSON.stringify test cases for ObjectArray, toJSON, and replacer function

  • IsNumericPropertyId: fast path for internal properties

  • use requestContext instead of instance->scriptContext

  • use wmemcpy instead of memcpy

// We should look only into object's own properties here. When an object is serialized, only the own properties are considered,
// the prototype chain is not considered. However, the property names can be selected via an array replacer. In this case
// ES5 spec doesn't say the property has to own property or even to be enumerable. So, properties from the prototype, or non enum properties,
// can end up being serialized. Well, that is the ES5 spec word.
//if(!Js::RecyclableObject::FromVar(holder)->GetType()->GetProperty(holder, keyId, &value))

if (VirtualTableInfo<Js::PropertyString>::HasVirtualTable(key))
if (value == nullptr)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make sure I understand this change- value is not null only for the stringification of the initial object, right? That is, from the call from Line 328 ( result = stringifySession.Str(scriptContext->GetLibrary()->GetEmptyString(), propertyId, wrapper, value);)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See Str definition on JSON.h file. value = nullptr default argument. Some of the consumers of Str knows the value hence no need to lookup for it once again

{
Js::Var toJSON = nullptr;
Js::RecyclableObject* object = Js::RecyclableObject::FromVar(*value);
while (typeId != Js::TypeIds_Null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of doing the prototype walk yourself, why not just call Js::JavascriptOperators::GetProperty here? LTO should take care of optimizing this here, and if the concern is GetProperty does more than we want it to (eg. update the property caches), consider adding a new template parameter to GetProperty_Internal to not update the caches. It's probably better to have property getting centralized to avoid edge cases (eg. what happens if TypeFlagMask_SkipsPrototype is set etc)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I consider this more than a loop hanging around. The logic here is defined underGet_ToJSON. It is probably the second hottest place in JSON.stringify and specifics are clear enough. GetPropertyQuery is a centralized interface no?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about TypeFlagMask_SkipsPrototype? What is the expected behavior if object is a proxy? etc.


In reply to: 139480874 [](ancestors = 139480874)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about dealing with TypeFlagMask_SkipsPrototype? What is the expected behavior with Proxies?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what is the problem there? Can you please elaborate a case where it fails?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In JavascriptOperators::GetProperty_Internal, they explicit break out of the loop if object->SkipsPrototype() is true. I don't quite understand when that flag is used but it looks like its in ChakraFull, so the DOMs type system relies on it. I don't have a ready case that fails but my point is that these are things that JavascriptOperators::GetProperty_Internal that takes into account that you'll need to keep in mind if you're going to reimplement it here, which is why centralizing on JavascriptOperators::GetProperty_Internal is not a bad idea.

Re proxies, again, don't have a failing case- it was more a question of what is the expected behavior.

Copy link
Collaborator Author

@obastemur obastemur Sep 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@digitalinfinity this particular Get_ToJSON, we know it's an object. So, no need for redundant is number, is that or this check. Also we skip cache as well as other centralized stuff we must check for GetProperty_Internal.

Lets say we ended up with version of GetProperty_Internal that we don't do any of these given above. It could be hard to decide on any place that whether we should use that version ever or not.

I didn't want to rule caching out straight and implement GetProperty_InternalWithoutCache, because at this moment, I'm not sure where that cache is beneficial and where it's not. If you prefer, we might also add a TODO note here but I'm also not sure what that could be.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That logic makes sense for number etc but the SkipsPrototype case I mentioned is an edge specific object case- should that be handled here or not?

And I'd still love to understand what the expected behavior here should be for proxies (not saying there's an issue, just want to understand if proxy traps could be invoked as a part of this, in which case there could be other issues).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That logic makes sense for number etc but the SkipsPrototype case I mentioned is an edge specific object case- should that be handled here or not?

I had went through TypeFlagMask_SkipsPrototype and couldn't see any reason why we check it for toJSON. That object doesn't have toJSON in it's body. Well, IMHO we should disable that check for ChakraCore for good. No?

About Proxy; I don't see any reason (on code) why this wouldn't work. There is no proxy trap problem. If the check for HasSpecialType returns true and type was proxy, well we end up doing the same call we do here. However, if the type is null? or other specialized type (which there is no such a thing exist), well they are not the problem of Get_toJSON anyways.

Js::Var undefined = scriptContext->GetLibrary()->GetUndefined();
Js::TypeId id = Js::JavascriptOperators::GetTypeId(value);

//check and apply 'toJSON' filter
if (Js::JavascriptOperators::IsJsNativeObject(value) || (Js::JavascriptOperators::IsObject(value)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both these functions call GetTypeId- is it worth creating a new function IsNativeOrUserObject which does the following:
Check if Var is recycler aligned- if not, returns false
Check if the type id is either Native object- if so return true
Check if type id > lastPrimitiveType- return true

Not sure if it would make a huge difference but if this path is hot, it might be noticeable

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@digitalinfinity this is one of the two places we use Js::JavascriptOperators::IsJsNativeObject (the one under this function) hence I don't expect perf diff under especially LTO. However, I really liked the idea and will add the interface.

@@ -1230,6 +1230,8 @@ void ThreadContext::AddBuiltInPropertyRecord(const Js::PropertyRecord *propertyR

BOOL ThreadContext::IsNumericPropertyId(Js::PropertyId propertyId, uint32* value)
{
if (Js::IsInternalPropertyId(propertyId)) return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit : please have brace even for a single statement body.
Also curious why this function's signature has BOOL even though we return bool (true/false).

{
return scriptContext->GetLibrary()->GetUndefined();
Js::Var values[2];
Js::Arguments args(0, values);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: why not pass 2, instead of 0 here?

//-------------------------------------------------------------------------------------------------------


var TEST = function(a, b) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious why not use the unittestframework here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

script load stuff requires working path to be the script's folder. These extra 4 lines make my life more easier.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RL should take care of that for you

Copy link
Collaborator Author

@obastemur obastemur Sep 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Windows indeed. ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't on xplat? I would expect to see a lot more broken.. What changes would we need to make to have a better experience?

UnitTestFramework has a lot of advantages including a uniform way for us to see errors. Generally the only tests that don't use it are ones that need to run at global scope or syntax errors that have different behavior in function scoped evals.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect to see a lot more broken..

No, because runtests.py sets the path for javascript file's location.

What changes would we need to make to have a better experience?

Some cross platform file system / path related improvements to ch but I don't see any reason to push this.

Copy link
Contributor

@tcare tcare Sep 19, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough, but new tests of this format should still be using UnitTestFramework unless they can't.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surely there are some tests, they use multiple stuff from UnitTestFramework. I don't see any reason to push it on these tests...

Does it hurt readability ? or performance of test case? or dependability? or reliability? ... Well, this version of these tests help me to try easier and JSON is and probably will be a hot topic for some time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UnitTestFramework has many of the benefits you've listed.

  • It's silent in RL, but verbose in console.
  • Exceptions explicitly name the type and error message to help regressions where the message changes but the type doesn't
  • It helps us group tests together by functionality
  • It encourages descriptions of what you're expecting and why it's an interesting test
  • Dynamic tests can generate dynamic descriptions
  • Asserts are smart about deep comparisons, NaN, etc and provide a centralized location to update with edge cases as the language evolves
  • It helps when someone unfamiliar with the test needs to quickly grok what went wrong (e.g., when merging code, someone not on our team / familiar with our product)
  • Other JS engines use our unit test framework to run our tests for coverage

E.g.

assert.areEqual(e3, 3, "First identifier in catch param as pattern is matched and initialized correctly");

Language tests from ES6 onward make extensive use of this framework for the above reasons.

Ultimately it's up to you, but we've had a lot of benefits from using the framework. If it's more than a pushd tests/JSON & ch replacerFunction.js I'd be interested to know.

{
//if unsuccessful get propertyId from the string
// if unsuccessful get propertyId from the string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this correct? Previously this was under a if (id == Js::Constants::NoProperty) conditional, and so the comment/behaviour made sense as a fallback, but now it always gets the property record.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MSLaguana there are differences in both cases. In this particular case, I don't know any reason for checking id == Js::Constants::NoProperty.

}
}

for (PropertyIndex i = 0; i < propertyCount; i++)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused by what's going on here. Could you please add some comments?

return v;
}

var until = (!WScript.Platform || WScript.Platform.BUILD_TYPE == 'Debug') ? 12 : 1290;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the point of this line here? why construct a larger case in non-debug builds?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the changes in test configs. So we don't hide this test behind 'Slow' configuration

@@ -223,8 +223,7 @@ namespace Js

// Check numeric propertyId only if objectArray available
uint32 indexVal;
ScriptContext* scriptContext = instance->GetScriptContext();
if (instance->HasObjectArray() && scriptContext->IsNumericPropertyId(propertyId, &indexVal))
if (instance->HasObjectArray() && requestContext->IsNumericPropertyId(propertyId, &indexVal))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we using requestContext here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. Why we shouldn't? It will look through ThreadContext either way. No?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codeflow seemed to lose my comment, apologies if it's repeated..

In this particular case it doesn't matter. The general convention throughout the codebase is to only use requestContext if you have a specific CrossSite marshalling concern.

In a stable class like this we wouldn't expect this to change except in rare circumstances.

As a thought exercise, if these would produce different results I would still expect the instance's scriptContext to be correct since you are asking the object whether this is a numeric id for it's purposes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would still expect the instance's scriptContext to be correct since you are asking the object whether this is a numeric id for it's purposes.

Makes sense. Will roll back the change.

Js::DynamicObject * dynamicObject = ((Js::DynamicObject*)object);
if (ReplacerFunction != replacerType || !dynamicObject->HasObjectArray())
{
int propertyCount = dynamicObject->GetPropertyCount();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

int [](start = 24, length = 3)

GetPropertyCount returns uint32

{
if (id == Js::Constants::NoProperty)
int totalNumPropertyCount = this->GetPropertyCount(object, &enumerator) - propertyCount;
Assert(totalNumPropertyCount > 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assert [](start = 28, length = 6)

failfast ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes/no? Sorry, didn't understand?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assert won't do anything under test/release bits. We can use a failfast to die out if the property count is unexpected.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, I understood as if it happens in release mode, so the loop below goes into infinite loop, would we fail fast?....

The asserted scenario should not happen (if it's happening, something is off and we must see it on debug mode). If anyone expects it might happen??, then it's easy to replace assert with an if check.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the intention of failfast. We might not see this in debug mode because of the lack of coverage compared to in-product coverage, so we shouldn't rely on it to catch this case. This is a common pattern in security issues.

AssertOrFailFast is better to use unless there's a specific way we want to recover here, or the branch has perf implications.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tcare or anyone else, from the implementation, do you know any tangible reason that we should explicitly check this? Otherwise, well, we could failfast so many unlikely stuff though.

@obastemur obastemur force-pushed the fjs branch 2 times, most recently from 93d4dec to e77b514 Compare September 29, 2017 14:02
- Improve `replacer != function && !HasObjectArray` case. (most common
use of JSON.stringify)

- Add JSON.stringify test cases for ObjectArray, toJSON, and replacer
function
@obastemur
Copy link
Collaborator Author

@akroshg anything else?

@akroshg
Copy link
Contributor

akroshg commented Sep 29, 2017

LGTM

@chakrabot chakrabot merged commit f759841 into chakra-core:release/1.7 Sep 29, 2017
chakrabot pushed a commit that referenced this pull request Sep 29, 2017
Merge pull request #3751 from obastemur:fjs

Kraken/json-stringify-* perf ~25% better. Acme Air - LTO gain ~1.5%

PR Details:

- Improve `replacer != function && !HasObjectArray` case. (most common use of JSON.stringify)

- Add JSON.stringify test cases for ObjectArray, toJSON, and replacer function

- IsNumericPropertyId: fast path for internal properties

- use requestContext instead of instance->scriptContext

- use wmemcpy instead of memcpy
chakrabot pushed a commit that referenced this pull request Sep 29, 2017
…rformance

Merge pull request #3751 from obastemur:fjs

Kraken/json-stringify-* perf ~25% better. Acme Air - LTO gain ~1.5%

PR Details:

- Improve `replacer != function && !HasObjectArray` case. (most common use of JSON.stringify)

- Add JSON.stringify test cases for ObjectArray, toJSON, and replacer function

- IsNumericPropertyId: fast path for internal properties

- use requestContext instead of instance->scriptContext

- use wmemcpy instead of memcpy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants