java - El rendimiento de XPath.evaluate se ralentiza(absurdamente) a través de múltiples llamadas
android performance (5)
Estoy tratando de usar el paquete javax.xml.xpath para ejecutar expresiones XPath en un documento con múltiples espacios de nombres, y estoy teniendo problemas de rendimiento tontos.
Mi documento de prueba se extrae de un ejemplo de producción real. Se trata de 600k de xml. El documento es un feed Atom bastante complejo.
Me doy cuenta de que lo que estoy haciendo con XPath podría hacerse sin él. Sin embargo, la misma implementación en otras plataformas infinitamente inferiores funciona de manera absurdamente mejor. En este momento, reconstruir mi sistema para no usar XPath está más allá del alcance de lo que puedo hacer en el tiempo que tengo.
Mi código de prueba es algo como esto:
void testXPathPerformance()
{
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(loadTestDocument());
XPathFactory xpf = XPathFactory.newInstance();
XPath xp = xpf.newXPath();
NamespaceContext names = loadTestNamespaces();
//there are 12 namespaces in names. In this example code, I''m using
//''samplens'' instead of the actual namespaces that my application uses
//for simplicity. In my real code, the queries are different text, but
//precisely the same complexity.
xp.setNamespaceContext(names);
NodeList nodes = (NodeList) xp.evaluate("/atom:feed/atom:entry",
doc.getDocumentElement(), XPathConstants.NODESET);
for(int i=0;i<nodes.getLength();i++)
{
printTimestamp(1);
xp.evaluate("atom:id/text()", nodes.item(i));
printTimestamp(2);
xp.evaluate("samplens:fieldA/text()", nodes.item(i));
printTimestamp(3);
xp.evaluate("atom:author/atom:uri/text()", nodes.item(i));
printTimestamp(4);
xp.evaluate("samplens:fieldA/samplens:fieldB/&at;attrC", nodes.item(i));
printTimestamp(5);
//etc. My real example has 10 of these xp.evaluate lines
}
}
Cuando corro en un Nexus One, (no en el depurador, pero con un USB conectado), la primera vez que pasa el ciclo, cada xp.evaluate toma entre 10 ms y 20 ms. A la decimoquinta vez a través del ciclo, cada xp.evaluate toma entre 200 ms y 300 ms. Al final del ciclo (hay 150 elementos en los nodes
), se requieren aproximadamente 500ms-600ms para cada xp.evaluate.
He intentado usar xp.compile (). Todas las compilaciones toman <5ms. He hecho xp.reset () (no hace diferencia). He hecho un nuevo objeto XPath para cada evaluación (agrega unos 4 ms).
El uso de la memoria no parece descontrolarse durante la ejecución.
Estoy ejecutando esto en un solo hilo en un caso de prueba JUnit que no crea una actividad ni nada.
Estoy realmente desconcertado.
¿Alguien tiene alguna idea de qué más probar?
¡Gracias!
actualizar
Si ejecuto el ciclo for hacia atrás ( for(int i=nodes.getLength()-1;i>=0;i--)
), entonces los primeros nodos toman 500ms-600ms, y los últimos van rápido 10ms -20 ms. Por lo tanto, parece que no tiene nada que ver con el número de llamadas, sino que las expresiones cuyo contexto está cerca del final del documento toman más tiempo que las expresiones cuyo contexto está cerca del comienzo del documento.
¿Alguien tiene alguna idea sobre lo que puedo hacer al respecto?
Cada vez que tomas un Nodo de una Nodelist, parece que mantiene referencias a la estructura completa de xml; por esta razón, cuando navegas por el nodo, el proceso de xpath comienza cada vez desde la raíz de xml, y por esta razón, cuando bajas en el trhee, lleva más tiempo.
Por esta razón, cuando tomas un nodo, antes de navegarlo, debes lanzar la cadena con este método:
private String nodeToString(Node node) {
StringWriter sw = new StringWriter();
try {
Transformer t = TransformerFactory.newInstance().newTransformer();
t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
t.transform(new DOMSource(node), new StreamResult(sw));
} catch (TransformerException te) {
System.out.println("nodeToString Transformer Exception");
}
return sw.toString();
}
y luego retransformarlo en un Elemento / Nodo:
String xml = nodeToString(node);
Element nodeNew = DocumentBuilderFactory
.newInstance()
.newDocumentBuilder()
.parse(new ByteArrayInputStream(xml.getBytes()))
.getDocumentElement();
node = nodeNew;
De esta manera, el nuevo Elemento, perdió todas las referencias a sus antepasados, y se utilizará como un simple nodo y no como un nodo anidado. Obviamente, este método es bueno solo si tiene que navegar profundamente en un nodo.
Este parece ser otro caso donde el uso de XPath parece ser lento, pero en lugar de XPath, la razón probablemente sea causada por el método DOM nodelist.item(i)
La implementación predeterminada de NodeList
en Java tiene ciertas características:
- Se evalúa perezosamente
- La lista DOM está en vivo
- Se implementa como una lista vinculada
- La lista tiene algo de almacenamiento en caché
Cuando observa esas características por separado, podría preguntarse por qué el objeto resultante de una expresión XPath debe tener una característica como esa, pero tienen más sentido cuando las junta.
1) La evaluación diferida puede difuminar la ubicación de un cuello de botella de rendimiento. Debido a esto, devolver el NodeList parece ser rápido, pero si la tarea es iterar siempre a través de la lista, más o menos solo difiere el costo de rendimiento. La evaluación diferida se vuelve costosa, si la evaluación de toda la lista debe procesarse de nuevo cada vez que se lea el siguiente elemento de la lista.
2) NodeList
es una lista " NodeList
" que significa que se actualiza y se refiere a los nodos que se encuentran actualmente en el árbol de documentos, no a los nodos que estaban en el árbol cuando la lista se construyó inicialmente o a los clones de esos nodos. Esta es una característica importante para los principiantes DOM. Por ejemplo, si selecciona una NodeList
de elementos hermanos e intenta agregar un nuevo elemento hermano a cada nodo, dar un paso al item(i+1)
siempre alcanzará el nodo agregado más reciente y el ciclo nunca terminará.
3) La lista en vivo también brinda una explicación de por qué se implementa como una lista vinculada (o AFAIK la implementación real es una lista doblemente vinculada). El efecto de esto se puede ver claramente en su prueba, donde el acceso a los últimos elementos es siempre el más lento, ya sea que lo itere hacia atrás o hacia adelante.
4) Debido al almacenamiento en caché, hacer un bucle sobre una sola lista sin causar ningún cambio en el árbol debería ser bastante eficiente, si el caché se mantiene limpio. En algunas versiones de Java, ha habido problemas con este almacenamiento en caché. No he investigado qué procedimientos invalidan el almacenamiento en caché, pero probablemente las apuestas más seguras sean asesorar para mantener la misma expresión evaluada, no realizar cambios en el árbol, recorrer una lista a la vez y pasar siempre al elemento de lista siguiente o anterior.
Las ganancias de rendimiento real dependen del caso de uso, por supuesto. En lugar de simplemente ajustar la lista de bucles, debería intentar deshacerse del bucle de una lista en vivo por completo, al menos como referencia. La clonación hace que la lista no esté activa. El acceso directo a los nodos se puede lograr al copiar los nodos a una matriz. Si la estructura es adecuada, también puede usar otros métodos DOM como getNextSibling()
que dice dar resultados más efectivos que el bucle sobre una NodeList.
Esto es un poco tarde, pero me encontré con la misma situación, pero parecía que mi documento era tan grande que ninguna de las otras respuestas realmente resolvió el problema.
Eventualmente, encontré Jaxen . Una vez que lo usé, el documento que previamente tomó 15 segundos para analizar tomó solo milisegundos.
Desafortunadamente, Jaxen está bastante mal documentado, pero funcionó bastante bien:
DOMXPath myXPath = new DOMXPath("atom:id/text()");
String myContent = myXPath.stringValueOf(myDocument);
El Java Doc se puede encontrar aquí http://jaxen.codehaus.org/apidocs/org/jaxen/dom/DOMXPath.html
Intente agregar este código dentro del bucle en la parte superior;
Node singleNode = nodes.item(i);
singleNode.getParentNode().removeChild(singleNode);
luego ejecute cada evaluación utilizando la variable singleNode
lugar de nodes.item(i);
(por supuesto que cambias el nombre)
Al hacerlo, se separa el nodo con el que está trabajando desde el documento principal grande. Esto acelerará el tiempo de procesamiento de los métodos de evaluación en una gran cantidad.
EX:
for(int i=0;i<nodes.getLength();i++)
{
Node singleNode = nodes.item(i);
singleNode.getParentNode().removeChild(singleNode);
printTimestamp(1);
xp.evaluate("atom:id/text()", singleNode );
printTimestamp(2);
xp.evaluate("samplens:fieldA/text()", singleNode );
printTimestamp(3);
xp.evaluate("atom:author/atom:uri/text()", singleNode );
printTimestamp(4);
xp.evaluate("samplens:fieldA/samplens:fieldB/&at;attrC", singleNode );
printTimestamp(5);
//etc. My real example has 10 of these xp.evaluate lines
}
Intente clonar el nodo (para que no tenga referencias innecesarias de sus antepasados)
Node singleNode = nodes.item(i).cloneNode(true);
Si elimina hijos, perderá referencias y solo obtendrá la mitad de los nodos que desea procesar.