Existe uma solução alternativa para o mau desempenho do Java em andar diretórios enormes?

Eu estou tentando processar arquivos um de cada vez que são armazenados em uma rede. Lendo os arquivos é rápido devido ao buffer não é o problema. O problema que tenho é apenas listar os diretórios em uma pasta. Eu tenho pelo menos 10k arquivos por pasta em muitas pastas.

O desempenho é super lento, pois o File.list () retorna um array em vez de um iterável. Java vai e recolhe todos os nomes em uma pasta e empacota em um array antes de retornar.

A input de bug para isso é http://bugs.sun.com/view_bug.do;jsessionid=db7fcf25bcce13541c4289edeb4?bug_id=4285834 e não tem um trabalho ao redor. Eles apenas dizem que isso foi corrigido para o JDK7.

Algumas questões:

  1. Alguém tem uma solução para esse gargalo de desempenho?
  2. Estou tentando alcançar o impossível? O desempenho ainda será ruim mesmo se apenas repetir os diretórios?
  3. Eu poderia usar as versões beta do JDK7 que têm essa funcionalidade sem ter que construir todo o meu projeto nela?

Embora não seja bonito, resolvi esse tipo de problema uma vez colocando a saída de dir / ls em um arquivo antes de iniciar meu aplicativo e passando o nome do arquivo.

Se você precisasse fazê-lo dentro do aplicativo, você poderia apenas usar o system.exec (), mas criaria alguma maldade.

Você perguntou. A primeira forma será incrivelmente rápida, a segunda deve ser bem rápida também.

Certifique-se de fazer o item por linha (nu, sem decoração, sem charts), caminho completo e opções de recurs de seu comando selecionado.

EDITAR:

30 minutos apenas para obter uma lista de diretórios, uau.

Achei que se você usar exec (), você pode obter seu stdout redirecionado para um pipe em vez de gravá-lo em um arquivo.

Se você fez isso, você deve começar a obter os arquivos imediatamente e começar a processar antes que o comando seja concluído.

A interação pode realmente atrasar as coisas, mas talvez não – você pode tentar.

Uau, eu só fui encontrar a syntax do comando .exec para você e me deparei com isso, possivelmente exatamente o que você quer (lista um diretório usando exec e “ls” e canaliza o resultado para o seu programa para processamento): good link em wayback (Jörg forneceu em um comentário para replace este do sol que a Oracle quebrou)

De qualquer forma, a ideia é simples, mas acertar o código é irritante. Eu vou roubar alguns códigos dos internets e hackear eles – brb


 / **
  * Nota: Use isso apenas como último recurso!  É específico para janelas e até mesmo
  * Não é uma boa solução, mas deve ser rápida.
  * 
  * para usá-lo, estender o FileProcessor e chamar processFiles ("...") com uma lista
  * de opções se você quiser como / s ... eu recomendo altamente / b
  * 
  * Substitui processFile e será chamado uma vez para cada linha de saída.
  * /
 import java.io. *;

 class abstrata pública FileProcessor
 {
    public void processFiles (String dirOptions)
    {
       Process theProcess = null;
       BufferedReader inStream = nulo;

       // chama a class Hello
       experimentar
       {
           theProcess = Runtime.getRuntime (). exec ("diretórios cmd / c" + dirOptions);
       }
       catch (IOException e)
       {
          System.err.println ("Erro no método exec ()");
          e.printStackTrace ();  
       }

       // lê o stream de saída padrão do programa chamado
       experimentar
       {
          inStream = new BufferedReader (
                                 new InputStreamReader (theProcess.getInputStream ()));  
          processFile (inStream.readLine ());
       }
       catch (IOException e)
       {
          System.err.println ("Erro no inStream.readLine ()");
          e.printStackTrace ();  
       }

    } // fim do método
    / ** Sobrescreve este método - ele será chamado uma vez para cada arquivo * /
    public abstract void processFile (nome do arquivo String);


 } // fim da aula

E obrigado doador de código na IBM

Uma alternativa é ter os arquivos exibidos em um protocolo diferente. Pelo que entendi você está usando o SMB para isso e java está apenas tentando listá-los como um arquivo regular.

O problema aqui pode não ser o java sozinho (como ele se comporta quando você abre esse diretório com o Microsoft Explorer x: \ shared). Na minha experiência, ele também leva uma quantidade considerável de tempo.

Você pode alterar o protocolo para algo como HTTP, apenas para buscar os nomes dos arquivos. Dessa forma, você pode recuperar a lista de arquivos através de http (linhas de 10k não devem ser demais) e deixar o servidor lidar com a listview de arquivos. Isso seria muito rápido, uma vez que será executado com resources locais (aqueles no servidor)

Então, quando você tem a lista, você pode processá-los exatamente da maneira que você está fazendo agora.

O ponto chave é ter um mecanismo de ajuda no outro lado do nó.

Isso é viável?

Hoje:

 File [] content = new File("X:\\remote\\dir").listFiles(); for ( File f : content ) { process( f ); } 

Proposta:

 String [] content = fetchViaHttpTheListNameOf("x:\\remote\\dir"); for ( String fileName : content ) { process( new File( fileName ) ); } 

O servidor http pode ser um arquivo pequeno e simples muito pequeno.

Se esta é a maneira que você tem agora, o que você está fazendo é buscar todas as informações dos arquivos 10k para a sua máquina cliente (eu não sei o quanto dessas informações) quando você só precisa do nome do arquivo para processamento posterior .

Se o processamento for muito rápido agora, ele pode ser desacelerado um pouco. Isso ocorre porque as informações pré-buscadas não estão mais disponíveis.

De uma chance.

Eu duvido que o problema esteja relacionado ao relatório de bug que você mencionou. A questão lá é “apenas” o uso da memory, mas não necessariamente a velocidade. Se você tem memory suficiente, o bug não é relevante para o seu problema.

Você deve medir se o seu problema está relacionado à memory ou não. Ative seu log do Garbage Collector e use, por exemplo, o gcviewer para analisar seu uso de memory.

Eu suspeito que isso tenha a ver com o protocolo SMB causando o problema. Você pode tentar escrever um teste em outro idioma e ver se ele é mais rápido, ou você pode tentar obter a lista de nomes de arquivos através de algum outro método, como descrito aqui em outro post.

Que tal usar o método File.list (FilenameFilter filter) e implementar FilenameFilter.accept (File dir, String name) para processar cada arquivo e retornar false.

Eu corri isso no Linux vm para o diretório com 10k + arquivos e levou <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)); } } 

Uma solução não portátil seria fazer chamadas nativas para o sistema operacional e transmitir os resultados.

Para Linux

Você pode olhar para algo como readdir . Você pode percorrer a estrutura de diretórios como uma lista encadeada e retornar resultados em lotes ou individualmente.

Para Windows

No Windows, o comportamento seria bastante semelhante usando as APIs FindFirstFile e FindNextFile .

Se você precisar processar todos os arquivos, então ter Iterable over String [] não lhe dará nenhuma vantagem, pois você ainda terá que ir buscar toda a lista de arquivos.

Se você estiver no Java 1.5 ou 1.6, descartar comandos “dir” e analisar o stream de saída padrão no Windows é uma abordagem perfeitamente aceitável. Eu usei essa abordagem no passado para o processamento de unidades de rede e geralmente tem sido muito mais rápido do que esperar pelo retorno do método nativo java.io.File listFiles ().

Obviamente, uma chamada JNI deve ser mais rápida e potencialmente mais segura do que enviar comandos “dir”. O código JNI a seguir pode ser usado para recuperar uma lista de arquivos / diretórios usando a API do Windows. Essa function pode ser facilmente refatorada em uma nova class para que o chamador possa recuperar caminhos de arquivo de forma incremental (ou seja, obter um caminho de cada vez). Por exemplo, você pode refatorar o código para que FindFirstFileW seja chamado em um construtor e tenha um método separado para chamar 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

Mesmo com essa abordagem, ainda existem eficiências a serem obtidas. Se você serializar o caminho para um java.io.File, haverá um grande impacto no desempenho – especialmente se o caminho representar um arquivo em uma unidade de rede. Eu não tenho idéia do que a Sun / Oracle está fazendo sob o capô, mas se você precisar de atributos de arquivo adicionais além do caminho do arquivo (por exemplo, tamanho, data de modificação, etc), descobri que a seguinte function JNI é muito mais rápida do que instanciar um java Objeto .io.File em uma rede o caminho.

 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; } 

Você pode encontrar um exemplo completo de trabalho dessa abordagem baseada em JNI na biblioteca javaxt-core . Nos meus testes usando o Java 1.6.0_38 com um host do Windows atingindo um compartilhamento do Windows, eu encontrei essa abordagem JNI aproximadamente 10x mais rápida, em seguida, chamando java.io.File listFiles () ou enviando comandos “dir”.

Eu me pergunto por que existem 10k arquivos em um diretório. Alguns filesystems não funcionam bem com tantos arquivos. Existem limitações específicas para filesystems, como quantidade máxima de arquivos por diretório e quantidade máxima de níveis de subdiretório.

Eu resolvo um problema semelhante com uma solução iteradora.

Eu precisava percorrer enormes diretórios e vários níveis de tree de diretórios de forma recursiva.

Eu tento FileUtils.iterateFiles () do Apache commons io. Mas implementar o iterador, adicionando todos os arquivos em uma lista e, em seguida, retornando List.iterator (). É muito ruim para a memory.

Então eu prefiro escrever algo assim:

 private static class SequentialIterator implements Iterator { 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(); } } 

Observe que o iterador pára por uma quantidade de arquivos iterados e também possui um FileFilter.

E o DirectoryStack é:

 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 set = new TreeSet(); 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 stack=null; private Stack getStack(){ if(stack==null){ stack = new Stack(); } 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(); } } 

Usando um Iterable não implica que os arquivos serão transmitidos para você. Na verdade, é geralmente o oposto. Portanto, uma matriz é geralmente mais rápida que uma iterável.

Você tem certeza que é devido a Java, não apenas um problema geral em ter 10k inputs em um diretório, particularmente na rede?

Você já tentou escrever um programa de prova de conceito para fazer a mesma coisa em C usando as funções find32st / findnext do win32 para ver se é mais rápido?

Não conheço os meandros do SMB, mas suspeito fortemente que ele precisa de uma viagem de ida e volta para todos os arquivos da lista – o que não será rápido, especialmente em uma rede com latência moderada.

Ter 10k strings em uma matriz soa como algo que também não deveria sobrecarregar a moderna Java VM.