Skip to content

Commit ae65065

Browse files
committed
Implement multithreading
1 parent 8ebedfc commit ae65065

14 files changed

+350
-279
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ java -jar mc-resource-analyzer-x.x.x.jar [-hHmsStV] [-B=PATH] [-M=PATH] [-o=STRI
3131
- `-T`, `--table-template`: When used in conjunction with `table`, the generated table will replace any instances of the string `{{{TABLE}}}` in a copy of the template file. Note that the table will not include `<table></table>` tags when using this argument.
3232
- `-s`, `--statistics`: Outputs a file with statistics about the analysis.
3333
- `-o`, `--output-prefix`: Use this argument to add a prefix to the program's output files. For example, using `-o abc` would result in the files `abc.csv` and `abc_table.html`.
34-
- `-v`, `--version-select`: Use this argument if you want to analyze a world that was not generated with the latest version of Minecraft. Selecting a version that does not match the version in which the regions were generated may result in unexpected behavior. The following versions are supported:
34+
- `-v`, `--version-select`: Use this argument if you want to analyze a world that was not generated with the latest version of Minecraft. Selecting a version that does not match the version with which the regions were generated may result in unexpected behavior. The following versions are supported:
3535
- `ANVIL_118` for 1.18
3636
- `ANVIL_2021` for 1.16 to 1.17
3737
- `ANVIL_2018` for 1.13 to 1.15
@@ -43,6 +43,7 @@ java -jar mc-resource-analyzer-x.x.x.jar [-hHmsStV] [-B=PATH] [-M=PATH] [-o=STRI
4343
- `-B`, `--block-ids`: When using the `--modernize-ids` option on a world with block IDs outside the range of 0-255, use this to specify the path to a file containing block IDs in the same format as [blocks.properties](https://github.com/Meeples10/MCResourceAnalyzer/blob/master/src/main/resources/blocks.properties).
4444
- `-M`, `--merge-ids`: When analyzing a world with block IDs outside the range of 0-255, use this to specify the path to a file containing block IDs in the same format as [merge.properties](https://github.com/Meeples10/MCResourceAnalyzer/blob/master/src/main/resources/merge.properties). Any block with an ID listed in this file will have all of its variants merged into a single value.
4545
- `-H`, `--no-hack`: The program attempts to compensate for the aforementioned inaccuracies at high Y values by assuming that empty chunk sections are filled with air. Use this argument to disable this hack.
46+
- `-n`, `--num-threads` (default: `8`): The maximum number of threads to use for analysis.
4647
- `-S`, `--silent`: Prevents the program from printing output, other than errors. This may result in marginally improved performance.
4748

4849
### Version compatibility
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.github.meeples10.mcresourceanalyzer;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
public class Analysis {
7+
public Map<String, Long> blocks = new HashMap<String, Long>();
8+
public Map<String, HashMap<Integer, Long>> heights = new HashMap<String, HashMap<Integer, Long>>();
9+
}

src/main/java/io/github/meeples10/mcresourceanalyzer/AnalyzerThread.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package io.github.meeples10.mcresourceanalyzer;
22

3-
public abstract class AnalyzerThread implements Runnable {
4-
Region region;
5-
Chunk chunk;
3+
import java.util.HashSet;
4+
import java.util.Set;
65

7-
public AnalyzerThread(Region region, Chunk chunk) {
8-
this.region = region;
9-
this.chunk = chunk;
6+
public abstract class AnalyzerThread extends Thread {
7+
final Region r;
8+
final Set<Analysis> analyses = new HashSet<>();
9+
10+
public AnalyzerThread(Region region) {
11+
r = region;
12+
setName(r.name);
1013
}
1114

1215
public abstract void run();
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
package io.github.meeples10.mcresourceanalyzer;
22

3-
public record Chunk(int x, int z) {}
3+
record Chunk(int x, int z) {}

src/main/java/io/github/meeples10/mcresourceanalyzer/Main.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import picocli.CommandLine.ParseResult;
2626

2727
public class Main {
28-
public static final int THREAD_COUNT = 8;
2928
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("dd MMM yyyy 'at' hh:mm:ss a zzz");
3029
public static final FilenameFilter DS_STORE_FILTER = new FilenameFilter() {
3130
@Override
@@ -46,6 +45,7 @@ public boolean accept(File dir, String name) {
4645
static String outputPrefix = "";
4746
static String tableTemplatePath = "";
4847
static String tableTemplate = "";
48+
static int numThreads = 8;
4949

5050
public static void main(String[] args) {
5151
CommandLine commandLine = new CommandLine(createCommandSpec());
@@ -124,19 +124,21 @@ private static CommandSpec createCommandSpec() {
124124
"The program attempts to compensate for inaccuracies at high Y values by assuming that empty chunk"
125125
+ "sections are filled with air. Use this option to disable this hack.")
126126
.build());
127+
spec.addOption(OptionSpec.builder("-n", "--num-threads").paramLabel("COUNT").type(int.class)
128+
.description("The maximum number of threads to use for analysis. (default: 8)").build());
127129
spec.addPositional(PositionalParamSpec.builder().paramLabel("INPUT").arity("0..1").type(String.class)
128-
.description("The region directory or .mclevel file to analyze.").build());
130+
.description("The region directory or .mclevel file to analyze. (default: 'region')").build());
129131
return spec;
130132
}
131133

132134
private static int parseArgs(ParseResult pr) {
135+
if(pr.hasMatchedPositional(0)) inputFile = new File((String) pr.matchedPositional(0).getValue());
133136
saveStatistics = pr.hasMatchedOption('s');
134-
allowHack = pr.hasMatchedOption('H');
137+
allowHack = !pr.hasMatchedOption('H');
135138
generateTable = pr.hasMatchedOption('t');
136139
modernizeIDs = pr.hasMatchedOption('m');
137140
silent = pr.hasMatchedOption('S');
138141
if(pr.hasMatchedOption('v')) selectedVersion = pr.matchedOption('v').getValue();
139-
if(pr.hasMatchedPositional(0)) inputFile = new File((String) pr.matchedPositional(0).getValue());
140142
if(pr.hasMatchedOption('o')) outputPrefix = pr.matchedOption('o').getValue();
141143
if(pr.hasMatchedOption('B')) {
142144
try {
@@ -173,6 +175,9 @@ private static int parseArgs(ParseResult pr) {
173175
System.exit(1);
174176
}
175177
}
178+
if(pr.hasMatchedOption('n')) {
179+
numThreads = pr.matchedOption('n').getValue();
180+
}
176181
return 0;
177182
}
178183

@@ -283,4 +288,8 @@ public static void print(Object s) {
283288
public static void println(Object s) {
284289
if(!silent) System.out.println(s);
285290
}
291+
292+
public static void printf(String format, Object... args) {
293+
if(!silent) System.out.printf(format, args);
294+
}
286295
}

src/main/java/io/github/meeples10/mcresourceanalyzer/Region.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import java.util.HashSet;
55
import java.util.Set;
66

7-
public class Region {
7+
class Region {
88
public final RegionFile file;
99
public final String name;
1010
public final Set<Chunk> chunks = new HashSet<>();

src/main/java/io/github/meeples10/mcresourceanalyzer/RegionAnalyzer.java

Lines changed: 72 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,28 @@
44
import java.io.IOException;
55
import java.util.ArrayList;
66
import java.util.Comparator;
7+
import java.util.HashMap;
8+
import java.util.HashSet;
79
import java.util.LinkedHashMap;
810
import java.util.List;
911
import java.util.Map;
10-
import java.util.concurrent.ConcurrentHashMap;
12+
import java.util.Set;
13+
import java.util.concurrent.ExecutorService;
14+
import java.util.concurrent.Executors;
15+
import java.util.concurrent.TimeUnit;
1116
import java.util.concurrent.atomic.AtomicInteger;
1217
import java.util.stream.Collectors;
1318

1419
public abstract class RegionAnalyzer {
1520
private Version version;
16-
public long chunkCount = 0;
17-
public Map<String, Long> blockCounter = new ConcurrentHashMap<String, Long>();
18-
public Map<String, ConcurrentHashMap<Integer, Long>> heightCounter = new ConcurrentHashMap<String, ConcurrentHashMap<Integer, Long>>();
21+
public int chunkCount = 0;
22+
public Map<String, Long> blockCounter = new HashMap<String, Long>();
23+
public Map<String, HashMap<Integer, Long>> heightCounter = new HashMap<String, HashMap<Integer, Long>>();
1924
private long firstStartTime;
2025
public long duration;
2126
List<Region> regions = new ArrayList<>();
22-
int maxThreads = Main.THREAD_COUNT;
23-
private AtomicInteger complete = new AtomicInteger(0);
27+
Set<AnalyzerThread> threads = new HashSet<>();
28+
private AtomicInteger completed = new AtomicInteger(0);
2429

2530
public RegionAnalyzer() {
2631
firstStartTime = System.currentTimeMillis();
@@ -32,16 +37,52 @@ public void setVersion(Version version) {
3237

3338
public void run(File input) {
3439
validateInput(input);
40+
41+
Main.print("Scanning for chunks... ");
3542
findChunks(input);
3643

3744
for(Region r : regions) {
3845
chunkCount += r.size();
3946
}
40-
System.out.printf("%d region%s, %d chunk%s found\n", regions.size(), regions.size() == 1 ? "s" : "", chunkCount,
41-
chunkCount == 1 ? "s" : "");
42-
maxThreads = Math.min(Main.THREAD_COUNT, regions.size());
47+
Main.printf("%d region%s, %d chunk%s found\n", regions.size(), regions.size() == 1 ? "" : "s", chunkCount,
48+
chunkCount == 1 ? "" : "s");
4349

44-
analyze(input);
50+
analyze();
51+
52+
if(threads.size() > 0) {
53+
System.out.println();
54+
ExecutorService pool = Executors.newFixedThreadPool(Math.min(Main.numThreads, 1024));
55+
for(AnalyzerThread t : threads) {
56+
pool.submit(t);
57+
}
58+
pool.shutdown();
59+
try {
60+
pool.awaitTermination(100, TimeUnit.DAYS);
61+
} catch(InterruptedException e) {
62+
e.printStackTrace();
63+
}
64+
for(AnalyzerThread t : threads) {
65+
for(Analysis a : t.analyses) {
66+
for(String s : a.blocks.keySet()) {
67+
blockCounter.put(s, blockCounter.getOrDefault(s, 0L) + a.blocks.get(s));
68+
}
69+
for(String s : a.heights.keySet()) {
70+
if(heightCounter.containsKey(s)) {
71+
Map<Integer, Long> existing = heightCounter.get(s);
72+
Map<Integer, Long> heights = a.heights.get(s);
73+
for(int i : heights.keySet()) {
74+
existing.put(i, existing.getOrDefault(i, 0L) + heights.get(i));
75+
}
76+
} else {
77+
heightCounter.put(s, a.heights.get(s));
78+
}
79+
}
80+
}
81+
}
82+
}
83+
84+
duration = System.currentTimeMillis() - getStartTime();
85+
Main.println("Completed analysis in " + Main.millisToHMS(duration));
4586

4687
long totalBlocks = 0L;
4788
for(String key : blockCounter.keySet()) {
@@ -72,12 +113,7 @@ public int compare(String arg0, String arg1) {
72113
data.append(",");
73114
}
74115
data.append("total,percent_of_total,percent_excluding_air\n");
75-
int digits = String.valueOf(blockCounter.size()).length();
76-
String completionFormat = "[%0" + digits + "d/%0" + digits + "d]";
77-
int keyIndex = 0;
78116
for(String key : heightCounter.keySet()) {
79-
keyIndex += 1;
80-
Main.print("\rGenerating CSV... " + String.format(completionFormat, keyIndex, blockCounter.size()));
81117
data.append(Main.modernizeIDs ? Main.getStringID(key) : key);
82118
data.append(",");
83119
for(int i = minY; i <= maxY; i++) {
@@ -100,10 +136,11 @@ public int compare(String arg0, String arg1) {
100136
}
101137
data.append("\n");
102138
}
139+
Main.println("Done");
103140
try {
104141
File out = new File(Main.getOutputPrefix() + ".csv");
105142
Main.writeStringToFile(out, data.toString());
106-
Main.println("\nData written to " + out.getAbsolutePath());
143+
Main.println("CSV written to " + out.getAbsolutePath());
107144
} catch(IOException e) {
108145
e.printStackTrace();
109146
System.exit(1);
@@ -134,13 +171,20 @@ public int compare(String arg0, String arg1) {
134171

135172
public abstract void findChunks(File input);
136173

137-
public abstract void analyze(File input);
174+
public abstract void analyze();
175+
176+
public synchronized void updateProgress() {
177+
int c = completed.incrementAndGet() + 1;
178+
if(c > chunkCount) return;
179+
Main.print("Analyzing chunks [" + c + "/" + chunkCount + "]\r");
180+
if(c == chunkCount) System.out.println("\n");
181+
}
138182

139-
public void updateProgress() {
140-
int c = complete.incrementAndGet();
141-
if(c + 1 == regions.size()) {
142-
System.out.println("DONE");
183+
public synchronized void halt() {
184+
for(AnalyzerThread t : threads) {
185+
t.interrupt();
143186
}
187+
System.exit(1);
144188
}
145189

146190
public String generateTable(double totalBlocks, double totalExcludingAir) {
@@ -209,17 +253,17 @@ static boolean mergeStates(byte id) {
209253
}
210254

211255
/* THIS IS A HACK TO ACCOUNT FOR NONEXISTENT SECTIONS AT HIGH Y VALUES */
212-
void airHack(int sectionY, String airID) {
256+
static void airHack(Analysis a, int sectionY, String airID) {
213257
if(Main.allowHack && sectionY < 15) {
214-
if(!blockCounter.containsKey(airID)) blockCounter.put(airID, 0L);
215-
if(!heightCounter.containsKey(airID)) heightCounter.put(airID, new ConcurrentHashMap<Integer, Long>());
258+
if(!a.blocks.containsKey(airID)) a.blocks.put(airID, 0L);
259+
if(!a.heights.containsKey(airID)) a.heights.put(airID, new HashMap<Integer, Long>());
216260
for(; sectionY < 16; sectionY++) {
217-
blockCounter.put(airID, blockCounter.get(airID) + 4096L);
261+
a.blocks.put(airID, a.blocks.get(airID) + 4096L);
218262
for(int y = sectionY * 16; y < sectionY * 16 + 16; y++) {
219-
if(heightCounter.get(airID).containsKey(y)) {
220-
heightCounter.get(airID).put(y, heightCounter.get(airID).get(y) + 256L);
263+
if(a.heights.get(airID).containsKey(y)) {
264+
a.heights.get(airID).put(y, a.heights.get(airID).get(y) + 256L);
221265
} else {
222-
heightCounter.get(airID).put(y, 256L);
266+
a.heights.get(airID).put(y, 256L);
223267
}
224268
}
225269
}

src/main/java/io/github/meeples10/mcresourceanalyzer/RegionAnalyzerAlpha.java

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
import java.io.File;
44
import java.io.FileInputStream;
55
import java.util.ArrayList;
6+
import java.util.HashMap;
67
import java.util.List;
7-
import java.util.concurrent.ConcurrentHashMap;
88

99
import net.minecraft.nbt.CompressedStreamTools;
1010
import net.minecraft.nbt.NBTTagCompound;
1111

1212
public class RegionAnalyzerAlpha extends RegionAnalyzer {
13+
final List<File> chunkFiles = new ArrayList<>();
14+
private Analysis a = new Analysis();
1315

1416
@Override
1517
public void validateInput(File world) {
@@ -20,13 +22,7 @@ public void validateInput(File world) {
2022
}
2123

2224
@Override
23-
public void findChunks(File input) {
24-
// TODO
25-
}
26-
27-
@Override
28-
public void analyze(File world) {
29-
List<File> chunkFiles = new ArrayList<>();
25+
public void findChunks(File world) {
3026
for(File f : world.listFiles(Main.DS_STORE_FILTER)) {
3127
if(!f.isDirectory()) continue;
3228
chunkFiles.addAll(traverseSubdirectories(f));
@@ -36,24 +32,23 @@ public void analyze(File world) {
3632
System.exit(1);
3733
}
3834
Main.println(chunkFiles.size() + " chunks found");
39-
int cnum = 1;
35+
}
36+
37+
@Override
38+
public void analyze() {
39+
int i = 1;
4040
for(File f : chunkFiles) {
41-
long startTime = System.currentTimeMillis();
42-
String name = f.getName();
43-
System.out.print("Scanning chunk " + name + " [" + cnum + "/" + chunkFiles.size() + "]... ");
41+
System.out.print("Scanning chunks [" + i + "/" + chunkFiles.size() + "]\r");
4442

4543
try {
4644
processChunk(f);
4745
} catch(Exception e) {
4846
e.printStackTrace();
4947
}
50-
51-
Main.println(
52-
"Done (" + String.format("%.2f", (double) (System.currentTimeMillis() - startTime) / 1000) + "s)");
53-
cnum++;
48+
i++;
5449
}
55-
duration = System.currentTimeMillis() - getStartTime();
56-
Main.println(("Completed analysis in " + Main.millisToHMS(duration) + " (" + chunkCount + " chunks)"));
50+
blockCounter.putAll(a.blocks);
51+
heightCounter.putAll(a.heights);
5752
}
5853

5954
private void processChunk(File chunkFile) throws Exception {
@@ -88,18 +83,18 @@ private void analyzeChunk(byte[] blocks, byte[] data) {
8883
: Byte.toString(blockID) + ":" + Byte.toString(blockData);
8984
}
9085

91-
if(blockCounter.containsKey(blockName)) {
92-
blockCounter.put(blockName, blockCounter.get(blockName) + 1L);
86+
if(a.blocks.containsKey(blockName)) {
87+
a.blocks.put(blockName, a.blocks.get(blockName) + 1L);
9388
} else {
94-
blockCounter.put(blockName, 1L);
89+
a.blocks.put(blockName, 1L);
9590
}
96-
if(!heightCounter.containsKey(blockName)) {
97-
heightCounter.put(blockName, new ConcurrentHashMap<Integer, Long>());
91+
if(!a.heights.containsKey(blockName)) {
92+
a.heights.put(blockName, new HashMap<Integer, Long>());
9893
}
99-
if(heightCounter.get(blockName).containsKey(y)) {
100-
heightCounter.get(blockName).put(y, heightCounter.get(blockName).get(y) + 1L);
94+
if(a.heights.get(blockName).containsKey(y)) {
95+
a.heights.get(blockName).put(y, a.heights.get(blockName).get(y) + 1L);
10196
} else {
102-
heightCounter.get(blockName).put(y, 1L);
97+
a.heights.get(blockName).put(y, 1L);
10398
}
10499
}
105100
}

0 commit comments

Comments
 (0)