Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Make SVGs and CSS dynamically recolourable #77

Merged
merged 15 commits into from
Jan 7, 2016
Merged

Conversation

ara4n
Copy link
Member

@ara4n ara4n commented Jan 5, 2016

I'm not proud of this.

Turns any colour-themed SVGs from img tags into object tags to make them dynamically editable from JS, and implements a simple /tint command to let you clobber the CSS and appropriate SVG attributes with a custom colour scheme.

Requires facebook/react#5781 to work, as otherwise react doesn't understand onLoad on object tags.

@ara4n
Copy link
Member Author

ara4n commented Jan 5, 2016

Could particularly do with review on Tinter.js - is it safe to store state in plain old vars like this, or should it be all bundled up into an exported object? Or is that just hygiene?

@ara4n ara4n changed the title Matthew/dynamic svg Make SVG and CSS dynamically recolourable Jan 5, 2016
@ara4n ara4n changed the title Make SVG and CSS dynamically recolourable Make SVGs and CSS dynamically recolourable Jan 5, 2016
@richvdh richvdh assigned richvdh and unassigned dbkr Jan 5, 2016
@@ -42,6 +43,11 @@ var commands = {
return reject("Usage: /nick <display_name>");
},

tint: function(room_id, args) {
Copy link
Member

Choose a reason for hiding this comment

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

(most of) the other commands have a comment saying what they do. wouldn't hurt here.

Copy link
Member Author

Choose a reason for hiding this comment

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

done

@kegsay
Copy link
Member

kegsay commented Jan 5, 2016

is it safe to store state in plain old vars like this, or should it be all bundled up into an exported object? Or is that just hygiene?

It should be safe but in our case it isn't. It's usually safe because require() calls cache the first time they are called then return the same instance of module.exports again (along with closures containing those vars). It isn't safe for us because we tend to use the same file in multiple projects and we want that to refer to the same variables cross-project. This isn't how CommonJS modules work. You only get singleton-like behaviour in files in the same project. Cross-project references to the same file returns multiple instances (one for each project).

So what does this actually mean? It means that as it stands, if vector-web were to require("matrix-react-sdk/lib/Tinter") it would get an entirely new set of vars to the Tinter being required in matrix-react-sdk. You can refer to the same instance by doing a hack:

module.exports = {
  ...
};

if (!global.mxTinter) { // this attaches to 'window' (!)
    global.mxTinter = module.exports;
}
module.exports = global.mxTinter;

You would need to move all your var declarations to be attached to module.exports. This is all 'orrible - and is filed as element-hq/element-web#464

for (var k = 0; k < cssAttrs.length; k++) {
var attr = cssAttrs[k];
for (var l = 0; l < keyRgb.length; l++) {
if (rule.style && rule.style[attr] === keyRgb[l]) {
Copy link
Member

Choose a reason for hiding this comment

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

can you add if (!rule.style) continue; at line 88, and simplify this?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes, although it's very rare for a CSS rule to not have a style (it happens with weird rules like animation data which don't actually define styles), so it doesn't buy us much. have done it anyway.

@kegsay kegsay assigned ara4n and unassigned richvdh Jan 5, 2016
@ara4n
Copy link
Member Author

ara4n commented Jan 6, 2016

I've addressed all the feedback other than:

  • the big point about putting Tinter's state into modules.exports rather than plain old vars (given we're only ever going to be importing Tinter from matrix-react-sdk, I'm not sure it's worth the effort right now)
  • the big point about not leaking bits of SVG DOM into Tinter's state, but instead making Tinter functional and apply to the DOM components themselves. Agreed this would be an improvement, but not sure it's worth the effort right now versus fixing other stuff.

Assuming we're agreed, I'll shift these to be maintenance bugs to address when we're out of crunch.

@ara4n
Copy link
Member Author

ara4n commented Jan 6, 2016

@kegsay PTAL

@ara4n ara4n assigned kegsay and unassigned ara4n Jan 6, 2016
@ara4n
Copy link
Member Author

ara4n commented Jan 6, 2016

It's also worth noting that I was deliberately caching the required fixups for SVGs in the singleton Tinter rather than recalculating them every time onLoad (given fixups are still valid from previous loads). Given SVGs could theoretically be quite big, wasting time re-walking them everytime they reload to generate fixups would chew CPU and time for no reason at all.

...oh, except I ended up backing out that caching. ignore me. yup, it leaks, as the XXX comment says.

@kegsay
Copy link
Member

kegsay commented Jan 6, 2016

putting Tinter's state into modules.exports rather than plain old vars (given we're only ever going to be importing Tinter from matrix-react-sdk, I'm not sure it's worth the effort right now)

WFM. You've put a big warning up so I'm happy as is.

not leaking bits of SVG DOM into Tinter's state, but instead making Tinter functional and apply to the DOM components themselves. Agreed this would be an improvement, but not sure it's worth the effort right now versus fixing other stuff.

So I had a look at how badly it leaks. My test was as follows:

  • Refresh page and swap to a room.
  • Take Heap Snapshot.
  • Change tint (if applicable)
  • Swap to different rooms ~10 times.
  • Take Heap Snapshot.

On develop - The heap snapshot delta was <1MB (21.6MB to 22.4MB).
On matthew/dynamic-svg - The heap snapshot delta was 34MB (24MB to 58MB).

This equates to about 3MB per room swap. This is too much for a long-lived web app. I don't mind fixing the leak for you if you're stretched for time, but I would be unhappy leaving it as-is.

@ara4n
Copy link
Member Author

ara4n commented Jan 6, 2016

i'm very surprised the leak is this bad. will fix it. suggestions on how to refer to the set of tintablesvgs when the user runs /tint?

On 6 Jan 2016, at 12:01, Kegsay notifications@github.com wrote:

putting Tinter's state into modules.exports rather than plain old vars (given we're only ever going to be importing Tinter from matrix-react-sdk, I'm not sure it's worth the effort right now)

WFM. You've put a big warning up so I'm happy as is.

not leaking bits of SVG DOM into Tinter's state, but instead making Tinter functional and apply to the DOM components themselves. Agreed this would be an improvement, but not sure it's worth the effort right now versus fixing other stuff.

So I had a look at how badly it leaks. My test was as follows:

Refresh page and swap to a room.
Take Heap Snapshot.
Change tint (if applicable)
Swap to different rooms ~10 times.
Take Heap Snapshot.
On develop - The heap snapshot delta was <1MB (21.6MB to 22.4MB).
On matthew/dynamic-svg - The heap snapshot delta was 34MB (24MB to 58MB).

This equates to about 3MB per room swap. This is too much for a long-lived web app. I don't mind fixing the leak for you if you're stretched for time, but I would be unhappy leaving it as-is.


Reply to this email directly or view it on GitHub.

@kegsay
Copy link
Member

kegsay commented Jan 6, 2016

You just need to invert the control flow a bit. When people run /tint you should emit an event on the dispatcher (e.g. tint_change). Then you just need to listen for this in your TintableSvg and then change the tint. You never maintain the full set of tintable svgs.

@kegsay
Copy link
Member

kegsay commented Jan 6, 2016

So the full flow would look like:

// SlashCommands.js
tint: function(newColour) {
  Tinter.setTint(newColour);
  dispatcher.dispatch({
    action: "tint_change"
  });
}
// ============

// TintableSvg.js
componentWillMount: function() {
  dis.register(this._onAction);
},

componentDidMount: function() {
  Tinter.applyTint(ReactDOM.findDOMNode(this));
},

_onAction: function(payload) {
  if (payload.action !== "tint_change") { return; }
  Tinter.applyTint(ReactDOM.findDOMNode(this));
}

// ============

// Tinter.js
setTint: function(newColour) {
  currentColour = newColour;
},

applyTint: function(domNode) {
  // apply `currentColour` to `domNode`
}

And make sure that Tinter never persists the passed domNode. It should only persist the current colour.

@ara4n
Copy link
Member Author

ara4n commented Jan 6, 2016

oh, sorry, yes. was being thick.

…storing SVG DOM fragments in Tinter to avoid leaking them
@ara4n
Copy link
Member Author

ara4n commented Jan 7, 2016

0f52c0a fixes the leak by making TintableSvgs responsible for updating their own tints, and responding to tint_update dispatch events. However I've kept the existing functions in Tinter.js rather than refactoring as per Kegan's full suggestion, as it retains symmetry with the half which is still needed for fixing up CSS (rather than SVGs), and i don't want to burn yet more time on this.

@kegsay: PTAL


onAction: function(payload) {
if (payload.action !== 'tint_update') return;
Tinter.applySvgFixups(this.fixups);
Copy link
Member

Choose a reason for hiding this comment

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

If this is called prior to onLoad then this will NPE on Tinter.js:192.

Copy link
Member Author

Choose a reason for hiding this comment

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

why? this.fixups is initialised to [], not null?

Copy link
Member

Choose a reason for hiding this comment

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

Oh, right you are. However, the way you're doing it isn't what you intend I think. You're defining fixups on :27 as a prototype property, which is shared across all instances. You can't define instance properties like that, you need to use:

componentWillMount: function() {
  this.fixups = [];
}

Without this, all TintableSvg elements would share the same fixups array(!)

Copy link
Member Author

Choose a reason for hiding this comment

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

oops, fixed, thanks

@kegsay
Copy link
Member

kegsay commented Jan 7, 2016

LGTM - Did more heap dumps and now we no longer leak :)

ara4n added a commit that referenced this pull request Jan 7, 2016
Make SVGs and CSS dynamically recolourable
@ara4n ara4n merged commit 8170288 into develop Jan 7, 2016
dtygel pushed a commit to coletivoEITA/matrix-react-sdk that referenced this pull request May 15, 2017
This pull request was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants