memprofiler.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. import sys
  2. import codecs
  3. from collections import namedtuple
  4. import random
  5. import bisect
  6. from distutils.version import StrictVersion
  7. try:
  8. import ujson as json
  9. except:
  10. import json
  11. from rdbtools.parser import RdbCallback
  12. from rdbtools.encodehelpers import bytes_to_unicode
  13. from heapq import heappush, nlargest, heappop
  14. ZSKIPLIST_MAXLEVEL=32
  15. ZSKIPLIST_P=0.25
  16. REDIS_SHARED_INTEGERS = 10000
  17. MemoryRecord = namedtuple('MemoryRecord', ['database', 'type', 'key', 'bytes', 'encoding','size', 'len_largest_element'])
  18. class StatsAggregator(object):
  19. def __init__(self, key_groupings = None):
  20. self.aggregates = {}
  21. self.scatters = {}
  22. self.histograms = {}
  23. def next_record(self, record):
  24. self.add_aggregate('database_memory', record.database, record.bytes)
  25. self.add_aggregate('type_memory', record.type, record.bytes)
  26. self.add_aggregate('encoding_memory', record.encoding, record.bytes)
  27. self.add_aggregate('type_count', record.type, 1)
  28. self.add_aggregate('encoding_count', record.encoding, 1)
  29. self.add_histogram(record.type + "_length", record.size)
  30. self.add_histogram(record.type + "_memory", (record.bytes/10) * 10)
  31. if record.type == 'list':
  32. self.add_scatter('list_memory_by_length', record.bytes, record.size)
  33. elif record.type == 'hash':
  34. self.add_scatter('hash_memory_by_length', record.bytes, record.size)
  35. elif record.type == 'set':
  36. self.add_scatter('set_memory_by_length', record.bytes, record.size)
  37. elif record.type == 'sortedset':
  38. self.add_scatter('sortedset_memory_by_length', record.bytes, record.size)
  39. elif record.type == 'string':
  40. self.add_scatter('string_memory_by_length', record.bytes, record.size)
  41. elif record.type == 'dict':
  42. pass
  43. else:
  44. raise Exception('Invalid data type %s' % record.type)
  45. def add_aggregate(self, heading, subheading, metric):
  46. if not heading in self.aggregates :
  47. self.aggregates[heading] = {}
  48. if not subheading in self.aggregates[heading]:
  49. self.aggregates[heading][subheading] = 0
  50. self.aggregates[heading][subheading] += metric
  51. def add_histogram(self, heading, metric):
  52. if not heading in self.histograms:
  53. self.histograms[heading] = {}
  54. if not metric in self.histograms[heading]:
  55. self.histograms[heading][metric] = 1
  56. else :
  57. self.histograms[heading][metric] += 1
  58. def add_scatter(self, heading, x, y):
  59. if not heading in self.scatters:
  60. self.scatters[heading] = []
  61. self.scatters[heading].append([x, y])
  62. def get_json(self):
  63. return json.dumps({"aggregates":self.aggregates, "scatters":self.scatters, "histograms":self.histograms})
  64. class PrintAllKeys(object):
  65. def __init__(self, out, bytes, largest):
  66. self._bytes = bytes
  67. self._largest = largest
  68. self._out = out
  69. headers = "%s,%s,%s,%s,%s,%s,%s\n" % (
  70. "database", "type", "key", "size_in_bytes", "encoding", "num_elements", "len_largest_element")
  71. self._out.write(codecs.encode(headers, 'latin-1'))
  72. if self._largest is not None:
  73. self._heap = []
  74. def next_record(self, record) :
  75. if record.key is None:
  76. return # some records are not keys (e.g. dict)
  77. if self._largest is None:
  78. if self._bytes is None or record.bytes >= int(self._bytes):
  79. rec_str = "%d,%s,%s,%d,%s,%d,%d\n" % (
  80. record.database, record.type, record.key, record.bytes, record.encoding, record.size,
  81. record.len_largest_element)
  82. self._out.write(codecs.encode(rec_str, 'latin-1'))
  83. else:
  84. heappush(self._heap, (record.bytes, record))
  85. def end_rdb(self):
  86. if self._largest is not None:
  87. self._heap = nlargest(int(self._largest), self._heap)
  88. self._largest = None
  89. while self._heap:
  90. bytes, record = heappop(self._heap)
  91. self.next_record(record)
  92. class PrintJustKeys(object):
  93. def __init__(self, out):
  94. self._out = out
  95. def next_record(self, record):
  96. self._out.write(codecs.encode("%s\n" % record.key, 'latin-1'))
  97. class MemoryCallback(RdbCallback):
  98. '''Calculates the memory used if this rdb file were loaded into RAM
  99. The memory usage is approximate, and based on heuristics.
  100. '''
  101. def __init__(self, stream, architecture, redis_version='3.2', string_escape=None):
  102. super(MemoryCallback, self).__init__(string_escape)
  103. self._stream = stream
  104. self._dbnum = 0
  105. self._current_size = 0
  106. self._current_encoding = None
  107. self._current_length = 0
  108. self._len_largest_element = 0
  109. self._db_keys = 0
  110. self._db_expires = 0
  111. self._aux_used_mem = None
  112. self._aux_redis_ver = None
  113. self._aux_redis_bits = None
  114. self._redis_version = StrictVersion(redis_version)
  115. self._total_internal_frag = 0
  116. if architecture == 64 or architecture == '64':
  117. self._pointer_size = 8
  118. self._long_size = 8
  119. self._architecture = 64
  120. elif architecture == 32 or architecture == '32':
  121. self._pointer_size = 4
  122. self._long_size = 4
  123. self._architecture = 32
  124. def emit_record(self, record_type, key, byte_count, encoding, size, largest_el):
  125. if key is not None:
  126. key = bytes_to_unicode(key, self._escape, skip_printable=True)
  127. record = MemoryRecord(self._dbnum, record_type, key, byte_count, encoding, size, largest_el)
  128. self._stream.next_record(record)
  129. def start_rdb(self):
  130. pass
  131. def aux_field(self, key, value):
  132. #print('aux: %s %s' % (key, value))
  133. if key == 'used-mem':
  134. self._aux_used_mem = int(value)
  135. if key == 'redis-ver':
  136. self._aux_redis_ver = value
  137. if key == 'redis-bits':
  138. self._aux_redis_bits = int(value)
  139. def start_database(self, db_number):
  140. self._dbnum = db_number
  141. self._db_keys = 0
  142. self._db_expires = 0
  143. def end_database(self, db_number):
  144. self.emit_record("dict", None, self.hashtable_overhead(self._db_keys), None, None, None)
  145. self.emit_record("dict", None, self.hashtable_overhead(self._db_expires), None, None, None)
  146. if hasattr(self._stream, 'end_database'):
  147. self._stream.end_database(db_number)
  148. def end_rdb(self):
  149. #print('internal fragmentation: %s' % self._total_internal_frag)
  150. if hasattr(self._stream, 'end_rdb'):
  151. self._stream.end_rdb()
  152. def set(self, key, value, expiry, info):
  153. self._current_encoding = info['encoding']
  154. size = self.top_level_object_overhead(key, expiry) + self.sizeof_string(value)
  155. length = self.element_length(value)
  156. self.emit_record("string", key, size, self._current_encoding, length, length)
  157. self.end_key()
  158. def start_hash(self, key, length, expiry, info):
  159. self._current_encoding = info['encoding']
  160. self._current_length = length
  161. size = self.top_level_object_overhead(key, expiry)
  162. if 'sizeof_value' in info:
  163. size += info['sizeof_value']
  164. elif 'encoding' in info and info['encoding'] == 'hashtable':
  165. size += self.hashtable_overhead(length)
  166. else:
  167. raise Exception('start_hash', 'Could not find encoding or sizeof_value in info object %s' % info)
  168. self._current_size = size
  169. def hset(self, key, field, value):
  170. if(self.element_length(field) > self._len_largest_element) :
  171. self._len_largest_element = self.element_length(field)
  172. if(self.element_length(value) > self._len_largest_element) :
  173. self._len_largest_element = self.element_length(value)
  174. if self._current_encoding == 'hashtable':
  175. self._current_size += self.sizeof_string(field)
  176. self._current_size += self.sizeof_string(value)
  177. self._current_size += self.hashtable_entry_overhead()
  178. if self._redis_version < StrictVersion('4.0'):
  179. self._current_size += 2*self.robj_overhead()
  180. def end_hash(self, key):
  181. self.emit_record("hash", key, self._current_size, self._current_encoding, self._current_length,
  182. self._len_largest_element)
  183. self.end_key()
  184. def start_set(self, key, cardinality, expiry, info):
  185. # A set is exactly like a hashmap
  186. self.start_hash(key, cardinality, expiry, info)
  187. def sadd(self, key, member):
  188. if(self.element_length(member) > self._len_largest_element) :
  189. self._len_largest_element = self.element_length(member)
  190. if self._current_encoding == 'hashtable':
  191. self._current_size += self.sizeof_string(member)
  192. self._current_size += self.hashtable_entry_overhead()
  193. if self._redis_version < StrictVersion('4.0'):
  194. self._current_size += self.robj_overhead()
  195. def end_set(self, key):
  196. self.emit_record("set", key, self._current_size, self._current_encoding, self._current_length,
  197. self._len_largest_element)
  198. self.end_key()
  199. def start_list(self, key, expiry, info):
  200. self._current_length = 0
  201. self._list_items_size = 0
  202. self._list_items_zipped_size = 0
  203. self._current_encoding = info['encoding']
  204. size = self.top_level_object_overhead(key, expiry)
  205. # ignore the encoding in the rdb, and predict the encoding that will be used at the target redis version
  206. if self._redis_version >= StrictVersion('3.2'):
  207. # default configuration of redis 3.2
  208. self._current_encoding = "quicklist"
  209. self._list_max_ziplist_size = 8192 # default is -2 which means 8k
  210. self._list_compress_depth = 0 # currently we only support no compression which is the default
  211. self._cur_zips = 1
  212. self._cur_zip_size = 0
  213. else:
  214. # default configuration fo redis 2.8 -> 3.0
  215. self._current_encoding = "ziplist"
  216. self._list_max_ziplist_entries = 512
  217. self._list_max_ziplist_value = 64
  218. self._current_size = size
  219. def rpush(self, key, value):
  220. self._current_length += 1
  221. size = self.sizeof_string(value) if type(value) != int else 4
  222. if(self.element_length(value) > self._len_largest_element):
  223. self._len_largest_element = self.element_length(value)
  224. if self._current_encoding == "ziplist":
  225. self._list_items_zipped_size += self.ziplist_entry_overhead(value)
  226. if self._current_length > self._list_max_ziplist_entries or size > self._list_max_ziplist_value:
  227. self._current_encoding = "linkedlist"
  228. elif self._current_encoding == "quicklist":
  229. if self._cur_zip_size + size > self._list_max_ziplist_size:
  230. self._cur_zip_size = size
  231. self._cur_zips += 1
  232. else:
  233. self._cur_zip_size += size
  234. self._list_items_zipped_size += self.ziplist_entry_overhead(value)
  235. self._list_items_size += size # not to be used in case of ziplist or quicklist
  236. def end_list(self, key, info):
  237. if self._current_encoding == 'quicklist':
  238. self._current_size += self.quicklist_overhead(self._cur_zips)
  239. self._current_size += self.ziplist_header_overhead() * self._cur_zips
  240. self._current_size += self._list_items_zipped_size
  241. elif self._current_encoding == 'ziplist':
  242. self._current_size += self.ziplist_header_overhead()
  243. self._current_size += self._list_items_zipped_size
  244. else: # linkedlist
  245. self._current_size += self.linkedlist_entry_overhead() * self._current_length
  246. self._current_size += self.linkedlist_overhead()
  247. if self._redis_version < StrictVersion('4.0'):
  248. self._current_size += self.robj_overhead() * self._current_length
  249. self._current_size += self._list_items_size
  250. self.emit_record("list", key, self._current_size, self._current_encoding, self._current_length,
  251. self._len_largest_element)
  252. self.end_key()
  253. def start_module(self, key, module_id, expiry):
  254. self._current_encoding = module_id
  255. self._current_size = self.top_level_object_overhead(key, expiry)
  256. self._current_size += 8 + 1 # add the module id length and EOF byte
  257. return False # don't build the full key buffer
  258. def end_module(self, key, buffer_size, buffer=None):
  259. size = self._current_size + buffer_size
  260. self.emit_record("module", key, size, self._current_encoding, 1, size)
  261. self.end_key()
  262. def start_sorted_set(self, key, length, expiry, info):
  263. self._current_length = length
  264. self._current_encoding = info['encoding']
  265. size = self.top_level_object_overhead(key, expiry)
  266. if 'sizeof_value' in info:
  267. size += info['sizeof_value']
  268. elif 'encoding' in info and info['encoding'] == 'skiplist':
  269. size += self.skiplist_overhead(length)
  270. else:
  271. raise Exception('start_sorted_set', 'Could not find encoding or sizeof_value in info object %s' % info)
  272. self._current_size = size
  273. def zadd(self, key, score, member):
  274. if(self.element_length(member) > self._len_largest_element):
  275. self._len_largest_element = self.element_length(member)
  276. if self._current_encoding == 'skiplist':
  277. self._current_size += 8 # score (double)
  278. self._current_size += self.sizeof_string(member)
  279. if self._redis_version < StrictVersion('4.0'):
  280. self._current_size += self.robj_overhead()
  281. self._current_size += self.skiplist_entry_overhead()
  282. def end_sorted_set(self, key):
  283. self.emit_record("sortedset", key, self._current_size, self._current_encoding, self._current_length,
  284. self._len_largest_element)
  285. self.end_key()
  286. def end_key(self):
  287. self._db_keys += 1
  288. self._current_encoding = None
  289. self._current_size = 0
  290. self._len_largest_element = 0
  291. def sizeof_string(self, string):
  292. # https://github.com/antirez/redis/blob/unstable/src/sds.h
  293. try:
  294. num = int(string)
  295. if num < REDIS_SHARED_INTEGERS :
  296. return 0
  297. else :
  298. return 8
  299. except ValueError:
  300. pass
  301. l = len(string)
  302. if self._redis_version < StrictVersion('3.2'):
  303. return self.malloc_overhead(l + 8 + 1)
  304. if l < 2**5:
  305. return self.malloc_overhead(l + 1 + 1)
  306. if l < 2**8:
  307. return self.malloc_overhead(l + 1 + 2 + 1)
  308. if l < 2**16:
  309. return self.malloc_overhead(l + 1 + 4 + 1)
  310. if l < 2**32:
  311. return self.malloc_overhead(l + 1 + 8 + 1)
  312. return self.malloc_overhead(l + 1 + 16 + 1)
  313. def top_level_object_overhead(self, key, expiry):
  314. # Each top level object is an entry in a dictionary, and so we have to include
  315. # the overhead of a dictionary entry
  316. return self.hashtable_entry_overhead() + self.sizeof_string(key) + self.robj_overhead() + self.key_expiry_overhead(expiry)
  317. def key_expiry_overhead(self, expiry):
  318. # If there is no expiry, there isn't any overhead
  319. if not expiry:
  320. return 0
  321. self._db_expires += 1
  322. # Key expiry is stored in a hashtable, so we have to pay for the cost of a hashtable entry
  323. # The timestamp itself is stored as an int64, which is a 8 bytes
  324. return self.hashtable_entry_overhead() + 8
  325. def hashtable_overhead(self, size):
  326. # See https://github.com/antirez/redis/blob/unstable/src/dict.h
  327. # See the structures dict and dictht
  328. # 2 * (3 unsigned longs + 1 pointer) + int + long + 2 pointers
  329. #
  330. # Additionally, see **table in dictht
  331. # The length of the table is the next power of 2
  332. # When the hashtable is rehashing, another instance of **table is created
  333. # Due to the possibility of rehashing during loading, we calculate the worse
  334. # case in which both tables are allocated, and so multiply
  335. # the size of **table by 1.5
  336. return 4 + 7*self.sizeof_long() + 4*self.sizeof_pointer() + self.next_power(size)*self.sizeof_pointer()*1.5
  337. def hashtable_entry_overhead(self):
  338. # See https://github.com/antirez/redis/blob/unstable/src/dict.h
  339. # Each dictEntry has 2 pointers + int64
  340. return 2*self.sizeof_pointer() + 8
  341. def linkedlist_overhead(self):
  342. # See https://github.com/antirez/redis/blob/unstable/src/adlist.h
  343. # A list has 5 pointers + an unsigned long
  344. return self.sizeof_long() + 5*self.sizeof_pointer()
  345. def quicklist_overhead(self, zip_count):
  346. quicklist = 2*self.sizeof_pointer()+self.sizeof_long()+2*4
  347. quickitem = 4*self.sizeof_pointer()+self.sizeof_long()+2*4
  348. return quicklist + zip_count*quickitem
  349. def linkedlist_entry_overhead(self):
  350. # See https://github.com/antirez/redis/blob/unstable/src/adlist.h
  351. # A node has 3 pointers
  352. return 3*self.sizeof_pointer()
  353. def ziplist_header_overhead(self):
  354. # See https://github.com/antirez/redis/blob/unstable/src/ziplist.c
  355. # <zlbytes><zltail><zllen><entry><entry><zlend>
  356. return 4 + 4 + 2 + 1
  357. def ziplist_entry_overhead(self, value):
  358. # See https://github.com/antirez/redis/blob/unstable/src/ziplist.c
  359. if type(value) == int:
  360. header = 1
  361. if value < 12:
  362. size = 0
  363. elif value < 2**8:
  364. size = 1
  365. elif value < 2**16:
  366. size = 2
  367. elif value < 2**24:
  368. size = 3
  369. elif value < 2**32:
  370. size = 4
  371. else:
  372. size = 8
  373. else:
  374. size = len(value)
  375. if size <= 63:
  376. header = 1
  377. elif size <= 16383:
  378. header = 2
  379. else:
  380. header = 5
  381. # add len again for prev_len of the next record
  382. prev_len = 1 if size < 254 else 5
  383. return prev_len + header + size
  384. def skiplist_overhead(self, size):
  385. return 2*self.sizeof_pointer() + self.hashtable_overhead(size) + (2*self.sizeof_pointer() + 16)
  386. def skiplist_entry_overhead(self):
  387. return self.hashtable_entry_overhead() + 2*self.sizeof_pointer() + 8 + (self.sizeof_pointer() + 8) * self.zset_random_level()
  388. def robj_overhead(self):
  389. return self.sizeof_pointer() + 8
  390. def malloc_overhead(self, size):
  391. alloc = get_jemalloc_allocation(size)
  392. self._total_internal_frag += alloc - size
  393. return alloc
  394. def size_t(self):
  395. return self.sizeof_pointer()
  396. def sizeof_pointer(self):
  397. return self._pointer_size
  398. def sizeof_long(self):
  399. return self._long_size
  400. def next_power(self, size):
  401. power = 1
  402. while (power <= size) :
  403. power = power << 1
  404. return power
  405. def zset_random_level(self):
  406. level = 1
  407. rint = random.randint(0, 0xFFFF)
  408. while (rint < ZSKIPLIST_P * 0xFFFF):
  409. level += 1
  410. rint = random.randint(0, 0xFFFF)
  411. if level < ZSKIPLIST_MAXLEVEL :
  412. return level
  413. else:
  414. return ZSKIPLIST_MAXLEVEL
  415. def element_length(self, element):
  416. if isinstance(element, int):
  417. return self._long_size
  418. if sys.version_info < (3,):
  419. if isinstance(element, long):
  420. return self._long_size
  421. return len(element)
  422. # size classes from jemalloc 4.0.4 using LG_QUANTUM=3
  423. jemalloc_size_classes = [
  424. 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 640, 768, 896, 1024,
  425. 1280, 1536, 1792, 2048, 2560, 3072, 3584, 4096, 5120, 6144, 7168, 8192, 10240, 12288, 14336, 16384, 20480, 24576,
  426. 28672, 32768, 40960, 49152, 57344, 65536, 81920, 98304, 114688,131072, 163840, 196608, 229376, 262144, 327680,
  427. 393216, 458752, 524288, 655360, 786432, 917504, 1048576, 1310720, 1572864, 1835008, 2097152, 2621440, 3145728,
  428. 3670016, 4194304, 5242880, 6291456, 7340032, 8388608, 10485760, 12582912, 14680064, 16777216, 20971520, 25165824,
  429. 29360128, 33554432, 41943040, 50331648, 58720256, 67108864, 83886080, 100663296, 117440512, 134217728, 167772160,
  430. 201326592, 234881024, 268435456, 335544320, 402653184, 469762048, 536870912, 671088640, 805306368, 939524096,
  431. 1073741824, 1342177280, 1610612736, 1879048192, 2147483648, 2684354560, 3221225472, 3758096384, 4294967296,
  432. 5368709120, 6442450944, 7516192768, 8589934592, 10737418240, 12884901888, 15032385536, 17179869184, 21474836480,
  433. 25769803776, 30064771072, 34359738368, 42949672960, 51539607552, 60129542144, 68719476736, 85899345920,
  434. 103079215104, 120259084288, 137438953472, 171798691840, 206158430208, 240518168576, 274877906944, 343597383680,
  435. 412316860416, 481036337152, 549755813888, 687194767360, 824633720832, 962072674304, 1099511627776,1374389534720,
  436. 1649267441664, 1924145348608, 2199023255552, 2748779069440, 3298534883328, 3848290697216, 4398046511104,
  437. 5497558138880, 6597069766656, 7696581394432, 8796093022208, 10995116277760, 13194139533312, 15393162788864,
  438. 17592186044416, 21990232555520, 26388279066624, 30786325577728, 35184372088832, 43980465111040, 52776558133248,
  439. 61572651155456, 70368744177664, 87960930222080, 105553116266496, 123145302310912, 140737488355328, 175921860444160,
  440. 211106232532992, 246290604621824, 281474976710656, 351843720888320, 422212465065984, 492581209243648,
  441. 562949953421312, 703687441776640, 844424930131968, 985162418487296, 1125899906842624, 1407374883553280,
  442. 1688849860263936, 1970324836974592, 2251799813685248, 2814749767106560, 3377699720527872, 3940649673949184,
  443. 4503599627370496, 5629499534213120, 6755399441055744, 7881299347898368, 9007199254740992, 11258999068426240,
  444. 13510798882111488, 15762598695796736, 18014398509481984, 22517998136852480, 27021597764222976,31525197391593472,
  445. 36028797018963968, 45035996273704960, 54043195528445952, 63050394783186944, 72057594037927936, 90071992547409920,
  446. 108086391056891904, 126100789566373888, 144115188075855872, 180143985094819840, 216172782113783808,
  447. 252201579132747776, 288230376151711744, 360287970189639680, 432345564227567616, 504403158265495552,
  448. 576460752303423488, 720575940379279360, 864691128455135232, 1008806316530991104, 1152921504606846976,
  449. 1441151880758558720, 1729382256910270464, 2017612633061982208, 2305843009213693952, 2882303761517117440,
  450. 3458764513820540928, 4035225266123964416, 4611686018427387904, 5764607523034234880, 6917529027641081856,
  451. 8070450532247928832, 9223372036854775808, 11529215046068469760, 13835058055282163712, 16140901064495857664
  452. ] # TODO: use different table depending oon the redis-version used
  453. def get_jemalloc_allocation(size):
  454. idx = bisect.bisect_left(jemalloc_size_classes, size)
  455. alloc = jemalloc_size_classes[idx] if idx < len(jemalloc_size_classes) else size
  456. return alloc