-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
fix: patching namespaced attributes (#1049) #1061
fix: patching namespaced attributes (#1049) #1061
Conversation
benchmarking shows that the approach used here is slow... benchmark script// $ node ~/software/performance.js
const common = require('./node/benchmark/common.js');
const bench = common.createBenchmark(main, {
n: [10000000],
type: ['charCodeAt','rev1_indexOf_split', 'rev2_indexOf_slice', 'rev3_indexOf_slice'],
string: ['svg:test', 'xmlns:test', 'nonamespace']
});
function main(conf) {
const string = conf.string
const type = conf.type
const legacyNamespaceQualifierMap = {
svg: "http://www.w3.org/2000/svg",
html: "http://www.w3.org/1999/xhtml",
xml: "http://www.w3.org/XML/1998/namespace",
xlink: "http://www.w3.org/1999/xlink",
xmlns: "http://www.w3.org/2000/xmlns/",
};
if (type === 'charCodeAt') {
const colonChar = 58;
bench.start();
for (let i = conf.n, res; i--;) {
if (string.charCodeAt(3) === colonChar)
res = 'xml'
else if (string.charCodeAt(5) === colonChar)
res = 'xlink'
else
res = 'default'
}
bench.end(conf.n);
}
if (type === 'rev3_indexOf_slice') {
bench.start();
for (let i = conf.n, index, res; i--;) {
index = string.indexOf(':', 2)
res = index > -1
? legacyNamespaceQualifierMap[string.slice(0, index).toLowerCase()] || 'default'
: 'default'
}
bench.end(conf.n);
}
if (type === 'rev2_indexOf_slice') {
bench.start();
for (let i = conf.n, index, res; i--;) {
index = string.indexOf(':')
res = index > -1
? legacyNamespaceQualifierMap[string.slice(0, index).toLowerCase()] || 'default'
: 'default'
}
bench.end(conf.n);
}
if (type === 'rev1_indexOf_split') {
bench.start();
for (let i = conf.n, res; i--;) {
if (string.indexOf(':') !== -1) {
const namespaceQualifier = string.split(":")[0].toLowerCase();
const namespaceUri = legacyNamespaceQualifierMap[namespaceQualifier];
if (namespaceUri) {
res = namespaceUri
} else {
res = 'default'
}
} else {
res = 'default'
}
}
bench.end(conf.n);
}
} The results of the original and the results of version currently used in the PR $ node performance.js type=charCodeAt
string="svg:test" type="charCodeAt" n=10000000: 484,433,058.54513997
string="xmlns:test" type="charCodeAt" n=10000000: 525,356,129.72660935
string="nonamespace" type="charCodeAt" n=10000000: 619,385,464.3238618
$ node performance.js type=rev1_indexOf_split
string="svg:test" type="rev1_indexOf_split" n=10000000: 3,843,110.7793953097
string="xmlns:test" type="rev1_indexOf_split" n=10000000: 3,788,721.676558171
string="nonamespace" type="rev1_indexOf_split" n=10000000: 150,363,388.46361864 It is orders of magnitude slower. I tried optimising it this way index = string.indexOf(':', 2)
res = index > -1
? legacyNamespaceQualifierMap[string.slice(0, index).toLowerCase()] || 'default'
: 'default' The results are better but still not great $ node performance.js type=charCodeAt
string="svg:test" type="charCodeAt" n=10000000: 484,433,058.54513997
string="xmlns:test" type="charCodeAt" n=10000000: 525,356,129.72660935
string="nonamespace" type="charCodeAt" n=10000000: 619,385,464.3238618
node performance.js type=rev3_indexOf_slice
string="svg:test" type="rev3_indexOf_slice" n=10000000: 26,143,789.96112609
string="xmlns:test" type="rev3_indexOf_slice" n=10000000: 22,633,673.126783233
string="nonamespace" type="rev3_indexOf_slice" n=10000000: 155,416,322.56197715
$ node performance.js type=rev1_indexOf_split
string="svg:test" type="rev1_indexOf_split" n=10000000: 3,843,110.7793953097
string="xmlns:test" type="rev1_indexOf_split" n=10000000: 3,788,721.676558171
string="nonamespace" type="rev1_indexOf_split" n=10000000: 150,363,388.46361864 Maybe we could try using an approach that continues using charCodeAt and is less dynamic? cc @paldepind (feel free to ignore this, pinging you because this is generally interesting) |
if (string.charCodeAt(3) === colonChar)
res = string.startsWith('svg') ? 'svg' : 'xml'
else if (string.charCodeAt(5) === colonChar)
res = string.startsWith('xmlns') ? 'xmlns' : 'xlink'
else if (string.charCodeAt(4) === colonChar)
res = 'html'
else
res = 'default' This one is better. The original one only handles two namepaces so it will always have fewer operations but I have few more ideas to try here... $ node performance.js type=charCodeAt
string="svg:test" type="charCodeAt" n=10000000: 484,433,058.54513997
string="xmlns:test" type="charCodeAt" n=10000000: 525,356,129.72660935
string="nonamespace" type="charCodeAt" n=10000000: 619,385,464.3238618
$ node performance.js type=charCodeAt_expanded
string="svg:test" type="charCodeAt_expanded" n=10000000: 186,871,461.34361067
string="xmlns:test" type="charCodeAt_expanded" n=10000000: 66,944,352.734400325
string="nonamespace" type="charCodeAt_expanded" n=10000000: 236,245,983.28673068 |
if (string.charCodeAt(3) === colonChar)
res = string.charCodeAt(0) === sChar ? 'svg' : 'xml'
else if (string.charCodeAt(5) === colonChar)
res = string.charCodeAt(1) === mChar ? 'xmlns' : 'xlink'
else if (string.charCodeAt(4) === colonChar)
res = 'html'
else
res = 'default' $ node performance.js type=charCodeAt
string="svg:test" type="charCodeAt" n=10000000: 484,433,058.54513997
string="xmlns:test" type="charCodeAt" n=10000000: 525,356,129.72660935
string="nonamespace" type="charCodeAt" n=10000000: 619,385,464.3238618
$ node performance.js type=charCodeAt_expanded2
string="svg:test" type="charCodeAt_expanded2" n=10000000: 338,774,997.0636677
string="xmlns:test" type="charCodeAt_expanded2" n=10000000: 247,869,671.7129792
string="nonamespace" type="charCodeAt_expanded2" n=10000000: 232,190,851.40646937 |
$ node performance.js type=charCodeAt
string="svg:test" type="charCodeAt" n=10000000: 484,433,058.54513997
string="xmlns:test" type="charCodeAt" n=10000000: 525,356,129.72660935
string="nonamespace" type="charCodeAt" n=10000000: 619,385,464.3238618
$ node performance.js type=charCodeAt_expanded3_nohtml
string="svg:test" type="charCodeAt_expanded3_nohtml" n=10000000: 331,880,823.72023934
string="xmlns:test" type="charCodeAt_expanded3_nohtml" n=10000000: 253,863,085.3422704
string="nonamespace" type="charCodeAt_expanded3_nohtml" n=10000000: 295,297,778.575213
$ node performance.js type=charCodeAt_expanded2
string="svg:test" type="charCodeAt_expanded2" n=10000000: 338,774,997.0636677
string="xmlns:test" type="charCodeAt_expanded2" n=10000000: 247,869,671.7129792
string="nonamespace" type="charCodeAt_expanded2" n=10000000: 232,190,851.40646937 |
(string.charCodeAt(3) === colonChar)
? (string.charCodeAt(0) === sChar ? 'svg' : 'xml')
: (string.charCodeAt(5) === colonChar)
&& (string.charCodeAt(1) === mChar ? 'xmlns' : 'xlink')
|| 'default' results vary on each run but this one does seem to return results that closest to the original $ node performance.js type=charCodeAt
string="svg:test" type="charCodeAt" n=10000000: 484,433,058.54513997
string="xmlns:test" type="charCodeAt" n=10000000: 525,356,129.72660935
string="nonamespace" type="charCodeAt" n=10000000: 619,385,464.3238618
$ node performance.js type=charCodeAt_expanded4
string="svg:test" type="charCodeAt_expanded4" n=10000000: 352,228,849.56457293
string="xmlns:test" type="charCodeAt_expanded4" n=10000000: 251,956,848.05611262
string="nonamespace" type="charCodeAt_expanded4" n=10000000: 303,769,087.86223173 |
let's amend the commit with this latest version |
the most recent benchmark script and results are attached at the top of this PR discussion |
To make sure I understand correctly: The attributes module currently handles attributes that begin with "xml:" and "xlink:". As mentioned in #1049 attributes that begin with "xmlns:" are not handled correctly. According to the linked SO answer, there are also attributes that begin with |
Please see the comment I've left. If we decide that "svg:" and "html:" attributes exist and are important then that bug should be fixed (and probably tests should be added as well for those prefixes). Otherwise, we can just remove the handling of "svg:" and "html:" and the check that starts by seeing whether the first character is "x" will still be valid. |
The stack overflow link maps the following namespaces and uris: link, const NAMESPACES = {
svg: 'http://www.w3.org/2000/svg',
html: 'http://www.w3.org/1999/xhtml',
xml: 'http://www.w3.org/XML/1998/namespace',
xlink: 'http://www.w3.org/1999/xlink',
xmlns: 'http://www.w3.org/2000/xmlns/' // sic for the final slash...
} namespaces currently supported in the master branch are: I'll look for information about |
Google search results return only few references to the <html xmlns="http://www.w3.org/1999/xhtml"> The elm.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space","preserve");
elm.text(`some test with spaces and
newlines`); @paldepind should the svg url be supported? Do you also think it is okay to skip support for the html namespace url? |
let's remove the svg condition and maybe add it later through a separate PR, if that is wanted |
This patch adds support for the svg namespace, here for reference in case it is added later diff --git a/src/modules/attributes.ts b/src/modules/attributes.ts
index c82f918..a481a15 100755
--- a/src/modules/attributes.ts
+++ b/src/modules/attributes.ts
@@ -6,6 +6,7 @@ export type Attrs = Record<string, string | number | boolean>;
const xlinkNS = "http://www.w3.org/1999/xlink";
const xmlnsNS = "http://www.w3.org/2000/xmlns/";
const xmlNS = "http://www.w3.org/XML/1998/namespace";
+const svgNS = "http://www.w3.org/2000/svg";
const colonChar = 58;
const xChar = 120;
const mChar = 109;
@@ -31,8 +32,10 @@ function updateAttrs(oldVnode: VNode, vnode: VNode): void {
} else if (cur === false) {
elm.removeAttribute(key);
} else if (key.charCodeAt(3) === colonChar) {
- // Assume 'xml' namespace
- elm.setAttributeNS(xmlNS, key, cur as any);
+ // Assume 'xml' or 'svg' namespace
+ key.charCodeAt(1) === mChar
+ ? elm.setAttributeNS(xmlNS, key, cur as any)
+ : elm.setAttributeNS(svgNS, key, cur as any);
} else if (key.charCodeAt(5) === colonChar) {
// Assume 'xmlns' or 'xlink' namespace
key.charCodeAt(1) === mChar
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for looking into this in more detail. From this, I think its reasonable to only extend the support to "xmlns:" attributes.
The PR looks good except for the one comment I've left.
the original if/else arrangement is restored so that the diff is as small and specific as possible |
Superb! Thanks a lot for the PR and the surrounding research. |
closes #1049
Node's runtime appeared to be unfarily optimising part the benchmark test for the original charCodeAt implementation. The final benchmark was updated to more closely resemble the actual behaviour of snabbdom. The final benchmark test and results are attached.
benchmark
Feel free to give advice or criticism