IcuMemoryUsage.java (9992B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import java.io.*; 6 import java.nio.charset.StandardCharsets; 7 import java.util.*; 8 import java.util.regex.*; 9 import java.util.stream.Collectors; 10 11 /** 12 * Java program to estimate the memory usage of ICU objects (bug 1585536). 13 * 14 * It computes for each Intl constructor the amount of allocated memory. We're 15 * currently using the maximum memory ("max" in the output) to estimate the 16 * memory consumption of ICU objects. 17 * 18 * Insert before {@code JS_InitWithFailureDiagnostic} in "js.cpp": 19 * 20 * <pre> 21 * <code> 22 * JS_SetICUMemoryFunctions( 23 * [](const void*, size_t size) { 24 * void* ptr = malloc(size); 25 * if (ptr) { 26 * printf(" alloc: %p -> %zu\n", ptr, size); 27 * } 28 * return ptr; 29 * }, 30 * [](const void*, void* p, size_t size) { 31 * void* ptr = realloc(p, size); 32 * if (p) { 33 * printf(" realloc: %p -> %p -> %zu\n", p, ptr, size); 34 * } else { 35 * printf(" alloc: %p -> %zu\n", ptr, size); 36 * } 37 * return ptr; 38 * }, 39 * [](const void*, void* p) { 40 * if (p) { 41 * printf(" free: %p\n", p); 42 * } 43 * free(p); 44 * }); 45 * </code> 46 * </pre> 47 * 48 * Run this script with: 49 * {@code java IcuMemoryUsage.java $MOZ_JS_SHELL}. 50 */ 51 @SuppressWarnings("preview") 52 public class IcuMemoryUsage { 53 private enum Phase { 54 None, Create, Init, Destroy, Collect, Quit 55 } 56 57 private static final class Memory { 58 private Phase phase = Phase.None; 59 private HashMap<Long, Map.Entry<Phase, Long>> allocations = new HashMap<>(); 60 private HashSet<Long> freed = new HashSet<>(); 61 private HashMap<Long, Map.Entry<Phase, Long>> completeAllocations = new HashMap<>(); 62 private int allocCount = 0; 63 private ArrayList<Long> allocSizes = new ArrayList<>(); 64 65 void transition(Phase nextPhase) { 66 assert phase.ordinal() + 1 == nextPhase.ordinal() || (phase == Phase.Collect && nextPhase == Phase.Create); 67 phase = nextPhase; 68 69 // Create a clean slate when starting a new create cycle or before termination. 70 if (phase == Phase.Create || phase == Phase.Quit) { 71 transferAllocations(); 72 } 73 74 // Only measure the allocation size when creating the second object with the 75 // same locale. 76 if (phase == Phase.Collect && ++allocCount % 2 == 0) { 77 long size = allocations.values().stream().map(Map.Entry::getValue).reduce(0L, (a, c) -> a + c); 78 allocSizes.add(size); 79 } 80 } 81 82 void transferAllocations() { 83 completeAllocations.putAll(allocations); 84 completeAllocations.keySet().removeAll(freed); 85 allocations.clear(); 86 freed.clear(); 87 } 88 89 void alloc(long ptr, long size) { 90 allocations.put(ptr, Map.entry(phase, size)); 91 } 92 93 void realloc(long oldPtr, long newPtr, long size) { 94 free(oldPtr); 95 allocations.put(newPtr, Map.entry(phase, size)); 96 } 97 98 void free(long ptr) { 99 if (allocations.remove(ptr) == null) { 100 freed.add(ptr); 101 } 102 } 103 104 LongSummaryStatistics statistics() { 105 return allocSizes.stream().collect(Collectors.summarizingLong(Long::valueOf)); 106 } 107 108 double percentile(double p) { 109 var size = allocSizes.size(); 110 return allocSizes.stream().sorted().skip((long) ((size - 1) * p)).limit(2 - size % 2) 111 .mapToDouble(Long::doubleValue).average().getAsDouble(); 112 } 113 114 long persistent() { 115 return completeAllocations.values().stream().map(Map.Entry::getValue).reduce(0L, (a, c) -> a + c); 116 } 117 } 118 119 private static long parseSize(Matcher m, int group) { 120 return Long.parseLong(m.group(group), 10); 121 } 122 123 private static long parsePointer(Matcher m, int group) { 124 return Long.parseLong(m.group(group), 16); 125 } 126 127 private static void measure(String exec, String constructor, String description, String initializer) throws IOException { 128 var pb = new ProcessBuilder(exec, "--file=-", "--", constructor, initializer); 129 var process = pb.start(); 130 131 try (var writer = new BufferedWriter( 132 new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) { 133 writer.write(sourceCode); 134 writer.flush(); 135 } 136 137 var memory = new Memory(); 138 139 try (var reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { 140 var reAlloc = Pattern.compile("\\s+alloc: 0x(\\p{XDigit}+) -> (\\p{Digit}+)"); 141 var reRealloc = Pattern.compile("\\s+realloc: 0x(\\p{XDigit}+) -> 0x(\\p{XDigit}+) -> (\\p{Digit}+)"); 142 var reFree = Pattern.compile("\\s+free: 0x(\\p{XDigit}+)"); 143 144 String line; 145 while ((line = reader.readLine()) != null) { 146 Matcher m; 147 if ((m = reAlloc.matcher(line)).matches()) { 148 var ptr = parsePointer(m, 1); 149 var size = parseSize(m, 2); 150 memory.alloc(ptr, size); 151 } else if ((m = reRealloc.matcher(line)).matches()) { 152 var oldPtr = parsePointer(m, 1); 153 var newPtr = parsePointer(m, 2); 154 var size = parseSize(m, 3); 155 memory.realloc(oldPtr, newPtr, size); 156 } else if ((m = reFree.matcher(line)).matches()) { 157 var ptr = parsePointer(m, 1); 158 memory.free(ptr); 159 } else { 160 memory.transition(Phase.valueOf(line)); 161 } 162 } 163 } 164 165 try (var errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { 166 String line; 167 while ((line = errorReader.readLine()) != null) { 168 System.err.println(line); 169 } 170 } 171 172 var stats = memory.statistics(); 173 174 System.out.printf("%s%n", description); 175 System.out.printf(" max: %d%n", stats.getMax()); 176 System.out.printf(" min: %d%n", stats.getMin()); 177 System.out.printf(" avg: %.0f%n", stats.getAverage()); 178 System.out.printf(" 50p: %.0f%n", memory.percentile(0.50)); 179 System.out.printf(" 75p: %.0f%n", memory.percentile(0.75)); 180 System.out.printf(" 85p: %.0f%n", memory.percentile(0.85)); 181 System.out.printf(" 95p: %.0f%n", memory.percentile(0.95)); 182 System.out.printf(" 99p: %.0f%n", memory.percentile(0.99)); 183 System.out.printf(" mem: %d%n", memory.persistent()); 184 185 memory.transferAllocations(); 186 assert memory.persistent() == 0 : String.format("Leaked %d bytes", memory.persistent()); 187 } 188 189 public static void main(String[] args) throws IOException { 190 if (args.length == 0) { 191 throw new RuntimeException("The first argument must point to the SpiderMonkey shell executable"); 192 } 193 194 record Entry (String constructor, String description, String initializer) { 195 public static Entry of(String constructor, String description, String initializer) { 196 return new Entry(constructor, description, initializer); 197 } 198 199 public static Entry of(String constructor, String initializer) { 200 return new Entry(constructor, constructor, initializer); 201 } 202 } 203 204 var objects = new ArrayList<Entry>(); 205 objects.add(Entry.of("Intl.Collator", "o.compare('a', 'b')")); 206 objects.add(Entry.of("Intl.DateTimeFormat", "DateTimeFormat (UDateFormat)", "o.format(0)")); 207 objects.add(Entry.of("Intl.DateTimeFormat", "DateTimeFormat (UDateFormat+UDateIntervalFormat)", 208 "o.formatRange(0, 24*60*60*1000)")); 209 objects.add(Entry.of("Intl.DisplayNames", "o.of('en')")); 210 objects.add(Entry.of("Intl.ListFormat", "o.format(['a', 'b'])")); 211 objects.add(Entry.of("Intl.NumberFormat", "o.format(0)")); 212 objects.add(Entry.of("Intl.NumberFormat", "NumberFormat (UNumberRangeFormatter)", 213 "o.formatRange(0, 1000)")); 214 objects.add(Entry.of("Intl.PluralRules", "o.select(0)")); 215 objects.add(Entry.of("Intl.RelativeTimeFormat", "o.format(0, 'hour')")); 216 objects.add(Entry.of("Temporal.TimeZone", "o.getNextTransition(new Temporal.Instant(0n))")); 217 218 for (var entry : objects) { 219 measure(args[0], entry.constructor, entry.description, entry.initializer); 220 } 221 } 222 223 private static final String sourceCode = """ 224 const constructorName = scriptArgs[0]; 225 const initializer = Function("o", scriptArgs[1]); 226 227 const extras = {}; 228 addIntlExtras(extras); 229 230 let constructor; 231 let inputs; 232 if (constructorName.startsWith("Intl.")) { 233 let simpleName = constructorName.substring("Intl.".length); 234 constructor = Intl[simpleName]; 235 inputs = getAvailableLocalesOf(simpleName); 236 } else if (constructorName === "Temporal.TimeZone") { 237 constructor = Temporal.TimeZone; 238 inputs = Intl.supportedValuesOf("timeZone"); 239 } else { 240 throw new Error("Unsupported constructor name: " + constructorName); 241 } 242 243 for (let i = 0; i < inputs.length; ++i) { 244 // Loop twice in case the first time we create an object with a new locale 245 // allocates additional memory when loading the locale data. 246 for (let j = 0; j < 2; ++j) { 247 let options = undefined; 248 if (constructor === Intl.DisplayNames) { 249 options = {type: "language"}; 250 } 251 252 print("Create"); 253 let obj = new constructor(inputs[i], options); 254 255 print("Init"); 256 initializer(obj); 257 258 print("Destroy"); 259 gc(); 260 gc(); 261 print("Collect"); 262 } 263 } 264 265 print("Quit"); 266 quit(); 267 """; 268 }