The case of the not so ConcurrentDictionary
I was looking at our DevOps dashboards and saw some really weird patterns:
So I pinged my colleague who owns this service and he noticed it was actual very predictable:
Like clockwork, once a minute - he went further and got a PerfView which showed high contention on a newly added ConcurrentDictionary:
He then asked me to take a look since that ConcurrentDictionary was added on my suggestion to work around another issue (which I will blog about one day). Having had that problem before, I figured we either had a hot spot or a hashing function problem - so I got a dump of the process to see which (I could have saved some time and looked at the source...but as they say, there's nothing like a good dump).
0:000> !do 0000015d19e676f0
Name: System.Collections.Concurrent.ConcurrentDictionary`2
MethodTable: 00007ff8e18f9728
EEClass: 00007ff8e18c5de0
Size: 64(0x40) bytes
File: D:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ff8e187d930 4001830 8 ....Byte, mscorlib]] 0 instance 0000015e3a819078 m_tables
00007ff93b3c4300 4001831 10 ...Canon, mscorlib]] 0 instance 0000000000000000 m_comparer
00007ff93b3b1f28 4001832 30 System.Boolean 1 instance 1 m_growLockArray
00007ff93b3a9288 4001833 20 System.Int32 1 instance 0 m_keyRehashCount
00007ff93b3a9288 4001834 24 System.Int32 1 instance 256 m_budget
0000000000000000 4001835 18 SZARRAY 0 instance 0000000000000000 m_serializationArray
00007ff93b3a9288 4001836 28 System.Int32 1 instance 0 m_serializationConcurrencyLevel
00007ff93b3a9288 4001837 2c System.Int32 1 instance 0 m_serializationCapacity
00007ff93b3b1f28 400183b 10 System.Boolean 1 static <no information>
0:000> !do 0000015e3a819078
Name: System.Collections.Concurrent.ConcurrentDictionary`2+Tables
MethodTable: 00007ff8e18fafd0
EEClass: 00007ff8e18c6ab0
Size: 48(0x30) bytes
File: D:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
0000000000000000 400341d 8 SZARRAY 0 instance 0000015e3a816ca8 m_buckets
00007ff93b3a6fc0 400341e 10 System.Object[] 0 instance 0000015e3a816290 m_locks
00007ff93b3a9220 400341f 18 System.Int32[] 0 instance 0000015e3a817e28 m_countPerLock
00007ff93b3c4300 4003420 20...Canon, mscorlib]] 0 instance 0000015d19e677a0 m_comparer
0:000> !DumpArray 0000015e3a816ca8
Name: System.Collections.Concurrent.ConcurrentDictionary`2+Node
MethodTable: 00007ff8e18fae50
EEClass: 00007ff93ad6aa00
Size: 4480(0x1180) bytes
Array: Rank 1, Number of elements 557, Type CLASS
Element Methodtable: 00007ff8e18fad88
[0] null
[1] null
<...>
[428] null
[429] 0000015d46de8280
[430] null
<...>
[556] null
0:000> !do 0000015d46de8280
Name: System.Collections.Concurrent.ConcurrentDictionary`2+Node
MethodTable: 00007ff8e18fad88
EEClass: 00007ff8e18c6990
Size: 40(0x28) bytes
File: D:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ff93b3abf10 4003421 8 System.__Canon 0 instance 0000015d46de8240 m_key
00007ff93b3a8940 4003422 1c System.Byte 1 instance 0 m_value
00007ff8e187d800 4003423 10 ....Byte, mscorlib]] 0 instance 0000015e47c672e8 m_next
00007ff93b3a9288 4003424 18 System.Int32 1 instance 37103870 m_hashcode
0:000> !do 0000015e47c672e8
Name: System.Collections.Concurrent.ConcurrentDictionary`2+Node
MethodTable: 00007ff8e18fad88
EEClass: 00007ff8e18c6990
Size: 40(0x28) bytes
File: D:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ff93b3abf10 4003421 8 System.__Canon 0 instance 0000015e47c672a8 m_key
00007ff93b3a8940 4003422 1c System.Byte 1 instance 0 m_value
00007ff8e187d800 4003423 10 ....Byte, mscorlib]] 0 instance 0000015e4781f3f8 m_next
00007ff93b3a9288 4003424 18 System.Int32 1 instance 37103870 m_hashcode
Sure enough, there's only one bucket occupied, and all of the items in that bucket have the same hash code, so our ConcurrentDictionary is really a giant linked list, with a giant lock on top...
Our ConcurrentDictionary's keys are System.EventHandler which is really a delegate - which default HashCode implementation is...the hash code of the underlying type which means all of our delegates have the same hashcode, hence the same bucket....DOH!