Optimize XML parsing

Using XPath expressions appears to be expensive so that has been
replaced with direct child node access. This seems to have improved
performance by 5%.

A side effect of this change is that XML_IGNORE_NAMESPACE option is
no longer needed as namespaces are ignored by default.
This commit is contained in:
Zaid Abdulla committed 2018-09-08 23:49:34 -07:00
1 parent 037aa9786d
commit ec215ffb82
2 files changed
+240 -122

No files matched your search

-4
View File
@@ -79,7 +79,6 @@ const options = {
XML_FORMAT: { type: "bool", default: false }, XML_FORMAT: { type: "bool", default: false },
XML_NO_DECL: { type: "bool", default: false }, XML_NO_DECL: { type: "bool", default: false },
XML_NO_EMPTY: { type: "bool", default: false }, XML_NO_EMPTY: { type: "bool", default: false },
XML_IGNORE_NAMESPACE: { type: "bool", default: false },
// Should probably never be changed // Should probably never be changed
DEVICE_ONLINE_THRESHOLD: { type: "int", default: 4000 } DEVICE_ONLINE_THRESHOLD: { type: "int", default: 4000 }
@@ -106,9 +105,6 @@ function setConfig(name, value, commandLineArgument) {
) )
setConfig("GPV_BATCH_SIZE", value); setConfig("GPV_BATCH_SIZE", value);
if (name === "IGNORE_XML_NAMESPACES" || name === "ignore-xml-namespaces")
setConfig("XML_IGNORE_NAMESPACE", value);
if (name === "XML_PARSE_IGNORE_ENC" || name === "xml-parse-ignore-enc") if (name === "XML_PARSE_IGNORE_ENC" || name === "xml-parse-ignore-enc")
setConfig("XML_IGNORE_ENC", value); setConfig("XML_IGNORE_ENC", value);
+240 -118
View File
@@ -74,18 +74,6 @@ const LIBXMLJS_SAVE_OPTIONS = {
selfCloseEmpty: !config.get("XML_NO_EMPTY") selfCloseEmpty: !config.get("XML_NO_EMPTY")
}; };
const XML_IGNORE_NAMESPACE = config.get("XML_IGNORE_NAMESPACE");
if (XML_IGNORE_NAMESPACE) {
libxmljs.Element.prototype.__find = libxmljs.Element.prototype.find;
libxmljs.Element.prototype.find = function(xpath, namespaces) {
const p = xpath
.replace(/([^/:]+:)?([^/]+)(\/|$)/g, "$2$3")
.replace(/([a-zA-Z0-9_-]+)([^/]*)(\/|$)/g, '*[local-name()="$1"]$2$3');
return libxmljs.Element.prototype.__find.call(this, p, namespaces);
};
}
let warnings; let warnings;
function parseBool(v) { function parseBool(v) {
@@ -97,13 +85,35 @@ function parseBool(v) {
} }
function event(xml) { function event(xml) {
return xml.find("EventStruct/EventCode").map(e => e.text().trim()); return xml
.childNodes()
.filter(n => n.name() === "EventStruct")
.map(c =>
c
.childNodes()
.find(n => n.name() === "EventCode")
.text()
.trim()
);
} }
function parameterInfoList(xml) { function parameterInfoList(xml) {
return xml.find("ParameterInfoStruct").map(e => { return xml
const param = e.get("Name").text(); .childNodes()
const value = e.get("Writable").text(); .filter(e => e.name() === "ParameterInfoStruct")
.map(e => {
let param, value;
for (const c of e.childNodes()) {
switch (c.name()) {
case "Name":
param = c.text();
break;
case "Writable":
value = c.text();
break;
}
}
let parsed = parseBool(value); let parsed = parseBool(value);
if (parsed === null) { if (parsed === null) {
@@ -119,10 +129,23 @@ function parameterInfoList(xml) {
} }
function parameterValueList(xml) { function parameterValueList(xml) {
return xml.find("ParameterValueStruct").map(e => { return xml
const valueElement = e.get("Value"); .childNodes()
.filter(e => e.name() === "ParameterValueStruct")
.map(e => {
let valueElement, param;
for (const c of e.childNodes()) {
switch (c.name()) {
case "Name":
param = c.text();
break;
case "Value":
valueElement = c;
break;
}
}
const valueType = valueElement.attr("type").value(); const valueType = valueElement.attr("type").value();
const param = e.get("Name").text();
const value = valueElement.text(); const value = valueElement.text();
let parsed = value; let parsed = value;
if (valueType === "xsd:boolean") { if (valueType === "xsd:boolean") {
@@ -167,7 +190,9 @@ function GetParameterNames(xml, methodRequest) {
function GetParameterNamesResponse(xml) { function GetParameterNamesResponse(xml) {
return { return {
name: "GetParameterNamesResponse", name: "GetParameterNamesResponse",
parameterList: parameterInfoList(xml.get("ParameterList")) parameterList: parameterInfoList(
xml.childNodes().find(n => n.name() === "ParameterList")
)
}; };
} }
@@ -183,7 +208,9 @@ function GetParameterValues(xml, methodRequest) {
function GetParameterValuesResponse(xml) { function GetParameterValuesResponse(xml) {
return { return {
name: "GetParameterValuesResponse", name: "GetParameterValuesResponse",
parameterList: parameterValueList(xml.get("ParameterList")) parameterList: parameterValueList(
xml.childNodes().find(n => n.name() === "ParameterList")
)
}; };
} }
@@ -220,7 +247,12 @@ function SetParameterValues(xml, methodRequest) {
function SetParameterValuesResponse(xml) { function SetParameterValuesResponse(xml) {
return { return {
name: "SetParameterValuesResponse", name: "SetParameterValuesResponse",
status: parseInt(xml.get("Status").text()) status: parseInt(
xml
.childNodes()
.find(n => n.name() === "Status")
.text()
)
}; };
} }
@@ -231,10 +263,22 @@ function AddObject(xml, methodRequest) {
} }
function AddObjectResponse(xml) { function AddObjectResponse(xml) {
let instanceNumber, status;
for (const c of xml.childNodes()) {
switch (c.name()) {
case "InstanceNumber":
instanceNumber = parseInt(c.text());
break;
case "Status":
status = parseInt(c.text());
break;
}
}
return { return {
name: "AddObjectResponse", name: "AddObjectResponse",
instanceNumber: parseInt(xml.get("InstanceNumber").text()), instanceNumber: instanceNumber,
status: parseInt(xml.get("Status").text()) status: status
}; };
} }
@@ -247,7 +291,12 @@ function DeleteObject(xml, methodRequest) {
function DeleteObjectResponse(xml) { function DeleteObjectResponse(xml) {
return { return {
name: "DeleteObjectResponse", name: "DeleteObjectResponse",
status: parseInt(xml.get("Status").text()) status: parseInt(
xml
.childNodes()
.find(n => n.name() === "Status")
.text()
)
}; };
} }
@@ -287,31 +336,64 @@ function Download(xml, methodRequest) {
} }
function DownloadResponse(xml) { function DownloadResponse(xml) {
const res = { let status, startTime, completeTime;
name: "DownloadResponse", for (const c of xml.childNodes()) {
status: parseInt(xml.get("Status").text()) switch (c.name()) {
}; case "Status":
status = parseInt(c.text());
if (res.status === 0) { break;
res.startTime = Date.parse(xml.get("StartTime").text()); case "StartTime":
res.completeTime = Date.parse(xml.get("CompleteTime").text()); startTime = Date.parse(c.text());
break;
case "CompleteTime":
completeTime = Date.parse(c.text());
break;
}
} }
return res; return {
name: "DownloadResponse",
status: status,
startTime: startTime,
completeTime: completeTime
};
} }
function Inform(xml) { function Inform(xml) {
let retryCount, evnt, parameterList;
const deviceId = {
Manufacturer: null,
OUI: null,
ProductClass: null,
SerialNumber: null
};
for (const c of xml.childNodes()) {
switch (c.name()) {
case "ParameterList":
parameterList = parameterValueList(c);
break;
case "DeviceId":
for (const cc of c.childNodes()) {
const n = cc.name();
if (n in deviceId) deviceId[n] = cc.text();
}
break;
case "Event":
evnt = event(c);
break;
case "RetryCount":
retryCount = parseInt(c.text());
break;
}
}
return { return {
name: "Inform", name: "Inform",
parameterList: parameterValueList(xml.get("ParameterList")), parameterList: parameterList,
deviceId: { deviceId: deviceId,
Manufacturer: xml.get("DeviceId/Manufacturer").text(), event: evnt,
OUI: xml.get("DeviceId/OUI").text(), retryCount: retryCount
ProductClass: xml.get("DeviceId/ProductClass").text(),
SerialNumber: xml.get("DeviceId/SerialNumber").text()
},
event: event(xml.get("Event")),
retryCount: parseInt(xml.get("RetryCount").text())
}; };
} }
@@ -336,12 +418,30 @@ function GetRPCMethodsResponse(xml, methodResponse) {
} }
function TransferComplete(xml) { function TransferComplete(xml) {
let commandKey, _faultStruct, startTime, completeTime;
for (const c of xml.childNodes()) {
switch (c.name()) {
case "CommandKey":
commandKey = c.text();
break;
case "FaultStruct":
_faultStruct = faultStruct(c);
break;
case "StartTime":
startTime = Date.parse(c.text());
break;
case "CompleteTime":
completeTime = Date.parse(c.text());
break;
}
}
return { return {
name: "TransferComplete", name: "TransferComplete",
commandKey: xml.get("CommandKey").text(), commandKey: commandKey,
faultStruct: faultStruct(xml.get("FaultStruct")), faultStruct: _faultStruct,
startTime: Date.parse(xml.get("StartTime").text()), startTime: startTime,
completeTime: Date.parse(xml.get("CompleteTime").text()) completeTime: completeTime
}; };
} }
@@ -352,7 +452,10 @@ function TransferCompleteResponse(xml) {
function RequestDownload(xml) { function RequestDownload(xml) {
return { return {
name: "RequestDownload", name: "RequestDownload",
fileType: xml.get("FileType").text() fileType: xml
.childNodes()
.find(n => n.name() === "FileType")
.text()
}; };
} }
@@ -361,57 +464,104 @@ function RequestDownloadResponse(xml) {
} }
function faultStruct(xml) { function faultStruct(xml) {
const f = { let faultCode, faultString, setParameterValuesFault, pn, fc, fs;
faultCode: xml.get("FaultCode").text(), for (const c of xml.childNodes()) {
faultString: xml.get("FaultString").text() switch (c.name()) {
}; case "FaultCode":
faultCode = c.text();
for (const e of xml.find("SetParameterValuesFault")) { break;
if (!f.setParameterValuesFault) f.setParameterValuesFault = []; case "FaultString":
faultString = c.text();
f.setParameterValuesFault.push({ break;
parameterName: e.get("ParameterName").text(), case "SetParameterValuesFault":
faultCode: e.get("FaultCode").text(), setParameterValuesFault = setParameterValuesFault || [];
faultString: e.get("FaultString").text() for (const cc of c.childNodes()) {
switch (cc.name()) {
case "ParameterName":
pn = cc.text();
break;
case "FaultCode":
fc = cc.text();
break;
case "FaultString":
fs = cc.text();
break;
}
}
setParameterValuesFault.push({
parameterName: pn,
faultCode: fc,
faultString: fs
}); });
} }
return f;
} }
function fault(xml, cwmpVersion) { return { faultCode, faultString, setParameterValuesFault };
return { }
faultCode: xml.get("faultcode").text(),
faultString: xml.get("faultstring").text(), function fault(xml) {
detail: faultStruct(xml.get("detail/cwmp:Fault", NAMESPACES[cwmpVersion])) let faultCode, faultString, detail;
}; for (const c of xml.childNodes) {
switch (c.name()) {
case "faultcode":
faultCode = c.text();
break;
case "faultstring":
faultString = c.text();
break;
case "detail":
detail = faultStruct(c.childNodes().find(n => n.name() === "Fault"));
break;
}
}
return { faultCode, faultString, detail };
} }
function request(data, cwmpVersion, warn) { function request(data, cwmpVersion, warn) {
warnings = warn; warnings = warn;
const rpc = { const rpc = {
cwmpVersion: cwmpVersion id: null,
cwmpVersion: cwmpVersion,
sessionTimeout: null,
cpeRequest: null,
cpeFault: null
}; };
if (!data.length) return rpc; if (!data.length) return rpc;
const xml = libxmljs.parseXml(data, LIBXMLJS_PARSE_OPTIONS); const xml = libxmljs.parseXml(data, LIBXMLJS_PARSE_OPTIONS);
let methodElement; let headerElement, bodyElement;
if (rpc.cwmpVersion) { for (const c of xml.childNodes()) {
methodElement = xml.get( switch (c.name()) {
"/soap-env:Envelope/soap-env:Body/cwmp:*", case "Header":
NAMESPACES[rpc.cwmpVersion] headerElement = c;
); break;
} else { case "Body":
// cwmpVersion not passed, thus it's an inform request bodyElement = c;
methodElement = xml.get( break;
"/soap-env:Envelope/soap-env:Body/*", }
NAMESPACES["1.0"] }
);
let timeoutElement; for (const c of headerElement.childNodes()) {
switch (c.name()) {
case "ID":
rpc.id = c.text();
break;
case "sessionTimeout":
rpc.sessionTimeout = parseInt(c.text());
break;
}
}
for (const methodElement of bodyElement.childNodes()) {
const methodName = methodElement.name();
if (methodName === "text") continue;
if (!rpc.cwmpVersion && methodName !== "Fault") {
switch (methodElement.namespace().href()) { switch (methodElement.namespace().href()) {
case "urn:dslforum-org:cwmp-1-0": case "urn:dslforum-org:cwmp-1-0":
rpc.cwmpVersion = "1.0"; rpc.cwmpVersion = "1.0";
@@ -420,16 +570,9 @@ function request(data, cwmpVersion, warn) {
rpc.cwmpVersion = "1.1"; rpc.cwmpVersion = "1.1";
break; break;
case "urn:dslforum-org:cwmp-1-2": case "urn:dslforum-org:cwmp-1-2":
timeoutElement = xml.get( if (rpc.sessionTimeout) rpc.cwmpVersion = "1.3";
"/soap-env:Envelope/soap-env:Header/cwmp:sessionTimeout", else rpc.cwmpVersion = "1.2";
NAMESPACES["1.2"]
);
if (timeoutElement) {
rpc.sessionTimeout = parseInt(timeoutElement.text());
rpc.cwmpVersion = "1.3";
} else {
rpc.cwmpVersion = "1.2";
}
break; break;
case "urn:dslforum-org:cwmp-1-3": case "urn:dslforum-org:cwmp-1-3":
rpc.cwmpVersion = "1.4"; rpc.cwmpVersion = "1.4";
@@ -437,27 +580,9 @@ function request(data, cwmpVersion, warn) {
default: default:
throw new Error("Unrecognized CWMP version"); throw new Error("Unrecognized CWMP version");
} }
if (!rpc.sessionTimeout) {
timeoutElement = xml.get(
"/soap-env:Envelope/soap-env:Header/cwmp:sessionTimeout",
NAMESPACES[rpc.cwmpVersion]
);
if (timeoutElement) rpc.sessionTimeout = parseInt(timeoutElement.text());
}
} }
const idElement = xml.get( switch (methodName) {
"/soap-env:Envelope/soap-env:Header/cwmp:ID",
NAMESPACES[rpc.cwmpVersion]
);
if (idElement) rpc.id = idElement.text();
if (
methodElement &&
!(XML_IGNORE_NAMESPACE && methodElement.name() === "Fault")
) {
switch (methodElement.name()) {
case "Inform": case "Inform":
rpc.cpeRequest = Inform(methodElement); rpc.cpeRequest = Inform(methodElement);
break; break;
@@ -494,15 +619,12 @@ function request(data, cwmpVersion, warn) {
case "DownloadResponse": case "DownloadResponse":
rpc.cpeResponse = DownloadResponse(methodElement); rpc.cpeResponse = DownloadResponse(methodElement);
break; break;
case "Fault":
rpc.cpeFault = fault(methodElement, rpc.cwmpVersion);
break;
default: default:
throw new Error(`8000 Method not supported ${methodElement.name()}`); throw new Error(`8000 Method not supported ${methodName}`);
} }
} else {
const faultElement = xml.get(
"/soap-env:Envelope/soap-env:Body/soap-env:Fault",
NAMESPACES[rpc.cwmpVersion]
);
rpc.cpeFault = fault(faultElement, rpc.cwmpVersion);
} }
return rpc; return rpc;