@@ -290,12 +290,66 @@ func (v *view) hashChangedNodes(ctx context.Context) {
290
290
// Calculates the ID of all descendants of [n] which need to be recalculated,
291
291
// and then calculates the ID of [n] itself.
292
292
func (v * view ) hashChangedNode (n * node ) ids.ID {
293
- // We use [wg] to wait until all descendants of [n] have been updated.
294
- var wg sync.WaitGroup
293
+ // If there are no children, we can avoid allocating [keyBuffer].
294
+ if len (n .children ) == 0 {
295
+ return n .calculateID (v .db .metrics )
296
+ }
297
+
298
+ // Calculate the size of the largest child key of this node. This allows
299
+ // only allocating a single slice for all of the keys.
300
+ var maxChildBitLength int
301
+ for _ , childEntry := range n .children {
302
+ maxChildBitLength = max (maxChildBitLength , childEntry .compressedKey .length )
303
+ }
304
+
305
+ var (
306
+ maxBytesNeeded = bytesNeeded (n .key .length + v .tokenSize + maxChildBitLength )
307
+ // keyBuffer is allocated onto the heap because it is dynamically sized.
308
+ keyBuffer = make ([]byte , maxBytesNeeded )
309
+ // childBuffer is allocated on the stack.
310
+ childBuffer = make ([]byte , 1 )
311
+ dualIndex = dualBitIndex (v .tokenSize )
312
+ bytesForKey = bytesNeeded (n .key .length )
313
+ // We track the last byte of [n.key] so that we can reset the value for
314
+ // each key. This is needed because the child buffer may get ORed at
315
+ // this byte.
316
+ lastKeyByte byte
317
+
318
+ // We use [wg] to wait until all descendants of [n] have been updated.
319
+ wg sync.WaitGroup
320
+ )
321
+
322
+ if bytesForKey > 0 {
323
+ // We only need to copy this node's key once because it does not change
324
+ // as we iterate over the children.
325
+ copy (keyBuffer , n .key .value )
326
+ lastKeyByte = keyBuffer [bytesForKey - 1 ]
327
+ }
295
328
296
329
for childIndex , childEntry := range n .children {
297
- childEntry := childEntry // New variable so goroutine doesn't capture loop variable.
298
- childKey := n .key .Extend (ToToken (childIndex , v .tokenSize ), childEntry .compressedKey )
330
+ childBuffer [0 ] = childIndex << dualIndex
331
+ childIndexAsKey := Key {
332
+ // It is safe to use byteSliceToString because [childBuffer] is not
333
+ // modified while [childIndexAsKey] is in use.
334
+ value : byteSliceToString (childBuffer ),
335
+ length : v .tokenSize ,
336
+ }
337
+
338
+ totalBitLength := n .key .length + v .tokenSize + childEntry .compressedKey .length
339
+ buffer := keyBuffer [:bytesNeeded (totalBitLength )]
340
+ // Make sure the last byte of the key is originally set correctly
341
+ if bytesForKey > 0 {
342
+ buffer [bytesForKey - 1 ] = lastKeyByte
343
+ }
344
+ extendIntoBuffer (buffer , childIndexAsKey , n .key .length )
345
+ extendIntoBuffer (buffer , childEntry .compressedKey , n .key .length + v .tokenSize )
346
+ childKey := Key {
347
+ // It is safe to use byteSliceToString because [buffer] is not
348
+ // modified while [childKey] is in use.
349
+ value : byteSliceToString (buffer ),
350
+ length : totalBitLength ,
351
+ }
352
+
299
353
childNodeChange , ok := v .changes .nodes [childKey ]
300
354
if ! ok {
301
355
// This child wasn't changed.
@@ -306,11 +360,11 @@ func (v *view) hashChangedNode(n *node) ids.ID {
306
360
// Try updating the child and its descendants in a goroutine.
307
361
if ok := v .db .hashNodesSema .TryAcquire (1 ); ok {
308
362
wg .Add (1 )
309
- go func () {
363
+ go func (childEntry * child ) {
310
364
childEntry .id = v .hashChangedNode (childNodeChange .after )
311
365
v .db .hashNodesSema .Release (1 )
312
366
wg .Done ()
313
- }()
367
+ }(childEntry )
314
368
} else {
315
369
// We're at the goroutine limit; do the work in this goroutine.
316
370
childEntry .id = v .hashChangedNode (childNodeChange .after )
0 commit comments