Skip to content

Instantly share code, notes, and snippets.

@dead-claudia
Last active August 2, 2019 18:40
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dead-claudia/a2aa7012b3c38d889da5fccb1cabf54d to your computer and use it in GitHub Desktop.
Save dead-claudia/a2aa7012b3c38d889da5fccb1cabf54d to your computer and use it in GitHub Desktop.
Mithril optimization ideas

Mithril Optimization Ideas

This is more of a list of ideas, so I can get some feedback on each one, but it's also in rough chronological order, since I have to wait for some things before I can do others. It deals a lot with optimization within the rendering, so be prepared to see some more arcane details about it and optimization in general.

I created this as a gist as to not litter the issue tracker with a long-term proposal likely to get lost in the flow of things.

  1. This monstrosity should be split up, so engines can actually optimize the thing better.

  2. This issue (#1653) should be fixed.

  • Compared to the rest, this is low hanging fruit.
  1. The original tree should be kept for the entire lifetime of the component, and new trees patching the original one.
  • GCs collect objects faster when they have shorter lifetimes, and often reuse them when in hot loops.
  • Currently, the new tree replaces the old one, with the DOM node and state transferred to the new node. This is wasteful.
  1. The backing representation should be generated independently from the initial vnode.
  • This ensures no data dependency between the API and internal representation.
  • This will enable most of the following optimizations.
  • The IR is still exposed via the vnode argument.
  1. The vnode objects can be made polymorphic.
  • The types will remain few enough and small enough to still allow optimization.
  • Only 4 vnode types are actually used in practice, and these are sufficient to cover the entire API:
    • Parent DOM/component nodes (type/tag, attrs, array children)
    • Child DOM/component node (type/tag, attrs, string text)
    • text/raw node (type, string children)
    • fragment (type, array children).
  • Smaller objects with short lifetimes are much easier for the engine to automatically pool.
  • Small-scale polymorphism (2-6 or so types) is still optimizable. V8 was a laggard a few months ago in enabling polymorphic inlining for hot functions, but every browser (including IE) has optimized object polymorphism for several years now.
  1. Pool the internal vnodes and DOM nodes aggressively to avoid larger object allocation.
  • This can be regularly trimmed via a requestIdleCallback, falling back to using setTimeout + time since last allocation or pooling.
  • This will drastically improve the memory profile, since vnode objects are big.
  • DOM nodes are super costly to create compared to regular objects, because it requires a full round trip to C++ for a complex node setup.
  • DOM nodes should be pooled per-type to speed up access.
  • For practical reasons, custom elements are unpooled by default, since they aren't purely host objects. (These can be detected by having a dash and not being SVG's <color-profile /> or MathML's <annotation-xml />.)
  • Obviously, when a component explicitly disables pooling, the relevant DOM node should not be re-inserted into the pool.
  • Note that for memory reasons, the pool should be a static array explicitly grown in increments of a power of 2.

Effects

Of course, this is a wide-reachine proposal, so there are pros and cons to it.

  1. This will really increase speed and reduce memory constraints by a significant margin, through fewer and smaller allocations.
  • The overhead of limited polymorphism (i.e. non-megamorphic calls) is much lower than the common larger object allocations for engines.
  • Object monomorphism only becomes important in super hot loops, and small amounts of object monomorphism doesn't make nearly the difference primitive polymorphism does. Objects are non-callable pointers under the hood, and smaller objects will likely be cache hits after initial access, so it won't have the performance breakdown you'd get out of mixing integers and doubles.
  1. This will increase the size of the compressed bundle by probably a few kilobytes to account for the pools and separate IR.
  2. Note that the last two steps (4 and 5) will be breaking changes, small enough to only merit a minor increment (most varying fields are already documented nullable).
  3. For testing reasons, we will need to create and expose utilities for creating and managing component instances directly.
  • Add the following utility methods to mithril/render/vnode:
    • Vnode.virtual(component/tag, attrs, children) (handles .text vs .children to hide the abstraction)
    • Vnode.text(text)
    • Vnode.fragment(frag)
    • inst = Vnode.init(vnode)
    • update(inst, attrs, children)
    • etc.
  • The existing Vnode export should be deprecated for future removal, and fixed to do conditional dispatch short-term.
  • mithril-node-render and mithril-objectify will need patched accordingly.
  1. The advice against memoizing vnode trees no longer applies
  2. This particular detail regarding vnode creation will need removed.
@lhorie
Copy link

lhorie commented Mar 2, 2017

My two cents:

1 - That "monstrosity" is the search space reduction algorithm for list diffs. Last I checked, none of the other libraries split this down, precisely for perf reasons. Can you provide some more concrete suggestions? I don't think "break into smaller functions so JIT can re-inline them into effectively the same thing" is a task worth pursuing.

2 - I've kept my hads off of that because afaik @pygy is working on that

3 - not sure I understand this one. You're saying we should instead copy everything from the new tree back to the old one? That makes no sense. For example, say you have data = [1,2,3] and in the view data.map(n => m("li", n)). Then you do data.shift(). This causes O(n) worth of copying when diffing, which is horrible.

4 - again, I don't understand the proposal. Are you saying we should create twice as many objects to enforce encapsulation of properties like .events?

5 - I don't understand this one either. Why is 4+ vnode types better than one? The vast majority of vnode properties apply to all vnode types, so conceptually dividing them doesn't really make any potential polymorphic types that much smaller, and it just ups the smartness requirement for the JIT engine.

6 - You keep saying vnodes are big, but they really aren't. They have 11 properties. By comparison, Inferno nodes have 8. FWIW, Dominic mentioned a while back that they went back to straight vdom, no vnode pooling. DOM pooling already happens with recycling. You're welcome to try to optimize it more but I already spent a fair amount of time fidgeting w/ that trying to lower vdom bench results (where recycling improvements can be tested most easily) and the numbers are very close to inferno already. At this point, I'm more interested in making sure it's correct than optimizing it

Anyways, I don't mean to just come here and shoot the whole thing down, so I'm going to add my own brain dump of perf-related things:

1 - First and foremost, I have done ZERO IrHydra work on Mithril. That's the lowest hanging fruit performance-wise.
2 - Mithril doesn't implement the kivi/ivi algorithm for reducing the number of DOM shuffles in large mostly non-sorted sorts. IMHO, this affects very few real use cases, but it is nonetheless a known optimization that has not been implemented.
3 - Mithril currently completely chokes on the asmjs validator. Making it not choke would likely uncover some addressable issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment