diff --git a/org.eclipse.jgit.test/exttst/org/eclipse/jgit/treewalk/FileTreeIteratorPerformanceTest.java b/org.eclipse.jgit.test/exttst/org/eclipse/jgit/treewalk/FileTreeIteratorPerformanceTest.java new file mode 100644 index 000000000..b23838984 --- /dev/null +++ b/org.eclipse.jgit.test/exttst/org/eclipse/jgit/treewalk/FileTreeIteratorPerformanceTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2018, Thomas Wolf + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.treewalk; + +import static org.junit.Assert.fail; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.treewalk.filter.PathFilter; +import org.junit.Test; + +/** + * Simple performance test for git add / FileTreeIterator. + */ +public class FileTreeIteratorPerformanceTest extends RepositoryTestCase { + + private static int N_OF_FILES = 501; + + @Test + public void testPerformance() throws Exception { + try (Git git = new Git(db)) { + long times[] = new long[N_OF_FILES]; + long sum = 0; + String lastName = null; + for (int i = 0; i < N_OF_FILES; i++) { + lastName = "a" + (i + 100000) + ".txt"; + writeTrashFile(lastName, ""); + long start = System.nanoTime(); + git.add().addFilepattern(lastName).call(); + long elapsed = System.nanoTime() - start; + times[i] = elapsed; + sum += elapsed; + } + System.out.println("Total (µs) " + sum / 1000.0); + for (int i = 0; i < times.length; i++) { + System.out + .println("Time " + i + " (µs) = " + times[i] / 1000.0); + } + FileTreeIterator iter = new FileTreeIterator(db); + try (TreeWalk walk = new TreeWalk(db)) { + walk.setFilter(PathFilter.create(lastName)); + walk.addTree(iter); + long start = System.nanoTime(); + if (walk.next()) { + System.out.println("Walk time (µs) = " + + (System.nanoTime() - start) / 1000.0); + } else { + fail("File not found"); + } + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java index 0130cf97e..06f53c2f3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java @@ -211,13 +211,7 @@ public AbstractTreeIterator createSubtreeIterator(final ObjectReader reader) } private Entry[] entries() { - final File[] all = directory.listFiles(); - if (all == null) - return EOF; - final Entry[] r = new Entry[all.length]; - for (int i = 0; i < r.length; i++) - r[i] = new FileEntry(all[i], fs, fileModeStrategy); - return r; + return fs.list(directory, fileModeStrategy); } /** @@ -246,7 +240,7 @@ public interface FileModeStrategy { * * @since 4.3 */ - static public class DefaultFileModeStrategy implements FileModeStrategy { + public static class DefaultFileModeStrategy implements FileModeStrategy { /** * a singleton instance of the default FileModeStrategy */ @@ -279,7 +273,7 @@ public FileMode getMode(File f, FS.Attributes attributes) { * * @since 4.3 */ - static public class NoGitlinksStrategy implements FileModeStrategy { + public static class NoGitlinksStrategy implements FileModeStrategy { /** * a singleton instance of the default FileModeStrategy @@ -304,7 +298,7 @@ public FileMode getMode(File f, FS.Attributes attributes) { /** * Wrapper for a standard Java IO file */ - static public class FileEntry extends Entry { + public static class FileEntry extends Entry { private final FileMode mode; private FS.Attributes attributes; @@ -343,6 +337,29 @@ public FileEntry(File f, FS fs, FileModeStrategy fileModeStrategy) { mode = fileModeStrategy.getMode(f, attributes); } + /** + * Create a new file entry given the specified FileModeStrategy + * + * @param f + * file + * @param fs + * file system + * @param attributes + * of the file + * @param fileModeStrategy + * the strategy to use when determining the FileMode of a + * file; controls gitlinks etc. + * + * @since 5.0 + */ + public FileEntry(File f, FS fs, FS.Attributes attributes, + FileModeStrategy fileModeStrategy) { + this.fs = fs; + this.attributes = attributes; + f = fs.normalize(f); + mode = fileModeStrategy.getMode(f, attributes); + } + @Override public FileMode getMode() { return mode; @@ -365,12 +382,12 @@ public long getLastModified() { @Override public InputStream openInputStream() throws IOException { - if (fs.isSymLink(getFile())) + if (attributes.isSymbolicLink()) { return new ByteArrayInputStream(fs.readSymLink(getFile()) - .getBytes( - Constants.CHARACTER_ENCODING)); - else + .getBytes(Constants.CHARACTER_ENCODING)); + } else { return new FileInputStream(getFile()); + } } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java index e6566939f..cb68cee7b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java @@ -1165,8 +1165,12 @@ private byte[] computeHash(InputStream in, long length) throws IOException { return contentDigest.digest(); } - /** A single entry within a working directory tree. */ - protected static abstract class Entry { + /** + * A single entry within a working directory tree. + * + * @since 5.0 + */ + public static abstract class Entry { byte[] encodedName; int encodedNameLen; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java index a2756e845..27a437f4f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java @@ -71,6 +71,9 @@ import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.treewalk.FileTreeIterator.FileEntry; +import org.eclipse.jgit.treewalk.FileTreeIterator.FileModeStrategy; +import org.eclipse.jgit.treewalk.WorkingTreeIterator.Entry; import org.eclipse.jgit.util.ProcessResult.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -81,6 +84,14 @@ public abstract class FS { private static final Logger LOG = LoggerFactory.getLogger(FS.class); + /** + * An empty array of entries, suitable as a return value for + * {@link #list(File, FileModeStrategy)}. + * + * @since 5.0 + */ + protected static final Entry[] NO_ENTRIES = {}; + /** * This class creates FS instances. It will be overridden by a Java7 variant * if such can be detected in {@link #detect(Boolean)}. @@ -886,6 +897,29 @@ public String relativize(String base, String other) { return FileUtils.relativizePath(base, other, File.separator, this.isCaseSensitive()); } + /** + * Enumerates children of a directory. + * + * @param directory + * to get the children of + * @param fileModeStrategy + * to use to calculate the git mode of a child + * @return an array of entries for the children + * + * @since 5.0 + */ + public Entry[] list(File directory, FileModeStrategy fileModeStrategy) { + final File[] all = directory.listFiles(); + if (all == null) { + return NO_ENTRIES; + } + final Entry[] result = new Entry[all.length]; + for (int i = 0; i < result.length; i++) { + result[i] = new FileEntry(all[i], this, fileModeStrategy); + } + return result; + } + /** * Checks whether the given hook is defined for the given repository, then * runs it with the given arguments. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java index 29fe5dc82..a0ed04fe6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java @@ -47,11 +47,21 @@ import java.io.File; import java.io.IOException; import java.nio.charset.Charset; +import java.nio.file.FileVisitOption; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; +import java.util.EnumSet; import java.util.List; import org.eclipse.jgit.errors.CommandFailedException; +import org.eclipse.jgit.treewalk.FileTreeIterator.FileEntry; +import org.eclipse.jgit.treewalk.FileTreeIterator.FileModeStrategy; +import org.eclipse.jgit.treewalk.WorkingTreeIterator.Entry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -119,6 +129,49 @@ public boolean retryFailedLockFileCommit() { return true; } + /** {@inheritDoc} */ + @Override + public Entry[] list(File directory, FileModeStrategy fileModeStrategy) { + List result = new ArrayList<>(); + FS fs = this; + boolean checkExecutable = fs.supportsExecute(); + try { + Files.walkFileTree(directory.toPath(), + EnumSet.noneOf(FileVisitOption.class), 1, + new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, + BasicFileAttributes attrs) throws IOException { + File f = file.toFile(); + FS.Attributes attributes = new FS.Attributes(fs, f, + true, attrs.isDirectory(), + checkExecutable && f.canExecute(), + attrs.isSymbolicLink(), + attrs.isRegularFile(), + attrs.creationTime().toMillis(), + attrs.lastModifiedTime().toMillis(), + attrs.size()); + result.add(new FileEntry(f, fs, attributes, + fileModeStrategy)); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, + IOException exc) throws IOException { + // Just ignore it + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + // Ignore + } + if (result.isEmpty()) { + return NO_ENTRIES; + } + return result.toArray(new Entry[result.size()]); + } + /** {@inheritDoc} */ @Override protected File discoverGitExe() {