Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

Memory usage statistics #2755

Closed
dead-claudia opened this issue Feb 26, 2022 · 3 comments
Closed

Memory usage statistics #2755

dead-claudia opened this issue Feb 26, 2022 · 3 comments
Assignees
Labels
Area: Core For anything dealing with Mithril core itself Type: Meta/Feedback For high-level discussion around the project and/or community itself

Comments

@dead-claudia
Copy link
Member

dead-claudia commented Feb 26, 2022

I did some research into our memory allocation patterns within Chrome.

  • Each vnode requires 52 bytes total allocated.
    • 10 slots for 10 properties, 4 bytes per slot
    • 12 bytes of general overhead per object
  • An empty attributes object allocates 28 bytes. Adding a single property (say, {a: 1}) reduces that allocation to only 16 bytes, indicating it's allocating 4 slots by default for empty objects and only the minimum slots needed for non-empty objects.
  • A simple <div class="foo">foo</div> inserted into the body as its first child requires 140 bytes for the <div> and 96 bytes for the inner text node, resulting in 236 bytes total.
  • A simple cached m("div.foo", "foo") allocates 80 bytes
    • 52 bytes for the vnode
    • 28 bytes for the (unnecessarily) cloned vnode.attrs assembled from the selector cache entry, created from an empty object with 1 property added
  • A simple cached m("div.container", m("div.foo", "foo")) allocates 252 bytes
    • 80 each for 2 vnodes, same as above
    • 92 bytes for a single-element array: 16 bytes for the wrapper object and 12 bytes of overhead + 16 slots reserved (as it's starting from empty) for the underlying elements array
  • A simple cached m("div.container", m("div.foo", "foo"), m("div.bar", "bar")) allocates 332 bytes
    • 80 each for 3 vnodes, same as above
    • 92 bytes for a single-element array: 16 bytes for the wrapper object and 12 bytes of overhead + 16 slots reserved (as it's starting from empty) for the underlying elements array
  • A full m.render(root, m("div.foo", "foo")) against an empty, pre-initialized detached DOM node allocates:
    • 236 bytes for the generated DOM, same as above:
      • 140 bytes for the <div>
      • 96 bytes for the detached text node
    • 108 bytes of object overhead
      • 80 bytes for the vnode, same as above
      • 28 bytes for the empty vnode.state object allocated for each vnode
  • A full m.render(root, m("div.container", m("div.foo", "foo"))) against an empty, pre-initialized detached DOM node allocates:
    • 376 bytes for the generated DOM, same as above:
      • 140 bytes each for 2 <div>s
      • 96 bytes for the detached text node
    • 308 bytes of object overhead
      • 252 bytes can be explained away per above
      • 28 bytes each for empty vnode.state objects allocated for 2 vnodes
  • A full m.render(root, m("div.container", m("div.foo", "foo"), m("div.bar", "bar")) against an empty, pre-initialized detached DOM node allocates:
    • 612 bytes for the generated DOM, same as above:
      • 140 bytes each for 3 <div>s
      • 96 bytes each for 2 detached text nodes
    • 416 bytes of object overhead
      • 332 bytes can be explained away per above
      • 28 bytes each for empty vnode.state objects allocated for 3 vnodes

I see a few easy ways to cut down on memory allocation dramatically:

  1. Fix execSelector to return the underlying frozen object directly rather than cloning it when no user-provided attributes exist
    • Saves 28 bytes per DOM vnode reusing a cached selector
  2. (Breaking) Ditch vnode.state on DOM elements - it's rarely used anyways
    • Saves 28 bytes per DOM vnode
  3. Optimize for single-element children (common case) to only allocate 1 slot for it
    • Saves 60 bytes per non-empty DOM vnode created via m("tag", child) (a relatively common case)

In total, I predict that would reduce total memory consumption of front-end view code by roughly 5-20% along with a 20-30% reduction in object generation depending on the nature of the trees (I'm accounting for style rendering in that overestimate):

  • A full m.render(root, m("div.foo", "foo")) against an empty, pre-initialized detached DOM node allocates:
    • 236 bytes for the generated DOM, same as above:
      • 140 bytes for the <div>
      • 96 bytes for the detached text node
    • 52 bytes of object overhead for the vnode
    • Savings: 16% memory (344 B → 288 B) + 40% objects (5 → 3)
  • A full m.render(root, m("div.container", m("div.foo", "foo"))) against an empty, pre-initialized detached DOM node allocates:
    • 376 bytes for the generated DOM, same as above:
      • 280 bytes for 2 <div>s
      • 96 bytes for the detached text node
    • 136 bytes of object overhead:
      • 52 bytes each for 2 vnodes
      • 32 bytes for a single-element array: 16 bytes for the wrapper object and 12 bytes of overhead + 1 slot for the underlying elements array
    • Savings: 25% memory (684 B → 512 B) + 22% objects (9 → 7)
  • A full m.render(root, m("div.container", m("div.foo", "foo"), m("div.bar", "bar")) against an empty, pre-initialized detached DOM node allocates:
    • 612 bytes for the generated DOM, same as above:
      • 140 bytes each for 3 <div>s
      • 96 bytes each for 2 detached text nodes
    • 332 bytes of object overhead:
      • 80 each for 3 vnodes, same as above
      • 92 bytes for a single-element array: 16 bytes for the wrapper object and 12 bytes of overhead + 16 slots reserved (as it's starting from empty) for the underlying elements array
    • Savings: 8% memory (1028 B → 944 B) + 23% objects (13 → 10)

Notes:

  • Those with lots of single-child nodes would see a much larger improvement in memory than those with relatively few.
  • Those with lots of empty nodes will see a much larger reduction in objects allocated (and thus reduced GC pressure and more nursery usage) than those with relatively few.

This would be relatively straightforward to implement:

  • The change in how we allocate attributes would just add an extra condition, and might even speed that path up slightly.
  • Removing vnode.state from DOM vnodes would be as simple as just moving that to the create component flow.
  • Fixing Vnode.normalizeChildren to special-case single-element arrays - hyperscriptVnode already returns them, but Vnode.normalizeChildren doesn't check for that.
    • That branch would also be very simple: if (input.length === 1) return [Vnode.normalize(input[0])]. It wouldn't need to verify keys as it's a degenerate case, so a mild perf boost may also come out of it.
@tbreuss
Copy link
Contributor

tbreuss commented Feb 27, 2022

Very interesting and accurate findings!

@StephanHoyer
Copy link
Member

Great findings

@dead-claudia
Copy link
Member Author

Also forgot to mention the part on reusing cached attributes will also moderately reduce object count and mildly memory usage on update.

@dead-claudia dead-claudia added the Area: Core For anything dealing with Mithril core itself label Sep 2, 2024
@MithrilJS MithrilJS locked and limited conversation to collaborators Sep 2, 2024
@dead-claudia dead-claudia converted this issue into discussion #2951 Sep 2, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
Area: Core For anything dealing with Mithril core itself Type: Meta/Feedback For high-level discussion around the project and/or community itself
Projects
None yet
Development

No branches or pull requests

3 participants