2008-12-10 13 views
16

Estoy tratando de procesar archivos de uno en uno que se almacenan a través de una red. Leer los archivos es rápido debido al almacenamiento en búfer no es el problema. El problema que tengo es solo enumerar los directorios en una carpeta. Tengo al menos 10k archivos por carpeta en muchas carpetas.¿Hay alguna solución para el bajo rendimiento de Java al recorrer directorios enormes?

El rendimiento es super lento ya que File.list() devuelve una matriz en lugar de una iterable. Java se apaga y recoge todos los nombres en una carpeta y los empaqueta en una matriz antes de volver.

La entrada del error para esto es http://bugs.sun.com/view_bug.do;jsessionid=db7fcf25bcce13541c4289edeb4?bug_id=4285834 y no tiene un problema. Solo dicen que esto ha sido arreglado para JDK7.

algunas preguntas:

  1. ¿Alguien tiene una solución a este cuello de botella?
  2. ¿Estoy tratando de lograr lo imposible? ¿El rendimiento seguirá siendo pobre incluso si solo itera sobre los directorios?
  3. podría utilizar la versión beta JDK7 generaciones que tiene esta funcionalidad sin tener que construir toda mi proyecto en él?
+0

¿Cuál es su arquitectura de destino? ¿Necesita mantener la ejecución multiplataforma? –

+0

Windows es el objetivo – Pyrolistical

+4

Me doy cuenta de que esta es una pregunta muy antigua, pero para cualquiera que use el último JDK7, esta funcionalidad ahora está disponible a través de ['Files.newDirectoryStream()'] (http://docs.oracle.com /javase/7/docs/api/java/nio/file/Files.html#newDirectoryStream%28java.nio.file.Path%29) API en el paquete 'java.nio.file'. –

Respuesta

7

Aunque no es bonito, resolví este tipo de problema una vez conectando la salida de dir/ls a un archivo antes de iniciar mi aplicación y pasando el nombre del archivo.

Si necesitabas hacerlo dentro de la aplicación, solo podías usar system.exec(), pero crearía algo desagradable.

Usted ha preguntado. La primera forma será increíblemente rápida, la segunda también será bastante rápida.

Asegúrese de hacer un artículo por línea (desnudo, sin decoración, sin gráficos), ruta completa y opciones de recurse de su comando seleccionado.

EDIT:

30 minutos para conseguir un listado de directorios, guau.

Me acabo de dar cuenta de que si utiliza exec(), puede hacer que se redireccione en una tubería en vez de escribirla en un archivo.

Si lo hizo, debería comenzar a obtener los archivos inmediatamente y poder comenzar a procesarlos antes de que se complete el comando.

La interacción en realidad puede ralentizar las cosas, pero tal vez no, puede intentarlo.

Vaya, encontré la sintaxis del comando .exec para usted y encontré esto, posiblemente exactamente lo que quiere (enumera un directorio usando exec y "ls" y canaliza el resultado en su programa para su procesamiento): good link in wayback (Jörg proporcionado en un comentario para reemplazar this one del sol que Oracle rompió)

De todos modos, la idea es clara pero obtener el código correcto es molesto. Voy a ir a robar algunos códigos de la internets y hackeo para arriba - BRB

 

/** 
* Note: Only use this as a last resort! It's specific to windows and even 
* at that it's not a good solution, but it should be fast. 
* 
* to use it, extend FileProcessor and call processFiles("...") with a list 
* of options if you want them like /s... I highly recommend /b 
* 
* override processFile and it will be called once for each line of output. 
*/ 
import java.io.*; 

public abstract class FileProcessor 
{ 
    public void processFiles(String dirOptions) 
    { 
     Process theProcess = null; 
     BufferedReader inStream = null; 

     // call the Hello class 
     try 
     { 
      theProcess = Runtime.getRuntime().exec("cmd /c dir " + dirOptions); 
     } 
     catch(IOException e) 
     { 
     System.err.println("Error on exec() method"); 
     e.printStackTrace(); 
     } 

     // read from the called program's standard output stream 
     try 
     { 
     inStream = new BufferedReader(
           new InputStreamReader(theProcess.getInputStream())); 
     processFile(inStream.readLine()); 
     } 
     catch(IOException e) 
     { 
     System.err.println("Error on inStream.readLine()"); 
     e.printStackTrace(); 
     } 

    } // end method 
    /** Override this method--it will be called once for each file */ 
    public abstract void processFile(String filename); 


} // end class 

y gracias donante de código en IBM

+0

esta es una buena solución para árboles o árboles estáticos que no cambian a menudo, pero es necesario que la atraviese con frecuencia – Pyrolistical

+0

Sí, la usé para casos en los que era un tipo de programa de procesamiento por lotes. Si lo hiciste a menudo, bueno, no estoy seguro de que usar exec() para recrear ese archivo no funcione, pero independientemente de que sea una solución poco elegante y propensa a errores. –

+0

Acabo de probar esto y un "dir/B/S ..." tardó unos 30 minutos en recolectar más de 200MB de rutas, mientras que me tomó alrededor de 3.5 horas atravesar el mismo árbol usando File.list() – Pyrolistical

2

Una solución no portátil sería realizar llamadas nativas al sistema operativo y transmitir los resultados.

Para Linux

se puede ver en algo así como readdir. Puede recorrer la estructura del directorio como una lista vinculada y devolver resultados en lotes o individualmente.

Para Windows

En las ventanas del comportamiento sería bastante similar usando FindFirstFile y FindNextFile apis.

+0

por el momento, estaría de acuerdo en que quizás desee ajustar FFF/FNF como se sugiere aquí. –

+0

Tenga en cuenta que tender un puente sobre la barrera nativa tendrá un impacto en el rendimiento. Si adopta este enfoque, puede considerar agrupar varios resultados de FNF por llamada JNI (o posiblemente utilizar un DirectByteBuffer para la transferencia de datos) –

+0

+1 por sugerir FFF/FNF. He publicado un ejemplo de trabajo a continuación. – Peter

4

Una alternativa es tener los archivos servidos a través de un protocolo diferente. Como entiendo, estás usando SMB para eso y java solo intenta listarlos como un archivo normal.

El problema aquí no sean java sola (¿cómo se comporta cuando se abre ese directorio con Microsoft Explorer X: \ shared) En mi experiencia es también tomar una considerable cantidad de tiempo.

Usted puede cambiar el protocolo a algo así como HTTP, sólo para sacar los nombres de archivo. De esta forma puede recuperar la lista de archivos en http (10k líneas no deberían ser demasiado) y dejar que el servidor se ocupe de la lista de archivos. Esto sería muy rápido, ya que se ejecutará con recursos locales (los que están en el servidor)

Luego, cuando tenga la lista, puede procesarlos exactamente como lo está haciendo en este momento.

El punto clave es tener un mecanismo de ayuda en el otro lado del nodo.

¿Esto es factible?

Hoy:

File [] content = new File("X:\\remote\\dir").listFiles(); 

for (File f : content) { 
    process(f); 
} 

propuesto:

String [] content = fetchViaHttpTheListNameOf("x:\\remote\\dir"); 

for (String fileName : content) { 
    process(new File(fileName)); 
} 

El servidor HTTP podría ser un pequeño archivo pequeño y simple.

Si esta es la forma en que lo tiene ahora, lo que está haciendo es buscar toda la información de los archivos 10k en su equipo cliente (no sé cuánto de esa información) cuando solo necesita el archivo nombre para un procesamiento posterior.

Si el procesamiento es muy rápido, ahora puede ralentizarse un poco. Esto se debe a que la información extraída previamente ya no está disponible.

Pruébalo.

+0

que implica muchas cosas: un servidor web, implementar algún tipo de seguridad que SMB/etc ya tenga, y así sucesivamente. –

+0

No realmente. Es como tener un sitio web LAN no es? Creo que este es un problema ya resuelto. Aunque ahora que lo menciona, no tengo idea de cómo configurar un sitio web que podría ser visible solo en la LAN. Estoy bastante seguro de que existe un .exe que los archivos del servidor y la autenticación de Windows ya existe. – OscarRyz

+0

¿No es compatible IIS con este tipo de funcionalidad? ... Creo que puede autenticar usuarios de ventanas muy fácilmente. Pero estoy de acuerdo, no es transparente. Sin embargo, me parece mucho más fácil/más rápido que ajustar el código nativo a través de JNI y seguir usando SMB como protocolo. – OscarRyz

3

dudo que el problema es relacionar al informe de fallo al que hizo referencia. El problema es "solo" el uso de memoria, pero no necesariamente la velocidad. Si tiene suficiente memoria, el error no es relevante para su problema.

Debe medir si su problema está relacionado con la memoria o no.Encienda su registro de Garbage Collector y use por ejemplo gcviewer para analizar el uso de su memoria.

Sospecho que tiene que ver con el protocolo SMB que causa el problema. Puede intentar escribir una prueba en otro idioma y ver si es más rápido, o puede tratar de obtener la lista de nombres de archivo a través de algún otro método, como se describe aquí en otra publicación.

0

Me pregunto por qué hay 10k archivos en un directorio. Algunos sistemas de archivos no funcionan bien con tantos archivos. Existen limitaciones específicas para los sistemas de archivos, como la cantidad máxima de archivos por directorio y la cantidad máxima de niveles de subdirectorio.

Resuelvo un problema similar con una solución de iterador.

Necesitaba caminar a través de enormes directores y varios niveles de árbol de directorios recursivamente.

Intento FileUtils.iterateFiles() de Apache commons io. Pero implementa el iterador al agregar todos los archivos en una lista y luego devolver List.iterator(). Es muy malo para la memoria.

así que prefiero escribir algo como esto:

private static class SequentialIterator implements Iterator<File> { 
    private DirectoryStack dir = null; 
    private File current = null; 
    private long limit; 
    private FileFilter filter = null; 

    public SequentialIterator(String path, long limit, FileFilter ff) { 
     current = new File(path); 
     this.limit = limit; 
     filter = ff; 
     dir = DirectoryStack.getNewStack(current); 
    } 

    public boolean hasNext() { 
     while(walkOver()); 
     return isMore && (limit > count || limit < 0) && dir.getCurrent() != null; 
    } 

    private long count = 0; 

    public File next() { 
     File aux = dir.getCurrent(); 
     dir.advancePostition(); 
     count++; 
     return aux; 
    } 

    private boolean walkOver() { 
     if (dir.isOutOfDirListRange()) { 
      if (dir.isCantGoParent()) { 
       isMore = false; 
       return false; 
      } else { 
       dir.goToParent(); 
       dir.advancePostition(); 
       return true; 
      } 
     } else { 
      if (dir.isCurrentDirectory()) { 
       if (dir.isDirectoryEmpty()) { 
        dir.advancePostition(); 
       } else { 
        dir.goIntoDir(); 
       } 
       return true; 
      } else { 
       if (filter.accept(dir.getCurrent())) { 
        return false; 
       } else { 
        dir.advancePostition(); 
        return true; 
       } 
      } 
     } 
    } 

    private boolean isMore = true; 

    public void remove() { 
     throw new UnsupportedOperationException(); 
    } 

} 

Tenga en cuenta que la parada de iterador por una cantidad de archivos iterateds y tiene un FileFilter también.

Y DirectoryStack es:

public class DirectoryStack { 
    private class Element{ 
     private File files[] = null; 
     private int currentPointer; 
     public Element(File current) { 
      currentPointer = 0; 
      if (current.exists()) { 
       if(current.isDirectory()){ 
        files = current.listFiles(); 
        Set<File> set = new TreeSet<File>(); 
        for (int i = 0; i < files.length; i++) { 
         File file = files[i]; 
         set.add(file); 
        } 
        set.toArray(files); 
       }else{ 
        throw new IllegalArgumentException("File current must be directory"); 
       } 
      } else { 
       throw new IllegalArgumentException("File current not exist"); 
      } 

     } 
     public String toString(){ 
      return "current="+getCurrent().toString(); 
     } 
     public int getCurrentPointer() { 
      return currentPointer; 
     } 
     public void setCurrentPointer(int currentPointer) { 
      this.currentPointer = currentPointer; 
     } 
     public File[] getFiles() { 
      return files; 
     } 
     public File getCurrent(){ 
      File ret = null; 
      try{ 
       ret = getFiles()[getCurrentPointer()]; 
      }catch (Exception e){ 
      } 
      return ret; 
     } 
     public boolean isDirectoryEmpty(){ 
      return !(getFiles().length>0); 
     } 
     public Element advancePointer(){ 
      setCurrentPointer(getCurrentPointer()+1); 
      return this; 
     } 
    } 
    private DirectoryStack(File first){ 
     getStack().push(new Element(first)); 
    } 
    public static DirectoryStack getNewStack(File first){ 
     return new DirectoryStack(first); 
    } 
    public String toString(){ 
     String ret = "stack:\n"; 
     int i = 0; 
     for (Element elem : stack) { 
      ret += "nivel " + i++ + elem.toString()+"\n"; 
     } 
     return ret; 
    } 
    private Stack<Element> stack=null; 
    private Stack<Element> getStack(){ 
     if(stack==null){ 
      stack = new Stack<Element>(); 
     } 
     return stack; 
    } 
    public File getCurrent(){ 
     return getStack().peek().getCurrent(); 
    } 
    public boolean isDirectoryEmpty(){ 
     return getStack().peek().isDirectoryEmpty(); 
    } 
    public DirectoryStack downLevel(){ 
     getStack().pop(); 
     return this; 
    } 
    public DirectoryStack goToParent(){ 
     return downLevel(); 
    } 
    public DirectoryStack goIntoDir(){ 
     return upLevel(); 
    } 
    public DirectoryStack upLevel(){ 
     if(isCurrentNotNull()) 
      getStack().push(new Element(getCurrent())); 
     return this; 
    } 
    public DirectoryStack advancePostition(){ 
     getStack().peek().advancePointer(); 
     return this; 
    } 
    public File[] peekDirectory(){ 
     return getStack().peek().getFiles(); 
    } 
    public boolean isLastFileOfDirectory(){ 
     return getStack().peek().getFiles().length <= getStack().peek().getCurrentPointer(); 
    } 
    public boolean gotMoreLevels() { 
     return getStack().size()>0; 
    } 
    public boolean gotMoreInCurrentLevel() { 
     return getStack().peek().getFiles().length > getStack().peek().getCurrentPointer()+1; 
    } 
    public boolean isRoot() { 
     return !(getStack().size()>1); 
    } 
    public boolean isCurrentNotNull() { 
     if(!getStack().isEmpty()){ 
      int currentPointer = getStack().peek().getCurrentPointer(); 
      int maxFiles = getStack().peek().getFiles().length; 
      return currentPointer < maxFiles; 
     }else{ 
      return false; 
     } 
    } 
    public boolean isCurrentDirectory() { 
     return getStack().peek().getCurrent().isDirectory(); 
    } 
    public boolean isLastFromDirList() { 
     return getStack().peek().getCurrentPointer() == (getStack().peek().getFiles().length-1); 
    } 
    public boolean isCantGoParent() { 
     return !(getStack().size()>1); 
    } 
    public boolean isOutOfDirListRange() { 
     return getStack().peek().getFiles().length <= getStack().peek().getCurrentPointer(); 
    } 

} 
+0

? Usas listFiles() de todos modos, que terminaría con el mismo rendimiento. Tu código no hace una gran cantidad de nada. listFiles(). iterator() habría sido el mismo. – Pyrolistical

+0

Si quiere caminar recursivamente a través de 5 niveles de subdirectorio con 10.000 archivos por directorio listFiles.iterator() mata su memoria. Para varios archivos en el mismo directorio es el mismo. – user2427

1

Si tiene que procesar todos los archivos con el tiempo, y luego tener Iterable sobre String [] no le dará ninguna ventaja, ya que todavía tiene que ir a buscar la toda la lista de archivos.

0

El uso de un Iterable no implica que los archivos se transmitirán a usted. De hecho, generalmente es todo lo contrario. Entonces una matriz es típicamente más rápida que una Iterable.

0

¿Estás seguro de que se debe a Java, no solo a un problema general con tener 10k entradas en un directorio, particularmente a través de la red?

¿Ha intentado escribir un programa de prueba de concepto para hacer lo mismo en C usando las funciones findfirst/findnext de win32 para ver si es más rápido?

No conozco los pormenores de SMB, pero sospecho que es necesario un viaje redondo para cada archivo de la lista, lo que no será rápido, especialmente en una red con una latencia moderada.

Tener cadenas de 10k en una matriz suena como algo que tampoco debería gravar demasiado la VM de Java moderna.

1

Si tiene Java 1.5 o 1.6, desgranar comandos "dir" y analizar el flujo de salida estándar en Windows es un enfoque perfectamente aceptable. He utilizado este enfoque en el pasado para procesar unidades de red y, en general, ha sido mucho más rápido que esperar a que vuelva el método nativo java.io.File listFiles().

Por supuesto, una llamada JNI debería ser más rápida y potencialmente más segura que desgranar comandos "dir". El siguiente código JNI se puede usar para recuperar una lista de archivos/directorios utilizando la API de Windows. Esta función se puede refactorizar fácilmente en una nueva clase para que la persona que llama pueda recuperar rutas de archivos de forma incremental (es decir, obtener una ruta a la vez). Por ejemplo, puede refactorizar el código para llamar a FindFirstFileW en un constructor y tener un método separado para llamar a FindNextFileW.

JNIEXPORT jstring JNICALL Java_javaxt_io_File_GetFiles(JNIEnv *env, jclass, jstring directory) 
{ 
    HANDLE hFind; 
    try { 

     //Convert jstring to wstring 
     const jchar *_directory = env->GetStringChars(directory, 0); 
     jsize x = env->GetStringLength(directory); 
     wstring path; //L"C:\\temp\\*"; 
     path.assign(_directory, _directory + x); 
     env->ReleaseStringChars(directory, _directory); 

     if (x<2){ 
      jclass exceptionClass = env->FindClass("java/lang/Exception"); 
      env->ThrowNew(exceptionClass, "Invalid path, less than 2 characters long."); 
     } 

     wstringstream ss; 
     BOOL bContinue = TRUE; 
     WIN32_FIND_DATAW data; 
     hFind = FindFirstFileW(path.c_str(), &data); 
     if (INVALID_HANDLE_VALUE == hFind){ 
      jclass exceptionClass = env->FindClass("java/lang/Exception"); 
      env->ThrowNew(exceptionClass, "FindFirstFileW returned invalid handle."); 
     } 


     //HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); 
     //DWORD dwBytesWritten; 


     // If we have no error, loop thru the files in this dir 
     while (hFind && bContinue){ 

      /* 
      //Debug Print Statment. DO NOT DELETE! cout and wcout do not print unicode correctly. 
      WriteConsole(hStdOut, data.cFileName, (DWORD)_tcslen(data.cFileName), &dwBytesWritten, NULL); 
      WriteConsole(hStdOut, L"\n", 1, &dwBytesWritten, NULL); 
      */ 

      //Check if this entry is a directory 
      if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY){ 
       // Make sure this dir is not . or .. 
       if (wstring(data.cFileName) != L"." && 
        wstring(data.cFileName) != L"..") 
       { 
        ss << wstring(data.cFileName) << L"\\" << L"\n"; 
       } 
      } 
      else{ 
       ss << wstring(data.cFileName) << L"\n"; 
      } 
      bContinue = FindNextFileW(hFind, &data); 
     } 
     FindClose(hFind); // Free the dir structure 



     wstring cstr = ss.str(); 
     int len = cstr.size(); 
     //WriteConsole(hStdOut, cstr.c_str(), len, &dwBytesWritten, NULL); 
     //WriteConsole(hStdOut, L"\n", 1, &dwBytesWritten, NULL); 
     jchar* raw = new jchar[len]; 
     memcpy(raw, cstr.c_str(), len*sizeof(wchar_t)); 
     jstring result = env->NewString(raw, len); 
     delete[] raw; 
     return result; 
    } 
    catch(...){ 
     FindClose(hFind); 
     jclass exceptionClass = env->FindClass("java/lang/Exception"); 
     env->ThrowNew(exceptionClass, "Exception occured."); 
    } 

    return NULL; 
} 

Crédito: https://sites.google.com/site/jozsefbekes/Home/windows-programming/miscellaneous-functions

Incluso con este enfoque, todavía hay eficiencias que se pueden obtener. Si serializa la ruta a java.io.File, hay un enorme impacto en el rendimiento, especialmente si la ruta representa un archivo en una unidad de red. No tengo idea de qué hace Sun/Oracle bajo el capó, pero si necesita atributos de archivo adicionales a la ruta del archivo (por ejemplo, tamaño, fecha de modificación, etc.), he encontrado que la siguiente función JNI es mucho más rápida que crear una instancia de Java .io.Archivo objeto en una red de la ruta.

JNIEXPORT jlongArray JNICALL Java_javaxt_io_File_GetFileAttributesEx(JNIEnv *env, jclass, jstring filename) 
{ 

    //Convert jstring to wstring 
    const jchar *_filename = env->GetStringChars(filename, 0); 
    jsize len = env->GetStringLength(filename); 
    wstring path; 
    path.assign(_filename, _filename + len); 
    env->ReleaseStringChars(filename, _filename); 


    //Get attributes 
    WIN32_FILE_ATTRIBUTE_DATA fileAttrs; 
    BOOL result = GetFileAttributesExW(path.c_str(), GetFileExInfoStandard, &fileAttrs); 
    if (!result) { 
     jclass exceptionClass = env->FindClass("java/lang/Exception"); 
     env->ThrowNew(exceptionClass, "Exception Occurred"); 
    } 

    //Create an array to store the WIN32_FILE_ATTRIBUTE_DATA 
    jlong buffer[6]; 
    buffer[0] = fileAttrs.dwFileAttributes; 
    buffer[1] = date2int(fileAttrs.ftCreationTime); 
    buffer[2] = date2int(fileAttrs.ftLastAccessTime); 
    buffer[3] = date2int(fileAttrs.ftLastWriteTime); 
    buffer[4] = fileAttrs.nFileSizeHigh; 
    buffer[5] = fileAttrs.nFileSizeLow; 

    jlongArray jLongArray = env->NewLongArray(6); 
    env->SetLongArrayRegion(jLongArray, 0, 6, buffer); 
    return jLongArray; 
} 

puede encontrar un ejemplo de trabajo completa de este enfoque basado en JNI en la biblioteca javaxt-core. En mis pruebas usando Java 1.6.0_38 con un host de Windows que golpea un recurso compartido de Windows, he encontrado este enfoque JNI aproximadamente 10 veces más rápido que llamar a java.io.File listFiles() o descascarar comandos "dir".

3

Cómo utilizar el método File.list (FilenameFilter filter) e implementar FilenameFilter.accept (File dir, String name) para procesar cada archivo y devolver false.

Ejecuté esto en Linux vm para el directorio con 10K + archivos y tomó < 10 segundos.

import java.io.File; 
import java.io.FilenameFilter; 

public class Temp { 
    private static void processFile(File dir, String name) { 
     File file = new File(dir, name); 
     System.out.println("processing file " + file.getName()); 
    } 

    private static void forEachFile(File dir) { 
     String [] ignore = dir.list(new FilenameFilter() { 
      public boolean accept(File dir, String name) { 
       processFile(dir, name); 
       return false; 
      } 
     }); 
    } 

    public static void main(String[] args) { 
     long before, after; 
     File dot = new File("."); 
     before = System.currentTimeMillis(); 
     forEachFile(dot); 
     after = System.currentTimeMillis(); 
     System.out.println("after call, delta is " + (after - before)); 
    } 
} 
+0

en Windows, listando una forma de carpeta remota> 20 segundos pasó a 500ms ... +1 – Lesto

Cuestiones relacionadas