2010-09-23 9 views
20

Estoy intentando utilizar el paquete javax.xml.xpath para ejecutar expresiones XPath en un documento con varios espacios de nombres, y estoy teniendo problemas de rendimiento ridículos.El rendimiento de XPath.evaluate se ralentiza (absurdamente) en llamadas múltiples

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 si se usa USB), la primera vez a través del bucle, cada xp.evaluate lleva de 10 ms a 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 nodes), se requieren aproximadamente 500ms-600ms para cada xp.evaluate.

He intentado usar xp.compile(). 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!

actualización

Si funciono el bucle hacia atrás (for(int i=nodes.getLength()-1;i>=0;i--)), entonces los primeros nodos toman los 500ms-600ms, y los últimos en ir rápido 10ms-20ms. 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?

+0

@Andrew Shelansky: ¿Intentó ejecutar solo una consulta utilizando el nodo 'union' set oparator? El conjunto de nodos resultante estaría en orden de documento. –

+1

@Andrew Shelansky: supongo que la NodeList que devuelve la expresión XPath se evalúa con pereza. Entonces, cada vez que haces nodes.item (i) es necesario contar a través de ítems para encontrar el nodo. Intente almacenar el nodo en la variable al comienzo del ciclo y vea si eso ayuda. –

+0

@Nick Jones. En mi código de prueba, estoy haciendo lazy eval para nodes.item (i). En mi código de producción, en realidad estoy iterando a través de los nodos inmediatamente después de llamar al primer xp.evaluate. Los nodos resultantes se almacenan en un hashmap de UUID a Node, y se evalúan de esa manera. El código de producción muestra el mismo problema. Buena idea, sin embargo. –

Respuesta

9

Este parece ser otro caso en el que el uso de XPath parece ser lento, pero en lugar de XPath, la razón es probablemente causado por el método DOM nodelist.item(i)

La implementación predeterminada de NodeList en Java tiene ciertas características:

  1. se evalúa con pereza
  2. La lista DOM es en vivo
  3. se implementa como una lista enlazada
  4. La lista tiene alguna almacenamiento en caché

Cuando nos fijamos en esas funciones por separado, usted podría preguntarse por qué debería el objeto resultado de una expresión XPath tener una característica de esa manera, sino que tiene más sentido cuando se los pone juntos .

1) La evaluación diferida puede desenfocar 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 ser una lista "en vivo" significa que se actualiza y se refiere a los nodos que están actualmente en la estructura del documento, y no a los nodos que estaban en el árbol cuando la lista se construyó inicialmente o a clones de esos nodos. Esta es una característica importante para los principiantes DOM. Por ejemplo, si selecciona un NodeList de elementos hermanos e intenta agregar un nuevo elemento hermano a cada nodo, dar un paso hacia item(i+1) siempre alcanzará el nodo agregado más reciente y el bucle nunca terminará.

3) La lista sea vivo también da una explicación por la que se implementa como una lista enlazada (o yo sepa la implementación real es una lista doblemente enlazada). 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é, de enlace a través de una lista única sin causar ningún cambio en el árbol debe ser bastante eficiente, si la 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 en el 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 activa 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 dicen dar resultados más efectivos que el bucle sobre una NodeList.

+2

Genial responder. Me encantaría ver algunos ejemplos de código: ¿cómo se clona una lista de nodos, cuál es la forma más rápida de convertirla en una matriz de nodos, etc.? –

46

Intente agregar este código dentro del bucle en la parte superior;

Node singleNode = nodes.item(i); 
singleNode.getParentNode().removeChild(singleNode); 

continuación, ejecute cada evaluación utilizando la variable singleNode en lugar de nodes.item(i); (por supuesto cambia el nombre)

Hacer esto se desprende el nodo que está trabajando desde el gran documento principal. 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 

} 
+4

+1 para punta de desprendimiento. ¡Mejoré mi código de varios minutos a menos de 10 segundos! – Adam

+3

Sí eso hace una gran diferencia. – Lee

+4

No puedo creer que funcione, pero lo hace. En mi caso, en lugar de eliminar el nodo, lo cloné y todavía vi una mejora de rendimiento de veinte veces. – CurtainDog

0

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 resuelve el problema.

Finalmente, encontré jaxen. Una vez que lo usé, el documento que previamente tomó 15 segundos para analizar tomó solo milisegundos.

Jaxen es, por desgracia 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

2

Trate clonar el nodo (por lo que no tendrá referencias innecesarias a partir de su ancestros)

Node singleNode = nodes.item(i).clone(true); 

Si elimina los niños, perderá referencias y sólo conseguir la mitad de los nodos que desea procesar.

0

Cada vez que toma un Nodo de una Nodelist, parece que mantiene referencias a toda la estructura de xml; por este motivo cuando navega por el nodo, el proceso xpath se inicia cada vez desde la raíz de xml, y por esta razón, cuando se baja en el trhee tarda más tiempo.

Por esta razón, cuando se toma un nodo, antes de navegar en ella, tiene que emitir en cadena por 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 transformar de nuevo 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.

Cuestiones relacionadas