aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/collect/nestedset/DigestMap.java
blob: 30d53f0a14fb6dbfd22e9208b16465e6f275d954 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
// Copyright 2018 The Bazel Authors. All rights reserved.
//
// 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.
package com.google.devtools.build.lib.collect.nestedset;

import com.google.common.base.Preconditions;
import com.google.devtools.build.lib.util.Fingerprint;
import com.google.devtools.build.lib.vfs.DigestHashFunction;
import com.google.devtools.build.lib.vfs.DigestHashFunction.DigestLength;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.concurrent.locks.StampedLock;

/**
 * Map of key -> [digest bytes].
 *
 * <p>This class uses a single array of keys and a big single block of bytes. To read/store digests
 * we index straight into the byte array. This is more memory-efficient and uses less GC than a
 * corresponding Map<Object, byte[]>.
 *
 * <p>Keys use reference equality.
 *
 * <p>Reading is lock free. During writes a read lock is taken. If we need to resize the table, a
 * write lock is taken to flush all the readers and writers before the table is resized.
 */
final class DigestMap {
  private static final Object INSERTION_IN_PROGRESS = new Object();
  private final DigestLength digestLength;
  private final StampedLock readWriteLock = new StampedLock();

  static class Table {
    final int tableSize;
    final int nextResize;
    final AtomicReferenceArray<Object> keys;
    final byte[] bytes;

    Table(int tableSize, int digestLength) {
      this.tableSize = tableSize;
      this.nextResize = getNextResize(tableSize);
      this.keys = new AtomicReferenceArray<>(tableSize);
      this.bytes = new byte[tableSize * digestLength];
    }
  }

  private volatile Table table;
  private final AtomicInteger allocatedSlots;

  DigestMap(DigestHashFunction digestHashFunction, int initialSize) {
    Preconditions.checkArgument(
        initialSize > 0 && (initialSize & (initialSize - 1)) == 0,
        "initialSize must be a power of 2 greater than 0");
    this.digestLength = digestHashFunction.getDigestLength();
    this.table = new Table(initialSize, digestLength.getDigestMaximumLength());
    this.allocatedSlots = new AtomicInteger();
  }

  /** Finds the digest for the corresponding key and adds it to the passed fingerprint. */
  boolean readDigest(Object key, Fingerprint fingerprint) {
    Table table = this.table; // Read once for duration of method
    int index = findKey(table, key);
    if (index >= 0) {
      int offset = index * this.digestLength.getDigestMaximumLength();
      int digestLength = this.digestLength.getDigestLength(table.bytes, offset);
      fingerprint.addBytes(table.bytes, offset, digestLength);
      return true;
    }
    return false;
  }

  private static int findKey(Table table, Object key) {
    int hash = hash(key);
    int index = hash & (table.tableSize - 1);
    while (true) {
      Object currentKey = table.keys.get(index);
      if (currentKey == key) {
        return index;
      } else if (currentKey == null) {
        return -1;
      }
      index = probe(index, table.tableSize);
    }
  }

  /**
   * Inserts a digest for the corresponding key, then immediately reads it into another fingerprint.
   *
   * <p>This is equivalent to <code>
   * digestMap.insertDigest(key, digest.digestAndReset(); digestMap.readDigest(key, readTo); </code>
   * but it will be faster.
   *
   * @param key The key to insert.
   * @param digest The fingerprint to insert. This will reset the fingerprint instance.
   * @param readTo A fingerprint to read the just-added fingerprint into.
   */
  void insertAndReadDigest(Object key, Fingerprint digest, Fingerprint readTo) {
    // Check if we have to resize the table first and do that under write lock
    // We assume that we are going to insert an item. If we do not do this, multiple
    // threads could race and all think they do not need to resize, then some get stuck
    // trying to insert the item.
    Table table = this.table;
    if (allocatedSlots.incrementAndGet() >= table.nextResize) {
      long resizeLock = readWriteLock.writeLock();
      try {
        // Guard against race to make sure only one thread resizes
        if (table == this.table) {
          resizeTableWriteLocked();
        }
      } finally {
        readWriteLock.unlockWrite(resizeLock);
      }
    }
    final int index;
    long stamp = readWriteLock.readLock();
    try {
      table = this.table; // Grab the table again under read lock
      index = insertKey(table, key, digest);
    } finally {
      readWriteLock.unlockRead(stamp);
    }
    // This can be done outside of the read lock since the slot is immutable once inserted
    int offset = index * this.digestLength.getDigestMaximumLength();
    int digestLength = this.digestLength.getDigestLength(table.bytes, offset);
    readTo.addBytes(table.bytes, offset, digestLength);
  }

  // Inserts a key into the passed table and returns the index.
  @SuppressWarnings("ThreadPriorityCheck") // We're not relying on thread scheduler for correctness
  private int insertKey(Table table, Object key, Fingerprint digest) {
    int hash = hash(key);
    int index = hash & (table.tableSize - 1);
    while (true) {
      Object currentKey = table.keys.get(index);
      if (currentKey == null) {
        if (!table.keys.compareAndSet(index, null, INSERTION_IN_PROGRESS)) {
          // We raced to insert a key in a free slot, retry this slot in case it's the same key.
          // Failure to do so could lead to a double insertion.
          continue;
        }
        digest.digestAndReset(
            table.bytes,
            index * digestLength.getDigestMaximumLength(),
            digestLength.getDigestMaximumLength());
        table.keys.set(index, key);
        return index;
      } else if (currentKey == key) {
        // Key is already present, give back the slot allocation
        allocatedSlots.decrementAndGet();
        return index;
      } else if (currentKey == INSERTION_IN_PROGRESS) {
        // We are in the progress of inserting an item in this slot, but we don't yet know
        // what the item is. Since it could be an insertion of ourselves we need to wait
        // until done to avoid double insertion. We yield the thread in case the other
        // thread is stuck between insertion and completion.
        Thread.yield();
        continue;
      }
      index = probe(index, table.tableSize);
    }
  }

  private void resizeTableWriteLocked() {
    int digestSize = this.digestLength.getDigestMaximumLength();
    Table oldTable = this.table;
    Table newTable = new Table(oldTable.tableSize * 2, digestSize);
    for (int i = 0; i < oldTable.tableSize; ++i) {
      Object key = oldTable.keys.get(i);
      if (key != null) {
        int newIndex = firstFreeIndex(newTable.keys, newTable.tableSize, key);
        newTable.keys.set(newIndex, key);
        System.arraycopy(
            oldTable.bytes, i * digestSize, newTable.bytes, newIndex * digestSize, digestSize);
      }
    }
    this.table = newTable;
  }

  private static int firstFreeIndex(AtomicReferenceArray<Object> keys, int tableSize, Object key) {
    int hash = hash(key);
    int index = hash & (tableSize - 1);
    while (true) {
      Object currentKey = keys.get(index);
      if (currentKey == null) {
        return index;
      }
      index = probe(index, tableSize);
    }
  }

  private static int hash(Object key) {
    return smear(System.identityHashCode(key));
  }

  private static int probe(int index, int tableSize) {
    return (index + 1) & (tableSize - 1);
  }

  private static int getNextResize(int newTableSize) {
    // 75% load
    return (newTableSize * 3) / 4;
  }

  /*
   * This method was rewritten in Java from an intermediate step of the Murmur hash function in
   * http://code.google.com/p/smhasher/source/browse/trunk/MurmurHash3.cpp, which contained the
   * following header:
   *
   * MurmurHash3 was written by Austin Appleby, and is placed in the public domain. The author
   * hereby disclaims copyright to this source code.
   */
  private static int smear(int hashCode) {
    return 0x1b873593 * Integer.rotateLeft(hashCode * 0xcc9e2d51, 15);
  }
}