package core.benchmark; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.List; import core.Config; import dataStructures.Pair; import utils.io.DirectoryMonitor; import utils.io.FileUtils; import utils.io.MonitoredFile; // All things considered, this doesn't need to be particularly efficient since anything // less than 10ms of search time won't be noticable to an end user really, not for // a networked application that's not performance bound. public class BenchmarkManager { // Variables public final String directory = Config.AssetDirectory + "userbench/"; public final String extension = ".csv"; public final String lineDelimiter = "\n"; private List raw; private DirectoryMonitor directoryMonitor; private boolean needsUpdate; // Read in all extensions public BenchmarkManager() { long start = Instant.now().toEpochMilli(); raw = new ArrayList(); directoryMonitor = new DirectoryMonitor(directory); needsUpdate = false; rebuildLookup(); BenchmarkLog.log("Took " + (Instant.now().toEpochMilli() - start) + " ms to load " + raw.size() + " entries from " + directoryMonitor.getCurrentFiles().size() + " file" + (raw.size() > 1 ? "s" : "")); } // Rebuilds the files being monitored public void rebuildLookup() { long start = Instant.now().toEpochMilli(); synchronized(raw) { raw.clear(); List files; synchronized(directoryMonitor) { files = directoryMonitor.getCurrentFiles(); } if(files != null) { for(MonitoredFile mf : files) { String contents = FileUtils.readContent(mf.path); String[] lines = contents.split(lineDelimiter); for(int i = 1; i < lines.length; ++i) raw.add(new BenchmarkEntry(lines[i])); } } else { BenchmarkLog.warn("No " + extension + " files where found in " + directory); } } long end = Instant.now().toEpochMilli(); BenchmarkLog.log("Rebuilt data in " + (end - start) + "ms"); } // Re-evaluates and re-orders the input list based on the Levenshtein distance of the // original search and the contents of the provided list of benchmark entries. public List evaluateLevenshteinDistance(List entries, String search) { // Populate cost list with heuristic results List> cost = new ArrayList>(); int bestSoFar = Integer.MAX_VALUE; for(int i = 0; i < entries.size(); ++i) { BenchmarkEntry entry = entries.get(i); String entryString = entry.brand + " " + entry.model; int heuristic = levenshteinHeuristic(entryString, search, bestSoFar); if(heuristic < bestSoFar) bestSoFar = heuristic; cost.add(new Pair(entry, heuristic)); } // Sort list Collections.sort(cost, (i1, i2) -> i1.Second.compareTo(i2.Second)); // Build new output list List sortedEntries = new ArrayList(); for(int i = 0; i < cost.size(); ++i) sortedEntries.add(cost.get(i).First); return sortedEntries; } // Finds the minimum integer in an array of ints and returns it. private int min(int[] arr) { int min = Integer.MAX_VALUE; for(int i = 0; i < arr.length; ++i) { if(arr[i] < min) min = arr[i]; } return min; } // Returns cost based on distance of characters from string. This is a kinda sloppy way // to do it, but because I keep tabs on the best result so far, it could be much worse. // Still chucks a lot - a non-recursive result w/ memoization would be best but this will do. private int levenshteinHeuristic(String str1, String str2, int bestSoFar) { int cost; if(str1.length() <= 0) return str2.length(); if(str2.length() <= 0) return str1.length(); if(str1.charAt(0) == str2.charAt(0)) cost = 0; else cost = 1; int distance = Math.abs(str1.length() - str2.length()); if(distance > bestSoFar) return distance; int s1 = levenshteinHeuristic(str1.substring(1), str2, bestSoFar) + 1; int s2 = levenshteinHeuristic(str1, str2.substring(1), bestSoFar) + 1; int s3 = levenshteinHeuristic(str1.substring(1), str2.substring(1), bestSoFar) + cost; return min(new int[] {s1, s2, s3 }); } // Search for a substring in the model name public List findModel(String modelSubstr) { long start = Instant.now().toEpochMilli(); List matching = new ArrayList(); String searchSubstr = modelSubstr.toLowerCase(); for(BenchmarkEntry e : raw) { String model = e.model.toLowerCase().trim(); if(model.contains(searchSubstr)) matching.add(e); } BenchmarkLog.log("Searched for '" + searchSubstr + "' for "+ (Instant.now().toEpochMilli() - start) + "ms and found " + matching.size() + " entries."); return matching; } // Keeps tabs on any changes of the files. public void update() { needsUpdate = false; directoryMonitor.update(this::onRescan, this::onRescan, this::onRescan); if(needsUpdate) rebuildLookup(); } // When a file is changed, handle it. private void onRescan(MonitoredFile file) { // For now, all we need is to note that something was adjusted. if(file.path.toString().contains(extension)) { BenchmarkLog.log("File status changed: " + file.path); needsUpdate = true; } } }