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

Create constructor function .prototype property on-demand #1873

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

svaarala
Copy link
Owner

@svaarala svaarala commented Apr 14, 2018

(At this point just some quick testing, not sure if this approach will be merged.)

All constructable functions have a .prototype property. It points, by default, to an object whose .constructor property points back to the function, creating a reference loop. This reference loop prevents most function objects, especially inline callbacks, from being refcount freed. There are several approaches to allow refcount collection of such function objects.

One of them is to postpone creation of the .prototype property until it is actually observed somehow and we must commit to the property's existence. Relevant situations include:

  • Reading the property: value must be created.
  • Writing the property: since .prototype is writable but not configurable, the new value can be written without creating the object prior to the (over)write.
  • Existence check: prototype in MyConstructor must return true; does not require creation of the object.
  • Deleting the property: .prototype is non-configurable so delete must fail; does not require creation of the object.
  • Object.getOwnPropertyNames() or duk_enum() with "include non-enumerable": requires listing the "prototype" key but doesn't require creation of the object (except for duk_enum() with both keys and values requested). Enum order has a few issues.
  • Object.defineProperty(): some cases might be handled without triggering creation, but safest would be to create the property if Object.defineProperty(MyConstructor, "prototype", { ... }) is called.

This approach allows refcount collection of a few basic cases, in particular anonymous functions which are not captured by an outer scope, e.g.:

setTimeout(function () { ... }, 1000);

Unfortunately if the function is given a name, this currently creates another kind of reference loop (scope object containing the function name binding points to the function, and the function points to the scope) so this doesn't get refcount collected:

setTimeout(function mycb() { ... }, 1000);

This case could be allowed to work by reworking the function name binding scope handling. But other cases will still remain.

Tasks:

  • Property get
  • Property set
  • Property has
  • Property delete
  • Object.getOwnPropertyNames() and other enumeration cases
  • Object.defineProperty()
  • Treatment of Duktape/C functions - allow them to have an on-demand .prototype now? (This would be the behavior by default without an explicit internal flag which doesn't yet exist.)
  • Testcase coverage
  • Short internal document
  • Releases entry

Future work:

  • Fix .prototype enumeration order so it is stable (as if created when instance was created)

@svaarala
Copy link
Owner Author

As a rough example of the memory impact of this optimization, loading TypeScript 2.5 typescriptServices.js into the global object on x64 takes:

  • 16.78MB peak in master
  • 16.25MB peak in this pull at present

So the memory savings are around 3% in this case - not huge, but still significant considering there is next to no functional downside (but of course some complexity downsides).

For a targeted memory test exercising function instance creation maximally (tests/memory/test-function-expression-1.js):

  • 37.06MB peak in master
  • 24.58MB in this pull

which comes to around 33%, representing the maximum possible benefit from this change alone.

@svaarala
Copy link
Owner Author

For the Promise polyfill, which is not very compact memory-wise:

  • tests/memory/test-promise-instance-resolve-reject.js:
    • 1.37kB per instance+resolve+reject, master
    • 1.07kB per instance+resolve+reject, this pull
  • tests/memory/test-promise-then-chain.js:
    • 1.93MB total, master
    • 1.59MB total, this pull

So this change improves the Promise polyfill memory behavior quite a bit, which is quite natural because Promises (even behind the user-provided callbacks) are function-heavy.

@fatcerberus
Copy link
Contributor

So besides reducing GC pressure, this also reduces memory pressure. Nice. 😁

@svaarala
Copy link
Owner Author

It would be really nice to have some structured, extensible way of virtualizing properties like this, so that the approach could be applied to a few other properties too. But it seems hard to generalize: the knowledge that the property needs to be virtualized cannot come from the prototype (it would be user visible), and in some cases one doesn't want to actually instantiate the property but still behave as if it existed (e.g. enumeration).

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.

2 participants