blob: bca18f21cfc1125dbe9fcb4c0702c4de160b33a1 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.subversion;
import org.netbeans.modules.versioning.util.FileUtils;
import org.netbeans.modules.turbo.CacheIndex;
import org.netbeans.modules.subversion.util.*;
import org.netbeans.modules.turbo.TurboProvider;
import java.io.*;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.openide.modules.Places;
import org.openide.util.NbBundle;
/**
* Storage of file attributes with shortcut to retrieve all stored values.
*
* @author Maros Sandor
*/
class DiskMapTurboProvider implements TurboProvider {
static final String ATTR_STATUS_MAP = "subversion.STATUS_MAP"; // NOI18N
private static final int STATUS_VALUABLE = FileInformation.STATUS_MANAGED
& ~FileInformation.STATUS_VERSIONED_UPTODATE & ~FileInformation.STATUS_NOTVERSIONED_EXCLUDED;
private static final String CACHE_DIRECTORY = "svncache"; // NOI18N
private static final int DIRECTORY = Integer.highestOneBit(Integer.MAX_VALUE);
private static final Logger LOG = Logger.getLogger(DiskMapTurboProvider.class.getName());
private File cacheStore;
private final CacheIndex index = createCacheIndex();
private final CacheIndex conflictedIndex = createCacheIndex();
private final CacheIndex ignoresIndex = createCacheIndex();
private final boolean logModifiedFiles = Boolean.getBoolean("versioning.subversion.turbo.logModifiedFiles"); //NOI18N
DiskMapTurboProvider() {
initCacheStore();
}
File[] getIndexValues(File file, int includeStatus) {
if (includeStatus == FileInformation.STATUS_VERSIONED_CONFLICT) {
return conflictedIndex.get(file);
} else if (includeStatus == FileInformation.STATUS_NOTVERSIONED_EXCLUDED) {
return ignoresIndex.get(file);
} else if ((includeStatus & FileInformation.STATUS_NOTVERSIONED_EXCLUDED) != 0) {
File[] files = index.get(file);
File[] ignores = ignoresIndex.get(file);
return mergeArrays(files, ignores);
} else {
return index.get(file);
}
}
File[] getAllIndexValues() {
File[] files = index.getAllValues();
File[] ignores = ignoresIndex.getAllValues();
return mergeArrays(files, ignores);
}
private File[] mergeArrays (File[] arr1, File[] arr2) {
if (arr1.length == 0) {
return arr2;
} else if (arr2.length == 0) {
return arr1;
} else {
Set<File> merged = new HashSet<>(Arrays.asList(arr2));
merged.addAll(Arrays.asList(arr1));
return merged.toArray(new File[merged.size()]);
}
}
public void computeIndex() {
long ts = System.currentTimeMillis();
long entriesCount = 0;
long failedReadCount = 0;
try {
if (!cacheStore.isDirectory()) {
cacheStore.mkdirs();
}
File [] files;
synchronized(this) {
files = cacheStore.listFiles();
}
if(files == null) {
return;
}
int modifiedFiles = 0;
int locallyNewFiles = 0;
Map<String, Integer> locallyNewFolders = new HashMap<String, Integer>();
Map<String, Integer> modifiedFolders = new HashMap<String, Integer>();
for (int i = 0; i < files.length; i++) {
File file = files[i];
synchronized(this) {
if (file.getName().endsWith(".bin") == false) { // NOI18N
// on windows list returns already deleted .new files
continue;
}
boolean readFailed = false;
int itemIndex = -1;
DataInputStream dis = null;
try {
int retry = 0;
while (true) {
try {
dis = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));
break;
} catch (IOException ioex) {
retry++;
if (retry > 7) {
throw ioex;
}
Thread.sleep(retry * 30);
}
}
itemIndex = 0;
for (;;) {
++itemIndex;
int pathLen;
try {
pathLen = dis.readInt();
} catch (EOFException e) {
// reached EOF, no entry for this key
break;
}
dis.readInt();
String path = readChars(dis, pathLen);
Map value = readValue(dis, path);
for (Iterator j = value.keySet().iterator(); j.hasNext();) {
entriesCount++;
File f = (File) j.next();
FileInformation info = (FileInformation) value.get(f);
if((info.getStatus() & FileInformation.STATUS_VERSIONED_CONFLICT) != 0) {
conflictedIndex.add(f);
}
if ((info.getStatus() & STATUS_VALUABLE) != 0) {
index.add(f);
modifiedFiles++;
addModifiedFile(modifiedFolders, f);
if ((info.getStatus() & FileInformation.STATUS_NOTVERSIONED_NEWLOCALLY) != 0) {
locallyNewFiles++;
addLocallyNewFile(locallyNewFolders, info.isDirectory() ? f.getAbsolutePath() : f.getParent());
}
}
}
}
} catch (EOFException e) {
logCorruptedCacheFile(file, itemIndex, e);
readFailed = true;
} catch (Exception e) {
Subversion.LOG.log(Level.SEVERE, null, e);
} finally {
if (dis != null) try { dis.close(); } catch (IOException e) {}
}
if (readFailed) {
// cache file is corrupted, delete it (will be recreated on-demand later)
file.delete();
failedReadCount++;
}
}
}
if (locallyNewFiles > 1000) {
logTooManyNewFiles(locallyNewFolders, locallyNewFiles);
} else if (modifiedFiles > 5000) {
logTooManyModifications(modifiedFolders, modifiedFiles);
}
} finally {
Subversion.LOG.log(Level.INFO, "Finished indexing svn cache with {0} entries. Elapsed time: {1} ms.", new Object[]{entriesCount, System.currentTimeMillis() - ts});
if(failedReadCount > 0) {
Subversion.LOG.log(Level.INFO, " read failed {0} times.", failedReadCount);
}
}
}
@Override
public boolean recognizesAttribute(String name) {
return ATTR_STATUS_MAP.equals(name);
}
@Override
public boolean recognizesEntity(Object key) {
return key instanceof File;
}
@Override
public synchronized Object readEntry(Object key, String name, MemoryCache memoryCache) {
assert key instanceof File;
assert name != null;
boolean readFailed = false;
File dir = (File) key;
File store = getStore(dir);
if (!store.isFile()) {
return null;
}
String dirPath = dir.getAbsolutePath();
int dirPathLen = dirPath.length();
DataInputStream dis = null;
int itemIndex = -1;
try {
int retry = 0;
while (true) {
try {
dis = new DataInputStream(new BufferedInputStream(new FileInputStream(store)));
break;
} catch (IOException ioex) {
retry++;
if (retry > 7) {
throw ioex;
}
Thread.sleep(retry * 30);
}
}
itemIndex = 0;
for (;;) {
++itemIndex;
int pathLen;
try {
pathLen = dis.readInt();
} catch (EOFException e) {
// reached EOF, no entry for this key
break;
}
int mapLen = dis.readInt();
if (pathLen != dirPathLen) {
skip(dis, pathLen * 2 + mapLen);
} else {
String path = readChars(dis, pathLen);
if (dirPath.equals(path)) {
return readValue(dis, path);
} else {
skip(dis, mapLen);
}
}
}
} catch (EOFException e) {
logCorruptedCacheFile(store, itemIndex, e);
readFailed = true;
} catch (Exception e) {
Subversion.LOG.log(Level.INFO, e.getMessage(), e);
readFailed = true;
} finally {
if (dis != null) try { dis.close(); } catch (IOException e) {}
}
if (readFailed) store.delete(); // cache file is corrupted, delete it (will be recreated on-demand later)
return null;
}
@Override
public synchronized boolean writeEntry(Object key, String name, Object value) {
assert key instanceof File;
assert name != null;
if (value != null) {
if (!(value instanceof Map)) return false;
if (!isValuable(value)) value = null;
}
File dir = (File) key;
String dirPath = dir.getAbsolutePath();
int dirPathLen = dirPath.length();
File store = getStore(dir);
if (value == null && !store.exists()) return true;
File storeNew = new File(store.getParentFile(), store.getName() + ".new"); // NOI18N
if (!cacheStore.isDirectory()) {
cacheStore.mkdirs();
}
DataOutputStream oos = null;
DataInputStream dis = null;
boolean readFailed = false;
int itemIndex = -1;
try {
oos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(storeNew)));
if (value != null) {
writeEntry(oos, dirPath, value);
}
if (store.exists()) {
int retry = 0;
while (true) {
try {
dis = new DataInputStream(new BufferedInputStream(new FileInputStream(store)));
break;
} catch (IOException ioex) {
retry++;
if (retry > 7) {
throw ioex;
}
Thread.sleep(retry * 30);
}
}
itemIndex = 0;
for (;;) {
++itemIndex;
int pathLen;
try {
pathLen = dis.readInt();
} catch (EOFException e) {
break;
}
int mapLen = dis.readInt();
if (pathLen == dirPathLen) {
String path = readChars(dis, pathLen);
if (dirPath.equals(path)) {
skip(dis, mapLen);
} else {
oos.writeInt(pathLen);
oos.writeInt(mapLen);
oos.writeChars(path);
copyStreams(oos, dis, mapLen);
}
} else {
oos.writeInt(pathLen);
oos.writeInt(mapLen);
copyStreams(oos, dis, mapLen + pathLen * 2);
}
}
}
} catch (EOFException e) {
logCorruptedCacheFile(store, itemIndex, e);
readFailed = true;
} catch (FileNotFoundException ex) {
Subversion.LOG.log(Level.INFO, "File could not be created, check if you are running only a single instance of netbeans for this userdir", ex); //NOI18N
return true;
} catch (Exception e) {
Subversion.LOG.log(Level.INFO, "Copy: " + store.getAbsolutePath() + " to: " + storeNew.getAbsolutePath(), e); // NOI18N
return true;
} finally {
if (oos != null) try { oos.close(); } catch (IOException e) {}
if (dis != null) try { dis.close(); } catch (IOException e) {}
}
adjustIndex(dir, value);
if (readFailed) {
store.delete(); // cache file is corrupted, delete it (will be recreated on-demand later)
return true;
}
try {
FileUtils.renameFile(storeNew, store);
} catch (FileNotFoundException ex) {
Subversion.LOG.log(Level.INFO,
"File could not be renamed, check if you are running only a single instance of netbeans for this userdir", //NOI18N
ex);
} catch (IOException ex) {
Subversion.LOG.log(Level.SEVERE, null, ex);
}
return true;
}
private void adjustIndex(File dir, Object value) {
// the file must be a folder or must not exist
// adding existing file is forbidden
assert !dir.isFile();
Map map = (Map) value;
Set set = map != null ? map.keySet() : null;
// all modified files
Set<File> conflictedSet = new HashSet<File>();
Set<File> newSet = new HashSet<File>();
Set<File> ignoredSet = new HashSet<File>();
if(set != null) {
for (Iterator i = set.iterator(); i.hasNext();) {
File file = (File) i.next();
FileInformation info = (FileInformation) map.get(file);
// conflict
if((info.getStatus() & FileInformation.STATUS_VERSIONED_CONFLICT) != 0) {
conflictedSet.add(file);
}
if (info.getStatus() == FileInformation.STATUS_NOTVERSIONED_EXCLUDED) {
ignoredSet.add(file);
} else {
if ((info.getStatus() & FileInformation.STATUS_NOTVERSIONED_EXCLUDED) != 0) {
// this can hardly happen
assert false;
ignoredSet.add(file);
}
// all but uptodate
if((info.getStatus() & STATUS_VALUABLE) != 0) {
newSet.add(file);
}
}
}
}
index.add(dir, newSet);
ignoresIndex.add(dir, ignoredSet);
conflictedIndex.add(dir, conflictedSet);
}
/**
* Logs the EOFException and the corrupted cache file
* @param file file which caused the error
* @param itemIndex a position in the file when the error showed
* @param e
*/
private void logCorruptedCacheFile(File file, int itemIndex, EOFException e) {
try {
File tmpFile = File.createTempFile("svn_", ".bin");
Subversion.LOG.log(Level.INFO, "Corrupted cache file " + file.getAbsolutePath() + " at position " + itemIndex, e);
FileUtils.copyFile(file, tmpFile);
byte[] contents = FileUtils.getFileContentsAsByteArray(tmpFile);
Subversion.LOG.log(Level.INFO, "Corrupted cache file length: {0}", contents.length);
String encodedContent = Base64.getEncoder().encodeToString(contents); // log the file contents
Subversion.LOG.log(Level.INFO, "Corrupted cache file content:\n{0}\n", encodedContent);
Exception ex = new Exception("Corrupted cache file \"" + file.getAbsolutePath() + "\", please report in subversion module issues and attach "
+ tmpFile.getAbsolutePath() + " plus the IDE message log", e);
Subversion.LOG.log(Level.INFO, null, ex);
} catch (IOException ex) {
Subversion.LOG.log(Level.SEVERE, null, ex);
}
}
private void skip(InputStream is, long len) throws IOException {
while (len > 0) {
long n = is.skip(len);
if (n < 0) throw new EOFException("Missing " + len + " bytes."); // NOI18N
len -= n;
}
}
private String readChars(DataInputStream dis, int len) throws IOException {
if (len < 0 || len > 1024 * 1024 * 10) throw new EOFException("Len: " + len); // preventing from OOME
StringBuilder sb = new StringBuilder(len);
while (len-- > 0) {
sb.append(dis.readChar());
}
return sb.toString();
}
private Map<File, FileInformation> readValue(DataInputStream dis, String dirPath) throws IOException {
Map<File, FileInformation> map = new HashMap<File, FileInformation>();
int len = dis.readInt();
while (len-- > 0) {
int nameLen = dis.readInt();
String name = readChars(dis, nameLen);
File file = new File(dirPath, name);
int status = dis.readInt();
FileInformation info = new FileInformation(status & (DIRECTORY - 1), status > (DIRECTORY - 1));
map.put(file, info);
}
return map;
}
private void writeEntry(DataOutputStream dos, String dirPath, Object value) throws IOException {
Map map = (Map) value;
Set set = map.keySet();
ByteArrayOutputStream baos = new ByteArrayOutputStream(set.size() * 50);
DataOutputStream temp = new DataOutputStream(baos);
temp.writeInt(set.size());
for (Iterator i = set.iterator(); i.hasNext();) {
File file = (File) i.next();
FileInformation info = (FileInformation) map.get(file);
temp.writeInt(file.getName().length());
temp.writeChars(file.getName());
temp.writeInt(info.getStatus() + (info.isDirectory() ? DIRECTORY : 0));
}
temp.close();
byte [] valueBytes = baos.toByteArray();
dos.writeInt(dirPath.length());
dos.writeInt(valueBytes.length);
dos.writeChars(dirPath);
dos.write(valueBytes);
}
private boolean isValuable(Object value) {
Map map = (Map) value;
for (Iterator i = map.values().iterator(); i.hasNext();) {
FileInformation info = (FileInformation) i.next();
if ((info.getStatus() & STATUS_VALUABLE) != 0) return true;
}
return false;
}
private File getStore(File dir) {
String dirPath = dir.getAbsolutePath();
int dirHash = dirPath.hashCode();
return new File(cacheStore, Integer.toString(dirHash % 173 + 172) + ".bin"); // NOI18N
}
private void initCacheStore() {
cacheStore = Places.getCacheSubdirectory(CACHE_DIRECTORY);
}
private static void copyStreams(OutputStream out, InputStream in, int len) throws IOException {
byte [] buffer = new byte[4096];
int totalLen = len;
for (;;) {
int n = (len >= 0 && len <= 4096) ? len : 4096;
n = in.read(buffer, 0, n);
if (n < 0) throw new EOFException("Missing " + len + " bytes from total " + totalLen + " bytes."); // NOI18N
out.write(buffer, 0, n);
if ((len -= n) == 0) break;
}
out.flush();
}
private static CacheIndex createCacheIndex() {
return new CacheIndex() {
@Override
protected boolean isManaged(File file) {
return SvnUtils.isManaged(file);
}
};
}
private void addLocallyNewFile (Map<String, Integer> locallyNewFolders, String path) {
if (path == null) {
return;
}
boolean toAdd = true;
String toRemove = null;
Integer val = locallyNewFolders.get(path);
if (val != null) {
locallyNewFolders.put(path, val + 1);
return;
}
for (Map.Entry<String, Integer> e : locallyNewFolders.entrySet()) {
if (path.startsWith(e.getKey() + File.separator)) {
e.setValue(e.getValue() + 1);
toAdd = false;
break;
} else if (e.getKey().startsWith(path + File.separator)) {
toRemove = e.getKey();
break;
}
}
if (toRemove != null) {
locallyNewFolders.put(path, locallyNewFolders.remove(toRemove));
} else if (toAdd) {
locallyNewFolders.put(path, 1);
}
}
private void addModifiedFile (Map<String, Integer> modifiedFolders, File file) {
if (logModifiedFiles) {
File topmost = Subversion.getInstance().getTopmostManagedAncestor(file);
if (topmost != null) {
String path = topmost.getAbsolutePath();
Integer val = modifiedFolders.get(path);
if (val == null) {
modifiedFolders.put(path, 1);
} else {
modifiedFolders.put(path, val + 1);
}
}
}
}
@NbBundle.Messages({
"# {0} - number of changes", "# {1} - the biggest unversioned folders",
"MSG_FileStatusCache.cacheTooBig.newFiles.text=Subversion cache contains {0} locally new (uncommitted) files. "
+ "That many uncommitted files may cause performance problems when accessing the working copy. "
+ "You should consider committing or permanently ignoring these files. "
+ "Candidates for ignoring are: {1}",
"# {0} - folder path", "# {1} - number of contained unversioned files",
"MSG_FileStatusCache.cacheTooBig.ignoreCandidate={0}: {1} new files"
})
private void logTooManyNewFiles (Map<String, Integer> locallyNewFolders, int locallyNewFiles) {
Map<Integer, List<String>> sortedFolders = sortFolders(locallyNewFolders);
List<String> biggestFolders = new ArrayList<String>(3);
outer: for (Map.Entry<Integer, List<String>> e : sortedFolders.entrySet()) {
for (String folder : e.getValue()) {
biggestFolders.add(Bundle.MSG_FileStatusCache_cacheTooBig_ignoreCandidate(folder, e.getKey()));
if (biggestFolders.size() == 3) {
break outer;
}
}
}
LOG.log(Level.WARNING, Bundle.MSG_FileStatusCache_cacheTooBig_newFiles_text(locallyNewFiles, biggestFolders));
}
@NbBundle.Messages({
"# {0} - number of changes", "# {1} - checkouts with the highest number of modifications",
"MSG_FileStatusCache.cacheTooBig.text=Subversion cache contains {0} locally modified files. "
+ "That many uncommitted files may cause performance problems when accessing the working copy. "
+ "You should consider committing or reverting these changes. "
+ "Checkouts: {1}",
"# {0} - number of changes",
"MSG_FileStatusCache.cacheTooBig.text.unknownFiles=Subversion cache contains {0} locally modified files. "
+ "That many uncommitted files may cause performance problems when accessing the working copy. "
+ "Run the IDE with -J-Dversioning.subversion.turbo.logModifiedFiles=true to know the exact checkout causing the problem.",
"# {0} - checkout folder", "# {1} - number of contained modified files",
"MSG_FileStatusCache.cacheTooBig.checkoutWithModifications={0}: {1} modifications"
})
private void logTooManyModifications (Map<String, Integer> modifiedFolders, int modifiedFiles) {
Map<Integer, List<String>> sortedFolders = sortFolders(modifiedFolders);
List<String> biggestFolders = new ArrayList<String>(3);
outer: for (Map.Entry<Integer, List<String>> e : sortedFolders.entrySet()) {
for (String folder : e.getValue()) {
biggestFolders.add(Bundle.MSG_FileStatusCache_cacheTooBig_checkoutWithModifications(folder, e.getKey()));
if (biggestFolders.size() == 3) {
break outer;
}
}
}
if (logModifiedFiles) {
LOG.log(Level.WARNING, Bundle.MSG_FileStatusCache_cacheTooBig_text(modifiedFiles, biggestFolders));
} else {
LOG.log(Level.WARNING, Bundle.MSG_FileStatusCache_cacheTooBig_text_unknownFiles(modifiedFiles));
}
}
private static Map<Integer, List<String>> sortFolders (Map<String, Integer> unsortedFolders) {
Map<Integer, List<String>> sortedFolders = new TreeMap<Integer, List<String>>(new Comparator<Integer>() {
@Override
public int compare (Integer o1, Integer o2) {
return - o1.compareTo(o2);
}
});
for (Map.Entry<String, Integer> e : unsortedFolders.entrySet()) {
List<String> folders = sortedFolders.get(e.getValue());
if (folders == null) {
sortedFolders.put(e.getValue(), folders = new ArrayList<String>());
}
folders.add(e.getKey());
}
return sortedFolders;
}
}