tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }