1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.pm;
18
19import android.util.ArrayMap;
20import android.util.AtomicFile;
21import android.util.Log;
22
23import com.android.internal.util.FastPrintWriter;
24import com.android.internal.util.IndentingPrintWriter;
25
26import libcore.io.IoUtils;
27
28import java.io.BufferedReader;
29import java.io.File;
30import java.io.FileNotFoundException;
31import java.io.FileOutputStream;
32import java.io.IOException;
33import java.io.InputStreamReader;
34import java.io.OutputStreamWriter;
35import java.io.Reader;
36import java.io.Writer;
37import java.util.HashMap;
38import java.util.Map;
39
40/**
41 * A class that collects, serializes and deserializes compiler-related statistics on a
42 * per-package per-code-path basis.
43 *
44 * Currently used to track compile times.
45 */
46class CompilerStats extends AbstractStatsBase<Void> {
47
48    private final static String COMPILER_STATS_VERSION_HEADER = "PACKAGE_MANAGER__COMPILER_STATS__";
49    private final static int COMPILER_STATS_VERSION = 1;
50
51    /**
52     * Class to collect all stats pertaining to one package.
53     */
54    static class PackageStats {
55
56        private final String packageName;
57
58        /**
59         * This map stores compile-times for all code paths in the package. The value
60         * is in milliseconds.
61         */
62        private final Map<String, Long> compileTimePerCodePath;
63
64        /**
65         * @param packageName
66         */
67        public PackageStats(String packageName) {
68            this.packageName = packageName;
69            // We expect at least one element in here, but let's make it minimal.
70            compileTimePerCodePath = new ArrayMap<>(2);
71        }
72
73        public String getPackageName() {
74            return packageName;
75        }
76
77        /**
78         * Return the recorded compile time for a given code path. Returns
79         * 0 if there is no recorded time.
80         */
81        public long getCompileTime(String codePath) {
82            String storagePath = getStoredPathFromCodePath(codePath);
83            synchronized (compileTimePerCodePath) {
84                Long l = compileTimePerCodePath.get(storagePath);
85                if (l == null) {
86                    return 0;
87                }
88                return l;
89            }
90        }
91
92        public void setCompileTime(String codePath, long compileTimeInMs) {
93            String storagePath = getStoredPathFromCodePath(codePath);
94            synchronized (compileTimePerCodePath) {
95                if (compileTimeInMs <= 0) {
96                    compileTimePerCodePath.remove(storagePath);
97                } else {
98                    compileTimePerCodePath.put(storagePath, compileTimeInMs);
99                }
100            }
101        }
102
103        private static String getStoredPathFromCodePath(String codePath) {
104            int lastSlash = codePath.lastIndexOf(File.separatorChar);
105            return codePath.substring(lastSlash + 1);
106        }
107
108        public void dump(IndentingPrintWriter ipw) {
109            synchronized (compileTimePerCodePath) {
110                if (compileTimePerCodePath.size() == 0) {
111                    ipw.println("(No recorded stats)");
112                } else {
113                    for (Map.Entry<String, Long> e : compileTimePerCodePath.entrySet()) {
114                        ipw.println(" " + e.getKey() + " - " + e.getValue());
115                    }
116                }
117            }
118        }
119    }
120
121    private final Map<String, PackageStats> packageStats;
122
123    public CompilerStats() {
124        super("package-cstats.list", "CompilerStats_DiskWriter", /* lock */ false);
125        packageStats = new HashMap<>();
126    }
127
128    public PackageStats getPackageStats(String packageName) {
129        synchronized (packageStats) {
130            return packageStats.get(packageName);
131        }
132    }
133
134    public void setPackageStats(String packageName, PackageStats stats) {
135        synchronized (packageStats) {
136            packageStats.put(packageName, stats);
137        }
138    }
139
140    public PackageStats createPackageStats(String packageName) {
141        synchronized (packageStats) {
142            PackageStats newStats = new PackageStats(packageName);
143            packageStats.put(packageName, newStats);
144            return newStats;
145        }
146    }
147
148    public PackageStats getOrCreatePackageStats(String packageName) {
149        synchronized (packageStats) {
150            PackageStats existingStats = packageStats.get(packageName);
151            if (existingStats != null) {
152                return existingStats;
153            }
154
155            return createPackageStats(packageName);
156        }
157    }
158
159    public void deletePackageStats(String packageName) {
160        synchronized (packageStats) {
161            packageStats.remove(packageName);
162        }
163    }
164
165    // I/O
166
167    // The encoding is simple:
168    //
169    // 1) The first line is a line consisting of the version header and the version number.
170    //
171    // 2) The rest of the file is package data.
172    // 2.1) A package is started by any line not starting with "-";
173    // 2.2) Any line starting with "-" is code path data. The format is:
174    //      '-'{code-path}':'{compile-time}
175
176    public void write(Writer out) {
177        @SuppressWarnings("resource")
178        FastPrintWriter fpw = new FastPrintWriter(out);
179
180        fpw.print(COMPILER_STATS_VERSION_HEADER);
181        fpw.println(COMPILER_STATS_VERSION);
182
183        synchronized (packageStats) {
184            for (PackageStats pkg : packageStats.values()) {
185                synchronized (pkg.compileTimePerCodePath) {
186                    if (!pkg.compileTimePerCodePath.isEmpty()) {
187                        fpw.println(pkg.getPackageName());
188
189                        for (Map.Entry<String, Long> e : pkg.compileTimePerCodePath.entrySet()) {
190                            fpw.println("-" + e.getKey() + ":" + e.getValue());
191                        }
192                    }
193                }
194            }
195        }
196
197        fpw.flush();
198    }
199
200    public boolean read(Reader r) {
201        synchronized (packageStats) {
202            // TODO: Could make this a final switch, then we wouldn't have to synchronize over
203            //       the whole reading.
204            packageStats.clear();
205
206            try {
207                BufferedReader in = new BufferedReader(r);
208
209                // Read header, do version check.
210                String versionLine = in.readLine();
211                if (versionLine == null) {
212                    throw new IllegalArgumentException("No version line found.");
213                } else {
214                    if (!versionLine.startsWith(COMPILER_STATS_VERSION_HEADER)) {
215                        throw new IllegalArgumentException("Invalid version line: " + versionLine);
216                    }
217                    int version = Integer.parseInt(
218                            versionLine.substring(COMPILER_STATS_VERSION_HEADER.length()));
219                    if (version != COMPILER_STATS_VERSION) {
220                        // TODO: Upgrade older formats? For now, just reject and regenerate.
221                        throw new IllegalArgumentException("Unexpected version: " + version);
222                    }
223                }
224
225                // For simpler code, we ignore any data lines before the first package. We
226                // collect it in a fake package.
227                PackageStats currentPackage = new PackageStats("fake package");
228
229                String s = null;
230                while ((s = in.readLine()) != null) {
231                    if (s.startsWith("-")) {
232                        int colonIndex = s.indexOf(':');
233                        if (colonIndex == -1 || colonIndex == 1) {
234                            throw new IllegalArgumentException("Could not parse data " + s);
235                        }
236                        String codePath = s.substring(1, colonIndex);
237                        long time = Long.parseLong(s.substring(colonIndex + 1));
238                        currentPackage.setCompileTime(codePath, time);
239                    } else {
240                        currentPackage = getOrCreatePackageStats(s);
241                    }
242                }
243            } catch (Exception e) {
244                Log.e(PackageManagerService.TAG, "Error parsing compiler stats", e);
245                return false;
246            }
247
248            return true;
249        }
250    }
251
252    void writeNow() {
253        writeNow(null);
254    }
255
256    boolean maybeWriteAsync() {
257        return maybeWriteAsync(null);
258    }
259
260    @Override
261    protected void writeInternal(Void data) {
262        AtomicFile file = getFile();
263        FileOutputStream f = null;
264
265        try {
266            f = file.startWrite();
267            OutputStreamWriter osw = new OutputStreamWriter(f);
268            write(osw);
269            osw.flush();
270            file.finishWrite(f);
271        } catch (IOException e) {
272            if (f != null) {
273                file.failWrite(f);
274            }
275            Log.e(PackageManagerService.TAG, "Failed to write compiler stats", e);
276        }
277    }
278
279    void read() {
280        read((Void)null);
281    }
282
283    @Override
284    protected void readInternal(Void data) {
285        AtomicFile file = getFile();
286        BufferedReader in = null;
287        try {
288            in = new BufferedReader(new InputStreamReader(file.openRead()));
289            read(in);
290        } catch (FileNotFoundException expected) {
291        } finally {
292            IoUtils.closeQuietly(in);
293        }
294    }
295}
296