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