Skip to content

Create constructor function .prototype property on-demand - #1873

Open
svaarala wants to merge 2 commits into
masterfrom
virtual-prototype-property
Open

svaarala wants to merge 2 commits into
masterfrom
virtual-prototype-property

Conversation

@svaarala

@svaarala svaarala commented Apr 14, 2018

Copy link
Copy Markdown
Owner

(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
svaarala force-pushed the virtual-prototype-property branch from d1cb6ac to 78ea49b Compare April 20, 2018 16:31
@svaarala

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Contributor

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

@svaarala

Copy link
Copy Markdown
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).

@svaarala
svaarala force-pushed the virtual-prototype-property branch from 78ea49b to 909b3ab Compare April 28, 2018 21:01
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