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 --enable-preview --source=14 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 transition(Phase nextPhase)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 transferAllocations()82 void transferAllocations() { 83 completeAllocations.putAll(allocations); 84 completeAllocations.keySet().removeAll(freed); 85 allocations.clear(); 86 freed.clear(); 87 } 88 alloc(long ptr, long size)89 void alloc(long ptr, long size) { 90 allocations.put(ptr, Map.entry(phase, size)); 91 } 92 realloc(long oldPtr, long newPtr, long size)93 void realloc(long oldPtr, long newPtr, long size) { 94 free(oldPtr); 95 allocations.put(newPtr, Map.entry(phase, size)); 96 } 97 free(long ptr)98 void free(long ptr) { 99 if (allocations.remove(ptr) == null) { 100 freed.add(ptr); 101 } 102 } 103 statistics()104 LongSummaryStatistics statistics() { 105 return allocSizes.stream().collect(Collectors.summarizingLong(Long::valueOf)); 106 } 107 percentile(double p)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 persistent()114 long persistent() { 115 return completeAllocations.values().stream().map(Map.Entry::getValue).reduce(0L, (a, c) -> a + c); 116 } 117 } 118 parseSize(Matcher m, int group)119 private static long parseSize(Matcher m, int group) { 120 return Long.parseLong(m.group(group), 10); 121 } 122 parsePointer(Matcher m, int group)123 private static long parsePointer(Matcher m, int group) { 124 return Long.parseLong(m.group(group), 16); 125 } 126 measure(String exec, String constructor, String description, String initializer)127 private static void measure(String exec, String constructor, String description, String initializer) throws IOException { 128 var locales = Arrays.stream(Locale.getAvailableLocales()).map(Locale::toLanguageTag).sorted() 129 .collect(Collectors.toUnmodifiableList()); 130 131 var pb = new ProcessBuilder(exec, "--file=-", "--", constructor, initializer, 132 locales.stream().collect(Collectors.joining(","))); 133 var process = pb.start(); 134 135 try (var writer = new BufferedWriter( 136 new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) { 137 writer.write(sourceCode); 138 writer.flush(); 139 } 140 141 var memory = new Memory(); 142 143 try (var reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { 144 var reAlloc = Pattern.compile("\\s+alloc: 0x(\\p{XDigit}+) -> (\\p{Digit}+)"); 145 var reRealloc = Pattern.compile("\\s+realloc: 0x(\\p{XDigit}+) -> 0x(\\p{XDigit}+) -> (\\p{Digit}+)"); 146 var reFree = Pattern.compile("\\s+free: 0x(\\p{XDigit}+)"); 147 148 String line; 149 while ((line = reader.readLine()) != null) { 150 Matcher m; 151 if ((m = reAlloc.matcher(line)).matches()) { 152 var ptr = parsePointer(m, 1); 153 var size = parseSize(m, 2); 154 memory.alloc(ptr, size); 155 } else if ((m = reRealloc.matcher(line)).matches()) { 156 var oldPtr = parsePointer(m, 1); 157 var newPtr = parsePointer(m, 2); 158 var size = parseSize(m, 3); 159 memory.realloc(oldPtr, newPtr, size); 160 } else if ((m = reFree.matcher(line)).matches()) { 161 var ptr = parsePointer(m, 1); 162 memory.free(ptr); 163 } else { 164 memory.transition(Phase.valueOf(line)); 165 } 166 } 167 } 168 169 try (var errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { 170 String line; 171 while ((line = errorReader.readLine()) != null) { 172 System.err.println(line); 173 } 174 } 175 176 var stats = memory.statistics(); 177 178 System.out.printf("%s%n", description); 179 System.out.printf(" max: %d%n", stats.getMax()); 180 System.out.printf(" min: %d%n", stats.getMin()); 181 System.out.printf(" avg: %.0f%n", stats.getAverage()); 182 System.out.printf(" 50p: %.0f%n", memory.percentile(0.50)); 183 System.out.printf(" 75p: %.0f%n", memory.percentile(0.75)); 184 System.out.printf(" 85p: %.0f%n", memory.percentile(0.85)); 185 System.out.printf(" 95p: %.0f%n", memory.percentile(0.95)); 186 System.out.printf(" 99p: %.0f%n", memory.percentile(0.99)); 187 System.out.printf(" mem: %d%n", memory.persistent()); 188 189 memory.transferAllocations(); 190 assert memory.persistent() == 0 : String.format("Leaked %d bytes", memory.persistent()); 191 } 192 main(String[] args)193 public static void main(String[] args) throws IOException { 194 if (args.length == 0) { 195 throw new RuntimeException("The first argument must point to the SpiderMonkey shell executable"); 196 } 197 198 record Entry (String constructor, String description, String initializer) { 199 public static Entry of(String constructor, String description, String initializer) { 200 return new Entry(constructor, description, initializer); 201 } 202 203 public static Entry of(String constructor, String initializer) { 204 return new Entry(constructor, constructor, initializer); 205 } 206 } 207 208 var objects = new ArrayList<Entry>(); 209 objects.add(Entry.of("Collator", "o.compare('a', 'b')")); 210 objects.add(Entry.of("DateTimeFormat", "DateTimeFormat (UDateFormat)", "o.format(0)")); 211 objects.add(Entry.of("DateTimeFormat", "DateTimeFormat (UDateFormat+UDateIntervalFormat)", 212 "o.formatRange(0, 24*60*60*1000)")); 213 objects.add(Entry.of("DisplayNames", "o.of('en')")); 214 objects.add(Entry.of("ListFormat", "o.format(['a', 'b'])")); 215 objects.add(Entry.of("NumberFormat", "o.format(0)")); 216 objects.add(Entry.of("NumberFormat", "NumberFormat (UNumberRangeFormatter)", 217 "o.formatRange(0, 1000)")); 218 objects.add(Entry.of("PluralRules", "o.select(0)")); 219 objects.add(Entry.of("RelativeTimeFormat", "o.format(0, 'hour')")); 220 221 for (var entry : objects) { 222 measure(args[0], entry.constructor, entry.description, entry.initializer); 223 } 224 } 225 226 private static final String sourceCode = """ 227 const constructorName = scriptArgs[0]; 228 const initializer = Function("o", scriptArgs[1]); 229 const locales = scriptArgs[2].split(","); 230 231 const extras = {}; 232 addIntlExtras(extras); 233 234 for (let i = 0; i < locales.length; ++i) { 235 // Loop twice in case the first time we create an object with a new locale 236 // allocates additional memory when loading the locale data. 237 for (let j = 0; j < 2; ++j) { 238 let constructor = Intl[constructorName]; 239 let options = undefined; 240 if (constructor === Intl.DisplayNames) { 241 options = {type: "language"}; 242 } 243 244 print("Create"); 245 let obj = new constructor(locales[i], options); 246 247 print("Init"); 248 initializer(obj); 249 250 print("Destroy"); 251 gc(); 252 gc(); 253 print("Collect"); 254 } 255 } 256 257 print("Quit"); 258 quit(); 259 """; 260 } 261