From 9066cfe9886ac131c34d59ed0e2d287b0e3c0087 Mon Sep 17 00:00:00 2001 From: The Android Open Source Project Date: Tue, 3 Mar 2009 19:31:44 -0800 Subject: auto import from //depot/cupcake/@135843 --- tools/preload/20080522.compiled | Bin 0 -> 11414749 bytes tools/preload/Android.mk | 23 ++ tools/preload/ClassRank.java | 53 ++++ tools/preload/Compile.java | 73 +++++ tools/preload/LoadedClass.java | 163 ++++++++++ tools/preload/MemoryUsage.java | 283 +++++++++++++++++ tools/preload/Operation.java | 136 +++++++++ tools/preload/Policy.java | 114 +++++++ tools/preload/PrintCsv.java | 89 ++++++ tools/preload/PrintPsTree.java | 46 +++ tools/preload/Proc.java | 261 ++++++++++++++++ tools/preload/Record.java | 135 +++++++++ tools/preload/Root.java | 163 ++++++++++ tools/preload/WritePreloadedClassFile.java | 102 +++++++ tools/preload/loadclass/Android.mk | 8 + tools/preload/loadclass/LoadClass.java | 80 +++++ tools/preload/preload.iml | 15 + tools/preload/preload.ipr | 467 +++++++++++++++++++++++++++++ 18 files changed, 2211 insertions(+) create mode 100644 tools/preload/20080522.compiled create mode 100644 tools/preload/Android.mk create mode 100644 tools/preload/ClassRank.java create mode 100644 tools/preload/Compile.java create mode 100644 tools/preload/LoadedClass.java create mode 100644 tools/preload/MemoryUsage.java create mode 100644 tools/preload/Operation.java create mode 100644 tools/preload/Policy.java create mode 100644 tools/preload/PrintCsv.java create mode 100644 tools/preload/PrintPsTree.java create mode 100644 tools/preload/Proc.java create mode 100644 tools/preload/Record.java create mode 100644 tools/preload/Root.java create mode 100644 tools/preload/WritePreloadedClassFile.java create mode 100644 tools/preload/loadclass/Android.mk create mode 100644 tools/preload/loadclass/LoadClass.java create mode 100644 tools/preload/preload.iml create mode 100644 tools/preload/preload.ipr (limited to 'tools/preload') diff --git a/tools/preload/20080522.compiled b/tools/preload/20080522.compiled new file mode 100644 index 0000000..a2af422 Binary files /dev/null and b/tools/preload/20080522.compiled differ diff --git a/tools/preload/Android.mk b/tools/preload/Android.mk new file mode 100644 index 0000000..e6fa103 --- /dev/null +++ b/tools/preload/Android.mk @@ -0,0 +1,23 @@ +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := \ + ClassRank.java \ + Compile.java \ + LoadedClass.java \ + MemoryUsage.java \ + Operation.java \ + Policy.java \ + PrintCsv.java \ + PrintPsTree.java \ + Proc.java \ + Record.java \ + Root.java \ + WritePreloadedClassFile.java + +LOCAL_MODULE:= preload + +include $(BUILD_HOST_JAVA_LIBRARY) + +include $(call all-subdir-makefiles) diff --git a/tools/preload/ClassRank.java b/tools/preload/ClassRank.java new file mode 100644 index 0000000..3699b89 --- /dev/null +++ b/tools/preload/ClassRank.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.util.Comparator; + +/** + * Ranks classes for preloading based on how long their operations took + * and how early the operations happened. Higher ranked classes come first. + */ +class ClassRank implements Comparator { + + /** + * Increase this number to add more weight to classes which were loaded + * earlier. + */ + static final int SEQUENCE_WEIGHT = 500; // 5 ms + + static final int BUCKET_SIZE = 5; + + public int compare(Operation a, Operation b) { + // Higher ranked operations should come first. + int result = rankOf(b) - rankOf(a); + if (result != 0) { + return result; + } + + // Make sure we don't drop one of two classes w/ the same rank. + // If a load and an initialization have the same rank, it's OK + // to treat the operations equally. + return a.loadedClass.name.compareTo(b.loadedClass.name); + } + + /** Ranks the given operation. */ + private static int rankOf(Operation o) { + return o.medianExclusiveTimeMicros() + + SEQUENCE_WEIGHT / (o.index / BUCKET_SIZE + 1); + } +} + + diff --git a/tools/preload/Compile.java b/tools/preload/Compile.java new file mode 100644 index 0000000..67258ef --- /dev/null +++ b/tools/preload/Compile.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +/** + * Parses and analyzes a log, pulling our PRELOAD information. If you have + * an emulator or device running in the background, this class will use it + * to measure and record the memory usage of each class. + * + * TODO: Should analyze lines and select substring dynamically (instead of hardcoded 19) + */ +public class Compile { + + public static void main(String[] args) throws IOException { + if (args.length != 2) { + System.err.println("Usage: Compile [log file] [output file]"); + System.exit(0); + } + + Root root = new Root(); + + List records = new ArrayList(); + + BufferedReader in = new BufferedReader(new InputStreamReader( + new FileInputStream(args[0]))); + + String line; + int lineNumber = 0; + while ((line = in.readLine()) != null) { + lineNumber++; + if (line.startsWith("I/PRELOAD")) { + try { + String clipped = line.substring(19); + records.add(new Record(clipped, lineNumber)); + } catch (RuntimeException e) { + throw new RuntimeException( + "Exception while recording line " + lineNumber + ": " + line, e); + } + } + } + + for (Record record : records) { + root.indexProcess(record); + } + + for (Record record : records) { + root.indexClassOperation(record); + } + + in.close(); + + root.toFile(args[1]); + } +} diff --git a/tools/preload/LoadedClass.java b/tools/preload/LoadedClass.java new file mode 100644 index 0000000..5782807 --- /dev/null +++ b/tools/preload/LoadedClass.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +/** + * A loaded class. + */ +class LoadedClass implements Serializable, Comparable { + + private static final long serialVersionUID = 0; + + /** Class name. */ + final String name; + + /** Load operations. */ + final List loads = new ArrayList(); + + /** Static initialization operations. */ + final List initializations = new ArrayList(); + + /** Memory usage gathered by loading only this class in its own VM. */ + MemoryUsage memoryUsage = MemoryUsage.NOT_AVAILABLE; + + /** + * Whether or not this class was loaded in the system class loader. + */ + final boolean systemClass; + + /** Whether or not this class will be preloaded. */ + boolean preloaded; + + /** Constructs a new class. */ + LoadedClass(String name, boolean systemClass) { + this.name = name; + this.systemClass = systemClass; + } + + void measureMemoryUsage() { + this.memoryUsage = MemoryUsage.forClass(name); + } + + int mlt = -1; + + /** Median time to load this class. */ + int medianLoadTimeMicros() { + if (mlt != -1) { + return mlt; + } + + return mlt = calculateMedian(loads); + } + + int mit = -1; + + /** Median time to initialize this class. */ + int medianInitTimeMicros() { + if (mit != -1) { + return mit; + } + + return mit = calculateMedian(initializations); + } + + /** Calculates the median duration for a list of operations. */ + private static int calculateMedian(List operations) { + int size = operations.size(); + if (size == 0) { + return 0; + } + + int[] times = new int[size]; + for (int i = 0; i < size; i++) { + times[i] = operations.get(i).exclusiveTimeMicros(); + } + + Arrays.sort(times); + int middle = size / 2; + if (size % 2 == 1) { + // Odd + return times[middle]; + } else { + // Even -- average the two. + return (times[middle - 1] + times[middle]) / 2; + } + } + + /** + * Counts loads by apps. + */ + int appLoads() { + return operationsByApps(loads); + } + + /** + * Counts inits by apps. + */ + int appInits() { + return operationsByApps(initializations); + } + + /** + * Counts number of app operations in the given list. + */ + private static int operationsByApps(List operations) { + int byApps = 0; + for (Operation operation : operations) { + if (operation.process.isApplication()) { + byApps++; + } + } + return byApps; + } + + public int compareTo(LoadedClass o) { + return name.compareTo(o.name); + } + + @Override + public String toString() { + return name; + } + + /** + * Returns true if this class's initialization causes the given class to + * initialize. + */ + public boolean initializes(LoadedClass clazz, Set visited) { + // Avoid infinite recursion. + if (!visited.add(this)) { + return false; + } + + if (clazz == this) { + return true; + } + + for (Operation initialization : initializations) { + if (initialization.loadedClass.initializes(clazz, visited)) { + return true; + } + } + + return false; + } +} diff --git a/tools/preload/MemoryUsage.java b/tools/preload/MemoryUsage.java new file mode 100644 index 0000000..e5dfb2a --- /dev/null +++ b/tools/preload/MemoryUsage.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.Serializable; +import java.io.IOException; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Memory usage information. + */ +class MemoryUsage implements Serializable { + + private static final long serialVersionUID = 0; + + static final MemoryUsage NOT_AVAILABLE = new MemoryUsage(); + + static int errorCount = 0; + static final int MAXIMUM_ERRORS = 10; // give up after this many fails + + final int nativeSharedPages; + final int javaSharedPages; + final int otherSharedPages; + final int nativePrivatePages; + final int javaPrivatePages; + final int otherPrivatePages; + + final int allocCount; + final int allocSize; + final int freedCount; + final int freedSize; + final long nativeHeapSize; + + public MemoryUsage(String line) { + String[] parsed = line.split(","); + + nativeSharedPages = Integer.parseInt(parsed[1]); + javaSharedPages = Integer.parseInt(parsed[2]); + otherSharedPages = Integer.parseInt(parsed[3]); + nativePrivatePages = Integer.parseInt(parsed[4]); + javaPrivatePages = Integer.parseInt(parsed[5]); + otherPrivatePages = Integer.parseInt(parsed[6]); + allocCount = Integer.parseInt(parsed[7]); + allocSize = Integer.parseInt(parsed[8]); + freedCount = Integer.parseInt(parsed[9]); + freedSize = Integer.parseInt(parsed[10]); + nativeHeapSize = Long.parseLong(parsed[11]); + } + + MemoryUsage() { + nativeSharedPages = -1; + javaSharedPages = -1; + otherSharedPages = -1; + nativePrivatePages = -1; + javaPrivatePages = -1; + otherPrivatePages = -1; + + allocCount = -1; + allocSize = -1; + freedCount = -1; + freedSize = -1; + nativeHeapSize = -1; + } + + MemoryUsage(int nativeSharedPages, + int javaSharedPages, + int otherSharedPages, + int nativePrivatePages, + int javaPrivatePages, + int otherPrivatePages, + int allocCount, + int allocSize, + int freedCount, + int freedSize, + long nativeHeapSize) { + this.nativeSharedPages = nativeSharedPages; + this.javaSharedPages = javaSharedPages; + this.otherSharedPages = otherSharedPages; + this.nativePrivatePages = nativePrivatePages; + this.javaPrivatePages = javaPrivatePages; + this.otherPrivatePages = otherPrivatePages; + this.allocCount = allocCount; + this.allocSize = allocSize; + this.freedCount = freedCount; + this.freedSize = freedSize; + this.nativeHeapSize = nativeHeapSize; + } + + MemoryUsage subtract(MemoryUsage baseline) { + return new MemoryUsage( + nativeSharedPages - baseline.nativeSharedPages, + javaSharedPages - baseline.javaSharedPages, + otherSharedPages - baseline.otherSharedPages, + nativePrivatePages - baseline.nativePrivatePages, + javaPrivatePages - baseline.javaPrivatePages, + otherPrivatePages - baseline.otherPrivatePages, + allocCount - baseline.allocCount, + allocSize - baseline.allocSize, + freedCount - baseline.freedCount, + freedSize - baseline.freedSize, + nativeHeapSize - baseline.nativeHeapSize); + } + + int javaHeapSize() { + return allocSize - freedSize; + } + + int javaPagesInK() { + return (javaSharedPages + javaPrivatePages) * 4; + } + + int nativePagesInK() { + return (nativeSharedPages + nativePrivatePages) * 4; + } + int otherPagesInK() { + return (otherSharedPages + otherPrivatePages) * 4; + } + + /** + * Was this information available? + */ + boolean isAvailable() { + return nativeSharedPages != -1; + } + + /** + * Measures baseline memory usage. + */ + static MemoryUsage baseline() { + return forClass(null); + } + + private static final String CLASS_PATH = "-Xbootclasspath" + + ":/system/framework/core.jar" + + ":/system/framework/ext.jar" + + ":/system/framework/framework.jar" + + ":/system/framework/framework-tests.jar" + + ":/system/framework/services.jar" + + ":/system/framework/loadclass.jar"; + + private static final String[] GET_DIRTY_PAGES = { + "adb", "-e", "shell", "dalvikvm", CLASS_PATH, "LoadClass" }; + + /** + * Measures memory usage for the given class. + */ + static MemoryUsage forClass(String className) { + + // This is a coarse approximation for determining that no device is connected, + // or that the communication protocol has changed, but we'll keep going and stop whining. + if (errorCount >= MAXIMUM_ERRORS) { + return NOT_AVAILABLE; + } + + MeasureWithTimeout measurer = new MeasureWithTimeout(className); + + new Thread(measurer).start(); + + synchronized (measurer) { + if (measurer.memoryUsage == null) { + // Wait up to 10s. + try { + measurer.wait(30000); + } catch (InterruptedException e) { + System.err.println("Interrupted waiting for measurement."); + e.printStackTrace(); + return NOT_AVAILABLE; + } + + // If it's still null. + if (measurer.memoryUsage == null) { + System.err.println("Timed out while measuring " + + className + "."); + return NOT_AVAILABLE; + } + } + + System.err.println("Got memory usage for " + className + "."); + return measurer.memoryUsage; + } + } + + static class MeasureWithTimeout implements Runnable { + + final String className; + MemoryUsage memoryUsage = null; + + MeasureWithTimeout(String className) { + this.className = className; + } + + public void run() { + MemoryUsage measured = measure(); + + synchronized (this) { + memoryUsage = measured; + notifyAll(); + } + } + + private MemoryUsage measure() { + String[] commands = GET_DIRTY_PAGES; + if (className != null) { + List commandList = new ArrayList( + GET_DIRTY_PAGES.length + 1); + commandList.addAll(Arrays.asList(commands)); + commandList.add(className); + commands = commandList.toArray(new String[commandList.size()]); + } + + try { + final Process process = Runtime.getRuntime().exec(commands); + + final InputStream err = process.getErrorStream(); + + // Send error output to stderr. + Thread errThread = new Thread() { + @Override + public void run() { + copy(err, System.err); + } + }; + errThread.setDaemon(true); + errThread.start(); + + BufferedReader in = new BufferedReader( + new InputStreamReader(process.getInputStream())); + String line = in.readLine(); + if (line == null || !line.startsWith("DECAFBAD,")) { + System.err.println("Got bad response for " + className + + ": " + line); + errorCount += 1; + return NOT_AVAILABLE; + } + + in.close(); + err.close(); + process.destroy(); + + return new MemoryUsage(line); + } catch (IOException e) { + System.err.println("Error getting stats for " + + className + "."); + e.printStackTrace(); + return NOT_AVAILABLE; + } + } + + } + + /** + * Copies from one stream to another. + */ + private static void copy(InputStream in, OutputStream out) { + byte[] buffer = new byte[1024]; + int read; + try { + while ((read = in.read(buffer)) > -1) { + out.write(buffer, 0, read); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/tools/preload/Operation.java b/tools/preload/Operation.java new file mode 100644 index 0000000..4f1938e --- /dev/null +++ b/tools/preload/Operation.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.util.List; +import java.util.ArrayList; +import java.io.Serializable; + +/** + * An operation with a duration. Could represent a class load or initialization. + */ +class Operation implements Serializable { + + private static final long serialVersionUID = 0; + + /** + * Type of operation. + */ + enum Type { + LOAD, INIT + } + + /** Process this operation occurred in. */ + final Proc process; + + /** Start time for this operation. */ + final long startTimeNanos; + + /** Index of this operation relative to its process. */ + final int index; + + /** Type of operation. */ + final Type type; + + /** End time for this operation. */ + long endTimeNanos = -1; + + /** The class that this operation loaded or initialized. */ + final LoadedClass loadedClass; + + /** Other operations that occurred during this one. */ + final List subops = new ArrayList(); + + /** Constructs a new operation. */ + Operation(Proc process, LoadedClass loadedClass, long startTimeNanos, + int index, Type type) { + this.process = process; + this.loadedClass = loadedClass; + this.startTimeNanos = startTimeNanos; + this.index = index; + this.type = type; + } + + /** + * Returns how long this class initialization and all the nested class + * initializations took. + */ + private long inclusiveTimeNanos() { + if (endTimeNanos == -1) { + throw new IllegalStateException("End time hasn't been set yet: " + + loadedClass.name); + } + + return endTimeNanos - startTimeNanos; + } + + /** + * Returns how long this class initialization took. + */ + int exclusiveTimeMicros() { + long exclusive = inclusiveTimeNanos(); + + for (Operation child : subops) { + exclusive -= child.inclusiveTimeNanos(); + } + + if (exclusive < 0) { + throw new AssertionError(loadedClass.name); + } + + return nanosToMicros(exclusive); + } + + /** Gets the median time that this operation took across all processes. */ + int medianExclusiveTimeMicros() { + switch (type) { + case LOAD: return loadedClass.medianLoadTimeMicros(); + case INIT: return loadedClass.medianInitTimeMicros(); + default: throw new AssertionError(); + } + } + + /** + * Converts nanoseconds to microseconds. + * + * @throws RuntimeException if overflow occurs + */ + private static int nanosToMicros(long nanos) { + long micros = nanos / 1000; + int microsInt = (int) micros; + if (microsInt != micros) { + throw new RuntimeException("Integer overflow: " + nanos); + } + return microsInt; + } + + /** + * Primarily for debugger support + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(type.toString()); + sb.append(' '); + sb.append(loadedClass.toString()); + if (subops.size() > 0) { + sb.append(" ("); + sb.append(subops.size()); + sb.append(" sub ops)"); + } + return sb.toString(); + } + +} diff --git a/tools/preload/Policy.java b/tools/preload/Policy.java new file mode 100644 index 0000000..554966b --- /dev/null +++ b/tools/preload/Policy.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * This is not instantiated - we just provide data for other classes to use + */ +public class Policy { + + /** + * This location (in the build system) of the preloaded-classes file. + */ + private static final String PRELOADED_CLASS_FILE = "frameworks/base/preloaded-classes"; + + /** + * The internal process name of the system process. Note, this also shows up as + * "system_process", e.g. in ddms. + */ + private static final String SYSTEM_SERVER_PROCESS_NAME = "system_server"; + + /** + * Names of non-application processes - these will not be checked for preloaded classes. + * + * TODO: Replace this hardcoded list with a walk up the parent chain looking for zygote. + */ + private static final Set NOT_FROM_ZYGOTE = new HashSet(Arrays.asList( + "zygote", + "dexopt", + "unknown", + SYSTEM_SERVER_PROCESS_NAME, + "com.android.development", + "app_process" // am & other shell commands + )); + + /** + * Long running services. These are restricted in their contribution to the preloader + * because their launch time is less critical. + */ + private static final Set SERVICES = new HashSet(Arrays.asList( + SYSTEM_SERVER_PROCESS_NAME, + "com.android.acore", + // Commented out to make sure DefaultTimeZones gets preloaded. + // "com.android.phone", + "com.google.process.content", + "android.process.media" + )); + + /** + * Classes which we shouldn't load from the Zygote. + */ + private static final Set EXCLUDED_CLASSES = new HashSet(Arrays.asList( + // Binders + "android.app.AlarmManager", + "android.app.SearchManager", + "android.os.FileObserver", + "com.android.server.PackageManagerService$AppDirObserver", + + // Threads + "android.os.AsyncTask", + "android.pim.ContactsAsyncHelper", + "java.lang.ProcessManager" + + )); + + /** + * No constructor - use static methods only + */ + private Policy() {} + + /** + * Returns the path/file name of the preloaded classes file that will be written + * by WritePreloadedClassFile. + */ + public static String getPreloadedClassFileName() { + return PRELOADED_CLASS_FILE; + } + + /** + * Reports if a given process name was created from zygote + */ + public static boolean isFromZygote(String processName) { + return !NOT_FROM_ZYGOTE.contains(processName); + } + + /** + * Reports if the given process name is a "long running" process or service + */ + public static boolean isService(String processName) { + return SERVICES.contains(processName); + } + + /** + * Reports if the given class should never be preloaded + */ + public static boolean isPreloadableClass(String className) { + return !EXCLUDED_CLASSES.contains(className); + } +} diff --git a/tools/preload/PrintCsv.java b/tools/preload/PrintCsv.java new file mode 100644 index 0000000..9f2a318 --- /dev/null +++ b/tools/preload/PrintCsv.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.IOException; +import java.io.FileInputStream; +import java.io.ObjectInputStream; +import java.io.BufferedInputStream; + +/** + * Prints raw information in CSV format. + */ +public class PrintCsv { + + public static void main(String[] args) + throws IOException, ClassNotFoundException { + if (args.length != 1) { + System.err.println("Usage: PrintCsv [compiled log file]"); + System.exit(0); + } + + Root root = Root.fromFile(args[0]); + + System.out.println("Name" + + ",Preloaded" + + ",Median Load Time (us)" + + ",Median Init Time (us)" + + ",Load Count" + + ",Init Count" + + ",Managed Heap (B)" + + ",Native Heap (B)" + + ",Managed Pages (kB)" + + ",Native Pages (kB)" + + ",Other Pages (kB)"); + + MemoryUsage baseline = root.baseline; + + for (LoadedClass loadedClass : root.loadedClasses.values()) { + if (!loadedClass.systemClass) { + continue; + } + + System.out.print(loadedClass.name); + System.out.print(','); + System.out.print(loadedClass.preloaded); + System.out.print(','); + System.out.print(loadedClass.medianLoadTimeMicros()); + System.out.print(','); + System.out.print(loadedClass.medianInitTimeMicros()); + System.out.print(','); + System.out.print(loadedClass.loads.size()); + System.out.print(','); + System.out.print(loadedClass.initializations.size()); + + if (loadedClass.memoryUsage.isAvailable()) { + MemoryUsage subtracted + = loadedClass.memoryUsage.subtract(baseline); + + System.out.print(','); + System.out.print(subtracted.javaHeapSize()); + System.out.print(','); + System.out.print(subtracted.nativeHeapSize); + System.out.print(','); + System.out.print(subtracted.javaPagesInK()); + System.out.print(','); + System.out.print(subtracted.nativePagesInK()); + System.out.print(','); + System.out.print(subtracted.otherPagesInK()); + + } else { + System.out.print(",n/a,n/a,n/a,n/a,n/a"); + } + + System.out.println(); + } + } +} diff --git a/tools/preload/PrintPsTree.java b/tools/preload/PrintPsTree.java new file mode 100644 index 0000000..22701fa --- /dev/null +++ b/tools/preload/PrintPsTree.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.IOException; +import java.io.FileInputStream; +import java.io.ObjectInputStream; +import java.io.BufferedInputStream; + +/** + * Prints raw information in CSV format. + */ +public class PrintPsTree { + + public static void main(String[] args) + throws IOException, ClassNotFoundException { + if (args.length != 1) { + System.err.println("Usage: PrintCsv [compiled log file]"); + System.exit(0); + } + + FileInputStream fin = new FileInputStream(args[0]); + ObjectInputStream oin = new ObjectInputStream( + new BufferedInputStream(fin)); + + Root root = (Root) oin.readObject(); + + for (Proc proc : root.processes.values()) { + if (proc.parent == null) { + proc.print(); + } + } + } +} diff --git a/tools/preload/Proc.java b/tools/preload/Proc.java new file mode 100644 index 0000000..22697f8 --- /dev/null +++ b/tools/preload/Proc.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.util.Set; +import java.util.HashSet; +import java.util.Arrays; +import java.util.List; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.Map; +import java.util.HashMap; +import java.util.Collections; +import java.util.TreeSet; +import java.io.Serializable; + +/** + * A Dalvik process. + */ +class Proc implements Serializable { + + private static final long serialVersionUID = 0; + + /** + * Default percentage of time to cut off of app class loading times. + */ + static final int PERCENTAGE_TO_PRELOAD = 75; + + /** + * Maximum number of classes to preload for a given process. + */ + static final int MAX_TO_PRELOAD = 100; + + /** Parent process. */ + final Proc parent; + + /** Process ID. */ + final int id; + + /** + * Name of this process. We may not have the correct name at first, i.e. + * some classes could have been loaded before the process name was set. + */ + String name; + + /** Child processes. */ + final List children = new ArrayList(); + + /** Maps thread ID to operation stack. */ + transient final Map> stacks + = new HashMap>(); + + /** Number of operations. */ + int operationCount; + + /** Sequential list of operations that happened in this process. */ + final List operations = new ArrayList(); + + /** List of past process names. */ + final List nameHistory = new ArrayList(); + + /** Constructs a new process. */ + Proc(Proc parent, int id) { + this.parent = parent; + this.id = id; + } + + /** Sets name of this process. */ + void setName(String name) { + if (!name.equals(this.name)) { + if (this.name != null) { + nameHistory.add(this.name); + } + this.name = name; + } + } + + /** + * Returns the percentage of time we should cut by preloading for this + * app. + */ + int percentageToPreload() { + return PERCENTAGE_TO_PRELOAD; + } + + /** + * Returns a list of classes which should be preloaded. + * + * @param takeAllClasses forces all classes to be taken (irrespective of ranking) + */ + List highestRankedClasses(boolean takeAllClasses) { + if (!isApplication()) { + return Collections.emptyList(); + } + + // Sort by rank. + Operation[] ranked = new Operation[operations.size()]; + ranked = operations.toArray(ranked); + Arrays.sort(ranked, new ClassRank()); + + // The percentage of time to save by preloading. + int timeToSave = totalTimeMicros() * percentageToPreload() / 100; + int timeSaved = 0; + + boolean service = Policy.isService(this.name); + + List highest = new ArrayList(); + for (Operation operation : ranked) { + + // These are actual ranking decisions, which can be overridden + if (!takeAllClasses) { + if (highest.size() >= MAX_TO_PRELOAD) { + System.out.println(name + " got " + + (timeSaved * 100 / timeToSave) + "% through"); + break; + } + + if (timeSaved >= timeToSave) { + break; + } + } + + // The remaining rules apply even to wired-down processes + if (!Policy.isPreloadableClass(operation.loadedClass.name)) { + continue; + } + + if (!operation.loadedClass.systemClass) { + continue; + } + + // Only load java.* class for services. + if (!service || operation.loadedClass.name.startsWith("java.")) { + highest.add(operation.loadedClass); + } + + // For services, still count the time even if it's not in java.* + timeSaved += operation.medianExclusiveTimeMicros(); + } + + return highest; + } + + /** + * Total time spent class loading and initializing. + */ + int totalTimeMicros() { + int totalTime = 0; + for (Operation operation : operations) { + totalTime += operation.medianExclusiveTimeMicros(); + } + return totalTime; + } + + /** + * Returns true if this process is an app. + * + * TODO: Replace the hardcoded list with a walk up the parent chain looking for zygote. + */ + public boolean isApplication() { + return Policy.isFromZygote(name); + } + + /** + * Starts an operation. + * + * @param threadId thread the operation started in + * @param loadedClass class operation happened to + * @param time the operation started + */ + void startOperation(int threadId, LoadedClass loadedClass, long time, + Operation.Type type) { + Operation o = new Operation( + this, loadedClass, time, operationCount++, type); + operations.add(o); + + LinkedList stack = stacks.get(threadId); + if (stack == null) { + stack = new LinkedList(); + stacks.put(threadId, stack); + } + + if (!stack.isEmpty()) { + stack.getLast().subops.add(o); + } + + stack.add(o); + } + + /** + * Ends an operation. + * + * @param threadId thread the operation ended in + * @param loadedClass class operation happened to + * @param time the operation ended + */ + Operation endOperation(int threadId, String className, + LoadedClass loadedClass, long time) { + LinkedList stack = stacks.get(threadId); + + if (stack == null || stack.isEmpty()) { + didNotStart(className); + return null; + } + + Operation o = stack.getLast(); + if (loadedClass != o.loadedClass) { + didNotStart(className); + return null; + } + + stack.removeLast(); + + o.endTimeNanos = time; + return o; + } + + /** + * Prints an error indicating that we saw the end of an operation but not + * the start. A bug in the logging framework which results in dropped logs + * causes this. + */ + private static void didNotStart(String name) { + System.err.println("Warning: An operation ended on " + name + + " but it never started!"); + } + + /** + * Prints this process tree to stdout. + */ + void print() { + print(""); + } + + /** + * Prints a child proc to standard out. + */ + private void print(String prefix) { + System.out.println(prefix + "id=" + id + ", name=" + name); + for (Proc child : children) { + child.print(prefix + " "); + } + } + + @Override + public String toString() { + return this.name; + } +} diff --git a/tools/preload/Record.java b/tools/preload/Record.java new file mode 100644 index 0000000..b2be4d4 --- /dev/null +++ b/tools/preload/Record.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * One line from the loaded-classes file. + */ +class Record { + + enum Type { + /** Start of initialization. */ + START_LOAD, + + /** End of initialization. */ + END_LOAD, + + /** Start of initialization. */ + START_INIT, + + /** End of initialization. */ + END_INIT + } + + /** Parent process ID. */ + final int ppid; + + /** Process ID. */ + final int pid; + + /** Thread ID. */ + final int tid; + + /** Process name. */ + final String processName; + + /** Class loader pointer. */ + final int classLoader; + + /** Type of record. */ + final Type type; + + /** Name of loaded class. */ + final String className; + + /** Record time (ns). */ + final long time; + + /** Source file line# */ + int sourceLineNumber; + + /** + * Parses a line from the loaded-classes file. + */ + Record(String line, int lineNum) { + char typeChar = line.charAt(0); + switch (typeChar) { + case '>': type = Type.START_LOAD; break; + case '<': type = Type.END_LOAD; break; + case '+': type = Type.START_INIT; break; + case '-': type = Type.END_INIT; break; + default: throw new AssertionError("Bad line: " + line); + } + + sourceLineNumber = lineNum; + + line = line.substring(1); + String[] parts = line.split(":"); + + ppid = Integer.parseInt(parts[0]); + pid = Integer.parseInt(parts[1]); + tid = Integer.parseInt(parts[2]); + + processName = decode(parts[3]).intern(); + + classLoader = Integer.parseInt(parts[4]); + className = vmTypeToLanguage(decode(parts[5])).intern(); + + time = Long.parseLong(parts[6]); + } + + /** + * Decode any escaping that may have been written to the log line. + * + * Supports unicode-style escaping: \\uXXXX = character in hex + * + * @param rawField the field as it was written into the log + * @result the same field with any escaped characters replaced + */ + String decode(String rawField) { + String result = rawField; + int offset = result.indexOf("\\u"); + while (offset >= 0) { + String before = result.substring(0, offset); + String escaped = result.substring(offset+2, offset+6); + String after = result.substring(offset+6); + + result = String.format("%s%c%s", before, Integer.parseInt(escaped, 16), after); + + // find another but don't recurse + offset = result.indexOf("\\u", offset + 1); + } + return result; + } + + /** + * Converts a VM-style name to a language-style name. + */ + String vmTypeToLanguage(String typeName) { + // if the typename is (null), just return it as-is. This is probably in dexopt and + // will be discarded anyway. NOTE: This corresponds to the case in dalvik/vm/oo/Class.c + // where dvmLinkClass() returns false and we clean up and exit. + if ("(null)".equals(typeName)) { + return typeName; + } + + if (!typeName.startsWith("L") || !typeName.endsWith(";") ) { + throw new AssertionError("Bad name: " + typeName + " in line " + sourceLineNumber); + } + + typeName = typeName.substring(1, typeName.length() - 1); + return typeName.replace("/", "."); + } +} diff --git a/tools/preload/Root.java b/tools/preload/Root.java new file mode 100644 index 0000000..949f9b7 --- /dev/null +++ b/tools/preload/Root.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.Serializable; +import java.io.IOException; +import java.io.Writer; +import java.io.BufferedWriter; +import java.io.OutputStreamWriter; +import java.io.FileOutputStream; +import java.io.FileInputStream; +import java.io.ObjectInputStream; +import java.io.BufferedInputStream; +import java.io.ObjectOutputStream; +import java.io.BufferedOutputStream; +import java.util.Map; +import java.util.HashMap; +import java.util.Set; +import java.util.TreeSet; +import java.util.Arrays; +import java.nio.charset.Charset; + +/** + * Root of our data model. + */ +public class Root implements Serializable { + + private static final long serialVersionUID = 0; + + /** pid -> Proc */ + final Map processes = new HashMap(); + + /** Class name -> LoadedClass */ + final Map loadedClasses + = new HashMap(); + + final MemoryUsage baseline = MemoryUsage.baseline(); + + /** + * Records class loads and initializations. + */ + void indexClassOperation(Record record) { + Proc process = processes.get(record.pid); + + // Ignore dexopt output. It loads applications classes through the + // system class loader and messes us up. + if (record.processName.equals("dexopt")) { + return; + } + + String name = record.className; + LoadedClass loadedClass = loadedClasses.get(name); + Operation o = null; + + switch (record.type) { + case START_LOAD: + case START_INIT: + if (loadedClass == null) { + loadedClass = new LoadedClass( + name, record.classLoader == 0); + if (loadedClass.systemClass) { + // Only measure memory for classes in the boot + // classpath. + loadedClass.measureMemoryUsage(); + } + loadedClasses.put(name, loadedClass); + } + break; + + case END_LOAD: + case END_INIT: + o = process.endOperation(record.tid, record.className, + loadedClass, record.time); + if (o == null) { + return; + } + } + + switch (record.type) { + case START_LOAD: + process.startOperation(record.tid, loadedClass, record.time, + Operation.Type.LOAD); + break; + + case START_INIT: + process.startOperation(record.tid, loadedClass, record.time, + Operation.Type.INIT); + break; + + case END_LOAD: + loadedClass.loads.add(o); + break; + + case END_INIT: + loadedClass.initializations.add(o); + break; + } + } + + /** + * Indexes information about the process from the given record. + */ + void indexProcess(Record record) { + Proc proc = processes.get(record.pid); + + if (proc == null) { + // Create a new process object. + Proc parent = processes.get(record.ppid); + proc = new Proc(parent, record.pid); + processes.put(proc.id, proc); + if (parent != null) { + parent.children.add(proc); + } + } + + proc.setName(record.processName); + } + + /** + * Writes this graph to a file. + */ + void toFile(String fileName) throws IOException { + FileOutputStream out = new FileOutputStream(fileName); + ObjectOutputStream oout = new ObjectOutputStream( + new BufferedOutputStream(out)); + + System.err.println("Writing object model..."); + + oout.writeObject(this); + + oout.close(); + + System.err.println("Done!"); + } + + /** + * Reads Root from a file. + */ + static Root fromFile(String fileName) + throws IOException, ClassNotFoundException { + FileInputStream fin = new FileInputStream(fileName); + ObjectInputStream oin = new ObjectInputStream( + new BufferedInputStream(fin)); + + Root root = (Root) oin.readObject(); + + oin.close(); + + return root; + } +} diff --git a/tools/preload/WritePreloadedClassFile.java b/tools/preload/WritePreloadedClassFile.java new file mode 100644 index 0000000..d87b1f0 --- /dev/null +++ b/tools/preload/WritePreloadedClassFile.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +/** + * Writes /frameworks/base/preloaded-classes. Also updates LoadedClass.preloaded + * fields and writes over compiled log file. + */ +public class WritePreloadedClassFile { + + public static void main(String[] args) throws IOException, ClassNotFoundException { + + // Process command-line arguments first + List wiredProcesses = new ArrayList(); + String inputFileName = null; + int argOffset = 0; + try { + while ("--preload-all-process".equals(args[argOffset])) { + argOffset++; + wiredProcesses.add(args[argOffset++]); + } + + inputFileName = args[argOffset++]; + } catch (RuntimeException e) { + System.err.println("Usage: WritePreloadedClassFile " + + "[--preload-all-process process-name] " + + "[compiled log file]"); + System.exit(0); + } + + Root root = Root.fromFile(inputFileName); + + for (LoadedClass loadedClass : root.loadedClasses.values()) { + loadedClass.preloaded = false; + } + + Writer out = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(Policy.getPreloadedClassFileName()), + Charset.forName("US-ASCII"))); + + out.write("# Classes which are preloaded by com.android.internal.os.ZygoteInit.\n"); + out.write("# Automatically generated by /frameworks/base/tools/preload.\n"); + out.write("# percent=" + Proc.PERCENTAGE_TO_PRELOAD + ", weight=" + + ClassRank.SEQUENCE_WEIGHT + + ", bucket_size=" + ClassRank.BUCKET_SIZE + + "\n"); + for (String wiredProcess : wiredProcesses) { + out.write("# forcing classes loaded by: " + wiredProcess + "\n"); + } + + Set highestRanked = new TreeSet(); + for (Proc proc : root.processes.values()) { + // test to see if this is one of the wired-down ("take all classes") processes + boolean isWired = wiredProcesses.contains(proc.name); + + List highestForProc = proc.highestRankedClasses(isWired); + + System.out.println(proc.name + ": " + highestForProc.size()); + + for (LoadedClass loadedClass : highestForProc) { + loadedClass.preloaded = true; + } + highestRanked.addAll(highestForProc); + } + + for (LoadedClass loadedClass : highestRanked) { + out.write(loadedClass.name); + out.write('\n'); + } + + out.close(); + + System.out.println(highestRanked.size() + + " classes will be preloaded."); + + // Update data to reflect LoadedClass.preloaded changes. + root.toFile(inputFileName); + } +} diff --git a/tools/preload/loadclass/Android.mk b/tools/preload/loadclass/Android.mk new file mode 100644 index 0000000..435699d --- /dev/null +++ b/tools/preload/loadclass/Android.mk @@ -0,0 +1,8 @@ +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_MODULE := loadclass + +include $(BUILD_JAVA_LIBRARY) diff --git a/tools/preload/loadclass/LoadClass.java b/tools/preload/loadclass/LoadClass.java new file mode 100644 index 0000000..471cc84 --- /dev/null +++ b/tools/preload/loadclass/LoadClass.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.util.Log; +import android.os.Debug; + +/** + * Loads a class, runs the garbage collector, and prints showmap output. + * + *

Usage: dalvikvm LoadClass [class name] + */ +class LoadClass { + + public static void main(String[] args) { + System.loadLibrary("android_runtime"); + + if (registerNatives() < 0) { + throw new RuntimeException("Error registering natives."); + } + + Debug.startAllocCounting(); + + if (args.length > 0) { + try { + Class.forName(args[0]); + } catch (ClassNotFoundException e) { + Log.w("LoadClass", e); + return; + } + } + + System.gc(); + + int allocCount = Debug.getGlobalAllocCount(); + int allocSize = Debug.getGlobalAllocSize(); + int freedCount = Debug.getGlobalFreedCount(); + int freedSize = Debug.getGlobalFreedSize(); + long nativeHeapSize = Debug.getNativeHeapSize(); + + Debug.stopAllocCounting(); + + StringBuilder response = new StringBuilder("DECAFBAD"); + + int[] pages = new int[6]; + Debug.MemoryInfo memoryInfo = new Debug.MemoryInfo(); + Debug.getMemoryInfo(memoryInfo); + response.append(',').append(memoryInfo.nativeSharedDirty); + response.append(',').append(memoryInfo.dalvikSharedDirty); + response.append(',').append(memoryInfo.otherSharedDirty); + response.append(',').append(memoryInfo.nativePrivateDirty); + response.append(',').append(memoryInfo.dalvikPrivateDirty); + response.append(',').append(memoryInfo.otherPrivateDirty); + + response.append(',').append(allocCount); + response.append(',').append(allocSize); + response.append(',').append(freedCount); + response.append(',').append(freedSize); + response.append(',').append(nativeHeapSize); + + System.out.println(response.toString()); + } + + /** + * Registers native functions. See AndroidRuntime.cpp. + */ + static native int registerNatives(); +} diff --git a/tools/preload/preload.iml b/tools/preload/preload.iml new file mode 100644 index 0000000..d1fab57 --- /dev/null +++ b/tools/preload/preload.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/tools/preload/preload.ipr b/tools/preload/preload.ipr new file mode 100644 index 0000000..c5613ad --- /dev/null +++ b/tools/preload/preload.ipr @@ -0,0 +1,467 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -- cgit v1.1