1/*
2 * Copyright (C) 2013 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.internal.app.procstats;
18
19import android.os.Build;
20import android.os.Parcel;
21import android.util.Slog;
22import libcore.util.EmptyArray;
23
24import java.util.ArrayList;
25import java.util.Arrays;
26
27import com.android.internal.util.GrowingArrayUtils;
28
29/**
30 * Class that contains a set of tables mapping byte ids to long values.
31 *
32 * This class is used to store the ProcessStats data.  This data happens to be
33 * a set of very sparse tables, that is mostly append or overwrite, with infrequent
34 * resets of the data.
35 *
36 * Data is stored as a list of large long[] arrays containing the actual values.  There are a
37 * set of Table objects that each contain a small array mapping the byte IDs to a position
38 * in the larger arrays.
39 *
40 * The data itself is either a single long value or a range of long values which are always
41 * stored continguously in one of the long arrays. When the caller allocates a slot with
42 * getOrAddKey, an int key is returned.  That key can be re-retreived with getKey without
43 * allocating the value.  The data can then be set or retrieved with that key.
44 */
45public class SparseMappingTable {
46    private static final String TAG = "SparseMappingTable";
47
48    // How big each array is.
49    public static final int ARRAY_SIZE = 4096;
50
51    public static final int INVALID_KEY = 0xffffffff;
52
53    // Where the "type"/"state" part of the data appears in an offset integer.
54    private static final int ID_SHIFT = 0;
55    private static final int ID_MASK = 0xff;
56    // Where the "which array" part of the data appears in an offset integer.
57    private static final int ARRAY_SHIFT = 8;
58    private static final int ARRAY_MASK = 0xff;
59    // Where the "index into array" part of the data appears in an offset integer.
60    private static final int INDEX_SHIFT = 16;
61    private static final int INDEX_MASK = 0xffff;
62
63    private int mSequence;
64    private int mNextIndex;
65    private final ArrayList<long[]> mLongs = new ArrayList<long[]>();
66
67    /**
68     * A table of data as stored in a SparseMappingTable.
69     */
70    public static class Table {
71        private SparseMappingTable mParent;
72        private int mSequence = 1;
73        private int[] mTable;
74        private int mSize;
75
76        public Table(SparseMappingTable parent) {
77            mParent = parent;
78            mSequence = parent.mSequence;
79        }
80
81        /**
82         * Pulls the data from 'copyFrom' and stores it in our own longs table.
83         *
84         * @param copyFrom   The Table to copy from
85         * @param valueCount The number of values to copy for each key
86         */
87        public void copyFrom(Table copyFrom, int valueCount) {
88            mTable = null;
89            mSize = 0;
90
91            final int N = copyFrom.getKeyCount();
92            for (int i=0; i<N; i++) {
93                final int theirKey = copyFrom.getKeyAt(i);
94                final long[] theirLongs = copyFrom.mParent.mLongs.get(getArrayFromKey(theirKey));
95
96                final byte id = SparseMappingTable.getIdFromKey(theirKey);
97
98                final int myKey = this.getOrAddKey((byte)id, valueCount);
99                final long[] myLongs = mParent.mLongs.get(getArrayFromKey(myKey));
100
101                System.arraycopy(theirLongs, getIndexFromKey(theirKey),
102                        myLongs, getIndexFromKey(myKey), valueCount);
103            }
104        }
105
106        /**
107         * Allocates data in the buffer, and stores that key in the mapping for this
108         * table.
109         *
110         * @param id    The id of the item (will be used in making the key)
111         * @param count The number of bytes to allocate.  Must be less than
112         *              SparseMappingTable.ARRAY_SIZE.
113         *
114         * @return The 'key' for this data value, which contains both the id itself
115         *         and the location in the long arrays that the data is actually stored
116         *         but should be considered opaque to the caller.
117         */
118        public int getOrAddKey(byte id, int count) {
119            assertConsistency();
120
121            final int idx = binarySearch(id);
122            if (idx >= 0) {
123                // Found
124                return mTable[idx];
125            } else {
126                // Not found. Need to allocate it.
127
128                // Get an array with enough space to store 'count' values.
129                final ArrayList<long[]> list = mParent.mLongs;
130                int whichArray = list.size()-1;
131                long[] array = list.get(whichArray);
132                if (mParent.mNextIndex + count > array.length) {
133                    // if it won't fit then make a new array.
134                    array = new long[ARRAY_SIZE];
135                    list.add(array);
136                    whichArray++;
137                    mParent.mNextIndex = 0;
138                }
139
140                // The key is a combination of whichArray, which index in that array, and
141                // the table value itself, which will be used for lookup
142                final int key = (whichArray << ARRAY_SHIFT)
143                        | (mParent.mNextIndex << INDEX_SHIFT)
144                        | (((int)id) << ID_SHIFT);
145
146                mParent.mNextIndex += count;
147
148                // Store the key in the sparse lookup table for this Table object.
149                mTable = GrowingArrayUtils.insert(mTable != null ? mTable : EmptyArray.INT,
150                        mSize, ~idx, key);
151                mSize++;
152
153                return key;
154            }
155        }
156
157        /**
158         * Looks up a key in the table.
159         *
160         * @return The key from this table or INVALID_KEY if the id is not found.
161         */
162        public int getKey(byte id) {
163            assertConsistency();
164
165            final int idx = binarySearch(id);
166            if (idx >= 0) {
167                return mTable[idx];
168            } else {
169                return INVALID_KEY;
170            }
171        }
172
173        /**
174         * Get the value for the given key and offset from that key.
175         *
176         * @param key   A key as obtained from getKey or getOrAddKey.
177         * @param value The value to set.
178         */
179        public long getValue(int key) {
180            return getValue(key, 0);
181        }
182
183        /**
184         * Get the value for the given key and offset from that key.
185         *
186         * @param key   A key as obtained from getKey or getOrAddKey.
187         * @param index The offset from that key.  Must be less than the count
188         *              provided to getOrAddKey when the space was allocated.
189         * @param value The value to set.
190         *
191         * @return the value, or 0 in case of an error
192         */
193        public long getValue(int key, int index) {
194            assertConsistency();
195
196            try {
197                final long[] array = mParent.mLongs.get(getArrayFromKey(key));
198                return array[getIndexFromKey(key) + index];
199            } catch (IndexOutOfBoundsException ex) {
200                logOrThrow("key=0x" + Integer.toHexString(key)
201                        + " index=" + index + " -- " + dumpInternalState(), ex);
202                return 0;
203            }
204        }
205
206        /**
207         * Set the value for the given id at offset 0 from that id.
208         * If the id is not found, return 0 instead.
209         *
210         * @param id    The id of the item.
211         */
212        public long getValueForId(byte id) {
213            return getValueForId(id, 0);
214        }
215
216        /**
217         * Set the value for the given id and index offset from that id.
218         * If the id is not found, return 0 instead.
219         *
220         * @param id    The id of the item.
221         * @param index The offset from that key.  Must be less than the count
222         *              provided to getOrAddKey when the space was allocated.
223         */
224        public long getValueForId(byte id, int index) {
225            assertConsistency();
226
227            final int idx = binarySearch(id);
228            if (idx >= 0) {
229                final int key = mTable[idx];
230                try {
231                    final long[] array = mParent.mLongs.get(getArrayFromKey(key));
232                    return array[getIndexFromKey(key) + index];
233                } catch (IndexOutOfBoundsException ex) {
234                    logOrThrow("id=0x" + Integer.toHexString(id) + " idx=" + idx
235                            + " key=0x" + Integer.toHexString(key) + " index=" + index
236                            + " -- " + dumpInternalState(), ex);
237                    return 0;
238                }
239            } else {
240                return 0;
241            }
242        }
243
244        /**
245         * Return the raw storage long[] for the given key.
246         */
247        public long[] getArrayForKey(int key) {
248            assertConsistency();
249
250            return mParent.mLongs.get(getArrayFromKey(key));
251        }
252
253        /**
254         * Set the value for the given key and offset from that key.
255         *
256         * @param key   A key as obtained from getKey or getOrAddKey.
257         * @param value The value to set.
258         */
259        public void setValue(int key, long value) {
260            setValue(key, 0, value);
261        }
262
263        /**
264         * Set the value for the given key and offset from that key.
265         *
266         * @param key   A key as obtained from getKey or getOrAddKey.
267         * @param index The offset from that key.  Must be less than the count
268         *              provided to getOrAddKey when the space was allocated.
269         * @param value The value to set.
270         */
271        public void setValue(int key, int index, long value) {
272            assertConsistency();
273
274            if (value < 0) {
275                logOrThrow("can't store negative values"
276                        + " key=0x" + Integer.toHexString(key)
277                        + " index=" + index + " value=" + value
278                        + " -- " + dumpInternalState());
279                return;
280            }
281
282            try {
283                final long[] array = mParent.mLongs.get(getArrayFromKey(key));
284                array[getIndexFromKey(key) + index] = value;
285            } catch (IndexOutOfBoundsException ex) {
286                logOrThrow("key=0x" + Integer.toHexString(key)
287                        + " index=" + index + " value=" + value
288                        + " -- " + dumpInternalState(), ex);
289                return;
290            }
291        }
292
293        /**
294         * Clear out the table, and reset the sequence numbers so future writes
295         * without allocations will assert.
296         */
297        public void resetTable() {
298            // Clear out our table.
299            mTable = null;
300            mSize = 0;
301
302            // Reset our sequence number.  This will make all read/write calls
303            // start to fail, and then when we re-allocate it will be re-synced
304            // to that of mParent.
305            mSequence = mParent.mSequence;
306        }
307
308        /**
309         * Write the keys stored in the table to the parcel. The parent must
310         * be separately written. Does not save the actual data.
311         */
312        public void writeToParcel(Parcel out) {
313            out.writeInt(mSequence);
314            out.writeInt(mSize);
315            for (int i=0; i<mSize; i++) {
316                out.writeInt(mTable[i]);
317            }
318        }
319
320        /**
321         * Read the keys from the parcel. The parent (with its long array) must
322         * have been previously initialized.
323         */
324        public boolean readFromParcel(Parcel in) {
325            // Read the state
326            mSequence = in.readInt();
327            mSize = in.readInt();
328            if (mSize != 0) {
329                mTable = new int[mSize];
330                for (int i=0; i<mSize; i++) {
331                    mTable[i] = in.readInt();
332                }
333            } else {
334                mTable = null;
335            }
336
337            // Make sure we're all healthy
338            if (validateKeys(true)) {
339                return true;
340            } else {
341                // Clear it out
342                mSize = 0;
343                mTable = null;
344                return false;
345            }
346        }
347
348        /**
349         * Return the number of keys that have been added to this Table.
350         */
351        public int getKeyCount() {
352            return mSize;
353        }
354
355        /**
356         * Get the key at the given index in our table.
357         */
358        public int getKeyAt(int i) {
359            return mTable[i];
360        }
361
362        /**
363         * Throw an exception if one of a variety of internal consistency checks fails.
364         */
365        private void assertConsistency() {
366            // Something with this checking isn't working and is triggering
367            // more problems than it's helping to debug.
368            //   Original bug: b/27045736
369            //   New bug: b/27960286
370            if (false) {
371                // Assert that our sequence number matches mParent's.  If it isn't that means
372                // we have been reset and our.  If our sequence is UNITIALIZED_SEQUENCE, then
373                // it's possible that everything is working fine and we just haven't been
374                // written to since the last resetTable().
375                if (mSequence != mParent.mSequence) {
376                    if (mSequence < mParent.mSequence) {
377                        logOrThrow("Sequence mismatch. SparseMappingTable.reset()"
378                                + " called but not Table.resetTable() -- "
379                                + dumpInternalState());
380                        return;
381                    } else if (mSequence > mParent.mSequence) {
382                        logOrThrow("Sequence mismatch. Table.resetTable()"
383                                + " called but not SparseMappingTable.reset() -- "
384                                + dumpInternalState());
385                        return;
386                    }
387                }
388            }
389        }
390
391        /**
392         * Finds the 'id' inside the array of length size (physical size of the array
393         * is not used).
394         *
395         * @return The index of the array, or the bitwise not (~index) of where it
396         * would go if you wanted to insert 'id' into the array.
397         */
398        private int binarySearch(byte id) {
399            int lo = 0;
400            int hi = mSize - 1;
401
402            while (lo <= hi) {
403                int mid = (lo + hi) >>> 1;
404                byte midId = (byte)((mTable[mid] >> ID_SHIFT) & ID_MASK);
405
406                if (midId < id) {
407                    lo = mid + 1;
408                } else if (midId > id) {
409                    hi = mid - 1;
410                } else {
411                    return mid;  // id found
412                }
413            }
414            return ~lo;  // id not present
415        }
416
417        /**
418         * Check that all the keys are valid locations in the long arrays.
419         *
420         * If any aren't, log it and return false. Else return true.
421         */
422        private boolean validateKeys(boolean log) {
423            ArrayList<long[]> longs = mParent.mLongs;
424            final int longsSize = longs.size();
425
426            final int N = mSize;
427            for (int i=0; i<N; i++) {
428                final int key = mTable[i];
429                final int arrayIndex = getArrayFromKey(key);
430                final int index = getIndexFromKey(key);
431                if (arrayIndex >= longsSize || index >= longs.get(arrayIndex).length) {
432                    if (log) {
433                        Slog.w(TAG, "Invalid stats at index " + i + " -- " + dumpInternalState());
434                    }
435                    return false;
436                }
437            }
438
439            return true;
440        }
441
442        public String dumpInternalState() {
443            StringBuilder sb = new StringBuilder();
444            sb.append("SparseMappingTable.Table{mSequence=");
445            sb.append(mSequence);
446            sb.append(" mParent.mSequence=");
447            sb.append(mParent.mSequence);
448            sb.append(" mParent.mLongs.size()=");
449            sb.append(mParent.mLongs.size());
450            sb.append(" mSize=");
451            sb.append(mSize);
452            sb.append(" mTable=");
453            if (mTable == null) {
454                sb.append("null");
455            } else {
456                final int N = mTable.length;
457                sb.append('[');
458                for (int i=0; i<N; i++) {
459                    final int key = mTable[i];
460                    sb.append("0x");
461                    sb.append(Integer.toHexString((key >> ID_SHIFT) & ID_MASK));
462                    sb.append("/0x");
463                    sb.append(Integer.toHexString((key >> ARRAY_SHIFT) & ARRAY_MASK));
464                    sb.append("/0x");
465                    sb.append(Integer.toHexString((key >> INDEX_SHIFT) & INDEX_MASK));
466                    if (i != N-1) {
467                        sb.append(", ");
468                    }
469                }
470                sb.append(']');
471            }
472            sb.append(" clazz=");
473            sb.append(getClass().getName());
474            sb.append('}');
475
476            return sb.toString();
477        }
478    }
479
480    public SparseMappingTable() {
481        mLongs.add(new long[ARRAY_SIZE]);
482    }
483
484    /**
485     * Wipe out all the data.
486     */
487    public void reset() {
488        // Clear out mLongs, and prime it with a new array of data
489        mLongs.clear();
490        mLongs.add(new long[ARRAY_SIZE]);
491        mNextIndex = 0;
492
493        // Increment out sequence counter, because all of the tables will
494        // now be out of sync with the data.
495        mSequence++;
496    }
497
498    /**
499     * Write the data arrays to the parcel.
500     */
501    public void writeToParcel(Parcel out) {
502        out.writeInt(mSequence);
503        out.writeInt(mNextIndex);
504        final int N = mLongs.size();
505        out.writeInt(N);
506        for (int i=0; i<N-1; i++) {
507            final long[] array = mLongs.get(i);
508            out.writeInt(array.length);
509            writeCompactedLongArray(out, array, array.length);
510        }
511        // save less for the last one. upon re-loading they'll just start a new array.
512        final long[] lastLongs = mLongs.get(N-1);
513        out.writeInt(mNextIndex);
514        writeCompactedLongArray(out, lastLongs, mNextIndex);
515    }
516
517    /**
518     * Read the data arrays from the parcel.
519     */
520    public void readFromParcel(Parcel in) {
521        mSequence = in.readInt();
522        mNextIndex = in.readInt();
523
524        mLongs.clear();
525        final int N = in.readInt();
526        for (int i=0; i<N; i++) {
527            final int size = in.readInt();
528            final long[] array = new long[size];
529            readCompactedLongArray(in, array, size);
530            mLongs.add(array);
531        }
532    }
533
534    /**
535     * Return a string for debugging.
536     */
537    public String dumpInternalState(boolean includeData) {
538        final StringBuilder sb = new StringBuilder();
539        sb.append("SparseMappingTable{");
540        sb.append("mSequence=");
541        sb.append(mSequence);
542        sb.append(" mNextIndex=");
543        sb.append(mNextIndex);
544        sb.append(" mLongs.size=");
545        final int N = mLongs.size();
546        sb.append(N);
547        sb.append("\n");
548        if (includeData) {
549            for (int i=0; i<N; i++) {
550                final long[] array = mLongs.get(i);
551                for (int j=0; j<array.length; j++) {
552                    if (i == N-1 && j == mNextIndex) {
553                        break;
554                    }
555                    sb.append(String.format(" %4d %d 0x%016x %-19d\n", i, j, array[j], array[j]));
556                }
557            }
558        }
559        sb.append("}");
560        return sb.toString();
561    }
562
563    /**
564     * Write the long array to the parcel in a compacted form.  Does not allow negative
565     * values in the array.
566     */
567    private static void writeCompactedLongArray(Parcel out, long[] array, int num) {
568        for (int i=0; i<num; i++) {
569            long val = array[i];
570            if (val < 0) {
571                Slog.w(TAG, "Time val negative: " + val);
572                val = 0;
573            }
574            if (val <= Integer.MAX_VALUE) {
575                out.writeInt((int)val);
576            } else {
577                int top = ~((int)((val>>32)&0x7fffffff));
578                int bottom = (int)(val&0x0ffffffffL);
579                out.writeInt(top);
580                out.writeInt(bottom);
581            }
582        }
583    }
584
585    /**
586     * Read the compacted array into the long[].
587     */
588    private static void readCompactedLongArray(Parcel in, long[] array, int num) {
589        final int alen = array.length;
590        if (num > alen) {
591            logOrThrow("bad array lengths: got " + num + " array is " + alen);
592            return;
593        }
594        int i;
595        for (i=0; i<num; i++) {
596            int val = in.readInt();
597            if (val >= 0) {
598                array[i] = val;
599            } else {
600                int bottom = in.readInt();
601                array[i] = (((long)~val)<<32) | bottom;
602            }
603        }
604        while (i < alen) {
605            array[i] = 0;
606            i++;
607        }
608    }
609
610    /**
611     * Extract the id from a key.
612     */
613    public static byte getIdFromKey(int key) {
614        return (byte)((key >> ID_SHIFT) & ID_MASK);
615    }
616
617    /**
618     * Gets the index of the array in the list of arrays.
619     *
620     * Not to be confused with getIndexFromKey.
621     */
622    public static int getArrayFromKey(int key) {
623        return (key >> ARRAY_SHIFT) & ARRAY_MASK;
624    }
625
626    /**
627     * Gets the index of a value in a long[].
628     *
629     * Not to be confused with getArrayFromKey.
630     */
631    public static int getIndexFromKey(int key) {
632        return (key >> INDEX_SHIFT) & INDEX_MASK;
633    }
634
635    /**
636     * Do a Slog.wtf or throw an exception (thereby crashing the system process if
637     * this is a debug build.)
638     */
639    private static void logOrThrow(String message) {
640        logOrThrow(message, new RuntimeException("Stack trace"));
641    }
642
643    /**
644     * Do a Slog.wtf or throw an exception (thereby crashing the system process if
645     * this is an eng build.)
646     */
647    private static void logOrThrow(String message, Throwable th) {
648        Slog.e(TAG, message, th);
649        if (Build.TYPE.equals("eng")) {
650            throw new RuntimeException(message, th);
651        }
652    }
653}
654